@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
|
@@ -31,6 +31,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var payments_v2_exports = {};
|
|
32
32
|
__export(payments_v2_exports, {
|
|
33
33
|
ATTENTION_MINT_UNRESOLVED: () => ATTENTION_MINT_UNRESOLVED,
|
|
34
|
+
ATTENTION_RESEED_REJECTED: () => ATTENTION_RESEED_REJECTED,
|
|
35
|
+
HEARTBEAT_CAP_MS: () => HEARTBEAT_CAP_MS,
|
|
36
|
+
HEARTBEAT_SEED_MS: () => HEARTBEAT_SEED_MS,
|
|
34
37
|
MAX_RESELECT: () => MAX_RESELECT,
|
|
35
38
|
PaymentsFacade: () => PaymentsFacade,
|
|
36
39
|
STORE_KEYS: () => STORE_KEYS,
|
|
@@ -64,7 +67,10 @@ var STORE_KEYS = {
|
|
|
64
67
|
shortfalls: "shortfalls",
|
|
65
68
|
settlingLinks: "settling",
|
|
66
69
|
streamCursor: (s) => `cursor:${s}`,
|
|
67
|
-
epochLatch: "epoch-latch"
|
|
70
|
+
epochLatch: "epoch-latch",
|
|
71
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
72
|
+
suspectedSpent: "suspected-spent",
|
|
73
|
+
knownSpends: "known-spends"
|
|
68
74
|
};
|
|
69
75
|
|
|
70
76
|
// node_modules/@noble/hashes/utils.js
|
|
@@ -590,6 +596,10 @@ function isValidationReject(err) {
|
|
|
590
596
|
if (e === null) return false;
|
|
591
597
|
return e.status === 422 || e.statusCode === 422 || e.code === "VALIDATION" || e.code === "VALIDATION_FAILED";
|
|
592
598
|
}
|
|
599
|
+
function retryAfterMsOf(err) {
|
|
600
|
+
const retryAfter = err !== null && typeof err === "object" ? err.retryAfter : void 0;
|
|
601
|
+
return typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : null;
|
|
602
|
+
}
|
|
593
603
|
var chainRegistry = /* @__PURE__ */ new WeakMap();
|
|
594
604
|
var replayRegistry = /* @__PURE__ */ new WeakMap();
|
|
595
605
|
function registryFor(registry, kv, storeKey, make) {
|
|
@@ -852,7 +862,7 @@ var TransferMachine = class {
|
|
|
852
862
|
}
|
|
853
863
|
if (cls === null) {
|
|
854
864
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
855
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
865
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
856
866
|
return { committed, deliveryPending };
|
|
857
867
|
}
|
|
858
868
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -878,10 +888,10 @@ var TransferMachine = class {
|
|
|
878
888
|
}
|
|
879
889
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
880
890
|
if (conflicts.length > 0) {
|
|
881
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
891
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
882
892
|
}
|
|
883
893
|
await this.completeIntent(r.transferId, r.payload);
|
|
884
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
894
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
885
895
|
return { deliveryPending, partial: null };
|
|
886
896
|
}
|
|
887
897
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -1063,7 +1073,7 @@ var TransferMachine = class {
|
|
|
1063
1073
|
}
|
|
1064
1074
|
return added;
|
|
1065
1075
|
}
|
|
1066
|
-
async settlePartial(r, committed, conflicts) {
|
|
1076
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
1067
1077
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
1068
1078
|
const entry = {
|
|
1069
1079
|
transferId: r.transferId,
|
|
@@ -1075,26 +1085,27 @@ var TransferMachine = class {
|
|
|
1075
1085
|
};
|
|
1076
1086
|
await this.stores.shortfalls.upsert(entry);
|
|
1077
1087
|
await this.completeIntent(r.transferId, r.payload);
|
|
1078
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
1088
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
1079
1089
|
return entry;
|
|
1080
1090
|
}
|
|
1081
|
-
closeTail(transferId, payload) {
|
|
1091
|
+
closeTail(engine, transferId, payload, committed) {
|
|
1082
1092
|
void (async () => {
|
|
1083
1093
|
try {
|
|
1084
1094
|
await this.completeIntent(transferId, payload);
|
|
1085
1095
|
} catch {
|
|
1086
1096
|
return;
|
|
1087
1097
|
}
|
|
1088
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
1098
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
1089
1099
|
})();
|
|
1090
1100
|
}
|
|
1091
1101
|
async completeIntent(transferId, payload) {
|
|
1092
1102
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
1093
1103
|
await this.deps.intents.complete(transferId, signature);
|
|
1094
1104
|
}
|
|
1095
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
1105
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
1096
1106
|
try {
|
|
1097
|
-
await this.
|
|
1107
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
1108
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
1098
1109
|
} catch {
|
|
1099
1110
|
}
|
|
1100
1111
|
try {
|
|
@@ -1102,6 +1113,26 @@ var TransferMachine = class {
|
|
|
1102
1113
|
} catch {
|
|
1103
1114
|
}
|
|
1104
1115
|
}
|
|
1116
|
+
/**
|
|
1117
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
1118
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
1119
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
1120
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
1121
|
+
*/
|
|
1122
|
+
async settledAmount(engine, coinId, committed) {
|
|
1123
|
+
let total = 0n;
|
|
1124
|
+
for (const outcome of committed) {
|
|
1125
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
1126
|
+
const token = await engine.decodeToken({
|
|
1127
|
+
v: TOKEN_BLOB_VERSION,
|
|
1128
|
+
network: 0,
|
|
1129
|
+
tokenId: "",
|
|
1130
|
+
token: outcome.recipientBlob
|
|
1131
|
+
});
|
|
1132
|
+
total += engine.balanceOf(token, coinId);
|
|
1133
|
+
}
|
|
1134
|
+
return total.toString();
|
|
1135
|
+
}
|
|
1105
1136
|
async disposeOnFailure(transferId, committed, cls) {
|
|
1106
1137
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
1107
1138
|
try {
|
|
@@ -1126,7 +1157,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
1126
1157
|
}
|
|
1127
1158
|
|
|
1128
1159
|
// modules/payments-v2/machine/resume.ts
|
|
1129
|
-
async function resumeAll(deps) {
|
|
1160
|
+
async function resumeAll(deps, options = {}) {
|
|
1130
1161
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
1131
1162
|
const stores = createMachineStores(deps.kv);
|
|
1132
1163
|
const open = await deps.intents.listOpen();
|
|
@@ -1136,7 +1167,12 @@ async function resumeAll(deps) {
|
|
|
1136
1167
|
} catch {
|
|
1137
1168
|
return report;
|
|
1138
1169
|
}
|
|
1139
|
-
await
|
|
1170
|
+
await dropStaleBackstops(
|
|
1171
|
+
stores,
|
|
1172
|
+
locals,
|
|
1173
|
+
new Set(open.map((i) => i.transferId)),
|
|
1174
|
+
options.isLocallyActive ?? (() => false)
|
|
1175
|
+
);
|
|
1140
1176
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
1141
1177
|
const ctx = {
|
|
1142
1178
|
deps,
|
|
@@ -1149,11 +1185,16 @@ async function resumeAll(deps) {
|
|
|
1149
1185
|
}
|
|
1150
1186
|
return report;
|
|
1151
1187
|
}
|
|
1152
|
-
async function
|
|
1188
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
1153
1189
|
for (const entry of locals.values()) {
|
|
1154
|
-
if (
|
|
1190
|
+
if (openIds.has(entry.transferId)) continue;
|
|
1191
|
+
if (entry.disposition !== "open") {
|
|
1155
1192
|
await stores.backstop.removeByKey(entry.transferId);
|
|
1193
|
+
continue;
|
|
1156
1194
|
}
|
|
1195
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
1196
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
1197
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
1157
1198
|
}
|
|
1158
1199
|
}
|
|
1159
1200
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -1266,6 +1307,349 @@ function validatePayload(raw) {
|
|
|
1266
1307
|
return p;
|
|
1267
1308
|
}
|
|
1268
1309
|
|
|
1310
|
+
// modules/payments-v2/convergence.ts
|
|
1311
|
+
var HEARTBEAT_SEED_MS = 5e3;
|
|
1312
|
+
var HEARTBEAT_CAP_MS = 12e4;
|
|
1313
|
+
var ConvergenceHeartbeat = class {
|
|
1314
|
+
constructor(deps) {
|
|
1315
|
+
this.deps = deps;
|
|
1316
|
+
}
|
|
1317
|
+
timer = null;
|
|
1318
|
+
delayMs = HEARTBEAT_SEED_MS;
|
|
1319
|
+
notBefore = 0;
|
|
1320
|
+
running = false;
|
|
1321
|
+
start() {
|
|
1322
|
+
this.running = true;
|
|
1323
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
1324
|
+
this.notBefore = 0;
|
|
1325
|
+
}
|
|
1326
|
+
/** §7 quiescence unchanged: a mid-backoff stop() cancels cleanly. */
|
|
1327
|
+
stop() {
|
|
1328
|
+
this.running = false;
|
|
1329
|
+
this.clearTimer();
|
|
1330
|
+
}
|
|
1331
|
+
/** Reset to seed on progress, ×2 to the cap otherwise; idle stops rescheduling. */
|
|
1332
|
+
settle(outcome, pending) {
|
|
1333
|
+
if (outcome.retryAfterMs !== null) {
|
|
1334
|
+
this.notBefore = Math.max(this.notBefore, this.deps.now() + outcome.retryAfterMs);
|
|
1335
|
+
}
|
|
1336
|
+
if (!pending) {
|
|
1337
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const delay = outcome.progress ? HEARTBEAT_SEED_MS : this.delayMs;
|
|
1341
|
+
this.delayMs = Math.min(delay * 2, HEARTBEAT_CAP_MS);
|
|
1342
|
+
this.schedule(delay);
|
|
1343
|
+
}
|
|
1344
|
+
/** New pending work (keep-open park / journaled delivery / mint hold): prompt retry. */
|
|
1345
|
+
arm() {
|
|
1346
|
+
if (!this.running) return;
|
|
1347
|
+
this.delayMs = Math.min(HEARTBEAT_SEED_MS * 2, HEARTBEAT_CAP_MS);
|
|
1348
|
+
this.schedule(HEARTBEAT_SEED_MS);
|
|
1349
|
+
}
|
|
1350
|
+
/** connection:status recovery: the backoff resets; a pending tick re-fires at the seed. */
|
|
1351
|
+
recovered() {
|
|
1352
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
1353
|
+
if (this.timer !== null) this.arm();
|
|
1354
|
+
}
|
|
1355
|
+
schedule(delayMs) {
|
|
1356
|
+
this.clearTimer();
|
|
1357
|
+
if (!this.running) return;
|
|
1358
|
+
const floored = Math.max(delayMs, this.notBefore - this.deps.now());
|
|
1359
|
+
this.timer = setTimeout(() => {
|
|
1360
|
+
this.timer = null;
|
|
1361
|
+
this.deps.onTick();
|
|
1362
|
+
}, floored);
|
|
1363
|
+
}
|
|
1364
|
+
clearTimer() {
|
|
1365
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
1366
|
+
this.timer = null;
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
var Converger = class {
|
|
1370
|
+
constructor(deps) {
|
|
1371
|
+
this.deps = deps;
|
|
1372
|
+
}
|
|
1373
|
+
/** Resume open intents, then drain the delivery + mint journals (all idempotent). */
|
|
1374
|
+
async convergeOnce() {
|
|
1375
|
+
let progress = false;
|
|
1376
|
+
let retryAfterMs = null;
|
|
1377
|
+
try {
|
|
1378
|
+
progress = await this.resumePass();
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
retryAfterMs = retryAfterMsOf(err);
|
|
1381
|
+
}
|
|
1382
|
+
try {
|
|
1383
|
+
const stats = await this.deps.stores.deliveryJournal.replay(this.replayDeps());
|
|
1384
|
+
progress = progress || stats.delivered > 0;
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
1387
|
+
}
|
|
1388
|
+
try {
|
|
1389
|
+
progress = await this.deps.replayMints() > 0 || progress;
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
1392
|
+
}
|
|
1393
|
+
return { progress, retryAfterMs };
|
|
1394
|
+
}
|
|
1395
|
+
/** §4 heartbeat predicate — read fresh from the §6 stores, never cached. */
|
|
1396
|
+
async pendingWork() {
|
|
1397
|
+
const [backstop, journal, mints] = await Promise.all([
|
|
1398
|
+
this.deps.stores.backstop.list(),
|
|
1399
|
+
this.deps.stores.deliveryJournal.list(),
|
|
1400
|
+
this.deps.stores.mintJournal.list()
|
|
1401
|
+
]);
|
|
1402
|
+
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
1403
|
+
}
|
|
1404
|
+
async resumePass() {
|
|
1405
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
1406
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
1407
|
+
});
|
|
1408
|
+
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
1409
|
+
await this.deps.reconcile({
|
|
1410
|
+
resumed: report.resumed,
|
|
1411
|
+
conflicted: report.conflicted,
|
|
1412
|
+
open: report.failed
|
|
1413
|
+
});
|
|
1414
|
+
if (progress) await this.deps.refreshView().catch(() => void 0);
|
|
1415
|
+
return progress;
|
|
1416
|
+
}
|
|
1417
|
+
filteredMachineDeps() {
|
|
1418
|
+
const intents = this.deps.machineDeps.intents;
|
|
1419
|
+
return {
|
|
1420
|
+
...this.deps.machineDeps,
|
|
1421
|
+
intents: {
|
|
1422
|
+
...intents,
|
|
1423
|
+
listOpen: async () => (await intents.listOpen()).filter((intent) => !this.deps.isActiveOp(intent.transferId))
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
replayDeps() {
|
|
1428
|
+
return {
|
|
1429
|
+
delivery: this.deps.delivery,
|
|
1430
|
+
now: () => this.deps.now(),
|
|
1431
|
+
attention: (transferId, code, detail) => {
|
|
1432
|
+
this.deps.emit(
|
|
1433
|
+
"transfer:attention",
|
|
1434
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
async function derivePendingTransfers(stores, decryptPayload) {
|
|
1441
|
+
const [backstop, journal, shortfalls] = await Promise.all([
|
|
1442
|
+
stores.backstop.list(),
|
|
1443
|
+
stores.deliveryJournal.list(),
|
|
1444
|
+
stores.shortfalls.list()
|
|
1445
|
+
]);
|
|
1446
|
+
const byTransfer = groupJournal(journal);
|
|
1447
|
+
const out = [];
|
|
1448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1449
|
+
for (const entry of backstop) {
|
|
1450
|
+
if (entry.disposition !== "open") continue;
|
|
1451
|
+
seen.add(entry.transferId);
|
|
1452
|
+
out.push(await openRow(entry, byTransfer.get(entry.transferId) ?? [], decryptPayload));
|
|
1453
|
+
}
|
|
1454
|
+
for (const [transferId, entries] of byTransfer) {
|
|
1455
|
+
if (seen.has(transferId)) continue;
|
|
1456
|
+
seen.add(transferId);
|
|
1457
|
+
out.push(journalOnlyRow(transferId, entries));
|
|
1458
|
+
}
|
|
1459
|
+
for (const entry of shortfalls) {
|
|
1460
|
+
if (!seen.has(entry.transferId)) out.push(shortfallRow(entry));
|
|
1461
|
+
}
|
|
1462
|
+
return out;
|
|
1463
|
+
}
|
|
1464
|
+
function groupJournal(journal) {
|
|
1465
|
+
const byTransfer = /* @__PURE__ */ new Map();
|
|
1466
|
+
for (const entry of journal) {
|
|
1467
|
+
const mine = byTransfer.get(entry.transferId);
|
|
1468
|
+
if (mine === void 0) byTransfer.set(entry.transferId, [entry]);
|
|
1469
|
+
else mine.push(entry);
|
|
1470
|
+
}
|
|
1471
|
+
return byTransfer;
|
|
1472
|
+
}
|
|
1473
|
+
async function openRow(entry, journal, decryptPayload) {
|
|
1474
|
+
let payload = null;
|
|
1475
|
+
try {
|
|
1476
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
1477
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
1478
|
+
} catch {
|
|
1479
|
+
payload = null;
|
|
1480
|
+
}
|
|
1481
|
+
const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
|
|
1482
|
+
const certified = distinctOpCount(journal);
|
|
1483
|
+
return {
|
|
1484
|
+
transferId: entry.transferId,
|
|
1485
|
+
kind: "open",
|
|
1486
|
+
recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
|
|
1487
|
+
coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
|
|
1488
|
+
amount: typeof payload?.amount === "string" ? payload.amount : "",
|
|
1489
|
+
legs: { certified, total: Math.max(total, certified) },
|
|
1490
|
+
deliveryPending: journal.length > 0,
|
|
1491
|
+
createdAt: entry.createdAt
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
function journalOnlyRow(transferId, entries) {
|
|
1495
|
+
const certified = distinctOpCount(entries);
|
|
1496
|
+
return {
|
|
1497
|
+
transferId,
|
|
1498
|
+
kind: "open",
|
|
1499
|
+
recipient: entries[0].recipientPubkey,
|
|
1500
|
+
coinId: "",
|
|
1501
|
+
amount: "",
|
|
1502
|
+
legs: { certified, total: certified },
|
|
1503
|
+
deliveryPending: true,
|
|
1504
|
+
createdAt: 0
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
function shortfallRow(entry) {
|
|
1508
|
+
return {
|
|
1509
|
+
transferId: entry.transferId,
|
|
1510
|
+
kind: "shortfall",
|
|
1511
|
+
recipient: entry.recipient,
|
|
1512
|
+
coinId: entry.coinId,
|
|
1513
|
+
amount: entry.remainingAmount,
|
|
1514
|
+
legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
|
|
1515
|
+
deliveryPending: false,
|
|
1516
|
+
createdAt: entry.createdAt
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function distinctOpCount(entries) {
|
|
1520
|
+
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// modules/payments-v2/restore.ts
|
|
1524
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
1525
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
1526
|
+
async function reseedAndReset(deps) {
|
|
1527
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
1528
|
+
if (entry.disposition !== "open") continue;
|
|
1529
|
+
try {
|
|
1530
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
1531
|
+
if (entry.requiresSeedClose) {
|
|
1532
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
1533
|
+
}
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
1539
|
+
await deps.fullRePull();
|
|
1540
|
+
}
|
|
1541
|
+
async function splitOpIndex(deps, envelope) {
|
|
1542
|
+
const payload = await deps.decryptPayload(envelope);
|
|
1543
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
1547
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
1548
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
1549
|
+
return {
|
|
1550
|
+
id: tokenId,
|
|
1551
|
+
coinId: asset.coinId,
|
|
1552
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
1553
|
+
name: registry.getName(asset.coinId),
|
|
1554
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
1555
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
1556
|
+
amount: asset.amount,
|
|
1557
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
1558
|
+
createdAt: snapshot.createdAt,
|
|
1559
|
+
updatedAt: snapshot.updatedAt,
|
|
1560
|
+
lazy: true,
|
|
1561
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
1565
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1566
|
+
for (const [tokenId, entry] of entries) {
|
|
1567
|
+
const moving = isTransferring(tokenId);
|
|
1568
|
+
for (const asset of entry.assets) {
|
|
1569
|
+
let group = groups.get(asset.coinId);
|
|
1570
|
+
if (!group) {
|
|
1571
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
1572
|
+
groups.set(asset.coinId, group);
|
|
1573
|
+
}
|
|
1574
|
+
if (moving) {
|
|
1575
|
+
group.transferring += BigInt(asset.amount);
|
|
1576
|
+
group.transferringCount += 1;
|
|
1577
|
+
} else {
|
|
1578
|
+
group.total += BigInt(asset.amount);
|
|
1579
|
+
group.count += 1;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
1584
|
+
}
|
|
1585
|
+
function toAsset(coinId, totals, registry) {
|
|
1586
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
1587
|
+
return {
|
|
1588
|
+
coinId,
|
|
1589
|
+
symbol: registry.getSymbol(coinId),
|
|
1590
|
+
name: registry.getName(coinId),
|
|
1591
|
+
decimals: registry.getDecimals(coinId),
|
|
1592
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
1593
|
+
totalAmount: totals.total.toString(),
|
|
1594
|
+
tokenCount: totals.count,
|
|
1595
|
+
confirmedAmount: totals.total.toString(),
|
|
1596
|
+
unconfirmedAmount: "0",
|
|
1597
|
+
confirmedTokenCount: totals.count,
|
|
1598
|
+
unconfirmedTokenCount: 0,
|
|
1599
|
+
transferringTokenCount: totals.transferringCount,
|
|
1600
|
+
transferringAmount: totals.transferring.toString(),
|
|
1601
|
+
priceUsd: null,
|
|
1602
|
+
priceEur: null,
|
|
1603
|
+
change24h: null,
|
|
1604
|
+
fiatValueUsd: null,
|
|
1605
|
+
fiatValueEur: null
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
async function withPrices(raw, registry, price) {
|
|
1609
|
+
try {
|
|
1610
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
1611
|
+
const quotes = await price.getPrices(names);
|
|
1612
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
1613
|
+
} catch {
|
|
1614
|
+
return raw;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function priceOne(asset, quote) {
|
|
1618
|
+
if (!quote) return asset;
|
|
1619
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
1620
|
+
return {
|
|
1621
|
+
...asset,
|
|
1622
|
+
priceUsd: quote.priceUsd,
|
|
1623
|
+
priceEur: quote.priceEur ?? null,
|
|
1624
|
+
change24h: quote.change24h ?? null,
|
|
1625
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
1626
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// modules/payments-v2/machine/payload.ts
|
|
1631
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
1632
|
+
return {
|
|
1633
|
+
v: 2,
|
|
1634
|
+
recipient: recipientPubkey,
|
|
1635
|
+
coinId: request.coinId,
|
|
1636
|
+
amount: request.amount,
|
|
1637
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
1638
|
+
direct: [...spend.plan.direct],
|
|
1639
|
+
...spend.plan.split !== void 0 ? {
|
|
1640
|
+
split: {
|
|
1641
|
+
tokenId: spend.plan.split.tokenId,
|
|
1642
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
1643
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
1644
|
+
}
|
|
1645
|
+
} : {},
|
|
1646
|
+
spentStates
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
function messageOf(err) {
|
|
1650
|
+
return err instanceof Error ? err.message : String(err);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1269
1653
|
// node_modules/@noble/ciphers/utils.js
|
|
1270
1654
|
function isBytes2(a) {
|
|
1271
1655
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
@@ -2174,13 +2558,14 @@ var History = class {
|
|
|
2174
2558
|
}
|
|
2175
2559
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
2176
2560
|
async post(build) {
|
|
2561
|
+
const record = build();
|
|
2177
2562
|
try {
|
|
2178
|
-
await this.deps.client.postHistory([
|
|
2563
|
+
await this.deps.client.postHistory([record]);
|
|
2179
2564
|
} catch (err) {
|
|
2180
2565
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
2181
2566
|
return;
|
|
2182
2567
|
}
|
|
2183
|
-
this.deps.emit("history:updated");
|
|
2568
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
2184
2569
|
}
|
|
2185
2570
|
ts(timestamp) {
|
|
2186
2571
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -2237,93 +2622,9 @@ var History = class {
|
|
|
2237
2622
|
}
|
|
2238
2623
|
};
|
|
2239
2624
|
|
|
2240
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
2241
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
2242
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
2243
|
-
return {
|
|
2244
|
-
id: tokenId,
|
|
2245
|
-
coinId: asset.coinId,
|
|
2246
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
2247
|
-
name: registry.getName(asset.coinId),
|
|
2248
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
2249
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
2250
|
-
amount: asset.amount,
|
|
2251
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
2252
|
-
createdAt: snapshot.createdAt,
|
|
2253
|
-
updatedAt: snapshot.updatedAt,
|
|
2254
|
-
lazy: true,
|
|
2255
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
2256
|
-
};
|
|
2257
|
-
}
|
|
2258
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
2259
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2260
|
-
for (const [tokenId, entry] of entries) {
|
|
2261
|
-
const moving = isTransferring(tokenId);
|
|
2262
|
-
for (const asset of entry.assets) {
|
|
2263
|
-
let group = groups.get(asset.coinId);
|
|
2264
|
-
if (!group) {
|
|
2265
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
2266
|
-
groups.set(asset.coinId, group);
|
|
2267
|
-
}
|
|
2268
|
-
if (moving) {
|
|
2269
|
-
group.transferring += BigInt(asset.amount);
|
|
2270
|
-
group.transferringCount += 1;
|
|
2271
|
-
} else {
|
|
2272
|
-
group.total += BigInt(asset.amount);
|
|
2273
|
-
group.count += 1;
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
2278
|
-
}
|
|
2279
|
-
function toAsset(coinId, totals, registry) {
|
|
2280
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
2281
|
-
return {
|
|
2282
|
-
coinId,
|
|
2283
|
-
symbol: registry.getSymbol(coinId),
|
|
2284
|
-
name: registry.getName(coinId),
|
|
2285
|
-
decimals: registry.getDecimals(coinId),
|
|
2286
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
2287
|
-
totalAmount: totals.total.toString(),
|
|
2288
|
-
tokenCount: totals.count,
|
|
2289
|
-
confirmedAmount: totals.total.toString(),
|
|
2290
|
-
unconfirmedAmount: "0",
|
|
2291
|
-
confirmedTokenCount: totals.count,
|
|
2292
|
-
unconfirmedTokenCount: 0,
|
|
2293
|
-
transferringTokenCount: totals.transferringCount,
|
|
2294
|
-
transferringAmount: totals.transferring.toString(),
|
|
2295
|
-
priceUsd: null,
|
|
2296
|
-
priceEur: null,
|
|
2297
|
-
change24h: null,
|
|
2298
|
-
fiatValueUsd: null,
|
|
2299
|
-
fiatValueEur: null
|
|
2300
|
-
};
|
|
2301
|
-
}
|
|
2302
|
-
async function withPrices(raw, registry, price) {
|
|
2303
|
-
try {
|
|
2304
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
2305
|
-
const quotes = await price.getPrices(names);
|
|
2306
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
2307
|
-
} catch {
|
|
2308
|
-
return raw;
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
function priceOne(asset, quote) {
|
|
2312
|
-
if (!quote) return asset;
|
|
2313
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
2314
|
-
return {
|
|
2315
|
-
...asset,
|
|
2316
|
-
priceUsd: quote.priceUsd,
|
|
2317
|
-
priceEur: quote.priceEur ?? null,
|
|
2318
|
-
change24h: quote.change24h ?? null,
|
|
2319
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
2320
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
2321
|
-
};
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
2625
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
2325
|
-
var SUSPECTED_KEY =
|
|
2326
|
-
var KNOWN_SPENDS_KEY =
|
|
2626
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
2627
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
2327
2628
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
2328
2629
|
function stateKey(tokenId, stateHash) {
|
|
2329
2630
|
return `${tokenId}:${stateHash}`;
|
|
@@ -2719,19 +3020,24 @@ var Receive = class {
|
|
|
2719
3020
|
const engine = deps.engine();
|
|
2720
3021
|
const stored = [];
|
|
2721
3022
|
const pending = [];
|
|
2722
|
-
|
|
3023
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
2723
3024
|
try {
|
|
2724
|
-
epoch = deps.syncEpoch();
|
|
2725
3025
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
2726
|
-
|
|
2727
|
-
for
|
|
2728
|
-
await
|
|
2729
|
-
|
|
3026
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
3027
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
3028
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
3029
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
3030
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
3031
|
+
}
|
|
3032
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
3033
|
+
const served = deps.delivery.incomingEpoch();
|
|
3034
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
3035
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
3036
|
+
basis = null;
|
|
2730
3037
|
}
|
|
2731
|
-
await flushAcks(deps, pending, epoch);
|
|
2732
3038
|
} catch (err) {
|
|
2733
3039
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
2734
|
-
await flushAcks(deps, pending,
|
|
3040
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
2735
3041
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
2736
3042
|
});
|
|
2737
3043
|
}
|
|
@@ -2804,7 +3110,7 @@ async function announce(deps, entry, record) {
|
|
|
2804
3110
|
receivedAt
|
|
2805
3111
|
};
|
|
2806
3112
|
}
|
|
2807
|
-
async function flushAcks(deps, pending,
|
|
3113
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
2808
3114
|
let lastAcked = null;
|
|
2809
3115
|
try {
|
|
2810
3116
|
while (pending.length > 0) {
|
|
@@ -2815,7 +3121,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
2815
3121
|
}
|
|
2816
3122
|
} finally {
|
|
2817
3123
|
if (lastAcked !== null) {
|
|
2818
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
3124
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
2819
3125
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
2820
3126
|
}
|
|
2821
3127
|
}
|
|
@@ -2916,9 +3222,19 @@ var Requests = class {
|
|
|
2916
3222
|
}
|
|
2917
3223
|
});
|
|
2918
3224
|
}
|
|
3225
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
2919
3226
|
writeLink(requestId, transferId, committed) {
|
|
2920
3227
|
return this.mutateJournal((m) => {
|
|
2921
|
-
|
|
3228
|
+
const prev = m.get(requestId);
|
|
3229
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
3230
|
+
const next = {
|
|
3231
|
+
requestId,
|
|
3232
|
+
transferId,
|
|
3233
|
+
committed: committed || same && prev.committed,
|
|
3234
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
3235
|
+
};
|
|
3236
|
+
if (same && prev.committed === next.committed) return false;
|
|
3237
|
+
m.set(requestId, next);
|
|
2922
3238
|
return true;
|
|
2923
3239
|
});
|
|
2924
3240
|
}
|
|
@@ -3071,23 +3387,46 @@ var Requests = class {
|
|
|
3071
3387
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
3072
3388
|
if (transferId !== void 0) {
|
|
3073
3389
|
const committed = error instanceof PartialSendConflictError;
|
|
3074
|
-
await this.
|
|
3075
|
-
this.setStatus(id, "settling");
|
|
3390
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
3076
3391
|
} else {
|
|
3077
3392
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
3078
3393
|
this.setStatus(id, "paid");
|
|
3079
3394
|
}
|
|
3080
3395
|
} else {
|
|
3081
|
-
await this.
|
|
3082
|
-
this.setStatus(id, "pending");
|
|
3396
|
+
await this.revertPayable(id);
|
|
3083
3397
|
}
|
|
3084
3398
|
throw error;
|
|
3085
3399
|
}
|
|
3086
|
-
this.
|
|
3087
|
-
await this.respondPaid(id, result.id);
|
|
3088
|
-
await this.clearLink(id);
|
|
3400
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
3089
3401
|
return result;
|
|
3090
3402
|
}
|
|
3403
|
+
/**
|
|
3404
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
3405
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
3406
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
3407
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
3408
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
3409
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
3410
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
3411
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
3412
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
3413
|
+
*/
|
|
3414
|
+
async settle(requestId, transferId, opts) {
|
|
3415
|
+
let journalDown = false;
|
|
3416
|
+
try {
|
|
3417
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
if (!opts.respond) throw err;
|
|
3420
|
+
journalDown = true;
|
|
3421
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
3422
|
+
}
|
|
3423
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
3424
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
3428
|
+
this.setStatus(requestId, "paid");
|
|
3429
|
+
}
|
|
3091
3430
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
3092
3431
|
async respondPaid(id, transferId) {
|
|
3093
3432
|
try {
|
|
@@ -3123,32 +3462,32 @@ var Requests = class {
|
|
|
3123
3462
|
let aborted = null;
|
|
3124
3463
|
for (const [requestId, link] of [...this.journal]) {
|
|
3125
3464
|
if (link.committed || resumed.has(link.transferId)) {
|
|
3126
|
-
await this.resolvePaid(requestId, link
|
|
3465
|
+
await this.resolvePaid(requestId, link);
|
|
3127
3466
|
} else if (conflicted.has(link.transferId)) {
|
|
3128
3467
|
await this.revertPayable(requestId);
|
|
3129
3468
|
} else if (open.has(link.transferId)) {
|
|
3130
3469
|
} else {
|
|
3131
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
3470
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
3132
3471
|
}
|
|
3133
3472
|
}
|
|
3134
3473
|
}
|
|
3135
3474
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
3136
3475
|
// a probe failure defers the link to the next session.
|
|
3137
|
-
async reconcileUnaccounted(requestId,
|
|
3476
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
3138
3477
|
try {
|
|
3139
3478
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
3140
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
3141
|
-
else await this.resolvePaid(requestId,
|
|
3479
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
3480
|
+
else await this.resolvePaid(requestId, link);
|
|
3142
3481
|
} catch (err) {
|
|
3143
3482
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
3144
3483
|
}
|
|
3145
3484
|
return aborted;
|
|
3146
3485
|
}
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
this.setStatus(requestId, "paid");
|
|
3486
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
3487
|
+
resolvePaid(requestId, link) {
|
|
3488
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
3151
3489
|
}
|
|
3490
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
3152
3491
|
async revertPayable(requestId) {
|
|
3153
3492
|
await this.clearLink(requestId);
|
|
3154
3493
|
this.setStatus(requestId, "pending");
|
|
@@ -3472,11 +3811,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3472
3811
|
client: deps.client,
|
|
3473
3812
|
fieldKey: deps.fieldKey,
|
|
3474
3813
|
registry: deps.registry,
|
|
3475
|
-
emit: (event) => deps.emit(event,
|
|
3814
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
3476
3815
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
3477
3816
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
3478
3817
|
});
|
|
3479
3818
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
3819
|
+
const machineStores = createMachineStores(deps.kv);
|
|
3480
3820
|
const heldStates = /* @__PURE__ */ new Map();
|
|
3481
3821
|
return {
|
|
3482
3822
|
ownPubkeyBytes,
|
|
@@ -3484,12 +3824,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3484
3824
|
ledger,
|
|
3485
3825
|
queue,
|
|
3486
3826
|
historyStore,
|
|
3487
|
-
machineStores
|
|
3827
|
+
machineStores,
|
|
3488
3828
|
machineDeps,
|
|
3489
3829
|
machine: new TransferMachine(machineDeps),
|
|
3490
3830
|
heldStates,
|
|
3491
3831
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
3492
|
-
requests: buildRequests(deps, hooks)
|
|
3832
|
+
requests: buildRequests(deps, hooks),
|
|
3833
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
3837
|
+
return {
|
|
3838
|
+
stores: machineStores,
|
|
3839
|
+
kv: deps.kv,
|
|
3840
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
3841
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
3842
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
3843
|
+
fullRePull: () => view.onEpochReset(),
|
|
3844
|
+
attention: (transferId, code, detail) => {
|
|
3845
|
+
deps.emit(
|
|
3846
|
+
"transfer:attention",
|
|
3847
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3848
|
+
);
|
|
3849
|
+
}
|
|
3493
3850
|
};
|
|
3494
3851
|
}
|
|
3495
3852
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -3511,11 +3868,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
3511
3868
|
ownPubkey: hexToBytes(deps.ownPubkey),
|
|
3512
3869
|
emit: deps.emit,
|
|
3513
3870
|
now: deps.now ?? Date.now,
|
|
3514
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
3871
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
3515
3872
|
await historyStore.recordSent({
|
|
3516
3873
|
transferId,
|
|
3517
3874
|
coinId: payload.coinId,
|
|
3518
|
-
|
|
3875
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
3876
|
+
// recipient blobs), never payload.amount — the plan.
|
|
3877
|
+
amount: committedAmount,
|
|
3519
3878
|
recipientPubkey: payload.recipient,
|
|
3520
3879
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
3521
3880
|
});
|
|
@@ -3542,7 +3901,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
3542
3901
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3543
3902
|
);
|
|
3544
3903
|
},
|
|
3545
|
-
syncEpoch: deps.syncEpoch
|
|
3904
|
+
syncEpoch: deps.syncEpoch,
|
|
3546
3905
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3547
3906
|
});
|
|
3548
3907
|
}
|
|
@@ -3595,8 +3954,24 @@ var PaymentsFacade = class {
|
|
|
3595
3954
|
this.receiveLoop = parts.receiveLoop;
|
|
3596
3955
|
this.heldStates = parts.heldStates;
|
|
3597
3956
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3957
|
+
this.restoreDeps = parts.restoreDeps;
|
|
3598
3958
|
this.requests = parts.requests;
|
|
3599
3959
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3960
|
+
this.heartbeat = new ConvergenceHeartbeat({
|
|
3961
|
+
now: () => this.nowMs(),
|
|
3962
|
+
onTick: () => this.trackTail(this.runConvergencePass())
|
|
3963
|
+
});
|
|
3964
|
+
this.converger = new Converger({
|
|
3965
|
+
machineDeps: this.machineDeps,
|
|
3966
|
+
stores: this.machineStores,
|
|
3967
|
+
delivery: deps.deliveryPort,
|
|
3968
|
+
reconcile: (report) => this.requests.reconcile(report),
|
|
3969
|
+
refreshView: () => this.view.delta(),
|
|
3970
|
+
replayMints: () => this.replayMints(),
|
|
3971
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id),
|
|
3972
|
+
emit: deps.emit,
|
|
3973
|
+
now: () => this.nowMs()
|
|
3974
|
+
});
|
|
3600
3975
|
}
|
|
3601
3976
|
view;
|
|
3602
3977
|
ledger;
|
|
@@ -3608,17 +3983,28 @@ var PaymentsFacade = class {
|
|
|
3608
3983
|
receiveLoop;
|
|
3609
3984
|
heldStates;
|
|
3610
3985
|
ownPubkeyBytes;
|
|
3986
|
+
restoreDeps;
|
|
3611
3987
|
requests;
|
|
3612
3988
|
currentEngine = null;
|
|
3613
3989
|
pendingOps = /* @__PURE__ */ new Set();
|
|
3614
3990
|
unsubscribers = [];
|
|
3615
3991
|
started = false;
|
|
3992
|
+
// §7 heartbeat: in-memory timing cell + pass runner (see convergence.ts).
|
|
3993
|
+
heartbeat;
|
|
3994
|
+
converger;
|
|
3995
|
+
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
3996
|
+
resumeFlight = new SingleFlight();
|
|
3997
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
3998
|
+
passChain = new SerialChain();
|
|
3999
|
+
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
4000
|
+
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
3616
4001
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
3617
4002
|
async start() {
|
|
3618
4003
|
if (this.started) return;
|
|
3619
4004
|
this.started = true;
|
|
3620
|
-
|
|
4005
|
+
this.heartbeat.start();
|
|
3621
4006
|
this.unsubscribers.push(
|
|
4007
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
3622
4008
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
3623
4009
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
3624
4010
|
}),
|
|
@@ -3629,20 +4015,12 @@ var PaymentsFacade = class {
|
|
|
3629
4015
|
this.trackTail(this.requests.drainIncoming());
|
|
3630
4016
|
})
|
|
3631
4017
|
);
|
|
3632
|
-
this.
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
this.deps.emit(
|
|
3639
|
-
"transfer:attention",
|
|
3640
|
-
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3641
|
-
);
|
|
3642
|
-
}
|
|
3643
|
-
})
|
|
3644
|
-
);
|
|
3645
|
-
this.trackTail(this.replayMints());
|
|
4018
|
+
const statusUnsub = this.deps.session.subscribeStatus?.((status) => {
|
|
4019
|
+
if (status === "connected") this.heartbeat.recovered();
|
|
4020
|
+
});
|
|
4021
|
+
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
4022
|
+
await this.deps.session.start();
|
|
4023
|
+
this.trackTail(this.runConvergencePass());
|
|
3646
4024
|
this.trackTail(this.seedHeldStates());
|
|
3647
4025
|
await this.view.fullPull().catch(() => void 0);
|
|
3648
4026
|
this.receiveLoop.start(this.deps.receivePollMs);
|
|
@@ -3650,6 +4028,7 @@ var PaymentsFacade = class {
|
|
|
3650
4028
|
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
3651
4029
|
async stop() {
|
|
3652
4030
|
this.started = false;
|
|
4031
|
+
this.heartbeat.stop();
|
|
3653
4032
|
this.receiveLoop.stop();
|
|
3654
4033
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
3655
4034
|
this.unsubscribers = [];
|
|
@@ -3675,9 +4054,13 @@ var PaymentsFacade = class {
|
|
|
3675
4054
|
history(page) {
|
|
3676
4055
|
return this.historyStore.page(page ?? {});
|
|
3677
4056
|
}
|
|
4057
|
+
/** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
|
|
4058
|
+
pendingTransfers() {
|
|
4059
|
+
return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
|
|
4060
|
+
}
|
|
3678
4061
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
3679
4062
|
send(request) {
|
|
3680
|
-
return this.track(this.
|
|
4063
|
+
return this.track(this.sendOutcome(request));
|
|
3681
4064
|
}
|
|
3682
4065
|
async receive() {
|
|
3683
4066
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -3687,9 +4070,32 @@ var PaymentsFacade = class {
|
|
|
3687
4070
|
mint(coinId, amount) {
|
|
3688
4071
|
return this.track(this.mintInner(coinId, amount));
|
|
3689
4072
|
}
|
|
4073
|
+
// §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
|
|
4074
|
+
async resumeNow() {
|
|
4075
|
+
if (!this.started) return;
|
|
4076
|
+
await this.track(this.runConvergencePass());
|
|
4077
|
+
}
|
|
4078
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
4079
|
+
async handleEpochChange(_newEpoch) {
|
|
4080
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
4081
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
4082
|
+
}
|
|
4083
|
+
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
4084
|
+
runConvergencePass() {
|
|
4085
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
4086
|
+
}
|
|
4087
|
+
async convergeBody() {
|
|
4088
|
+
const outcome = await this.converger.convergeOnce();
|
|
4089
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
4090
|
+
this.heartbeat.settle(outcome, pending);
|
|
4091
|
+
}
|
|
4092
|
+
nowMs() {
|
|
4093
|
+
return (this.deps.now ?? Date.now)();
|
|
4094
|
+
}
|
|
3690
4095
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
3691
|
-
|
|
3692
|
-
|
|
4096
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
4097
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
4098
|
+
async sendOutcome(request) {
|
|
3693
4099
|
const run = {
|
|
3694
4100
|
amount: request.amount,
|
|
3695
4101
|
delivered: [],
|
|
@@ -3697,12 +4103,36 @@ var PaymentsFacade = class {
|
|
|
3697
4103
|
tokenTransfers: [],
|
|
3698
4104
|
settledShortfalls: [],
|
|
3699
4105
|
firstPartialId: void 0,
|
|
3700
|
-
pendingAny: false
|
|
4106
|
+
pendingAny: false,
|
|
4107
|
+
lastTransferId: ""
|
|
3701
4108
|
};
|
|
4109
|
+
try {
|
|
4110
|
+
return await this.sendWithPolicy(request, run);
|
|
4111
|
+
} catch (err) {
|
|
4112
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
4113
|
+
throw err;
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
4117
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
4118
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
4119
|
+
emitCleanFailure(err, transferId) {
|
|
4120
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
4121
|
+
const failed = {
|
|
4122
|
+
id: transferId,
|
|
4123
|
+
status: "failed",
|
|
4124
|
+
tokens: [],
|
|
4125
|
+
tokenTransfers: [],
|
|
4126
|
+
error: messageOf(err)
|
|
4127
|
+
};
|
|
4128
|
+
this.deps.emit("transfer:updated", failed);
|
|
4129
|
+
}
|
|
4130
|
+
async sendWithPolicy(request, run) {
|
|
4131
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
3702
4132
|
for (let attempt = 0; ; attempt++) {
|
|
3703
4133
|
let ctx;
|
|
3704
4134
|
try {
|
|
3705
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
4135
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
3706
4136
|
} catch (err) {
|
|
3707
4137
|
throw this.partialize(err, run);
|
|
3708
4138
|
}
|
|
@@ -3736,21 +4166,24 @@ var PaymentsFacade = class {
|
|
|
3736
4166
|
return recipient;
|
|
3737
4167
|
}
|
|
3738
4168
|
async runAttempt(ctx) {
|
|
4169
|
+
this.activeMoneyOps.add(ctx.transferId);
|
|
3739
4170
|
try {
|
|
3740
4171
|
const { deliveryPending } = await this.machine.run(ctx.plan);
|
|
3741
4172
|
this.settleSuccess(ctx, ctx.sourceIds);
|
|
3742
4173
|
return { kind: "success", deliveryPending };
|
|
3743
4174
|
} catch (err) {
|
|
3744
|
-
return this.disposeFailedAttempt(ctx, err);
|
|
4175
|
+
return await this.disposeFailedAttempt(ctx, err);
|
|
4176
|
+
} finally {
|
|
4177
|
+
this.activeMoneyOps.delete(ctx.transferId);
|
|
3745
4178
|
}
|
|
3746
4179
|
}
|
|
3747
|
-
/**
|
|
3748
|
-
*
|
|
3749
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
3750
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
3751
|
-
*/
|
|
4180
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
4181
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
3752
4182
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
3753
|
-
|
|
4183
|
+
if (disposition.deliveryPending) {
|
|
4184
|
+
run.pendingAny = true;
|
|
4185
|
+
this.heartbeat.arm();
|
|
4186
|
+
}
|
|
3754
4187
|
const partial = disposition.entry;
|
|
3755
4188
|
run.delivered.push(...partial.committedTokenIds);
|
|
3756
4189
|
run.firstPartialId ??= partial.transferId;
|
|
@@ -3771,11 +4204,8 @@ var PaymentsFacade = class {
|
|
|
3771
4204
|
run.amount = partial.remainingAmount;
|
|
3772
4205
|
return null;
|
|
3773
4206
|
}
|
|
3774
|
-
/**
|
|
3775
|
-
*
|
|
3776
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
3777
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
3778
|
-
*/
|
|
4207
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
4208
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
3779
4209
|
async disposeFailedAttempt(ctx, err) {
|
|
3780
4210
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
3781
4211
|
this.settleKeepOpen(ctx);
|
|
@@ -3824,8 +4254,9 @@ var PaymentsFacade = class {
|
|
|
3824
4254
|
}
|
|
3825
4255
|
return { kind: "rethrow", error: err };
|
|
3826
4256
|
}
|
|
3827
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
4257
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
3828
4258
|
const transferId = this.newId();
|
|
4259
|
+
run.lastTransferId = transferId;
|
|
3829
4260
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
3830
4261
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
3831
4262
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -3908,6 +4339,7 @@ var PaymentsFacade = class {
|
|
|
3908
4339
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
3909
4340
|
settleKeepOpen(ctx) {
|
|
3910
4341
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
4342
|
+
this.heartbeat.arm();
|
|
3911
4343
|
}
|
|
3912
4344
|
settlePartial(ctx, partial) {
|
|
3913
4345
|
this.ledger.cancel(ctx.transferId);
|
|
@@ -3944,6 +4376,7 @@ var PaymentsFacade = class {
|
|
|
3944
4376
|
for (const id of run.settledShortfalls.splice(0)) {
|
|
3945
4377
|
await this.machineStores.shortfalls.removeByKey(id).catch(() => void 0);
|
|
3946
4378
|
}
|
|
4379
|
+
if (run.pendingAny) this.heartbeat.arm();
|
|
3947
4380
|
const result = {
|
|
3948
4381
|
id: transferId,
|
|
3949
4382
|
status: run.pendingAny ? "confirmed" : "delivered",
|
|
@@ -3955,11 +4388,7 @@ var PaymentsFacade = class {
|
|
|
3955
4388
|
this.deps.emit("transfer:updated", result);
|
|
3956
4389
|
return result;
|
|
3957
4390
|
}
|
|
3958
|
-
/**
|
|
3959
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
3960
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
3961
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
3962
|
-
*/
|
|
4391
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
3963
4392
|
partialize(err, run) {
|
|
3964
4393
|
if (run.delivered.length === 0) return err;
|
|
3965
4394
|
return new PartialSendConflictError(
|
|
@@ -3988,21 +4417,29 @@ var PaymentsFacade = class {
|
|
|
3988
4417
|
if (!/^(?:[0-9a-f]{2})+$/.test(coinId) || amount <= 0n) {
|
|
3989
4418
|
return { success: false, error: "coinId must be even-length lowercase hex and amount positive" };
|
|
3990
4419
|
}
|
|
3991
|
-
const engine = this.engine();
|
|
3992
4420
|
const mintId = this.newId();
|
|
3993
|
-
|
|
4421
|
+
this.activeMoneyOps.add(mintId);
|
|
4422
|
+
try {
|
|
4423
|
+
return await this.mintUnderJournal(mintId, coinId, amount);
|
|
4424
|
+
} finally {
|
|
4425
|
+
this.activeMoneyOps.delete(mintId);
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
async mintUnderJournal(mintId, coinId, amount) {
|
|
4429
|
+
const engine = this.engine();
|
|
3994
4430
|
const entry = {
|
|
3995
4431
|
mintId,
|
|
3996
4432
|
coinId,
|
|
3997
4433
|
amount: amount.toString(),
|
|
3998
4434
|
tokenId: "",
|
|
3999
|
-
createdAt:
|
|
4435
|
+
createdAt: this.nowMs()
|
|
4000
4436
|
};
|
|
4001
4437
|
await this.machineStores.mintJournal.upsert(entry);
|
|
4002
4438
|
let token;
|
|
4003
4439
|
try {
|
|
4004
|
-
token = await engine.mint(
|
|
4440
|
+
token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
|
|
4005
4441
|
} catch (err) {
|
|
4442
|
+
this.heartbeat.arm();
|
|
4006
4443
|
return { success: false, error: messageOf(err) };
|
|
4007
4444
|
}
|
|
4008
4445
|
if (token.blob.tokenId !== entry.tokenId) {
|
|
@@ -4011,6 +4448,7 @@ var PaymentsFacade = class {
|
|
|
4011
4448
|
try {
|
|
4012
4449
|
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
4013
4450
|
} catch (err) {
|
|
4451
|
+
this.heartbeat.arm();
|
|
4014
4452
|
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
4015
4453
|
}
|
|
4016
4454
|
return { success: true, tokenId: token.blob.tokenId };
|
|
@@ -4033,18 +4471,22 @@ var PaymentsFacade = class {
|
|
|
4033
4471
|
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
4034
4472
|
this.trackTail(this.view.delta());
|
|
4035
4473
|
}
|
|
4474
|
+
/** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
|
|
4036
4475
|
async replayMints() {
|
|
4476
|
+
let resolved = 0;
|
|
4037
4477
|
for (const entry of await this.machineStores.mintJournal.list()) {
|
|
4478
|
+
if (this.activeMoneyOps.has(entry.mintId)) continue;
|
|
4038
4479
|
try {
|
|
4039
|
-
await this.replayMint(entry);
|
|
4480
|
+
if (await this.replayMint(entry)) resolved += 1;
|
|
4040
4481
|
} catch {
|
|
4041
4482
|
}
|
|
4042
4483
|
}
|
|
4484
|
+
return resolved;
|
|
4043
4485
|
}
|
|
4044
4486
|
async replayMint(entry) {
|
|
4045
4487
|
if (entry.tokenId !== "" && await this.tokenInServerInventory(entry.tokenId)) {
|
|
4046
4488
|
await this.machineStores.mintJournal.removeByKey(entry.mintId);
|
|
4047
|
-
return;
|
|
4489
|
+
return true;
|
|
4048
4490
|
}
|
|
4049
4491
|
const engine = this.engine();
|
|
4050
4492
|
if (!supportsDeterministicMint(engine)) {
|
|
@@ -4052,7 +4494,7 @@ var PaymentsFacade = class {
|
|
|
4052
4494
|
transferId: entry.mintId,
|
|
4053
4495
|
code: ATTENTION_MINT_UNRESOLVED
|
|
4054
4496
|
});
|
|
4055
|
-
return;
|
|
4497
|
+
return false;
|
|
4056
4498
|
}
|
|
4057
4499
|
const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
|
|
4058
4500
|
transferId: entry.mintId
|
|
@@ -4061,6 +4503,7 @@ var PaymentsFacade = class {
|
|
|
4061
4503
|
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
4062
4504
|
}
|
|
4063
4505
|
await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
|
|
4506
|
+
return true;
|
|
4064
4507
|
}
|
|
4065
4508
|
mintParams(coinId, amount) {
|
|
4066
4509
|
return {
|
|
@@ -4086,17 +4529,6 @@ var PaymentsFacade = class {
|
|
|
4086
4529
|
newId() {
|
|
4087
4530
|
return (this.deps.newId ?? randomUUID)();
|
|
4088
4531
|
}
|
|
4089
|
-
async runResume() {
|
|
4090
|
-
const report = await resumeAll(this.machineDeps);
|
|
4091
|
-
await this.requests.reconcile({
|
|
4092
|
-
resumed: report.resumed,
|
|
4093
|
-
conflicted: report.conflicted,
|
|
4094
|
-
open: report.failed
|
|
4095
|
-
});
|
|
4096
|
-
if (report.resumed.length > 0 || report.conflicted.length > 0) {
|
|
4097
|
-
await this.view.delta().catch(() => void 0);
|
|
4098
|
-
}
|
|
4099
|
-
}
|
|
4100
4532
|
async seedHeldStates() {
|
|
4101
4533
|
let page = await this.deps.storagePort.listInventory();
|
|
4102
4534
|
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
@@ -4108,22 +4540,14 @@ var PaymentsFacade = class {
|
|
|
4108
4540
|
}
|
|
4109
4541
|
}
|
|
4110
4542
|
toUiToken(tokenId, coinId, amount) {
|
|
4111
|
-
const
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
decimals: registry.getDecimals(coinId),
|
|
4120
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
4121
|
-
amount: amount.toString(),
|
|
4122
|
-
status: "transferring",
|
|
4123
|
-
createdAt: now,
|
|
4124
|
-
updatedAt: now,
|
|
4125
|
-
lazy: true
|
|
4126
|
-
};
|
|
4543
|
+
const now = this.nowMs();
|
|
4544
|
+
return toToken(
|
|
4545
|
+
tokenId,
|
|
4546
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
4547
|
+
{ coinId, amount: amount.toString() },
|
|
4548
|
+
this.deps.registry,
|
|
4549
|
+
{ transferring: true, suspectedSpent: false }
|
|
4550
|
+
);
|
|
4127
4551
|
}
|
|
4128
4552
|
track(op) {
|
|
4129
4553
|
const settled = op.then(
|
|
@@ -4138,30 +4562,12 @@ var PaymentsFacade = class {
|
|
|
4138
4562
|
this.track(op).catch(() => void 0);
|
|
4139
4563
|
}
|
|
4140
4564
|
};
|
|
4141
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
4142
|
-
return {
|
|
4143
|
-
v: 2,
|
|
4144
|
-
recipient: recipientPubkey,
|
|
4145
|
-
coinId: request.coinId,
|
|
4146
|
-
amount: request.amount,
|
|
4147
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
4148
|
-
direct: [...spend.plan.direct],
|
|
4149
|
-
...spend.plan.split !== void 0 ? {
|
|
4150
|
-
split: {
|
|
4151
|
-
tokenId: spend.plan.split.tokenId,
|
|
4152
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
4153
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
4154
|
-
}
|
|
4155
|
-
} : {},
|
|
4156
|
-
spentStates
|
|
4157
|
-
};
|
|
4158
|
-
}
|
|
4159
|
-
function messageOf(err) {
|
|
4160
|
-
return err instanceof Error ? err.message : String(err);
|
|
4161
|
-
}
|
|
4162
4565
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4163
4566
|
0 && (module.exports = {
|
|
4164
4567
|
ATTENTION_MINT_UNRESOLVED,
|
|
4568
|
+
ATTENTION_RESEED_REJECTED,
|
|
4569
|
+
HEARTBEAT_CAP_MS,
|
|
4570
|
+
HEARTBEAT_SEED_MS,
|
|
4165
4571
|
MAX_RESELECT,
|
|
4166
4572
|
PaymentsFacade,
|
|
4167
4573
|
STORE_KEYS,
|