@zerc20/wallet-sdk 0.4.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 +8 -2
- package/dist/context.js +15 -2
- 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/index.d.ts +4 -2
- package/dist/index.js +4 -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 +49 -18
- package/dist/sync.js.map +1 -1
- package/dist/wallet.d.ts +92 -14
- package/dist/wallet.js +191 -56
- package/dist/wallet.js.map +1 -1
- package/package.json +2 -2
- package/src/accountId.ts +22 -0
- package/src/chain.ts +95 -49
- package/src/context.ts +27 -3
- package/src/core.ts +109 -24
- package/src/index.ts +15 -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 +54 -19
- package/src/wallet.ts +246 -72
- package/wasm/zerc20_wallet_core.d.ts +1 -1
- package/wasm/zerc20_wallet_core.js +1 -1
- package/wasm/zerc20_wallet_core_bg.wasm +0 -0
package/dist/wallet.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// The wallet an app works with. Amounts are bigints in a token's base units.
|
|
2
2
|
import { CanisterClient, codeKey, commit, decodeCode, encodeCode, lookupRecipient, nextPosLane, notifyKey, planDeposit, planPosLane, posKey, registeredViewKey, } from '@zerc20/storage-sdk';
|
|
3
|
-
import { bytesToHex, formatUnits, getAddress, hexToBytes, isAddress, numberToHex, zeroAddress, } from 'viem';
|
|
3
|
+
import { bytesToHex, formatUnits, getAddress, hexToBytes, isAddress, isAddressEqual, numberToHex, zeroAddress, } from 'viem';
|
|
4
4
|
import { toAccount } from 'viem/accounts';
|
|
5
|
+
import { idAccount } from './accountId.js';
|
|
5
6
|
import { Chain, MAX_REFERRAL_BPS, NATIVE, erc20Abi, tokenAbi } from './chain.js';
|
|
6
|
-
import { chainCtx, chainState, owner, switched, walletOn, walletSees, } from './context.js';
|
|
7
|
+
import { chainCtx, chainState, owner, refundSigner, switched, walletOn, walletSees, } from './context.js';
|
|
7
8
|
import { burnAddressFor, core, initCore } from './core.js';
|
|
8
9
|
import { WalletError, fail, guard } from './errors.js';
|
|
9
10
|
import { checkNetwork } from './network.js';
|
|
10
11
|
import { Asp, Indexer, PAGE } from './servers.js';
|
|
11
|
-
import { rageExit } from './rageExit.js';
|
|
12
|
-
import { registerDeposit, relayerFees } from './services.js';
|
|
13
|
-
import { advanceSpend, chosenSettling, coins, grossed, inputsOf, landed, maxPayment, mergePool, mergeRound, payWith, planStep, roundsToMerge, settling, withdrawalOf, } from './spend.js';
|
|
12
|
+
import { flushDepositAddress, rageExit, refundDepositAddress } from './rageExit.js';
|
|
13
|
+
import { registerDeposit, relayerFees, rewatchDeposit } from './services.js';
|
|
14
|
+
import { advanceSpend, bpsOf, chosenSettling, coins, grossed, inputsOf, landed, maxPayment, mergePool, mergeRound, payWith, planStep, roundsToMerge, settling, withdrawalOf, } from './spend.js';
|
|
14
15
|
import { emptyWalletState, openState, sealState, stateKey, unspent, } from './state.js';
|
|
15
16
|
import { WAIT_MS, commitmentTx, keyAll, nextBurn, recordBurn, storageState, syncAll, takeRefresh, tokenIds, topUpBurns, unusedBurn, } from './sync.js';
|
|
16
17
|
import { decodeViewKey, encodeViewKey } from './viewKey.js';
|
|
@@ -102,6 +103,7 @@ export class Wallet {
|
|
|
102
103
|
network: opts.network,
|
|
103
104
|
seed,
|
|
104
105
|
owners: new Map(),
|
|
106
|
+
refundSigners: new Map(),
|
|
105
107
|
address,
|
|
106
108
|
account,
|
|
107
109
|
walletClient: opts.walletClient ?? null,
|
|
@@ -132,6 +134,7 @@ export class Wallet {
|
|
|
132
134
|
network: opts.network,
|
|
133
135
|
seed: null,
|
|
134
136
|
owners: new Map([['', owner]]),
|
|
137
|
+
refundSigners: new Map(),
|
|
135
138
|
address,
|
|
136
139
|
// signs nothing: every call that would send is refused first
|
|
137
140
|
account: toAccount(address),
|
|
@@ -261,10 +264,8 @@ export class Wallet {
|
|
|
261
264
|
async #catchUp() {
|
|
262
265
|
const errors = await syncAll(this.#ctx);
|
|
263
266
|
errors.push(...(await this.#readRoots()));
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
.catch((e) => (errors.push(e), this.#tokens.filter((t) => t.chainId === c.config.chainId)))));
|
|
267
|
-
this.#tokens = listed.flat();
|
|
267
|
+
// as syncAll read them: a chain it could not read keeps those read before
|
|
268
|
+
this.#tokens = this.#ctx.chains.flatMap((c) => c.chain.known);
|
|
268
269
|
for (const op of this.#ctx.state.ops.filter((o) => o.status === 'pending')) {
|
|
269
270
|
try {
|
|
270
271
|
if (op.kind === 'send' || op.kind === 'withdraw' || op.kind === 'consolidate')
|
|
@@ -380,7 +381,9 @@ export class Wallet {
|
|
|
380
381
|
amount: n.value,
|
|
381
382
|
state,
|
|
382
383
|
from: n.peer,
|
|
384
|
+
via: null,
|
|
383
385
|
screenedBy: null,
|
|
386
|
+
at: f.atMs === undefined ? null : new Date(f.atMs),
|
|
384
387
|
});
|
|
385
388
|
}
|
|
386
389
|
}
|
|
@@ -407,7 +410,9 @@ export class Wallet {
|
|
|
407
410
|
amount: f.amount,
|
|
408
411
|
state,
|
|
409
412
|
from: f.sender,
|
|
413
|
+
via: this.#via(d, f.sender),
|
|
410
414
|
screenedBy,
|
|
415
|
+
at: f.atMs === undefined ? null : new Date(f.atMs),
|
|
411
416
|
});
|
|
412
417
|
}
|
|
413
418
|
}
|
|
@@ -478,24 +483,17 @@ export class Wallet {
|
|
|
478
483
|
}
|
|
479
484
|
}
|
|
480
485
|
for (const d of s.deposits) {
|
|
481
|
-
// registered on this device, or on another, which says so in storage
|
|
482
|
-
const registered = d.depositAddress ??
|
|
483
|
-
s.receipts.flatMap((r) => r.kind === 'deposit-address' && r.chainId === d.chainId && r.burnAddress === d.address.toLowerCase()
|
|
484
|
-
? [r.depositAddress]
|
|
485
|
-
: [])[0];
|
|
486
486
|
// on chain once found; shown once its commitment is read, which names the amount
|
|
487
487
|
for (const f of d.found.filter((f) => f.amount !== null)) {
|
|
488
|
-
const forwarded = registered && f.sender?.toLowerCase() === registered.toLowerCase();
|
|
489
488
|
// into a burn address a payer chose: theirs to this wallet, as a payment is
|
|
490
489
|
const kind = d.managed ? 'sale' : d.payer ? 'received' : 'deposited';
|
|
491
|
-
const via = forwarded ? { depositAddress: registered } : null;
|
|
492
490
|
const entry = {
|
|
493
491
|
...at(kind, d.chainId, f, d.createdMs),
|
|
494
492
|
token: this.#token(d.chainId, f.tokenId),
|
|
495
493
|
amount: f.amount,
|
|
496
494
|
burnAddress: d.address,
|
|
497
495
|
};
|
|
498
|
-
out.push({ ...entry, fee: null, counterparty: f.sender, via, txHash: null });
|
|
496
|
+
out.push({ ...entry, fee: null, counterparty: f.sender, via: this.#via(d, f.sender), txHash: null });
|
|
499
497
|
}
|
|
500
498
|
}
|
|
501
499
|
// a deposit that paid someone (`deposit({ to })`), once its transfer is found
|
|
@@ -538,9 +536,17 @@ export class Wallet {
|
|
|
538
536
|
const found = s.sent.some((x) => x.chainId === op.chainId &&
|
|
539
537
|
x.address === op.data.burnAddress.toLowerCase() &&
|
|
540
538
|
x.found.some((f) => f.amount !== null));
|
|
541
|
-
|
|
539
|
+
const { to, code, txHash } = op.data;
|
|
540
|
+
if ((!to && !code) || !txHash || op.status === 'pending' || found)
|
|
542
541
|
continue;
|
|
543
|
-
out.push({
|
|
542
|
+
out.push({
|
|
543
|
+
...base,
|
|
544
|
+
kind: 'sent',
|
|
545
|
+
fee: null,
|
|
546
|
+
counterparty: to ?? null,
|
|
547
|
+
via: code ? { code } : null,
|
|
548
|
+
txHash,
|
|
549
|
+
});
|
|
544
550
|
continue;
|
|
545
551
|
}
|
|
546
552
|
// under way (in operations()), or shown by its change
|
|
@@ -558,6 +564,17 @@ export class Wallet {
|
|
|
558
564
|
}
|
|
559
565
|
return out.sort((a, b) => (b.at?.getTime() ?? 0) - (a.at?.getTime() ?? 0));
|
|
560
566
|
}
|
|
567
|
+
/** The deposit address `d` registered (here, or on another device, which says so in storage) if `sender` is it: what
|
|
568
|
+
* a deposit from `sender` came through. */
|
|
569
|
+
#via(d, sender) {
|
|
570
|
+
const registered = d.depositAddress ??
|
|
571
|
+
this.#ctx.state.receipts.flatMap((r) => r.kind === 'deposit-address' && r.chainId === d.chainId && r.burnAddress === d.address.toLowerCase()
|
|
572
|
+
? [r.depositAddress]
|
|
573
|
+
: [])[0];
|
|
574
|
+
return registered && sender && isAddressEqual(sender, registered)
|
|
575
|
+
? { depositAddress: getAddress(registered) }
|
|
576
|
+
: null;
|
|
577
|
+
}
|
|
561
578
|
/** The text of a receive code this wallet paid, by its burn address, from storage's record of paying it. */
|
|
562
579
|
#paidCode(chainId, burnAddress) {
|
|
563
580
|
for (const c of this.#ctx.state.storage?.outgoing.lanes ?? []) {
|
|
@@ -599,6 +616,31 @@ export class Wallet {
|
|
|
599
616
|
state: f.spent ? 'spent' : f.asp === null ? 'screening' : f.asp,
|
|
600
617
|
})));
|
|
601
618
|
}
|
|
619
|
+
/** The deposit addresses this wallet handed out (`depositAddress`), on any of its devices, newest first: for an app to
|
|
620
|
+
* show each while what is sent to it is on its way, which `activity()` shows once deposited. Each is dated, and its
|
|
621
|
+
* fee told, by its receipt in storage, alike on every device. */
|
|
622
|
+
depositAddresses() {
|
|
623
|
+
return this.#ctx.state.receipts
|
|
624
|
+
.flatMap((r) => {
|
|
625
|
+
if (r.kind !== 'deposit-address')
|
|
626
|
+
return [];
|
|
627
|
+
const token = this.#token(r.chainId, r.tokenId);
|
|
628
|
+
return [{ address: getAddress(r.depositAddress), token, fee: r.args?.fee ?? null, at: new Date(r.atMs) }];
|
|
629
|
+
})
|
|
630
|
+
.sort((a, b) => b.at.getTime() - a.at.getTime());
|
|
631
|
+
}
|
|
632
|
+
/** What `address` is to this wallet on `chainId`: one of its own burn addresses (`deposit`), one it paid someone
|
|
633
|
+
* into (`payment`, `deposit({ to })` to an address), or neither (null), as is a receive code's that it paid. On chain
|
|
634
|
+
* either is a plain transfer to an address that names no one: this tells an app listing the account's public
|
|
635
|
+
* transfers which they were. Both are recorded before the transfer is sent. */
|
|
636
|
+
burnAddressOf(chainId, address) {
|
|
637
|
+
const s = this.#ctx.state;
|
|
638
|
+
const a = address.toLowerCase();
|
|
639
|
+
if (s.deposits.some((d) => d.chainId === chainId && d.address.toLowerCase() === a))
|
|
640
|
+
return { kind: 'deposit' };
|
|
641
|
+
const paid = s.sent.find((x) => x.chainId === chainId && x.address.toLowerCase() === a);
|
|
642
|
+
return paid ? { kind: 'payment', to: paid.to } : null;
|
|
643
|
+
}
|
|
602
644
|
/** The payments to the burn addresses this wallet's points of sale handed out. Spent ones are, normally, in a
|
|
603
645
|
* merged note. */
|
|
604
646
|
payments() {
|
|
@@ -622,8 +664,12 @@ export class Wallet {
|
|
|
622
664
|
});
|
|
623
665
|
}
|
|
624
666
|
/** Refunds deposit `id` to its sender without the ASP, the attester or a proved root: for a deposit the ASP
|
|
625
|
-
* rejected, or when those services stop. The account's wallet sends the transactions and pays their gas
|
|
626
|
-
|
|
667
|
+
* rejected, or when those services stop. The account's wallet sends the transactions and pays their gas, or with
|
|
668
|
+
* `from` another wallet of the user's does, as `deposit({ from })` pays. One through a deposit address of this
|
|
669
|
+
* wallet's (`CoinInfo.via`) goes to `to`, by default `from`'s account, else this account; any other goes back to its
|
|
670
|
+
* sender, and one through a point of sale's deposit address to the refund address its payer named. */
|
|
671
|
+
rageExit(id, opts = {}) {
|
|
672
|
+
const route = this.#route(opts);
|
|
627
673
|
return this.#seeded(async () => {
|
|
628
674
|
const [chain, index] = id.split(':');
|
|
629
675
|
const record = this.#ctx.state.deposits.find((d) => `${d.chainId}` === chain && d.found.some((f) => `${f.index}` === index)) ??
|
|
@@ -632,9 +678,32 @@ export class Wallet {
|
|
|
632
678
|
const op = this.#ctx.state.ops.find((o) => `${o.chainId}` === chain && inputsOf(o).some((c) => `${c.index}` === index));
|
|
633
679
|
if (op)
|
|
634
680
|
fail('invalid', `deposit ${id} is an input of operation ${op.id}, which is in flight`);
|
|
635
|
-
return rageExit(this.#ctx, record, Number(index));
|
|
681
|
+
return rageExit(this.#ctx, record, Number(index), route);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/** Sends what deposit address `address` of this wallet's holds of `asset` (NATIVE for the chain's own currency) to
|
|
685
|
+
* `to`: another token sent to it, which it cannot wrap. What it can wrap is wrapped instead, as a payment to it is.
|
|
686
|
+
* It deploys on its first flush, and refunds nothing before. `from` and `to` as for `rageExit`. */
|
|
687
|
+
refundDepositAddress(address, asset, opts = {}) {
|
|
688
|
+
if (!isAddress(asset, { strict: false }) || asset.toLowerCase() === zeroAddress)
|
|
689
|
+
fail('invalid', `${asset} is not an asset's address`);
|
|
690
|
+
const route = this.#route(opts);
|
|
691
|
+
return this.#seeded(async () => {
|
|
692
|
+
const record = this.#ctx.state.deposits.find((d) => !d.managed && this.#via(d, address)) ??
|
|
693
|
+
fail('invalid', `${address} is not a deposit address of this wallet's`);
|
|
694
|
+
return refundDepositAddress(this.#ctx, record, getAddress(address), getAddress(asset), route);
|
|
636
695
|
});
|
|
637
696
|
}
|
|
697
|
+
/** Who sends a refund and where it goes (`rageExit`), checked before the seed is asked for. */
|
|
698
|
+
#route(opts) {
|
|
699
|
+
const from = opts.from ?? null;
|
|
700
|
+
if (from && !from.account)
|
|
701
|
+
fail('invalid', 'the wallet client has no account');
|
|
702
|
+
const to = opts.to ?? from?.account?.address ?? this.address;
|
|
703
|
+
if (!isAddress(to, { strict: false }) || to.toLowerCase() === zeroAddress)
|
|
704
|
+
fail('invalid', `${to} is not an address to refund to`);
|
|
705
|
+
return { from, to: getAddress(to) };
|
|
706
|
+
}
|
|
638
707
|
operations() {
|
|
639
708
|
return this.#ctx.state.ops.map((op) => this.#operation(op));
|
|
640
709
|
}
|
|
@@ -699,7 +768,9 @@ export class Wallet {
|
|
|
699
768
|
let record = unusedBurn(ctx, token.chainId, token.tokenId);
|
|
700
769
|
if (!record) {
|
|
701
770
|
recording();
|
|
702
|
-
record = await recordBurn(ctx, token.chainId, token.tokenId, await tokenIds(ctx),
|
|
771
|
+
record = await recordBurn(ctx, token.chainId, token.tokenId, await tokenIds(ctx), {
|
|
772
|
+
signal: this.#closed.signal,
|
|
773
|
+
});
|
|
703
774
|
}
|
|
704
775
|
const next = chainState(ctx, token.chainId).burnNext;
|
|
705
776
|
// recording may have read another device's later ones
|
|
@@ -713,10 +784,12 @@ export class Wallet {
|
|
|
713
784
|
* burn address (approving it first if needed). The deposit is spendable once the ASP has screened it. More than the
|
|
714
785
|
* account holds fails at once (`insufficient-funds`), before anything is asked of the wallet.
|
|
715
786
|
*
|
|
716
|
-
* With `to`, an
|
|
717
|
-
* under the public key they registered, told to them in storage first as a note is
|
|
718
|
-
* receive it as a deposit of their own, screened like any; the burn address names no one. It
|
|
719
|
-
* as sent. Finding the address takes about 2^16 hashes (`burn-address`).
|
|
787
|
+
* With `to`, an account that registered, by its address or its zERC20 ID, it pays them instead, privately: into a
|
|
788
|
+
* fresh burn address of theirs, under the public key they registered, told to them in storage first as a note is
|
|
789
|
+
* (`planDeposit`). They receive it as a deposit of their own, screened like any; the burn address names no one. It
|
|
790
|
+
* shows in `activity()` as sent. Finding the address takes about 2^16 hashes (`burn-address`). Or `to` is a receive
|
|
791
|
+
* code (`receiveCode`), paid into its burn address, which its owner already watches: storage is told nothing, so
|
|
792
|
+
* only this device's `activity()` shows it as sent.
|
|
720
793
|
*
|
|
721
794
|
* With `wrapped`, it pays from the account's zERC20 itself, as a withdrawal pays out, rather than its underlying
|
|
722
795
|
* asset: a plain transfer into the burn address, with no wrap and no approval.
|
|
@@ -731,6 +804,7 @@ export class Wallet {
|
|
|
731
804
|
if (opts.amount <= 0n)
|
|
732
805
|
fail('invalid', 'the amount must be positive');
|
|
733
806
|
const payer = opts.from ? (opts.from.account ?? fail('invalid', 'the wallet client has no account')) : null;
|
|
807
|
+
const to = opts.to === undefined ? null : recipientOf(opts.to, opts.token);
|
|
734
808
|
const step = opts.onStep ?? (() => { });
|
|
735
809
|
if (this.#busy > 0)
|
|
736
810
|
step('queued');
|
|
@@ -753,10 +827,16 @@ export class Wallet {
|
|
|
753
827
|
const of = wrapped ? token.symbol : `${token.symbol}'s underlying asset`;
|
|
754
828
|
fail('insufficient-funds', `${payer ? 'the wallet' : 'the account'} holds ${formatUnits(held, token.decimals)} of ${of}, less than ${formatUnits(amount, token.decimals)}`);
|
|
755
829
|
}
|
|
756
|
-
const address =
|
|
757
|
-
?
|
|
758
|
-
:
|
|
759
|
-
|
|
830
|
+
const address = to?.kind === 'code'
|
|
831
|
+
? to.burnAddress
|
|
832
|
+
: to
|
|
833
|
+
? await this.#burnFor(token, amount, to.address, () => step('burn-address'))
|
|
834
|
+
: (await this.#burnAddress(token, null, () => step('burn-address'))).address;
|
|
835
|
+
const op = this.#newOp('deposit', token, amount, to?.kind === 'code'
|
|
836
|
+
? { burnAddress: address, code: to.code }
|
|
837
|
+
: to
|
|
838
|
+
? { burnAddress: address, to: to.address }
|
|
839
|
+
: { burnAddress: address });
|
|
760
840
|
await this.#ctx.save();
|
|
761
841
|
try {
|
|
762
842
|
const wc = opts.from ? await switched(opts.from, token.chainId) : await walletOn(this.#ctx, token.chainId);
|
|
@@ -826,7 +906,7 @@ export class Wallet {
|
|
|
826
906
|
const ctx = this.#ctx;
|
|
827
907
|
const recipient = await lookupRecipient(ctx.storage, hexToBytes(to));
|
|
828
908
|
if (!recipient)
|
|
829
|
-
fail('recipient-not-registered', `${to}
|
|
909
|
+
fail('recipient-not-registered', `${to} never opened a zERC20 wallet: ask for a receive code`);
|
|
830
910
|
searching();
|
|
831
911
|
const burn = await burnAddressFor(token.chainId, bytesToHex(recipient.publicKey), this.#closed.signal);
|
|
832
912
|
// with the address, so that every device of this wallet finds the transfer it then sends
|
|
@@ -845,18 +925,28 @@ export class Wallet {
|
|
|
845
925
|
/** An address that takes `token`'s underlying asset from anywhere (an exchange withdrawal, another wallet) and
|
|
846
926
|
* deposits it privately, less `fee` (in the underlying's base units) to the deposit relayer each time: what arrives
|
|
847
927
|
* must exceed it. A wallet opened with a `referrer` also pays it its `bps` of the rest. Watched for 7 days by the
|
|
848
|
-
* deposit relayer
|
|
928
|
+
* deposit relayer. It names no account: its refunds go where a key of its own signs for (`rageExit`'s `to`,
|
|
929
|
+
* `refundDepositAddress`). `depositAddresses()` lists it. `onStep` hears what it waits on (`queued`,
|
|
930
|
+
* `burn-address`). */
|
|
849
931
|
depositAddress(token, opts = {}) {
|
|
850
932
|
const step = opts.onStep ?? (() => { });
|
|
933
|
+
const ctx = this.#ctx;
|
|
934
|
+
const code = (index) => ({ chainId: token.chainId, tokenId: token.tokenId, index });
|
|
935
|
+
// the seed is asked for only past the refund signers derived ahead
|
|
936
|
+
const derived = !!refundSigner(ctx, code(nextBurn(ctx, token.chainId, token.tokenId)));
|
|
851
937
|
if (this.#busy > 0)
|
|
852
938
|
step('queued');
|
|
853
|
-
return this.#
|
|
854
|
-
|
|
855
|
-
const c = chainCtx(this.#ctx, token.chainId);
|
|
939
|
+
return this.#seeded(async () => {
|
|
940
|
+
const c = chainCtx(ctx, token.chainId);
|
|
856
941
|
const record = await this.#burnAddress(token, null, () => step('burn-address'));
|
|
857
|
-
|
|
942
|
+
if (!refundSigner(ctx, code(record.burnIndex)))
|
|
943
|
+
await this.#seed(); // calls queued before took the next ones
|
|
944
|
+
const signer = refundSigner(ctx, code(record.burnIndex));
|
|
945
|
+
const referrer = this.#referrer ?? { address: zeroAddress, bps: 0 };
|
|
946
|
+
const { depositAddress, feeRecipient, fee } = await registerDeposit(c.chain, token.address, record.address, { signer }, referrer);
|
|
858
947
|
record.depositAddress = depositAddress;
|
|
859
|
-
// for the wallet's other devices, to show what comes through it
|
|
948
|
+
// for the wallet's other devices, to show what comes through it and what it takes, and for a flush without the
|
|
949
|
+
// relayer: written by the next sync
|
|
860
950
|
const burnAddress = record.address.toLowerCase();
|
|
861
951
|
this.#ctx.state.receipts.push({
|
|
862
952
|
kind: 'deposit-address',
|
|
@@ -864,12 +954,42 @@ export class Wallet {
|
|
|
864
954
|
tokenId: token.tokenId,
|
|
865
955
|
burnAddress,
|
|
866
956
|
depositAddress,
|
|
957
|
+
args: { fee, feeRecipient, refundSigner: signer, referrer: referrer.address, referralBps: referrer.bps },
|
|
867
958
|
atMs: Date.now(),
|
|
868
959
|
unwritten: true,
|
|
869
960
|
});
|
|
870
961
|
await this.#ctx.save();
|
|
871
962
|
return { address: depositAddress, fee };
|
|
872
|
-
});
|
|
963
|
+
}, !derived);
|
|
964
|
+
}
|
|
965
|
+
/** Has the deposit relayer watch deposit address `address` of this wallet's for another 7 days, at the fee it was
|
|
966
|
+
* handed out with: what reached it after its 7 days is deposited then, and so is what reaches it within the next.
|
|
967
|
+
* `depositAddresses()` dates it from now, on every device. */
|
|
968
|
+
rewatchDepositAddress(address) {
|
|
969
|
+
return this.#seeded(async () => {
|
|
970
|
+
const receipt = this.#depositReceipt(address);
|
|
971
|
+
const fee = await rewatchDeposit(chainCtx(this.#ctx, receipt.chainId).chain, getAddress(address));
|
|
972
|
+
// for every device of the wallet, written by the next sync: storage dates it anew
|
|
973
|
+
receipt.atMs = Date.now();
|
|
974
|
+
receipt.unwritten = true;
|
|
975
|
+
await this.#ctx.save();
|
|
976
|
+
return { address: getAddress(address), fee };
|
|
977
|
+
}, false);
|
|
978
|
+
}
|
|
979
|
+
/** Deposits what deposit address `address` of this wallet's holds, as the deposit relayer would, for when it does
|
|
980
|
+
* not: down, or no longer watching the address. Its receipt says all the address was registered with, wherever its
|
|
981
|
+
* `fee` is known (`depositAddresses()`), so no relayer is asked. `from`, another wallet of the user's, sends the
|
|
982
|
+
* flush and pays its gas, and shows on chain as the depositor: never the account's own, which the address does not
|
|
983
|
+
* name. What it holds must exceed the fee (`insufficient-funds`). It shows in `activity()` as any deposit through
|
|
984
|
+
* the address. */
|
|
985
|
+
flushDepositAddress(address, opts) {
|
|
986
|
+
if (!opts.from.account)
|
|
987
|
+
fail('invalid', 'the wallet client has no account');
|
|
988
|
+
return this.#seeded(() => flushDepositAddress(this.#ctx, this.#depositReceipt(address), opts.from), false);
|
|
989
|
+
}
|
|
990
|
+
/** The receipt of deposit address `address` of this wallet's. */
|
|
991
|
+
#depositReceipt(address) {
|
|
992
|
+
return (this.#ctx.state.receipts.find((r) => r.kind === 'deposit-address' && isAddressEqual(r.depositAddress, address)) ?? fail('invalid', `${address} is not a deposit address of this wallet's`));
|
|
873
993
|
}
|
|
874
994
|
/** A receive code for `token`, to hand to one payer: it pays any amount, as often as it likes, publicly by a transfer
|
|
875
995
|
* of the token to the code's burn address (a deposit, like a point of sale's payment addresses), or privately by
|
|
@@ -913,14 +1033,20 @@ export class Wallet {
|
|
|
913
1033
|
const steps = BigInt(roundsToMerge(mergePool(all, chosen)));
|
|
914
1034
|
// a merge pays the relayer publicly, so the protocol takes its cut of that, grossed up as a payment's is
|
|
915
1035
|
const cut = steps === 0n ? 0n : (await grossed(this.#ctx, token.chainId, token.tokenId, relayer)) - relayer;
|
|
916
|
-
return {
|
|
1036
|
+
return {
|
|
1037
|
+
relayer: relayer * steps,
|
|
1038
|
+
protocol: cut * steps,
|
|
1039
|
+
protocolBps: bpsOf(cut, relayer + cut),
|
|
1040
|
+
fee: (relayer + cut) * steps,
|
|
1041
|
+
steps: Number(steps),
|
|
1042
|
+
};
|
|
917
1043
|
}
|
|
918
1044
|
// as the payment's plan reckons it, so that `fee` is a `maxFee` it passes
|
|
919
|
-
const { protocol } = await withdrawalOf(this.#ctx, token.chainId, token.tokenId, opts.kind === 'withdraw', amount, relayer);
|
|
1045
|
+
const { withdrawn, protocol } = await withdrawalOf(this.#ctx, token.chainId, token.tokenId, opts.kind === 'withdraw', amount, relayer);
|
|
920
1046
|
const onTheWay = this.#ctx.state.ops.some((o) => o.status === 'pending') ||
|
|
921
1047
|
settling(this.#ctx, token.chainId, token.tokenId, size);
|
|
922
1048
|
payWith(all, amount + relayer + protocol, relayer, onTheWay);
|
|
923
|
-
return { relayer, protocol, fee: relayer + protocol, steps: 1 };
|
|
1049
|
+
return { relayer, protocol, protocolBps: bpsOf(protocol, withdrawn), fee: relayer + protocol, steps: 1 };
|
|
924
1050
|
}
|
|
925
1051
|
/** The most a send or withdrawal of `token` can pay: what is spendable, less the fees `fees` quotes on top of it, and
|
|
926
1052
|
* the merges first when one JoinSplit cannot spend it all, which `fees` then asks for (`needs-consolidation`). */
|
|
@@ -942,9 +1068,10 @@ export class Wallet {
|
|
|
942
1068
|
const merge = mergeRound(coins(this.#ctx, token.chainId, token.tokenId, size), fee);
|
|
943
1069
|
return merge && merge.map((m) => `${token.chainId}:${m.index}`);
|
|
944
1070
|
}
|
|
945
|
-
/** Pays `amount` privately: to an
|
|
946
|
-
* (`receiveCode`). An
|
|
947
|
-
* what is spendable now, or it fails here: `needs-consolidation`,
|
|
1071
|
+
/** Pays `amount` privately: to an account that has registered (any wallet that ever opened), by its address or its
|
|
1072
|
+
* zERC20 ID (`accountId`), or to a receive code (`receiveCode`). An account that never registered fails with
|
|
1073
|
+
* `recipient-not-registered`. One JoinSplit from what is spendable now, or it fails here: `needs-consolidation`,
|
|
1074
|
+
* `funds-settling` or `insufficient-funds`.
|
|
948
1075
|
* `amount` is what the recipient gets, and `maxFee` caps what it pays on top, `fees({ kind: 'send' }).fee`: the
|
|
949
1076
|
* relayer names its fee, so a quote the user saw is the cap. */
|
|
950
1077
|
async send(opts) {
|
|
@@ -953,21 +1080,16 @@ export class Wallet {
|
|
|
953
1080
|
fail('invalid', 'the amount must be positive');
|
|
954
1081
|
// the JoinSplit is proved by a later sync, which holds the seed until the payment is done
|
|
955
1082
|
return this.#seeded(async () => {
|
|
1083
|
+
const r = recipientOf(opts.to, token);
|
|
956
1084
|
let to;
|
|
957
|
-
if (
|
|
958
|
-
const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(
|
|
1085
|
+
if (r.kind === 'address') {
|
|
1086
|
+
const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(r.address));
|
|
959
1087
|
if (!recipient)
|
|
960
|
-
fail('recipient-not-registered', `${opts.to}
|
|
1088
|
+
fail('recipient-not-registered', `${opts.to.trim()} never opened a zERC20 wallet: ask for a receive code`);
|
|
961
1089
|
to = { kind: 'address', recipient: recipient };
|
|
962
1090
|
}
|
|
963
|
-
else
|
|
964
|
-
|
|
965
|
-
if (!code)
|
|
966
|
-
return fail('invalid', 'not an address or a receive code');
|
|
967
|
-
if (code.chainId !== token.chainId || code.tokenId !== token.tokenId)
|
|
968
|
-
fail('invalid', 'the receive code is for another token');
|
|
969
|
-
to = { kind: 'code', code: opts.to.trim() };
|
|
970
|
-
}
|
|
1091
|
+
else
|
|
1092
|
+
to = { kind: 'code', code: r.code };
|
|
971
1093
|
return this.#newPayment('send', token, amount, { to, maxFee: opts.maxFee, step: null, paid: 0n });
|
|
972
1094
|
});
|
|
973
1095
|
}
|
|
@@ -1036,6 +1158,19 @@ export class Wallet {
|
|
|
1036
1158
|
return this.#operation(op);
|
|
1037
1159
|
}
|
|
1038
1160
|
}
|
|
1161
|
+
/** Whom `to` names, as `send` and `deposit` take it: an account, by its address or its zERC20 ID, or a receive code of
|
|
1162
|
+
* `token`'s. */
|
|
1163
|
+
function recipientOf(to, token) {
|
|
1164
|
+
const address = isAddress(to) ? to : idAccount(to);
|
|
1165
|
+
if (address)
|
|
1166
|
+
return { kind: 'address', address };
|
|
1167
|
+
const code = decodeCode(to);
|
|
1168
|
+
if (!code)
|
|
1169
|
+
return fail('invalid', 'not an address, a zERC20 ID or a receive code');
|
|
1170
|
+
if (code.chainId !== token.chainId || code.tokenId !== token.tokenId)
|
|
1171
|
+
fail('invalid', 'the receive code is for another token');
|
|
1172
|
+
return { kind: 'code', code: to.trim(), burnAddress: bytesToHex(code.burnAddress) };
|
|
1173
|
+
}
|
|
1039
1174
|
/** Each chain of `network`, with its servers. */
|
|
1040
1175
|
const chainsOf = (network) => network.chains.map((config) => ({
|
|
1041
1176
|
config,
|