@unicitylabs/sphere-sdk 0.14.0-dev.1 → 0.14.0-dev.2
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/dist/connect/index.cjs +1 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +1 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +1 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +1 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/wallet-api-v2/index.cjs +4366 -0
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -0
- package/dist/impl/wallet-api-v2/index.d.cts +577 -0
- package/dist/impl/wallet-api-v2/index.d.ts +577 -0
- package/dist/impl/wallet-api-v2/index.js +4327 -0
- package/dist/impl/wallet-api-v2/index.js.map +1 -0
- package/dist/modules/payments-v2/index.cjs +4273 -0
- package/dist/modules/payments-v2/index.cjs.map +1 -0
- package/dist/modules/payments-v2/index.d.cts +1069 -0
- package/dist/modules/payments-v2/index.d.ts +1069 -0
- package/dist/modules/payments-v2/index.js +4231 -0
- package/dist/modules/payments-v2/index.js.map +1 -0
- package/package.json +21 -1
|
@@ -0,0 +1,4231 @@
|
|
|
1
|
+
// modules/payments-v2/stores.ts
|
|
2
|
+
function createScopedKV(storage, network, chainPubkey) {
|
|
3
|
+
const prefix = `pv2:${network}:${chainPubkey}:`;
|
|
4
|
+
return {
|
|
5
|
+
async get(key) {
|
|
6
|
+
const raw = await storage.get(prefix + key);
|
|
7
|
+
if (raw === null || raw === void 0) return null;
|
|
8
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
9
|
+
},
|
|
10
|
+
async set(key, value) {
|
|
11
|
+
await storage.set(prefix + key, JSON.stringify(value));
|
|
12
|
+
},
|
|
13
|
+
async remove(key) {
|
|
14
|
+
await storage.remove(prefix + key);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
var STORE_KEYS = {
|
|
19
|
+
intentBackstop: "intents",
|
|
20
|
+
checkpointCache: "checkpoints",
|
|
21
|
+
deliveryJournal: "delivery-journal",
|
|
22
|
+
mintJournal: "mint-journal",
|
|
23
|
+
shortfalls: "shortfalls",
|
|
24
|
+
settlingLinks: "settling",
|
|
25
|
+
streamCursor: (s) => `cursor:${s}`,
|
|
26
|
+
epochLatch: "epoch-latch"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// node_modules/@noble/hashes/utils.js
|
|
30
|
+
function isBytes(a) {
|
|
31
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
32
|
+
}
|
|
33
|
+
function abytes(value, length, title = "") {
|
|
34
|
+
const bytes = isBytes(value);
|
|
35
|
+
const len = value?.length;
|
|
36
|
+
const needsLen = length !== void 0;
|
|
37
|
+
if (!bytes || needsLen && len !== length) {
|
|
38
|
+
const prefix = title && `"${title}" `;
|
|
39
|
+
const ofLen = needsLen ? ` of length ${length}` : "";
|
|
40
|
+
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
41
|
+
const message = prefix + "expected Uint8Array" + ofLen + ", got " + got;
|
|
42
|
+
if (!bytes)
|
|
43
|
+
throw new TypeError(message);
|
|
44
|
+
throw new RangeError(message);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function aexists(instance, checkFinished = true) {
|
|
49
|
+
if (instance.destroyed)
|
|
50
|
+
throw new Error("Hash instance has been destroyed");
|
|
51
|
+
if (checkFinished && instance.finished)
|
|
52
|
+
throw new Error("Hash#digest() has already been called");
|
|
53
|
+
}
|
|
54
|
+
function aoutput(out, instance) {
|
|
55
|
+
abytes(out, void 0, "digestInto() output");
|
|
56
|
+
const min = instance.outputLen;
|
|
57
|
+
if (out.length < min) {
|
|
58
|
+
throw new RangeError('"digestInto() output" expected to be of length >=' + min);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function clean(...arrays) {
|
|
62
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
63
|
+
arrays[i].fill(0);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function createView(arr) {
|
|
67
|
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
68
|
+
}
|
|
69
|
+
function rotr(word, shift) {
|
|
70
|
+
return word << 32 - shift | word >>> shift;
|
|
71
|
+
}
|
|
72
|
+
function createHasher(hashCons, info = {}) {
|
|
73
|
+
const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
|
|
74
|
+
const tmp = hashCons(void 0);
|
|
75
|
+
hashC.outputLen = tmp.outputLen;
|
|
76
|
+
hashC.blockLen = tmp.blockLen;
|
|
77
|
+
hashC.canXOF = tmp.canXOF;
|
|
78
|
+
hashC.create = (opts) => hashCons(opts);
|
|
79
|
+
Object.assign(hashC, info);
|
|
80
|
+
return Object.freeze(hashC);
|
|
81
|
+
}
|
|
82
|
+
var oidNist = (suffix) => ({
|
|
83
|
+
// Current NIST hashAlgs suffixes used here fit in one DER subidentifier octet.
|
|
84
|
+
// Larger suffix values would need base-128 OID encoding and a different length byte.
|
|
85
|
+
oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// node_modules/@noble/hashes/_md.js
|
|
89
|
+
function Chi(a, b, c) {
|
|
90
|
+
return a & b ^ ~a & c;
|
|
91
|
+
}
|
|
92
|
+
function Maj(a, b, c) {
|
|
93
|
+
return a & b ^ a & c ^ b & c;
|
|
94
|
+
}
|
|
95
|
+
var HashMD = class {
|
|
96
|
+
blockLen;
|
|
97
|
+
outputLen;
|
|
98
|
+
canXOF = false;
|
|
99
|
+
padOffset;
|
|
100
|
+
isLE;
|
|
101
|
+
// For partial updates less than block size
|
|
102
|
+
buffer;
|
|
103
|
+
view;
|
|
104
|
+
finished = false;
|
|
105
|
+
length = 0;
|
|
106
|
+
pos = 0;
|
|
107
|
+
destroyed = false;
|
|
108
|
+
constructor(blockLen, outputLen, padOffset, isLE2) {
|
|
109
|
+
this.blockLen = blockLen;
|
|
110
|
+
this.outputLen = outputLen;
|
|
111
|
+
this.padOffset = padOffset;
|
|
112
|
+
this.isLE = isLE2;
|
|
113
|
+
this.buffer = new Uint8Array(blockLen);
|
|
114
|
+
this.view = createView(this.buffer);
|
|
115
|
+
}
|
|
116
|
+
update(data) {
|
|
117
|
+
aexists(this);
|
|
118
|
+
abytes(data);
|
|
119
|
+
const { view, buffer, blockLen } = this;
|
|
120
|
+
const len = data.length;
|
|
121
|
+
for (let pos = 0; pos < len; ) {
|
|
122
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
123
|
+
if (take === blockLen) {
|
|
124
|
+
const dataView = createView(data);
|
|
125
|
+
for (; blockLen <= len - pos; pos += blockLen)
|
|
126
|
+
this.process(dataView, pos);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
buffer.set(data.subarray(pos, pos + take), this.pos);
|
|
130
|
+
this.pos += take;
|
|
131
|
+
pos += take;
|
|
132
|
+
if (this.pos === blockLen) {
|
|
133
|
+
this.process(view, 0);
|
|
134
|
+
this.pos = 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
this.length += data.length;
|
|
138
|
+
this.roundClean();
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
digestInto(out) {
|
|
142
|
+
aexists(this);
|
|
143
|
+
aoutput(out, this);
|
|
144
|
+
this.finished = true;
|
|
145
|
+
const { buffer, view, blockLen, isLE: isLE2 } = this;
|
|
146
|
+
let { pos } = this;
|
|
147
|
+
buffer[pos++] = 128;
|
|
148
|
+
clean(this.buffer.subarray(pos));
|
|
149
|
+
if (this.padOffset > blockLen - pos) {
|
|
150
|
+
this.process(view, 0);
|
|
151
|
+
pos = 0;
|
|
152
|
+
}
|
|
153
|
+
for (let i = pos; i < blockLen; i++)
|
|
154
|
+
buffer[i] = 0;
|
|
155
|
+
view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE2);
|
|
156
|
+
this.process(view, 0);
|
|
157
|
+
const oview = createView(out);
|
|
158
|
+
const len = this.outputLen;
|
|
159
|
+
if (len % 4)
|
|
160
|
+
throw new Error("_sha2: outputLen must be aligned to 32bit");
|
|
161
|
+
const outLen = len / 4;
|
|
162
|
+
const state = this.get();
|
|
163
|
+
if (outLen > state.length)
|
|
164
|
+
throw new Error("_sha2: outputLen bigger than state");
|
|
165
|
+
for (let i = 0; i < outLen; i++)
|
|
166
|
+
oview.setUint32(4 * i, state[i], isLE2);
|
|
167
|
+
}
|
|
168
|
+
digest() {
|
|
169
|
+
const { buffer, outputLen } = this;
|
|
170
|
+
this.digestInto(buffer);
|
|
171
|
+
const res = buffer.slice(0, outputLen);
|
|
172
|
+
this.destroy();
|
|
173
|
+
return res;
|
|
174
|
+
}
|
|
175
|
+
_cloneInto(to) {
|
|
176
|
+
to ||= new this.constructor();
|
|
177
|
+
to.set(...this.get());
|
|
178
|
+
const { blockLen, buffer, length, finished, destroyed, pos } = this;
|
|
179
|
+
to.destroyed = destroyed;
|
|
180
|
+
to.finished = finished;
|
|
181
|
+
to.length = length;
|
|
182
|
+
to.pos = pos;
|
|
183
|
+
if (length % blockLen)
|
|
184
|
+
to.buffer.set(buffer);
|
|
185
|
+
return to;
|
|
186
|
+
}
|
|
187
|
+
clone() {
|
|
188
|
+
return this._cloneInto();
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var SHA256_IV = /* @__PURE__ */ Uint32Array.from([
|
|
192
|
+
1779033703,
|
|
193
|
+
3144134277,
|
|
194
|
+
1013904242,
|
|
195
|
+
2773480762,
|
|
196
|
+
1359893119,
|
|
197
|
+
2600822924,
|
|
198
|
+
528734635,
|
|
199
|
+
1541459225
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
// node_modules/@noble/hashes/sha2.js
|
|
203
|
+
var SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
204
|
+
1116352408,
|
|
205
|
+
1899447441,
|
|
206
|
+
3049323471,
|
|
207
|
+
3921009573,
|
|
208
|
+
961987163,
|
|
209
|
+
1508970993,
|
|
210
|
+
2453635748,
|
|
211
|
+
2870763221,
|
|
212
|
+
3624381080,
|
|
213
|
+
310598401,
|
|
214
|
+
607225278,
|
|
215
|
+
1426881987,
|
|
216
|
+
1925078388,
|
|
217
|
+
2162078206,
|
|
218
|
+
2614888103,
|
|
219
|
+
3248222580,
|
|
220
|
+
3835390401,
|
|
221
|
+
4022224774,
|
|
222
|
+
264347078,
|
|
223
|
+
604807628,
|
|
224
|
+
770255983,
|
|
225
|
+
1249150122,
|
|
226
|
+
1555081692,
|
|
227
|
+
1996064986,
|
|
228
|
+
2554220882,
|
|
229
|
+
2821834349,
|
|
230
|
+
2952996808,
|
|
231
|
+
3210313671,
|
|
232
|
+
3336571891,
|
|
233
|
+
3584528711,
|
|
234
|
+
113926993,
|
|
235
|
+
338241895,
|
|
236
|
+
666307205,
|
|
237
|
+
773529912,
|
|
238
|
+
1294757372,
|
|
239
|
+
1396182291,
|
|
240
|
+
1695183700,
|
|
241
|
+
1986661051,
|
|
242
|
+
2177026350,
|
|
243
|
+
2456956037,
|
|
244
|
+
2730485921,
|
|
245
|
+
2820302411,
|
|
246
|
+
3259730800,
|
|
247
|
+
3345764771,
|
|
248
|
+
3516065817,
|
|
249
|
+
3600352804,
|
|
250
|
+
4094571909,
|
|
251
|
+
275423344,
|
|
252
|
+
430227734,
|
|
253
|
+
506948616,
|
|
254
|
+
659060556,
|
|
255
|
+
883997877,
|
|
256
|
+
958139571,
|
|
257
|
+
1322822218,
|
|
258
|
+
1537002063,
|
|
259
|
+
1747873779,
|
|
260
|
+
1955562222,
|
|
261
|
+
2024104815,
|
|
262
|
+
2227730452,
|
|
263
|
+
2361852424,
|
|
264
|
+
2428436474,
|
|
265
|
+
2756734187,
|
|
266
|
+
3204031479,
|
|
267
|
+
3329325298
|
|
268
|
+
]);
|
|
269
|
+
var SHA256_W = /* @__PURE__ */ new Uint32Array(64);
|
|
270
|
+
var SHA2_32B = class extends HashMD {
|
|
271
|
+
constructor(outputLen) {
|
|
272
|
+
super(64, outputLen, 8, false);
|
|
273
|
+
}
|
|
274
|
+
get() {
|
|
275
|
+
const { A, B, C, D, E, F, G, H } = this;
|
|
276
|
+
return [A, B, C, D, E, F, G, H];
|
|
277
|
+
}
|
|
278
|
+
// prettier-ignore
|
|
279
|
+
set(A, B, C, D, E, F, G, H) {
|
|
280
|
+
this.A = A | 0;
|
|
281
|
+
this.B = B | 0;
|
|
282
|
+
this.C = C | 0;
|
|
283
|
+
this.D = D | 0;
|
|
284
|
+
this.E = E | 0;
|
|
285
|
+
this.F = F | 0;
|
|
286
|
+
this.G = G | 0;
|
|
287
|
+
this.H = H | 0;
|
|
288
|
+
}
|
|
289
|
+
process(view, offset) {
|
|
290
|
+
for (let i = 0; i < 16; i++, offset += 4)
|
|
291
|
+
SHA256_W[i] = view.getUint32(offset, false);
|
|
292
|
+
for (let i = 16; i < 64; i++) {
|
|
293
|
+
const W15 = SHA256_W[i - 15];
|
|
294
|
+
const W2 = SHA256_W[i - 2];
|
|
295
|
+
const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
|
|
296
|
+
const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
|
|
297
|
+
SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
|
|
298
|
+
}
|
|
299
|
+
let { A, B, C, D, E, F, G, H } = this;
|
|
300
|
+
for (let i = 0; i < 64; i++) {
|
|
301
|
+
const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
|
|
302
|
+
const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
|
|
303
|
+
const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
|
|
304
|
+
const T2 = sigma0 + Maj(A, B, C) | 0;
|
|
305
|
+
H = G;
|
|
306
|
+
G = F;
|
|
307
|
+
F = E;
|
|
308
|
+
E = D + T1 | 0;
|
|
309
|
+
D = C;
|
|
310
|
+
C = B;
|
|
311
|
+
B = A;
|
|
312
|
+
A = T1 + T2 | 0;
|
|
313
|
+
}
|
|
314
|
+
A = A + this.A | 0;
|
|
315
|
+
B = B + this.B | 0;
|
|
316
|
+
C = C + this.C | 0;
|
|
317
|
+
D = D + this.D | 0;
|
|
318
|
+
E = E + this.E | 0;
|
|
319
|
+
F = F + this.F | 0;
|
|
320
|
+
G = G + this.G | 0;
|
|
321
|
+
H = H + this.H | 0;
|
|
322
|
+
this.set(A, B, C, D, E, F, G, H);
|
|
323
|
+
}
|
|
324
|
+
roundClean() {
|
|
325
|
+
clean(SHA256_W);
|
|
326
|
+
}
|
|
327
|
+
destroy() {
|
|
328
|
+
this.destroyed = true;
|
|
329
|
+
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
|
330
|
+
clean(this.buffer);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var _SHA256 = class extends SHA2_32B {
|
|
334
|
+
// We cannot use array here since array allows indexing by variable
|
|
335
|
+
// which means optimizer/compiler cannot use registers.
|
|
336
|
+
A = SHA256_IV[0] | 0;
|
|
337
|
+
B = SHA256_IV[1] | 0;
|
|
338
|
+
C = SHA256_IV[2] | 0;
|
|
339
|
+
D = SHA256_IV[3] | 0;
|
|
340
|
+
E = SHA256_IV[4] | 0;
|
|
341
|
+
F = SHA256_IV[5] | 0;
|
|
342
|
+
G = SHA256_IV[6] | 0;
|
|
343
|
+
H = SHA256_IV[7] | 0;
|
|
344
|
+
constructor() {
|
|
345
|
+
super(32);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
var sha256 = /* @__PURE__ */ createHasher(
|
|
349
|
+
() => new _SHA256(),
|
|
350
|
+
/* @__PURE__ */ oidNist(1)
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// core/crypto.ts
|
|
354
|
+
import * as bip39 from "bip39";
|
|
355
|
+
import CryptoJS from "crypto-js";
|
|
356
|
+
|
|
357
|
+
// core/errors.ts
|
|
358
|
+
var SphereError = class extends Error {
|
|
359
|
+
code;
|
|
360
|
+
cause;
|
|
361
|
+
/**
|
|
362
|
+
* #441 deferred-paid linkage: the transferId of the possibly-committed send,
|
|
363
|
+
* stamped by `sendOnce` before the error leaves so a payment-request consumer
|
|
364
|
+
* can durably journal request→transfer and resolve 'paid' only when that
|
|
365
|
+
* transfer actually completes (never optimistically). Present ONLY on
|
|
366
|
+
* possibly-committed outcomes ({@link isPossiblyCommittedSendOutcome}).
|
|
367
|
+
*/
|
|
368
|
+
transferId;
|
|
369
|
+
constructor(message, code, cause) {
|
|
370
|
+
super(message);
|
|
371
|
+
this.name = "SphereError";
|
|
372
|
+
this.code = code;
|
|
373
|
+
this.cause = cause;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
var PartialSendConflictError = class extends SphereError {
|
|
377
|
+
/**
|
|
378
|
+
* The FIRST partial attempt's transferId — its intent stays open for resume,
|
|
379
|
+
* and it is the §6 handoff anchor. NOTE: `send()` may re-plan the remainder across
|
|
380
|
+
* SEVERAL internal attempts before giving up, so {@link committedTokenIds} can
|
|
381
|
+
* span MULTIPLE transferIds — they do NOT all map to this single id.
|
|
382
|
+
*/
|
|
383
|
+
transferId;
|
|
384
|
+
/**
|
|
385
|
+
* Source token ids whose spend already certified on-chain (and whose finished
|
|
386
|
+
* output blob is journaled for delivery). Their value has already been
|
|
387
|
+
* delivered; NEVER re-send these. May accumulate across several internal
|
|
388
|
+
* remainder-re-plan attempts (so not all are journaled under {@link transferId}).
|
|
389
|
+
*/
|
|
390
|
+
committedTokenIds;
|
|
391
|
+
/**
|
|
392
|
+
* The still-undelivered portion (base units, decimal string) — `send()` could
|
|
393
|
+
* not COMPLETE it from the remaining live sources (insufficient funds or a
|
|
394
|
+
* transient error; see `cause`). Re-plan ONLY this amount, under a new
|
|
395
|
+
* transferId; the delivered legs converge via the recipient's §6 claim.
|
|
396
|
+
*/
|
|
397
|
+
remainingAmount;
|
|
398
|
+
constructor(message, transferId, committedTokenIds, remainingAmount, cause) {
|
|
399
|
+
super(message, "SEND_PARTIALLY_COMPLETED", cause);
|
|
400
|
+
this.name = "PartialSendConflictError";
|
|
401
|
+
this.transferId = transferId;
|
|
402
|
+
this.committedTokenIds = [...committedTokenIds];
|
|
403
|
+
this.remainingAmount = remainingAmount;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
var POSSIBLY_COMMITTED_SEND_CODES = /* @__PURE__ */ new Set([
|
|
407
|
+
"SEND_SYNC_PENDING",
|
|
408
|
+
"CERTIFICATION_UNCONFIRMED",
|
|
409
|
+
"CHECKPOINT_PERSIST_FAILED",
|
|
410
|
+
"SPLIT_CHECKPOINT_LOST",
|
|
411
|
+
"CHECKPOINT_TRUSTBASE_MISMATCH",
|
|
412
|
+
"SEND_PARTIALLY_COMPLETED"
|
|
413
|
+
]);
|
|
414
|
+
function isPossiblyCommittedSendOutcome(err) {
|
|
415
|
+
return isSphereError(err) && POSSIBLY_COMMITTED_SEND_CODES.has(err.code);
|
|
416
|
+
}
|
|
417
|
+
function isSphereError(err) {
|
|
418
|
+
return err instanceof SphereError;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// core/crypto.ts
|
|
422
|
+
var CURVE_ORDER = BigInt(
|
|
423
|
+
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"
|
|
424
|
+
);
|
|
425
|
+
function hexToBytes(hex) {
|
|
426
|
+
const matches = hex.match(/../g);
|
|
427
|
+
if (!matches) {
|
|
428
|
+
return new Uint8Array(0);
|
|
429
|
+
}
|
|
430
|
+
return Uint8Array.from(matches.map((x) => parseInt(x, 16)));
|
|
431
|
+
}
|
|
432
|
+
function bytesToHex(bytes) {
|
|
433
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// core/uuid.ts
|
|
437
|
+
function randomUUID() {
|
|
438
|
+
const c = globalThis.crypto;
|
|
439
|
+
if (typeof c !== "undefined" && typeof c.randomUUID === "function") {
|
|
440
|
+
return c.randomUUID();
|
|
441
|
+
}
|
|
442
|
+
const b = c.getRandomValues(new Uint8Array(16));
|
|
443
|
+
b[6] = b[6] & 15 | 64;
|
|
444
|
+
b[8] = b[8] & 63 | 128;
|
|
445
|
+
const h = Array.from(b, (x) => x.toString(16).padStart(2, "0"));
|
|
446
|
+
return `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// token-engine/sdk.ts
|
|
450
|
+
import { StateTransitionClient } from "@unicitylabs/state-transition-sdk/lib/StateTransitionClient.js";
|
|
451
|
+
import { AggregatorClient } from "@unicitylabs/state-transition-sdk/lib/api/AggregatorClient.js";
|
|
452
|
+
import { NetworkId } from "@unicitylabs/state-transition-sdk/lib/api/NetworkId.js";
|
|
453
|
+
import { CertificationData } from "@unicitylabs/state-transition-sdk/lib/api/CertificationData.js";
|
|
454
|
+
import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-transition-sdk/lib/api/CertificationResponse.js";
|
|
455
|
+
import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
|
|
456
|
+
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
|
|
457
|
+
import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
|
|
458
|
+
import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
|
|
459
|
+
import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
|
|
460
|
+
import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
|
|
461
|
+
import { Token } from "@unicitylabs/state-transition-sdk/lib/transaction/Token.js";
|
|
462
|
+
import { MintTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransaction.js";
|
|
463
|
+
import { TransferTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction.js";
|
|
464
|
+
import { CertifiedMintTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/CertifiedMintTransaction.js";
|
|
465
|
+
import { CertifiedTransferTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/CertifiedTransferTransaction.js";
|
|
466
|
+
import { TokenId } from "@unicitylabs/state-transition-sdk/lib/transaction/TokenId.js";
|
|
467
|
+
import { TokenType } from "@unicitylabs/state-transition-sdk/lib/transaction/TokenType.js";
|
|
468
|
+
import { TokenSalt } from "@unicitylabs/state-transition-sdk/lib/transaction/TokenSalt.js";
|
|
469
|
+
import { StateMask } from "@unicitylabs/state-transition-sdk/lib/transaction/StateMask.js";
|
|
470
|
+
import { MintJustificationVerifierService } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/MintJustificationVerifierService.js";
|
|
471
|
+
import { TokenIssuanceVerifierService } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/TokenIssuanceVerifierService.js";
|
|
472
|
+
import { VerificationContext } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/VerificationContext.js";
|
|
473
|
+
import { WorkerTokenVerifier } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/worker/WorkerTokenVerifier.js";
|
|
474
|
+
import { EncodedPredicate } from "@unicitylabs/state-transition-sdk/lib/predicate/EncodedPredicate.js";
|
|
475
|
+
import { SignaturePredicate } from "@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicate.js";
|
|
476
|
+
import { SignaturePredicateUnlockScript } from "@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicateUnlockScript.js";
|
|
477
|
+
import { BurnPredicate } from "@unicitylabs/state-transition-sdk/lib/predicate/builtin/BurnPredicate.js";
|
|
478
|
+
import { PredicateVerifierService } from "@unicitylabs/state-transition-sdk/lib/predicate/verification/PredicateVerifierService.js";
|
|
479
|
+
import { SigningService } from "@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/SigningService.js";
|
|
480
|
+
import { Signature } from "@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/Signature.js";
|
|
481
|
+
import { MintSigningService } from "@unicitylabs/state-transition-sdk/lib/crypto/MintSigningService.js";
|
|
482
|
+
import { HashAlgorithm } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/HashAlgorithm.js";
|
|
483
|
+
import { DataHash } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHash.js";
|
|
484
|
+
import { DataHasher } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasher.js";
|
|
485
|
+
import { DataHasherFactory } from "@unicitylabs/state-transition-sdk/lib/crypto/hash/DataHasherFactory.js";
|
|
486
|
+
import { CborSerializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js";
|
|
487
|
+
import { CborDeserializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js";
|
|
488
|
+
import { CborError } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js";
|
|
489
|
+
import { Asset } from "@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js";
|
|
490
|
+
import { AssetId } from "@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js";
|
|
491
|
+
import { PaymentAssetCollection } from "@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js";
|
|
492
|
+
import { TokenSplit } from "@unicitylabs/state-transition-sdk/lib/payment/TokenSplit.js";
|
|
493
|
+
import { SplitTokenRequest } from "@unicitylabs/state-transition-sdk/lib/payment/SplitTokenRequest.js";
|
|
494
|
+
import { SplitToken } from "@unicitylabs/state-transition-sdk/lib/payment/SplitToken.js";
|
|
495
|
+
import { SplitAllocationProof } from "@unicitylabs/state-transition-sdk/lib/payment/SplitAllocationProof.js";
|
|
496
|
+
import { SplitMintJustification } from "@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustification.js";
|
|
497
|
+
import { SplitMintJustificationVerifier } from "@unicitylabs/state-transition-sdk/lib/payment/SplitMintJustificationVerifier.js";
|
|
498
|
+
import { VerificationStatus } from "@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js";
|
|
499
|
+
import { VerificationResult } from "@unicitylabs/state-transition-sdk/lib/verification/VerificationResult.js";
|
|
500
|
+
import { HexConverter } from "@unicitylabs/state-transition-sdk/lib/util/HexConverter.js";
|
|
501
|
+
import { BigintConverter } from "@unicitylabs/state-transition-sdk/lib/util/BigintConverter.js";
|
|
502
|
+
import { BitString } from "@unicitylabs/state-transition-sdk/lib/util/BitString.js";
|
|
503
|
+
|
|
504
|
+
// token-engine/token-blob.ts
|
|
505
|
+
var TOKEN_BLOB_VERSION = 1;
|
|
506
|
+
|
|
507
|
+
// modules/payments-v2/machine/journal.ts
|
|
508
|
+
var MAX_DELIVERY_ATTEMPTS = 6;
|
|
509
|
+
var RATE_LIMIT_DEFER_MS = 3e4;
|
|
510
|
+
var ATTENTION_DELIVERY_DEFERRED = "delivery:deferred";
|
|
511
|
+
var ATTENTION_DELIVERY_UNDELIVERABLE = "delivery:undeliverable";
|
|
512
|
+
var ATTENTION_CHECKPOINT_STUCK = "split:checkpoint-stuck";
|
|
513
|
+
var ATTENTION_SYNC_PENDING = "sync:pending";
|
|
514
|
+
function wireShape(err) {
|
|
515
|
+
return err !== null && typeof err === "object" ? err : null;
|
|
516
|
+
}
|
|
517
|
+
function isRateLimited(err) {
|
|
518
|
+
const e = wireShape(err);
|
|
519
|
+
if (e === null) return false;
|
|
520
|
+
return e.status === 429 || e.statusCode === 429 || e.code === "QUOTA_EXCEEDED" || e.code === "RATE_LIMITED";
|
|
521
|
+
}
|
|
522
|
+
function isValidationReject(err) {
|
|
523
|
+
const e = wireShape(err);
|
|
524
|
+
if (e === null) return false;
|
|
525
|
+
return e.status === 422 || e.statusCode === 422 || e.code === "VALIDATION" || e.code === "VALIDATION_FAILED";
|
|
526
|
+
}
|
|
527
|
+
var chainRegistry = /* @__PURE__ */ new WeakMap();
|
|
528
|
+
var replayRegistry = /* @__PURE__ */ new WeakMap();
|
|
529
|
+
function registryFor(registry, kv) {
|
|
530
|
+
let perKv = registry.get(kv);
|
|
531
|
+
if (perKv === void 0) {
|
|
532
|
+
perKv = /* @__PURE__ */ new Map();
|
|
533
|
+
registry.set(kv, perKv);
|
|
534
|
+
}
|
|
535
|
+
return perKv;
|
|
536
|
+
}
|
|
537
|
+
var ListStore = class {
|
|
538
|
+
constructor(kv, storeKey, keyOf) {
|
|
539
|
+
this.kv = kv;
|
|
540
|
+
this.storeKey = storeKey;
|
|
541
|
+
this.keyOf = keyOf;
|
|
542
|
+
}
|
|
543
|
+
list() {
|
|
544
|
+
return this.chained(() => this.read());
|
|
545
|
+
}
|
|
546
|
+
getByKey(key) {
|
|
547
|
+
return this.chained(async () => (await this.read()).find((e) => this.keyOf(e) === key));
|
|
548
|
+
}
|
|
549
|
+
/** Upsert; a byte-identical re-save is a no-op (content-idempotent). */
|
|
550
|
+
upsert(entry) {
|
|
551
|
+
return this.mutate((list) => {
|
|
552
|
+
const key = this.keyOf(entry);
|
|
553
|
+
const at = list.findIndex((e) => this.keyOf(e) === key);
|
|
554
|
+
if (at >= 0 && JSON.stringify(list[at]) === JSON.stringify(entry)) return { result: void 0 };
|
|
555
|
+
const next = [...list];
|
|
556
|
+
if (at >= 0) next[at] = entry;
|
|
557
|
+
else next.push(entry);
|
|
558
|
+
return { list: next, result: void 0 };
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
removeByKey(key) {
|
|
562
|
+
return this.mutate((list) => {
|
|
563
|
+
const next = list.filter((e) => this.keyOf(e) !== key);
|
|
564
|
+
return next.length === list.length ? { result: void 0 } : { list: next, result: void 0 };
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
patch(key, fn) {
|
|
568
|
+
return this.mutate((list) => {
|
|
569
|
+
const at = list.findIndex((e) => this.keyOf(e) === key);
|
|
570
|
+
if (at < 0) return { result: void 0 };
|
|
571
|
+
const next = [...list];
|
|
572
|
+
next[at] = fn(next[at]);
|
|
573
|
+
return { list: next, result: next[at] };
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
mutate(fn) {
|
|
577
|
+
return this.chained(async () => {
|
|
578
|
+
const { list, result } = fn(await this.read());
|
|
579
|
+
if (list !== void 0) await this.kv.set(this.storeKey, list);
|
|
580
|
+
return result;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
chained(run) {
|
|
584
|
+
const perKv = registryFor(chainRegistry, this.kv);
|
|
585
|
+
const prev = perKv.get(this.storeKey) ?? Promise.resolve();
|
|
586
|
+
const next = prev.then(run, run);
|
|
587
|
+
perKv.set(
|
|
588
|
+
this.storeKey,
|
|
589
|
+
next.then(
|
|
590
|
+
() => void 0,
|
|
591
|
+
() => void 0
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
return next;
|
|
595
|
+
}
|
|
596
|
+
async read() {
|
|
597
|
+
return await this.kv.get(this.storeKey) ?? [];
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
var BackstopStore = class extends ListStore {
|
|
601
|
+
constructor(kv) {
|
|
602
|
+
super(kv, STORE_KEYS.intentBackstop, (e) => e.transferId);
|
|
603
|
+
}
|
|
604
|
+
save(entry) {
|
|
605
|
+
return this.upsert(entry);
|
|
606
|
+
}
|
|
607
|
+
async setDisposition(transferId, disposition) {
|
|
608
|
+
await this.patch(transferId, (e) => ({ ...e, disposition }));
|
|
609
|
+
}
|
|
610
|
+
drop(transferId) {
|
|
611
|
+
return this.removeByKey(transferId);
|
|
612
|
+
}
|
|
613
|
+
get(transferId) {
|
|
614
|
+
return this.getByKey(transferId);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
var DeliveryJournal = class extends ListStore {
|
|
618
|
+
constructor(kv) {
|
|
619
|
+
super(kv, STORE_KEYS.deliveryJournal, (e) => `${e.transferId}:${String(e.opIndex)}`);
|
|
620
|
+
}
|
|
621
|
+
record(entry) {
|
|
622
|
+
return this.upsert(entry);
|
|
623
|
+
}
|
|
624
|
+
async byTransfer(transferId) {
|
|
625
|
+
const mine = (await this.list()).filter((e) => e.transferId === transferId);
|
|
626
|
+
return new Map(mine.map((e) => [e.opIndex, e]));
|
|
627
|
+
}
|
|
628
|
+
remove(transferId, opIndex) {
|
|
629
|
+
return this.removeByKey(`${transferId}:${String(opIndex)}`);
|
|
630
|
+
}
|
|
631
|
+
/** 429 deferral: pushes deferredUntil out WITHOUT touching the poison budget. */
|
|
632
|
+
async defer(transferId, opIndex, until) {
|
|
633
|
+
await this.patch(`${transferId}:${String(opIndex)}`, (e) => ({ ...e, deferredUntil: until }));
|
|
634
|
+
}
|
|
635
|
+
/** Genuine failure: attempts++, poisoning (undeliverable) at the budget. */
|
|
636
|
+
async recordFailure(transferId, opIndex, attention) {
|
|
637
|
+
const updated = await this.patch(`${transferId}:${String(opIndex)}`, (e) => {
|
|
638
|
+
if (e.undeliverable === true) return e;
|
|
639
|
+
const attempts = e.attempts + 1;
|
|
640
|
+
return attempts >= MAX_DELIVERY_ATTEMPTS ? { ...e, attempts, undeliverable: true } : { ...e, attempts };
|
|
641
|
+
});
|
|
642
|
+
const poisoned = updated?.undeliverable === true;
|
|
643
|
+
if (poisoned && updated.attempts === MAX_DELIVERY_ATTEMPTS) {
|
|
644
|
+
attention(transferId, ATTENTION_DELIVERY_UNDELIVERABLE, `op ${String(opIndex)} after ${String(updated.attempts)} attempts`);
|
|
645
|
+
}
|
|
646
|
+
return poisoned;
|
|
647
|
+
}
|
|
648
|
+
/** Single-flight per (kv, store); concurrent calls coalesce onto one pass. */
|
|
649
|
+
replay(deps) {
|
|
650
|
+
const perKv = registryFor(replayRegistry, this.kv);
|
|
651
|
+
const existing = perKv.get(this.storeKey);
|
|
652
|
+
if (existing !== void 0) return existing;
|
|
653
|
+
const run = this.replayOnce(deps).finally(() => {
|
|
654
|
+
perKv.delete(this.storeKey);
|
|
655
|
+
});
|
|
656
|
+
perKv.set(this.storeKey, run);
|
|
657
|
+
return run;
|
|
658
|
+
}
|
|
659
|
+
async replayOnce(deps) {
|
|
660
|
+
const stats = { delivered: 0, deferred: 0, failed: 0, poisoned: 0 };
|
|
661
|
+
for (const entry of await this.list()) {
|
|
662
|
+
if (entry.undeliverable === true) continue;
|
|
663
|
+
if (entry.deferredUntil !== void 0 && entry.deferredUntil > deps.now()) {
|
|
664
|
+
stats.deferred += 1;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
await this.replayEntry(entry, deps, stats);
|
|
668
|
+
}
|
|
669
|
+
return stats;
|
|
670
|
+
}
|
|
671
|
+
async replayEntry(entry, deps, stats) {
|
|
672
|
+
try {
|
|
673
|
+
await deps.delivery.deliver(entry.recipientPubkey, hexToBytes(entry.blobHex), {
|
|
674
|
+
transferId: entry.transferId,
|
|
675
|
+
...entry.memo !== void 0 ? { memo: entry.memo } : {}
|
|
676
|
+
});
|
|
677
|
+
await this.remove(entry.transferId, entry.opIndex);
|
|
678
|
+
stats.delivered += 1;
|
|
679
|
+
} catch (err) {
|
|
680
|
+
if (isRateLimited(err)) {
|
|
681
|
+
await this.defer(entry.transferId, entry.opIndex, deps.now() + RATE_LIMIT_DEFER_MS);
|
|
682
|
+
deps.attention(entry.transferId, ATTENTION_DELIVERY_DEFERRED);
|
|
683
|
+
stats.deferred += 1;
|
|
684
|
+
} else if (await this.recordFailure(entry.transferId, entry.opIndex, deps.attention)) {
|
|
685
|
+
stats.poisoned += 1;
|
|
686
|
+
} else {
|
|
687
|
+
stats.failed += 1;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
var MintJournal = class extends ListStore {
|
|
693
|
+
constructor(kv) {
|
|
694
|
+
super(kv, STORE_KEYS.mintJournal, (e) => e.mintId);
|
|
695
|
+
}
|
|
696
|
+
record(entry) {
|
|
697
|
+
return this.upsert(entry);
|
|
698
|
+
}
|
|
699
|
+
remove(mintId) {
|
|
700
|
+
return this.removeByKey(mintId);
|
|
701
|
+
}
|
|
702
|
+
get(mintId) {
|
|
703
|
+
return this.getByKey(mintId);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
var ShortfallStore = class extends ListStore {
|
|
707
|
+
constructor(kv) {
|
|
708
|
+
super(kv, STORE_KEYS.shortfalls, (e) => e.transferId);
|
|
709
|
+
}
|
|
710
|
+
save(entry) {
|
|
711
|
+
return this.upsert(entry);
|
|
712
|
+
}
|
|
713
|
+
clear(transferId) {
|
|
714
|
+
return this.removeByKey(transferId);
|
|
715
|
+
}
|
|
716
|
+
get(transferId) {
|
|
717
|
+
return this.getByKey(transferId);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
function createMachineStores(kv) {
|
|
721
|
+
return {
|
|
722
|
+
backstop: new BackstopStore(kv),
|
|
723
|
+
deliveryJournal: new DeliveryJournal(kv),
|
|
724
|
+
mintJournal: new MintJournal(kv),
|
|
725
|
+
shortfalls: new ShortfallStore(kv)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// token-engine/errors.ts
|
|
730
|
+
var TransferConflictError = class extends SphereError {
|
|
731
|
+
/**
|
|
732
|
+
* #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
|
|
733
|
+
* Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
|
|
734
|
+
*/
|
|
735
|
+
conflictedSourceId;
|
|
736
|
+
constructor(message, cause) {
|
|
737
|
+
super(message, "TRANSFER_CONFLICT", cause);
|
|
738
|
+
this.name = "TransferConflictError";
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
var ProofUnconfirmedError = class extends SphereError {
|
|
742
|
+
mayHaveCertified = true;
|
|
743
|
+
constructor(message, cause) {
|
|
744
|
+
super(message, "CERTIFICATION_UNCONFIRMED", cause);
|
|
745
|
+
this.name = "ProofUnconfirmedError";
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
var CheckpointPersistFailedError = class extends SphereError {
|
|
749
|
+
mayHaveCertified = true;
|
|
750
|
+
constructor(message, cause) {
|
|
751
|
+
super(message, "CHECKPOINT_PERSIST_FAILED", cause);
|
|
752
|
+
this.name = "CheckpointPersistFailedError";
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
var SplitCheckpointLostError = class extends SphereError {
|
|
756
|
+
constructor(message, cause) {
|
|
757
|
+
super(message, "SPLIT_CHECKPOINT_LOST", cause);
|
|
758
|
+
this.name = "SplitCheckpointLostError";
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
var CheckpointTrustbaseMismatchError = class extends SphereError {
|
|
762
|
+
constructor(message, cause) {
|
|
763
|
+
super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
|
|
764
|
+
this.name = "CheckpointTrustbaseMismatchError";
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// modules/payments-v2/machine/TransferMachine.ts
|
|
769
|
+
var CERTIFY_WIDTH = 8;
|
|
770
|
+
var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
|
|
771
|
+
"CERTIFICATION_UNCONFIRMED",
|
|
772
|
+
"CHECKPOINT_PERSIST_FAILED",
|
|
773
|
+
"SPLIT_CHECKPOINT_LOST",
|
|
774
|
+
"CHECKPOINT_TRUSTBASE_MISMATCH"
|
|
775
|
+
]);
|
|
776
|
+
function buildOps(payload) {
|
|
777
|
+
const ops = payload.direct.map((sourceTokenId, opIndex) => ({
|
|
778
|
+
kind: "direct",
|
|
779
|
+
opIndex,
|
|
780
|
+
sourceTokenId,
|
|
781
|
+
deliveredAmount: 0n
|
|
782
|
+
}));
|
|
783
|
+
if (payload.split !== void 0) {
|
|
784
|
+
ops.push({
|
|
785
|
+
kind: "split",
|
|
786
|
+
opIndex: payload.direct.length,
|
|
787
|
+
sourceTokenId: payload.split.tokenId,
|
|
788
|
+
deliveredAmount: BigInt(payload.split.splitAmount)
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return ops;
|
|
792
|
+
}
|
|
793
|
+
function classifyError(e) {
|
|
794
|
+
if (e instanceof ProofUnconfirmedError || e instanceof SplitCheckpointLostError || e instanceof CheckpointPersistFailedError || e instanceof CheckpointTrustbaseMismatchError) {
|
|
795
|
+
return "keep-open";
|
|
796
|
+
}
|
|
797
|
+
if (e instanceof TransferConflictError) return "conflict";
|
|
798
|
+
const coded = e !== null && typeof e === "object" ? e : null;
|
|
799
|
+
if (coded !== null) {
|
|
800
|
+
if (coded.mayHaveCertified === true) return "keep-open";
|
|
801
|
+
if (typeof coded.code === "string" && KEEP_OPEN_CODES.has(coded.code)) return "keep-open";
|
|
802
|
+
if (coded.code === "TRANSFER_CONFLICT") return "conflict";
|
|
803
|
+
}
|
|
804
|
+
return "other";
|
|
805
|
+
}
|
|
806
|
+
function summarize(outcomes) {
|
|
807
|
+
const committed = outcomes.filter((o) => o.certified);
|
|
808
|
+
const rank = { "keep-open": 3, conflict: 2, "clean-reject": 1, other: 1 };
|
|
809
|
+
let cls = null;
|
|
810
|
+
let firstError;
|
|
811
|
+
for (const o of outcomes) {
|
|
812
|
+
if (o.error === void 0) continue;
|
|
813
|
+
const c = classifyError(o.error);
|
|
814
|
+
if (cls === null || rank[c] > rank[cls]) {
|
|
815
|
+
cls = c;
|
|
816
|
+
firstError = o.error;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return { committed, cls, firstError };
|
|
820
|
+
}
|
|
821
|
+
var TransferMachine = class {
|
|
822
|
+
constructor(deps) {
|
|
823
|
+
this.deps = deps;
|
|
824
|
+
this.stores = createMachineStores(deps.kv);
|
|
825
|
+
}
|
|
826
|
+
stores;
|
|
827
|
+
// ── send path ─────────────────────────────────────────────────────────────
|
|
828
|
+
async run(plan) {
|
|
829
|
+
const engine = this.deps.engine();
|
|
830
|
+
const envelope = await this.deps.encryptPayload(plan.payload);
|
|
831
|
+
await this.stores.backstop.save({
|
|
832
|
+
transferId: plan.transferId,
|
|
833
|
+
payloadEnvelope: envelope,
|
|
834
|
+
requiresSeedClose: plan.payload.split !== void 0,
|
|
835
|
+
disposition: "open",
|
|
836
|
+
createdAt: this.deps.now()
|
|
837
|
+
});
|
|
838
|
+
await this.putIntentGate(plan, envelope);
|
|
839
|
+
const outcomes = await this.certifyAll(plan, engine);
|
|
840
|
+
const { committed, cls, firstError } = summarize(outcomes);
|
|
841
|
+
let deliveryPending = false;
|
|
842
|
+
if (committed.length > 0) {
|
|
843
|
+
deliveryPending = await this.deliverSet(plan.transferId, plan.recipientPubkey, plan.memo, committed);
|
|
844
|
+
}
|
|
845
|
+
if (cls === null) {
|
|
846
|
+
await this.applyCommitted(engine, plan.transferId, committed);
|
|
847
|
+
this.closeTail(plan.transferId, plan.payload);
|
|
848
|
+
return { committed, deliveryPending };
|
|
849
|
+
}
|
|
850
|
+
if (cls === "conflict" && committed.length > 0) {
|
|
851
|
+
await this.applyBestEffort(engine, plan.transferId, committed);
|
|
852
|
+
}
|
|
853
|
+
await this.disposeOnFailure(plan.transferId, committed, cls);
|
|
854
|
+
throw firstError;
|
|
855
|
+
}
|
|
856
|
+
// ── resume path: the SAME machine, rehydrated from an open intent ─────────
|
|
857
|
+
async resumeIntent(r) {
|
|
858
|
+
const engine = this.deps.engine();
|
|
859
|
+
const ctx = {
|
|
860
|
+
transferId: r.transferId,
|
|
861
|
+
recipientPubkey: r.payload.recipient,
|
|
862
|
+
payload: r.payload,
|
|
863
|
+
...r.payload.memo !== void 0 ? { memo: r.payload.memo } : {}
|
|
864
|
+
};
|
|
865
|
+
const { committed, conflicts } = await this.resumeOps(ctx, engine, r);
|
|
866
|
+
if (committed.length === 0 && conflicts.length > 0) throw conflicts[0].error;
|
|
867
|
+
let deliveryPending = false;
|
|
868
|
+
if (committed.length > 0) {
|
|
869
|
+
deliveryPending = await this.deliverSet(r.transferId, r.payload.recipient, r.payload.memo, committed);
|
|
870
|
+
}
|
|
871
|
+
await this.applyCommitted(engine, r.transferId, committed);
|
|
872
|
+
if (conflicts.length > 0) {
|
|
873
|
+
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
874
|
+
}
|
|
875
|
+
await this.completeIntent(r.transferId, r.payload);
|
|
876
|
+
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
877
|
+
return { deliveryPending, partial: null };
|
|
878
|
+
}
|
|
879
|
+
// ── shared transitions ─────────────────────────────────────────────────────
|
|
880
|
+
async putIntentGate(plan, envelope) {
|
|
881
|
+
try {
|
|
882
|
+
await this.deps.intents.put(plan.transferId, envelope, plan.payload.split !== void 0);
|
|
883
|
+
} catch (err) {
|
|
884
|
+
if (isValidationReject(err)) await this.stores.backstop.drop(plan.transferId);
|
|
885
|
+
throw err;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
async certifyAll(plan, engine) {
|
|
889
|
+
const ctx = {
|
|
890
|
+
transferId: plan.transferId,
|
|
891
|
+
recipientPubkey: plan.recipientPubkey,
|
|
892
|
+
payload: plan.payload,
|
|
893
|
+
...plan.memo !== void 0 ? { memo: plan.memo } : {}
|
|
894
|
+
};
|
|
895
|
+
const outcomes = [];
|
|
896
|
+
for (let at = 0; at < plan.ops.length; at += CERTIFY_WIDTH) {
|
|
897
|
+
const batch = plan.ops.slice(at, at + CERTIFY_WIDTH);
|
|
898
|
+
const settled = await Promise.all(
|
|
899
|
+
batch.map((op) => this.certifyOne(ctx, engine, op, plan.sources.get(op.sourceTokenId)))
|
|
900
|
+
);
|
|
901
|
+
outcomes.push(...settled);
|
|
902
|
+
if (settled.some((o) => o.error !== void 0 && !o.certified)) break;
|
|
903
|
+
}
|
|
904
|
+
return outcomes;
|
|
905
|
+
}
|
|
906
|
+
async certifyOne(ctx, engine, op, source) {
|
|
907
|
+
if (source === void 0) {
|
|
908
|
+
return {
|
|
909
|
+
op,
|
|
910
|
+
certified: false,
|
|
911
|
+
error: new SphereError(`source ${op.sourceTokenId} is not materialized`, "VALIDATION_ERROR")
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
let produced;
|
|
915
|
+
try {
|
|
916
|
+
produced = await this.certifyOnChain(ctx, engine, op, source);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
return { op, certified: false, error };
|
|
919
|
+
}
|
|
920
|
+
const outcome = { op, certified: true, ...produced };
|
|
921
|
+
try {
|
|
922
|
+
await this.stores.deliveryJournal.record({
|
|
923
|
+
transferId: ctx.transferId,
|
|
924
|
+
opIndex: op.opIndex,
|
|
925
|
+
recipientPubkey: ctx.recipientPubkey,
|
|
926
|
+
blobHex: bytesToHex(produced.recipientBlob),
|
|
927
|
+
...ctx.memo !== void 0 ? { memo: ctx.memo } : {},
|
|
928
|
+
attempts: 0
|
|
929
|
+
});
|
|
930
|
+
return outcome;
|
|
931
|
+
} catch (error) {
|
|
932
|
+
return { ...outcome, error };
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async certifyOnChain(ctx, engine, op, source) {
|
|
936
|
+
const recipientPubkey = hexToBytes(ctx.recipientPubkey);
|
|
937
|
+
const opts = { transferId: ctx.transferId, opIndex: op.opIndex, checkpointStore: this.deps.checkpointStore };
|
|
938
|
+
if (op.kind === "direct") {
|
|
939
|
+
const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
|
|
940
|
+
return { recipientBlob: finished.blob.token };
|
|
941
|
+
}
|
|
942
|
+
const split = ctx.payload.split;
|
|
943
|
+
const { outputs } = await engine.split(
|
|
944
|
+
{
|
|
945
|
+
token: source,
|
|
946
|
+
outputs: [
|
|
947
|
+
{ recipientPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.splitAmount) },
|
|
948
|
+
{ recipientPubkey: this.deps.ownPubkey, coinId: ctx.payload.coinId, amount: BigInt(split.remainderAmount) }
|
|
949
|
+
]
|
|
950
|
+
},
|
|
951
|
+
opts
|
|
952
|
+
);
|
|
953
|
+
return { recipientBlob: outputs[0].blob.token, changeBlob: outputs[1].blob.token };
|
|
954
|
+
}
|
|
955
|
+
async resumeOps(ctx, engine, r) {
|
|
956
|
+
const committed = [];
|
|
957
|
+
const conflicts = [];
|
|
958
|
+
for (const op of buildOps(r.payload)) {
|
|
959
|
+
const journaled = op.kind === "direct" ? r.journaled.get(op.opIndex) : void 0;
|
|
960
|
+
if (journaled !== void 0) {
|
|
961
|
+
committed.push({ op, certified: true, recipientBlob: hexToBytes(journaled.blobHex) });
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const source = r.sources.get(op.sourceTokenId);
|
|
965
|
+
const outcome = await this.certifyOne(ctx, engine, op, source);
|
|
966
|
+
if (outcome.certified) {
|
|
967
|
+
committed.push(outcome);
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (classifyError(outcome.error) !== "conflict") throw outcome.error;
|
|
971
|
+
conflicts.push({ op, error: outcome.error, amount: conflictAmount(engine, r.payload, op, source) });
|
|
972
|
+
}
|
|
973
|
+
return { committed, conflicts };
|
|
974
|
+
}
|
|
975
|
+
async deliverSet(transferId, recipientPubkey, memo, committed) {
|
|
976
|
+
const journaled = committed.filter((o) => o.error === void 0 && o.recipientBlob !== void 0);
|
|
977
|
+
let pending = journaled.length < committed.length;
|
|
978
|
+
if (journaled.length === 0) return pending;
|
|
979
|
+
const options = { transferId, ...memo !== void 0 ? { memo } : {} };
|
|
980
|
+
if (await this.tryBatchDeposit(recipientPubkey, journaled, options)) return pending;
|
|
981
|
+
for (const outcome of journaled) {
|
|
982
|
+
const stillPending = await this.deliverOne(recipientPubkey, outcome, options);
|
|
983
|
+
pending = pending || stillPending;
|
|
984
|
+
}
|
|
985
|
+
return pending;
|
|
986
|
+
}
|
|
987
|
+
async tryBatchDeposit(recipientPubkey, journaled, options) {
|
|
988
|
+
const batch = this.deps.delivery.deliverBatch?.bind(this.deps.delivery);
|
|
989
|
+
if (batch === void 0) return false;
|
|
990
|
+
try {
|
|
991
|
+
await batch(recipientPubkey, journaled.map((o) => o.recipientBlob), options);
|
|
992
|
+
} catch {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
for (const o of journaled) await this.removeJournaled(options.transferId, o.op.opIndex);
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
async deliverOne(recipientPubkey, outcome, options) {
|
|
999
|
+
try {
|
|
1000
|
+
await this.deps.delivery.deliver(recipientPubkey, outcome.recipientBlob, options);
|
|
1001
|
+
await this.removeJournaled(options.transferId, outcome.op.opIndex);
|
|
1002
|
+
return false;
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
if (isRateLimited(err)) {
|
|
1005
|
+
await this.stores.deliveryJournal.defer(options.transferId, outcome.op.opIndex, this.deps.now() + RATE_LIMIT_DEFER_MS);
|
|
1006
|
+
this.attention()(options.transferId, ATTENTION_DELIVERY_DEFERRED);
|
|
1007
|
+
} else {
|
|
1008
|
+
await this.stores.deliveryJournal.recordFailure(options.transferId, outcome.op.opIndex, this.attention());
|
|
1009
|
+
}
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
async removeJournaled(transferId, opIndex) {
|
|
1014
|
+
try {
|
|
1015
|
+
await this.stores.deliveryJournal.remove(transferId, opIndex);
|
|
1016
|
+
} catch {
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async applyCommitted(engine, transferId, committed) {
|
|
1020
|
+
try {
|
|
1021
|
+
const added = await this.uploadChangeBlobs(engine, committed);
|
|
1022
|
+
await this.deps.storage.applyDelta({
|
|
1023
|
+
transferId,
|
|
1024
|
+
spent: committed.map((o) => o.op.sourceTokenId),
|
|
1025
|
+
added
|
|
1026
|
+
});
|
|
1027
|
+
} catch (cause) {
|
|
1028
|
+
const pending = new SphereError(
|
|
1029
|
+
"The spend committed on-chain; the wallet-api mirror is catching up.",
|
|
1030
|
+
"SEND_SYNC_PENDING",
|
|
1031
|
+
cause
|
|
1032
|
+
);
|
|
1033
|
+
pending.transferId = transferId;
|
|
1034
|
+
throw pending;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
async applyBestEffort(engine, transferId, committed) {
|
|
1038
|
+
if (committed.some((o) => o.error !== void 0)) return;
|
|
1039
|
+
try {
|
|
1040
|
+
await this.applyCommitted(engine, transferId, committed);
|
|
1041
|
+
} catch {
|
|
1042
|
+
this.attention()(transferId, ATTENTION_SYNC_PENDING);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async uploadChangeBlobs(engine, committed) {
|
|
1046
|
+
const added = [];
|
|
1047
|
+
for (const o of committed) {
|
|
1048
|
+
if (o.changeBlob === void 0) continue;
|
|
1049
|
+
const digest = bytesToHex(sha256(o.changeBlob));
|
|
1050
|
+
const keys = await this.deps.storage.uploadBlobs([{ sha256: digest, bytes: o.changeBlob }]);
|
|
1051
|
+
const key = keys.get(digest);
|
|
1052
|
+
if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
|
|
1053
|
+
const { tokenId } = await engine.deliveryKeys(o.changeBlob);
|
|
1054
|
+
added.push({ tokenId, key });
|
|
1055
|
+
}
|
|
1056
|
+
return added;
|
|
1057
|
+
}
|
|
1058
|
+
async settlePartial(r, committed, conflicts) {
|
|
1059
|
+
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
1060
|
+
const entry = {
|
|
1061
|
+
transferId: r.transferId,
|
|
1062
|
+
remainingAmount: undelivered.toString(),
|
|
1063
|
+
coinId: r.payload.coinId,
|
|
1064
|
+
recipient: r.payload.recipient,
|
|
1065
|
+
committedTokenIds: committed.map((o) => o.op.sourceTokenId),
|
|
1066
|
+
createdAt: this.deps.now()
|
|
1067
|
+
};
|
|
1068
|
+
await this.stores.shortfalls.save(entry);
|
|
1069
|
+
await this.completeIntent(r.transferId, r.payload);
|
|
1070
|
+
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
1071
|
+
return entry;
|
|
1072
|
+
}
|
|
1073
|
+
closeTail(transferId, payload) {
|
|
1074
|
+
void (async () => {
|
|
1075
|
+
try {
|
|
1076
|
+
await this.completeIntent(transferId, payload);
|
|
1077
|
+
} catch {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
await this.finishBookkeeping(transferId, payload, "sent");
|
|
1081
|
+
})();
|
|
1082
|
+
}
|
|
1083
|
+
async completeIntent(transferId, payload) {
|
|
1084
|
+
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
1085
|
+
await this.deps.intents.complete(transferId, signature);
|
|
1086
|
+
}
|
|
1087
|
+
async finishBookkeeping(transferId, payload, phase) {
|
|
1088
|
+
try {
|
|
1089
|
+
await this.deps.recordHistory?.({ transferId, payload, phase });
|
|
1090
|
+
} catch {
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
await this.stores.backstop.drop(transferId);
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async disposeOnFailure(transferId, committed, cls) {
|
|
1098
|
+
if (committed.length > 0 || cls === "keep-open") return;
|
|
1099
|
+
try {
|
|
1100
|
+
await this.stores.backstop.setDisposition(transferId, "abortPending");
|
|
1101
|
+
} catch {
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
await this.deps.intents.abort(transferId);
|
|
1105
|
+
await this.stores.backstop.drop(transferId);
|
|
1106
|
+
} catch {
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
attention() {
|
|
1110
|
+
return (transferId, code, detail) => {
|
|
1111
|
+
this.deps.emit("transfer:attention", detail === void 0 ? { transferId, code } : { transferId, code, detail });
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
function conflictAmount(engine, payload, op, source) {
|
|
1116
|
+
if (op.kind === "split") return BigInt(payload.split.splitAmount);
|
|
1117
|
+
return source !== void 0 ? engine.balanceOf(source, payload.coinId) : 0n;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// modules/payments-v2/machine/resume.ts
|
|
1121
|
+
async function resumeAll(deps) {
|
|
1122
|
+
const report = { resumed: [], conflicted: [], failed: [] };
|
|
1123
|
+
const stores = createMachineStores(deps.kv);
|
|
1124
|
+
const open = await deps.intents.listOpen();
|
|
1125
|
+
let locals;
|
|
1126
|
+
try {
|
|
1127
|
+
locals = new Map((await stores.backstop.list()).map((e) => [e.transferId, e]));
|
|
1128
|
+
} catch {
|
|
1129
|
+
return report;
|
|
1130
|
+
}
|
|
1131
|
+
await dropStaleLocalAborts(stores, locals, new Set(open.map((i) => i.transferId)));
|
|
1132
|
+
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
1133
|
+
const ctx = {
|
|
1134
|
+
deps,
|
|
1135
|
+
stores,
|
|
1136
|
+
machine: new TransferMachine(deps),
|
|
1137
|
+
sources: await fetchSources(deps, jobs)
|
|
1138
|
+
};
|
|
1139
|
+
for (const job of jobs) {
|
|
1140
|
+
await runOne(ctx, job, report);
|
|
1141
|
+
}
|
|
1142
|
+
return report;
|
|
1143
|
+
}
|
|
1144
|
+
async function dropStaleLocalAborts(stores, locals, openIds) {
|
|
1145
|
+
for (const entry of locals.values()) {
|
|
1146
|
+
if (entry.disposition !== "open" && !openIds.has(entry.transferId)) {
|
|
1147
|
+
await stores.backstop.drop(entry.transferId);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
1152
|
+
const jobs = [];
|
|
1153
|
+
for (const intent of open) {
|
|
1154
|
+
const local = locals.get(intent.transferId);
|
|
1155
|
+
if (local !== void 0 && local.disposition !== "open") {
|
|
1156
|
+
try {
|
|
1157
|
+
await deps.intents.abort(intent.transferId);
|
|
1158
|
+
await stores.backstop.drop(intent.transferId);
|
|
1159
|
+
} catch {
|
|
1160
|
+
}
|
|
1161
|
+
report.conflicted.push(intent.transferId);
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
let payload;
|
|
1165
|
+
try {
|
|
1166
|
+
payload = validatePayload(await deps.decryptPayload(intent.payload));
|
|
1167
|
+
} catch {
|
|
1168
|
+
report.failed.push(intent.transferId);
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
jobs.push({
|
|
1172
|
+
transferId: intent.transferId,
|
|
1173
|
+
payload,
|
|
1174
|
+
journaled: await stores.deliveryJournal.byTransfer(intent.transferId)
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
return jobs;
|
|
1178
|
+
}
|
|
1179
|
+
async function fetchSources(deps, jobs) {
|
|
1180
|
+
const needed = /* @__PURE__ */ new Set();
|
|
1181
|
+
for (const job of jobs) {
|
|
1182
|
+
for (const op of buildOps(job.payload)) {
|
|
1183
|
+
if (op.kind === "direct" && job.journaled.has(op.opIndex)) continue;
|
|
1184
|
+
needed.add(op.sourceTokenId);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
1188
|
+
if (needed.size === 0) return decoded;
|
|
1189
|
+
let blobs;
|
|
1190
|
+
try {
|
|
1191
|
+
blobs = await deps.storage.getBlobs([...needed]);
|
|
1192
|
+
} catch {
|
|
1193
|
+
return decoded;
|
|
1194
|
+
}
|
|
1195
|
+
const engine = deps.engine();
|
|
1196
|
+
for (const [tokenId, bytes] of blobs) {
|
|
1197
|
+
try {
|
|
1198
|
+
decoded.set(tokenId, await engine.decodeToken({ v: TOKEN_BLOB_VERSION, network: 0, tokenId, token: bytes }));
|
|
1199
|
+
} catch {
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return decoded;
|
|
1203
|
+
}
|
|
1204
|
+
async function runOne(ctx, job, report) {
|
|
1205
|
+
const mine = /* @__PURE__ */ new Map();
|
|
1206
|
+
for (const op of buildOps(job.payload)) {
|
|
1207
|
+
if (op.kind === "direct" && job.journaled.has(op.opIndex)) continue;
|
|
1208
|
+
const token = ctx.sources.get(op.sourceTokenId);
|
|
1209
|
+
if (token === void 0) {
|
|
1210
|
+
report.failed.push(job.transferId);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
mine.set(op.sourceTokenId, token);
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
await ctx.machine.resumeIntent({
|
|
1217
|
+
transferId: job.transferId,
|
|
1218
|
+
payload: job.payload,
|
|
1219
|
+
sources: mine,
|
|
1220
|
+
journaled: job.journaled
|
|
1221
|
+
});
|
|
1222
|
+
report.resumed.push(job.transferId);
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
await classifyResumeFailure(ctx.deps, ctx.stores, job.transferId, err, report);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
async function classifyResumeFailure(deps, stores, transferId, err, report) {
|
|
1228
|
+
if (classifyError(err) === "conflict") {
|
|
1229
|
+
try {
|
|
1230
|
+
await deps.intents.abort(transferId);
|
|
1231
|
+
await stores.backstop.drop(transferId);
|
|
1232
|
+
} catch {
|
|
1233
|
+
}
|
|
1234
|
+
report.conflicted.push(transferId);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const code = err !== null && typeof err === "object" ? err.code : void 0;
|
|
1238
|
+
if (code === "SPLIT_CHECKPOINT_LOST" || code === "CHECKPOINT_TRUSTBASE_MISMATCH") {
|
|
1239
|
+
deps.emit("transfer:attention", { transferId, code: ATTENTION_CHECKPOINT_STUCK, detail: String(code) });
|
|
1240
|
+
}
|
|
1241
|
+
report.failed.push(transferId);
|
|
1242
|
+
}
|
|
1243
|
+
function validatePayload(raw) {
|
|
1244
|
+
const p = raw !== null && typeof raw === "object" ? raw : null;
|
|
1245
|
+
if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
|
|
1246
|
+
throw new SphereError(
|
|
1247
|
+
`unsupported intent payload shape (v=${String(raw?.v)}) \u2014 not resumable`,
|
|
1248
|
+
"VALIDATION_ERROR"
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
if (typeof p.recipient !== "string" || typeof p.coinId !== "string" || typeof p.amount !== "string") {
|
|
1252
|
+
throw new SphereError("intent payload is missing recipient/coinId/amount", "VALIDATION_ERROR");
|
|
1253
|
+
}
|
|
1254
|
+
const s = p.split;
|
|
1255
|
+
if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
|
|
1256
|
+
throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
|
|
1257
|
+
}
|
|
1258
|
+
return p;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// node_modules/@noble/ciphers/utils.js
|
|
1262
|
+
function isBytes2(a) {
|
|
1263
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
1264
|
+
}
|
|
1265
|
+
function abool(b) {
|
|
1266
|
+
if (typeof b !== "boolean")
|
|
1267
|
+
throw new TypeError(`boolean expected, not ${b}`);
|
|
1268
|
+
}
|
|
1269
|
+
function anumber(n) {
|
|
1270
|
+
if (typeof n !== "number")
|
|
1271
|
+
throw new TypeError("number expected, got " + typeof n);
|
|
1272
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
1273
|
+
throw new RangeError("positive integer expected, got " + n);
|
|
1274
|
+
}
|
|
1275
|
+
function abytes2(value, length, title = "") {
|
|
1276
|
+
const bytes = isBytes2(value);
|
|
1277
|
+
const len = value?.length;
|
|
1278
|
+
const needsLen = length !== void 0;
|
|
1279
|
+
if (!bytes || needsLen && len !== length) {
|
|
1280
|
+
const prefix = title && `"${title}" `;
|
|
1281
|
+
const ofLen = needsLen ? ` of length ${length}` : "";
|
|
1282
|
+
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
1283
|
+
const message = prefix + "expected Uint8Array" + ofLen + ", got " + got;
|
|
1284
|
+
if (!bytes)
|
|
1285
|
+
throw new TypeError(message);
|
|
1286
|
+
throw new RangeError(message);
|
|
1287
|
+
}
|
|
1288
|
+
return value;
|
|
1289
|
+
}
|
|
1290
|
+
function aexists2(instance, checkFinished = true) {
|
|
1291
|
+
if (instance.destroyed)
|
|
1292
|
+
throw new Error("Hash instance has been destroyed");
|
|
1293
|
+
if (checkFinished && instance.finished)
|
|
1294
|
+
throw new Error("Hash#digest() has already been called");
|
|
1295
|
+
}
|
|
1296
|
+
function aoutput2(out, instance, onlyAligned = false) {
|
|
1297
|
+
abytes2(out, void 0, "output");
|
|
1298
|
+
const min = instance.outputLen;
|
|
1299
|
+
if (out.length < min) {
|
|
1300
|
+
throw new RangeError("digestInto() expects output buffer of length at least " + min);
|
|
1301
|
+
}
|
|
1302
|
+
if (onlyAligned && !isAligned32(out))
|
|
1303
|
+
throw new Error("invalid output, must be aligned");
|
|
1304
|
+
}
|
|
1305
|
+
function u32(arr) {
|
|
1306
|
+
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
|
1307
|
+
}
|
|
1308
|
+
function clean2(...arrays) {
|
|
1309
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
1310
|
+
arrays[i].fill(0);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
function createView2(arr) {
|
|
1314
|
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
1315
|
+
}
|
|
1316
|
+
var isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68)();
|
|
1317
|
+
var byteSwap = (word) => word << 24 & 4278190080 | word << 8 & 16711680 | word >>> 8 & 65280 | word >>> 24 & 255;
|
|
1318
|
+
var swap8IfBE = isLE ? (n) => n : (n) => byteSwap(n) >>> 0;
|
|
1319
|
+
var byteSwap32 = (arr) => {
|
|
1320
|
+
for (let i = 0; i < arr.length; i++)
|
|
1321
|
+
arr[i] = byteSwap(arr[i]);
|
|
1322
|
+
return arr;
|
|
1323
|
+
};
|
|
1324
|
+
var swap32IfBE = isLE ? (u) => u : byteSwap32;
|
|
1325
|
+
function checkOpts(defaults, opts) {
|
|
1326
|
+
if (opts == null || typeof opts !== "object")
|
|
1327
|
+
throw new Error("options must be defined");
|
|
1328
|
+
const merged = Object.assign(defaults, opts);
|
|
1329
|
+
return merged;
|
|
1330
|
+
}
|
|
1331
|
+
function equalBytes(a, b) {
|
|
1332
|
+
if (a.length !== b.length)
|
|
1333
|
+
return false;
|
|
1334
|
+
let diff = 0;
|
|
1335
|
+
for (let i = 0; i < a.length; i++)
|
|
1336
|
+
diff |= a[i] ^ b[i];
|
|
1337
|
+
return diff === 0;
|
|
1338
|
+
}
|
|
1339
|
+
function wrapMacConstructor(keyLen, macCons, fromMsg) {
|
|
1340
|
+
const mac = macCons;
|
|
1341
|
+
const getArgs = fromMsg || (() => []);
|
|
1342
|
+
const macC = (msg, key) => mac(key, ...getArgs(msg)).update(msg).digest();
|
|
1343
|
+
const tmp = mac(new Uint8Array(keyLen), ...getArgs(new Uint8Array(0)));
|
|
1344
|
+
macC.outputLen = tmp.outputLen;
|
|
1345
|
+
macC.blockLen = tmp.blockLen;
|
|
1346
|
+
macC.create = (key, ...args) => mac(key, ...args);
|
|
1347
|
+
return macC;
|
|
1348
|
+
}
|
|
1349
|
+
var wrapCipher = /* @__NO_SIDE_EFFECTS__ */ (params, constructor) => {
|
|
1350
|
+
function wrappedCipher(key, ...args) {
|
|
1351
|
+
abytes2(key, void 0, "key");
|
|
1352
|
+
if (params.nonceLength !== void 0) {
|
|
1353
|
+
const nonce = args[0];
|
|
1354
|
+
abytes2(nonce, params.varSizeNonce ? void 0 : params.nonceLength, "nonce");
|
|
1355
|
+
}
|
|
1356
|
+
const tagl = params.tagLength;
|
|
1357
|
+
if (tagl && args[1] !== void 0)
|
|
1358
|
+
abytes2(args[1], void 0, "AAD");
|
|
1359
|
+
const cipher = constructor(key, ...args);
|
|
1360
|
+
const checkOutput = (fnLength, output) => {
|
|
1361
|
+
if (output !== void 0) {
|
|
1362
|
+
if (fnLength !== 2)
|
|
1363
|
+
throw new Error("cipher output not supported");
|
|
1364
|
+
abytes2(output, void 0, "output");
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
let called = false;
|
|
1368
|
+
const wrCipher = {
|
|
1369
|
+
encrypt(data, output) {
|
|
1370
|
+
if (called)
|
|
1371
|
+
throw new Error("cannot encrypt() twice with same key + nonce");
|
|
1372
|
+
called = true;
|
|
1373
|
+
abytes2(data);
|
|
1374
|
+
checkOutput(cipher.encrypt.length, output);
|
|
1375
|
+
return cipher.encrypt(data, output);
|
|
1376
|
+
},
|
|
1377
|
+
decrypt(data, output) {
|
|
1378
|
+
abytes2(data);
|
|
1379
|
+
if (tagl && data.length < tagl)
|
|
1380
|
+
throw new Error('"ciphertext" expected length bigger than tagLength=' + tagl);
|
|
1381
|
+
checkOutput(cipher.decrypt.length, output);
|
|
1382
|
+
return cipher.decrypt(data, output);
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
return wrCipher;
|
|
1386
|
+
}
|
|
1387
|
+
Object.assign(wrappedCipher, params);
|
|
1388
|
+
return wrappedCipher;
|
|
1389
|
+
};
|
|
1390
|
+
function getOutput(expectedLength, out, onlyAligned = true) {
|
|
1391
|
+
if (out === void 0)
|
|
1392
|
+
return new Uint8Array(expectedLength);
|
|
1393
|
+
abytes2(out, void 0, "output");
|
|
1394
|
+
if (out.length !== expectedLength)
|
|
1395
|
+
throw new Error('"output" expected Uint8Array of length ' + expectedLength + ", got: " + out.length);
|
|
1396
|
+
if (onlyAligned && !isAligned32(out))
|
|
1397
|
+
throw new Error("invalid output, must be aligned");
|
|
1398
|
+
return out;
|
|
1399
|
+
}
|
|
1400
|
+
function u64Lengths(dataLength, aadLength, isLE2) {
|
|
1401
|
+
anumber(dataLength);
|
|
1402
|
+
anumber(aadLength);
|
|
1403
|
+
abool(isLE2);
|
|
1404
|
+
const num = new Uint8Array(16);
|
|
1405
|
+
const view = createView2(num);
|
|
1406
|
+
view.setBigUint64(0, BigInt(aadLength), isLE2);
|
|
1407
|
+
view.setBigUint64(8, BigInt(dataLength), isLE2);
|
|
1408
|
+
return num;
|
|
1409
|
+
}
|
|
1410
|
+
function isAligned32(bytes) {
|
|
1411
|
+
return bytes.byteOffset % 4 === 0;
|
|
1412
|
+
}
|
|
1413
|
+
function copyBytes(bytes) {
|
|
1414
|
+
return Uint8Array.from(abytes2(bytes));
|
|
1415
|
+
}
|
|
1416
|
+
function randomBytes(bytesLength = 32) {
|
|
1417
|
+
anumber(bytesLength);
|
|
1418
|
+
const cr = typeof globalThis === "object" ? globalThis.crypto : null;
|
|
1419
|
+
if (typeof cr?.getRandomValues !== "function")
|
|
1420
|
+
throw new Error("crypto.getRandomValues must be defined");
|
|
1421
|
+
return cr.getRandomValues(new Uint8Array(bytesLength));
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// node_modules/@noble/ciphers/_arx.js
|
|
1425
|
+
var encodeStr = (str) => Uint8Array.from(str.split(""), (c) => c.charCodeAt(0));
|
|
1426
|
+
var sigma16_32 = /* @__PURE__ */ (() => swap32IfBE(u32(encodeStr("expand 16-byte k"))))();
|
|
1427
|
+
var sigma32_32 = /* @__PURE__ */ (() => swap32IfBE(u32(encodeStr("expand 32-byte k"))))();
|
|
1428
|
+
function rotl(a, b) {
|
|
1429
|
+
return a << b | a >>> 32 - b;
|
|
1430
|
+
}
|
|
1431
|
+
var BLOCK_LEN = 64;
|
|
1432
|
+
var BLOCK_LEN32 = 16;
|
|
1433
|
+
var MAX_COUNTER = /* @__PURE__ */ (() => 2 ** 32 - 1)();
|
|
1434
|
+
var U32_EMPTY = /* @__PURE__ */ Uint32Array.of();
|
|
1435
|
+
function runCipher(core, sigma, key, nonce, data, output, counter, rounds) {
|
|
1436
|
+
const len = data.length;
|
|
1437
|
+
const block = new Uint8Array(BLOCK_LEN);
|
|
1438
|
+
const b32 = u32(block);
|
|
1439
|
+
const isAligned = isLE && isAligned32(data) && isAligned32(output);
|
|
1440
|
+
const d32 = isAligned ? u32(data) : U32_EMPTY;
|
|
1441
|
+
const o32 = isAligned ? u32(output) : U32_EMPTY;
|
|
1442
|
+
if (!isLE) {
|
|
1443
|
+
for (let pos = 0; pos < len; counter++) {
|
|
1444
|
+
core(sigma, key, nonce, b32, counter, rounds);
|
|
1445
|
+
swap32IfBE(b32);
|
|
1446
|
+
if (counter >= MAX_COUNTER)
|
|
1447
|
+
throw new Error("arx: counter overflow");
|
|
1448
|
+
const take = Math.min(BLOCK_LEN, len - pos);
|
|
1449
|
+
for (let j = 0, posj; j < take; j++) {
|
|
1450
|
+
posj = pos + j;
|
|
1451
|
+
output[posj] = data[posj] ^ block[j];
|
|
1452
|
+
}
|
|
1453
|
+
pos += take;
|
|
1454
|
+
}
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
for (let pos = 0; pos < len; counter++) {
|
|
1458
|
+
core(sigma, key, nonce, b32, counter, rounds);
|
|
1459
|
+
if (counter >= MAX_COUNTER)
|
|
1460
|
+
throw new Error("arx: counter overflow");
|
|
1461
|
+
const take = Math.min(BLOCK_LEN, len - pos);
|
|
1462
|
+
if (isAligned && take === BLOCK_LEN) {
|
|
1463
|
+
const pos32 = pos / 4;
|
|
1464
|
+
if (pos % 4 !== 0)
|
|
1465
|
+
throw new Error("arx: invalid block position");
|
|
1466
|
+
for (let j = 0, posj; j < BLOCK_LEN32; j++) {
|
|
1467
|
+
posj = pos32 + j;
|
|
1468
|
+
o32[posj] = d32[posj] ^ b32[j];
|
|
1469
|
+
}
|
|
1470
|
+
pos += BLOCK_LEN;
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
for (let j = 0, posj; j < take; j++) {
|
|
1474
|
+
posj = pos + j;
|
|
1475
|
+
output[posj] = data[posj] ^ block[j];
|
|
1476
|
+
}
|
|
1477
|
+
pos += take;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function createCipher(core, opts) {
|
|
1481
|
+
const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = checkOpts({ allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20 }, opts);
|
|
1482
|
+
if (typeof core !== "function")
|
|
1483
|
+
throw new Error("core must be a function");
|
|
1484
|
+
anumber(counterLength);
|
|
1485
|
+
anumber(rounds);
|
|
1486
|
+
abool(counterRight);
|
|
1487
|
+
abool(allowShortKeys);
|
|
1488
|
+
return (key, nonce, data, output, counter = 0) => {
|
|
1489
|
+
abytes2(key, void 0, "key");
|
|
1490
|
+
abytes2(nonce, void 0, "nonce");
|
|
1491
|
+
abytes2(data, void 0, "data");
|
|
1492
|
+
const len = data.length;
|
|
1493
|
+
output = getOutput(len, output, false);
|
|
1494
|
+
anumber(counter);
|
|
1495
|
+
if (counter < 0 || counter >= MAX_COUNTER)
|
|
1496
|
+
throw new Error("arx: counter overflow");
|
|
1497
|
+
const toClean = [];
|
|
1498
|
+
let l = key.length;
|
|
1499
|
+
let k;
|
|
1500
|
+
let sigma;
|
|
1501
|
+
if (l === 32) {
|
|
1502
|
+
toClean.push(k = copyBytes(key));
|
|
1503
|
+
sigma = sigma32_32;
|
|
1504
|
+
} else if (l === 16 && allowShortKeys) {
|
|
1505
|
+
k = new Uint8Array(32);
|
|
1506
|
+
k.set(key);
|
|
1507
|
+
k.set(key, 16);
|
|
1508
|
+
sigma = sigma16_32;
|
|
1509
|
+
toClean.push(k);
|
|
1510
|
+
} else {
|
|
1511
|
+
abytes2(key, 32, "arx key");
|
|
1512
|
+
throw new Error("invalid key size");
|
|
1513
|
+
}
|
|
1514
|
+
if (!isLE || !isAligned32(nonce))
|
|
1515
|
+
toClean.push(nonce = copyBytes(nonce));
|
|
1516
|
+
let k32 = u32(k);
|
|
1517
|
+
if (extendNonceFn) {
|
|
1518
|
+
if (nonce.length !== 24)
|
|
1519
|
+
throw new Error(`arx: extended nonce must be 24 bytes`);
|
|
1520
|
+
const n16 = nonce.subarray(0, 16);
|
|
1521
|
+
if (isLE)
|
|
1522
|
+
extendNonceFn(sigma, k32, u32(n16), k32);
|
|
1523
|
+
else {
|
|
1524
|
+
const sigmaRaw = swap32IfBE(Uint32Array.from(sigma));
|
|
1525
|
+
extendNonceFn(sigmaRaw, k32, u32(n16), k32);
|
|
1526
|
+
clean2(sigmaRaw);
|
|
1527
|
+
swap32IfBE(k32);
|
|
1528
|
+
}
|
|
1529
|
+
nonce = nonce.subarray(16);
|
|
1530
|
+
} else if (!isLE)
|
|
1531
|
+
swap32IfBE(k32);
|
|
1532
|
+
const nonceNcLen = 16 - counterLength;
|
|
1533
|
+
if (nonceNcLen !== nonce.length)
|
|
1534
|
+
throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`);
|
|
1535
|
+
if (nonceNcLen !== 12) {
|
|
1536
|
+
const nc = new Uint8Array(12);
|
|
1537
|
+
nc.set(nonce, counterRight ? 0 : 12 - nonce.length);
|
|
1538
|
+
nonce = nc;
|
|
1539
|
+
toClean.push(nonce);
|
|
1540
|
+
}
|
|
1541
|
+
const n32 = swap32IfBE(u32(nonce));
|
|
1542
|
+
try {
|
|
1543
|
+
runCipher(core, sigma, k32, n32, data, output, counter, rounds);
|
|
1544
|
+
return output;
|
|
1545
|
+
} finally {
|
|
1546
|
+
clean2(...toClean);
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// node_modules/@noble/ciphers/_poly1305.js
|
|
1552
|
+
function u8to16(a, i) {
|
|
1553
|
+
return a[i++] & 255 | (a[i++] & 255) << 8;
|
|
1554
|
+
}
|
|
1555
|
+
var Poly1305 = class {
|
|
1556
|
+
blockLen = 16;
|
|
1557
|
+
outputLen = 16;
|
|
1558
|
+
buffer = new Uint8Array(16);
|
|
1559
|
+
r = new Uint16Array(10);
|
|
1560
|
+
// Allocating 1 array with .subarray() here is slower than 3
|
|
1561
|
+
h = new Uint16Array(10);
|
|
1562
|
+
pad = new Uint16Array(8);
|
|
1563
|
+
pos = 0;
|
|
1564
|
+
finished = false;
|
|
1565
|
+
destroyed = false;
|
|
1566
|
+
// Can be speed-up using BigUint64Array, at the cost of complexity
|
|
1567
|
+
constructor(key) {
|
|
1568
|
+
key = copyBytes(abytes2(key, 32, "key"));
|
|
1569
|
+
const t0 = u8to16(key, 0);
|
|
1570
|
+
const t1 = u8to16(key, 2);
|
|
1571
|
+
const t2 = u8to16(key, 4);
|
|
1572
|
+
const t3 = u8to16(key, 6);
|
|
1573
|
+
const t4 = u8to16(key, 8);
|
|
1574
|
+
const t5 = u8to16(key, 10);
|
|
1575
|
+
const t6 = u8to16(key, 12);
|
|
1576
|
+
const t7 = u8to16(key, 14);
|
|
1577
|
+
this.r[0] = t0 & 8191;
|
|
1578
|
+
this.r[1] = (t0 >>> 13 | t1 << 3) & 8191;
|
|
1579
|
+
this.r[2] = (t1 >>> 10 | t2 << 6) & 7939;
|
|
1580
|
+
this.r[3] = (t2 >>> 7 | t3 << 9) & 8191;
|
|
1581
|
+
this.r[4] = (t3 >>> 4 | t4 << 12) & 255;
|
|
1582
|
+
this.r[5] = t4 >>> 1 & 8190;
|
|
1583
|
+
this.r[6] = (t4 >>> 14 | t5 << 2) & 8191;
|
|
1584
|
+
this.r[7] = (t5 >>> 11 | t6 << 5) & 8065;
|
|
1585
|
+
this.r[8] = (t6 >>> 8 | t7 << 8) & 8191;
|
|
1586
|
+
this.r[9] = t7 >>> 5 & 127;
|
|
1587
|
+
for (let i = 0; i < 8; i++)
|
|
1588
|
+
this.pad[i] = u8to16(key, 16 + 2 * i);
|
|
1589
|
+
}
|
|
1590
|
+
process(data, offset, isLast = false) {
|
|
1591
|
+
const hibit = isLast ? 0 : 1 << 11;
|
|
1592
|
+
const { h, r } = this;
|
|
1593
|
+
const r0 = r[0];
|
|
1594
|
+
const r1 = r[1];
|
|
1595
|
+
const r2 = r[2];
|
|
1596
|
+
const r3 = r[3];
|
|
1597
|
+
const r4 = r[4];
|
|
1598
|
+
const r5 = r[5];
|
|
1599
|
+
const r6 = r[6];
|
|
1600
|
+
const r7 = r[7];
|
|
1601
|
+
const r8 = r[8];
|
|
1602
|
+
const r9 = r[9];
|
|
1603
|
+
const t0 = u8to16(data, offset + 0);
|
|
1604
|
+
const t1 = u8to16(data, offset + 2);
|
|
1605
|
+
const t2 = u8to16(data, offset + 4);
|
|
1606
|
+
const t3 = u8to16(data, offset + 6);
|
|
1607
|
+
const t4 = u8to16(data, offset + 8);
|
|
1608
|
+
const t5 = u8to16(data, offset + 10);
|
|
1609
|
+
const t6 = u8to16(data, offset + 12);
|
|
1610
|
+
const t7 = u8to16(data, offset + 14);
|
|
1611
|
+
let h0 = h[0] + (t0 & 8191);
|
|
1612
|
+
let h1 = h[1] + ((t0 >>> 13 | t1 << 3) & 8191);
|
|
1613
|
+
let h2 = h[2] + ((t1 >>> 10 | t2 << 6) & 8191);
|
|
1614
|
+
let h3 = h[3] + ((t2 >>> 7 | t3 << 9) & 8191);
|
|
1615
|
+
let h4 = h[4] + ((t3 >>> 4 | t4 << 12) & 8191);
|
|
1616
|
+
let h5 = h[5] + (t4 >>> 1 & 8191);
|
|
1617
|
+
let h6 = h[6] + ((t4 >>> 14 | t5 << 2) & 8191);
|
|
1618
|
+
let h7 = h[7] + ((t5 >>> 11 | t6 << 5) & 8191);
|
|
1619
|
+
let h8 = h[8] + ((t6 >>> 8 | t7 << 8) & 8191);
|
|
1620
|
+
let h9 = h[9] + (t7 >>> 5 | hibit);
|
|
1621
|
+
let c = 0;
|
|
1622
|
+
let d0 = c + h0 * r0 + h1 * (5 * r9) + h2 * (5 * r8) + h3 * (5 * r7) + h4 * (5 * r6);
|
|
1623
|
+
c = d0 >>> 13;
|
|
1624
|
+
d0 &= 8191;
|
|
1625
|
+
d0 += h5 * (5 * r5) + h6 * (5 * r4) + h7 * (5 * r3) + h8 * (5 * r2) + h9 * (5 * r1);
|
|
1626
|
+
c += d0 >>> 13;
|
|
1627
|
+
d0 &= 8191;
|
|
1628
|
+
let d1 = c + h0 * r1 + h1 * r0 + h2 * (5 * r9) + h3 * (5 * r8) + h4 * (5 * r7);
|
|
1629
|
+
c = d1 >>> 13;
|
|
1630
|
+
d1 &= 8191;
|
|
1631
|
+
d1 += h5 * (5 * r6) + h6 * (5 * r5) + h7 * (5 * r4) + h8 * (5 * r3) + h9 * (5 * r2);
|
|
1632
|
+
c += d1 >>> 13;
|
|
1633
|
+
d1 &= 8191;
|
|
1634
|
+
let d2 = c + h0 * r2 + h1 * r1 + h2 * r0 + h3 * (5 * r9) + h4 * (5 * r8);
|
|
1635
|
+
c = d2 >>> 13;
|
|
1636
|
+
d2 &= 8191;
|
|
1637
|
+
d2 += h5 * (5 * r7) + h6 * (5 * r6) + h7 * (5 * r5) + h8 * (5 * r4) + h9 * (5 * r3);
|
|
1638
|
+
c += d2 >>> 13;
|
|
1639
|
+
d2 &= 8191;
|
|
1640
|
+
let d3 = c + h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * (5 * r9);
|
|
1641
|
+
c = d3 >>> 13;
|
|
1642
|
+
d3 &= 8191;
|
|
1643
|
+
d3 += h5 * (5 * r8) + h6 * (5 * r7) + h7 * (5 * r6) + h8 * (5 * r5) + h9 * (5 * r4);
|
|
1644
|
+
c += d3 >>> 13;
|
|
1645
|
+
d3 &= 8191;
|
|
1646
|
+
let d4 = c + h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0;
|
|
1647
|
+
c = d4 >>> 13;
|
|
1648
|
+
d4 &= 8191;
|
|
1649
|
+
d4 += h5 * (5 * r9) + h6 * (5 * r8) + h7 * (5 * r7) + h8 * (5 * r6) + h9 * (5 * r5);
|
|
1650
|
+
c += d4 >>> 13;
|
|
1651
|
+
d4 &= 8191;
|
|
1652
|
+
let d5 = c + h0 * r5 + h1 * r4 + h2 * r3 + h3 * r2 + h4 * r1;
|
|
1653
|
+
c = d5 >>> 13;
|
|
1654
|
+
d5 &= 8191;
|
|
1655
|
+
d5 += h5 * r0 + h6 * (5 * r9) + h7 * (5 * r8) + h8 * (5 * r7) + h9 * (5 * r6);
|
|
1656
|
+
c += d5 >>> 13;
|
|
1657
|
+
d5 &= 8191;
|
|
1658
|
+
let d6 = c + h0 * r6 + h1 * r5 + h2 * r4 + h3 * r3 + h4 * r2;
|
|
1659
|
+
c = d6 >>> 13;
|
|
1660
|
+
d6 &= 8191;
|
|
1661
|
+
d6 += h5 * r1 + h6 * r0 + h7 * (5 * r9) + h8 * (5 * r8) + h9 * (5 * r7);
|
|
1662
|
+
c += d6 >>> 13;
|
|
1663
|
+
d6 &= 8191;
|
|
1664
|
+
let d7 = c + h0 * r7 + h1 * r6 + h2 * r5 + h3 * r4 + h4 * r3;
|
|
1665
|
+
c = d7 >>> 13;
|
|
1666
|
+
d7 &= 8191;
|
|
1667
|
+
d7 += h5 * r2 + h6 * r1 + h7 * r0 + h8 * (5 * r9) + h9 * (5 * r8);
|
|
1668
|
+
c += d7 >>> 13;
|
|
1669
|
+
d7 &= 8191;
|
|
1670
|
+
let d8 = c + h0 * r8 + h1 * r7 + h2 * r6 + h3 * r5 + h4 * r4;
|
|
1671
|
+
c = d8 >>> 13;
|
|
1672
|
+
d8 &= 8191;
|
|
1673
|
+
d8 += h5 * r3 + h6 * r2 + h7 * r1 + h8 * r0 + h9 * (5 * r9);
|
|
1674
|
+
c += d8 >>> 13;
|
|
1675
|
+
d8 &= 8191;
|
|
1676
|
+
let d9 = c + h0 * r9 + h1 * r8 + h2 * r7 + h3 * r6 + h4 * r5;
|
|
1677
|
+
c = d9 >>> 13;
|
|
1678
|
+
d9 &= 8191;
|
|
1679
|
+
d9 += h5 * r4 + h6 * r3 + h7 * r2 + h8 * r1 + h9 * r0;
|
|
1680
|
+
c += d9 >>> 13;
|
|
1681
|
+
d9 &= 8191;
|
|
1682
|
+
c = (c << 2) + c | 0;
|
|
1683
|
+
c = c + d0 | 0;
|
|
1684
|
+
d0 = c & 8191;
|
|
1685
|
+
c = c >>> 13;
|
|
1686
|
+
d1 += c;
|
|
1687
|
+
h[0] = d0;
|
|
1688
|
+
h[1] = d1;
|
|
1689
|
+
h[2] = d2;
|
|
1690
|
+
h[3] = d3;
|
|
1691
|
+
h[4] = d4;
|
|
1692
|
+
h[5] = d5;
|
|
1693
|
+
h[6] = d6;
|
|
1694
|
+
h[7] = d7;
|
|
1695
|
+
h[8] = d8;
|
|
1696
|
+
h[9] = d9;
|
|
1697
|
+
}
|
|
1698
|
+
finalize() {
|
|
1699
|
+
const { h, pad } = this;
|
|
1700
|
+
const g = new Uint16Array(10);
|
|
1701
|
+
let c = h[1] >>> 13;
|
|
1702
|
+
h[1] &= 8191;
|
|
1703
|
+
for (let i = 2; i < 10; i++) {
|
|
1704
|
+
h[i] += c;
|
|
1705
|
+
c = h[i] >>> 13;
|
|
1706
|
+
h[i] &= 8191;
|
|
1707
|
+
}
|
|
1708
|
+
h[0] += c * 5;
|
|
1709
|
+
c = h[0] >>> 13;
|
|
1710
|
+
h[0] &= 8191;
|
|
1711
|
+
h[1] += c;
|
|
1712
|
+
c = h[1] >>> 13;
|
|
1713
|
+
h[1] &= 8191;
|
|
1714
|
+
h[2] += c;
|
|
1715
|
+
g[0] = h[0] + 5;
|
|
1716
|
+
c = g[0] >>> 13;
|
|
1717
|
+
g[0] &= 8191;
|
|
1718
|
+
for (let i = 1; i < 10; i++) {
|
|
1719
|
+
g[i] = h[i] + c;
|
|
1720
|
+
c = g[i] >>> 13;
|
|
1721
|
+
g[i] &= 8191;
|
|
1722
|
+
}
|
|
1723
|
+
g[9] -= 1 << 13;
|
|
1724
|
+
let mask = (c ^ 1) - 1;
|
|
1725
|
+
for (let i = 0; i < 10; i++)
|
|
1726
|
+
g[i] &= mask;
|
|
1727
|
+
mask = ~mask;
|
|
1728
|
+
for (let i = 0; i < 10; i++)
|
|
1729
|
+
h[i] = h[i] & mask | g[i];
|
|
1730
|
+
h[0] = (h[0] | h[1] << 13) & 65535;
|
|
1731
|
+
h[1] = (h[1] >>> 3 | h[2] << 10) & 65535;
|
|
1732
|
+
h[2] = (h[2] >>> 6 | h[3] << 7) & 65535;
|
|
1733
|
+
h[3] = (h[3] >>> 9 | h[4] << 4) & 65535;
|
|
1734
|
+
h[4] = (h[4] >>> 12 | h[5] << 1 | h[6] << 14) & 65535;
|
|
1735
|
+
h[5] = (h[6] >>> 2 | h[7] << 11) & 65535;
|
|
1736
|
+
h[6] = (h[7] >>> 5 | h[8] << 8) & 65535;
|
|
1737
|
+
h[7] = (h[8] >>> 8 | h[9] << 5) & 65535;
|
|
1738
|
+
let f = h[0] + pad[0];
|
|
1739
|
+
h[0] = f & 65535;
|
|
1740
|
+
for (let i = 1; i < 8; i++) {
|
|
1741
|
+
f = (h[i] + pad[i] | 0) + (f >>> 16) | 0;
|
|
1742
|
+
h[i] = f & 65535;
|
|
1743
|
+
}
|
|
1744
|
+
clean2(g);
|
|
1745
|
+
}
|
|
1746
|
+
update(data) {
|
|
1747
|
+
aexists2(this);
|
|
1748
|
+
abytes2(data);
|
|
1749
|
+
data = copyBytes(data);
|
|
1750
|
+
const { buffer, blockLen } = this;
|
|
1751
|
+
const len = data.length;
|
|
1752
|
+
for (let pos = 0; pos < len; ) {
|
|
1753
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
1754
|
+
if (take === blockLen) {
|
|
1755
|
+
for (; blockLen <= len - pos; pos += blockLen)
|
|
1756
|
+
this.process(data, pos);
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
buffer.set(data.subarray(pos, pos + take), this.pos);
|
|
1760
|
+
this.pos += take;
|
|
1761
|
+
pos += take;
|
|
1762
|
+
if (this.pos === blockLen) {
|
|
1763
|
+
this.process(buffer, 0, false);
|
|
1764
|
+
this.pos = 0;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return this;
|
|
1768
|
+
}
|
|
1769
|
+
destroy() {
|
|
1770
|
+
this.destroyed = true;
|
|
1771
|
+
clean2(this.h, this.r, this.buffer, this.pad);
|
|
1772
|
+
}
|
|
1773
|
+
digestInto(out) {
|
|
1774
|
+
aexists2(this);
|
|
1775
|
+
aoutput2(out, this);
|
|
1776
|
+
this.finished = true;
|
|
1777
|
+
const { buffer, h } = this;
|
|
1778
|
+
let { pos } = this;
|
|
1779
|
+
if (pos) {
|
|
1780
|
+
buffer[pos++] = 1;
|
|
1781
|
+
for (; pos < 16; pos++)
|
|
1782
|
+
buffer[pos] = 0;
|
|
1783
|
+
this.process(buffer, 0, true);
|
|
1784
|
+
}
|
|
1785
|
+
this.finalize();
|
|
1786
|
+
let opos = 0;
|
|
1787
|
+
for (let i = 0; i < 8; i++) {
|
|
1788
|
+
out[opos++] = h[i] >>> 0;
|
|
1789
|
+
out[opos++] = h[i] >>> 8;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
digest() {
|
|
1793
|
+
const { buffer, outputLen } = this;
|
|
1794
|
+
this.digestInto(buffer);
|
|
1795
|
+
const res = buffer.slice(0, outputLen);
|
|
1796
|
+
this.destroy();
|
|
1797
|
+
return res;
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
var poly1305 = /* @__PURE__ */ wrapMacConstructor(32, (key) => new Poly1305(key));
|
|
1801
|
+
|
|
1802
|
+
// node_modules/@noble/ciphers/chacha.js
|
|
1803
|
+
function chachaCore(s, k, n, out, cnt, rounds = 20) {
|
|
1804
|
+
let y00 = s[0], y01 = s[1], y02 = s[2], y03 = s[3], y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2];
|
|
1805
|
+
let x00 = y00, x01 = y01, x02 = y02, x03 = y03, x04 = y04, x05 = y05, x06 = y06, x07 = y07, x08 = y08, x09 = y09, x10 = y10, x11 = y11, x12 = y12, x13 = y13, x14 = y14, x15 = y15;
|
|
1806
|
+
for (let r = 0; r < rounds; r += 2) {
|
|
1807
|
+
x00 = x00 + x04 | 0;
|
|
1808
|
+
x12 = rotl(x12 ^ x00, 16);
|
|
1809
|
+
x08 = x08 + x12 | 0;
|
|
1810
|
+
x04 = rotl(x04 ^ x08, 12);
|
|
1811
|
+
x00 = x00 + x04 | 0;
|
|
1812
|
+
x12 = rotl(x12 ^ x00, 8);
|
|
1813
|
+
x08 = x08 + x12 | 0;
|
|
1814
|
+
x04 = rotl(x04 ^ x08, 7);
|
|
1815
|
+
x01 = x01 + x05 | 0;
|
|
1816
|
+
x13 = rotl(x13 ^ x01, 16);
|
|
1817
|
+
x09 = x09 + x13 | 0;
|
|
1818
|
+
x05 = rotl(x05 ^ x09, 12);
|
|
1819
|
+
x01 = x01 + x05 | 0;
|
|
1820
|
+
x13 = rotl(x13 ^ x01, 8);
|
|
1821
|
+
x09 = x09 + x13 | 0;
|
|
1822
|
+
x05 = rotl(x05 ^ x09, 7);
|
|
1823
|
+
x02 = x02 + x06 | 0;
|
|
1824
|
+
x14 = rotl(x14 ^ x02, 16);
|
|
1825
|
+
x10 = x10 + x14 | 0;
|
|
1826
|
+
x06 = rotl(x06 ^ x10, 12);
|
|
1827
|
+
x02 = x02 + x06 | 0;
|
|
1828
|
+
x14 = rotl(x14 ^ x02, 8);
|
|
1829
|
+
x10 = x10 + x14 | 0;
|
|
1830
|
+
x06 = rotl(x06 ^ x10, 7);
|
|
1831
|
+
x03 = x03 + x07 | 0;
|
|
1832
|
+
x15 = rotl(x15 ^ x03, 16);
|
|
1833
|
+
x11 = x11 + x15 | 0;
|
|
1834
|
+
x07 = rotl(x07 ^ x11, 12);
|
|
1835
|
+
x03 = x03 + x07 | 0;
|
|
1836
|
+
x15 = rotl(x15 ^ x03, 8);
|
|
1837
|
+
x11 = x11 + x15 | 0;
|
|
1838
|
+
x07 = rotl(x07 ^ x11, 7);
|
|
1839
|
+
x00 = x00 + x05 | 0;
|
|
1840
|
+
x15 = rotl(x15 ^ x00, 16);
|
|
1841
|
+
x10 = x10 + x15 | 0;
|
|
1842
|
+
x05 = rotl(x05 ^ x10, 12);
|
|
1843
|
+
x00 = x00 + x05 | 0;
|
|
1844
|
+
x15 = rotl(x15 ^ x00, 8);
|
|
1845
|
+
x10 = x10 + x15 | 0;
|
|
1846
|
+
x05 = rotl(x05 ^ x10, 7);
|
|
1847
|
+
x01 = x01 + x06 | 0;
|
|
1848
|
+
x12 = rotl(x12 ^ x01, 16);
|
|
1849
|
+
x11 = x11 + x12 | 0;
|
|
1850
|
+
x06 = rotl(x06 ^ x11, 12);
|
|
1851
|
+
x01 = x01 + x06 | 0;
|
|
1852
|
+
x12 = rotl(x12 ^ x01, 8);
|
|
1853
|
+
x11 = x11 + x12 | 0;
|
|
1854
|
+
x06 = rotl(x06 ^ x11, 7);
|
|
1855
|
+
x02 = x02 + x07 | 0;
|
|
1856
|
+
x13 = rotl(x13 ^ x02, 16);
|
|
1857
|
+
x08 = x08 + x13 | 0;
|
|
1858
|
+
x07 = rotl(x07 ^ x08, 12);
|
|
1859
|
+
x02 = x02 + x07 | 0;
|
|
1860
|
+
x13 = rotl(x13 ^ x02, 8);
|
|
1861
|
+
x08 = x08 + x13 | 0;
|
|
1862
|
+
x07 = rotl(x07 ^ x08, 7);
|
|
1863
|
+
x03 = x03 + x04 | 0;
|
|
1864
|
+
x14 = rotl(x14 ^ x03, 16);
|
|
1865
|
+
x09 = x09 + x14 | 0;
|
|
1866
|
+
x04 = rotl(x04 ^ x09, 12);
|
|
1867
|
+
x03 = x03 + x04 | 0;
|
|
1868
|
+
x14 = rotl(x14 ^ x03, 8);
|
|
1869
|
+
x09 = x09 + x14 | 0;
|
|
1870
|
+
x04 = rotl(x04 ^ x09, 7);
|
|
1871
|
+
}
|
|
1872
|
+
let oi = 0;
|
|
1873
|
+
out[oi++] = y00 + x00 | 0;
|
|
1874
|
+
out[oi++] = y01 + x01 | 0;
|
|
1875
|
+
out[oi++] = y02 + x02 | 0;
|
|
1876
|
+
out[oi++] = y03 + x03 | 0;
|
|
1877
|
+
out[oi++] = y04 + x04 | 0;
|
|
1878
|
+
out[oi++] = y05 + x05 | 0;
|
|
1879
|
+
out[oi++] = y06 + x06 | 0;
|
|
1880
|
+
out[oi++] = y07 + x07 | 0;
|
|
1881
|
+
out[oi++] = y08 + x08 | 0;
|
|
1882
|
+
out[oi++] = y09 + x09 | 0;
|
|
1883
|
+
out[oi++] = y10 + x10 | 0;
|
|
1884
|
+
out[oi++] = y11 + x11 | 0;
|
|
1885
|
+
out[oi++] = y12 + x12 | 0;
|
|
1886
|
+
out[oi++] = y13 + x13 | 0;
|
|
1887
|
+
out[oi++] = y14 + x14 | 0;
|
|
1888
|
+
out[oi++] = y15 + x15 | 0;
|
|
1889
|
+
}
|
|
1890
|
+
function hchacha(s, k, i, out) {
|
|
1891
|
+
let x00 = swap8IfBE(s[0]), x01 = swap8IfBE(s[1]), x02 = swap8IfBE(s[2]), x03 = swap8IfBE(s[3]), x04 = swap8IfBE(k[0]), x05 = swap8IfBE(k[1]), x06 = swap8IfBE(k[2]), x07 = swap8IfBE(k[3]), x08 = swap8IfBE(k[4]), x09 = swap8IfBE(k[5]), x10 = swap8IfBE(k[6]), x11 = swap8IfBE(k[7]), x12 = swap8IfBE(i[0]), x13 = swap8IfBE(i[1]), x14 = swap8IfBE(i[2]), x15 = swap8IfBE(i[3]);
|
|
1892
|
+
for (let r = 0; r < 20; r += 2) {
|
|
1893
|
+
x00 = x00 + x04 | 0;
|
|
1894
|
+
x12 = rotl(x12 ^ x00, 16);
|
|
1895
|
+
x08 = x08 + x12 | 0;
|
|
1896
|
+
x04 = rotl(x04 ^ x08, 12);
|
|
1897
|
+
x00 = x00 + x04 | 0;
|
|
1898
|
+
x12 = rotl(x12 ^ x00, 8);
|
|
1899
|
+
x08 = x08 + x12 | 0;
|
|
1900
|
+
x04 = rotl(x04 ^ x08, 7);
|
|
1901
|
+
x01 = x01 + x05 | 0;
|
|
1902
|
+
x13 = rotl(x13 ^ x01, 16);
|
|
1903
|
+
x09 = x09 + x13 | 0;
|
|
1904
|
+
x05 = rotl(x05 ^ x09, 12);
|
|
1905
|
+
x01 = x01 + x05 | 0;
|
|
1906
|
+
x13 = rotl(x13 ^ x01, 8);
|
|
1907
|
+
x09 = x09 + x13 | 0;
|
|
1908
|
+
x05 = rotl(x05 ^ x09, 7);
|
|
1909
|
+
x02 = x02 + x06 | 0;
|
|
1910
|
+
x14 = rotl(x14 ^ x02, 16);
|
|
1911
|
+
x10 = x10 + x14 | 0;
|
|
1912
|
+
x06 = rotl(x06 ^ x10, 12);
|
|
1913
|
+
x02 = x02 + x06 | 0;
|
|
1914
|
+
x14 = rotl(x14 ^ x02, 8);
|
|
1915
|
+
x10 = x10 + x14 | 0;
|
|
1916
|
+
x06 = rotl(x06 ^ x10, 7);
|
|
1917
|
+
x03 = x03 + x07 | 0;
|
|
1918
|
+
x15 = rotl(x15 ^ x03, 16);
|
|
1919
|
+
x11 = x11 + x15 | 0;
|
|
1920
|
+
x07 = rotl(x07 ^ x11, 12);
|
|
1921
|
+
x03 = x03 + x07 | 0;
|
|
1922
|
+
x15 = rotl(x15 ^ x03, 8);
|
|
1923
|
+
x11 = x11 + x15 | 0;
|
|
1924
|
+
x07 = rotl(x07 ^ x11, 7);
|
|
1925
|
+
x00 = x00 + x05 | 0;
|
|
1926
|
+
x15 = rotl(x15 ^ x00, 16);
|
|
1927
|
+
x10 = x10 + x15 | 0;
|
|
1928
|
+
x05 = rotl(x05 ^ x10, 12);
|
|
1929
|
+
x00 = x00 + x05 | 0;
|
|
1930
|
+
x15 = rotl(x15 ^ x00, 8);
|
|
1931
|
+
x10 = x10 + x15 | 0;
|
|
1932
|
+
x05 = rotl(x05 ^ x10, 7);
|
|
1933
|
+
x01 = x01 + x06 | 0;
|
|
1934
|
+
x12 = rotl(x12 ^ x01, 16);
|
|
1935
|
+
x11 = x11 + x12 | 0;
|
|
1936
|
+
x06 = rotl(x06 ^ x11, 12);
|
|
1937
|
+
x01 = x01 + x06 | 0;
|
|
1938
|
+
x12 = rotl(x12 ^ x01, 8);
|
|
1939
|
+
x11 = x11 + x12 | 0;
|
|
1940
|
+
x06 = rotl(x06 ^ x11, 7);
|
|
1941
|
+
x02 = x02 + x07 | 0;
|
|
1942
|
+
x13 = rotl(x13 ^ x02, 16);
|
|
1943
|
+
x08 = x08 + x13 | 0;
|
|
1944
|
+
x07 = rotl(x07 ^ x08, 12);
|
|
1945
|
+
x02 = x02 + x07 | 0;
|
|
1946
|
+
x13 = rotl(x13 ^ x02, 8);
|
|
1947
|
+
x08 = x08 + x13 | 0;
|
|
1948
|
+
x07 = rotl(x07 ^ x08, 7);
|
|
1949
|
+
x03 = x03 + x04 | 0;
|
|
1950
|
+
x14 = rotl(x14 ^ x03, 16);
|
|
1951
|
+
x09 = x09 + x14 | 0;
|
|
1952
|
+
x04 = rotl(x04 ^ x09, 12);
|
|
1953
|
+
x03 = x03 + x04 | 0;
|
|
1954
|
+
x14 = rotl(x14 ^ x03, 8);
|
|
1955
|
+
x09 = x09 + x14 | 0;
|
|
1956
|
+
x04 = rotl(x04 ^ x09, 7);
|
|
1957
|
+
}
|
|
1958
|
+
let oi = 0;
|
|
1959
|
+
out[oi++] = x00;
|
|
1960
|
+
out[oi++] = x01;
|
|
1961
|
+
out[oi++] = x02;
|
|
1962
|
+
out[oi++] = x03;
|
|
1963
|
+
out[oi++] = x12;
|
|
1964
|
+
out[oi++] = x13;
|
|
1965
|
+
out[oi++] = x14;
|
|
1966
|
+
out[oi++] = x15;
|
|
1967
|
+
swap32IfBE(out);
|
|
1968
|
+
}
|
|
1969
|
+
var xchacha20 = /* @__PURE__ */ createCipher(chachaCore, {
|
|
1970
|
+
counterRight: false,
|
|
1971
|
+
counterLength: 8,
|
|
1972
|
+
extendNonceFn: hchacha,
|
|
1973
|
+
allowShortKeys: false
|
|
1974
|
+
});
|
|
1975
|
+
var ZEROS16 = /* @__PURE__ */ new Uint8Array(16);
|
|
1976
|
+
var updatePadded = (h, msg) => {
|
|
1977
|
+
h.update(msg);
|
|
1978
|
+
const leftover = msg.length % 16;
|
|
1979
|
+
if (leftover)
|
|
1980
|
+
h.update(ZEROS16.subarray(leftover));
|
|
1981
|
+
};
|
|
1982
|
+
var ZEROS32 = /* @__PURE__ */ new Uint8Array(32);
|
|
1983
|
+
function computeTag(fn, key, nonce, ciphertext, AAD) {
|
|
1984
|
+
if (AAD !== void 0)
|
|
1985
|
+
abytes2(AAD, void 0, "AAD");
|
|
1986
|
+
const authKey = fn(key, nonce, ZEROS32);
|
|
1987
|
+
const lengths = u64Lengths(ciphertext.length, AAD ? AAD.length : 0, true);
|
|
1988
|
+
const h = poly1305.create(authKey);
|
|
1989
|
+
if (AAD)
|
|
1990
|
+
updatePadded(h, AAD);
|
|
1991
|
+
updatePadded(h, ciphertext);
|
|
1992
|
+
h.update(lengths);
|
|
1993
|
+
const res = h.digest();
|
|
1994
|
+
clean2(authKey, lengths);
|
|
1995
|
+
return res;
|
|
1996
|
+
}
|
|
1997
|
+
var _poly1305_aead = (xorStream) => (key, nonce, AAD) => {
|
|
1998
|
+
const tagLength = 16;
|
|
1999
|
+
return {
|
|
2000
|
+
encrypt(plaintext, output) {
|
|
2001
|
+
const plength = plaintext.length;
|
|
2002
|
+
output = getOutput(plength + tagLength, output, false);
|
|
2003
|
+
output.set(plaintext);
|
|
2004
|
+
const oPlain = output.subarray(0, -tagLength);
|
|
2005
|
+
xorStream(key, nonce, oPlain, oPlain, 1);
|
|
2006
|
+
const tag = computeTag(xorStream, key, nonce, oPlain, AAD);
|
|
2007
|
+
output.set(tag, plength);
|
|
2008
|
+
clean2(tag);
|
|
2009
|
+
return output;
|
|
2010
|
+
},
|
|
2011
|
+
decrypt(ciphertext, output) {
|
|
2012
|
+
output = getOutput(ciphertext.length - tagLength, output, false);
|
|
2013
|
+
const data = ciphertext.subarray(0, -tagLength);
|
|
2014
|
+
const passedTag = ciphertext.subarray(-tagLength);
|
|
2015
|
+
const tag = computeTag(xorStream, key, nonce, data, AAD);
|
|
2016
|
+
if (!equalBytes(passedTag, tag)) {
|
|
2017
|
+
clean2(tag);
|
|
2018
|
+
throw new Error("invalid tag");
|
|
2019
|
+
}
|
|
2020
|
+
output.set(ciphertext.subarray(0, -tagLength));
|
|
2021
|
+
xorStream(key, nonce, output, output, 1);
|
|
2022
|
+
clean2(tag);
|
|
2023
|
+
return output;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
};
|
|
2027
|
+
var xchacha20poly1305 = /* @__PURE__ */ wrapCipher(
|
|
2028
|
+
{ blockSize: 64, nonceLength: 24, tagLength: 16 },
|
|
2029
|
+
/* @__PURE__ */ _poly1305_aead(xchacha20)
|
|
2030
|
+
);
|
|
2031
|
+
|
|
2032
|
+
// core/field-encryption.ts
|
|
2033
|
+
var FIELD_ENVELOPE_PREFIX = "enc1.";
|
|
2034
|
+
var FIELD_ENVELOPE_NONCE_BYTES = 24;
|
|
2035
|
+
var POLY1305_TAG_BYTES = 16;
|
|
2036
|
+
var B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
2037
|
+
var B64_LOOKUP = {};
|
|
2038
|
+
for (let i = 0; i < B64_ALPHABET.length; i++) B64_LOOKUP[B64_ALPHABET[i]] = i;
|
|
2039
|
+
function base64Encode(bytes) {
|
|
2040
|
+
let out = "";
|
|
2041
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
2042
|
+
const b0 = bytes[i];
|
|
2043
|
+
const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
2044
|
+
const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
2045
|
+
out += B64_ALPHABET[b0 >> 2];
|
|
2046
|
+
out += B64_ALPHABET[(b0 & 3) << 4 | b1 >> 4];
|
|
2047
|
+
out += i + 1 < bytes.length ? B64_ALPHABET[(b1 & 15) << 2 | b2 >> 6] : "=";
|
|
2048
|
+
out += i + 2 < bytes.length ? B64_ALPHABET[b2 & 63] : "=";
|
|
2049
|
+
}
|
|
2050
|
+
return out;
|
|
2051
|
+
}
|
|
2052
|
+
function base64Decode(s) {
|
|
2053
|
+
if (s.length % 4 !== 0) {
|
|
2054
|
+
throw new SphereError("Invalid field envelope: base64 length not a multiple of 4", "DECRYPTION_ERROR");
|
|
2055
|
+
}
|
|
2056
|
+
let padding = 0;
|
|
2057
|
+
if (s.endsWith("==")) padding = 2;
|
|
2058
|
+
else if (s.endsWith("=")) padding = 1;
|
|
2059
|
+
const body = s.slice(0, s.length - padding);
|
|
2060
|
+
if (body.includes("=")) {
|
|
2061
|
+
throw new SphereError("Invalid field envelope: misplaced base64 padding", "DECRYPTION_ERROR");
|
|
2062
|
+
}
|
|
2063
|
+
const out = new Uint8Array(s.length / 4 * 3 - padding);
|
|
2064
|
+
let o = 0;
|
|
2065
|
+
for (let i = 0; i < body.length; i += 4) {
|
|
2066
|
+
const n0 = B64_LOOKUP[body[i]];
|
|
2067
|
+
const n1 = B64_LOOKUP[body[i + 1]];
|
|
2068
|
+
const n2 = body[i + 2] !== void 0 ? B64_LOOKUP[body[i + 2]] : 0;
|
|
2069
|
+
const n3 = body[i + 3] !== void 0 ? B64_LOOKUP[body[i + 3]] : 0;
|
|
2070
|
+
if (n0 === void 0 || n1 === void 0 || n2 === void 0 || n3 === void 0) {
|
|
2071
|
+
throw new SphereError("Invalid field envelope: non-base64 character", "DECRYPTION_ERROR");
|
|
2072
|
+
}
|
|
2073
|
+
const triple = n0 << 18 | n1 << 12 | n2 << 6 | n3;
|
|
2074
|
+
if (o < out.length) out[o++] = triple >> 16 & 255;
|
|
2075
|
+
if (o < out.length) out[o++] = triple >> 8 & 255;
|
|
2076
|
+
if (o < out.length) out[o++] = triple & 255;
|
|
2077
|
+
}
|
|
2078
|
+
return out;
|
|
2079
|
+
}
|
|
2080
|
+
function encryptFieldBytes(key, plaintext, aad) {
|
|
2081
|
+
const nonce = randomBytes(FIELD_ENVELOPE_NONCE_BYTES);
|
|
2082
|
+
const ciphertext = xchacha20poly1305(key, nonce, aad).encrypt(plaintext);
|
|
2083
|
+
const packed = new Uint8Array(nonce.length + ciphertext.length);
|
|
2084
|
+
packed.set(nonce, 0);
|
|
2085
|
+
packed.set(ciphertext, nonce.length);
|
|
2086
|
+
return FIELD_ENVELOPE_PREFIX + base64Encode(packed);
|
|
2087
|
+
}
|
|
2088
|
+
function decryptFieldBytes(key, envelope, aad) {
|
|
2089
|
+
if (typeof envelope !== "string" || !envelope.startsWith(FIELD_ENVELOPE_PREFIX)) {
|
|
2090
|
+
throw new SphereError('Invalid field envelope: missing "enc1." prefix', "DECRYPTION_ERROR");
|
|
2091
|
+
}
|
|
2092
|
+
const packed = base64Decode(envelope.slice(FIELD_ENVELOPE_PREFIX.length));
|
|
2093
|
+
if (packed.length < FIELD_ENVELOPE_NONCE_BYTES + POLY1305_TAG_BYTES) {
|
|
2094
|
+
throw new SphereError("Invalid field envelope: too short for nonce + tag", "DECRYPTION_ERROR");
|
|
2095
|
+
}
|
|
2096
|
+
const nonce = packed.slice(0, FIELD_ENVELOPE_NONCE_BYTES);
|
|
2097
|
+
const ciphertext = packed.slice(FIELD_ENVELOPE_NONCE_BYTES);
|
|
2098
|
+
try {
|
|
2099
|
+
return xchacha20poly1305(key, nonce, aad).decrypt(ciphertext);
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
throw new SphereError("Field envelope authentication failed (tampered, wrong key, or wrong AAD)", "DECRYPTION_ERROR", err);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
function encryptField(key, plaintext) {
|
|
2105
|
+
return encryptFieldBytes(key, new TextEncoder().encode(plaintext));
|
|
2106
|
+
}
|
|
2107
|
+
function decryptField(key, envelope) {
|
|
2108
|
+
return new TextDecoder().decode(decryptFieldBytes(key, envelope));
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// modules/payments-v2/history/History.ts
|
|
2112
|
+
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
2113
|
+
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
2114
|
+
var History = class {
|
|
2115
|
+
constructor(deps) {
|
|
2116
|
+
this.deps = deps;
|
|
2117
|
+
}
|
|
2118
|
+
// ── read-through ───────────────────────────────────────────────────────────
|
|
2119
|
+
async page(opts = {}) {
|
|
2120
|
+
const result = await this.deps.client.listHistory({
|
|
2121
|
+
...opts.before !== void 0 ? { before: opts.before } : {},
|
|
2122
|
+
...opts.limit !== void 0 ? { limit: opts.limit } : {}
|
|
2123
|
+
});
|
|
2124
|
+
return {
|
|
2125
|
+
entries: result.records.map((record) => this.toEntry(record)),
|
|
2126
|
+
more: result.more,
|
|
2127
|
+
cursor: result.cursor
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
// ── client POSTs (best-effort — NEVER throw into the money path) ────────────
|
|
2131
|
+
async recordSent(input) {
|
|
2132
|
+
await this.post(() => ({
|
|
2133
|
+
dedupKey: input.transferId,
|
|
2134
|
+
id: (this.deps.newId ?? randomUUID)(),
|
|
2135
|
+
type: "SENT",
|
|
2136
|
+
assets: [{ coinId: input.coinId, amount: input.amount }],
|
|
2137
|
+
ts: this.ts(input.timestamp),
|
|
2138
|
+
transferId: input.transferId,
|
|
2139
|
+
...this.wireTokenId(input.tokenId),
|
|
2140
|
+
...this.wireCounterparty(input.recipientPubkey, input.recipientNametag),
|
|
2141
|
+
...this.wireMemo(input.memo)
|
|
2142
|
+
}));
|
|
2143
|
+
}
|
|
2144
|
+
async recordReceived(input) {
|
|
2145
|
+
await this.post(() => ({
|
|
2146
|
+
dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
|
|
2147
|
+
id: (this.deps.newId ?? randomUUID)(),
|
|
2148
|
+
type: "RECEIVED",
|
|
2149
|
+
assets: [{ coinId: input.coinId, amount: input.amount }],
|
|
2150
|
+
ts: this.ts(input.timestamp),
|
|
2151
|
+
...input.transferId !== void 0 ? { transferId: input.transferId } : {},
|
|
2152
|
+
...this.wireTokenId(input.tokenId),
|
|
2153
|
+
...this.wireCounterparty(input.senderPubkey, input.senderNametag),
|
|
2154
|
+
...this.wireMemo(input.memo)
|
|
2155
|
+
}));
|
|
2156
|
+
}
|
|
2157
|
+
async recordMint(input) {
|
|
2158
|
+
await this.post(() => ({
|
|
2159
|
+
dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
|
|
2160
|
+
id: (this.deps.newId ?? randomUUID)(),
|
|
2161
|
+
type: "MINT",
|
|
2162
|
+
assets: [{ coinId: input.coinId, amount: input.amount }],
|
|
2163
|
+
ts: this.ts(input.timestamp),
|
|
2164
|
+
...this.wireTokenId(input.tokenId)
|
|
2165
|
+
}));
|
|
2166
|
+
}
|
|
2167
|
+
// ── internals ────────────────────────────────────────────────────────────────
|
|
2168
|
+
async post(build) {
|
|
2169
|
+
try {
|
|
2170
|
+
await this.deps.client.postHistory([build()]);
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
this.deps.emit("history:updated");
|
|
2176
|
+
}
|
|
2177
|
+
ts(timestamp) {
|
|
2178
|
+
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
2179
|
+
}
|
|
2180
|
+
wireTokenId(tokenId) {
|
|
2181
|
+
const lower = tokenId?.toLowerCase();
|
|
2182
|
+
return lower !== void 0 && WIRE_TOKEN_ID.test(lower) ? { tokenId: lower } : {};
|
|
2183
|
+
}
|
|
2184
|
+
// Server zod is strict — a non-pubkey counterparty stays off the wire; the
|
|
2185
|
+
// nametag rides regardless (S6-encrypted, operator-blind).
|
|
2186
|
+
wireCounterparty(pubkey, nametag) {
|
|
2187
|
+
const lower = pubkey?.toLowerCase();
|
|
2188
|
+
return {
|
|
2189
|
+
...lower !== void 0 && WIRE_PUBKEY.test(lower) ? { counterpartyPubkey: lower } : {},
|
|
2190
|
+
...nametag !== void 0 ? { counterpartyNametag: encryptField(this.deps.fieldKey, nametag) } : {}
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
wireMemo(memo) {
|
|
2194
|
+
return memo !== void 0 ? { memo: encryptField(this.deps.fieldKey, memo) } : {};
|
|
2195
|
+
}
|
|
2196
|
+
// Consumed mapping: wire `ts` (ISO 8601, any offset) → epoch-ms `timestamp`;
|
|
2197
|
+
// coinId/amount from the FIRST asset (multi-asset remainder surfaces via
|
|
2198
|
+
// tokenIds[]); counterparty lands on the role-appropriate side by type.
|
|
2199
|
+
toEntry(wire) {
|
|
2200
|
+
const first = wire.assets[0];
|
|
2201
|
+
const coinId = first?.coinId ?? "";
|
|
2202
|
+
const received = wire.type === "RECEIVED";
|
|
2203
|
+
const memo = this.tryDecrypt(wire.memo);
|
|
2204
|
+
const nametag = this.tryDecrypt(wire.counterpartyNametag);
|
|
2205
|
+
return {
|
|
2206
|
+
id: wire.id,
|
|
2207
|
+
type: wire.type,
|
|
2208
|
+
coinId,
|
|
2209
|
+
amount: first?.amount ?? "0",
|
|
2210
|
+
symbol: this.deps.registry.getSymbol(coinId),
|
|
2211
|
+
timestamp: Date.parse(wire.ts),
|
|
2212
|
+
...wire.transferId !== void 0 ? { transferId: wire.transferId } : {},
|
|
2213
|
+
...wire.tokenId !== void 0 ? { tokenId: wire.tokenId } : {},
|
|
2214
|
+
...wire.assets.length > 1 ? { tokenIds: wire.assets.map((asset) => ({ id: asset.coinId, amount: asset.amount })) } : {},
|
|
2215
|
+
...wire.counterpartyPubkey !== void 0 ? received ? { senderPubkey: wire.counterpartyPubkey } : { recipientPubkey: wire.counterpartyPubkey } : {},
|
|
2216
|
+
...nametag !== void 0 ? received ? { senderNametag: nametag } : { recipientNametag: nametag } : {},
|
|
2217
|
+
...memo !== void 0 ? { memo } : {}
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
// Self-scoped decrypt: a foreign/undecryptable/plaintext value is absent,
|
|
2221
|
+
// never ciphertext and never a throw.
|
|
2222
|
+
tryDecrypt(envelope) {
|
|
2223
|
+
if (envelope === void 0) return void 0;
|
|
2224
|
+
try {
|
|
2225
|
+
return decryptField(this.deps.fieldKey, envelope);
|
|
2226
|
+
} catch {
|
|
2227
|
+
return void 0;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
|
|
2232
|
+
// modules/payments-v2/inventory/InventoryView.ts
|
|
2233
|
+
var SUSPECTED_KEY = "suspected-spent";
|
|
2234
|
+
var KNOWN_SPENDS_KEY = "known-spends";
|
|
2235
|
+
function stateKey(tokenId, stateHash) {
|
|
2236
|
+
return `${tokenId}:${stateHash}`;
|
|
2237
|
+
}
|
|
2238
|
+
var InventoryView = class {
|
|
2239
|
+
constructor(deps) {
|
|
2240
|
+
this.deps = deps;
|
|
2241
|
+
}
|
|
2242
|
+
mirror = /* @__PURE__ */ new Map();
|
|
2243
|
+
suspected = /* @__PURE__ */ new Set();
|
|
2244
|
+
knownSpends = /* @__PURE__ */ new Set();
|
|
2245
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
2246
|
+
firstPullOk = false;
|
|
2247
|
+
hydrated = null;
|
|
2248
|
+
chain = Promise.resolve();
|
|
2249
|
+
overlayChain = Promise.resolve();
|
|
2250
|
+
sharedPull = null;
|
|
2251
|
+
fullPull() {
|
|
2252
|
+
if (this.sharedPull) return this.sharedPull;
|
|
2253
|
+
const run = this.enqueue(() => this.doFullPull(false)).finally(() => {
|
|
2254
|
+
this.sharedPull = null;
|
|
2255
|
+
});
|
|
2256
|
+
this.sharedPull = run;
|
|
2257
|
+
return run;
|
|
2258
|
+
}
|
|
2259
|
+
delta(since) {
|
|
2260
|
+
return this.enqueue(async () => {
|
|
2261
|
+
await this.hydrate();
|
|
2262
|
+
if (!this.firstPullOk) {
|
|
2263
|
+
await this.doFullPull(false);
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
const flags = { changed: false };
|
|
2267
|
+
try {
|
|
2268
|
+
await this.runDeltaLoop(since, void 0, flags);
|
|
2269
|
+
} finally {
|
|
2270
|
+
if (flags.changed) this.deps.emit("inventory:updated");
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
// Epoch reset (§5.1): full re-pull with per-key replace; entries absent from
|
|
2275
|
+
// the restored server drop only AFTER the new snapshot is fully applied.
|
|
2276
|
+
onEpochReset() {
|
|
2277
|
+
return this.enqueue(() => this.doFullPull(true));
|
|
2278
|
+
}
|
|
2279
|
+
async demote(tokenId, stateHash) {
|
|
2280
|
+
if (tokenId === "" || stateHash === "") {
|
|
2281
|
+
throw new Error("demote requires both tokenId and stateHash");
|
|
2282
|
+
}
|
|
2283
|
+
const key = stateKey(tokenId, stateHash);
|
|
2284
|
+
if (this.suspected.has(key)) return;
|
|
2285
|
+
const added = await this.inOverlayChain(async () => {
|
|
2286
|
+
await this.hydrate();
|
|
2287
|
+
if (this.suspected.has(key)) return false;
|
|
2288
|
+
await this.deps.kv.set(SUSPECTED_KEY, [...this.suspected, key]);
|
|
2289
|
+
this.suspected.add(key);
|
|
2290
|
+
return true;
|
|
2291
|
+
});
|
|
2292
|
+
if (added) this.deps.emit("inventory:updated");
|
|
2293
|
+
}
|
|
2294
|
+
recoverRemoved(getBlobs, isSpent, reAdd) {
|
|
2295
|
+
return this.enqueue(() => this.doRecover(getBlobs, isSpent, reAdd));
|
|
2296
|
+
}
|
|
2297
|
+
markInFlight(tokenId) {
|
|
2298
|
+
if (this.inFlight.has(tokenId)) return;
|
|
2299
|
+
this.inFlight.add(tokenId);
|
|
2300
|
+
this.deps.emit("inventory:updated");
|
|
2301
|
+
}
|
|
2302
|
+
release(tokenId) {
|
|
2303
|
+
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
2304
|
+
}
|
|
2305
|
+
pool(coinId) {
|
|
2306
|
+
const out = [];
|
|
2307
|
+
for (const [tokenId, entry] of this.mirror) {
|
|
2308
|
+
if (entry.status !== "active" || this.inFlight.has(tokenId)) continue;
|
|
2309
|
+
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
2310
|
+
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
2311
|
+
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
2312
|
+
}
|
|
2313
|
+
return out;
|
|
2314
|
+
}
|
|
2315
|
+
tokens(registry, filter) {
|
|
2316
|
+
const out = [];
|
|
2317
|
+
for (const [tokenId, entry] of this.mirror) {
|
|
2318
|
+
if (entry.status !== "active") continue;
|
|
2319
|
+
const asset = entry.assets[0];
|
|
2320
|
+
if (!asset) continue;
|
|
2321
|
+
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
2322
|
+
out.push(this.toToken(tokenId, entry, asset, registry));
|
|
2323
|
+
}
|
|
2324
|
+
return out;
|
|
2325
|
+
}
|
|
2326
|
+
async assets(registry, price) {
|
|
2327
|
+
const raw = [];
|
|
2328
|
+
for (const [coinId, totals] of this.aggregate()) {
|
|
2329
|
+
raw.push(this.toAsset(coinId, totals, registry));
|
|
2330
|
+
}
|
|
2331
|
+
if (!price || raw.length === 0) return raw;
|
|
2332
|
+
return this.withPrices(raw, registry, price);
|
|
2333
|
+
}
|
|
2334
|
+
enqueue(fn) {
|
|
2335
|
+
const run = this.chain.then(fn, fn);
|
|
2336
|
+
this.chain = run.then(
|
|
2337
|
+
() => void 0,
|
|
2338
|
+
() => void 0
|
|
2339
|
+
);
|
|
2340
|
+
return run;
|
|
2341
|
+
}
|
|
2342
|
+
inOverlayChain(fn) {
|
|
2343
|
+
const run = this.overlayChain.then(fn, fn);
|
|
2344
|
+
this.overlayChain = run.then(
|
|
2345
|
+
() => void 0,
|
|
2346
|
+
() => void 0
|
|
2347
|
+
);
|
|
2348
|
+
return run;
|
|
2349
|
+
}
|
|
2350
|
+
hydrate() {
|
|
2351
|
+
this.hydrated ??= this.loadDurable().catch((error) => {
|
|
2352
|
+
this.hydrated = null;
|
|
2353
|
+
throw error;
|
|
2354
|
+
});
|
|
2355
|
+
return this.hydrated;
|
|
2356
|
+
}
|
|
2357
|
+
async loadDurable() {
|
|
2358
|
+
const suspected = await this.deps.kv.get(SUSPECTED_KEY);
|
|
2359
|
+
for (const key of suspected ?? []) this.suspected.add(key);
|
|
2360
|
+
const known = await this.deps.kv.get(KNOWN_SPENDS_KEY);
|
|
2361
|
+
for (const key of known ?? []) this.knownSpends.add(key);
|
|
2362
|
+
}
|
|
2363
|
+
now() {
|
|
2364
|
+
return (this.deps.now ?? Date.now)();
|
|
2365
|
+
}
|
|
2366
|
+
async doFullPull(reconcileAbsent) {
|
|
2367
|
+
await this.hydrate();
|
|
2368
|
+
const seen = reconcileAbsent ? /* @__PURE__ */ new Set() : void 0;
|
|
2369
|
+
const flags = { changed: false };
|
|
2370
|
+
try {
|
|
2371
|
+
let page = await this.deps.port.listInventory();
|
|
2372
|
+
const firstCursor = page.cursor;
|
|
2373
|
+
if (this.applyItems(page.items, seen)) flags.changed = true;
|
|
2374
|
+
while (page.more) {
|
|
2375
|
+
page = await this.deps.port.listInventory(page.cursor);
|
|
2376
|
+
if (this.applyItems(page.items, seen)) flags.changed = true;
|
|
2377
|
+
}
|
|
2378
|
+
await this.runDeltaLoop(firstCursor, seen, flags);
|
|
2379
|
+
if (seen && this.dropAbsent(seen)) flags.changed = true;
|
|
2380
|
+
this.firstPullOk = true;
|
|
2381
|
+
} finally {
|
|
2382
|
+
if (flags.changed) this.deps.emit("inventory:updated");
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
async runDeltaLoop(since, seen, flags) {
|
|
2386
|
+
let page = await this.deps.port.listInventory(since);
|
|
2387
|
+
if (this.applyItems(page.items, seen)) flags.changed = true;
|
|
2388
|
+
while (page.more) {
|
|
2389
|
+
page = await this.deps.port.listInventory(page.cursor);
|
|
2390
|
+
if (this.applyItems(page.items, seen)) flags.changed = true;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
// Synchronous per-key application; the overlay prune runs in this same
|
|
2394
|
+
// synchronous step (F7) — never against a cleared or half-filled view.
|
|
2395
|
+
applyItems(items, seen) {
|
|
2396
|
+
const now = this.now();
|
|
2397
|
+
let changed = false;
|
|
2398
|
+
for (const item of items) {
|
|
2399
|
+
seen?.add(item.tokenId);
|
|
2400
|
+
if (this.applyOne(item, now)) changed = true;
|
|
2401
|
+
}
|
|
2402
|
+
const pruned = this.pruneOverlaySync(false);
|
|
2403
|
+
return changed || pruned;
|
|
2404
|
+
}
|
|
2405
|
+
applyOne(item, now) {
|
|
2406
|
+
const prev = this.mirror.get(item.tokenId);
|
|
2407
|
+
if (prev && item.seq < prev.seq) return false;
|
|
2408
|
+
if (prev && prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash) {
|
|
2409
|
+
return false;
|
|
2410
|
+
}
|
|
2411
|
+
this.mirror.set(item.tokenId, {
|
|
2412
|
+
stateHash: item.stateHash,
|
|
2413
|
+
seq: item.seq,
|
|
2414
|
+
status: item.status,
|
|
2415
|
+
assets: item.assets ?? prev?.assets ?? [],
|
|
2416
|
+
createdAt: prev?.createdAt ?? now,
|
|
2417
|
+
updatedAt: now
|
|
2418
|
+
});
|
|
2419
|
+
return true;
|
|
2420
|
+
}
|
|
2421
|
+
// Prunes only on positive evidence (state advance or tombstone). Absence is
|
|
2422
|
+
// evidence only when the caller holds a complete post-reset snapshot.
|
|
2423
|
+
pruneOverlaySync(allowAbsent) {
|
|
2424
|
+
let pruned = false;
|
|
2425
|
+
for (const key of this.suspected) {
|
|
2426
|
+
const cut = key.lastIndexOf(":");
|
|
2427
|
+
const entry = this.mirror.get(key.slice(0, cut));
|
|
2428
|
+
const live = entry !== void 0 ? entry.status === "active" && entry.stateHash === key.slice(cut + 1) : !allowAbsent;
|
|
2429
|
+
if (!live) {
|
|
2430
|
+
this.suspected.delete(key);
|
|
2431
|
+
pruned = true;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
if (pruned) this.schedulePersistSuspected();
|
|
2435
|
+
return pruned;
|
|
2436
|
+
}
|
|
2437
|
+
schedulePersistSuspected() {
|
|
2438
|
+
void this.inOverlayChain(() => this.deps.kv.set(SUSPECTED_KEY, [...this.suspected])).catch(
|
|
2439
|
+
() => void 0
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
dropAbsent(seen) {
|
|
2443
|
+
let changed = false;
|
|
2444
|
+
for (const tokenId of [...this.mirror.keys()]) {
|
|
2445
|
+
if (!seen.has(tokenId)) {
|
|
2446
|
+
this.mirror.delete(tokenId);
|
|
2447
|
+
changed = true;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
if (changed) this.pruneOverlaySync(true);
|
|
2451
|
+
return changed;
|
|
2452
|
+
}
|
|
2453
|
+
async doRecover(getBlobs, isSpent, reAdd) {
|
|
2454
|
+
await this.hydrate();
|
|
2455
|
+
const candidates = this.firstPullOk ? this.recoveryCandidates() : [];
|
|
2456
|
+
if (candidates.length === 0) return { recovered: [], confirmedSpent: [] };
|
|
2457
|
+
const blobs = await getBlobs(candidates.map((c) => c.tokenId));
|
|
2458
|
+
const recovered = [];
|
|
2459
|
+
const confirmedSpent = [];
|
|
2460
|
+
try {
|
|
2461
|
+
for (const candidate of candidates) {
|
|
2462
|
+
const blob = blobs.get(candidate.tokenId);
|
|
2463
|
+
if (!blob) continue;
|
|
2464
|
+
const outcome = await this.recoverOne(candidate, blob, isSpent, reAdd);
|
|
2465
|
+
if (outcome === "recovered") recovered.push(candidate.tokenId);
|
|
2466
|
+
if (outcome === "spent") confirmedSpent.push(candidate);
|
|
2467
|
+
}
|
|
2468
|
+
} finally {
|
|
2469
|
+
if (recovered.length > 0) this.deps.emit("inventory:updated");
|
|
2470
|
+
}
|
|
2471
|
+
return { recovered, confirmedSpent };
|
|
2472
|
+
}
|
|
2473
|
+
recoveryCandidates() {
|
|
2474
|
+
const out = [];
|
|
2475
|
+
for (const [tokenId, entry] of this.mirror) {
|
|
2476
|
+
if (entry.status !== "removed") continue;
|
|
2477
|
+
if (this.knownSpends.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
2478
|
+
out.push({ tokenId, stateHash: entry.stateHash });
|
|
2479
|
+
}
|
|
2480
|
+
return out;
|
|
2481
|
+
}
|
|
2482
|
+
async recoverOne(candidate, blob, isSpent, reAdd) {
|
|
2483
|
+
if (await isSpent(candidate.tokenId, blob)) {
|
|
2484
|
+
await this.recordKnownSpend(candidate);
|
|
2485
|
+
return "spent";
|
|
2486
|
+
}
|
|
2487
|
+
if (await reAdd(candidate.tokenId, blob) === "evidenced-tombstone") {
|
|
2488
|
+
await this.recordKnownSpend(candidate);
|
|
2489
|
+
return "spent";
|
|
2490
|
+
}
|
|
2491
|
+
const current = this.mirror.get(candidate.tokenId);
|
|
2492
|
+
if (!current || current.status !== "removed" || current.stateHash !== candidate.stateHash) {
|
|
2493
|
+
return "skipped";
|
|
2494
|
+
}
|
|
2495
|
+
current.status = "active";
|
|
2496
|
+
current.updatedAt = this.now();
|
|
2497
|
+
return "recovered";
|
|
2498
|
+
}
|
|
2499
|
+
async recordKnownSpend(spend) {
|
|
2500
|
+
if (spend.tokenId === "" || spend.stateHash === "") {
|
|
2501
|
+
throw new Error("knownSpend requires both tokenId and stateHash");
|
|
2502
|
+
}
|
|
2503
|
+
this.knownSpends.add(stateKey(spend.tokenId, spend.stateHash));
|
|
2504
|
+
await this.deps.kv.set(KNOWN_SPENDS_KEY, [...this.knownSpends]);
|
|
2505
|
+
}
|
|
2506
|
+
toToken(tokenId, entry, asset, registry) {
|
|
2507
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
2508
|
+
const suspected = this.suspected.has(stateKey(tokenId, entry.stateHash));
|
|
2509
|
+
return {
|
|
2510
|
+
id: tokenId,
|
|
2511
|
+
coinId: asset.coinId,
|
|
2512
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
2513
|
+
name: registry.getName(asset.coinId),
|
|
2514
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
2515
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
2516
|
+
amount: asset.amount,
|
|
2517
|
+
status: this.inFlight.has(tokenId) ? "transferring" : "confirmed",
|
|
2518
|
+
createdAt: entry.createdAt,
|
|
2519
|
+
updatedAt: entry.updatedAt,
|
|
2520
|
+
lazy: true,
|
|
2521
|
+
...suspected ? { suspectedSpent: true } : {}
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
aggregate() {
|
|
2525
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2526
|
+
for (const [tokenId, entry] of this.mirror) {
|
|
2527
|
+
if (entry.status !== "active") continue;
|
|
2528
|
+
const moving = this.inFlight.has(tokenId);
|
|
2529
|
+
for (const asset of entry.assets) {
|
|
2530
|
+
let group = groups.get(asset.coinId);
|
|
2531
|
+
if (!group) {
|
|
2532
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
2533
|
+
groups.set(asset.coinId, group);
|
|
2534
|
+
}
|
|
2535
|
+
if (moving) {
|
|
2536
|
+
group.transferring += BigInt(asset.amount);
|
|
2537
|
+
group.transferringCount += 1;
|
|
2538
|
+
} else {
|
|
2539
|
+
group.total += BigInt(asset.amount);
|
|
2540
|
+
group.count += 1;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return groups;
|
|
2545
|
+
}
|
|
2546
|
+
toAsset(coinId, totals, registry) {
|
|
2547
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
2548
|
+
return {
|
|
2549
|
+
coinId,
|
|
2550
|
+
symbol: registry.getSymbol(coinId),
|
|
2551
|
+
name: registry.getName(coinId),
|
|
2552
|
+
decimals: registry.getDecimals(coinId),
|
|
2553
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
2554
|
+
totalAmount: totals.total.toString(),
|
|
2555
|
+
tokenCount: totals.count,
|
|
2556
|
+
confirmedAmount: totals.total.toString(),
|
|
2557
|
+
unconfirmedAmount: "0",
|
|
2558
|
+
confirmedTokenCount: totals.count,
|
|
2559
|
+
unconfirmedTokenCount: 0,
|
|
2560
|
+
transferringTokenCount: totals.transferringCount,
|
|
2561
|
+
transferringAmount: totals.transferring.toString(),
|
|
2562
|
+
priceUsd: null,
|
|
2563
|
+
priceEur: null,
|
|
2564
|
+
change24h: null,
|
|
2565
|
+
fiatValueUsd: null,
|
|
2566
|
+
fiatValueEur: null
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
async withPrices(raw, registry, price) {
|
|
2570
|
+
try {
|
|
2571
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
2572
|
+
const quotes = await price.getPrices(names);
|
|
2573
|
+
return raw.map((a) => this.priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
2574
|
+
} catch {
|
|
2575
|
+
return raw;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
// Fiat display conversion is the one sanctioned Number use (display only).
|
|
2579
|
+
priceOne(asset, quote) {
|
|
2580
|
+
if (!quote) return asset;
|
|
2581
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
2582
|
+
return {
|
|
2583
|
+
...asset,
|
|
2584
|
+
priceUsd: quote.priceUsd,
|
|
2585
|
+
priceEur: quote.priceEur ?? null,
|
|
2586
|
+
change24h: quote.change24h ?? null,
|
|
2587
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
2588
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
|
|
2593
|
+
// modules/payments-v2/receive/Receive.ts
|
|
2594
|
+
var ACK_BATCH_SIZE = 200;
|
|
2595
|
+
var POLL_INTERVAL_MS = 3e4;
|
|
2596
|
+
function receivedDedupKey(tokenId, stateHash) {
|
|
2597
|
+
return `RECEIVED:${tokenId}:${stateHash}`;
|
|
2598
|
+
}
|
|
2599
|
+
var Receive = class {
|
|
2600
|
+
constructor(deps) {
|
|
2601
|
+
this.deps = deps;
|
|
2602
|
+
}
|
|
2603
|
+
inFlight = null;
|
|
2604
|
+
pollTimer = null;
|
|
2605
|
+
unsubscribeWake = null;
|
|
2606
|
+
drainOnce() {
|
|
2607
|
+
if (this.inFlight) return this.inFlight;
|
|
2608
|
+
const run = this.doDrain().finally(() => {
|
|
2609
|
+
this.inFlight = null;
|
|
2610
|
+
});
|
|
2611
|
+
this.inFlight = run;
|
|
2612
|
+
return run;
|
|
2613
|
+
}
|
|
2614
|
+
start(pollIntervalMs = POLL_INTERVAL_MS) {
|
|
2615
|
+
this.unsubscribeWake ??= this.deps.delivery.onWake?.(() => {
|
|
2616
|
+
void this.drainOnce();
|
|
2617
|
+
}) ?? null;
|
|
2618
|
+
this.pollTimer ??= setInterval(() => {
|
|
2619
|
+
void this.drainOnce();
|
|
2620
|
+
}, pollIntervalMs);
|
|
2621
|
+
}
|
|
2622
|
+
stop() {
|
|
2623
|
+
if (this.pollTimer !== null) clearInterval(this.pollTimer);
|
|
2624
|
+
this.pollTimer = null;
|
|
2625
|
+
this.unsubscribeWake?.();
|
|
2626
|
+
this.unsubscribeWake = null;
|
|
2627
|
+
}
|
|
2628
|
+
async doDrain() {
|
|
2629
|
+
const deps = this.deps;
|
|
2630
|
+
const stored = [];
|
|
2631
|
+
const pending = [];
|
|
2632
|
+
let epoch = "";
|
|
2633
|
+
try {
|
|
2634
|
+
epoch = deps.syncEpoch();
|
|
2635
|
+
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
2636
|
+
const since = record !== null && record.syncEpoch === epoch ? String(record.cursor) : void 0;
|
|
2637
|
+
for await (const entry of deps.delivery.incoming(since)) {
|
|
2638
|
+
await this.processEntry(deps, entry, pending, stored);
|
|
2639
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, epoch);
|
|
2640
|
+
}
|
|
2641
|
+
await flushAcks(deps, pending, epoch);
|
|
2642
|
+
} catch {
|
|
2643
|
+
await flushAcks(deps, pending, epoch).catch(() => void 0);
|
|
2644
|
+
}
|
|
2645
|
+
return stored;
|
|
2646
|
+
}
|
|
2647
|
+
async processEntry(deps, entry, pending, stored) {
|
|
2648
|
+
const screened = await screen(deps, entry);
|
|
2649
|
+
if (screened.kind === "ack") {
|
|
2650
|
+
pending.push(screened.ack);
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
const outcome = await deps.view.store(screened.record);
|
|
2654
|
+
if (outcome === "rejected") {
|
|
2655
|
+
pending.push(rejectAck(entry, "storage-rejected"));
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
pending.push(claimAck(entry));
|
|
2659
|
+
const transfer = await announce(deps, entry, screened.record);
|
|
2660
|
+
stored.push(transfer);
|
|
2661
|
+
deps.emit("transfer:incoming", transfer);
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2664
|
+
async function screen(deps, entry) {
|
|
2665
|
+
const blobBytes = await entry.fetchBlob();
|
|
2666
|
+
let token;
|
|
2667
|
+
try {
|
|
2668
|
+
token = await deps.engine.decodeToken({
|
|
2669
|
+
v: TOKEN_BLOB_VERSION,
|
|
2670
|
+
network: 0,
|
|
2671
|
+
tokenId: "",
|
|
2672
|
+
token: blobBytes
|
|
2673
|
+
});
|
|
2674
|
+
} catch {
|
|
2675
|
+
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
2676
|
+
}
|
|
2677
|
+
const verdict = await deps.engine.verify(token);
|
|
2678
|
+
if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
2679
|
+
if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
|
|
2680
|
+
return { kind: "ack", ack: rejectAck(entry, "not-owned") };
|
|
2681
|
+
}
|
|
2682
|
+
const keys = await deps.engine.deliveryKeys(blobBytes);
|
|
2683
|
+
const held = deps.view.heldState(keys.tokenId);
|
|
2684
|
+
if (held === keys.stateHash) return { kind: "ack", ack: claimAck(entry) };
|
|
2685
|
+
if (held !== null && await deps.engine.isSpent(token)) {
|
|
2686
|
+
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
2687
|
+
}
|
|
2688
|
+
return {
|
|
2689
|
+
kind: "accept",
|
|
2690
|
+
record: { tokenId: keys.tokenId, stateHash: keys.stateHash, assets: toAssetAmounts(token) }
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
async function announce(deps, entry, record) {
|
|
2694
|
+
const receivedAt = (deps.now ?? Date.now)();
|
|
2695
|
+
try {
|
|
2696
|
+
await deps.recordReceived({
|
|
2697
|
+
dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
|
|
2698
|
+
tokenId: record.tokenId,
|
|
2699
|
+
stateHash: record.stateHash,
|
|
2700
|
+
assets: record.assets,
|
|
2701
|
+
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
2702
|
+
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
2703
|
+
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
2704
|
+
receivedAt
|
|
2705
|
+
});
|
|
2706
|
+
} catch {
|
|
2707
|
+
}
|
|
2708
|
+
return {
|
|
2709
|
+
id: record.tokenId,
|
|
2710
|
+
senderPubkey: entry.senderPubkey ?? "",
|
|
2711
|
+
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
2712
|
+
tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
|
|
2713
|
+
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
2714
|
+
receivedAt
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
async function flushAcks(deps, pending, epoch) {
|
|
2718
|
+
let lastAcked = null;
|
|
2719
|
+
try {
|
|
2720
|
+
while (pending.length > 0) {
|
|
2721
|
+
const next = pending[0];
|
|
2722
|
+
await deps.delivery.ack(next.deliveryId, next.disposition, next.reason);
|
|
2723
|
+
lastAcked = next.cursor;
|
|
2724
|
+
pending.shift();
|
|
2725
|
+
}
|
|
2726
|
+
} finally {
|
|
2727
|
+
if (lastAcked !== null) {
|
|
2728
|
+
const record = { cursor: lastAcked, syncEpoch: epoch };
|
|
2729
|
+
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
function claimAck(entry) {
|
|
2734
|
+
return { deliveryId: entry.deliveryId, disposition: "claimed", cursor: entry.cursor };
|
|
2735
|
+
}
|
|
2736
|
+
function rejectAck(entry, reason) {
|
|
2737
|
+
return { deliveryId: entry.deliveryId, disposition: "rejected", reason, cursor: entry.cursor };
|
|
2738
|
+
}
|
|
2739
|
+
function toAssetAmounts(token) {
|
|
2740
|
+
return (token.value?.assets ?? []).map((asset) => ({
|
|
2741
|
+
coinId: asset.coinId,
|
|
2742
|
+
amount: asset.amount.toString()
|
|
2743
|
+
}));
|
|
2744
|
+
}
|
|
2745
|
+
function toUiToken(tokenId, asset, registry, receivedAt) {
|
|
2746
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
2747
|
+
return {
|
|
2748
|
+
id: tokenId,
|
|
2749
|
+
coinId: asset.coinId,
|
|
2750
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
2751
|
+
name: registry.getName(asset.coinId),
|
|
2752
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
2753
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
2754
|
+
amount: asset.amount,
|
|
2755
|
+
status: "confirmed",
|
|
2756
|
+
createdAt: receivedAt,
|
|
2757
|
+
updatedAt: receivedAt,
|
|
2758
|
+
lazy: true
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// core/logger.ts
|
|
2763
|
+
var LOGGER_KEY = "__sphere_sdk_logger__";
|
|
2764
|
+
function getState() {
|
|
2765
|
+
const g = globalThis;
|
|
2766
|
+
if (!g[LOGGER_KEY]) {
|
|
2767
|
+
g[LOGGER_KEY] = { debug: false, tags: {}, handler: null };
|
|
2768
|
+
}
|
|
2769
|
+
return g[LOGGER_KEY];
|
|
2770
|
+
}
|
|
2771
|
+
function isEnabled(tag) {
|
|
2772
|
+
const state = getState();
|
|
2773
|
+
if (tag in state.tags) return state.tags[tag];
|
|
2774
|
+
return state.debug;
|
|
2775
|
+
}
|
|
2776
|
+
var logger = {
|
|
2777
|
+
/**
|
|
2778
|
+
* Configure the logger. Can be called multiple times (last write wins).
|
|
2779
|
+
* Typically called by createBrowserProviders(), createNodeProviders(), or Sphere.init().
|
|
2780
|
+
*/
|
|
2781
|
+
configure(config) {
|
|
2782
|
+
const state = getState();
|
|
2783
|
+
if (config.debug !== void 0) state.debug = config.debug;
|
|
2784
|
+
if (config.handler !== void 0) state.handler = config.handler;
|
|
2785
|
+
},
|
|
2786
|
+
/**
|
|
2787
|
+
* Enable/disable debug logging for a specific tag.
|
|
2788
|
+
* Per-tag setting overrides the global debug flag.
|
|
2789
|
+
*
|
|
2790
|
+
* @example
|
|
2791
|
+
* ```ts
|
|
2792
|
+
* logger.setTagDebug('Nostr', true); // enable only Nostr logs
|
|
2793
|
+
* logger.setTagDebug('Nostr', false); // disable Nostr logs even if global debug=true
|
|
2794
|
+
* ```
|
|
2795
|
+
*/
|
|
2796
|
+
setTagDebug(tag, enabled) {
|
|
2797
|
+
getState().tags[tag] = enabled;
|
|
2798
|
+
},
|
|
2799
|
+
/**
|
|
2800
|
+
* Clear per-tag override, falling back to global debug flag.
|
|
2801
|
+
*/
|
|
2802
|
+
clearTagDebug(tag) {
|
|
2803
|
+
delete getState().tags[tag];
|
|
2804
|
+
},
|
|
2805
|
+
/** Returns true if debug mode is enabled for the given tag (or globally). */
|
|
2806
|
+
isDebugEnabled(tag) {
|
|
2807
|
+
if (tag) return isEnabled(tag);
|
|
2808
|
+
return getState().debug;
|
|
2809
|
+
},
|
|
2810
|
+
/**
|
|
2811
|
+
* Debug-level log. Only shown when debug is enabled (globally or for this tag).
|
|
2812
|
+
* Use for detailed operational information.
|
|
2813
|
+
*/
|
|
2814
|
+
debug(tag, message, ...args) {
|
|
2815
|
+
if (!isEnabled(tag)) return;
|
|
2816
|
+
const state = getState();
|
|
2817
|
+
if (state.handler) {
|
|
2818
|
+
state.handler("debug", tag, message, ...args);
|
|
2819
|
+
} else {
|
|
2820
|
+
console.log(`[${tag}]`, message, ...args);
|
|
2821
|
+
}
|
|
2822
|
+
},
|
|
2823
|
+
/**
|
|
2824
|
+
* Warning-level log. ALWAYS shown regardless of debug flag.
|
|
2825
|
+
* Use for important but non-critical issues (timeouts, retries, degraded state).
|
|
2826
|
+
*/
|
|
2827
|
+
warn(tag, message, ...args) {
|
|
2828
|
+
const state = getState();
|
|
2829
|
+
if (state.handler) {
|
|
2830
|
+
state.handler("warn", tag, message, ...args);
|
|
2831
|
+
} else {
|
|
2832
|
+
console.warn(`[${tag}]`, message, ...args);
|
|
2833
|
+
}
|
|
2834
|
+
},
|
|
2835
|
+
/**
|
|
2836
|
+
* Error-level log. ALWAYS shown regardless of debug flag.
|
|
2837
|
+
* Use for critical failures that should never be silenced.
|
|
2838
|
+
*/
|
|
2839
|
+
error(tag, message, ...args) {
|
|
2840
|
+
const state = getState();
|
|
2841
|
+
if (state.handler) {
|
|
2842
|
+
state.handler("error", tag, message, ...args);
|
|
2843
|
+
} else {
|
|
2844
|
+
console.error(`[${tag}]`, message, ...args);
|
|
2845
|
+
}
|
|
2846
|
+
},
|
|
2847
|
+
/** Reset all logger state (debug flag, tags, handler). Primarily for tests. */
|
|
2848
|
+
reset() {
|
|
2849
|
+
const g = globalThis;
|
|
2850
|
+
delete g[LOGGER_KEY];
|
|
2851
|
+
}
|
|
2852
|
+
};
|
|
2853
|
+
|
|
2854
|
+
// modules/payments-v2/requests/Requests.ts
|
|
2855
|
+
var WIRE_STATUS = {
|
|
2856
|
+
open: "pending",
|
|
2857
|
+
paid: "paid",
|
|
2858
|
+
declined: "rejected",
|
|
2859
|
+
expired: "expired"
|
|
2860
|
+
};
|
|
2861
|
+
var TERMINAL = /* @__PURE__ */ new Set(["paid", "rejected", "expired"]);
|
|
2862
|
+
var MAX_HYDRATION_PAGES = 100;
|
|
2863
|
+
var CHAIN_PUBKEY_RE = /^0[23][0-9a-fA-F]{64}$/;
|
|
2864
|
+
var CURSOR_KEY = STORE_KEYS.streamCursor("payment_requests");
|
|
2865
|
+
function isConflict409(err) {
|
|
2866
|
+
if (typeof err !== "object" || err === null) return false;
|
|
2867
|
+
const e = err;
|
|
2868
|
+
return e.status === 409 || e.statusCode === 409 || e.code === "CONFLICT";
|
|
2869
|
+
}
|
|
2870
|
+
var Requests = class {
|
|
2871
|
+
constructor(deps) {
|
|
2872
|
+
this.deps = deps;
|
|
2873
|
+
}
|
|
2874
|
+
mirror = /* @__PURE__ */ new Map();
|
|
2875
|
+
hydrated = false;
|
|
2876
|
+
drainInFlight = null;
|
|
2877
|
+
journal = null;
|
|
2878
|
+
journalLoad = null;
|
|
2879
|
+
journalTail = Promise.resolve();
|
|
2880
|
+
payInFlight = /* @__PURE__ */ new Map();
|
|
2881
|
+
reconcileInFlight = false;
|
|
2882
|
+
now() {
|
|
2883
|
+
return this.deps.now?.() ?? Date.now();
|
|
2884
|
+
}
|
|
2885
|
+
// ── settling journal (#441; single-flight load, serialized RMW) ──────────
|
|
2886
|
+
ensureJournalLoaded() {
|
|
2887
|
+
return this.journalLoad ??= (async () => {
|
|
2888
|
+
let entries = [];
|
|
2889
|
+
try {
|
|
2890
|
+
const raw = await this.deps.kv.get(STORE_KEYS.settlingLinks);
|
|
2891
|
+
if (raw !== null && typeof raw === "object") entries = Object.entries(raw);
|
|
2892
|
+
} catch (err) {
|
|
2893
|
+
logger.error("PaymentsV2", "Settling journal unreadable \u2014 failing open to empty:", err);
|
|
2894
|
+
}
|
|
2895
|
+
this.journal = new Map(entries);
|
|
2896
|
+
})();
|
|
2897
|
+
}
|
|
2898
|
+
mutateJournal(fn) {
|
|
2899
|
+
this.journalTail = this.journalTail.catch(() => void 0).then(async () => {
|
|
2900
|
+
await this.ensureJournalLoaded();
|
|
2901
|
+
if (fn(this.journal)) {
|
|
2902
|
+
await this.deps.kv.set(STORE_KEYS.settlingLinks, Object.fromEntries(this.journal));
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2905
|
+
return this.journalTail;
|
|
2906
|
+
}
|
|
2907
|
+
writeLink(requestId, transferId, committed) {
|
|
2908
|
+
return this.mutateJournal((m) => {
|
|
2909
|
+
m.set(requestId, { requestId, transferId, committed, createdAt: this.now() });
|
|
2910
|
+
return true;
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
clearLink(requestId) {
|
|
2914
|
+
return this.mutateJournal((m) => m.delete(requestId));
|
|
2915
|
+
}
|
|
2916
|
+
// ── incoming stream (gap-free ?since= upsert-by-id) ──────────────────────
|
|
2917
|
+
drainIncoming() {
|
|
2918
|
+
return this.drainInFlight ??= this.doDrain().finally(() => {
|
|
2919
|
+
this.drainInFlight = null;
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
async doDrain() {
|
|
2923
|
+
await this.ensureJournalLoaded();
|
|
2924
|
+
const persisted = await this.deps.kv.get(CURSOR_KEY);
|
|
2925
|
+
if (!this.hydrated) {
|
|
2926
|
+
let page2 = await this.deps.client.listPaymentRequests({ role: "incoming", since: 0 });
|
|
2927
|
+
for (let i = 0; i < MAX_HYDRATION_PAGES; i++) {
|
|
2928
|
+
for (const wire of page2.requests) this.surface(wire);
|
|
2929
|
+
if (!page2.more || typeof page2.cursor !== "number") break;
|
|
2930
|
+
page2 = await this.deps.client.listPaymentRequests({ role: "incoming", since: page2.cursor });
|
|
2931
|
+
}
|
|
2932
|
+
this.hydrated = true;
|
|
2933
|
+
}
|
|
2934
|
+
let since = typeof persisted?.cursor === "number" ? persisted.cursor : 0;
|
|
2935
|
+
let page = await this.deps.client.listPaymentRequests({ role: "incoming", since });
|
|
2936
|
+
if (persisted !== null && String(page.syncEpoch) !== persisted.syncEpoch) {
|
|
2937
|
+
since = 0;
|
|
2938
|
+
page = await this.deps.client.listPaymentRequests({ role: "incoming", since: 0 });
|
|
2939
|
+
}
|
|
2940
|
+
for (; ; ) {
|
|
2941
|
+
for (const wire of page.requests) this.surface(wire);
|
|
2942
|
+
const record = {
|
|
2943
|
+
cursor: typeof page.cursor === "number" ? page.cursor : since,
|
|
2944
|
+
syncEpoch: String(page.syncEpoch)
|
|
2945
|
+
};
|
|
2946
|
+
await this.deps.kv.set(CURSOR_KEY, record);
|
|
2947
|
+
if (!page.more || typeof page.cursor !== "number") break;
|
|
2948
|
+
since = page.cursor;
|
|
2949
|
+
page = await this.deps.client.listPaymentRequests({ role: "incoming", since });
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
surface(wire) {
|
|
2953
|
+
if (wire.toPubkey !== this.deps.ownPubkey) {
|
|
2954
|
+
logger.warn("PaymentsV2", `payment request ${wire.id.slice(0, 12)}\u2026 addressed to a different owner \u2014 dropped`);
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
let status = WIRE_STATUS[wire.status];
|
|
2958
|
+
const link = this.journal?.get(wire.id);
|
|
2959
|
+
if (link !== void 0) {
|
|
2960
|
+
if (wire.status === "open") {
|
|
2961
|
+
status = "settling";
|
|
2962
|
+
} else {
|
|
2963
|
+
void this.clearLink(wire.id).catch(
|
|
2964
|
+
(err) => logger.warn("PaymentsV2", `failed to clear stale settling link for ${wire.id}:`, err)
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
const existing = this.mirror.get(wire.id);
|
|
2969
|
+
if (existing !== void 0) {
|
|
2970
|
+
if (TERMINAL.has(status) && !TERMINAL.has(existing.status)) this.setStatus(wire.id, status);
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
const bundle = this.decryptMemo(wire);
|
|
2974
|
+
const view = {
|
|
2975
|
+
id: wire.id,
|
|
2976
|
+
requestId: wire.id,
|
|
2977
|
+
senderPubkey: wire.fromPubkey,
|
|
2978
|
+
...bundle.senderNametag !== void 0 ? { senderNametag: bundle.senderNametag } : {},
|
|
2979
|
+
amount: wire.assets[0]?.amount ?? "0",
|
|
2980
|
+
coinId: wire.assets[0]?.coinId ?? "",
|
|
2981
|
+
...bundle.memo !== void 0 ? { message: bundle.memo } : {},
|
|
2982
|
+
timestamp: Date.parse(wire.createdAt),
|
|
2983
|
+
status
|
|
2984
|
+
};
|
|
2985
|
+
this.mirror.set(wire.id, view);
|
|
2986
|
+
if (status === "pending") this.deps.emit("payment_request:incoming", { ...view });
|
|
2987
|
+
}
|
|
2988
|
+
decryptMemo(wire) {
|
|
2989
|
+
if (wire.memo === void 0) return {};
|
|
2990
|
+
try {
|
|
2991
|
+
return this.deps.memo.decrypt(wire.fromPubkey, wire.memo);
|
|
2992
|
+
} catch {
|
|
2993
|
+
logger.debug("PaymentsV2", `payment request ${wire.id.slice(0, 12)}\u2026 memo not decryptable here`);
|
|
2994
|
+
return {};
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
setStatus(id, status) {
|
|
2998
|
+
const entry = this.mirror.get(id);
|
|
2999
|
+
if (entry === void 0 || entry.status === status) return;
|
|
3000
|
+
entry.status = status;
|
|
3001
|
+
this.deps.emit("payment_request:updated", { id, status });
|
|
3002
|
+
}
|
|
3003
|
+
// ── public surface (PaymentsRequestsApi) ─────────────────────────────────
|
|
3004
|
+
list() {
|
|
3005
|
+
const views = [...this.mirror.values()].map((entry) => ({
|
|
3006
|
+
...entry,
|
|
3007
|
+
// A live settling link is NEVER payable, whatever the mirror says.
|
|
3008
|
+
status: entry.status === "pending" && this.journal?.has(entry.id) ? "settling" : entry.status
|
|
3009
|
+
}));
|
|
3010
|
+
return views.sort((a, b) => b.timestamp - a.timestamp || (a.id < b.id ? -1 : 1));
|
|
3011
|
+
}
|
|
3012
|
+
async create(to, terms) {
|
|
3013
|
+
try {
|
|
3014
|
+
const toPubkey = CHAIN_PUBKEY_RE.test(to) ? to.toLowerCase() : await this.deps.resolvePubkey(to);
|
|
3015
|
+
if (toPubkey === null || toPubkey === void 0) {
|
|
3016
|
+
return { success: false, error: `Recipient ${to} has no published chain pubkey` };
|
|
3017
|
+
}
|
|
3018
|
+
const amount = BigInt(terms.amount);
|
|
3019
|
+
if (amount <= 0n) return { success: false, error: "Amount must be a positive base-unit decimal string" };
|
|
3020
|
+
const senderNametag = this.deps.ownNametag?.();
|
|
3021
|
+
const envelope = this.deps.memo.encrypt(toPubkey, {
|
|
3022
|
+
...terms.memo !== void 0 ? { memo: terms.memo } : {},
|
|
3023
|
+
...senderNametag !== void 0 ? { senderNametag } : {}
|
|
3024
|
+
});
|
|
3025
|
+
const wire = await this.deps.client.createPaymentRequest({
|
|
3026
|
+
toPubkey,
|
|
3027
|
+
assets: [{ coinId: terms.coinId, amount: amount.toString() }],
|
|
3028
|
+
...envelope !== void 0 ? { memo: envelope } : {}
|
|
3029
|
+
});
|
|
3030
|
+
return { success: true, requestId: wire.id };
|
|
3031
|
+
} catch (error) {
|
|
3032
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
pay(id) {
|
|
3036
|
+
const inFlight = this.payInFlight.get(id);
|
|
3037
|
+
if (inFlight !== void 0) return inFlight;
|
|
3038
|
+
const run = this.payInner(id).finally(() => {
|
|
3039
|
+
this.payInFlight.delete(id);
|
|
3040
|
+
});
|
|
3041
|
+
this.payInFlight.set(id, run);
|
|
3042
|
+
return run;
|
|
3043
|
+
}
|
|
3044
|
+
async payInner(id) {
|
|
3045
|
+
await this.ensureJournalLoaded();
|
|
3046
|
+
const entry = this.mirror.get(id);
|
|
3047
|
+
if (entry === void 0) throw new SphereError(`Payment request not found: ${id}`, "VALIDATION_ERROR");
|
|
3048
|
+
if (entry.status !== "pending" || this.journal.has(id)) {
|
|
3049
|
+
throw new SphereError(`Payment request is not payable: ${entry.status}`, "VALIDATION_ERROR");
|
|
3050
|
+
}
|
|
3051
|
+
let result;
|
|
3052
|
+
try {
|
|
3053
|
+
result = await this.deps.send({
|
|
3054
|
+
recipient: entry.senderPubkey,
|
|
3055
|
+
amount: entry.amount,
|
|
3056
|
+
coinId: entry.coinId,
|
|
3057
|
+
...entry.message !== void 0 ? { memo: entry.message } : {}
|
|
3058
|
+
});
|
|
3059
|
+
} catch (error) {
|
|
3060
|
+
if (isPossiblyCommittedSendOutcome(error)) {
|
|
3061
|
+
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
3062
|
+
if (transferId !== void 0) {
|
|
3063
|
+
const committed = error instanceof PartialSendConflictError;
|
|
3064
|
+
await this.writeLink(id, transferId, committed);
|
|
3065
|
+
this.setStatus(id, "settling");
|
|
3066
|
+
} else {
|
|
3067
|
+
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
3068
|
+
this.setStatus(id, "paid");
|
|
3069
|
+
}
|
|
3070
|
+
} else {
|
|
3071
|
+
await this.clearLink(id);
|
|
3072
|
+
this.setStatus(id, "pending");
|
|
3073
|
+
}
|
|
3074
|
+
throw error;
|
|
3075
|
+
}
|
|
3076
|
+
this.setStatus(id, "paid");
|
|
3077
|
+
await this.respondPaid(id, result.id);
|
|
3078
|
+
await this.clearLink(id);
|
|
3079
|
+
return result;
|
|
3080
|
+
}
|
|
3081
|
+
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
3082
|
+
async respondPaid(id, transferId) {
|
|
3083
|
+
try {
|
|
3084
|
+
await this.deps.client.respondPaymentRequest(id, { action: "paid", transferId });
|
|
3085
|
+
return true;
|
|
3086
|
+
} catch (err) {
|
|
3087
|
+
if (isConflict409(err)) return true;
|
|
3088
|
+
logger.warn("PaymentsV2", `paid respond failed for ${id} (deferred):`, err);
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
async decline(id) {
|
|
3093
|
+
const entry = this.mirror.get(id);
|
|
3094
|
+
if (entry === void 0) throw new SphereError(`Payment request not found: ${id}`, "VALIDATION_ERROR");
|
|
3095
|
+
await this.deps.client.respondPaymentRequest(id, { action: "declined" });
|
|
3096
|
+
this.setStatus(id, "rejected");
|
|
3097
|
+
}
|
|
3098
|
+
dismissProcessed() {
|
|
3099
|
+
for (const [id, entry] of this.mirror) {
|
|
3100
|
+
if (TERMINAL.has(entry.status)) this.mirror.delete(id);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
// ── reconcile (#441, fed by resume outcomes) ─────────────────────────────
|
|
3104
|
+
async reconcile(outcomes) {
|
|
3105
|
+
if (this.reconcileInFlight) return;
|
|
3106
|
+
await this.ensureJournalLoaded();
|
|
3107
|
+
if (this.journal.size === 0) return;
|
|
3108
|
+
this.reconcileInFlight = true;
|
|
3109
|
+
try {
|
|
3110
|
+
const resumed = new Set(outcomes.resumed);
|
|
3111
|
+
const conflicted = new Set(outcomes.conflicted);
|
|
3112
|
+
const open = new Set(outcomes.open);
|
|
3113
|
+
let aborted = null;
|
|
3114
|
+
for (const [requestId, link] of [...this.journal]) {
|
|
3115
|
+
if (link.committed || resumed.has(link.transferId)) {
|
|
3116
|
+
await this.resolvePaid(requestId, link.transferId);
|
|
3117
|
+
} else if (conflicted.has(link.transferId)) {
|
|
3118
|
+
await this.revertPayable(requestId);
|
|
3119
|
+
} else if (open.has(link.transferId)) {
|
|
3120
|
+
} else {
|
|
3121
|
+
aborted = await this.reconcileUnaccounted(requestId, link.transferId, aborted);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
} finally {
|
|
3125
|
+
this.reconcileInFlight = false;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
3129
|
+
// a probe failure defers the link to the next session.
|
|
3130
|
+
async reconcileUnaccounted(requestId, transferId, aborted) {
|
|
3131
|
+
try {
|
|
3132
|
+
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
3133
|
+
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
3134
|
+
else await this.resolvePaid(requestId, transferId);
|
|
3135
|
+
} catch (err) {
|
|
3136
|
+
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
3137
|
+
}
|
|
3138
|
+
return aborted;
|
|
3139
|
+
}
|
|
3140
|
+
async resolvePaid(requestId, transferId) {
|
|
3141
|
+
if (!await this.respondPaid(requestId, transferId)) return;
|
|
3142
|
+
await this.clearLink(requestId);
|
|
3143
|
+
this.setStatus(requestId, "paid");
|
|
3144
|
+
}
|
|
3145
|
+
async revertPayable(requestId) {
|
|
3146
|
+
await this.clearLink(requestId);
|
|
3147
|
+
this.setStatus(requestId, "pending");
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
|
|
3151
|
+
// modules/payments-v2/select/ledger.ts
|
|
3152
|
+
var ReservationLedger = class {
|
|
3153
|
+
reservations = /* @__PURE__ */ new Map();
|
|
3154
|
+
tokenIndex = /* @__PURE__ */ new Map();
|
|
3155
|
+
// All-or-nothing: every entry is validated before any state mutates.
|
|
3156
|
+
reserve(reservationId, entries) {
|
|
3157
|
+
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
3158
|
+
if (this.reservations.has(reservationId)) throw new Error("DUPLICATE_RESERVATION_ID");
|
|
3159
|
+
const amounts = /* @__PURE__ */ new Map();
|
|
3160
|
+
const tokenAmounts = /* @__PURE__ */ new Map();
|
|
3161
|
+
for (const entry of entries) {
|
|
3162
|
+
if (entry.amount <= 0n) throw new Error("INVALID_RESERVATION_AMOUNT");
|
|
3163
|
+
amounts.set(entry.tokenId, (amounts.get(entry.tokenId) ?? 0n) + entry.amount);
|
|
3164
|
+
const known = tokenAmounts.get(entry.tokenId);
|
|
3165
|
+
if (known === void 0 || entry.tokenAmount > known) {
|
|
3166
|
+
tokenAmounts.set(entry.tokenId, entry.tokenAmount);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
for (const [tokenId, amount] of amounts) {
|
|
3170
|
+
if (this.getFreeAmount(tokenId, tokenAmounts.get(tokenId)) < amount) {
|
|
3171
|
+
throw new Error("INSUFFICIENT_FREE_AMOUNT");
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
this.reservations.set(reservationId, amounts);
|
|
3175
|
+
for (const tokenId of amounts.keys()) {
|
|
3176
|
+
let ids = this.tokenIndex.get(tokenId);
|
|
3177
|
+
if (!ids) {
|
|
3178
|
+
ids = /* @__PURE__ */ new Set();
|
|
3179
|
+
this.tokenIndex.set(tokenId, ids);
|
|
3180
|
+
}
|
|
3181
|
+
ids.add(reservationId);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
commit(reservationId) {
|
|
3185
|
+
this.release(reservationId);
|
|
3186
|
+
}
|
|
3187
|
+
cancel(reservationId) {
|
|
3188
|
+
this.release(reservationId);
|
|
3189
|
+
}
|
|
3190
|
+
cancelForToken(tokenId, excludeReservationId) {
|
|
3191
|
+
const ids = this.tokenIndex.get(tokenId);
|
|
3192
|
+
if (!ids) return [];
|
|
3193
|
+
const cancelled = [];
|
|
3194
|
+
for (const reservationId of [...ids]) {
|
|
3195
|
+
if (reservationId === excludeReservationId) continue;
|
|
3196
|
+
this.release(reservationId);
|
|
3197
|
+
cancelled.push(reservationId);
|
|
3198
|
+
}
|
|
3199
|
+
return cancelled;
|
|
3200
|
+
}
|
|
3201
|
+
getFreeAmount(tokenId, tokenAmount) {
|
|
3202
|
+
let reserved = 0n;
|
|
3203
|
+
const ids = this.tokenIndex.get(tokenId);
|
|
3204
|
+
if (ids) {
|
|
3205
|
+
for (const reservationId of ids) {
|
|
3206
|
+
reserved += this.reservations.get(reservationId)?.get(tokenId) ?? 0n;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
const free = tokenAmount - reserved;
|
|
3210
|
+
return free > 0n ? free : 0n;
|
|
3211
|
+
}
|
|
3212
|
+
clear() {
|
|
3213
|
+
this.reservations.clear();
|
|
3214
|
+
this.tokenIndex.clear();
|
|
3215
|
+
}
|
|
3216
|
+
release(reservationId) {
|
|
3217
|
+
const amounts = this.reservations.get(reservationId);
|
|
3218
|
+
if (!amounts) return;
|
|
3219
|
+
this.reservations.delete(reservationId);
|
|
3220
|
+
for (const tokenId of amounts.keys()) {
|
|
3221
|
+
const ids = this.tokenIndex.get(tokenId);
|
|
3222
|
+
if (!ids) continue;
|
|
3223
|
+
ids.delete(reservationId);
|
|
3224
|
+
if (ids.size === 0) this.tokenIndex.delete(tokenId);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
3228
|
+
|
|
3229
|
+
// modules/payments-v2/select/selector.ts
|
|
3230
|
+
var DEFAULT_WORK_BUDGET = 2e4;
|
|
3231
|
+
var MAX_COMBINATION_SIZE = 5;
|
|
3232
|
+
function selectCoins(pool, target, options = {}) {
|
|
3233
|
+
if (target <= 0n) return null;
|
|
3234
|
+
const candidates = pool.filter((e) => e.amount > 0n).sort(byAmountAscending);
|
|
3235
|
+
let total = 0n;
|
|
3236
|
+
for (const c of candidates) total += c.amount;
|
|
3237
|
+
if (total < target) return null;
|
|
3238
|
+
const single = candidates.find((c) => c.amount === target);
|
|
3239
|
+
if (single) return { direct: [single.tokenId] };
|
|
3240
|
+
const search = {
|
|
3241
|
+
examined: 0,
|
|
3242
|
+
budget: options.workBudget ?? DEFAULT_WORK_BUDGET
|
|
3243
|
+
};
|
|
3244
|
+
const combo = findExactCombination(candidates, target, search);
|
|
3245
|
+
if (options.stats) options.stats.combinationsExamined = search.examined;
|
|
3246
|
+
if (combo) return { direct: combo.map((c) => c.tokenId) };
|
|
3247
|
+
return greedyWithSplit(candidates, target);
|
|
3248
|
+
}
|
|
3249
|
+
function byAmountAscending(a, b) {
|
|
3250
|
+
if (a.amount !== b.amount) return a.amount < b.amount ? -1 : 1;
|
|
3251
|
+
return a.tokenId < b.tokenId ? -1 : a.tokenId > b.tokenId ? 1 : 0;
|
|
3252
|
+
}
|
|
3253
|
+
function greedyWithSplit(sortedAscending, target) {
|
|
3254
|
+
const direct = [];
|
|
3255
|
+
let sum = 0n;
|
|
3256
|
+
for (const candidate of sortedAscending) {
|
|
3257
|
+
const next = sum + candidate.amount;
|
|
3258
|
+
if (next < target) {
|
|
3259
|
+
direct.push(candidate.tokenId);
|
|
3260
|
+
sum = next;
|
|
3261
|
+
} else if (next === target) {
|
|
3262
|
+
direct.push(candidate.tokenId);
|
|
3263
|
+
return { direct };
|
|
3264
|
+
} else {
|
|
3265
|
+
const splitAmount = target - sum;
|
|
3266
|
+
return {
|
|
3267
|
+
direct,
|
|
3268
|
+
split: {
|
|
3269
|
+
tokenId: candidate.tokenId,
|
|
3270
|
+
splitAmount,
|
|
3271
|
+
remainderAmount: candidate.amount - splitAmount
|
|
3272
|
+
}
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return null;
|
|
3277
|
+
}
|
|
3278
|
+
function findExactCombination(sortedAscending, target, search) {
|
|
3279
|
+
const maxSize = Math.min(MAX_COMBINATION_SIZE, sortedAscending.length);
|
|
3280
|
+
for (let size = 2; size <= maxSize; size++) {
|
|
3281
|
+
if (search.examined >= search.budget) return null;
|
|
3282
|
+
const window = orderNearestTo(sortedAscending, target / BigInt(size));
|
|
3283
|
+
const found = searchCombinationsOfSize(window, size, target, search);
|
|
3284
|
+
if (found) return found;
|
|
3285
|
+
}
|
|
3286
|
+
return null;
|
|
3287
|
+
}
|
|
3288
|
+
function orderNearestTo(entries, pivot) {
|
|
3289
|
+
const distance = (e) => e.amount >= pivot ? e.amount - pivot : pivot - e.amount;
|
|
3290
|
+
return [...entries].sort((a, b) => {
|
|
3291
|
+
const da = distance(a);
|
|
3292
|
+
const db = distance(b);
|
|
3293
|
+
if (da !== db) return da < db ? -1 : 1;
|
|
3294
|
+
return byAmountAscending(a, b);
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
function searchCombinationsOfSize(window, size, target, search) {
|
|
3298
|
+
const chosen = [];
|
|
3299
|
+
const recurse = (start, sum) => {
|
|
3300
|
+
if (chosen.length === size) {
|
|
3301
|
+
search.examined += 1;
|
|
3302
|
+
return sum === target ? [...chosen] : null;
|
|
3303
|
+
}
|
|
3304
|
+
for (let i = start; i < window.length; i++) {
|
|
3305
|
+
if (search.examined >= search.budget) return null;
|
|
3306
|
+
chosen.push(window[i]);
|
|
3307
|
+
const found = recurse(i + 1, sum + window[i].amount);
|
|
3308
|
+
chosen.pop();
|
|
3309
|
+
if (found) return found;
|
|
3310
|
+
}
|
|
3311
|
+
return null;
|
|
3312
|
+
};
|
|
3313
|
+
return recurse(0, 0n);
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
// modules/payments-v2/select/queue.ts
|
|
3317
|
+
var QUEUE_TIMEOUT_MS = 3e4;
|
|
3318
|
+
var QUEUE_MAX_SIZE = 100;
|
|
3319
|
+
var SpendQueue = class {
|
|
3320
|
+
constructor(deps) {
|
|
3321
|
+
this.deps = deps;
|
|
3322
|
+
}
|
|
3323
|
+
queues = /* @__PURE__ */ new Map();
|
|
3324
|
+
expectedChange = /* @__PURE__ */ new Map();
|
|
3325
|
+
destroyed = false;
|
|
3326
|
+
plan(reservationId, request) {
|
|
3327
|
+
if (this.destroyed) {
|
|
3328
|
+
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
3329
|
+
}
|
|
3330
|
+
const amount = parseAmount(request.amount);
|
|
3331
|
+
const { freeView, freeTotal } = this.freeView(request.coinId);
|
|
3332
|
+
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
3333
|
+
throw new SphereError(
|
|
3334
|
+
"Insufficient balance for this transaction",
|
|
3335
|
+
"SEND_INSUFFICIENT_BALANCE"
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3338
|
+
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
3339
|
+
if (plan !== null) {
|
|
3340
|
+
this.reservePlan(reservationId, plan, freeView);
|
|
3341
|
+
return { kind: "planned", spend: { reservationId, plan } };
|
|
3342
|
+
}
|
|
3343
|
+
return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
|
|
3344
|
+
}
|
|
3345
|
+
// Coverage math: free + Σ declared expected change of in-flight sends —
|
|
3346
|
+
// never the full amount of in-flight sources (§5.3).
|
|
3347
|
+
declareExpectedChange(transferId, coinId, amount) {
|
|
3348
|
+
this.expectedChange.set(transferId, { coinId, amount });
|
|
3349
|
+
}
|
|
3350
|
+
clearExpectedChange(transferId) {
|
|
3351
|
+
this.expectedChange.delete(transferId);
|
|
3352
|
+
}
|
|
3353
|
+
notifyChange(coinId) {
|
|
3354
|
+
if (this.destroyed) return;
|
|
3355
|
+
const entries = this.queues.get(coinId);
|
|
3356
|
+
if (!entries) return;
|
|
3357
|
+
let i = 0;
|
|
3358
|
+
while (i < entries.length) {
|
|
3359
|
+
const entry = entries[i];
|
|
3360
|
+
if (this.tryServe(entry) === "blocked") {
|
|
3361
|
+
i += 1;
|
|
3362
|
+
continue;
|
|
3363
|
+
}
|
|
3364
|
+
clearTimeout(entry.timer);
|
|
3365
|
+
entries.splice(i, 1);
|
|
3366
|
+
}
|
|
3367
|
+
if (entries.length === 0) this.queues.delete(coinId);
|
|
3368
|
+
}
|
|
3369
|
+
destroy() {
|
|
3370
|
+
if (this.destroyed) return;
|
|
3371
|
+
this.destroyed = true;
|
|
3372
|
+
for (const entries of this.queues.values()) {
|
|
3373
|
+
for (const entry of entries) {
|
|
3374
|
+
clearTimeout(entry.timer);
|
|
3375
|
+
entry.reject(new SphereError("Module destroyed", "MODULE_DESTROYED"));
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
this.queues.clear();
|
|
3379
|
+
this.expectedChange.clear();
|
|
3380
|
+
}
|
|
3381
|
+
tryServe(entry) {
|
|
3382
|
+
if (Date.now() - entry.enqueuedAt > QUEUE_TIMEOUT_MS) {
|
|
3383
|
+
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3384
|
+
return "rejected";
|
|
3385
|
+
}
|
|
3386
|
+
const { freeView, freeTotal } = this.freeView(entry.coinId);
|
|
3387
|
+
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
3388
|
+
entry.reject(
|
|
3389
|
+
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
3390
|
+
);
|
|
3391
|
+
return "rejected";
|
|
3392
|
+
}
|
|
3393
|
+
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
3394
|
+
if (plan === null) return "blocked";
|
|
3395
|
+
try {
|
|
3396
|
+
this.reservePlan(entry.id, plan, freeView);
|
|
3397
|
+
} catch (e) {
|
|
3398
|
+
entry.reject(e instanceof Error ? e : new Error(String(e)));
|
|
3399
|
+
return "rejected";
|
|
3400
|
+
}
|
|
3401
|
+
entry.resolve({ reservationId: entry.id, plan });
|
|
3402
|
+
return "served";
|
|
3403
|
+
}
|
|
3404
|
+
enqueue(id, coinId, amount) {
|
|
3405
|
+
if (this.totalQueued() >= QUEUE_MAX_SIZE) {
|
|
3406
|
+
throw new SphereError("Send queue is full", "SEND_QUEUE_FULL");
|
|
3407
|
+
}
|
|
3408
|
+
let resolve;
|
|
3409
|
+
let reject;
|
|
3410
|
+
const settled = new Promise((res, rej) => {
|
|
3411
|
+
resolve = res;
|
|
3412
|
+
reject = rej;
|
|
3413
|
+
});
|
|
3414
|
+
settled.catch(() => {
|
|
3415
|
+
});
|
|
3416
|
+
const timer = setTimeout(() => this.expire(id, coinId), QUEUE_TIMEOUT_MS);
|
|
3417
|
+
const entry = { id, coinId, amount, enqueuedAt: Date.now(), resolve, reject, timer };
|
|
3418
|
+
let queue = this.queues.get(coinId);
|
|
3419
|
+
if (!queue) {
|
|
3420
|
+
queue = [];
|
|
3421
|
+
this.queues.set(coinId, queue);
|
|
3422
|
+
}
|
|
3423
|
+
queue.push(entry);
|
|
3424
|
+
return settled;
|
|
3425
|
+
}
|
|
3426
|
+
expire(id, coinId) {
|
|
3427
|
+
const entries = this.queues.get(coinId);
|
|
3428
|
+
if (!entries) return;
|
|
3429
|
+
const index = entries.findIndex((e) => e.id === id);
|
|
3430
|
+
if (index === -1) return;
|
|
3431
|
+
const [entry] = entries.splice(index, 1);
|
|
3432
|
+
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3433
|
+
if (entries.length === 0) this.queues.delete(coinId);
|
|
3434
|
+
}
|
|
3435
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
3436
|
+
freeView(coinId) {
|
|
3437
|
+
const freeView = [];
|
|
3438
|
+
let freeTotal = 0n;
|
|
3439
|
+
for (const entry of this.deps.getPool(coinId)) {
|
|
3440
|
+
if (entry.amount <= 0n) continue;
|
|
3441
|
+
if (this.deps.ledger.getFreeAmount(entry.tokenId, entry.amount) === entry.amount) {
|
|
3442
|
+
freeView.push(entry);
|
|
3443
|
+
freeTotal += entry.amount;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
return { freeView, freeTotal };
|
|
3447
|
+
}
|
|
3448
|
+
expectedChangeTotal(coinId) {
|
|
3449
|
+
let total = 0n;
|
|
3450
|
+
for (const change of this.expectedChange.values()) {
|
|
3451
|
+
if (change.coinId === coinId) total += change.amount;
|
|
3452
|
+
}
|
|
3453
|
+
return total;
|
|
3454
|
+
}
|
|
3455
|
+
reservePlan(reservationId, plan, freeView) {
|
|
3456
|
+
const amounts = new Map(freeView.map((e) => [e.tokenId, e.amount]));
|
|
3457
|
+
const requests = plan.direct.map((tokenId) => wholeToken(tokenId, amounts));
|
|
3458
|
+
if (plan.split) requests.push(wholeToken(plan.split.tokenId, amounts));
|
|
3459
|
+
this.deps.ledger.reserve(reservationId, requests);
|
|
3460
|
+
}
|
|
3461
|
+
totalQueued() {
|
|
3462
|
+
let total = 0;
|
|
3463
|
+
for (const entries of this.queues.values()) total += entries.length;
|
|
3464
|
+
return total;
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
function wholeToken(tokenId, amounts) {
|
|
3468
|
+
const amount = amounts.get(tokenId);
|
|
3469
|
+
if (amount === void 0) {
|
|
3470
|
+
throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
|
|
3471
|
+
}
|
|
3472
|
+
return { tokenId, amount, tokenAmount: amount };
|
|
3473
|
+
}
|
|
3474
|
+
function parseAmount(raw) {
|
|
3475
|
+
if (!/^\d+$/.test(raw)) {
|
|
3476
|
+
throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
|
|
3477
|
+
}
|
|
3478
|
+
const amount = BigInt(raw);
|
|
3479
|
+
if (amount <= 0n) {
|
|
3480
|
+
throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
|
|
3481
|
+
}
|
|
3482
|
+
return amount;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// modules/payments-v2/compose.ts
|
|
3486
|
+
function supportsDeterministicMint(engine) {
|
|
3487
|
+
const candidate = engine;
|
|
3488
|
+
return candidate.deterministicMint === true && typeof candidate.deriveMintTokenId === "function";
|
|
3489
|
+
}
|
|
3490
|
+
var HeldStateCache = class {
|
|
3491
|
+
held = /* @__PURE__ */ new Map();
|
|
3492
|
+
get(tokenId) {
|
|
3493
|
+
return this.held.get(tokenId) ?? null;
|
|
3494
|
+
}
|
|
3495
|
+
set(tokenId, stateHash) {
|
|
3496
|
+
this.held.set(tokenId, stateHash);
|
|
3497
|
+
}
|
|
3498
|
+
};
|
|
3499
|
+
function composeFacadeParts(deps, hooks) {
|
|
3500
|
+
const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
|
|
3501
|
+
const view = new InventoryView({
|
|
3502
|
+
port: deps.storagePort,
|
|
3503
|
+
kv: deps.kv,
|
|
3504
|
+
emit: (event) => deps.emit(event, {}),
|
|
3505
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3506
|
+
});
|
|
3507
|
+
const ledger = new ReservationLedger();
|
|
3508
|
+
const queue = new SpendQueue({
|
|
3509
|
+
ledger,
|
|
3510
|
+
getPool: (coinId) => view.pool(coinId),
|
|
3511
|
+
...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
|
|
3512
|
+
});
|
|
3513
|
+
const historyStore = new History({
|
|
3514
|
+
client: deps.client,
|
|
3515
|
+
fieldKey: deps.fieldKey,
|
|
3516
|
+
registry: deps.registry,
|
|
3517
|
+
emit: (event) => deps.emit(event, {}),
|
|
3518
|
+
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
3519
|
+
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
3520
|
+
});
|
|
3521
|
+
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
3522
|
+
const heldStates = new HeldStateCache();
|
|
3523
|
+
return {
|
|
3524
|
+
ownPubkeyBytes,
|
|
3525
|
+
view,
|
|
3526
|
+
ledger,
|
|
3527
|
+
queue,
|
|
3528
|
+
historyStore,
|
|
3529
|
+
machineStores: createMachineStores(deps.kv),
|
|
3530
|
+
machineDeps,
|
|
3531
|
+
machine: new TransferMachine(machineDeps),
|
|
3532
|
+
heldStates,
|
|
3533
|
+
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
3534
|
+
requests: buildRequests(deps, hooks)
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
function buildMachineDeps(deps, hooks, historyStore) {
|
|
3538
|
+
return {
|
|
3539
|
+
engine: () => hooks.engine(),
|
|
3540
|
+
storage: deps.storagePort,
|
|
3541
|
+
delivery: deps.deliveryPort,
|
|
3542
|
+
intents: intentApi(deps.client),
|
|
3543
|
+
checkpointStore: deps.checkpointStore,
|
|
3544
|
+
kv: deps.kv,
|
|
3545
|
+
encryptPayload: async (payload) => encryptField(deps.fieldKey, JSON.stringify(payload)),
|
|
3546
|
+
decryptPayload: async (envelope) => JSON.parse(decryptField(deps.fieldKey, envelope)),
|
|
3547
|
+
signComplete: deps.signComplete,
|
|
3548
|
+
ownPubkey: hexToBytes(deps.ownPubkey),
|
|
3549
|
+
emit: deps.emit,
|
|
3550
|
+
now: deps.now ?? Date.now,
|
|
3551
|
+
recordHistory: async ({ transferId, payload }) => {
|
|
3552
|
+
await historyStore.recordSent({
|
|
3553
|
+
transferId,
|
|
3554
|
+
coinId: payload.coinId,
|
|
3555
|
+
amount: payload.amount,
|
|
3556
|
+
recipientPubkey: payload.recipient,
|
|
3557
|
+
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
function intentApi(client) {
|
|
3563
|
+
return {
|
|
3564
|
+
put: (transferId, envelope, requiresSeedClose) => client.putIntent(transferId, envelope, requiresSeedClose),
|
|
3565
|
+
listOpen: async () => (await client.listIntents("open")).map((intent) => ({
|
|
3566
|
+
transferId: intent.transferId,
|
|
3567
|
+
payload: intent.payload,
|
|
3568
|
+
createdAt: intent.createdAt
|
|
3569
|
+
})),
|
|
3570
|
+
abort: (transferId) => client.abortIntent(transferId),
|
|
3571
|
+
complete: (transferId, signature) => client.completeIntent(transferId, signature)
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
3575
|
+
return new Receive({
|
|
3576
|
+
delivery: deps.deliveryPort,
|
|
3577
|
+
engine: receiveEngine(hooks),
|
|
3578
|
+
view: {
|
|
3579
|
+
heldState: (tokenId) => heldStates.get(tokenId),
|
|
3580
|
+
store: async (entry) => {
|
|
3581
|
+
heldStates.set(entry.tokenId, entry.stateHash);
|
|
3582
|
+
return "stored";
|
|
3583
|
+
}
|
|
3584
|
+
},
|
|
3585
|
+
kv: deps.kv,
|
|
3586
|
+
registry: deps.registry,
|
|
3587
|
+
recordReceived: (record) => recordReceived(historyStore, record),
|
|
3588
|
+
emit: (event, transfer) => deps.emit(event, transfer),
|
|
3589
|
+
syncEpoch: deps.syncEpoch ?? (() => ""),
|
|
3590
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
function receiveEngine(hooks) {
|
|
3594
|
+
return {
|
|
3595
|
+
getIdentity: () => hooks.engine().getIdentity(),
|
|
3596
|
+
decodeToken: (blob) => hooks.engine().decodeToken(blob),
|
|
3597
|
+
verify: (token, options) => hooks.engine().verify(token, options),
|
|
3598
|
+
isOwnedBy: (token, pubkey) => hooks.engine().isOwnedBy(token, pubkey),
|
|
3599
|
+
isSpent: (token, options) => hooks.engine().isSpent(token, options),
|
|
3600
|
+
tokenId: (token) => hooks.engine().tokenId(token),
|
|
3601
|
+
deliveryKeys: (blobBytes) => hooks.engine().deliveryKeys(blobBytes)
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
async function recordReceived(historyStore, record) {
|
|
3605
|
+
const first = record.assets[0];
|
|
3606
|
+
await historyStore.recordReceived({
|
|
3607
|
+
tokenId: record.tokenId,
|
|
3608
|
+
stateHash: record.stateHash,
|
|
3609
|
+
coinId: first?.coinId ?? "",
|
|
3610
|
+
amount: first?.amount ?? "0",
|
|
3611
|
+
...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
|
|
3612
|
+
...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
|
|
3613
|
+
...record.memo !== void 0 ? { memo: record.memo } : {},
|
|
3614
|
+
timestamp: record.receivedAt
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
function buildRequests(deps, hooks) {
|
|
3618
|
+
return new Requests({
|
|
3619
|
+
client: deps.client,
|
|
3620
|
+
kv: deps.kv,
|
|
3621
|
+
ownPubkey: deps.ownPubkey,
|
|
3622
|
+
send: (request) => hooks.send(request),
|
|
3623
|
+
resolvePubkey: async (identifier) => (await deps.resolveRecipient(identifier))?.chainPubkey ?? null,
|
|
3624
|
+
listAbortedTransferIds: async () => (await deps.client.listIntents("aborted")).map((intent) => intent.transferId),
|
|
3625
|
+
emit: deps.emit,
|
|
3626
|
+
memo: deps.requestMemo,
|
|
3627
|
+
...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
|
|
3628
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
// modules/payments-v2/PaymentsFacade.ts
|
|
3633
|
+
var MAX_RESELECT = 8;
|
|
3634
|
+
var ATTENTION_MINT_UNRESOLVED = "mint:unresolved";
|
|
3635
|
+
var INVENTORY_SCAN_PAGE_LIMIT = 50;
|
|
3636
|
+
var PaymentsFacade = class {
|
|
3637
|
+
constructor(deps) {
|
|
3638
|
+
this.deps = deps;
|
|
3639
|
+
const parts = composeFacadeParts(deps, {
|
|
3640
|
+
engine: () => this.engine(),
|
|
3641
|
+
send: (request) => this.send(request)
|
|
3642
|
+
});
|
|
3643
|
+
this.view = parts.view;
|
|
3644
|
+
this.ledger = parts.ledger;
|
|
3645
|
+
this.queue = parts.queue;
|
|
3646
|
+
this.machine = parts.machine;
|
|
3647
|
+
this.machineDeps = parts.machineDeps;
|
|
3648
|
+
this.machineStores = parts.machineStores;
|
|
3649
|
+
this.historyStore = parts.historyStore;
|
|
3650
|
+
this.receiveLoop = parts.receiveLoop;
|
|
3651
|
+
this.heldStates = parts.heldStates;
|
|
3652
|
+
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3653
|
+
this.requests = parts.requests;
|
|
3654
|
+
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3655
|
+
}
|
|
3656
|
+
view;
|
|
3657
|
+
ledger;
|
|
3658
|
+
queue;
|
|
3659
|
+
machine;
|
|
3660
|
+
machineDeps;
|
|
3661
|
+
machineStores;
|
|
3662
|
+
historyStore;
|
|
3663
|
+
receiveLoop;
|
|
3664
|
+
heldStates;
|
|
3665
|
+
ownPubkeyBytes;
|
|
3666
|
+
requests;
|
|
3667
|
+
currentEngine = null;
|
|
3668
|
+
pendingOps = /* @__PURE__ */ new Set();
|
|
3669
|
+
unsubscribers = [];
|
|
3670
|
+
started = false;
|
|
3671
|
+
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
3672
|
+
async start() {
|
|
3673
|
+
if (this.started) return;
|
|
3674
|
+
this.started = true;
|
|
3675
|
+
await this.deps.session.start();
|
|
3676
|
+
this.unsubscribers.push(
|
|
3677
|
+
this.deps.session.subscribeStream("mailbox", () => {
|
|
3678
|
+
this.trackTail(this.receiveLoop.drainOnce());
|
|
3679
|
+
}),
|
|
3680
|
+
this.deps.session.subscribeStream("inventory", () => {
|
|
3681
|
+
this.trackTail(this.view.delta(0));
|
|
3682
|
+
}),
|
|
3683
|
+
this.deps.session.subscribeStream("payment_requests", () => {
|
|
3684
|
+
this.trackTail(this.requests.drainIncoming());
|
|
3685
|
+
})
|
|
3686
|
+
);
|
|
3687
|
+
this.trackTail(this.runResume());
|
|
3688
|
+
this.trackTail(
|
|
3689
|
+
this.machineStores.deliveryJournal.replay({
|
|
3690
|
+
delivery: this.deps.deliveryPort,
|
|
3691
|
+
now: this.deps.now ?? Date.now,
|
|
3692
|
+
attention: (transferId, code, detail) => {
|
|
3693
|
+
this.deps.emit(
|
|
3694
|
+
"transfer:attention",
|
|
3695
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
})
|
|
3699
|
+
);
|
|
3700
|
+
this.trackTail(this.replayMints());
|
|
3701
|
+
this.trackTail(this.seedHeldStates());
|
|
3702
|
+
await this.view.fullPull().catch(() => void 0);
|
|
3703
|
+
this.receiveLoop.start(this.deps.receivePollMs);
|
|
3704
|
+
}
|
|
3705
|
+
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
3706
|
+
async stop() {
|
|
3707
|
+
this.started = false;
|
|
3708
|
+
this.receiveLoop.stop();
|
|
3709
|
+
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
3710
|
+
this.unsubscribers = [];
|
|
3711
|
+
await this.deps.session.stop();
|
|
3712
|
+
while (this.pendingOps.size > 0) {
|
|
3713
|
+
await Promise.allSettled([...this.pendingOps]);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
/** Swaps what FUTURE operations snapshot; in-flight ops finish on the old engine. */
|
|
3717
|
+
setEngine(next) {
|
|
3718
|
+
const previous = this.currentEngine ?? this.deps.engineRef();
|
|
3719
|
+
this.currentEngine = next;
|
|
3720
|
+
if (previous !== next) previous.dispose?.();
|
|
3721
|
+
}
|
|
3722
|
+
// ── reads ──────────────────────────────────────────────────────────────────
|
|
3723
|
+
async assets(coinId) {
|
|
3724
|
+
const all = await this.view.assets(this.deps.registry, this.deps.price);
|
|
3725
|
+
return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
|
|
3726
|
+
}
|
|
3727
|
+
tokens(filter) {
|
|
3728
|
+
return this.view.tokens(this.deps.registry, filter);
|
|
3729
|
+
}
|
|
3730
|
+
history(page) {
|
|
3731
|
+
return this.historyStore.page(page ?? {});
|
|
3732
|
+
}
|
|
3733
|
+
// ── money movement ─────────────────────────────────────────────────────────
|
|
3734
|
+
send(request) {
|
|
3735
|
+
return this.track(this.sendWithPolicy(request));
|
|
3736
|
+
}
|
|
3737
|
+
async receive() {
|
|
3738
|
+
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
3739
|
+
if (transfers.length > 0) this.trackTail(this.view.delta(0));
|
|
3740
|
+
return { transfers };
|
|
3741
|
+
}
|
|
3742
|
+
mint(coinId, amount) {
|
|
3743
|
+
return this.track(this.mintInner(coinId, amount));
|
|
3744
|
+
}
|
|
3745
|
+
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
3746
|
+
async sendWithPolicy(request) {
|
|
3747
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
3748
|
+
const run = {
|
|
3749
|
+
amount: request.amount,
|
|
3750
|
+
delivered: [],
|
|
3751
|
+
sentTokens: [],
|
|
3752
|
+
tokenTransfers: [],
|
|
3753
|
+
settledShortfalls: [],
|
|
3754
|
+
firstPartialId: void 0,
|
|
3755
|
+
pendingAny: false
|
|
3756
|
+
};
|
|
3757
|
+
for (let attempt = 0; ; attempt++) {
|
|
3758
|
+
let ctx;
|
|
3759
|
+
try {
|
|
3760
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
3761
|
+
} catch (err) {
|
|
3762
|
+
throw this.partialize(err, run);
|
|
3763
|
+
}
|
|
3764
|
+
const disposition = await this.runAttempt(ctx);
|
|
3765
|
+
if (disposition.kind === "rethrow") throw this.partialize(disposition.error, run);
|
|
3766
|
+
if (disposition.kind === "retry-full") {
|
|
3767
|
+
if (attempt >= MAX_RESELECT) throw this.partialize(disposition.error, run);
|
|
3768
|
+
continue;
|
|
3769
|
+
}
|
|
3770
|
+
if (disposition.kind === "success") {
|
|
3771
|
+
run.pendingAny = run.pendingAny || disposition.deliveryPending;
|
|
3772
|
+
this.accumulate(ctx, ctx.sourceIds, run);
|
|
3773
|
+
return await this.finishSend(ctx.transferId, run);
|
|
3774
|
+
}
|
|
3775
|
+
const surfaced = await this.consumePartial(ctx, disposition, run, attempt);
|
|
3776
|
+
if (surfaced !== null) return surfaced;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
/** §5.6 cross-network deposit trap: refused BEFORE any reserve/certification. */
|
|
3780
|
+
async requireSameNetworkRecipient(identifier) {
|
|
3781
|
+
const recipient = await this.deps.resolveRecipient(identifier);
|
|
3782
|
+
if (recipient === null || recipient.chainPubkey === "") {
|
|
3783
|
+
throw new SphereError(`Recipient ${identifier} has no published chain pubkey`, "INVALID_RECIPIENT");
|
|
3784
|
+
}
|
|
3785
|
+
if (recipient.network !== this.deps.network) {
|
|
3786
|
+
throw new SphereError(
|
|
3787
|
+
`Recipient ${identifier} is on network "${recipient.network}" but this session is on "${this.deps.network}" \u2014 a cross-network deposit is unrecoverable`,
|
|
3788
|
+
"INVALID_RECIPIENT"
|
|
3789
|
+
);
|
|
3790
|
+
}
|
|
3791
|
+
return recipient;
|
|
3792
|
+
}
|
|
3793
|
+
async runAttempt(ctx) {
|
|
3794
|
+
try {
|
|
3795
|
+
const { deliveryPending } = await this.machine.run(ctx.plan);
|
|
3796
|
+
this.settleSuccess(ctx, ctx.sourceIds);
|
|
3797
|
+
return { kind: "success", deliveryPending };
|
|
3798
|
+
} catch (err) {
|
|
3799
|
+
return this.disposeFailedAttempt(ctx, err);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
/**
|
|
3803
|
+
* Partial outcome (#677/#690): the shortfall is already durable (written by
|
|
3804
|
+
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
3805
|
+
* the remainder under a NEW transferId — never the full amount.
|
|
3806
|
+
*/
|
|
3807
|
+
async consumePartial(ctx, disposition, run, attempt) {
|
|
3808
|
+
run.pendingAny = run.pendingAny || disposition.deliveryPending;
|
|
3809
|
+
const partial = disposition.entry;
|
|
3810
|
+
run.delivered.push(...partial.committedTokenIds);
|
|
3811
|
+
run.firstPartialId ??= partial.transferId;
|
|
3812
|
+
run.settledShortfalls.push(partial.transferId);
|
|
3813
|
+
this.accumulate(ctx, partial.committedTokenIds, run);
|
|
3814
|
+
this.settlePartial(ctx, partial);
|
|
3815
|
+
if (BigInt(partial.remainingAmount) <= 0n) {
|
|
3816
|
+
return this.finishSend(ctx.transferId, run);
|
|
3817
|
+
}
|
|
3818
|
+
if (attempt >= MAX_RESELECT) {
|
|
3819
|
+
throw new PartialSendConflictError(
|
|
3820
|
+
"Part of your payment was sent, but the remainder could not be completed within the re-plan budget. The delivered portion is final \u2014 re-plan only the shortfall, never the full amount.",
|
|
3821
|
+
run.firstPartialId,
|
|
3822
|
+
run.delivered,
|
|
3823
|
+
partial.remainingAmount
|
|
3824
|
+
);
|
|
3825
|
+
}
|
|
3826
|
+
run.amount = partial.remainingAmount;
|
|
3827
|
+
return null;
|
|
3828
|
+
}
|
|
3829
|
+
/**
|
|
3830
|
+
* One attempt's failure disposition: possibly-committed → rethrow UNWRAPPED;
|
|
3831
|
+
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
3832
|
+
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
3833
|
+
*/
|
|
3834
|
+
async disposeFailedAttempt(ctx, err) {
|
|
3835
|
+
if (isPossiblyCommittedSendOutcome(err)) {
|
|
3836
|
+
this.settleKeepOpen(ctx);
|
|
3837
|
+
this.stampTransferId(err, ctx.transferId);
|
|
3838
|
+
return { kind: "rethrow", error: err };
|
|
3839
|
+
}
|
|
3840
|
+
const backstop = await this.machineStores.backstop.get(ctx.transferId);
|
|
3841
|
+
if (backstop !== void 0 && backstop.disposition === "open") {
|
|
3842
|
+
return this.convergePartial(ctx);
|
|
3843
|
+
}
|
|
3844
|
+
this.settleFailure(ctx.transferId, ctx.coinId, ctx.sourceIds);
|
|
3845
|
+
if (classifyError(err) !== "conflict") return { kind: "rethrow", error: err };
|
|
3846
|
+
const demoted = await this.demoteSpentSources(ctx);
|
|
3847
|
+
return demoted > 0 ? { kind: "retry-full", error: err } : { kind: "rethrow", error: err };
|
|
3848
|
+
}
|
|
3849
|
+
/** #677/#690: ≥1 leg committed — the SAME machine, resumed in-process, converges it. */
|
|
3850
|
+
async convergePartial(ctx) {
|
|
3851
|
+
let outcome;
|
|
3852
|
+
try {
|
|
3853
|
+
outcome = await this.machine.resumeIntent({
|
|
3854
|
+
transferId: ctx.transferId,
|
|
3855
|
+
payload: ctx.plan.payload,
|
|
3856
|
+
sources: ctx.plan.sources,
|
|
3857
|
+
journaled: await this.machineStores.deliveryJournal.byTransfer(ctx.transferId)
|
|
3858
|
+
});
|
|
3859
|
+
} catch (err) {
|
|
3860
|
+
return this.disposeConvergeFailure(ctx, err);
|
|
3861
|
+
}
|
|
3862
|
+
if (outcome.partial === null) {
|
|
3863
|
+
this.settleSuccess(ctx, ctx.sourceIds);
|
|
3864
|
+
return { kind: "success", deliveryPending: outcome.deliveryPending };
|
|
3865
|
+
}
|
|
3866
|
+
return { kind: "partial", entry: outcome.partial, deliveryPending: outcome.deliveryPending };
|
|
3867
|
+
}
|
|
3868
|
+
async disposeConvergeFailure(ctx, err) {
|
|
3869
|
+
if (isPossiblyCommittedSendOutcome(err)) {
|
|
3870
|
+
this.settleKeepOpen(ctx);
|
|
3871
|
+
this.stampTransferId(err, ctx.transferId);
|
|
3872
|
+
return { kind: "rethrow", error: err };
|
|
3873
|
+
}
|
|
3874
|
+
if (classifyError(err) === "conflict") {
|
|
3875
|
+
await this.softAbort(ctx.transferId);
|
|
3876
|
+
this.settleFailure(ctx.transferId, ctx.coinId, ctx.sourceIds);
|
|
3877
|
+
const demoted = await this.demoteSpentSources(ctx);
|
|
3878
|
+
return demoted > 0 ? { kind: "retry-full", error: err } : { kind: "rethrow", error: err };
|
|
3879
|
+
}
|
|
3880
|
+
return { kind: "rethrow", error: err };
|
|
3881
|
+
}
|
|
3882
|
+
async planAndMaterialize(recipientPubkey, request) {
|
|
3883
|
+
const transferId = this.newId();
|
|
3884
|
+
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
3885
|
+
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
3886
|
+
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
3887
|
+
try {
|
|
3888
|
+
return await this.materialize(transferId, recipientPubkey, request, spend, sourceIds);
|
|
3889
|
+
} catch (err) {
|
|
3890
|
+
this.settleFailure(transferId, request.coinId, sourceIds);
|
|
3891
|
+
throw err;
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
/** Reserve happened synchronously inside queue.plan; mark + declare with no await between. */
|
|
3895
|
+
markPlanned(transferId, coinId, spend) {
|
|
3896
|
+
const sourceIds = [
|
|
3897
|
+
...spend.plan.direct,
|
|
3898
|
+
...spend.plan.split !== void 0 ? [spend.plan.split.tokenId] : []
|
|
3899
|
+
];
|
|
3900
|
+
for (const tokenId of sourceIds) this.view.markInFlight(tokenId);
|
|
3901
|
+
if (spend.plan.split !== void 0) {
|
|
3902
|
+
this.queue.declareExpectedChange(transferId, coinId, spend.plan.split.remainderAmount);
|
|
3903
|
+
}
|
|
3904
|
+
return sourceIds;
|
|
3905
|
+
}
|
|
3906
|
+
async materialize(transferId, recipientPubkey, request, spend, sourceIds) {
|
|
3907
|
+
const engine = this.engine();
|
|
3908
|
+
const blobs = await this.deps.storagePort.getBlobs([...sourceIds]);
|
|
3909
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3910
|
+
const sourceTokens = [];
|
|
3911
|
+
const spentStates = {};
|
|
3912
|
+
for (const tokenId of sourceIds) {
|
|
3913
|
+
const bytes = blobs.get(tokenId);
|
|
3914
|
+
if (bytes === void 0) {
|
|
3915
|
+
throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
3916
|
+
}
|
|
3917
|
+
const token = await engine.decodeToken({ v: TOKEN_BLOB_VERSION, network: 0, tokenId, token: bytes });
|
|
3918
|
+
const keys = await engine.deliveryKeys(bytes);
|
|
3919
|
+
spentStates[tokenId] = { local: keys.stateHash, protocol: keys.stateHash };
|
|
3920
|
+
sources.set(tokenId, token);
|
|
3921
|
+
sourceTokens.push(this.toUiToken(tokenId, request.coinId, engine.balanceOf(token, request.coinId)));
|
|
3922
|
+
}
|
|
3923
|
+
const payload = buildPayload(recipientPubkey, request, spend, spentStates);
|
|
3924
|
+
const plan = {
|
|
3925
|
+
transferId,
|
|
3926
|
+
recipientPubkey,
|
|
3927
|
+
payload,
|
|
3928
|
+
ops: buildOps(payload),
|
|
3929
|
+
sources,
|
|
3930
|
+
...request.memo !== void 0 ? { memo: request.memo } : {}
|
|
3931
|
+
};
|
|
3932
|
+
return { transferId, coinId: request.coinId, plan, sourceIds, sourceTokens };
|
|
3933
|
+
}
|
|
3934
|
+
/** isSpent sweep over the attempt's sources; demote proven-spent states (durable). */
|
|
3935
|
+
async demoteSpentSources(ctx) {
|
|
3936
|
+
const engine = this.engine();
|
|
3937
|
+
let demoted = 0;
|
|
3938
|
+
for (const [tokenId, token] of ctx.plan.sources) {
|
|
3939
|
+
const state = ctx.plan.payload.spentStates?.[tokenId];
|
|
3940
|
+
if (state === void 0) continue;
|
|
3941
|
+
try {
|
|
3942
|
+
if (await engine.isSpent(token)) {
|
|
3943
|
+
await this.view.demote(tokenId, state.protocol);
|
|
3944
|
+
demoted += 1;
|
|
3945
|
+
}
|
|
3946
|
+
} catch {
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
return demoted;
|
|
3950
|
+
}
|
|
3951
|
+
// ── attempt settlement (reservation / in-flight / expected-change hygiene) ─
|
|
3952
|
+
settleSuccess(ctx, spentIds) {
|
|
3953
|
+
this.ledger.commit(ctx.transferId);
|
|
3954
|
+
this.queue.clearExpectedChange(ctx.transferId);
|
|
3955
|
+
this.trackTail(this.refreshThenRelease(ctx.coinId, spentIds));
|
|
3956
|
+
}
|
|
3957
|
+
settleFailure(transferId, coinId, releaseIds) {
|
|
3958
|
+
this.ledger.cancel(transferId);
|
|
3959
|
+
this.queue.clearExpectedChange(transferId);
|
|
3960
|
+
for (const tokenId of releaseIds) this.view.release(tokenId);
|
|
3961
|
+
this.queue.notifyChange(coinId);
|
|
3962
|
+
}
|
|
3963
|
+
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
3964
|
+
settleKeepOpen(ctx) {
|
|
3965
|
+
this.queue.clearExpectedChange(ctx.transferId);
|
|
3966
|
+
}
|
|
3967
|
+
settlePartial(ctx, partial) {
|
|
3968
|
+
this.ledger.cancel(ctx.transferId);
|
|
3969
|
+
this.queue.clearExpectedChange(ctx.transferId);
|
|
3970
|
+
const committed = new Set(partial.committedTokenIds);
|
|
3971
|
+
for (const tokenId of ctx.sourceIds) {
|
|
3972
|
+
if (committed.has(tokenId)) continue;
|
|
3973
|
+
const state = ctx.plan.payload.spentStates?.[tokenId];
|
|
3974
|
+
if (state !== void 0) this.trackTail(this.view.demote(tokenId, state.protocol));
|
|
3975
|
+
this.view.release(tokenId);
|
|
3976
|
+
}
|
|
3977
|
+
this.trackTail(this.refreshThenRelease(ctx.coinId, [...committed]));
|
|
3978
|
+
}
|
|
3979
|
+
/** Release spent sources only AFTER the mirror refresh tombstoned them. */
|
|
3980
|
+
async refreshThenRelease(coinId, spentIds) {
|
|
3981
|
+
try {
|
|
3982
|
+
await this.view.delta(0);
|
|
3983
|
+
for (const tokenId of spentIds) this.view.release(tokenId);
|
|
3984
|
+
} finally {
|
|
3985
|
+
this.queue.notifyChange(coinId);
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
accumulate(ctx, committedIds, run) {
|
|
3989
|
+
const committed = new Set(committedIds);
|
|
3990
|
+
for (const op of ctx.plan.ops) {
|
|
3991
|
+
if (!committed.has(op.sourceTokenId)) continue;
|
|
3992
|
+
run.tokenTransfers.push({ sourceTokenId: op.sourceTokenId, method: op.kind });
|
|
3993
|
+
}
|
|
3994
|
+
for (const token of ctx.sourceTokens) {
|
|
3995
|
+
if (committed.has(token.id)) run.sentTokens.push(token);
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
async finishSend(transferId, run) {
|
|
3999
|
+
for (const id of run.settledShortfalls.splice(0)) {
|
|
4000
|
+
await this.machineStores.shortfalls.clear(id).catch(() => void 0);
|
|
4001
|
+
}
|
|
4002
|
+
const result = {
|
|
4003
|
+
id: transferId,
|
|
4004
|
+
status: run.pendingAny ? "confirmed" : "delivered",
|
|
4005
|
+
tokens: [...run.sentTokens],
|
|
4006
|
+
tokenTransfers: [...run.tokenTransfers],
|
|
4007
|
+
deliveryPending: run.pendingAny,
|
|
4008
|
+
deliveryState: run.pendingAny ? "pending-delivery" : "landed"
|
|
4009
|
+
};
|
|
4010
|
+
this.deps.emit("transfer:updated", result);
|
|
4011
|
+
return result;
|
|
4012
|
+
}
|
|
4013
|
+
/**
|
|
4014
|
+
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
4015
|
+
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
4016
|
+
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
4017
|
+
*/
|
|
4018
|
+
partialize(err, run) {
|
|
4019
|
+
if (run.delivered.length === 0) return err;
|
|
4020
|
+
return new PartialSendConflictError(
|
|
4021
|
+
"Part of your payment was sent; the remaining amount could not be completed (see cause). The delivered portion is final \u2014 re-plan only the shortfall, never the full amount.",
|
|
4022
|
+
run.firstPartialId ?? "",
|
|
4023
|
+
run.delivered,
|
|
4024
|
+
run.amount,
|
|
4025
|
+
err
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
4028
|
+
/** #441: possibly-committed errors must carry the transferId for the settling journal. */
|
|
4029
|
+
stampTransferId(err, transferId) {
|
|
4030
|
+
if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
|
|
4031
|
+
err.transferId = transferId;
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
async softAbort(transferId) {
|
|
4035
|
+
try {
|
|
4036
|
+
await this.deps.client.abortIntent(transferId);
|
|
4037
|
+
await this.machineStores.backstop.drop(transferId);
|
|
4038
|
+
} catch {
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
// ── mint (§5.9: journal-first, replayed at start, never blind re-mint) ─────
|
|
4042
|
+
async mintInner(coinId, amount) {
|
|
4043
|
+
if (!/^(?:[0-9a-f]{2})+$/.test(coinId) || amount <= 0n) {
|
|
4044
|
+
return { success: false, error: "coinId must be even-length lowercase hex and amount positive" };
|
|
4045
|
+
}
|
|
4046
|
+
const engine = this.engine();
|
|
4047
|
+
const mintId = this.newId();
|
|
4048
|
+
const params = this.mintParams(coinId, amount);
|
|
4049
|
+
const preDerived = supportsDeterministicMint(engine) ? await engine.deriveMintTokenId(params, mintId) : "";
|
|
4050
|
+
const entry = {
|
|
4051
|
+
mintId,
|
|
4052
|
+
coinId,
|
|
4053
|
+
amount: amount.toString(),
|
|
4054
|
+
tokenId: preDerived,
|
|
4055
|
+
createdAt: (this.deps.now ?? Date.now)()
|
|
4056
|
+
};
|
|
4057
|
+
await this.machineStores.mintJournal.record(entry);
|
|
4058
|
+
let token;
|
|
4059
|
+
try {
|
|
4060
|
+
token = await engine.mint(params, { transferId: mintId });
|
|
4061
|
+
} catch (err) {
|
|
4062
|
+
return { success: false, error: messageOf(err) };
|
|
4063
|
+
}
|
|
4064
|
+
if (token.blob.tokenId !== entry.tokenId) {
|
|
4065
|
+
await this.machineStores.mintJournal.record({ ...entry, tokenId: token.blob.tokenId });
|
|
4066
|
+
}
|
|
4067
|
+
try {
|
|
4068
|
+
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
4071
|
+
}
|
|
4072
|
+
return { success: true, tokenId: token.blob.tokenId };
|
|
4073
|
+
}
|
|
4074
|
+
async finalizeMint(engine, mintId, token, coinId, amount) {
|
|
4075
|
+
const bytes = token.blob.token;
|
|
4076
|
+
const digest = bytesToHex(sha256(bytes));
|
|
4077
|
+
const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
|
|
4078
|
+
const key = keys.get(digest);
|
|
4079
|
+
if (key === void 0) {
|
|
4080
|
+
throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
|
|
4081
|
+
}
|
|
4082
|
+
await this.deps.storagePort.applyDelta({
|
|
4083
|
+
transferId: mintId,
|
|
4084
|
+
spent: [],
|
|
4085
|
+
added: [{ tokenId: token.blob.tokenId, key }]
|
|
4086
|
+
});
|
|
4087
|
+
await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
|
|
4088
|
+
await this.machineStores.mintJournal.remove(mintId);
|
|
4089
|
+
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
4090
|
+
this.trackTail(this.view.delta(0));
|
|
4091
|
+
}
|
|
4092
|
+
async replayMints() {
|
|
4093
|
+
for (const entry of await this.machineStores.mintJournal.list()) {
|
|
4094
|
+
try {
|
|
4095
|
+
await this.replayMint(entry);
|
|
4096
|
+
} catch {
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
async replayMint(entry) {
|
|
4101
|
+
if (entry.tokenId !== "" && await this.tokenInServerInventory(entry.tokenId)) {
|
|
4102
|
+
await this.machineStores.mintJournal.remove(entry.mintId);
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
const engine = this.engine();
|
|
4106
|
+
if (!supportsDeterministicMint(engine)) {
|
|
4107
|
+
this.deps.emit("transfer:attention", {
|
|
4108
|
+
transferId: entry.mintId,
|
|
4109
|
+
code: ATTENTION_MINT_UNRESOLVED
|
|
4110
|
+
});
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
|
|
4114
|
+
transferId: entry.mintId
|
|
4115
|
+
});
|
|
4116
|
+
if (token.blob.tokenId !== entry.tokenId) {
|
|
4117
|
+
await this.machineStores.mintJournal.record({ ...entry, tokenId: token.blob.tokenId });
|
|
4118
|
+
}
|
|
4119
|
+
await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
|
|
4120
|
+
}
|
|
4121
|
+
mintParams(coinId, amount) {
|
|
4122
|
+
return {
|
|
4123
|
+
recipientPubkey: this.ownPubkeyBytes,
|
|
4124
|
+
value: { assets: [{ coinId, amount }] }
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
async tokenInServerInventory(tokenId) {
|
|
4128
|
+
let page = await this.deps.storagePort.listInventory();
|
|
4129
|
+
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
4130
|
+
if (page.items.some((item) => item.tokenId === tokenId && item.status === "active")) {
|
|
4131
|
+
return true;
|
|
4132
|
+
}
|
|
4133
|
+
if (!page.more) return false;
|
|
4134
|
+
page = await this.deps.storagePort.listInventory(page.cursor);
|
|
4135
|
+
}
|
|
4136
|
+
return false;
|
|
4137
|
+
}
|
|
4138
|
+
// ── wiring internals ───────────────────────────────────────────────────────
|
|
4139
|
+
engine() {
|
|
4140
|
+
return this.currentEngine ?? this.deps.engineRef();
|
|
4141
|
+
}
|
|
4142
|
+
newId() {
|
|
4143
|
+
return (this.deps.newId ?? randomUUID)();
|
|
4144
|
+
}
|
|
4145
|
+
async runResume() {
|
|
4146
|
+
const report = await resumeAll(this.machineDeps);
|
|
4147
|
+
await this.requests.reconcile({
|
|
4148
|
+
resumed: report.resumed,
|
|
4149
|
+
conflicted: report.conflicted,
|
|
4150
|
+
open: report.failed
|
|
4151
|
+
});
|
|
4152
|
+
if (report.resumed.length > 0 || report.conflicted.length > 0) {
|
|
4153
|
+
await this.view.delta(0).catch(() => void 0);
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
async seedHeldStates() {
|
|
4157
|
+
let page = await this.deps.storagePort.listInventory();
|
|
4158
|
+
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
4159
|
+
for (const item of page.items) {
|
|
4160
|
+
if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
|
|
4161
|
+
}
|
|
4162
|
+
if (!page.more) return;
|
|
4163
|
+
page = await this.deps.storagePort.listInventory(page.cursor);
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
toUiToken(tokenId, coinId, amount) {
|
|
4167
|
+
const registry = this.deps.registry;
|
|
4168
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
4169
|
+
const now = (this.deps.now ?? Date.now)();
|
|
4170
|
+
return {
|
|
4171
|
+
id: tokenId,
|
|
4172
|
+
coinId,
|
|
4173
|
+
symbol: registry.getSymbol(coinId),
|
|
4174
|
+
name: registry.getName(coinId),
|
|
4175
|
+
decimals: registry.getDecimals(coinId),
|
|
4176
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
4177
|
+
amount: amount.toString(),
|
|
4178
|
+
status: "transferring",
|
|
4179
|
+
createdAt: now,
|
|
4180
|
+
updatedAt: now,
|
|
4181
|
+
lazy: true
|
|
4182
|
+
};
|
|
4183
|
+
}
|
|
4184
|
+
track(op) {
|
|
4185
|
+
const settled = op.then(
|
|
4186
|
+
() => void 0,
|
|
4187
|
+
() => void 0
|
|
4188
|
+
);
|
|
4189
|
+
this.pendingOps.add(settled);
|
|
4190
|
+
void settled.finally(() => this.pendingOps.delete(settled));
|
|
4191
|
+
return op;
|
|
4192
|
+
}
|
|
4193
|
+
trackTail(op) {
|
|
4194
|
+
this.track(op).catch(() => void 0);
|
|
4195
|
+
}
|
|
4196
|
+
};
|
|
4197
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
4198
|
+
return {
|
|
4199
|
+
v: 2,
|
|
4200
|
+
recipient: recipientPubkey,
|
|
4201
|
+
coinId: request.coinId,
|
|
4202
|
+
amount: request.amount,
|
|
4203
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
4204
|
+
direct: [...spend.plan.direct],
|
|
4205
|
+
...spend.plan.split !== void 0 ? {
|
|
4206
|
+
split: {
|
|
4207
|
+
tokenId: spend.plan.split.tokenId,
|
|
4208
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
4209
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
4210
|
+
}
|
|
4211
|
+
} : {},
|
|
4212
|
+
spentStates
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
4215
|
+
function messageOf(err) {
|
|
4216
|
+
return err instanceof Error ? err.message : String(err);
|
|
4217
|
+
}
|
|
4218
|
+
export {
|
|
4219
|
+
ATTENTION_MINT_UNRESOLVED,
|
|
4220
|
+
MAX_RESELECT,
|
|
4221
|
+
PaymentsFacade,
|
|
4222
|
+
STORE_KEYS,
|
|
4223
|
+
createScopedKV,
|
|
4224
|
+
supportsDeterministicMint
|
|
4225
|
+
};
|
|
4226
|
+
/*! Bundled license information:
|
|
4227
|
+
|
|
4228
|
+
@noble/ciphers/utils.js:
|
|
4229
|
+
(*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *)
|
|
4230
|
+
*/
|
|
4231
|
+
//# sourceMappingURL=index.js.map
|