@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/src/wallet.ts
CHANGED
|
@@ -27,10 +27,12 @@ import {
|
|
|
27
27
|
getAddress,
|
|
28
28
|
hexToBytes,
|
|
29
29
|
isAddress,
|
|
30
|
+
isAddressEqual,
|
|
30
31
|
numberToHex,
|
|
31
32
|
zeroAddress,
|
|
32
33
|
} from 'viem';
|
|
33
34
|
import { toAccount } from 'viem/accounts';
|
|
35
|
+
import { idAccount } from './accountId.js';
|
|
34
36
|
import { Chain, MAX_REFERRAL_BPS, NATIVE, type Referrer, type Token, erc20Abi, tokenAbi } from './chain.js';
|
|
35
37
|
import {
|
|
36
38
|
type ChainCtx,
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
chainCtx,
|
|
40
42
|
chainState,
|
|
41
43
|
owner,
|
|
44
|
+
refundSigner,
|
|
42
45
|
switched,
|
|
43
46
|
walletOn,
|
|
44
47
|
walletSees,
|
|
@@ -47,14 +50,15 @@ import { burnAddressFor, core, initCore } from './core.js';
|
|
|
47
50
|
import { type ErrorCode, WalletError, fail, guard } from './errors.js';
|
|
48
51
|
import { type Network, checkNetwork } from './network.js';
|
|
49
52
|
import { Asp, Indexer, type IndexerStatus, PAGE } from './servers.js';
|
|
50
|
-
import { rageExit } from './rageExit.js';
|
|
51
|
-
import { registerDeposit, relayerFees } from './services.js';
|
|
53
|
+
import { type Route, flushDepositAddress, rageExit, refundDepositAddress } from './rageExit.js';
|
|
54
|
+
import { registerDeposit, relayerFees, rewatchDeposit } from './services.js';
|
|
52
55
|
import {
|
|
53
56
|
type Destination,
|
|
54
57
|
type MergeStep,
|
|
55
58
|
type SpendData,
|
|
56
59
|
type Step,
|
|
57
60
|
advanceSpend,
|
|
61
|
+
bpsOf,
|
|
58
62
|
chosenSettling,
|
|
59
63
|
coins,
|
|
60
64
|
grossed,
|
|
@@ -70,7 +74,9 @@ import {
|
|
|
70
74
|
withdrawalOf,
|
|
71
75
|
} from './spend.js';
|
|
72
76
|
import {
|
|
77
|
+
type DepositAddressReceipt,
|
|
73
78
|
type DepositOperation,
|
|
79
|
+
type DepositRecord,
|
|
74
80
|
type OperationRecord,
|
|
75
81
|
type WalletState,
|
|
76
82
|
emptyWalletState,
|
|
@@ -170,7 +176,8 @@ export interface Activity {
|
|
|
170
176
|
* paid none. */
|
|
171
177
|
fee: bigint | null;
|
|
172
178
|
/** The other side, where known: a payer's claimed address (unverified), where a payment or withdrawal went, or whom a
|
|
173
|
-
* 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. */
|
|
174
181
|
counterparty: Address | null;
|
|
175
182
|
/** What the counterparty alone does not say: the receive code a payment went to (it has no counterparty), or the
|
|
176
183
|
* deposit address a deposit came through (its counterparty, which forwarded what was sent to it). */
|
|
@@ -204,8 +211,13 @@ export interface CoinInfo {
|
|
|
204
211
|
state: 'spendable' | Exclude<PendingReason, 'incoming'> | 'spending' | 'blocked';
|
|
205
212
|
/** A note's payer (as claimed, unverified) or a deposit's sender, where known. */
|
|
206
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;
|
|
207
217
|
/** For a deposit in `screening`, when the ASP should decide it: an estimate, null until there is one. */
|
|
208
218
|
screenedBy: Date | null;
|
|
219
|
+
/** When it landed: its block's time, null until read. */
|
|
220
|
+
at: Date | null;
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
export interface DepositInfo {
|
|
@@ -221,12 +233,25 @@ export interface DepositInfo {
|
|
|
221
233
|
state: 'screening' | 'approved' | 'rejected' | 'spent';
|
|
222
234
|
}
|
|
223
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
|
+
|
|
224
247
|
/** A send's or withdrawal's recipient gets all of its amount, and the private balance pays `fee` on top. */
|
|
225
248
|
export interface FeeQuote {
|
|
226
249
|
/** To the relayer. */
|
|
227
250
|
relayer: bigint;
|
|
228
251
|
/** The protocol's cut of the public withdrawal: a withdrawal's, or the one that pays the relayer. */
|
|
229
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;
|
|
230
255
|
/** `relayer + protocol`: the `maxFee` this quote passes. */
|
|
231
256
|
fee: bigint;
|
|
232
257
|
/** Transactions needed: one JoinSplit for a send or a withdrawal, a consolidation's merges (each pays the relayer's
|
|
@@ -234,6 +259,16 @@ export interface FeeQuote {
|
|
|
234
259
|
steps: number;
|
|
235
260
|
}
|
|
236
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
|
+
|
|
237
272
|
export type OperationKind = OperationRecord['kind'];
|
|
238
273
|
export type OperationStatus = OperationRecord['status'];
|
|
239
274
|
export type CoinState = CoinInfo['state'];
|
|
@@ -256,7 +291,8 @@ export interface Operation {
|
|
|
256
291
|
/** How far a send, withdrawal or consolidation round has come, for an app to show; null for a deposit, or between
|
|
257
292
|
* rounds. It moves during a sync: read it while one runs. */
|
|
258
293
|
readonly stage: Step['stage'] | MergeStep['stage'] | null;
|
|
259
|
-
/** 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()`. */
|
|
260
296
|
readonly to: Address | null;
|
|
261
297
|
/** Syncs until the operation is done; throws if it fails. */
|
|
262
298
|
wait(): Promise<void>;
|
|
@@ -359,6 +395,7 @@ export class Wallet {
|
|
|
359
395
|
network: opts.network,
|
|
360
396
|
seed,
|
|
361
397
|
owners: new Map(),
|
|
398
|
+
refundSigners: new Map(),
|
|
362
399
|
address,
|
|
363
400
|
account,
|
|
364
401
|
walletClient: opts.walletClient ?? null,
|
|
@@ -394,6 +431,7 @@ export class Wallet {
|
|
|
394
431
|
network: opts.network,
|
|
395
432
|
seed: null,
|
|
396
433
|
owners: new Map([['', owner]]),
|
|
434
|
+
refundSigners: new Map(),
|
|
397
435
|
address,
|
|
398
436
|
// signs nothing: every call that would send is refused first
|
|
399
437
|
account: toAccount(address),
|
|
@@ -539,14 +577,8 @@ export class Wallet {
|
|
|
539
577
|
async #catchUp(): Promise<void> {
|
|
540
578
|
const errors = await syncAll(this.#ctx);
|
|
541
579
|
errors.push(...(await this.#readRoots()));
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
c.chain
|
|
545
|
-
.tokens()
|
|
546
|
-
.catch((e: unknown) => (errors.push(e), this.#tokens.filter((t) => t.chainId === c.config.chainId))),
|
|
547
|
-
),
|
|
548
|
-
);
|
|
549
|
-
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);
|
|
550
582
|
for (const op of this.#ctx.state.ops.filter((o) => o.status === 'pending')) {
|
|
551
583
|
try {
|
|
552
584
|
if (op.kind === 'send' || op.kind === 'withdraw' || op.kind === 'consolidate')
|
|
@@ -670,7 +702,9 @@ export class Wallet {
|
|
|
670
702
|
amount: n.value,
|
|
671
703
|
state,
|
|
672
704
|
from: n.peer,
|
|
705
|
+
via: null,
|
|
673
706
|
screenedBy: null,
|
|
707
|
+
at: f.atMs === undefined ? null : new Date(f.atMs),
|
|
674
708
|
});
|
|
675
709
|
}
|
|
676
710
|
}
|
|
@@ -697,7 +731,9 @@ export class Wallet {
|
|
|
697
731
|
amount: f.amount!,
|
|
698
732
|
state,
|
|
699
733
|
from: f.sender,
|
|
734
|
+
via: this.#via(d, f.sender),
|
|
700
735
|
screenedBy,
|
|
736
|
+
at: f.atMs === undefined ? null : new Date(f.atMs),
|
|
701
737
|
});
|
|
702
738
|
}
|
|
703
739
|
}
|
|
@@ -771,27 +807,17 @@ export class Wallet {
|
|
|
771
807
|
}
|
|
772
808
|
}
|
|
773
809
|
for (const d of s.deposits) {
|
|
774
|
-
// registered on this device, or on another, which says so in storage
|
|
775
|
-
const registered =
|
|
776
|
-
d.depositAddress ??
|
|
777
|
-
s.receipts.flatMap((r) =>
|
|
778
|
-
r.kind === 'deposit-address' && r.chainId === d.chainId && r.burnAddress === d.address.toLowerCase()
|
|
779
|
-
? [r.depositAddress]
|
|
780
|
-
: [],
|
|
781
|
-
)[0];
|
|
782
810
|
// on chain once found; shown once its commitment is read, which names the amount
|
|
783
811
|
for (const f of d.found.filter((f) => f.amount !== null)) {
|
|
784
|
-
const forwarded = registered && f.sender?.toLowerCase() === registered.toLowerCase();
|
|
785
812
|
// into a burn address a payer chose: theirs to this wallet, as a payment is
|
|
786
813
|
const kind = d.managed ? 'sale' : d.payer ? 'received' : 'deposited';
|
|
787
|
-
const via = forwarded ? { depositAddress: registered } : null;
|
|
788
814
|
const entry = {
|
|
789
815
|
...at(kind, d.chainId, f, d.createdMs),
|
|
790
816
|
token: this.#token(d.chainId, f.tokenId),
|
|
791
817
|
amount: f.amount!,
|
|
792
818
|
burnAddress: d.address,
|
|
793
819
|
};
|
|
794
|
-
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 });
|
|
795
821
|
}
|
|
796
822
|
}
|
|
797
823
|
// a deposit that paid someone (`deposit({ to })`), once its transfer is found
|
|
@@ -836,8 +862,16 @@ export class Wallet {
|
|
|
836
862
|
x.address === op.data.burnAddress.toLowerCase() &&
|
|
837
863
|
x.found.some((f) => f.amount !== null),
|
|
838
864
|
);
|
|
839
|
-
|
|
840
|
-
|
|
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
|
+
});
|
|
841
875
|
continue;
|
|
842
876
|
}
|
|
843
877
|
// under way (in operations()), or shown by its change
|
|
@@ -856,6 +890,21 @@ export class Wallet {
|
|
|
856
890
|
return out.sort((a, b) => (b.at?.getTime() ?? 0) - (a.at?.getTime() ?? 0));
|
|
857
891
|
}
|
|
858
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
|
+
|
|
859
908
|
/** The text of a receive code this wallet paid, by its burn address, from storage's record of paying it. */
|
|
860
909
|
#paidCode(chainId: bigint, burnAddress: Hex): string | null {
|
|
861
910
|
for (const c of this.#ctx.state.storage?.outgoing.lanes ?? []) {
|
|
@@ -900,6 +949,31 @@ export class Wallet {
|
|
|
900
949
|
);
|
|
901
950
|
}
|
|
902
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
|
+
|
|
903
977
|
/** The payments to the burn addresses this wallet's points of sale handed out. Spent ones are, normally, in a
|
|
904
978
|
* merged note. */
|
|
905
979
|
payments(): DepositInfo[] {
|
|
@@ -925,8 +999,12 @@ export class Wallet {
|
|
|
925
999
|
}
|
|
926
1000
|
|
|
927
1001
|
/** Refunds deposit `id` to its sender without the ASP, the attester or a proved root: for a deposit the ASP
|
|
928
|
-
* rejected, or when those services stop. The account's wallet sends the transactions and pays their gas
|
|
929
|
-
|
|
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);
|
|
930
1008
|
return this.#seeded(async () => {
|
|
931
1009
|
const [chain, index] = id.split(':');
|
|
932
1010
|
const record =
|
|
@@ -937,10 +1015,39 @@ export class Wallet {
|
|
|
937
1015
|
(o) => `${o.chainId}` === chain && inputsOf(o).some((c) => `${c.index}` === index),
|
|
938
1016
|
);
|
|
939
1017
|
if (op) fail('invalid', `deposit ${id} is an input of operation ${op.id}, which is in flight`);
|
|
940
|
-
return rageExit(this.#ctx, record, Number(index));
|
|
1018
|
+
return rageExit(this.#ctx, record, Number(index), route);
|
|
941
1019
|
});
|
|
942
1020
|
}
|
|
943
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);
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
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
|
+
|
|
944
1051
|
operations(): Operation[] {
|
|
945
1052
|
return this.#ctx.state.ops.map((op) => this.#operation(op));
|
|
946
1053
|
}
|
|
@@ -1010,7 +1117,9 @@ export class Wallet {
|
|
|
1010
1117
|
let record = unusedBurn(ctx, token.chainId, token.tokenId);
|
|
1011
1118
|
if (!record) {
|
|
1012
1119
|
recording();
|
|
1013
|
-
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
|
+
});
|
|
1014
1123
|
}
|
|
1015
1124
|
const next = chainState(ctx, token.chainId).burnNext;
|
|
1016
1125
|
// recording may have read another device's later ones
|
|
@@ -1025,10 +1134,12 @@ export class Wallet {
|
|
|
1025
1134
|
* burn address (approving it first if needed). The deposit is spendable once the ASP has screened it. More than the
|
|
1026
1135
|
* account holds fails at once (`insufficient-funds`), before anything is asked of the wallet.
|
|
1027
1136
|
*
|
|
1028
|
-
* With `to`, an
|
|
1029
|
-
* under the public key they registered, told to them in storage first as a note is
|
|
1030
|
-
* receive it as a deposit of their own, screened like any; the burn address names no one. It
|
|
1031
|
-
* 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.
|
|
1032
1143
|
*
|
|
1033
1144
|
* With `wrapped`, it pays from the account's zERC20 itself, as a withdrawal pays out, rather than its underlying
|
|
1034
1145
|
* asset: a plain transfer into the burn address, with no wrap and no approval.
|
|
@@ -1041,7 +1152,7 @@ export class Wallet {
|
|
|
1041
1152
|
async deposit(opts: {
|
|
1042
1153
|
token: Token;
|
|
1043
1154
|
amount: bigint;
|
|
1044
|
-
to?:
|
|
1155
|
+
to?: string;
|
|
1045
1156
|
wrapped?: boolean;
|
|
1046
1157
|
from?: WalletClient;
|
|
1047
1158
|
onStep?: (step: DepositStep) => void;
|
|
@@ -1049,6 +1160,7 @@ export class Wallet {
|
|
|
1049
1160
|
this.#writable();
|
|
1050
1161
|
if (opts.amount <= 0n) fail('invalid', 'the amount must be positive');
|
|
1051
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);
|
|
1052
1164
|
const step = opts.onStep ?? (() => {});
|
|
1053
1165
|
if (this.#busy > 0) step('queued');
|
|
1054
1166
|
return this.#exclusive(async () => {
|
|
@@ -1073,14 +1185,21 @@ export class Wallet {
|
|
|
1073
1185
|
`${payer ? 'the wallet' : 'the account'} holds ${formatUnits(held, token.decimals)} of ${of}, less than ${formatUnits(amount, token.decimals)}`,
|
|
1074
1186
|
);
|
|
1075
1187
|
}
|
|
1076
|
-
const address =
|
|
1077
|
-
|
|
1078
|
-
|
|
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;
|
|
1079
1194
|
const op = this.#newOp(
|
|
1080
1195
|
'deposit',
|
|
1081
1196
|
token,
|
|
1082
1197
|
amount,
|
|
1083
|
-
|
|
1198
|
+
to?.kind === 'code'
|
|
1199
|
+
? { burnAddress: address, code: to.code }
|
|
1200
|
+
: to
|
|
1201
|
+
? { burnAddress: address, to: to.address }
|
|
1202
|
+
: { burnAddress: address },
|
|
1084
1203
|
);
|
|
1085
1204
|
await this.#ctx.save();
|
|
1086
1205
|
try {
|
|
@@ -1153,11 +1272,7 @@ export class Wallet {
|
|
|
1153
1272
|
async #burnFor(token: Token, amount: bigint, to: Address, searching: () => void): Promise<Hex> {
|
|
1154
1273
|
const ctx = this.#ctx;
|
|
1155
1274
|
const recipient = await lookupRecipient(ctx.storage, hexToBytes(to));
|
|
1156
|
-
if (!recipient)
|
|
1157
|
-
fail(
|
|
1158
|
-
'recipient-not-registered',
|
|
1159
|
-
`${to} cannot be paid privately: withdraw to it publicly, or ask for a receive code`,
|
|
1160
|
-
);
|
|
1275
|
+
if (!recipient) fail('recipient-not-registered', `${to} never opened a zERC20 wallet: ask for a receive code`);
|
|
1161
1276
|
searching();
|
|
1162
1277
|
const burn = await burnAddressFor(token.chainId, bytesToHex(recipient!.publicKey), this.#closed.signal);
|
|
1163
1278
|
// with the address, so that every device of this wallet finds the transfer it then sends
|
|
@@ -1179,26 +1294,35 @@ export class Wallet {
|
|
|
1179
1294
|
/** An address that takes `token`'s underlying asset from anywhere (an exchange withdrawal, another wallet) and
|
|
1180
1295
|
* deposits it privately, less `fee` (in the underlying's base units) to the deposit relayer each time: what arrives
|
|
1181
1296
|
* must exceed it. A wallet opened with a `referrer` also pays it its `bps` of the rest. Watched for 7 days by the
|
|
1182
|
-
* 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`). */
|
|
1183
1300
|
depositAddress(
|
|
1184
1301
|
token: Token,
|
|
1185
1302
|
opts: { onStep?: (step: DepositStep) => void } = {},
|
|
1186
1303
|
): Promise<{ address: Address; fee: bigint }> {
|
|
1187
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)));
|
|
1188
1309
|
if (this.#busy > 0) step('queued');
|
|
1189
|
-
return this.#
|
|
1190
|
-
|
|
1191
|
-
const c = chainCtx(this.#ctx, token.chainId);
|
|
1310
|
+
return this.#seeded(async () => {
|
|
1311
|
+
const c = chainCtx(ctx, token.chainId);
|
|
1192
1312
|
const record = await this.#burnAddress(token, null, () => step('burn-address'));
|
|
1193
|
-
|
|
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(
|
|
1194
1317
|
c.chain,
|
|
1195
1318
|
token.address,
|
|
1196
1319
|
record.address,
|
|
1197
|
-
|
|
1198
|
-
|
|
1320
|
+
{ signer },
|
|
1321
|
+
referrer,
|
|
1199
1322
|
);
|
|
1200
1323
|
record.depositAddress = depositAddress;
|
|
1201
|
-
// 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
|
|
1202
1326
|
const burnAddress = record.address.toLowerCase() as Hex;
|
|
1203
1327
|
this.#ctx.state.receipts.push({
|
|
1204
1328
|
kind: 'deposit-address',
|
|
@@ -1206,12 +1330,48 @@ export class Wallet {
|
|
|
1206
1330
|
tokenId: token.tokenId,
|
|
1207
1331
|
burnAddress,
|
|
1208
1332
|
depositAddress,
|
|
1333
|
+
args: { fee, feeRecipient, refundSigner: signer, referrer: referrer.address, referralBps: referrer.bps },
|
|
1209
1334
|
atMs: Date.now(),
|
|
1210
1335
|
unwritten: true,
|
|
1211
1336
|
});
|
|
1212
1337
|
await this.#ctx.save();
|
|
1213
1338
|
return { address: depositAddress, fee };
|
|
1214
|
-
});
|
|
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
|
+
);
|
|
1215
1375
|
}
|
|
1216
1376
|
|
|
1217
1377
|
/** A receive code for `token`, to hand to one payer: it pays any amount, as often as it likes, publicly by a transfer
|
|
@@ -1265,10 +1425,16 @@ export class Wallet {
|
|
|
1265
1425
|
const steps = BigInt(roundsToMerge(mergePool(all, chosen)));
|
|
1266
1426
|
// a merge pays the relayer publicly, so the protocol takes its cut of that, grossed up as a payment's is
|
|
1267
1427
|
const cut = steps === 0n ? 0n : (await grossed(this.#ctx, token.chainId, token.tokenId, relayer)) - relayer;
|
|
1268
|
-
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
|
+
};
|
|
1269
1435
|
}
|
|
1270
1436
|
// as the payment's plan reckons it, so that `fee` is a `maxFee` it passes
|
|
1271
|
-
const { protocol } = await withdrawalOf(
|
|
1437
|
+
const { withdrawn, protocol } = await withdrawalOf(
|
|
1272
1438
|
this.#ctx,
|
|
1273
1439
|
token.chainId,
|
|
1274
1440
|
token.tokenId,
|
|
@@ -1280,12 +1446,12 @@ export class Wallet {
|
|
|
1280
1446
|
this.#ctx.state.ops.some((o) => o.status === 'pending') ||
|
|
1281
1447
|
settling(this.#ctx, token.chainId, token.tokenId, size);
|
|
1282
1448
|
payWith(all, amount + relayer + protocol, relayer, onTheWay);
|
|
1283
|
-
return { relayer, protocol, fee: relayer + protocol, steps: 1 };
|
|
1449
|
+
return { relayer, protocol, protocolBps: bpsOf(protocol, withdrawn), fee: relayer + protocol, steps: 1 };
|
|
1284
1450
|
}
|
|
1285
1451
|
|
|
1286
1452
|
/** The most a send or withdrawal of `token` can pay: what is spendable, less the fees `fees` quotes on top of it, and
|
|
1287
1453
|
* the merges first when one JoinSplit cannot spend it all, which `fees` then asks for (`needs-consolidation`). */
|
|
1288
|
-
async maxAmount(opts: { kind: 'send' | 'withdraw'; token: Token }): Promise<
|
|
1454
|
+
async maxAmount(opts: { kind: 'send' | 'withdraw'; token: Token }): Promise<MaxAmount> {
|
|
1289
1455
|
const { token } = opts;
|
|
1290
1456
|
const c = chainCtx(this.#ctx, token.chainId);
|
|
1291
1457
|
const relayer =
|
|
@@ -1314,9 +1480,10 @@ export class Wallet {
|
|
|
1314
1480
|
return merge && merge.map((m) => `${token.chainId}:${m.index}`);
|
|
1315
1481
|
}
|
|
1316
1482
|
|
|
1317
|
-
/** Pays `amount` privately: to an
|
|
1318
|
-
* (`receiveCode`). An
|
|
1319
|
-
* 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`.
|
|
1320
1487
|
* `amount` is what the recipient gets, and `maxFee` caps what it pays on top, `fees({ kind: 'send' }).fee`: the
|
|
1321
1488
|
* relayer names its fee, so a quote the user saw is the cap. */
|
|
1322
1489
|
async send(opts: { to: string; token: Token; amount: bigint; maxFee: bigint }): Promise<Operation> {
|
|
@@ -1324,22 +1491,14 @@ export class Wallet {
|
|
|
1324
1491
|
if (amount <= 0n) fail('invalid', 'the amount must be positive');
|
|
1325
1492
|
// the JoinSplit is proved by a later sync, which holds the seed until the payment is done
|
|
1326
1493
|
return this.#seeded(async () => {
|
|
1494
|
+
const r = recipientOf(opts.to, token);
|
|
1327
1495
|
let to: Destination;
|
|
1328
|
-
if (
|
|
1329
|
-
const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(
|
|
1496
|
+
if (r.kind === 'address') {
|
|
1497
|
+
const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(r.address));
|
|
1330
1498
|
if (!recipient)
|
|
1331
|
-
fail(
|
|
1332
|
-
'recipient-not-registered',
|
|
1333
|
-
`${opts.to} cannot be paid privately: withdraw to it publicly, or ask for a receive code`,
|
|
1334
|
-
);
|
|
1499
|
+
fail('recipient-not-registered', `${opts.to.trim()} never opened a zERC20 wallet: ask for a receive code`);
|
|
1335
1500
|
to = { kind: 'address', recipient: recipient! };
|
|
1336
|
-
} else {
|
|
1337
|
-
const code = decodeCode(opts.to);
|
|
1338
|
-
if (!code) return fail('invalid', 'not an address or a receive code');
|
|
1339
|
-
if (code.chainId !== token.chainId || code.tokenId !== token.tokenId)
|
|
1340
|
-
fail('invalid', 'the receive code is for another token');
|
|
1341
|
-
to = { kind: 'code', code: opts.to.trim() };
|
|
1342
|
-
}
|
|
1501
|
+
} else to = { kind: 'code', code: r.code };
|
|
1343
1502
|
return this.#newPayment('send', token, amount, { to, maxFee: opts.maxFee, step: null, paid: 0n });
|
|
1344
1503
|
});
|
|
1345
1504
|
}
|
|
@@ -1412,6 +1571,21 @@ export class Wallet {
|
|
|
1412
1571
|
}
|
|
1413
1572
|
}
|
|
1414
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
|
+
|
|
1415
1589
|
/** Each chain of `network`, with its servers. */
|
|
1416
1590
|
const chainsOf = (network: Network): ChainCtx[] =>
|
|
1417
1591
|
network.chains.map((config) => ({
|
|
@@ -105,7 +105,7 @@ export function signMerge(statement: string, witness: string): string;
|
|
|
105
105
|
* `tokenId`, `chainId`, `commitmentRoot` and `aspRoot`; the rest is ignored), and the attester's witness for it:
|
|
106
106
|
* `{statement, witness}`. The statement is the public signals the proof and the attestation cover. The attester's
|
|
107
107
|
* witness has each input's private key replaced by its signing and nullifying keys, and its owner's signature on the
|
|
108
|
-
* statement. Fails with the
|
|
108
|
+
* statement. Fails, with the reason, where the attester would refuse the witness.
|
|
109
109
|
*/
|
|
110
110
|
export function signTransact(statement: string, witness: string): string;
|
|
111
111
|
|
|
@@ -436,7 +436,7 @@ export function signMerge(statement, witness) {
|
|
|
436
436
|
* `tokenId`, `chainId`, `commitmentRoot` and `aspRoot`; the rest is ignored), and the attester's witness for it:
|
|
437
437
|
* `{statement, witness}`. The statement is the public signals the proof and the attestation cover. The attester's
|
|
438
438
|
* witness has each input's private key replaced by its signing and nullifying keys, and its owner's signature on the
|
|
439
|
-
* statement. Fails with the
|
|
439
|
+
* statement. Fails, with the reason, where the attester would refuse the witness.
|
|
440
440
|
* @param {string} statement
|
|
441
441
|
* @param {string} witness
|
|
442
442
|
* @returns {string}
|
|
Binary file
|