UNPKG

@lit-protocol/e2e

Version:

Lit Protocol E2E testing package for running comprehensive integration tests

628 lines (626 loc) 24 kB
import { createAccBuilder } from '@lit-protocol/access-control-conditions'; import { ViemAccountAuthenticator } from '@lit-protocol/auth'; import { api as wrappedKeysApi, config as wrappedKeysConfig, } from '@lit-protocol/wrapped-keys'; import { litActionRepository, litActionRepositoryCommon, } from '@lit-protocol/wrapped-keys-lit-actions'; import { Wallet } from 'ethers'; import { createPublicClient, formatEther, http, parseEther } from 'viem'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { createLitClient } from '@lit-protocol/lit-client'; import { createEnvVars } from '../helper/createEnvVars'; import { createTestEnv } from '../helper/createTestEnv'; import { fundAccount } from '../helper/fundAccount'; import { persistGeneratedAccount } from '../helper/generated-accounts'; const LITE_DEBUG_MINT = process.env['LITE_DEBUG_MINT'] === '1'; const LITE_DEBUG_EXIT_BEFORE_MINT = process.env['LITE_DEBUG_EXIT_BEFORE_MINT'] === '1'; export const LITE_CORE_FUNCTIONS = [ 'handshake', 'pkpSign', 'signSessionKey', 'executeJs', 'encryptDecrypt', 'wrappedKeys', ]; export const LITE_FUNCTIONS = [ ...LITE_CORE_FUNCTIONS, 'paymentDelegation', ]; const getRpcUrl = (testEnv, envVars) => envVars.rpcUrl ?? testEnv.networkModule.getChainConfig().rpcUrls.default.http[0]; const assertNativeBalance = async (testEnv, account, envVars) => { const rpcUrl = getRpcUrl(testEnv, envVars); const publicClient = createPublicClient({ chain: testEnv.networkModule.getChainConfig(), transport: http(rpcUrl), }); const balance = await publicClient.getBalance({ address: account.address, }); const minWei = parseEther(testEnv.config.nativeFundingAmount); if (balance < minWei) { throw new Error(`MASTER account ${account.address} balance ${formatEther(balance)} ETH is below minimum ${testEnv.config.nativeFundingAmount} ETH.`); } }; const ensureLedgerBalance = async (testEnv, userAddress) => { const desiredWei = parseEther(testEnv.config.ledgerDepositAmount); const balance = await testEnv.masterPaymentManager.getBalance({ userAddress, }); const currentWei = balance.raw.availableBalance; const deltaWei = desiredWei > currentWei ? desiredWei - currentWei : 0n; if (deltaWei > 0n) { await testEnv.masterPaymentManager.depositForUser({ userAddress, amountInLitkey: formatEther(deltaWei), }); } }; const ensureAuthDataPublicKey = (authData, account) => authData.publicKey ? authData : { ...authData, publicKey: account.publicKey }; const formatAuthDataForLog = (authData) => { const authMethodIdBytes = authData.authMethodId ? (authData.authMethodId.length - 2) / 2 : 0; const publicKeyBytes = authData.publicKey ? (authData.publicKey.length - 2) / 2 : 0; let authSigAddress; let authSigLength; try { const parsed = JSON.parse(authData.accessToken ?? '{}'); authSigAddress = parsed.address; authSigLength = parsed.sig?.length; } catch { authSigAddress = '[parse_error]'; } return { authMethodType: authData.authMethodType, authMethodId: authData.authMethodId, authMethodIdBytes, publicKey: authData.publicKey ?? '[unset]', publicKeyBytes, authSigAddress, authSigLength, accessTokenLength: authData.accessToken?.length ?? 0, }; }; const formatPkpForLog = (pkp) => pkp ? { tokenId: pkp.tokenId, pubkey: pkp.pubkey, ethAddress: pkp.ethAddress, } : '[none]'; const logMintDebug = (label, stage, data) => { if (!LITE_DEBUG_MINT) { return; } console.log(`[lite-mint-debug] ${label} :: ${stage}`, data); }; const getOrCreatePkpLite = async (testEnv, account, authData, label) => { logMintDebug(label, 'viewPKPsByAuthData.request', { account: account.address, authData: formatAuthDataForLog(authData), }); const { pkps } = await testEnv.litClient.viewPKPsByAuthData({ authData, pagination: { limit: 5, }, }); if (pkps && pkps[0]) { logMintDebug(label, 'viewPKPsByAuthData.response', { count: pkps.length, first: formatPkpForLog(pkps[0]), }); return pkps[0]; } logMintDebug(label, 'viewPKPsByAuthData.response', { count: pkps?.length ?? 0, first: formatPkpForLog(pkps?.[0]), }); const scopes = ['sign-anything']; logMintDebug(label, 'mintWithAuth.request', { account: account.address, authData: formatAuthDataForLog(authData), scopes, }); if (LITE_DEBUG_EXIT_BEFORE_MINT) { console.log('❌ Exiting before mint for debug.'); console.log('label:', label); console.log('account.address:', account.address); console.log('account.publicKey:', account.publicKey); console.log('authData.authMethodType:', authData.authMethodType); console.log('authData.authMethodId:', authData.authMethodId); console.log('authData.publicKey:', authData.publicKey ?? '[unset]'); console.log('authData.accessToken.length:', authData.accessToken?.length ?? 0); process.exit(1); } let mintResult; try { mintResult = await testEnv.litClient.mintWithAuth({ authData, account, scopes, }); } catch (error) { logMintDebug(label, 'mintWithAuth.error', error); throw error; } logMintDebug(label, 'mintWithAuth.response', { txHash: mintResult?.txHash, data: mintResult?.data ? formatPkpForLog(mintResult.data) : mintResult, }); const { pkps: mintedPkps } = await testEnv.litClient.viewPKPsByAuthData({ authData, pagination: { limit: 5, }, }); logMintDebug(label, 'viewPKPsByAuthData.afterMint', { count: mintedPkps?.length ?? 0, first: formatPkpForLog(mintedPkps?.[0]), }); if (!mintedPkps?.[0]) { throw new Error('PKP mint completed but no PKP returned via lookup.'); } return mintedPkps[0]; }; const createTestAccountLite = async (testEnv, opts) => { console.log(`--- ${`[${opts.label}]`} Creating test account ---`); const privateKey = generatePrivateKey(); persistGeneratedAccount({ label: `createTestAccount:${opts.label}`, privateKey, network: typeof testEnv.networkModule.getNetworkName === 'function' ? testEnv.networkModule.getNetworkName() : process.env['NETWORK'], }); const account = privateKeyToAccount(privateKey); const baseAuthData = await ViemAccountAuthenticator.authenticate(account); const authData = ensureAuthDataPublicKey(baseAuthData, account); const person = { account, pkp: undefined, eoaAuthContext: undefined, pkpAuthContext: undefined, pkpViemAccount: undefined, paymentManager: undefined, authData, }; console.log(`Address`, person.account.address); console.log(`opts:`, opts); if (opts.fundAccount) { await fundAccount(person.account, testEnv.masterAccount, testEnv.networkModule, { label: 'owner', ifLessThan: testEnv.config.nativeFundingAmount, thenFund: testEnv.config.nativeFundingAmount, }); 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, }); } } if (opts.fundLedger) { const desiredWei = parseEther(testEnv.config.ledgerDepositAmount); const balance = await testEnv.masterPaymentManager.getBalance({ userAddress: person.account.address, }); const currentWei = balance.raw.availableBalance; const deltaWei = desiredWei > currentWei ? desiredWei - currentWei : 0n; if (deltaWei > 0n) { await testEnv.masterPaymentManager.depositForUser({ userAddress: person.account.address, amountInLitkey: formatEther(deltaWei), }); } } if (opts.hasPKP) { person.pkp = await getOrCreatePkpLite(testEnv, account, authData, opts.label); if (opts.fundPKP) { await fundAccount(person.pkp.ethAddress, testEnv.masterAccount, testEnv.networkModule, { label: 'PKP', ifLessThan: testEnv.config.nativeFundingAmount, thenFund: testEnv.config.nativeFundingAmount, }); } if (opts.fundPKPLedger) { const desiredWei = parseEther(testEnv.config.ledgerDepositAmount); const balance = await testEnv.masterPaymentManager.getBalance({ userAddress: person.pkp.ethAddress, }); const currentWei = balance.raw.availableBalance; const deltaWei = desiredWei > currentWei ? desiredWei - currentWei : 0n; if (deltaWei > 0n) { await testEnv.masterPaymentManager.depositForUser({ userAddress: person.pkp.ethAddress, amountInLitkey: formatEther(deltaWei), }); } } if (opts.hasPKPAuthContext) { person.pkpAuthContext = await testEnv.authManager.createPkpAuthContext({ authData, pkpPublicKey: person.pkp.pubkey, authConfig: { resources: [ ['pkp-signing', '*'], ['lit-action-execution', '*'], ['access-control-condition-decryption', '*'], ], expiration: new Date(Date.now() + 1000 * 60 * 30).toISOString(), }, litClient: testEnv.litClient, }); } const authContext = person.pkpAuthContext ?? person.eoaAuthContext; if (authContext) { person.pkpViemAccount = await testEnv.litClient.getPkpViemAccount({ pkpPublicKey: person.pkp.pubkey, authContext, chainConfig: testEnv.networkModule.getChainConfig(), }); } } if (opts.sponsor) { person.paymentManager = await testEnv.litClient.getPaymentManager({ account: person.account, }); try { const tx = await person.paymentManager.setRestriction({ totalMaxPrice: opts.sponsor.restrictions.totalMaxPriceInWei, requestsPerPeriod: opts.sponsor.restrictions.requestsPerPeriod, periodSeconds: opts.sponsor.restrictions.periodSeconds, }); console.log(`- [setRestriction] TX Hash: ${tx.hash}`); } catch (e) { throw new Error(`❌ Failed to set sponsorship restrictions: ${e}`); } const userAddresses = opts.sponsor.userAddresses; if (!userAddresses || userAddresses.length === 0) { throw new Error('❌ User addresses are required for the sponsor to fund.'); } try { console.log(`- Sponsoring users:`, userAddresses); const tx = await person.paymentManager.delegatePaymentsBatch({ userAddresses: userAddresses, }); console.log(`[delegatePaymentsBatch] TX Hash: ${tx.hash}`); } catch (e) { throw new Error(`❌ Failed to delegate sponsorship to users: ${e}`); } } return person; }; /** * Initialize shared lite mainnet context for e2e runs. * * Validates the network, builds the test environment, ensures the master * account and PKP are ready, and configures wrapped keys actions. */ export const initLiteMainnetContext = async () => { const envVars = createEnvVars(); if (envVars.network !== 'naga') { throw new Error(`Lite mainnet suite requires NETWORK=naga, received ${envVars.network}`); } const testEnv = await createTestEnv(envVars); const masterAccount = testEnv.masterAccount; await assertNativeBalance(testEnv, masterAccount, envVars); const baseAuthData = await ViemAccountAuthenticator.authenticate(masterAccount); const masterAuthData = ensureAuthDataPublicKey(baseAuthData, masterAccount); const masterEoaAuthContext = await testEnv.authManager.createEoaAuthContext({ config: { account: masterAccount, }, authConfig: { statement: 'Lite mainnet e2e authorization.', domain: 'lite-mainnet.e2e', resources: [ ['lit-action-execution', '*'], ['pkp-signing', '*'], ['access-control-condition-decryption', '*'], ], expiration: new Date(Date.now() + 1000 * 60 * 30).toISOString(), }, litClient: testEnv.litClient, }); const masterPkp = await getOrCreatePkpLite(testEnv, masterAccount, masterAuthData, 'MASTER'); await ensureLedgerBalance(testEnv, masterAccount.address); await ensureLedgerBalance(testEnv, masterPkp.ethAddress); wrappedKeysConfig.setLitActionsCode(litActionRepository); wrappedKeysConfig.setLitActionsCodeCommon(litActionRepositoryCommon); return { envVars, testEnv, masterAccount, masterAuthData, masterEoaAuthContext, masterPkp, }; }; /** * Ensure the master EOA and its PKP have the minimum ledger balances. */ export const ensureMasterLedgerBalances = async (ctx) => { await ensureLedgerBalance(ctx.testEnv, ctx.masterAccount.address); await ensureLedgerBalance(ctx.testEnv, ctx.masterPkp.ethAddress); }; const LITE_EXECUTE_JS_CODE = ` (async () => { const { sigName, toSign, publicKey } = jsParams; const { keccak256, arrayify } = ethers.utils; const toSignBytes = new TextEncoder().encode(toSign); const toSignBytes32 = keccak256(toSignBytes); const toSignBytes32Array = arrayify(toSignBytes32); await Lit.Actions.signEcdsa({ toSign: toSignBytes32Array, publicKey, sigName, }); })();`; /** * Test 1: Handshake * * Confirms a fresh client handshake returns server keys and meets threshold, * while timing the client creation for latency tracking. */ export const runHandshakeTest = async (ctx) => { const initStart = Date.now(); const litClient = await createLitClient({ network: ctx.testEnv.networkModule, }); const initDurationMs = Date.now() - initStart; const handshakeStart = Date.now(); const clientContext = await litClient.getContext(); const handshakeDurationMs = Date.now() - handshakeStart; console.log(`[lite-mainnet] createLitClient=${initDurationMs}ms getContext=${handshakeDurationMs}ms`); expect(clientContext?.handshakeResult).toBeDefined(); const { serverKeys, connectedNodes, threshold } = clientContext.handshakeResult; const numServers = serverKeys ? Object.keys(serverKeys).length : 0; const numConnected = connectedNodes ? connectedNodes.size : 0; expect(numServers).toBeGreaterThan(0); if (typeof threshold === 'number') { expect(numConnected).toBeGreaterThanOrEqual(threshold); } }; /** * Test 2: PKP Sign * * Verifies the PKP signing endpoint works for the master PKP. */ export const runPkpSignTest = async (ctx) => { const result = await ctx.testEnv.litClient.chain.ethereum.pkpSign({ authContext: ctx.masterEoaAuthContext, pubKey: ctx.masterPkp.pubkey, toSign: 'Hello from lite mainnet e2e', }); expect(result.signature).toBeDefined(); }; /** * Test 3: Sign Session Key * * Creates a PKP auth context to exercise the signSessionKey endpoint. */ export const runSignSessionKeyTest = async (ctx) => { const pkpAuthContext = await ctx.testEnv.authManager.createPkpAuthContext({ authData: ctx.masterAuthData, pkpPublicKey: ctx.masterPkp.pubkey, authConfig: { resources: [ ['pkp-signing', '*'], ['lit-action-execution', '*'], ], expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), }, litClient: ctx.testEnv.litClient, }); expect(pkpAuthContext).toBeDefined(); }; /** * Test 4: Execute JS * * Executes a Lit Action that signs a payload to validate the runtime. */ export const runExecuteJsTest = async (ctx) => { const result = await ctx.testEnv.litClient.executeJs({ code: LITE_EXECUTE_JS_CODE, authContext: ctx.masterEoaAuthContext, jsParams: { sigName: 'lite-mainnet-sig', toSign: 'Lite mainnet executeJs', publicKey: ctx.masterPkp.pubkey, }, }); expect(result).toBeDefined(); expect(result.signatures).toBeDefined(); }; /** * Test 5: Encrypt/Decrypt * * Encrypts data with ACCs and decrypts it using the master auth context. */ export const runEncryptDecryptTest = async (ctx) => { const builder = createAccBuilder(); const accs = builder .requireWalletOwnership(ctx.masterAccount.address) .on('ethereum') .build(); const testData = 'Lite mainnet decrypt test'; const encryptedData = await ctx.testEnv.litClient.encrypt({ dataToEncrypt: testData, unifiedAccessControlConditions: accs, chain: 'ethereum', }); expect(encryptedData.ciphertext).toBeDefined(); expect(encryptedData.dataToEncryptHash).toBeDefined(); const decryptedData = await ctx.testEnv.litClient.decrypt({ data: encryptedData, unifiedAccessControlConditions: accs, chain: 'ethereum', authContext: ctx.masterEoaAuthContext, }); expect(decryptedData.convertedData).toBe(testData); }; /** * Test 6: Wrapped Keys * * Imports and exports a private key via wrapped keys using PKP session sigs. */ export const runWrappedKeysTest = async (ctx) => { const sessionKeyPair = ctx.testEnv.sessionKeyPair; const delegationAuthSig = await ctx.testEnv.authManager.generatePkpDelegationAuthSig({ pkpPublicKey: ctx.masterPkp.pubkey, authData: ctx.masterAuthData, sessionKeyPair, authConfig: { resources: [ ['pkp-signing', '*'], ['lit-action-execution', '*'], ['access-control-condition-decryption', '*'], ], expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), }, litClient: ctx.testEnv.litClient, }); const pkpSessionSigs = await ctx.testEnv.authManager.createPkpSessionSigs({ pkpPublicKey: ctx.masterPkp.pubkey, sessionKeyPair, delegationAuthSig, litClient: ctx.testEnv.litClient, }); const wallet = Wallet.createRandom(); const memo = `lite-mainnet-import-${Date.now()}`; const importResult = await wrappedKeysApi.importPrivateKey({ pkpSessionSigs, litClient: ctx.testEnv.litClient, privateKey: wallet.privateKey, publicKey: wallet.publicKey, keyType: 'K256', memo, }); expect(importResult.id).toBeDefined(); const exportResult = await wrappedKeysApi.exportPrivateKey({ pkpSessionSigs, litClient: ctx.testEnv.litClient, id: importResult.id, network: 'evm', }); expect(exportResult.decryptedPrivateKey?.toLowerCase()).toBe(wallet.privateKey.toLowerCase()); }; /** * Test 7: Payment Delegation * * Ensures Alice can sponsor Bob's PKP execution and that removing sponsorship * causes Bob's next request to fail while reducing Alice's ledger balance. */ export const runPaymentDelegationTest = async (testEnv) => { const bobAccount = await createTestAccountLite(testEnv, { label: 'Bob', fundAccount: true, hasEoaAuthContext: true, fundLedger: false, hasPKP: true, fundPKP: false, hasPKPAuthContext: false, fundPKPLedger: false, }); console.log('bobAccount:', bobAccount); if (!bobAccount.pkp?.ethAddress) { throw new Error("Bob's PKP does not have an ethAddress"); } const alice = await createTestAccountLite(testEnv, { label: 'Alice', fundAccount: true, fundLedger: true, hasPKP: true, fundPKP: true, fundPKPLedger: true, sponsor: { restrictions: { totalMaxPriceInWei: testEnv.config.sponsorshipLimits.totalMaxPriceInWei, requestsPerPeriod: '100', periodSeconds: '600', }, userAddresses: [bobAccount.account.address], }, }); const aliceBeforeBalance = await testEnv.masterPaymentManager.getBalance({ userAddress: alice.account.address, }); console.log("[BEFORE] Alice's Ledger balance before Bob's request:", aliceBeforeBalance); await testEnv.litClient.chain.ethereum.pkpSign({ authContext: bobAccount.eoaAuthContext, pubKey: bobAccount.pkp?.pubkey, toSign: 'Hello, world!', userMaxPrice: testEnv.config.sponsorshipLimits.userMaxPrice, }); await alice.paymentManager.undelegatePaymentsBatch({ userAddresses: [bobAccount.account.address], }); let didFail = false; try { await testEnv.litClient.chain.ethereum.pkpSign({ authContext: bobAccount.eoaAuthContext, pubKey: bobAccount.pkp?.pubkey, toSign: 'Hello again, world!', userMaxPrice: testEnv.config.sponsorshipLimits.userMaxPrice, }); } catch (e) { didFail = true; console.log("As expected, Bob's PKP sign failed after Alice removed sponsorship:", e); } expect(didFail).toBe(true); await new Promise((resolve) => setTimeout(resolve, 5000)); const aliceBalanceAfter = await testEnv.masterPaymentManager.getBalance({ userAddress: alice.account.address, }); console.log("[AFTER] Alice's Ledger balance after Bob's request:", aliceBalanceAfter); expect(BigInt(aliceBalanceAfter.raw.availableBalance)).toBeLessThan(BigInt(aliceBeforeBalance.raw.availableBalance)); }; /** * Run the core lite endpoints in sequence, topping up balances as needed. */ const runCoreEndpointsSuite = async (ctx, runner) => { const steps = [ ['handshake', () => runHandshakeTest(ctx)], ['pkpSign', () => runPkpSignTest(ctx)], ['signSessionKey', () => runSignSessionKeyTest(ctx)], ['executeJs', () => runExecuteJsTest(ctx)], ['encryptDecrypt', () => runEncryptDecryptTest(ctx)], ['wrappedKeys', () => runWrappedKeysTest(ctx)], ]; for (const [name, fn] of steps) { await ensureMasterLedgerBalances(ctx); await runner(name, fn); } }; /** * Run the full lite mainnet suite once, including payment delegation. */ export const runLiteMainnetOnce = async (runner) => { const ctx = await initLiteMainnetContext(); await runCoreEndpointsSuite(ctx, runner); const envVars = createEnvVars(); const testEnv = await createTestEnv(envVars); await runner('paymentDelegation', () => runPaymentDelegationTest(testEnv)); }; //# sourceMappingURL=mainnet-lite.runner.js.map