@unicitylabs/sphere-sdk 0.14.0-dev.5 → 0.14.0-dev.6
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 +303 -35
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +21 -0
- package/dist/core/index.d.ts +21 -0
- package/dist/core/index.js +303 -35
- 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 +7 -0
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.d.cts +3 -0
- package/dist/impl/wallet-api-v2/index.d.ts +3 -0
- package/dist/impl/wallet-api-v2/index.js +7 -0
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +303 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +303 -35
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +300 -35
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +45 -2
- package/dist/modules/payments-v2/index.d.ts +45 -2
- package/dist/modules/payments-v2/index.js +298 -35
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -549,6 +549,10 @@ function isValidationReject(err) {
|
|
|
549
549
|
if (e === null) return false;
|
|
550
550
|
return e.status === 422 || e.statusCode === 422 || e.code === "VALIDATION" || e.code === "VALIDATION_FAILED";
|
|
551
551
|
}
|
|
552
|
+
function retryAfterMsOf(err) {
|
|
553
|
+
const retryAfter = err !== null && typeof err === "object" ? err.retryAfter : void 0;
|
|
554
|
+
return typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : null;
|
|
555
|
+
}
|
|
552
556
|
var chainRegistry = /* @__PURE__ */ new WeakMap();
|
|
553
557
|
var replayRegistry = /* @__PURE__ */ new WeakMap();
|
|
554
558
|
function registryFor(registry, kv, storeKey, make) {
|
|
@@ -1225,6 +1229,217 @@ function validatePayload(raw) {
|
|
|
1225
1229
|
return p;
|
|
1226
1230
|
}
|
|
1227
1231
|
|
|
1232
|
+
// modules/payments-v2/convergence.ts
|
|
1233
|
+
var HEARTBEAT_SEED_MS = 5e3;
|
|
1234
|
+
var HEARTBEAT_CAP_MS = 12e4;
|
|
1235
|
+
var ConvergenceHeartbeat = class {
|
|
1236
|
+
constructor(deps) {
|
|
1237
|
+
this.deps = deps;
|
|
1238
|
+
}
|
|
1239
|
+
timer = null;
|
|
1240
|
+
delayMs = HEARTBEAT_SEED_MS;
|
|
1241
|
+
notBefore = 0;
|
|
1242
|
+
running = false;
|
|
1243
|
+
start() {
|
|
1244
|
+
this.running = true;
|
|
1245
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
1246
|
+
this.notBefore = 0;
|
|
1247
|
+
}
|
|
1248
|
+
/** §7 quiescence unchanged: a mid-backoff stop() cancels cleanly. */
|
|
1249
|
+
stop() {
|
|
1250
|
+
this.running = false;
|
|
1251
|
+
this.clearTimer();
|
|
1252
|
+
}
|
|
1253
|
+
/** Reset to seed on progress, ×2 to the cap otherwise; idle stops rescheduling. */
|
|
1254
|
+
settle(outcome, pending) {
|
|
1255
|
+
if (outcome.retryAfterMs !== null) {
|
|
1256
|
+
this.notBefore = Math.max(this.notBefore, this.deps.now() + outcome.retryAfterMs);
|
|
1257
|
+
}
|
|
1258
|
+
if (!pending) {
|
|
1259
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const delay = outcome.progress ? HEARTBEAT_SEED_MS : this.delayMs;
|
|
1263
|
+
this.delayMs = Math.min(delay * 2, HEARTBEAT_CAP_MS);
|
|
1264
|
+
this.schedule(delay);
|
|
1265
|
+
}
|
|
1266
|
+
/** New pending work (keep-open park / journaled delivery / mint hold): prompt retry. */
|
|
1267
|
+
arm() {
|
|
1268
|
+
if (!this.running) return;
|
|
1269
|
+
this.delayMs = Math.min(HEARTBEAT_SEED_MS * 2, HEARTBEAT_CAP_MS);
|
|
1270
|
+
this.schedule(HEARTBEAT_SEED_MS);
|
|
1271
|
+
}
|
|
1272
|
+
/** connection:status recovery: the backoff resets; a pending tick re-fires at the seed. */
|
|
1273
|
+
recovered() {
|
|
1274
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
1275
|
+
if (this.timer !== null) this.arm();
|
|
1276
|
+
}
|
|
1277
|
+
schedule(delayMs) {
|
|
1278
|
+
this.clearTimer();
|
|
1279
|
+
if (!this.running) return;
|
|
1280
|
+
const floored = Math.max(delayMs, this.notBefore - this.deps.now());
|
|
1281
|
+
this.timer = setTimeout(() => {
|
|
1282
|
+
this.timer = null;
|
|
1283
|
+
this.deps.onTick();
|
|
1284
|
+
}, floored);
|
|
1285
|
+
}
|
|
1286
|
+
clearTimer() {
|
|
1287
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
1288
|
+
this.timer = null;
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
var Converger = class {
|
|
1292
|
+
constructor(deps) {
|
|
1293
|
+
this.deps = deps;
|
|
1294
|
+
}
|
|
1295
|
+
/** Resume open intents, then drain the delivery + mint journals (all idempotent). */
|
|
1296
|
+
async convergeOnce() {
|
|
1297
|
+
let progress = false;
|
|
1298
|
+
let retryAfterMs = null;
|
|
1299
|
+
try {
|
|
1300
|
+
progress = await this.resumePass();
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
retryAfterMs = retryAfterMsOf(err);
|
|
1303
|
+
}
|
|
1304
|
+
try {
|
|
1305
|
+
const stats = await this.deps.stores.deliveryJournal.replay(this.replayDeps());
|
|
1306
|
+
progress = progress || stats.delivered > 0;
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
progress = await this.deps.replayMints() > 0 || progress;
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
1314
|
+
}
|
|
1315
|
+
return { progress, retryAfterMs };
|
|
1316
|
+
}
|
|
1317
|
+
/** §4 heartbeat predicate — read fresh from the §6 stores, never cached. */
|
|
1318
|
+
async pendingWork() {
|
|
1319
|
+
const [backstop, journal, mints] = await Promise.all([
|
|
1320
|
+
this.deps.stores.backstop.list(),
|
|
1321
|
+
this.deps.stores.deliveryJournal.list(),
|
|
1322
|
+
this.deps.stores.mintJournal.list()
|
|
1323
|
+
]);
|
|
1324
|
+
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
1325
|
+
}
|
|
1326
|
+
async resumePass() {
|
|
1327
|
+
const report = await resumeAll(this.filteredMachineDeps());
|
|
1328
|
+
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
1329
|
+
await this.deps.reconcile({
|
|
1330
|
+
resumed: report.resumed,
|
|
1331
|
+
conflicted: report.conflicted,
|
|
1332
|
+
open: report.failed
|
|
1333
|
+
});
|
|
1334
|
+
if (progress) await this.deps.refreshView().catch(() => void 0);
|
|
1335
|
+
return progress;
|
|
1336
|
+
}
|
|
1337
|
+
filteredMachineDeps() {
|
|
1338
|
+
const intents = this.deps.machineDeps.intents;
|
|
1339
|
+
return {
|
|
1340
|
+
...this.deps.machineDeps,
|
|
1341
|
+
intents: {
|
|
1342
|
+
...intents,
|
|
1343
|
+
listOpen: async () => (await intents.listOpen()).filter((intent) => !this.deps.isActiveOp(intent.transferId))
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
replayDeps() {
|
|
1348
|
+
return {
|
|
1349
|
+
delivery: this.deps.delivery,
|
|
1350
|
+
now: () => this.deps.now(),
|
|
1351
|
+
attention: (transferId, code, detail) => {
|
|
1352
|
+
this.deps.emit(
|
|
1353
|
+
"transfer:attention",
|
|
1354
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
async function derivePendingTransfers(stores, decryptPayload) {
|
|
1361
|
+
const [backstop, journal, shortfalls] = await Promise.all([
|
|
1362
|
+
stores.backstop.list(),
|
|
1363
|
+
stores.deliveryJournal.list(),
|
|
1364
|
+
stores.shortfalls.list()
|
|
1365
|
+
]);
|
|
1366
|
+
const byTransfer = groupJournal(journal);
|
|
1367
|
+
const out = [];
|
|
1368
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1369
|
+
for (const entry of backstop) {
|
|
1370
|
+
if (entry.disposition !== "open") continue;
|
|
1371
|
+
seen.add(entry.transferId);
|
|
1372
|
+
out.push(await openRow(entry, byTransfer.get(entry.transferId) ?? [], decryptPayload));
|
|
1373
|
+
}
|
|
1374
|
+
for (const [transferId, entries] of byTransfer) {
|
|
1375
|
+
if (seen.has(transferId)) continue;
|
|
1376
|
+
seen.add(transferId);
|
|
1377
|
+
out.push(journalOnlyRow(transferId, entries));
|
|
1378
|
+
}
|
|
1379
|
+
for (const entry of shortfalls) {
|
|
1380
|
+
if (!seen.has(entry.transferId)) out.push(shortfallRow(entry));
|
|
1381
|
+
}
|
|
1382
|
+
return out;
|
|
1383
|
+
}
|
|
1384
|
+
function groupJournal(journal) {
|
|
1385
|
+
const byTransfer = /* @__PURE__ */ new Map();
|
|
1386
|
+
for (const entry of journal) {
|
|
1387
|
+
const mine = byTransfer.get(entry.transferId);
|
|
1388
|
+
if (mine === void 0) byTransfer.set(entry.transferId, [entry]);
|
|
1389
|
+
else mine.push(entry);
|
|
1390
|
+
}
|
|
1391
|
+
return byTransfer;
|
|
1392
|
+
}
|
|
1393
|
+
async function openRow(entry, journal, decryptPayload) {
|
|
1394
|
+
let payload = null;
|
|
1395
|
+
try {
|
|
1396
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
1397
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
1398
|
+
} catch {
|
|
1399
|
+
payload = null;
|
|
1400
|
+
}
|
|
1401
|
+
const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
|
|
1402
|
+
const certified = distinctOpCount(journal);
|
|
1403
|
+
return {
|
|
1404
|
+
transferId: entry.transferId,
|
|
1405
|
+
kind: "open",
|
|
1406
|
+
recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
|
|
1407
|
+
coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
|
|
1408
|
+
amount: typeof payload?.amount === "string" ? payload.amount : "",
|
|
1409
|
+
legs: { certified, total: Math.max(total, certified) },
|
|
1410
|
+
deliveryPending: journal.length > 0,
|
|
1411
|
+
createdAt: entry.createdAt
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function journalOnlyRow(transferId, entries) {
|
|
1415
|
+
const certified = distinctOpCount(entries);
|
|
1416
|
+
return {
|
|
1417
|
+
transferId,
|
|
1418
|
+
kind: "open",
|
|
1419
|
+
recipient: entries[0].recipientPubkey,
|
|
1420
|
+
coinId: "",
|
|
1421
|
+
amount: "",
|
|
1422
|
+
legs: { certified, total: certified },
|
|
1423
|
+
deliveryPending: true,
|
|
1424
|
+
createdAt: 0
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
function shortfallRow(entry) {
|
|
1428
|
+
return {
|
|
1429
|
+
transferId: entry.transferId,
|
|
1430
|
+
kind: "shortfall",
|
|
1431
|
+
recipient: entry.recipient,
|
|
1432
|
+
coinId: entry.coinId,
|
|
1433
|
+
amount: entry.remainingAmount,
|
|
1434
|
+
legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
|
|
1435
|
+
deliveryPending: false,
|
|
1436
|
+
createdAt: entry.createdAt
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function distinctOpCount(entries) {
|
|
1440
|
+
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1228
1443
|
// node_modules/@noble/ciphers/utils.js
|
|
1229
1444
|
function isBytes2(a) {
|
|
1230
1445
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
@@ -3556,6 +3771,21 @@ var PaymentsFacade = class {
|
|
|
3556
3771
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3557
3772
|
this.requests = parts.requests;
|
|
3558
3773
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3774
|
+
this.heartbeat = new ConvergenceHeartbeat({
|
|
3775
|
+
now: () => this.nowMs(),
|
|
3776
|
+
onTick: () => this.trackTail(this.runConvergencePass())
|
|
3777
|
+
});
|
|
3778
|
+
this.converger = new Converger({
|
|
3779
|
+
machineDeps: this.machineDeps,
|
|
3780
|
+
stores: this.machineStores,
|
|
3781
|
+
delivery: deps.deliveryPort,
|
|
3782
|
+
reconcile: (report) => this.requests.reconcile(report),
|
|
3783
|
+
refreshView: () => this.view.delta(),
|
|
3784
|
+
replayMints: () => this.replayMints(),
|
|
3785
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id),
|
|
3786
|
+
emit: deps.emit,
|
|
3787
|
+
now: () => this.nowMs()
|
|
3788
|
+
});
|
|
3559
3789
|
}
|
|
3560
3790
|
view;
|
|
3561
3791
|
ledger;
|
|
@@ -3572,10 +3802,18 @@ var PaymentsFacade = class {
|
|
|
3572
3802
|
pendingOps = /* @__PURE__ */ new Set();
|
|
3573
3803
|
unsubscribers = [];
|
|
3574
3804
|
started = false;
|
|
3805
|
+
// §7 heartbeat: in-memory timing cell + pass runner (see convergence.ts).
|
|
3806
|
+
heartbeat;
|
|
3807
|
+
converger;
|
|
3808
|
+
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
3809
|
+
resumeFlight = new SingleFlight();
|
|
3810
|
+
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
3811
|
+
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
3575
3812
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
3576
3813
|
async start() {
|
|
3577
3814
|
if (this.started) return;
|
|
3578
3815
|
this.started = true;
|
|
3816
|
+
this.heartbeat.start();
|
|
3579
3817
|
await this.deps.session.start();
|
|
3580
3818
|
this.unsubscribers.push(
|
|
3581
3819
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
@@ -3588,20 +3826,11 @@ var PaymentsFacade = class {
|
|
|
3588
3826
|
this.trackTail(this.requests.drainIncoming());
|
|
3589
3827
|
})
|
|
3590
3828
|
);
|
|
3591
|
-
this.
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
attention: (transferId, code, detail) => {
|
|
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());
|
|
3829
|
+
const statusUnsub = this.deps.session.subscribeStatus?.((status) => {
|
|
3830
|
+
if (status === "connected") this.heartbeat.recovered();
|
|
3831
|
+
});
|
|
3832
|
+
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
3833
|
+
this.trackTail(this.runConvergencePass());
|
|
3605
3834
|
this.trackTail(this.seedHeldStates());
|
|
3606
3835
|
await this.view.fullPull().catch(() => void 0);
|
|
3607
3836
|
this.receiveLoop.start(this.deps.receivePollMs);
|
|
@@ -3609,6 +3838,7 @@ var PaymentsFacade = class {
|
|
|
3609
3838
|
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
3610
3839
|
async stop() {
|
|
3611
3840
|
this.started = false;
|
|
3841
|
+
this.heartbeat.stop();
|
|
3612
3842
|
this.receiveLoop.stop();
|
|
3613
3843
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
3614
3844
|
this.unsubscribers = [];
|
|
@@ -3634,6 +3864,10 @@ var PaymentsFacade = class {
|
|
|
3634
3864
|
history(page) {
|
|
3635
3865
|
return this.historyStore.page(page ?? {});
|
|
3636
3866
|
}
|
|
3867
|
+
/** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
|
|
3868
|
+
pendingTransfers() {
|
|
3869
|
+
return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
|
|
3870
|
+
}
|
|
3637
3871
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
3638
3872
|
send(request) {
|
|
3639
3873
|
return this.track(this.sendWithPolicy(request));
|
|
@@ -3646,6 +3880,22 @@ var PaymentsFacade = class {
|
|
|
3646
3880
|
mint(coinId, amount) {
|
|
3647
3881
|
return this.track(this.mintInner(coinId, amount));
|
|
3648
3882
|
}
|
|
3883
|
+
// §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
|
|
3884
|
+
async resumeNow() {
|
|
3885
|
+
if (!this.started) return;
|
|
3886
|
+
await this.track(this.runConvergencePass());
|
|
3887
|
+
}
|
|
3888
|
+
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
3889
|
+
runConvergencePass() {
|
|
3890
|
+
return this.resumeFlight.run(async () => {
|
|
3891
|
+
const outcome = await this.converger.convergeOnce();
|
|
3892
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
3893
|
+
this.heartbeat.settle(outcome, pending);
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
nowMs() {
|
|
3897
|
+
return (this.deps.now ?? Date.now)();
|
|
3898
|
+
}
|
|
3649
3899
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
3650
3900
|
async sendWithPolicy(request) {
|
|
3651
3901
|
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
@@ -3695,12 +3945,15 @@ var PaymentsFacade = class {
|
|
|
3695
3945
|
return recipient;
|
|
3696
3946
|
}
|
|
3697
3947
|
async runAttempt(ctx) {
|
|
3948
|
+
this.activeMoneyOps.add(ctx.transferId);
|
|
3698
3949
|
try {
|
|
3699
3950
|
const { deliveryPending } = await this.machine.run(ctx.plan);
|
|
3700
3951
|
this.settleSuccess(ctx, ctx.sourceIds);
|
|
3701
3952
|
return { kind: "success", deliveryPending };
|
|
3702
3953
|
} catch (err) {
|
|
3703
|
-
return this.disposeFailedAttempt(ctx, err);
|
|
3954
|
+
return await this.disposeFailedAttempt(ctx, err);
|
|
3955
|
+
} finally {
|
|
3956
|
+
this.activeMoneyOps.delete(ctx.transferId);
|
|
3704
3957
|
}
|
|
3705
3958
|
}
|
|
3706
3959
|
/**
|
|
@@ -3709,7 +3962,10 @@ var PaymentsFacade = class {
|
|
|
3709
3962
|
* the remainder under a NEW transferId — never the full amount.
|
|
3710
3963
|
*/
|
|
3711
3964
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
3712
|
-
|
|
3965
|
+
if (disposition.deliveryPending) {
|
|
3966
|
+
run.pendingAny = true;
|
|
3967
|
+
this.heartbeat.arm();
|
|
3968
|
+
}
|
|
3713
3969
|
const partial = disposition.entry;
|
|
3714
3970
|
run.delivered.push(...partial.committedTokenIds);
|
|
3715
3971
|
run.firstPartialId ??= partial.transferId;
|
|
@@ -3867,6 +4123,7 @@ var PaymentsFacade = class {
|
|
|
3867
4123
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
3868
4124
|
settleKeepOpen(ctx) {
|
|
3869
4125
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
4126
|
+
this.heartbeat.arm();
|
|
3870
4127
|
}
|
|
3871
4128
|
settlePartial(ctx, partial) {
|
|
3872
4129
|
this.ledger.cancel(ctx.transferId);
|
|
@@ -3903,6 +4160,7 @@ var PaymentsFacade = class {
|
|
|
3903
4160
|
for (const id of run.settledShortfalls.splice(0)) {
|
|
3904
4161
|
await this.machineStores.shortfalls.removeByKey(id).catch(() => void 0);
|
|
3905
4162
|
}
|
|
4163
|
+
if (run.pendingAny) this.heartbeat.arm();
|
|
3906
4164
|
const result = {
|
|
3907
4165
|
id: transferId,
|
|
3908
4166
|
status: run.pendingAny ? "confirmed" : "delivered",
|
|
@@ -3947,21 +4205,29 @@ var PaymentsFacade = class {
|
|
|
3947
4205
|
if (!/^(?:[0-9a-f]{2})+$/.test(coinId) || amount <= 0n) {
|
|
3948
4206
|
return { success: false, error: "coinId must be even-length lowercase hex and amount positive" };
|
|
3949
4207
|
}
|
|
3950
|
-
const engine = this.engine();
|
|
3951
4208
|
const mintId = this.newId();
|
|
3952
|
-
|
|
4209
|
+
this.activeMoneyOps.add(mintId);
|
|
4210
|
+
try {
|
|
4211
|
+
return await this.mintUnderJournal(mintId, coinId, amount);
|
|
4212
|
+
} finally {
|
|
4213
|
+
this.activeMoneyOps.delete(mintId);
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
async mintUnderJournal(mintId, coinId, amount) {
|
|
4217
|
+
const engine = this.engine();
|
|
3953
4218
|
const entry = {
|
|
3954
4219
|
mintId,
|
|
3955
4220
|
coinId,
|
|
3956
4221
|
amount: amount.toString(),
|
|
3957
4222
|
tokenId: "",
|
|
3958
|
-
createdAt:
|
|
4223
|
+
createdAt: this.nowMs()
|
|
3959
4224
|
};
|
|
3960
4225
|
await this.machineStores.mintJournal.upsert(entry);
|
|
3961
4226
|
let token;
|
|
3962
4227
|
try {
|
|
3963
|
-
token = await engine.mint(
|
|
4228
|
+
token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
|
|
3964
4229
|
} catch (err) {
|
|
4230
|
+
this.heartbeat.arm();
|
|
3965
4231
|
return { success: false, error: messageOf(err) };
|
|
3966
4232
|
}
|
|
3967
4233
|
if (token.blob.tokenId !== entry.tokenId) {
|
|
@@ -3970,6 +4236,7 @@ var PaymentsFacade = class {
|
|
|
3970
4236
|
try {
|
|
3971
4237
|
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
3972
4238
|
} catch (err) {
|
|
4239
|
+
this.heartbeat.arm();
|
|
3973
4240
|
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
3974
4241
|
}
|
|
3975
4242
|
return { success: true, tokenId: token.blob.tokenId };
|
|
@@ -3992,18 +4259,22 @@ var PaymentsFacade = class {
|
|
|
3992
4259
|
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
3993
4260
|
this.trackTail(this.view.delta());
|
|
3994
4261
|
}
|
|
4262
|
+
/** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
|
|
3995
4263
|
async replayMints() {
|
|
4264
|
+
let resolved = 0;
|
|
3996
4265
|
for (const entry of await this.machineStores.mintJournal.list()) {
|
|
4266
|
+
if (this.activeMoneyOps.has(entry.mintId)) continue;
|
|
3997
4267
|
try {
|
|
3998
|
-
await this.replayMint(entry);
|
|
4268
|
+
if (await this.replayMint(entry)) resolved += 1;
|
|
3999
4269
|
} catch {
|
|
4000
4270
|
}
|
|
4001
4271
|
}
|
|
4272
|
+
return resolved;
|
|
4002
4273
|
}
|
|
4003
4274
|
async replayMint(entry) {
|
|
4004
4275
|
if (entry.tokenId !== "" && await this.tokenInServerInventory(entry.tokenId)) {
|
|
4005
4276
|
await this.machineStores.mintJournal.removeByKey(entry.mintId);
|
|
4006
|
-
return;
|
|
4277
|
+
return true;
|
|
4007
4278
|
}
|
|
4008
4279
|
const engine = this.engine();
|
|
4009
4280
|
if (!supportsDeterministicMint(engine)) {
|
|
@@ -4011,7 +4282,7 @@ var PaymentsFacade = class {
|
|
|
4011
4282
|
transferId: entry.mintId,
|
|
4012
4283
|
code: ATTENTION_MINT_UNRESOLVED
|
|
4013
4284
|
});
|
|
4014
|
-
return;
|
|
4285
|
+
return false;
|
|
4015
4286
|
}
|
|
4016
4287
|
const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
|
|
4017
4288
|
transferId: entry.mintId
|
|
@@ -4020,6 +4291,7 @@ var PaymentsFacade = class {
|
|
|
4020
4291
|
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
4021
4292
|
}
|
|
4022
4293
|
await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
|
|
4294
|
+
return true;
|
|
4023
4295
|
}
|
|
4024
4296
|
mintParams(coinId, amount) {
|
|
4025
4297
|
return {
|
|
@@ -4045,17 +4317,6 @@ var PaymentsFacade = class {
|
|
|
4045
4317
|
newId() {
|
|
4046
4318
|
return (this.deps.newId ?? randomUUID)();
|
|
4047
4319
|
}
|
|
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
4320
|
async seedHeldStates() {
|
|
4060
4321
|
let page = await this.deps.storagePort.listInventory();
|
|
4061
4322
|
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
@@ -4069,7 +4330,7 @@ var PaymentsFacade = class {
|
|
|
4069
4330
|
toUiToken(tokenId, coinId, amount) {
|
|
4070
4331
|
const registry = this.deps.registry;
|
|
4071
4332
|
const iconUrl = registry.getIconUrl(coinId);
|
|
4072
|
-
const now =
|
|
4333
|
+
const now = this.nowMs();
|
|
4073
4334
|
return {
|
|
4074
4335
|
id: tokenId,
|
|
4075
4336
|
coinId,
|
|
@@ -4120,6 +4381,8 @@ function messageOf(err) {
|
|
|
4120
4381
|
}
|
|
4121
4382
|
export {
|
|
4122
4383
|
ATTENTION_MINT_UNRESOLVED,
|
|
4384
|
+
HEARTBEAT_CAP_MS,
|
|
4385
|
+
HEARTBEAT_SEED_MS,
|
|
4123
4386
|
MAX_RESELECT,
|
|
4124
4387
|
PaymentsFacade,
|
|
4125
4388
|
STORE_KEYS,
|