@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/core/index.cjs
CHANGED
|
@@ -9288,6 +9288,7 @@ var import_CertificationResponse = require("@unicitylabs/state-transition-sdk/li
|
|
|
9288
9288
|
var import_StateId = require("@unicitylabs/state-transition-sdk/lib/api/StateId.js");
|
|
9289
9289
|
var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js");
|
|
9290
9290
|
var import_InclusionProofResponse = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js");
|
|
9291
|
+
var import_JsonRpcNetworkError = require("@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js");
|
|
9291
9292
|
var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js");
|
|
9292
9293
|
var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js");
|
|
9293
9294
|
var import_InclusionProofVerificationRule = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js");
|
|
@@ -9453,6 +9454,86 @@ function decodeSpherePaymentData(bytes) {
|
|
|
9453
9454
|
return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
|
|
9454
9455
|
}
|
|
9455
9456
|
|
|
9457
|
+
// token-engine/proof-wait.ts
|
|
9458
|
+
var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
|
|
9459
|
+
function resolveProofTimeoutMs(configured) {
|
|
9460
|
+
if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
|
|
9461
|
+
if (!Number.isFinite(configured) || configured <= 0) {
|
|
9462
|
+
throw new SphereError(
|
|
9463
|
+
`proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
|
|
9464
|
+
"INVALID_CONFIG"
|
|
9465
|
+
);
|
|
9466
|
+
}
|
|
9467
|
+
return configured;
|
|
9468
|
+
}
|
|
9469
|
+
function lateDeliveringSignal(signal) {
|
|
9470
|
+
return new Proxy(signal, {
|
|
9471
|
+
get(target, prop) {
|
|
9472
|
+
if (prop === "addEventListener") {
|
|
9473
|
+
return (type, listener, options) => {
|
|
9474
|
+
if (type === "abort" && target.aborted) {
|
|
9475
|
+
queueMicrotask(() => {
|
|
9476
|
+
listener.call(target, new Event("abort"));
|
|
9477
|
+
});
|
|
9478
|
+
return;
|
|
9479
|
+
}
|
|
9480
|
+
target.addEventListener(type, listener, options);
|
|
9481
|
+
};
|
|
9482
|
+
}
|
|
9483
|
+
const value = Reflect.get(target, prop, target);
|
|
9484
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
9485
|
+
}
|
|
9486
|
+
});
|
|
9487
|
+
}
|
|
9488
|
+
function rejectOnAbort(signal) {
|
|
9489
|
+
return new Promise((_resolve, reject) => {
|
|
9490
|
+
if (signal.aborted) {
|
|
9491
|
+
reject(signal.reason);
|
|
9492
|
+
return;
|
|
9493
|
+
}
|
|
9494
|
+
signal.addEventListener(
|
|
9495
|
+
"abort",
|
|
9496
|
+
() => {
|
|
9497
|
+
reject(signal.reason);
|
|
9498
|
+
},
|
|
9499
|
+
{ once: true }
|
|
9500
|
+
);
|
|
9501
|
+
});
|
|
9502
|
+
}
|
|
9503
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9504
|
+
const controller = new AbortController();
|
|
9505
|
+
const relayAbort = () => {
|
|
9506
|
+
controller.abort(external?.reason);
|
|
9507
|
+
};
|
|
9508
|
+
if (external !== void 0) {
|
|
9509
|
+
if (external.aborted) relayAbort();
|
|
9510
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9511
|
+
}
|
|
9512
|
+
const timer = setTimeout(() => {
|
|
9513
|
+
controller.abort(
|
|
9514
|
+
new SphereError(
|
|
9515
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9516
|
+
"AGGREGATOR_ERROR"
|
|
9517
|
+
)
|
|
9518
|
+
);
|
|
9519
|
+
}, deps.timeoutMs);
|
|
9520
|
+
const wait = (0, import_InclusionProofUtils.waitInclusionProof)(
|
|
9521
|
+
deps.client,
|
|
9522
|
+
deps.trustBase,
|
|
9523
|
+
deps.predicateVerifier,
|
|
9524
|
+
transaction,
|
|
9525
|
+
lateDeliveringSignal(controller.signal),
|
|
9526
|
+
deps.intervalMs
|
|
9527
|
+
);
|
|
9528
|
+
wait.catch(() => void 0);
|
|
9529
|
+
try {
|
|
9530
|
+
return await Promise.race([wait, rejectOnAbort(controller.signal)]);
|
|
9531
|
+
} finally {
|
|
9532
|
+
clearTimeout(timer);
|
|
9533
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9534
|
+
}
|
|
9535
|
+
}
|
|
9536
|
+
|
|
9456
9537
|
// token-engine/errors.ts
|
|
9457
9538
|
var TransferConflictError = class extends SphereError {
|
|
9458
9539
|
/**
|
|
@@ -9677,11 +9758,13 @@ var SphereTokenEngine = class {
|
|
|
9677
9758
|
constructor(deps) {
|
|
9678
9759
|
this.deps = deps;
|
|
9679
9760
|
this.privateKeyHex = import_HexConverter.HexConverter.encode(deps.privateKey);
|
|
9761
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9680
9762
|
}
|
|
9681
9763
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9682
9764
|
deterministicMint = true;
|
|
9683
9765
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9684
9766
|
privateKeyHex;
|
|
9767
|
+
proofTimeoutMs;
|
|
9685
9768
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9686
9769
|
getIdentity() {
|
|
9687
9770
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -10024,14 +10107,17 @@ var SphereTokenEngine = class {
|
|
|
10024
10107
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
10025
10108
|
}
|
|
10026
10109
|
try {
|
|
10027
|
-
return await (
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10110
|
+
return await awaitProofBounded(
|
|
10111
|
+
{
|
|
10112
|
+
client: this.deps.client,
|
|
10113
|
+
trustBase: this.deps.trustBase,
|
|
10114
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10115
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10116
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10117
|
+
// #683
|
|
10118
|
+
},
|
|
10031
10119
|
transaction,
|
|
10032
|
-
options?.signal
|
|
10033
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10034
|
-
// #683 finer poll cadence
|
|
10120
|
+
options?.signal
|
|
10035
10121
|
);
|
|
10036
10122
|
} catch (err) {
|
|
10037
10123
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -10047,6 +10133,13 @@ var SphereTokenEngine = class {
|
|
|
10047
10133
|
);
|
|
10048
10134
|
}
|
|
10049
10135
|
}
|
|
10136
|
+
/**
|
|
10137
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10138
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10139
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10140
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10141
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10142
|
+
*/
|
|
10050
10143
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
10051
10144
|
assertOwned(token) {
|
|
10052
10145
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10146,6 +10239,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10146
10239
|
networkId: trustBase.networkId,
|
|
10147
10240
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10148
10241
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10242
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10243
|
+
// #739: was declared but never wired
|
|
10149
10244
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10150
10245
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10151
10246
|
};
|
|
@@ -12055,6 +12150,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
12055
12150
|
}
|
|
12056
12151
|
return out;
|
|
12057
12152
|
}
|
|
12153
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12154
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12155
|
+
let complete = true;
|
|
12156
|
+
for (const entry of await stores.backstop.list()) {
|
|
12157
|
+
if (entry.disposition !== "open") continue;
|
|
12158
|
+
let payload = null;
|
|
12159
|
+
try {
|
|
12160
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12161
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12162
|
+
} catch {
|
|
12163
|
+
complete = false;
|
|
12164
|
+
continue;
|
|
12165
|
+
}
|
|
12166
|
+
if (payload === null) {
|
|
12167
|
+
complete = false;
|
|
12168
|
+
continue;
|
|
12169
|
+
}
|
|
12170
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12171
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12172
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12173
|
+
}
|
|
12174
|
+
return { open: holds, complete };
|
|
12175
|
+
}
|
|
12058
12176
|
function groupJournal(journal) {
|
|
12059
12177
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
12060
12178
|
for (const entry of journal) {
|
|
@@ -12224,11 +12342,14 @@ function toAsset(coinId, totals, registry) {
|
|
|
12224
12342
|
fiatValueEur: null
|
|
12225
12343
|
};
|
|
12226
12344
|
}
|
|
12345
|
+
function priceIdOf(registry, coinId) {
|
|
12346
|
+
return registry.getName(coinId).toLowerCase();
|
|
12347
|
+
}
|
|
12227
12348
|
async function withPrices(raw, registry, price) {
|
|
12228
12349
|
try {
|
|
12229
|
-
const
|
|
12230
|
-
const quotes = await price.getPrices(
|
|
12231
|
-
return raw.map((a) => priceOne(a, quotes.get(registry
|
|
12350
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12351
|
+
const quotes = await price.getPrices(ids);
|
|
12352
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12232
12353
|
} catch {
|
|
12233
12354
|
return raw;
|
|
12234
12355
|
}
|
|
@@ -12462,10 +12583,21 @@ var InventoryView = class {
|
|
|
12462
12583
|
release(tokenId) {
|
|
12463
12584
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12464
12585
|
}
|
|
12586
|
+
pinned(tokenId) {
|
|
12587
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12588
|
+
}
|
|
12589
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12590
|
+
held(tokenId) {
|
|
12591
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12592
|
+
}
|
|
12593
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12594
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12595
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12465
12596
|
pool(coinId) {
|
|
12466
12597
|
const out = [];
|
|
12467
12598
|
for (const [tokenId, entry] of this.mirror) {
|
|
12468
|
-
if (entry.status !== "active"
|
|
12599
|
+
if (entry.status !== "active") continue;
|
|
12600
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12469
12601
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12470
12602
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12471
12603
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12481,7 +12613,7 @@ var InventoryView = class {
|
|
|
12481
12613
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12482
12614
|
out.push(
|
|
12483
12615
|
toToken(tokenId, entry, asset, registry, {
|
|
12484
|
-
transferring: this.
|
|
12616
|
+
transferring: this.held(tokenId),
|
|
12485
12617
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12486
12618
|
})
|
|
12487
12619
|
);
|
|
@@ -12489,7 +12621,7 @@ var InventoryView = class {
|
|
|
12489
12621
|
return out;
|
|
12490
12622
|
}
|
|
12491
12623
|
async assets(registry, price) {
|
|
12492
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12624
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12493
12625
|
if (!price || raw.length === 0) return raw;
|
|
12494
12626
|
return withPrices(raw, registry, price);
|
|
12495
12627
|
}
|
|
@@ -13175,6 +13307,20 @@ var Requests = class {
|
|
|
13175
13307
|
var ReservationLedger = class {
|
|
13176
13308
|
reservations = /* @__PURE__ */ new Map();
|
|
13177
13309
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13310
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13311
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13312
|
+
authoritative = false;
|
|
13313
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13314
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13315
|
+
setAuthoritative(value) {
|
|
13316
|
+
const moved = this.authoritative !== value;
|
|
13317
|
+
this.authoritative = value;
|
|
13318
|
+
return moved;
|
|
13319
|
+
}
|
|
13320
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13321
|
+
unprovenReason() {
|
|
13322
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13323
|
+
}
|
|
13178
13324
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13179
13325
|
reserve(reservationId, entries) {
|
|
13180
13326
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13201,6 +13347,13 @@ var ReservationLedger = class {
|
|
|
13201
13347
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13202
13348
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13203
13349
|
}
|
|
13350
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13351
|
+
holderOf(tokenId) {
|
|
13352
|
+
return this.tokenHolder.get(tokenId);
|
|
13353
|
+
}
|
|
13354
|
+
tokensOf(reservationId) {
|
|
13355
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13356
|
+
}
|
|
13204
13357
|
clear() {
|
|
13205
13358
|
this.reservations.clear();
|
|
13206
13359
|
this.tokenHolder.clear();
|
|
@@ -13213,6 +13366,44 @@ var ReservationLedger = class {
|
|
|
13213
13366
|
}
|
|
13214
13367
|
};
|
|
13215
13368
|
|
|
13369
|
+
// modules/payments-v2/select/pins.ts
|
|
13370
|
+
var IntentPins = class {
|
|
13371
|
+
constructor(deps) {
|
|
13372
|
+
this.deps = deps;
|
|
13373
|
+
}
|
|
13374
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13375
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13376
|
+
adopt(transferId) {
|
|
13377
|
+
this.pinned.add(transferId);
|
|
13378
|
+
}
|
|
13379
|
+
async sync() {
|
|
13380
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13381
|
+
let changed = false;
|
|
13382
|
+
for (const transferId of [...this.pinned]) {
|
|
13383
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13384
|
+
this.pinned.delete(transferId);
|
|
13385
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13386
|
+
this.deps.ledger.cancel(transferId);
|
|
13387
|
+
changed = true;
|
|
13388
|
+
}
|
|
13389
|
+
for (const [transferId, tokenIds] of open) {
|
|
13390
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13391
|
+
this.pinned.add(transferId);
|
|
13392
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13393
|
+
}
|
|
13394
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13395
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13396
|
+
}
|
|
13397
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13398
|
+
pin(transferId, tokenIds) {
|
|
13399
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13400
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13401
|
+
if (free.length === 0) return false;
|
|
13402
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13403
|
+
return true;
|
|
13404
|
+
}
|
|
13405
|
+
};
|
|
13406
|
+
|
|
13216
13407
|
// modules/payments-v2/select/selector.ts
|
|
13217
13408
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13218
13409
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13303,6 +13494,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13303
13494
|
// modules/payments-v2/select/queue.ts
|
|
13304
13495
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13305
13496
|
var QUEUE_MAX_SIZE = 100;
|
|
13497
|
+
function insufficientBalance(view, amount) {
|
|
13498
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13499
|
+
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";
|
|
13500
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13501
|
+
}
|
|
13306
13502
|
var SpendQueue = class {
|
|
13307
13503
|
constructor(deps) {
|
|
13308
13504
|
this.deps = deps;
|
|
@@ -13315,12 +13511,10 @@ var SpendQueue = class {
|
|
|
13315
13511
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13316
13512
|
}
|
|
13317
13513
|
const amount = parseAmount(request.amount);
|
|
13318
|
-
const
|
|
13514
|
+
const view = this.freeView(request.coinId);
|
|
13515
|
+
const { freeView, freeTotal } = view;
|
|
13319
13516
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13320
|
-
throw
|
|
13321
|
-
"Insufficient balance for this transaction",
|
|
13322
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13323
|
-
);
|
|
13517
|
+
throw insufficientBalance(view, amount);
|
|
13324
13518
|
}
|
|
13325
13519
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13326
13520
|
if (plan !== null) {
|
|
@@ -13370,11 +13564,10 @@ var SpendQueue = class {
|
|
|
13370
13564
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13371
13565
|
return "rejected";
|
|
13372
13566
|
}
|
|
13373
|
-
const
|
|
13567
|
+
const view = this.freeView(entry.coinId);
|
|
13568
|
+
const { freeView, freeTotal } = view;
|
|
13374
13569
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13375
|
-
entry.reject(
|
|
13376
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13377
|
-
);
|
|
13570
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13378
13571
|
return "rejected";
|
|
13379
13572
|
}
|
|
13380
13573
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13419,18 +13612,26 @@ var SpendQueue = class {
|
|
|
13419
13612
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13420
13613
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13421
13614
|
}
|
|
13422
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13615
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13616
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13423
13617
|
freeView(coinId) {
|
|
13424
13618
|
const freeView = [];
|
|
13425
13619
|
let freeTotal = 0n;
|
|
13620
|
+
let pinnedTotal = 0n;
|
|
13621
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13622
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13426
13623
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13427
13624
|
if (entry.amount <= 0n) continue;
|
|
13428
|
-
|
|
13625
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13626
|
+
if (holder === void 0) {
|
|
13429
13627
|
freeView.push(entry);
|
|
13430
13628
|
freeTotal += entry.amount;
|
|
13629
|
+
} else {
|
|
13630
|
+
pinnedTotal += entry.amount;
|
|
13631
|
+
holders.add(holder);
|
|
13431
13632
|
}
|
|
13432
13633
|
}
|
|
13433
|
-
return { freeView, freeTotal };
|
|
13634
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13434
13635
|
}
|
|
13435
13636
|
expectedChangeTotal(coinId) {
|
|
13436
13637
|
let total = 0n;
|
|
@@ -13472,13 +13673,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13472
13673
|
}
|
|
13473
13674
|
function composeFacadeParts(deps, hooks) {
|
|
13474
13675
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13676
|
+
const ledger = new ReservationLedger();
|
|
13475
13677
|
const view = new InventoryView({
|
|
13476
13678
|
port: deps.storagePort,
|
|
13477
13679
|
kv: deps.kv,
|
|
13478
13680
|
emit: (event) => deps.emit(event, {}),
|
|
13681
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13682
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13683
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13684
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13479
13685
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13480
13686
|
});
|
|
13481
|
-
const ledger = new ReservationLedger();
|
|
13482
13687
|
const queue = new SpendQueue({
|
|
13483
13688
|
ledger,
|
|
13484
13689
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13499,6 +13704,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13499
13704
|
ownPubkeyBytes,
|
|
13500
13705
|
view,
|
|
13501
13706
|
ledger,
|
|
13707
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13502
13708
|
queue,
|
|
13503
13709
|
historyStore,
|
|
13504
13710
|
machineStores,
|
|
@@ -13510,6 +13716,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13510
13716
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13511
13717
|
};
|
|
13512
13718
|
}
|
|
13719
|
+
function buildPins(deps, hooks, parts) {
|
|
13720
|
+
return new IntentPins({
|
|
13721
|
+
ledger: parts.ledger,
|
|
13722
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13723
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13724
|
+
release: (tokenId) => {
|
|
13725
|
+
parts.view.release(tokenId);
|
|
13726
|
+
},
|
|
13727
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13728
|
+
});
|
|
13729
|
+
}
|
|
13513
13730
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13514
13731
|
return {
|
|
13515
13732
|
stores: machineStores,
|
|
@@ -13619,7 +13836,8 @@ var PaymentsFacade = class {
|
|
|
13619
13836
|
this.deps = deps;
|
|
13620
13837
|
const parts = composeFacadeParts(deps, {
|
|
13621
13838
|
engine: () => this.engine(),
|
|
13622
|
-
send: (request) => this.send(request)
|
|
13839
|
+
send: (request) => this.send(request),
|
|
13840
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13623
13841
|
});
|
|
13624
13842
|
this.view = parts.view;
|
|
13625
13843
|
this.ledger = parts.ledger;
|
|
@@ -13633,6 +13851,7 @@ var PaymentsFacade = class {
|
|
|
13633
13851
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13634
13852
|
this.restoreDeps = parts.restoreDeps;
|
|
13635
13853
|
this.requests = parts.requests;
|
|
13854
|
+
this.pins = parts.pins;
|
|
13636
13855
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13637
13856
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13638
13857
|
now: () => this.nowMs(),
|
|
@@ -13675,6 +13894,8 @@ var PaymentsFacade = class {
|
|
|
13675
13894
|
passChain = new SerialChain();
|
|
13676
13895
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13677
13896
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13897
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13898
|
+
pins;
|
|
13678
13899
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13679
13900
|
async start() {
|
|
13680
13901
|
if (this.started) return;
|
|
@@ -13697,6 +13918,7 @@ var PaymentsFacade = class {
|
|
|
13697
13918
|
});
|
|
13698
13919
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13699
13920
|
await this.deps.session.start();
|
|
13921
|
+
await this.pins.sync().catch(() => void 0);
|
|
13700
13922
|
this.trackTail(this.runConvergencePass());
|
|
13701
13923
|
this.trackTail(this.seedHeldStates());
|
|
13702
13924
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13767,6 +13989,7 @@ var PaymentsFacade = class {
|
|
|
13767
13989
|
}
|
|
13768
13990
|
async convergeBody() {
|
|
13769
13991
|
const outcome = await this.converger.convergeOnce();
|
|
13992
|
+
await this.pins.sync().catch(() => void 0);
|
|
13770
13993
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13771
13994
|
this.heartbeat.settle(outcome, pending);
|
|
13772
13995
|
}
|
|
@@ -14006,6 +14229,7 @@ var PaymentsFacade = class {
|
|
|
14006
14229
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
14007
14230
|
settleKeepOpen(ctx) {
|
|
14008
14231
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14232
|
+
this.pins.adopt(ctx.transferId);
|
|
14009
14233
|
this.heartbeat.arm();
|
|
14010
14234
|
}
|
|
14011
14235
|
settlePartial(ctx, partial) {
|