@zerc20/wallet-sdk 0.1.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/chain.d.ts +19 -5
- package/dist/chain.js +17 -11
- package/dist/chain.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/errors.d.ts +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- 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/services.d.ts +5 -5
- package/dist/services.js +10 -7
- package/dist/services.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 +27 -13
- package/dist/wallet.js +125 -62
- package/dist/wallet.js.map +1 -1
- package/package.json +4 -3
- package/src/burnWorker.ts +9 -10
- package/src/chain.ts +35 -12
- package/src/context.ts +48 -8
- package/src/core.ts +279 -87
- package/src/errors.ts +2 -0
- package/src/index.ts +2 -2
- package/src/pos.ts +2 -2
- package/src/rageExit.ts +3 -4
- package/src/seed.ts +13 -1
- package/src/services.ts +18 -7
- package/src/spend.ts +5 -5
- package/src/state.ts +17 -7
- package/src/sync.ts +95 -24
- package/src/wallet.ts +159 -64
- 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,15 +19,17 @@ 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,
|
|
25
26
|
hexToBytes,
|
|
26
27
|
isAddress,
|
|
27
28
|
numberToHex,
|
|
29
|
+
zeroAddress,
|
|
28
30
|
} from 'viem';
|
|
29
|
-
import { Chain, NATIVE, type Token, erc20Abi, tokenAbi } from './chain.js';
|
|
30
|
-
import { type Ctx, type SpendKey, chainCtx, chainState,
|
|
31
|
+
import { Chain, MAX_REFERRAL_BPS, NATIVE, type Referrer, type Token, erc20Abi, tokenAbi } from './chain.js';
|
|
32
|
+
import { type Ctx, type SpendKey, chainCtx, chainState, owner, switched, walletOn, walletSees } from './context.js';
|
|
31
33
|
import { burnAddressFor, core, initCore } from './core.js';
|
|
32
34
|
import { type ErrorCode, WalletError, fail, guard } from './errors.js';
|
|
33
35
|
import { type Network, checkNetwork } from './network.js';
|
|
@@ -61,10 +63,14 @@ import {
|
|
|
61
63
|
emptyWalletState,
|
|
62
64
|
openState,
|
|
63
65
|
sealState,
|
|
66
|
+
stateKey,
|
|
67
|
+
unspent,
|
|
64
68
|
} from './state.js';
|
|
65
69
|
import {
|
|
66
70
|
WAIT_MS,
|
|
67
71
|
commitmentTx,
|
|
72
|
+
keyAll,
|
|
73
|
+
nextBurn,
|
|
68
74
|
recordBurn,
|
|
69
75
|
storageState,
|
|
70
76
|
syncAll,
|
|
@@ -74,21 +80,30 @@ import {
|
|
|
74
80
|
unusedBurn,
|
|
75
81
|
} from './sync.js';
|
|
76
82
|
|
|
77
|
-
const DAY_MS = 86_400_000;
|
|
78
83
|
/** The ledger's bound outside an emergency. */
|
|
79
84
|
const NO_BOUND = 2n ** 64n - 1n;
|
|
80
85
|
|
|
81
86
|
export interface OpenOptions {
|
|
82
87
|
network: Network;
|
|
83
|
-
/** The
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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>);
|
|
87
100
|
/** Where the encrypted state lives between sessions. */
|
|
88
101
|
persist: { load(): Promise<Uint8Array | null | undefined>; save(bytes: Uint8Array): Promise<void> };
|
|
89
102
|
/** Where a wallet with no saved state starts looking: the block its seed was first used at, or roughly when. Without
|
|
90
103
|
* it, an address registered before is looked for from the chain's start, and a new one from now on. */
|
|
91
104
|
birthday?: bigint | Date;
|
|
105
|
+
/** The app opening the wallet, when it takes a share of what reaches the deposit addresses it hands out. */
|
|
106
|
+
referrer?: Referrer;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
/** What a deposit, or a deposit address, waits on: for an app to show while it does. */
|
|
@@ -105,7 +120,7 @@ export type DepositStep =
|
|
|
105
120
|
|
|
106
121
|
/** Why an amount is not spendable yet. */
|
|
107
122
|
export type PendingReason =
|
|
108
|
-
/**
|
|
123
|
+
/** A note this device's payment, withdrawal or merge under way is making, not yet read on chain. */
|
|
109
124
|
| 'incoming'
|
|
110
125
|
/** Waiting for a proved root that covers it (minutes). */
|
|
111
126
|
| 'awaiting-root'
|
|
@@ -154,7 +169,7 @@ export interface Activity {
|
|
|
154
169
|
txHash: Hex | null;
|
|
155
170
|
/** `done` once on chain (in a block: a reorg may still take it back until final), `failed` for a payment or
|
|
156
171
|
* withdrawal that cannot land, shown only on the device that tried it. What is not on chain yet is not activity:
|
|
157
|
-
* an operation under way is in `operations()`,
|
|
172
|
+
* an operation under way is in `operations()`, and the note it is making `incoming` in `balances()`. */
|
|
158
173
|
status: 'done' | 'failed';
|
|
159
174
|
}
|
|
160
175
|
|
|
@@ -236,10 +251,13 @@ export class Wallet {
|
|
|
236
251
|
/** Calls running or queued. */
|
|
237
252
|
#busy = 0;
|
|
238
253
|
#release = () => {};
|
|
254
|
+
#referrer: Referrer | undefined;
|
|
255
|
+
readonly #unlock: (() => Promise<Uint8Array>) | undefined;
|
|
239
256
|
#closed = new AbortController();
|
|
240
257
|
|
|
241
|
-
private constructor(ctx: Ctx) {
|
|
258
|
+
private constructor(ctx: Ctx, unlock?: () => Promise<Uint8Array>) {
|
|
242
259
|
this.#ctx = ctx;
|
|
260
|
+
this.#unlock = unlock;
|
|
243
261
|
}
|
|
244
262
|
|
|
245
263
|
/** Lets this wallet open again: in another tab, or here for another account. This object is done with: what is
|
|
@@ -256,20 +274,32 @@ export class Wallet {
|
|
|
256
274
|
|
|
257
275
|
/** Opens the wallet. The first time on this address, registers it for payments: one signature. */
|
|
258
276
|
static async open(opts: OpenOptions): Promise<Wallet> {
|
|
259
|
-
await initCore();
|
|
260
277
|
checkNetwork(opts.network);
|
|
261
|
-
|
|
262
|
-
const
|
|
278
|
+
// the factory refuses these, which the relayer would report as its own failure
|
|
279
|
+
const r = opts.referrer;
|
|
280
|
+
if (r) {
|
|
281
|
+
if (!isAddress(r.address, { strict: false }) || r.address.toLowerCase() === zeroAddress)
|
|
282
|
+
fail('invalid', `the referrer ${r.address} is not an address to pay`);
|
|
283
|
+
if (!Number.isInteger(r.bps) || r.bps < 0 || r.bps > MAX_REFERRAL_BPS)
|
|
284
|
+
fail('invalid', `a referrer takes 0 to ${MAX_REFERRAL_BPS} bps, not ${r.bps}`);
|
|
285
|
+
}
|
|
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();
|
|
263
293
|
const release = await lockWallet(account.address);
|
|
264
294
|
try {
|
|
265
|
-
return await Wallet.#open(opts, account, release);
|
|
295
|
+
return await Wallet.#open(opts, seed, account, release);
|
|
266
296
|
} catch (e) {
|
|
267
297
|
release();
|
|
268
298
|
throw e;
|
|
269
299
|
}
|
|
270
300
|
}
|
|
271
301
|
|
|
272
|
-
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> {
|
|
273
303
|
const address = account.address;
|
|
274
304
|
const chains = opts.network.chains.map((config) => ({
|
|
275
305
|
config,
|
|
@@ -282,16 +312,23 @@ export class Wallet {
|
|
|
282
312
|
if (code && code !== '0x' && !code.startsWith('0xef0100'))
|
|
283
313
|
fail('unsupported-account', 'contract accounts cannot sign for a wallet');
|
|
284
314
|
const signer: Signer = async (message) =>
|
|
285
|
-
hexToBytes(
|
|
315
|
+
hexToBytes(
|
|
316
|
+
await guard(
|
|
317
|
+
account.type === 'local'
|
|
318
|
+
? account.signMessage({ message })
|
|
319
|
+
: opts.walletClient!.signMessage({ account, message }),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
286
322
|
const storage = await CanisterClient.connect(opts.network.storage.host, opts.network.storage.canisterId);
|
|
287
|
-
const publicKey = hexToBytes((core.spendKey(
|
|
323
|
+
const publicKey = hexToBytes((core.spendKey(seed) as SpendKey).publicKey);
|
|
288
324
|
const saved = await opts.persist.load();
|
|
289
325
|
// registered already: used before, here or on another device, so its past is looked for
|
|
290
326
|
// ponytail: from genesis (~4 MB per million commitments); ask for a birthday once the chain has many
|
|
291
327
|
const restoring =
|
|
292
328
|
!saved?.length && opts.birthday === undefined && !!(await storage.registration(hexToBytes(address)));
|
|
293
|
-
const view = await registeredViewKey(storage,
|
|
294
|
-
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;
|
|
295
332
|
// never started over in place: the first save would overwrite what it held, operations in flight included
|
|
296
333
|
if (saved?.length && !loaded) {
|
|
297
334
|
fail(
|
|
@@ -304,26 +341,60 @@ export class Wallet {
|
|
|
304
341
|
const state: WalletState = loaded ?? emptyWalletState(address);
|
|
305
342
|
const ctx: Ctx = {
|
|
306
343
|
network: opts.network,
|
|
307
|
-
seed
|
|
344
|
+
seed,
|
|
345
|
+
owners: new Map(),
|
|
308
346
|
address,
|
|
309
|
-
|
|
347
|
+
account,
|
|
348
|
+
walletClient: opts.walletClient ?? null,
|
|
310
349
|
storage,
|
|
311
350
|
view,
|
|
312
351
|
chains,
|
|
313
352
|
state,
|
|
314
|
-
save: () => opts.persist.save(sealState(
|
|
353
|
+
save: () => opts.persist.save(sealState(key, state)),
|
|
315
354
|
lastFullRefreshMs: 0,
|
|
316
355
|
};
|
|
317
356
|
if (!loaded) await start(ctx, restoring ? 0n : opts.birthday);
|
|
318
|
-
const wallet = new Wallet(ctx);
|
|
357
|
+
const wallet = new Wallet(ctx, typeof opts.seed === 'function' ? opts.seed : undefined);
|
|
319
358
|
wallet.#release = release;
|
|
359
|
+
wallet.#referrer = opts.referrer;
|
|
320
360
|
// a saved state names its tokens by id, and its coins stand by the chains' roots: read both now, so balances, coins
|
|
321
361
|
// and history work before the first sync
|
|
322
362
|
const [tokens] = await Promise.all([Promise.all(chains.map((c) => c.chain.tokens())), wallet.#readRoots()]);
|
|
323
363
|
wallet.#tokens = tokens.flat();
|
|
364
|
+
keyAll(ctx, wallet.#tokens);
|
|
324
365
|
return wallet;
|
|
325
366
|
}
|
|
326
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
|
+
|
|
327
398
|
/** Runs `f` after everything queued before it: one state, changed by one task at a time. */
|
|
328
399
|
#exclusive<T>(f: () => Promise<T>): Promise<T> {
|
|
329
400
|
this.#busy++;
|
|
@@ -359,13 +430,14 @@ export class Wallet {
|
|
|
359
430
|
|
|
360
431
|
/** Catches up: new notes, deposits, spends and roots, then advances every operation in flight. A chain or server that
|
|
361
432
|
* fails holds back only what needs it, and the first error is thrown once the rest is done. Settles before the burn
|
|
362
|
-
* addresses are topped up, which can take a phone
|
|
433
|
+
* addresses are topped up, which can take a phone some seconds. */
|
|
363
434
|
sync(): Promise<void> {
|
|
364
435
|
return new Promise((resolve, reject) => {
|
|
365
436
|
this.#exclusive(async () => {
|
|
366
437
|
await this.#catchUp().then(resolve, reject);
|
|
367
438
|
// still this task's: a deposit queued meanwhile finds the one recorded ahead, rather than writing at handout
|
|
368
439
|
await topUpBurns(this.#ctx, this.#closed.signal).catch(() => {}); // tried again after the next sync
|
|
440
|
+
this.#dropSeed();
|
|
369
441
|
}).catch(reject); // closed before it ran
|
|
370
442
|
});
|
|
371
443
|
}
|
|
@@ -411,7 +483,7 @@ export class Wallet {
|
|
|
411
483
|
if (errors.length > 0) throw errors[0];
|
|
412
484
|
}
|
|
413
485
|
|
|
414
|
-
/** 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. */
|
|
415
487
|
autoSync(onChange?: () => void, onError?: (e: unknown) => void): () => void {
|
|
416
488
|
let stopped = false;
|
|
417
489
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -424,7 +496,7 @@ export class Wallet {
|
|
|
424
496
|
}
|
|
425
497
|
if (stopped || this.#closed.signal.aborted) return;
|
|
426
498
|
const busy = this.#ctx.state.ops.some((o) => o.status === 'pending' && Date.now() - o.createdMs < WAIT_MS * 4);
|
|
427
|
-
timer = setTimeout(tick, busy ?
|
|
499
|
+
timer = setTimeout(tick, busy ? 3_000 : 10_000);
|
|
428
500
|
};
|
|
429
501
|
void tick();
|
|
430
502
|
return () => {
|
|
@@ -447,18 +519,22 @@ export class Wallet {
|
|
|
447
519
|
|
|
448
520
|
balances(): Balance[] {
|
|
449
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
|
+
);
|
|
450
531
|
return this.#tokens.map((token) => {
|
|
451
532
|
const { chainId, tokenId } = token;
|
|
452
533
|
const b: Balance = { token, spendable: 0n, pending: [], blocked: 0n };
|
|
453
|
-
// a merge that never landed (another device spent an input first) stops counting after a day; a zero-value
|
|
454
|
-
// change, there to carry its payment's receipt, never counts. A note is a coin once its commitment is read, so
|
|
455
|
-
// until then it is incoming, as a payment's change is between its relay and the next sync
|
|
456
534
|
for (const n of this.#ctx.state.notes.filter(
|
|
457
|
-
(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),
|
|
458
536
|
)) {
|
|
459
|
-
|
|
460
|
-
n.receipt?.kind === 'merged' && n.atNs !== null && Date.now() - Number(n.atNs / 1_000_000n) > DAY_MS;
|
|
461
|
-
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' });
|
|
462
538
|
}
|
|
463
539
|
for (const c of coins.filter((c) => c.token === token)) {
|
|
464
540
|
if (c.state === 'spendable') b.spendable += c.amount;
|
|
@@ -470,13 +546,14 @@ export class Wallet {
|
|
|
470
546
|
}
|
|
471
547
|
|
|
472
548
|
/** Every unspent note and deposit on chain, and where each stands, as of opening or the last sync. A note announced
|
|
473
|
-
* 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. */
|
|
474
550
|
coins(): CoinInfo[] {
|
|
475
551
|
const size = (chainId: bigint) => this.#status.get(chainId)?.latestRoot?.size ?? 0;
|
|
476
552
|
const unproved = (chainId: bigint) => (this.#status.get(chainId)?.halted ? 'roots-halted' : 'awaiting-root');
|
|
477
553
|
const spending = new Set(this.#ctx.state.ops.flatMap((o) => inputsOf(o).map((c) => `${o.chainId}:${c.index}`)));
|
|
478
|
-
// spent by a JoinSplit in a block, which waits only to be final: gone, as activity() counts it
|
|
479
|
-
//
|
|
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)
|
|
480
557
|
const landed = new Set(
|
|
481
558
|
this.#ctx.state.ops.flatMap((o) =>
|
|
482
559
|
o.kind !== 'deposit' && o.data.step?.stage === 'relayed'
|
|
@@ -487,7 +564,7 @@ export class Wallet {
|
|
|
487
564
|
const out: CoinInfo[] = [];
|
|
488
565
|
// a zero-value change carries its payment's receipt, and is no coin
|
|
489
566
|
for (const n of this.#ctx.state.notes.filter((n) => n.value > 0n)) {
|
|
490
|
-
for (const f of n.found.filter(
|
|
567
|
+
for (const f of n.found.filter(unspent)) {
|
|
491
568
|
const id = `${n.chainId}:${f.index}`;
|
|
492
569
|
// a 4-byte tag alone matches someone else's now and then: a coin once its commitment is read
|
|
493
570
|
if (!f.confirmed || landed.has(id)) continue;
|
|
@@ -505,7 +582,7 @@ export class Wallet {
|
|
|
505
582
|
}
|
|
506
583
|
for (const d of this.#ctx.state.deposits) {
|
|
507
584
|
// a coin once its commitment is read, which names the amount
|
|
508
|
-
for (const f of d.found.filter((f) =>
|
|
585
|
+
for (const f of d.found.filter((f) => unspent(f) && f.amount !== null)) {
|
|
509
586
|
const id = `${d.chainId}:${f.index}`;
|
|
510
587
|
if (landed.has(id)) continue;
|
|
511
588
|
const state =
|
|
@@ -755,7 +832,7 @@ export class Wallet {
|
|
|
755
832
|
/** Refunds deposit `id` to its sender without the ASP, the attester or a proved root: for a deposit the ASP
|
|
756
833
|
* rejected, or when those services stop. The account's wallet sends the transactions and pays their gas. */
|
|
757
834
|
rageExit(id: string): Promise<Hex[]> {
|
|
758
|
-
return this.#
|
|
835
|
+
return this.#seeded(async () => {
|
|
759
836
|
const [chain, index] = id.split(':');
|
|
760
837
|
const record =
|
|
761
838
|
this.#ctx.state.deposits.find((d) => `${d.chainId}` === chain && d.found.some((f) => `${f.index}` === index)) ??
|
|
@@ -861,22 +938,26 @@ export class Wallet {
|
|
|
861
938
|
* With `wrapped`, it pays from the account's zERC20 itself, as a withdrawal pays out, rather than its underlying
|
|
862
939
|
* asset: a plain transfer into the burn address, with no wrap and no approval.
|
|
863
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
|
+
*
|
|
864
944
|
* Returns once the wrap is sent: the operation is done once it is mined, which `wait()` or the next sync finds.
|
|
865
945
|
* `onStep` hears what it waits on meanwhile, for an app to show. */
|
|
866
|
-
deposit(opts: {
|
|
946
|
+
async deposit(opts: {
|
|
867
947
|
token: Token;
|
|
868
948
|
amount: bigint;
|
|
869
949
|
to?: Address;
|
|
870
950
|
wrapped?: boolean;
|
|
951
|
+
from?: WalletClient;
|
|
871
952
|
onStep?: (step: DepositStep) => void;
|
|
872
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;
|
|
873
956
|
const step = opts.onStep ?? (() => {});
|
|
874
957
|
if (this.#busy > 0) step('queued');
|
|
875
958
|
return this.#exclusive(async () => {
|
|
876
959
|
const { token, amount } = opts;
|
|
877
|
-
|
|
878
|
-
const wc = this.#ctx.walletClient;
|
|
879
|
-
const account = wc.account!;
|
|
960
|
+
const account = payer ?? this.#ctx.account;
|
|
880
961
|
const client = chainCtx(this.#ctx, token.chainId).chain.client;
|
|
881
962
|
const { wrapped } = opts;
|
|
882
963
|
const native = !wrapped && token.underlying.toLowerCase() === NATIVE.toLowerCase();
|
|
@@ -893,7 +974,7 @@ export class Wallet {
|
|
|
893
974
|
const of = wrapped ? token.symbol : `${token.symbol}'s underlying asset`;
|
|
894
975
|
fail(
|
|
895
976
|
'insufficient-funds',
|
|
896
|
-
|
|
977
|
+
`${payer ? 'the wallet' : 'the account'} holds ${formatUnits(held, token.decimals)} of ${of}, less than ${formatUnits(amount, token.decimals)}`,
|
|
897
978
|
);
|
|
898
979
|
}
|
|
899
980
|
const address = opts.to
|
|
@@ -907,8 +988,7 @@ export class Wallet {
|
|
|
907
988
|
);
|
|
908
989
|
await this.#ctx.save();
|
|
909
990
|
try {
|
|
910
|
-
|
|
911
|
-
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);
|
|
912
992
|
if (!native && !wrapped) {
|
|
913
993
|
const allowance = await client.readContract({
|
|
914
994
|
address: token.underlying,
|
|
@@ -1002,8 +1082,8 @@ export class Wallet {
|
|
|
1002
1082
|
|
|
1003
1083
|
/** An address that takes `token`'s underlying asset from anywhere (an exchange withdrawal, another wallet) and
|
|
1004
1084
|
* deposits it privately, less `fee` (in the underlying's base units) to the deposit relayer each time: what arrives
|
|
1005
|
-
* must exceed it.
|
|
1006
|
-
* waits on (`queued`, `burn-address`). */
|
|
1085
|
+
* must exceed it. A wallet opened with a `referrer` also pays it its `bps` of the rest. Watched for 7 days by the
|
|
1086
|
+
* deposit relayer; refunds go back to this account. `onStep` hears what it waits on (`queued`, `burn-address`). */
|
|
1007
1087
|
depositAddress(
|
|
1008
1088
|
token: Token,
|
|
1009
1089
|
opts: { onStep?: (step: DepositStep) => void } = {},
|
|
@@ -1013,7 +1093,13 @@ export class Wallet {
|
|
|
1013
1093
|
return this.#exclusive(async () => {
|
|
1014
1094
|
const c = chainCtx(this.#ctx, token.chainId);
|
|
1015
1095
|
const record = await this.#burnAddress(token, null, () => step('burn-address'));
|
|
1016
|
-
const { depositAddress, fee } = await registerDeposit(
|
|
1096
|
+
const { depositAddress, fee } = await registerDeposit(
|
|
1097
|
+
c.chain,
|
|
1098
|
+
token.address,
|
|
1099
|
+
record.address,
|
|
1100
|
+
this.address,
|
|
1101
|
+
this.#referrer,
|
|
1102
|
+
);
|
|
1017
1103
|
record.depositAddress = depositAddress;
|
|
1018
1104
|
// for the wallet's other devices, to show what comes through it: written by the next sync
|
|
1019
1105
|
const burnAddress = record.address.toLowerCase() as Hex;
|
|
@@ -1037,19 +1123,21 @@ export class Wallet {
|
|
|
1037
1123
|
* nothing; it names neither this address nor its public key. Anyone who gets the code can pay it, and see what its
|
|
1038
1124
|
* other payers paid privately: give each payer its own. It is checksummed text (bech32m, `zerc201…`). */
|
|
1039
1125
|
receiveCode(token: Token): Promise<string> {
|
|
1040
|
-
|
|
1041
|
-
|
|
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 () => {
|
|
1042
1131
|
const { burnIndex, address } = await this.#burnAddress(token);
|
|
1132
|
+
if (!owner(ctx, code(burnIndex))) await this.#seed(); // calls queued before took the next ones
|
|
1043
1133
|
return encodeCode({
|
|
1044
1134
|
chainId: token.chainId,
|
|
1045
1135
|
tokenId: token.tokenId,
|
|
1046
1136
|
burnAddress: hexToBytes(address),
|
|
1047
|
-
publicKey: hexToBytes(
|
|
1048
|
-
spendKey(ctx, { chainId: token.chainId, tokenId: token.tokenId, index: burnIndex }).publicKey,
|
|
1049
|
-
),
|
|
1137
|
+
publicKey: hexToBytes(owner(ctx, code(burnIndex))!.publicKey),
|
|
1050
1138
|
key: codeKey(ctx.view.seed, token.chainId, token.tokenId, burnIndex),
|
|
1051
1139
|
});
|
|
1052
|
-
});
|
|
1140
|
+
}, !derived);
|
|
1053
1141
|
}
|
|
1054
1142
|
|
|
1055
1143
|
/** What a send or withdrawal of `amount` would cost now, or a consolidation of what the wallet holds. A send or
|
|
@@ -1134,10 +1222,11 @@ export class Wallet {
|
|
|
1134
1222
|
* what is spendable now, or it fails here: `needs-consolidation`, `funds-settling` or `insufficient-funds`.
|
|
1135
1223
|
* `amount` is what the recipient gets, and `maxFee` caps what it pays on top, `fees({ kind: 'send' }).fee`: the
|
|
1136
1224
|
* relayer names its fee, so a quote the user saw is the cap. */
|
|
1137
|
-
send(opts: { to: string; token: Token; amount: bigint; maxFee: bigint }): Promise<Operation> {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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 () => {
|
|
1141
1230
|
let to: Destination;
|
|
1142
1231
|
if (isAddress(opts.to)) {
|
|
1143
1232
|
const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(opts.to));
|
|
@@ -1169,7 +1258,7 @@ export class Wallet {
|
|
|
1169
1258
|
* touched; one still settling is waited for. `maxFee` caps the fees of every merge together,
|
|
1170
1259
|
* `fees({ kind: 'consolidate' }).fee`. */
|
|
1171
1260
|
consolidate(opts: { token: Token; coins?: string[]; maxFee: bigint }): Promise<Operation> {
|
|
1172
|
-
return this.#
|
|
1261
|
+
return this.#seeded(async () => {
|
|
1173
1262
|
if (!this.#ctx.network.mergeProver) fail('invalid', 'this network has no merge prover');
|
|
1174
1263
|
const to: Destination = { kind: 'self' };
|
|
1175
1264
|
const data: SpendData = { to, maxFee: opts.maxFee, step: null, paid: 0n };
|
|
@@ -1196,10 +1285,16 @@ export class Wallet {
|
|
|
1196
1285
|
/** Withdraws `amount` publicly to `to`, as zERC20 or with `unwrap` the underlying asset itself, so `to` needs no gas
|
|
1197
1286
|
* to unwrap. One JoinSplit, as `send`, and as there `to` gets all of `amount` and `maxFee` caps what is paid on top
|
|
1198
1287
|
* (`fees({ kind: 'withdraw' }).fee`). */
|
|
1199
|
-
withdraw(opts: {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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 () => {
|
|
1203
1298
|
const to: Destination = { kind: 'public', address: opts.to, unwrap: opts.unwrap };
|
|
1204
1299
|
return this.#newPayment('withdraw', opts.token, opts.amount, { to, maxFee: opts.maxFee, step: null, paid: 0n });
|
|
1205
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];
|