@zerc20/wallet-sdk 0.4.0 → 0.6.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/LICENSE +17 -20
- package/README.md +2 -2
- 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 +86 -9
- package/dist/chain.js +61 -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 +98 -25
- package/dist/core.js.map +1 -1
- package/dist/errors.d.ts +20 -3
- package/dist/errors.js +7 -1
- package/dist/errors.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/network.d.ts +4 -2
- package/dist/network.js +3 -1
- package/dist/network.js.map +1 -1
- package/dist/pos.d.ts +10 -1
- package/dist/pos.js +219 -55
- package/dist/pos.js.map +1 -1
- package/dist/prove.d.ts +3 -0
- package/dist/prove.js +9 -0
- package/dist/prove.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 +5 -0
- package/dist/servers.js +2 -0
- package/dist/servers.js.map +1 -1
- package/dist/services.d.ts +14 -6
- package/dist/services.js +35 -14
- package/dist/services.js.map +1 -1
- package/dist/spend.d.ts +53 -11
- package/dist/spend.js +171 -65
- 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 +132 -21
- package/dist/wallet.js +342 -91
- package/dist/wallet.js.map +1 -1
- package/package.json +3 -3
- package/src/accountId.ts +22 -0
- package/src/chain.ts +101 -49
- package/src/context.ts +27 -3
- package/src/core.ts +112 -26
- package/src/errors.ts +15 -2
- package/src/index.ts +15 -2
- package/src/network.ts +6 -3
- package/src/pos.ts +247 -61
- package/src/prove.ts +10 -0
- package/src/rageExit.ts +237 -61
- package/src/seed.ts +24 -1
- package/src/servers.ts +9 -1
- package/src/services.ts +36 -22
- package/src/spend.ts +188 -75
- package/src/state.ts +25 -1
- package/src/sync.ts +54 -19
- package/src/wallet.ts +437 -109
- 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/spend.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Recipient } from '@zerc20/storage-sdk';
|
|
2
2
|
import { type Address, type Hex } from 'viem';
|
|
3
3
|
import { type Ctx } from './context.js';
|
|
4
|
-
import
|
|
4
|
+
import type { Indexer } from './servers.js';
|
|
5
|
+
import { type NoteRecord, type OperationRecord, type Position, type SpendOperation } from './state.js';
|
|
5
6
|
/** Something spendable. */
|
|
6
7
|
export interface Coin {
|
|
7
8
|
kind: 'note' | 'burn';
|
|
@@ -38,9 +39,16 @@ export interface Step {
|
|
|
38
39
|
request?: object;
|
|
39
40
|
/** Reverts with no sign of it on chain: proved again with the same outputs a few times, then failed. */
|
|
40
41
|
attempts: number;
|
|
41
|
-
/** Once relayed: the
|
|
42
|
-
|
|
42
|
+
/** Once relayed: the block the relayer saw it mined in. */
|
|
43
|
+
relayedBlock?: number;
|
|
44
|
+
/** The indexer's nullifier count once it is past that block, or just after a relay that reverted on an input spent
|
|
45
|
+
* already: that leaves the step `attested`, not relayed again until its change tells whose transaction spent it.
|
|
46
|
+
* Final nullifiers past it, and its inputs still not among them, means whatever spent them is gone: a reorg dropped
|
|
47
|
+
* it, or the relayer never sent it. */
|
|
43
48
|
relayedAt?: number;
|
|
49
|
+
/** The final commitment count once an input is seen spent. Final commitments past it, and the change still not among
|
|
50
|
+
* them, means another transaction spent the input, and this step can never land. */
|
|
51
|
+
spentAt?: number;
|
|
44
52
|
}
|
|
45
53
|
/** The most inputs one merge spends: the attester takes requests up to 2 MB, at 8 KiB of sealed witness per input
|
|
46
54
|
* sent as hex, and the prover folds them in one job. */
|
|
@@ -62,7 +70,13 @@ export interface MergeStep {
|
|
|
62
70
|
/** Refused by the prover, or reverting with no sign of it on chain: built again with the same output a few times,
|
|
63
71
|
* then failed. */
|
|
64
72
|
attempts: number;
|
|
73
|
+
/** Built again after a relay of it reverted, which counts in `attempts` beside the prover's refusals: the request
|
|
74
|
+
* out may still land, so the step is not cancelled. */
|
|
75
|
+
sent?: true;
|
|
76
|
+
/** As a JoinSplit's, with `proved` where a JoinSplit is `attested`. */
|
|
77
|
+
relayedBlock?: number;
|
|
65
78
|
relayedAt?: number;
|
|
79
|
+
spentAt?: number;
|
|
66
80
|
}
|
|
67
81
|
/** Where a send or withdrawal goes; `self` merges the wallet's own notes and nothing leaves. */
|
|
68
82
|
export type Destination = {
|
|
@@ -116,7 +130,11 @@ export declare function mergePool(all: Coin[], chosen: number[] | undefined, cha
|
|
|
116
130
|
* into one note first would pay it, coins `onTheWay` might, or nothing will. A payment never waits or merges itself:
|
|
117
131
|
* that is the app's call. */
|
|
118
132
|
export declare function payWith(all: Coin[], need: bigint, fee: bigint, onTheWay: boolean): Coin[];
|
|
119
|
-
/** The smallest public withdrawal that still pays `net` after the protocol's cut.
|
|
133
|
+
/** The smallest public withdrawal that still pays `net` after the protocol's cut. The cut is a rate of the whole
|
|
134
|
+
* withdrawal, rounded down, and the rate read off one withdrawal (`fee / gross`) is at most its tier's: the smallest
|
|
135
|
+
* gross that pays `net` at that rate is never more than the answer within the tier, so a read or two settles it.
|
|
136
|
+
* ponytail: where a lower tier's bound lies between two reads, that bound may already pay `net`, a little less than
|
|
137
|
+
* the gross this returns; a getter for the schedule's tiers would find it. */
|
|
120
138
|
export declare function grossed(ctx: Ctx, chainId: bigint, tokenId: number, net: bigint): Promise<bigint>;
|
|
121
139
|
/** A payment's one public withdrawal, and the protocol's cut of it, which the payer pays on top: a withdrawal's pays
|
|
122
140
|
* its recipient `amount` (and the relayer by a note), a private payment's pays the relayer its `fee`. */
|
|
@@ -124,11 +142,19 @@ export declare function withdrawalOf(ctx: Ctx, chainId: bigint, tokenId: number,
|
|
|
124
142
|
withdrawn: bigint;
|
|
125
143
|
protocol: bigint;
|
|
126
144
|
}>;
|
|
145
|
+
/** The rate, in basis points, of a protocol's `cut` out of a withdrawal of `gross`. The cut is rounded down, so this is
|
|
146
|
+
* exact once `gross` is 10,000 base units or more. */
|
|
147
|
+
export declare const bpsOf: (cut: bigint, gross: bigint) => number;
|
|
127
148
|
/** The most a payment out of `all` pays its recipient, the relayer's `fee` and the protocol's cut on top
|
|
128
149
|
* (`withdrawalOf`): a withdrawal of what the relayer leaves, less the cut on it, or a private payment of what the
|
|
129
150
|
* relayer's grossed fee leaves. Coins one JoinSplit cannot spend together (`pick`) are merged first, each merge
|
|
130
|
-
* paying the relayer's grossed fee, as a consolidation's quote does.
|
|
131
|
-
|
|
151
|
+
* paying the relayer's grossed fee, as a consolidation's quote does. `fee` is all the fees take of `all`; where they
|
|
152
|
+
* take it all, what they would be with nothing left to pay. */
|
|
153
|
+
export declare function maxPayment(ctx: Ctx, chainId: bigint, tokenId: number, isPublic: boolean, all: Coin[], fee: bigint): Promise<{
|
|
154
|
+
amount: bigint;
|
|
155
|
+
fee: bigint;
|
|
156
|
+
merges: number;
|
|
157
|
+
}>;
|
|
132
158
|
/** Plans the next step of `op`: a payment's only JoinSplit (or throws why there is none), or a consolidation's next
|
|
133
159
|
* merge. `null`: wait for the last merge's note to become spendable. `'done'`: nothing left worth merging. */
|
|
134
160
|
export declare function planStep(ctx: Ctx, op: SpendOperation): Promise<Step | MergeStep | null | 'done'>;
|
|
@@ -140,12 +166,28 @@ export declare const codeOf: (code: string) => import("@zerc20/storage-sdk").Rec
|
|
|
140
166
|
/** What `step` paid besides `op`'s amount, the relayer's fee and the protocol's cut: what its inputs hold beyond the
|
|
141
167
|
* amount and the change. */
|
|
142
168
|
export declare const feeOf: (op: SpendOperation, step: Step) => bigint;
|
|
143
|
-
/** Where a
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
|
|
169
|
+
/** Where a relayed step stands, by its `change` (a merge's note) and its `inputs` as sync last saw them:
|
|
170
|
+
* - `landed`: the change is final where this wallet expects it;
|
|
171
|
+
* - `taken`: an input is spent and the final commitments passed `spentAt` without the change, so another transaction
|
|
172
|
+
* spent it, and this step can never land;
|
|
173
|
+
* - `dropped`: no input is spent and the final nullifiers passed `relayedAt` without them: whatever was spending them
|
|
174
|
+
* is gone, a reorg dropped it or the relayer never sent it;
|
|
175
|
+
* - `mined`: the change is in a block, not final yet;
|
|
176
|
+
* - `waiting`: none of these yet. */
|
|
177
|
+
export declare function relayedState(change: Pick<NoteRecord, 'found' | 'watch'> | undefined, inputs: (Position | undefined)[], relayedAt: number | undefined, spentAt: number | undefined): 'landed' | 'taken' | 'dropped' | 'mined' | 'waiting';
|
|
178
|
+
/** The indexer's nullifier count once it is past `block`, so past the nullifiers of a transaction mined there unless a
|
|
179
|
+
* reorg moved them; `undefined` until then. The relayer answers once the block is mined, which may be before the
|
|
180
|
+
* indexer has read it. */
|
|
181
|
+
export declare function nullifiersPast(indexer: Pick<Indexer, 'status'>, block: number): Promise<number | undefined>;
|
|
147
182
|
/** Whether `op`'s payment or withdrawal is on chain: done, or relayed, which the relayer answers once it is mined. Until
|
|
148
183
|
* it is final a reorg may still drop it, and the operation relays it again. */
|
|
149
184
|
export declare function landed(op: SpendOperation): boolean;
|
|
150
|
-
/**
|
|
185
|
+
/** Whether `op` can still be cancelled, having paid nothing: a payment, withdrawal or consolidation under way with no
|
|
186
|
+
* request out to the relayer, and no merge of it landed. A step at its relay stage has one out, as its request leaves
|
|
187
|
+
* the moment it gets there; so has one built again after a relay of it reverted, as that request may still land: a
|
|
188
|
+
* JoinSplit's `attempts` count only those, and a merge, whose `attempts` count its prover's refusals too, is `sent`. */
|
|
189
|
+
export declare function cancellable(op: OperationRecord): boolean;
|
|
190
|
+
/** Takes `op` as far as it can go now, saving before each step that leaves the device. One cancelled meanwhile
|
|
191
|
+
* (`cancel`) finishes the stage under way and starts no other: its status is looked at after each, and nothing it does
|
|
192
|
+
* later is kept, as it is out of the state. A relayer that is relaying its request already throws `Relaying`. */
|
|
151
193
|
export declare function advanceSpend(ctx: Ctx, op: SpendOperation): Promise<void>;
|
package/dist/spend.js
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
// and deposits into one note to self, proved by the merge prover (a wallet cannot: its key is 6.3 GB). More take
|
|
7
7
|
// another merge, once a root is over the first one's note.
|
|
8
8
|
//
|
|
9
|
-
// A relayed JoinSplit or merge is done once its
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// A relayed JoinSplit or merge is done once its change (a merge's note) is final where this wallet expects it, as sync
|
|
10
|
+
// sees it: only its own transaction puts that note there, as its salt is fresh and the proof binds it to the inputs'
|
|
11
|
+
// nullifiers. The relayer's answer alone could be a lie, a reorg could drop the transaction, and a spent input alone
|
|
12
|
+
// could be another device's doing: each device of a wallet locks its inputs in its own state. Only one that cannot
|
|
13
|
+
// land fails, which frees its inputs: one that kept reverting with no input spent, or one whose input another
|
|
14
|
+
// transaction spent.
|
|
12
15
|
import { commit, decodeCode, planPayCode, planSelf, planSend } from '@zerc20/storage-sdk';
|
|
13
16
|
import { bytesToHex, decodeAbiParameters, hexToBytes, numberToHex, parseAbiParameters, } from 'viem';
|
|
14
17
|
import { UNWRAP_INTENT } from './chain.js';
|
|
@@ -17,7 +20,7 @@ import { core } from './core.js';
|
|
|
17
20
|
import { fail } from './errors.js';
|
|
18
21
|
import { prove } from './prove.js';
|
|
19
22
|
import { scan } from './scan.js';
|
|
20
|
-
import {
|
|
23
|
+
import { attest, proofOf, relay, relayMerge, relayerFees, requestProof } from './services.js';
|
|
21
24
|
import { unspent } from './state.js';
|
|
22
25
|
import { commitmentsAt, noteReceipt, storageState, takeRefresh, tokenIds } from './sync.js';
|
|
23
26
|
import { aspPath, aspPaths, commitmentPath, commitmentPaths } from './tree.js';
|
|
@@ -136,15 +139,22 @@ export function payWith(all, need, fee, onTheWay) {
|
|
|
136
139
|
fail('funds-settling', `${need} is not spendable yet: notes or deposits are still settling`);
|
|
137
140
|
return fail('insufficient-funds', `${need} is more than the balance`);
|
|
138
141
|
}
|
|
139
|
-
/** The smallest public withdrawal that still pays `net` after the protocol's cut.
|
|
142
|
+
/** The smallest public withdrawal that still pays `net` after the protocol's cut. The cut is a rate of the whole
|
|
143
|
+
* withdrawal, rounded down, and the rate read off one withdrawal (`fee / gross`) is at most its tier's: the smallest
|
|
144
|
+
* gross that pays `net` at that rate is never more than the answer within the tier, so a read or two settles it.
|
|
145
|
+
* ponytail: where a lower tier's bound lies between two reads, that bound may already pay `net`, a little less than
|
|
146
|
+
* the gross this returns; a getter for the schedule's tiers would find it. */
|
|
140
147
|
export async function grossed(ctx, chainId, tokenId, net) {
|
|
141
148
|
const chain = chainCtx(ctx, chainId).chain;
|
|
142
149
|
let gross = net;
|
|
143
|
-
|
|
150
|
+
// each read moves on by a unit at least, and a few settle each tier a withdrawal crosses
|
|
151
|
+
for (let i = 0; i < 64; i++) {
|
|
144
152
|
const paid = gross - (await chain.withdrawFee(tokenId, gross));
|
|
145
153
|
if (paid >= net)
|
|
146
154
|
return gross;
|
|
147
|
-
|
|
155
|
+
if (paid <= 0n)
|
|
156
|
+
break;
|
|
157
|
+
gross = ((net - 1n) * gross) / paid + 1n;
|
|
148
158
|
}
|
|
149
159
|
return fail('unavailable', 'the withdrawal fee schedule does not settle');
|
|
150
160
|
}
|
|
@@ -155,18 +165,25 @@ export async function withdrawalOf(ctx, chainId, tokenId, isPublic, amount, fee)
|
|
|
155
165
|
const withdrawn = await grossed(ctx, chainId, tokenId, net);
|
|
156
166
|
return { withdrawn, protocol: withdrawn - net };
|
|
157
167
|
}
|
|
168
|
+
/** The rate, in basis points, of a protocol's `cut` out of a withdrawal of `gross`. The cut is rounded down, so this is
|
|
169
|
+
* exact once `gross` is 10,000 base units or more. */
|
|
170
|
+
export const bpsOf = (cut, gross) => gross === 0n ? 0 : Number((cut * 10000n + gross - 1n) / gross);
|
|
158
171
|
/** The most a payment out of `all` pays its recipient, the relayer's `fee` and the protocol's cut on top
|
|
159
172
|
* (`withdrawalOf`): a withdrawal of what the relayer leaves, less the cut on it, or a private payment of what the
|
|
160
173
|
* relayer's grossed fee leaves. Coins one JoinSplit cannot spend together (`pick`) are merged first, each merge
|
|
161
|
-
* paying the relayer's grossed fee, as a consolidation's quote does.
|
|
174
|
+
* paying the relayer's grossed fee, as a consolidation's quote does. `fee` is all the fees take of `all`; where they
|
|
175
|
+
* take it all, what they would be with nothing left to pay. */
|
|
162
176
|
export async function maxPayment(ctx, chainId, tokenId, isPublic, all, fee) {
|
|
163
177
|
const whole = all.length < 2 || (all.length === 2 && all.some((c) => c.kind === 'note'));
|
|
164
|
-
const merges = whole ?
|
|
165
|
-
const
|
|
166
|
-
const
|
|
178
|
+
const merges = whole ? 0 : roundsToMerge(all);
|
|
179
|
+
const gross = !isPublic || merges > 0 ? await grossed(ctx, chainId, tokenId, fee) : fee;
|
|
180
|
+
const fees = BigInt(merges) * gross + (isPublic ? fee : gross);
|
|
181
|
+
const total = all.reduce((s, c) => s + c.value, 0n);
|
|
182
|
+
const left = total - fees;
|
|
167
183
|
if (left <= 0n)
|
|
168
|
-
return 0n;
|
|
169
|
-
|
|
184
|
+
return { amount: 0n, fee: fees, merges };
|
|
185
|
+
const amount = isPublic ? left - (await chainCtx(ctx, chainId).chain.withdrawFee(tokenId, left)) : left;
|
|
186
|
+
return { amount, fee: total - amount, merges };
|
|
170
187
|
}
|
|
171
188
|
/** Plans the next step of `op`: a payment's only JoinSplit (or throws why there is none), or a consolidation's next
|
|
172
189
|
* merge. `null`: wait for the last merge's note to become spendable. `'done'`: nothing left worth merging. */
|
|
@@ -460,35 +477,94 @@ const positionsAt = (ctx, chainId, index) => [...ctx.state.notes, ...ctx.state.d
|
|
|
460
477
|
.flatMap((r) => r.found.filter((f) => f.index === index));
|
|
461
478
|
/** The final one of them: an input is final. */
|
|
462
479
|
const positionOf = (ctx, chainId, index) => positionsAt(ctx, chainId, index).find((f) => !f.tentative);
|
|
463
|
-
/** Whether the step's
|
|
464
|
-
* newer ones.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (f?.spent)
|
|
480
|
+
/** Whether any of the step's inputs is spent on chain: among the final nullifiers (as sync last saw them), or among
|
|
481
|
+
* the newer ones. Reads the pages everyone polls, never asks about a nullifier. */
|
|
482
|
+
async function inputsSpent(ctx, op, step) {
|
|
483
|
+
const inputs = step.inputs.map((coin) => positionOf(ctx, op.chainId, coin.index));
|
|
484
|
+
if (inputs.some((f) => f?.spent))
|
|
469
485
|
return true;
|
|
470
|
-
|
|
486
|
+
// copies: sync alone moves the watches, over final entries
|
|
487
|
+
const watches = inputs.flatMap((f) => (f?.nullifier ? [{ ...f.nullifier }] : []));
|
|
488
|
+
if (watches.length === 0)
|
|
471
489
|
return false;
|
|
472
490
|
const c = chainCtx(ctx, op.chainId);
|
|
473
491
|
const { nullifiers } = await c.indexer.status();
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
const hits = await scan([watch], 8, (p) => c.indexer.page('nullifiers', p), watch.scannedTo, nullifiers);
|
|
492
|
+
const from = Math.min(...watches.map((w) => w.scannedTo));
|
|
493
|
+
const hits = await scan(watches, 8, (p) => c.indexer.page('nullifiers', p), from, nullifiers);
|
|
477
494
|
return hits.length > 0;
|
|
478
495
|
}
|
|
479
|
-
/** Where a
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
|
|
483
|
-
|
|
496
|
+
/** Where a relayed step stands, by its `change` (a merge's note) and its `inputs` as sync last saw them:
|
|
497
|
+
* - `landed`: the change is final where this wallet expects it;
|
|
498
|
+
* - `taken`: an input is spent and the final commitments passed `spentAt` without the change, so another transaction
|
|
499
|
+
* spent it, and this step can never land;
|
|
500
|
+
* - `dropped`: no input is spent and the final nullifiers passed `relayedAt` without them: whatever was spending them
|
|
501
|
+
* is gone, a reorg dropped it or the relayer never sent it;
|
|
502
|
+
* - `mined`: the change is in a block, not final yet;
|
|
503
|
+
* - `waiting`: none of these yet. */
|
|
504
|
+
export function relayedState(change, inputs, relayedAt, spentAt) {
|
|
505
|
+
const seen = change?.found.filter((f) => f.confirmed) ?? [];
|
|
506
|
+
if (seen.some((f) => !f.tentative))
|
|
484
507
|
return 'landed';
|
|
485
|
-
|
|
508
|
+
if (inputs.some((f) => f?.spent)) {
|
|
509
|
+
// a final position not read yet may be the change
|
|
510
|
+
const missed = spentAt !== undefined &&
|
|
511
|
+
!!change?.watch &&
|
|
512
|
+
change.watch.scannedTo >= spentAt &&
|
|
513
|
+
!change.found.some((f) => !f.tentative);
|
|
514
|
+
if (missed)
|
|
515
|
+
return 'taken';
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
const watches = inputs.flatMap((f) => (f?.nullifier ? [f.nullifier] : []));
|
|
519
|
+
if (relayedAt !== undefined && watches.length > 0 && watches.every((w) => w.scannedTo >= relayedAt))
|
|
520
|
+
return 'dropped';
|
|
521
|
+
}
|
|
522
|
+
return seen.length > 0 ? 'mined' : 'waiting';
|
|
523
|
+
}
|
|
524
|
+
/** `relayedState` of `step`, failing the operation once another transaction took an input. The first time an input
|
|
525
|
+
* is seen spent, short of the step landing, it notes the final commitment count: a later status counts no fewer, so
|
|
526
|
+
* once the change's watch passes it, it has looked wherever that transaction put its notes. */
|
|
527
|
+
async function standing(ctx, op, step) {
|
|
528
|
+
const inputs = step.inputs.map((coin) => positionOf(ctx, op.chainId, coin.index));
|
|
529
|
+
const salt = 'output' in step ? step.output.salt : step.outputs[1].salt;
|
|
530
|
+
const change = ctx.state.notes.find((n) => n.chainId === op.chainId && n.salt === salt);
|
|
531
|
+
const now = relayedState(change, inputs, step.relayedAt, step.spentAt);
|
|
532
|
+
if (now === 'taken')
|
|
533
|
+
fail('spent-elsewhere', 'another device of this wallet spent a note or deposit of it first: it paid nothing');
|
|
534
|
+
if (now !== 'landed' && step.spentAt === undefined && inputs.some((f) => f?.spent)) {
|
|
535
|
+
step.spentAt = (await chainCtx(ctx, op.chainId).indexer.status()).commitmentsFinal;
|
|
536
|
+
await ctx.save();
|
|
537
|
+
}
|
|
538
|
+
return now;
|
|
539
|
+
}
|
|
540
|
+
/** The indexer's nullifier count once it is past `block`, so past the nullifiers of a transaction mined there unless a
|
|
541
|
+
* reorg moved them; `undefined` until then. The relayer answers once the block is mined, which may be before the
|
|
542
|
+
* indexer has read it. */
|
|
543
|
+
export async function nullifiersPast(indexer, block) {
|
|
544
|
+
if ((await indexer.status()).cursor < block)
|
|
545
|
+
return undefined;
|
|
546
|
+
// a status reads its counts before its cursor, so the count comes from the next one
|
|
547
|
+
return (await indexer.status()).nullifiers;
|
|
486
548
|
}
|
|
487
549
|
/** Whether `op`'s payment or withdrawal is on chain: done, or relayed, which the relayer answers once it is mined. Until
|
|
488
550
|
* it is final a reorg may still drop it, and the operation relays it again. */
|
|
489
551
|
export function landed(op) {
|
|
490
552
|
return (op.status === 'done' || (op.status === 'pending' && op.kind !== 'consolidate' && op.data.step?.stage === 'relayed'));
|
|
491
553
|
}
|
|
554
|
+
/** Whether `op` can still be cancelled, having paid nothing: a payment, withdrawal or consolidation under way with no
|
|
555
|
+
* request out to the relayer, and no merge of it landed. A step at its relay stage has one out, as its request leaves
|
|
556
|
+
* the moment it gets there; so has one built again after a relay of it reverted, as that request may still land: a
|
|
557
|
+
* JoinSplit's `attempts` count only those, and a merge, whose `attempts` count its prover's refusals too, is `sent`. */
|
|
558
|
+
export function cancellable(op) {
|
|
559
|
+
if (op.status !== 'pending' || op.kind === 'deposit' || op.data.paid > 0n)
|
|
560
|
+
return false;
|
|
561
|
+
const step = op.data.step;
|
|
562
|
+
if (!step)
|
|
563
|
+
return true;
|
|
564
|
+
if ('output' in step)
|
|
565
|
+
return !step.sent && step.stage !== 'proved' && step.stage !== 'relayed';
|
|
566
|
+
return step.attempts === 0 && step.stage !== 'attested' && step.stage !== 'relayed';
|
|
567
|
+
}
|
|
492
568
|
function markSpent(ctx, op, step) {
|
|
493
569
|
for (const coin of step.inputs)
|
|
494
570
|
for (const f of positionsAt(ctx, op.chainId, coin.index))
|
|
@@ -501,14 +577,18 @@ function placeChange(ctx, op, salt, index) {
|
|
|
501
577
|
note.found.push({ index, tentative: true, spent: false });
|
|
502
578
|
}
|
|
503
579
|
}
|
|
504
|
-
/** Takes `op` as far as it can go now, saving before each step that leaves the device.
|
|
580
|
+
/** Takes `op` as far as it can go now, saving before each step that leaves the device. One cancelled meanwhile
|
|
581
|
+
* (`cancel`) finishes the stage under way and starts no other: its status is looked at after each, and nothing it does
|
|
582
|
+
* later is kept, as it is out of the state. A relayer that is relaying its request already throws `Relaying`. */
|
|
505
583
|
export async function advanceSpend(ctx, op) {
|
|
506
584
|
const data = op.data;
|
|
507
585
|
const c = chainCtx(ctx, op.chainId);
|
|
508
586
|
for (;;) {
|
|
587
|
+
if (op.status !== 'pending')
|
|
588
|
+
return;
|
|
509
589
|
if (!data.step) {
|
|
510
590
|
const step = await planStep(ctx, op);
|
|
511
|
-
if (!step)
|
|
591
|
+
if (!step || op.status !== 'pending')
|
|
512
592
|
return;
|
|
513
593
|
if (step === 'done') {
|
|
514
594
|
op.status = 'done';
|
|
@@ -529,6 +609,8 @@ export async function advanceSpend(ctx, op) {
|
|
|
529
609
|
step.stage = 'noted';
|
|
530
610
|
await ctx.save();
|
|
531
611
|
}
|
|
612
|
+
if (op.status !== 'pending')
|
|
613
|
+
return;
|
|
532
614
|
if (step.stage === 'noted') {
|
|
533
615
|
const built = await witnessOf(ctx, op, step);
|
|
534
616
|
if (!built)
|
|
@@ -573,6 +655,8 @@ export async function advanceSpend(ctx, op) {
|
|
|
573
655
|
step.stage = 'proved';
|
|
574
656
|
await ctx.save();
|
|
575
657
|
}
|
|
658
|
+
if (op.status !== 'pending')
|
|
659
|
+
return;
|
|
576
660
|
if (step.stage === 'proved') {
|
|
577
661
|
const attestation = await attest(c.config, step.statement, step.witness);
|
|
578
662
|
step.request = { ...step.request, attestation };
|
|
@@ -580,19 +664,25 @@ export async function advanceSpend(ctx, op) {
|
|
|
580
664
|
step.stage = 'attested';
|
|
581
665
|
await ctx.save();
|
|
582
666
|
}
|
|
667
|
+
if (op.status !== 'pending')
|
|
668
|
+
return;
|
|
669
|
+
if (step.stage === 'attested' && step.relayedAt !== undefined) {
|
|
670
|
+
// a relay of it reverted on an input spent already: an earlier relay of it landed, one whose answer was lost, or
|
|
671
|
+
// another transaction spent the input. Relaying it again would tell nothing more
|
|
672
|
+
const now = await standing(ctx, op, step);
|
|
673
|
+
if (now === 'waiting')
|
|
674
|
+
return;
|
|
675
|
+
if (now === 'dropped')
|
|
676
|
+
delete step.relayedAt;
|
|
677
|
+
else
|
|
678
|
+
step.stage = 'relayed';
|
|
679
|
+
await ctx.save();
|
|
680
|
+
}
|
|
583
681
|
if (step.stage === 'attested') {
|
|
584
682
|
const relayerOut = step.outputs.find((o) => o.role === 'relayer');
|
|
585
|
-
|
|
586
|
-
try {
|
|
587
|
-
result = await relay(c.config, step.request, relayerOut ? { value: relayerOut.value, salt: relayerOut.salt } : undefined);
|
|
588
|
-
}
|
|
589
|
-
catch (e) {
|
|
590
|
-
if (e instanceof RelayUnknown)
|
|
591
|
-
return; // try the same request again next time
|
|
592
|
-
throw e;
|
|
593
|
-
}
|
|
683
|
+
const result = await relay(c.config, step.request, relayerOut ? { value: relayerOut.value, salt: relayerOut.salt } : undefined);
|
|
594
684
|
if ('reverted' in result) {
|
|
595
|
-
if (!(await
|
|
685
|
+
if (!(await inputsSpent(ctx, op, step))) {
|
|
596
686
|
// stale roots, most likely: prove again with the same outputs, next sync. A JoinSplit that keeps reverting
|
|
597
687
|
// is not on chain and cannot land, so failing it frees its inputs.
|
|
598
688
|
if (++step.attempts > 3)
|
|
@@ -601,21 +691,24 @@ export async function advanceSpend(ctx, op) {
|
|
|
601
691
|
await ctx.save();
|
|
602
692
|
return;
|
|
603
693
|
}
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
data.txHashes = [...(data.txHashes ?? []), result.txHash];
|
|
694
|
+
// the indexer counts the nullifiers of whatever spent it already
|
|
695
|
+
step.relayedAt = (await c.indexer.status()).nullifiers;
|
|
696
|
+
await ctx.save();
|
|
697
|
+
return;
|
|
609
698
|
}
|
|
610
|
-
step.
|
|
699
|
+
placeChange(ctx, op, step.outputs[1].salt, result.noteIndex2);
|
|
700
|
+
data.txHashes = [...(data.txHashes ?? []), result.txHash];
|
|
701
|
+
step.relayedBlock = result.block;
|
|
611
702
|
step.stage = 'relayed';
|
|
612
703
|
await ctx.save();
|
|
613
704
|
}
|
|
614
705
|
if (step.stage === 'relayed') {
|
|
615
|
-
|
|
706
|
+
step.relayedAt ??= await nullifiersPast(c.indexer, step.relayedBlock);
|
|
707
|
+
const now = await standing(ctx, op, step);
|
|
616
708
|
if (now === 'dropped') {
|
|
617
709
|
// relay it again next sync; if it did land after all, that reverts and finds it
|
|
618
710
|
step.stage = 'attested';
|
|
711
|
+
delete step.relayedAt;
|
|
619
712
|
await ctx.save();
|
|
620
713
|
}
|
|
621
714
|
if (now !== 'landed')
|
|
@@ -648,6 +741,8 @@ async function advanceMerge(ctx, op, step) {
|
|
|
648
741
|
step.stage = 'noted';
|
|
649
742
|
await ctx.save();
|
|
650
743
|
}
|
|
744
|
+
if (op.status !== 'pending')
|
|
745
|
+
return false;
|
|
651
746
|
if (step.stage === 'noted') {
|
|
652
747
|
const built = await mergeWitnessOf(ctx, op, step);
|
|
653
748
|
if (!built)
|
|
@@ -682,6 +777,8 @@ async function advanceMerge(ctx, op, step) {
|
|
|
682
777
|
step.stage = 'attested';
|
|
683
778
|
await ctx.save();
|
|
684
779
|
}
|
|
780
|
+
if (op.status !== 'pending')
|
|
781
|
+
return false;
|
|
685
782
|
if (step.stage === 'attested') {
|
|
686
783
|
await requestProof(c.config, prover, step.statement, step.witness, step.inputs.length);
|
|
687
784
|
delete step.witness; // the prover has it: the inputs' openings need not stay saved here
|
|
@@ -705,34 +802,43 @@ async function advanceMerge(ctx, op, step) {
|
|
|
705
802
|
step.stage = 'proved';
|
|
706
803
|
await ctx.save();
|
|
707
804
|
}
|
|
805
|
+
if (op.status !== 'pending')
|
|
806
|
+
return false;
|
|
807
|
+
if (step.stage === 'proved' && step.relayedAt !== undefined) {
|
|
808
|
+
// as a JoinSplit's, told apart by its note
|
|
809
|
+
const now = await standing(ctx, op, step);
|
|
810
|
+
if (now === 'waiting')
|
|
811
|
+
return false;
|
|
812
|
+
if (now === 'dropped')
|
|
813
|
+
delete step.relayedAt;
|
|
814
|
+
else
|
|
815
|
+
step.stage = 'relayed';
|
|
816
|
+
await ctx.save();
|
|
817
|
+
}
|
|
708
818
|
if (step.stage === 'proved') {
|
|
709
|
-
|
|
710
|
-
try {
|
|
711
|
-
result = await relayMerge(c.config, step.request);
|
|
712
|
-
}
|
|
713
|
-
catch (e) {
|
|
714
|
-
if (e instanceof RelayUnknown)
|
|
715
|
-
return false; // try the same request again next time
|
|
716
|
-
throw e;
|
|
717
|
-
}
|
|
819
|
+
const result = await relayMerge(c.config, step.request);
|
|
718
820
|
if ('reverted' in result) {
|
|
719
821
|
// stale roots, most likely: built again with the same output, next sync
|
|
720
|
-
if (!(await
|
|
822
|
+
if (!(await inputsSpent(ctx, op, step))) {
|
|
823
|
+
step.sent = true;
|
|
721
824
|
return again(result.reverted);
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
data.txHashes = [...(data.txHashes ?? []), result.txHash];
|
|
825
|
+
}
|
|
826
|
+
step.relayedAt = (await c.indexer.status()).nullifiers;
|
|
827
|
+
await ctx.save();
|
|
828
|
+
return false;
|
|
727
829
|
}
|
|
728
|
-
step.
|
|
830
|
+
placeChange(ctx, op, step.output.salt, result.noteIndex);
|
|
831
|
+
data.txHashes = [...(data.txHashes ?? []), result.txHash];
|
|
832
|
+
step.relayedBlock = result.block;
|
|
729
833
|
step.stage = 'relayed';
|
|
730
834
|
await ctx.save();
|
|
731
835
|
}
|
|
732
|
-
|
|
836
|
+
step.relayedAt ??= await nullifiersPast(c.indexer, step.relayedBlock);
|
|
837
|
+
const now = await standing(ctx, op, step);
|
|
733
838
|
if (now === 'dropped') {
|
|
734
839
|
// relay it again next sync; if it did land after all, that reverts and finds it
|
|
735
840
|
step.stage = 'proved';
|
|
841
|
+
delete step.relayedAt;
|
|
736
842
|
await ctx.save();
|
|
737
843
|
}
|
|
738
844
|
if (now !== 'landed')
|