@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/src/pos.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// every wallet downloads. Stolen, it gives away which addresses are the merchant's, and nothing else: the merchant's
|
|
4
4
|
// wallet alone spends what they receive, merges it and withdraws it.
|
|
5
5
|
|
|
6
|
-
import { BURN_BLOCK, CanisterClient, lookupRecipient,
|
|
6
|
+
import { BURN_BLOCK, CanisterClient, MAX_POS_BLOCKS, lookupRecipient, writePosBlocks } from '@zerc20/storage-sdk';
|
|
7
7
|
import {
|
|
8
8
|
type Address,
|
|
9
9
|
type Hex,
|
|
@@ -16,11 +16,11 @@ import {
|
|
|
16
16
|
zeroAddress,
|
|
17
17
|
} from 'viem';
|
|
18
18
|
import { Chain, type Token } from './chain.js';
|
|
19
|
-
import { burnAddress, core, initCore } from './core.js';
|
|
20
|
-
import { fail } from './errors.js';
|
|
19
|
+
import { type Burn, burnAddress, core, initCore } from './core.js';
|
|
20
|
+
import { WalletError, fail } from './errors.js';
|
|
21
21
|
import { type ChainConfig, type Network, checkNetwork } from './network.js';
|
|
22
22
|
import { scan } from './scan.js';
|
|
23
|
-
import { Asp, Indexer } from './servers.js';
|
|
23
|
+
import { Asp, Indexer, type IndexerStatus } from './servers.js';
|
|
24
24
|
import { registerDeposit } from './services.js';
|
|
25
25
|
import type { DepositRecord } from './state.js';
|
|
26
26
|
import { applyHits, commitmentFields, commitmentTx, dropReorged, readDecisions, readTransfers } from './sync.js';
|
|
@@ -28,6 +28,8 @@ import { applyHits, commitmentFields, commitmentTx, dropReorged, readDecisions,
|
|
|
28
28
|
/** The longest a refill waits once a token's addresses run low: a write at a random time after the sale that drew
|
|
29
29
|
* them down, not right after it, so that the write's time does not mark that sale. */
|
|
30
30
|
const REFILL_WITHIN_MS = 10 * 60_000;
|
|
31
|
+
/** How long what a pass over the chains read may go unsaved: lost, it is read from the chains again. */
|
|
32
|
+
const PASS_SAVED_WITHIN_MS = 60_000;
|
|
31
33
|
|
|
32
34
|
/** What a point of sale needs of a token: where it is, and its id for the burn addresses' derivation and tags. */
|
|
33
35
|
export type PosToken = Pick<Token, 'chainId' | 'tokenId' | 'address'>;
|
|
@@ -74,6 +76,16 @@ export interface SaleStatus {
|
|
|
74
76
|
|
|
75
77
|
type Watched = Pick<DepositRecord, 'address' | 'watches' | 'found'>;
|
|
76
78
|
|
|
79
|
+
/** A token's addresses: made and recorded ahead of its sales. */
|
|
80
|
+
interface Pool {
|
|
81
|
+
/** The next counter to derive. */
|
|
82
|
+
next: number;
|
|
83
|
+
/** Addresses made and not recorded yet, a block of counters `first..` each. */
|
|
84
|
+
made: { first: number; burns: Burn[] }[];
|
|
85
|
+
/** Addresses recorded in the lane that no sale has had. */
|
|
86
|
+
ready: Address[];
|
|
87
|
+
}
|
|
88
|
+
|
|
77
89
|
interface Connection {
|
|
78
90
|
merchant: Address;
|
|
79
91
|
/** The zERC20 public key the merchant registered in storage, which its burn addresses are under. */
|
|
@@ -81,8 +93,8 @@ interface Connection {
|
|
|
81
93
|
laneKey: Hex;
|
|
82
94
|
/** Per chain, the lane's first slot not known to be taken. */
|
|
83
95
|
slots: Record<string, number>;
|
|
84
|
-
/** Per `chainId:tokenId
|
|
85
|
-
pools: Record<string,
|
|
96
|
+
/** Per `chainId:tokenId`. */
|
|
97
|
+
pools: Record<string, Pool>;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
interface Saved {
|
|
@@ -103,7 +115,29 @@ const decode = (b: Uint8Array): Saved =>
|
|
|
103
115
|
) as Saved;
|
|
104
116
|
|
|
105
117
|
const same = (a: string, b: string) => a.toLowerCase() === b.toLowerCase();
|
|
106
|
-
const pairOf = (token: PosToken) => `${token.chainId}:${token.tokenId}`;
|
|
118
|
+
const pairOf = (token: Pick<PosToken, 'chainId' | 'tokenId'>) => `${token.chainId}:${token.tokenId}`;
|
|
119
|
+
const madeOf = (pool: Pool) => pool.made.length * BURN_BLOCK;
|
|
120
|
+
|
|
121
|
+
/** A chain's indexer as one pass reads it: its status, read once for every point of sale of the process that asks while
|
|
122
|
+
* it is read, and the tag pages they read after it, each once. The points of sale of a server run their passes side by
|
|
123
|
+
* side, so the indexer serves each page once a pass, not once a merchant; and a page read after the status is never
|
|
124
|
+
* older than it, which the scan needs. */
|
|
125
|
+
interface Pass {
|
|
126
|
+
status: IndexerStatus;
|
|
127
|
+
tags: Map<number, Promise<Uint8Array>>;
|
|
128
|
+
}
|
|
129
|
+
const passes = new Map<string, Promise<Pass>>();
|
|
130
|
+
|
|
131
|
+
function passOf(indexer: Indexer): Promise<Pass> {
|
|
132
|
+
let pass = passes.get(indexer.url);
|
|
133
|
+
if (!pass) {
|
|
134
|
+
const read = indexer.status().then((status) => ({ status, tags: new Map() }));
|
|
135
|
+
const done = () => void (passes.get(indexer.url) === read && passes.delete(indexer.url));
|
|
136
|
+
read.then(done, done);
|
|
137
|
+
passes.set(indexer.url, (pass = read));
|
|
138
|
+
}
|
|
139
|
+
return pass;
|
|
140
|
+
}
|
|
107
141
|
|
|
108
142
|
export class PointOfSale {
|
|
109
143
|
readonly #network: Network;
|
|
@@ -111,12 +145,23 @@ export class PointOfSale {
|
|
|
111
145
|
readonly #persist: { save(bytes: Uint8Array): Promise<void> };
|
|
112
146
|
readonly #refillWithinMs: number;
|
|
113
147
|
readonly #chains = new Map<bigint, Chain>();
|
|
114
|
-
/**
|
|
115
|
-
readonly #
|
|
148
|
+
/** Sales by lowercase id. */
|
|
149
|
+
readonly #byId = new Map<string, Sale>();
|
|
150
|
+
/** The block each token is making ahead: one at a time, beside those made for a sale that waits. */
|
|
151
|
+
readonly #making = new Map<string, Promise<void>>();
|
|
152
|
+
/** The write each token has under way: one at a time. */
|
|
153
|
+
readonly #recording = new Map<string, Promise<void>>();
|
|
154
|
+
/** The write each token waits to do, at a random time. */
|
|
116
155
|
readonly #timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
156
|
+
/** How many sales of each token wait for an address. */
|
|
157
|
+
readonly #waiting = new Map<string, number>();
|
|
158
|
+
/** The searches of the lane connected now: another lane, or closing, stops them. */
|
|
159
|
+
#searches = new AbortController();
|
|
117
160
|
#storage: Promise<CanisterClient> | undefined;
|
|
118
161
|
#queue: Promise<unknown> = Promise.resolve();
|
|
119
162
|
#saving: Promise<unknown> = Promise.resolve();
|
|
163
|
+
#passSave: ReturnType<typeof setTimeout> | undefined;
|
|
164
|
+
#closed = false;
|
|
120
165
|
|
|
121
166
|
private constructor(
|
|
122
167
|
network: Network,
|
|
@@ -128,10 +173,12 @@ export class PointOfSale {
|
|
|
128
173
|
this.#state = state;
|
|
129
174
|
this.#persist = persist;
|
|
130
175
|
this.#refillWithinMs = refillWithinMs;
|
|
176
|
+
for (const s of state.sales) this.#byId.set(s.id.toLowerCase(), s);
|
|
131
177
|
}
|
|
132
178
|
|
|
133
179
|
/** Opens the point of sale's saved state. It keeps which addresses it handed out: lose that, and it may hand one out
|
|
134
|
-
* twice. One state, one point of sale: two open on it would hand out the same addresses
|
|
180
|
+
* twice. One state, one point of sale: two open on it would hand out the same addresses, so `close` the one before
|
|
181
|
+
* opening it again. */
|
|
135
182
|
static async open(opts: {
|
|
136
183
|
network: Network;
|
|
137
184
|
persist: { load(): Promise<Uint8Array | null | undefined>; save(bytes: Uint8Array): Promise<void> };
|
|
@@ -141,9 +188,18 @@ export class PointOfSale {
|
|
|
141
188
|
await initCore();
|
|
142
189
|
const saved = await opts.persist.load();
|
|
143
190
|
const state: Saved = saved?.length ? decode(saved) : { connection: null, sales: [], seen: {}, txs: {} };
|
|
191
|
+
// a state saved before addresses were made ahead
|
|
192
|
+
for (const pool of Object.values(state.connection?.pools ?? {})) pool.made ??= [];
|
|
144
193
|
return new PointOfSale(opts.network, state, opts.persist, opts.refillWithinMs ?? REFILL_WITHIN_MS);
|
|
145
194
|
}
|
|
146
195
|
|
|
196
|
+
/** Whether `merchant` registered for private payments in storage, as `connect` needs: a server may ask before it keeps
|
|
197
|
+
* anything for an account, since storage admits registrations at a bounded rate and accounts cost nothing. */
|
|
198
|
+
static async registered(network: Network, merchant: Address): Promise<boolean> {
|
|
199
|
+
const client = await CanisterClient.connect(network.storage.host, network.storage.canisterId);
|
|
200
|
+
return (await lookupRecipient(client, hexToBytes(merchant))) !== null;
|
|
201
|
+
}
|
|
202
|
+
|
|
147
203
|
get connected(): boolean {
|
|
148
204
|
return this.#state.connection !== null;
|
|
149
205
|
}
|
|
@@ -159,9 +215,7 @@ export class PointOfSale {
|
|
|
159
215
|
if (c && same(c.merchant, merchant) && same(c.laneKey, laneKey)) return;
|
|
160
216
|
const recipient = await lookupRecipient(await this.#client(), hexToBytes(merchant));
|
|
161
217
|
if (!recipient) fail('invalid', `${merchant} has not registered for private payments: open its wallet once`);
|
|
162
|
-
|
|
163
|
-
this.#timers.clear();
|
|
164
|
-
this.#fills.clear();
|
|
218
|
+
this.#stop();
|
|
165
219
|
this.#state.connection = {
|
|
166
220
|
merchant: getAddress(merchant),
|
|
167
221
|
publicKey: bytesToHex(recipient!.publicKey),
|
|
@@ -173,11 +227,20 @@ export class PointOfSale {
|
|
|
173
227
|
});
|
|
174
228
|
}
|
|
175
229
|
|
|
230
|
+
/** Stops making and recording, then saves the state as it is: nothing is saved after, so the state may be opened
|
|
231
|
+
* again. A sale under way then fails rather than hand out an address it could not save. */
|
|
232
|
+
async close(): Promise<void> {
|
|
233
|
+
this.#stop();
|
|
234
|
+
const last = this.#save();
|
|
235
|
+
this.#closed = true;
|
|
236
|
+
await last;
|
|
237
|
+
}
|
|
238
|
+
|
|
176
239
|
/** Readies addresses of `token` ahead of its sales: an app calls it for each token it takes, as it starts and after
|
|
177
240
|
* sales. Nothing happens while enough are ready, or a refill is under way or due. */
|
|
178
241
|
prepare(token: PosToken): void {
|
|
179
242
|
chainOf(this.#network, token.chainId);
|
|
180
|
-
this.#
|
|
243
|
+
this.#supply(token);
|
|
181
244
|
}
|
|
182
245
|
|
|
183
246
|
/** How many addresses of `token` are recorded and wait for a sale. */
|
|
@@ -191,9 +254,19 @@ export class PointOfSale {
|
|
|
191
254
|
const { token } = opts;
|
|
192
255
|
const c = chainOf(this.#network, token.chainId);
|
|
193
256
|
this.#connection();
|
|
194
|
-
const
|
|
257
|
+
const pair = pairOf(token);
|
|
258
|
+
// counted from the start, so that the sales that come at once have their addresses made at once
|
|
259
|
+
this.#waiting.set(pair, (this.#waiting.get(pair) ?? 0) + 1);
|
|
195
260
|
let burnAddress: Address | undefined;
|
|
196
|
-
|
|
261
|
+
let from: number;
|
|
262
|
+
try {
|
|
263
|
+
({ commitmentsFinal: from } = await new Indexer(c.indexer).status());
|
|
264
|
+
while (!(burnAddress = this.#pool(token).ready.shift())) await this.#record(token);
|
|
265
|
+
} finally {
|
|
266
|
+
const left = this.#waiting.get(pair)! - 1;
|
|
267
|
+
if (left) this.#waiting.set(pair, left);
|
|
268
|
+
else this.#waiting.delete(pair);
|
|
269
|
+
}
|
|
197
270
|
// saved as taken before the deposit relayer is asked: a sale that fails half-way never hands it out again
|
|
198
271
|
const sale: Sale = {
|
|
199
272
|
id: burnAddress,
|
|
@@ -209,9 +282,10 @@ export class PointOfSale {
|
|
|
209
282
|
};
|
|
210
283
|
const watch = { key: tag(token.tokenId, burnAddress), from, scannedTo: from };
|
|
211
284
|
this.#state.sales.push(sale);
|
|
285
|
+
this.#byId.set(burnAddress.toLowerCase(), sale);
|
|
212
286
|
this.#state.seen[burnAddress] = { address: burnAddress, watches: [{ tokenId: token.tokenId, watch }], found: [] };
|
|
213
287
|
await this.#save();
|
|
214
|
-
this.#
|
|
288
|
+
this.#supply(token);
|
|
215
289
|
return opts.via === 'deposit' ? this.#exclusive(() => this.#register(sale, opts.refundTo)) : sale;
|
|
216
290
|
}
|
|
217
291
|
|
|
@@ -231,7 +305,9 @@ export class PointOfSale {
|
|
|
231
305
|
if (same(sale.refundTo!, to)) return sale;
|
|
232
306
|
return fail('invalid', `sale ${sale.id} already has a deposit address, refunding to ${sale.refundTo}`);
|
|
233
307
|
}
|
|
234
|
-
const { depositAddress, fee } = await registerDeposit(this.#chain(sale.chainId), sale.token, sale.burnAddress,
|
|
308
|
+
const { depositAddress, fee } = await registerDeposit(this.#chain(sale.chainId), sale.token, sale.burnAddress, {
|
|
309
|
+
address: to,
|
|
310
|
+
});
|
|
235
311
|
Object.assign(sale, { depositAddress, depositFee: fee, refundTo: to });
|
|
236
312
|
await this.#save();
|
|
237
313
|
return sale;
|
|
@@ -241,6 +317,11 @@ export class PointOfSale {
|
|
|
241
317
|
return [...this.#state.sales];
|
|
242
318
|
}
|
|
243
319
|
|
|
320
|
+
/** Sale `id`. */
|
|
321
|
+
sale(id: Address): Sale {
|
|
322
|
+
return this.#sale(id);
|
|
323
|
+
}
|
|
324
|
+
|
|
244
325
|
/** What reached each of sales `ids`: every transfer to its burn address, with where each stands. One pass per chain
|
|
245
326
|
* for them all, reading only what every wallet downloads, so no server learns which addresses it asks about. A chain
|
|
246
327
|
* that fails keeps what was read before, and its error is returned. */
|
|
@@ -257,7 +338,7 @@ export class PointOfSale {
|
|
|
257
338
|
).catch((e: unknown) => void errors.push(e)),
|
|
258
339
|
),
|
|
259
340
|
);
|
|
260
|
-
|
|
341
|
+
this.#saveSoon();
|
|
261
342
|
return { sales: sales.map((s) => this.#statusOf(s)), errors };
|
|
262
343
|
});
|
|
263
344
|
}
|
|
@@ -278,9 +359,14 @@ export class PointOfSale {
|
|
|
278
359
|
const c = chainOf(this.#network, chainId);
|
|
279
360
|
const indexer = new Indexer(c.indexer);
|
|
280
361
|
const seen = sales.map((s) => this.#state.seen[s.id]);
|
|
281
|
-
const status = await indexer
|
|
362
|
+
const { status, tags } = await passOf(indexer);
|
|
363
|
+
const page = (p: number) => {
|
|
364
|
+
let read = tags.get(p);
|
|
365
|
+
if (!read) tags.set(p, (read = indexer.page('tags', p)));
|
|
366
|
+
return read;
|
|
367
|
+
};
|
|
282
368
|
const watches = seen.flatMap((r) => r.watches.map((w) => w.watch));
|
|
283
|
-
const hits = await scan(watches, 4,
|
|
369
|
+
const hits = await scan(watches, 4, page, status.commitmentsFinal, status.commitments);
|
|
284
370
|
applyHits([], seen, hits);
|
|
285
371
|
dropReorged(seen, status.commitmentsFinal);
|
|
286
372
|
await readTransfers(commitmentFields(indexer), seen);
|
|
@@ -314,67 +400,156 @@ export class PointOfSale {
|
|
|
314
400
|
return { sale, payments, received };
|
|
315
401
|
}
|
|
316
402
|
|
|
317
|
-
/**
|
|
318
|
-
*
|
|
319
|
-
#
|
|
403
|
+
/** How many addresses of a token to keep made, and recorded, ahead: twice its sales of the last `refillWithinMs`, so
|
|
404
|
+
* that they last through the wait for a refill as sales pick up; in whole blocks, at least one, at most one write. */
|
|
405
|
+
#low(pair: string): number {
|
|
406
|
+
const sales = this.#state.sales;
|
|
407
|
+
const since = Date.now() - this.#refillWithinMs;
|
|
408
|
+
let recent = 0;
|
|
409
|
+
// sales are kept in the order they were made
|
|
410
|
+
for (let i = sales.length - 1; i >= 0 && sales[i].createdMs >= since; i--) if (pairOf(sales[i]) === pair) recent++;
|
|
411
|
+
return BURN_BLOCK * Math.min(MAX_POS_BLOCKS, Math.max(1, Math.ceil((2 * recent) / BURN_BLOCK)));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Keeps `token` supplied. Its addresses are recorded once fewer than `#low` are ready: right away when none is,
|
|
415
|
+
* otherwise at a random time within `refillWithinMs`, so that the time of a write does not mark the sale that drew
|
|
416
|
+
* them down. They are made ahead, which shows nobody anything, until `#low` are made. */
|
|
417
|
+
#supply(token: PosToken): void {
|
|
418
|
+
if (!this.#state.connection || this.#closed) return;
|
|
320
419
|
const pair = pairOf(token);
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}, Math.random() * this.#refillWithinMs);
|
|
330
|
-
(timer as { unref?: () => void }).unref?.(); // a process need not stay up for it
|
|
331
|
-
this.#timers.set(pair, timer);
|
|
420
|
+
const pool = this.#pool(token);
|
|
421
|
+
const low = this.#low(pair);
|
|
422
|
+
if (pool.ready.length < low && !this.#recording.has(pair) && !this.#timers.has(pair)) {
|
|
423
|
+
// a write that fails is tried again by the next sale, which tells why
|
|
424
|
+
if (pool.ready.length === 0) void this.#record(token).catch(() => undefined);
|
|
425
|
+
else this.#later(token);
|
|
426
|
+
}
|
|
427
|
+
if (madeOf(pool) < low) void this.#ahead(token).catch(() => undefined);
|
|
332
428
|
}
|
|
333
429
|
|
|
334
|
-
/**
|
|
335
|
-
|
|
336
|
-
#fill(token: PosToken): Promise<void> {
|
|
430
|
+
/** The block `token` is making ahead, begun if there is none; the next follows while fewer than `#low` are made. */
|
|
431
|
+
#ahead(token: PosToken): Promise<void> {
|
|
337
432
|
const pair = pairOf(token);
|
|
338
|
-
const running = this.#
|
|
433
|
+
const running = this.#making.get(pair);
|
|
434
|
+
if (running) return running;
|
|
435
|
+
const run: Promise<void> = this.#make(token, false).finally(() => {
|
|
436
|
+
if (this.#making.get(pair) === run) this.#making.delete(pair);
|
|
437
|
+
});
|
|
438
|
+
this.#making.set(pair, run);
|
|
439
|
+
// one that fails is made again by the next sale, or the next pass's `prepare`
|
|
440
|
+
run.then(
|
|
441
|
+
() => this.#supply(token),
|
|
442
|
+
() => undefined,
|
|
443
|
+
);
|
|
444
|
+
return run;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Makes `token`'s next block of addresses. Its counters are taken as it starts, so that blocks made side by side
|
|
448
|
+
* never share one; a block that fails, or ends after the lane changed, leaves a gap in the counters, which no reader
|
|
449
|
+
* minds. What is made is saved with the next save: lost, it is made again from the same counters, to the same
|
|
450
|
+
* addresses, none of which a sale has had. */
|
|
451
|
+
async #make(token: PosToken, urgent: boolean): Promise<void> {
|
|
452
|
+
const c = this.#connection();
|
|
453
|
+
const pool = this.#pool(token);
|
|
454
|
+
const first = pool.next;
|
|
455
|
+
pool.next += BURN_BLOCK;
|
|
456
|
+
const laneKey = hexToBytes(c.laneKey);
|
|
457
|
+
const signal = this.#searches.signal;
|
|
458
|
+
const burns = await Promise.all(
|
|
459
|
+
Array.from({ length: BURN_BLOCK }, (_, i) =>
|
|
460
|
+
burnAddress(laneKey, token.chainId, token.tokenId, first + i, c.publicKey, { signal, urgent }),
|
|
461
|
+
),
|
|
462
|
+
);
|
|
463
|
+
if (this.#state.connection !== c) return;
|
|
464
|
+
pool.made.push({ first, burns });
|
|
465
|
+
this.#saveSoon();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Records made addresses of `token` in the lane, in one write, and readies them: as many blocks as the sales that
|
|
469
|
+
* wait and the low mark ask for. One write at a time per token: a sale that finds none ready waits for the one under
|
|
470
|
+
* way. A pool with none ready has the blocks its waiting sales need, one at least, made side by side and ahead of
|
|
471
|
+
* every search made ahead, since the next sale waits for them. A timer that finds none made waits again: writing
|
|
472
|
+
* right after the making would mark the sale that began it. */
|
|
473
|
+
#record(token: PosToken): Promise<void> {
|
|
474
|
+
const pair = pairOf(token);
|
|
475
|
+
const running = this.#recording.get(pair);
|
|
339
476
|
if (running) return running;
|
|
340
477
|
clearTimeout(this.#timers.get(pair));
|
|
341
478
|
this.#timers.delete(pair);
|
|
342
479
|
const run: Promise<void> = (async () => {
|
|
343
480
|
const c = this.#connection();
|
|
344
|
-
const pool = (
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
Array.from({ length:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
);
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
tokenId: token.tokenId,
|
|
355
|
-
first: k,
|
|
356
|
-
salts: burns.map((b) => hexToBytes(b.salt)),
|
|
357
|
-
};
|
|
481
|
+
const pool = this.#pool(token);
|
|
482
|
+
const waiting = Math.ceil((this.#waiting.get(pair) ?? 0) / BURN_BLOCK);
|
|
483
|
+
if (pool.ready.length === 0) {
|
|
484
|
+
const short = Math.max(waiting, 1) - pool.made.length;
|
|
485
|
+
await Promise.all(Array.from({ length: Math.max(0, short) }, () => this.#make(token, true)));
|
|
486
|
+
} else if (pool.made.length === 0) return this.#later(token);
|
|
487
|
+
if (this.#state.connection !== c) return;
|
|
488
|
+
const short = Math.ceil((this.#low(pair) - pool.ready.length) / BURN_BLOCK);
|
|
489
|
+
const blocks = pool.made.slice(0, Math.min(MAX_POS_BLOCKS, Math.max(waiting, short, 1)));
|
|
490
|
+
if (!blocks.length) return;
|
|
358
491
|
const chain = `${token.chainId}`;
|
|
359
|
-
const slot = await
|
|
360
|
-
|
|
492
|
+
const slot = await writePosBlocks(
|
|
493
|
+
await this.#client(),
|
|
494
|
+
hexToBytes(c.laneKey),
|
|
495
|
+
blocks.map((b) => ({
|
|
496
|
+
chainId: token.chainId,
|
|
497
|
+
tokenId: token.tokenId,
|
|
498
|
+
first: b.first,
|
|
499
|
+
salts: b.burns.map((burn) => hexToBytes(burn.salt)),
|
|
500
|
+
})),
|
|
501
|
+
c.slots[chain] ?? 0,
|
|
502
|
+
);
|
|
503
|
+
if (this.#state.connection !== c) return;
|
|
361
504
|
c.slots[chain] = Math.max(c.slots[chain] ?? 0, slot);
|
|
362
|
-
|
|
363
|
-
pool.
|
|
505
|
+
// what is made is only ever added to, after these
|
|
506
|
+
pool.made.splice(0, blocks.length);
|
|
507
|
+
pool.ready.push(...blocks.flatMap((b) => b.burns.map((burn) => burn.address)));
|
|
364
508
|
await this.#save();
|
|
365
509
|
})().finally(() => {
|
|
366
|
-
if (this.#
|
|
510
|
+
if (this.#recording.get(pair) === run) this.#recording.delete(pair);
|
|
367
511
|
});
|
|
368
|
-
this.#
|
|
512
|
+
this.#recording.set(pair, run);
|
|
513
|
+
// what was made ahead is recorded now: make the next, and record again if still short
|
|
514
|
+
run.then(
|
|
515
|
+
() => this.#supply(token),
|
|
516
|
+
() => undefined,
|
|
517
|
+
);
|
|
369
518
|
return run;
|
|
370
519
|
}
|
|
371
520
|
|
|
521
|
+
/** Records `token`'s addresses at a random time within `refillWithinMs`. */
|
|
522
|
+
#later(token: PosToken): void {
|
|
523
|
+
const pair = pairOf(token);
|
|
524
|
+
const timer = setTimeout(() => {
|
|
525
|
+
this.#timers.delete(pair);
|
|
526
|
+
void this.#record(token).catch(() => undefined);
|
|
527
|
+
}, Math.random() * this.#refillWithinMs);
|
|
528
|
+
(timer as { unref?: () => void }).unref?.(); // a process need not stay up for it
|
|
529
|
+
this.#timers.set(pair, timer);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Stops the searches, timers and writes of the lane connected now. Those under way finish, and drop what they
|
|
533
|
+
* bring, since the lane is no longer the one connected or the point of sale is closed. */
|
|
534
|
+
#stop(): void {
|
|
535
|
+
this.#searches.abort();
|
|
536
|
+
this.#searches = new AbortController();
|
|
537
|
+
for (const t of this.#timers.values()) clearTimeout(t);
|
|
538
|
+
this.#timers.clear();
|
|
539
|
+
this.#making.clear();
|
|
540
|
+
this.#recording.clear();
|
|
541
|
+
}
|
|
542
|
+
|
|
372
543
|
#connection(): Connection {
|
|
373
544
|
return this.#state.connection ?? fail('invalid', "not connected: connect the merchant's wallet first");
|
|
374
545
|
}
|
|
375
546
|
|
|
547
|
+
#pool(token: PosToken): Pool {
|
|
548
|
+
return (this.#connection().pools[pairOf(token)] ??= { next: 0, made: [], ready: [] });
|
|
549
|
+
}
|
|
550
|
+
|
|
376
551
|
#sale(id: Address): Sale {
|
|
377
|
-
return this.#
|
|
552
|
+
return this.#byId.get(id.toLowerCase()) ?? fail('invalid', `no sale ${id}`);
|
|
378
553
|
}
|
|
379
554
|
|
|
380
555
|
#chain(chainId: bigint): Chain {
|
|
@@ -400,12 +575,23 @@ export class PointOfSale {
|
|
|
400
575
|
}
|
|
401
576
|
|
|
402
577
|
/** Saves the state as it is when the save runs, after every save asked for before: an older state never lands last,
|
|
403
|
-
* which would hand out again an address taken since. */
|
|
578
|
+
* which would hand out again an address taken since. Closed, it saves nothing and fails. */
|
|
404
579
|
#save(): Promise<void> {
|
|
580
|
+
if (this.#closed) return Promise.reject(new WalletError('unavailable', 'the point of sale is closed'));
|
|
581
|
+
clearTimeout(this.#passSave);
|
|
582
|
+
this.#passSave = undefined;
|
|
405
583
|
const run = this.#saving.then(() => this.#persist.save(encode(this.#state)));
|
|
406
584
|
this.#saving = run.catch(() => undefined);
|
|
407
585
|
return run;
|
|
408
586
|
}
|
|
587
|
+
|
|
588
|
+
/** Saves within `PASS_SAVED_WITHIN_MS`, for what can be read or made again: a pass's findings, addresses made. */
|
|
589
|
+
// ponytail: the whole state on every save; split it once a merchant has some 10k sales
|
|
590
|
+
#saveSoon(): void {
|
|
591
|
+
if (this.#passSave || this.#closed) return;
|
|
592
|
+
this.#passSave = setTimeout(() => void this.#save().catch(() => undefined), PASS_SAVED_WITHIN_MS);
|
|
593
|
+
(this.#passSave as { unref?: () => void }).unref?.(); // a process need not stay up for it
|
|
594
|
+
}
|
|
409
595
|
}
|
|
410
596
|
|
|
411
597
|
function chainOf(network: Network, chainId: bigint): ChainConfig {
|
package/src/prove.ts
CHANGED
|
@@ -33,6 +33,16 @@ function artifact(url: string, sha: string): Promise<Uint8Array> {
|
|
|
33
33
|
return p;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Fetches and checks `circuit`'s artifacts, and loads snarkjs, ahead of its first proof, which then waits for neither:
|
|
37
|
+
* a proof shares the fetch under way. A failed fetch is tried again by the proof, which reports it. */
|
|
38
|
+
export async function preload(circuit: Circuit): Promise<void> {
|
|
39
|
+
await Promise.all([
|
|
40
|
+
artifact(circuit.wasm, circuit.wasmSha256),
|
|
41
|
+
artifact(circuit.zkey, circuit.zkeySha256),
|
|
42
|
+
import('snarkjs'),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
/** `Verifier.Groth16Proof`, with pB in the order the Solidity verifier takes. */
|
|
37
47
|
export interface Groth16 {
|
|
38
48
|
pA: [bigint, bigint];
|