Skip to content

Commit a41a71d

Browse files
committed
Fix handling for multiple explicit signers
1 parent 6f34c5d commit a41a71d

5 files changed

Lines changed: 385 additions & 34 deletions

File tree

packages/wallet/core/src/signers/session-manager.ts

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
SessionSigner,
1919
SessionSignerInvalidReason,
2020
isImplicitSessionSigner,
21+
isIncrementCall,
2122
UsageLimit,
2223
} from './session/index.js'
2324

@@ -130,21 +131,16 @@ export class SessionManager implements SapientSigner {
130131
}))
131132
}
132133

133-
async findSignersForCalls(wallet: Address.Address, chainId: number, calls: Payload.Call[]): Promise<SessionSigner[]> {
134-
// Only use signers that match the topology
135-
const topology = await this.topology
136-
const identitySigners = SessionConfig.getIdentitySigners(topology)
137-
if (identitySigners.length === 0) {
138-
throw new Error('Identity signers not found')
139-
}
140-
141-
// Prioritize implicit signers
142-
const availableSigners = [...this._implicitSigners, ...this._explicitSigners]
143-
if (availableSigners.length === 0) {
144-
throw new Error('No signers match the topology')
145-
}
146-
147-
// Find supported signers for each call
134+
/**
135+
* Find one signer per call from the given candidate list (first that supports each call).
136+
*/
137+
private async findSignersForCallsWithCandidates(
138+
wallet: Address.Address,
139+
chainId: number,
140+
calls: Payload.Call[],
141+
topology: SessionConfig.SessionsTopology,
142+
availableSigners: SessionSigner[],
143+
): Promise<SessionSigner[]> {
148144
const signers: SessionSigner[] = []
149145
for (const call of calls) {
150146
let supported = false
@@ -173,9 +169,67 @@ export class SessionManager implements SapientSigner {
173169
if (expiredSupportedSigner) {
174170
throw new Error(`Signer supporting call is expired: ${expiredSupportedSigner.address}`)
175171
}
176-
throw new Error(
177-
`No signer supported for call. ` + `Call: to=${call.to}, data=${call.data}, value=${call.value}, `,
178-
)
172+
throw new Error(`No signer supported for call. Call: to=${call.to}, data=${call.data}, value=${call.value}, `)
173+
}
174+
}
175+
return signers
176+
}
177+
178+
async findSignersForCalls(wallet: Address.Address, chainId: number, calls: Payload.Call[]): Promise<SessionSigner[]> {
179+
const topology = await this.topology
180+
const identitySigners = SessionConfig.getIdentitySigners(topology)
181+
if (identitySigners.length === 0) {
182+
throw new Error('Identity signers not found')
183+
}
184+
185+
const availableSigners = [...this._implicitSigners, ...this._explicitSigners]
186+
if (availableSigners.length === 0) {
187+
throw new Error('No signers match the topology')
188+
}
189+
190+
const nonIncrementCalls: Payload.Call[] = []
191+
const incrementCalls: Payload.Call[] = []
192+
for (const call of calls) {
193+
if (isIncrementCall(call, this.address)) {
194+
incrementCalls.push(call)
195+
} else {
196+
nonIncrementCalls.push(call)
197+
}
198+
}
199+
200+
// Find signers for non-increment calls
201+
const nonIncrementSigners =
202+
nonIncrementCalls.length > 0
203+
? await this.findSignersForCallsWithCandidates(wallet, chainId, nonIncrementCalls, topology, availableSigners)
204+
: []
205+
206+
let incrementSigners: SessionSigner[] = []
207+
if (incrementCalls.length > 0) {
208+
// Find signers for increment calls, preferring signers that signed non-increment calls
209+
const incrementCandidates = [
210+
...nonIncrementSigners,
211+
...availableSigners.filter((s) => !nonIncrementSigners.includes(s)),
212+
]
213+
incrementSigners = await this.findSignersForCallsWithCandidates(
214+
wallet,
215+
chainId,
216+
incrementCalls,
217+
topology,
218+
incrementCandidates,
219+
)
220+
}
221+
222+
// Merge back in original call order
223+
const signers: SessionSigner[] = []
224+
let nonIncrementIndex = 0
225+
let incrementIndex = 0
226+
for (const call of calls) {
227+
if (isIncrementCall(call, this.address)) {
228+
signers.push(incrementSigners[incrementIndex]!)
229+
incrementIndex++
230+
} else {
231+
signers.push(nonIncrementSigners[nonIncrementIndex]!)
232+
nonIncrementIndex++
179233
}
180234
}
181235
return signers
@@ -191,20 +245,23 @@ export class SessionManager implements SapientSigner {
191245
}
192246
const signers = await this.findSignersForCalls(wallet, chainId, calls)
193247

194-
// Create a map of signers to their associated calls
195-
const signerToCalls = new Map<SessionSigner, Payload.Call[]>()
248+
// Map each signer to only their non-increment calls
249+
const signerToNonIncrementCalls = new Map<SessionSigner, Payload.Call[]>()
196250
signers.forEach((signer, index) => {
197251
const call = calls[index]!
198-
const existingCalls = signerToCalls.get(signer) || []
199-
signerToCalls.set(signer, [...existingCalls, call])
252+
if (isIncrementCall(call, this.address)) {
253+
return
254+
}
255+
const existing = signerToNonIncrementCalls.get(signer) || []
256+
signerToNonIncrementCalls.set(signer, [...existing, call])
200257
})
201258

202-
// Prepare increments for each explicit signer with their associated calls
259+
// Prepare increments for each explicit signer from their non-increment calls only
203260
const increments: UsageLimit[] = (
204261
await Promise.all(
205-
Array.from(signerToCalls.entries()).map(async ([signer, associatedCalls]) => {
262+
Array.from(signerToNonIncrementCalls.entries()).map(async ([signer, nonIncrementCalls]) => {
206263
if (isExplicitSessionSigner(signer)) {
207-
return signer.prepareIncrements(wallet, chainId, associatedCalls, this.address, this._provider!)
264+
return signer.prepareIncrements(wallet, chainId, nonIncrementCalls, this.address, this._provider!)
208265
}
209266
return []
210267
}),

packages/wallet/core/src/signers/session/explicit.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Constants, Payload, Permission, SessionConfig, SessionSignature } from '@0xsequence/wallet-primitives'
22
import { AbiFunction, AbiParameters, Address, Bytes, Hash, Hex, Provider } from 'ox'
33
import { MemoryPkStore, PkStore } from '../pk/index.js'
4-
import { ExplicitSessionSigner, SessionSignerValidity, UsageLimit } from './session.js'
4+
import { ExplicitSessionSigner, isIncrementCall, SessionSignerValidity, UsageLimit } from './session.js'
55

66
export type ExplicitParams = Omit<Permission.SessionPermissions, 'signer'>
77

@@ -208,11 +208,7 @@ export class Explicit implements ExplicitSessionSigner {
208208
sessionManagerAddress: Address.Address,
209209
provider?: Provider.Provider,
210210
): Promise<boolean> {
211-
if (
212-
Address.isEqual(call.to, sessionManagerAddress) &&
213-
Hex.size(call.data) > 4 &&
214-
Hex.isEqual(Hex.slice(call.data, 0, 4), AbiFunction.getSelector(Constants.INCREMENT_USAGE_LIMIT))
215-
) {
211+
if (isIncrementCall(call, sessionManagerAddress)) {
216212
// Can sign increment usage calls
217213
return true
218214
}

packages/wallet/core/src/signers/session/session.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Payload, SessionConfig, SessionSignature } from '@0xsequence/wallet-primitives'
2-
import { Address, Hex, Provider } from 'ox'
1+
import { Constants, Payload, SessionConfig, SessionSignature } from '@0xsequence/wallet-primitives'
2+
import { AbiFunction, Address, Hex, Provider } from 'ox'
33

44
export type SessionSignerInvalidReason =
55
| 'Expired'
@@ -68,3 +68,11 @@ export function isExplicitSessionSigner(signer: SessionSigner): signer is Explic
6868
export function isImplicitSessionSigner(signer: SessionSigner): signer is ImplicitSessionSigner {
6969
return 'identitySigner' in signer
7070
}
71+
72+
export function isIncrementCall(call: Payload.Call, sessionManagerAddress: Address.Address): boolean {
73+
return (
74+
Address.isEqual(call.to, sessionManagerAddress) &&
75+
Hex.size(call.data) >= 4 &&
76+
Hex.isEqual(Hex.slice(call.data, 0, 4), AbiFunction.getSelector(Constants.INCREMENT_USAGE_LIMIT))
77+
)
78+
}

packages/wallet/core/test/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Abi, AbiEvent, Address } from 'ox'
55
const envFile = process.env.CI ? '.env.test' : '.env.test.local'
66
dotenvConfig({ path: envFile })
77

8+
// Contracts are deployed on Arbitrum
9+
810
// Requires https://example.com redirectUrl
911
export const EMITTER_ADDRESS1: Address.Address = '0xad90eB52BC180Bd9f66f50981E196f3E996278D3'
1012
// Requires https://another-example.com redirectUrl

0 commit comments

Comments
 (0)