@zerc20/wallet-sdk 0.3.0 → 0.5.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/README.md +1 -1
- package/dist/accountId.d.ts +5 -0
- package/dist/accountId.js +19 -0
- package/dist/accountId.js.map +1 -0
- package/dist/chain.d.ts +85 -9
- package/dist/chain.js +60 -33
- package/dist/chain.js.map +1 -1
- package/dist/context.d.ts +14 -6
- package/dist/context.js +17 -3
- package/dist/context.js.map +1 -1
- package/dist/core.d.ts +35 -5
- package/dist/core.js +95 -23
- 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 +5 -2
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/pos.d.ts +7 -1
- package/dist/pos.js +213 -55
- package/dist/pos.js.map +1 -1
- package/dist/rageExit.d.ts +27 -4
- package/dist/rageExit.js +160 -49
- package/dist/rageExit.js.map +1 -1
- package/dist/seed.d.ts +4 -0
- package/dist/seed.js +13 -1
- package/dist/seed.js.map +1 -1
- package/dist/servers.d.ts +2 -0
- package/dist/servers.js +1 -0
- package/dist/servers.js.map +1 -1
- package/dist/services.d.ts +11 -4
- package/dist/services.js +26 -7
- package/dist/services.js.map +1 -1
- package/dist/spend.d.ts +30 -9
- package/dist/spend.js +46 -17
- package/dist/spend.js.map +1 -1
- package/dist/state.d.ts +21 -0
- package/dist/state.js.map +1 -1
- package/dist/sync.d.ts +10 -5
- package/dist/sync.js +53 -19
- package/dist/sync.js.map +1 -1
- package/dist/viewKey.d.ts +14 -0
- package/dist/viewKey.js +45 -0
- package/dist/viewKey.js.map +1 -0
- package/dist/wallet.d.ts +109 -16
- package/dist/wallet.js +291 -85
- package/dist/wallet.js.map +1 -1
- package/package.json +3 -2
- package/src/accountId.ts +22 -0
- package/src/chain.ts +95 -49
- package/src/context.ts +35 -8
- package/src/core.ts +109 -24
- package/src/errors.ts +2 -0
- package/src/index.ts +18 -2
- package/src/pos.ts +240 -61
- package/src/rageExit.ts +237 -61
- package/src/seed.ts +24 -1
- package/src/servers.ts +4 -1
- package/src/services.ts +28 -15
- package/src/spend.ts +58 -23
- package/src/state.ts +25 -1
- package/src/sync.ts +57 -20
- package/src/viewKey.ts +49 -0
- package/src/wallet.ts +387 -106
- package/wasm/zerc20_wallet_core.d.ts +8 -1
- package/wasm/zerc20_wallet_core.js +31 -1
- package/wasm/zerc20_wallet_core_bg.wasm +0 -0
- package/wasm/zerc20_wallet_core_bg.wasm.d.ts +1 -0
package/src/wallet.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
encodeCode,
|
|
11
11
|
lookupRecipient,
|
|
12
12
|
nextPosLane,
|
|
13
|
+
notifyKey,
|
|
13
14
|
planDeposit,
|
|
14
15
|
planPosLane,
|
|
15
16
|
posKey,
|
|
@@ -23,25 +24,41 @@ import {
|
|
|
23
24
|
type WalletClient,
|
|
24
25
|
bytesToHex,
|
|
25
26
|
formatUnits,
|
|
27
|
+
getAddress,
|
|
26
28
|
hexToBytes,
|
|
27
29
|
isAddress,
|
|
30
|
+
isAddressEqual,
|
|
28
31
|
numberToHex,
|
|
29
32
|
zeroAddress,
|
|
30
33
|
} from 'viem';
|
|
34
|
+
import { toAccount } from 'viem/accounts';
|
|
35
|
+
import { idAccount } from './accountId.js';
|
|
31
36
|
import { Chain, MAX_REFERRAL_BPS, NATIVE, type Referrer, type Token, erc20Abi, tokenAbi } from './chain.js';
|
|
32
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
type ChainCtx,
|
|
39
|
+
type Ctx,
|
|
40
|
+
type SpendKey,
|
|
41
|
+
chainCtx,
|
|
42
|
+
chainState,
|
|
43
|
+
owner,
|
|
44
|
+
refundSigner,
|
|
45
|
+
switched,
|
|
46
|
+
walletOn,
|
|
47
|
+
walletSees,
|
|
48
|
+
} from './context.js';
|
|
33
49
|
import { burnAddressFor, core, initCore } from './core.js';
|
|
34
50
|
import { type ErrorCode, WalletError, fail, guard } from './errors.js';
|
|
35
51
|
import { type Network, checkNetwork } from './network.js';
|
|
36
52
|
import { Asp, Indexer, type IndexerStatus, PAGE } from './servers.js';
|
|
37
|
-
import { rageExit } from './rageExit.js';
|
|
38
|
-
import { registerDeposit, relayerFees } from './services.js';
|
|
53
|
+
import { type Route, flushDepositAddress, rageExit, refundDepositAddress } from './rageExit.js';
|
|
54
|
+
import { registerDeposit, relayerFees, rewatchDeposit } from './services.js';
|
|
39
55
|
import {
|
|
40
56
|
type Destination,
|
|
41
57
|
type MergeStep,
|
|
42
58
|
type SpendData,
|
|
43
59
|
type Step,
|
|
44
60
|
advanceSpend,
|
|
61
|
+
bpsOf,
|
|
45
62
|
chosenSettling,
|
|
46
63
|
coins,
|
|
47
64
|
grossed,
|
|
@@ -57,7 +74,9 @@ import {
|
|
|
57
74
|
withdrawalOf,
|
|
58
75
|
} from './spend.js';
|
|
59
76
|
import {
|
|
77
|
+
type DepositAddressReceipt,
|
|
60
78
|
type DepositOperation,
|
|
79
|
+
type DepositRecord,
|
|
61
80
|
type OperationRecord,
|
|
62
81
|
type WalletState,
|
|
63
82
|
emptyWalletState,
|
|
@@ -79,6 +98,7 @@ import {
|
|
|
79
98
|
topUpBurns,
|
|
80
99
|
unusedBurn,
|
|
81
100
|
} from './sync.js';
|
|
101
|
+
import { decodeViewKey, encodeViewKey } from './viewKey.js';
|
|
82
102
|
|
|
83
103
|
/** The ledger's bound outside an emergency. */
|
|
84
104
|
const NO_BOUND = 2n ** 64n - 1n;
|
|
@@ -106,6 +126,11 @@ export interface OpenOptions {
|
|
|
106
126
|
referrer?: Referrer;
|
|
107
127
|
}
|
|
108
128
|
|
|
129
|
+
export interface WatchOptions extends Pick<OpenOptions, 'network' | 'persist' | 'birthday'> {
|
|
130
|
+
/** The account's view key, from its wallet's `viewKey()`. */
|
|
131
|
+
viewKey: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
109
134
|
/** What a deposit, or a deposit address, waits on: for an app to show while it does. */
|
|
110
135
|
export type DepositStep =
|
|
111
136
|
/** Behind another call of this wallet: a sync, or another operation. */
|
|
@@ -151,7 +176,8 @@ export interface Activity {
|
|
|
151
176
|
* paid none. */
|
|
152
177
|
fee: bigint | null;
|
|
153
178
|
/** The other side, where known: a payer's claimed address (unverified), where a payment or withdrawal went, or whom a
|
|
154
|
-
* returned deposit went back to.
|
|
179
|
+
* returned deposit went back to. A payment's other side, `sent`'s or that of a `received` with no `burnAddress`, is a
|
|
180
|
+
* zERC20 account, which an app shows by its zERC20 ID (`accountId`); any other is a public address. */
|
|
155
181
|
counterparty: Address | null;
|
|
156
182
|
/** What the counterparty alone does not say: the receive code a payment went to (it has no counterparty), or the
|
|
157
183
|
* deposit address a deposit came through (its counterparty, which forwarded what was sent to it). */
|
|
@@ -185,8 +211,13 @@ export interface CoinInfo {
|
|
|
185
211
|
state: 'spendable' | Exclude<PendingReason, 'incoming'> | 'spending' | 'blocked';
|
|
186
212
|
/** A note's payer (as claimed, unverified) or a deposit's sender, where known. */
|
|
187
213
|
from: Address | null;
|
|
214
|
+
/** The deposit address of this wallet's that a deposit came through (its `from`), as `Activity.via` names it: its
|
|
215
|
+
* `rageExit` goes where `to` says. */
|
|
216
|
+
via: { depositAddress: Address } | null;
|
|
188
217
|
/** For a deposit in `screening`, when the ASP should decide it: an estimate, null until there is one. */
|
|
189
218
|
screenedBy: Date | null;
|
|
219
|
+
/** When it landed: its block's time, null until read. */
|
|
220
|
+
at: Date | null;
|
|
190
221
|
}
|
|
191
222
|
|
|
192
223
|
export interface DepositInfo {
|
|
@@ -202,12 +233,25 @@ export interface DepositInfo {
|
|
|
202
233
|
state: 'screening' | 'approved' | 'rejected' | 'spent';
|
|
203
234
|
}
|
|
204
235
|
|
|
236
|
+
export interface DepositAddressInfo {
|
|
237
|
+
address: Address;
|
|
238
|
+
token: Token;
|
|
239
|
+
/** What the relayer takes out of each payment through it. Null where its receipt in storage does not say what it was
|
|
240
|
+
* registered with: `flushDepositAddress` needs that, so only the relayer flushes such an address. */
|
|
241
|
+
fee: bigint | null;
|
|
242
|
+
/** When it was handed out, or watched again (`rewatchDepositAddress`), from which the deposit relayer watches it for
|
|
243
|
+
* 7 days: when storage took its receipt. */
|
|
244
|
+
at: Date;
|
|
245
|
+
}
|
|
246
|
+
|
|
205
247
|
/** A send's or withdrawal's recipient gets all of its amount, and the private balance pays `fee` on top. */
|
|
206
248
|
export interface FeeQuote {
|
|
207
249
|
/** To the relayer. */
|
|
208
250
|
relayer: bigint;
|
|
209
251
|
/** The protocol's cut of the public withdrawal: a withdrawal's, or the one that pays the relayer. */
|
|
210
252
|
protocol: bigint;
|
|
253
|
+
/** `protocol` as a rate of that withdrawal, in basis points (35 is 0.35%): lower in tiers as the withdrawal grows. */
|
|
254
|
+
protocolBps: number;
|
|
211
255
|
/** `relayer + protocol`: the `maxFee` this quote passes. */
|
|
212
256
|
fee: bigint;
|
|
213
257
|
/** Transactions needed: one JoinSplit for a send or a withdrawal, a consolidation's merges (each pays the relayer's
|
|
@@ -215,6 +259,16 @@ export interface FeeQuote {
|
|
|
215
259
|
steps: number;
|
|
216
260
|
}
|
|
217
261
|
|
|
262
|
+
/** The most a send or withdrawal can pay now, and what its fees keep back of what is spendable. */
|
|
263
|
+
export interface MaxAmount {
|
|
264
|
+
/** What the recipient would get: 0 where the fees take everything. */
|
|
265
|
+
amount: bigint;
|
|
266
|
+
/** The fees: the payment's, and those of the merges it needs first. Where they take everything, what they would be. */
|
|
267
|
+
fee: bigint;
|
|
268
|
+
/** Merges first, when one JoinSplit cannot spend all of it: each pays the relayer again. */
|
|
269
|
+
merges: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
218
272
|
export type OperationKind = OperationRecord['kind'];
|
|
219
273
|
export type OperationStatus = OperationRecord['status'];
|
|
220
274
|
export type CoinState = CoinInfo['state'];
|
|
@@ -237,7 +291,8 @@ export interface Operation {
|
|
|
237
291
|
/** How far a send, withdrawal or consolidation round has come, for an app to show; null for a deposit, or between
|
|
238
292
|
* rounds. It moves during a sync: read it while one runs. */
|
|
239
293
|
readonly stage: Step['stage'] | MergeStep['stage'] | null;
|
|
240
|
-
/** Whom a deposit pays, when
|
|
294
|
+
/** Whom a deposit pays, when `deposit({ to })` named an address; null otherwise. One to a receive code shows its code
|
|
295
|
+
* as `via` in `activity()`. */
|
|
241
296
|
readonly to: Address | null;
|
|
242
297
|
/** Syncs until the operation is done; throws if it fails. */
|
|
243
298
|
wait(): Promise<void>;
|
|
@@ -253,6 +308,7 @@ export class Wallet {
|
|
|
253
308
|
#release = () => {};
|
|
254
309
|
#referrer: Referrer | undefined;
|
|
255
310
|
readonly #unlock: (() => Promise<Uint8Array>) | undefined;
|
|
311
|
+
#readOnly = false;
|
|
256
312
|
#closed = new AbortController();
|
|
257
313
|
|
|
258
314
|
private constructor(ctx: Ctx, unlock?: () => Promise<Uint8Array>) {
|
|
@@ -272,6 +328,18 @@ export class Wallet {
|
|
|
272
328
|
return this.#ctx.address;
|
|
273
329
|
}
|
|
274
330
|
|
|
331
|
+
/** The account's view key, for `watch` to follow it with on another device: whoever holds it sees what this wallet
|
|
332
|
+
* shows, except the private payments to receive codes, and can spend none of it. Checksummed text (bech32m,
|
|
333
|
+
* `zerc20view1…`). */
|
|
334
|
+
viewKey(): string {
|
|
335
|
+
return encodeViewKey(this.#ctx.view, owner(this.#ctx)!);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Whether this wallet only watches its account (`watch`): it shows what the account holds and spends nothing. */
|
|
339
|
+
get readOnly(): boolean {
|
|
340
|
+
return this.#readOnly;
|
|
341
|
+
}
|
|
342
|
+
|
|
275
343
|
/** Opens the wallet. The first time on this address, registers it for payments: one signature. */
|
|
276
344
|
static async open(opts: OpenOptions): Promise<Wallet> {
|
|
277
345
|
checkNetwork(opts.network);
|
|
@@ -301,12 +369,7 @@ export class Wallet {
|
|
|
301
369
|
|
|
302
370
|
static async #open(opts: OpenOptions, seed: Uint8Array, account: Account, release: () => void): Promise<Wallet> {
|
|
303
371
|
const address = account.address;
|
|
304
|
-
const chains = opts.network
|
|
305
|
-
config,
|
|
306
|
-
chain: new Chain(config),
|
|
307
|
-
indexer: new Indexer(config.indexer),
|
|
308
|
-
asp: new Asp(config.asp),
|
|
309
|
-
}));
|
|
372
|
+
const chains = chainsOf(opts.network);
|
|
310
373
|
const code = await chains[0]?.chain.client.getCode({ address });
|
|
311
374
|
// an EIP-7702 delegated account is still an EOA that signs
|
|
312
375
|
if (code && code !== '0x' && !code.startsWith('0xef0100'))
|
|
@@ -327,39 +390,101 @@ export class Wallet {
|
|
|
327
390
|
const restoring =
|
|
328
391
|
!saved?.length && opts.birthday === undefined && !!(await storage.registration(hexToBytes(address)));
|
|
329
392
|
const view = await registeredViewKey(storage, seed, signer, hexToBytes(address), publicKey);
|
|
330
|
-
const
|
|
393
|
+
const wallet = await Wallet.#begin(
|
|
394
|
+
{
|
|
395
|
+
network: opts.network,
|
|
396
|
+
seed,
|
|
397
|
+
owners: new Map(),
|
|
398
|
+
refundSigners: new Map(),
|
|
399
|
+
address,
|
|
400
|
+
account,
|
|
401
|
+
walletClient: opts.walletClient ?? null,
|
|
402
|
+
storage,
|
|
403
|
+
view,
|
|
404
|
+
chains,
|
|
405
|
+
},
|
|
406
|
+
{ persist: opts.persist, saved, key: stateKey(seed), from: restoring ? 0n : opts.birthday },
|
|
407
|
+
typeof opts.seed === 'function' ? opts.seed : undefined,
|
|
408
|
+
);
|
|
409
|
+
wallet.#release = release;
|
|
410
|
+
wallet.#referrer = opts.referrer;
|
|
411
|
+
return wallet;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Opens an account to follow by its view key (`viewKey()`): its balances, coins and history as its own wallet shows
|
|
415
|
+
* them, less the private payments to its receive codes, whose keys only the seed derives. It spends and writes
|
|
416
|
+
* nothing: a call that would fails with `read-only`. The key must be the account's current one on this network. */
|
|
417
|
+
static async watch(opts: WatchOptions): Promise<Wallet> {
|
|
418
|
+
checkNetwork(opts.network);
|
|
419
|
+
await initCore();
|
|
420
|
+
const { view, owner } = decodeViewKey(opts.viewKey) ?? fail('invalid', 'not a view key');
|
|
421
|
+
const address = getAddress(bytesToHex(view.address));
|
|
422
|
+
const release = await lockWallet(address, 'view');
|
|
423
|
+
try {
|
|
424
|
+
const storage = await CanisterClient.connect(opts.network.storage.host, opts.network.storage.canisterId);
|
|
425
|
+
// what the account signed: a nullifying key that is not the account's would show spent coins as unspent
|
|
426
|
+
const r = await lookupRecipient(storage, view.address);
|
|
427
|
+
if (!r || bytesToHex(r.publicKey) !== owner.publicKey || bytesToHex(r.notifyKey) !== bytesToHex(notifyKey(view)))
|
|
428
|
+
fail('invalid', `not ${address}'s view key on ${opts.network.name}, or one it has replaced`);
|
|
429
|
+
const wallet = await Wallet.#begin(
|
|
430
|
+
{
|
|
431
|
+
network: opts.network,
|
|
432
|
+
seed: null,
|
|
433
|
+
owners: new Map([['', owner]]),
|
|
434
|
+
refundSigners: new Map(),
|
|
435
|
+
address,
|
|
436
|
+
// signs nothing: every call that would send is refused first
|
|
437
|
+
account: toAccount(address),
|
|
438
|
+
walletClient: null,
|
|
439
|
+
storage,
|
|
440
|
+
view,
|
|
441
|
+
chains: chainsOf(opts.network),
|
|
442
|
+
},
|
|
443
|
+
// registered, so its past is looked for
|
|
444
|
+
{
|
|
445
|
+
persist: opts.persist,
|
|
446
|
+
saved: await opts.persist.load(),
|
|
447
|
+
key: stateKey(view.seed),
|
|
448
|
+
from: opts.birthday ?? 0n,
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
wallet.#release = release;
|
|
452
|
+
wallet.#readOnly = true;
|
|
453
|
+
return wallet;
|
|
454
|
+
} catch (e) {
|
|
455
|
+
release();
|
|
456
|
+
throw e;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The wallet over `base`, its state opened from what `persist` saved, with `key`, or started `from` a block or date. */
|
|
461
|
+
static async #begin(
|
|
462
|
+
base: Omit<Ctx, 'state' | 'save' | 'lastFullRefreshMs'>,
|
|
463
|
+
{
|
|
464
|
+
persist,
|
|
465
|
+
saved,
|
|
466
|
+
key,
|
|
467
|
+
from,
|
|
468
|
+
}: { persist: OpenOptions['persist']; saved: Uint8Array | null | undefined; key: Uint8Array; from?: bigint | Date },
|
|
469
|
+
unlock?: () => Promise<Uint8Array>,
|
|
470
|
+
): Promise<Wallet> {
|
|
331
471
|
const loaded = saved?.length ? openState(key, saved) : null;
|
|
332
472
|
// never started over in place: the first save would overwrite what it held, operations in flight included
|
|
333
473
|
if (saved?.length && !loaded) {
|
|
334
474
|
fail(
|
|
335
475
|
'invalid',
|
|
336
|
-
'the saved state does not open with this
|
|
476
|
+
'the saved state does not open with this key (another seed or view key, a newer version, or damaged): clear it to start over, which finds what it held again',
|
|
337
477
|
);
|
|
338
478
|
}
|
|
339
|
-
if (loaded && loaded.address.toLowerCase() !== address.toLowerCase())
|
|
479
|
+
if (loaded && loaded.address.toLowerCase() !== base.address.toLowerCase())
|
|
340
480
|
fail('invalid', `the saved state is ${loaded.address}'s: keep one per address`);
|
|
341
|
-
const state: WalletState = loaded ?? emptyWalletState(address);
|
|
342
|
-
const ctx: Ctx = {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
owners: new Map(),
|
|
346
|
-
address,
|
|
347
|
-
account,
|
|
348
|
-
walletClient: opts.walletClient ?? null,
|
|
349
|
-
storage,
|
|
350
|
-
view,
|
|
351
|
-
chains,
|
|
352
|
-
state,
|
|
353
|
-
save: () => opts.persist.save(sealState(key, state)),
|
|
354
|
-
lastFullRefreshMs: 0,
|
|
355
|
-
};
|
|
356
|
-
if (!loaded) await start(ctx, restoring ? 0n : opts.birthday);
|
|
357
|
-
const wallet = new Wallet(ctx, typeof opts.seed === 'function' ? opts.seed : undefined);
|
|
358
|
-
wallet.#release = release;
|
|
359
|
-
wallet.#referrer = opts.referrer;
|
|
481
|
+
const state: WalletState = loaded ?? emptyWalletState(base.address);
|
|
482
|
+
const ctx: Ctx = { ...base, state, save: () => persist.save(sealState(key, state)), lastFullRefreshMs: 0 };
|
|
483
|
+
if (!loaded) await start(ctx, from);
|
|
484
|
+
const wallet = new Wallet(ctx, unlock);
|
|
360
485
|
// a saved state names its tokens by id, and its coins stand by the chains' roots: read both now, so balances, coins
|
|
361
486
|
// and history work before the first sync
|
|
362
|
-
const [tokens] = await Promise.all([Promise.all(chains.map((c) => c.chain.tokens())), wallet.#readRoots()]);
|
|
487
|
+
const [tokens] = await Promise.all([Promise.all(ctx.chains.map((c) => c.chain.tokens())), wallet.#readRoots()]);
|
|
363
488
|
wallet.#tokens = tokens.flat();
|
|
364
489
|
keyAll(ctx, wallet.#tokens);
|
|
365
490
|
return wallet;
|
|
@@ -391,10 +516,16 @@ export class Wallet {
|
|
|
391
516
|
* anything else (a browser shows a passkey prompt only in the click that asked), and queued in the step that took it,
|
|
392
517
|
* so that a sync ending meanwhile does not let it go. */
|
|
393
518
|
async #seeded<T>(f: () => Promise<T>, needs = true): Promise<T> {
|
|
519
|
+
this.#writable();
|
|
394
520
|
if (needs && !this.#ctx.seed) this.#hold(await this.#unlock!());
|
|
395
521
|
return this.#exclusive(f);
|
|
396
522
|
}
|
|
397
523
|
|
|
524
|
+
/** Refuses a call that spends or writes to storage, in a wallet that watches. */
|
|
525
|
+
#writable(): void {
|
|
526
|
+
if (this.#readOnly) fail('read-only', 'this wallet watches its account by its view key: open it with its seed');
|
|
527
|
+
}
|
|
528
|
+
|
|
398
529
|
/** Runs `f` after everything queued before it: one state, changed by one task at a time. */
|
|
399
530
|
#exclusive<T>(f: () => Promise<T>): Promise<T> {
|
|
400
531
|
this.#busy++;
|
|
@@ -435,8 +566,9 @@ export class Wallet {
|
|
|
435
566
|
return new Promise((resolve, reject) => {
|
|
436
567
|
this.#exclusive(async () => {
|
|
437
568
|
await this.#catchUp().then(resolve, reject);
|
|
438
|
-
// still this task's: a deposit queued meanwhile finds the one recorded ahead, rather than writing at handout
|
|
439
|
-
|
|
569
|
+
// still this task's: a deposit queued meanwhile finds the one recorded ahead, rather than writing at handout;
|
|
570
|
+
// tried again after the next sync. A wallet that watches records none, as it writes nothing.
|
|
571
|
+
if (!this.#readOnly) await topUpBurns(this.#ctx, this.#closed.signal).catch(() => {});
|
|
440
572
|
this.#dropSeed();
|
|
441
573
|
}).catch(reject); // closed before it ran
|
|
442
574
|
});
|
|
@@ -445,14 +577,8 @@ export class Wallet {
|
|
|
445
577
|
async #catchUp(): Promise<void> {
|
|
446
578
|
const errors = await syncAll(this.#ctx);
|
|
447
579
|
errors.push(...(await this.#readRoots()));
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
c.chain
|
|
451
|
-
.tokens()
|
|
452
|
-
.catch((e: unknown) => (errors.push(e), this.#tokens.filter((t) => t.chainId === c.config.chainId))),
|
|
453
|
-
),
|
|
454
|
-
);
|
|
455
|
-
this.#tokens = listed.flat();
|
|
580
|
+
// as syncAll read them: a chain it could not read keeps those read before
|
|
581
|
+
this.#tokens = this.#ctx.chains.flatMap((c) => c.chain.known);
|
|
456
582
|
for (const op of this.#ctx.state.ops.filter((o) => o.status === 'pending')) {
|
|
457
583
|
try {
|
|
458
584
|
if (op.kind === 'send' || op.kind === 'withdraw' || op.kind === 'consolidate')
|
|
@@ -576,7 +702,9 @@ export class Wallet {
|
|
|
576
702
|
amount: n.value,
|
|
577
703
|
state,
|
|
578
704
|
from: n.peer,
|
|
705
|
+
via: null,
|
|
579
706
|
screenedBy: null,
|
|
707
|
+
at: f.atMs === undefined ? null : new Date(f.atMs),
|
|
580
708
|
});
|
|
581
709
|
}
|
|
582
710
|
}
|
|
@@ -603,7 +731,9 @@ export class Wallet {
|
|
|
603
731
|
amount: f.amount!,
|
|
604
732
|
state,
|
|
605
733
|
from: f.sender,
|
|
734
|
+
via: this.#via(d, f.sender),
|
|
606
735
|
screenedBy,
|
|
736
|
+
at: f.atMs === undefined ? null : new Date(f.atMs),
|
|
607
737
|
});
|
|
608
738
|
}
|
|
609
739
|
}
|
|
@@ -677,27 +807,17 @@ export class Wallet {
|
|
|
677
807
|
}
|
|
678
808
|
}
|
|
679
809
|
for (const d of s.deposits) {
|
|
680
|
-
// registered on this device, or on another, which says so in storage
|
|
681
|
-
const registered =
|
|
682
|
-
d.depositAddress ??
|
|
683
|
-
s.receipts.flatMap((r) =>
|
|
684
|
-
r.kind === 'deposit-address' && r.chainId === d.chainId && r.burnAddress === d.address.toLowerCase()
|
|
685
|
-
? [r.depositAddress]
|
|
686
|
-
: [],
|
|
687
|
-
)[0];
|
|
688
810
|
// on chain once found; shown once its commitment is read, which names the amount
|
|
689
811
|
for (const f of d.found.filter((f) => f.amount !== null)) {
|
|
690
|
-
const forwarded = registered && f.sender?.toLowerCase() === registered.toLowerCase();
|
|
691
812
|
// into a burn address a payer chose: theirs to this wallet, as a payment is
|
|
692
813
|
const kind = d.managed ? 'sale' : d.payer ? 'received' : 'deposited';
|
|
693
|
-
const via = forwarded ? { depositAddress: registered } : null;
|
|
694
814
|
const entry = {
|
|
695
815
|
...at(kind, d.chainId, f, d.createdMs),
|
|
696
816
|
token: this.#token(d.chainId, f.tokenId),
|
|
697
817
|
amount: f.amount!,
|
|
698
818
|
burnAddress: d.address,
|
|
699
819
|
};
|
|
700
|
-
out.push({ ...entry, fee: null, counterparty: f.sender, via, txHash: null });
|
|
820
|
+
out.push({ ...entry, fee: null, counterparty: f.sender, via: this.#via(d, f.sender), txHash: null });
|
|
701
821
|
}
|
|
702
822
|
}
|
|
703
823
|
// a deposit that paid someone (`deposit({ to })`), once its transfer is found
|
|
@@ -742,8 +862,16 @@ export class Wallet {
|
|
|
742
862
|
x.address === op.data.burnAddress.toLowerCase() &&
|
|
743
863
|
x.found.some((f) => f.amount !== null),
|
|
744
864
|
);
|
|
745
|
-
|
|
746
|
-
|
|
865
|
+
const { to, code, txHash } = op.data;
|
|
866
|
+
if ((!to && !code) || !txHash || op.status === 'pending' || found) continue;
|
|
867
|
+
out.push({
|
|
868
|
+
...base,
|
|
869
|
+
kind: 'sent',
|
|
870
|
+
fee: null,
|
|
871
|
+
counterparty: to ?? null,
|
|
872
|
+
via: code ? { code } : null,
|
|
873
|
+
txHash,
|
|
874
|
+
});
|
|
747
875
|
continue;
|
|
748
876
|
}
|
|
749
877
|
// under way (in operations()), or shown by its change
|
|
@@ -762,6 +890,21 @@ export class Wallet {
|
|
|
762
890
|
return out.sort((a, b) => (b.at?.getTime() ?? 0) - (a.at?.getTime() ?? 0));
|
|
763
891
|
}
|
|
764
892
|
|
|
893
|
+
/** The deposit address `d` registered (here, or on another device, which says so in storage) if `sender` is it: what
|
|
894
|
+
* a deposit from `sender` came through. */
|
|
895
|
+
#via(d: DepositRecord, sender: Hex | null): { depositAddress: Address } | null {
|
|
896
|
+
const registered =
|
|
897
|
+
d.depositAddress ??
|
|
898
|
+
this.#ctx.state.receipts.flatMap((r) =>
|
|
899
|
+
r.kind === 'deposit-address' && r.chainId === d.chainId && r.burnAddress === d.address.toLowerCase()
|
|
900
|
+
? [r.depositAddress]
|
|
901
|
+
: [],
|
|
902
|
+
)[0];
|
|
903
|
+
return registered && sender && isAddressEqual(sender, registered)
|
|
904
|
+
? { depositAddress: getAddress(registered) }
|
|
905
|
+
: null;
|
|
906
|
+
}
|
|
907
|
+
|
|
765
908
|
/** The text of a receive code this wallet paid, by its burn address, from storage's record of paying it. */
|
|
766
909
|
#paidCode(chainId: bigint, burnAddress: Hex): string | null {
|
|
767
910
|
for (const c of this.#ctx.state.storage?.outgoing.lanes ?? []) {
|
|
@@ -806,6 +949,31 @@ export class Wallet {
|
|
|
806
949
|
);
|
|
807
950
|
}
|
|
808
951
|
|
|
952
|
+
/** The deposit addresses this wallet handed out (`depositAddress`), on any of its devices, newest first: for an app to
|
|
953
|
+
* show each while what is sent to it is on its way, which `activity()` shows once deposited. Each is dated, and its
|
|
954
|
+
* fee told, by its receipt in storage, alike on every device. */
|
|
955
|
+
depositAddresses(): DepositAddressInfo[] {
|
|
956
|
+
return this.#ctx.state.receipts
|
|
957
|
+
.flatMap((r) => {
|
|
958
|
+
if (r.kind !== 'deposit-address') return [];
|
|
959
|
+
const token = this.#token(r.chainId, r.tokenId);
|
|
960
|
+
return [{ address: getAddress(r.depositAddress), token, fee: r.args?.fee ?? null, at: new Date(r.atMs) }];
|
|
961
|
+
})
|
|
962
|
+
.sort((a, b) => b.at.getTime() - a.at.getTime());
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** What `address` is to this wallet on `chainId`: one of its own burn addresses (`deposit`), one it paid someone
|
|
966
|
+
* into (`payment`, `deposit({ to })` to an address), or neither (null), as is a receive code's that it paid. On chain
|
|
967
|
+
* either is a plain transfer to an address that names no one: this tells an app listing the account's public
|
|
968
|
+
* transfers which they were. Both are recorded before the transfer is sent. */
|
|
969
|
+
burnAddressOf(chainId: bigint, address: Address): { kind: 'deposit' } | { kind: 'payment'; to: Address } | null {
|
|
970
|
+
const s = this.#ctx.state;
|
|
971
|
+
const a = address.toLowerCase();
|
|
972
|
+
if (s.deposits.some((d) => d.chainId === chainId && d.address.toLowerCase() === a)) return { kind: 'deposit' };
|
|
973
|
+
const paid = s.sent.find((x) => x.chainId === chainId && x.address.toLowerCase() === a);
|
|
974
|
+
return paid ? { kind: 'payment', to: paid.to } : null;
|
|
975
|
+
}
|
|
976
|
+
|
|
809
977
|
/** The payments to the burn addresses this wallet's points of sale handed out. Spent ones are, normally, in a
|
|
810
978
|
* merged note. */
|
|
811
979
|
payments(): DepositInfo[] {
|
|
@@ -818,6 +986,7 @@ export class Wallet {
|
|
|
818
986
|
* addresses. */
|
|
819
987
|
posKey(): Promise<Hex> {
|
|
820
988
|
return this.#exclusive(async () => {
|
|
989
|
+
this.#writable();
|
|
821
990
|
const ctx = this.#ctx;
|
|
822
991
|
const tokens = await tokenIds(ctx);
|
|
823
992
|
let n = 0;
|
|
@@ -830,8 +999,12 @@ export class Wallet {
|
|
|
830
999
|
}
|
|
831
1000
|
|
|
832
1001
|
/** Refunds deposit `id` to its sender without the ASP, the attester or a proved root: for a deposit the ASP
|
|
833
|
-
* rejected, or when those services stop. The account's wallet sends the transactions and pays their gas
|
|
834
|
-
|
|
1002
|
+
* rejected, or when those services stop. The account's wallet sends the transactions and pays their gas, or with
|
|
1003
|
+
* `from` another wallet of the user's does, as `deposit({ from })` pays. One through a deposit address of this
|
|
1004
|
+
* wallet's (`CoinInfo.via`) goes to `to`, by default `from`'s account, else this account; any other goes back to its
|
|
1005
|
+
* sender, and one through a point of sale's deposit address to the refund address its payer named. */
|
|
1006
|
+
rageExit(id: string, opts: { from?: WalletClient; to?: Address } = {}): Promise<Hex[]> {
|
|
1007
|
+
const route = this.#route(opts);
|
|
835
1008
|
return this.#seeded(async () => {
|
|
836
1009
|
const [chain, index] = id.split(':');
|
|
837
1010
|
const record =
|
|
@@ -842,10 +1015,39 @@ export class Wallet {
|
|
|
842
1015
|
(o) => `${o.chainId}` === chain && inputsOf(o).some((c) => `${c.index}` === index),
|
|
843
1016
|
);
|
|
844
1017
|
if (op) fail('invalid', `deposit ${id} is an input of operation ${op.id}, which is in flight`);
|
|
845
|
-
return rageExit(this.#ctx, record, Number(index));
|
|
1018
|
+
return rageExit(this.#ctx, record, Number(index), route);
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/** Sends what deposit address `address` of this wallet's holds of `asset` (NATIVE for the chain's own currency) to
|
|
1023
|
+
* `to`: another token sent to it, which it cannot wrap. What it can wrap is wrapped instead, as a payment to it is.
|
|
1024
|
+
* It deploys on its first flush, and refunds nothing before. `from` and `to` as for `rageExit`. */
|
|
1025
|
+
refundDepositAddress(
|
|
1026
|
+
address: Address,
|
|
1027
|
+
asset: Address,
|
|
1028
|
+
opts: { from?: WalletClient; to?: Address } = {},
|
|
1029
|
+
): Promise<Hex> {
|
|
1030
|
+
if (!isAddress(asset, { strict: false }) || asset.toLowerCase() === zeroAddress)
|
|
1031
|
+
fail('invalid', `${asset} is not an asset's address`);
|
|
1032
|
+
const route = this.#route(opts);
|
|
1033
|
+
return this.#seeded(async () => {
|
|
1034
|
+
const record =
|
|
1035
|
+
this.#ctx.state.deposits.find((d) => !d.managed && this.#via(d, address)) ??
|
|
1036
|
+
fail('invalid', `${address} is not a deposit address of this wallet's`);
|
|
1037
|
+
return refundDepositAddress(this.#ctx, record, getAddress(address), getAddress(asset), route);
|
|
846
1038
|
});
|
|
847
1039
|
}
|
|
848
1040
|
|
|
1041
|
+
/** Who sends a refund and where it goes (`rageExit`), checked before the seed is asked for. */
|
|
1042
|
+
#route(opts: { from?: WalletClient; to?: Address }): Route {
|
|
1043
|
+
const from = opts.from ?? null;
|
|
1044
|
+
if (from && !from.account) fail('invalid', 'the wallet client has no account');
|
|
1045
|
+
const to = opts.to ?? from?.account?.address ?? this.address;
|
|
1046
|
+
if (!isAddress(to, { strict: false }) || to.toLowerCase() === zeroAddress)
|
|
1047
|
+
fail('invalid', `${to} is not an address to refund to`);
|
|
1048
|
+
return { from, to: getAddress(to) };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
849
1051
|
operations(): Operation[] {
|
|
850
1052
|
return this.#ctx.state.ops.map((op) => this.#operation(op));
|
|
851
1053
|
}
|
|
@@ -915,7 +1117,9 @@ export class Wallet {
|
|
|
915
1117
|
let record = unusedBurn(ctx, token.chainId, token.tokenId);
|
|
916
1118
|
if (!record) {
|
|
917
1119
|
recording();
|
|
918
|
-
record = await recordBurn(ctx, token.chainId, token.tokenId, await tokenIds(ctx),
|
|
1120
|
+
record = await recordBurn(ctx, token.chainId, token.tokenId, await tokenIds(ctx), {
|
|
1121
|
+
signal: this.#closed.signal,
|
|
1122
|
+
});
|
|
919
1123
|
}
|
|
920
1124
|
const next = chainState(ctx, token.chainId).burnNext;
|
|
921
1125
|
// recording may have read another device's later ones
|
|
@@ -930,10 +1134,12 @@ export class Wallet {
|
|
|
930
1134
|
* burn address (approving it first if needed). The deposit is spendable once the ASP has screened it. More than the
|
|
931
1135
|
* account holds fails at once (`insufficient-funds`), before anything is asked of the wallet.
|
|
932
1136
|
*
|
|
933
|
-
* With `to`, an
|
|
934
|
-
* under the public key they registered, told to them in storage first as a note is
|
|
935
|
-
* receive it as a deposit of their own, screened like any; the burn address names no one. It
|
|
936
|
-
* as sent. Finding the address takes about 2^16 hashes (`burn-address`).
|
|
1137
|
+
* With `to`, an account that registered, by its address or its zERC20 ID, it pays them instead, privately: into a
|
|
1138
|
+
* fresh burn address of theirs, under the public key they registered, told to them in storage first as a note is
|
|
1139
|
+
* (`planDeposit`). They receive it as a deposit of their own, screened like any; the burn address names no one. It
|
|
1140
|
+
* shows in `activity()` as sent. Finding the address takes about 2^16 hashes (`burn-address`). Or `to` is a receive
|
|
1141
|
+
* code (`receiveCode`), paid into its burn address, which its owner already watches: storage is told nothing, so
|
|
1142
|
+
* only this device's `activity()` shows it as sent.
|
|
937
1143
|
*
|
|
938
1144
|
* With `wrapped`, it pays from the account's zERC20 itself, as a withdrawal pays out, rather than its underlying
|
|
939
1145
|
* asset: a plain transfer into the burn address, with no wrap and no approval.
|
|
@@ -946,13 +1152,15 @@ export class Wallet {
|
|
|
946
1152
|
async deposit(opts: {
|
|
947
1153
|
token: Token;
|
|
948
1154
|
amount: bigint;
|
|
949
|
-
to?:
|
|
1155
|
+
to?: string;
|
|
950
1156
|
wrapped?: boolean;
|
|
951
1157
|
from?: WalletClient;
|
|
952
1158
|
onStep?: (step: DepositStep) => void;
|
|
953
1159
|
}): Promise<Operation> {
|
|
1160
|
+
this.#writable();
|
|
954
1161
|
if (opts.amount <= 0n) fail('invalid', 'the amount must be positive');
|
|
955
1162
|
const payer = opts.from ? (opts.from.account ?? fail('invalid', 'the wallet client has no account')) : null;
|
|
1163
|
+
const to = opts.to === undefined ? null : recipientOf(opts.to, opts.token);
|
|
956
1164
|
const step = opts.onStep ?? (() => {});
|
|
957
1165
|
if (this.#busy > 0) step('queued');
|
|
958
1166
|
return this.#exclusive(async () => {
|
|
@@ -977,14 +1185,21 @@ export class Wallet {
|
|
|
977
1185
|
`${payer ? 'the wallet' : 'the account'} holds ${formatUnits(held, token.decimals)} of ${of}, less than ${formatUnits(amount, token.decimals)}`,
|
|
978
1186
|
);
|
|
979
1187
|
}
|
|
980
|
-
const address =
|
|
981
|
-
|
|
982
|
-
|
|
1188
|
+
const address =
|
|
1189
|
+
to?.kind === 'code'
|
|
1190
|
+
? to.burnAddress
|
|
1191
|
+
: to
|
|
1192
|
+
? await this.#burnFor(token, amount, to.address, () => step('burn-address'))
|
|
1193
|
+
: (await this.#burnAddress(token, null, () => step('burn-address'))).address;
|
|
983
1194
|
const op = this.#newOp(
|
|
984
1195
|
'deposit',
|
|
985
1196
|
token,
|
|
986
1197
|
amount,
|
|
987
|
-
|
|
1198
|
+
to?.kind === 'code'
|
|
1199
|
+
? { burnAddress: address, code: to.code }
|
|
1200
|
+
: to
|
|
1201
|
+
? { burnAddress: address, to: to.address }
|
|
1202
|
+
: { burnAddress: address },
|
|
988
1203
|
);
|
|
989
1204
|
await this.#ctx.save();
|
|
990
1205
|
try {
|
|
@@ -1057,11 +1272,7 @@ export class Wallet {
|
|
|
1057
1272
|
async #burnFor(token: Token, amount: bigint, to: Address, searching: () => void): Promise<Hex> {
|
|
1058
1273
|
const ctx = this.#ctx;
|
|
1059
1274
|
const recipient = await lookupRecipient(ctx.storage, hexToBytes(to));
|
|
1060
|
-
if (!recipient)
|
|
1061
|
-
fail(
|
|
1062
|
-
'recipient-not-registered',
|
|
1063
|
-
`${to} cannot be paid privately: withdraw to it publicly, or ask for a receive code`,
|
|
1064
|
-
);
|
|
1275
|
+
if (!recipient) fail('recipient-not-registered', `${to} never opened a zERC20 wallet: ask for a receive code`);
|
|
1065
1276
|
searching();
|
|
1066
1277
|
const burn = await burnAddressFor(token.chainId, bytesToHex(recipient!.publicKey), this.#closed.signal);
|
|
1067
1278
|
// with the address, so that every device of this wallet finds the transfer it then sends
|
|
@@ -1083,25 +1294,35 @@ export class Wallet {
|
|
|
1083
1294
|
/** An address that takes `token`'s underlying asset from anywhere (an exchange withdrawal, another wallet) and
|
|
1084
1295
|
* deposits it privately, less `fee` (in the underlying's base units) to the deposit relayer each time: what arrives
|
|
1085
1296
|
* 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
|
|
1297
|
+
* deposit relayer. It names no account: its refunds go where a key of its own signs for (`rageExit`'s `to`,
|
|
1298
|
+
* `refundDepositAddress`). `depositAddresses()` lists it. `onStep` hears what it waits on (`queued`,
|
|
1299
|
+
* `burn-address`). */
|
|
1087
1300
|
depositAddress(
|
|
1088
1301
|
token: Token,
|
|
1089
1302
|
opts: { onStep?: (step: DepositStep) => void } = {},
|
|
1090
1303
|
): Promise<{ address: Address; fee: bigint }> {
|
|
1091
1304
|
const step = opts.onStep ?? (() => {});
|
|
1305
|
+
const ctx = this.#ctx;
|
|
1306
|
+
const code = (index: number) => ({ chainId: token.chainId, tokenId: token.tokenId, index });
|
|
1307
|
+
// the seed is asked for only past the refund signers derived ahead
|
|
1308
|
+
const derived = !!refundSigner(ctx, code(nextBurn(ctx, token.chainId, token.tokenId)));
|
|
1092
1309
|
if (this.#busy > 0) step('queued');
|
|
1093
|
-
return this.#
|
|
1094
|
-
const c = chainCtx(
|
|
1310
|
+
return this.#seeded(async () => {
|
|
1311
|
+
const c = chainCtx(ctx, token.chainId);
|
|
1095
1312
|
const record = await this.#burnAddress(token, null, () => step('burn-address'));
|
|
1096
|
-
|
|
1313
|
+
if (!refundSigner(ctx, code(record.burnIndex))) await this.#seed(); // calls queued before took the next ones
|
|
1314
|
+
const signer = refundSigner(ctx, code(record.burnIndex))!;
|
|
1315
|
+
const referrer = this.#referrer ?? { address: zeroAddress, bps: 0 };
|
|
1316
|
+
const { depositAddress, feeRecipient, fee } = await registerDeposit(
|
|
1097
1317
|
c.chain,
|
|
1098
1318
|
token.address,
|
|
1099
1319
|
record.address,
|
|
1100
|
-
|
|
1101
|
-
|
|
1320
|
+
{ signer },
|
|
1321
|
+
referrer,
|
|
1102
1322
|
);
|
|
1103
1323
|
record.depositAddress = depositAddress;
|
|
1104
|
-
// for the wallet's other devices, to show what comes through it
|
|
1324
|
+
// for the wallet's other devices, to show what comes through it and what it takes, and for a flush without the
|
|
1325
|
+
// relayer: written by the next sync
|
|
1105
1326
|
const burnAddress = record.address.toLowerCase() as Hex;
|
|
1106
1327
|
this.#ctx.state.receipts.push({
|
|
1107
1328
|
kind: 'deposit-address',
|
|
@@ -1109,12 +1330,48 @@ export class Wallet {
|
|
|
1109
1330
|
tokenId: token.tokenId,
|
|
1110
1331
|
burnAddress,
|
|
1111
1332
|
depositAddress,
|
|
1333
|
+
args: { fee, feeRecipient, refundSigner: signer, referrer: referrer.address, referralBps: referrer.bps },
|
|
1112
1334
|
atMs: Date.now(),
|
|
1113
1335
|
unwritten: true,
|
|
1114
1336
|
});
|
|
1115
1337
|
await this.#ctx.save();
|
|
1116
1338
|
return { address: depositAddress, fee };
|
|
1117
|
-
});
|
|
1339
|
+
}, !derived);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** Has the deposit relayer watch deposit address `address` of this wallet's for another 7 days, at the fee it was
|
|
1343
|
+
* handed out with: what reached it after its 7 days is deposited then, and so is what reaches it within the next.
|
|
1344
|
+
* `depositAddresses()` dates it from now, on every device. */
|
|
1345
|
+
rewatchDepositAddress(address: Address): Promise<{ address: Address; fee: bigint }> {
|
|
1346
|
+
return this.#seeded(async () => {
|
|
1347
|
+
const receipt = this.#depositReceipt(address);
|
|
1348
|
+
const fee = await rewatchDeposit(chainCtx(this.#ctx, receipt.chainId).chain, getAddress(address));
|
|
1349
|
+
// for every device of the wallet, written by the next sync: storage dates it anew
|
|
1350
|
+
receipt.atMs = Date.now();
|
|
1351
|
+
receipt.unwritten = true;
|
|
1352
|
+
await this.#ctx.save();
|
|
1353
|
+
return { address: getAddress(address), fee };
|
|
1354
|
+
}, false);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/** Deposits what deposit address `address` of this wallet's holds, as the deposit relayer would, for when it does
|
|
1358
|
+
* not: down, or no longer watching the address. Its receipt says all the address was registered with, wherever its
|
|
1359
|
+
* `fee` is known (`depositAddresses()`), so no relayer is asked. `from`, another wallet of the user's, sends the
|
|
1360
|
+
* flush and pays its gas, and shows on chain as the depositor: never the account's own, which the address does not
|
|
1361
|
+
* name. What it holds must exceed the fee (`insufficient-funds`). It shows in `activity()` as any deposit through
|
|
1362
|
+
* the address. */
|
|
1363
|
+
flushDepositAddress(address: Address, opts: { from: WalletClient }): Promise<Hex> {
|
|
1364
|
+
if (!opts.from.account) fail('invalid', 'the wallet client has no account');
|
|
1365
|
+
return this.#seeded(() => flushDepositAddress(this.#ctx, this.#depositReceipt(address), opts.from), false);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/** The receipt of deposit address `address` of this wallet's. */
|
|
1369
|
+
#depositReceipt(address: Address): DepositAddressReceipt {
|
|
1370
|
+
return (
|
|
1371
|
+
this.#ctx.state.receipts.find(
|
|
1372
|
+
(r): r is DepositAddressReceipt => r.kind === 'deposit-address' && isAddressEqual(r.depositAddress, address),
|
|
1373
|
+
) ?? fail('invalid', `${address} is not a deposit address of this wallet's`)
|
|
1374
|
+
);
|
|
1118
1375
|
}
|
|
1119
1376
|
|
|
1120
1377
|
/** A receive code for `token`, to hand to one payer: it pays any amount, as often as it likes, publicly by a transfer
|
|
@@ -1168,10 +1425,16 @@ export class Wallet {
|
|
|
1168
1425
|
const steps = BigInt(roundsToMerge(mergePool(all, chosen)));
|
|
1169
1426
|
// a merge pays the relayer publicly, so the protocol takes its cut of that, grossed up as a payment's is
|
|
1170
1427
|
const cut = steps === 0n ? 0n : (await grossed(this.#ctx, token.chainId, token.tokenId, relayer)) - relayer;
|
|
1171
|
-
return {
|
|
1428
|
+
return {
|
|
1429
|
+
relayer: relayer * steps,
|
|
1430
|
+
protocol: cut * steps,
|
|
1431
|
+
protocolBps: bpsOf(cut, relayer + cut),
|
|
1432
|
+
fee: (relayer + cut) * steps,
|
|
1433
|
+
steps: Number(steps),
|
|
1434
|
+
};
|
|
1172
1435
|
}
|
|
1173
1436
|
// as the payment's plan reckons it, so that `fee` is a `maxFee` it passes
|
|
1174
|
-
const { protocol } = await withdrawalOf(
|
|
1437
|
+
const { withdrawn, protocol } = await withdrawalOf(
|
|
1175
1438
|
this.#ctx,
|
|
1176
1439
|
token.chainId,
|
|
1177
1440
|
token.tokenId,
|
|
@@ -1183,12 +1446,12 @@ export class Wallet {
|
|
|
1183
1446
|
this.#ctx.state.ops.some((o) => o.status === 'pending') ||
|
|
1184
1447
|
settling(this.#ctx, token.chainId, token.tokenId, size);
|
|
1185
1448
|
payWith(all, amount + relayer + protocol, relayer, onTheWay);
|
|
1186
|
-
return { relayer, protocol, fee: relayer + protocol, steps: 1 };
|
|
1449
|
+
return { relayer, protocol, protocolBps: bpsOf(protocol, withdrawn), fee: relayer + protocol, steps: 1 };
|
|
1187
1450
|
}
|
|
1188
1451
|
|
|
1189
1452
|
/** The most a send or withdrawal of `token` can pay: what is spendable, less the fees `fees` quotes on top of it, and
|
|
1190
1453
|
* the merges first when one JoinSplit cannot spend it all, which `fees` then asks for (`needs-consolidation`). */
|
|
1191
|
-
async maxAmount(opts: { kind: 'send' | 'withdraw'; token: Token }): Promise<
|
|
1454
|
+
async maxAmount(opts: { kind: 'send' | 'withdraw'; token: Token }): Promise<MaxAmount> {
|
|
1192
1455
|
const { token } = opts;
|
|
1193
1456
|
const c = chainCtx(this.#ctx, token.chainId);
|
|
1194
1457
|
const relayer =
|
|
@@ -1217,9 +1480,10 @@ export class Wallet {
|
|
|
1217
1480
|
return merge && merge.map((m) => `${token.chainId}:${m.index}`);
|
|
1218
1481
|
}
|
|
1219
1482
|
|
|
1220
|
-
/** Pays `amount` privately: to an
|
|
1221
|
-
* (`receiveCode`). An
|
|
1222
|
-
* what is spendable now, or it fails here: `needs-consolidation`,
|
|
1483
|
+
/** Pays `amount` privately: to an account that has registered (any wallet that ever opened), by its address or its
|
|
1484
|
+
* zERC20 ID (`accountId`), or to a receive code (`receiveCode`). An account that never registered fails with
|
|
1485
|
+
* `recipient-not-registered`. One JoinSplit from what is spendable now, or it fails here: `needs-consolidation`,
|
|
1486
|
+
* `funds-settling` or `insufficient-funds`.
|
|
1223
1487
|
* `amount` is what the recipient gets, and `maxFee` caps what it pays on top, `fees({ kind: 'send' }).fee`: the
|
|
1224
1488
|
* relayer names its fee, so a quote the user saw is the cap. */
|
|
1225
1489
|
async send(opts: { to: string; token: Token; amount: bigint; maxFee: bigint }): Promise<Operation> {
|
|
@@ -1227,22 +1491,14 @@ export class Wallet {
|
|
|
1227
1491
|
if (amount <= 0n) fail('invalid', 'the amount must be positive');
|
|
1228
1492
|
// the JoinSplit is proved by a later sync, which holds the seed until the payment is done
|
|
1229
1493
|
return this.#seeded(async () => {
|
|
1494
|
+
const r = recipientOf(opts.to, token);
|
|
1230
1495
|
let to: Destination;
|
|
1231
|
-
if (
|
|
1232
|
-
const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(
|
|
1496
|
+
if (r.kind === 'address') {
|
|
1497
|
+
const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(r.address));
|
|
1233
1498
|
if (!recipient)
|
|
1234
|
-
fail(
|
|
1235
|
-
'recipient-not-registered',
|
|
1236
|
-
`${opts.to} cannot be paid privately: withdraw to it publicly, or ask for a receive code`,
|
|
1237
|
-
);
|
|
1499
|
+
fail('recipient-not-registered', `${opts.to.trim()} never opened a zERC20 wallet: ask for a receive code`);
|
|
1238
1500
|
to = { kind: 'address', recipient: recipient! };
|
|
1239
|
-
} else {
|
|
1240
|
-
const code = decodeCode(opts.to);
|
|
1241
|
-
if (!code) return fail('invalid', 'not an address or a receive code');
|
|
1242
|
-
if (code.chainId !== token.chainId || code.tokenId !== token.tokenId)
|
|
1243
|
-
fail('invalid', 'the receive code is for another token');
|
|
1244
|
-
to = { kind: 'code', code: opts.to.trim() };
|
|
1245
|
-
}
|
|
1501
|
+
} else to = { kind: 'code', code: r.code };
|
|
1246
1502
|
return this.#newPayment('send', token, amount, { to, maxFee: opts.maxFee, step: null, paid: 0n });
|
|
1247
1503
|
});
|
|
1248
1504
|
}
|
|
@@ -1315,15 +1571,40 @@ export class Wallet {
|
|
|
1315
1571
|
}
|
|
1316
1572
|
}
|
|
1317
1573
|
|
|
1574
|
+
/** Whom `to` names, as `send` and `deposit` take it: an account, by its address or its zERC20 ID, or a receive code of
|
|
1575
|
+
* `token`'s. */
|
|
1576
|
+
function recipientOf(
|
|
1577
|
+
to: string,
|
|
1578
|
+
token: Token,
|
|
1579
|
+
): { kind: 'address'; address: Address } | { kind: 'code'; code: string; burnAddress: Hex } {
|
|
1580
|
+
const address = isAddress(to) ? to : idAccount(to);
|
|
1581
|
+
if (address) return { kind: 'address', address };
|
|
1582
|
+
const code = decodeCode(to);
|
|
1583
|
+
if (!code) return fail('invalid', 'not an address, a zERC20 ID or a receive code');
|
|
1584
|
+
if (code.chainId !== token.chainId || code.tokenId !== token.tokenId)
|
|
1585
|
+
fail('invalid', 'the receive code is for another token');
|
|
1586
|
+
return { kind: 'code', code: to.trim(), burnAddress: bytesToHex(code.burnAddress) };
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/** Each chain of `network`, with its servers. */
|
|
1590
|
+
const chainsOf = (network: Network): ChainCtx[] =>
|
|
1591
|
+
network.chains.map((config) => ({
|
|
1592
|
+
config,
|
|
1593
|
+
chain: new Chain(config),
|
|
1594
|
+
indexer: new Indexer(config.indexer),
|
|
1595
|
+
asp: new Asp(config.asp),
|
|
1596
|
+
}));
|
|
1597
|
+
|
|
1318
1598
|
/** Holds `address`'s wallet until the returned function is called: two tabs on one wallet would each overwrite the
|
|
1319
|
-
* other's state and spend the same notes.
|
|
1320
|
-
|
|
1599
|
+
* other's state and spend the same notes. A wallet that watches it (`view`) is held apart, as its state is its own.
|
|
1600
|
+
* Nothing where there are no Web Locks. */
|
|
1601
|
+
export async function lockWallet(address: Address, kind: 'wallet' | 'view' = 'wallet'): Promise<() => void> {
|
|
1321
1602
|
const locks = (globalThis as { navigator?: Navigator }).navigator?.locks;
|
|
1322
1603
|
if (!locks) return () => {};
|
|
1323
1604
|
let release = () => {};
|
|
1324
1605
|
const held = new Promise<void>((r) => (release = r));
|
|
1325
1606
|
const got = await new Promise<boolean>((resolve) => {
|
|
1326
|
-
void locks.request(`zerc20
|
|
1607
|
+
void locks.request(`zerc20-${kind}:${address.toLowerCase()}`, { ifAvailable: true }, (lock) => {
|
|
1327
1608
|
resolve(lock !== null);
|
|
1328
1609
|
return lock ? held : undefined;
|
|
1329
1610
|
});
|