diff --git a/packages/wallet/core/src/signers/session/explicit.ts b/packages/wallet/core/src/signers/session/explicit.ts index e373b0469..cd72b2256 100644 --- a/packages/wallet/core/src/signers/session/explicit.ts +++ b/packages/wallet/core/src/signers/session/explicit.ts @@ -263,10 +263,7 @@ export class Explicit implements ExplicitSessionSigner { } // Sign it - const useDeprecatedHash = - Address.isEqual(sessionManagerAddress, Extensions.Dev1.sessions) || - Address.isEqual(sessionManagerAddress, Extensions.Dev2.sessions) - const callHash = SessionSignature.hashCallWithReplayProtection(payload, callIdx, chainId, useDeprecatedHash) + const callHash = SessionSignature.hashPayloadWithCallIdx(wallet, payload, callIdx, chainId, sessionManagerAddress) const sessionSignature = await this._privateKey.signDigest(Bytes.fromHex(callHash)) return { permissionIndex: BigInt(permissionIndex), diff --git a/packages/wallet/core/src/signers/session/implicit.ts b/packages/wallet/core/src/signers/session/implicit.ts index 4e74c3a0f..71b112865 100644 --- a/packages/wallet/core/src/signers/session/implicit.ts +++ b/packages/wallet/core/src/signers/session/implicit.ts @@ -115,10 +115,7 @@ export class Implicit implements ImplicitSessionSigner { if (!isSupported) { throw new Error('Unsupported call') } - const useDeprecatedHash = - Address.isEqual(sessionManagerAddress, Extensions.Dev1.sessions) || - Address.isEqual(sessionManagerAddress, Extensions.Dev2.sessions) - const callHash = SessionSignature.hashCallWithReplayProtection(payload, callIdx, chainId, useDeprecatedHash) + const callHash = SessionSignature.hashPayloadWithCallIdx(wallet, payload, callIdx, chainId, sessionManagerAddress) const sessionSignature = await this._privateKey.signDigest(Bytes.fromHex(callHash)) return { attestation: this._attestation, diff --git a/packages/wallet/core/test/session-manager.test.ts b/packages/wallet/core/test/session-manager.test.ts index 6a0bd156b..bfbc28b17 100644 --- a/packages/wallet/core/test/session-manager.test.ts +++ b/packages/wallet/core/test/session-manager.test.ts @@ -13,7 +13,7 @@ import { USDC_ADDRESS, } from './constants' import { Extensions } from '@0xsequence/wallet-primitives' -import { ExplicitSessionConfig } from '../../wdk/src/sequence/types/sessions.js' +import { ExplicitSessionConfig } from '../src/utils/session/types.js' const { PermissionBuilder, ERC20PermissionBuilder } = Utils @@ -34,6 +34,10 @@ const ALL_EXTENSIONS = [ name: 'Rc3', ...Extensions.Rc3, }, + { + name: 'Rc4', + ...Extensions.Rc4, + }, ] // Handle the increment call being first or last depending on the session manager version @@ -561,7 +565,7 @@ for (const extension of ALL_EXTENSIONS) { } // Sign the transaction - expect(sessionManager.signSapient(wallet.address, chainId, payload, imageHash)).rejects.toThrow( + await expect(sessionManager.signSapient(wallet.address, chainId, payload, imageHash)).rejects.toThrow( `Signer supporting call is expired: ${explicitSigner.address}`, ) }, diff --git a/packages/wallet/dapp-client/src/ChainSessionManager.ts b/packages/wallet/dapp-client/src/ChainSessionManager.ts index 6a7b3572e..6785c18cd 100644 --- a/packages/wallet/dapp-client/src/ChainSessionManager.ts +++ b/packages/wallet/dapp-client/src/ChainSessionManager.ts @@ -221,7 +221,7 @@ export class ChainSessionManager { stateProvider: this.stateProvider, }) this.sessionManager = new Signers.SessionManager(this.wallet, { - sessionManagerAddress: Extensions.Rc3.sessions, + sessionManagerAddress: Extensions.Rc4.sessions, provider: this.provider!, }) this.isInitialized = true @@ -730,7 +730,7 @@ export class ChainSessionManager { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const tempManager = new Signers.SessionManager(this.wallet, { - sessionManagerAddress: Extensions.Rc3.sessions, + sessionManagerAddress: Extensions.Rc4.sessions, provider: this.provider, }) const topology = await tempManager.getTopology() diff --git a/packages/wallet/primitives/src/extensions/index.ts b/packages/wallet/primitives/src/extensions/index.ts index 3d7582cbc..82fadf786 100644 --- a/packages/wallet/primitives/src/extensions/index.ts +++ b/packages/wallet/primitives/src/extensions/index.ts @@ -24,5 +24,12 @@ export const Rc3: Extensions = { sessions: '0x0000000000CC58810c33F3a0D78aA1Ed80FaDcD8', } +//FIXME This is a placeholder for the actual Rc4 extension +export const Rc4: Extensions = { + passkeys: Rc3.passkeys, + recovery: Rc3.recovery, + sessions: '0x6f1092241e82bD0786C5DA6b6919AD38966fff8E', +} + export * as Passkeys from './passkeys.js' export * as Recovery from './recovery.js' diff --git a/packages/wallet/primitives/src/payload.ts b/packages/wallet/primitives/src/payload.ts index c8f05ee8d..1359abdbe 100644 --- a/packages/wallet/primitives/src/payload.ts +++ b/packages/wallet/primitives/src/payload.ts @@ -184,6 +184,10 @@ export function isCalls4337_07(payload: Payload): payload is Calls4337_07 { return payload.type === 'call_4337_07' } +export function isParented(payload: Payload): payload is Parented { + return 'parentWallets' in payload +} + export function toRecovery(payload: T): Recovery { if (isRecovery(payload)) { return payload diff --git a/packages/wallet/primitives/src/session-signature.ts b/packages/wallet/primitives/src/session-signature.ts index a9f5144a1..c3f67ca24 100644 --- a/packages/wallet/primitives/src/session-signature.ts +++ b/packages/wallet/primitives/src/session-signature.ts @@ -1,4 +1,5 @@ import { Address, Bytes, Hash, Hex } from 'ox' +import { Attestation, Extensions, Payload } from './index.js' import { MAX_PERMISSIONS_COUNT } from './permission.js' import { decodeSessionsTopology, @@ -10,7 +11,6 @@ import { } from './session-config.js' import { RSY } from './signature.js' import { minBytesFor, packRSY, unpackRSY } from './utils.js' -import { Attestation, Payload } from './index.js' export type ImplicitSessionCallSignature = { attestation: Attestation.Attestation @@ -273,22 +273,48 @@ export function decodeSessionSignature(encodedSignatures: Bytes.Bytes): { // Call encoding -export function hashCallWithReplayProtection( - payload: Payload.Calls, +/** + * Hashes a call with replay protection parameters. + * @param payload The payload to hash. + * @param callIdx The index of the call to hash. + * @param chainId The chain ID. Use 0 when noChainId enabled. + * @param sessionManagerAddress The session manager address to compile the hash for. Only required to support deprecated hash encodings for Dev1, Dev2 and Rc3. + * @returns The hash of the call with replay protection parameters for sessions. + */ +export function hashPayloadWithCallIdx( + wallet: Address.Address, + payload: Payload.Calls & Payload.Parent, callIdx: number, chainId: number, - skipCallIdx: boolean = false, // Deprecated. Dev1 and Dev2 support + sessionManagerAddress?: Address.Address, ): Hex.Hex { - const call = payload.calls[callIdx]! - return Hex.fromBytes( - Hash.keccak256( - Bytes.concat( - Bytes.fromNumber(chainId, { size: 32 }), - Bytes.fromNumber(payload.space, { size: 32 }), - Bytes.fromNumber(payload.nonce, { size: 32 }), - skipCallIdx ? Bytes.from([]) : Bytes.fromNumber(callIdx, { size: 32 }), - Bytes.fromHex(Payload.hashCall(call)), + // Support deprecated hashes for Dev1, Dev2 and Rc3 + const deprecatedHashing = + sessionManagerAddress && + (Address.isEqual(sessionManagerAddress, Extensions.Dev1.sessions) || + Address.isEqual(sessionManagerAddress, Extensions.Dev2.sessions) || + Address.isEqual(sessionManagerAddress, Extensions.Rc3.sessions)) + if (deprecatedHashing) { + const call = payload.calls[callIdx]! + const ignoreCallIdx = !Address.isEqual(sessionManagerAddress, Extensions.Rc3.sessions) + return Hex.fromBytes( + Hash.keccak256( + Bytes.concat( + Bytes.fromNumber(chainId, { size: 32 }), + Bytes.fromNumber(payload.space, { size: 32 }), + Bytes.fromNumber(payload.nonce, { size: 32 }), + ignoreCallIdx ? Bytes.from([]) : Bytes.fromNumber(callIdx, { size: 32 }), + Bytes.fromHex(Payload.hashCall(call)), + ), ), - ), - ) + ) + } + // Current hashing scheme uses entire payload hash and call index (without last parent) + const parentWallets = payload.parentWallets + if (payload.parentWallets && payload.parentWallets.length > 0) { + payload.parentWallets.pop() + } + const payloadHash = Payload.hash(wallet, chainId, payload) + payload.parentWallets = parentWallets + return Hex.fromBytes(Hash.keccak256(Bytes.concat(payloadHash, Bytes.fromNumber(callIdx, { size: 32 })))) } diff --git a/packages/wallet/primitives/test/session-signature.test.ts b/packages/wallet/primitives/test/session-signature.test.ts index b8efe28c2..a1fd0fe23 100644 --- a/packages/wallet/primitives/test/session-signature.test.ts +++ b/packages/wallet/primitives/test/session-signature.test.ts @@ -11,7 +11,7 @@ import { encodeSessionCallSignatureForJson, encodeSessionSignature, ExplicitSessionCallSignature, - hashCallWithReplayProtection, + hashPayloadWithCallIdx, ImplicitSessionCallSignature, isExplicitSessionCallSignature, isImplicitSessionCallSignature, @@ -21,6 +21,7 @@ import { sessionCallSignatureToJson, } from '../src/session-signature.js' import { RSY } from '../src/signature.js' +import { Extensions } from '../src/index.js' describe('Session Signature', () => { // Test data @@ -444,24 +445,25 @@ describe('Session Signature', () => { }) describe('Helper Functions', () => { - describe('hashCallWithReplayProtection', () => { + describe('hashPayloadWithCallIdx', () => { it('should hash call with replay protection parameters', () => { - const result = hashCallWithReplayProtection(samplePayload, 0, testChainId) + const result = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId) expect(result).toMatch(/^0x[0-9a-f]{64}$/) // 32-byte hex string expect(Hex.size(result)).toBe(32) }) it('should produce different hashes for different chain IDs', () => { - const hash1 = hashCallWithReplayProtection(samplePayload, 0, ChainId.MAINNET) - const hash2 = hashCallWithReplayProtection(samplePayload, 0, ChainId.POLYGON) + const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, ChainId.MAINNET) + const hash2 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, ChainId.POLYGON) expect(hash1).not.toBe(hash2) }) it('should produce different hashes for different spaces', () => { - const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId) - const hash2 = hashCallWithReplayProtection( + const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId) + const hash2 = hashPayloadWithCallIdx( + testAddress1, { ...samplePayload, space: samplePayload.space + 1n }, 0, testChainId, @@ -471,8 +473,9 @@ describe('Session Signature', () => { }) it('should produce different hashes for different nonces', () => { - const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId) - const hash2 = hashCallWithReplayProtection( + const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId) + const hash2 = hashPayloadWithCallIdx( + testAddress1, { ...samplePayload, nonce: samplePayload.nonce + 1n }, 0, testChainId, @@ -488,17 +491,51 @@ describe('Session Signature', () => { } const payload2 = { ...samplePayload, calls: [call2] } - const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId) - const hash2 = hashCallWithReplayProtection(payload2, 0, testChainId) + const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId) + const hash2 = hashPayloadWithCallIdx(testAddress1, payload2, 0, testChainId) expect(hash1).not.toBe(hash2) }) + it('should produce different hashes for different wallets', () => { + const payload = { ...samplePayload, calls: [sampleCall, sampleCall] } + + const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId) + const hash2 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId) + + expect(hash1).not.toBe(hash2) + }) + + it('should NOT produce different hashes for different wallets when using deprecated hash encoding for Dev1 and Dev2', () => { + // This is ONLY for backward compatibility with Dev1 and Dev2 + // This is exploitable and should not be used in practice + const payload = { ...samplePayload, calls: [sampleCall, sampleCall] } + + const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions) + const hash2 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId, Extensions.Dev2.sessions) + + expect(hash1).toBe(hash2) + }) + + it('should produce different hashes for different wallets when using deprecated hash encoding for Dev1/2, Rc3 and latest', () => { + // This is ONLY for backward compatibility with Rc3 + // This is exploitable and should not be used in practice + const payload = { ...samplePayload, calls: [sampleCall, sampleCall] } + + const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions) + const hash2 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId, Extensions.Rc3.sessions) + const hash3 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId) + + expect(hash1).not.toBe(hash2) + expect(hash1).not.toBe(hash3) + expect(hash2).not.toBe(hash3) + }) + it('should produce different hashes for same call at different index', () => { const payload = { ...samplePayload, calls: [sampleCall, sampleCall] } - const hash1 = hashCallWithReplayProtection(payload, 0, testChainId) - const hash2 = hashCallWithReplayProtection(payload, 1, testChainId) + const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId) + const hash2 = hashPayloadWithCallIdx(testAddress1, payload, 1, testChainId) expect(hash1).not.toBe(hash2) }) @@ -508,15 +545,15 @@ describe('Session Signature', () => { // This is exploitable and should not be used in practice const payload = { ...samplePayload, calls: [sampleCall, sampleCall] } - const hash1 = hashCallWithReplayProtection(payload, 0, testChainId, true) - const hash2 = hashCallWithReplayProtection(payload, 1, testChainId, true) + const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions) + const hash2 = hashPayloadWithCallIdx(testAddress1, payload, 1, testChainId, Extensions.Dev1.sessions) expect(hash1).toBe(hash2) }) it('should be deterministic', () => { - const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId) - const hash2 = hashCallWithReplayProtection(samplePayload, 0, testChainId) + const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId) + const hash2 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId) expect(hash1).toBe(hash2) }) @@ -526,7 +563,8 @@ describe('Session Signature', () => { const largeSpace = 2n ** 16n const largeNonce = 2n ** 24n - const result = hashCallWithReplayProtection( + const result = hashPayloadWithCallIdx( + testAddress1, { ...samplePayload, space: largeSpace, nonce: largeNonce }, 0, largeChainId, @@ -535,7 +573,7 @@ describe('Session Signature', () => { }) it('should handle zero values', () => { - const result = hashCallWithReplayProtection({ ...samplePayload, space: 0n, nonce: 0n }, 0, 0) + const result = hashPayloadWithCallIdx(testAddress1, { ...samplePayload, space: 0n, nonce: 0n }, 0, 0) expect(result).toMatch(/^0x[0-9a-f]{64}$/) }) @@ -546,7 +584,7 @@ describe('Session Signature', () => { } const payload = { ...samplePayload, calls: [callWithEmptyData] } - const result = hashCallWithReplayProtection(payload, 0, testChainId) + const result = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId) expect(result).toMatch(/^0x[0-9a-f]{64}$/) }) @@ -557,8 +595,8 @@ describe('Session Signature', () => { } const payload = { ...samplePayload, calls: [delegateCall] } - const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId) - const hash2 = hashCallWithReplayProtection(payload, 0, testChainId) + const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId) + const hash2 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId) expect(hash1).not.toBe(hash2) }) @@ -735,12 +773,15 @@ describe('Session Signature', () => { const calls: Payload.Call[] = [ sampleCall, { ...sampleCall, to: testAddress2 }, + { ...sampleCall, to: testAddress2 }, // Repeat call { ...sampleCall, value: 500000000000000000n }, ] const payload = { ...samplePayload, calls: calls } // Generate hashes for each call - const hashes = calls.map((call) => hashCallWithReplayProtection(payload, calls.indexOf(call), testChainId)) + const hashes = calls.map((call) => + hashPayloadWithCallIdx(testAddress1, payload, calls.indexOf(call), testChainId), + ) // All hashes should be valid and different for (let i = 0; i < hashes.length; i++) { diff --git a/packages/wallet/wdk/src/sequence/manager.ts b/packages/wallet/wdk/src/sequence/manager.ts index 1d6d39c01..c402a705a 100644 --- a/packages/wallet/wdk/src/sequence/manager.ts +++ b/packages/wallet/wdk/src/sequence/manager.ts @@ -87,7 +87,7 @@ export type ManagerOptions = { export const ManagerOptionsDefaults = { verbose: false, - extensions: Extensions.Rc3, + extensions: Extensions.Rc4, context: Context.Rc3, context4337: Context.Rc3_4337, guest: Constants.DefaultGuestAddress, diff --git a/packages/wallet/wdk/test/sessions.test.ts b/packages/wallet/wdk/test/sessions.test.ts index f6d8a144b..cec6c1bc9 100644 --- a/packages/wallet/wdk/test/sessions.test.ts +++ b/packages/wallet/wdk/test/sessions.test.ts @@ -1,523 +1,558 @@ import { AbiFunction, Address, Bytes, Hex, Mnemonic, Provider, RpcTransport } from 'ox' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { Signers as CoreSigners, Wallet as CoreWallet, Envelope, Relayer, State } from '../../core/src/index.js' +import { Signers as CoreSigners, Wallet as CoreWallet, Envelope, State } from '../../core/src/index.js' +import { Relayer } from '@0xsequence/relayer' import { Attestation, Constants, Extensions, Network, Payload, Permission } from '../../primitives/src/index.js' import { Sequence } from '../src/index.js' +import { ExplicitSession } from '../../core/src/utils/session/types.js' import { CAN_RUN_LIVE, EMITTER_ABI, EMITTER_ADDRESS, PRIVATE_KEY, RPC_URL } from './constants' -import { ExplicitSession } from '../src/sequence/types/sessions.js' - -describe('Sessions (via Manager)', () => { - // Shared components - let provider: Provider.Provider - let chainId: number - let stateProvider: State.Provider - - // Wallet webapp components - let wdk: { - identitySignerAddress: Address.Address - manager: Sequence.Manager - } - - // Dapp components - let dapp: { - pkStore: CoreSigners.Pk.Encrypted.EncryptedPksDb - wallet: CoreWallet - sessionManager: CoreSigners.SessionManager - } - - const setupExplicitSession = async (explicitSession: ExplicitSession, isModify = false) => { - let requestId: string - if (isModify) { - requestId = await wdk.manager.sessions.modifyExplicitSession(dapp.wallet.address, explicitSession) - } else { - requestId = await wdk.manager.sessions.addExplicitSession(dapp.wallet.address, explicitSession) - } - // Sign and complete the request - const sigRequest = await wdk.manager.signatures.get(requestId) - const identitySigner = sigRequest.signers.find((s) => Address.isEqual(s.address, wdk.identitySignerAddress)) - if (!identitySigner || (identitySigner.status !== 'actionable' && identitySigner.status !== 'ready')) { - throw new Error(`Identity signer not found or not ready/actionable: ${identitySigner?.status}`) - } - const handled = await identitySigner.handle() - if (!handled) { - throw new Error('Failed to handle identity signer') +const ALL_EXTENSIONS = [ + { + name: 'Dev1', + ...Extensions.Dev1, + }, + { + name: 'Dev2', + ...Extensions.Dev2, + }, + { + name: 'Rc3', + ...Extensions.Rc3, + }, + { + name: 'Rc4', + ...Extensions.Rc4, + }, +] + +for (const extension of ALL_EXTENSIONS) { + describe(`Sessions (via Manager ${extension.name})`, () => { + // Shared components + let provider: Provider.Provider + let chainId: number + let stateProvider: State.Provider + + // Wallet webapp components + let wdk: { + identitySignerAddress: Address.Address + manager: Sequence.Manager } - await wdk.manager.sessions.complete(requestId) - } - - beforeEach(async () => { - // Create provider or use arbitrum sepolia - if (RPC_URL) { - provider = Provider.from( - RpcTransport.fromHttp(RPC_URL, { - fetchOptions: { - headers: { - 'x-requested-with': 'XMLHttpRequest', - }, - }, - }), - ) - chainId = Number(await provider.request({ method: 'eth_chainId' })) - } else { - provider = vi.mocked({ - request: vi.fn(), - on: vi.fn(), - removeListener: vi.fn(), - }) - chainId = Network.ChainId.MAINNET - } - - // Create state provider - stateProvider = new State.Local.Provider() - - // Create manager - const opts = Sequence.applyManagerOptionsDefaults({ - stateProvider, - relayers: [], // No relayers needed for testing - networks: [ - { - chainId, - type: Network.NetworkType.MAINNET, - rpcUrl: RPC_URL ?? 'XXX', - name: 'XXX', - blockExplorer: { url: 'XXX' }, - nativeCurrency: { - name: 'Ether', - symbol: 'ETH', - decimals: 18, - }, - }, - ], - }) - // Create manager - const manager = new Sequence.Manager(opts) - - // Use a mnemonic to create the wallet - const identitySignerMnemonic = Mnemonic.random(Mnemonic.english) - const identitySignerPk = Mnemonic.toPrivateKey(identitySignerMnemonic, { as: 'Hex' }) - const identitySignerAddress = new CoreSigners.Pk.Pk(identitySignerPk).address - const walletAddress = await manager.wallets.signUp({ - kind: 'mnemonic', - mnemonic: identitySignerMnemonic, - noGuard: true, - noSessionManager: false, - }) - if (!walletAddress) { - throw new Error('Failed to create wallet') + // Dapp components + let dapp: { + pkStore: CoreSigners.Pk.Encrypted.EncryptedPksDb + wallet: CoreWallet + sessionManager: CoreSigners.SessionManager } - // Initialize the wdk components - wdk = { - identitySignerAddress, - manager, - } - manager.registerMnemonicUI(async (respond) => { - await respond(identitySignerMnemonic) - }) + const setupExplicitSession = async (explicitSession: ExplicitSession, isModify = false) => { + let requestId: string + if (isModify) { + requestId = await wdk.manager.sessions.modifyExplicitSession(dapp.wallet.address, explicitSession) + } else { + requestId = await wdk.manager.sessions.addExplicitSession(dapp.wallet.address, explicitSession) + } - // Create the pk store and pk - const pkStore = new CoreSigners.Pk.Encrypted.EncryptedPksDb() + // Sign and complete the request + const sigRequest = await wdk.manager.signatures.get(requestId) + const identitySigner = sigRequest.signers.find((s) => Address.isEqual(s.address, wdk.identitySignerAddress)) + if (!identitySigner || (identitySigner.status !== 'actionable' && identitySigner.status !== 'ready')) { + throw new Error(`Identity signer not found or not ready/actionable: ${identitySigner?.status}`) + } + const handled = await identitySigner.handle() + if (!handled) { + throw new Error('Failed to handle identity signer') + } + await wdk.manager.sessions.complete(requestId) + } - // Create wallet in core - const coreWallet = new CoreWallet(walletAddress, { - guest: opts.guest, - // Share the state provider with wdk. In practice this will be the key machine. - stateProvider, - }) + beforeEach(async () => { + // Create provider or use arbitrum sepolia + if (RPC_URL) { + provider = Provider.from( + RpcTransport.fromHttp(RPC_URL, { + fetchOptions: { + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + }, + }), + ) + chainId = Number(await provider.request({ method: 'eth_chainId' })) + } else { + provider = vi.mocked({ + request: vi.fn(), + on: vi.fn(), + removeListener: vi.fn(), + }) + chainId = Network.ChainId.MAINNET + } - dapp = { - pkStore, - wallet: coreWallet, - sessionManager: new CoreSigners.SessionManager(coreWallet, { - provider, - sessionManagerAddress: Extensions.Rc3.sessions, - }), - } - }) + // Create state provider + stateProvider = new State.Local.Provider() - const signAndSend = async (call: Payload.Call) => { - const envelope = await dapp.wallet.prepareTransaction(provider, [call], { noConfigUpdate: true }) - const parentedEnvelope: Payload.Parented = { - ...envelope.payload, - parentWallets: [dapp.wallet.address], - } + // Create manager + const opts = Sequence.applyManagerOptionsDefaults({ + stateProvider, + relayers: [], // No relayers needed for testing + networks: [ + { + chainId, + type: Network.NetworkType.MAINNET, + rpcUrl: RPC_URL ?? 'XXX', + name: 'XXX', + blockExplorer: { url: 'XXX' }, + nativeCurrency: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + }, + }, + ], + extensions: extension, + }) - // Sign the envelope - const sessionImageHash = await dapp.sessionManager.imageHash - if (!sessionImageHash) { - throw new Error('Session image hash not found') - } - const signature = await dapp.sessionManager.signSapient( - dapp.wallet.address, - chainId ?? 1n, - parentedEnvelope, - sessionImageHash, - ) - const sapientSignature: Envelope.SapientSignature = { - imageHash: sessionImageHash, - signature, - } - const signedEnvelope = Envelope.toSigned(envelope, [sapientSignature]) - - // Build the transaction - const transaction = await dapp.wallet.buildTransaction(provider, signedEnvelope) - console.log('tx', transaction) - - // Send the transaction - if (CAN_RUN_LIVE && PRIVATE_KEY) { - // Load the sender - const senderPk = Hex.from(PRIVATE_KEY as `0x${string}`) - const pkRelayer = new Relayer.Standard.PkRelayer(senderPk, provider) - const tx = await pkRelayer.relay(transaction.to, transaction.data, chainId, undefined) - console.log('Transaction sent', tx) - await new Promise((resolve) => setTimeout(resolve, 3000)) - const receipt = await provider.request({ method: 'eth_getTransactionReceipt', params: [tx.opHash] }) - console.log('Transaction receipt', receipt) - return tx.opHash - } - } + // Create manager + const manager = new Sequence.Manager(opts) - it( - 'should add the session manager leaf when not present', - async () => { - // Recreate the wallet specifically for this test + // Use a mnemonic to create the wallet const identitySignerMnemonic = Mnemonic.random(Mnemonic.english) const identitySignerPk = Mnemonic.toPrivateKey(identitySignerMnemonic, { as: 'Hex' }) const identitySignerAddress = new CoreSigners.Pk.Pk(identitySignerPk).address - const walletAddress = await wdk.manager.wallets.signUp({ + const walletAddress = await manager.wallets.signUp({ kind: 'mnemonic', mnemonic: identitySignerMnemonic, noGuard: true, - noSessionManager: true, + noSessionManager: false, }) if (!walletAddress) { throw new Error('Failed to create wallet') } // Initialize the wdk components - wdk.identitySignerAddress = identitySignerAddress - wdk.manager.registerMnemonicUI(async (respond) => { + wdk = { + identitySignerAddress, + manager, + } + manager.registerMnemonicUI(async (respond) => { await respond(identitySignerMnemonic) }) + // Create the pk store and pk + const pkStore = new CoreSigners.Pk.Encrypted.EncryptedPksDb() + // Create wallet in core const coreWallet = new CoreWallet(walletAddress, { + guest: opts.guest, + // Share the state provider with wdk. In practice this will be the key machine. stateProvider, }) - dapp.wallet = coreWallet - dapp.sessionManager = new CoreSigners.SessionManager(coreWallet, { - provider, - sessionManagerAddress: Extensions.Rc3.sessions, - }) - - // At this point the wallet should NOT have a session topology - await expect(wdk.manager.sessions.getTopology(walletAddress)).rejects.toThrow('Session manager not found') - - // Create the explicit session signer - const e = await dapp.pkStore.generateAndStore() - const s = await dapp.pkStore.getEncryptedPkStore(e.address) - if (!s) { - throw new Error('Failed to create pk store') - } - const explicitSession: ExplicitSession = { - sessionAddress: e.address, - chainId, - valueLimit: 0n, - deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now - permissions: [ - { - target: EMITTER_ADDRESS, - rules: [], - }, - ], - } - const explicitSigner = new CoreSigners.Session.Explicit(s, explicitSession) - // Add to manager - dapp.sessionManager = dapp.sessionManager.withExplicitSigner(explicitSigner) - - await setupExplicitSession(explicitSession) - - // Create a call payload - const call: Payload.Call = { - to: EMITTER_ADDRESS, - value: 0n, - data: AbiFunction.encodeData(EMITTER_ABI[0]), - gasLimit: 0n, - delegateCall: false, - onlyFallback: false, - behaviorOnError: 'revert', + dapp = { + pkStore, + wallet: coreWallet, + sessionManager: new CoreSigners.SessionManager(coreWallet, { + provider, + sessionManagerAddress: extension.sessions, + }), } + }) - if (!RPC_URL) { - // Configure mock provider - ;(provider as any).request.mockImplementation(({ method, params }) => { - if (method === 'eth_chainId') { - return Promise.resolve(chainId.toString()) - } - if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { - // Undeployed wallet - return Promise.resolve('0x') - } - if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) { - // Nonce is 0 - return Promise.resolve('0x00') - } - if (method === 'eth_call' && params[0].data?.startsWith(AbiFunction.getSelector(Constants.GET_LIMIT_USAGE))) { - // Return 0 for usage limit (no usage yet) - return Promise.resolve('0x0000000000000000000000000000000000000000000000000000000000000000') - } - }) + const signAndSend = async (call: Payload.Call) => { + const envelope = await dapp.wallet.prepareTransaction(provider, [call], { noConfigUpdate: true }) + const parentedEnvelope: Payload.Parented = { + ...envelope.payload, + parentWallets: [dapp.wallet.address], } - // Sign and send the transaction - await signAndSend(call) - }, - PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined, - ) - - it( - 'should create and sign with an explicit session', - async () => { - // Create the explicit session signer - const e = await dapp.pkStore.generateAndStore() - const s = await dapp.pkStore.getEncryptedPkStore(e.address) - if (!s) { - throw new Error('Failed to create pk store') + // Sign the envelope + const sessionImageHash = await dapp.sessionManager.imageHash + if (!sessionImageHash) { + throw new Error('Session image hash not found') } - const explicitSession: ExplicitSession = { - sessionAddress: e.address, - chainId, - valueLimit: 0n, - deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now - permissions: [ - { - target: EMITTER_ADDRESS, - rules: [ - { - // Require the explicitEmit selector - cumulative: false, - operation: Permission.ParameterOperation.EQUAL, - value: Bytes.fromHex(AbiFunction.getSelector(EMITTER_ABI[0]), { size: 32 }), - offset: 0n, - mask: Bytes.fromHex('0xffffffff', { size: 32 }), - }, - ], - }, - ], + const signature = await dapp.sessionManager.signSapient( + dapp.wallet.address, + chainId ?? 1n, + parentedEnvelope, + sessionImageHash, + ) + const sapientSignature: Envelope.SapientSignature = { + imageHash: sessionImageHash, + signature, } - const explicitSigner = new CoreSigners.Session.Explicit(s, explicitSession) - // Add to manager - dapp.sessionManager = dapp.sessionManager.withExplicitSigner(explicitSigner) - - await setupExplicitSession(explicitSession) - - // Create a call payload - const call: Payload.Call = { - to: EMITTER_ADDRESS, - value: 0n, - data: AbiFunction.encodeData(EMITTER_ABI[0]), - gasLimit: 0n, - delegateCall: false, - onlyFallback: false, - behaviorOnError: 'revert', + const signedEnvelope = Envelope.toSigned(envelope, [sapientSignature]) + + // Build the transaction + const transaction = await dapp.wallet.buildTransaction(provider, signedEnvelope) + console.log('tx', transaction) + + // Send the transaction + if (CAN_RUN_LIVE && PRIVATE_KEY) { + // Load the sender + const senderPk = Hex.from(PRIVATE_KEY as `0x${string}`) + const pkRelayer = new Relayer.PkRelayer(senderPk, provider) + const tx = await pkRelayer.relay(transaction.to, transaction.data, chainId, undefined) + console.log('Transaction sent', tx) + await new Promise((resolve) => setTimeout(resolve, 3000)) + const receipt = await provider.request({ method: 'eth_getTransactionReceipt', params: [tx.opHash] }) + console.log('Transaction receipt', receipt) + return tx.opHash } + } - if (!RPC_URL) { - // Configure mock provider - ;(provider as any).request.mockImplementation(({ method, params }) => { - if (method === 'eth_chainId') { - return Promise.resolve(chainId.toString()) - } - if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { - // Undeployed wallet - return Promise.resolve('0x') - } - if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) { - // Nonce is 0 - return Promise.resolve('0x00') - } - if (method === 'eth_call' && params[0].data?.startsWith(AbiFunction.getSelector(Constants.GET_LIMIT_USAGE))) { - // Return 0 for usage limit (no usage yet) - return Promise.resolve('0x0000000000000000000000000000000000000000000000000000000000000000') - } + it( + 'should add the session manager leaf when not present', + async () => { + // Recreate the wallet specifically for this test + const identitySignerMnemonic = Mnemonic.random(Mnemonic.english) + const identitySignerPk = Mnemonic.toPrivateKey(identitySignerMnemonic, { as: 'Hex' }) + const identitySignerAddress = new CoreSigners.Pk.Pk(identitySignerPk).address + const walletAddress = await wdk.manager.wallets.signUp({ + kind: 'mnemonic', + mnemonic: identitySignerMnemonic, + noGuard: true, + noSessionManager: true, }) - } - - // Sign and send the transaction - await signAndSend(call) - }, - PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined, - ) - - it( - 'should modify an explicit session permission', - async () => { - // First we create the explicit sessions signer - const e = await dapp.pkStore.generateAndStore() - const s = await dapp.pkStore.getEncryptedPkStore(e.address) - if (!s) { - throw new Error('Failed to create pk store') - } - // Create the initial permissions - let explicitSession: ExplicitSession = { - sessionAddress: e.address, - chainId, - valueLimit: 0n, - deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now - permissions: [ - { - target: EMITTER_ADDRESS, - rules: [ - { - // Require the explicitEmit selector - cumulative: false, - operation: Permission.ParameterOperation.EQUAL, - value: Bytes.fromHex(AbiFunction.getSelector(EMITTER_ABI[0]), { size: 32 }), - offset: 0n, - mask: Bytes.fromHex('0xffffffff', { size: 32 }), - }, - ], - }, - ], - } - const explicitSigner = new CoreSigners.Session.Explicit(s, explicitSession) - // Add to manager - dapp.sessionManager = dapp.sessionManager.withExplicitSigner(explicitSigner) - - await setupExplicitSession(explicitSession) - - // Create a call payload - const call: Payload.Call = { - to: EMITTER_ADDRESS, - value: 0n, - data: AbiFunction.encodeData(EMITTER_ABI[0]), - gasLimit: 0n, - delegateCall: false, - onlyFallback: false, - behaviorOnError: 'revert', - } - - if (!RPC_URL) { - // Configure mock provider - ;(provider as any).request.mockImplementation(({ method, params }) => { - if (method === 'eth_chainId') { - return Promise.resolve(chainId.toString()) - } - if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { - // Undeployed wallet - return Promise.resolve('0x') - } - if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) { - // Nonce is 0 - return Promise.resolve('0x00') - } - if (method === 'eth_call' && params[0].data?.startsWith(AbiFunction.getSelector(Constants.GET_LIMIT_USAGE))) { - // Return 0 for usage limit (no usage yet) - return Promise.resolve('0x0000000000000000000000000000000000000000000000000000000000000000') - } + if (!walletAddress) { + throw new Error('Failed to create wallet') + } + + // Initialize the wdk components + wdk.identitySignerAddress = identitySignerAddress + wdk.manager.registerMnemonicUI(async (respond) => { + await respond(identitySignerMnemonic) }) - } - // Sign and send the transaction - await signAndSend(call) - - // Now we modify the permissions target contract to zero address - // This should cause any session call to the EMITTER_ADDRESS contract to fail - explicitSession.permissions[0].target = '0x0000000000000000000000000000000000000000' - - await setupExplicitSession(explicitSession, true) - - // Sign and send the transaction - // Should fail with 'No signer supported for call' - await expect(signAndSend(call)).rejects.toThrow('No signer supported for call') - }, - PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined, - ) - - it( - 'should create and sign with an implicit session', - async () => { - // Create the implicit session signer - const e = await dapp.pkStore.generateAndStore() - const s = await dapp.pkStore.getEncryptedPkStore(e.address) - if (!s) { - throw new Error('Failed to create pk store') - } + // Create wallet in core + const coreWallet = new CoreWallet(walletAddress, { + stateProvider, + }) - // Request the session authorization from the WDK - const requestId = await wdk.manager.sessions.prepareAuthorizeImplicitSession(dapp.wallet.address, e.address, { - target: 'https://example.com', - }) + dapp.wallet = coreWallet + dapp.sessionManager = new CoreSigners.SessionManager(coreWallet, { + provider, + sessionManagerAddress: extension.sessions, + }) - // Sign the request (Wallet UI action) - const sigRequest = await wdk.manager.signatures.get(requestId) - const identitySigner = sigRequest.signers[0] - if (!identitySigner || (identitySigner.status !== 'actionable' && identitySigner.status !== 'ready')) { - throw new Error(`Identity signer not found or not ready/actionable: ${identitySigner?.status}`) - } - const handled = await identitySigner.handle() - if (!handled) { - throw new Error('Failed to handle identity signer') - } + // At this point the wallet should NOT have a session topology + await expect(wdk.manager.sessions.getTopology(walletAddress)).rejects.toThrow('Session manager not found') + + // Create the explicit session signer + const e = await dapp.pkStore.generateAndStore() + const s = await dapp.pkStore.getEncryptedPkStore(e.address) + if (!s) { + throw new Error('Failed to create pk store') + } + const explicitSession: ExplicitSession = { + type: 'explicit', + sessionAddress: e.address, + chainId, + valueLimit: 0n, + deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now + permissions: [ + { + target: EMITTER_ADDRESS, + rules: [], + }, + ], + } + const explicitSigner = new CoreSigners.Session.Explicit(s, explicitSession) + // Add to manager + dapp.sessionManager = dapp.sessionManager.withExplicitSigner(explicitSigner) + + await setupExplicitSession(explicitSession) + + // Create a call payload + const call: Payload.Call = { + to: EMITTER_ADDRESS, + value: 0n, + data: AbiFunction.encodeData(EMITTER_ABI[0]), + gasLimit: 0n, + delegateCall: false, + onlyFallback: false, + behaviorOnError: 'revert', + } + + if (!RPC_URL) { + // Configure mock provider + ;(provider as any).request.mockImplementation(({ method, params }) => { + if (method === 'eth_chainId') { + return Promise.resolve(chainId.toString()) + } + if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { + // Undeployed wallet + return Promise.resolve('0x') + } + if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) { + // Nonce is 0 + return Promise.resolve('0x00') + } + if ( + method === 'eth_call' && + params[0].data?.startsWith(AbiFunction.getSelector(Constants.GET_LIMIT_USAGE)) + ) { + // Return 0 for usage limit (no usage yet) + return Promise.resolve('0x0000000000000000000000000000000000000000000000000000000000000000') + } + }) + } + + // Sign and send the transaction + await signAndSend(call) + }, + PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined, + ) - // Complete the request - const { attestation, signature: identitySignature } = - await wdk.manager.sessions.completeAuthorizeImplicitSession(requestId) + it( + 'should create and sign with an explicit session', + async () => { + // Create the explicit session signer + const e = await dapp.pkStore.generateAndStore() + const s = await dapp.pkStore.getEncryptedPkStore(e.address) + if (!s) { + throw new Error('Failed to create pk store') + } + const explicitSession: ExplicitSession = { + type: 'explicit', + sessionAddress: e.address, + chainId, + valueLimit: 0n, + deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now + permissions: [ + { + target: EMITTER_ADDRESS, + rules: [ + { + // Require the explicitEmit selector + cumulative: false, + operation: Permission.ParameterOperation.EQUAL, + value: Bytes.fromHex(AbiFunction.getSelector(EMITTER_ABI[0]), { size: 32 }), + offset: 0n, + mask: Bytes.fromHex('0xffffffff', { size: 32 }), + }, + ], + }, + ], + } + const explicitSigner = new CoreSigners.Session.Explicit(s, explicitSession) + // Add to manager + dapp.sessionManager = dapp.sessionManager.withExplicitSigner(explicitSigner) + + await setupExplicitSession(explicitSession) + + // Create a call payload + const call: Payload.Call = { + to: EMITTER_ADDRESS, + value: 0n, + data: AbiFunction.encodeData(EMITTER_ABI[0]), + gasLimit: 0n, + delegateCall: false, + onlyFallback: false, + behaviorOnError: 'revert', + } + + if (!RPC_URL) { + // Configure mock provider + ;(provider as any).request.mockImplementation(({ method, params }) => { + if (method === 'eth_chainId') { + return Promise.resolve(chainId.toString()) + } + if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { + // Undeployed wallet + return Promise.resolve('0x') + } + if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) { + // Nonce is 0 + return Promise.resolve('0x00') + } + if ( + method === 'eth_call' && + params[0].data?.startsWith(AbiFunction.getSelector(Constants.GET_LIMIT_USAGE)) + ) { + // Return 0 for usage limit (no usage yet) + return Promise.resolve('0x0000000000000000000000000000000000000000000000000000000000000000') + } + }) + } + + // Sign and send the transaction + await signAndSend(call) + }, + PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined, + ) - // Load the implicit signer - const implicitSigner = new CoreSigners.Session.Implicit( - s, - attestation, - identitySignature, - dapp.sessionManager.address, - ) - dapp.sessionManager = dapp.sessionManager.withImplicitSigner(implicitSigner) - - // Create a call payload - const call: Payload.Call = { - to: EMITTER_ADDRESS, - value: 0n, - data: AbiFunction.encodeData(EMITTER_ABI[1]), // implicitEmit - gasLimit: 0n, - delegateCall: false, - onlyFallback: false, - behaviorOnError: 'revert', - } + it( + 'should modify an explicit session permission', + async () => { + // First we create the explicit sessions signer + const e = await dapp.pkStore.generateAndStore() + const s = await dapp.pkStore.getEncryptedPkStore(e.address) + if (!s) { + throw new Error('Failed to create pk store') + } + // Create the initial permissions + let explicitSession: ExplicitSession = { + type: 'explicit', + sessionAddress: e.address, + chainId, + valueLimit: 0n, + deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now + permissions: [ + { + target: EMITTER_ADDRESS, + rules: [ + { + // Require the explicitEmit selector + cumulative: false, + operation: Permission.ParameterOperation.EQUAL, + value: Bytes.fromHex(AbiFunction.getSelector(EMITTER_ABI[0]), { size: 32 }), + offset: 0n, + mask: Bytes.fromHex('0xffffffff', { size: 32 }), + }, + ], + }, + ], + } + const explicitSigner = new CoreSigners.Session.Explicit(s, explicitSession) + // Add to manager + dapp.sessionManager = dapp.sessionManager.withExplicitSigner(explicitSigner) + + await setupExplicitSession(explicitSession) + + // Create a call payload + const call: Payload.Call = { + to: EMITTER_ADDRESS, + value: 0n, + data: AbiFunction.encodeData(EMITTER_ABI[0]), + gasLimit: 0n, + delegateCall: false, + onlyFallback: false, + behaviorOnError: 'revert', + } + + if (!RPC_URL) { + // Configure mock provider + ;(provider as any).request.mockImplementation(({ method, params }) => { + if (method === 'eth_chainId') { + return Promise.resolve(chainId.toString()) + } + if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { + // Undeployed wallet + return Promise.resolve('0x') + } + if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) { + // Nonce is 0 + return Promise.resolve('0x00') + } + if ( + method === 'eth_call' && + params[0].data?.startsWith(AbiFunction.getSelector(Constants.GET_LIMIT_USAGE)) + ) { + // Return 0 for usage limit (no usage yet) + return Promise.resolve('0x0000000000000000000000000000000000000000000000000000000000000000') + } + }) + } + + // Sign and send the transaction + await signAndSend(call) + + // Now we modify the permissions target contract to zero address + // This should cause any session call to the EMITTER_ADDRESS contract to fail + explicitSession.permissions[0].target = '0x0000000000000000000000000000000000000000' + + await setupExplicitSession(explicitSession, true) + + // Sign and send the transaction + // Should fail with 'No signer supported for call' + await expect(signAndSend(call)).rejects.toThrow('No signer supported for call') + }, + PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined, + ) - if (!RPC_URL) { - // Configure mock provider - ;(provider as any).request.mockImplementation(({ method, params }) => { - if (method === 'eth_chainId') { - return Promise.resolve(chainId.toString()) - } - if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { - // Undeployed wallet - return Promise.resolve('0x') - } - if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) { - // Nonce is 0 - return Promise.resolve('0x00') - } - if ( - method === 'eth_call' && - Address.isEqual(params[0].from, dapp.sessionManager.address) && - Address.isEqual(params[0].to, call.to) - ) { - // Implicit request simulation result - const expectedResult = Bytes.toHex( - Attestation.generateImplicitRequestMagic(attestation, dapp.wallet.address), - ) - return Promise.resolve(expectedResult) - } + it( + 'should create and sign with an implicit session', + async () => { + // Create the implicit session signer + const e = await dapp.pkStore.generateAndStore() + const s = await dapp.pkStore.getEncryptedPkStore(e.address) + if (!s) { + throw new Error('Failed to create pk store') + } + + // Request the session authorization from the WDK + const requestId = await wdk.manager.sessions.prepareAuthorizeImplicitSession(dapp.wallet.address, e.address, { + target: 'https://example.com', }) - } - // Sign and send the transaction - await signAndSend(call) - }, - PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined, - ) -}) + // Sign the request (Wallet UI action) + const sigRequest = await wdk.manager.signatures.get(requestId) + const identitySigner = sigRequest.signers[0] + if (!identitySigner || (identitySigner.status !== 'actionable' && identitySigner.status !== 'ready')) { + throw new Error(`Identity signer not found or not ready/actionable: ${identitySigner?.status}`) + } + const handled = await identitySigner.handle() + if (!handled) { + throw new Error('Failed to handle identity signer') + } + + // Complete the request + const { attestation, signature: identitySignature } = + await wdk.manager.sessions.completeAuthorizeImplicitSession(requestId) + + // Load the implicit signer + const implicitSigner = new CoreSigners.Session.Implicit( + s, + attestation, + identitySignature, + dapp.sessionManager.address, + ) + dapp.sessionManager = dapp.sessionManager.withImplicitSigner(implicitSigner) + + // Create a call payload + const call: Payload.Call = { + to: EMITTER_ADDRESS, + value: 0n, + data: AbiFunction.encodeData(EMITTER_ABI[1]), // implicitEmit + gasLimit: 0n, + delegateCall: false, + onlyFallback: false, + behaviorOnError: 'revert', + } + + if (!RPC_URL) { + // Configure mock provider + ;(provider as any).request.mockImplementation(({ method, params }) => { + if (method === 'eth_chainId') { + return Promise.resolve(chainId.toString()) + } + if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { + // Undeployed wallet + return Promise.resolve('0x') + } + if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) { + // Nonce is 0 + return Promise.resolve('0x00') + } + if ( + method === 'eth_call' && + Address.isEqual(params[0].from, dapp.sessionManager.address) && + Address.isEqual(params[0].to, call.to) + ) { + // Implicit request simulation result + const expectedResult = Bytes.toHex( + Attestation.generateImplicitRequestMagic(attestation, dapp.wallet.address), + ) + return Promise.resolve(expectedResult) + } + }) + } + + // Sign and send the transaction + await signAndSend(call) + }, + PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined, + ) + }) +}