@unicitylabs/sphere-sdk 0.14.3 → 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 +245 -24
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +245 -24
- 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 +245 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +245 -24
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +144 -17
- 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 +144 -17
- 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
|
@@ -503,6 +503,7 @@ var import_CertificationResponse = require("@unicitylabs/state-transition-sdk/li
|
|
|
503
503
|
var import_StateId = require("@unicitylabs/state-transition-sdk/lib/api/StateId.js");
|
|
504
504
|
var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js");
|
|
505
505
|
var import_InclusionProofResponse = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js");
|
|
506
|
+
var import_JsonRpcNetworkError = require("@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js");
|
|
506
507
|
var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js");
|
|
507
508
|
var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js");
|
|
508
509
|
var import_InclusionProofVerificationRule = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js");
|
|
@@ -1462,6 +1463,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
1462
1463
|
}
|
|
1463
1464
|
return out;
|
|
1464
1465
|
}
|
|
1466
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
1467
|
+
const holds = /* @__PURE__ */ new Map();
|
|
1468
|
+
let complete = true;
|
|
1469
|
+
for (const entry of await stores.backstop.list()) {
|
|
1470
|
+
if (entry.disposition !== "open") continue;
|
|
1471
|
+
let payload = null;
|
|
1472
|
+
try {
|
|
1473
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
1474
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
1475
|
+
} catch {
|
|
1476
|
+
complete = false;
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (payload === null) {
|
|
1480
|
+
complete = false;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
1484
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
1485
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
1486
|
+
}
|
|
1487
|
+
return { open: holds, complete };
|
|
1488
|
+
}
|
|
1465
1489
|
function groupJournal(journal) {
|
|
1466
1490
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
1467
1491
|
for (const entry of journal) {
|
|
@@ -2722,10 +2746,21 @@ var InventoryView = class {
|
|
|
2722
2746
|
release(tokenId) {
|
|
2723
2747
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
2724
2748
|
}
|
|
2749
|
+
pinned(tokenId) {
|
|
2750
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
2751
|
+
}
|
|
2752
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
2753
|
+
held(tokenId) {
|
|
2754
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
2755
|
+
}
|
|
2756
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
2757
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
2758
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
2725
2759
|
pool(coinId) {
|
|
2726
2760
|
const out = [];
|
|
2727
2761
|
for (const [tokenId, entry] of this.mirror) {
|
|
2728
|
-
if (entry.status !== "active"
|
|
2762
|
+
if (entry.status !== "active") continue;
|
|
2763
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
2729
2764
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
2730
2765
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
2731
2766
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -2741,7 +2776,7 @@ var InventoryView = class {
|
|
|
2741
2776
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
2742
2777
|
out.push(
|
|
2743
2778
|
toToken(tokenId, entry, asset, registry, {
|
|
2744
|
-
transferring: this.
|
|
2779
|
+
transferring: this.held(tokenId),
|
|
2745
2780
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
2746
2781
|
})
|
|
2747
2782
|
);
|
|
@@ -2749,7 +2784,7 @@ var InventoryView = class {
|
|
|
2749
2784
|
return out;
|
|
2750
2785
|
}
|
|
2751
2786
|
async assets(registry, price) {
|
|
2752
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
2787
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
2753
2788
|
if (!price || raw.length === 0) return raw;
|
|
2754
2789
|
return withPrices(raw, registry, price);
|
|
2755
2790
|
}
|
|
@@ -3527,6 +3562,20 @@ var Requests = class {
|
|
|
3527
3562
|
var ReservationLedger = class {
|
|
3528
3563
|
reservations = /* @__PURE__ */ new Map();
|
|
3529
3564
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
3565
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
3566
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
3567
|
+
authoritative = false;
|
|
3568
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
3569
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
3570
|
+
setAuthoritative(value) {
|
|
3571
|
+
const moved = this.authoritative !== value;
|
|
3572
|
+
this.authoritative = value;
|
|
3573
|
+
return moved;
|
|
3574
|
+
}
|
|
3575
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
3576
|
+
unprovenReason() {
|
|
3577
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
3578
|
+
}
|
|
3530
3579
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
3531
3580
|
reserve(reservationId, entries) {
|
|
3532
3581
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -3553,6 +3602,13 @@ var ReservationLedger = class {
|
|
|
3553
3602
|
getFreeAmount(tokenId, tokenAmount) {
|
|
3554
3603
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
3555
3604
|
}
|
|
3605
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
3606
|
+
holderOf(tokenId) {
|
|
3607
|
+
return this.tokenHolder.get(tokenId);
|
|
3608
|
+
}
|
|
3609
|
+
tokensOf(reservationId) {
|
|
3610
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
3611
|
+
}
|
|
3556
3612
|
clear() {
|
|
3557
3613
|
this.reservations.clear();
|
|
3558
3614
|
this.tokenHolder.clear();
|
|
@@ -3565,6 +3621,44 @@ var ReservationLedger = class {
|
|
|
3565
3621
|
}
|
|
3566
3622
|
};
|
|
3567
3623
|
|
|
3624
|
+
// modules/payments-v2/select/pins.ts
|
|
3625
|
+
var IntentPins = class {
|
|
3626
|
+
constructor(deps) {
|
|
3627
|
+
this.deps = deps;
|
|
3628
|
+
}
|
|
3629
|
+
pinned = /* @__PURE__ */ new Set();
|
|
3630
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
3631
|
+
adopt(transferId) {
|
|
3632
|
+
this.pinned.add(transferId);
|
|
3633
|
+
}
|
|
3634
|
+
async sync() {
|
|
3635
|
+
const { open, complete } = await this.deps.openIntents();
|
|
3636
|
+
let changed = false;
|
|
3637
|
+
for (const transferId of [...this.pinned]) {
|
|
3638
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
3639
|
+
this.pinned.delete(transferId);
|
|
3640
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
3641
|
+
this.deps.ledger.cancel(transferId);
|
|
3642
|
+
changed = true;
|
|
3643
|
+
}
|
|
3644
|
+
for (const [transferId, tokenIds] of open) {
|
|
3645
|
+
if (this.deps.isActive(transferId)) continue;
|
|
3646
|
+
this.pinned.add(transferId);
|
|
3647
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
3648
|
+
}
|
|
3649
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
3650
|
+
if (changed || gateMoved) this.deps.changed();
|
|
3651
|
+
}
|
|
3652
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
3653
|
+
pin(transferId, tokenIds) {
|
|
3654
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
3655
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
3656
|
+
if (free.length === 0) return false;
|
|
3657
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
3658
|
+
return true;
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
|
|
3568
3662
|
// modules/payments-v2/select/selector.ts
|
|
3569
3663
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
3570
3664
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -3655,6 +3749,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
3655
3749
|
// modules/payments-v2/select/queue.ts
|
|
3656
3750
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
3657
3751
|
var QUEUE_MAX_SIZE = 100;
|
|
3752
|
+
function insufficientBalance(view, amount) {
|
|
3753
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
3754
|
+
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";
|
|
3755
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
3756
|
+
}
|
|
3658
3757
|
var SpendQueue = class {
|
|
3659
3758
|
constructor(deps) {
|
|
3660
3759
|
this.deps = deps;
|
|
@@ -3667,12 +3766,10 @@ var SpendQueue = class {
|
|
|
3667
3766
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
3668
3767
|
}
|
|
3669
3768
|
const amount = parseAmount(request.amount);
|
|
3670
|
-
const
|
|
3769
|
+
const view = this.freeView(request.coinId);
|
|
3770
|
+
const { freeView, freeTotal } = view;
|
|
3671
3771
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
3672
|
-
throw
|
|
3673
|
-
"Insufficient balance for this transaction",
|
|
3674
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
3675
|
-
);
|
|
3772
|
+
throw insufficientBalance(view, amount);
|
|
3676
3773
|
}
|
|
3677
3774
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
3678
3775
|
if (plan !== null) {
|
|
@@ -3722,11 +3819,10 @@ var SpendQueue = class {
|
|
|
3722
3819
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3723
3820
|
return "rejected";
|
|
3724
3821
|
}
|
|
3725
|
-
const
|
|
3822
|
+
const view = this.freeView(entry.coinId);
|
|
3823
|
+
const { freeView, freeTotal } = view;
|
|
3726
3824
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
3727
|
-
entry.reject(
|
|
3728
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
3729
|
-
);
|
|
3825
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
3730
3826
|
return "rejected";
|
|
3731
3827
|
}
|
|
3732
3828
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -3771,18 +3867,26 @@ var SpendQueue = class {
|
|
|
3771
3867
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3772
3868
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
3773
3869
|
}
|
|
3774
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
3870
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
3871
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
3775
3872
|
freeView(coinId) {
|
|
3776
3873
|
const freeView = [];
|
|
3777
3874
|
let freeTotal = 0n;
|
|
3875
|
+
let pinnedTotal = 0n;
|
|
3876
|
+
const holders = /* @__PURE__ */ new Set();
|
|
3877
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
3778
3878
|
for (const entry of this.deps.getPool(coinId)) {
|
|
3779
3879
|
if (entry.amount <= 0n) continue;
|
|
3780
|
-
|
|
3880
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
3881
|
+
if (holder === void 0) {
|
|
3781
3882
|
freeView.push(entry);
|
|
3782
3883
|
freeTotal += entry.amount;
|
|
3884
|
+
} else {
|
|
3885
|
+
pinnedTotal += entry.amount;
|
|
3886
|
+
holders.add(holder);
|
|
3783
3887
|
}
|
|
3784
3888
|
}
|
|
3785
|
-
return { freeView, freeTotal };
|
|
3889
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
3786
3890
|
}
|
|
3787
3891
|
expectedChangeTotal(coinId) {
|
|
3788
3892
|
let total = 0n;
|
|
@@ -3824,13 +3928,17 @@ function supportsDeterministicMint(engine) {
|
|
|
3824
3928
|
}
|
|
3825
3929
|
function composeFacadeParts(deps, hooks) {
|
|
3826
3930
|
const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
|
|
3931
|
+
const ledger = new ReservationLedger();
|
|
3827
3932
|
const view = new InventoryView({
|
|
3828
3933
|
port: deps.storagePort,
|
|
3829
3934
|
kv: deps.kv,
|
|
3830
3935
|
emit: (event) => deps.emit(event, {}),
|
|
3936
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
3937
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
3938
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
3939
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
3831
3940
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3832
3941
|
});
|
|
3833
|
-
const ledger = new ReservationLedger();
|
|
3834
3942
|
const queue = new SpendQueue({
|
|
3835
3943
|
ledger,
|
|
3836
3944
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -3851,6 +3959,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3851
3959
|
ownPubkeyBytes,
|
|
3852
3960
|
view,
|
|
3853
3961
|
ledger,
|
|
3962
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
3854
3963
|
queue,
|
|
3855
3964
|
historyStore,
|
|
3856
3965
|
machineStores,
|
|
@@ -3862,6 +3971,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3862
3971
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
3863
3972
|
};
|
|
3864
3973
|
}
|
|
3974
|
+
function buildPins(deps, hooks, parts) {
|
|
3975
|
+
return new IntentPins({
|
|
3976
|
+
ledger: parts.ledger,
|
|
3977
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
3978
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
3979
|
+
release: (tokenId) => {
|
|
3980
|
+
parts.view.release(tokenId);
|
|
3981
|
+
},
|
|
3982
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
3983
|
+
});
|
|
3984
|
+
}
|
|
3865
3985
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
3866
3986
|
return {
|
|
3867
3987
|
stores: machineStores,
|
|
@@ -3971,7 +4091,8 @@ var PaymentsFacade = class {
|
|
|
3971
4091
|
this.deps = deps;
|
|
3972
4092
|
const parts = composeFacadeParts(deps, {
|
|
3973
4093
|
engine: () => this.engine(),
|
|
3974
|
-
send: (request) => this.send(request)
|
|
4094
|
+
send: (request) => this.send(request),
|
|
4095
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
3975
4096
|
});
|
|
3976
4097
|
this.view = parts.view;
|
|
3977
4098
|
this.ledger = parts.ledger;
|
|
@@ -3985,6 +4106,7 @@ var PaymentsFacade = class {
|
|
|
3985
4106
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3986
4107
|
this.restoreDeps = parts.restoreDeps;
|
|
3987
4108
|
this.requests = parts.requests;
|
|
4109
|
+
this.pins = parts.pins;
|
|
3988
4110
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3989
4111
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
3990
4112
|
now: () => this.nowMs(),
|
|
@@ -4027,6 +4149,8 @@ var PaymentsFacade = class {
|
|
|
4027
4149
|
passChain = new SerialChain();
|
|
4028
4150
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
4029
4151
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
4152
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
4153
|
+
pins;
|
|
4030
4154
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
4031
4155
|
async start() {
|
|
4032
4156
|
if (this.started) return;
|
|
@@ -4049,6 +4173,7 @@ var PaymentsFacade = class {
|
|
|
4049
4173
|
});
|
|
4050
4174
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
4051
4175
|
await this.deps.session.start();
|
|
4176
|
+
await this.pins.sync().catch(() => void 0);
|
|
4052
4177
|
this.trackTail(this.runConvergencePass());
|
|
4053
4178
|
this.trackTail(this.seedHeldStates());
|
|
4054
4179
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -4119,6 +4244,7 @@ var PaymentsFacade = class {
|
|
|
4119
4244
|
}
|
|
4120
4245
|
async convergeBody() {
|
|
4121
4246
|
const outcome = await this.converger.convergeOnce();
|
|
4247
|
+
await this.pins.sync().catch(() => void 0);
|
|
4122
4248
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
4123
4249
|
this.heartbeat.settle(outcome, pending);
|
|
4124
4250
|
}
|
|
@@ -4358,6 +4484,7 @@ var PaymentsFacade = class {
|
|
|
4358
4484
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
4359
4485
|
settleKeepOpen(ctx) {
|
|
4360
4486
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
4487
|
+
this.pins.adopt(ctx.transferId);
|
|
4361
4488
|
this.heartbeat.arm();
|
|
4362
4489
|
}
|
|
4363
4490
|
settlePartial(ctx, partial) {
|