@zerc20/wallet-sdk 0.5.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 +1 -1
- package/dist/chain.d.ts +6 -5
- package/dist/chain.js +8 -7
- package/dist/chain.js.map +1 -1
- package/dist/context.js +1 -1
- package/dist/context.js.map +1 -1
- package/dist/core.js +3 -2
- 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/network.d.ts +4 -2
- package/dist/network.js +3 -1
- package/dist/network.js.map +1 -1
- package/dist/pos.d.ts +3 -0
- package/dist/pos.js +6 -0
- 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/servers.d.ts +3 -0
- package/dist/servers.js +1 -0
- package/dist/servers.js.map +1 -1
- package/dist/services.d.ts +3 -2
- package/dist/services.js +9 -7
- package/dist/services.js.map +1 -1
- package/dist/spend.d.ts +30 -9
- package/dist/spend.js +135 -58
- package/dist/spend.js.map +1 -1
- package/dist/wallet.d.ts +40 -7
- package/dist/wallet.js +153 -37
- package/dist/wallet.js.map +1 -1
- package/package.json +3 -3
- package/src/chain.ts +12 -6
- package/src/context.ts +1 -1
- package/src/core.ts +3 -2
- package/src/errors.ts +15 -2
- package/src/network.ts +6 -3
- package/src/pos.ts +7 -0
- package/src/prove.ts +10 -0
- package/src/servers.ts +5 -0
- package/src/services.ts +8 -7
- package/src/spend.ts +146 -68
- package/src/wallet.ts +192 -38
package/dist/wallet.js
CHANGED
|
@@ -8,15 +8,33 @@ import { chainCtx, chainState, owner, refundSigner, switched, walletOn, walletSe
|
|
|
8
8
|
import { burnAddressFor, core, initCore } from './core.js';
|
|
9
9
|
import { WalletError, fail, guard } from './errors.js';
|
|
10
10
|
import { checkNetwork } from './network.js';
|
|
11
|
+
import { preload } from './prove.js';
|
|
11
12
|
import { Asp, Indexer, PAGE } from './servers.js';
|
|
12
13
|
import { flushDepositAddress, rageExit, refundDepositAddress } from './rageExit.js';
|
|
13
|
-
import { registerDeposit, relayerFees, rewatchDeposit } from './services.js';
|
|
14
|
-
import { advanceSpend, bpsOf, chosenSettling, coins, grossed, inputsOf, landed, maxPayment, mergePool, mergeRound, payWith, planStep, roundsToMerge, settling, withdrawalOf, } from './spend.js';
|
|
14
|
+
import { Relaying, registerDeposit, relayerFees, rewatchDeposit } from './services.js';
|
|
15
|
+
import { advanceSpend, bpsOf, cancellable, chosenSettling, coins, grossed, inputsOf, landed, maxPayment, mergePool, mergeRound, payWith, planStep, roundsToMerge, settling, withdrawalOf, } from './spend.js';
|
|
15
16
|
import { emptyWalletState, openState, sealState, stateKey, unspent, } from './state.js';
|
|
16
17
|
import { WAIT_MS, commitmentTx, keyAll, nextBurn, recordBurn, storageState, syncAll, takeRefresh, tokenIds, topUpBurns, unusedBurn, } from './sync.js';
|
|
17
18
|
import { decodeViewKey, encodeViewKey } from './viewKey.js';
|
|
18
19
|
/** The ledger's bound outside an emergency. */
|
|
19
20
|
const NO_BOUND = 2n ** 64n - 1n;
|
|
21
|
+
/** A coin on its way to spendable by itself: screening or settling, which `readyBy` has an estimate for. */
|
|
22
|
+
const onItsWay = (c) => c.state === 'screening' || c.state === 'awaiting-root';
|
|
23
|
+
const total = (coins) => coins.reduce((s, c) => s + c.amount, 0n);
|
|
24
|
+
/** Of `coins`, the one whose turning spendable makes them `need`, the soonest first; null where they never do, or the
|
|
25
|
+
* one it takes has no estimate. */
|
|
26
|
+
export function waitsOn(coins, need) {
|
|
27
|
+
const at = (c) => c.readyBy?.getTime() ?? Infinity;
|
|
28
|
+
// two coins with no estimate compare as equal, not as NaN
|
|
29
|
+
const soonest = [...coins].sort((a, b) => Math.sign(at(a) - at(b)) || 0);
|
|
30
|
+
let left = need;
|
|
31
|
+
for (const c of soonest) {
|
|
32
|
+
left -= c.amount;
|
|
33
|
+
if (left <= 0n)
|
|
34
|
+
return c.readyBy ? c : null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
20
38
|
export class Wallet {
|
|
21
39
|
#ctx;
|
|
22
40
|
#tokens = [];
|
|
@@ -24,21 +42,30 @@ export class Wallet {
|
|
|
24
42
|
#queue = Promise.resolve();
|
|
25
43
|
/** Calls running or queued. */
|
|
26
44
|
#busy = 0;
|
|
27
|
-
#
|
|
45
|
+
#lock;
|
|
28
46
|
#referrer;
|
|
29
47
|
#unlock;
|
|
30
48
|
#readOnly = false;
|
|
31
49
|
#closed = new AbortController();
|
|
32
|
-
|
|
50
|
+
/** A wallet over `ctx`, held by `lock`: by default held nowhere, as where there are no Web Locks. */
|
|
51
|
+
constructor(ctx, unlock, lock = { release: () => { }, taken: new AbortController().signal, guard: (f) => f() }) {
|
|
33
52
|
this.#ctx = ctx;
|
|
34
53
|
this.#unlock = unlock;
|
|
54
|
+
this.#lock = lock;
|
|
55
|
+
lock.taken.addEventListener('abort', () => this.#closed.abort(lock.taken.reason));
|
|
35
56
|
}
|
|
36
|
-
/** Lets this wallet
|
|
57
|
+
/** Lets this wallet go, for another tab or another account here. This object is done with: what is
|
|
37
58
|
* queued fails with `wallet-closed`, a burn address search stops, and the wallet is let go once the task under way
|
|
38
59
|
* ends, so that two never write its state at once. */
|
|
39
60
|
close() {
|
|
40
61
|
this.#closed.abort(new WalletError('wallet-closed', 'this wallet was closed: open it again'));
|
|
41
|
-
return this.#queue.then(() => this.#release());
|
|
62
|
+
return this.#queue.then(() => this.#lock.release());
|
|
63
|
+
}
|
|
64
|
+
/** Aborts once another tab or window takes this wallet (`take`), which closes it here: what is queued fails with
|
|
65
|
+
* `wallet-taken`, a burn address search stops, and the task under way saves nothing more, as the other tab reads the
|
|
66
|
+
* state and writes it now. */
|
|
67
|
+
get taken() {
|
|
68
|
+
return this.#lock.taken;
|
|
42
69
|
}
|
|
43
70
|
get address() {
|
|
44
71
|
return this.#ctx.address;
|
|
@@ -73,16 +100,16 @@ export class Wallet {
|
|
|
73
100
|
if (seed.length !== 32)
|
|
74
101
|
fail('invalid', 'the seed is 32 bytes');
|
|
75
102
|
await initCore();
|
|
76
|
-
const
|
|
103
|
+
const hold = await lockWallet(account.address, 'wallet', opts.take);
|
|
77
104
|
try {
|
|
78
|
-
return await Wallet.#open(opts, seed, account,
|
|
105
|
+
return await Wallet.#open(opts, seed, account, hold);
|
|
79
106
|
}
|
|
80
107
|
catch (e) {
|
|
81
|
-
release();
|
|
108
|
+
hold.release();
|
|
82
109
|
throw e;
|
|
83
110
|
}
|
|
84
111
|
}
|
|
85
|
-
static async #open(opts, seed, account,
|
|
112
|
+
static async #open(opts, seed, account, hold) {
|
|
86
113
|
const address = account.address;
|
|
87
114
|
const chains = chainsOf(opts.network);
|
|
88
115
|
const code = await chains[0]?.chain.client.getCode({ address });
|
|
@@ -94,7 +121,7 @@ export class Wallet {
|
|
|
94
121
|
: opts.walletClient.signMessage({ account, message })));
|
|
95
122
|
const storage = await CanisterClient.connect(opts.network.storage.host, opts.network.storage.canisterId);
|
|
96
123
|
const publicKey = hexToBytes(core.spendKey(seed).publicKey);
|
|
97
|
-
const saved = await opts.persist.load();
|
|
124
|
+
const saved = await hold.guard(() => opts.persist.load());
|
|
98
125
|
// registered already: used before, here or on another device, so its past is looked for
|
|
99
126
|
// ponytail: from genesis (~4 MB per million commitments); ask for a birthday once the chain has many
|
|
100
127
|
const restoring = !saved?.length && opts.birthday === undefined && !!(await storage.registration(hexToBytes(address)));
|
|
@@ -110,8 +137,7 @@ export class Wallet {
|
|
|
110
137
|
storage,
|
|
111
138
|
view,
|
|
112
139
|
chains,
|
|
113
|
-
}, { persist: opts.persist, saved, key: stateKey(seed), from: restoring ? 0n : opts.birthday }, typeof opts.seed === 'function' ? opts.seed : undefined);
|
|
114
|
-
wallet.#release = release;
|
|
140
|
+
}, { persist: opts.persist, saved, key: stateKey(seed), from: restoring ? 0n : opts.birthday, hold }, typeof opts.seed === 'function' ? opts.seed : undefined);
|
|
115
141
|
wallet.#referrer = opts.referrer;
|
|
116
142
|
return wallet;
|
|
117
143
|
}
|
|
@@ -123,7 +149,7 @@ export class Wallet {
|
|
|
123
149
|
await initCore();
|
|
124
150
|
const { view, owner } = decodeViewKey(opts.viewKey) ?? fail('invalid', 'not a view key');
|
|
125
151
|
const address = getAddress(bytesToHex(view.address));
|
|
126
|
-
const
|
|
152
|
+
const hold = await lockWallet(address, 'view');
|
|
127
153
|
try {
|
|
128
154
|
const storage = await CanisterClient.connect(opts.network.storage.host, opts.network.storage.canisterId);
|
|
129
155
|
// what the account signed: a nullifying key that is not the account's would show spent coins as unspent
|
|
@@ -146,21 +172,22 @@ export class Wallet {
|
|
|
146
172
|
// registered, so its past is looked for
|
|
147
173
|
{
|
|
148
174
|
persist: opts.persist,
|
|
149
|
-
saved: await opts.persist.load(),
|
|
175
|
+
saved: await hold.guard(() => opts.persist.load()),
|
|
150
176
|
key: stateKey(view.seed),
|
|
151
177
|
from: opts.birthday ?? 0n,
|
|
178
|
+
hold,
|
|
152
179
|
});
|
|
153
|
-
wallet.#release = release;
|
|
154
180
|
wallet.#readOnly = true;
|
|
155
181
|
return wallet;
|
|
156
182
|
}
|
|
157
183
|
catch (e) {
|
|
158
|
-
release();
|
|
184
|
+
hold.release();
|
|
159
185
|
throw e;
|
|
160
186
|
}
|
|
161
187
|
}
|
|
162
|
-
/** The wallet over `base`, its state opened from what `persist` saved, with `key`, or started `from` a block or date
|
|
163
|
-
|
|
188
|
+
/** The wallet over `base`, its state opened from what `persist` saved, with `key`, or started `from` a block or date,
|
|
189
|
+
* as long as `hold` holds it. */
|
|
190
|
+
static async #begin(base, { persist, saved, key, from, hold, }, unlock) {
|
|
164
191
|
const loaded = saved?.length ? openState(key, saved) : null;
|
|
165
192
|
// never started over in place: the first save would overwrite what it held, operations in flight included
|
|
166
193
|
if (saved?.length && !loaded) {
|
|
@@ -169,15 +196,18 @@ export class Wallet {
|
|
|
169
196
|
if (loaded && loaded.address.toLowerCase() !== base.address.toLowerCase())
|
|
170
197
|
fail('invalid', `the saved state is ${loaded.address}'s: keep one per address`);
|
|
171
198
|
const state = loaded ?? emptyWalletState(base.address);
|
|
172
|
-
const
|
|
199
|
+
const save = () => hold.guard(() => persist.save(sealState(key, state)));
|
|
200
|
+
const ctx = { ...base, state, save, lastFullRefreshMs: 0 };
|
|
173
201
|
if (!loaded)
|
|
174
202
|
await start(ctx, from);
|
|
175
|
-
const wallet = new Wallet(ctx, unlock);
|
|
203
|
+
const wallet = new Wallet(ctx, unlock, hold);
|
|
176
204
|
// a saved state names its tokens by id, and its coins stand by the chains' roots: read both now, so balances, coins
|
|
177
205
|
// and history work before the first sync
|
|
178
206
|
const [tokens] = await Promise.all([Promise.all(ctx.chains.map((c) => c.chain.tokens())), wallet.#readRoots()]);
|
|
179
207
|
wallet.#tokens = tokens.flat();
|
|
180
208
|
keyAll(ctx, wallet.#tokens);
|
|
209
|
+
// taken while it opened, before it listened
|
|
210
|
+
hold.taken.throwIfAborted();
|
|
181
211
|
return wallet;
|
|
182
212
|
}
|
|
183
213
|
/** Holds `seed`, asked of `#unlock`, once it is checked to be this wallet's. */
|
|
@@ -275,6 +305,10 @@ export class Wallet {
|
|
|
275
305
|
delete op.retrying;
|
|
276
306
|
}
|
|
277
307
|
catch (e) {
|
|
308
|
+
// cancelled while it ran: it stays so. And a relayer relaying its request already answers the request that
|
|
309
|
+
// started it, so whether a service keeps failing stays as the tries before found it.
|
|
310
|
+
if (op.status !== 'pending' || e instanceof Relaying)
|
|
311
|
+
continue;
|
|
278
312
|
// only what trying again cannot change fails an operation: a server or an RPC that does not answer (any
|
|
279
313
|
// error that is not the wallet's own), or answers what the chain does not show yet, is tried next sync. It is
|
|
280
314
|
// kept on the operation, so that one that never gets past it does not look merely slow.
|
|
@@ -358,6 +392,12 @@ export class Wallet {
|
|
|
358
392
|
coins() {
|
|
359
393
|
const size = (chainId) => this.#status.get(chainId)?.latestRoot?.size ?? 0;
|
|
360
394
|
const unproved = (chainId) => (this.#status.get(chainId)?.halted ? 'roots-halted' : 'awaiting-root');
|
|
395
|
+
// when a root should cover a coin that landed at `atMs`
|
|
396
|
+
const covered = (chainId, atMs) => {
|
|
397
|
+
const secs = this.#status.get(chainId)?.settleSecs ?? null;
|
|
398
|
+
return atMs === undefined || secs === null ? null : atMs + secs * 1000;
|
|
399
|
+
};
|
|
400
|
+
const date = (ms) => (ms === null ? null : new Date(ms));
|
|
361
401
|
const spending = new Set(this.#ctx.state.ops.flatMap((o) => inputsOf(o).map((c) => `${o.chainId}:${c.index}`)));
|
|
362
402
|
// spent by a JoinSplit in a block, which waits only to be final: gone, as activity() counts it. Sync sees it by its
|
|
363
403
|
// nullifier (`unspent`); this device's own is gone already from its relay on (a reorg that drops it brings them
|
|
@@ -382,7 +422,7 @@ export class Wallet {
|
|
|
382
422
|
state,
|
|
383
423
|
from: n.peer,
|
|
384
424
|
via: null,
|
|
385
|
-
|
|
425
|
+
readyBy: state === 'awaiting-root' ? date(covered(n.chainId, f.atMs)) : null,
|
|
386
426
|
at: f.atMs === undefined ? null : new Date(f.atMs),
|
|
387
427
|
});
|
|
388
428
|
}
|
|
@@ -402,7 +442,13 @@ export class Wallet {
|
|
|
402
442
|
: f.index >= size(d.chainId)
|
|
403
443
|
? unproved(d.chainId)
|
|
404
444
|
: 'spendable';
|
|
405
|
-
|
|
445
|
+
// a root covers a deposit while it is screened, most often before its check ends
|
|
446
|
+
const settles = covered(d.chainId, f.atMs);
|
|
447
|
+
const readyBy = state === 'screening'
|
|
448
|
+
? date(f.decidedByMs === undefined ? null : Math.max(f.decidedByMs, settles ?? 0))
|
|
449
|
+
: state === 'awaiting-root'
|
|
450
|
+
? date(settles)
|
|
451
|
+
: null;
|
|
406
452
|
out.push({
|
|
407
453
|
id,
|
|
408
454
|
kind: 'deposit',
|
|
@@ -411,7 +457,7 @@ export class Wallet {
|
|
|
411
457
|
state,
|
|
412
458
|
from: f.sender,
|
|
413
459
|
via: this.#via(d, f.sender),
|
|
414
|
-
|
|
460
|
+
readyBy,
|
|
415
461
|
at: f.atMs === undefined ? null : new Date(f.atMs),
|
|
416
462
|
});
|
|
417
463
|
}
|
|
@@ -732,6 +778,9 @@ export class Wallet {
|
|
|
732
778
|
return op.kind === 'deposit' ? null : (op.data.step?.stage ?? null);
|
|
733
779
|
},
|
|
734
780
|
to: op.kind === 'deposit' ? (op.data.to ?? null) : null,
|
|
781
|
+
get cancellable() {
|
|
782
|
+
return cancellable(op);
|
|
783
|
+
},
|
|
735
784
|
wait: async () => {
|
|
736
785
|
for (;;) {
|
|
737
786
|
if (op.status === 'done')
|
|
@@ -743,6 +792,31 @@ export class Wallet {
|
|
|
743
792
|
await new Promise((r) => setTimeout(r, 15_000));
|
|
744
793
|
}
|
|
745
794
|
},
|
|
795
|
+
// at once, outside the queue: a sync under way may wait long on a service that does not answer, and a page closed
|
|
796
|
+
// before a queued save would find the operation pending and relay it. That sync starts no further stage of it.
|
|
797
|
+
cancel: async () => {
|
|
798
|
+
if (op.errorCode === 'cancelled')
|
|
799
|
+
return;
|
|
800
|
+
if (!cancellable(op))
|
|
801
|
+
fail('invalid', op.status === 'pending'
|
|
802
|
+
? 'it has a request out to the relayer, or has paid: it finishes or fails on its own'
|
|
803
|
+
: `it is ${op.status}`);
|
|
804
|
+
const ops = this.#ctx.state.ops;
|
|
805
|
+
const at = ops.indexOf(op);
|
|
806
|
+
if (at < 0)
|
|
807
|
+
fail('invalid', 'this wallet no longer holds it');
|
|
808
|
+
ops.splice(at, 1);
|
|
809
|
+
Object.assign(op, { status: 'failed', errorCode: 'cancelled', error: 'cancelled' });
|
|
810
|
+
try {
|
|
811
|
+
await this.#ctx.save();
|
|
812
|
+
}
|
|
813
|
+
catch (e) {
|
|
814
|
+
// not saved, so not cancelled: the saved state would resume it
|
|
815
|
+
Object.assign(op, { status: 'pending', errorCode: undefined, error: undefined });
|
|
816
|
+
ops.splice(at, 0, op);
|
|
817
|
+
throw e;
|
|
818
|
+
}
|
|
819
|
+
},
|
|
746
820
|
};
|
|
747
821
|
}
|
|
748
822
|
#newOp(kind, token, amount, data) {
|
|
@@ -1028,7 +1102,11 @@ export class Wallet {
|
|
|
1028
1102
|
if (opts.kind === 'consolidate') {
|
|
1029
1103
|
const chosen = opts.coins && this.#chosen(token, opts.coins);
|
|
1030
1104
|
if (chosen && chosenSettling(this.#ctx, token.chainId, chosen, all)) {
|
|
1031
|
-
|
|
1105
|
+
// all of them must be spendable: the last one on its way, where each is a coin with an estimate
|
|
1106
|
+
const ids = new Set(opts.coins);
|
|
1107
|
+
const picked = this.coins().filter((c) => ids.has(c.id));
|
|
1108
|
+
const waiting = picked.filter((c) => c.state !== 'spendable');
|
|
1109
|
+
throw new WalletError('funds-settling', 'a chosen note or deposit is still settling: quote again once coins() shows it spendable', picked.length === ids.size && waiting.every(onItsWay) ? waitsOn(waiting, total(waiting)) : null);
|
|
1032
1110
|
}
|
|
1033
1111
|
const steps = BigInt(roundsToMerge(mergePool(all, chosen)));
|
|
1034
1112
|
// a merge pays the relayer publicly, so the protocol takes its cut of that, grossed up as a payment's is
|
|
@@ -1045,9 +1123,25 @@ export class Wallet {
|
|
|
1045
1123
|
const { withdrawn, protocol } = await withdrawalOf(this.#ctx, token.chainId, token.tokenId, opts.kind === 'withdraw', amount, relayer);
|
|
1046
1124
|
const onTheWay = this.#ctx.state.ops.some((o) => o.status === 'pending') ||
|
|
1047
1125
|
settling(this.#ctx, token.chainId, token.tokenId, size);
|
|
1048
|
-
|
|
1126
|
+
const need = amount + relayer + protocol;
|
|
1127
|
+
try {
|
|
1128
|
+
payWith(all, need, relayer, onTheWay);
|
|
1129
|
+
}
|
|
1130
|
+
catch (e) {
|
|
1131
|
+
if (!(e instanceof WalletError) || e.code !== 'funds-settling')
|
|
1132
|
+
throw e;
|
|
1133
|
+
throw new WalletError(e.code, e.message, this.#waitsFor(token, need));
|
|
1134
|
+
}
|
|
1049
1135
|
return { relayer, protocol, protocolBps: bpsOf(protocol, withdrawn), fee: relayer + protocol, steps: 1 };
|
|
1050
1136
|
}
|
|
1137
|
+
/** Of `token`'s coins on their way, the one whose turning spendable lets a payment of `need` go, as
|
|
1138
|
+
* `WalletError.waitsOn` tells. None while an operation is in flight, as its change is not a coin yet. */
|
|
1139
|
+
#waitsFor(token, need) {
|
|
1140
|
+
if (this.#ctx.state.ops.some((o) => o.status === 'pending'))
|
|
1141
|
+
return null;
|
|
1142
|
+
const mine = this.coins().filter((c) => c.token.chainId === token.chainId && c.token.tokenId === token.tokenId);
|
|
1143
|
+
return waitsOn(mine.filter(onItsWay), need - total(mine.filter((c) => c.state === 'spendable')));
|
|
1144
|
+
}
|
|
1051
1145
|
/** The most a send or withdrawal of `token` can pay: what is spendable, less the fees `fees` quotes on top of it, and
|
|
1052
1146
|
* the merges first when one JoinSplit cannot spend it all, which `fees` then asks for (`needs-consolidation`). */
|
|
1053
1147
|
async maxAmount(opts) {
|
|
@@ -1068,6 +1162,11 @@ export class Wallet {
|
|
|
1068
1162
|
const merge = mergeRound(coins(this.#ctx, token.chainId, token.tokenId, size), fee);
|
|
1069
1163
|
return merge && merge.map((m) => `${token.chainId}:${m.index}`);
|
|
1070
1164
|
}
|
|
1165
|
+
/** Fetches what a send or withdrawal proves with, some 25 MB, once a page: an app calls it as a payment form opens,
|
|
1166
|
+
* so that the payment's proof does not wait for the download. Outside the queue of calls: a sync goes on meanwhile. */
|
|
1167
|
+
preloadProver() {
|
|
1168
|
+
return preload(this.#ctx.network.circuits.joinsplit);
|
|
1169
|
+
}
|
|
1071
1170
|
/** Pays `amount` privately: to an account that has registered (any wallet that ever opened), by its address or its
|
|
1072
1171
|
* zERC20 ID (`accountId`), or to a receive code (`receiveCode`). An account that never registered fails with
|
|
1073
1172
|
* `recipient-not-registered`. One JoinSplit from what is spendable now, or it fails here: `needs-consolidation`,
|
|
@@ -1178,24 +1277,41 @@ const chainsOf = (network) => network.chains.map((config) => ({
|
|
|
1178
1277
|
indexer: new Indexer(config.indexer),
|
|
1179
1278
|
asp: new Asp(config.asp),
|
|
1180
1279
|
}));
|
|
1181
|
-
/** Holds `address`'s wallet
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
export async function lockWallet(address, kind = 'wallet') {
|
|
1280
|
+
/** Holds `address`'s wallet: two tabs on one wallet would each overwrite the other's state and spend the same notes. It
|
|
1281
|
+
* fails with `wallet-in-use` while another tab or window holds it, unless it `take`s it from that one. A wallet that
|
|
1282
|
+
* watches it (`view`) is held apart, as its state is its own. Neither where there are no Web Locks. */
|
|
1283
|
+
export async function lockWallet(address, kind = 'wallet', take = false) {
|
|
1284
|
+
const taken = new AbortController();
|
|
1185
1285
|
const locks = globalThis.navigator?.locks;
|
|
1186
1286
|
if (!locks)
|
|
1187
|
-
return () => { };
|
|
1287
|
+
return { release: () => { }, taken: taken.signal, guard: (f) => f() };
|
|
1288
|
+
const name = `zerc20-${kind}:${address.toLowerCase()}`;
|
|
1289
|
+
const lose = () => taken.abort(new WalletError('wallet-taken', 'this wallet was opened in another tab or window'));
|
|
1188
1290
|
let release = () => { };
|
|
1189
1291
|
const held = new Promise((r) => (release = r));
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1292
|
+
let got = false;
|
|
1293
|
+
await new Promise((resolve, reject) => {
|
|
1294
|
+
locks
|
|
1295
|
+
.request(name, take ? { steal: true } : { ifAvailable: true }, (lock) => {
|
|
1296
|
+
got = lock !== null;
|
|
1297
|
+
resolve();
|
|
1298
|
+
return got ? held : undefined;
|
|
1299
|
+
})
|
|
1300
|
+
// a held lock's request rejects as another takes it
|
|
1301
|
+
.catch((e) => (got ? lose() : reject(e)));
|
|
1195
1302
|
});
|
|
1196
1303
|
if (!got)
|
|
1197
1304
|
fail('wallet-in-use', 'this wallet is open in another tab or window: close it there first');
|
|
1198
|
-
|
|
1305
|
+
const state = `${name}/state`;
|
|
1306
|
+
const guard = (f) => locks.request(state, async () => {
|
|
1307
|
+
const holders = (await locks.query()).held ?? [];
|
|
1308
|
+
const holder = (n) => holders.find((l) => l.name === n)?.clientId;
|
|
1309
|
+
if (holder(name) !== holder(state))
|
|
1310
|
+
lose();
|
|
1311
|
+
taken.signal.throwIfAborted();
|
|
1312
|
+
return f();
|
|
1313
|
+
});
|
|
1314
|
+
return { release, taken: taken.signal, guard };
|
|
1199
1315
|
}
|
|
1200
1316
|
/** A deposit `deposit()` did not see through (a reload, a lost connection): settled by its transaction's receipt, or
|
|
1201
1317
|
* failed if it never sent one. Runs only between tasks, so no `deposit()` is still on its way to sending it. */
|