@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,632 @@
|
|
|
1
|
+
import { Wallet as CoreWallet, Envelope, Signers, State } from '@0xsequence/wallet-core';
|
|
2
|
+
import { Config, GenericTree, Payload, SessionConfig } from '@0xsequence/wallet-primitives';
|
|
3
|
+
import { Address, Provider, RpcTransport } from 'ox';
|
|
4
|
+
import { MnemonicHandler } from './handlers/mnemonic.js';
|
|
5
|
+
import { Kinds } from './types/signer.js';
|
|
6
|
+
export function isLoginToWalletArgs(args) {
|
|
7
|
+
return 'wallet' in args;
|
|
8
|
+
}
|
|
9
|
+
export function isLoginToMnemonicArgs(args) {
|
|
10
|
+
return 'kind' in args && args.kind === 'mnemonic';
|
|
11
|
+
}
|
|
12
|
+
export function isLoginToPasskeyArgs(args) {
|
|
13
|
+
return 'kind' in args && args.kind === 'passkey';
|
|
14
|
+
}
|
|
15
|
+
export function isAuthCodeArgs(args) {
|
|
16
|
+
return 'kind' in args && (args.kind === 'google-pkce' || args.kind === 'apple');
|
|
17
|
+
}
|
|
18
|
+
function buildCappedTree(members) {
|
|
19
|
+
const loginMemberWeight = 1n;
|
|
20
|
+
if (members.length === 0) {
|
|
21
|
+
// We need to maintain the general structure of the tree, so we can't have an empty node here
|
|
22
|
+
// instead, we add a dummy signer with weight 0
|
|
23
|
+
return {
|
|
24
|
+
type: 'signer',
|
|
25
|
+
address: '0x0000000000000000000000000000000000000000',
|
|
26
|
+
weight: 0n,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (members.length === 1) {
|
|
30
|
+
if (members[0].imageHash) {
|
|
31
|
+
return {
|
|
32
|
+
type: 'sapient-signer',
|
|
33
|
+
address: members[0].address,
|
|
34
|
+
imageHash: members[0].imageHash,
|
|
35
|
+
weight: loginMemberWeight,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
return {
|
|
40
|
+
type: 'signer',
|
|
41
|
+
address: members[0].address,
|
|
42
|
+
weight: loginMemberWeight,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
type: 'nested',
|
|
48
|
+
weight: loginMemberWeight,
|
|
49
|
+
threshold: 1n,
|
|
50
|
+
tree: Config.flatLeavesToTopology(members.map((member) => member.imageHash
|
|
51
|
+
? {
|
|
52
|
+
type: 'sapient-signer',
|
|
53
|
+
address: member.address,
|
|
54
|
+
imageHash: member.imageHash,
|
|
55
|
+
weight: 1n,
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
type: 'signer',
|
|
59
|
+
address: member.address,
|
|
60
|
+
weight: 1n,
|
|
61
|
+
})),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function buildCappedTreeFromTopology(weight, topology) {
|
|
65
|
+
// We may optimize this for some topology types
|
|
66
|
+
// but it is not worth it, because the topology
|
|
67
|
+
// that we will use for prod won't be optimizable
|
|
68
|
+
return {
|
|
69
|
+
type: 'nested',
|
|
70
|
+
weight: weight,
|
|
71
|
+
threshold: weight,
|
|
72
|
+
tree: topology,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function toConfig(checkpoint, loginTopology, devicesTopology, modules, guardTopology) {
|
|
76
|
+
if (!guardTopology) {
|
|
77
|
+
return {
|
|
78
|
+
checkpoint: checkpoint,
|
|
79
|
+
threshold: 1n,
|
|
80
|
+
topology: [[loginTopology, devicesTopology], toModulesTopology(modules)],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
return {
|
|
85
|
+
checkpoint: checkpoint,
|
|
86
|
+
threshold: 2n,
|
|
87
|
+
topology: [[[loginTopology, devicesTopology], guardTopology], toModulesTopology(modules)],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function toModulesTopology(modules) {
|
|
92
|
+
// We always include a modules topology, even if there are no modules
|
|
93
|
+
// in that case we just add a signer with address 0 and no weight
|
|
94
|
+
if (modules.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
type: 'signer',
|
|
97
|
+
address: '0x0000000000000000000000000000000000000000',
|
|
98
|
+
weight: 0n,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return Config.flatLeavesToTopology(modules);
|
|
102
|
+
}
|
|
103
|
+
function fromModulesTopology(topology) {
|
|
104
|
+
let modules = [];
|
|
105
|
+
if (Config.isNode(topology)) {
|
|
106
|
+
modules = [...fromModulesTopology(topology[0]), ...fromModulesTopology(topology[1])];
|
|
107
|
+
}
|
|
108
|
+
else if (Config.isSapientSignerLeaf(topology)) {
|
|
109
|
+
modules.push(topology);
|
|
110
|
+
}
|
|
111
|
+
else if (Config.isSignerLeaf(topology)) {
|
|
112
|
+
// This signals that the wallet has no modules, so we just ignore it
|
|
113
|
+
if (topology.address !== '0x0000000000000000000000000000000000000000') {
|
|
114
|
+
throw new Error('signer-leaf-not-allowed-in-modules-topology');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
throw new Error('unknown-modules-topology-format');
|
|
119
|
+
}
|
|
120
|
+
return modules;
|
|
121
|
+
}
|
|
122
|
+
function fromConfig(config) {
|
|
123
|
+
if (config.threshold === 1n) {
|
|
124
|
+
if (Config.isNode(config.topology) && Config.isNode(config.topology[0])) {
|
|
125
|
+
return {
|
|
126
|
+
loginTopology: config.topology[0][0],
|
|
127
|
+
devicesTopology: config.topology[0][1],
|
|
128
|
+
modules: fromModulesTopology(config.topology[1]),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
throw new Error('unknown-config-format');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (config.threshold === 2n) {
|
|
136
|
+
if (Config.isNode(config.topology) && Config.isNode(config.topology[0]) && Config.isNode(config.topology[0][0])) {
|
|
137
|
+
return {
|
|
138
|
+
loginTopology: config.topology[0][0][0],
|
|
139
|
+
devicesTopology: config.topology[0][0][1],
|
|
140
|
+
guardTopology: config.topology[0][1],
|
|
141
|
+
modules: fromModulesTopology(config.topology[1]),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
throw new Error('unknown-config-format');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new Error('unknown-config-format');
|
|
149
|
+
}
|
|
150
|
+
export class Wallets {
|
|
151
|
+
shared;
|
|
152
|
+
walletSelectionUiHandler = null;
|
|
153
|
+
constructor(shared) {
|
|
154
|
+
this.shared = shared;
|
|
155
|
+
}
|
|
156
|
+
async exists(wallet) {
|
|
157
|
+
return this.shared.databases.manager.get(wallet).then((r) => r !== undefined);
|
|
158
|
+
}
|
|
159
|
+
async get(walletAddress) {
|
|
160
|
+
return await this.shared.databases.manager.get(walletAddress);
|
|
161
|
+
}
|
|
162
|
+
async list() {
|
|
163
|
+
return this.shared.databases.manager.list();
|
|
164
|
+
}
|
|
165
|
+
registerWalletSelector(handler) {
|
|
166
|
+
if (this.walletSelectionUiHandler) {
|
|
167
|
+
throw new Error('wallet-selector-already-registered');
|
|
168
|
+
}
|
|
169
|
+
this.walletSelectionUiHandler = handler;
|
|
170
|
+
return () => {
|
|
171
|
+
this.unregisterWalletSelector(handler);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
unregisterWalletSelector(handler) {
|
|
175
|
+
if (handler && this.walletSelectionUiHandler !== handler) {
|
|
176
|
+
throw new Error('wallet-selector-not-registered');
|
|
177
|
+
}
|
|
178
|
+
this.walletSelectionUiHandler = null;
|
|
179
|
+
}
|
|
180
|
+
onWalletsUpdate(cb, trigger) {
|
|
181
|
+
const undo = this.shared.databases.manager.addListener(() => {
|
|
182
|
+
this.list().then((wallets) => {
|
|
183
|
+
cb(wallets);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
if (trigger) {
|
|
187
|
+
this.list().then((wallets) => {
|
|
188
|
+
cb(wallets);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return undo;
|
|
192
|
+
}
|
|
193
|
+
async prepareSignUp(args) {
|
|
194
|
+
switch (args.kind) {
|
|
195
|
+
case 'passkey':
|
|
196
|
+
const passkeySigner = await Signers.Passkey.Passkey.create(this.shared.sequence.extensions, {
|
|
197
|
+
stateProvider: this.shared.sequence.stateProvider,
|
|
198
|
+
});
|
|
199
|
+
this.shared.modules.logger.log('Created new passkey signer:', passkeySigner.address);
|
|
200
|
+
return {
|
|
201
|
+
signer: passkeySigner,
|
|
202
|
+
extra: {
|
|
203
|
+
signerKind: Kinds.LoginPasskey,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
case 'mnemonic':
|
|
207
|
+
const mnemonicSigner = MnemonicHandler.toSigner(args.mnemonic);
|
|
208
|
+
if (!mnemonicSigner) {
|
|
209
|
+
throw new Error('invalid-mnemonic');
|
|
210
|
+
}
|
|
211
|
+
this.shared.modules.logger.log('Created new mnemonic signer:', mnemonicSigner.address);
|
|
212
|
+
return {
|
|
213
|
+
signer: mnemonicSigner,
|
|
214
|
+
extra: {
|
|
215
|
+
signerKind: Kinds.LoginMnemonic,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
case 'email-otp': {
|
|
219
|
+
const handler = this.shared.handlers.get(Kinds.LoginEmailOtp);
|
|
220
|
+
if (!handler) {
|
|
221
|
+
throw new Error('email-otp-handler-not-registered');
|
|
222
|
+
}
|
|
223
|
+
const signer = await handler.getSigner(args.email);
|
|
224
|
+
this.shared.modules.logger.log('Created new email otp signer:', signer.address);
|
|
225
|
+
return {
|
|
226
|
+
signer,
|
|
227
|
+
extra: {
|
|
228
|
+
signerKind: Kinds.LoginEmailOtp,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
case 'google-pkce':
|
|
233
|
+
case 'apple': {
|
|
234
|
+
const handler = this.shared.handlers.get('login-' + args.kind);
|
|
235
|
+
if (!handler) {
|
|
236
|
+
throw new Error('handler-not-registered');
|
|
237
|
+
}
|
|
238
|
+
const [signer, metadata] = await handler.completeAuth(args.commitment, args.code);
|
|
239
|
+
this.shared.modules.logger.log('Created new auth code pkce signer:', signer.address);
|
|
240
|
+
return {
|
|
241
|
+
signer,
|
|
242
|
+
extra: {
|
|
243
|
+
signerKind: 'login-' + args.kind,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async startSignUpWithRedirect(args) {
|
|
250
|
+
const handler = this.shared.handlers.get('login-' + args.kind);
|
|
251
|
+
if (!handler) {
|
|
252
|
+
throw new Error('handler-not-registered');
|
|
253
|
+
}
|
|
254
|
+
return handler.commitAuth(args.target, true);
|
|
255
|
+
}
|
|
256
|
+
async completeRedirect(args) {
|
|
257
|
+
const commitment = await this.shared.databases.authCommitments.get(args.state);
|
|
258
|
+
if (!commitment) {
|
|
259
|
+
throw new Error('invalid-state');
|
|
260
|
+
}
|
|
261
|
+
if (commitment.isSignUp) {
|
|
262
|
+
await this.signUp({
|
|
263
|
+
kind: commitment.kind,
|
|
264
|
+
commitment,
|
|
265
|
+
code: args.code,
|
|
266
|
+
noGuard: args.noGuard,
|
|
267
|
+
target: commitment.target,
|
|
268
|
+
isRedirect: true,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const handler = this.shared.handlers.get('login-' + commitment.kind);
|
|
273
|
+
if (!handler) {
|
|
274
|
+
throw new Error('handler-not-registered');
|
|
275
|
+
}
|
|
276
|
+
await handler.completeAuth(commitment, args.code);
|
|
277
|
+
}
|
|
278
|
+
return commitment.target;
|
|
279
|
+
}
|
|
280
|
+
async signUp(args) {
|
|
281
|
+
const loginSigner = await this.prepareSignUp(args);
|
|
282
|
+
// If there is an existing wallet callback, we check if any wallet already exist for this login signer
|
|
283
|
+
if (this.walletSelectionUiHandler) {
|
|
284
|
+
const existingWallets = await State.getWalletsFor(this.shared.sequence.stateProvider, loginSigner.signer);
|
|
285
|
+
if (existingWallets.length > 0) {
|
|
286
|
+
const result = await this.walletSelectionUiHandler({
|
|
287
|
+
existingWallets: existingWallets.map((w) => w.wallet),
|
|
288
|
+
signerAddress: await loginSigner.signer.address,
|
|
289
|
+
context: isAuthCodeArgs(args)
|
|
290
|
+
? {
|
|
291
|
+
isRedirect: args.isRedirect,
|
|
292
|
+
target: args.target,
|
|
293
|
+
}
|
|
294
|
+
: {
|
|
295
|
+
isRedirect: false,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
if (result) {
|
|
299
|
+
// A wallet was selected, we can exit early
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
console.warn('No wallet selector registered, creating a new wallet');
|
|
306
|
+
}
|
|
307
|
+
// Create the first session
|
|
308
|
+
const device = await this.shared.modules.devices.create();
|
|
309
|
+
if (!args.noGuard && !this.shared.sequence.defaultGuardTopology) {
|
|
310
|
+
throw new Error('guard is required for signup');
|
|
311
|
+
}
|
|
312
|
+
// Build the login tree
|
|
313
|
+
const loginSignerAddress = await loginSigner.signer.address;
|
|
314
|
+
const loginTopology = buildCappedTree([
|
|
315
|
+
{
|
|
316
|
+
address: loginSignerAddress,
|
|
317
|
+
imageHash: Signers.isSapientSigner(loginSigner.signer) ? await loginSigner.signer.imageHash : undefined,
|
|
318
|
+
},
|
|
319
|
+
]);
|
|
320
|
+
const devicesTopology = buildCappedTree([{ address: device.address }]);
|
|
321
|
+
const guardTopology = args.noGuard
|
|
322
|
+
? undefined
|
|
323
|
+
: buildCappedTreeFromTopology(1n, this.shared.sequence.defaultGuardTopology);
|
|
324
|
+
// TODO: Add recovery module
|
|
325
|
+
// TODO: Add smart sessions module
|
|
326
|
+
// Placeholder
|
|
327
|
+
let modules = [];
|
|
328
|
+
if (!args.noSessionManager) {
|
|
329
|
+
// Calculate image hash with the identity signer
|
|
330
|
+
const sessionsTopology = SessionConfig.emptySessionsTopology(loginSignerAddress);
|
|
331
|
+
// Store this tree in the state provider
|
|
332
|
+
const sessionsConfigTree = SessionConfig.sessionsTopologyToConfigurationTree(sessionsTopology);
|
|
333
|
+
this.shared.sequence.stateProvider.saveTree(sessionsConfigTree);
|
|
334
|
+
// Prepare the configuration leaf
|
|
335
|
+
const sessionsImageHash = GenericTree.hash(sessionsConfigTree);
|
|
336
|
+
modules.push({
|
|
337
|
+
...this.shared.sequence.defaultSessionsTopology,
|
|
338
|
+
imageHash: sessionsImageHash,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (!args.noRecovery) {
|
|
342
|
+
await this.shared.modules.recovery.initRecoveryModule(modules, device.address);
|
|
343
|
+
}
|
|
344
|
+
// Create initial configuration
|
|
345
|
+
const initialConfiguration = toConfig(0n, loginTopology, devicesTopology, modules, guardTopology);
|
|
346
|
+
console.log('initialConfiguration', initialConfiguration);
|
|
347
|
+
// Create wallet
|
|
348
|
+
const wallet = await CoreWallet.fromConfiguration(initialConfiguration, {
|
|
349
|
+
context: this.shared.sequence.context,
|
|
350
|
+
stateProvider: this.shared.sequence.stateProvider,
|
|
351
|
+
guest: this.shared.sequence.guest,
|
|
352
|
+
});
|
|
353
|
+
this.shared.modules.logger.log('Created new sequence wallet:', wallet.address);
|
|
354
|
+
// Sign witness using device signer
|
|
355
|
+
await this.shared.modules.devices.witness(device.address, wallet.address);
|
|
356
|
+
// Sign witness using the passkey signer
|
|
357
|
+
await loginSigner.signer.witness(this.shared.sequence.stateProvider, wallet.address, loginSigner.extra);
|
|
358
|
+
// Save entry in the manager db
|
|
359
|
+
await this.shared.databases.manager.set({
|
|
360
|
+
address: wallet.address,
|
|
361
|
+
status: 'ready',
|
|
362
|
+
loginDate: new Date().toISOString(),
|
|
363
|
+
device: device.address,
|
|
364
|
+
loginType: loginSigner.extra.signerKind,
|
|
365
|
+
useGuard: !args.noGuard,
|
|
366
|
+
});
|
|
367
|
+
return wallet.address;
|
|
368
|
+
}
|
|
369
|
+
async getConfigurationParts(address) {
|
|
370
|
+
const wallet = new CoreWallet(address, {
|
|
371
|
+
context: this.shared.sequence.context,
|
|
372
|
+
stateProvider: this.shared.sequence.stateProvider,
|
|
373
|
+
guest: this.shared.sequence.guest,
|
|
374
|
+
});
|
|
375
|
+
const status = await wallet.getStatus();
|
|
376
|
+
return fromConfig(status.configuration);
|
|
377
|
+
}
|
|
378
|
+
async requestConfigurationUpdate(address, changes, action, origin) {
|
|
379
|
+
const wallet = new CoreWallet(address, {
|
|
380
|
+
context: this.shared.sequence.context,
|
|
381
|
+
stateProvider: this.shared.sequence.stateProvider,
|
|
382
|
+
guest: this.shared.sequence.guest,
|
|
383
|
+
});
|
|
384
|
+
const status = await wallet.getStatus();
|
|
385
|
+
const { loginTopology, devicesTopology, modules, guardTopology } = fromConfig(status.configuration);
|
|
386
|
+
const nextLoginTopology = changes.loginTopology ?? loginTopology;
|
|
387
|
+
const nextDevicesTopology = changes.devicesTopology ?? devicesTopology;
|
|
388
|
+
const nextModules = changes.modules ?? modules;
|
|
389
|
+
const nextGuardTopology = changes.guardTopology ?? guardTopology;
|
|
390
|
+
const envelope = await wallet.prepareUpdate(toConfig(status.configuration.checkpoint + 1n, nextLoginTopology, nextDevicesTopology, nextModules, nextGuardTopology));
|
|
391
|
+
const requestId = await this.shared.modules.signatures.request(envelope, action, {
|
|
392
|
+
origin,
|
|
393
|
+
});
|
|
394
|
+
return requestId;
|
|
395
|
+
}
|
|
396
|
+
async completeConfigurationUpdate(requestId) {
|
|
397
|
+
const request = await this.shared.modules.signatures.get(requestId);
|
|
398
|
+
if (!Payload.isConfigUpdate(request.envelope.payload)) {
|
|
399
|
+
throw new Error('invalid-request-payload');
|
|
400
|
+
}
|
|
401
|
+
if (!Envelope.reachedThreshold(request.envelope)) {
|
|
402
|
+
throw new Error('insufficient-weight');
|
|
403
|
+
}
|
|
404
|
+
const wallet = new CoreWallet(request.wallet, {
|
|
405
|
+
context: this.shared.sequence.context,
|
|
406
|
+
stateProvider: this.shared.sequence.stateProvider,
|
|
407
|
+
guest: this.shared.sequence.guest,
|
|
408
|
+
});
|
|
409
|
+
await wallet.submitUpdate(request.envelope);
|
|
410
|
+
await this.shared.modules.signatures.complete(requestId);
|
|
411
|
+
}
|
|
412
|
+
async login(args) {
|
|
413
|
+
if (isLoginToWalletArgs(args)) {
|
|
414
|
+
const prevWallet = await this.exists(args.wallet);
|
|
415
|
+
if (prevWallet) {
|
|
416
|
+
throw new Error('wallet-already-logged-in');
|
|
417
|
+
}
|
|
418
|
+
const device = await this.shared.modules.devices.create();
|
|
419
|
+
const { devicesTopology, modules, guardTopology } = await this.getConfigurationParts(args.wallet);
|
|
420
|
+
// Witness the wallet
|
|
421
|
+
await this.shared.modules.devices.witness(device.address, args.wallet);
|
|
422
|
+
// Add device to devices topology
|
|
423
|
+
const prevDevices = Config.getSigners(devicesTopology);
|
|
424
|
+
if (prevDevices.sapientSigners.length > 0) {
|
|
425
|
+
throw new Error('found-sapient-signer-in-devices-topology');
|
|
426
|
+
}
|
|
427
|
+
if (!prevDevices.isComplete) {
|
|
428
|
+
throw new Error('devices-topology-incomplete');
|
|
429
|
+
}
|
|
430
|
+
const nextDevicesTopology = buildCappedTree([
|
|
431
|
+
...prevDevices.signers
|
|
432
|
+
.filter((x) => x !== '0x0000000000000000000000000000000000000000')
|
|
433
|
+
.map((x) => ({ address: x })),
|
|
434
|
+
...prevDevices.sapientSigners.map((x) => ({ address: x.address, imageHash: x.imageHash })),
|
|
435
|
+
{ address: device.address },
|
|
436
|
+
]);
|
|
437
|
+
if (this.shared.modules.recovery.hasRecoveryModule(modules)) {
|
|
438
|
+
await this.shared.modules.recovery.addRecoverySignerToModules(modules, device.address);
|
|
439
|
+
}
|
|
440
|
+
await this.shared.databases.manager.set({
|
|
441
|
+
address: args.wallet,
|
|
442
|
+
status: 'logging-in',
|
|
443
|
+
loginDate: new Date().toISOString(),
|
|
444
|
+
device: device.address,
|
|
445
|
+
loginType: 'wallet',
|
|
446
|
+
useGuard: guardTopology !== undefined,
|
|
447
|
+
});
|
|
448
|
+
return this.requestConfigurationUpdate(args.wallet, {
|
|
449
|
+
devicesTopology: nextDevicesTopology,
|
|
450
|
+
modules,
|
|
451
|
+
}, 'login', 'wallet-webapp');
|
|
452
|
+
}
|
|
453
|
+
if (isLoginToMnemonicArgs(args)) {
|
|
454
|
+
const mnemonicSigner = MnemonicHandler.toSigner(args.mnemonic);
|
|
455
|
+
if (!mnemonicSigner) {
|
|
456
|
+
throw new Error('invalid-mnemonic');
|
|
457
|
+
}
|
|
458
|
+
const wallets = await State.getWalletsFor(this.shared.sequence.stateProvider, mnemonicSigner);
|
|
459
|
+
if (wallets.length === 0) {
|
|
460
|
+
throw new Error('no-wallets-found');
|
|
461
|
+
}
|
|
462
|
+
const wallet = await args.selectWallet(wallets.map((w) => w.wallet));
|
|
463
|
+
if (!wallets.some((w) => Address.isEqual(w.wallet, wallet))) {
|
|
464
|
+
throw new Error('wallet-not-found');
|
|
465
|
+
}
|
|
466
|
+
return this.login({ wallet });
|
|
467
|
+
}
|
|
468
|
+
if (isLoginToPasskeyArgs(args)) {
|
|
469
|
+
const passkeySigner = await Signers.Passkey.Passkey.find(this.shared.sequence.stateProvider, this.shared.sequence.extensions);
|
|
470
|
+
if (!passkeySigner) {
|
|
471
|
+
throw new Error('no-passkey-found');
|
|
472
|
+
}
|
|
473
|
+
const wallets = await State.getWalletsFor(this.shared.sequence.stateProvider, passkeySigner);
|
|
474
|
+
if (wallets.length === 0) {
|
|
475
|
+
throw new Error('no-wallets-found');
|
|
476
|
+
}
|
|
477
|
+
const wallet = await args.selectWallet(wallets.map((w) => w.wallet));
|
|
478
|
+
if (!wallets.some((w) => Address.isEqual(w.wallet, wallet))) {
|
|
479
|
+
throw new Error('wallet-not-found');
|
|
480
|
+
}
|
|
481
|
+
return this.login({ wallet });
|
|
482
|
+
}
|
|
483
|
+
throw new Error('invalid-login-args');
|
|
484
|
+
}
|
|
485
|
+
async completeLogin(requestId) {
|
|
486
|
+
const request = await this.shared.modules.signatures.get(requestId);
|
|
487
|
+
const walletEntry = await this.shared.databases.manager.get(request.wallet);
|
|
488
|
+
if (!walletEntry) {
|
|
489
|
+
throw new Error('login-for-wallet-not-found');
|
|
490
|
+
}
|
|
491
|
+
await this.completeConfigurationUpdate(requestId);
|
|
492
|
+
// Save entry in the manager db
|
|
493
|
+
await this.shared.databases.manager.set({
|
|
494
|
+
...walletEntry,
|
|
495
|
+
status: 'ready',
|
|
496
|
+
loginDate: new Date().toISOString(),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async logout(wallet, options) {
|
|
500
|
+
const walletEntry = await this.shared.databases.manager.get(wallet);
|
|
501
|
+
if (!walletEntry) {
|
|
502
|
+
throw new Error('wallet-not-found');
|
|
503
|
+
}
|
|
504
|
+
// Prevent starting logout if already logging out or not ready
|
|
505
|
+
if (walletEntry.status !== 'ready') {
|
|
506
|
+
console.warn(`Logout called on wallet ${wallet} with status ${walletEntry.status}. Aborting.`);
|
|
507
|
+
throw new Error(`Wallet is not in 'ready' state for logout (current: ${walletEntry.status})`);
|
|
508
|
+
}
|
|
509
|
+
if (options?.skipRemoveDevice) {
|
|
510
|
+
await Promise.all([
|
|
511
|
+
this.shared.databases.manager.del(wallet),
|
|
512
|
+
this.shared.modules.devices.remove(walletEntry.device),
|
|
513
|
+
]);
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
const device = await this.shared.modules.devices.get(walletEntry.device);
|
|
517
|
+
if (!device) {
|
|
518
|
+
throw new Error('device-not-found');
|
|
519
|
+
}
|
|
520
|
+
const { devicesTopology, modules } = await this.getConfigurationParts(wallet);
|
|
521
|
+
const nextDevicesTopology = buildCappedTree([
|
|
522
|
+
...Config.getSigners(devicesTopology)
|
|
523
|
+
.signers.filter((x) => x !== '0x0000000000000000000000000000000000000000' && x !== device.address)
|
|
524
|
+
.map((x) => ({ address: x })),
|
|
525
|
+
...Config.getSigners(devicesTopology).sapientSigners,
|
|
526
|
+
]);
|
|
527
|
+
// Remove device from the recovery topology, if it exists
|
|
528
|
+
if (this.shared.modules.recovery.hasRecoveryModule(modules)) {
|
|
529
|
+
await this.shared.modules.recovery.removeRecoverySignerFromModules(modules, device.address);
|
|
530
|
+
}
|
|
531
|
+
const requestId = await this.requestConfigurationUpdate(wallet, {
|
|
532
|
+
devicesTopology: nextDevicesTopology,
|
|
533
|
+
modules,
|
|
534
|
+
}, 'logout', 'wallet-webapp');
|
|
535
|
+
await this.shared.databases.manager.set({ ...walletEntry, status: 'logging-out' });
|
|
536
|
+
return requestId;
|
|
537
|
+
}
|
|
538
|
+
async completeLogout(requestId, options) {
|
|
539
|
+
const request = await this.shared.modules.signatures.get(requestId);
|
|
540
|
+
const walletEntry = await this.shared.databases.manager.get(request.wallet);
|
|
541
|
+
if (!walletEntry) {
|
|
542
|
+
throw new Error('wallet-not-found');
|
|
543
|
+
}
|
|
544
|
+
// Wallet entry should ideally be 'logging-out' here, but we proceed regardless
|
|
545
|
+
if (walletEntry.status !== 'logging-out') {
|
|
546
|
+
this.shared.modules.logger.log(`Warning: Wallet ${request.wallet} status was ${walletEntry.status} during completeLogout.`);
|
|
547
|
+
}
|
|
548
|
+
await this.completeConfigurationUpdate(requestId);
|
|
549
|
+
await this.shared.databases.manager.del(request.wallet);
|
|
550
|
+
await this.shared.modules.devices.remove(walletEntry.device);
|
|
551
|
+
}
|
|
552
|
+
async getConfiguration(wallet) {
|
|
553
|
+
const walletObject = new CoreWallet(wallet, {
|
|
554
|
+
context: this.shared.sequence.context,
|
|
555
|
+
stateProvider: this.shared.sequence.stateProvider,
|
|
556
|
+
guest: this.shared.sequence.guest,
|
|
557
|
+
});
|
|
558
|
+
const status = await walletObject.getStatus();
|
|
559
|
+
const raw = fromConfig(status.configuration);
|
|
560
|
+
const deviceSigners = Config.getSigners(raw.devicesTopology);
|
|
561
|
+
const loginSigners = Config.getSigners(raw.loginTopology);
|
|
562
|
+
return {
|
|
563
|
+
devices: await this.shared.modules.signers.resolveKinds(wallet, [
|
|
564
|
+
...deviceSigners.signers,
|
|
565
|
+
...deviceSigners.sapientSigners,
|
|
566
|
+
]),
|
|
567
|
+
login: await this.shared.modules.signers.resolveKinds(wallet, [
|
|
568
|
+
...loginSigners.signers,
|
|
569
|
+
...loginSigners.sapientSigners,
|
|
570
|
+
]),
|
|
571
|
+
raw,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async getNonce(chainId, address, space) {
|
|
575
|
+
const wallet = new CoreWallet(address, {
|
|
576
|
+
context: this.shared.sequence.context,
|
|
577
|
+
stateProvider: this.shared.sequence.stateProvider,
|
|
578
|
+
guest: this.shared.sequence.guest,
|
|
579
|
+
});
|
|
580
|
+
const network = this.shared.sequence.networks.find((n) => n.chainId === chainId);
|
|
581
|
+
if (!network) {
|
|
582
|
+
throw new Error('network-not-found');
|
|
583
|
+
}
|
|
584
|
+
const provider = Provider.from(RpcTransport.fromHttp(network.rpc));
|
|
585
|
+
return wallet.getNonce(provider, space);
|
|
586
|
+
}
|
|
587
|
+
async getOnchainConfiguration(wallet, chainId) {
|
|
588
|
+
const walletObject = new CoreWallet(wallet, {
|
|
589
|
+
context: this.shared.sequence.context,
|
|
590
|
+
stateProvider: this.shared.sequence.stateProvider,
|
|
591
|
+
guest: this.shared.sequence.guest,
|
|
592
|
+
});
|
|
593
|
+
const network = this.shared.sequence.networks.find((n) => n.chainId === chainId);
|
|
594
|
+
if (!network) {
|
|
595
|
+
throw new Error('network-not-found');
|
|
596
|
+
}
|
|
597
|
+
const provider = Provider.from(RpcTransport.fromHttp(network.rpc));
|
|
598
|
+
const status = await walletObject.getStatus(provider);
|
|
599
|
+
const onchainConfiguration = await this.shared.sequence.stateProvider.getConfiguration(status.onChainImageHash);
|
|
600
|
+
if (!onchainConfiguration) {
|
|
601
|
+
throw new Error('onchain-configuration-not-found');
|
|
602
|
+
}
|
|
603
|
+
const raw = fromConfig(status.configuration);
|
|
604
|
+
const deviceSigners = Config.getSigners(raw.devicesTopology);
|
|
605
|
+
const loginSigners = Config.getSigners(raw.loginTopology);
|
|
606
|
+
return {
|
|
607
|
+
devices: await this.shared.modules.signers.resolveKinds(wallet, [
|
|
608
|
+
...deviceSigners.signers,
|
|
609
|
+
...deviceSigners.sapientSigners,
|
|
610
|
+
]),
|
|
611
|
+
login: await this.shared.modules.signers.resolveKinds(wallet, [
|
|
612
|
+
...loginSigners.signers,
|
|
613
|
+
...loginSigners.sapientSigners,
|
|
614
|
+
]),
|
|
615
|
+
raw,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
async isUpdatedOnchain(wallet, chainId) {
|
|
619
|
+
const walletObject = new CoreWallet(wallet, {
|
|
620
|
+
context: this.shared.sequence.context,
|
|
621
|
+
stateProvider: this.shared.sequence.stateProvider,
|
|
622
|
+
guest: this.shared.sequence.guest,
|
|
623
|
+
});
|
|
624
|
+
const network = this.shared.sequence.networks.find((n) => n.chainId === chainId);
|
|
625
|
+
if (!network) {
|
|
626
|
+
throw new Error('network-not-found');
|
|
627
|
+
}
|
|
628
|
+
const provider = Provider.from(RpcTransport.fromHttp(network.rpc));
|
|
629
|
+
const onchainStatus = await walletObject.getStatus(provider);
|
|
630
|
+
return onchainStatus.imageHash === onchainStatus.onChainImageHash;
|
|
631
|
+
}
|
|
632
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@0xsequence/wallet-wdk",
|
|
3
|
+
"version": "0.0.0-20250520201059",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"private": false,
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.13.9",
|
|
18
|
+
"dotenv": "^16.4.7",
|
|
19
|
+
"fake-indexeddb": "^6.0.0",
|
|
20
|
+
"happy-dom": "^13.2.0",
|
|
21
|
+
"typescript": "^5.7.3",
|
|
22
|
+
"vitest": "^3.1.2",
|
|
23
|
+
"@repo/typescript-config": "^0.0.0-20250520201059"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@0xsequence/tee-verifier": "^0.1.0",
|
|
27
|
+
"idb": "^7.1.1",
|
|
28
|
+
"jwt-decode": "^4.0.0",
|
|
29
|
+
"ox": "^0.7.0",
|
|
30
|
+
"uuid": "^11.1.0",
|
|
31
|
+
"@0xsequence/wallet-primitives": "^0.0.0-20250520201059",
|
|
32
|
+
"@0xsequence/wallet-core": "^0.0.0-20250520201059",
|
|
33
|
+
"@0xsequence/identity-instrument": "^0.0.0-20250520201059"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc",
|
|
37
|
+
"dev": "tsc --watch",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Generic } from './generic.js'
|
|
2
|
+
|
|
3
|
+
const TABLE_NAME = 'auth-commitments'
|
|
4
|
+
|
|
5
|
+
export type AuthCommitment = {
|
|
6
|
+
id: string
|
|
7
|
+
kind: 'google-pkce' | 'apple'
|
|
8
|
+
metadata: { [key: string]: string }
|
|
9
|
+
verifier?: string
|
|
10
|
+
challenge?: string
|
|
11
|
+
target: string
|
|
12
|
+
isSignUp: boolean
|
|
13
|
+
signer?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class AuthCommitments extends Generic<AuthCommitment, 'id'> {
|
|
17
|
+
constructor(dbName: string = 'sequence-auth-commitments') {
|
|
18
|
+
super(dbName, TABLE_NAME, 'id', [
|
|
19
|
+
(db: IDBDatabase) => {
|
|
20
|
+
if (!db.objectStoreNames.contains(TABLE_NAME)) {
|
|
21
|
+
db.createObjectStore(TABLE_NAME)
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
])
|
|
25
|
+
}
|
|
26
|
+
}
|