@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
package/dist/core/index.cjs
CHANGED
|
@@ -10674,14 +10674,14 @@ var IntentResume = class {
|
|
|
10674
10674
|
}
|
|
10675
10675
|
let changeOutput = null;
|
|
10676
10676
|
if (payload.split) {
|
|
10677
|
-
const
|
|
10677
|
+
const splitOpIndex2 = payload.direct.length;
|
|
10678
10678
|
let splitDelivered = false;
|
|
10679
10679
|
{
|
|
10680
10680
|
const source = await engine.decodeToken(await provider.getToken(payload.split.tokenId));
|
|
10681
10681
|
const outcome = await this.host.certifyOperation(
|
|
10682
10682
|
{
|
|
10683
10683
|
kind: "split",
|
|
10684
|
-
opIndex:
|
|
10684
|
+
opIndex: splitOpIndex2,
|
|
10685
10685
|
uiTokenId: `v2_${payload.split.tokenId}`,
|
|
10686
10686
|
genesisId: payload.split.tokenId,
|
|
10687
10687
|
sourceSdkData: "",
|
|
@@ -20785,6 +20785,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
20785
20785
|
const unsubHistory = deps.on(
|
|
20786
20786
|
"history:updated",
|
|
20787
20787
|
(entry) => {
|
|
20788
|
+
if (!("dedupKey" in entry)) return;
|
|
20788
20789
|
this._handleHistoryUpdated(entry).catch((err) => {
|
|
20789
20790
|
logger.warn(LOG_TAG2, "Error handling history:updated event:", err);
|
|
20790
20791
|
});
|
|
@@ -26054,7 +26055,10 @@ var STORE_KEYS = {
|
|
|
26054
26055
|
shortfalls: "shortfalls",
|
|
26055
26056
|
settlingLinks: "settling",
|
|
26056
26057
|
streamCursor: (s) => `cursor:${s}`,
|
|
26057
|
-
epochLatch: "epoch-latch"
|
|
26058
|
+
epochLatch: "epoch-latch",
|
|
26059
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
26060
|
+
suspectedSpent: "suspected-spent",
|
|
26061
|
+
knownSpends: "known-spends"
|
|
26058
26062
|
};
|
|
26059
26063
|
|
|
26060
26064
|
// modules/payments-v2/machine/journal.ts
|
|
@@ -26077,6 +26081,10 @@ function isValidationReject(err) {
|
|
|
26077
26081
|
if (e === null) return false;
|
|
26078
26082
|
return e.status === 422 || e.statusCode === 422 || e.code === "VALIDATION" || e.code === "VALIDATION_FAILED";
|
|
26079
26083
|
}
|
|
26084
|
+
function retryAfterMsOf(err) {
|
|
26085
|
+
const retryAfter = err !== null && typeof err === "object" ? err.retryAfter : void 0;
|
|
26086
|
+
return typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : null;
|
|
26087
|
+
}
|
|
26080
26088
|
var chainRegistry = /* @__PURE__ */ new WeakMap();
|
|
26081
26089
|
var replayRegistry = /* @__PURE__ */ new WeakMap();
|
|
26082
26090
|
function registryFor(registry, kv, storeKey, make) {
|
|
@@ -26300,7 +26308,7 @@ var TransferMachine = class {
|
|
|
26300
26308
|
}
|
|
26301
26309
|
if (cls === null) {
|
|
26302
26310
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
26303
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
26311
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
26304
26312
|
return { committed, deliveryPending };
|
|
26305
26313
|
}
|
|
26306
26314
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -26326,10 +26334,10 @@ var TransferMachine = class {
|
|
|
26326
26334
|
}
|
|
26327
26335
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
26328
26336
|
if (conflicts.length > 0) {
|
|
26329
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
26337
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
26330
26338
|
}
|
|
26331
26339
|
await this.completeIntent(r.transferId, r.payload);
|
|
26332
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
26340
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
26333
26341
|
return { deliveryPending, partial: null };
|
|
26334
26342
|
}
|
|
26335
26343
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -26511,7 +26519,7 @@ var TransferMachine = class {
|
|
|
26511
26519
|
}
|
|
26512
26520
|
return added;
|
|
26513
26521
|
}
|
|
26514
|
-
async settlePartial(r, committed, conflicts) {
|
|
26522
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
26515
26523
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
26516
26524
|
const entry = {
|
|
26517
26525
|
transferId: r.transferId,
|
|
@@ -26523,26 +26531,27 @@ var TransferMachine = class {
|
|
|
26523
26531
|
};
|
|
26524
26532
|
await this.stores.shortfalls.upsert(entry);
|
|
26525
26533
|
await this.completeIntent(r.transferId, r.payload);
|
|
26526
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
26534
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
26527
26535
|
return entry;
|
|
26528
26536
|
}
|
|
26529
|
-
closeTail(transferId, payload) {
|
|
26537
|
+
closeTail(engine, transferId, payload, committed) {
|
|
26530
26538
|
void (async () => {
|
|
26531
26539
|
try {
|
|
26532
26540
|
await this.completeIntent(transferId, payload);
|
|
26533
26541
|
} catch {
|
|
26534
26542
|
return;
|
|
26535
26543
|
}
|
|
26536
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
26544
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
26537
26545
|
})();
|
|
26538
26546
|
}
|
|
26539
26547
|
async completeIntent(transferId, payload) {
|
|
26540
26548
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
26541
26549
|
await this.deps.intents.complete(transferId, signature);
|
|
26542
26550
|
}
|
|
26543
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
26551
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
26544
26552
|
try {
|
|
26545
|
-
await this.
|
|
26553
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
26554
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
26546
26555
|
} catch {
|
|
26547
26556
|
}
|
|
26548
26557
|
try {
|
|
@@ -26550,6 +26559,26 @@ var TransferMachine = class {
|
|
|
26550
26559
|
} catch {
|
|
26551
26560
|
}
|
|
26552
26561
|
}
|
|
26562
|
+
/**
|
|
26563
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
26564
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
26565
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
26566
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
26567
|
+
*/
|
|
26568
|
+
async settledAmount(engine, coinId, committed) {
|
|
26569
|
+
let total = 0n;
|
|
26570
|
+
for (const outcome of committed) {
|
|
26571
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
26572
|
+
const token = await engine.decodeToken({
|
|
26573
|
+
v: TOKEN_BLOB_VERSION,
|
|
26574
|
+
network: 0,
|
|
26575
|
+
tokenId: "",
|
|
26576
|
+
token: outcome.recipientBlob
|
|
26577
|
+
});
|
|
26578
|
+
total += engine.balanceOf(token, coinId);
|
|
26579
|
+
}
|
|
26580
|
+
return total.toString();
|
|
26581
|
+
}
|
|
26553
26582
|
async disposeOnFailure(transferId, committed, cls) {
|
|
26554
26583
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
26555
26584
|
try {
|
|
@@ -26574,7 +26603,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
26574
26603
|
}
|
|
26575
26604
|
|
|
26576
26605
|
// modules/payments-v2/machine/resume.ts
|
|
26577
|
-
async function resumeAll(deps) {
|
|
26606
|
+
async function resumeAll(deps, options = {}) {
|
|
26578
26607
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
26579
26608
|
const stores = createMachineStores(deps.kv);
|
|
26580
26609
|
const open = await deps.intents.listOpen();
|
|
@@ -26584,7 +26613,12 @@ async function resumeAll(deps) {
|
|
|
26584
26613
|
} catch {
|
|
26585
26614
|
return report;
|
|
26586
26615
|
}
|
|
26587
|
-
await
|
|
26616
|
+
await dropStaleBackstops(
|
|
26617
|
+
stores,
|
|
26618
|
+
locals,
|
|
26619
|
+
new Set(open.map((i) => i.transferId)),
|
|
26620
|
+
options.isLocallyActive ?? (() => false)
|
|
26621
|
+
);
|
|
26588
26622
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
26589
26623
|
const ctx = {
|
|
26590
26624
|
deps,
|
|
@@ -26597,11 +26631,16 @@ async function resumeAll(deps) {
|
|
|
26597
26631
|
}
|
|
26598
26632
|
return report;
|
|
26599
26633
|
}
|
|
26600
|
-
async function
|
|
26634
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
26601
26635
|
for (const entry of locals.values()) {
|
|
26602
|
-
if (
|
|
26636
|
+
if (openIds.has(entry.transferId)) continue;
|
|
26637
|
+
if (entry.disposition !== "open") {
|
|
26603
26638
|
await stores.backstop.removeByKey(entry.transferId);
|
|
26639
|
+
continue;
|
|
26604
26640
|
}
|
|
26641
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
26642
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
26643
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
26605
26644
|
}
|
|
26606
26645
|
}
|
|
26607
26646
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -26714,6 +26753,349 @@ function validatePayload(raw) {
|
|
|
26714
26753
|
return p;
|
|
26715
26754
|
}
|
|
26716
26755
|
|
|
26756
|
+
// modules/payments-v2/convergence.ts
|
|
26757
|
+
var HEARTBEAT_SEED_MS = 5e3;
|
|
26758
|
+
var HEARTBEAT_CAP_MS = 12e4;
|
|
26759
|
+
var ConvergenceHeartbeat = class {
|
|
26760
|
+
constructor(deps) {
|
|
26761
|
+
this.deps = deps;
|
|
26762
|
+
}
|
|
26763
|
+
timer = null;
|
|
26764
|
+
delayMs = HEARTBEAT_SEED_MS;
|
|
26765
|
+
notBefore = 0;
|
|
26766
|
+
running = false;
|
|
26767
|
+
start() {
|
|
26768
|
+
this.running = true;
|
|
26769
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
26770
|
+
this.notBefore = 0;
|
|
26771
|
+
}
|
|
26772
|
+
/** §7 quiescence unchanged: a mid-backoff stop() cancels cleanly. */
|
|
26773
|
+
stop() {
|
|
26774
|
+
this.running = false;
|
|
26775
|
+
this.clearTimer();
|
|
26776
|
+
}
|
|
26777
|
+
/** Reset to seed on progress, ×2 to the cap otherwise; idle stops rescheduling. */
|
|
26778
|
+
settle(outcome, pending) {
|
|
26779
|
+
if (outcome.retryAfterMs !== null) {
|
|
26780
|
+
this.notBefore = Math.max(this.notBefore, this.deps.now() + outcome.retryAfterMs);
|
|
26781
|
+
}
|
|
26782
|
+
if (!pending) {
|
|
26783
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
26784
|
+
return;
|
|
26785
|
+
}
|
|
26786
|
+
const delay = outcome.progress ? HEARTBEAT_SEED_MS : this.delayMs;
|
|
26787
|
+
this.delayMs = Math.min(delay * 2, HEARTBEAT_CAP_MS);
|
|
26788
|
+
this.schedule(delay);
|
|
26789
|
+
}
|
|
26790
|
+
/** New pending work (keep-open park / journaled delivery / mint hold): prompt retry. */
|
|
26791
|
+
arm() {
|
|
26792
|
+
if (!this.running) return;
|
|
26793
|
+
this.delayMs = Math.min(HEARTBEAT_SEED_MS * 2, HEARTBEAT_CAP_MS);
|
|
26794
|
+
this.schedule(HEARTBEAT_SEED_MS);
|
|
26795
|
+
}
|
|
26796
|
+
/** connection:status recovery: the backoff resets; a pending tick re-fires at the seed. */
|
|
26797
|
+
recovered() {
|
|
26798
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
26799
|
+
if (this.timer !== null) this.arm();
|
|
26800
|
+
}
|
|
26801
|
+
schedule(delayMs) {
|
|
26802
|
+
this.clearTimer();
|
|
26803
|
+
if (!this.running) return;
|
|
26804
|
+
const floored = Math.max(delayMs, this.notBefore - this.deps.now());
|
|
26805
|
+
this.timer = setTimeout(() => {
|
|
26806
|
+
this.timer = null;
|
|
26807
|
+
this.deps.onTick();
|
|
26808
|
+
}, floored);
|
|
26809
|
+
}
|
|
26810
|
+
clearTimer() {
|
|
26811
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
26812
|
+
this.timer = null;
|
|
26813
|
+
}
|
|
26814
|
+
};
|
|
26815
|
+
var Converger = class {
|
|
26816
|
+
constructor(deps) {
|
|
26817
|
+
this.deps = deps;
|
|
26818
|
+
}
|
|
26819
|
+
/** Resume open intents, then drain the delivery + mint journals (all idempotent). */
|
|
26820
|
+
async convergeOnce() {
|
|
26821
|
+
let progress = false;
|
|
26822
|
+
let retryAfterMs = null;
|
|
26823
|
+
try {
|
|
26824
|
+
progress = await this.resumePass();
|
|
26825
|
+
} catch (err) {
|
|
26826
|
+
retryAfterMs = retryAfterMsOf(err);
|
|
26827
|
+
}
|
|
26828
|
+
try {
|
|
26829
|
+
const stats = await this.deps.stores.deliveryJournal.replay(this.replayDeps());
|
|
26830
|
+
progress = progress || stats.delivered > 0;
|
|
26831
|
+
} catch (err) {
|
|
26832
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
26833
|
+
}
|
|
26834
|
+
try {
|
|
26835
|
+
progress = await this.deps.replayMints() > 0 || progress;
|
|
26836
|
+
} catch (err) {
|
|
26837
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
26838
|
+
}
|
|
26839
|
+
return { progress, retryAfterMs };
|
|
26840
|
+
}
|
|
26841
|
+
/** §4 heartbeat predicate — read fresh from the §6 stores, never cached. */
|
|
26842
|
+
async pendingWork() {
|
|
26843
|
+
const [backstop, journal, mints] = await Promise.all([
|
|
26844
|
+
this.deps.stores.backstop.list(),
|
|
26845
|
+
this.deps.stores.deliveryJournal.list(),
|
|
26846
|
+
this.deps.stores.mintJournal.list()
|
|
26847
|
+
]);
|
|
26848
|
+
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
26849
|
+
}
|
|
26850
|
+
async resumePass() {
|
|
26851
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
26852
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
26853
|
+
});
|
|
26854
|
+
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
26855
|
+
await this.deps.reconcile({
|
|
26856
|
+
resumed: report.resumed,
|
|
26857
|
+
conflicted: report.conflicted,
|
|
26858
|
+
open: report.failed
|
|
26859
|
+
});
|
|
26860
|
+
if (progress) await this.deps.refreshView().catch(() => void 0);
|
|
26861
|
+
return progress;
|
|
26862
|
+
}
|
|
26863
|
+
filteredMachineDeps() {
|
|
26864
|
+
const intents = this.deps.machineDeps.intents;
|
|
26865
|
+
return {
|
|
26866
|
+
...this.deps.machineDeps,
|
|
26867
|
+
intents: {
|
|
26868
|
+
...intents,
|
|
26869
|
+
listOpen: async () => (await intents.listOpen()).filter((intent) => !this.deps.isActiveOp(intent.transferId))
|
|
26870
|
+
}
|
|
26871
|
+
};
|
|
26872
|
+
}
|
|
26873
|
+
replayDeps() {
|
|
26874
|
+
return {
|
|
26875
|
+
delivery: this.deps.delivery,
|
|
26876
|
+
now: () => this.deps.now(),
|
|
26877
|
+
attention: (transferId, code, detail) => {
|
|
26878
|
+
this.deps.emit(
|
|
26879
|
+
"transfer:attention",
|
|
26880
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
26881
|
+
);
|
|
26882
|
+
}
|
|
26883
|
+
};
|
|
26884
|
+
}
|
|
26885
|
+
};
|
|
26886
|
+
async function derivePendingTransfers(stores, decryptPayload) {
|
|
26887
|
+
const [backstop, journal, shortfalls] = await Promise.all([
|
|
26888
|
+
stores.backstop.list(),
|
|
26889
|
+
stores.deliveryJournal.list(),
|
|
26890
|
+
stores.shortfalls.list()
|
|
26891
|
+
]);
|
|
26892
|
+
const byTransfer = groupJournal(journal);
|
|
26893
|
+
const out = [];
|
|
26894
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26895
|
+
for (const entry of backstop) {
|
|
26896
|
+
if (entry.disposition !== "open") continue;
|
|
26897
|
+
seen.add(entry.transferId);
|
|
26898
|
+
out.push(await openRow(entry, byTransfer.get(entry.transferId) ?? [], decryptPayload));
|
|
26899
|
+
}
|
|
26900
|
+
for (const [transferId, entries] of byTransfer) {
|
|
26901
|
+
if (seen.has(transferId)) continue;
|
|
26902
|
+
seen.add(transferId);
|
|
26903
|
+
out.push(journalOnlyRow(transferId, entries));
|
|
26904
|
+
}
|
|
26905
|
+
for (const entry of shortfalls) {
|
|
26906
|
+
if (!seen.has(entry.transferId)) out.push(shortfallRow(entry));
|
|
26907
|
+
}
|
|
26908
|
+
return out;
|
|
26909
|
+
}
|
|
26910
|
+
function groupJournal(journal) {
|
|
26911
|
+
const byTransfer = /* @__PURE__ */ new Map();
|
|
26912
|
+
for (const entry of journal) {
|
|
26913
|
+
const mine = byTransfer.get(entry.transferId);
|
|
26914
|
+
if (mine === void 0) byTransfer.set(entry.transferId, [entry]);
|
|
26915
|
+
else mine.push(entry);
|
|
26916
|
+
}
|
|
26917
|
+
return byTransfer;
|
|
26918
|
+
}
|
|
26919
|
+
async function openRow(entry, journal, decryptPayload) {
|
|
26920
|
+
let payload = null;
|
|
26921
|
+
try {
|
|
26922
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
26923
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
26924
|
+
} catch {
|
|
26925
|
+
payload = null;
|
|
26926
|
+
}
|
|
26927
|
+
const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
|
|
26928
|
+
const certified = distinctOpCount(journal);
|
|
26929
|
+
return {
|
|
26930
|
+
transferId: entry.transferId,
|
|
26931
|
+
kind: "open",
|
|
26932
|
+
recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
|
|
26933
|
+
coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
|
|
26934
|
+
amount: typeof payload?.amount === "string" ? payload.amount : "",
|
|
26935
|
+
legs: { certified, total: Math.max(total, certified) },
|
|
26936
|
+
deliveryPending: journal.length > 0,
|
|
26937
|
+
createdAt: entry.createdAt
|
|
26938
|
+
};
|
|
26939
|
+
}
|
|
26940
|
+
function journalOnlyRow(transferId, entries) {
|
|
26941
|
+
const certified = distinctOpCount(entries);
|
|
26942
|
+
return {
|
|
26943
|
+
transferId,
|
|
26944
|
+
kind: "open",
|
|
26945
|
+
recipient: entries[0].recipientPubkey,
|
|
26946
|
+
coinId: "",
|
|
26947
|
+
amount: "",
|
|
26948
|
+
legs: { certified, total: certified },
|
|
26949
|
+
deliveryPending: true,
|
|
26950
|
+
createdAt: 0
|
|
26951
|
+
};
|
|
26952
|
+
}
|
|
26953
|
+
function shortfallRow(entry) {
|
|
26954
|
+
return {
|
|
26955
|
+
transferId: entry.transferId,
|
|
26956
|
+
kind: "shortfall",
|
|
26957
|
+
recipient: entry.recipient,
|
|
26958
|
+
coinId: entry.coinId,
|
|
26959
|
+
amount: entry.remainingAmount,
|
|
26960
|
+
legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
|
|
26961
|
+
deliveryPending: false,
|
|
26962
|
+
createdAt: entry.createdAt
|
|
26963
|
+
};
|
|
26964
|
+
}
|
|
26965
|
+
function distinctOpCount(entries) {
|
|
26966
|
+
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
26967
|
+
}
|
|
26968
|
+
|
|
26969
|
+
// modules/payments-v2/restore.ts
|
|
26970
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
26971
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
26972
|
+
async function reseedAndReset(deps) {
|
|
26973
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
26974
|
+
if (entry.disposition !== "open") continue;
|
|
26975
|
+
try {
|
|
26976
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
26977
|
+
if (entry.requiresSeedClose) {
|
|
26978
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
26979
|
+
}
|
|
26980
|
+
} catch (err) {
|
|
26981
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
26982
|
+
}
|
|
26983
|
+
}
|
|
26984
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
26985
|
+
await deps.fullRePull();
|
|
26986
|
+
}
|
|
26987
|
+
async function splitOpIndex(deps, envelope) {
|
|
26988
|
+
const payload = await deps.decryptPayload(envelope);
|
|
26989
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
26990
|
+
}
|
|
26991
|
+
|
|
26992
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
26993
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
26994
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
26995
|
+
return {
|
|
26996
|
+
id: tokenId,
|
|
26997
|
+
coinId: asset.coinId,
|
|
26998
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
26999
|
+
name: registry.getName(asset.coinId),
|
|
27000
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
27001
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
27002
|
+
amount: asset.amount,
|
|
27003
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
27004
|
+
createdAt: snapshot.createdAt,
|
|
27005
|
+
updatedAt: snapshot.updatedAt,
|
|
27006
|
+
lazy: true,
|
|
27007
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
27008
|
+
};
|
|
27009
|
+
}
|
|
27010
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
27011
|
+
const groups = /* @__PURE__ */ new Map();
|
|
27012
|
+
for (const [tokenId, entry] of entries) {
|
|
27013
|
+
const moving = isTransferring(tokenId);
|
|
27014
|
+
for (const asset of entry.assets) {
|
|
27015
|
+
let group = groups.get(asset.coinId);
|
|
27016
|
+
if (!group) {
|
|
27017
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
27018
|
+
groups.set(asset.coinId, group);
|
|
27019
|
+
}
|
|
27020
|
+
if (moving) {
|
|
27021
|
+
group.transferring += BigInt(asset.amount);
|
|
27022
|
+
group.transferringCount += 1;
|
|
27023
|
+
} else {
|
|
27024
|
+
group.total += BigInt(asset.amount);
|
|
27025
|
+
group.count += 1;
|
|
27026
|
+
}
|
|
27027
|
+
}
|
|
27028
|
+
}
|
|
27029
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
27030
|
+
}
|
|
27031
|
+
function toAsset(coinId, totals, registry) {
|
|
27032
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
27033
|
+
return {
|
|
27034
|
+
coinId,
|
|
27035
|
+
symbol: registry.getSymbol(coinId),
|
|
27036
|
+
name: registry.getName(coinId),
|
|
27037
|
+
decimals: registry.getDecimals(coinId),
|
|
27038
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
27039
|
+
totalAmount: totals.total.toString(),
|
|
27040
|
+
tokenCount: totals.count,
|
|
27041
|
+
confirmedAmount: totals.total.toString(),
|
|
27042
|
+
unconfirmedAmount: "0",
|
|
27043
|
+
confirmedTokenCount: totals.count,
|
|
27044
|
+
unconfirmedTokenCount: 0,
|
|
27045
|
+
transferringTokenCount: totals.transferringCount,
|
|
27046
|
+
transferringAmount: totals.transferring.toString(),
|
|
27047
|
+
priceUsd: null,
|
|
27048
|
+
priceEur: null,
|
|
27049
|
+
change24h: null,
|
|
27050
|
+
fiatValueUsd: null,
|
|
27051
|
+
fiatValueEur: null
|
|
27052
|
+
};
|
|
27053
|
+
}
|
|
27054
|
+
async function withPrices(raw, registry, price) {
|
|
27055
|
+
try {
|
|
27056
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
27057
|
+
const quotes = await price.getPrices(names);
|
|
27058
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
27059
|
+
} catch {
|
|
27060
|
+
return raw;
|
|
27061
|
+
}
|
|
27062
|
+
}
|
|
27063
|
+
function priceOne(asset, quote) {
|
|
27064
|
+
if (!quote) return asset;
|
|
27065
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
27066
|
+
return {
|
|
27067
|
+
...asset,
|
|
27068
|
+
priceUsd: quote.priceUsd,
|
|
27069
|
+
priceEur: quote.priceEur ?? null,
|
|
27070
|
+
change24h: quote.change24h ?? null,
|
|
27071
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
27072
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
27073
|
+
};
|
|
27074
|
+
}
|
|
27075
|
+
|
|
27076
|
+
// modules/payments-v2/machine/payload.ts
|
|
27077
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
27078
|
+
return {
|
|
27079
|
+
v: 2,
|
|
27080
|
+
recipient: recipientPubkey,
|
|
27081
|
+
coinId: request.coinId,
|
|
27082
|
+
amount: request.amount,
|
|
27083
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
27084
|
+
direct: [...spend.plan.direct],
|
|
27085
|
+
...spend.plan.split !== void 0 ? {
|
|
27086
|
+
split: {
|
|
27087
|
+
tokenId: spend.plan.split.tokenId,
|
|
27088
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
27089
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
27090
|
+
}
|
|
27091
|
+
} : {},
|
|
27092
|
+
spentStates
|
|
27093
|
+
};
|
|
27094
|
+
}
|
|
27095
|
+
function messageOf(err) {
|
|
27096
|
+
return err instanceof Error ? err.message : String(err);
|
|
27097
|
+
}
|
|
27098
|
+
|
|
26717
27099
|
// modules/payments-v2/history/History.ts
|
|
26718
27100
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
26719
27101
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
@@ -26772,13 +27154,14 @@ var History = class {
|
|
|
26772
27154
|
}
|
|
26773
27155
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
26774
27156
|
async post(build) {
|
|
27157
|
+
const record = build();
|
|
26775
27158
|
try {
|
|
26776
|
-
await this.deps.client.postHistory([
|
|
27159
|
+
await this.deps.client.postHistory([record]);
|
|
26777
27160
|
} catch (err) {
|
|
26778
27161
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
26779
27162
|
return;
|
|
26780
27163
|
}
|
|
26781
|
-
this.deps.emit("history:updated");
|
|
27164
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
26782
27165
|
}
|
|
26783
27166
|
ts(timestamp) {
|
|
26784
27167
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -26835,93 +27218,9 @@ var History = class {
|
|
|
26835
27218
|
}
|
|
26836
27219
|
};
|
|
26837
27220
|
|
|
26838
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
26839
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
26840
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
26841
|
-
return {
|
|
26842
|
-
id: tokenId,
|
|
26843
|
-
coinId: asset.coinId,
|
|
26844
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
26845
|
-
name: registry.getName(asset.coinId),
|
|
26846
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
26847
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
26848
|
-
amount: asset.amount,
|
|
26849
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
26850
|
-
createdAt: snapshot.createdAt,
|
|
26851
|
-
updatedAt: snapshot.updatedAt,
|
|
26852
|
-
lazy: true,
|
|
26853
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
26854
|
-
};
|
|
26855
|
-
}
|
|
26856
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
26857
|
-
const groups = /* @__PURE__ */ new Map();
|
|
26858
|
-
for (const [tokenId, entry] of entries) {
|
|
26859
|
-
const moving = isTransferring(tokenId);
|
|
26860
|
-
for (const asset of entry.assets) {
|
|
26861
|
-
let group = groups.get(asset.coinId);
|
|
26862
|
-
if (!group) {
|
|
26863
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
26864
|
-
groups.set(asset.coinId, group);
|
|
26865
|
-
}
|
|
26866
|
-
if (moving) {
|
|
26867
|
-
group.transferring += BigInt(asset.amount);
|
|
26868
|
-
group.transferringCount += 1;
|
|
26869
|
-
} else {
|
|
26870
|
-
group.total += BigInt(asset.amount);
|
|
26871
|
-
group.count += 1;
|
|
26872
|
-
}
|
|
26873
|
-
}
|
|
26874
|
-
}
|
|
26875
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
26876
|
-
}
|
|
26877
|
-
function toAsset(coinId, totals, registry) {
|
|
26878
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
26879
|
-
return {
|
|
26880
|
-
coinId,
|
|
26881
|
-
symbol: registry.getSymbol(coinId),
|
|
26882
|
-
name: registry.getName(coinId),
|
|
26883
|
-
decimals: registry.getDecimals(coinId),
|
|
26884
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
26885
|
-
totalAmount: totals.total.toString(),
|
|
26886
|
-
tokenCount: totals.count,
|
|
26887
|
-
confirmedAmount: totals.total.toString(),
|
|
26888
|
-
unconfirmedAmount: "0",
|
|
26889
|
-
confirmedTokenCount: totals.count,
|
|
26890
|
-
unconfirmedTokenCount: 0,
|
|
26891
|
-
transferringTokenCount: totals.transferringCount,
|
|
26892
|
-
transferringAmount: totals.transferring.toString(),
|
|
26893
|
-
priceUsd: null,
|
|
26894
|
-
priceEur: null,
|
|
26895
|
-
change24h: null,
|
|
26896
|
-
fiatValueUsd: null,
|
|
26897
|
-
fiatValueEur: null
|
|
26898
|
-
};
|
|
26899
|
-
}
|
|
26900
|
-
async function withPrices(raw, registry, price) {
|
|
26901
|
-
try {
|
|
26902
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
26903
|
-
const quotes = await price.getPrices(names);
|
|
26904
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
26905
|
-
} catch {
|
|
26906
|
-
return raw;
|
|
26907
|
-
}
|
|
26908
|
-
}
|
|
26909
|
-
function priceOne(asset, quote) {
|
|
26910
|
-
if (!quote) return asset;
|
|
26911
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
26912
|
-
return {
|
|
26913
|
-
...asset,
|
|
26914
|
-
priceUsd: quote.priceUsd,
|
|
26915
|
-
priceEur: quote.priceEur ?? null,
|
|
26916
|
-
change24h: quote.change24h ?? null,
|
|
26917
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
26918
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
26919
|
-
};
|
|
26920
|
-
}
|
|
26921
|
-
|
|
26922
27221
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
26923
|
-
var SUSPECTED_KEY =
|
|
26924
|
-
var KNOWN_SPENDS_KEY =
|
|
27222
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
27223
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
26925
27224
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
26926
27225
|
function stateKey(tokenId, stateHash) {
|
|
26927
27226
|
return `${tokenId}:${stateHash}`;
|
|
@@ -27225,19 +27524,24 @@ var Receive = class {
|
|
|
27225
27524
|
const engine = deps.engine();
|
|
27226
27525
|
const stored = [];
|
|
27227
27526
|
const pending = [];
|
|
27228
|
-
|
|
27527
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
27229
27528
|
try {
|
|
27230
|
-
epoch = deps.syncEpoch();
|
|
27231
27529
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
27232
|
-
|
|
27233
|
-
for
|
|
27234
|
-
await
|
|
27235
|
-
|
|
27530
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
27531
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
27532
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
27533
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
27534
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
27535
|
+
}
|
|
27536
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
27537
|
+
const served = deps.delivery.incomingEpoch();
|
|
27538
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
27539
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
27540
|
+
basis = null;
|
|
27236
27541
|
}
|
|
27237
|
-
await flushAcks(deps, pending, epoch);
|
|
27238
27542
|
} catch (err) {
|
|
27239
27543
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
27240
|
-
await flushAcks(deps, pending,
|
|
27544
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
27241
27545
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
27242
27546
|
});
|
|
27243
27547
|
}
|
|
@@ -27310,7 +27614,7 @@ async function announce(deps, entry, record) {
|
|
|
27310
27614
|
receivedAt
|
|
27311
27615
|
};
|
|
27312
27616
|
}
|
|
27313
|
-
async function flushAcks(deps, pending,
|
|
27617
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
27314
27618
|
let lastAcked = null;
|
|
27315
27619
|
try {
|
|
27316
27620
|
while (pending.length > 0) {
|
|
@@ -27321,7 +27625,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
27321
27625
|
}
|
|
27322
27626
|
} finally {
|
|
27323
27627
|
if (lastAcked !== null) {
|
|
27324
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
27628
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
27325
27629
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
27326
27630
|
}
|
|
27327
27631
|
}
|
|
@@ -27422,9 +27726,19 @@ var Requests = class {
|
|
|
27422
27726
|
}
|
|
27423
27727
|
});
|
|
27424
27728
|
}
|
|
27729
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
27425
27730
|
writeLink(requestId, transferId, committed) {
|
|
27426
27731
|
return this.mutateJournal((m) => {
|
|
27427
|
-
|
|
27732
|
+
const prev = m.get(requestId);
|
|
27733
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
27734
|
+
const next = {
|
|
27735
|
+
requestId,
|
|
27736
|
+
transferId,
|
|
27737
|
+
committed: committed || same && prev.committed,
|
|
27738
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
27739
|
+
};
|
|
27740
|
+
if (same && prev.committed === next.committed) return false;
|
|
27741
|
+
m.set(requestId, next);
|
|
27428
27742
|
return true;
|
|
27429
27743
|
});
|
|
27430
27744
|
}
|
|
@@ -27577,23 +27891,46 @@ var Requests = class {
|
|
|
27577
27891
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
27578
27892
|
if (transferId !== void 0) {
|
|
27579
27893
|
const committed = error instanceof PartialSendConflictError;
|
|
27580
|
-
await this.
|
|
27581
|
-
this.setStatus(id, "settling");
|
|
27894
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
27582
27895
|
} else {
|
|
27583
27896
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
27584
27897
|
this.setStatus(id, "paid");
|
|
27585
27898
|
}
|
|
27586
27899
|
} else {
|
|
27587
|
-
await this.
|
|
27588
|
-
this.setStatus(id, "pending");
|
|
27900
|
+
await this.revertPayable(id);
|
|
27589
27901
|
}
|
|
27590
27902
|
throw error;
|
|
27591
27903
|
}
|
|
27592
|
-
this.
|
|
27593
|
-
await this.respondPaid(id, result.id);
|
|
27594
|
-
await this.clearLink(id);
|
|
27904
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
27595
27905
|
return result;
|
|
27596
27906
|
}
|
|
27907
|
+
/**
|
|
27908
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
27909
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
27910
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
27911
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
27912
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
27913
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
27914
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
27915
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
27916
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
27917
|
+
*/
|
|
27918
|
+
async settle(requestId, transferId, opts) {
|
|
27919
|
+
let journalDown = false;
|
|
27920
|
+
try {
|
|
27921
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
27922
|
+
} catch (err) {
|
|
27923
|
+
if (!opts.respond) throw err;
|
|
27924
|
+
journalDown = true;
|
|
27925
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
27926
|
+
}
|
|
27927
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
27928
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
27929
|
+
return;
|
|
27930
|
+
}
|
|
27931
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
27932
|
+
this.setStatus(requestId, "paid");
|
|
27933
|
+
}
|
|
27597
27934
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
27598
27935
|
async respondPaid(id, transferId) {
|
|
27599
27936
|
try {
|
|
@@ -27629,32 +27966,32 @@ var Requests = class {
|
|
|
27629
27966
|
let aborted = null;
|
|
27630
27967
|
for (const [requestId, link] of [...this.journal]) {
|
|
27631
27968
|
if (link.committed || resumed.has(link.transferId)) {
|
|
27632
|
-
await this.resolvePaid(requestId, link
|
|
27969
|
+
await this.resolvePaid(requestId, link);
|
|
27633
27970
|
} else if (conflicted.has(link.transferId)) {
|
|
27634
27971
|
await this.revertPayable(requestId);
|
|
27635
27972
|
} else if (open.has(link.transferId)) {
|
|
27636
27973
|
} else {
|
|
27637
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
27974
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
27638
27975
|
}
|
|
27639
27976
|
}
|
|
27640
27977
|
}
|
|
27641
27978
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
27642
27979
|
// a probe failure defers the link to the next session.
|
|
27643
|
-
async reconcileUnaccounted(requestId,
|
|
27980
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
27644
27981
|
try {
|
|
27645
27982
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
27646
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
27647
|
-
else await this.resolvePaid(requestId,
|
|
27983
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
27984
|
+
else await this.resolvePaid(requestId, link);
|
|
27648
27985
|
} catch (err) {
|
|
27649
27986
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
27650
27987
|
}
|
|
27651
27988
|
return aborted;
|
|
27652
27989
|
}
|
|
27653
|
-
|
|
27654
|
-
|
|
27655
|
-
|
|
27656
|
-
this.setStatus(requestId, "paid");
|
|
27990
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
27991
|
+
resolvePaid(requestId, link) {
|
|
27992
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
27657
27993
|
}
|
|
27994
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
27658
27995
|
async revertPayable(requestId) {
|
|
27659
27996
|
await this.clearLink(requestId);
|
|
27660
27997
|
this.setStatus(requestId, "pending");
|
|
@@ -27978,11 +28315,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
27978
28315
|
client: deps.client,
|
|
27979
28316
|
fieldKey: deps.fieldKey,
|
|
27980
28317
|
registry: deps.registry,
|
|
27981
|
-
emit: (event) => deps.emit(event,
|
|
28318
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
27982
28319
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
27983
28320
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
27984
28321
|
});
|
|
27985
28322
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
28323
|
+
const machineStores = createMachineStores(deps.kv);
|
|
27986
28324
|
const heldStates = /* @__PURE__ */ new Map();
|
|
27987
28325
|
return {
|
|
27988
28326
|
ownPubkeyBytes,
|
|
@@ -27990,12 +28328,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
27990
28328
|
ledger,
|
|
27991
28329
|
queue,
|
|
27992
28330
|
historyStore,
|
|
27993
|
-
machineStores
|
|
28331
|
+
machineStores,
|
|
27994
28332
|
machineDeps,
|
|
27995
28333
|
machine: new TransferMachine(machineDeps),
|
|
27996
28334
|
heldStates,
|
|
27997
28335
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
27998
|
-
requests: buildRequests(deps, hooks)
|
|
28336
|
+
requests: buildRequests(deps, hooks),
|
|
28337
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
28338
|
+
};
|
|
28339
|
+
}
|
|
28340
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
28341
|
+
return {
|
|
28342
|
+
stores: machineStores,
|
|
28343
|
+
kv: deps.kv,
|
|
28344
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
28345
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
28346
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
28347
|
+
fullRePull: () => view.onEpochReset(),
|
|
28348
|
+
attention: (transferId, code, detail) => {
|
|
28349
|
+
deps.emit(
|
|
28350
|
+
"transfer:attention",
|
|
28351
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
28352
|
+
);
|
|
28353
|
+
}
|
|
27999
28354
|
};
|
|
28000
28355
|
}
|
|
28001
28356
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -28017,11 +28372,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
28017
28372
|
ownPubkey: hexToBytes4(deps.ownPubkey),
|
|
28018
28373
|
emit: deps.emit,
|
|
28019
28374
|
now: deps.now ?? Date.now,
|
|
28020
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
28375
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
28021
28376
|
await historyStore.recordSent({
|
|
28022
28377
|
transferId,
|
|
28023
28378
|
coinId: payload.coinId,
|
|
28024
|
-
|
|
28379
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
28380
|
+
// recipient blobs), never payload.amount — the plan.
|
|
28381
|
+
amount: committedAmount,
|
|
28025
28382
|
recipientPubkey: payload.recipient,
|
|
28026
28383
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
28027
28384
|
});
|
|
@@ -28048,7 +28405,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
28048
28405
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
28049
28406
|
);
|
|
28050
28407
|
},
|
|
28051
|
-
syncEpoch: deps.syncEpoch
|
|
28408
|
+
syncEpoch: deps.syncEpoch,
|
|
28052
28409
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
28053
28410
|
});
|
|
28054
28411
|
}
|
|
@@ -28101,8 +28458,24 @@ var PaymentsFacade = class {
|
|
|
28101
28458
|
this.receiveLoop = parts.receiveLoop;
|
|
28102
28459
|
this.heldStates = parts.heldStates;
|
|
28103
28460
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
28461
|
+
this.restoreDeps = parts.restoreDeps;
|
|
28104
28462
|
this.requests = parts.requests;
|
|
28105
28463
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
28464
|
+
this.heartbeat = new ConvergenceHeartbeat({
|
|
28465
|
+
now: () => this.nowMs(),
|
|
28466
|
+
onTick: () => this.trackTail(this.runConvergencePass())
|
|
28467
|
+
});
|
|
28468
|
+
this.converger = new Converger({
|
|
28469
|
+
machineDeps: this.machineDeps,
|
|
28470
|
+
stores: this.machineStores,
|
|
28471
|
+
delivery: deps.deliveryPort,
|
|
28472
|
+
reconcile: (report) => this.requests.reconcile(report),
|
|
28473
|
+
refreshView: () => this.view.delta(),
|
|
28474
|
+
replayMints: () => this.replayMints(),
|
|
28475
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id),
|
|
28476
|
+
emit: deps.emit,
|
|
28477
|
+
now: () => this.nowMs()
|
|
28478
|
+
});
|
|
28106
28479
|
}
|
|
28107
28480
|
view;
|
|
28108
28481
|
ledger;
|
|
@@ -28114,17 +28487,28 @@ var PaymentsFacade = class {
|
|
|
28114
28487
|
receiveLoop;
|
|
28115
28488
|
heldStates;
|
|
28116
28489
|
ownPubkeyBytes;
|
|
28490
|
+
restoreDeps;
|
|
28117
28491
|
requests;
|
|
28118
28492
|
currentEngine = null;
|
|
28119
28493
|
pendingOps = /* @__PURE__ */ new Set();
|
|
28120
28494
|
unsubscribers = [];
|
|
28121
28495
|
started = false;
|
|
28496
|
+
// §7 heartbeat: in-memory timing cell + pass runner (see convergence.ts).
|
|
28497
|
+
heartbeat;
|
|
28498
|
+
converger;
|
|
28499
|
+
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
28500
|
+
resumeFlight = new SingleFlight();
|
|
28501
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
28502
|
+
passChain = new SerialChain();
|
|
28503
|
+
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
28504
|
+
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
28122
28505
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
28123
28506
|
async start() {
|
|
28124
28507
|
if (this.started) return;
|
|
28125
28508
|
this.started = true;
|
|
28126
|
-
|
|
28509
|
+
this.heartbeat.start();
|
|
28127
28510
|
this.unsubscribers.push(
|
|
28511
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
28128
28512
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
28129
28513
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
28130
28514
|
}),
|
|
@@ -28135,20 +28519,12 @@ var PaymentsFacade = class {
|
|
|
28135
28519
|
this.trackTail(this.requests.drainIncoming());
|
|
28136
28520
|
})
|
|
28137
28521
|
);
|
|
28138
|
-
this.
|
|
28139
|
-
|
|
28140
|
-
|
|
28141
|
-
|
|
28142
|
-
|
|
28143
|
-
|
|
28144
|
-
this.deps.emit(
|
|
28145
|
-
"transfer:attention",
|
|
28146
|
-
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
28147
|
-
);
|
|
28148
|
-
}
|
|
28149
|
-
})
|
|
28150
|
-
);
|
|
28151
|
-
this.trackTail(this.replayMints());
|
|
28522
|
+
const statusUnsub = this.deps.session.subscribeStatus?.((status) => {
|
|
28523
|
+
if (status === "connected") this.heartbeat.recovered();
|
|
28524
|
+
});
|
|
28525
|
+
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
28526
|
+
await this.deps.session.start();
|
|
28527
|
+
this.trackTail(this.runConvergencePass());
|
|
28152
28528
|
this.trackTail(this.seedHeldStates());
|
|
28153
28529
|
await this.view.fullPull().catch(() => void 0);
|
|
28154
28530
|
this.receiveLoop.start(this.deps.receivePollMs);
|
|
@@ -28156,6 +28532,7 @@ var PaymentsFacade = class {
|
|
|
28156
28532
|
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
28157
28533
|
async stop() {
|
|
28158
28534
|
this.started = false;
|
|
28535
|
+
this.heartbeat.stop();
|
|
28159
28536
|
this.receiveLoop.stop();
|
|
28160
28537
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
28161
28538
|
this.unsubscribers = [];
|
|
@@ -28181,9 +28558,13 @@ var PaymentsFacade = class {
|
|
|
28181
28558
|
history(page) {
|
|
28182
28559
|
return this.historyStore.page(page ?? {});
|
|
28183
28560
|
}
|
|
28561
|
+
/** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
|
|
28562
|
+
pendingTransfers() {
|
|
28563
|
+
return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
|
|
28564
|
+
}
|
|
28184
28565
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
28185
28566
|
send(request) {
|
|
28186
|
-
return this.track(this.
|
|
28567
|
+
return this.track(this.sendOutcome(request));
|
|
28187
28568
|
}
|
|
28188
28569
|
async receive() {
|
|
28189
28570
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -28193,9 +28574,32 @@ var PaymentsFacade = class {
|
|
|
28193
28574
|
mint(coinId, amount) {
|
|
28194
28575
|
return this.track(this.mintInner(coinId, amount));
|
|
28195
28576
|
}
|
|
28577
|
+
// §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
|
|
28578
|
+
async resumeNow() {
|
|
28579
|
+
if (!this.started) return;
|
|
28580
|
+
await this.track(this.runConvergencePass());
|
|
28581
|
+
}
|
|
28582
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
28583
|
+
async handleEpochChange(_newEpoch) {
|
|
28584
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
28585
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
28586
|
+
}
|
|
28587
|
+
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
28588
|
+
runConvergencePass() {
|
|
28589
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
28590
|
+
}
|
|
28591
|
+
async convergeBody() {
|
|
28592
|
+
const outcome = await this.converger.convergeOnce();
|
|
28593
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
28594
|
+
this.heartbeat.settle(outcome, pending);
|
|
28595
|
+
}
|
|
28596
|
+
nowMs() {
|
|
28597
|
+
return (this.deps.now ?? Date.now)();
|
|
28598
|
+
}
|
|
28196
28599
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
28197
|
-
|
|
28198
|
-
|
|
28600
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
28601
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
28602
|
+
async sendOutcome(request) {
|
|
28199
28603
|
const run = {
|
|
28200
28604
|
amount: request.amount,
|
|
28201
28605
|
delivered: [],
|
|
@@ -28203,12 +28607,36 @@ var PaymentsFacade = class {
|
|
|
28203
28607
|
tokenTransfers: [],
|
|
28204
28608
|
settledShortfalls: [],
|
|
28205
28609
|
firstPartialId: void 0,
|
|
28206
|
-
pendingAny: false
|
|
28610
|
+
pendingAny: false,
|
|
28611
|
+
lastTransferId: ""
|
|
28207
28612
|
};
|
|
28613
|
+
try {
|
|
28614
|
+
return await this.sendWithPolicy(request, run);
|
|
28615
|
+
} catch (err) {
|
|
28616
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
28617
|
+
throw err;
|
|
28618
|
+
}
|
|
28619
|
+
}
|
|
28620
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
28621
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
28622
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
28623
|
+
emitCleanFailure(err, transferId) {
|
|
28624
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
28625
|
+
const failed = {
|
|
28626
|
+
id: transferId,
|
|
28627
|
+
status: "failed",
|
|
28628
|
+
tokens: [],
|
|
28629
|
+
tokenTransfers: [],
|
|
28630
|
+
error: messageOf(err)
|
|
28631
|
+
};
|
|
28632
|
+
this.deps.emit("transfer:updated", failed);
|
|
28633
|
+
}
|
|
28634
|
+
async sendWithPolicy(request, run) {
|
|
28635
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
28208
28636
|
for (let attempt = 0; ; attempt++) {
|
|
28209
28637
|
let ctx;
|
|
28210
28638
|
try {
|
|
28211
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
28639
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
28212
28640
|
} catch (err) {
|
|
28213
28641
|
throw this.partialize(err, run);
|
|
28214
28642
|
}
|
|
@@ -28242,21 +28670,24 @@ var PaymentsFacade = class {
|
|
|
28242
28670
|
return recipient;
|
|
28243
28671
|
}
|
|
28244
28672
|
async runAttempt(ctx) {
|
|
28673
|
+
this.activeMoneyOps.add(ctx.transferId);
|
|
28245
28674
|
try {
|
|
28246
28675
|
const { deliveryPending } = await this.machine.run(ctx.plan);
|
|
28247
28676
|
this.settleSuccess(ctx, ctx.sourceIds);
|
|
28248
28677
|
return { kind: "success", deliveryPending };
|
|
28249
28678
|
} catch (err) {
|
|
28250
|
-
return this.disposeFailedAttempt(ctx, err);
|
|
28679
|
+
return await this.disposeFailedAttempt(ctx, err);
|
|
28680
|
+
} finally {
|
|
28681
|
+
this.activeMoneyOps.delete(ctx.transferId);
|
|
28251
28682
|
}
|
|
28252
28683
|
}
|
|
28253
|
-
/**
|
|
28254
|
-
*
|
|
28255
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
28256
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
28257
|
-
*/
|
|
28684
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
28685
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
28258
28686
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
28259
|
-
|
|
28687
|
+
if (disposition.deliveryPending) {
|
|
28688
|
+
run.pendingAny = true;
|
|
28689
|
+
this.heartbeat.arm();
|
|
28690
|
+
}
|
|
28260
28691
|
const partial = disposition.entry;
|
|
28261
28692
|
run.delivered.push(...partial.committedTokenIds);
|
|
28262
28693
|
run.firstPartialId ??= partial.transferId;
|
|
@@ -28277,11 +28708,8 @@ var PaymentsFacade = class {
|
|
|
28277
28708
|
run.amount = partial.remainingAmount;
|
|
28278
28709
|
return null;
|
|
28279
28710
|
}
|
|
28280
|
-
/**
|
|
28281
|
-
*
|
|
28282
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
28283
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
28284
|
-
*/
|
|
28711
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
28712
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
28285
28713
|
async disposeFailedAttempt(ctx, err) {
|
|
28286
28714
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
28287
28715
|
this.settleKeepOpen(ctx);
|
|
@@ -28330,8 +28758,9 @@ var PaymentsFacade = class {
|
|
|
28330
28758
|
}
|
|
28331
28759
|
return { kind: "rethrow", error: err };
|
|
28332
28760
|
}
|
|
28333
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
28761
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
28334
28762
|
const transferId = this.newId();
|
|
28763
|
+
run.lastTransferId = transferId;
|
|
28335
28764
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
28336
28765
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
28337
28766
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -28414,6 +28843,7 @@ var PaymentsFacade = class {
|
|
|
28414
28843
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
28415
28844
|
settleKeepOpen(ctx) {
|
|
28416
28845
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
28846
|
+
this.heartbeat.arm();
|
|
28417
28847
|
}
|
|
28418
28848
|
settlePartial(ctx, partial) {
|
|
28419
28849
|
this.ledger.cancel(ctx.transferId);
|
|
@@ -28450,6 +28880,7 @@ var PaymentsFacade = class {
|
|
|
28450
28880
|
for (const id of run.settledShortfalls.splice(0)) {
|
|
28451
28881
|
await this.machineStores.shortfalls.removeByKey(id).catch(() => void 0);
|
|
28452
28882
|
}
|
|
28883
|
+
if (run.pendingAny) this.heartbeat.arm();
|
|
28453
28884
|
const result = {
|
|
28454
28885
|
id: transferId,
|
|
28455
28886
|
status: run.pendingAny ? "confirmed" : "delivered",
|
|
@@ -28461,11 +28892,7 @@ var PaymentsFacade = class {
|
|
|
28461
28892
|
this.deps.emit("transfer:updated", result);
|
|
28462
28893
|
return result;
|
|
28463
28894
|
}
|
|
28464
|
-
/**
|
|
28465
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
28466
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
28467
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
28468
|
-
*/
|
|
28895
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
28469
28896
|
partialize(err, run) {
|
|
28470
28897
|
if (run.delivered.length === 0) return err;
|
|
28471
28898
|
return new PartialSendConflictError(
|
|
@@ -28494,21 +28921,29 @@ var PaymentsFacade = class {
|
|
|
28494
28921
|
if (!/^(?:[0-9a-f]{2})+$/.test(coinId) || amount <= 0n) {
|
|
28495
28922
|
return { success: false, error: "coinId must be even-length lowercase hex and amount positive" };
|
|
28496
28923
|
}
|
|
28497
|
-
const engine = this.engine();
|
|
28498
28924
|
const mintId = this.newId();
|
|
28499
|
-
|
|
28925
|
+
this.activeMoneyOps.add(mintId);
|
|
28926
|
+
try {
|
|
28927
|
+
return await this.mintUnderJournal(mintId, coinId, amount);
|
|
28928
|
+
} finally {
|
|
28929
|
+
this.activeMoneyOps.delete(mintId);
|
|
28930
|
+
}
|
|
28931
|
+
}
|
|
28932
|
+
async mintUnderJournal(mintId, coinId, amount) {
|
|
28933
|
+
const engine = this.engine();
|
|
28500
28934
|
const entry = {
|
|
28501
28935
|
mintId,
|
|
28502
28936
|
coinId,
|
|
28503
28937
|
amount: amount.toString(),
|
|
28504
28938
|
tokenId: "",
|
|
28505
|
-
createdAt:
|
|
28939
|
+
createdAt: this.nowMs()
|
|
28506
28940
|
};
|
|
28507
28941
|
await this.machineStores.mintJournal.upsert(entry);
|
|
28508
28942
|
let token;
|
|
28509
28943
|
try {
|
|
28510
|
-
token = await engine.mint(
|
|
28944
|
+
token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
|
|
28511
28945
|
} catch (err) {
|
|
28946
|
+
this.heartbeat.arm();
|
|
28512
28947
|
return { success: false, error: messageOf(err) };
|
|
28513
28948
|
}
|
|
28514
28949
|
if (token.blob.tokenId !== entry.tokenId) {
|
|
@@ -28517,6 +28952,7 @@ var PaymentsFacade = class {
|
|
|
28517
28952
|
try {
|
|
28518
28953
|
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
28519
28954
|
} catch (err) {
|
|
28955
|
+
this.heartbeat.arm();
|
|
28520
28956
|
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
28521
28957
|
}
|
|
28522
28958
|
return { success: true, tokenId: token.blob.tokenId };
|
|
@@ -28539,18 +28975,22 @@ var PaymentsFacade = class {
|
|
|
28539
28975
|
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
28540
28976
|
this.trackTail(this.view.delta());
|
|
28541
28977
|
}
|
|
28978
|
+
/** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
|
|
28542
28979
|
async replayMints() {
|
|
28980
|
+
let resolved = 0;
|
|
28543
28981
|
for (const entry of await this.machineStores.mintJournal.list()) {
|
|
28982
|
+
if (this.activeMoneyOps.has(entry.mintId)) continue;
|
|
28544
28983
|
try {
|
|
28545
|
-
await this.replayMint(entry);
|
|
28984
|
+
if (await this.replayMint(entry)) resolved += 1;
|
|
28546
28985
|
} catch {
|
|
28547
28986
|
}
|
|
28548
28987
|
}
|
|
28988
|
+
return resolved;
|
|
28549
28989
|
}
|
|
28550
28990
|
async replayMint(entry) {
|
|
28551
28991
|
if (entry.tokenId !== "" && await this.tokenInServerInventory(entry.tokenId)) {
|
|
28552
28992
|
await this.machineStores.mintJournal.removeByKey(entry.mintId);
|
|
28553
|
-
return;
|
|
28993
|
+
return true;
|
|
28554
28994
|
}
|
|
28555
28995
|
const engine = this.engine();
|
|
28556
28996
|
if (!supportsDeterministicMint(engine)) {
|
|
@@ -28558,7 +28998,7 @@ var PaymentsFacade = class {
|
|
|
28558
28998
|
transferId: entry.mintId,
|
|
28559
28999
|
code: ATTENTION_MINT_UNRESOLVED
|
|
28560
29000
|
});
|
|
28561
|
-
return;
|
|
29001
|
+
return false;
|
|
28562
29002
|
}
|
|
28563
29003
|
const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
|
|
28564
29004
|
transferId: entry.mintId
|
|
@@ -28567,6 +29007,7 @@ var PaymentsFacade = class {
|
|
|
28567
29007
|
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
28568
29008
|
}
|
|
28569
29009
|
await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
|
|
29010
|
+
return true;
|
|
28570
29011
|
}
|
|
28571
29012
|
mintParams(coinId, amount) {
|
|
28572
29013
|
return {
|
|
@@ -28592,17 +29033,6 @@ var PaymentsFacade = class {
|
|
|
28592
29033
|
newId() {
|
|
28593
29034
|
return (this.deps.newId ?? randomUUID)();
|
|
28594
29035
|
}
|
|
28595
|
-
async runResume() {
|
|
28596
|
-
const report = await resumeAll(this.machineDeps);
|
|
28597
|
-
await this.requests.reconcile({
|
|
28598
|
-
resumed: report.resumed,
|
|
28599
|
-
conflicted: report.conflicted,
|
|
28600
|
-
open: report.failed
|
|
28601
|
-
});
|
|
28602
|
-
if (report.resumed.length > 0 || report.conflicted.length > 0) {
|
|
28603
|
-
await this.view.delta().catch(() => void 0);
|
|
28604
|
-
}
|
|
28605
|
-
}
|
|
28606
29036
|
async seedHeldStates() {
|
|
28607
29037
|
let page = await this.deps.storagePort.listInventory();
|
|
28608
29038
|
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
@@ -28614,22 +29044,14 @@ var PaymentsFacade = class {
|
|
|
28614
29044
|
}
|
|
28615
29045
|
}
|
|
28616
29046
|
toUiToken(tokenId, coinId, amount) {
|
|
28617
|
-
const
|
|
28618
|
-
|
|
28619
|
-
|
|
28620
|
-
|
|
28621
|
-
|
|
28622
|
-
|
|
28623
|
-
|
|
28624
|
-
|
|
28625
|
-
decimals: registry.getDecimals(coinId),
|
|
28626
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
28627
|
-
amount: amount.toString(),
|
|
28628
|
-
status: "transferring",
|
|
28629
|
-
createdAt: now,
|
|
28630
|
-
updatedAt: now,
|
|
28631
|
-
lazy: true
|
|
28632
|
-
};
|
|
29047
|
+
const now = this.nowMs();
|
|
29048
|
+
return toToken(
|
|
29049
|
+
tokenId,
|
|
29050
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
29051
|
+
{ coinId, amount: amount.toString() },
|
|
29052
|
+
this.deps.registry,
|
|
29053
|
+
{ transferring: true, suspectedSpent: false }
|
|
29054
|
+
);
|
|
28633
29055
|
}
|
|
28634
29056
|
track(op) {
|
|
28635
29057
|
const settled = op.then(
|
|
@@ -28644,27 +29066,6 @@ var PaymentsFacade = class {
|
|
|
28644
29066
|
this.track(op).catch(() => void 0);
|
|
28645
29067
|
}
|
|
28646
29068
|
};
|
|
28647
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
28648
|
-
return {
|
|
28649
|
-
v: 2,
|
|
28650
|
-
recipient: recipientPubkey,
|
|
28651
|
-
coinId: request.coinId,
|
|
28652
|
-
amount: request.amount,
|
|
28653
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
28654
|
-
direct: [...spend.plan.direct],
|
|
28655
|
-
...spend.plan.split !== void 0 ? {
|
|
28656
|
-
split: {
|
|
28657
|
-
tokenId: spend.plan.split.tokenId,
|
|
28658
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
28659
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
28660
|
-
}
|
|
28661
|
-
} : {},
|
|
28662
|
-
spentStates
|
|
28663
|
-
};
|
|
28664
|
-
}
|
|
28665
|
-
function messageOf(err) {
|
|
28666
|
-
return err instanceof Error ? err.message : String(err);
|
|
28667
|
-
}
|
|
28668
29069
|
|
|
28669
29070
|
// impl/wallet-api-v2/http.ts
|
|
28670
29071
|
var WalletApiHttpError = class extends Error {
|
|
@@ -29010,7 +29411,9 @@ var WalletApiSession = class {
|
|
|
29010
29411
|
epochLatch = null;
|
|
29011
29412
|
reseedChain = Promise.resolve();
|
|
29012
29413
|
reseedsPending = 0;
|
|
29414
|
+
epochHandlers = /* @__PURE__ */ new Set();
|
|
29013
29415
|
handlers = /* @__PURE__ */ new Map();
|
|
29416
|
+
statusHandlers = /* @__PURE__ */ new Set();
|
|
29014
29417
|
socket = null;
|
|
29015
29418
|
started = false;
|
|
29016
29419
|
stopped = false;
|
|
@@ -29111,6 +29514,18 @@ var WalletApiSession = class {
|
|
|
29111
29514
|
await this.deps.kv.set(this.refreshKey, tokens.refreshToken);
|
|
29112
29515
|
}
|
|
29113
29516
|
// ── epoch latch (F12) ─────────────────────────────────────────────────────
|
|
29517
|
+
/** §5.1: the latched server syncEpoch ('' before first server contact). */
|
|
29518
|
+
currentEpoch() {
|
|
29519
|
+
return this.epochLatch ?? "";
|
|
29520
|
+
}
|
|
29521
|
+
/**
|
|
29522
|
+
* §5.1 restore hook (FacadeSession contract): handlers are AWAITED inside
|
|
29523
|
+
* the reseed chain, BEFORE any stream nudge resumes.
|
|
29524
|
+
*/
|
|
29525
|
+
subscribeEpochChange(handler) {
|
|
29526
|
+
this.epochHandlers.add(handler);
|
|
29527
|
+
return () => this.epochHandlers.delete(handler);
|
|
29528
|
+
}
|
|
29114
29529
|
noteEpoch(epoch) {
|
|
29115
29530
|
if (this.epochLatch === epoch) return;
|
|
29116
29531
|
const previous = this.epochLatch;
|
|
@@ -29124,7 +29539,7 @@ var WalletApiSession = class {
|
|
|
29124
29539
|
}
|
|
29125
29540
|
async runReseed(epoch, previous) {
|
|
29126
29541
|
try {
|
|
29127
|
-
|
|
29542
|
+
for (const handler of [...this.epochHandlers]) await handler(epoch);
|
|
29128
29543
|
await this.deps.kv.set(STORE_KEYS.epochLatch, epoch);
|
|
29129
29544
|
} catch {
|
|
29130
29545
|
if (this.epochLatch === epoch) this.epochLatch = previous;
|
|
@@ -29161,10 +29576,16 @@ var WalletApiSession = class {
|
|
|
29161
29576
|
status() {
|
|
29162
29577
|
return this.lastStatus ?? "offline";
|
|
29163
29578
|
}
|
|
29579
|
+
/** Transition feed off the existing emitStatus point (FacadeSession.subscribeStatus). */
|
|
29580
|
+
subscribeStatus(handler) {
|
|
29581
|
+
this.statusHandlers.add(handler);
|
|
29582
|
+
return () => this.statusHandlers.delete(handler);
|
|
29583
|
+
}
|
|
29164
29584
|
emitStatus(status) {
|
|
29165
29585
|
if (this.stopped || this.lastStatus === status) return;
|
|
29166
29586
|
this.lastStatus = status;
|
|
29167
29587
|
this.deps.emitStatus(status);
|
|
29588
|
+
for (const handler of this.statusHandlers) handler(status);
|
|
29168
29589
|
}
|
|
29169
29590
|
async connect() {
|
|
29170
29591
|
if (this.stopped) return;
|
|
@@ -29380,6 +29801,7 @@ var WalletApiDeliveryPort = class {
|
|
|
29380
29801
|
wake;
|
|
29381
29802
|
deriveFn;
|
|
29382
29803
|
seenChain = new SerialChain();
|
|
29804
|
+
lastIncomingEpoch = null;
|
|
29383
29805
|
constructor(config) {
|
|
29384
29806
|
this.client = config.client;
|
|
29385
29807
|
this.kv = config.kv;
|
|
@@ -29474,6 +29896,7 @@ var WalletApiDeliveryPort = class {
|
|
|
29474
29896
|
const seen = await this.readSeen();
|
|
29475
29897
|
let page = await this.client.listMailbox(sinceCursor === void 0 ? 0 : Number(sinceCursor));
|
|
29476
29898
|
for (; ; ) {
|
|
29899
|
+
this.lastIncomingEpoch = String(page.syncEpoch);
|
|
29477
29900
|
for (const entry of page.entries) {
|
|
29478
29901
|
if (entry.status !== "unclaimed") continue;
|
|
29479
29902
|
if (seen.has(entry.entryId)) continue;
|
|
@@ -29483,6 +29906,10 @@ var WalletApiDeliveryPort = class {
|
|
|
29483
29906
|
page = await this.client.listMailbox(page.cursor);
|
|
29484
29907
|
}
|
|
29485
29908
|
}
|
|
29909
|
+
/** §5.7: the page-honest epoch — the restore-detection source Receive keys on. */
|
|
29910
|
+
incomingEpoch() {
|
|
29911
|
+
return this.lastIncomingEpoch;
|
|
29912
|
+
}
|
|
29486
29913
|
toIncoming(entry) {
|
|
29487
29914
|
const bundle = this.openEnvelope(entry);
|
|
29488
29915
|
return {
|
|
@@ -29560,7 +29987,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
29560
29987
|
return utf8(`${transferId}:${String(opIndex)}`);
|
|
29561
29988
|
}
|
|
29562
29989
|
async put(transferId, opIndex, bytes) {
|
|
29563
|
-
const {
|
|
29990
|
+
const { kv, fieldKey } = this.config;
|
|
29564
29991
|
const aad = this.aad(transferId, opIndex);
|
|
29565
29992
|
const cacheKey = this.cacheKey(transferId, opIndex);
|
|
29566
29993
|
let envelope = await kv.get(cacheKey);
|
|
@@ -29568,10 +29995,21 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
29568
29995
|
envelope = encryptFieldBytes(fieldKey, bytes, aad);
|
|
29569
29996
|
await kv.set(cacheKey, envelope);
|
|
29570
29997
|
}
|
|
29571
|
-
const
|
|
29572
|
-
const record = await client.postProgress(transferId, opIndex, envelope, signature);
|
|
29998
|
+
const record = await this.post(transferId, opIndex, envelope);
|
|
29573
29999
|
return this.open(record.payload, aad);
|
|
29574
30000
|
}
|
|
30001
|
+
/** §5.1 restore (CheckpointReseeder): re-POST the cached ciphertext byte-identical; false = none cached. */
|
|
30002
|
+
async reseedCheckpoint(transferId, opIndex) {
|
|
30003
|
+
const envelope = await this.config.kv.get(this.cacheKey(transferId, opIndex));
|
|
30004
|
+
if (envelope === null) return false;
|
|
30005
|
+
await this.post(transferId, opIndex, envelope);
|
|
30006
|
+
return true;
|
|
30007
|
+
}
|
|
30008
|
+
/** The ONE signed progress-append path put() and reseedCheckpoint() share. */
|
|
30009
|
+
async post(transferId, opIndex, envelope) {
|
|
30010
|
+
const signature = await this.config.signProgress(progressSignMessage(transferId, opIndex, envelope));
|
|
30011
|
+
return this.config.client.postProgress(transferId, opIndex, envelope, signature);
|
|
30012
|
+
}
|
|
29575
30013
|
async get(transferId, opIndex) {
|
|
29576
30014
|
const records = await this.config.client.getProgress(transferId);
|
|
29577
30015
|
const record = records.find((r) => r.opIndex === opIndex);
|
|
@@ -29594,7 +30032,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
29594
30032
|
};
|
|
29595
30033
|
|
|
29596
30034
|
// core/payments-v2-wiring.ts
|
|
29597
|
-
function resolvePaymentsV2Composition(walletApi) {
|
|
30035
|
+
function resolvePaymentsV2Composition(walletApi, sphereNetwork) {
|
|
29598
30036
|
const source = walletApi;
|
|
29599
30037
|
const network = typeof source?.network === "string" && source.network !== "" ? source.network : null;
|
|
29600
30038
|
if (!source || network === null) {
|
|
@@ -29603,6 +30041,18 @@ function resolvePaymentsV2Composition(walletApi) {
|
|
|
29603
30041
|
"INVALID_CONFIG"
|
|
29604
30042
|
);
|
|
29605
30043
|
}
|
|
30044
|
+
if (!(network in NETWORKS)) {
|
|
30045
|
+
throw new SphereError(
|
|
30046
|
+
`paymentsV2: walletApi.network "${network}" is not a known network (${Object.keys(NETWORKS).join("/")}) \u2014 a payments composition is single-network (Sphere network "${String(sphereNetwork)}").`,
|
|
30047
|
+
"INVALID_CONFIG"
|
|
30048
|
+
);
|
|
30049
|
+
}
|
|
30050
|
+
if (network !== sphereNetwork) {
|
|
30051
|
+
throw new SphereError(
|
|
30052
|
+
`paymentsV2: walletApi.network "${network}" does not match the Sphere network "${String(sphereNetwork)}" the engine/registry run on \u2014 a payments composition is single-network.`,
|
|
30053
|
+
"INVALID_CONFIG"
|
|
30054
|
+
);
|
|
30055
|
+
}
|
|
29606
30056
|
if (typeof source.paymentsV2Transport === "function") {
|
|
29607
30057
|
return { network, factory: source.paymentsV2Transport.bind(source) };
|
|
29608
30058
|
}
|
|
@@ -29668,8 +30118,7 @@ function defaultPaymentsV2Transport(config) {
|
|
|
29668
30118
|
deviceId: `${config.deviceId}:pv2`,
|
|
29669
30119
|
kv: args.kv,
|
|
29670
30120
|
webSocketFactory,
|
|
29671
|
-
emitStatus: args.emitStatus
|
|
29672
|
-
onEpochChange: args.onEpochChange
|
|
30121
|
+
emitStatus: args.emitStatus
|
|
29673
30122
|
});
|
|
29674
30123
|
return { session, client: authedWireClient(session, client) };
|
|
29675
30124
|
};
|
|
@@ -29739,8 +30188,7 @@ function composePaymentsV2(spec) {
|
|
|
29739
30188
|
identity: identitySlice,
|
|
29740
30189
|
network,
|
|
29741
30190
|
kv,
|
|
29742
|
-
emitStatus: (status) => host.emit("connection:status", { status })
|
|
29743
|
-
onEpochChange: async () => void 0
|
|
30191
|
+
emitStatus: (status) => host.emit("connection:status", { status })
|
|
29744
30192
|
});
|
|
29745
30193
|
const deps = {
|
|
29746
30194
|
session: transport.session,
|
|
@@ -29770,7 +30218,10 @@ function composePaymentsV2(spec) {
|
|
|
29770
30218
|
network,
|
|
29771
30219
|
ownPubkey: identity.chainPubkey,
|
|
29772
30220
|
ownNametag: host.nametag,
|
|
29773
|
-
requestMemo: requestMemoCodec(identity.privateKey)
|
|
30221
|
+
requestMemo: requestMemoCodec(identity.privateKey),
|
|
30222
|
+
// §5.1: the session is the epoch authority — the facade registers its
|
|
30223
|
+
// restore hook on the session itself (FacadeSession.subscribeEpochChange).
|
|
30224
|
+
syncEpoch: () => transport.session.currentEpoch()
|
|
29774
30225
|
};
|
|
29775
30226
|
return new PaymentsFacade(deps);
|
|
29776
30227
|
}
|
|
@@ -30100,18 +30551,20 @@ var Sphere = class _Sphere {
|
|
|
30100
30551
|
}
|
|
30101
30552
|
/**
|
|
30102
30553
|
* P9: validate + resolve the paymentsV2 opt-in BEFORE any storage writes —
|
|
30103
|
-
* fail-closed at init, never a silently-degraded composition.
|
|
30554
|
+
* fail-closed at init, never a silently-degraded composition. Returns the
|
|
30555
|
+
* resolved composition (null = flag off); #728: resolving here also asserts
|
|
30556
|
+
* the single-network invariant (walletApi.network known + equal to the
|
|
30557
|
+
* Sphere network) before any session/KV/provider exists.
|
|
30104
30558
|
*/
|
|
30105
|
-
static resolvePaymentsV2Flag(flag, accountingConfig, swapConfig, walletApi) {
|
|
30106
|
-
if (flag !== true) return
|
|
30559
|
+
static resolvePaymentsV2Flag(flag, accountingConfig, swapConfig, walletApi, network) {
|
|
30560
|
+
if (flag !== true) return null;
|
|
30107
30561
|
if (accountingConfig || swapConfig) {
|
|
30108
30562
|
throw new SphereError(
|
|
30109
30563
|
"paymentsV2 is incompatible with the accounting/swap modules (both are built on the legacy PaymentsModule and are deleted with the P11 flip) \u2014 disable them or the flag.",
|
|
30110
30564
|
"INVALID_CONFIG"
|
|
30111
30565
|
);
|
|
30112
30566
|
}
|
|
30113
|
-
resolvePaymentsV2Composition(walletApi);
|
|
30114
|
-
return true;
|
|
30567
|
+
return resolvePaymentsV2Composition(walletApi, network);
|
|
30115
30568
|
}
|
|
30116
30569
|
/**
|
|
30117
30570
|
* Configure TokenRegistry in the main bundle context.
|
|
@@ -30166,12 +30619,14 @@ var Sphere = class _Sphere {
|
|
|
30166
30619
|
options.walletApi
|
|
30167
30620
|
);
|
|
30168
30621
|
sphere._verification = options.verification;
|
|
30169
|
-
sphere.
|
|
30622
|
+
sphere._paymentsV2Composition = _Sphere.resolvePaymentsV2Flag(
|
|
30170
30623
|
options.paymentsV2,
|
|
30171
30624
|
accountingConfig,
|
|
30172
30625
|
swapConfig,
|
|
30173
|
-
options.walletApi
|
|
30626
|
+
options.walletApi,
|
|
30627
|
+
options.network
|
|
30174
30628
|
);
|
|
30629
|
+
sphere._paymentsV2Enabled = sphere._paymentsV2Composition !== null;
|
|
30175
30630
|
sphere._password = options.password ?? null;
|
|
30176
30631
|
sphere._passwordProtected = sphere._password !== null;
|
|
30177
30632
|
progress?.({ step: "storing_keys", message: "Storing wallet keys..." });
|
|
@@ -30238,12 +30693,14 @@ var Sphere = class _Sphere {
|
|
|
30238
30693
|
options.walletApi
|
|
30239
30694
|
);
|
|
30240
30695
|
sphere._verification = options.verification;
|
|
30241
|
-
sphere.
|
|
30696
|
+
sphere._paymentsV2Composition = _Sphere.resolvePaymentsV2Flag(
|
|
30242
30697
|
options.paymentsV2,
|
|
30243
30698
|
accountingConfig,
|
|
30244
30699
|
swapConfig,
|
|
30245
|
-
options.walletApi
|
|
30700
|
+
options.walletApi,
|
|
30701
|
+
options.network
|
|
30246
30702
|
);
|
|
30703
|
+
sphere._paymentsV2Enabled = sphere._paymentsV2Composition !== null;
|
|
30247
30704
|
sphere._password = options.password ?? null;
|
|
30248
30705
|
sphere._passwordProtected = sphere._password !== null;
|
|
30249
30706
|
if (!options.storage.isConnected()) {
|
|
@@ -30317,12 +30774,14 @@ var Sphere = class _Sphere {
|
|
|
30317
30774
|
options.walletApi
|
|
30318
30775
|
);
|
|
30319
30776
|
sphere._verification = options.verification;
|
|
30320
|
-
sphere.
|
|
30777
|
+
sphere._paymentsV2Composition = _Sphere.resolvePaymentsV2Flag(
|
|
30321
30778
|
options.paymentsV2,
|
|
30322
30779
|
accountingConfig,
|
|
30323
30780
|
swapConfig,
|
|
30324
|
-
options.walletApi
|
|
30781
|
+
options.walletApi,
|
|
30782
|
+
options.network
|
|
30325
30783
|
);
|
|
30784
|
+
sphere._paymentsV2Enabled = sphere._paymentsV2Composition !== null;
|
|
30326
30785
|
sphere._password = options.password ?? null;
|
|
30327
30786
|
sphere._passwordProtected = sphere._password !== null;
|
|
30328
30787
|
progress?.({ step: "storing_keys", message: "Storing wallet keys..." });
|
|
@@ -32980,7 +33439,9 @@ var Sphere = class _Sphere {
|
|
|
32980
33439
|
* setOracleApiKey rebuild is what future operations snapshot.
|
|
32981
33440
|
*/
|
|
32982
33441
|
async startPaymentsV2Inner(index, identity) {
|
|
32983
|
-
this._paymentsV2Composition
|
|
33442
|
+
if (this._paymentsV2Composition === null) {
|
|
33443
|
+
throw new SphereError("paymentsV2: composition unresolved \u2014 init never ran the flag gate", "INVALID_CONFIG");
|
|
33444
|
+
}
|
|
32984
33445
|
if (!this._addressModules.get(index)?.tokenEngine) {
|
|
32985
33446
|
throw new SphereError(
|
|
32986
33447
|
"paymentsV2 requires the v2 token engine \u2014 the oracle must supply a trust base + gateway URL (and API key where required).",
|