diff --git a/.github/workflows/e2e-naga.yml b/.github/workflows/e2e-naga.yml index 231fa7e70..2902c242f 100644 --- a/.github/workflows/e2e-naga.yml +++ b/.github/workflows/e2e-naga.yml @@ -61,8 +61,10 @@ jobs: privateKey: LIVE_MASTER_ACCOUNT_NAGA_TEST env: LOG_LEVEL: debug2 + NETWORK: ${{ matrix.network }} LIVE_MASTER_ACCOUNT: ${{ secrets[matrix.privateKey] }} LOCAL_MASTER_ACCOUNT: ${{ secrets[matrix.privateKey] }} + LIT_YELLOWSTONE_PRIVATE_RPC_URL: ${{ vars.LIT_YELLOWSTONE_PRIVATE_RPC_URL }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -101,8 +103,8 @@ jobs: echo "LOCAL_MASTER_ACCOUNT is not set for network ${{ matrix.network }}" >&2 exit 1 fi - - name: Run health check (${{ matrix.network }}) - run: NETWORK=${{ matrix.network }} pnpm run test:e2e:ci -- packages/e2e/src/e2e.spec.ts --testNamePattern "^all " + - name: Run e2e tests (${{ matrix.network }}) + run: pnpm run test:e2e:ci -- packages/e2e/src/e2e.spec.ts timeout-minutes: 10 release: diff --git a/.gitignore b/.gitignore index da87bd13b..69e472aac 100644 --- a/.gitignore +++ b/.gitignore @@ -98,4 +98,5 @@ alice-auth-manager-data alice-auth-manager-data !/packages/contracts/dist/ !/packages/contracts/dist/dev -.secret \ No newline at end of file +.secret +.jest-cache \ No newline at end of file diff --git a/package.json b/package.json index 17d706fd4..7c0dd1b95 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "lint": "npx nx run-many --target=lint --all", "lint:fix": "npx nx run-many --target=lint --all -- --fix", "format:check": "npx nx format:check --all", - "test:e2e": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} dotenvx run --env-file=.env -- jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000 --runTestsByPath packages/e2e/src/e2e.spec.ts", - "test:target": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} dotenvx run --env-file=.env -- jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000", - "test:e2e:ci": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} npx jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000 --runTestsByPath", + "test:e2e": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} dotenvx run --env-file=.env -- jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000 --cacheDirectory .jest-cache --runTestsByPath packages/e2e/src/e2e.spec.ts", + "test:target": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} dotenvx run --env-file=.env -- jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000 --cacheDirectory .jest-cache", + "test:e2e:ci": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} npx jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000 --cacheDirectory .jest-cache --runTestsByPath", "pretest:e2e": "pnpm run generate:lit-actions", "pretest:e2e:ci": "pnpm run generate:lit-actions", "pretest:custom": "pnpm run generate:lit-actions", diff --git a/packages/e2e/src/e2e-revamp.spec.ts b/packages/e2e/src/e2e-revamp.spec.ts deleted file mode 100644 index 5bd2c40e3..000000000 --- a/packages/e2e/src/e2e-revamp.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -// WIP! Use e2e.spec.ts instead -import { createEnvVars } from './helper/createEnvVars'; -import { createTestEnv } from './helper/createTestEnv'; -import { - createTestAccount, - CreateTestAccountResult, -} from './helper/createTestAccount'; - -const registerEoaExecuteJsSuite = () => { - describe('EOA auth (revamp)', () => { - let testEnv: Awaited>; - let alice: CreateTestAccountResult; - - beforeAll(async () => { - const envVars = createEnvVars(); - testEnv = await createTestEnv(envVars); - - alice = await createTestAccount(testEnv, { - label: 'Alice', - fundAccount: true, - hasEoaAuthContext: true, - fundLedger: true, - hasPKP: true, - fundPKP: true, - hasPKPAuthContext: true, - fundPKPLedger: true, - }); - }); - - test('executeJs signs with Alice PKP', async () => { - if (!alice.eoaAuthContext) { - throw new Error('Alice is missing an EOA auth context'); - } - if (!alice.pkp) { - throw new Error('Alice is missing a PKP'); - } - - const litActionCode = ` -(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); - - const sigShare = await Lit.Actions.signEcdsa({ - toSign: toSignBytes32Array, - publicKey, - sigName, - }); -})();`; - - const toSign = 'Revamp executeJs test'; - const result = await testEnv.litClient.executeJs({ - code: litActionCode, - authContext: alice.eoaAuthContext, - jsParams: { - message: toSign, - sigName: 'revamp-e2e-sig', - toSign, - publicKey: alice.pkp.pubkey, - }, - }); - - expect(result).toBeDefined(); - expect(result.signatures).toBeDefined(); - }); - }); -}; - -describe('revamped e2e suite', () => { - registerEoaExecuteJsSuite(); -}); diff --git a/packages/e2e/src/e2e.spec.ts b/packages/e2e/src/e2e.spec.ts index 211f0d88f..7f639dc47 100644 --- a/packages/e2e/src/e2e.spec.ts +++ b/packages/e2e/src/e2e.spec.ts @@ -1,317 +1,149 @@ -import type { AuthContext } from '@lit-protocol/e2e'; +import { createEnvVars } from './helper/createEnvVars'; +import { createTestEnv } from './helper/createTestEnv'; +import { getNetworkConfig } from './helper/network'; +import type { ResolvedNetwork } from './helper/network'; +import type { AuthContext } from './types'; + +/** + * How to run: + * - Full revamped suite (canonical): + * `NETWORK=naga-dev pnpm run test:e2e` + * + * - Just this file: + * `NETWORK=naga-dev pnpm exec dotenvx run --env-file=.env -- jest --runInBand --config ./jest.e2e.config.ts --cacheDirectory .jest-cache --runTestsByPath packages/e2e/src/e2e.spec.ts` + * + * - Single test / suite (use Jest name filtering): + * `NETWORK=naga-dev pnpm exec dotenvx run --env-file=.env -- jest --runInBand --config ./jest.e2e.config.ts --cacheDirectory .jest-cache --runTestsByPath packages/e2e/src/e2e.spec.ts --testNamePattern "EOA auth.*pkpSign"` + * + * - Alternative local workflow: + * add `.only` to a `describe` or `it` block temporarily. + */ import { - createCustomAuthContext, - createEncryptDecryptFlowTest, - createEoaNativeAuthFlowTest, - createExecuteJsTest, - createPaymentDelegationFlowTest, - createPaymentManagerFlowTest, - createPkpAuthContextWithPreGeneratedMaterials, - createPkpEncryptDecryptTest, - createPkpPermissionsManagerFlowTest, - createPkpSignTest, - createPregenDelegationServerReuseTest, - createViemSignMessageTest, - createViemSignTransactionTest, - createViemSignTypedDataTest, - createViewPKPsByAddressTest, - createViewPKPsByAuthDataTest, - init, - registerPaymentDelegationTicketSuite, -} from '@lit-protocol/e2e'; -import { registerWrappedKeysTests } from './test-helpers/executeJs/wrappedKeys'; + createTestAccount, + CreateTestAccountResult, +} from './helper/createTestAccount'; +import { registerEndpointSuite } from './suites/endpoints.suite'; +import { registerViemSuite } from './suites/viem.suite'; +import { registerPkpPreGeneratedMaterialsSuite } from './suites/pkp-pre-generated-materials.suite'; +import { registerEoaNativeSuite } from './suites/eoa-native.suite'; +import { registerWrappedKeysSuite } from './suites/wrapped-keys.suite'; +import { registerCustomAuthSuite } from './suites/custom-auth.suite'; +import { registerPaymentDelegationTicketSuite } from './tickets/delegation.suite'; const SELECTED_NETWORK = process.env['NETWORK']; const IS_PAID_NETWORK = SELECTED_NETWORK !== 'naga-dev'; -const RPC_OVERRIDE_ENV_VAR = - SELECTED_NETWORK === 'naga' || SELECTED_NETWORK === 'naga-proto' - ? 'LIT_MAINNET_RPC_URL' - : 'LIT_YELLOWSTONE_PRIVATE_RPC_URL'; const describeIfPaid = IS_PAID_NETWORK ? describe : describe.skip; -const RPC_OVERRIDE = process.env[RPC_OVERRIDE_ENV_VAR]; -if (RPC_OVERRIDE) { - console.log( - `๐Ÿงช E2E: Using RPC override (${RPC_OVERRIDE_ENV_VAR}):`, - RPC_OVERRIDE - ); -} - -describe('all', () => { - describe('full alice, bob, and eve', () => { - let ctx: Awaited>; - - // Auth contexts for testing - let eveCustomAuthContext: AuthContext; - beforeAll(async () => { - try { - ctx = await init(); - // Replenish ledger balances for the Alice EOA + PKP before the suite runs. - await ctx.masterDepositForUser(ctx.aliceViemAccount.address); - await ctx.masterDepositForUser(ctx.aliceViemAccountPkp.ethAddress); - eveCustomAuthContext = await createCustomAuthContext(ctx); - } catch (e) { - console.error('โŒ Failed to initialise E2E test context', e); - process.exit(1); - } +describe('revamped e2e suite', () => { + let envVars: ReturnType; + let testEnv: Awaited>; + let resolvedNetwork: ResolvedNetwork; + let alice: CreateTestAccountResult; + let bob: CreateTestAccountResult; + + beforeAll(async () => { + envVars = createEnvVars(); + testEnv = await createTestEnv(envVars); + const { name, importName, type } = getNetworkConfig(envVars.network); + resolvedNetwork = { + name, + importName, + type, + networkModule: testEnv.networkModule, + }; + + alice = await createTestAccount(testEnv, { + label: 'Alice', + fundAccount: true, + hasEoaAuthContext: true, + fundLedger: true, + hasPKP: true, + fundPKP: true, + hasPKPAuthContext: true, + fundPKPLedger: true, }); - describe('EOA Auth', () => { - console.log('๐Ÿ” Testing using Externally Owned Account authentication'); - - describe('endpoints', () => { - it('pkpSign', () => - createPkpSignTest(ctx, () => ctx.aliceEoaAuthContext)()); - it('executeJs', () => - createExecuteJsTest(ctx, () => ctx.aliceEoaAuthContext)()); - it('viewPKPsByAddress', () => createViewPKPsByAddressTest(ctx)()); - it('viewPKPsByAuthData', () => - createViewPKPsByAuthDataTest(ctx, () => ctx.aliceEoaAuthContext)()); - it('pkpEncryptDecrypt', () => - createPkpEncryptDecryptTest(ctx, () => ctx.aliceEoaAuthContext)()); - it('encryptDecryptFlow', () => - createEncryptDecryptFlowTest(ctx, () => ctx.aliceEoaAuthContext)()); - it('pkpPermissionsManagerFlow', () => - createPkpPermissionsManagerFlowTest( - ctx, - () => ctx.aliceEoaAuthContext - )()); - it('paymentManagerFlow', () => - createPaymentManagerFlowTest(ctx, () => ctx.aliceEoaAuthContext)()); - it('paymentDelegationFlow', () => - createPaymentDelegationFlowTest( - ctx, - () => ctx.aliceEoaAuthContext - )()); - - describe('integrations', () => { - describe('pkp viem account', () => { - it('sign message', () => - createViemSignMessageTest(ctx, () => ctx.aliceEoaAuthContext)()); - it('sign transaction', () => - createViemSignTransactionTest( - ctx, - () => ctx.aliceEoaAuthContext - )()); - it('sign typed data', () => - createViemSignTypedDataTest( - ctx, - () => ctx.aliceEoaAuthContext - )()); - }); - }); - }); - - describe('PKP Auth', () => { - console.log('๐Ÿ” Testing using Programmable Key Pair authentication'); - - describe('endpoints', () => { - it('pkpSign', () => - createPkpSignTest(ctx, () => ctx.alicePkpAuthContext)()); - it('executeJs', () => - createExecuteJsTest(ctx, () => ctx.alicePkpAuthContext)()); - it('viewPKPsByAddress', () => createViewPKPsByAddressTest(ctx)()); - it('viewPKPsByAuthData', () => - createViewPKPsByAuthDataTest(ctx, () => ctx.alicePkpAuthContext)()); - it('pkpEncryptDecrypt', () => - createPkpEncryptDecryptTest(ctx, () => ctx.alicePkpAuthContext)()); - it('encryptDecryptFlow', () => - createEncryptDecryptFlowTest(ctx, () => ctx.alicePkpAuthContext)()); - it('pkpPermissionsManagerFlow', () => - createPkpPermissionsManagerFlowTest( - ctx, - () => ctx.alicePkpAuthContext - )()); - }); - - describe('integrations', () => { - describe('pkp viem account', () => { - it('sign message', () => - createViemSignMessageTest(ctx, () => ctx.alicePkpAuthContext)()); - it('sign transaction', () => - createViemSignTransactionTest( - ctx, - () => ctx.alicePkpAuthContext - )()); - it('sign typed data', () => - createViemSignTypedDataTest( - ctx, - () => ctx.alicePkpAuthContext - )()); - }); - }); - }); - - describe('Custom Auth', () => { - console.log('๐Ÿ” Testing using Custom authentication method'); - - describe('endpoints', () => { - it('pkpSign', () => - createPkpSignTest( - ctx, - () => eveCustomAuthContext, - ctx.eveViemAccountPkp.pubkey - )()); - it('executeJs', () => - createExecuteJsTest( - ctx, - () => eveCustomAuthContext, - ctx.eveViemAccountPkp.pubkey - )()); - it('viewPKPsByAddress', () => createViewPKPsByAddressTest(ctx)()); - it('viewPKPsByAuthData', () => - createViewPKPsByAuthDataTest(ctx, () => eveCustomAuthContext)()); - it('pkpEncryptDecrypt', () => - createPkpEncryptDecryptTest(ctx, () => ctx.aliceEoaAuthContext)()); - it('encryptDecryptFlow', () => - createEncryptDecryptFlowTest(ctx, () => ctx.aliceEoaAuthContext)()); - - // Disable for now because it requires a different flow - // it('pkpPermissionsManagerFlow', () => - // createPkpPermissionsManagerFlowTest( - // ctx, - // () => eveCustomAuthContext, ctx.eveViemAccountPkp.pubkey - // )()); - }); + bob = await createTestAccount(testEnv, { + label: 'Bob', + fundAccount: true, + hasEoaAuthContext: true, + fundLedger: true, + hasPKP: false, + fundPKP: false, + fundPKPLedger: false, + hasPKPAuthContext: false, + }); + }); - // describe('integrations', () => { - // describe('pkp viem account', () => { - // it('sign message', () => - // createViemSignMessageTest(ctx, () => eveCustomAuthContext, ctx.eveViemAccountPkp.pubkey)()); - // it('sign transaction', () => - // createViemSignTransactionTest(ctx, () => eveCustomAuthContext, ctx.eveViemAccountPkp.pubkey)()); - // it('sign typed data', () => - // createViemSignTypedDataTest(ctx, () => eveCustomAuthContext, ctx.eveViemAccountPkp.pubkey)()); - // }); - // }); + const authModes = [ + { + label: 'EOA', + getAuthContext: () => alice.eoaAuthContext!, + includePaymentFlows: true, + getAccsAddress: (_authContext: AuthContext) => alice.account.address, + }, + { + label: 'PKP', + getAuthContext: () => alice.pkpAuthContext!, + includePaymentFlows: false, + }, + ] satisfies Array<{ + label: string; + getAuthContext: () => AuthContext; + includePaymentFlows: boolean; + getAccsAddress?: (authContext: AuthContext) => string; + }>; + + authModes.forEach((mode) => { + describe(`${mode.label} auth`, () => { + const getTestEnv = () => testEnv; + const getAliceAccount = () => alice; + const getBobAccount = () => bob; + const getPkpPublicKey = () => { + if (!alice.pkp) { + throw new Error('Alice is missing a PKP'); + } + return alice.pkp.pubkey; + }; + const getPkpEthAddress = () => { + if (!alice.pkp) { + throw new Error('Alice is missing a PKP'); + } + return alice.pkp.ethAddress as `0x${string}`; + }; + + registerEndpointSuite(getTestEnv, mode.getAuthContext, { + getPkpPublicKey, + getPkpEthAddress, + getAliceAccount, + getBobAccount, + includePaymentFlows: mode.includePaymentFlows, + getAccsAddress: mode.getAccsAddress, }); - describe('PKP Auth with Pre-generated Materials', () => { - console.log('๐Ÿ” Testing PKP auth with pre-generated session materials'); - - let preGeneratedAuthContext: any; - - beforeAll(async () => { - try { - preGeneratedAuthContext = - await createPkpAuthContextWithPreGeneratedMaterials(ctx); - } catch (e) { - console.error('Failed to create pre-generated auth context:', e); - throw e; - } - }); - - describe('endpoints', () => { - it('pkpSign with pre-generated materials', () => - createPkpSignTest(ctx, () => preGeneratedAuthContext)()); - - it('executeJs with pre-generated materials', () => - createExecuteJsTest(ctx, () => preGeneratedAuthContext)()); - - it('pkpEncryptDecrypt with pre-generated materials', () => - createPkpEncryptDecryptTest(ctx, () => preGeneratedAuthContext)()); - }); - - describe('error handling', () => { - it('should reject when only sessionKeyPair is provided', async () => { - const tempAuthContext = await ctx.authManager.createPkpAuthContext({ - authData: ctx.aliceViemAccountAuthData, - pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, - authConfig: { - resources: [['pkp-signing', '*']], - expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), - }, - litClient: ctx.litClient, - }); - - const sessionKeyPair = tempAuthContext.sessionKeyPair; - - await expect( - ctx.authManager.createPkpAuthContext({ - authData: ctx.aliceViemAccountAuthData, - pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, - authConfig: { - resources: [['pkp-signing', '*']], - expiration: new Date( - Date.now() + 1000 * 60 * 15 - ).toISOString(), - }, - litClient: ctx.litClient, - sessionKeyPair, // Only providing sessionKeyPair - // delegationAuthSig is missing - }) - ).rejects.toThrow( - 'Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided' - ); - }); - - it('should reject when only delegationAuthSig is provided', async () => { - const tempAuthContext = await ctx.authManager.createPkpAuthContext({ - authData: ctx.aliceViemAccountAuthData, - pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, - authConfig: { - resources: [['pkp-signing', '*']], - expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), - }, - litClient: ctx.litClient, - }); + registerViemSuite(getTestEnv, mode.getAuthContext, getPkpPublicKey); + }); + }); - const delegationAuthSig = - await tempAuthContext.authNeededCallback(); + registerPkpPreGeneratedMaterialsSuite( + () => testEnv, + () => alice, + () => resolvedNetwork + ); - await expect( - ctx.authManager.createPkpAuthContext({ - authData: ctx.aliceViemAccountAuthData, - pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, - authConfig: { - resources: [['pkp-signing', '*']], - expiration: new Date( - Date.now() + 1000 * 60 * 15 - ).toISOString(), - }, - litClient: ctx.litClient, - // sessionKeyPair is missing - delegationAuthSig, // Only providing delegationAuthSig - }) - ).rejects.toThrow( - 'Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided' - ); - }); - }); + registerEoaNativeSuite( + () => testEnv, + () => alice + ); - /** - * This scenario mirrors the client/server hand-off used in production: - * 1. A client generates session materials and a delegation auth sig. - * 2. The bundle travels over the wire (simulated via JSON serialisation). - * 3. A server restores those materials with a fresh AuthManager instance and - * proves it can sign with the delegated PKP using an independently created LitClient. - * Keeping this in the main e2e suite ensures we catch regressions in CI without - * relying on the ad-hoc ticket test. - */ - describe('server reuse flow', () => { - it('should sign using materials shipped over the wire', () => - createPregenDelegationServerReuseTest({ - authManager: ctx.authManager, - authData: ctx.aliceViemAccountAuthData, - pkpPublicKey: ctx.aliceViemAccountPkp.pubkey, - clientLitClient: ctx.litClient, - resolvedNetwork: ctx.resolvedNetwork, - })()); - }); - }); + registerWrappedKeysSuite(); - describe('EOA Native', () => { - console.log('๐Ÿ” Testing EOA native authentication and PKP minting'); - it('eoaNativeAuthFlow', () => createEoaNativeAuthFlowTest(ctx)()); - }); - }); - describe('wrapped keys', () => { - registerWrappedKeysTests(); - }); - }); + registerCustomAuthSuite( + () => testEnv, + () => bob + ); }); -// ====== These tests only run on paid networks ====== describeIfPaid('Paid networks tests', () => { registerPaymentDelegationTicketSuite(); }); diff --git a/packages/e2e/src/helper/constants.ts b/packages/e2e/src/helper/constants.ts new file mode 100644 index 000000000..6490f6727 --- /dev/null +++ b/packages/e2e/src/helper/constants.ts @@ -0,0 +1,2 @@ +export const EVE_VALIDATION_IPFS_CID = + 'QmcxWmo3jefFsPUnskJXYBwsJYtiFuMAH1nDQEs99AwzDe'; diff --git a/packages/e2e/src/helper/createEnvVars.ts b/packages/e2e/src/helper/createEnvVars.ts index cd64df726..a208a68c8 100644 --- a/packages/e2e/src/helper/createEnvVars.ts +++ b/packages/e2e/src/helper/createEnvVars.ts @@ -25,6 +25,8 @@ export type EnvVars = { localContextPath?: string; }; +const PrivateKeySchema = /^(0x)?[0-9a-fA-F]{64}$/; + // -- configure const testEnv: Record< EnvName, @@ -36,14 +38,17 @@ const testEnv: Record< export function createEnvVars(): EnvVars { // 1. Get network string - const networkEnv = process.env['NETWORK']; + const networkEnvRaw = process.env['NETWORK']; + const networkEnv = networkEnvRaw?.trim(); if ( !networkEnv || !SUPPORTED_NETWORKS.includes(networkEnv as SupportedNetwork) ) { throw new Error( - `โŒ NETWORK env var is not set or not supported. Found. ${networkEnv}` + `โŒ NETWORK env var is not set or not supported. Found: ${ + networkEnvRaw ?? 'undefined' + }` ); } @@ -53,20 +58,33 @@ export function createEnvVars(): EnvVars { // 2. Get private key let privateKey: `0x${string}`; + let privateKeyEnvKey: (typeof testEnv)[EnvName]['key']; if (network.includes('local')) { Object.assign(testEnv.local, { type: 'local' }); - privateKey = process.env[testEnv.local.key]!! as `0x${string}`; + privateKeyEnvKey = testEnv.local.key; + privateKey = (process.env[privateKeyEnvKey] ?? '').trim() as `0x${string}`; } else { Object.assign(testEnv.live, { type: 'live' }); - privateKey = process.env[testEnv.live.key]!! as `0x${string}`; + privateKeyEnvKey = testEnv.live.key; + privateKey = (process.env[privateKeyEnvKey] ?? '').trim() as `0x${string}`; } if (!privateKey) { throw new Error( - `โŒ You are on "${selectedNetwork}" environment, network ${network}. We are expecting ` + `โŒ Missing required env var ${privateKeyEnvKey} for "${selectedNetwork}" environment (${network}).` ); } + if (!PrivateKeySchema.test(privateKey)) { + throw new Error( + `โŒ Invalid private key format in ${privateKeyEnvKey}. Expected 32-byte hex string (64 chars) with optional 0x prefix.` + ); + } + + if (!privateKey.startsWith('0x')) { + privateKey = `0x${privateKey}` as unknown as `0x${string}`; + } + // 3. Get RPC URL let rpcUrl: string | undefined; let localContextPath: string | undefined; diff --git a/packages/e2e/src/helper/tests/payment-manager-flow.ts b/packages/e2e/src/helper/tests/payment-manager-flow.ts index 550fad65a..8ace68442 100644 --- a/packages/e2e/src/helper/tests/payment-manager-flow.ts +++ b/packages/e2e/src/helper/tests/payment-manager-flow.ts @@ -12,11 +12,22 @@ export const createPaymentManagerFlowTest = ( account: ctx.aliceViemAccount, }); - // Get the user's address from authContext (assuming it has a wallet or account) - const userAddress = - authContext.wallet?.account?.address || - authContext.account?.address || - ctx.aliceViemAccount.address; + // Extract address from authContext if it's an EOA auth context + // For EOA: account can be Account (has address) or WalletClient (has account.address) + // For PKP: account doesn't exist, fall back to aliceViemAccount + let userAddress: string; + if ('account' in authContext && authContext.account) { + const account = authContext.account as any; + if ('address' in account && account.address) { + userAddress = account.address; + } else if (account.account?.address) { + userAddress = account.account.address; + } else { + userAddress = ctx.aliceViemAccount.address; + } + } else { + userAddress = ctx.aliceViemAccount.address; + } console.log('๐Ÿ’ฐ Testing deposit functionality...'); // Test deposit diff --git a/packages/e2e/src/init.ts b/packages/e2e/src/init.ts index cfdd3f7dc..090eeff21 100644 --- a/packages/e2e/src/init.ts +++ b/packages/e2e/src/init.ts @@ -14,6 +14,7 @@ import { } from './helper/network'; import { z } from 'zod'; import { fundAccount } from './helper/fundAccount'; +import { EVE_VALIDATION_IPFS_CID } from './helper/constants'; import { getOrCreatePkp } from './helper/pkp-utils'; import { PKPData, AuthData, CustomAuthData } from '@lit-protocol/schemas'; import { @@ -46,9 +47,6 @@ const LIVE_NETWORK_LEDGER_DEPOSIT_AMOUNT = '1'; const MAINNET_NETWORK_FUNDING_AMOUNT = '0.01'; const MAINNET_LEDGER_DEPOSIT_AMOUNT = '0.01'; -const EVE_VALIDATION_IPFS_CID = - 'QmcxWmo3jefFsPUnskJXYBwsJYtiFuMAH1nDQEs99AwzDe'; - type BaseInitResult = { litClient: LitClientInstance; authManager: AuthManagerInstance; diff --git a/packages/e2e/src/suites/custom-auth.suite.ts b/packages/e2e/src/suites/custom-auth.suite.ts new file mode 100644 index 000000000..918f1ddb5 --- /dev/null +++ b/packages/e2e/src/suites/custom-auth.suite.ts @@ -0,0 +1,115 @@ +import { utils as litUtils } from '@lit-protocol/lit-client'; +import type { CustomAuthData } from '@lit-protocol/schemas'; +import type { AuthContext } from '../types'; +import { + createTestAccount, + CreateTestAccountResult, +} from '../helper/createTestAccount'; +import type { TestEnv } from '../helper/createTestEnv'; +import { fundAccount } from '../helper/fundAccount'; +import { EVE_VALIDATION_IPFS_CID } from '../helper/constants'; +import { registerEndpointSuite } from './endpoints.suite'; + +export function registerCustomAuthSuite( + getTestEnv: () => TestEnv, + getBobAccount: () => CreateTestAccountResult +) { + describe('Custom auth', () => { + let eve: CreateTestAccountResult; + let evePkp: { pubkey: string; ethAddress: `0x${string}`; tokenId: bigint }; + let eveCustomAuthData: CustomAuthData; + let eveCustomAuthContext: AuthContext; + + beforeAll(async () => { + const testEnv = getTestEnv(); + + eve = await createTestAccount(testEnv, { + label: 'Eve', + fundAccount: true, + fundLedger: true, + hasPKP: false, + fundPKP: false, + fundPKPLedger: false, + hasEoaAuthContext: false, + hasPKPAuthContext: false, + }); + + // Must match the validation Lit Action's expected dapp name. + const uniqueDappName = 'e2e-test-dapp'; + const authMethodConfig = litUtils.generateUniqueAuthMethodType({ + uniqueDappName, + }); + eveCustomAuthData = litUtils.generateAuthData({ + uniqueDappName, + uniqueAuthMethodType: authMethodConfig.bigint, + userId: 'eve', + }); + + const { pkpData } = await testEnv.litClient.mintWithCustomAuth({ + account: eve.account, + authData: eveCustomAuthData, + scope: 'sign-anything', + validationIpfsCid: EVE_VALIDATION_IPFS_CID, + }); + + evePkp = { + ...pkpData.data, + tokenId: pkpData.data.tokenId, + ethAddress: pkpData.data.ethAddress as `0x${string}`, + }; + + await fundAccount( + evePkp.ethAddress, + testEnv.masterAccount, + testEnv.networkModule, + { + label: 'Eve PKP', + ifLessThan: testEnv.config.nativeFundingAmount, + thenFund: testEnv.config.nativeFundingAmount, + } + ); + + await testEnv.masterPaymentManager.depositForUser({ + userAddress: evePkp.ethAddress, + amountInEth: testEnv.config.ledgerDepositAmount, + }); + + eveCustomAuthContext = await testEnv.authManager.createCustomAuthContext({ + pkpPublicKey: evePkp.pubkey, + authConfig: { + resources: [ + ['pkp-signing', '*'], + ['lit-action-execution', '*'], + ['access-control-condition-decryption', '*'], + ], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: testEnv.litClient, + customAuthParams: { + litActionIpfsId: EVE_VALIDATION_IPFS_CID, + jsParams: { + pkpPublicKey: evePkp.pubkey, + username: 'eve', + password: 'lit', + authMethodId: eveCustomAuthData.authMethodId, + }, + }, + }); + }); + + const getEveAccount = () => eve; + const getPkpPublicKey = () => evePkp.pubkey; + const getPkpEthAddress = () => evePkp.ethAddress; + + registerEndpointSuite(getTestEnv, () => eveCustomAuthContext, { + getPkpPublicKey, + getPkpEthAddress, + getAliceAccount: getEveAccount, + getBobAccount, + includePaymentFlows: false, + includeEncryptDecryptFlow: false, + includePermissionsFlow: false, + includeViewPkpsByAuthData: false, + }); + }); +} diff --git a/packages/e2e/src/suites/endpoints.suite.ts b/packages/e2e/src/suites/endpoints.suite.ts new file mode 100644 index 000000000..29dac46cc --- /dev/null +++ b/packages/e2e/src/suites/endpoints.suite.ts @@ -0,0 +1,452 @@ +import { createAccBuilder } from '@lit-protocol/access-control-conditions'; +import { ViemAccountAuthenticator } from '@lit-protocol/auth'; +import type { AuthData } from '@lit-protocol/schemas'; +import type { AuthContext } from '../types'; +import type { TestEnv } from '../helper/createTestEnv'; +import type { CreateTestAccountResult } from '../helper/createTestAccount'; +import { + PKP_SIGN_TRANSIENT_FRAGMENTS, + SIGN_ECDSA_LIT_ACTION_CODE, + withRetry, +} from './suite-utils'; + +export type EndpointSuiteOptions = { + getPkpPublicKey: () => string; + getPkpEthAddress: () => `0x${string}`; + getAliceAccount: () => CreateTestAccountResult; + getBobAccount: () => CreateTestAccountResult; + includePaymentFlows?: boolean; + includeEncryptDecryptFlow?: boolean; + includePermissionsFlow?: boolean; + includeViewPkpsByAuthData?: boolean; + authDataOverride?: AuthData; + getAccsAddress?: (authContext: AuthContext) => string; +}; + +export function registerEndpointSuite( + getTestEnv: () => TestEnv, + getAuthContext: () => AuthContext, + opts: EndpointSuiteOptions +) { + describe('endpoints', () => { + it('pkpSign', async () => { + const testEnv = getTestEnv(); + const res = await withRetry( + () => + testEnv.litClient.chain.ethereum.pkpSign({ + authContext: getAuthContext(), + pubKey: opts.getPkpPublicKey(), + toSign: 'Hello, world!', + }), + { transientMessageFragments: PKP_SIGN_TRANSIENT_FRAGMENTS } + ); + + expect(res.signature).toBeDefined(); + }); + + it('executeJs', async () => { + const testEnv = getTestEnv(); + const result = await testEnv.litClient.executeJs({ + code: SIGN_ECDSA_LIT_ACTION_CODE, + authContext: getAuthContext(), + jsParams: { + message: 'Test message from revamp e2e executeJs', + sigName: 'revamp-e2e-sig', + toSign: 'Test message from revamp e2e executeJs', + publicKey: opts.getPkpPublicKey(), + }, + }); + + expect(result).toBeDefined(); + expect(result.signatures).toBeDefined(); + }); + + it('viewPKPsByAddress', async () => { + const testEnv = getTestEnv(); + const pkps = await testEnv.litClient.viewPKPsByAddress({ + ownerAddress: opts.getPkpEthAddress(), + pagination: { limit: 10, offset: 0 }, + }); + + expect(pkps).toBeDefined(); + expect(Array.isArray(pkps.pkps)).toBe(true); + expect(typeof pkps.pagination.total).toBe('number'); + expect(typeof pkps.pagination.hasMore).toBe('boolean'); + }); + + if (opts.includeViewPkpsByAuthData ?? true) { + it('viewPKPsByAuthData', async () => { + const testEnv = getTestEnv(); + const aliceAccount = opts.getAliceAccount(); + const pkps = await withRetry( + async () => { + const authData = + opts.authDataOverride ?? + (await ViemAccountAuthenticator.authenticate( + aliceAccount.account + )); + + const res = await testEnv.litClient.viewPKPsByAuthData({ + authData: { + authMethodType: authData.authMethodType, + authMethodId: authData.authMethodId, + accessToken: authData.accessToken || 'mock-token', + }, + pagination: { limit: 10, offset: 0 }, + }); + + if (!res.pkps?.length) { + throw new Error('No PKPs found yet'); + } + + return res; + }, + { + transientMessageFragments: [ + 'Verification failed', + 'Failed to verify signature', + 'authentication failed', + 'No PKPs found yet', + ...PKP_SIGN_TRANSIENT_FRAGMENTS, + ], + } + ); + + expect(pkps).toBeDefined(); + expect(Array.isArray(pkps.pkps)).toBe(true); + + const firstPkp = pkps.pkps[0]; + expect(firstPkp.tokenId).toBeDefined(); + expect(firstPkp.pubkey).toBeDefined(); + expect(firstPkp.ethAddress).toBeDefined(); + }); + } + + it('pkpEncryptDecrypt', async () => { + const testEnv = getTestEnv(); + const authContext = getAuthContext(); + const addressForAccs = + opts.getAccsAddress?.(authContext) ?? opts.getPkpEthAddress(); + + const builder = createAccBuilder(); + const accs = builder + .requireWalletOwnership(addressForAccs) + .on('ethereum') + .build(); + + const dataToEncrypt = 'Hello from PKP encrypt-decrypt revamp test!'; + const encryptedData = await testEnv.litClient.encrypt({ + dataToEncrypt, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + }); + + expect(encryptedData.ciphertext).toBeDefined(); + expect(encryptedData.dataToEncryptHash).toBeDefined(); + + const decryptedData = await testEnv.litClient.decrypt({ + data: encryptedData, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + authContext, + }); + + expect(decryptedData.convertedData).toBe(dataToEncrypt); + }); + + if (opts.includeEncryptDecryptFlow ?? true) { + it('encryptDecryptFlow', async () => { + const testEnv = getTestEnv(); + const aliceAccount = opts.getAliceAccount(); + const bobAccount = opts.getBobAccount(); + const authContext = getAuthContext(); + const senderAddress = + opts.getAccsAddress?.(authContext) ?? opts.getPkpEthAddress(); + const builder = createAccBuilder(); + const accs = builder + .requireWalletOwnership(bobAccount.account.address) + .on('ethereum') + .build(); + + const stringData = 'Hello from encrypt-decrypt flow revamp test!'; + const encryptedStringData = await testEnv.litClient.encrypt({ + dataToEncrypt: stringData, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + }); + + expect(encryptedStringData.metadata?.dataType).toBe('string'); + + const jsonData = { + message: 'Test JSON data', + sender: senderAddress, + recipient: bobAccount.account.address, + timestamp: Date.now(), + }; + + const encryptedJsonData = await testEnv.litClient.encrypt({ + dataToEncrypt: jsonData, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + }); + + expect(encryptedJsonData.metadata?.dataType).toBe('json'); + + const uint8Data = new Uint8Array([72, 101, 108, 108, 111]); + const encryptedUint8Data = await testEnv.litClient.encrypt({ + dataToEncrypt: uint8Data, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + }); + + expect(encryptedUint8Data.ciphertext).toBeDefined(); + + const documentData = new TextEncoder().encode( + 'This is a PDF document content...' + ); + const encryptedFileData = await testEnv.litClient.encrypt({ + dataToEncrypt: documentData, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + metadata: { + dataType: 'file', + mimeType: 'application/pdf', + filename: 'secret-document.pdf', + size: documentData.length, + custom: { + author: 'Alice', + createdDate: new Date().toISOString(), + confidential: true, + }, + }, + }); + + expect(encryptedFileData.metadata?.dataType).toBe('file'); + + const bobAuthContext = + bobAccount.eoaAuthContext ?? + (await testEnv.authManager.createEoaAuthContext({ + config: { account: bobAccount.account }, + authConfig: { + domain: 'localhost', + statement: 'Decrypt test data', + expiration: new Date( + Date.now() + 1000 * 60 * 60 * 24 + ).toISOString(), + resources: [['access-control-condition-decryption', '*']], + }, + litClient: testEnv.litClient, + })); + + const decryptedStringResponse = await testEnv.litClient.decrypt({ + data: encryptedStringData, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + authContext: bobAuthContext, + }); + + expect(decryptedStringResponse.convertedData).toBe(stringData); + + const decryptedJsonResponse = await testEnv.litClient.decrypt({ + ciphertext: encryptedJsonData.ciphertext, + dataToEncryptHash: encryptedJsonData.dataToEncryptHash, + metadata: encryptedJsonData.metadata, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + authContext: bobAuthContext, + }); + + expect(decryptedJsonResponse.convertedData).toEqual(jsonData); + + const decryptedUint8Response = await testEnv.litClient.decrypt({ + data: encryptedUint8Data, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + authContext: bobAuthContext, + }); + + if (decryptedUint8Response.convertedData) { + expect(decryptedUint8Response.convertedData).toEqual(uint8Data); + } else { + expect(decryptedUint8Response.decryptedData).toEqual(uint8Data); + } + + const decryptedFileResponse = await testEnv.litClient.decrypt({ + data: encryptedFileData, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + authContext: bobAuthContext, + }); + + expect(decryptedFileResponse.metadata?.dataType).toBe('file'); + expect(decryptedFileResponse.metadata?.filename).toBe( + 'secret-document.pdf' + ); + expect(decryptedFileResponse.metadata?.custom?.author).toBe('Alice'); + + if ( + typeof File !== 'undefined' && + decryptedFileResponse.convertedData instanceof File + ) { + const fileArrayBuffer = + await decryptedFileResponse.convertedData.arrayBuffer(); + const fileUint8Array = new Uint8Array(fileArrayBuffer); + expect(fileUint8Array).toEqual(documentData); + } else { + expect(decryptedFileResponse.convertedData).toEqual(documentData); + } + }); + } + + if (opts.includePermissionsFlow ?? true) { + it('pkpPermissionsManagerFlow', async () => { + const testEnv = getTestEnv(); + const aliceAccount = opts.getAliceAccount(); + const authContext = getAuthContext(); + const pkpPublicKey = opts.getPkpPublicKey(); + + const pkpViemAccount = await testEnv.litClient.getPkpViemAccount({ + pkpPublicKey, + authContext, + chainConfig: testEnv.litClient.getChainConfig().viemConfig, + }); + + const pkpPermissionsManager = + await testEnv.litClient.getPKPPermissionsManager({ + pkpIdentifier: { tokenId: aliceAccount.pkp?.tokenId }, + account: pkpViemAccount, + }); + + const initialContext = + await pkpPermissionsManager.getPermissionsContext(); + const initialAuthMethodsCount = initialContext.authMethods.length; + + const testAuthMethodParams = { + authMethodType: 1, + authMethodId: '0x1234567890abcdef1234567890abcdef12345678', + userPubkey: + '0x04abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + scopes: ['sign-anything'] as ( + | 'sign-anything' + | 'no-permissions' + | 'personal-sign' + )[], + }; + + const addAuthMethodTx = + await pkpPermissionsManager.addPermittedAuthMethod( + testAuthMethodParams + ); + expect(addAuthMethodTx.receipt.status).toBe('success'); + + const authMethodsAfterAdd = + await pkpPermissionsManager.getPermittedAuthMethods(); + expect(authMethodsAfterAdd.length).toBe(initialAuthMethodsCount + 1); + + const removeScopeTx = + await pkpPermissionsManager.removePermittedAuthMethodScope({ + authMethodType: testAuthMethodParams.authMethodType, + authMethodId: testAuthMethodParams.authMethodId, + scopeId: 1, + }); + expect(removeScopeTx.receipt.status).toBe('success'); + + const removeAuthMethodTx = + await pkpPermissionsManager.removePermittedAuthMethod({ + authMethodType: testAuthMethodParams.authMethodType, + authMethodId: testAuthMethodParams.authMethodId, + }); + expect(removeAuthMethodTx.receipt.status).toBe('success'); + + const finalAuthMethods = + await pkpPermissionsManager.getPermittedAuthMethods(); + expect(finalAuthMethods.length).toBe(initialAuthMethodsCount); + }); + } + + if (opts.includePaymentFlows) { + it('paymentManagerFlow', async () => { + const testEnv = getTestEnv(); + const aliceAccount = opts.getAliceAccount(); + const authContext = getAuthContext(); + const paymentManager = await testEnv.litClient.getPaymentManager({ + account: aliceAccount.account, + }); + + // Extract address from authContext if it's an EOA auth context + // For EOA: account can be Account (has address) or WalletClient (has account.address) + // For PKP: account doesn't exist, fall back to aliceAccount + let userAddress: string; + if ('account' in authContext && authContext.account) { + const account = authContext.account as any; + if ('address' in account && account.address) { + userAddress = account.address; + } else if (account.account?.address) { + userAddress = account.account.address; + } else { + userAddress = aliceAccount.account.address; + } + } else { + userAddress = aliceAccount.account.address; + } + + const depositAmount = '0.00001'; + const depositResult = await paymentManager.deposit({ + amountInEth: depositAmount, + }); + expect(depositResult.receipt).toBeDefined(); + + const balanceInfo = await paymentManager.getBalance({ userAddress }); + expect(Number(balanceInfo.raw.totalBalance)).toBeGreaterThan(0); + + const withdrawAmount = '0.000005'; + const withdrawRequestResult = await paymentManager.requestWithdraw({ + amountInEth: withdrawAmount, + }); + expect(withdrawRequestResult.receipt).toBeDefined(); + }); + + it('paymentDelegationFlow', async () => { + const testEnv = getTestEnv(); + const aliceAccount = opts.getAliceAccount(); + const bobAccount = opts.getBobAccount(); + const alicePaymentManager = await testEnv.litClient.getPaymentManager({ + account: aliceAccount.account, + }); + const bobPaymentManager = await testEnv.litClient.getPaymentManager({ + account: bobAccount.account, + }); + + const aliceAddress = aliceAccount.account.address; + const bobAddress = bobAccount.account.address; + + const initialPayers = await bobPaymentManager.getPayers({ + userAddress: bobAddress, + }); + const initialUsers = await alicePaymentManager.getUsers({ + payerAddress: aliceAddress, + }); + + const delegateTx = await alicePaymentManager.delegatePayments({ + userAddress: bobAddress, + }); + expect(delegateTx.receipt.status).toBe('success'); + + const payersAfterDelegate = await bobPaymentManager.getPayers({ + userAddress: bobAddress, + }); + expect(payersAfterDelegate.length).toBe(initialPayers.length + 1); + + const usersAfterDelegate = await alicePaymentManager.getUsers({ + payerAddress: aliceAddress, + }); + expect(usersAfterDelegate.length).toBe(initialUsers.length + 1); + + const undelegateTx = await alicePaymentManager.undelegatePayments({ + userAddress: bobAddress, + }); + expect(undelegateTx.receipt.status).toBe('success'); + }); + } + }); +} diff --git a/packages/e2e/src/suites/eoa-native.suite.ts b/packages/e2e/src/suites/eoa-native.suite.ts new file mode 100644 index 000000000..a1e7b33c8 --- /dev/null +++ b/packages/e2e/src/suites/eoa-native.suite.ts @@ -0,0 +1,53 @@ +import { ViemAccountAuthenticator } from '@lit-protocol/auth'; +import type { CreateTestAccountResult } from '../helper/createTestAccount'; +import type { TestEnv } from '../helper/createTestEnv'; + +export function registerEoaNativeSuite( + getTestEnv: () => TestEnv, + getAliceAccount: () => CreateTestAccountResult +) { + describe('EOA native authentication and PKP minting', () => { + it('authenticates via ViemAccountAuthenticator', async () => { + const alice = getAliceAccount(); + const authDataViemAccount = await ViemAccountAuthenticator.authenticate( + alice.account + ); + + expect(authDataViemAccount.accessToken).toBeDefined(); + expect(authDataViemAccount.authMethodType).toBeDefined(); + expect(authDataViemAccount.authMethodId).toBeDefined(); + + const authSig = JSON.parse(authDataViemAccount.accessToken); + expect(authSig.sig).toBeDefined(); + expect(authSig.derivedVia).toBeDefined(); + expect(authSig.signedMessage).toBeDefined(); + expect(authSig.address).toBeDefined(); + + const authData = await ViemAccountAuthenticator.authenticate( + alice.account + ); + expect(authData.authMethodType).toBe(authDataViemAccount.authMethodType); + expect(authData.authMethodId).toBe(authDataViemAccount.authMethodId); + }); + + it('mints a PKP using EOA', async () => { + const testEnv = getTestEnv(); + const alice = getAliceAccount(); + + const mintedPkpWithEoa = await testEnv.litClient.mintWithEoa({ + account: alice.account, + }); + + expect(mintedPkpWithEoa.data).toBeDefined(); + expect(mintedPkpWithEoa.txHash).toBeDefined(); + + const pkpData = mintedPkpWithEoa.data; + expect(pkpData.tokenId).toBeDefined(); + expect(pkpData.pubkey).toBeDefined(); + expect(pkpData.ethAddress).toMatch(/^0x[a-fA-F0-9]{40}$/); + expect(pkpData.pubkey).toMatch(/^0x04[a-fA-F0-9]{128}$/); + expect(typeof pkpData.tokenId).toBe('bigint'); + expect(mintedPkpWithEoa.txHash).toMatch(/^0x[a-fA-F0-9]{64}$/); + }); + }); +} diff --git a/packages/e2e/src/suites/pkp-pre-generated-materials.suite.ts b/packages/e2e/src/suites/pkp-pre-generated-materials.suite.ts new file mode 100644 index 000000000..d9da70ba6 --- /dev/null +++ b/packages/e2e/src/suites/pkp-pre-generated-materials.suite.ts @@ -0,0 +1,204 @@ +import { createAccBuilder } from '@lit-protocol/access-control-conditions'; +import { generateSessionKeyPair } from '@lit-protocol/auth'; +import type { AuthData } from '@lit-protocol/schemas'; +import type { ResolvedNetwork } from '../helper/network'; +import type { CreateTestAccountResult } from '../helper/createTestAccount'; +import type { TestEnv } from '../helper/createTestEnv'; +import type { AuthContext } from '../types'; +import { createPregenDelegationServerReuseTest } from '../test-helpers/signSessionKey/pregen-delegation'; +import { + PKP_SIGN_TRANSIENT_FRAGMENTS, + SIGN_ECDSA_LIT_ACTION_CODE, + withRetry, +} from './suite-utils'; + +export function registerPkpPreGeneratedMaterialsSuite( + getTestEnv: () => TestEnv, + getAliceAccount: () => CreateTestAccountResult, + getResolvedNetwork: () => ResolvedNetwork +) { + describe('PKP auth with pre-generated materials', () => { + let preGeneratedAuthContext: AuthContext; + + beforeAll(async () => { + const testEnv = getTestEnv(); + const alice = getAliceAccount(); + + if (!alice.pkp) { + throw new Error('Alice is missing a PKP'); + } + if (!alice.authData) { + throw new Error('Alice is missing authData'); + } + + const sessionKeyPair = generateSessionKeyPair(); + const delegationAuthSig = + await testEnv.authManager.generatePkpDelegationAuthSig({ + pkpPublicKey: alice.pkp.pubkey, + authData: alice.authData, + sessionKeyPair, + authConfig: { + resources: [ + ['pkp-signing', '*'], + ['lit-action-execution', '*'], + ['access-control-condition-decryption', '*'], + ], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: testEnv.litClient, + }); + + preGeneratedAuthContext = + await testEnv.authManager.createPkpAuthContextFromPreGenerated({ + pkpPublicKey: alice.pkp.pubkey, + sessionKeyPair, + delegationAuthSig, + authData: alice.authData, + }); + }); + + describe('endpoints', () => { + it('pkpSign with pre-generated materials', async () => { + const testEnv = getTestEnv(); + const alice = getAliceAccount(); + + const res = await withRetry( + () => + testEnv.litClient.chain.ethereum.pkpSign({ + authContext: preGeneratedAuthContext, + pubKey: alice.pkp!.pubkey, + toSign: 'Hello from pre-generated PKP auth', + }), + { transientMessageFragments: PKP_SIGN_TRANSIENT_FRAGMENTS } + ); + + expect(res.signature).toBeDefined(); + }); + + it('executeJs with pre-generated materials', async () => { + const testEnv = getTestEnv(); + const alice = getAliceAccount(); + + const result = await testEnv.litClient.executeJs({ + code: SIGN_ECDSA_LIT_ACTION_CODE, + authContext: preGeneratedAuthContext, + jsParams: { + message: 'Pre-generated materials executeJs test', + sigName: 'pregen-e2e-sig', + toSign: 'Pre-generated materials executeJs test', + publicKey: alice.pkp!.pubkey, + }, + }); + + expect(result).toBeDefined(); + expect(result.signatures).toBeDefined(); + }); + + it('pkpEncryptDecrypt with pre-generated materials', async () => { + const testEnv = getTestEnv(); + const alice = getAliceAccount(); + + const builder = createAccBuilder(); + const accs = builder + .requireWalletOwnership(alice.pkp!.ethAddress) + .on('ethereum') + .build(); + + const dataToEncrypt = 'Hello from pre-generated encrypt/decrypt test!'; + const encryptedData = await testEnv.litClient.encrypt({ + dataToEncrypt, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + }); + + const decryptedData = await testEnv.litClient.decrypt({ + data: encryptedData, + unifiedAccessControlConditions: accs, + chain: 'ethereum', + authContext: preGeneratedAuthContext, + }); + + expect(decryptedData.convertedData).toBe(dataToEncrypt); + }); + }); + + describe('error handling', () => { + it('should reject when only sessionKeyPair is provided', async () => { + const testEnv = getTestEnv(); + const alice = getAliceAccount(); + + const tempAuthContext: any = + await testEnv.authManager.createPkpAuthContext({ + authData: alice.authData as AuthData, + pkpPublicKey: alice.pkp!.pubkey, + authConfig: { + resources: [['pkp-signing', '*']], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: testEnv.litClient, + }); + + const sessionKeyPair = tempAuthContext.sessionKeyPair; + + await expect( + testEnv.authManager.createPkpAuthContext({ + authData: alice.authData as AuthData, + pkpPublicKey: alice.pkp!.pubkey, + authConfig: { + resources: [['pkp-signing', '*']], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: testEnv.litClient, + sessionKeyPair, + }) + ).rejects.toThrow( + 'Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided' + ); + }); + + it('should reject when only delegationAuthSig is provided', async () => { + const testEnv = getTestEnv(); + const alice = getAliceAccount(); + + const tempAuthContext: any = + await testEnv.authManager.createPkpAuthContext({ + authData: alice.authData as AuthData, + pkpPublicKey: alice.pkp!.pubkey, + authConfig: { + resources: [['pkp-signing', '*']], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: testEnv.litClient, + }); + + const delegationAuthSig = await tempAuthContext.authNeededCallback(); + + await expect( + testEnv.authManager.createPkpAuthContext({ + authData: alice.authData as AuthData, + pkpPublicKey: alice.pkp!.pubkey, + authConfig: { + resources: [['pkp-signing', '*']], + expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + }, + litClient: testEnv.litClient, + delegationAuthSig, + }) + ).rejects.toThrow( + 'Both sessionKeyPair and delegationAuthSig must be provided together, or neither should be provided' + ); + }); + }); + + describe('server reuse flow', () => { + it('should sign using materials shipped over the wire', () => + createPregenDelegationServerReuseTest({ + authManager: getTestEnv().authManager, + authData: getAliceAccount().authData as AuthData, + pkpPublicKey: getAliceAccount().pkp!.pubkey, + clientLitClient: getTestEnv().litClient, + resolvedNetwork: getResolvedNetwork(), + })()); + }); + }); +} diff --git a/packages/e2e/src/suites/suite-utils.ts b/packages/e2e/src/suites/suite-utils.ts new file mode 100644 index 000000000..d6a8cba01 --- /dev/null +++ b/packages/e2e/src/suites/suite-utils.ts @@ -0,0 +1,66 @@ +export const SIGN_ECDSA_LIT_ACTION_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, + }); +})();`; + +const DEFAULT_TRANSIENT_FRAGMENTS = [ + 'Rate Limit Exceeded', + 'rate limit', + '429', +] as const; + +export const PKP_SIGN_TRANSIENT_FRAGMENTS = [ + ...DEFAULT_TRANSIENT_FRAGMENTS, + 'Pubkey share not found', + 'unable to get signature share', + 'NodeUnknownError', +] as const; + +export async function withRetry( + fn: () => Promise, + options: { + retries?: number; + baseDelayMs?: number; + transientMessageFragments?: readonly string[]; + } = {} +): Promise { + const { + retries = 3, + baseDelayMs = 1500, + transientMessageFragments = DEFAULT_TRANSIENT_FRAGMENTS, + } = options; + + let lastError: unknown; + for (let attempt = 1; attempt <= retries; attempt++) { + try { + return await fn(); + } catch (err: any) { + lastError = err; + const message = String(err?.message ?? err); + const isTransient = transientMessageFragments.some((fragment) => + message.includes(fragment) + ); + + if (isTransient && attempt < retries) { + const delay = baseDelayMs * attempt; + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + + throw err; + } + } + + throw lastError; +} diff --git a/packages/e2e/src/suites/viem.suite.ts b/packages/e2e/src/suites/viem.suite.ts new file mode 100644 index 000000000..2dc94bd23 --- /dev/null +++ b/packages/e2e/src/suites/viem.suite.ts @@ -0,0 +1,106 @@ +import type { AuthContext } from '../types'; +import type { TestEnv } from '../helper/createTestEnv'; +import { withRetry } from './suite-utils'; + +export function registerViemSuite( + getTestEnv: () => TestEnv, + getAuthContext: () => AuthContext, + getPkpPublicKey: () => string +) { + describe('integrations', () => { + describe('pkp viem account', () => { + it('sign message', async () => { + const testEnv = getTestEnv(); + const pkpViemAccount = await testEnv.litClient.getPkpViemAccount({ + pkpPublicKey: getPkpPublicKey(), + authContext: getAuthContext(), + chainConfig: testEnv.litClient.getChainConfig().viemConfig, + }); + + const signature = await withRetry(() => + pkpViemAccount.signMessage({ + message: 'Hello Viem + Lit', + }) + ); + + expect(signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + }); + + it('sign transaction', async () => { + const testEnv = getTestEnv(); + const pkpViemAccount = await testEnv.litClient.getPkpViemAccount({ + pkpPublicKey: getPkpPublicKey(), + authContext: getAuthContext(), + chainConfig: testEnv.litClient.getChainConfig().viemConfig, + }); + + const txRequest = { + chainId: testEnv.litClient.getChainConfig().viemConfig.id, + to: pkpViemAccount.address, + value: BigInt('1000000000000000'), + }; + + const signedTx = await withRetry(() => + pkpViemAccount.signTransaction(txRequest) + ); + expect(signedTx).toMatch(/^0x[a-fA-F0-9]+$/); + }); + + it('sign typed data', async () => { + const testEnv = getTestEnv(); + const pkpViemAccount = await testEnv.litClient.getPkpViemAccount({ + pkpPublicKey: getPkpPublicKey(), + authContext: getAuthContext(), + chainConfig: testEnv.litClient.getChainConfig().viemConfig, + }); + + const { getAddress } = await import('viem'); + + const typedData = { + domain: { + name: 'E2E Test Service', + version: '1', + chainId: BigInt(1), + verifyingContract: getAddress( + '0x1e0Ae8205e9726E6F296ab8869930607a853204C' + ), + }, + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + }, + primaryType: 'Mail' as const, + message: { + from: { + name: 'Alice', + wallet: getAddress('0x2111111111111111111111111111111111111111'), + }, + to: { + name: 'Bob', + wallet: getAddress('0x3111111111111111111111111111111111111111'), + }, + contents: 'Hello from revamp e2e typed data test!', + }, + } as const; + + const signature = await withRetry(() => + pkpViemAccount.signTypedData(typedData) + ); + expect(signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + }); + }); + }); +} diff --git a/packages/e2e/src/suites/wrapped-keys.suite.ts b/packages/e2e/src/suites/wrapped-keys.suite.ts new file mode 100644 index 000000000..c7edbcd39 --- /dev/null +++ b/packages/e2e/src/suites/wrapped-keys.suite.ts @@ -0,0 +1,7 @@ +import { registerWrappedKeysTests } from '../test-helpers/executeJs/wrappedKeys'; + +export function registerWrappedKeysSuite() { + describe('wrapped keys', () => { + registerWrappedKeysTests(); + }); +}