@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.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) {
|
|
@@ -12154,11 +12272,14 @@ function toAsset(coinId, totals, registry) {
|
|
|
12154
12272
|
fiatValueEur: null
|
|
12155
12273
|
};
|
|
12156
12274
|
}
|
|
12275
|
+
function priceIdOf(registry, coinId) {
|
|
12276
|
+
return registry.getName(coinId).toLowerCase();
|
|
12277
|
+
}
|
|
12157
12278
|
async function withPrices(raw, registry, price) {
|
|
12158
12279
|
try {
|
|
12159
|
-
const
|
|
12160
|
-
const quotes = await price.getPrices(
|
|
12161
|
-
return raw.map((a) => priceOne(a, quotes.get(registry
|
|
12280
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12281
|
+
const quotes = await price.getPrices(ids);
|
|
12282
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12162
12283
|
} catch {
|
|
12163
12284
|
return raw;
|
|
12164
12285
|
}
|
|
@@ -12392,10 +12513,21 @@ var InventoryView = class {
|
|
|
12392
12513
|
release(tokenId) {
|
|
12393
12514
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12394
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.
|
|
12395
12526
|
pool(coinId) {
|
|
12396
12527
|
const out = [];
|
|
12397
12528
|
for (const [tokenId, entry] of this.mirror) {
|
|
12398
|
-
if (entry.status !== "active"
|
|
12529
|
+
if (entry.status !== "active") continue;
|
|
12530
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12399
12531
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12400
12532
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12401
12533
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12411,7 +12543,7 @@ var InventoryView = class {
|
|
|
12411
12543
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12412
12544
|
out.push(
|
|
12413
12545
|
toToken(tokenId, entry, asset, registry, {
|
|
12414
|
-
transferring: this.
|
|
12546
|
+
transferring: this.held(tokenId),
|
|
12415
12547
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12416
12548
|
})
|
|
12417
12549
|
);
|
|
@@ -12419,7 +12551,7 @@ var InventoryView = class {
|
|
|
12419
12551
|
return out;
|
|
12420
12552
|
}
|
|
12421
12553
|
async assets(registry, price) {
|
|
12422
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12554
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12423
12555
|
if (!price || raw.length === 0) return raw;
|
|
12424
12556
|
return withPrices(raw, registry, price);
|
|
12425
12557
|
}
|
|
@@ -13105,6 +13237,20 @@ var Requests = class {
|
|
|
13105
13237
|
var ReservationLedger = class {
|
|
13106
13238
|
reservations = /* @__PURE__ */ new Map();
|
|
13107
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
|
+
}
|
|
13108
13254
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13109
13255
|
reserve(reservationId, entries) {
|
|
13110
13256
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13131,6 +13277,13 @@ var ReservationLedger = class {
|
|
|
13131
13277
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13132
13278
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13133
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
|
+
}
|
|
13134
13287
|
clear() {
|
|
13135
13288
|
this.reservations.clear();
|
|
13136
13289
|
this.tokenHolder.clear();
|
|
@@ -13143,6 +13296,44 @@ var ReservationLedger = class {
|
|
|
13143
13296
|
}
|
|
13144
13297
|
};
|
|
13145
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
|
+
|
|
13146
13337
|
// modules/payments-v2/select/selector.ts
|
|
13147
13338
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13148
13339
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13233,6 +13424,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13233
13424
|
// modules/payments-v2/select/queue.ts
|
|
13234
13425
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13235
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
|
+
}
|
|
13236
13432
|
var SpendQueue = class {
|
|
13237
13433
|
constructor(deps) {
|
|
13238
13434
|
this.deps = deps;
|
|
@@ -13245,12 +13441,10 @@ var SpendQueue = class {
|
|
|
13245
13441
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13246
13442
|
}
|
|
13247
13443
|
const amount = parseAmount(request.amount);
|
|
13248
|
-
const
|
|
13444
|
+
const view = this.freeView(request.coinId);
|
|
13445
|
+
const { freeView, freeTotal } = view;
|
|
13249
13446
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13250
|
-
throw
|
|
13251
|
-
"Insufficient balance for this transaction",
|
|
13252
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13253
|
-
);
|
|
13447
|
+
throw insufficientBalance(view, amount);
|
|
13254
13448
|
}
|
|
13255
13449
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13256
13450
|
if (plan !== null) {
|
|
@@ -13300,11 +13494,10 @@ var SpendQueue = class {
|
|
|
13300
13494
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13301
13495
|
return "rejected";
|
|
13302
13496
|
}
|
|
13303
|
-
const
|
|
13497
|
+
const view = this.freeView(entry.coinId);
|
|
13498
|
+
const { freeView, freeTotal } = view;
|
|
13304
13499
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13305
|
-
entry.reject(
|
|
13306
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13307
|
-
);
|
|
13500
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13308
13501
|
return "rejected";
|
|
13309
13502
|
}
|
|
13310
13503
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13349,18 +13542,26 @@ var SpendQueue = class {
|
|
|
13349
13542
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13350
13543
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13351
13544
|
}
|
|
13352
|
-
// 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).
|
|
13353
13547
|
freeView(coinId) {
|
|
13354
13548
|
const freeView = [];
|
|
13355
13549
|
let freeTotal = 0n;
|
|
13550
|
+
let pinnedTotal = 0n;
|
|
13551
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13552
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13356
13553
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13357
13554
|
if (entry.amount <= 0n) continue;
|
|
13358
|
-
|
|
13555
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13556
|
+
if (holder === void 0) {
|
|
13359
13557
|
freeView.push(entry);
|
|
13360
13558
|
freeTotal += entry.amount;
|
|
13559
|
+
} else {
|
|
13560
|
+
pinnedTotal += entry.amount;
|
|
13561
|
+
holders.add(holder);
|
|
13361
13562
|
}
|
|
13362
13563
|
}
|
|
13363
|
-
return { freeView, freeTotal };
|
|
13564
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13364
13565
|
}
|
|
13365
13566
|
expectedChangeTotal(coinId) {
|
|
13366
13567
|
let total = 0n;
|
|
@@ -13402,13 +13603,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13402
13603
|
}
|
|
13403
13604
|
function composeFacadeParts(deps, hooks) {
|
|
13404
13605
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13606
|
+
const ledger = new ReservationLedger();
|
|
13405
13607
|
const view = new InventoryView({
|
|
13406
13608
|
port: deps.storagePort,
|
|
13407
13609
|
kv: deps.kv,
|
|
13408
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,
|
|
13409
13615
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13410
13616
|
});
|
|
13411
|
-
const ledger = new ReservationLedger();
|
|
13412
13617
|
const queue = new SpendQueue({
|
|
13413
13618
|
ledger,
|
|
13414
13619
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13429,6 +13634,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13429
13634
|
ownPubkeyBytes,
|
|
13430
13635
|
view,
|
|
13431
13636
|
ledger,
|
|
13637
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13432
13638
|
queue,
|
|
13433
13639
|
historyStore,
|
|
13434
13640
|
machineStores,
|
|
@@ -13440,6 +13646,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13440
13646
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13441
13647
|
};
|
|
13442
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
|
+
}
|
|
13443
13660
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13444
13661
|
return {
|
|
13445
13662
|
stores: machineStores,
|
|
@@ -13549,7 +13766,8 @@ var PaymentsFacade = class {
|
|
|
13549
13766
|
this.deps = deps;
|
|
13550
13767
|
const parts = composeFacadeParts(deps, {
|
|
13551
13768
|
engine: () => this.engine(),
|
|
13552
|
-
send: (request) => this.send(request)
|
|
13769
|
+
send: (request) => this.send(request),
|
|
13770
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13553
13771
|
});
|
|
13554
13772
|
this.view = parts.view;
|
|
13555
13773
|
this.ledger = parts.ledger;
|
|
@@ -13563,6 +13781,7 @@ var PaymentsFacade = class {
|
|
|
13563
13781
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13564
13782
|
this.restoreDeps = parts.restoreDeps;
|
|
13565
13783
|
this.requests = parts.requests;
|
|
13784
|
+
this.pins = parts.pins;
|
|
13566
13785
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13567
13786
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13568
13787
|
now: () => this.nowMs(),
|
|
@@ -13605,6 +13824,8 @@ var PaymentsFacade = class {
|
|
|
13605
13824
|
passChain = new SerialChain();
|
|
13606
13825
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13607
13826
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13827
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13828
|
+
pins;
|
|
13608
13829
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13609
13830
|
async start() {
|
|
13610
13831
|
if (this.started) return;
|
|
@@ -13627,6 +13848,7 @@ var PaymentsFacade = class {
|
|
|
13627
13848
|
});
|
|
13628
13849
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13629
13850
|
await this.deps.session.start();
|
|
13851
|
+
await this.pins.sync().catch(() => void 0);
|
|
13630
13852
|
this.trackTail(this.runConvergencePass());
|
|
13631
13853
|
this.trackTail(this.seedHeldStates());
|
|
13632
13854
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13697,6 +13919,7 @@ var PaymentsFacade = class {
|
|
|
13697
13919
|
}
|
|
13698
13920
|
async convergeBody() {
|
|
13699
13921
|
const outcome = await this.converger.convergeOnce();
|
|
13922
|
+
await this.pins.sync().catch(() => void 0);
|
|
13700
13923
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13701
13924
|
this.heartbeat.settle(outcome, pending);
|
|
13702
13925
|
}
|
|
@@ -13936,6 +14159,7 @@ var PaymentsFacade = class {
|
|
|
13936
14159
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
13937
14160
|
settleKeepOpen(ctx) {
|
|
13938
14161
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14162
|
+
this.pins.adopt(ctx.transferId);
|
|
13939
14163
|
this.heartbeat.arm();
|
|
13940
14164
|
}
|
|
13941
14165
|
settlePartial(ctx, partial) {
|