@unicitylabs/sphere-sdk 0.14.0-dev.5 → 0.14.0-dev.7
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/core/index.cjs +713 -252
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +111 -87
- package/dist/core/index.d.ts +111 -87
- package/dist/core/index.js +713 -252
- package/dist/core/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 +45 -5
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.d.cts +26 -2
- package/dist/impl/wallet-api-v2/index.d.ts +26 -2
- package/dist/impl/wallet-api-v2/index.js +45 -5
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +713 -252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -88
- package/dist/index.d.ts +112 -88
- package/dist/index.js +713 -252
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +634 -228
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +112 -20
- package/dist/modules/payments-v2/index.d.ts +112 -20
- package/dist/modules/payments-v2/index.js +631 -228
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -23,7 +23,10 @@ var STORE_KEYS = {
|
|
|
23
23
|
shortfalls: "shortfalls",
|
|
24
24
|
settlingLinks: "settling",
|
|
25
25
|
streamCursor: (s) => `cursor:${s}`,
|
|
26
|
-
epochLatch: "epoch-latch"
|
|
26
|
+
epochLatch: "epoch-latch",
|
|
27
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
28
|
+
suspectedSpent: "suspected-spent",
|
|
29
|
+
knownSpends: "known-spends"
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
// node_modules/@noble/hashes/utils.js
|
|
@@ -549,6 +552,10 @@ function isValidationReject(err) {
|
|
|
549
552
|
if (e === null) return false;
|
|
550
553
|
return e.status === 422 || e.statusCode === 422 || e.code === "VALIDATION" || e.code === "VALIDATION_FAILED";
|
|
551
554
|
}
|
|
555
|
+
function retryAfterMsOf(err) {
|
|
556
|
+
const retryAfter = err !== null && typeof err === "object" ? err.retryAfter : void 0;
|
|
557
|
+
return typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : null;
|
|
558
|
+
}
|
|
552
559
|
var chainRegistry = /* @__PURE__ */ new WeakMap();
|
|
553
560
|
var replayRegistry = /* @__PURE__ */ new WeakMap();
|
|
554
561
|
function registryFor(registry, kv, storeKey, make) {
|
|
@@ -811,7 +818,7 @@ var TransferMachine = class {
|
|
|
811
818
|
}
|
|
812
819
|
if (cls === null) {
|
|
813
820
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
814
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
821
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
815
822
|
return { committed, deliveryPending };
|
|
816
823
|
}
|
|
817
824
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -837,10 +844,10 @@ var TransferMachine = class {
|
|
|
837
844
|
}
|
|
838
845
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
839
846
|
if (conflicts.length > 0) {
|
|
840
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
847
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
841
848
|
}
|
|
842
849
|
await this.completeIntent(r.transferId, r.payload);
|
|
843
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
850
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
844
851
|
return { deliveryPending, partial: null };
|
|
845
852
|
}
|
|
846
853
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -1022,7 +1029,7 @@ var TransferMachine = class {
|
|
|
1022
1029
|
}
|
|
1023
1030
|
return added;
|
|
1024
1031
|
}
|
|
1025
|
-
async settlePartial(r, committed, conflicts) {
|
|
1032
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
1026
1033
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
1027
1034
|
const entry = {
|
|
1028
1035
|
transferId: r.transferId,
|
|
@@ -1034,26 +1041,27 @@ var TransferMachine = class {
|
|
|
1034
1041
|
};
|
|
1035
1042
|
await this.stores.shortfalls.upsert(entry);
|
|
1036
1043
|
await this.completeIntent(r.transferId, r.payload);
|
|
1037
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
1044
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
1038
1045
|
return entry;
|
|
1039
1046
|
}
|
|
1040
|
-
closeTail(transferId, payload) {
|
|
1047
|
+
closeTail(engine, transferId, payload, committed) {
|
|
1041
1048
|
void (async () => {
|
|
1042
1049
|
try {
|
|
1043
1050
|
await this.completeIntent(transferId, payload);
|
|
1044
1051
|
} catch {
|
|
1045
1052
|
return;
|
|
1046
1053
|
}
|
|
1047
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
1054
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
1048
1055
|
})();
|
|
1049
1056
|
}
|
|
1050
1057
|
async completeIntent(transferId, payload) {
|
|
1051
1058
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
1052
1059
|
await this.deps.intents.complete(transferId, signature);
|
|
1053
1060
|
}
|
|
1054
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
1061
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
1055
1062
|
try {
|
|
1056
|
-
await this.
|
|
1063
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
1064
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
1057
1065
|
} catch {
|
|
1058
1066
|
}
|
|
1059
1067
|
try {
|
|
@@ -1061,6 +1069,26 @@ var TransferMachine = class {
|
|
|
1061
1069
|
} catch {
|
|
1062
1070
|
}
|
|
1063
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
1074
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
1075
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
1076
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
1077
|
+
*/
|
|
1078
|
+
async settledAmount(engine, coinId, committed) {
|
|
1079
|
+
let total = 0n;
|
|
1080
|
+
for (const outcome of committed) {
|
|
1081
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
1082
|
+
const token = await engine.decodeToken({
|
|
1083
|
+
v: TOKEN_BLOB_VERSION,
|
|
1084
|
+
network: 0,
|
|
1085
|
+
tokenId: "",
|
|
1086
|
+
token: outcome.recipientBlob
|
|
1087
|
+
});
|
|
1088
|
+
total += engine.balanceOf(token, coinId);
|
|
1089
|
+
}
|
|
1090
|
+
return total.toString();
|
|
1091
|
+
}
|
|
1064
1092
|
async disposeOnFailure(transferId, committed, cls) {
|
|
1065
1093
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
1066
1094
|
try {
|
|
@@ -1085,7 +1113,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
1085
1113
|
}
|
|
1086
1114
|
|
|
1087
1115
|
// modules/payments-v2/machine/resume.ts
|
|
1088
|
-
async function resumeAll(deps) {
|
|
1116
|
+
async function resumeAll(deps, options = {}) {
|
|
1089
1117
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
1090
1118
|
const stores = createMachineStores(deps.kv);
|
|
1091
1119
|
const open = await deps.intents.listOpen();
|
|
@@ -1095,7 +1123,12 @@ async function resumeAll(deps) {
|
|
|
1095
1123
|
} catch {
|
|
1096
1124
|
return report;
|
|
1097
1125
|
}
|
|
1098
|
-
await
|
|
1126
|
+
await dropStaleBackstops(
|
|
1127
|
+
stores,
|
|
1128
|
+
locals,
|
|
1129
|
+
new Set(open.map((i) => i.transferId)),
|
|
1130
|
+
options.isLocallyActive ?? (() => false)
|
|
1131
|
+
);
|
|
1099
1132
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
1100
1133
|
const ctx = {
|
|
1101
1134
|
deps,
|
|
@@ -1108,11 +1141,16 @@ async function resumeAll(deps) {
|
|
|
1108
1141
|
}
|
|
1109
1142
|
return report;
|
|
1110
1143
|
}
|
|
1111
|
-
async function
|
|
1144
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
1112
1145
|
for (const entry of locals.values()) {
|
|
1113
|
-
if (
|
|
1146
|
+
if (openIds.has(entry.transferId)) continue;
|
|
1147
|
+
if (entry.disposition !== "open") {
|
|
1114
1148
|
await stores.backstop.removeByKey(entry.transferId);
|
|
1149
|
+
continue;
|
|
1115
1150
|
}
|
|
1151
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
1152
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
1153
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
1116
1154
|
}
|
|
1117
1155
|
}
|
|
1118
1156
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -1225,6 +1263,349 @@ function validatePayload(raw) {
|
|
|
1225
1263
|
return p;
|
|
1226
1264
|
}
|
|
1227
1265
|
|
|
1266
|
+
// modules/payments-v2/convergence.ts
|
|
1267
|
+
var HEARTBEAT_SEED_MS = 5e3;
|
|
1268
|
+
var HEARTBEAT_CAP_MS = 12e4;
|
|
1269
|
+
var ConvergenceHeartbeat = class {
|
|
1270
|
+
constructor(deps) {
|
|
1271
|
+
this.deps = deps;
|
|
1272
|
+
}
|
|
1273
|
+
timer = null;
|
|
1274
|
+
delayMs = HEARTBEAT_SEED_MS;
|
|
1275
|
+
notBefore = 0;
|
|
1276
|
+
running = false;
|
|
1277
|
+
start() {
|
|
1278
|
+
this.running = true;
|
|
1279
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
1280
|
+
this.notBefore = 0;
|
|
1281
|
+
}
|
|
1282
|
+
/** §7 quiescence unchanged: a mid-backoff stop() cancels cleanly. */
|
|
1283
|
+
stop() {
|
|
1284
|
+
this.running = false;
|
|
1285
|
+
this.clearTimer();
|
|
1286
|
+
}
|
|
1287
|
+
/** Reset to seed on progress, ×2 to the cap otherwise; idle stops rescheduling. */
|
|
1288
|
+
settle(outcome, pending) {
|
|
1289
|
+
if (outcome.retryAfterMs !== null) {
|
|
1290
|
+
this.notBefore = Math.max(this.notBefore, this.deps.now() + outcome.retryAfterMs);
|
|
1291
|
+
}
|
|
1292
|
+
if (!pending) {
|
|
1293
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const delay = outcome.progress ? HEARTBEAT_SEED_MS : this.delayMs;
|
|
1297
|
+
this.delayMs = Math.min(delay * 2, HEARTBEAT_CAP_MS);
|
|
1298
|
+
this.schedule(delay);
|
|
1299
|
+
}
|
|
1300
|
+
/** New pending work (keep-open park / journaled delivery / mint hold): prompt retry. */
|
|
1301
|
+
arm() {
|
|
1302
|
+
if (!this.running) return;
|
|
1303
|
+
this.delayMs = Math.min(HEARTBEAT_SEED_MS * 2, HEARTBEAT_CAP_MS);
|
|
1304
|
+
this.schedule(HEARTBEAT_SEED_MS);
|
|
1305
|
+
}
|
|
1306
|
+
/** connection:status recovery: the backoff resets; a pending tick re-fires at the seed. */
|
|
1307
|
+
recovered() {
|
|
1308
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
1309
|
+
if (this.timer !== null) this.arm();
|
|
1310
|
+
}
|
|
1311
|
+
schedule(delayMs) {
|
|
1312
|
+
this.clearTimer();
|
|
1313
|
+
if (!this.running) return;
|
|
1314
|
+
const floored = Math.max(delayMs, this.notBefore - this.deps.now());
|
|
1315
|
+
this.timer = setTimeout(() => {
|
|
1316
|
+
this.timer = null;
|
|
1317
|
+
this.deps.onTick();
|
|
1318
|
+
}, floored);
|
|
1319
|
+
}
|
|
1320
|
+
clearTimer() {
|
|
1321
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
1322
|
+
this.timer = null;
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
var Converger = class {
|
|
1326
|
+
constructor(deps) {
|
|
1327
|
+
this.deps = deps;
|
|
1328
|
+
}
|
|
1329
|
+
/** Resume open intents, then drain the delivery + mint journals (all idempotent). */
|
|
1330
|
+
async convergeOnce() {
|
|
1331
|
+
let progress = false;
|
|
1332
|
+
let retryAfterMs = null;
|
|
1333
|
+
try {
|
|
1334
|
+
progress = await this.resumePass();
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
retryAfterMs = retryAfterMsOf(err);
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
const stats = await this.deps.stores.deliveryJournal.replay(this.replayDeps());
|
|
1340
|
+
progress = progress || stats.delivered > 0;
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
1343
|
+
}
|
|
1344
|
+
try {
|
|
1345
|
+
progress = await this.deps.replayMints() > 0 || progress;
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
1348
|
+
}
|
|
1349
|
+
return { progress, retryAfterMs };
|
|
1350
|
+
}
|
|
1351
|
+
/** §4 heartbeat predicate — read fresh from the §6 stores, never cached. */
|
|
1352
|
+
async pendingWork() {
|
|
1353
|
+
const [backstop, journal, mints] = await Promise.all([
|
|
1354
|
+
this.deps.stores.backstop.list(),
|
|
1355
|
+
this.deps.stores.deliveryJournal.list(),
|
|
1356
|
+
this.deps.stores.mintJournal.list()
|
|
1357
|
+
]);
|
|
1358
|
+
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
1359
|
+
}
|
|
1360
|
+
async resumePass() {
|
|
1361
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
1362
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
1363
|
+
});
|
|
1364
|
+
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
1365
|
+
await this.deps.reconcile({
|
|
1366
|
+
resumed: report.resumed,
|
|
1367
|
+
conflicted: report.conflicted,
|
|
1368
|
+
open: report.failed
|
|
1369
|
+
});
|
|
1370
|
+
if (progress) await this.deps.refreshView().catch(() => void 0);
|
|
1371
|
+
return progress;
|
|
1372
|
+
}
|
|
1373
|
+
filteredMachineDeps() {
|
|
1374
|
+
const intents = this.deps.machineDeps.intents;
|
|
1375
|
+
return {
|
|
1376
|
+
...this.deps.machineDeps,
|
|
1377
|
+
intents: {
|
|
1378
|
+
...intents,
|
|
1379
|
+
listOpen: async () => (await intents.listOpen()).filter((intent) => !this.deps.isActiveOp(intent.transferId))
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
replayDeps() {
|
|
1384
|
+
return {
|
|
1385
|
+
delivery: this.deps.delivery,
|
|
1386
|
+
now: () => this.deps.now(),
|
|
1387
|
+
attention: (transferId, code, detail) => {
|
|
1388
|
+
this.deps.emit(
|
|
1389
|
+
"transfer:attention",
|
|
1390
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
async function derivePendingTransfers(stores, decryptPayload) {
|
|
1397
|
+
const [backstop, journal, shortfalls] = await Promise.all([
|
|
1398
|
+
stores.backstop.list(),
|
|
1399
|
+
stores.deliveryJournal.list(),
|
|
1400
|
+
stores.shortfalls.list()
|
|
1401
|
+
]);
|
|
1402
|
+
const byTransfer = groupJournal(journal);
|
|
1403
|
+
const out = [];
|
|
1404
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1405
|
+
for (const entry of backstop) {
|
|
1406
|
+
if (entry.disposition !== "open") continue;
|
|
1407
|
+
seen.add(entry.transferId);
|
|
1408
|
+
out.push(await openRow(entry, byTransfer.get(entry.transferId) ?? [], decryptPayload));
|
|
1409
|
+
}
|
|
1410
|
+
for (const [transferId, entries] of byTransfer) {
|
|
1411
|
+
if (seen.has(transferId)) continue;
|
|
1412
|
+
seen.add(transferId);
|
|
1413
|
+
out.push(journalOnlyRow(transferId, entries));
|
|
1414
|
+
}
|
|
1415
|
+
for (const entry of shortfalls) {
|
|
1416
|
+
if (!seen.has(entry.transferId)) out.push(shortfallRow(entry));
|
|
1417
|
+
}
|
|
1418
|
+
return out;
|
|
1419
|
+
}
|
|
1420
|
+
function groupJournal(journal) {
|
|
1421
|
+
const byTransfer = /* @__PURE__ */ new Map();
|
|
1422
|
+
for (const entry of journal) {
|
|
1423
|
+
const mine = byTransfer.get(entry.transferId);
|
|
1424
|
+
if (mine === void 0) byTransfer.set(entry.transferId, [entry]);
|
|
1425
|
+
else mine.push(entry);
|
|
1426
|
+
}
|
|
1427
|
+
return byTransfer;
|
|
1428
|
+
}
|
|
1429
|
+
async function openRow(entry, journal, decryptPayload) {
|
|
1430
|
+
let payload = null;
|
|
1431
|
+
try {
|
|
1432
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
1433
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
1434
|
+
} catch {
|
|
1435
|
+
payload = null;
|
|
1436
|
+
}
|
|
1437
|
+
const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
|
|
1438
|
+
const certified = distinctOpCount(journal);
|
|
1439
|
+
return {
|
|
1440
|
+
transferId: entry.transferId,
|
|
1441
|
+
kind: "open",
|
|
1442
|
+
recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
|
|
1443
|
+
coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
|
|
1444
|
+
amount: typeof payload?.amount === "string" ? payload.amount : "",
|
|
1445
|
+
legs: { certified, total: Math.max(total, certified) },
|
|
1446
|
+
deliveryPending: journal.length > 0,
|
|
1447
|
+
createdAt: entry.createdAt
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function journalOnlyRow(transferId, entries) {
|
|
1451
|
+
const certified = distinctOpCount(entries);
|
|
1452
|
+
return {
|
|
1453
|
+
transferId,
|
|
1454
|
+
kind: "open",
|
|
1455
|
+
recipient: entries[0].recipientPubkey,
|
|
1456
|
+
coinId: "",
|
|
1457
|
+
amount: "",
|
|
1458
|
+
legs: { certified, total: certified },
|
|
1459
|
+
deliveryPending: true,
|
|
1460
|
+
createdAt: 0
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function shortfallRow(entry) {
|
|
1464
|
+
return {
|
|
1465
|
+
transferId: entry.transferId,
|
|
1466
|
+
kind: "shortfall",
|
|
1467
|
+
recipient: entry.recipient,
|
|
1468
|
+
coinId: entry.coinId,
|
|
1469
|
+
amount: entry.remainingAmount,
|
|
1470
|
+
legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
|
|
1471
|
+
deliveryPending: false,
|
|
1472
|
+
createdAt: entry.createdAt
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function distinctOpCount(entries) {
|
|
1476
|
+
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// modules/payments-v2/restore.ts
|
|
1480
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
1481
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
1482
|
+
async function reseedAndReset(deps) {
|
|
1483
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
1484
|
+
if (entry.disposition !== "open") continue;
|
|
1485
|
+
try {
|
|
1486
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
1487
|
+
if (entry.requiresSeedClose) {
|
|
1488
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
1489
|
+
}
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
1495
|
+
await deps.fullRePull();
|
|
1496
|
+
}
|
|
1497
|
+
async function splitOpIndex(deps, envelope) {
|
|
1498
|
+
const payload = await deps.decryptPayload(envelope);
|
|
1499
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
1503
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
1504
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
1505
|
+
return {
|
|
1506
|
+
id: tokenId,
|
|
1507
|
+
coinId: asset.coinId,
|
|
1508
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
1509
|
+
name: registry.getName(asset.coinId),
|
|
1510
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
1511
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
1512
|
+
amount: asset.amount,
|
|
1513
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
1514
|
+
createdAt: snapshot.createdAt,
|
|
1515
|
+
updatedAt: snapshot.updatedAt,
|
|
1516
|
+
lazy: true,
|
|
1517
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
1521
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1522
|
+
for (const [tokenId, entry] of entries) {
|
|
1523
|
+
const moving = isTransferring(tokenId);
|
|
1524
|
+
for (const asset of entry.assets) {
|
|
1525
|
+
let group = groups.get(asset.coinId);
|
|
1526
|
+
if (!group) {
|
|
1527
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
1528
|
+
groups.set(asset.coinId, group);
|
|
1529
|
+
}
|
|
1530
|
+
if (moving) {
|
|
1531
|
+
group.transferring += BigInt(asset.amount);
|
|
1532
|
+
group.transferringCount += 1;
|
|
1533
|
+
} else {
|
|
1534
|
+
group.total += BigInt(asset.amount);
|
|
1535
|
+
group.count += 1;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
1540
|
+
}
|
|
1541
|
+
function toAsset(coinId, totals, registry) {
|
|
1542
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
1543
|
+
return {
|
|
1544
|
+
coinId,
|
|
1545
|
+
symbol: registry.getSymbol(coinId),
|
|
1546
|
+
name: registry.getName(coinId),
|
|
1547
|
+
decimals: registry.getDecimals(coinId),
|
|
1548
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
1549
|
+
totalAmount: totals.total.toString(),
|
|
1550
|
+
tokenCount: totals.count,
|
|
1551
|
+
confirmedAmount: totals.total.toString(),
|
|
1552
|
+
unconfirmedAmount: "0",
|
|
1553
|
+
confirmedTokenCount: totals.count,
|
|
1554
|
+
unconfirmedTokenCount: 0,
|
|
1555
|
+
transferringTokenCount: totals.transferringCount,
|
|
1556
|
+
transferringAmount: totals.transferring.toString(),
|
|
1557
|
+
priceUsd: null,
|
|
1558
|
+
priceEur: null,
|
|
1559
|
+
change24h: null,
|
|
1560
|
+
fiatValueUsd: null,
|
|
1561
|
+
fiatValueEur: null
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
async function withPrices(raw, registry, price) {
|
|
1565
|
+
try {
|
|
1566
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
1567
|
+
const quotes = await price.getPrices(names);
|
|
1568
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
1569
|
+
} catch {
|
|
1570
|
+
return raw;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function priceOne(asset, quote) {
|
|
1574
|
+
if (!quote) return asset;
|
|
1575
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
1576
|
+
return {
|
|
1577
|
+
...asset,
|
|
1578
|
+
priceUsd: quote.priceUsd,
|
|
1579
|
+
priceEur: quote.priceEur ?? null,
|
|
1580
|
+
change24h: quote.change24h ?? null,
|
|
1581
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
1582
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// modules/payments-v2/machine/payload.ts
|
|
1587
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
1588
|
+
return {
|
|
1589
|
+
v: 2,
|
|
1590
|
+
recipient: recipientPubkey,
|
|
1591
|
+
coinId: request.coinId,
|
|
1592
|
+
amount: request.amount,
|
|
1593
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
1594
|
+
direct: [...spend.plan.direct],
|
|
1595
|
+
...spend.plan.split !== void 0 ? {
|
|
1596
|
+
split: {
|
|
1597
|
+
tokenId: spend.plan.split.tokenId,
|
|
1598
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
1599
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
1600
|
+
}
|
|
1601
|
+
} : {},
|
|
1602
|
+
spentStates
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
function messageOf(err) {
|
|
1606
|
+
return err instanceof Error ? err.message : String(err);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1228
1609
|
// node_modules/@noble/ciphers/utils.js
|
|
1229
1610
|
function isBytes2(a) {
|
|
1230
1611
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
@@ -2133,13 +2514,14 @@ var History = class {
|
|
|
2133
2514
|
}
|
|
2134
2515
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
2135
2516
|
async post(build) {
|
|
2517
|
+
const record = build();
|
|
2136
2518
|
try {
|
|
2137
|
-
await this.deps.client.postHistory([
|
|
2519
|
+
await this.deps.client.postHistory([record]);
|
|
2138
2520
|
} catch (err) {
|
|
2139
2521
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
2140
2522
|
return;
|
|
2141
2523
|
}
|
|
2142
|
-
this.deps.emit("history:updated");
|
|
2524
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
2143
2525
|
}
|
|
2144
2526
|
ts(timestamp) {
|
|
2145
2527
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -2196,93 +2578,9 @@ var History = class {
|
|
|
2196
2578
|
}
|
|
2197
2579
|
};
|
|
2198
2580
|
|
|
2199
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
2200
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
2201
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
2202
|
-
return {
|
|
2203
|
-
id: tokenId,
|
|
2204
|
-
coinId: asset.coinId,
|
|
2205
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
2206
|
-
name: registry.getName(asset.coinId),
|
|
2207
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
2208
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
2209
|
-
amount: asset.amount,
|
|
2210
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
2211
|
-
createdAt: snapshot.createdAt,
|
|
2212
|
-
updatedAt: snapshot.updatedAt,
|
|
2213
|
-
lazy: true,
|
|
2214
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
2215
|
-
};
|
|
2216
|
-
}
|
|
2217
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
2218
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2219
|
-
for (const [tokenId, entry] of entries) {
|
|
2220
|
-
const moving = isTransferring(tokenId);
|
|
2221
|
-
for (const asset of entry.assets) {
|
|
2222
|
-
let group = groups.get(asset.coinId);
|
|
2223
|
-
if (!group) {
|
|
2224
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
2225
|
-
groups.set(asset.coinId, group);
|
|
2226
|
-
}
|
|
2227
|
-
if (moving) {
|
|
2228
|
-
group.transferring += BigInt(asset.amount);
|
|
2229
|
-
group.transferringCount += 1;
|
|
2230
|
-
} else {
|
|
2231
|
-
group.total += BigInt(asset.amount);
|
|
2232
|
-
group.count += 1;
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
2237
|
-
}
|
|
2238
|
-
function toAsset(coinId, totals, registry) {
|
|
2239
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
2240
|
-
return {
|
|
2241
|
-
coinId,
|
|
2242
|
-
symbol: registry.getSymbol(coinId),
|
|
2243
|
-
name: registry.getName(coinId),
|
|
2244
|
-
decimals: registry.getDecimals(coinId),
|
|
2245
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
2246
|
-
totalAmount: totals.total.toString(),
|
|
2247
|
-
tokenCount: totals.count,
|
|
2248
|
-
confirmedAmount: totals.total.toString(),
|
|
2249
|
-
unconfirmedAmount: "0",
|
|
2250
|
-
confirmedTokenCount: totals.count,
|
|
2251
|
-
unconfirmedTokenCount: 0,
|
|
2252
|
-
transferringTokenCount: totals.transferringCount,
|
|
2253
|
-
transferringAmount: totals.transferring.toString(),
|
|
2254
|
-
priceUsd: null,
|
|
2255
|
-
priceEur: null,
|
|
2256
|
-
change24h: null,
|
|
2257
|
-
fiatValueUsd: null,
|
|
2258
|
-
fiatValueEur: null
|
|
2259
|
-
};
|
|
2260
|
-
}
|
|
2261
|
-
async function withPrices(raw, registry, price) {
|
|
2262
|
-
try {
|
|
2263
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
2264
|
-
const quotes = await price.getPrices(names);
|
|
2265
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
2266
|
-
} catch {
|
|
2267
|
-
return raw;
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
function priceOne(asset, quote) {
|
|
2271
|
-
if (!quote) return asset;
|
|
2272
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
2273
|
-
return {
|
|
2274
|
-
...asset,
|
|
2275
|
-
priceUsd: quote.priceUsd,
|
|
2276
|
-
priceEur: quote.priceEur ?? null,
|
|
2277
|
-
change24h: quote.change24h ?? null,
|
|
2278
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
2279
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
2280
|
-
};
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
2581
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
2284
|
-
var SUSPECTED_KEY =
|
|
2285
|
-
var KNOWN_SPENDS_KEY =
|
|
2582
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
2583
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
2286
2584
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
2287
2585
|
function stateKey(tokenId, stateHash) {
|
|
2288
2586
|
return `${tokenId}:${stateHash}`;
|
|
@@ -2678,19 +2976,24 @@ var Receive = class {
|
|
|
2678
2976
|
const engine = deps.engine();
|
|
2679
2977
|
const stored = [];
|
|
2680
2978
|
const pending = [];
|
|
2681
|
-
|
|
2979
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
2682
2980
|
try {
|
|
2683
|
-
epoch = deps.syncEpoch();
|
|
2684
2981
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
2685
|
-
|
|
2686
|
-
for
|
|
2687
|
-
await
|
|
2688
|
-
|
|
2982
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
2983
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
2984
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
2985
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
2986
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
2987
|
+
}
|
|
2988
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
2989
|
+
const served = deps.delivery.incomingEpoch();
|
|
2990
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
2991
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
2992
|
+
basis = null;
|
|
2689
2993
|
}
|
|
2690
|
-
await flushAcks(deps, pending, epoch);
|
|
2691
2994
|
} catch (err) {
|
|
2692
2995
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
2693
|
-
await flushAcks(deps, pending,
|
|
2996
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
2694
2997
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
2695
2998
|
});
|
|
2696
2999
|
}
|
|
@@ -2763,7 +3066,7 @@ async function announce(deps, entry, record) {
|
|
|
2763
3066
|
receivedAt
|
|
2764
3067
|
};
|
|
2765
3068
|
}
|
|
2766
|
-
async function flushAcks(deps, pending,
|
|
3069
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
2767
3070
|
let lastAcked = null;
|
|
2768
3071
|
try {
|
|
2769
3072
|
while (pending.length > 0) {
|
|
@@ -2774,7 +3077,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
2774
3077
|
}
|
|
2775
3078
|
} finally {
|
|
2776
3079
|
if (lastAcked !== null) {
|
|
2777
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
3080
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
2778
3081
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
2779
3082
|
}
|
|
2780
3083
|
}
|
|
@@ -2875,9 +3178,19 @@ var Requests = class {
|
|
|
2875
3178
|
}
|
|
2876
3179
|
});
|
|
2877
3180
|
}
|
|
3181
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
2878
3182
|
writeLink(requestId, transferId, committed) {
|
|
2879
3183
|
return this.mutateJournal((m) => {
|
|
2880
|
-
|
|
3184
|
+
const prev = m.get(requestId);
|
|
3185
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
3186
|
+
const next = {
|
|
3187
|
+
requestId,
|
|
3188
|
+
transferId,
|
|
3189
|
+
committed: committed || same && prev.committed,
|
|
3190
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
3191
|
+
};
|
|
3192
|
+
if (same && prev.committed === next.committed) return false;
|
|
3193
|
+
m.set(requestId, next);
|
|
2881
3194
|
return true;
|
|
2882
3195
|
});
|
|
2883
3196
|
}
|
|
@@ -3030,23 +3343,46 @@ var Requests = class {
|
|
|
3030
3343
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
3031
3344
|
if (transferId !== void 0) {
|
|
3032
3345
|
const committed = error instanceof PartialSendConflictError;
|
|
3033
|
-
await this.
|
|
3034
|
-
this.setStatus(id, "settling");
|
|
3346
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
3035
3347
|
} else {
|
|
3036
3348
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
3037
3349
|
this.setStatus(id, "paid");
|
|
3038
3350
|
}
|
|
3039
3351
|
} else {
|
|
3040
|
-
await this.
|
|
3041
|
-
this.setStatus(id, "pending");
|
|
3352
|
+
await this.revertPayable(id);
|
|
3042
3353
|
}
|
|
3043
3354
|
throw error;
|
|
3044
3355
|
}
|
|
3045
|
-
this.
|
|
3046
|
-
await this.respondPaid(id, result.id);
|
|
3047
|
-
await this.clearLink(id);
|
|
3356
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
3048
3357
|
return result;
|
|
3049
3358
|
}
|
|
3359
|
+
/**
|
|
3360
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
3361
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
3362
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
3363
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
3364
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
3365
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
3366
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
3367
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
3368
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
3369
|
+
*/
|
|
3370
|
+
async settle(requestId, transferId, opts) {
|
|
3371
|
+
let journalDown = false;
|
|
3372
|
+
try {
|
|
3373
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
3374
|
+
} catch (err) {
|
|
3375
|
+
if (!opts.respond) throw err;
|
|
3376
|
+
journalDown = true;
|
|
3377
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
3378
|
+
}
|
|
3379
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
3380
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
3384
|
+
this.setStatus(requestId, "paid");
|
|
3385
|
+
}
|
|
3050
3386
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
3051
3387
|
async respondPaid(id, transferId) {
|
|
3052
3388
|
try {
|
|
@@ -3082,32 +3418,32 @@ var Requests = class {
|
|
|
3082
3418
|
let aborted = null;
|
|
3083
3419
|
for (const [requestId, link] of [...this.journal]) {
|
|
3084
3420
|
if (link.committed || resumed.has(link.transferId)) {
|
|
3085
|
-
await this.resolvePaid(requestId, link
|
|
3421
|
+
await this.resolvePaid(requestId, link);
|
|
3086
3422
|
} else if (conflicted.has(link.transferId)) {
|
|
3087
3423
|
await this.revertPayable(requestId);
|
|
3088
3424
|
} else if (open.has(link.transferId)) {
|
|
3089
3425
|
} else {
|
|
3090
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
3426
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
3091
3427
|
}
|
|
3092
3428
|
}
|
|
3093
3429
|
}
|
|
3094
3430
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
3095
3431
|
// a probe failure defers the link to the next session.
|
|
3096
|
-
async reconcileUnaccounted(requestId,
|
|
3432
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
3097
3433
|
try {
|
|
3098
3434
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
3099
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
3100
|
-
else await this.resolvePaid(requestId,
|
|
3435
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
3436
|
+
else await this.resolvePaid(requestId, link);
|
|
3101
3437
|
} catch (err) {
|
|
3102
3438
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
3103
3439
|
}
|
|
3104
3440
|
return aborted;
|
|
3105
3441
|
}
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
this.setStatus(requestId, "paid");
|
|
3442
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
3443
|
+
resolvePaid(requestId, link) {
|
|
3444
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
3110
3445
|
}
|
|
3446
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
3111
3447
|
async revertPayable(requestId) {
|
|
3112
3448
|
await this.clearLink(requestId);
|
|
3113
3449
|
this.setStatus(requestId, "pending");
|
|
@@ -3431,11 +3767,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3431
3767
|
client: deps.client,
|
|
3432
3768
|
fieldKey: deps.fieldKey,
|
|
3433
3769
|
registry: deps.registry,
|
|
3434
|
-
emit: (event) => deps.emit(event,
|
|
3770
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
3435
3771
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
3436
3772
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
3437
3773
|
});
|
|
3438
3774
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
3775
|
+
const machineStores = createMachineStores(deps.kv);
|
|
3439
3776
|
const heldStates = /* @__PURE__ */ new Map();
|
|
3440
3777
|
return {
|
|
3441
3778
|
ownPubkeyBytes,
|
|
@@ -3443,12 +3780,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3443
3780
|
ledger,
|
|
3444
3781
|
queue,
|
|
3445
3782
|
historyStore,
|
|
3446
|
-
machineStores
|
|
3783
|
+
machineStores,
|
|
3447
3784
|
machineDeps,
|
|
3448
3785
|
machine: new TransferMachine(machineDeps),
|
|
3449
3786
|
heldStates,
|
|
3450
3787
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
3451
|
-
requests: buildRequests(deps, hooks)
|
|
3788
|
+
requests: buildRequests(deps, hooks),
|
|
3789
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
3790
|
+
};
|
|
3791
|
+
}
|
|
3792
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
3793
|
+
return {
|
|
3794
|
+
stores: machineStores,
|
|
3795
|
+
kv: deps.kv,
|
|
3796
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
3797
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
3798
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
3799
|
+
fullRePull: () => view.onEpochReset(),
|
|
3800
|
+
attention: (transferId, code, detail) => {
|
|
3801
|
+
deps.emit(
|
|
3802
|
+
"transfer:attention",
|
|
3803
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3452
3806
|
};
|
|
3453
3807
|
}
|
|
3454
3808
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -3470,11 +3824,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
3470
3824
|
ownPubkey: hexToBytes(deps.ownPubkey),
|
|
3471
3825
|
emit: deps.emit,
|
|
3472
3826
|
now: deps.now ?? Date.now,
|
|
3473
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
3827
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
3474
3828
|
await historyStore.recordSent({
|
|
3475
3829
|
transferId,
|
|
3476
3830
|
coinId: payload.coinId,
|
|
3477
|
-
|
|
3831
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
3832
|
+
// recipient blobs), never payload.amount — the plan.
|
|
3833
|
+
amount: committedAmount,
|
|
3478
3834
|
recipientPubkey: payload.recipient,
|
|
3479
3835
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
3480
3836
|
});
|
|
@@ -3501,7 +3857,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
3501
3857
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3502
3858
|
);
|
|
3503
3859
|
},
|
|
3504
|
-
syncEpoch: deps.syncEpoch
|
|
3860
|
+
syncEpoch: deps.syncEpoch,
|
|
3505
3861
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3506
3862
|
});
|
|
3507
3863
|
}
|
|
@@ -3554,8 +3910,24 @@ var PaymentsFacade = class {
|
|
|
3554
3910
|
this.receiveLoop = parts.receiveLoop;
|
|
3555
3911
|
this.heldStates = parts.heldStates;
|
|
3556
3912
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3913
|
+
this.restoreDeps = parts.restoreDeps;
|
|
3557
3914
|
this.requests = parts.requests;
|
|
3558
3915
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3916
|
+
this.heartbeat = new ConvergenceHeartbeat({
|
|
3917
|
+
now: () => this.nowMs(),
|
|
3918
|
+
onTick: () => this.trackTail(this.runConvergencePass())
|
|
3919
|
+
});
|
|
3920
|
+
this.converger = new Converger({
|
|
3921
|
+
machineDeps: this.machineDeps,
|
|
3922
|
+
stores: this.machineStores,
|
|
3923
|
+
delivery: deps.deliveryPort,
|
|
3924
|
+
reconcile: (report) => this.requests.reconcile(report),
|
|
3925
|
+
refreshView: () => this.view.delta(),
|
|
3926
|
+
replayMints: () => this.replayMints(),
|
|
3927
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id),
|
|
3928
|
+
emit: deps.emit,
|
|
3929
|
+
now: () => this.nowMs()
|
|
3930
|
+
});
|
|
3559
3931
|
}
|
|
3560
3932
|
view;
|
|
3561
3933
|
ledger;
|
|
@@ -3567,17 +3939,28 @@ var PaymentsFacade = class {
|
|
|
3567
3939
|
receiveLoop;
|
|
3568
3940
|
heldStates;
|
|
3569
3941
|
ownPubkeyBytes;
|
|
3942
|
+
restoreDeps;
|
|
3570
3943
|
requests;
|
|
3571
3944
|
currentEngine = null;
|
|
3572
3945
|
pendingOps = /* @__PURE__ */ new Set();
|
|
3573
3946
|
unsubscribers = [];
|
|
3574
3947
|
started = false;
|
|
3948
|
+
// §7 heartbeat: in-memory timing cell + pass runner (see convergence.ts).
|
|
3949
|
+
heartbeat;
|
|
3950
|
+
converger;
|
|
3951
|
+
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
3952
|
+
resumeFlight = new SingleFlight();
|
|
3953
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
3954
|
+
passChain = new SerialChain();
|
|
3955
|
+
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
3956
|
+
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
3575
3957
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
3576
3958
|
async start() {
|
|
3577
3959
|
if (this.started) return;
|
|
3578
3960
|
this.started = true;
|
|
3579
|
-
|
|
3961
|
+
this.heartbeat.start();
|
|
3580
3962
|
this.unsubscribers.push(
|
|
3963
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
3581
3964
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
3582
3965
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
3583
3966
|
}),
|
|
@@ -3588,20 +3971,12 @@ var PaymentsFacade = class {
|
|
|
3588
3971
|
this.trackTail(this.requests.drainIncoming());
|
|
3589
3972
|
})
|
|
3590
3973
|
);
|
|
3591
|
-
this.
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
this.deps.emit(
|
|
3598
|
-
"transfer:attention",
|
|
3599
|
-
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3600
|
-
);
|
|
3601
|
-
}
|
|
3602
|
-
})
|
|
3603
|
-
);
|
|
3604
|
-
this.trackTail(this.replayMints());
|
|
3974
|
+
const statusUnsub = this.deps.session.subscribeStatus?.((status) => {
|
|
3975
|
+
if (status === "connected") this.heartbeat.recovered();
|
|
3976
|
+
});
|
|
3977
|
+
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
3978
|
+
await this.deps.session.start();
|
|
3979
|
+
this.trackTail(this.runConvergencePass());
|
|
3605
3980
|
this.trackTail(this.seedHeldStates());
|
|
3606
3981
|
await this.view.fullPull().catch(() => void 0);
|
|
3607
3982
|
this.receiveLoop.start(this.deps.receivePollMs);
|
|
@@ -3609,6 +3984,7 @@ var PaymentsFacade = class {
|
|
|
3609
3984
|
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
3610
3985
|
async stop() {
|
|
3611
3986
|
this.started = false;
|
|
3987
|
+
this.heartbeat.stop();
|
|
3612
3988
|
this.receiveLoop.stop();
|
|
3613
3989
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
3614
3990
|
this.unsubscribers = [];
|
|
@@ -3634,9 +4010,13 @@ var PaymentsFacade = class {
|
|
|
3634
4010
|
history(page) {
|
|
3635
4011
|
return this.historyStore.page(page ?? {});
|
|
3636
4012
|
}
|
|
4013
|
+
/** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
|
|
4014
|
+
pendingTransfers() {
|
|
4015
|
+
return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
|
|
4016
|
+
}
|
|
3637
4017
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
3638
4018
|
send(request) {
|
|
3639
|
-
return this.track(this.
|
|
4019
|
+
return this.track(this.sendOutcome(request));
|
|
3640
4020
|
}
|
|
3641
4021
|
async receive() {
|
|
3642
4022
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -3646,9 +4026,32 @@ var PaymentsFacade = class {
|
|
|
3646
4026
|
mint(coinId, amount) {
|
|
3647
4027
|
return this.track(this.mintInner(coinId, amount));
|
|
3648
4028
|
}
|
|
4029
|
+
// §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
|
|
4030
|
+
async resumeNow() {
|
|
4031
|
+
if (!this.started) return;
|
|
4032
|
+
await this.track(this.runConvergencePass());
|
|
4033
|
+
}
|
|
4034
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
4035
|
+
async handleEpochChange(_newEpoch) {
|
|
4036
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
4037
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
4038
|
+
}
|
|
4039
|
+
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
4040
|
+
runConvergencePass() {
|
|
4041
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
4042
|
+
}
|
|
4043
|
+
async convergeBody() {
|
|
4044
|
+
const outcome = await this.converger.convergeOnce();
|
|
4045
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
4046
|
+
this.heartbeat.settle(outcome, pending);
|
|
4047
|
+
}
|
|
4048
|
+
nowMs() {
|
|
4049
|
+
return (this.deps.now ?? Date.now)();
|
|
4050
|
+
}
|
|
3649
4051
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
3650
|
-
|
|
3651
|
-
|
|
4052
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
4053
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
4054
|
+
async sendOutcome(request) {
|
|
3652
4055
|
const run = {
|
|
3653
4056
|
amount: request.amount,
|
|
3654
4057
|
delivered: [],
|
|
@@ -3656,12 +4059,36 @@ var PaymentsFacade = class {
|
|
|
3656
4059
|
tokenTransfers: [],
|
|
3657
4060
|
settledShortfalls: [],
|
|
3658
4061
|
firstPartialId: void 0,
|
|
3659
|
-
pendingAny: false
|
|
4062
|
+
pendingAny: false,
|
|
4063
|
+
lastTransferId: ""
|
|
3660
4064
|
};
|
|
4065
|
+
try {
|
|
4066
|
+
return await this.sendWithPolicy(request, run);
|
|
4067
|
+
} catch (err) {
|
|
4068
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
4069
|
+
throw err;
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
4073
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
4074
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
4075
|
+
emitCleanFailure(err, transferId) {
|
|
4076
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
4077
|
+
const failed = {
|
|
4078
|
+
id: transferId,
|
|
4079
|
+
status: "failed",
|
|
4080
|
+
tokens: [],
|
|
4081
|
+
tokenTransfers: [],
|
|
4082
|
+
error: messageOf(err)
|
|
4083
|
+
};
|
|
4084
|
+
this.deps.emit("transfer:updated", failed);
|
|
4085
|
+
}
|
|
4086
|
+
async sendWithPolicy(request, run) {
|
|
4087
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
3661
4088
|
for (let attempt = 0; ; attempt++) {
|
|
3662
4089
|
let ctx;
|
|
3663
4090
|
try {
|
|
3664
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
4091
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
3665
4092
|
} catch (err) {
|
|
3666
4093
|
throw this.partialize(err, run);
|
|
3667
4094
|
}
|
|
@@ -3695,21 +4122,24 @@ var PaymentsFacade = class {
|
|
|
3695
4122
|
return recipient;
|
|
3696
4123
|
}
|
|
3697
4124
|
async runAttempt(ctx) {
|
|
4125
|
+
this.activeMoneyOps.add(ctx.transferId);
|
|
3698
4126
|
try {
|
|
3699
4127
|
const { deliveryPending } = await this.machine.run(ctx.plan);
|
|
3700
4128
|
this.settleSuccess(ctx, ctx.sourceIds);
|
|
3701
4129
|
return { kind: "success", deliveryPending };
|
|
3702
4130
|
} catch (err) {
|
|
3703
|
-
return this.disposeFailedAttempt(ctx, err);
|
|
4131
|
+
return await this.disposeFailedAttempt(ctx, err);
|
|
4132
|
+
} finally {
|
|
4133
|
+
this.activeMoneyOps.delete(ctx.transferId);
|
|
3704
4134
|
}
|
|
3705
4135
|
}
|
|
3706
|
-
/**
|
|
3707
|
-
*
|
|
3708
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
3709
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
3710
|
-
*/
|
|
4136
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
4137
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
3711
4138
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
3712
|
-
|
|
4139
|
+
if (disposition.deliveryPending) {
|
|
4140
|
+
run.pendingAny = true;
|
|
4141
|
+
this.heartbeat.arm();
|
|
4142
|
+
}
|
|
3713
4143
|
const partial = disposition.entry;
|
|
3714
4144
|
run.delivered.push(...partial.committedTokenIds);
|
|
3715
4145
|
run.firstPartialId ??= partial.transferId;
|
|
@@ -3730,11 +4160,8 @@ var PaymentsFacade = class {
|
|
|
3730
4160
|
run.amount = partial.remainingAmount;
|
|
3731
4161
|
return null;
|
|
3732
4162
|
}
|
|
3733
|
-
/**
|
|
3734
|
-
*
|
|
3735
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
3736
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
3737
|
-
*/
|
|
4163
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
4164
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
3738
4165
|
async disposeFailedAttempt(ctx, err) {
|
|
3739
4166
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
3740
4167
|
this.settleKeepOpen(ctx);
|
|
@@ -3783,8 +4210,9 @@ var PaymentsFacade = class {
|
|
|
3783
4210
|
}
|
|
3784
4211
|
return { kind: "rethrow", error: err };
|
|
3785
4212
|
}
|
|
3786
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
4213
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
3787
4214
|
const transferId = this.newId();
|
|
4215
|
+
run.lastTransferId = transferId;
|
|
3788
4216
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
3789
4217
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
3790
4218
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -3867,6 +4295,7 @@ var PaymentsFacade = class {
|
|
|
3867
4295
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
3868
4296
|
settleKeepOpen(ctx) {
|
|
3869
4297
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
4298
|
+
this.heartbeat.arm();
|
|
3870
4299
|
}
|
|
3871
4300
|
settlePartial(ctx, partial) {
|
|
3872
4301
|
this.ledger.cancel(ctx.transferId);
|
|
@@ -3903,6 +4332,7 @@ var PaymentsFacade = class {
|
|
|
3903
4332
|
for (const id of run.settledShortfalls.splice(0)) {
|
|
3904
4333
|
await this.machineStores.shortfalls.removeByKey(id).catch(() => void 0);
|
|
3905
4334
|
}
|
|
4335
|
+
if (run.pendingAny) this.heartbeat.arm();
|
|
3906
4336
|
const result = {
|
|
3907
4337
|
id: transferId,
|
|
3908
4338
|
status: run.pendingAny ? "confirmed" : "delivered",
|
|
@@ -3914,11 +4344,7 @@ var PaymentsFacade = class {
|
|
|
3914
4344
|
this.deps.emit("transfer:updated", result);
|
|
3915
4345
|
return result;
|
|
3916
4346
|
}
|
|
3917
|
-
/**
|
|
3918
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
3919
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
3920
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
3921
|
-
*/
|
|
4347
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
3922
4348
|
partialize(err, run) {
|
|
3923
4349
|
if (run.delivered.length === 0) return err;
|
|
3924
4350
|
return new PartialSendConflictError(
|
|
@@ -3947,21 +4373,29 @@ var PaymentsFacade = class {
|
|
|
3947
4373
|
if (!/^(?:[0-9a-f]{2})+$/.test(coinId) || amount <= 0n) {
|
|
3948
4374
|
return { success: false, error: "coinId must be even-length lowercase hex and amount positive" };
|
|
3949
4375
|
}
|
|
3950
|
-
const engine = this.engine();
|
|
3951
4376
|
const mintId = this.newId();
|
|
3952
|
-
|
|
4377
|
+
this.activeMoneyOps.add(mintId);
|
|
4378
|
+
try {
|
|
4379
|
+
return await this.mintUnderJournal(mintId, coinId, amount);
|
|
4380
|
+
} finally {
|
|
4381
|
+
this.activeMoneyOps.delete(mintId);
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
async mintUnderJournal(mintId, coinId, amount) {
|
|
4385
|
+
const engine = this.engine();
|
|
3953
4386
|
const entry = {
|
|
3954
4387
|
mintId,
|
|
3955
4388
|
coinId,
|
|
3956
4389
|
amount: amount.toString(),
|
|
3957
4390
|
tokenId: "",
|
|
3958
|
-
createdAt:
|
|
4391
|
+
createdAt: this.nowMs()
|
|
3959
4392
|
};
|
|
3960
4393
|
await this.machineStores.mintJournal.upsert(entry);
|
|
3961
4394
|
let token;
|
|
3962
4395
|
try {
|
|
3963
|
-
token = await engine.mint(
|
|
4396
|
+
token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
|
|
3964
4397
|
} catch (err) {
|
|
4398
|
+
this.heartbeat.arm();
|
|
3965
4399
|
return { success: false, error: messageOf(err) };
|
|
3966
4400
|
}
|
|
3967
4401
|
if (token.blob.tokenId !== entry.tokenId) {
|
|
@@ -3970,6 +4404,7 @@ var PaymentsFacade = class {
|
|
|
3970
4404
|
try {
|
|
3971
4405
|
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
3972
4406
|
} catch (err) {
|
|
4407
|
+
this.heartbeat.arm();
|
|
3973
4408
|
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
3974
4409
|
}
|
|
3975
4410
|
return { success: true, tokenId: token.blob.tokenId };
|
|
@@ -3992,18 +4427,22 @@ var PaymentsFacade = class {
|
|
|
3992
4427
|
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
3993
4428
|
this.trackTail(this.view.delta());
|
|
3994
4429
|
}
|
|
4430
|
+
/** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
|
|
3995
4431
|
async replayMints() {
|
|
4432
|
+
let resolved = 0;
|
|
3996
4433
|
for (const entry of await this.machineStores.mintJournal.list()) {
|
|
4434
|
+
if (this.activeMoneyOps.has(entry.mintId)) continue;
|
|
3997
4435
|
try {
|
|
3998
|
-
await this.replayMint(entry);
|
|
4436
|
+
if (await this.replayMint(entry)) resolved += 1;
|
|
3999
4437
|
} catch {
|
|
4000
4438
|
}
|
|
4001
4439
|
}
|
|
4440
|
+
return resolved;
|
|
4002
4441
|
}
|
|
4003
4442
|
async replayMint(entry) {
|
|
4004
4443
|
if (entry.tokenId !== "" && await this.tokenInServerInventory(entry.tokenId)) {
|
|
4005
4444
|
await this.machineStores.mintJournal.removeByKey(entry.mintId);
|
|
4006
|
-
return;
|
|
4445
|
+
return true;
|
|
4007
4446
|
}
|
|
4008
4447
|
const engine = this.engine();
|
|
4009
4448
|
if (!supportsDeterministicMint(engine)) {
|
|
@@ -4011,7 +4450,7 @@ var PaymentsFacade = class {
|
|
|
4011
4450
|
transferId: entry.mintId,
|
|
4012
4451
|
code: ATTENTION_MINT_UNRESOLVED
|
|
4013
4452
|
});
|
|
4014
|
-
return;
|
|
4453
|
+
return false;
|
|
4015
4454
|
}
|
|
4016
4455
|
const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
|
|
4017
4456
|
transferId: entry.mintId
|
|
@@ -4020,6 +4459,7 @@ var PaymentsFacade = class {
|
|
|
4020
4459
|
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
4021
4460
|
}
|
|
4022
4461
|
await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
|
|
4462
|
+
return true;
|
|
4023
4463
|
}
|
|
4024
4464
|
mintParams(coinId, amount) {
|
|
4025
4465
|
return {
|
|
@@ -4045,17 +4485,6 @@ var PaymentsFacade = class {
|
|
|
4045
4485
|
newId() {
|
|
4046
4486
|
return (this.deps.newId ?? randomUUID)();
|
|
4047
4487
|
}
|
|
4048
|
-
async runResume() {
|
|
4049
|
-
const report = await resumeAll(this.machineDeps);
|
|
4050
|
-
await this.requests.reconcile({
|
|
4051
|
-
resumed: report.resumed,
|
|
4052
|
-
conflicted: report.conflicted,
|
|
4053
|
-
open: report.failed
|
|
4054
|
-
});
|
|
4055
|
-
if (report.resumed.length > 0 || report.conflicted.length > 0) {
|
|
4056
|
-
await this.view.delta().catch(() => void 0);
|
|
4057
|
-
}
|
|
4058
|
-
}
|
|
4059
4488
|
async seedHeldStates() {
|
|
4060
4489
|
let page = await this.deps.storagePort.listInventory();
|
|
4061
4490
|
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
@@ -4067,22 +4496,14 @@ var PaymentsFacade = class {
|
|
|
4067
4496
|
}
|
|
4068
4497
|
}
|
|
4069
4498
|
toUiToken(tokenId, coinId, amount) {
|
|
4070
|
-
const
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
decimals: registry.getDecimals(coinId),
|
|
4079
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
4080
|
-
amount: amount.toString(),
|
|
4081
|
-
status: "transferring",
|
|
4082
|
-
createdAt: now,
|
|
4083
|
-
updatedAt: now,
|
|
4084
|
-
lazy: true
|
|
4085
|
-
};
|
|
4499
|
+
const now = this.nowMs();
|
|
4500
|
+
return toToken(
|
|
4501
|
+
tokenId,
|
|
4502
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
4503
|
+
{ coinId, amount: amount.toString() },
|
|
4504
|
+
this.deps.registry,
|
|
4505
|
+
{ transferring: true, suspectedSpent: false }
|
|
4506
|
+
);
|
|
4086
4507
|
}
|
|
4087
4508
|
track(op) {
|
|
4088
4509
|
const settled = op.then(
|
|
@@ -4097,29 +4518,11 @@ var PaymentsFacade = class {
|
|
|
4097
4518
|
this.track(op).catch(() => void 0);
|
|
4098
4519
|
}
|
|
4099
4520
|
};
|
|
4100
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
4101
|
-
return {
|
|
4102
|
-
v: 2,
|
|
4103
|
-
recipient: recipientPubkey,
|
|
4104
|
-
coinId: request.coinId,
|
|
4105
|
-
amount: request.amount,
|
|
4106
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
4107
|
-
direct: [...spend.plan.direct],
|
|
4108
|
-
...spend.plan.split !== void 0 ? {
|
|
4109
|
-
split: {
|
|
4110
|
-
tokenId: spend.plan.split.tokenId,
|
|
4111
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
4112
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
4113
|
-
}
|
|
4114
|
-
} : {},
|
|
4115
|
-
spentStates
|
|
4116
|
-
};
|
|
4117
|
-
}
|
|
4118
|
-
function messageOf(err) {
|
|
4119
|
-
return err instanceof Error ? err.message : String(err);
|
|
4120
|
-
}
|
|
4121
4521
|
export {
|
|
4122
4522
|
ATTENTION_MINT_UNRESOLVED,
|
|
4523
|
+
ATTENTION_RESEED_REJECTED,
|
|
4524
|
+
HEARTBEAT_CAP_MS,
|
|
4525
|
+
HEARTBEAT_SEED_MS,
|
|
4123
4526
|
MAX_RESELECT,
|
|
4124
4527
|
PaymentsFacade,
|
|
4125
4528
|
STORE_KEYS,
|