diff --git a/packages/wallet/core/src/signers/index.ts b/packages/wallet/core/src/signers/index.ts index 80ccc07f1..2eb143703 100644 --- a/packages/wallet/core/src/signers/index.ts +++ b/packages/wallet/core/src/signers/index.ts @@ -20,7 +20,7 @@ export interface Signer { export interface SapientSigner { readonly address: MaybePromise - readonly imageHash: MaybePromise + readonly imageHash: MaybePromise signSapient: ( wallet: Address.Address, diff --git a/packages/wallet/core/src/signers/session-manager.ts b/packages/wallet/core/src/signers/session-manager.ts index 99d189131..e4cba16ec 100644 --- a/packages/wallet/core/src/signers/session-manager.ts +++ b/packages/wallet/core/src/signers/session-manager.ts @@ -50,15 +50,15 @@ export class SessionManager implements SapientSigner { this._provider = options.provider } - get imageHash(): Promise { + get imageHash(): Promise { return this.getImageHash() } - async getImageHash(): Promise { + async getImageHash(): Promise { const { configuration } = await this.wallet.getStatus() const sessionConfigLeaf = Config.findSignerLeaf(configuration, this.address) if (!sessionConfigLeaf || !Config.isSapientSignerLeaf(sessionConfigLeaf)) { - return undefined + throw new Error(`Session configuration not found for wallet ${this.wallet.address}`) } return sessionConfigLeaf.imageHash } @@ -72,6 +72,10 @@ export class SessionManager implements SapientSigner { if (!imageHash) { throw new Error(`Session configuration not found for image hash ${imageHash}`) } + return this._getTopologyForImageHash(imageHash) + } + + private async _getTopologyForImageHash(imageHash: Hex.Hex): Promise { const tree = await this.stateProvider.getTree(imageHash) if (!tree) { throw new Error(`Session configuration not found for image hash ${imageHash}`) @@ -131,8 +135,21 @@ export class SessionManager implements SapientSigner { } async findSignersForCalls(wallet: Address.Address, chainId: number, calls: Payload.Call[]): Promise { + if (!Address.isEqual(this.wallet.address, wallet)) { + throw new Error('Wallet address mismatch') + } // Only use signers that match the topology const topology = await this.topology + return this._findSignersForCalls(wallet, chainId, calls, topology) + } + + private async _findSignersForCalls( + wallet: Address.Address, + chainId: number, + calls: Payload.Call[], + topology: SessionConfig.SessionsTopology, + ): Promise { + // Only use signers that match the topology const identitySigners = SessionConfig.getIdentitySigners(topology) if (identitySigners.length === 0) { throw new Error('Identity signers not found') @@ -173,11 +190,24 @@ export class SessionManager implements SapientSigner { wallet: Address.Address, chainId: number, calls: Payload.Call[], + ): Promise { + if (!Address.isEqual(wallet, this.wallet.address)) { + throw new Error('Wallet address mismatch') + } + const topology = await this.topology + return this._prepareIncrement(wallet, chainId, calls, topology) + } + + private async _prepareIncrement( + wallet: Address.Address, + chainId: number, + calls: Payload.Call[], + topology: SessionConfig.SessionsTopology, ): Promise { if (calls.length === 0) { throw new Error('No calls provided') } - const signers = await this.findSignersForCalls(wallet, chainId, calls) + const signers = await this._findSignersForCalls(wallet, chainId, calls, topology) // Create a map of signers to their associated calls const signerToCalls = new Map() @@ -233,18 +263,18 @@ export class SessionManager implements SapientSigner { if (!Address.isEqual(wallet, this.wallet.address)) { throw new Error('Wallet address mismatch') } + if (this._provider) { + const providerChainId = await this._provider.request({ + method: 'eth_chainId', + }) + if (providerChainId !== Hex.fromNumber(chainId)) { + throw new Error(`Provider chain id mismatch, expected ${Hex.fromNumber(chainId)} but got ${providerChainId}`) + } + } if ((await this.imageHash) !== imageHash) { throw new Error('Unexpected image hash') } - //FIXME Test chain id - // if (this._provider) { - // const providerChainId = await this._provider.request({ - // method: 'eth_chainId', - // }) - // if (providerChainId !== Hex.fromNumber(chainId)) { - // throw new Error(`Provider chain id mismatch, expected ${Hex.fromNumber(chainId)} but got ${providerChainId}`) - // } - // } + const topology = await this._getTopologyForImageHash(imageHash) if (!Payload.isCalls(payload) || payload.calls.length === 0) { throw new Error('Only calls are supported') } @@ -254,7 +284,7 @@ export class SessionManager implements SapientSigner { throw new Error(`Space ${payload.space} is too large`) } - const signers = await this.findSignersForCalls(wallet, chainId, payload.calls) + const signers = await this._findSignersForCalls(wallet, chainId, payload.calls, topology) if (signers.length !== payload.calls.length) { throw new Error('No signer supported for call') } @@ -270,7 +300,7 @@ export class SessionManager implements SapientSigner { ) // Check if the last call is an increment usage call - const expectedIncrement = await this.prepareIncrement(wallet, chainId, payload.calls) + const expectedIncrement = await this._prepareIncrement(wallet, chainId, payload.calls, topology) if (expectedIncrement) { let actualIncrement: Payload.Call if ( @@ -327,7 +357,7 @@ export class SessionManager implements SapientSigner { // Perform encoding const encodedSignature = SessionSignature.encodeSessionCallSignatures( signatures, - await this.topology, + topology, identitySigner, explicitSigners, implicitSigners, @@ -346,23 +376,23 @@ export class SessionManager implements SapientSigner { payload: Payload.Parented, signature: SignatureTypes.SignatureOfSapientSignerLeaf, ): Promise { - if (!Payload.isCalls(payload)) { + if (!Address.isEqual(wallet, this.wallet.address)) { + throw new Error('Wallet address mismatch') + } + if (!Payload.isCalls(payload) || payload.calls.length === 0) { // Only calls are supported return false } - if (!this._provider) { throw new Error('Provider not set') } - //FIXME Test chain id - // const providerChainId = await this._provider.request({ - // method: 'eth_chainId', - // }) - // if (providerChainId !== Hex.fromNumber(chainId)) { - // throw new Error( - // `Provider chain id mismatch, expected ${Hex.fromNumber(chainId)} but got ${providerChainId}`, - // ) - // } + // Test chain id + const providerChainId = await this._provider.request({ + method: 'eth_chainId', + }) + if (providerChainId !== Hex.fromNumber(chainId)) { + throw new Error(`Provider chain id mismatch, expected ${Hex.fromNumber(chainId)} but got ${providerChainId}`) + } const encodedPayload = Payload.encodeSapient(chainId, payload) const encodedCallData = AbiFunction.encodeData(Constants.RECOVER_SAPIENT_SIGNATURE, [ diff --git a/packages/wallet/core/src/wallet.ts b/packages/wallet/core/src/wallet.ts index db4733e2b..fe5198d58 100644 --- a/packages/wallet/core/src/wallet.ts +++ b/packages/wallet/core/src/wallet.ts @@ -371,7 +371,6 @@ export class Wallet { // If the latest configuration does not match the onchain configuration // then we bundle the update into the transaction envelope if (!options?.noConfigUpdate) { - const status = await this.getStatus(provider) if (status.imageHash !== status.onChainImageHash) { calls.push({ to: this.address, @@ -402,7 +401,7 @@ export class Wallet { factory, factoryData, }, - ...(await this.prepareBlankEnvelope(Number(chainId))), + ...(await this.prepareBlankEnvelope(Number(chainId), status.configuration)), } } @@ -461,15 +460,15 @@ export class Wallet { } } - const [chainId, nonce] = await Promise.all([ + const [chainId, nonce, status] = await Promise.all([ provider.request({ method: 'eth_chainId' }), this.getNonce(provider, space), + this.getStatus(provider), ]) // If the latest configuration does not match the onchain configuration // then we bundle the update into the transaction envelope if (!options?.noConfigUpdate) { - const status = await this.getStatus(provider) if (status.imageHash !== status.onChainImageHash) { calls.push({ to: this.address, @@ -490,7 +489,7 @@ export class Wallet { nonce, calls, }, - ...(await this.prepareBlankEnvelope(Number(chainId))), + ...(await this.prepareBlankEnvelope(Number(chainId), status.configuration)), } } @@ -597,13 +596,15 @@ export class Wallet { return encoded } - private async prepareBlankEnvelope(chainId: number) { - const status = await this.getStatus() - + private async prepareBlankEnvelope(chainId: number, configuration?: Config.Config) { + if (!configuration) { + const status = await this.getStatus() + configuration = status.configuration + } return { wallet: this.address, - chainId: chainId, - configuration: status.configuration, + chainId, + configuration, } } } diff --git a/packages/wallet/core/test/session-manager.test.ts b/packages/wallet/core/test/session-manager.test.ts index aa154df91..cbbb26fba 100644 --- a/packages/wallet/core/test/session-manager.test.ts +++ b/packages/wallet/core/test/session-manager.test.ts @@ -503,7 +503,7 @@ for (const extension of ALL_EXTENSIONS) { 'should fail to sign with an expired explicit session', async () => { const provider = Provider.from(RpcTransport.fromHttp(LOCAL_RPC_URL)) - const chainId = 0 + const chainId = Number(await provider.request({ method: 'eth_chainId' })) // Create unique identity and state provider for this test const identityPrivateKey = Secp256k1.randomPrivateKey() @@ -561,7 +561,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( 'No signers match the topology', ) }, diff --git a/packages/wallet/dapp-client/src/ChainSessionManager.ts b/packages/wallet/dapp-client/src/ChainSessionManager.ts index 14fbdc329..cfa89469c 100644 --- a/packages/wallet/dapp-client/src/ChainSessionManager.ts +++ b/packages/wallet/dapp-client/src/ChainSessionManager.ts @@ -673,27 +673,22 @@ export class ChainSessionManager { ): Promise { if (!this.provider || !this.wallet) throw new InitializationError('Manager core components not ready for explicit session.') + if (!this.sessionManager) throw new InitializationError('Main session manager is not initialized.') + + const signerAddress = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: pk })) const maxRetries = allowRetries ? 3 : 1 let lastError: Error | null = null for (let attempt = 1; attempt <= maxRetries; attempt++) { try { - const tempManager = new Signers.SessionManager(this.wallet, { - sessionManagerAddress: Extensions.Rc3.sessions, - provider: this.provider, - }) - const topology = await tempManager.getTopology() + const topology = await this.sessionManager.getTopology() - const signerAddress = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: pk })) const permissions = SessionConfig.getSessionPermissions(topology, signerAddress) - if (!permissions) { throw new InitializationError(`Permissions not found for session key.`) } - if (!this.sessionManager) throw new InitializationError('Main session manager is not initialized.') - const explicitSigner = new Signers.Session.Explicit(pk, permissions) this.sessionManager = this.sessionManager.withExplicitSigner(explicitSigner) @@ -711,9 +706,10 @@ export class ChainSessionManager { return } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)) - if (attempt < maxRetries) { - await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)) - } + } + if (attempt < maxRetries) { + console.error('Explicit session initialization failed, retrying...') + await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)) } } if (lastError) @@ -755,13 +751,31 @@ export class ChainSessionManager { } } + /** + * Checks if the current session has a valid signer. + * @returns A promise that resolves to true if the session has a valid signer, false otherwise. + */ + async hasValidSigner(): Promise { + if (!this.wallet || !this.sessionManager || !this.provider || !this.isInitialized) { + return false + } + + const signerValidity = await this.sessionManager.listSignerValidity(this.chainId) + if (signerValidity.some((s) => s.isValid)) { + return true + } + // SessionSignerInvalidReason available here + return false + } + /** * Fetches fee options for a set of transactions. + * @param wallet The wallet address to use for the fee options. * @param calls The transactions to estimate fees for. * @returns A promise that resolves with an array of fee options. * @throws {FeeOptionError} If fetching fee options fails. */ - async getFeeOptions(calls: Transaction[]): Promise { + async getFeeOptions(wallet: Address.Address, calls: Transaction[]): Promise { const callsToSend = calls.map((tx) => ({ to: tx.to, value: tx.value, @@ -772,8 +786,7 @@ export class ChainSessionManager { behaviorOnError: tx.behaviorOnError ?? ('revert' as const), })) try { - const signedCall = await this._buildAndSignCalls(callsToSend) - const feeOptions = await this.relayer.feeOptions(signedCall.to, this.chainId, callsToSend) + const feeOptions = await this.relayer.feeOptions(wallet, this.chainId, callsToSend) return feeOptions.options } catch (err) { throw new FeeOptionError(`Failed to get fee options: ${err instanceof Error ? err.message : String(err)}`) @@ -948,9 +961,7 @@ export class ChainSessionManager { ...envelope.payload, parentWallets: [this.wallet.address], } - const imageHash = await this.sessionManager.imageHash - if (imageHash === undefined) throw new SessionConfigError('Session manager image hash is undefined') - + const imageHash = await this.sessionManager.getImageHash() const signature = await this.sessionManager.signSapient( this.wallet.address, this.chainId, diff --git a/packages/wallet/dapp-client/src/DappClient.ts b/packages/wallet/dapp-client/src/DappClient.ts index dcc7d81ce..6253eaeb7 100644 --- a/packages/wallet/dapp-client/src/DappClient.ts +++ b/packages/wallet/dapp-client/src/DappClient.ts @@ -271,9 +271,11 @@ export class DappClient { * for previously established sessions. */ private async _loadStateFromStorage(): Promise { - const implicitSession = await this.sequenceStorage.getImplicitSession() + const [implicitSession, explicitSessions] = await Promise.all([ + this.sequenceStorage.getImplicitSession(), + this.sequenceStorage.getExplicitSessions(), + ]) - const explicitSessions = await this.sequenceStorage.getExplicitSessions() const chainIdsToInitialize = new Set([ ...(implicitSession?.chainId !== undefined ? [implicitSession.chainId] : []), ...explicitSessions.map((s) => s.chainId), @@ -551,11 +553,11 @@ export class DappClient { * } */ async getFeeOptions(chainId: number, transactions: Transaction[]): Promise { - if (!this.isInitialized) throw new InitializationError('Not initialized') + if (!this.isInitialized || !this.walletAddress) throw new InitializationError('Not initialized') const chainSessionManager = this.getChainSessionManager(chainId) if (!chainSessionManager.isInitialized) throw new InitializationError(`ChainSessionManager for chain ${chainId} is not initialized.`) - return await chainSessionManager.getFeeOptions(transactions) + return await chainSessionManager.getFeeOptions(this.walletAddress, transactions) } /** @@ -572,6 +574,19 @@ export class DappClient { return await chainSessionManager.hasPermission(transactions) } + /** + * Checks if the current session has a valid signer. + * @param chainId The chain ID on which to check the signer. + * @returns A promise that resolves to true if the session has a valid signer, otherwise false. + */ + async hasValidSigner(chainId: number): Promise { + const chainSessionManager = this.chainSessionManagers.get(chainId) + if (!chainSessionManager || !chainSessionManager.isInitialized) { + return false + } + return await chainSessionManager.hasValidSigner() + } + /** * Signs and sends a transaction using an available session signer. * @param chainId The chain ID on which to send the transaction. diff --git a/packages/wallet/wdk/test/sessions.test.ts b/packages/wallet/wdk/test/sessions.test.ts index f6d8a144b..5e3a2592b 100644 --- a/packages/wallet/wdk/test/sessions.test.ts +++ b/packages/wallet/wdk/test/sessions.test.ts @@ -151,7 +151,7 @@ describe('Sessions (via Manager)', () => { } const signature = await dapp.sessionManager.signSapient( dapp.wallet.address, - chainId ?? 1n, + chainId, parentedEnvelope, sessionImageHash, ) @@ -255,7 +255,7 @@ describe('Sessions (via Manager)', () => { // Configure mock provider ;(provider as any).request.mockImplementation(({ method, params }) => { if (method === 'eth_chainId') { - return Promise.resolve(chainId.toString()) + return Promise.resolve(Hex.fromNumber(chainId)) } if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { // Undeployed wallet @@ -329,7 +329,7 @@ describe('Sessions (via Manager)', () => { // Configure mock provider ;(provider as any).request.mockImplementation(({ method, params }) => { if (method === 'eth_chainId') { - return Promise.resolve(chainId.toString()) + return Promise.resolve(Hex.fromNumber(chainId)) } if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { // Undeployed wallet @@ -404,7 +404,7 @@ describe('Sessions (via Manager)', () => { // Configure mock provider ;(provider as any).request.mockImplementation(({ method, params }) => { if (method === 'eth_chainId') { - return Promise.resolve(chainId.toString()) + return Promise.resolve(Hex.fromNumber(chainId)) } if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { // Undeployed wallet @@ -491,7 +491,7 @@ describe('Sessions (via Manager)', () => { // Configure mock provider ;(provider as any).request.mockImplementation(({ method, params }) => { if (method === 'eth_chainId') { - return Promise.resolve(chainId.toString()) + return Promise.resolve(Hex.fromNumber(chainId)) } if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) { // Undeployed wallet