@zerc20/wallet-sdk 0.2.0 → 0.3.0
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/dist/burnWorker.js +6 -6
- package/dist/burnWorker.js.map +1 -1
- package/dist/context.d.ts +23 -4
- package/dist/context.js +35 -6
- package/dist/context.js.map +1 -1
- package/dist/core.d.ts +32 -17
- package/dist/core.js +253 -70
- package/dist/core.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pos.js +2 -2
- package/dist/pos.js.map +1 -1
- package/dist/rageExit.js +3 -5
- package/dist/rageExit.js.map +1 -1
- package/dist/seed.d.ts +4 -0
- package/dist/seed.js +12 -1
- package/dist/seed.js.map +1 -1
- package/dist/spend.d.ts +1 -1
- package/dist/spend.js +5 -4
- package/dist/spend.js.map +1 -1
- package/dist/state.d.ts +14 -4
- package/dist/state.js +10 -7
- package/dist/state.js.map +1 -1
- package/dist/sync.d.ts +16 -3
- package/dist/sync.js +90 -25
- package/dist/sync.js.map +1 -1
- package/dist/wallet.d.ts +22 -10
- package/dist/wallet.js +111 -58
- package/dist/wallet.js.map +1 -1
- package/package.json +4 -3
- package/src/burnWorker.ts +9 -10
- package/src/context.ts +48 -8
- package/src/core.ts +279 -87
- package/src/index.ts +1 -1
- package/src/pos.ts +2 -2
- package/src/rageExit.ts +3 -4
- package/src/seed.ts +13 -1
- package/src/spend.ts +5 -5
- package/src/state.ts +17 -7
- package/src/sync.ts +95 -24
- package/src/wallet.ts +136 -60
- package/wasm/zerc20_wallet_core.d.ts +16 -13
- package/wasm/zerc20_wallet_core.js +49 -42
- package/wasm/zerc20_wallet_core_bg.wasm +0 -0
- package/wasm/zerc20_wallet_core_bg.wasm.d.ts +3 -3
package/src/wallet.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type Account,
|
|
20
20
|
type Address,
|
|
21
21
|
type Hex,
|
|
22
|
+
type LocalAccount,
|
|
22
23
|
type WalletClient,
|
|
23
24
|
bytesToHex,
|
|
24
25
|
formatUnits,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
zeroAddress,
|
|
29
30
|
} from 'viem';
|
|
30
31
|
import { Chain, MAX_REFERRAL_BPS, NATIVE, type Referrer, type Token, erc20Abi, tokenAbi } from './chain.js';
|
|
31
|
-
import { type Ctx, type SpendKey, chainCtx, chainState,
|
|
32
|
+
import { type Ctx, type SpendKey, chainCtx, chainState, owner, switched, walletOn, walletSees } from './context.js';
|
|
32
33
|
import { burnAddressFor, core, initCore } from './core.js';
|
|
33
34
|
import { type ErrorCode, WalletError, fail, guard } from './errors.js';
|
|
34
35
|
import { type Network, checkNetwork } from './network.js';
|
|
@@ -62,10 +63,14 @@ import {
|
|
|
62
63
|
emptyWalletState,
|
|
63
64
|
openState,
|
|
64
65
|
sealState,
|
|
66
|
+
stateKey,
|
|
67
|
+
unspent,
|
|
65
68
|
} from './state.js';
|
|
66
69
|
import {
|
|
67
70
|
WAIT_MS,
|
|
68
71
|
commitmentTx,
|
|
72
|
+
keyAll,
|
|
73
|
+
nextBurn,
|
|
69
74
|
recordBurn,
|
|
70
75
|
storageState,
|
|
71
76
|
syncAll,
|
|
@@ -75,16 +80,23 @@ import {
|
|
|
75
80
|
unusedBurn,
|
|
76
81
|
} from './sync.js';
|
|
77
82
|
|
|
78
|
-
const DAY_MS = 86_400_000;
|
|
79
83
|
/** The ledger's bound outside an emergency. */
|
|
80
84
|
const NO_BOUND = 2n ** 64n - 1n;
|
|
81
85
|
|
|
82
86
|
export interface OpenOptions {
|
|
83
87
|
network: Network;
|
|
84
|
-
/** The
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
/** The account's key, held by the app as a viem local account (a private key's, or `accountKey`'s): the wallet sends
|
|
89
|
+
* its transactions through each chain's RPC. Give this or `walletClient`. */
|
|
90
|
+
account?: LocalAccount;
|
|
91
|
+
/** The user's wallet (MetaMask, WalletConnect, ...) as a viem client with its account, which the wallet switches to
|
|
92
|
+
* each chain it sends on. */
|
|
93
|
+
walletClient?: WalletClient;
|
|
94
|
+
/** The 32-byte root of every key (see `seedFromSignature`), held while the wallet is open. Or a function that gives
|
|
95
|
+
* it, a passkey's say, to hold the seed only while it is needed: asked as the wallet opens, and again to pay,
|
|
96
|
+
* withdraw, consolidate or exit, or for a receive code past those derived ahead, each time before the call waits on
|
|
97
|
+
* anything (a browser shows a passkey prompt only in the click that asked). The seed goes at the end of a sync that
|
|
98
|
+
* nothing waits behind, once nothing in flight signs again. */
|
|
99
|
+
seed: Uint8Array | (() => Promise<Uint8Array>);
|
|
88
100
|
/** Where the encrypted state lives between sessions. */
|
|
89
101
|
persist: { load(): Promise<Uint8Array | null | undefined>; save(bytes: Uint8Array): Promise<void> };
|
|
90
102
|
/** Where a wallet with no saved state starts looking: the block its seed was first used at, or roughly when. Without
|
|
@@ -108,7 +120,7 @@ export type DepositStep =
|
|
|
108
120
|
|
|
109
121
|
/** Why an amount is not spendable yet. */
|
|
110
122
|
export type PendingReason =
|
|
111
|
-
/**
|
|
123
|
+
/** A note this device's payment, withdrawal or merge under way is making, not yet read on chain. */
|
|
112
124
|
| 'incoming'
|
|
113
125
|
/** Waiting for a proved root that covers it (minutes). */
|
|
114
126
|
| 'awaiting-root'
|
|
@@ -157,7 +169,7 @@ export interface Activity {
|
|
|
157
169
|
txHash: Hex | null;
|
|
158
170
|
/** `done` once on chain (in a block: a reorg may still take it back until final), `failed` for a payment or
|
|
159
171
|
* withdrawal that cannot land, shown only on the device that tried it. What is not on chain yet is not activity:
|
|
160
|
-
* an operation under way is in `operations()`,
|
|
172
|
+
* an operation under way is in `operations()`, and the note it is making `incoming` in `balances()`. */
|
|
161
173
|
status: 'done' | 'failed';
|
|
162
174
|
}
|
|
163
175
|
|
|
@@ -240,10 +252,12 @@ export class Wallet {
|
|
|
240
252
|
#busy = 0;
|
|
241
253
|
#release = () => {};
|
|
242
254
|
#referrer: Referrer | undefined;
|
|
255
|
+
readonly #unlock: (() => Promise<Uint8Array>) | undefined;
|
|
243
256
|
#closed = new AbortController();
|
|
244
257
|
|
|
245
|
-
private constructor(ctx: Ctx) {
|
|
258
|
+
private constructor(ctx: Ctx, unlock?: () => Promise<Uint8Array>) {
|
|
246
259
|
this.#ctx = ctx;
|
|
260
|
+
this.#unlock = unlock;
|
|
247
261
|
}
|
|
248
262
|
|
|
249
263
|
/** Lets this wallet open again: in another tab, or here for another account. This object is done with: what is
|
|
@@ -260,9 +274,7 @@ export class Wallet {
|
|
|
260
274
|
|
|
261
275
|
/** Opens the wallet. The first time on this address, registers it for payments: one signature. */
|
|
262
276
|
static async open(opts: OpenOptions): Promise<Wallet> {
|
|
263
|
-
await initCore();
|
|
264
277
|
checkNetwork(opts.network);
|
|
265
|
-
if (opts.seed.length !== 32) fail('invalid', 'the seed is 32 bytes');
|
|
266
278
|
// the factory refuses these, which the relayer would report as its own failure
|
|
267
279
|
const r = opts.referrer;
|
|
268
280
|
if (r) {
|
|
@@ -271,17 +283,23 @@ export class Wallet {
|
|
|
271
283
|
if (!Number.isInteger(r.bps) || r.bps < 0 || r.bps > MAX_REFERRAL_BPS)
|
|
272
284
|
fail('invalid', `a referrer takes 0 to ${MAX_REFERRAL_BPS} bps, not ${r.bps}`);
|
|
273
285
|
}
|
|
274
|
-
|
|
286
|
+
if (!opts.account === !opts.walletClient) fail('invalid', 'give an account or a wallet client, one of the two');
|
|
287
|
+
const account = opts.account ?? opts.walletClient!.account ?? fail('invalid', 'the wallet client has no account');
|
|
288
|
+
// asked once the options are checked, and before anything is waited on: a browser shows a passkey prompt only in
|
|
289
|
+
// the click that opened
|
|
290
|
+
const seed = typeof opts.seed === 'function' ? await opts.seed() : opts.seed;
|
|
291
|
+
if (seed.length !== 32) fail('invalid', 'the seed is 32 bytes');
|
|
292
|
+
await initCore();
|
|
275
293
|
const release = await lockWallet(account.address);
|
|
276
294
|
try {
|
|
277
|
-
return await Wallet.#open(opts, account, release);
|
|
295
|
+
return await Wallet.#open(opts, seed, account, release);
|
|
278
296
|
} catch (e) {
|
|
279
297
|
release();
|
|
280
298
|
throw e;
|
|
281
299
|
}
|
|
282
300
|
}
|
|
283
301
|
|
|
284
|
-
static async #open(opts: OpenOptions, account: Account, release: () => void): Promise<Wallet> {
|
|
302
|
+
static async #open(opts: OpenOptions, seed: Uint8Array, account: Account, release: () => void): Promise<Wallet> {
|
|
285
303
|
const address = account.address;
|
|
286
304
|
const chains = opts.network.chains.map((config) => ({
|
|
287
305
|
config,
|
|
@@ -294,16 +312,23 @@ export class Wallet {
|
|
|
294
312
|
if (code && code !== '0x' && !code.startsWith('0xef0100'))
|
|
295
313
|
fail('unsupported-account', 'contract accounts cannot sign for a wallet');
|
|
296
314
|
const signer: Signer = async (message) =>
|
|
297
|
-
hexToBytes(
|
|
315
|
+
hexToBytes(
|
|
316
|
+
await guard(
|
|
317
|
+
account.type === 'local'
|
|
318
|
+
? account.signMessage({ message })
|
|
319
|
+
: opts.walletClient!.signMessage({ account, message }),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
298
322
|
const storage = await CanisterClient.connect(opts.network.storage.host, opts.network.storage.canisterId);
|
|
299
|
-
const publicKey = hexToBytes((core.spendKey(
|
|
323
|
+
const publicKey = hexToBytes((core.spendKey(seed) as SpendKey).publicKey);
|
|
300
324
|
const saved = await opts.persist.load();
|
|
301
325
|
// registered already: used before, here or on another device, so its past is looked for
|
|
302
326
|
// ponytail: from genesis (~4 MB per million commitments); ask for a birthday once the chain has many
|
|
303
327
|
const restoring =
|
|
304
328
|
!saved?.length && opts.birthday === undefined && !!(await storage.registration(hexToBytes(address)));
|
|
305
|
-
const view = await registeredViewKey(storage,
|
|
306
|
-
const
|
|
329
|
+
const view = await registeredViewKey(storage, seed, signer, hexToBytes(address), publicKey);
|
|
330
|
+
const key = stateKey(seed);
|
|
331
|
+
const loaded = saved?.length ? openState(key, saved) : null;
|
|
307
332
|
// never started over in place: the first save would overwrite what it held, operations in flight included
|
|
308
333
|
if (saved?.length && !loaded) {
|
|
309
334
|
fail(
|
|
@@ -316,27 +341,60 @@ export class Wallet {
|
|
|
316
341
|
const state: WalletState = loaded ?? emptyWalletState(address);
|
|
317
342
|
const ctx: Ctx = {
|
|
318
343
|
network: opts.network,
|
|
319
|
-
seed
|
|
344
|
+
seed,
|
|
345
|
+
owners: new Map(),
|
|
320
346
|
address,
|
|
321
|
-
|
|
347
|
+
account,
|
|
348
|
+
walletClient: opts.walletClient ?? null,
|
|
322
349
|
storage,
|
|
323
350
|
view,
|
|
324
351
|
chains,
|
|
325
352
|
state,
|
|
326
|
-
save: () => opts.persist.save(sealState(
|
|
353
|
+
save: () => opts.persist.save(sealState(key, state)),
|
|
327
354
|
lastFullRefreshMs: 0,
|
|
328
355
|
};
|
|
329
356
|
if (!loaded) await start(ctx, restoring ? 0n : opts.birthday);
|
|
330
|
-
const wallet = new Wallet(ctx);
|
|
357
|
+
const wallet = new Wallet(ctx, typeof opts.seed === 'function' ? opts.seed : undefined);
|
|
331
358
|
wallet.#release = release;
|
|
332
359
|
wallet.#referrer = opts.referrer;
|
|
333
360
|
// a saved state names its tokens by id, and its coins stand by the chains' roots: read both now, so balances, coins
|
|
334
361
|
// and history work before the first sync
|
|
335
362
|
const [tokens] = await Promise.all([Promise.all(chains.map((c) => c.chain.tokens())), wallet.#readRoots()]);
|
|
336
363
|
wallet.#tokens = tokens.flat();
|
|
364
|
+
keyAll(ctx, wallet.#tokens);
|
|
337
365
|
return wallet;
|
|
338
366
|
}
|
|
339
367
|
|
|
368
|
+
/** Holds `seed`, asked of `#unlock`, once it is checked to be this wallet's. */
|
|
369
|
+
#hold(seed: Uint8Array): void {
|
|
370
|
+
if ((core.spendKey(seed) as SpendKey).publicKey !== owner(this.#ctx)!.publicKey)
|
|
371
|
+
fail('invalid', "the seed given is another wallet's");
|
|
372
|
+
this.#ctx.seed = seed;
|
|
373
|
+
keyAll(this.#ctx, this.#tokens);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Within a task: the seed held, asked of `#unlock` if need be. */
|
|
377
|
+
async #seed(): Promise<void> {
|
|
378
|
+
if (!this.#ctx.seed) this.#hold(await this.#unlock!());
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** At the end of a sync: lets the seed go when it was opened with a function for it, nothing waits behind the sync,
|
|
382
|
+
* and no payment, withdrawal or consolidation is in flight, as each may sign again (a JoinSplit that reverts is
|
|
383
|
+
* proved again, and every merge round is signed). */
|
|
384
|
+
#dropSeed(): void {
|
|
385
|
+
if (!this.#unlock || this.#busy > 1) return;
|
|
386
|
+
if (this.#ctx.state.ops.some((o) => o.status === 'pending' && o.kind !== 'deposit')) return;
|
|
387
|
+
this.#ctx.seed = null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** `#exclusive` for a call that `needs` the seed: held first, asked of `#unlock` if need be before the call waits on
|
|
391
|
+
* anything else (a browser shows a passkey prompt only in the click that asked), and queued in the step that took it,
|
|
392
|
+
* so that a sync ending meanwhile does not let it go. */
|
|
393
|
+
async #seeded<T>(f: () => Promise<T>, needs = true): Promise<T> {
|
|
394
|
+
if (needs && !this.#ctx.seed) this.#hold(await this.#unlock!());
|
|
395
|
+
return this.#exclusive(f);
|
|
396
|
+
}
|
|
397
|
+
|
|
340
398
|
/** Runs `f` after everything queued before it: one state, changed by one task at a time. */
|
|
341
399
|
#exclusive<T>(f: () => Promise<T>): Promise<T> {
|
|
342
400
|
this.#busy++;
|
|
@@ -372,13 +430,14 @@ export class Wallet {
|
|
|
372
430
|
|
|
373
431
|
/** Catches up: new notes, deposits, spends and roots, then advances every operation in flight. A chain or server that
|
|
374
432
|
* fails holds back only what needs it, and the first error is thrown once the rest is done. Settles before the burn
|
|
375
|
-
* addresses are topped up, which can take a phone
|
|
433
|
+
* addresses are topped up, which can take a phone some seconds. */
|
|
376
434
|
sync(): Promise<void> {
|
|
377
435
|
return new Promise((resolve, reject) => {
|
|
378
436
|
this.#exclusive(async () => {
|
|
379
437
|
await this.#catchUp().then(resolve, reject);
|
|
380
438
|
// still this task's: a deposit queued meanwhile finds the one recorded ahead, rather than writing at handout
|
|
381
439
|
await topUpBurns(this.#ctx, this.#closed.signal).catch(() => {}); // tried again after the next sync
|
|
440
|
+
this.#dropSeed();
|
|
382
441
|
}).catch(reject); // closed before it ran
|
|
383
442
|
});
|
|
384
443
|
}
|
|
@@ -424,7 +483,7 @@ export class Wallet {
|
|
|
424
483
|
if (errors.length > 0) throw errors[0];
|
|
425
484
|
}
|
|
426
485
|
|
|
427
|
-
/** Syncs on its own: every
|
|
486
|
+
/** Syncs on its own: every 3 s while something is under way, every 10 s otherwise. Returns a stop function. */
|
|
428
487
|
autoSync(onChange?: () => void, onError?: (e: unknown) => void): () => void {
|
|
429
488
|
let stopped = false;
|
|
430
489
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -437,7 +496,7 @@ export class Wallet {
|
|
|
437
496
|
}
|
|
438
497
|
if (stopped || this.#closed.signal.aborted) return;
|
|
439
498
|
const busy = this.#ctx.state.ops.some((o) => o.status === 'pending' && Date.now() - o.createdMs < WAIT_MS * 4);
|
|
440
|
-
timer = setTimeout(tick, busy ?
|
|
499
|
+
timer = setTimeout(tick, busy ? 3_000 : 10_000);
|
|
441
500
|
};
|
|
442
501
|
void tick();
|
|
443
502
|
return () => {
|
|
@@ -460,18 +519,22 @@ export class Wallet {
|
|
|
460
519
|
|
|
461
520
|
balances(): Balance[] {
|
|
462
521
|
const coins = this.coins();
|
|
522
|
+
// storage says which notes to look for, not that they come: a payer writes its notes there before its JoinSplit
|
|
523
|
+
// leaves, and one that never lands leaves them behind. So a note counts once it is a coin (its commitment read);
|
|
524
|
+
// before that, only a note this device's own operation under way is making counts, as incoming. A zero-value
|
|
525
|
+
// change, there to carry its payment's receipt, never counts
|
|
526
|
+
const making = new Set(
|
|
527
|
+
this.#ctx.state.ops.flatMap((o) =>
|
|
528
|
+
o.status === 'pending' && o.kind !== 'deposit' ? (o.data.changes ?? []) : [],
|
|
529
|
+
),
|
|
530
|
+
);
|
|
463
531
|
return this.#tokens.map((token) => {
|
|
464
532
|
const { chainId, tokenId } = token;
|
|
465
533
|
const b: Balance = { token, spendable: 0n, pending: [], blocked: 0n };
|
|
466
|
-
// a merge that never landed (another device spent an input first) stops counting after a day; a zero-value
|
|
467
|
-
// change, there to carry its payment's receipt, never counts. A note is a coin once its commitment is read, so
|
|
468
|
-
// until then it is incoming, as a payment's change is between its relay and the next sync
|
|
469
534
|
for (const n of this.#ctx.state.notes.filter(
|
|
470
|
-
(n) => n.chainId === chainId && n.tokenId === tokenId && n.value > 0n,
|
|
535
|
+
(n) => n.chainId === chainId && n.tokenId === tokenId && n.value > 0n && making.has(n.salt),
|
|
471
536
|
)) {
|
|
472
|
-
|
|
473
|
-
n.receipt?.kind === 'merged' && n.atNs !== null && Date.now() - Number(n.atNs / 1_000_000n) > DAY_MS;
|
|
474
|
-
if (!n.found.some((f) => f.confirmed) && !stale) b.pending.push({ amount: n.value, reason: 'incoming' });
|
|
537
|
+
if (!n.found.some((f) => f.confirmed)) b.pending.push({ amount: n.value, reason: 'incoming' });
|
|
475
538
|
}
|
|
476
539
|
for (const c of coins.filter((c) => c.token === token)) {
|
|
477
540
|
if (c.state === 'spendable') b.spendable += c.amount;
|
|
@@ -483,13 +546,14 @@ export class Wallet {
|
|
|
483
546
|
}
|
|
484
547
|
|
|
485
548
|
/** Every unspent note and deposit on chain, and where each stands, as of opening or the last sync. A note announced
|
|
486
|
-
* but not on chain yet is not one: `balances()` counts it as incoming. */
|
|
549
|
+
* but not read on chain yet is not one: `balances()` counts it as incoming if this device's operation is making it. */
|
|
487
550
|
coins(): CoinInfo[] {
|
|
488
551
|
const size = (chainId: bigint) => this.#status.get(chainId)?.latestRoot?.size ?? 0;
|
|
489
552
|
const unproved = (chainId: bigint) => (this.#status.get(chainId)?.halted ? 'roots-halted' : 'awaiting-root');
|
|
490
553
|
const spending = new Set(this.#ctx.state.ops.flatMap((o) => inputsOf(o).map((c) => `${o.chainId}:${c.index}`)));
|
|
491
|
-
// spent by a JoinSplit in a block, which waits only to be final: gone, as activity() counts it
|
|
492
|
-
//
|
|
554
|
+
// spent by a JoinSplit in a block, which waits only to be final: gone, as activity() counts it. Sync sees it by its
|
|
555
|
+
// nullifier (`unspent`); this device's own is gone already from its relay on (a reorg that drops it brings them
|
|
556
|
+
// back as spending, and the operation relays it again)
|
|
493
557
|
const landed = new Set(
|
|
494
558
|
this.#ctx.state.ops.flatMap((o) =>
|
|
495
559
|
o.kind !== 'deposit' && o.data.step?.stage === 'relayed'
|
|
@@ -500,7 +564,7 @@ export class Wallet {
|
|
|
500
564
|
const out: CoinInfo[] = [];
|
|
501
565
|
// a zero-value change carries its payment's receipt, and is no coin
|
|
502
566
|
for (const n of this.#ctx.state.notes.filter((n) => n.value > 0n)) {
|
|
503
|
-
for (const f of n.found.filter(
|
|
567
|
+
for (const f of n.found.filter(unspent)) {
|
|
504
568
|
const id = `${n.chainId}:${f.index}`;
|
|
505
569
|
// a 4-byte tag alone matches someone else's now and then: a coin once its commitment is read
|
|
506
570
|
if (!f.confirmed || landed.has(id)) continue;
|
|
@@ -518,7 +582,7 @@ export class Wallet {
|
|
|
518
582
|
}
|
|
519
583
|
for (const d of this.#ctx.state.deposits) {
|
|
520
584
|
// a coin once its commitment is read, which names the amount
|
|
521
|
-
for (const f of d.found.filter((f) =>
|
|
585
|
+
for (const f of d.found.filter((f) => unspent(f) && f.amount !== null)) {
|
|
522
586
|
const id = `${d.chainId}:${f.index}`;
|
|
523
587
|
if (landed.has(id)) continue;
|
|
524
588
|
const state =
|
|
@@ -768,7 +832,7 @@ export class Wallet {
|
|
|
768
832
|
/** Refunds deposit `id` to its sender without the ASP, the attester or a proved root: for a deposit the ASP
|
|
769
833
|
* rejected, or when those services stop. The account's wallet sends the transactions and pays their gas. */
|
|
770
834
|
rageExit(id: string): Promise<Hex[]> {
|
|
771
|
-
return this.#
|
|
835
|
+
return this.#seeded(async () => {
|
|
772
836
|
const [chain, index] = id.split(':');
|
|
773
837
|
const record =
|
|
774
838
|
this.#ctx.state.deposits.find((d) => `${d.chainId}` === chain && d.found.some((f) => `${f.index}` === index)) ??
|
|
@@ -874,22 +938,26 @@ export class Wallet {
|
|
|
874
938
|
* With `wrapped`, it pays from the account's zERC20 itself, as a withdrawal pays out, rather than its underlying
|
|
875
939
|
* asset: a plain transfer into the burn address, with no wrap and no approval.
|
|
876
940
|
*
|
|
941
|
+
* With `from`, another wallet of the user's (a browser wallet) pays instead of the account: its balance is checked,
|
|
942
|
+
* it approves and wraps, and an exit refunds it there. The account pays nothing and signs nothing.
|
|
943
|
+
*
|
|
877
944
|
* Returns once the wrap is sent: the operation is done once it is mined, which `wait()` or the next sync finds.
|
|
878
945
|
* `onStep` hears what it waits on meanwhile, for an app to show. */
|
|
879
|
-
deposit(opts: {
|
|
946
|
+
async deposit(opts: {
|
|
880
947
|
token: Token;
|
|
881
948
|
amount: bigint;
|
|
882
949
|
to?: Address;
|
|
883
950
|
wrapped?: boolean;
|
|
951
|
+
from?: WalletClient;
|
|
884
952
|
onStep?: (step: DepositStep) => void;
|
|
885
953
|
}): Promise<Operation> {
|
|
954
|
+
if (opts.amount <= 0n) fail('invalid', 'the amount must be positive');
|
|
955
|
+
const payer = opts.from ? (opts.from.account ?? fail('invalid', 'the wallet client has no account')) : null;
|
|
886
956
|
const step = opts.onStep ?? (() => {});
|
|
887
957
|
if (this.#busy > 0) step('queued');
|
|
888
958
|
return this.#exclusive(async () => {
|
|
889
959
|
const { token, amount } = opts;
|
|
890
|
-
|
|
891
|
-
const wc = this.#ctx.walletClient;
|
|
892
|
-
const account = wc.account!;
|
|
960
|
+
const account = payer ?? this.#ctx.account;
|
|
893
961
|
const client = chainCtx(this.#ctx, token.chainId).chain.client;
|
|
894
962
|
const { wrapped } = opts;
|
|
895
963
|
const native = !wrapped && token.underlying.toLowerCase() === NATIVE.toLowerCase();
|
|
@@ -906,7 +974,7 @@ export class Wallet {
|
|
|
906
974
|
const of = wrapped ? token.symbol : `${token.symbol}'s underlying asset`;
|
|
907
975
|
fail(
|
|
908
976
|
'insufficient-funds',
|
|
909
|
-
|
|
977
|
+
`${payer ? 'the wallet' : 'the account'} holds ${formatUnits(held, token.decimals)} of ${of}, less than ${formatUnits(amount, token.decimals)}`,
|
|
910
978
|
);
|
|
911
979
|
}
|
|
912
980
|
const address = opts.to
|
|
@@ -920,8 +988,7 @@ export class Wallet {
|
|
|
920
988
|
);
|
|
921
989
|
await this.#ctx.save();
|
|
922
990
|
try {
|
|
923
|
-
|
|
924
|
-
await guard(wc.switchChain({ id: Number(token.chainId) }));
|
|
991
|
+
const wc = opts.from ? await switched(opts.from, token.chainId) : await walletOn(this.#ctx, token.chainId);
|
|
925
992
|
if (!native && !wrapped) {
|
|
926
993
|
const allowance = await client.readContract({
|
|
927
994
|
address: token.underlying,
|
|
@@ -1056,19 +1123,21 @@ export class Wallet {
|
|
|
1056
1123
|
* nothing; it names neither this address nor its public key. Anyone who gets the code can pay it, and see what its
|
|
1057
1124
|
* other payers paid privately: give each payer its own. It is checksummed text (bech32m, `zerc201…`). */
|
|
1058
1125
|
receiveCode(token: Token): Promise<string> {
|
|
1059
|
-
|
|
1060
|
-
|
|
1126
|
+
const ctx = this.#ctx;
|
|
1127
|
+
const code = (index: number) => ({ chainId: token.chainId, tokenId: token.tokenId, index });
|
|
1128
|
+
// the seed is asked for only past the codes derived ahead
|
|
1129
|
+
const derived = !!owner(ctx, code(nextBurn(ctx, token.chainId, token.tokenId)));
|
|
1130
|
+
return this.#seeded(async () => {
|
|
1061
1131
|
const { burnIndex, address } = await this.#burnAddress(token);
|
|
1132
|
+
if (!owner(ctx, code(burnIndex))) await this.#seed(); // calls queued before took the next ones
|
|
1062
1133
|
return encodeCode({
|
|
1063
1134
|
chainId: token.chainId,
|
|
1064
1135
|
tokenId: token.tokenId,
|
|
1065
1136
|
burnAddress: hexToBytes(address),
|
|
1066
|
-
publicKey: hexToBytes(
|
|
1067
|
-
spendKey(ctx, { chainId: token.chainId, tokenId: token.tokenId, index: burnIndex }).publicKey,
|
|
1068
|
-
),
|
|
1137
|
+
publicKey: hexToBytes(owner(ctx, code(burnIndex))!.publicKey),
|
|
1069
1138
|
key: codeKey(ctx.view.seed, token.chainId, token.tokenId, burnIndex),
|
|
1070
1139
|
});
|
|
1071
|
-
});
|
|
1140
|
+
}, !derived);
|
|
1072
1141
|
}
|
|
1073
1142
|
|
|
1074
1143
|
/** What a send or withdrawal of `amount` would cost now, or a consolidation of what the wallet holds. A send or
|
|
@@ -1153,10 +1222,11 @@ export class Wallet {
|
|
|
1153
1222
|
* what is spendable now, or it fails here: `needs-consolidation`, `funds-settling` or `insufficient-funds`.
|
|
1154
1223
|
* `amount` is what the recipient gets, and `maxFee` caps what it pays on top, `fees({ kind: 'send' }).fee`: the
|
|
1155
1224
|
* relayer names its fee, so a quote the user saw is the cap. */
|
|
1156
|
-
send(opts: { to: string; token: Token; amount: bigint; maxFee: bigint }): Promise<Operation> {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1225
|
+
async send(opts: { to: string; token: Token; amount: bigint; maxFee: bigint }): Promise<Operation> {
|
|
1226
|
+
const { token, amount } = opts;
|
|
1227
|
+
if (amount <= 0n) fail('invalid', 'the amount must be positive');
|
|
1228
|
+
// the JoinSplit is proved by a later sync, which holds the seed until the payment is done
|
|
1229
|
+
return this.#seeded(async () => {
|
|
1160
1230
|
let to: Destination;
|
|
1161
1231
|
if (isAddress(opts.to)) {
|
|
1162
1232
|
const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(opts.to));
|
|
@@ -1188,7 +1258,7 @@ export class Wallet {
|
|
|
1188
1258
|
* touched; one still settling is waited for. `maxFee` caps the fees of every merge together,
|
|
1189
1259
|
* `fees({ kind: 'consolidate' }).fee`. */
|
|
1190
1260
|
consolidate(opts: { token: Token; coins?: string[]; maxFee: bigint }): Promise<Operation> {
|
|
1191
|
-
return this.#
|
|
1261
|
+
return this.#seeded(async () => {
|
|
1192
1262
|
if (!this.#ctx.network.mergeProver) fail('invalid', 'this network has no merge prover');
|
|
1193
1263
|
const to: Destination = { kind: 'self' };
|
|
1194
1264
|
const data: SpendData = { to, maxFee: opts.maxFee, step: null, paid: 0n };
|
|
@@ -1215,10 +1285,16 @@ export class Wallet {
|
|
|
1215
1285
|
/** Withdraws `amount` publicly to `to`, as zERC20 or with `unwrap` the underlying asset itself, so `to` needs no gas
|
|
1216
1286
|
* to unwrap. One JoinSplit, as `send`, and as there `to` gets all of `amount` and `maxFee` caps what is paid on top
|
|
1217
1287
|
* (`fees({ kind: 'withdraw' }).fee`). */
|
|
1218
|
-
withdraw(opts: {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1288
|
+
async withdraw(opts: {
|
|
1289
|
+
to: Address;
|
|
1290
|
+
token: Token;
|
|
1291
|
+
amount: bigint;
|
|
1292
|
+
maxFee: bigint;
|
|
1293
|
+
unwrap?: boolean;
|
|
1294
|
+
}): Promise<Operation> {
|
|
1295
|
+
if (!isAddress(opts.to)) fail('invalid', `${String(opts.to)} is not an address`);
|
|
1296
|
+
if (opts.amount <= 0n) fail('invalid', 'the amount must be positive');
|
|
1297
|
+
return this.#seeded(async () => {
|
|
1222
1298
|
const to: Destination = { kind: 'public', address: opts.to, unwrap: opts.unwrap };
|
|
1223
1299
|
return this.#newPayment('withdraw', opts.token, opts.amount, { to, maxFee: opts.maxFee, step: null, paid: 0n });
|
|
1224
1300
|
});
|
|
@@ -7,18 +7,27 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export function aspLeaves(bitmap: Uint8Array, first: bigint, count: number): Uint8Array;
|
|
9
9
|
|
|
10
|
+
export function burnAddressOf(chain_id: bigint, public_key: string, salt: string): string | undefined;
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
13
|
+
* The `index`-th burn address of `public_key` on one chain and token from `burn_key`, and its salt, if the salt's
|
|
14
|
+
* counter is in `from..=to`: `{salt, address}` or `undefined`. Searched in blocks of counters from 0, the result is
|
|
15
|
+
* the first block's hit.
|
|
12
16
|
*/
|
|
13
|
-
export function
|
|
17
|
+
export function burnBlock(burn_key: Uint8Array, chain_id: bigint, token_id: number, index: number, public_key: string, from: number, to: number): any;
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
/**
|
|
20
|
+
* A burn address of someone else's `public_key` on a chain, with its salt: the first of the `count` salts from
|
|
21
|
+
* `start` that has the proof of work, `{salt, address}`, or `undefined`. A random `start` makes the salt
|
|
22
|
+
* unguessable.
|
|
23
|
+
*/
|
|
24
|
+
export function burnBlockFor(chain_id: bigint, public_key: string, start: string, count: number): any;
|
|
16
25
|
|
|
17
26
|
/**
|
|
18
27
|
* The key that owns the notes paid to a seed's `index`-th receive code for one chain and token, shaped like
|
|
19
28
|
* `spendKey`.
|
|
20
29
|
*/
|
|
21
|
-
export function
|
|
30
|
+
export function codeSpendKey(seed: Uint8Array, chain_id: bigint, token_id: number, index: number): any;
|
|
22
31
|
|
|
23
32
|
/**
|
|
24
33
|
* `CommitmentLedger`'s hash chain after appending a commitment with `fields` to `prev`.
|
|
@@ -64,12 +73,6 @@ export function path(i: bigint, n: bigint, chunk_leaves: Uint8Array, page_chunks
|
|
|
64
73
|
*/
|
|
65
74
|
export function pathRoot(leaf: string, i: bigint, siblings: string[]): string;
|
|
66
75
|
|
|
67
|
-
/**
|
|
68
|
-
* The `k`-th burn address a point of sale holding `lane_key` makes for `public_key` on one chain and token, with its
|
|
69
|
-
* salt: `{salt, address}`. About 2^16 hashes.
|
|
70
|
-
*/
|
|
71
|
-
export function posBurnAddress(lane_key: Uint8Array, chain_id: bigint, token_id: number, k: number, public_key: string): any;
|
|
72
|
-
|
|
73
76
|
/**
|
|
74
77
|
* Seals a merge witness (JSON) to the merge prover's key, padded with spaces to `pad_to` bytes. Take the key from
|
|
75
78
|
* `verifyAttestation`, as for the attester's.
|
|
@@ -123,9 +126,10 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
|
|
|
123
126
|
export interface InitOutput {
|
|
124
127
|
readonly memory: WebAssembly.Memory;
|
|
125
128
|
readonly aspLeaves: (a: number, b: number, c: bigint, d: number) => [number, number, number, number];
|
|
126
|
-
readonly burnAddress: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
|
|
127
129
|
readonly burnAddressOf: (a: bigint, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
128
|
-
readonly
|
|
130
|
+
readonly burnBlock: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number];
|
|
131
|
+
readonly burnBlockFor: (a: bigint, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
|
|
132
|
+
readonly codeSpendKey: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
|
|
129
133
|
readonly hashChain: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
|
130
134
|
readonly leaf: (a: number, b: number) => [number, number, number, number];
|
|
131
135
|
readonly leaves: (a: number, b: number) => [number, number, number, number];
|
|
@@ -134,7 +138,6 @@ export interface InitOutput {
|
|
|
134
138
|
readonly nullifier: (a: bigint, b: bigint, c: number, d: number) => [number, number, number, number];
|
|
135
139
|
readonly path: (a: bigint, b: bigint, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number, number, number];
|
|
136
140
|
readonly pathRoot: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number, number];
|
|
137
|
-
readonly posBurnAddress: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number) => [number, number, number];
|
|
138
141
|
readonly sealMergeWitness: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
139
142
|
readonly sealWitness: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
140
143
|
readonly signMerge: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
|
@@ -20,24 +20,6 @@ export function aspLeaves(bitmap, first, count) {
|
|
|
20
20
|
return v2;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
* A seed's `index`-th burn address for one chain and token, with its salt: `{salt, address}`. About 2^16 hashes.
|
|
25
|
-
* @param {Uint8Array} seed
|
|
26
|
-
* @param {bigint} chain_id
|
|
27
|
-
* @param {number} token_id
|
|
28
|
-
* @param {number} index
|
|
29
|
-
* @returns {any}
|
|
30
|
-
*/
|
|
31
|
-
export function burnAddress(seed, chain_id, token_id, index) {
|
|
32
|
-
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
|
|
33
|
-
const len0 = WASM_VECTOR_LEN;
|
|
34
|
-
const ret = wasm.burnAddress(ptr0, len0, chain_id, token_id, index);
|
|
35
|
-
if (ret[2]) {
|
|
36
|
-
throw takeFromExternrefTable0(ret[1]);
|
|
37
|
-
}
|
|
38
|
-
return takeFromExternrefTable0(ret[0]);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
23
|
/**
|
|
42
24
|
* @param {bigint} chain_id
|
|
43
25
|
* @param {string} public_key
|
|
@@ -61,6 +43,53 @@ export function burnAddressOf(chain_id, public_key, salt) {
|
|
|
61
43
|
return v3;
|
|
62
44
|
}
|
|
63
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The `index`-th burn address of `public_key` on one chain and token from `burn_key`, and its salt, if the salt's
|
|
48
|
+
* counter is in `from..=to`: `{salt, address}` or `undefined`. Searched in blocks of counters from 0, the result is
|
|
49
|
+
* the first block's hit.
|
|
50
|
+
* @param {Uint8Array} burn_key
|
|
51
|
+
* @param {bigint} chain_id
|
|
52
|
+
* @param {number} token_id
|
|
53
|
+
* @param {number} index
|
|
54
|
+
* @param {string} public_key
|
|
55
|
+
* @param {number} from
|
|
56
|
+
* @param {number} to
|
|
57
|
+
* @returns {any}
|
|
58
|
+
*/
|
|
59
|
+
export function burnBlock(burn_key, chain_id, token_id, index, public_key, from, to) {
|
|
60
|
+
const ptr0 = passArray8ToWasm0(burn_key, wasm.__wbindgen_malloc);
|
|
61
|
+
const len0 = WASM_VECTOR_LEN;
|
|
62
|
+
const ptr1 = passStringToWasm0(public_key, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
63
|
+
const len1 = WASM_VECTOR_LEN;
|
|
64
|
+
const ret = wasm.burnBlock(ptr0, len0, chain_id, token_id, index, ptr1, len1, from, to);
|
|
65
|
+
if (ret[2]) {
|
|
66
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
67
|
+
}
|
|
68
|
+
return takeFromExternrefTable0(ret[0]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A burn address of someone else's `public_key` on a chain, with its salt: the first of the `count` salts from
|
|
73
|
+
* `start` that has the proof of work, `{salt, address}`, or `undefined`. A random `start` makes the salt
|
|
74
|
+
* unguessable.
|
|
75
|
+
* @param {bigint} chain_id
|
|
76
|
+
* @param {string} public_key
|
|
77
|
+
* @param {string} start
|
|
78
|
+
* @param {number} count
|
|
79
|
+
* @returns {any}
|
|
80
|
+
*/
|
|
81
|
+
export function burnBlockFor(chain_id, public_key, start, count) {
|
|
82
|
+
const ptr0 = passStringToWasm0(public_key, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
83
|
+
const len0 = WASM_VECTOR_LEN;
|
|
84
|
+
const ptr1 = passStringToWasm0(start, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
85
|
+
const len1 = WASM_VECTOR_LEN;
|
|
86
|
+
const ret = wasm.burnBlockFor(chain_id, ptr0, len0, ptr1, len1, count);
|
|
87
|
+
if (ret[2]) {
|
|
88
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
89
|
+
}
|
|
90
|
+
return takeFromExternrefTable0(ret[0]);
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
/**
|
|
65
94
|
* The key that owns the notes paid to a seed's `index`-th receive code for one chain and token, shaped like
|
|
66
95
|
* `spendKey`.
|
|
@@ -70,10 +99,10 @@ export function burnAddressOf(chain_id, public_key, salt) {
|
|
|
70
99
|
* @param {number} index
|
|
71
100
|
* @returns {any}
|
|
72
101
|
*/
|
|
73
|
-
export function
|
|
102
|
+
export function codeSpendKey(seed, chain_id, token_id, index) {
|
|
74
103
|
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
|
|
75
104
|
const len0 = WASM_VECTOR_LEN;
|
|
76
|
-
const ret = wasm.
|
|
105
|
+
const ret = wasm.codeSpendKey(ptr0, len0, chain_id, token_id, index);
|
|
77
106
|
if (ret[2]) {
|
|
78
107
|
throw takeFromExternrefTable0(ret[1]);
|
|
79
108
|
}
|
|
@@ -295,28 +324,6 @@ export function pathRoot(leaf, i, siblings) {
|
|
|
295
324
|
}
|
|
296
325
|
}
|
|
297
326
|
|
|
298
|
-
/**
|
|
299
|
-
* The `k`-th burn address a point of sale holding `lane_key` makes for `public_key` on one chain and token, with its
|
|
300
|
-
* salt: `{salt, address}`. About 2^16 hashes.
|
|
301
|
-
* @param {Uint8Array} lane_key
|
|
302
|
-
* @param {bigint} chain_id
|
|
303
|
-
* @param {number} token_id
|
|
304
|
-
* @param {number} k
|
|
305
|
-
* @param {string} public_key
|
|
306
|
-
* @returns {any}
|
|
307
|
-
*/
|
|
308
|
-
export function posBurnAddress(lane_key, chain_id, token_id, k, public_key) {
|
|
309
|
-
const ptr0 = passArray8ToWasm0(lane_key, wasm.__wbindgen_malloc);
|
|
310
|
-
const len0 = WASM_VECTOR_LEN;
|
|
311
|
-
const ptr1 = passStringToWasm0(public_key, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
312
|
-
const len1 = WASM_VECTOR_LEN;
|
|
313
|
-
const ret = wasm.posBurnAddress(ptr0, len0, chain_id, token_id, k, ptr1, len1);
|
|
314
|
-
if (ret[2]) {
|
|
315
|
-
throw takeFromExternrefTable0(ret[1]);
|
|
316
|
-
}
|
|
317
|
-
return takeFromExternrefTable0(ret[0]);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
327
|
/**
|
|
321
328
|
* Seals a merge witness (JSON) to the merge prover's key, padded with spaces to `pad_to` bytes. Take the key from
|
|
322
329
|
* `verifyAttestation`, as for the attester's.
|
|
Binary file
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
export const memory: WebAssembly.Memory;
|
|
4
4
|
export const aspLeaves: (a: number, b: number, c: bigint, d: number) => [number, number, number, number];
|
|
5
|
-
export const burnAddress: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
|
|
6
5
|
export const burnAddressOf: (a: bigint, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
7
|
-
export const
|
|
6
|
+
export const burnBlock: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number];
|
|
7
|
+
export const burnBlockFor: (a: bigint, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
|
|
8
|
+
export const codeSpendKey: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
|
|
8
9
|
export const hashChain: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
|
9
10
|
export const leaf: (a: number, b: number) => [number, number, number, number];
|
|
10
11
|
export const leaves: (a: number, b: number) => [number, number, number, number];
|
|
@@ -13,7 +14,6 @@ export const noteTag: (a: number, b: number, c: number) => [number, number, numb
|
|
|
13
14
|
export const nullifier: (a: bigint, b: bigint, c: number, d: number) => [number, number, number, number];
|
|
14
15
|
export const path: (a: bigint, b: bigint, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number, number, number];
|
|
15
16
|
export const pathRoot: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number, number];
|
|
16
|
-
export const posBurnAddress: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number) => [number, number, number];
|
|
17
17
|
export const sealMergeWitness: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
18
18
|
export const sealWitness: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
|
19
19
|
export const signMerge: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|