@0xsequence/wallet-wdk 0.0.0-20250520201059
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.test +3 -0
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +202 -0
- package/dist/dbs/auth-commitments.d.ts +17 -0
- package/dist/dbs/auth-commitments.d.ts.map +1 -0
- package/dist/dbs/auth-commitments.js +13 -0
- package/dist/dbs/auth-keys.d.ts +19 -0
- package/dist/dbs/auth-keys.d.ts.map +1 -0
- package/dist/dbs/auth-keys.js +67 -0
- package/dist/dbs/generic.d.ts +33 -0
- package/dist/dbs/generic.d.ts.map +1 -0
- package/dist/dbs/generic.js +170 -0
- package/dist/dbs/index.d.ts +12 -0
- package/dist/dbs/index.d.ts.map +1 -0
- package/dist/dbs/index.js +8 -0
- package/dist/dbs/messages.d.ts +6 -0
- package/dist/dbs/messages.d.ts.map +1 -0
- package/dist/dbs/messages.js +13 -0
- package/dist/dbs/recovery.d.ts +6 -0
- package/dist/dbs/recovery.d.ts.map +1 -0
- package/dist/dbs/recovery.js +13 -0
- package/dist/dbs/signatures.d.ts +6 -0
- package/dist/dbs/signatures.d.ts.map +1 -0
- package/dist/dbs/signatures.js +13 -0
- package/dist/dbs/transactions.d.ts +6 -0
- package/dist/dbs/transactions.d.ts.map +1 -0
- package/dist/dbs/transactions.js +13 -0
- package/dist/dbs/wallets.d.ts +6 -0
- package/dist/dbs/wallets.d.ts.map +1 -0
- package/dist/dbs/wallets.js +13 -0
- package/dist/identity/signer.d.ts +17 -0
- package/dist/identity/signer.d.ts.map +1 -0
- package/dist/identity/signer.js +58 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/sequence/cron.d.ts +19 -0
- package/dist/sequence/cron.d.ts.map +1 -0
- package/dist/sequence/cron.js +118 -0
- package/dist/sequence/devices.d.ts +14 -0
- package/dist/sequence/devices.d.ts.map +1 -0
- package/dist/sequence/devices.js +43 -0
- package/dist/sequence/handlers/authcode-pkce.d.ts +14 -0
- package/dist/sequence/handlers/authcode-pkce.d.ts.map +1 -0
- package/dist/sequence/handlers/authcode-pkce.js +48 -0
- package/dist/sequence/handlers/authcode.d.ts +25 -0
- package/dist/sequence/handlers/authcode.d.ts.map +1 -0
- package/dist/sequence/handlers/authcode.js +91 -0
- package/dist/sequence/handlers/devices.d.ts +14 -0
- package/dist/sequence/handlers/devices.d.ts.map +1 -0
- package/dist/sequence/handlers/devices.js +39 -0
- package/dist/sequence/handlers/handler.d.ts +8 -0
- package/dist/sequence/handlers/handler.d.ts.map +1 -0
- package/dist/sequence/handlers/handler.js +1 -0
- package/dist/sequence/handlers/identity.d.ts +21 -0
- package/dist/sequence/handlers/identity.d.ts.map +1 -0
- package/dist/sequence/handlers/identity.js +86 -0
- package/dist/sequence/handlers/index.d.ts +7 -0
- package/dist/sequence/handlers/index.d.ts.map +1 -0
- package/dist/sequence/handlers/index.js +5 -0
- package/dist/sequence/handlers/mnemonic.d.ts +19 -0
- package/dist/sequence/handlers/mnemonic.d.ts.map +1 -0
- package/dist/sequence/handlers/mnemonic.js +67 -0
- package/dist/sequence/handlers/otp.d.ts +20 -0
- package/dist/sequence/handlers/otp.d.ts.map +1 -0
- package/dist/sequence/handlers/otp.js +83 -0
- package/dist/sequence/handlers/passkeys.d.ts +17 -0
- package/dist/sequence/handlers/passkeys.d.ts.map +1 -0
- package/dist/sequence/handlers/passkeys.js +63 -0
- package/dist/sequence/handlers/recovery.d.ts +15 -0
- package/dist/sequence/handlers/recovery.d.ts.map +1 -0
- package/dist/sequence/handlers/recovery.js +72 -0
- package/dist/sequence/index.d.ts +12 -0
- package/dist/sequence/index.d.ts.map +1 -0
- package/dist/sequence/index.js +9 -0
- package/dist/sequence/logger.d.ts +7 -0
- package/dist/sequence/logger.d.ts.map +1 -0
- package/dist/sequence/logger.js +11 -0
- package/dist/sequence/manager.d.ts +287 -0
- package/dist/sequence/manager.d.ts.map +1 -0
- package/dist/sequence/manager.js +356 -0
- package/dist/sequence/messages.d.ts +18 -0
- package/dist/sequence/messages.d.ts.map +1 -0
- package/dist/sequence/messages.js +115 -0
- package/dist/sequence/recovery.d.ts +30 -0
- package/dist/sequence/recovery.d.ts.map +1 -0
- package/dist/sequence/recovery.js +314 -0
- package/dist/sequence/sessions.d.ts +26 -0
- package/dist/sequence/sessions.d.ts.map +1 -0
- package/dist/sequence/sessions.js +169 -0
- package/dist/sequence/signatures.d.ts +21 -0
- package/dist/sequence/signatures.d.ts.map +1 -0
- package/dist/sequence/signatures.js +192 -0
- package/dist/sequence/signers.d.ts +14 -0
- package/dist/sequence/signers.d.ts.map +1 -0
- package/dist/sequence/signers.js +74 -0
- package/dist/sequence/transactions.d.ts +26 -0
- package/dist/sequence/transactions.d.ts.map +1 -0
- package/dist/sequence/transactions.js +201 -0
- package/dist/sequence/types/index.d.ts +9 -0
- package/dist/sequence/types/index.d.ts.map +1 -0
- package/dist/sequence/types/index.js +2 -0
- package/dist/sequence/types/message-request.d.ts +23 -0
- package/dist/sequence/types/message-request.d.ts.map +1 -0
- package/dist/sequence/types/message-request.js +1 -0
- package/dist/sequence/types/recovery.d.ts +15 -0
- package/dist/sequence/types/recovery.d.ts.map +1 -0
- package/dist/sequence/types/recovery.js +1 -0
- package/dist/sequence/types/signature-request.d.ts +76 -0
- package/dist/sequence/types/signature-request.d.ts.map +1 -0
- package/dist/sequence/types/signature-request.js +11 -0
- package/dist/sequence/types/signer.d.ts +28 -0
- package/dist/sequence/types/signer.d.ts.map +1 -0
- package/dist/sequence/types/signer.js +10 -0
- package/dist/sequence/types/transaction-request.d.ts +41 -0
- package/dist/sequence/types/transaction-request.d.ts.map +1 -0
- package/dist/sequence/types/transaction-request.js +1 -0
- package/dist/sequence/types/wallet.d.ts +21 -0
- package/dist/sequence/types/wallet.d.ts.map +1 -0
- package/dist/sequence/types/wallet.js +1 -0
- package/dist/sequence/wallets.d.ts +121 -0
- package/dist/sequence/wallets.d.ts.map +1 -0
- package/dist/sequence/wallets.js +632 -0
- package/package.json +40 -0
- package/src/dbs/auth-commitments.ts +26 -0
- package/src/dbs/auth-keys.ts +85 -0
- package/src/dbs/generic.ts +194 -0
- package/src/dbs/index.ts +13 -0
- package/src/dbs/messages.ts +16 -0
- package/src/dbs/recovery.ts +15 -0
- package/src/dbs/signatures.ts +15 -0
- package/src/dbs/transactions.ts +16 -0
- package/src/dbs/wallets.ts +16 -0
- package/src/identity/signer.ts +78 -0
- package/src/index.ts +2 -0
- package/src/sequence/cron.ts +134 -0
- package/src/sequence/devices.ts +53 -0
- package/src/sequence/handlers/authcode-pkce.ts +70 -0
- package/src/sequence/handlers/authcode.ts +116 -0
- package/src/sequence/handlers/devices.ts +53 -0
- package/src/sequence/handlers/handler.ts +14 -0
- package/src/sequence/handlers/identity.ts +101 -0
- package/src/sequence/handlers/index.ts +6 -0
- package/src/sequence/handlers/mnemonic.ts +88 -0
- package/src/sequence/handlers/otp.ts +107 -0
- package/src/sequence/handlers/passkeys.ts +84 -0
- package/src/sequence/handlers/recovery.ts +88 -0
- package/src/sequence/index.ts +25 -0
- package/src/sequence/logger.ts +11 -0
- package/src/sequence/manager.ts +634 -0
- package/src/sequence/messages.ts +146 -0
- package/src/sequence/recovery.ts +429 -0
- package/src/sequence/sessions.ts +238 -0
- package/src/sequence/signatures.ts +263 -0
- package/src/sequence/signers.ts +88 -0
- package/src/sequence/transactions.ts +281 -0
- package/src/sequence/types/index.ts +27 -0
- package/src/sequence/types/message-request.ts +26 -0
- package/src/sequence/types/recovery.ts +15 -0
- package/src/sequence/types/signature-request.ts +89 -0
- package/src/sequence/types/signer.ts +32 -0
- package/src/sequence/types/transaction-request.ts +47 -0
- package/src/sequence/types/wallet.ts +24 -0
- package/src/sequence/wallets.ts +853 -0
- package/test/constants.ts +62 -0
- package/test/recovery.test.ts +211 -0
- package/test/sessions.test.ts +324 -0
- package/test/setup.ts +63 -0
- package/test/transactions.test.ts +464 -0
- package/test/wallets.test.ts +381 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { config as dotenvConfig } from 'dotenv'
|
|
2
|
+
import { Abi, Address } from 'ox'
|
|
3
|
+
import { Manager, ManagerOptions, ManagerOptionsDefaults } from '../src/sequence'
|
|
4
|
+
import { mockEthereum } from './setup'
|
|
5
|
+
import { Signers as CoreSigners } from '@0xsequence/wallet-core'
|
|
6
|
+
import * as Db from '../src/dbs'
|
|
7
|
+
|
|
8
|
+
const envFile = process.env.CI ? '.env.test' : '.env.test.local'
|
|
9
|
+
dotenvConfig({ path: envFile })
|
|
10
|
+
|
|
11
|
+
export const EMITTER_ADDRESS: Address.Address = '0x7F6e420Ed3017A36bE6e1DA8e3AFE61569eb4840'
|
|
12
|
+
export const EMITTER_ABI = Abi.from(['function explicitEmit()', 'function implicitEmit()'])
|
|
13
|
+
|
|
14
|
+
// Environment variables
|
|
15
|
+
export const { RPC_URL, PRIVATE_KEY } = process.env
|
|
16
|
+
export const CAN_RUN_LIVE = !!RPC_URL && !!PRIVATE_KEY
|
|
17
|
+
export const LOCAL_RPC_URL = process.env.LOCAL_RPC_URL || 'http://localhost:8545'
|
|
18
|
+
|
|
19
|
+
let testIdCounter = 0
|
|
20
|
+
|
|
21
|
+
export function newManager(options?: ManagerOptions, noEthereumMock?: boolean, tag?: string) {
|
|
22
|
+
if (!noEthereumMock) {
|
|
23
|
+
mockEthereum()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
testIdCounter++
|
|
27
|
+
const dbSuffix = tag ? `_${tag}_testrun_${testIdCounter}` : `_testrun_${testIdCounter}`
|
|
28
|
+
|
|
29
|
+
// Ensure options and its identity sub-object exist for easier merging
|
|
30
|
+
const effectiveOptions = {
|
|
31
|
+
...options,
|
|
32
|
+
identity: { ...ManagerOptionsDefaults.identity, ...options?.identity },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return new Manager({
|
|
36
|
+
networks: [
|
|
37
|
+
{
|
|
38
|
+
name: 'Arbitrum (local fork)',
|
|
39
|
+
rpc: LOCAL_RPC_URL,
|
|
40
|
+
chainId: 42161n,
|
|
41
|
+
explorer: 'https://arbiscan.io/',
|
|
42
|
+
nativeCurrency: {
|
|
43
|
+
name: 'Ether',
|
|
44
|
+
symbol: 'ETH',
|
|
45
|
+
decimals: 18,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
// Override DBs with unique names if not provided in options,
|
|
50
|
+
// otherwise, use the provided DB instance.
|
|
51
|
+
// This assumes options?.someDb is either undefined or a fully constructed DB instance.
|
|
52
|
+
encryptedPksDb: effectiveOptions.encryptedPksDb || new CoreSigners.Pk.Encrypted.EncryptedPksDb('pk-db' + dbSuffix),
|
|
53
|
+
managerDb: effectiveOptions.managerDb || new Db.Wallets('sequence-manager' + dbSuffix),
|
|
54
|
+
transactionsDb: effectiveOptions.transactionsDb || new Db.Transactions('sequence-transactions' + dbSuffix),
|
|
55
|
+
signaturesDb: effectiveOptions.signaturesDb || new Db.Signatures('sequence-signature-requests' + dbSuffix),
|
|
56
|
+
authCommitmentsDb:
|
|
57
|
+
effectiveOptions.authCommitmentsDb || new Db.AuthCommitments('sequence-auth-commitments' + dbSuffix),
|
|
58
|
+
authKeysDb: effectiveOptions.authKeysDb || new Db.AuthKeys('sequence-auth-keys' + dbSuffix),
|
|
59
|
+
recoveryDb: effectiveOptions.recoveryDb || new Db.Recovery('sequence-recovery' + dbSuffix),
|
|
60
|
+
...effectiveOptions,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { Manager, QueuedRecoveryPayload, SignerReady, TransactionDefined } from '../src/sequence'
|
|
3
|
+
import { Bytes, Hex, Mnemonic, Provider, RpcTransport } from 'ox'
|
|
4
|
+
import { Payload } from '@0xsequence/wallet-primitives'
|
|
5
|
+
import { LOCAL_RPC_URL, newManager } from './constants'
|
|
6
|
+
|
|
7
|
+
describe('Recovery', () => {
|
|
8
|
+
it('Should execute a recovery', async () => {
|
|
9
|
+
const manager = newManager({
|
|
10
|
+
defaultRecoverySettings: {
|
|
11
|
+
requiredDeltaTime: 2n, // 2 seconds
|
|
12
|
+
minTimestamp: 0n,
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const mnemonic = Mnemonic.random(Mnemonic.english)
|
|
17
|
+
const wallet = await manager.signUp({ mnemonic, kind: 'mnemonic', noGuard: true })
|
|
18
|
+
expect(wallet).toBeDefined()
|
|
19
|
+
|
|
20
|
+
// Add recovery mnemonic
|
|
21
|
+
const mnemonic2 = Mnemonic.random(Mnemonic.english)
|
|
22
|
+
const requestId1 = await manager.addRecoveryMnemonic(wallet!, mnemonic2)
|
|
23
|
+
|
|
24
|
+
expect(requestId1).toBeDefined()
|
|
25
|
+
|
|
26
|
+
// Sign add recovery mnemonic
|
|
27
|
+
const request1 = await manager.getSignatureRequest(requestId1)
|
|
28
|
+
expect(request1).toBeDefined()
|
|
29
|
+
|
|
30
|
+
// Device must be the only ready signer now
|
|
31
|
+
const device = request1.signers.find((s) => s.status === 'ready')
|
|
32
|
+
expect(device).toBeDefined()
|
|
33
|
+
|
|
34
|
+
const result1 = await device?.handle()
|
|
35
|
+
expect(result1).toBeDefined()
|
|
36
|
+
expect(result1).toBeTruthy()
|
|
37
|
+
|
|
38
|
+
// Complete the add of the recovery mnemonic
|
|
39
|
+
await manager.completeRecoveryUpdate(requestId1)
|
|
40
|
+
|
|
41
|
+
// Get the recovery signers, there should be two one
|
|
42
|
+
// and one should not be the device address
|
|
43
|
+
const recoverySigners = await manager.getRecoverySigners(wallet!)
|
|
44
|
+
expect(recoverySigners).toBeDefined()
|
|
45
|
+
expect(recoverySigners!.length).toBe(2)
|
|
46
|
+
const nonDeviceSigner = recoverySigners!.find((s) => s.address !== device?.address)
|
|
47
|
+
expect(nonDeviceSigner).toBeDefined()
|
|
48
|
+
|
|
49
|
+
// Transfer 1 wei to the wallet
|
|
50
|
+
const provider = Provider.from(RpcTransport.fromHttp(LOCAL_RPC_URL))
|
|
51
|
+
await provider.request({
|
|
52
|
+
method: 'anvil_setBalance',
|
|
53
|
+
params: [wallet!, '0x1'],
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Create a new recovery payload
|
|
57
|
+
const requestId2 = await manager.queueRecoveryPayload(wallet!, 42161n, {
|
|
58
|
+
type: 'call',
|
|
59
|
+
space: Bytes.toBigInt(Bytes.random(20)),
|
|
60
|
+
nonce: 0n,
|
|
61
|
+
calls: [
|
|
62
|
+
{
|
|
63
|
+
to: Hex.from(Bytes.random(20)),
|
|
64
|
+
value: 1n,
|
|
65
|
+
data: '0x',
|
|
66
|
+
gasLimit: 1000000n,
|
|
67
|
+
delegateCall: false,
|
|
68
|
+
onlyFallback: false,
|
|
69
|
+
behaviorOnError: 'revert',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Needs to be signed using the recovery mnemonic
|
|
75
|
+
// for this we need to define a handler for it
|
|
76
|
+
let handledMnemonic2 = 0
|
|
77
|
+
const unregisterHandler = manager.registerMnemonicUI(async (respond) => {
|
|
78
|
+
handledMnemonic2++
|
|
79
|
+
await respond(mnemonic2)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Sign the queue recovery payload
|
|
83
|
+
const request2 = await manager.getSignatureRequest(requestId2)
|
|
84
|
+
expect(request2).toBeDefined()
|
|
85
|
+
|
|
86
|
+
// Complete the queue recovery payload
|
|
87
|
+
// the only signer available should be the device and the recovery mnemonic
|
|
88
|
+
// the both recovery deviecs that we have
|
|
89
|
+
expect(request2.signers.length).toBe(2)
|
|
90
|
+
expect(request2.signers.some((s) => s.handler?.kind === 'local-device')).toBeTruthy()
|
|
91
|
+
expect(request2.signers.some((s) => s.handler?.kind === 'login-mnemonic')).toBeTruthy()
|
|
92
|
+
|
|
93
|
+
// Handle the login-mnemonic signer
|
|
94
|
+
const request2Signer = request2.signers.find((s) => s.handler?.kind === 'login-mnemonic')
|
|
95
|
+
expect(request2Signer).toBeDefined()
|
|
96
|
+
const result2 = await (request2Signer as SignerReady).handle()
|
|
97
|
+
expect(result2).toBeDefined()
|
|
98
|
+
expect(result2).toBeTruthy()
|
|
99
|
+
expect(handledMnemonic2).toBe(1)
|
|
100
|
+
unregisterHandler()
|
|
101
|
+
|
|
102
|
+
// Complete the recovery payload
|
|
103
|
+
const { to, data } = await manager.completeRecoveryPayload(requestId2)
|
|
104
|
+
|
|
105
|
+
// Send this transaction to anvil so we queue the payload
|
|
106
|
+
await provider.request({
|
|
107
|
+
method: 'eth_sendTransaction',
|
|
108
|
+
params: [
|
|
109
|
+
{
|
|
110
|
+
to,
|
|
111
|
+
data,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Wait 3 seconds for the payload to become valid
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
118
|
+
await manager.updateQueuedRecoveryPayloads()
|
|
119
|
+
|
|
120
|
+
// Get the recovery payloads
|
|
121
|
+
const recoveryPayloads = await new Promise<QueuedRecoveryPayload[]>((resolve) => {
|
|
122
|
+
const unsubscribe = manager.onQueuedRecoveryPayloadsUpdate(
|
|
123
|
+
wallet!,
|
|
124
|
+
(payloads) => {
|
|
125
|
+
unsubscribe()
|
|
126
|
+
resolve(payloads)
|
|
127
|
+
},
|
|
128
|
+
true,
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(recoveryPayloads).toBeDefined()
|
|
133
|
+
expect(recoveryPayloads.length).toBe(1)
|
|
134
|
+
const recoveryPayload = recoveryPayloads![0]
|
|
135
|
+
expect(recoveryPayload).toBeDefined()
|
|
136
|
+
expect(Payload.isCalls(recoveryPayload!.payload!)).toBeTruthy()
|
|
137
|
+
expect((recoveryPayload!.payload as Payload.Calls).calls.length).toBe(1)
|
|
138
|
+
|
|
139
|
+
// Send this transaction as any other regular transaction
|
|
140
|
+
const requestId3 = await manager.requestTransaction(
|
|
141
|
+
wallet!,
|
|
142
|
+
42161n,
|
|
143
|
+
(recoveryPayload!.payload as Payload.Calls).calls,
|
|
144
|
+
{
|
|
145
|
+
noConfigUpdate: true,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
expect(requestId3).toBeDefined()
|
|
149
|
+
|
|
150
|
+
// Define the same nonce and space for the recovery payload
|
|
151
|
+
await manager.defineTransaction(requestId3, {
|
|
152
|
+
nonce: (recoveryPayload!.payload as Payload.Calls).nonce,
|
|
153
|
+
space: (recoveryPayload!.payload as Payload.Calls).space,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Complete the transaction
|
|
157
|
+
const tx = await manager.getTransaction(requestId3)
|
|
158
|
+
expect(tx).toBeDefined()
|
|
159
|
+
expect(tx.status).toBe('defined')
|
|
160
|
+
expect((tx as TransactionDefined).relayerOptions.length).toBe(1)
|
|
161
|
+
|
|
162
|
+
const localRelayer = (tx as TransactionDefined).relayerOptions[0]
|
|
163
|
+
expect(localRelayer).toBeDefined()
|
|
164
|
+
expect(localRelayer.relayerId).toBe('local')
|
|
165
|
+
|
|
166
|
+
// Define the relayer
|
|
167
|
+
const requestId4 = await manager.selectTransactionRelayer(requestId3, localRelayer.id)
|
|
168
|
+
expect(requestId4).toBeDefined()
|
|
169
|
+
|
|
170
|
+
// Now we sign using the recovery module
|
|
171
|
+
const request4 = await manager.getSignatureRequest(requestId4)
|
|
172
|
+
|
|
173
|
+
// Find the signer that is the recovery module handler
|
|
174
|
+
const recoverySigner = request4.signers.find((s) => s.handler?.kind === 'recovery-extension')
|
|
175
|
+
expect(recoverySigner).toBeDefined()
|
|
176
|
+
expect(recoverySigner!.status).toBe('ready')
|
|
177
|
+
1
|
|
178
|
+
// Handle the recovery signer
|
|
179
|
+
const result4 = await (recoverySigner as SignerReady).handle()
|
|
180
|
+
expect(result4).toBeDefined()
|
|
181
|
+
expect(result4).toBeTruthy()
|
|
182
|
+
|
|
183
|
+
// Complete the transaction
|
|
184
|
+
const txHash = await manager.relayTransaction(requestId4)
|
|
185
|
+
expect(txHash).toBeDefined()
|
|
186
|
+
|
|
187
|
+
// The balance of the wallet should be 0 wei
|
|
188
|
+
const balance = await provider.request({
|
|
189
|
+
method: 'eth_getBalance',
|
|
190
|
+
params: [wallet!, 'latest'],
|
|
191
|
+
})
|
|
192
|
+
expect(balance).toBeDefined()
|
|
193
|
+
expect(balance).toBe('0x0')
|
|
194
|
+
|
|
195
|
+
// Refresh the queued recovery payloads, the executed one
|
|
196
|
+
// should be removed
|
|
197
|
+
await manager.updateQueuedRecoveryPayloads()
|
|
198
|
+
const recoveryPayloads2 = await new Promise<QueuedRecoveryPayload[]>((resolve) => {
|
|
199
|
+
const unsubscribe = manager.onQueuedRecoveryPayloadsUpdate(
|
|
200
|
+
wallet!,
|
|
201
|
+
(payloads) => {
|
|
202
|
+
unsubscribe()
|
|
203
|
+
resolve(payloads)
|
|
204
|
+
},
|
|
205
|
+
true,
|
|
206
|
+
)
|
|
207
|
+
})
|
|
208
|
+
expect(recoveryPayloads2).toBeDefined()
|
|
209
|
+
expect(recoveryPayloads2.length).toBe(0)
|
|
210
|
+
}, 30000)
|
|
211
|
+
})
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { AbiFunction, Address, Bytes, Hex, Mnemonic, Provider, RpcTransport } from 'ox'
|
|
2
|
+
import { beforeEach, describe, it, vi } from 'vitest'
|
|
3
|
+
import { Signers as CoreSigners, Wallet as CoreWallet, Envelope, Relayer, State } from '../../core/src/index.js'
|
|
4
|
+
import { Attestation, Constants, Payload, Permission } from '../../primitives/src/index.js'
|
|
5
|
+
import { Sequence } from '../src/index.js'
|
|
6
|
+
import { CAN_RUN_LIVE, EMITTER_ABI, EMITTER_ADDRESS, PRIVATE_KEY, RPC_URL } from './constants'
|
|
7
|
+
|
|
8
|
+
describe('Sessions (via Manager)', () => {
|
|
9
|
+
// Shared components
|
|
10
|
+
let provider: Provider.Provider
|
|
11
|
+
let chainId: bigint
|
|
12
|
+
let stateProvider: State.Provider
|
|
13
|
+
|
|
14
|
+
// Wallet webapp components
|
|
15
|
+
let wdk: {
|
|
16
|
+
identitySignerAddress: Address.Address
|
|
17
|
+
manager: Sequence.Manager
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Dapp components
|
|
21
|
+
let dapp: {
|
|
22
|
+
pkStore: CoreSigners.Pk.Encrypted.EncryptedPksDb
|
|
23
|
+
wallet: CoreWallet
|
|
24
|
+
sessionManager: CoreSigners.SessionManager
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
// Create provider or use arbitrum sepolia
|
|
29
|
+
if (RPC_URL) {
|
|
30
|
+
provider = Provider.from(
|
|
31
|
+
RpcTransport.fromHttp(RPC_URL, {
|
|
32
|
+
fetchOptions: {
|
|
33
|
+
headers: {
|
|
34
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
chainId = BigInt(await provider.request({ method: 'eth_chainId' }))
|
|
40
|
+
} else {
|
|
41
|
+
provider = vi.mocked<Provider.Provider>({
|
|
42
|
+
request: vi.fn(),
|
|
43
|
+
on: vi.fn(),
|
|
44
|
+
removeListener: vi.fn(),
|
|
45
|
+
})
|
|
46
|
+
chainId = 1n
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create state provider
|
|
50
|
+
stateProvider = new State.Local.Provider()
|
|
51
|
+
|
|
52
|
+
// Create manager
|
|
53
|
+
const opts = Sequence.applyManagerOptionsDefaults({
|
|
54
|
+
stateProvider,
|
|
55
|
+
relayers: [], // No relayers needed for testing
|
|
56
|
+
networks: [
|
|
57
|
+
{
|
|
58
|
+
chainId,
|
|
59
|
+
rpc: RPC_URL ?? 'XXX',
|
|
60
|
+
name: 'XXX',
|
|
61
|
+
explorer: 'XXX',
|
|
62
|
+
nativeCurrency: {
|
|
63
|
+
name: 'Ether',
|
|
64
|
+
symbol: 'ETH',
|
|
65
|
+
decimals: 18,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Create manager
|
|
72
|
+
const manager = new Sequence.Manager(opts)
|
|
73
|
+
|
|
74
|
+
// Use a mnemonic to create the wallet
|
|
75
|
+
const identitySignerMnemonic = Mnemonic.random(Mnemonic.english)
|
|
76
|
+
const identitySignerPk = Mnemonic.toPrivateKey(identitySignerMnemonic, { as: 'Hex' })
|
|
77
|
+
const identitySignerAddress = new CoreSigners.Pk.Pk(identitySignerPk).address
|
|
78
|
+
const walletAddress = await manager.signUp({
|
|
79
|
+
kind: 'mnemonic',
|
|
80
|
+
mnemonic: identitySignerMnemonic,
|
|
81
|
+
noGuard: true,
|
|
82
|
+
noSessionManager: false,
|
|
83
|
+
})
|
|
84
|
+
if (!walletAddress) {
|
|
85
|
+
throw new Error('Failed to create wallet')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Initialize the wdk components
|
|
89
|
+
wdk = {
|
|
90
|
+
identitySignerAddress,
|
|
91
|
+
manager,
|
|
92
|
+
}
|
|
93
|
+
manager.registerMnemonicUI(async (respond) => {
|
|
94
|
+
await respond(identitySignerMnemonic)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Create the pk store and pk
|
|
98
|
+
const pkStore = new CoreSigners.Pk.Encrypted.EncryptedPksDb()
|
|
99
|
+
|
|
100
|
+
// Create wallet in core
|
|
101
|
+
const coreWallet = new CoreWallet(walletAddress, {
|
|
102
|
+
context: opts.context,
|
|
103
|
+
guest: opts.guest,
|
|
104
|
+
// Share the state provider with wdk. In practice this will be the key machine.
|
|
105
|
+
stateProvider,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
dapp = {
|
|
109
|
+
pkStore,
|
|
110
|
+
wallet: coreWallet,
|
|
111
|
+
sessionManager: new CoreSigners.SessionManager(coreWallet, {
|
|
112
|
+
provider,
|
|
113
|
+
}),
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const signAndSend = async (call: Payload.Call) => {
|
|
118
|
+
const envelope = await dapp.wallet.prepareTransaction(provider, [call], { noConfigUpdate: true })
|
|
119
|
+
const parentedEnvelope: Payload.Parented = {
|
|
120
|
+
...envelope.payload,
|
|
121
|
+
parentWallets: [dapp.wallet.address],
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Sign the envelope
|
|
125
|
+
const sessionImageHash = await dapp.sessionManager.imageHash
|
|
126
|
+
if (!sessionImageHash) {
|
|
127
|
+
throw new Error('Session image hash not found')
|
|
128
|
+
}
|
|
129
|
+
const signature = await dapp.sessionManager.signSapient(
|
|
130
|
+
dapp.wallet.address,
|
|
131
|
+
chainId ?? 1n,
|
|
132
|
+
parentedEnvelope,
|
|
133
|
+
sessionImageHash,
|
|
134
|
+
)
|
|
135
|
+
const sapientSignature: Envelope.SapientSignature = {
|
|
136
|
+
imageHash: sessionImageHash,
|
|
137
|
+
signature,
|
|
138
|
+
}
|
|
139
|
+
const signedEnvelope = Envelope.toSigned(envelope, [sapientSignature])
|
|
140
|
+
|
|
141
|
+
// Build the transaction
|
|
142
|
+
const transaction = await dapp.wallet.buildTransaction(provider, signedEnvelope)
|
|
143
|
+
console.log('tx', transaction)
|
|
144
|
+
|
|
145
|
+
// Send the transaction
|
|
146
|
+
if (CAN_RUN_LIVE && PRIVATE_KEY) {
|
|
147
|
+
// Load the sender
|
|
148
|
+
const senderPk = Hex.from(PRIVATE_KEY as `0x${string}`)
|
|
149
|
+
const pkRelayer = new Relayer.Pk.PkRelayer(senderPk, provider)
|
|
150
|
+
const tx = await pkRelayer.relay(transaction.to, transaction.data, chainId, undefined)
|
|
151
|
+
console.log('Transaction sent', tx)
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
153
|
+
const receipt = await provider.request({ method: 'eth_getTransactionReceipt', params: [tx.opHash] })
|
|
154
|
+
console.log('Transaction receipt', receipt)
|
|
155
|
+
return tx.opHash
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
it(
|
|
160
|
+
'should create and sign with an explicit session',
|
|
161
|
+
async () => {
|
|
162
|
+
// Create the explicit session signer
|
|
163
|
+
const e = await dapp.pkStore.generateAndStore()
|
|
164
|
+
const s = await dapp.pkStore.getEncryptedPkStore(e.address)
|
|
165
|
+
if (!s) {
|
|
166
|
+
throw new Error('Failed to create pk store')
|
|
167
|
+
}
|
|
168
|
+
const permission: Permission.SessionPermissions = {
|
|
169
|
+
signer: e.address,
|
|
170
|
+
valueLimit: 0n,
|
|
171
|
+
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now
|
|
172
|
+
permissions: [
|
|
173
|
+
{
|
|
174
|
+
target: EMITTER_ADDRESS,
|
|
175
|
+
rules: [
|
|
176
|
+
{
|
|
177
|
+
// Require the explicitEmit selector
|
|
178
|
+
cumulative: false,
|
|
179
|
+
operation: Permission.ParameterOperation.EQUAL,
|
|
180
|
+
value: Bytes.padRight(Bytes.fromHex(AbiFunction.getSelector(EMITTER_ABI[0])), 32),
|
|
181
|
+
offset: 0n,
|
|
182
|
+
mask: Bytes.padRight(Bytes.fromHex('0xffffffff'), 32),
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
}
|
|
188
|
+
const explicitSigner = new CoreSigners.Session.Explicit(s, permission)
|
|
189
|
+
// Add to manager
|
|
190
|
+
dapp.sessionManager = dapp.sessionManager.withExplicitSigner(explicitSigner)
|
|
191
|
+
|
|
192
|
+
// Request the session permissions from the WDK
|
|
193
|
+
const requestId = await wdk.manager.addExplicitSession(dapp.wallet.address, explicitSigner.address, permission)
|
|
194
|
+
|
|
195
|
+
// Sign and complete the request
|
|
196
|
+
const sigRequest = await wdk.manager.getSignatureRequest(requestId)
|
|
197
|
+
const identitySigner = sigRequest.signers.find((s) => s.address === wdk.identitySignerAddress)
|
|
198
|
+
if (!identitySigner || (identitySigner.status !== 'actionable' && identitySigner.status !== 'ready')) {
|
|
199
|
+
throw new Error(`Identity signer not found or not ready/actionable: ${identitySigner?.status}`)
|
|
200
|
+
}
|
|
201
|
+
const handled = await identitySigner.handle()
|
|
202
|
+
if (!handled) {
|
|
203
|
+
throw new Error('Failed to handle identity signer')
|
|
204
|
+
}
|
|
205
|
+
await wdk.manager.completeSessionUpdate(requestId)
|
|
206
|
+
|
|
207
|
+
// Create a call payload
|
|
208
|
+
const call: Payload.Call = {
|
|
209
|
+
to: EMITTER_ADDRESS,
|
|
210
|
+
value: 0n,
|
|
211
|
+
data: AbiFunction.encodeData(EMITTER_ABI[0]),
|
|
212
|
+
gasLimit: 0n,
|
|
213
|
+
delegateCall: false,
|
|
214
|
+
onlyFallback: false,
|
|
215
|
+
behaviorOnError: 'revert',
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!RPC_URL) {
|
|
219
|
+
// Configure mock provider
|
|
220
|
+
;(provider as any).request.mockImplementation(({ method, params }) => {
|
|
221
|
+
if (method === 'eth_chainId') {
|
|
222
|
+
return Promise.resolve(chainId.toString())
|
|
223
|
+
}
|
|
224
|
+
if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) {
|
|
225
|
+
// Undeployed wallet
|
|
226
|
+
return Promise.resolve('0x')
|
|
227
|
+
}
|
|
228
|
+
if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) {
|
|
229
|
+
// Nonce is 0
|
|
230
|
+
return Promise.resolve('0x00')
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Sign and send the transaction
|
|
236
|
+
await signAndSend(call)
|
|
237
|
+
},
|
|
238
|
+
PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
it(
|
|
242
|
+
'should create and sign with an implicit session',
|
|
243
|
+
async () => {
|
|
244
|
+
// Create the implicit session signer
|
|
245
|
+
const e = await dapp.pkStore.generateAndStore()
|
|
246
|
+
const s = await dapp.pkStore.getEncryptedPkStore(e.address)
|
|
247
|
+
if (!s) {
|
|
248
|
+
throw new Error('Failed to create pk store')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Request the session authorization from the WDK
|
|
252
|
+
const requestId = await wdk.manager.prepareAuthorizeImplicitSession(dapp.wallet.address, e.address, {
|
|
253
|
+
target: 'https://example.com',
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// Sign the request (Wallet UI action)
|
|
257
|
+
const sigRequest = await wdk.manager.getSignatureRequest(requestId)
|
|
258
|
+
const identitySigner = sigRequest.signers[0]
|
|
259
|
+
if (!identitySigner || (identitySigner.status !== 'actionable' && identitySigner.status !== 'ready')) {
|
|
260
|
+
throw new Error(`Identity signer not found or not ready/actionable: ${identitySigner?.status}`)
|
|
261
|
+
}
|
|
262
|
+
const handled = await identitySigner.handle()
|
|
263
|
+
if (!handled) {
|
|
264
|
+
throw new Error('Failed to handle identity signer')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Complete the request
|
|
268
|
+
const { attestation, signature: identitySignature } =
|
|
269
|
+
await wdk.manager.completeAuthorizeImplicitSession(requestId)
|
|
270
|
+
|
|
271
|
+
// Load the implicit signer
|
|
272
|
+
const implicitSigner = new CoreSigners.Session.Implicit(
|
|
273
|
+
s,
|
|
274
|
+
attestation,
|
|
275
|
+
identitySignature,
|
|
276
|
+
dapp.sessionManager.address,
|
|
277
|
+
)
|
|
278
|
+
dapp.sessionManager = dapp.sessionManager.withImplicitSigner(implicitSigner)
|
|
279
|
+
|
|
280
|
+
// Create a call payload
|
|
281
|
+
const call: Payload.Call = {
|
|
282
|
+
to: EMITTER_ADDRESS,
|
|
283
|
+
value: 0n,
|
|
284
|
+
data: AbiFunction.encodeData(EMITTER_ABI[1]), // implicitEmit
|
|
285
|
+
gasLimit: 0n,
|
|
286
|
+
delegateCall: false,
|
|
287
|
+
onlyFallback: false,
|
|
288
|
+
behaviorOnError: 'revert',
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!RPC_URL) {
|
|
292
|
+
// Configure mock provider
|
|
293
|
+
;(provider as any).request.mockImplementation(({ method, params }) => {
|
|
294
|
+
if (method === 'eth_chainId') {
|
|
295
|
+
return Promise.resolve(chainId.toString())
|
|
296
|
+
}
|
|
297
|
+
if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.GET_IMPLEMENTATION)) {
|
|
298
|
+
// Undeployed wallet
|
|
299
|
+
return Promise.resolve('0x')
|
|
300
|
+
}
|
|
301
|
+
if (method === 'eth_call' && params[0].data === AbiFunction.encodeData(Constants.READ_NONCE, [0n])) {
|
|
302
|
+
// Nonce is 0
|
|
303
|
+
return Promise.resolve('0x00')
|
|
304
|
+
}
|
|
305
|
+
if (
|
|
306
|
+
method === 'eth_call' &&
|
|
307
|
+
Address.isEqual(params[0].from, dapp.sessionManager.address) &&
|
|
308
|
+
Address.isEqual(params[0].to, call.to)
|
|
309
|
+
) {
|
|
310
|
+
// Implicit request simulation result
|
|
311
|
+
const expectedResult = Bytes.toHex(
|
|
312
|
+
Attestation.generateImplicitRequestMagic(attestation, dapp.wallet.address),
|
|
313
|
+
)
|
|
314
|
+
return Promise.resolve(expectedResult)
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Sign and send the transaction
|
|
320
|
+
await signAndSend(call)
|
|
321
|
+
},
|
|
322
|
+
PRIVATE_KEY || RPC_URL ? { timeout: 60000 } : undefined,
|
|
323
|
+
)
|
|
324
|
+
})
|
package/test/setup.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { indexedDB, IDBFactory } from 'fake-indexeddb'
|
|
2
|
+
import { Address, Bytes, Hex, Provider, RpcTransport } from 'ox'
|
|
3
|
+
import { vi } from 'vitest'
|
|
4
|
+
import { LOCAL_RPC_URL } from './constants'
|
|
5
|
+
|
|
6
|
+
// Add IndexedDB support to the test environment
|
|
7
|
+
global.indexedDB = indexedDB
|
|
8
|
+
global.IDBFactory = IDBFactory
|
|
9
|
+
|
|
10
|
+
// Mock navigator.locks API for Node.js environment ---
|
|
11
|
+
|
|
12
|
+
// 1. Ensure the global navigator object exists
|
|
13
|
+
if (typeof global.navigator === 'undefined') {
|
|
14
|
+
console.log('mocking navigator')
|
|
15
|
+
global.navigator = {} as Navigator
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 2. Define or redefine the 'locks' property on navigator
|
|
19
|
+
// Check if 'locks' is falsy (null or undefined), OR if it's an object
|
|
20
|
+
// that doesn't have the 'request' property we expect in our mock.
|
|
21
|
+
if (!global.navigator.locks || !('request' in global.navigator.locks)) {
|
|
22
|
+
Object.defineProperty(global.navigator, 'locks', {
|
|
23
|
+
// The value of the 'locks' property will be our mock object
|
|
24
|
+
value: {
|
|
25
|
+
// Mock the 'request' method
|
|
26
|
+
request: vi
|
|
27
|
+
.fn()
|
|
28
|
+
.mockImplementation(async (name: string, callback: (lock: { name: string } | null) => Promise<any>) => {
|
|
29
|
+
// Simulate acquiring the lock immediately in the test environment.
|
|
30
|
+
const mockLock = { name } // A minimal mock lock object
|
|
31
|
+
try {
|
|
32
|
+
// Execute the callback provided to navigator.locks.request
|
|
33
|
+
const result = await callback(mockLock)
|
|
34
|
+
return result // Return the result of the callback
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Log errors from the callback for better debugging in tests
|
|
37
|
+
console.error(`Error occurred within mocked lock callback for lock "${name}":`, e)
|
|
38
|
+
throw e // Re-throw the error so the test potentially fails
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
// Mock the 'query' method
|
|
42
|
+
query: vi.fn().mockResolvedValue({ held: [], pending: [] }),
|
|
43
|
+
},
|
|
44
|
+
writable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
enumerable: true,
|
|
47
|
+
})
|
|
48
|
+
} else {
|
|
49
|
+
console.log('navigator.locks already exists and appears to have a "request" property.')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function mockEthereum() {
|
|
53
|
+
// Add window.ethereum support, pointing to the the Anvil local RPC
|
|
54
|
+
if (typeof (window as any).ethereum === 'undefined') {
|
|
55
|
+
;(window as any).ethereum = {
|
|
56
|
+
request: vi.fn().mockImplementation(async (args: any) => {
|
|
57
|
+
// Pipe the request to the Anvil local RPC
|
|
58
|
+
const provider = Provider.from(RpcTransport.fromHttp(LOCAL_RPC_URL))
|
|
59
|
+
return provider.request(args)
|
|
60
|
+
}),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|