Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion packages/wallet/wdk/src/sequence/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export type ManagerOptions = {
guardUrl?: string
guardAddresses?: Record<GuardRole, Address.Address>

nonWitnessableSigners?: Address.Address[]

// The default guard topology MUST have a placeholder address for the guard address
defaultGuardTopology?: Config.Topology
defaultRecoverySettings?: RecoverySettings
Expand Down Expand Up @@ -123,6 +125,8 @@ export const ManagerOptionsDefaults = {
},
bundlers: [],

nonWitnessableSigners: [] as Address.Address[],

guardUrl: 'https://guard.sequence.app',
guardAddresses: {
wallet: '0x26f3D30F41FA897309Ae804A2AFf15CEb1dA5742',
Expand Down Expand Up @@ -183,11 +187,41 @@ export const CreateWalletOptionsDefaults = {
}

export function applyManagerOptionsDefaults(options?: ManagerOptions) {
return {
const merged = {
...ManagerOptionsDefaults,
...options,
identity: { ...ManagerOptionsDefaults.identity, ...options?.identity },
}

// Merge and normalize non-witnessable signers.
// We always include the sessions extension address for the active extensions set.
const nonWitnessable = new Set<string>()
for (const address of ManagerOptionsDefaults.nonWitnessableSigners ?? []) {
nonWitnessable.add(address.toLowerCase())
}
for (const address of options?.nonWitnessableSigners ?? []) {
nonWitnessable.add(address.toLowerCase())
}
nonWitnessable.add(merged.extensions.sessions.toLowerCase())

// Include static signer leaves from the guard topology (e.g. recovery guard signer),
// but ignore the placeholder address that is later replaced per-role.
if (merged.defaultGuardTopology) {
const guardTopologySigners = Config.getSigners(merged.defaultGuardTopology)
for (const signer of guardTopologySigners.signers) {
if (Address.isEqual(signer, Constants.PlaceholderAddress)) {
continue
}
nonWitnessable.add(signer.toLowerCase())
}
for (const signer of guardTopologySigners.sapientSigners) {
nonWitnessable.add(signer.address.toLowerCase())
}
}

merged.nonWitnessableSigners = Array.from(nonWitnessable) as Address.Address[]

return merged
}

export type RecoverySettings = {
Expand Down Expand Up @@ -221,6 +255,8 @@ export type Sequence = {
readonly relayers: Relayer.Relayer[]
readonly bundlers: Bundler.Bundler[]

readonly nonWitnessableSigners: ReadonlySet<Address.Address>

readonly defaultGuardTopology: Config.Topology
readonly defaultRecoverySettings: RecoverySettings

Expand Down Expand Up @@ -408,6 +444,10 @@ export class Manager {
relayers,
bundlers: ops.bundlers,

nonWitnessableSigners: new Set(
(ops.nonWitnessableSigners ?? []).map((address) => address.toLowerCase() as Address.Address),
),

defaultGuardTopology: ops.defaultGuardTopology,
defaultRecoverySettings: ops.defaultRecoverySettings,

Expand Down
13 changes: 13 additions & 0 deletions packages/wallet/wdk/src/sequence/signers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ export class Signers {
return Kinds.Guard
}

// Passkeys are a sapient signer module: the address alone identifies the kind.
// Metadata (credential id, public key, etc.) is loaded later by the PasskeysHandler
// via the witness payload, so we can skip the witness probe here.
if (Address.isEqual(this.shared.sequence.extensions.passkeys, address)) {
return Kinds.LoginPasskey
}

// Some signers are known to never publish a witness record (e.g. module signers).
// Skip probing the Sessions/Witness endpoint for them.
if (this.shared.sequence.nonWitnessableSigners.has(address.toLowerCase() as Address.Address)) {
return undefined
}

// We need to use the state provider (and witness) this will tell us the kind of signer
// NOTICE: This looks expensive, but this operation should be cached by the state provider
const witness = await (imageHash
Expand Down
40 changes: 40 additions & 0 deletions packages/wallet/wdk/test/signers-kindof.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from 'vitest'

import { Kinds } from '../src/sequence/index.js'
import { newManager } from './constants.js'

describe('Signers.kindOf', () => {
it('does not probe Sessions/Witness for non-witnessable signers', async () => {
const getWitnessFor = vi.fn().mockResolvedValue(undefined)
const getWitnessForSapient = vi.fn().mockResolvedValue(undefined)

const manager = newManager({
stateProvider: {
getWitnessFor,
getWitnessForSapient,
} as any,
})

const signers = (manager as any).shared.modules.signers
const extensions = (manager as any).shared.sequence.extensions

const wallet = '0x1111111111111111111111111111111111111111'
const imageHash = ('0x' + '00'.repeat(32)) as `0x${string}`

// Sessions extension signer (sapient leaf) never publishes a witness.
await signers.kindOf(wallet, extensions.sessions, imageHash)

// Passkeys module is a known sapient signer kind.
expect(await signers.kindOf(wallet, extensions.passkeys, imageHash)).toBe(Kinds.LoginPasskey)

// Sequence dev multisig (default guard topology leaf) never publishes a witness.
await signers.kindOf(wallet, '0x007a47e6BF40C1e0ed5c01aE42fDC75879140bc4')

expect(getWitnessFor).not.toHaveBeenCalled()
expect(getWitnessForSapient).not.toHaveBeenCalled()

// Unknown signers still rely on a witness probe.
await signers.kindOf(wallet, '0x2222222222222222222222222222222222222222')
expect(getWitnessFor).toHaveBeenCalledTimes(1)
})
})