Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@ LOCAL_MASTER_ACCOUNT=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf

NODE_NO_WARNINGS=1
NODE_OPTIONS=--no-deprecation

# These environment variables are optional and allow you to reuse a single test account.
# This is especially useful when testing against a paid Naga network, where the test
# account must have tokens deposited into the Ledger contract to cover request costs.
#
# By providing private key(s) here, you can avoid depositing funds for each test run.
# Without these variables, a new test account is generated on every run, requiring
# fresh funding each time.
TEST_ALICE_PRIVATE_KEY=
TEST_BOB_PRIVATE_KEY=
55 changes: 33 additions & 22 deletions packages/e2e/src/helper/createTestAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ type CreateTestAccountOpts = {
};
userAddresses: string[] | `0x${string}`[];
};
/**
* Optional: Use a fixed private key instead of generating a new random one.
* This allows reusing the same test account across multiple test runs.
*/
privateKey?: `0x${string}`;
};

export type CreateTestAccountResult = {
Expand All @@ -49,8 +54,9 @@ export async function createTestAccount(
): Promise<CreateTestAccountResult> {
console.log(`--- ${`[${opts.label}]`} Creating test account ---`);
// 1. store result
const privateKey = opts.privateKey ?? generatePrivateKey();
let person: CreateTestAccountResult = {
account: privateKeyToAccount(generatePrivateKey()),
account: privateKeyToAccount(privateKey),
pkp: undefined,
eoaAuthContext: undefined,
pkpAuthContext: undefined,
Expand All @@ -66,7 +72,12 @@ export async function createTestAccount(
person.authData = personAccountAuthData;

console.log(`Address`, person.account.address);
console.log(`opts:`, opts);
console.log(`opts:`, {
...opts,
privateKey: opts.privateKey
? opts.privateKey.slice(0, 6) + '...'
: undefined,
});

// 3. fund it
if (opts.fundAccount) {
Expand All @@ -80,27 +91,27 @@ export async function createTestAccount(
thenFund: testEnv.config.nativeFundingAmount,
}
);
}

// -- create EOA auth context
if (opts.hasEoaAuthContext) {
person.eoaAuthContext = await testEnv.authManager.createEoaAuthContext({
config: {
account: person.account,
},
authConfig: {
statement: 'I authorize the Lit Protocol to execute this Lit Action.',
domain: 'example.com',
resources: [
['lit-action-execution', '*'],
['pkp-signing', '*'],
['access-control-condition-decryption', '*'],
],
expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(),
},
litClient: testEnv.litClient,
});
}
} // ... end if fundAccount
// -- create EOA auth context
if (opts.hasEoaAuthContext) {
person.eoaAuthContext = await testEnv.authManager.createEoaAuthContext({
config: {
account: person.account,
},
authConfig: {
statement: 'I authorize the Lit Protocol to execute this Lit Action.',
domain: 'example.com',
resources: [
['lit-action-execution', '*'],
['pkp-signing', '*'],
['access-control-condition-decryption', '*'],
],
expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(),
},
litClient: testEnv.litClient,
});
}

// 4. also fund the ledger
if (opts.fundLedger) {
Expand Down
4 changes: 2 additions & 2 deletions packages/e2e/src/helper/createTestEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export const CONFIG = {
},
},
LIVE: {
nativeFundingAmount: '0.1',
ledgerDepositAmount: '10',
nativeFundingAmount: '0.8',
ledgerDepositAmount: '12',
Comment on lines +30 to +31
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped because of failing ci E2E tests running out of funds, could be reverted after @glitch003 reduces fees on naga-test

sponsorshipLimits: {
totalMaxPriceInWei: '50000000000000000',
userMaxPrice: 50000000000000000n,
Expand Down
4 changes: 2 additions & 2 deletions packages/e2e/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ const LogLevelSchema = z.enum(['silent', 'info', 'debug']);
type LogLevel = z.infer<typeof LogLevelSchema>;

// Configurations
const LIVE_NETWORK_FUNDING_AMOUNT = '0.01';
const LIVE_NETWORK_FUNDING_AMOUNT = '0.2';
const LOCAL_NETWORK_FUNDING_AMOUNT = '1';
const LIVE_NETWORK_LEDGER_DEPOSIT_AMOUNT = '1';
const LIVE_NETWORK_LEDGER_DEPOSIT_AMOUNT = '1.2';
Comment on lines +43 to +45
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped because of failing ci E2E tests running out of funds, could be reverted after @glitch003 reduces fees on naga-test

const MAINNET_NETWORK_FUNDING_AMOUNT = '0.01';
const MAINNET_LEDGER_DEPOSIT_AMOUNT = '0.01';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
declare const Lit: any;
declare const ethers: any;
declare const jsParams: any;

/**
* Lit Action: Cross-Chain Swap
*
* Simulates a realistic cross-chain swap flow with price discovery,
* liquidity checks, slippage calculation, and multi-step signing.
*/
async function crossChainSwap() {
// Swap parameters (in a real scenario, these would come from jsParams)
const swapParams = {
sourceChain: 'ethereum',
destChain: 'bitcoin',
sourceToken: 'ETH',
destToken: 'BTC',
amountIn: '1.0', // 1 ETH
userAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', // Example address
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example address appears to be truncated (missing the last character for a valid Ethereum address, which should be 42 characters including '0x'). Valid Ethereum addresses have 40 hexadecimal characters after '0x'. Consider using a complete example address like '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0' or a well-known test address.

Suggested change
userAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', // Example address
userAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0', // Example address

Copilot uses AI. Check for mistakes.
slippageTolerance: 0.5, // 0.5%
};

// Fetch prices and chain status (using runOnce to ensure consistent data across all nodes)
const priceDataResult = await Lit.Actions.runOnce(
{ waitForResponse: true, name: 'fetchPriceData' },
async () => {
try {
// Fetch 1: Get source token price (ETH) from Coinbase
const ethPriceResponse = await fetch(
'https://api.coinbase.com/v2/prices/ETH-USD/buy'
);
const ethPriceData = await ethPriceResponse.json();
console.log('ethPriceData', ethPriceData);
const ethPrice = parseFloat(ethPriceData.data.amount);

// Fetch 2: Get destination token price (BTC) from Coinbase
const bitcoinPriceResponse = await fetch(
'https://api.coinbase.com/v2/prices/BTC-USD/buy'
);
const bitcoinPriceData = await bitcoinPriceResponse.json();
console.log('bitcoinPriceData', bitcoinPriceData);
const bitcoinPrice = parseFloat(bitcoinPriceData.data.amount);

// Fetch 3: Check source chain status (simulated via Coinbase system status)
const sourceChainStatusResponse = await fetch(
'https://api.coinbase.com/v2/time'
);
const sourceChainStatusData = await sourceChainStatusResponse.json();
const sourceChainStatus = sourceChainStatusData.data
? 'healthy'
: 'error';

// Fetch 4: Check destination chain status (simulated via Coinbase system status)
const destChainStatusResponse = await fetch(
'https://api.coinbase.com/v2/time'
);
const destChainStatusData = await destChainStatusResponse.json();
const destChainStatus = destChainStatusData.data ? 'healthy' : 'error';

return JSON.stringify({
ethPrice,
bitcoinPrice,
sourceChainStatus,
destChainStatus,
});
} catch (error) {
console.error('Error fetching price data:', error);
return JSON.stringify({
error: error.message || 'Failed to fetch price data',
});
}
}
);

const { ethPrice, bitcoinPrice, sourceChainStatus, destChainStatus, error } =
JSON.parse(priceDataResult);

if (error !== undefined) {
Lit.Actions.setResponse({
response: JSON.stringify({ error }),
});
return;
}

// Calculate swap amounts based on real prices
const amountInUsd = parseFloat(swapParams.amountIn) * ethPrice;
const expectedAmountOut = amountInUsd / bitcoinPrice;

// Simulate bridge fees and slippage
const bridgeFeePercent = 0.3; // 0.3% bridge fee
const actualSlippage = 0.2; // 0.2% actual slippage (within tolerance)
const feesAndSlippage = (bridgeFeePercent + actualSlippage) / 100;
const amountOutAfterFees = expectedAmountOut * (1 - feesAndSlippage);
const minAmountOut =
expectedAmountOut * (1 - swapParams.slippageTolerance / 100);

// Prepare swap intent data
const swapIntent = {
params: swapParams,
pricing: {
ethPrice,
bitcoinPrice,
amountInUsd,
expectedAmountOut,
minAmountOut,
amountOutAfterFees,
bridgeFeePercent,
actualSlippage,
},
chainStatus: {
source: sourceChainStatus,
destination: destChainStatus,
},
timestamp: 1718000000000,
nonce: 123456789,
};

// Sign 1: Sign the swap intent (like approving the swap on source chain)
const swapIntentHash = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes(JSON.stringify(swapIntent))
);
await Lit.Actions.signEcdsa({
toSign: ethers.utils.arrayify(swapIntentHash),
publicKey: jsParams.pkpPublicKey,
sigName: 'swap-approval-signature',
});

// Simulate cross-chain bridge processing time (waiting for confirmations, relayers, etc.)
await new Promise((resolve) => setTimeout(resolve, 15000));

// Prepare execution proof (simulates proof that swap was executed on dest chain)
const executionProof = {
swapIntentHash: swapIntentHash,
executedAmountOut: amountOutAfterFees.toString(),
sourceTxHash: ethers.utils.keccak256(
ethers.utils.toUtf8Bytes(`source-${swapIntent.nonce}`)
),
destTxHash: ethers.utils.keccak256(
ethers.utils.toUtf8Bytes(`dest-${swapIntent.nonce}`)
),
sourceBlockNumber: 18500000,
destBlockNumber: 50000000,
status: 'completed',
executedAt: 1718000000000,
};

// Sign 2: Sign the execution proof (like attesting the swap completed on dest chain)
const executionProofHash = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes(JSON.stringify(executionProof))
);
await Lit.Actions.signEcdsa({
toSign: ethers.utils.arrayify(executionProofHash),
publicKey: jsParams.pkpPublicKey,
sigName: 'swap-execution-signature',
});

// Simulate remaining runtime to reach 20 seconds total
await new Promise((resolve) => setTimeout(resolve, 5000));

Lit.Actions.setResponse({
response: JSON.stringify({
swapIntent,
executionProof,
data: 'payment benchmark success',
}),
});
}

// Convert the function to a string and wrap it in an IIFE
export const CROSS_CHAIN_SWAP_LIT_ACTION = `(${crossChainSwap.toString()})();`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
declare const Lit: any;
declare const jsParams: any;

/**
* Lit Action: Decrypt within the Lit Action
*
* Decrypts an API key and makes a fetch request within the Lit Action.
*/
async function decryptWithinLitAction() {
const { accessControlConditions, ciphertext, dataToEncryptHash } = jsParams;

// Decrypt the API key
const decryptedApiKey = await Lit.Actions.decryptAndCombine({
accessControlConditions,
ciphertext,
dataToEncryptHash,
authSig: null,
chain: 'ethereum',
});

// Parse the decrypted API key
const apiKey = JSON.parse(decryptedApiKey);
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable apiKey.

Suggested change
const apiKey = JSON.parse(decryptedApiKey);
JSON.parse(decryptedApiKey);

Copilot uses AI. Check for mistakes.

// Use the API key in a fetch request (using Coinbase public API)
const response = await fetch('https://api.coinbase.com/v2/time', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// "Authorization": `Bearer ${apiKey.key}`,
},
});

const responseData = await response.json();

// Simulate runtime of 5 seconds
await new Promise((resolve) => setTimeout(resolve, 5000));

Lit.Actions.setResponse({
response: JSON.stringify({
success: true,
data: responseData,
// Note: We don't expose the actual API key in the response
}),
});
}

export const DECRYPT_WITHIN_LIT_ACTION = `(${decryptWithinLitAction.toString()})();`;
Loading
Loading