@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/spend.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { core } from './core.js';
|
|
|
26
26
|
import { fail } from './errors.js';
|
|
27
27
|
import { prove } from './prove.js';
|
|
28
28
|
import { scan } from './scan.js';
|
|
29
|
+
import type { Indexer } from './servers.js';
|
|
29
30
|
import {
|
|
30
31
|
type Relayed,
|
|
31
32
|
RelayUnknown,
|
|
@@ -74,8 +75,10 @@ export interface Step {
|
|
|
74
75
|
request?: object;
|
|
75
76
|
/** Reverts with no sign of it on chain: proved again with the same outputs a few times, then failed. */
|
|
76
77
|
attempts: number;
|
|
77
|
-
/** Once relayed: the
|
|
78
|
-
|
|
78
|
+
/** Once relayed: the block the relayer saw it mined in. */
|
|
79
|
+
relayedBlock?: number;
|
|
80
|
+
/** The indexer's nullifier count once it is past that block, or just after a relay that found it landed already.
|
|
81
|
+
* Final nullifiers past it, and the input's still not among them, means a reorg dropped the transaction. */
|
|
79
82
|
relayedAt?: number;
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -100,6 +103,8 @@ export interface MergeStep {
|
|
|
100
103
|
/** Refused by the prover, or reverting with no sign of it on chain: built again with the same output a few times,
|
|
101
104
|
* then failed. */
|
|
102
105
|
attempts: number;
|
|
106
|
+
/** As a JoinSplit's. */
|
|
107
|
+
relayedBlock?: number;
|
|
103
108
|
relayedAt?: number;
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -249,14 +254,20 @@ export function payWith(all: Coin[], need: bigint, fee: bigint, onTheWay: boolea
|
|
|
249
254
|
return fail('insufficient-funds', `${need} is more than the balance`);
|
|
250
255
|
}
|
|
251
256
|
|
|
252
|
-
/** The smallest public withdrawal that still pays `net` after the protocol's cut.
|
|
257
|
+
/** The smallest public withdrawal that still pays `net` after the protocol's cut. The cut is a rate of the whole
|
|
258
|
+
* withdrawal, rounded down, and the rate read off one withdrawal (`fee / gross`) is at most its tier's: the smallest
|
|
259
|
+
* gross that pays `net` at that rate is never more than the answer within the tier, so a read or two settles it.
|
|
260
|
+
* ponytail: where a lower tier's bound lies between two reads, that bound may already pay `net`, a little less than
|
|
261
|
+
* the gross this returns; a getter for the schedule's tiers would find it. */
|
|
253
262
|
export async function grossed(ctx: Ctx, chainId: bigint, tokenId: number, net: bigint): Promise<bigint> {
|
|
254
263
|
const chain = chainCtx(ctx, chainId).chain;
|
|
255
264
|
let gross = net;
|
|
256
|
-
|
|
265
|
+
// each read moves on by a unit at least, and a few settle each tier a withdrawal crosses
|
|
266
|
+
for (let i = 0; i < 64; i++) {
|
|
257
267
|
const paid = gross - (await chain.withdrawFee(tokenId, gross));
|
|
258
268
|
if (paid >= net) return gross;
|
|
259
|
-
|
|
269
|
+
if (paid <= 0n) break;
|
|
270
|
+
gross = ((net - 1n) * gross) / paid + 1n;
|
|
260
271
|
}
|
|
261
272
|
return fail('unavailable', 'the withdrawal fee schedule does not settle');
|
|
262
273
|
}
|
|
@@ -276,10 +287,16 @@ export async function withdrawalOf(
|
|
|
276
287
|
return { withdrawn, protocol: withdrawn - net };
|
|
277
288
|
}
|
|
278
289
|
|
|
290
|
+
/** The rate, in basis points, of a protocol's `cut` out of a withdrawal of `gross`. The cut is rounded down, so this is
|
|
291
|
+
* exact once `gross` is 10,000 base units or more. */
|
|
292
|
+
export const bpsOf = (cut: bigint, gross: bigint): number =>
|
|
293
|
+
gross === 0n ? 0 : Number((cut * 10_000n + gross - 1n) / gross);
|
|
294
|
+
|
|
279
295
|
/** The most a payment out of `all` pays its recipient, the relayer's `fee` and the protocol's cut on top
|
|
280
296
|
* (`withdrawalOf`): a withdrawal of what the relayer leaves, less the cut on it, or a private payment of what the
|
|
281
297
|
* relayer's grossed fee leaves. Coins one JoinSplit cannot spend together (`pick`) are merged first, each merge
|
|
282
|
-
* paying the relayer's grossed fee, as a consolidation's quote does.
|
|
298
|
+
* paying the relayer's grossed fee, as a consolidation's quote does. `fee` is all the fees take of `all`; where they
|
|
299
|
+
* take it all, what they would be with nothing left to pay. */
|
|
283
300
|
export async function maxPayment(
|
|
284
301
|
ctx: Ctx,
|
|
285
302
|
chainId: bigint,
|
|
@@ -287,13 +304,16 @@ export async function maxPayment(
|
|
|
287
304
|
isPublic: boolean,
|
|
288
305
|
all: Coin[],
|
|
289
306
|
fee: bigint,
|
|
290
|
-
): Promise<bigint> {
|
|
307
|
+
): Promise<{ amount: bigint; fee: bigint; merges: number }> {
|
|
291
308
|
const whole = all.length < 2 || (all.length === 2 && all.some((c) => c.kind === 'note'));
|
|
292
|
-
const merges = whole ?
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
309
|
+
const merges = whole ? 0 : roundsToMerge(all);
|
|
310
|
+
const gross = !isPublic || merges > 0 ? await grossed(ctx, chainId, tokenId, fee) : fee;
|
|
311
|
+
const fees = BigInt(merges) * gross + (isPublic ? fee : gross);
|
|
312
|
+
const total = all.reduce((s, c) => s + c.value, 0n);
|
|
313
|
+
const left = total - fees;
|
|
314
|
+
if (left <= 0n) return { amount: 0n, fee: fees, merges };
|
|
315
|
+
const amount = isPublic ? left - (await chainCtx(ctx, chainId).chain.withdrawFee(tokenId, left)) : left;
|
|
316
|
+
return { amount, fee: total - amount, merges };
|
|
297
317
|
}
|
|
298
318
|
|
|
299
319
|
/** Plans the next step of `op`: a payment's only JoinSplit (or throws why there is none), or a consolidation's next
|
|
@@ -643,12 +663,21 @@ async function inputSpent(ctx: Ctx, op: SpendOperation, step: { inputs: Coin[] }
|
|
|
643
663
|
return hits.length > 0;
|
|
644
664
|
}
|
|
645
665
|
|
|
646
|
-
/** Where a
|
|
647
|
-
*
|
|
648
|
-
* or neither yet. */
|
|
649
|
-
export function relayedState(f: Position | undefined, relayedAt: number): 'landed' | 'dropped' | 'waiting' {
|
|
666
|
+
/** Where a relayed JoinSplit stands, by its first input `f` as sync last saw it and `relayedAt`, a nullifier count past
|
|
667
|
+
* its own (`undefined` while not known): spent, or the final nullifiers passed that count without its own (a reorg
|
|
668
|
+
* dropped it, or the relayer never sent it), or neither yet. */
|
|
669
|
+
export function relayedState(f: Position | undefined, relayedAt: number | undefined): 'landed' | 'dropped' | 'waiting' {
|
|
650
670
|
if (f?.spent) return 'landed';
|
|
651
|
-
return f?.nullifier && f.nullifier.scannedTo >= relayedAt ? 'dropped' : 'waiting';
|
|
671
|
+
return f?.nullifier && relayedAt !== undefined && f.nullifier.scannedTo >= relayedAt ? 'dropped' : 'waiting';
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** The indexer's nullifier count once it is past `block`, so past the nullifiers of a transaction mined there unless a
|
|
675
|
+
* reorg moved them; `undefined` until then. The relayer answers once the block is mined, which may be before the
|
|
676
|
+
* indexer has read it. */
|
|
677
|
+
export async function nullifiersPast(indexer: Pick<Indexer, 'status'>, block: number): Promise<number | undefined> {
|
|
678
|
+
if ((await indexer.status()).cursor < block) return undefined;
|
|
679
|
+
// a status reads its counts before its cursor, so the count comes from the next one
|
|
680
|
+
return (await indexer.status()).nullifiers;
|
|
652
681
|
}
|
|
653
682
|
|
|
654
683
|
/** Whether `op`'s payment or withdrawal is on chain: done, or relayed, which the relayer answers once it is mined. Until
|
|
@@ -775,17 +804,20 @@ export async function advanceSpend(ctx: Ctx, op: SpendOperation): Promise<void>
|
|
|
775
804
|
await ctx.save();
|
|
776
805
|
return;
|
|
777
806
|
}
|
|
778
|
-
// an earlier relay of it landed, one whose answer was lost
|
|
807
|
+
// an earlier relay of it landed, one whose answer was lost, and the indexer counts its nullifier already
|
|
808
|
+
step.relayedAt = (await c.indexer.status()).nullifiers;
|
|
779
809
|
} else {
|
|
780
810
|
placeChange(ctx, op, step.outputs[1].salt, result.noteIndex2);
|
|
781
811
|
data.txHashes = [...(data.txHashes ?? []), result.txHash];
|
|
812
|
+
step.relayedBlock = result.block;
|
|
813
|
+
delete step.relayedAt;
|
|
782
814
|
}
|
|
783
|
-
step.relayedAt = (await c.indexer.status()).nullifiers;
|
|
784
815
|
step.stage = 'relayed';
|
|
785
816
|
await ctx.save();
|
|
786
817
|
}
|
|
787
818
|
if (step.stage === 'relayed') {
|
|
788
|
-
|
|
819
|
+
step.relayedAt ??= await nullifiersPast(c.indexer, step.relayedBlock!);
|
|
820
|
+
const now = relayedState(positionOf(ctx, op.chainId, step.inputs[0].index), step.relayedAt);
|
|
789
821
|
if (now === 'dropped') {
|
|
790
822
|
// relay it again next sync; if it did land after all, that reverts and finds it
|
|
791
823
|
step.stage = 'attested';
|
|
@@ -888,16 +920,19 @@ async function advanceMerge(ctx: Ctx, op: SpendOperation, step: MergeStep): Prom
|
|
|
888
920
|
if ('reverted' in result) {
|
|
889
921
|
// stale roots, most likely: built again with the same output, next sync
|
|
890
922
|
if (!(await inputSpent(ctx, op, step))) return again(result.reverted);
|
|
891
|
-
// an earlier relay of it landed, one whose answer was lost
|
|
923
|
+
// an earlier relay of it landed, one whose answer was lost, and the indexer counts its nullifiers already
|
|
924
|
+
step.relayedAt = (await c.indexer.status()).nullifiers;
|
|
892
925
|
} else {
|
|
893
926
|
placeChange(ctx, op, step.output.salt, result.noteIndex);
|
|
894
927
|
data.txHashes = [...(data.txHashes ?? []), result.txHash];
|
|
928
|
+
step.relayedBlock = result.block;
|
|
929
|
+
delete step.relayedAt;
|
|
895
930
|
}
|
|
896
|
-
step.relayedAt = (await c.indexer.status()).nullifiers;
|
|
897
931
|
step.stage = 'relayed';
|
|
898
932
|
await ctx.save();
|
|
899
933
|
}
|
|
900
|
-
|
|
934
|
+
step.relayedAt ??= await nullifiersPast(c.indexer, step.relayedBlock!);
|
|
935
|
+
const now = relayedState(positionOf(ctx, op.chainId, step.inputs[0].index), step.relayedAt);
|
|
901
936
|
if (now === 'dropped') {
|
|
902
937
|
// relay it again next sync; if it did land after all, that reverts and finds it
|
|
903
938
|
step.stage = 'proved';
|
package/src/state.ts
CHANGED
|
@@ -86,6 +86,10 @@ export interface DepositRecord {
|
|
|
86
86
|
/** Chosen by a payer, who told it in storage (a burn), as claimed: its salt is the payer's, with no index
|
|
87
87
|
* (`burnIndex` -1), and it is never handed out. */
|
|
88
88
|
payer?: Hex;
|
|
89
|
+
/** Recorded by another device of the wallet, or before a restore: taken as handed out, so never handed out here. A
|
|
90
|
+
* flag of its own rather than the token's `burnNext`, which would also pass the one this device recorded ahead and
|
|
91
|
+
* still holds: with two devices open, each would then record one more ahead each sync, without end. */
|
|
92
|
+
elsewhere?: true;
|
|
89
93
|
/** The transfer's tag under each token. */
|
|
90
94
|
watches: { tokenId: number; watch: Watch }[];
|
|
91
95
|
found: (Position & {
|
|
@@ -114,9 +118,27 @@ export interface SentRecord extends Pick<
|
|
|
114
118
|
* `unwritten` until storage holds it: every sync tries again. */
|
|
115
119
|
export type ReceiptRecord = { chainId: bigint; tokenId: number; burnAddress: Hex; atMs: number; unwritten?: true } & (
|
|
116
120
|
| { kind: 'returned'; index: number; recipient: Hex; amount: bigint; txHash: Hex }
|
|
117
|
-
| {
|
|
121
|
+
| {
|
|
122
|
+
kind: 'deposit-address';
|
|
123
|
+
depositAddress: Hex;
|
|
124
|
+
/** From the relayer's answer, or as storage told them. Absent where storage's record does not tell them, and then
|
|
125
|
+
* only the relayer flushes it. */
|
|
126
|
+
args?: DepositAddressArgs;
|
|
127
|
+
}
|
|
118
128
|
);
|
|
119
129
|
|
|
130
|
+
/** What a deposit address was registered with besides its token and burn address: with those, all a flush of it takes.
|
|
131
|
+
* A wallet's refunds go where a signer of its own signs for, so its refund address is zero. */
|
|
132
|
+
export interface DepositAddressArgs {
|
|
133
|
+
fee: bigint;
|
|
134
|
+
feeRecipient: Hex;
|
|
135
|
+
refundSigner: Hex;
|
|
136
|
+
referrer: Hex;
|
|
137
|
+
referralBps: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type DepositAddressReceipt = Extract<ReceiptRecord, { kind: 'deposit-address' }>;
|
|
141
|
+
|
|
120
142
|
/** A sample of the indexer's final counts, to bound where a note announced at some time can be. */
|
|
121
143
|
export interface Sample {
|
|
122
144
|
atMs: number;
|
|
@@ -155,6 +177,8 @@ export interface DepositOperation extends OperationBase {
|
|
|
155
177
|
burnAddress: Hex;
|
|
156
178
|
/** Whose burn address it is, when not this wallet's (`deposit({ to })`). */
|
|
157
179
|
to?: Hex;
|
|
180
|
+
/** The receive code it paid, when `to` was one. */
|
|
181
|
+
code?: string;
|
|
158
182
|
/** Its transaction, once sent. */
|
|
159
183
|
txHash?: Hex;
|
|
160
184
|
};
|
package/src/sync.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import {
|
|
5
5
|
type Deposit,
|
|
6
6
|
type LaneRecord,
|
|
7
|
+
type DepositArgs,
|
|
7
8
|
type NewRecord,
|
|
8
9
|
type Note,
|
|
9
10
|
type OwnReceipt,
|
|
@@ -18,11 +19,11 @@ import {
|
|
|
18
19
|
} from '@zerc20/storage-sdk';
|
|
19
20
|
import { type Hex, bytesToHex, hexToBytes, numberToHex, parseAbiItem } from 'viem';
|
|
20
21
|
import type { Chain, Token } from './chain.js';
|
|
21
|
-
import { type ChainCtx, type Ctx, chainCtx, chainState, owner } from './context.js';
|
|
22
|
+
import { type ChainCtx, type Ctx, chainCtx, chainState, owner, refundSigner } from './context.js';
|
|
22
23
|
import { burnAddress, core } from './core.js';
|
|
23
24
|
import { type Hit, type Watch, scan } from './scan.js';
|
|
24
25
|
import { type Asp, type AspStatus, type Indexer, PAGE } from './servers.js';
|
|
25
|
-
import type { DepositRecord, NoteReceipt, NoteRecord, Position, Sample } from './state.js';
|
|
26
|
+
import type { DepositAddressArgs, DepositRecord, NoteReceipt, NoteRecord, Position, Sample } from './state.js';
|
|
26
27
|
|
|
27
28
|
/** The ledger's log of each commitment, found by its index (`contracts/src/CommitmentLedger.sol`). */
|
|
28
29
|
const committedEvent = parseAbiItem('event Committed(uint64 indexed index, uint256[4] fields)');
|
|
@@ -95,7 +96,12 @@ async function writeReceipts(ctx: Ctx, tokens: Map<bigint, number[]>): Promise<v
|
|
|
95
96
|
amount: hexToBytes(numberToHex(r.amount, { size: 32 })),
|
|
96
97
|
txHash: hexToBytes(r.txHash),
|
|
97
98
|
}
|
|
98
|
-
: {
|
|
99
|
+
: {
|
|
100
|
+
...head,
|
|
101
|
+
kind: r.kind,
|
|
102
|
+
depositAddress: hexToBytes(r.depositAddress),
|
|
103
|
+
...(r.args && { args: depositArgsBytes(r.args) }),
|
|
104
|
+
};
|
|
99
105
|
takeRefresh(
|
|
100
106
|
ctx,
|
|
101
107
|
await commit(ctx.storage, ctx.view, storageState(ctx), (s) => planReceipt(ctx.view, s, receipt)),
|
|
@@ -106,6 +112,24 @@ async function writeReceipts(ctx: Ctx, tokens: Map<bigint, number[]>): Promise<v
|
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
/** A deposit address's arguments as the wallet keeps them, from storage's. */
|
|
116
|
+
const depositArgs = (a: DepositArgs): DepositAddressArgs => ({
|
|
117
|
+
fee: big(a.fee),
|
|
118
|
+
feeRecipient: bytesToHex(a.feeRecipient),
|
|
119
|
+
refundSigner: bytesToHex(a.refundSigner),
|
|
120
|
+
referrer: bytesToHex(a.referrer),
|
|
121
|
+
referralBps: a.referralBps,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/** And storage's, from the wallet's. */
|
|
125
|
+
const depositArgsBytes = (a: DepositAddressArgs): DepositArgs => ({
|
|
126
|
+
fee: hexToBytes(numberToHex(a.fee, { size: 32 })),
|
|
127
|
+
feeRecipient: hexToBytes(a.feeRecipient),
|
|
128
|
+
refundSigner: hexToBytes(a.refundSigner),
|
|
129
|
+
referrer: hexToBytes(a.referrer),
|
|
130
|
+
referralBps: a.referralBps,
|
|
131
|
+
});
|
|
132
|
+
|
|
109
133
|
/** A storage receipt as the wallet keeps it. */
|
|
110
134
|
export const noteReceipt = (r: Receipt): NoteReceipt => ({
|
|
111
135
|
kind: r.kind,
|
|
@@ -182,16 +206,24 @@ function takeSent(ctx: Ctx, d: Deposit, to: Uint8Array, tokens: Map<bigint, numb
|
|
|
182
206
|
function takeReceipt(ctx: Ctx, r: OwnReceipt & { atNs: bigint }): void {
|
|
183
207
|
const burnAddress = bytesToHex(r.burnAddress);
|
|
184
208
|
const atMs = Number(r.atNs / 1_000_000n);
|
|
209
|
+
// a return is told apart by its deposit, a deposit address by itself: one burn address can have more than one, each
|
|
210
|
+
// registered with arguments of its own
|
|
185
211
|
const same = ctx.state.receipts.find(
|
|
186
212
|
(x) =>
|
|
187
213
|
x.chainId === r.chainId &&
|
|
188
|
-
x.kind === r.kind &&
|
|
189
214
|
x.burnAddress === burnAddress &&
|
|
190
|
-
(x.kind
|
|
215
|
+
(x.kind === 'returned' && r.kind === 'returned'
|
|
216
|
+
? x.index === r.index
|
|
217
|
+
: x.kind === 'deposit-address' &&
|
|
218
|
+
r.kind === 'deposit-address' &&
|
|
219
|
+
x.depositAddress.toLowerCase() === bytesToHex(r.depositAddress)),
|
|
191
220
|
);
|
|
221
|
+
// what its address was registered with, where the record tells it
|
|
222
|
+
const args = r.kind === 'deposit-address' && r.args && { args: depositArgs(r.args) };
|
|
192
223
|
if (same) {
|
|
193
224
|
delete same.unwritten;
|
|
194
225
|
same.atMs = atMs;
|
|
226
|
+
Object.assign(same, args);
|
|
195
227
|
return;
|
|
196
228
|
}
|
|
197
229
|
const head = { chainId: r.chainId, tokenId: r.tokenId, burnAddress, atMs };
|
|
@@ -205,7 +237,7 @@ function takeReceipt(ctx: Ctx, r: OwnReceipt & { atNs: bigint }): void {
|
|
|
205
237
|
amount: big(r.amount),
|
|
206
238
|
txHash: bytesToHex(r.txHash),
|
|
207
239
|
}
|
|
208
|
-
: { ...head, kind: r.kind, depositAddress: bytesToHex(r.depositAddress) },
|
|
240
|
+
: { ...head, kind: r.kind, depositAddress: bytesToHex(r.depositAddress), ...args },
|
|
209
241
|
);
|
|
210
242
|
}
|
|
211
243
|
|
|
@@ -255,10 +287,6 @@ function takeBurnBlock(
|
|
|
255
287
|
const salt = hex32(bytes);
|
|
256
288
|
const address = core.burnAddressOf(b.chainId, publicKey, salt) as Hex | undefined;
|
|
257
289
|
if (!address || ctx.state.deposits.some((d) => d.chainId === b.chainId && d.salt === salt)) return;
|
|
258
|
-
if (!pos) {
|
|
259
|
-
const next = chainState(ctx, b.chainId).burnNext;
|
|
260
|
-
next[b.tokenId] = Math.max(next[b.tokenId] ?? 0, b.first + i + 1);
|
|
261
|
-
}
|
|
262
290
|
const watch = { key: numberToHex(core.transferTag(b.tokenId, address), { size: 4 }), from, scannedTo: from };
|
|
263
291
|
const record: DepositRecord = {
|
|
264
292
|
chainId: b.chainId,
|
|
@@ -272,6 +300,7 @@ function takeBurnBlock(
|
|
|
272
300
|
found: [],
|
|
273
301
|
};
|
|
274
302
|
if (pos) record.managed = true;
|
|
303
|
+
else record.elsewhere = true;
|
|
275
304
|
ctx.state.deposits.push(record);
|
|
276
305
|
});
|
|
277
306
|
}
|
|
@@ -281,7 +310,8 @@ export function unusedBurn(ctx: Ctx, chainId: bigint, tokenId: number): DepositR
|
|
|
281
310
|
const next = chainState(ctx, chainId).burnNext[tokenId] ?? 0;
|
|
282
311
|
let best: DepositRecord | undefined;
|
|
283
312
|
for (const d of ctx.state.deposits) {
|
|
284
|
-
if (d.chainId !== chainId || d.tokenId !== tokenId || d.managed || d.payer || d.burnIndex < next)
|
|
313
|
+
if (d.chainId !== chainId || d.tokenId !== tokenId || d.managed || d.payer || d.elsewhere || d.burnIndex < next)
|
|
314
|
+
continue;
|
|
285
315
|
if (!best || d.burnIndex < best.burnIndex) best = d;
|
|
286
316
|
}
|
|
287
317
|
return best;
|
|
@@ -302,17 +332,18 @@ export const nextBurn = (ctx: Ctx, chainId: bigint, tokenId: number): number =>
|
|
|
302
332
|
unusedBurn(ctx, chainId, tokenId)?.burnIndex ?? toRecord(ctx, chainId, tokenId);
|
|
303
333
|
|
|
304
334
|
/** Records this wallet's next burn address of a token in storage (a burn block of one), then watches it. Every device
|
|
305
|
-
* of the wallet, and a restore, then watches it too and never hands it out again. About 2^16 hashes
|
|
335
|
+
* of the wallet, and a restore, then watches it too and never hands it out again. About 2^16 hashes, `urgent` unless
|
|
336
|
+
* it is one made ahead; needs no seed. */
|
|
306
337
|
export async function recordBurn(
|
|
307
338
|
ctx: Ctx,
|
|
308
339
|
chainId: bigint,
|
|
309
340
|
tokenId: number,
|
|
310
341
|
tokens: Map<bigint, number[]>,
|
|
311
|
-
signal?: AbortSignal,
|
|
342
|
+
opts: { signal?: AbortSignal; urgent?: boolean } = {},
|
|
312
343
|
): Promise<DepositRecord> {
|
|
313
344
|
const index = toRecord(ctx, chainId, tokenId);
|
|
314
345
|
const key = burnKey(ctx.view.seed);
|
|
315
|
-
const { salt, address } = await burnAddress(key, chainId, tokenId, index, owner(ctx)!.publicKey,
|
|
346
|
+
const { salt, address } = await burnAddress(key, chainId, tokenId, index, owner(ctx)!.publicKey, opts);
|
|
316
347
|
const { commitmentsFinal: from } = await chainCtx(ctx, chainId).indexer.status();
|
|
317
348
|
const block = { chainId, tokenId, first: index, salts: [hexToBytes(salt)] };
|
|
318
349
|
const result = await commit(ctx.storage, ctx.view, storageState(ctx), (s) => planBurnBlock(ctx.view, s, block));
|
|
@@ -350,7 +381,7 @@ export async function topUpBurns(ctx: Ctx, signal?: AbortSignal): Promise<void>
|
|
|
350
381
|
let tokens: Map<bigint, number[]> | undefined;
|
|
351
382
|
for (const { chainId, tokenId } of used.values()) {
|
|
352
383
|
if (!unusedBurn(ctx, chainId, tokenId))
|
|
353
|
-
await recordBurn(ctx, chainId, tokenId, (tokens ??= await tokenIds(ctx)), signal);
|
|
384
|
+
await recordBurn(ctx, chainId, tokenId, (tokens ??= await tokenIds(ctx)), { signal, urgent: false });
|
|
354
385
|
}
|
|
355
386
|
}
|
|
356
387
|
|
|
@@ -394,12 +425,13 @@ function upsertNote(ctx: Ctx, a: NewRecord, note: Note, tokens: Map<bigint, numb
|
|
|
394
425
|
});
|
|
395
426
|
}
|
|
396
427
|
|
|
397
|
-
/** Receive codes of each token whose keys are derived while the seed is held, to hand out while it is not
|
|
428
|
+
/** Receive codes of each token whose keys are derived while the seed is held, to hand out while it is not, and as many
|
|
429
|
+
* deposit addresses' refund signers. */
|
|
398
430
|
export const CODES_AHEAD = 4;
|
|
399
431
|
|
|
400
432
|
/** With the seed held, each owner not derived yet: the wallet's own, each of its burn addresses' (a receive code each),
|
|
401
|
-
* the next `CODES_AHEAD` codes' of each of `tokens
|
|
402
|
-
* its key. */
|
|
433
|
+
* the next `CODES_AHEAD` codes' of each of `tokens` with their deposit addresses' refund signers, and each note's key's;
|
|
434
|
+
* and the watch of each note that waited for its key. Without it, the watch of each note whose key was derived before. */
|
|
403
435
|
export function keyAll(ctx: Ctx, tokens: Token[]): void {
|
|
404
436
|
owner(ctx);
|
|
405
437
|
// ponytail: one derivation per own burn address as the wallet opens; keep owners in the saved state if wallets grow
|
|
@@ -409,14 +441,19 @@ export function keyAll(ctx: Ctx, tokens: Token[]): void {
|
|
|
409
441
|
}
|
|
410
442
|
for (const { chainId, tokenId } of tokens) {
|
|
411
443
|
const next = nextBurn(ctx, chainId, tokenId);
|
|
412
|
-
for (let index = next; index < next + CODES_AHEAD; index++)
|
|
444
|
+
for (let index = next; index < next + CODES_AHEAD; index++) {
|
|
445
|
+
owner(ctx, { chainId, tokenId, index });
|
|
446
|
+
refundSigner(ctx, { chainId, tokenId, index });
|
|
447
|
+
}
|
|
413
448
|
}
|
|
414
449
|
for (const n of ctx.state.notes) {
|
|
415
450
|
if (n.keyIndex === undefined) continue;
|
|
416
451
|
owner(ctx, { chainId: n.chainId, tokenId: n.tokenId, index: n.keyIndex });
|
|
417
452
|
if (!n.unkeyed) continue;
|
|
418
453
|
const from = commitmentsAt(chainState(ctx, n.chainId).samples, Number((n.atNs ?? 0n) / 1_000_000n) - DAY_MS);
|
|
419
|
-
|
|
454
|
+
const watched = watchFor(ctx, n.chainId, n.tokenId, n.value, n.salt, n.keyIndex, from);
|
|
455
|
+
if (!watched) continue; // still waits for the seed
|
|
456
|
+
Object.assign(n, watched);
|
|
420
457
|
delete n.unkeyed;
|
|
421
458
|
}
|
|
422
459
|
}
|
package/src/viewKey.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// A view key as text: what `Wallet.watch` follows an account with, spending nothing.
|
|
2
|
+
|
|
3
|
+
import { bech32m } from '@scure/base';
|
|
4
|
+
import type { ViewKey } from '@zerc20/storage-sdk';
|
|
5
|
+
import { type Address, type Hex, bytesToHex, concat, getAddress, hexToBytes } from 'viem';
|
|
6
|
+
import type { Owner } from './context.js';
|
|
7
|
+
import { core } from './core.js';
|
|
8
|
+
|
|
9
|
+
const PREFIX = 'zerc20view';
|
|
10
|
+
|
|
11
|
+
/** `view` and the wallet's own `owner` as checksummed text (bech32m, `zerc20view1…`): `address(20) ‖ view seed(32) ‖
|
|
12
|
+
* auth key hash(32) ‖ nullifying key(32)`. The public key is left for the holder to make from the last two, so that a
|
|
13
|
+
* nullifying key that is not the account's makes a public key that is not either. */
|
|
14
|
+
export function encodeViewKey(view: ViewKey, owner: Owner): string {
|
|
15
|
+
const bytes = concat([view.address, view.seed, hexToBytes(owner.authKeyHash), hexToBytes(owner.nullifyingKey)]);
|
|
16
|
+
return bech32m.encode(PREFIX, bech32m.toWords(bytes), false);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function bytesOf(text: string): Uint8Array | null {
|
|
20
|
+
try {
|
|
21
|
+
const { prefix, bytes } = bech32m.decodeToBytes(text.trim(), false);
|
|
22
|
+
return prefix === PREFIX && bytes.length === 116 ? bytes : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The account `text` is a view key of, or `null` if it is not one: to tell a key apart before it is opened. */
|
|
29
|
+
export function viewKeyAccount(text: string): Address | null {
|
|
30
|
+
const bytes = bytesOf(text);
|
|
31
|
+
return bytes && getAddress(bytesToHex(bytes.subarray(0, 20)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The view key and owner `text` holds, or `null` for anything else. Needs `initCore`. */
|
|
35
|
+
export function decodeViewKey(text: string): { view: ViewKey; owner: Owner } | null {
|
|
36
|
+
const bytes = bytesOf(text);
|
|
37
|
+
if (!bytes) return null;
|
|
38
|
+
const authKeyHash = bytesToHex(bytes.subarray(52, 84));
|
|
39
|
+
const nullifyingKey = bytesToHex(bytes.subarray(84));
|
|
40
|
+
try {
|
|
41
|
+
const publicKey = core.publicKey(authKeyHash, nullifyingKey) as Hex;
|
|
42
|
+
return {
|
|
43
|
+
view: { address: bytes.slice(0, 20), seed: bytes.slice(20, 52) },
|
|
44
|
+
owner: { authKeyHash, publicKey, nullifyingKey },
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return null; // a value that is not a field element
|
|
48
|
+
}
|
|
49
|
+
}
|