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