diff --git a/.env.sample b/.env.sample index bb3fe9375..644361893 100644 --- a/.env.sample +++ b/.env.sample @@ -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= diff --git a/packages/e2e/src/helper/createTestAccount.ts b/packages/e2e/src/helper/createTestAccount.ts index 55cc897b4..94642fdd4 100644 --- a/packages/e2e/src/helper/createTestAccount.ts +++ b/packages/e2e/src/helper/createTestAccount.ts @@ -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 = { @@ -49,8 +54,9 @@ export async function createTestAccount( ): Promise { 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, @@ -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) { @@ -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) { diff --git a/packages/e2e/src/helper/createTestEnv.ts b/packages/e2e/src/helper/createTestEnv.ts index 4b409abef..deb27353d 100644 --- a/packages/e2e/src/helper/createTestEnv.ts +++ b/packages/e2e/src/helper/createTestEnv.ts @@ -27,8 +27,8 @@ export const CONFIG = { }, }, LIVE: { - nativeFundingAmount: '0.1', - ledgerDepositAmount: '10', + nativeFundingAmount: '0.8', + ledgerDepositAmount: '12', sponsorshipLimits: { totalMaxPriceInWei: '50000000000000000', userMaxPrice: 50000000000000000n, diff --git a/packages/e2e/src/init.ts b/packages/e2e/src/init.ts index cfdd3f7dc..260648fa6 100644 --- a/packages/e2e/src/init.ts +++ b/packages/e2e/src/init.ts @@ -40,9 +40,9 @@ const LogLevelSchema = z.enum(['silent', 'info', 'debug']); type LogLevel = z.infer; // 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'; const MAINNET_NETWORK_FUNDING_AMOUNT = '0.01'; const MAINNET_LEDGER_DEPOSIT_AMOUNT = '0.01'; diff --git a/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/crossChainSwap.ts b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/crossChainSwap.ts new file mode 100644 index 000000000..f07c43d8f --- /dev/null +++ b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/crossChainSwap.ts @@ -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 + 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()})();`; diff --git a/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/decryptWithinLitAction.ts b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/decryptWithinLitAction.ts new file mode 100644 index 000000000..40b5c6045 --- /dev/null +++ b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/decryptWithinLitAction.ts @@ -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); + + // 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()})();`; diff --git a/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/encryptDecryptWithinLitAction.ts b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/encryptDecryptWithinLitAction.ts new file mode 100644 index 000000000..5d325f33f --- /dev/null +++ b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/encryptDecryptWithinLitAction.ts @@ -0,0 +1,78 @@ +declare const Lit: any; +declare const ethers: any; + +/** + * Lit Action: Encrypt and Decrypt within the Lit Action + * + * Encrypts an API key and decrypts it within the Lit Action. + */ +async function encryptDecryptWithinLitAction() { + // First, encrypt an API key (simulating a stored encrypted API key) + // In a real scenario, this would already be encrypted and stored + const apiKeyData = JSON.stringify({ key: 'example-api-key-12345' }); + const currentCid = Lit.Auth.actionIpfsIdStack[0]; + + const accessControlConditions = [ + { + contractAddress: '', + standardContractType: '', + chain: 'ethereum', + method: '', + parameters: [':currentActionIpfsId'], + returnValueTest: { + comparator: '=', + value: currentCid, + }, + }, + ]; + + // Encrypt the API key (using runOnce to ensure it only runs on one node) + const encryptResult = await Lit.Actions.runOnce( + { waitForResponse: true, name: 'encryptApiKey' }, + async () => { + const result = await Lit.Actions.encrypt({ + accessControlConditions, + to_encrypt: ethers.utils.toUtf8Bytes(apiKeyData), + }); + return JSON.stringify(result); + } + ); + const { ciphertext, dataToEncryptHash } = JSON.parse(encryptResult); + + // Now decrypt the API key (this is the actual decrypt operation we're counting) + const decryptedApiKey = await Lit.Actions.decryptAndCombine({ + accessControlConditions, + ciphertext, + dataToEncryptHash, + authSig: null, + chain: 'ethereum', + }); + + // Parse the decrypted API key + const apiKey = JSON.parse(decryptedApiKey); + + // 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 + }), + }); +} + +// Convert the function to a string and wrap it in an IIFE +export const ENCRYPT_DECRYPT_WITHIN_LIT_ACTION = `(${encryptDecryptWithinLitAction.toString()})();`; diff --git a/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/oracleOperation.ts b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/oracleOperation.ts new file mode 100644 index 000000000..54fcb4eab --- /dev/null +++ b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/oracleOperation.ts @@ -0,0 +1,60 @@ +declare const Lit: any; +declare const ethers: any; + +/** + * Lit Action: Oracle Operation + * + * Fetches external data and signs the result using broadcastAndCollect to medianize prices. + */ +async function oracleOperation() { + // Helper function to calculate median + const median = (arr: number[]) => { + const arrSorted = arr.sort((a, b) => a - b); + return arrSorted.length % 2 === 0 + ? (arrSorted[arrSorted.length / 2 - 1] + + arrSorted[arrSorted.length / 2]) / + 2 + : arrSorted[Math.floor(arrSorted.length / 2)]; + }; + + // Fetch external data (e.g., price oracle data from Coinbase) + const response = await fetch( + 'https://api.coinbase.com/v2/prices/ETH-USD/spot' + ); + const data = await response.json(); + + // Collect prices from all the nodes + const allPrices = await Lit.Actions.broadcastAndCollect({ + name: 'ethPrice', + value: data.data.amount, + }); + + // Medianize the price, so that outliers don't skew the result + const medianPrice = median(allPrices); + + // Process the fetched data + const priceHash = ethers.utils.hashMessage( + ethers.utils.toUtf8Bytes(medianPrice.toString()) + ); + + // Sign the result + const toSign = ethers.utils.arrayify(priceHash); + await Lit.Actions.signAsAction({ + toSign, + signingScheme: 'EcdsaK256Sha256', + sigName: 'oracle-signature', + }); + + // Simulate runtime of 10 seconds + await new Promise((resolve) => setTimeout(resolve, 10000)); + + Lit.Actions.setResponse({ + response: JSON.stringify({ + medianPrice, + data: 'payment benchmark success', + }), + }); +} + +// Convert the function to a string and wrap it in an IIFE +export const ORACLE_OPERATION_LIT_ACTION = `(${oracleOperation.toString()})();`; diff --git a/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/verifiableDataJob.ts b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/verifiableDataJob.ts new file mode 100644 index 000000000..caf188e09 --- /dev/null +++ b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/litActions/verifiableDataJob.ts @@ -0,0 +1,68 @@ +declare const Lit: any; +declare const ethers: any; +declare const jsParams: any; + +/** + * Lit Action: Verifiable Data Job + * + * Processes data locally and signs the result to create a verifiable attestation. + */ +async function verifiableDataJob() { + // Generate and process data locally (using runOnce to ensure it only runs on one node) + const dataResult = await Lit.Actions.runOnce( + { waitForResponse: true, name: 'generateData' }, + async () => { + const dataPoints = []; + for (let i = 0; i < 1000; i++) { + const randomValue = Math.random() * 1000; + const processedValue = Math.sqrt(randomValue) * Math.PI; + dataPoints.push({ + index: i, + value: processedValue, + hash: ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(processedValue.toString()) + ), + }); + } + + // Aggregate the processed data + const aggregatedData = { + totalPoints: dataPoints.length, + averageValue: + dataPoints.reduce((sum, p) => sum + p.value, 0) / dataPoints.length, + dataHash: ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(dataPoints.map((p) => p.hash).join('')) + ), + timestamp: Date.now(), + }; + + return JSON.stringify(aggregatedData); + } + ); + + const aggregatedData = JSON.parse(dataResult); + console.log('aggregatedData', aggregatedData); + + // Simulate processing time + await new Promise((resolve) => setTimeout(resolve, 25000)); + + // Sign the processed result - hash the data first, then convert to bytes + const dataHash = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(JSON.stringify(aggregatedData)) + ); + await Lit.Actions.signEcdsa({ + toSign: ethers.utils.arrayify(dataHash), + publicKey: jsParams.pkpPublicKey, + sigName: 'verifiable-data-signature', + }); + + Lit.Actions.setResponse({ + response: JSON.stringify({ + aggregatedData, + data: 'payment benchmark success', + }), + }); +} + +// Convert the function to a string and wrap it in an IIFE +export const VERIFIABLE_DATA_JOB_LIT_ACTION = `(${verifiableDataJob.toString()})();`; diff --git a/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/paymentBenchmark.ts b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/paymentBenchmark.ts new file mode 100644 index 000000000..b55242f99 --- /dev/null +++ b/packages/e2e/src/test-helpers/executeJs/paymentBenchmarks/paymentBenchmark.ts @@ -0,0 +1,336 @@ +import { createEnvVars } from '../../../helper/createEnvVars'; +import { + createTestAccount, + CreateTestAccountResult, +} from '../../../helper/createTestAccount'; +import { createTestEnv } from '../../../helper/createTestEnv'; +import { DECRYPT_WITHIN_LIT_ACTION } from './litActions/decryptWithinLitAction'; +import { ENCRYPT_DECRYPT_WITHIN_LIT_ACTION } from './litActions/encryptDecryptWithinLitAction'; +import { VERIFIABLE_DATA_JOB_LIT_ACTION } from './litActions/verifiableDataJob'; +import { ORACLE_OPERATION_LIT_ACTION } from './litActions/oracleOperation'; +import { CROSS_CHAIN_SWAP_LIT_ACTION } from './litActions/crossChainSwap'; + +const stringifyWithBigInt = (value: unknown) => + JSON.stringify( + value, + (_key, val) => (typeof val === 'bigint' ? val.toString() : val), + 2 + ); + +type PaymentSummary = { + testName: string; + components: Array<{ + component: string; + quantity: number; + price: bigint; + }>; + totalCost: bigint; +}; + +export const registerPaymentBenchmarkTests = () => { + let testEnv: Awaited>; + let benchmarkUser: CreateTestAccountResult; + const paymentSummaries: PaymentSummary[] = []; + + // Helper function to process and log payment details + const logAndSavePaymentDetails = ( + testName: string, + paymentDetail: Array<{ component: string; quantity: number; price: bigint }> + ) => { + console.log('\nPayment Details:'); + console.log(stringifyWithBigInt(paymentDetail)); + + // Calculate total cost + const totalCost = paymentDetail.reduce((sum, entry) => { + return sum + entry.price; + }, 0n); + console.log(`\nTotal Cost: ${totalCost.toString()}`); + + // Add to summary + paymentSummaries.push({ + testName, + components: paymentDetail.map((entry) => ({ + component: entry.component, + quantity: entry.quantity, + price: entry.price, + })), + totalCost, + }); + }; + + beforeAll(async () => { + const envVars = createEnvVars(); + testEnv = await createTestEnv(envVars); + + // Use TEST_ALICE_PRIVATE_KEY if available to reuse the same account across test runs + const privateKey = process.env['TEST_ALICE_PRIVATE_KEY'] as + | `0x${string}` + | undefined; + + benchmarkUser = await createTestAccount(testEnv, { + label: 'Payment Benchmark User', + privateKey, // Reuse account if env var is set + fundAccount: true, + fundLedger: true, + hasEoaAuthContext: true, + hasPKP: true, + fundPKP: false, + hasPKPAuthContext: false, + fundPKPLedger: false, + }); + }, 120000); // Increased timeout for setup + + afterAll(() => { + if (paymentSummaries.length > 0) { + paymentSummaries.forEach((summary, index) => { + // Use a single console.log with console.table to title the table + const tableTitle = `${summary.testName}`; + + // Create table data + const tableData = summary.components.map((comp) => ({ + Component: comp.component, + Quantity: comp.quantity, + 'Price (wei)': comp.price.toString(), + 'Price (tstLPX)': (Number(comp.price) / 1e18).toFixed(10), + })); + + // Add total row + const totalInTstLPX = (Number(summary.totalCost) / 1e18).toFixed(10); + tableData.push({ + Component: '**TOTAL**', + Quantity: 0, + 'Price (wei)': summary.totalCost.toString(), + 'Price (tstLPX)': totalInTstLPX, + }); + + // Title above table and table itself in one group for readability + console.group(tableTitle); + console.table(tableData); + console.groupEnd(); + }); + + // Grand total + const grandTotal = paymentSummaries.reduce( + (sum, s) => sum + s.totalCost, + 0n + ); + const grandTotalInTstLPX = (Number(grandTotal) / 1e18).toFixed(10); + console.log( + `GRAND TOTAL (ALL TESTS): ${grandTotal.toString()} wei (${grandTotalInTstLPX} tstLPX)` + ); + } + }); + + describe('Payment Benchmark Tests', () => { + describe('Secure API Key Usage', () => { + test('should encrypt outside the Lit Action, and decrypt and make a fetch request inside the Lit Action', async () => { + // Encrypt the API key outside the Lit Action (simulating a pre-encrypted stored API key) + const apiKeyData = JSON.stringify({ key: 'example-api-key-12345' }); + + // Create always-true access control conditions for the benchmark + const accessControlConditions = [ + { + contractAddress: '', + standardContractType: '' as const, + chain: 'ethereum' as const, + method: '', + parameters: ['1'], + returnValueTest: { + comparator: '=' as const, + value: '1', + }, + }, + ]; + + const encryptedData = await testEnv.litClient.encrypt({ + dataToEncrypt: apiKeyData, + accessControlConditions, + chain: 'ethereum', + }); + + const executionResult = await testEnv.litClient.executeJs({ + code: DECRYPT_WITHIN_LIT_ACTION, + authContext: benchmarkUser.eoaAuthContext!, + jsParams: { + accessControlConditions, + ciphertext: encryptedData.ciphertext, + dataToEncryptHash: encryptedData.dataToEncryptHash, + }, + }); + console.log('executionResult', executionResult); + + // Verify successful execution + expect(executionResult.response).toBeDefined(); + const response = JSON.parse(executionResult.response as string); + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + + // Verify payment details are returned + expect(executionResult.paymentDetail).toBeDefined(); + expect(Array.isArray(executionResult.paymentDetail)).toBe(true); + expect(executionResult.paymentDetail!.length).toBeGreaterThan(0); + + const paymentDetail = executionResult.paymentDetail!; + console.log(executionResult); + logAndSavePaymentDetails('Decrypt within Lit Action', paymentDetail); + }, 120000); // 2 minute timeout + + test('should encrypt, decrypt and make a fetch request within the Lit Action', async () => { + const executionResult = await testEnv.litClient.executeJs({ + code: ENCRYPT_DECRYPT_WITHIN_LIT_ACTION, + authContext: benchmarkUser.eoaAuthContext!, + jsParams: {}, + }); + console.log('executionResult', executionResult); + + // Verify successful execution + expect(executionResult.response).toBeDefined(); + const response = JSON.parse(executionResult.response as string); + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + + // Verify payment details are returned + expect(executionResult.paymentDetail).toBeDefined(); + expect(Array.isArray(executionResult.paymentDetail)).toBe(true); + expect(executionResult.paymentDetail!.length).toBeGreaterThan(0); + + const paymentDetail = executionResult.paymentDetail!; + logAndSavePaymentDetails( + 'Encrypt & Decrypt within Lit Action', + paymentDetail + ); + }, 120000); // 2 minute timeout + }); + + describe('Verifiable Data Job', () => { + test('should process data and sign the result', async () => { + const executionResult = await testEnv.litClient.executeJs({ + code: VERIFIABLE_DATA_JOB_LIT_ACTION, + authContext: benchmarkUser.eoaAuthContext!, + jsParams: { + pkpPublicKey: benchmarkUser.pkp!.pubkey, + }, + }); + console.log('executionResult', executionResult); + + // Verify successful execution + expect(executionResult.response).toBeDefined(); + const { response } = executionResult as any; + expect(response.aggregatedData).toBeDefined(); + expect(response.aggregatedData.totalPoints).toBe(1000); + expect(response.aggregatedData.averageValue).toBeGreaterThan(0); + expect(response.aggregatedData.dataHash).toBeDefined(); + expect(response.aggregatedData.timestamp).toBeGreaterThan(0); + + expect(executionResult.signatures).toBeDefined(); + expect( + executionResult.signatures['verifiable-data-signature'] + ).toBeDefined(); + + // Verify payment details are returned + expect(executionResult.paymentDetail).toBeDefined(); + expect(Array.isArray(executionResult.paymentDetail)).toBe(true); + expect(executionResult.paymentDetail!.length).toBeGreaterThan(0); + + const paymentDetail = executionResult.paymentDetail!; + logAndSavePaymentDetails('Verifiable Data Job', paymentDetail); + }, 120000); // 2 minute timeout + }); + + describe('Oracle Operation', () => { + test('should fetch external data, medianize prices, and sign the result', async () => { + const executionResult = await testEnv.litClient.executeJs({ + code: ORACLE_OPERATION_LIT_ACTION, + authContext: benchmarkUser.eoaAuthContext!, + jsParams: {}, + }); + console.log('executionResult', executionResult); + + // Verify successful execution + expect(executionResult.response).toBeDefined(); + const { response } = executionResult as any; + expect(response.medianPrice).toBeDefined(); + expect(parseFloat(response.medianPrice)).toBeGreaterThan(0); + expect(response.data).toBe('payment benchmark success'); + + // Verify signature was created + expect(executionResult.signatures).toBeDefined(); + expect(executionResult.signatures['oracle-signature']).toBeDefined(); + + // Verify payment details are returned + expect(executionResult.paymentDetail).toBeDefined(); + expect(Array.isArray(executionResult.paymentDetail)).toBe(true); + expect(executionResult.paymentDetail!.length).toBeGreaterThan(0); + + const paymentDetail = executionResult.paymentDetail!; + logAndSavePaymentDetails('Oracle Operation', paymentDetail); + }, 120000); // 2 minute timeout + }); + + describe('Cross-Chain Swap', () => { + test('should perform realistic cross-chain swap with price discovery and multi-step signing', async () => { + const executionResult = await testEnv.litClient.executeJs({ + code: CROSS_CHAIN_SWAP_LIT_ACTION, + authContext: benchmarkUser.eoaAuthContext!, + jsParams: { + pkpPublicKey: benchmarkUser.pkp!.pubkey, + }, + }); + + console.log('executionResult', executionResult); + + // Verify successful execution + expect(executionResult.response).toBeDefined(); + const { response } = executionResult as any; + + // Verify swap intent structure + expect(response.swapIntent).toBeDefined(); + expect(response.swapIntent.params).toBeDefined(); + expect(response.swapIntent.params.sourceChain).toBe('ethereum'); + expect(response.swapIntent.params.destChain).toBe('bitcoin'); + expect(response.swapIntent.params.amountIn).toBe('1.0'); + + // Verify pricing calculations + expect(response.swapIntent.pricing).toBeDefined(); + expect(response.swapIntent.pricing.ethPrice).toBeGreaterThan(0); + expect(response.swapIntent.pricing.bitcoinPrice).toBeGreaterThan(0); + expect(response.swapIntent.pricing.expectedAmountOut).toBeGreaterThan( + 0 + ); + expect(response.swapIntent.pricing.amountOutAfterFees).toBeGreaterThan( + 0 + ); + expect(response.swapIntent.pricing.amountOutAfterFees).toBeLessThan( + response.swapIntent.pricing.expectedAmountOut + ); // Fees should reduce output + + // Verify execution proof + expect(response.executionProof).toBeDefined(); + expect(response.executionProof.status).toBe('completed'); + expect(response.executionProof.sourceTxHash).toBeDefined(); + expect(response.executionProof.destTxHash).toBeDefined(); + expect(response.executionProof.sourceBlockNumber).toBeGreaterThan(0); + expect(response.executionProof.destBlockNumber).toBeGreaterThan(0); + + expect(response.data).toBe('payment benchmark success'); + + // Verify both signatures were created (approval + execution) + expect(executionResult.signatures).toBeDefined(); + expect( + executionResult.signatures['swap-approval-signature'] + ).toBeDefined(); + expect( + executionResult.signatures['swap-execution-signature'] + ).toBeDefined(); + + // Verify payment details are returned + expect(executionResult.paymentDetail).toBeDefined(); + expect(Array.isArray(executionResult.paymentDetail)).toBe(true); + expect(executionResult.paymentDetail!.length).toBeGreaterThan(0); + + const paymentDetail = executionResult.paymentDetail!; + logAndSavePaymentDetails('Cross-Chain Swap', paymentDetail); + }, 240000); // 4 minute timeout + }); + }); +}; diff --git a/packages/e2e/src/tickets/payment-benchmark.spec.ts b/packages/e2e/src/tickets/payment-benchmark.spec.ts new file mode 100644 index 000000000..26601e27b --- /dev/null +++ b/packages/e2e/src/tickets/payment-benchmark.spec.ts @@ -0,0 +1,16 @@ +import { registerPaymentBenchmarkTests } from '../test-helpers/executeJs/paymentBenchmarks/paymentBenchmark'; + +/** + * Payment Benchmark Tests + * + * These tests benchmark the cost of executing various Lit Actions + * that perform different operations with varying execution times. + * + * Each test: + * - Executes a specific Lit Action scenario + * - Measures payment details (component costs, quantities, prices) + * - Logs detailed cost breakdown for documentation + * + * Test against: naga-test network + */ +registerPaymentBenchmarkTests();