@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
package/dist/index.cjs
CHANGED
|
@@ -9346,6 +9346,7 @@ var import_CertificationResponse = require("@unicitylabs/state-transition-sdk/li
|
|
|
9346
9346
|
var import_StateId = require("@unicitylabs/state-transition-sdk/lib/api/StateId.js");
|
|
9347
9347
|
var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js");
|
|
9348
9348
|
var import_InclusionProofResponse = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js");
|
|
9349
|
+
var import_JsonRpcNetworkError = require("@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js");
|
|
9349
9350
|
var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js");
|
|
9350
9351
|
var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js");
|
|
9351
9352
|
var import_InclusionProofVerificationRule = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js");
|
|
@@ -9511,6 +9512,86 @@ function decodeSpherePaymentData(bytes) {
|
|
|
9511
9512
|
return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
|
|
9512
9513
|
}
|
|
9513
9514
|
|
|
9515
|
+
// token-engine/proof-wait.ts
|
|
9516
|
+
var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
|
|
9517
|
+
function resolveProofTimeoutMs(configured) {
|
|
9518
|
+
if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
|
|
9519
|
+
if (!Number.isFinite(configured) || configured <= 0) {
|
|
9520
|
+
throw new SphereError(
|
|
9521
|
+
`proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
|
|
9522
|
+
"INVALID_CONFIG"
|
|
9523
|
+
);
|
|
9524
|
+
}
|
|
9525
|
+
return configured;
|
|
9526
|
+
}
|
|
9527
|
+
function lateDeliveringSignal(signal) {
|
|
9528
|
+
return new Proxy(signal, {
|
|
9529
|
+
get(target, prop) {
|
|
9530
|
+
if (prop === "addEventListener") {
|
|
9531
|
+
return (type, listener, options) => {
|
|
9532
|
+
if (type === "abort" && target.aborted) {
|
|
9533
|
+
queueMicrotask(() => {
|
|
9534
|
+
listener.call(target, new Event("abort"));
|
|
9535
|
+
});
|
|
9536
|
+
return;
|
|
9537
|
+
}
|
|
9538
|
+
target.addEventListener(type, listener, options);
|
|
9539
|
+
};
|
|
9540
|
+
}
|
|
9541
|
+
const value = Reflect.get(target, prop, target);
|
|
9542
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
9543
|
+
}
|
|
9544
|
+
});
|
|
9545
|
+
}
|
|
9546
|
+
function rejectOnAbort(signal) {
|
|
9547
|
+
return new Promise((_resolve, reject) => {
|
|
9548
|
+
if (signal.aborted) {
|
|
9549
|
+
reject(signal.reason);
|
|
9550
|
+
return;
|
|
9551
|
+
}
|
|
9552
|
+
signal.addEventListener(
|
|
9553
|
+
"abort",
|
|
9554
|
+
() => {
|
|
9555
|
+
reject(signal.reason);
|
|
9556
|
+
},
|
|
9557
|
+
{ once: true }
|
|
9558
|
+
);
|
|
9559
|
+
});
|
|
9560
|
+
}
|
|
9561
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9562
|
+
const controller = new AbortController();
|
|
9563
|
+
const relayAbort = () => {
|
|
9564
|
+
controller.abort(external?.reason);
|
|
9565
|
+
};
|
|
9566
|
+
if (external !== void 0) {
|
|
9567
|
+
if (external.aborted) relayAbort();
|
|
9568
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9569
|
+
}
|
|
9570
|
+
const timer = setTimeout(() => {
|
|
9571
|
+
controller.abort(
|
|
9572
|
+
new SphereError(
|
|
9573
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9574
|
+
"AGGREGATOR_ERROR"
|
|
9575
|
+
)
|
|
9576
|
+
);
|
|
9577
|
+
}, deps.timeoutMs);
|
|
9578
|
+
const wait = (0, import_InclusionProofUtils.waitInclusionProof)(
|
|
9579
|
+
deps.client,
|
|
9580
|
+
deps.trustBase,
|
|
9581
|
+
deps.predicateVerifier,
|
|
9582
|
+
transaction,
|
|
9583
|
+
lateDeliveringSignal(controller.signal),
|
|
9584
|
+
deps.intervalMs
|
|
9585
|
+
);
|
|
9586
|
+
wait.catch(() => void 0);
|
|
9587
|
+
try {
|
|
9588
|
+
return await Promise.race([wait, rejectOnAbort(controller.signal)]);
|
|
9589
|
+
} finally {
|
|
9590
|
+
clearTimeout(timer);
|
|
9591
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9592
|
+
}
|
|
9593
|
+
}
|
|
9594
|
+
|
|
9514
9595
|
// token-engine/errors.ts
|
|
9515
9596
|
var TransferConflictError = class extends SphereError {
|
|
9516
9597
|
/**
|
|
@@ -9735,11 +9816,13 @@ var SphereTokenEngine = class {
|
|
|
9735
9816
|
constructor(deps) {
|
|
9736
9817
|
this.deps = deps;
|
|
9737
9818
|
this.privateKeyHex = import_HexConverter.HexConverter.encode(deps.privateKey);
|
|
9819
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9738
9820
|
}
|
|
9739
9821
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9740
9822
|
deterministicMint = true;
|
|
9741
9823
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9742
9824
|
privateKeyHex;
|
|
9825
|
+
proofTimeoutMs;
|
|
9743
9826
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9744
9827
|
getIdentity() {
|
|
9745
9828
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -10082,14 +10165,17 @@ var SphereTokenEngine = class {
|
|
|
10082
10165
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
10083
10166
|
}
|
|
10084
10167
|
try {
|
|
10085
|
-
return await (
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10168
|
+
return await awaitProofBounded(
|
|
10169
|
+
{
|
|
10170
|
+
client: this.deps.client,
|
|
10171
|
+
trustBase: this.deps.trustBase,
|
|
10172
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10173
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10174
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10175
|
+
// #683
|
|
10176
|
+
},
|
|
10089
10177
|
transaction,
|
|
10090
|
-
options?.signal
|
|
10091
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10092
|
-
// #683 finer poll cadence
|
|
10178
|
+
options?.signal
|
|
10093
10179
|
);
|
|
10094
10180
|
} catch (err) {
|
|
10095
10181
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -10105,6 +10191,13 @@ var SphereTokenEngine = class {
|
|
|
10105
10191
|
);
|
|
10106
10192
|
}
|
|
10107
10193
|
}
|
|
10194
|
+
/**
|
|
10195
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10196
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10197
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10198
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10199
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10200
|
+
*/
|
|
10108
10201
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
10109
10202
|
assertOwned(token) {
|
|
10110
10203
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10204,6 +10297,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10204
10297
|
networkId: trustBase.networkId,
|
|
10205
10298
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10206
10299
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10300
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10301
|
+
// #739: was declared but never wired
|
|
10207
10302
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10208
10303
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10209
10304
|
};
|
|
@@ -12113,6 +12208,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
12113
12208
|
}
|
|
12114
12209
|
return out;
|
|
12115
12210
|
}
|
|
12211
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12212
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12213
|
+
let complete = true;
|
|
12214
|
+
for (const entry of await stores.backstop.list()) {
|
|
12215
|
+
if (entry.disposition !== "open") continue;
|
|
12216
|
+
let payload = null;
|
|
12217
|
+
try {
|
|
12218
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12219
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12220
|
+
} catch {
|
|
12221
|
+
complete = false;
|
|
12222
|
+
continue;
|
|
12223
|
+
}
|
|
12224
|
+
if (payload === null) {
|
|
12225
|
+
complete = false;
|
|
12226
|
+
continue;
|
|
12227
|
+
}
|
|
12228
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12229
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12230
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12231
|
+
}
|
|
12232
|
+
return { open: holds, complete };
|
|
12233
|
+
}
|
|
12116
12234
|
function groupJournal(journal) {
|
|
12117
12235
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
12118
12236
|
for (const entry of journal) {
|
|
@@ -12282,11 +12400,14 @@ function toAsset(coinId, totals, registry) {
|
|
|
12282
12400
|
fiatValueEur: null
|
|
12283
12401
|
};
|
|
12284
12402
|
}
|
|
12403
|
+
function priceIdOf(registry, coinId) {
|
|
12404
|
+
return registry.getName(coinId).toLowerCase();
|
|
12405
|
+
}
|
|
12285
12406
|
async function withPrices(raw, registry, price) {
|
|
12286
12407
|
try {
|
|
12287
|
-
const
|
|
12288
|
-
const quotes = await price.getPrices(
|
|
12289
|
-
return raw.map((a) => priceOne(a, quotes.get(registry
|
|
12408
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12409
|
+
const quotes = await price.getPrices(ids);
|
|
12410
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12290
12411
|
} catch {
|
|
12291
12412
|
return raw;
|
|
12292
12413
|
}
|
|
@@ -12520,10 +12641,21 @@ var InventoryView = class {
|
|
|
12520
12641
|
release(tokenId) {
|
|
12521
12642
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12522
12643
|
}
|
|
12644
|
+
pinned(tokenId) {
|
|
12645
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12646
|
+
}
|
|
12647
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12648
|
+
held(tokenId) {
|
|
12649
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12650
|
+
}
|
|
12651
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12652
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12653
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12523
12654
|
pool(coinId) {
|
|
12524
12655
|
const out = [];
|
|
12525
12656
|
for (const [tokenId, entry] of this.mirror) {
|
|
12526
|
-
if (entry.status !== "active"
|
|
12657
|
+
if (entry.status !== "active") continue;
|
|
12658
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12527
12659
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12528
12660
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12529
12661
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12539,7 +12671,7 @@ var InventoryView = class {
|
|
|
12539
12671
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12540
12672
|
out.push(
|
|
12541
12673
|
toToken(tokenId, entry, asset, registry, {
|
|
12542
|
-
transferring: this.
|
|
12674
|
+
transferring: this.held(tokenId),
|
|
12543
12675
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12544
12676
|
})
|
|
12545
12677
|
);
|
|
@@ -12547,7 +12679,7 @@ var InventoryView = class {
|
|
|
12547
12679
|
return out;
|
|
12548
12680
|
}
|
|
12549
12681
|
async assets(registry, price) {
|
|
12550
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12682
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12551
12683
|
if (!price || raw.length === 0) return raw;
|
|
12552
12684
|
return withPrices(raw, registry, price);
|
|
12553
12685
|
}
|
|
@@ -13233,6 +13365,20 @@ var Requests = class {
|
|
|
13233
13365
|
var ReservationLedger = class {
|
|
13234
13366
|
reservations = /* @__PURE__ */ new Map();
|
|
13235
13367
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13368
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13369
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13370
|
+
authoritative = false;
|
|
13371
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13372
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13373
|
+
setAuthoritative(value) {
|
|
13374
|
+
const moved = this.authoritative !== value;
|
|
13375
|
+
this.authoritative = value;
|
|
13376
|
+
return moved;
|
|
13377
|
+
}
|
|
13378
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13379
|
+
unprovenReason() {
|
|
13380
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13381
|
+
}
|
|
13236
13382
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13237
13383
|
reserve(reservationId, entries) {
|
|
13238
13384
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13259,6 +13405,13 @@ var ReservationLedger = class {
|
|
|
13259
13405
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13260
13406
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13261
13407
|
}
|
|
13408
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13409
|
+
holderOf(tokenId) {
|
|
13410
|
+
return this.tokenHolder.get(tokenId);
|
|
13411
|
+
}
|
|
13412
|
+
tokensOf(reservationId) {
|
|
13413
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13414
|
+
}
|
|
13262
13415
|
clear() {
|
|
13263
13416
|
this.reservations.clear();
|
|
13264
13417
|
this.tokenHolder.clear();
|
|
@@ -13271,6 +13424,44 @@ var ReservationLedger = class {
|
|
|
13271
13424
|
}
|
|
13272
13425
|
};
|
|
13273
13426
|
|
|
13427
|
+
// modules/payments-v2/select/pins.ts
|
|
13428
|
+
var IntentPins = class {
|
|
13429
|
+
constructor(deps) {
|
|
13430
|
+
this.deps = deps;
|
|
13431
|
+
}
|
|
13432
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13433
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13434
|
+
adopt(transferId) {
|
|
13435
|
+
this.pinned.add(transferId);
|
|
13436
|
+
}
|
|
13437
|
+
async sync() {
|
|
13438
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13439
|
+
let changed = false;
|
|
13440
|
+
for (const transferId of [...this.pinned]) {
|
|
13441
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13442
|
+
this.pinned.delete(transferId);
|
|
13443
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13444
|
+
this.deps.ledger.cancel(transferId);
|
|
13445
|
+
changed = true;
|
|
13446
|
+
}
|
|
13447
|
+
for (const [transferId, tokenIds] of open) {
|
|
13448
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13449
|
+
this.pinned.add(transferId);
|
|
13450
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13451
|
+
}
|
|
13452
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13453
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13454
|
+
}
|
|
13455
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13456
|
+
pin(transferId, tokenIds) {
|
|
13457
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13458
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13459
|
+
if (free.length === 0) return false;
|
|
13460
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13461
|
+
return true;
|
|
13462
|
+
}
|
|
13463
|
+
};
|
|
13464
|
+
|
|
13274
13465
|
// modules/payments-v2/select/selector.ts
|
|
13275
13466
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13276
13467
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13361,6 +13552,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13361
13552
|
// modules/payments-v2/select/queue.ts
|
|
13362
13553
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13363
13554
|
var QUEUE_MAX_SIZE = 100;
|
|
13555
|
+
function insufficientBalance(view, amount) {
|
|
13556
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13557
|
+
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";
|
|
13558
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13559
|
+
}
|
|
13364
13560
|
var SpendQueue = class {
|
|
13365
13561
|
constructor(deps) {
|
|
13366
13562
|
this.deps = deps;
|
|
@@ -13373,12 +13569,10 @@ var SpendQueue = class {
|
|
|
13373
13569
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13374
13570
|
}
|
|
13375
13571
|
const amount = parseAmount(request.amount);
|
|
13376
|
-
const
|
|
13572
|
+
const view = this.freeView(request.coinId);
|
|
13573
|
+
const { freeView, freeTotal } = view;
|
|
13377
13574
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13378
|
-
throw
|
|
13379
|
-
"Insufficient balance for this transaction",
|
|
13380
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13381
|
-
);
|
|
13575
|
+
throw insufficientBalance(view, amount);
|
|
13382
13576
|
}
|
|
13383
13577
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13384
13578
|
if (plan !== null) {
|
|
@@ -13428,11 +13622,10 @@ var SpendQueue = class {
|
|
|
13428
13622
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13429
13623
|
return "rejected";
|
|
13430
13624
|
}
|
|
13431
|
-
const
|
|
13625
|
+
const view = this.freeView(entry.coinId);
|
|
13626
|
+
const { freeView, freeTotal } = view;
|
|
13432
13627
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13433
|
-
entry.reject(
|
|
13434
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13435
|
-
);
|
|
13628
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13436
13629
|
return "rejected";
|
|
13437
13630
|
}
|
|
13438
13631
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13477,18 +13670,26 @@ var SpendQueue = class {
|
|
|
13477
13670
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13478
13671
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13479
13672
|
}
|
|
13480
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13673
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13674
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13481
13675
|
freeView(coinId) {
|
|
13482
13676
|
const freeView = [];
|
|
13483
13677
|
let freeTotal = 0n;
|
|
13678
|
+
let pinnedTotal = 0n;
|
|
13679
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13680
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13484
13681
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13485
13682
|
if (entry.amount <= 0n) continue;
|
|
13486
|
-
|
|
13683
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13684
|
+
if (holder === void 0) {
|
|
13487
13685
|
freeView.push(entry);
|
|
13488
13686
|
freeTotal += entry.amount;
|
|
13687
|
+
} else {
|
|
13688
|
+
pinnedTotal += entry.amount;
|
|
13689
|
+
holders.add(holder);
|
|
13489
13690
|
}
|
|
13490
13691
|
}
|
|
13491
|
-
return { freeView, freeTotal };
|
|
13692
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13492
13693
|
}
|
|
13493
13694
|
expectedChangeTotal(coinId) {
|
|
13494
13695
|
let total = 0n;
|
|
@@ -13530,13 +13731,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13530
13731
|
}
|
|
13531
13732
|
function composeFacadeParts(deps, hooks) {
|
|
13532
13733
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13734
|
+
const ledger = new ReservationLedger();
|
|
13533
13735
|
const view = new InventoryView({
|
|
13534
13736
|
port: deps.storagePort,
|
|
13535
13737
|
kv: deps.kv,
|
|
13536
13738
|
emit: (event) => deps.emit(event, {}),
|
|
13739
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13740
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13741
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13742
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13537
13743
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13538
13744
|
});
|
|
13539
|
-
const ledger = new ReservationLedger();
|
|
13540
13745
|
const queue = new SpendQueue({
|
|
13541
13746
|
ledger,
|
|
13542
13747
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13557,6 +13762,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13557
13762
|
ownPubkeyBytes,
|
|
13558
13763
|
view,
|
|
13559
13764
|
ledger,
|
|
13765
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13560
13766
|
queue,
|
|
13561
13767
|
historyStore,
|
|
13562
13768
|
machineStores,
|
|
@@ -13568,6 +13774,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13568
13774
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13569
13775
|
};
|
|
13570
13776
|
}
|
|
13777
|
+
function buildPins(deps, hooks, parts) {
|
|
13778
|
+
return new IntentPins({
|
|
13779
|
+
ledger: parts.ledger,
|
|
13780
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13781
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13782
|
+
release: (tokenId) => {
|
|
13783
|
+
parts.view.release(tokenId);
|
|
13784
|
+
},
|
|
13785
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13786
|
+
});
|
|
13787
|
+
}
|
|
13571
13788
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13572
13789
|
return {
|
|
13573
13790
|
stores: machineStores,
|
|
@@ -13677,7 +13894,8 @@ var PaymentsFacade = class {
|
|
|
13677
13894
|
this.deps = deps;
|
|
13678
13895
|
const parts = composeFacadeParts(deps, {
|
|
13679
13896
|
engine: () => this.engine(),
|
|
13680
|
-
send: (request) => this.send(request)
|
|
13897
|
+
send: (request) => this.send(request),
|
|
13898
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13681
13899
|
});
|
|
13682
13900
|
this.view = parts.view;
|
|
13683
13901
|
this.ledger = parts.ledger;
|
|
@@ -13691,6 +13909,7 @@ var PaymentsFacade = class {
|
|
|
13691
13909
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13692
13910
|
this.restoreDeps = parts.restoreDeps;
|
|
13693
13911
|
this.requests = parts.requests;
|
|
13912
|
+
this.pins = parts.pins;
|
|
13694
13913
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13695
13914
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13696
13915
|
now: () => this.nowMs(),
|
|
@@ -13733,6 +13952,8 @@ var PaymentsFacade = class {
|
|
|
13733
13952
|
passChain = new SerialChain();
|
|
13734
13953
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13735
13954
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13955
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13956
|
+
pins;
|
|
13736
13957
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13737
13958
|
async start() {
|
|
13738
13959
|
if (this.started) return;
|
|
@@ -13755,6 +13976,7 @@ var PaymentsFacade = class {
|
|
|
13755
13976
|
});
|
|
13756
13977
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13757
13978
|
await this.deps.session.start();
|
|
13979
|
+
await this.pins.sync().catch(() => void 0);
|
|
13758
13980
|
this.trackTail(this.runConvergencePass());
|
|
13759
13981
|
this.trackTail(this.seedHeldStates());
|
|
13760
13982
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13825,6 +14047,7 @@ var PaymentsFacade = class {
|
|
|
13825
14047
|
}
|
|
13826
14048
|
async convergeBody() {
|
|
13827
14049
|
const outcome = await this.converger.convergeOnce();
|
|
14050
|
+
await this.pins.sync().catch(() => void 0);
|
|
13828
14051
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13829
14052
|
this.heartbeat.settle(outcome, pending);
|
|
13830
14053
|
}
|
|
@@ -14064,6 +14287,7 @@ var PaymentsFacade = class {
|
|
|
14064
14287
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
14065
14288
|
settleKeepOpen(ctx) {
|
|
14066
14289
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14290
|
+
this.pins.adopt(ctx.transferId);
|
|
14067
14291
|
this.heartbeat.arm();
|
|
14068
14292
|
}
|
|
14069
14293
|
settlePartial(ctx, partial) {
|