@unicitylabs/sphere-sdk 0.14.2 → 0.14.4
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 +251 -27
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +251 -27
- 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 +1 -0
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.js +1 -0
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +251 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +251 -27
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +150 -20
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +2 -0
- package/dist/modules/payments-v2/index.d.ts +2 -0
- package/dist/modules/payments-v2/index.js +150 -20
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/dist/token-engine/index.cjs +102 -7
- package/dist/token-engine/index.cjs.map +1 -1
- package/dist/token-engine/index.d.cts +5 -1
- package/dist/token-engine/index.d.ts +5 -1
- package/dist/token-engine/index.js +102 -7
- package/dist/token-engine/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1066,6 +1066,8 @@ declare class PaymentsFacade implements PaymentsV2 {
|
|
|
1066
1066
|
private readonly passChain;
|
|
1067
1067
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
1068
1068
|
private readonly activeMoneyOps;
|
|
1069
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
1070
|
+
private readonly pins;
|
|
1069
1071
|
constructor(deps: PaymentsFacadeDeps);
|
|
1070
1072
|
start(): Promise<void>;
|
|
1071
1073
|
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
@@ -1066,6 +1066,8 @@ declare class PaymentsFacade implements PaymentsV2 {
|
|
|
1066
1066
|
private readonly passChain;
|
|
1067
1067
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
1068
1068
|
private readonly activeMoneyOps;
|
|
1069
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
1070
|
+
private readonly pins;
|
|
1069
1071
|
constructor(deps: PaymentsFacadeDeps);
|
|
1070
1072
|
start(): Promise<void>;
|
|
1071
1073
|
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
@@ -458,6 +458,7 @@ import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-t
|
|
|
458
458
|
import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
|
|
459
459
|
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
|
|
460
460
|
import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
|
|
461
|
+
import { JsonRpcNetworkError } from "@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js";
|
|
461
462
|
import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
|
|
462
463
|
import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
|
|
463
464
|
import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
|
|
@@ -1417,6 +1418,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
1417
1418
|
}
|
|
1418
1419
|
return out;
|
|
1419
1420
|
}
|
|
1421
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
1422
|
+
const holds = /* @__PURE__ */ new Map();
|
|
1423
|
+
let complete = true;
|
|
1424
|
+
for (const entry of await stores.backstop.list()) {
|
|
1425
|
+
if (entry.disposition !== "open") continue;
|
|
1426
|
+
let payload = null;
|
|
1427
|
+
try {
|
|
1428
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
1429
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
1430
|
+
} catch {
|
|
1431
|
+
complete = false;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (payload === null) {
|
|
1435
|
+
complete = false;
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
1439
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
1440
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
1441
|
+
}
|
|
1442
|
+
return { open: holds, complete };
|
|
1443
|
+
}
|
|
1420
1444
|
function groupJournal(journal) {
|
|
1421
1445
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
1422
1446
|
for (const entry of journal) {
|
|
@@ -1586,11 +1610,14 @@ function toAsset(coinId, totals, registry) {
|
|
|
1586
1610
|
fiatValueEur: null
|
|
1587
1611
|
};
|
|
1588
1612
|
}
|
|
1613
|
+
function priceIdOf(registry, coinId) {
|
|
1614
|
+
return registry.getName(coinId).toLowerCase();
|
|
1615
|
+
}
|
|
1589
1616
|
async function withPrices(raw, registry, price) {
|
|
1590
1617
|
try {
|
|
1591
|
-
const
|
|
1592
|
-
const quotes = await price.getPrices(
|
|
1593
|
-
return raw.map((a) => priceOne(a, quotes.get(registry
|
|
1618
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
1619
|
+
const quotes = await price.getPrices(ids);
|
|
1620
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
1594
1621
|
} catch {
|
|
1595
1622
|
return raw;
|
|
1596
1623
|
}
|
|
@@ -2674,10 +2701,21 @@ var InventoryView = class {
|
|
|
2674
2701
|
release(tokenId) {
|
|
2675
2702
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
2676
2703
|
}
|
|
2704
|
+
pinned(tokenId) {
|
|
2705
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
2706
|
+
}
|
|
2707
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
2708
|
+
held(tokenId) {
|
|
2709
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
2710
|
+
}
|
|
2711
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
2712
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
2713
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
2677
2714
|
pool(coinId) {
|
|
2678
2715
|
const out = [];
|
|
2679
2716
|
for (const [tokenId, entry] of this.mirror) {
|
|
2680
|
-
if (entry.status !== "active"
|
|
2717
|
+
if (entry.status !== "active") continue;
|
|
2718
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
2681
2719
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
2682
2720
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
2683
2721
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -2693,7 +2731,7 @@ var InventoryView = class {
|
|
|
2693
2731
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
2694
2732
|
out.push(
|
|
2695
2733
|
toToken(tokenId, entry, asset, registry, {
|
|
2696
|
-
transferring: this.
|
|
2734
|
+
transferring: this.held(tokenId),
|
|
2697
2735
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
2698
2736
|
})
|
|
2699
2737
|
);
|
|
@@ -2701,7 +2739,7 @@ var InventoryView = class {
|
|
|
2701
2739
|
return out;
|
|
2702
2740
|
}
|
|
2703
2741
|
async assets(registry, price) {
|
|
2704
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
2742
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
2705
2743
|
if (!price || raw.length === 0) return raw;
|
|
2706
2744
|
return withPrices(raw, registry, price);
|
|
2707
2745
|
}
|
|
@@ -3479,6 +3517,20 @@ var Requests = class {
|
|
|
3479
3517
|
var ReservationLedger = class {
|
|
3480
3518
|
reservations = /* @__PURE__ */ new Map();
|
|
3481
3519
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
3520
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
3521
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
3522
|
+
authoritative = false;
|
|
3523
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
3524
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
3525
|
+
setAuthoritative(value) {
|
|
3526
|
+
const moved = this.authoritative !== value;
|
|
3527
|
+
this.authoritative = value;
|
|
3528
|
+
return moved;
|
|
3529
|
+
}
|
|
3530
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
3531
|
+
unprovenReason() {
|
|
3532
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
3533
|
+
}
|
|
3482
3534
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
3483
3535
|
reserve(reservationId, entries) {
|
|
3484
3536
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -3505,6 +3557,13 @@ var ReservationLedger = class {
|
|
|
3505
3557
|
getFreeAmount(tokenId, tokenAmount) {
|
|
3506
3558
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
3507
3559
|
}
|
|
3560
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
3561
|
+
holderOf(tokenId) {
|
|
3562
|
+
return this.tokenHolder.get(tokenId);
|
|
3563
|
+
}
|
|
3564
|
+
tokensOf(reservationId) {
|
|
3565
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
3566
|
+
}
|
|
3508
3567
|
clear() {
|
|
3509
3568
|
this.reservations.clear();
|
|
3510
3569
|
this.tokenHolder.clear();
|
|
@@ -3517,6 +3576,44 @@ var ReservationLedger = class {
|
|
|
3517
3576
|
}
|
|
3518
3577
|
};
|
|
3519
3578
|
|
|
3579
|
+
// modules/payments-v2/select/pins.ts
|
|
3580
|
+
var IntentPins = class {
|
|
3581
|
+
constructor(deps) {
|
|
3582
|
+
this.deps = deps;
|
|
3583
|
+
}
|
|
3584
|
+
pinned = /* @__PURE__ */ new Set();
|
|
3585
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
3586
|
+
adopt(transferId) {
|
|
3587
|
+
this.pinned.add(transferId);
|
|
3588
|
+
}
|
|
3589
|
+
async sync() {
|
|
3590
|
+
const { open, complete } = await this.deps.openIntents();
|
|
3591
|
+
let changed = false;
|
|
3592
|
+
for (const transferId of [...this.pinned]) {
|
|
3593
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
3594
|
+
this.pinned.delete(transferId);
|
|
3595
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
3596
|
+
this.deps.ledger.cancel(transferId);
|
|
3597
|
+
changed = true;
|
|
3598
|
+
}
|
|
3599
|
+
for (const [transferId, tokenIds] of open) {
|
|
3600
|
+
if (this.deps.isActive(transferId)) continue;
|
|
3601
|
+
this.pinned.add(transferId);
|
|
3602
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
3603
|
+
}
|
|
3604
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
3605
|
+
if (changed || gateMoved) this.deps.changed();
|
|
3606
|
+
}
|
|
3607
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
3608
|
+
pin(transferId, tokenIds) {
|
|
3609
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
3610
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
3611
|
+
if (free.length === 0) return false;
|
|
3612
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
3613
|
+
return true;
|
|
3614
|
+
}
|
|
3615
|
+
};
|
|
3616
|
+
|
|
3520
3617
|
// modules/payments-v2/select/selector.ts
|
|
3521
3618
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
3522
3619
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -3607,6 +3704,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
3607
3704
|
// modules/payments-v2/select/queue.ts
|
|
3608
3705
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
3609
3706
|
var QUEUE_MAX_SIZE = 100;
|
|
3707
|
+
function insufficientBalance(view, amount) {
|
|
3708
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
3709
|
+
const message = view.pinnedTotal > 0n ? `Insufficient spendable balance: need ${amount.toString()}, ${view.freeTotal.toString()} free, ${view.pinnedTotal.toString()} pinned by ${String(view.pinnedBy)} transfer(s) still converging \u2014 see payments.pendingTransfers(); do not re-send.` : "Insufficient balance for this transaction";
|
|
3710
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
3711
|
+
}
|
|
3610
3712
|
var SpendQueue = class {
|
|
3611
3713
|
constructor(deps) {
|
|
3612
3714
|
this.deps = deps;
|
|
@@ -3619,12 +3721,10 @@ var SpendQueue = class {
|
|
|
3619
3721
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
3620
3722
|
}
|
|
3621
3723
|
const amount = parseAmount(request.amount);
|
|
3622
|
-
const
|
|
3724
|
+
const view = this.freeView(request.coinId);
|
|
3725
|
+
const { freeView, freeTotal } = view;
|
|
3623
3726
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
3624
|
-
throw
|
|
3625
|
-
"Insufficient balance for this transaction",
|
|
3626
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
3627
|
-
);
|
|
3727
|
+
throw insufficientBalance(view, amount);
|
|
3628
3728
|
}
|
|
3629
3729
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
3630
3730
|
if (plan !== null) {
|
|
@@ -3674,11 +3774,10 @@ var SpendQueue = class {
|
|
|
3674
3774
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3675
3775
|
return "rejected";
|
|
3676
3776
|
}
|
|
3677
|
-
const
|
|
3777
|
+
const view = this.freeView(entry.coinId);
|
|
3778
|
+
const { freeView, freeTotal } = view;
|
|
3678
3779
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
3679
|
-
entry.reject(
|
|
3680
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
3681
|
-
);
|
|
3780
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
3682
3781
|
return "rejected";
|
|
3683
3782
|
}
|
|
3684
3783
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -3723,18 +3822,26 @@ var SpendQueue = class {
|
|
|
3723
3822
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3724
3823
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
3725
3824
|
}
|
|
3726
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
3825
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
3826
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
3727
3827
|
freeView(coinId) {
|
|
3728
3828
|
const freeView = [];
|
|
3729
3829
|
let freeTotal = 0n;
|
|
3830
|
+
let pinnedTotal = 0n;
|
|
3831
|
+
const holders = /* @__PURE__ */ new Set();
|
|
3832
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
3730
3833
|
for (const entry of this.deps.getPool(coinId)) {
|
|
3731
3834
|
if (entry.amount <= 0n) continue;
|
|
3732
|
-
|
|
3835
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
3836
|
+
if (holder === void 0) {
|
|
3733
3837
|
freeView.push(entry);
|
|
3734
3838
|
freeTotal += entry.amount;
|
|
3839
|
+
} else {
|
|
3840
|
+
pinnedTotal += entry.amount;
|
|
3841
|
+
holders.add(holder);
|
|
3735
3842
|
}
|
|
3736
3843
|
}
|
|
3737
|
-
return { freeView, freeTotal };
|
|
3844
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
3738
3845
|
}
|
|
3739
3846
|
expectedChangeTotal(coinId) {
|
|
3740
3847
|
let total = 0n;
|
|
@@ -3776,13 +3883,17 @@ function supportsDeterministicMint(engine) {
|
|
|
3776
3883
|
}
|
|
3777
3884
|
function composeFacadeParts(deps, hooks) {
|
|
3778
3885
|
const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
|
|
3886
|
+
const ledger = new ReservationLedger();
|
|
3779
3887
|
const view = new InventoryView({
|
|
3780
3888
|
port: deps.storagePort,
|
|
3781
3889
|
kv: deps.kv,
|
|
3782
3890
|
emit: (event) => deps.emit(event, {}),
|
|
3891
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
3892
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
3893
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
3894
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
3783
3895
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3784
3896
|
});
|
|
3785
|
-
const ledger = new ReservationLedger();
|
|
3786
3897
|
const queue = new SpendQueue({
|
|
3787
3898
|
ledger,
|
|
3788
3899
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -3803,6 +3914,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3803
3914
|
ownPubkeyBytes,
|
|
3804
3915
|
view,
|
|
3805
3916
|
ledger,
|
|
3917
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
3806
3918
|
queue,
|
|
3807
3919
|
historyStore,
|
|
3808
3920
|
machineStores,
|
|
@@ -3814,6 +3926,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3814
3926
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
3815
3927
|
};
|
|
3816
3928
|
}
|
|
3929
|
+
function buildPins(deps, hooks, parts) {
|
|
3930
|
+
return new IntentPins({
|
|
3931
|
+
ledger: parts.ledger,
|
|
3932
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
3933
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
3934
|
+
release: (tokenId) => {
|
|
3935
|
+
parts.view.release(tokenId);
|
|
3936
|
+
},
|
|
3937
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3817
3940
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
3818
3941
|
return {
|
|
3819
3942
|
stores: machineStores,
|
|
@@ -3923,7 +4046,8 @@ var PaymentsFacade = class {
|
|
|
3923
4046
|
this.deps = deps;
|
|
3924
4047
|
const parts = composeFacadeParts(deps, {
|
|
3925
4048
|
engine: () => this.engine(),
|
|
3926
|
-
send: (request) => this.send(request)
|
|
4049
|
+
send: (request) => this.send(request),
|
|
4050
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
3927
4051
|
});
|
|
3928
4052
|
this.view = parts.view;
|
|
3929
4053
|
this.ledger = parts.ledger;
|
|
@@ -3937,6 +4061,7 @@ var PaymentsFacade = class {
|
|
|
3937
4061
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3938
4062
|
this.restoreDeps = parts.restoreDeps;
|
|
3939
4063
|
this.requests = parts.requests;
|
|
4064
|
+
this.pins = parts.pins;
|
|
3940
4065
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3941
4066
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
3942
4067
|
now: () => this.nowMs(),
|
|
@@ -3979,6 +4104,8 @@ var PaymentsFacade = class {
|
|
|
3979
4104
|
passChain = new SerialChain();
|
|
3980
4105
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
3981
4106
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
4107
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
4108
|
+
pins;
|
|
3982
4109
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
3983
4110
|
async start() {
|
|
3984
4111
|
if (this.started) return;
|
|
@@ -4001,6 +4128,7 @@ var PaymentsFacade = class {
|
|
|
4001
4128
|
});
|
|
4002
4129
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
4003
4130
|
await this.deps.session.start();
|
|
4131
|
+
await this.pins.sync().catch(() => void 0);
|
|
4004
4132
|
this.trackTail(this.runConvergencePass());
|
|
4005
4133
|
this.trackTail(this.seedHeldStates());
|
|
4006
4134
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -4071,6 +4199,7 @@ var PaymentsFacade = class {
|
|
|
4071
4199
|
}
|
|
4072
4200
|
async convergeBody() {
|
|
4073
4201
|
const outcome = await this.converger.convergeOnce();
|
|
4202
|
+
await this.pins.sync().catch(() => void 0);
|
|
4074
4203
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
4075
4204
|
this.heartbeat.settle(outcome, pending);
|
|
4076
4205
|
}
|
|
@@ -4310,6 +4439,7 @@ var PaymentsFacade = class {
|
|
|
4310
4439
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
4311
4440
|
settleKeepOpen(ctx) {
|
|
4312
4441
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
4442
|
+
this.pins.adopt(ctx.transferId);
|
|
4313
4443
|
this.heartbeat.arm();
|
|
4314
4444
|
}
|
|
4315
4445
|
settlePartial(ctx, partial) {
|