@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
package/dist/index.js
CHANGED
|
@@ -9218,6 +9218,7 @@ import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-t
|
|
|
9218
9218
|
import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
|
|
9219
9219
|
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
|
|
9220
9220
|
import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
|
|
9221
|
+
import { JsonRpcNetworkError } from "@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js";
|
|
9221
9222
|
import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
|
|
9222
9223
|
import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
|
|
9223
9224
|
import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
|
|
@@ -9383,6 +9384,86 @@ function decodeSpherePaymentData(bytes) {
|
|
|
9383
9384
|
return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
|
|
9384
9385
|
}
|
|
9385
9386
|
|
|
9387
|
+
// token-engine/proof-wait.ts
|
|
9388
|
+
var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
|
|
9389
|
+
function resolveProofTimeoutMs(configured) {
|
|
9390
|
+
if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
|
|
9391
|
+
if (!Number.isFinite(configured) || configured <= 0) {
|
|
9392
|
+
throw new SphereError(
|
|
9393
|
+
`proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
|
|
9394
|
+
"INVALID_CONFIG"
|
|
9395
|
+
);
|
|
9396
|
+
}
|
|
9397
|
+
return configured;
|
|
9398
|
+
}
|
|
9399
|
+
function lateDeliveringSignal(signal) {
|
|
9400
|
+
return new Proxy(signal, {
|
|
9401
|
+
get(target, prop) {
|
|
9402
|
+
if (prop === "addEventListener") {
|
|
9403
|
+
return (type, listener, options) => {
|
|
9404
|
+
if (type === "abort" && target.aborted) {
|
|
9405
|
+
queueMicrotask(() => {
|
|
9406
|
+
listener.call(target, new Event("abort"));
|
|
9407
|
+
});
|
|
9408
|
+
return;
|
|
9409
|
+
}
|
|
9410
|
+
target.addEventListener(type, listener, options);
|
|
9411
|
+
};
|
|
9412
|
+
}
|
|
9413
|
+
const value = Reflect.get(target, prop, target);
|
|
9414
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
9415
|
+
}
|
|
9416
|
+
});
|
|
9417
|
+
}
|
|
9418
|
+
function rejectOnAbort(signal) {
|
|
9419
|
+
return new Promise((_resolve, reject) => {
|
|
9420
|
+
if (signal.aborted) {
|
|
9421
|
+
reject(signal.reason);
|
|
9422
|
+
return;
|
|
9423
|
+
}
|
|
9424
|
+
signal.addEventListener(
|
|
9425
|
+
"abort",
|
|
9426
|
+
() => {
|
|
9427
|
+
reject(signal.reason);
|
|
9428
|
+
},
|
|
9429
|
+
{ once: true }
|
|
9430
|
+
);
|
|
9431
|
+
});
|
|
9432
|
+
}
|
|
9433
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9434
|
+
const controller = new AbortController();
|
|
9435
|
+
const relayAbort = () => {
|
|
9436
|
+
controller.abort(external?.reason);
|
|
9437
|
+
};
|
|
9438
|
+
if (external !== void 0) {
|
|
9439
|
+
if (external.aborted) relayAbort();
|
|
9440
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9441
|
+
}
|
|
9442
|
+
const timer = setTimeout(() => {
|
|
9443
|
+
controller.abort(
|
|
9444
|
+
new SphereError(
|
|
9445
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9446
|
+
"AGGREGATOR_ERROR"
|
|
9447
|
+
)
|
|
9448
|
+
);
|
|
9449
|
+
}, deps.timeoutMs);
|
|
9450
|
+
const wait = waitInclusionProof(
|
|
9451
|
+
deps.client,
|
|
9452
|
+
deps.trustBase,
|
|
9453
|
+
deps.predicateVerifier,
|
|
9454
|
+
transaction,
|
|
9455
|
+
lateDeliveringSignal(controller.signal),
|
|
9456
|
+
deps.intervalMs
|
|
9457
|
+
);
|
|
9458
|
+
wait.catch(() => void 0);
|
|
9459
|
+
try {
|
|
9460
|
+
return await Promise.race([wait, rejectOnAbort(controller.signal)]);
|
|
9461
|
+
} finally {
|
|
9462
|
+
clearTimeout(timer);
|
|
9463
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
|
|
9386
9467
|
// token-engine/errors.ts
|
|
9387
9468
|
var TransferConflictError = class extends SphereError {
|
|
9388
9469
|
/**
|
|
@@ -9607,11 +9688,13 @@ var SphereTokenEngine = class {
|
|
|
9607
9688
|
constructor(deps) {
|
|
9608
9689
|
this.deps = deps;
|
|
9609
9690
|
this.privateKeyHex = HexConverter.encode(deps.privateKey);
|
|
9691
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9610
9692
|
}
|
|
9611
9693
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9612
9694
|
deterministicMint = true;
|
|
9613
9695
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9614
9696
|
privateKeyHex;
|
|
9697
|
+
proofTimeoutMs;
|
|
9615
9698
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9616
9699
|
getIdentity() {
|
|
9617
9700
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -9954,14 +10037,17 @@ var SphereTokenEngine = class {
|
|
|
9954
10037
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
9955
10038
|
}
|
|
9956
10039
|
try {
|
|
9957
|
-
return await
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
10040
|
+
return await awaitProofBounded(
|
|
10041
|
+
{
|
|
10042
|
+
client: this.deps.client,
|
|
10043
|
+
trustBase: this.deps.trustBase,
|
|
10044
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10045
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10046
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10047
|
+
// #683
|
|
10048
|
+
},
|
|
9961
10049
|
transaction,
|
|
9962
|
-
options?.signal
|
|
9963
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
9964
|
-
// #683 finer poll cadence
|
|
10050
|
+
options?.signal
|
|
9965
10051
|
);
|
|
9966
10052
|
} catch (err) {
|
|
9967
10053
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -9977,6 +10063,13 @@ var SphereTokenEngine = class {
|
|
|
9977
10063
|
);
|
|
9978
10064
|
}
|
|
9979
10065
|
}
|
|
10066
|
+
/**
|
|
10067
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10068
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10069
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10070
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10071
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10072
|
+
*/
|
|
9980
10073
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
9981
10074
|
assertOwned(token) {
|
|
9982
10075
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10076,6 +10169,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10076
10169
|
networkId: trustBase.networkId,
|
|
10077
10170
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10078
10171
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10172
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10173
|
+
// #739: was declared but never wired
|
|
10079
10174
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10080
10175
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10081
10176
|
};
|
|
@@ -11985,6 +12080,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
11985
12080
|
}
|
|
11986
12081
|
return out;
|
|
11987
12082
|
}
|
|
12083
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12084
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12085
|
+
let complete = true;
|
|
12086
|
+
for (const entry of await stores.backstop.list()) {
|
|
12087
|
+
if (entry.disposition !== "open") continue;
|
|
12088
|
+
let payload = null;
|
|
12089
|
+
try {
|
|
12090
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12091
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12092
|
+
} catch {
|
|
12093
|
+
complete = false;
|
|
12094
|
+
continue;
|
|
12095
|
+
}
|
|
12096
|
+
if (payload === null) {
|
|
12097
|
+
complete = false;
|
|
12098
|
+
continue;
|
|
12099
|
+
}
|
|
12100
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12101
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12102
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12103
|
+
}
|
|
12104
|
+
return { open: holds, complete };
|
|
12105
|
+
}
|
|
11988
12106
|
function groupJournal(journal) {
|
|
11989
12107
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
11990
12108
|
for (const entry of journal) {
|
|
@@ -12395,10 +12513,21 @@ var InventoryView = class {
|
|
|
12395
12513
|
release(tokenId) {
|
|
12396
12514
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12397
12515
|
}
|
|
12516
|
+
pinned(tokenId) {
|
|
12517
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12518
|
+
}
|
|
12519
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12520
|
+
held(tokenId) {
|
|
12521
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12522
|
+
}
|
|
12523
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12524
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12525
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12398
12526
|
pool(coinId) {
|
|
12399
12527
|
const out = [];
|
|
12400
12528
|
for (const [tokenId, entry] of this.mirror) {
|
|
12401
|
-
if (entry.status !== "active"
|
|
12529
|
+
if (entry.status !== "active") continue;
|
|
12530
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12402
12531
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12403
12532
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12404
12533
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12414,7 +12543,7 @@ var InventoryView = class {
|
|
|
12414
12543
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12415
12544
|
out.push(
|
|
12416
12545
|
toToken(tokenId, entry, asset, registry, {
|
|
12417
|
-
transferring: this.
|
|
12546
|
+
transferring: this.held(tokenId),
|
|
12418
12547
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12419
12548
|
})
|
|
12420
12549
|
);
|
|
@@ -12422,7 +12551,7 @@ var InventoryView = class {
|
|
|
12422
12551
|
return out;
|
|
12423
12552
|
}
|
|
12424
12553
|
async assets(registry, price) {
|
|
12425
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12554
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12426
12555
|
if (!price || raw.length === 0) return raw;
|
|
12427
12556
|
return withPrices(raw, registry, price);
|
|
12428
12557
|
}
|
|
@@ -13108,6 +13237,20 @@ var Requests = class {
|
|
|
13108
13237
|
var ReservationLedger = class {
|
|
13109
13238
|
reservations = /* @__PURE__ */ new Map();
|
|
13110
13239
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13240
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13241
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13242
|
+
authoritative = false;
|
|
13243
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13244
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13245
|
+
setAuthoritative(value) {
|
|
13246
|
+
const moved = this.authoritative !== value;
|
|
13247
|
+
this.authoritative = value;
|
|
13248
|
+
return moved;
|
|
13249
|
+
}
|
|
13250
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13251
|
+
unprovenReason() {
|
|
13252
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13253
|
+
}
|
|
13111
13254
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13112
13255
|
reserve(reservationId, entries) {
|
|
13113
13256
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13134,6 +13277,13 @@ var ReservationLedger = class {
|
|
|
13134
13277
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13135
13278
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13136
13279
|
}
|
|
13280
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13281
|
+
holderOf(tokenId) {
|
|
13282
|
+
return this.tokenHolder.get(tokenId);
|
|
13283
|
+
}
|
|
13284
|
+
tokensOf(reservationId) {
|
|
13285
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13286
|
+
}
|
|
13137
13287
|
clear() {
|
|
13138
13288
|
this.reservations.clear();
|
|
13139
13289
|
this.tokenHolder.clear();
|
|
@@ -13146,6 +13296,44 @@ var ReservationLedger = class {
|
|
|
13146
13296
|
}
|
|
13147
13297
|
};
|
|
13148
13298
|
|
|
13299
|
+
// modules/payments-v2/select/pins.ts
|
|
13300
|
+
var IntentPins = class {
|
|
13301
|
+
constructor(deps) {
|
|
13302
|
+
this.deps = deps;
|
|
13303
|
+
}
|
|
13304
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13305
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13306
|
+
adopt(transferId) {
|
|
13307
|
+
this.pinned.add(transferId);
|
|
13308
|
+
}
|
|
13309
|
+
async sync() {
|
|
13310
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13311
|
+
let changed = false;
|
|
13312
|
+
for (const transferId of [...this.pinned]) {
|
|
13313
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13314
|
+
this.pinned.delete(transferId);
|
|
13315
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13316
|
+
this.deps.ledger.cancel(transferId);
|
|
13317
|
+
changed = true;
|
|
13318
|
+
}
|
|
13319
|
+
for (const [transferId, tokenIds] of open) {
|
|
13320
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13321
|
+
this.pinned.add(transferId);
|
|
13322
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13323
|
+
}
|
|
13324
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13325
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13326
|
+
}
|
|
13327
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13328
|
+
pin(transferId, tokenIds) {
|
|
13329
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13330
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13331
|
+
if (free.length === 0) return false;
|
|
13332
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13333
|
+
return true;
|
|
13334
|
+
}
|
|
13335
|
+
};
|
|
13336
|
+
|
|
13149
13337
|
// modules/payments-v2/select/selector.ts
|
|
13150
13338
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13151
13339
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13236,6 +13424,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13236
13424
|
// modules/payments-v2/select/queue.ts
|
|
13237
13425
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13238
13426
|
var QUEUE_MAX_SIZE = 100;
|
|
13427
|
+
function insufficientBalance(view, amount) {
|
|
13428
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13429
|
+
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";
|
|
13430
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13431
|
+
}
|
|
13239
13432
|
var SpendQueue = class {
|
|
13240
13433
|
constructor(deps) {
|
|
13241
13434
|
this.deps = deps;
|
|
@@ -13248,12 +13441,10 @@ var SpendQueue = class {
|
|
|
13248
13441
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13249
13442
|
}
|
|
13250
13443
|
const amount = parseAmount(request.amount);
|
|
13251
|
-
const
|
|
13444
|
+
const view = this.freeView(request.coinId);
|
|
13445
|
+
const { freeView, freeTotal } = view;
|
|
13252
13446
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13253
|
-
throw
|
|
13254
|
-
"Insufficient balance for this transaction",
|
|
13255
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13256
|
-
);
|
|
13447
|
+
throw insufficientBalance(view, amount);
|
|
13257
13448
|
}
|
|
13258
13449
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13259
13450
|
if (plan !== null) {
|
|
@@ -13303,11 +13494,10 @@ var SpendQueue = class {
|
|
|
13303
13494
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13304
13495
|
return "rejected";
|
|
13305
13496
|
}
|
|
13306
|
-
const
|
|
13497
|
+
const view = this.freeView(entry.coinId);
|
|
13498
|
+
const { freeView, freeTotal } = view;
|
|
13307
13499
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13308
|
-
entry.reject(
|
|
13309
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13310
|
-
);
|
|
13500
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13311
13501
|
return "rejected";
|
|
13312
13502
|
}
|
|
13313
13503
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13352,18 +13542,26 @@ var SpendQueue = class {
|
|
|
13352
13542
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13353
13543
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13354
13544
|
}
|
|
13355
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13545
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13546
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13356
13547
|
freeView(coinId) {
|
|
13357
13548
|
const freeView = [];
|
|
13358
13549
|
let freeTotal = 0n;
|
|
13550
|
+
let pinnedTotal = 0n;
|
|
13551
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13552
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13359
13553
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13360
13554
|
if (entry.amount <= 0n) continue;
|
|
13361
|
-
|
|
13555
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13556
|
+
if (holder === void 0) {
|
|
13362
13557
|
freeView.push(entry);
|
|
13363
13558
|
freeTotal += entry.amount;
|
|
13559
|
+
} else {
|
|
13560
|
+
pinnedTotal += entry.amount;
|
|
13561
|
+
holders.add(holder);
|
|
13364
13562
|
}
|
|
13365
13563
|
}
|
|
13366
|
-
return { freeView, freeTotal };
|
|
13564
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13367
13565
|
}
|
|
13368
13566
|
expectedChangeTotal(coinId) {
|
|
13369
13567
|
let total = 0n;
|
|
@@ -13405,13 +13603,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13405
13603
|
}
|
|
13406
13604
|
function composeFacadeParts(deps, hooks) {
|
|
13407
13605
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13606
|
+
const ledger = new ReservationLedger();
|
|
13408
13607
|
const view = new InventoryView({
|
|
13409
13608
|
port: deps.storagePort,
|
|
13410
13609
|
kv: deps.kv,
|
|
13411
13610
|
emit: (event) => deps.emit(event, {}),
|
|
13611
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13612
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13613
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13614
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13412
13615
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13413
13616
|
});
|
|
13414
|
-
const ledger = new ReservationLedger();
|
|
13415
13617
|
const queue = new SpendQueue({
|
|
13416
13618
|
ledger,
|
|
13417
13619
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13432,6 +13634,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13432
13634
|
ownPubkeyBytes,
|
|
13433
13635
|
view,
|
|
13434
13636
|
ledger,
|
|
13637
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13435
13638
|
queue,
|
|
13436
13639
|
historyStore,
|
|
13437
13640
|
machineStores,
|
|
@@ -13443,6 +13646,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13443
13646
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13444
13647
|
};
|
|
13445
13648
|
}
|
|
13649
|
+
function buildPins(deps, hooks, parts) {
|
|
13650
|
+
return new IntentPins({
|
|
13651
|
+
ledger: parts.ledger,
|
|
13652
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13653
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13654
|
+
release: (tokenId) => {
|
|
13655
|
+
parts.view.release(tokenId);
|
|
13656
|
+
},
|
|
13657
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13658
|
+
});
|
|
13659
|
+
}
|
|
13446
13660
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13447
13661
|
return {
|
|
13448
13662
|
stores: machineStores,
|
|
@@ -13552,7 +13766,8 @@ var PaymentsFacade = class {
|
|
|
13552
13766
|
this.deps = deps;
|
|
13553
13767
|
const parts = composeFacadeParts(deps, {
|
|
13554
13768
|
engine: () => this.engine(),
|
|
13555
|
-
send: (request) => this.send(request)
|
|
13769
|
+
send: (request) => this.send(request),
|
|
13770
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13556
13771
|
});
|
|
13557
13772
|
this.view = parts.view;
|
|
13558
13773
|
this.ledger = parts.ledger;
|
|
@@ -13566,6 +13781,7 @@ var PaymentsFacade = class {
|
|
|
13566
13781
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13567
13782
|
this.restoreDeps = parts.restoreDeps;
|
|
13568
13783
|
this.requests = parts.requests;
|
|
13784
|
+
this.pins = parts.pins;
|
|
13569
13785
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13570
13786
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13571
13787
|
now: () => this.nowMs(),
|
|
@@ -13608,6 +13824,8 @@ var PaymentsFacade = class {
|
|
|
13608
13824
|
passChain = new SerialChain();
|
|
13609
13825
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13610
13826
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13827
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13828
|
+
pins;
|
|
13611
13829
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13612
13830
|
async start() {
|
|
13613
13831
|
if (this.started) return;
|
|
@@ -13630,6 +13848,7 @@ var PaymentsFacade = class {
|
|
|
13630
13848
|
});
|
|
13631
13849
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13632
13850
|
await this.deps.session.start();
|
|
13851
|
+
await this.pins.sync().catch(() => void 0);
|
|
13633
13852
|
this.trackTail(this.runConvergencePass());
|
|
13634
13853
|
this.trackTail(this.seedHeldStates());
|
|
13635
13854
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13700,6 +13919,7 @@ var PaymentsFacade = class {
|
|
|
13700
13919
|
}
|
|
13701
13920
|
async convergeBody() {
|
|
13702
13921
|
const outcome = await this.converger.convergeOnce();
|
|
13922
|
+
await this.pins.sync().catch(() => void 0);
|
|
13703
13923
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13704
13924
|
this.heartbeat.settle(outcome, pending);
|
|
13705
13925
|
}
|
|
@@ -13939,6 +14159,7 @@ var PaymentsFacade = class {
|
|
|
13939
14159
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
13940
14160
|
settleKeepOpen(ctx) {
|
|
13941
14161
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14162
|
+
this.pins.adopt(ctx.transferId);
|
|
13942
14163
|
this.heartbeat.arm();
|
|
13943
14164
|
}
|
|
13944
14165
|
settlePartial(ctx, partial) {
|