@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.js
CHANGED
|
@@ -9202,6 +9202,7 @@ import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-t
|
|
|
9202
9202
|
import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
|
|
9203
9203
|
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
|
|
9204
9204
|
import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
|
|
9205
|
+
import { JsonRpcNetworkError } from "@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js";
|
|
9205
9206
|
import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
|
|
9206
9207
|
import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
|
|
9207
9208
|
import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
|
|
@@ -9367,6 +9368,86 @@ function decodeSpherePaymentData(bytes) {
|
|
|
9367
9368
|
return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
|
|
9368
9369
|
}
|
|
9369
9370
|
|
|
9371
|
+
// token-engine/proof-wait.ts
|
|
9372
|
+
var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
|
|
9373
|
+
function resolveProofTimeoutMs(configured) {
|
|
9374
|
+
if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
|
|
9375
|
+
if (!Number.isFinite(configured) || configured <= 0) {
|
|
9376
|
+
throw new SphereError(
|
|
9377
|
+
`proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
|
|
9378
|
+
"INVALID_CONFIG"
|
|
9379
|
+
);
|
|
9380
|
+
}
|
|
9381
|
+
return configured;
|
|
9382
|
+
}
|
|
9383
|
+
function lateDeliveringSignal(signal) {
|
|
9384
|
+
return new Proxy(signal, {
|
|
9385
|
+
get(target, prop) {
|
|
9386
|
+
if (prop === "addEventListener") {
|
|
9387
|
+
return (type, listener, options) => {
|
|
9388
|
+
if (type === "abort" && target.aborted) {
|
|
9389
|
+
queueMicrotask(() => {
|
|
9390
|
+
listener.call(target, new Event("abort"));
|
|
9391
|
+
});
|
|
9392
|
+
return;
|
|
9393
|
+
}
|
|
9394
|
+
target.addEventListener(type, listener, options);
|
|
9395
|
+
};
|
|
9396
|
+
}
|
|
9397
|
+
const value = Reflect.get(target, prop, target);
|
|
9398
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
9399
|
+
}
|
|
9400
|
+
});
|
|
9401
|
+
}
|
|
9402
|
+
function rejectOnAbort(signal) {
|
|
9403
|
+
return new Promise((_resolve, reject) => {
|
|
9404
|
+
if (signal.aborted) {
|
|
9405
|
+
reject(signal.reason);
|
|
9406
|
+
return;
|
|
9407
|
+
}
|
|
9408
|
+
signal.addEventListener(
|
|
9409
|
+
"abort",
|
|
9410
|
+
() => {
|
|
9411
|
+
reject(signal.reason);
|
|
9412
|
+
},
|
|
9413
|
+
{ once: true }
|
|
9414
|
+
);
|
|
9415
|
+
});
|
|
9416
|
+
}
|
|
9417
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9418
|
+
const controller = new AbortController();
|
|
9419
|
+
const relayAbort = () => {
|
|
9420
|
+
controller.abort(external?.reason);
|
|
9421
|
+
};
|
|
9422
|
+
if (external !== void 0) {
|
|
9423
|
+
if (external.aborted) relayAbort();
|
|
9424
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9425
|
+
}
|
|
9426
|
+
const timer = setTimeout(() => {
|
|
9427
|
+
controller.abort(
|
|
9428
|
+
new SphereError(
|
|
9429
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9430
|
+
"AGGREGATOR_ERROR"
|
|
9431
|
+
)
|
|
9432
|
+
);
|
|
9433
|
+
}, deps.timeoutMs);
|
|
9434
|
+
const wait = waitInclusionProof(
|
|
9435
|
+
deps.client,
|
|
9436
|
+
deps.trustBase,
|
|
9437
|
+
deps.predicateVerifier,
|
|
9438
|
+
transaction,
|
|
9439
|
+
lateDeliveringSignal(controller.signal),
|
|
9440
|
+
deps.intervalMs
|
|
9441
|
+
);
|
|
9442
|
+
wait.catch(() => void 0);
|
|
9443
|
+
try {
|
|
9444
|
+
return await Promise.race([wait, rejectOnAbort(controller.signal)]);
|
|
9445
|
+
} finally {
|
|
9446
|
+
clearTimeout(timer);
|
|
9447
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9450
|
+
|
|
9370
9451
|
// token-engine/errors.ts
|
|
9371
9452
|
var TransferConflictError = class extends SphereError {
|
|
9372
9453
|
/**
|
|
@@ -9591,11 +9672,13 @@ var SphereTokenEngine = class {
|
|
|
9591
9672
|
constructor(deps) {
|
|
9592
9673
|
this.deps = deps;
|
|
9593
9674
|
this.privateKeyHex = HexConverter.encode(deps.privateKey);
|
|
9675
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9594
9676
|
}
|
|
9595
9677
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9596
9678
|
deterministicMint = true;
|
|
9597
9679
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9598
9680
|
privateKeyHex;
|
|
9681
|
+
proofTimeoutMs;
|
|
9599
9682
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9600
9683
|
getIdentity() {
|
|
9601
9684
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -9938,14 +10021,17 @@ var SphereTokenEngine = class {
|
|
|
9938
10021
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
9939
10022
|
}
|
|
9940
10023
|
try {
|
|
9941
|
-
return await
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
10024
|
+
return await awaitProofBounded(
|
|
10025
|
+
{
|
|
10026
|
+
client: this.deps.client,
|
|
10027
|
+
trustBase: this.deps.trustBase,
|
|
10028
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10029
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10030
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10031
|
+
// #683
|
|
10032
|
+
},
|
|
9945
10033
|
transaction,
|
|
9946
|
-
options?.signal
|
|
9947
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
9948
|
-
// #683 finer poll cadence
|
|
10034
|
+
options?.signal
|
|
9949
10035
|
);
|
|
9950
10036
|
} catch (err) {
|
|
9951
10037
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -9961,6 +10047,13 @@ var SphereTokenEngine = class {
|
|
|
9961
10047
|
);
|
|
9962
10048
|
}
|
|
9963
10049
|
}
|
|
10050
|
+
/**
|
|
10051
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10052
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10053
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10054
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10055
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10056
|
+
*/
|
|
9964
10057
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
9965
10058
|
assertOwned(token) {
|
|
9966
10059
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10060,6 +10153,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10060
10153
|
networkId: trustBase.networkId,
|
|
10061
10154
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10062
10155
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10156
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10157
|
+
// #739: was declared but never wired
|
|
10063
10158
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10064
10159
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10065
10160
|
};
|
|
@@ -11969,6 +12064,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
11969
12064
|
}
|
|
11970
12065
|
return out;
|
|
11971
12066
|
}
|
|
12067
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12068
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12069
|
+
let complete = true;
|
|
12070
|
+
for (const entry of await stores.backstop.list()) {
|
|
12071
|
+
if (entry.disposition !== "open") continue;
|
|
12072
|
+
let payload = null;
|
|
12073
|
+
try {
|
|
12074
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12075
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12076
|
+
} catch {
|
|
12077
|
+
complete = false;
|
|
12078
|
+
continue;
|
|
12079
|
+
}
|
|
12080
|
+
if (payload === null) {
|
|
12081
|
+
complete = false;
|
|
12082
|
+
continue;
|
|
12083
|
+
}
|
|
12084
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12085
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12086
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12087
|
+
}
|
|
12088
|
+
return { open: holds, complete };
|
|
12089
|
+
}
|
|
11972
12090
|
function groupJournal(journal) {
|
|
11973
12091
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
11974
12092
|
for (const entry of journal) {
|
|
@@ -12138,11 +12256,14 @@ function toAsset(coinId, totals, registry) {
|
|
|
12138
12256
|
fiatValueEur: null
|
|
12139
12257
|
};
|
|
12140
12258
|
}
|
|
12259
|
+
function priceIdOf(registry, coinId) {
|
|
12260
|
+
return registry.getName(coinId).toLowerCase();
|
|
12261
|
+
}
|
|
12141
12262
|
async function withPrices(raw, registry, price) {
|
|
12142
12263
|
try {
|
|
12143
|
-
const
|
|
12144
|
-
const quotes = await price.getPrices(
|
|
12145
|
-
return raw.map((a) => priceOne(a, quotes.get(registry
|
|
12264
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12265
|
+
const quotes = await price.getPrices(ids);
|
|
12266
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12146
12267
|
} catch {
|
|
12147
12268
|
return raw;
|
|
12148
12269
|
}
|
|
@@ -12376,10 +12497,21 @@ var InventoryView = class {
|
|
|
12376
12497
|
release(tokenId) {
|
|
12377
12498
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12378
12499
|
}
|
|
12500
|
+
pinned(tokenId) {
|
|
12501
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12502
|
+
}
|
|
12503
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12504
|
+
held(tokenId) {
|
|
12505
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12506
|
+
}
|
|
12507
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12508
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12509
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12379
12510
|
pool(coinId) {
|
|
12380
12511
|
const out = [];
|
|
12381
12512
|
for (const [tokenId, entry] of this.mirror) {
|
|
12382
|
-
if (entry.status !== "active"
|
|
12513
|
+
if (entry.status !== "active") continue;
|
|
12514
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12383
12515
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12384
12516
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12385
12517
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12395,7 +12527,7 @@ var InventoryView = class {
|
|
|
12395
12527
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12396
12528
|
out.push(
|
|
12397
12529
|
toToken(tokenId, entry, asset, registry, {
|
|
12398
|
-
transferring: this.
|
|
12530
|
+
transferring: this.held(tokenId),
|
|
12399
12531
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12400
12532
|
})
|
|
12401
12533
|
);
|
|
@@ -12403,7 +12535,7 @@ var InventoryView = class {
|
|
|
12403
12535
|
return out;
|
|
12404
12536
|
}
|
|
12405
12537
|
async assets(registry, price) {
|
|
12406
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12538
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12407
12539
|
if (!price || raw.length === 0) return raw;
|
|
12408
12540
|
return withPrices(raw, registry, price);
|
|
12409
12541
|
}
|
|
@@ -13089,6 +13221,20 @@ var Requests = class {
|
|
|
13089
13221
|
var ReservationLedger = class {
|
|
13090
13222
|
reservations = /* @__PURE__ */ new Map();
|
|
13091
13223
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13224
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13225
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13226
|
+
authoritative = false;
|
|
13227
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13228
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13229
|
+
setAuthoritative(value) {
|
|
13230
|
+
const moved = this.authoritative !== value;
|
|
13231
|
+
this.authoritative = value;
|
|
13232
|
+
return moved;
|
|
13233
|
+
}
|
|
13234
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13235
|
+
unprovenReason() {
|
|
13236
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13237
|
+
}
|
|
13092
13238
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13093
13239
|
reserve(reservationId, entries) {
|
|
13094
13240
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13115,6 +13261,13 @@ var ReservationLedger = class {
|
|
|
13115
13261
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13116
13262
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13117
13263
|
}
|
|
13264
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13265
|
+
holderOf(tokenId) {
|
|
13266
|
+
return this.tokenHolder.get(tokenId);
|
|
13267
|
+
}
|
|
13268
|
+
tokensOf(reservationId) {
|
|
13269
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13270
|
+
}
|
|
13118
13271
|
clear() {
|
|
13119
13272
|
this.reservations.clear();
|
|
13120
13273
|
this.tokenHolder.clear();
|
|
@@ -13127,6 +13280,44 @@ var ReservationLedger = class {
|
|
|
13127
13280
|
}
|
|
13128
13281
|
};
|
|
13129
13282
|
|
|
13283
|
+
// modules/payments-v2/select/pins.ts
|
|
13284
|
+
var IntentPins = class {
|
|
13285
|
+
constructor(deps) {
|
|
13286
|
+
this.deps = deps;
|
|
13287
|
+
}
|
|
13288
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13289
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13290
|
+
adopt(transferId) {
|
|
13291
|
+
this.pinned.add(transferId);
|
|
13292
|
+
}
|
|
13293
|
+
async sync() {
|
|
13294
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13295
|
+
let changed = false;
|
|
13296
|
+
for (const transferId of [...this.pinned]) {
|
|
13297
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13298
|
+
this.pinned.delete(transferId);
|
|
13299
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13300
|
+
this.deps.ledger.cancel(transferId);
|
|
13301
|
+
changed = true;
|
|
13302
|
+
}
|
|
13303
|
+
for (const [transferId, tokenIds] of open) {
|
|
13304
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13305
|
+
this.pinned.add(transferId);
|
|
13306
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13307
|
+
}
|
|
13308
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13309
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13310
|
+
}
|
|
13311
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13312
|
+
pin(transferId, tokenIds) {
|
|
13313
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13314
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13315
|
+
if (free.length === 0) return false;
|
|
13316
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13317
|
+
return true;
|
|
13318
|
+
}
|
|
13319
|
+
};
|
|
13320
|
+
|
|
13130
13321
|
// modules/payments-v2/select/selector.ts
|
|
13131
13322
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13132
13323
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13217,6 +13408,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13217
13408
|
// modules/payments-v2/select/queue.ts
|
|
13218
13409
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13219
13410
|
var QUEUE_MAX_SIZE = 100;
|
|
13411
|
+
function insufficientBalance(view, amount) {
|
|
13412
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13413
|
+
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";
|
|
13414
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13415
|
+
}
|
|
13220
13416
|
var SpendQueue = class {
|
|
13221
13417
|
constructor(deps) {
|
|
13222
13418
|
this.deps = deps;
|
|
@@ -13229,12 +13425,10 @@ var SpendQueue = class {
|
|
|
13229
13425
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13230
13426
|
}
|
|
13231
13427
|
const amount = parseAmount(request.amount);
|
|
13232
|
-
const
|
|
13428
|
+
const view = this.freeView(request.coinId);
|
|
13429
|
+
const { freeView, freeTotal } = view;
|
|
13233
13430
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13234
|
-
throw
|
|
13235
|
-
"Insufficient balance for this transaction",
|
|
13236
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13237
|
-
);
|
|
13431
|
+
throw insufficientBalance(view, amount);
|
|
13238
13432
|
}
|
|
13239
13433
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13240
13434
|
if (plan !== null) {
|
|
@@ -13284,11 +13478,10 @@ var SpendQueue = class {
|
|
|
13284
13478
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13285
13479
|
return "rejected";
|
|
13286
13480
|
}
|
|
13287
|
-
const
|
|
13481
|
+
const view = this.freeView(entry.coinId);
|
|
13482
|
+
const { freeView, freeTotal } = view;
|
|
13288
13483
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13289
|
-
entry.reject(
|
|
13290
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13291
|
-
);
|
|
13484
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13292
13485
|
return "rejected";
|
|
13293
13486
|
}
|
|
13294
13487
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13333,18 +13526,26 @@ var SpendQueue = class {
|
|
|
13333
13526
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13334
13527
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13335
13528
|
}
|
|
13336
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13529
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13530
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13337
13531
|
freeView(coinId) {
|
|
13338
13532
|
const freeView = [];
|
|
13339
13533
|
let freeTotal = 0n;
|
|
13534
|
+
let pinnedTotal = 0n;
|
|
13535
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13536
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13340
13537
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13341
13538
|
if (entry.amount <= 0n) continue;
|
|
13342
|
-
|
|
13539
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13540
|
+
if (holder === void 0) {
|
|
13343
13541
|
freeView.push(entry);
|
|
13344
13542
|
freeTotal += entry.amount;
|
|
13543
|
+
} else {
|
|
13544
|
+
pinnedTotal += entry.amount;
|
|
13545
|
+
holders.add(holder);
|
|
13345
13546
|
}
|
|
13346
13547
|
}
|
|
13347
|
-
return { freeView, freeTotal };
|
|
13548
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13348
13549
|
}
|
|
13349
13550
|
expectedChangeTotal(coinId) {
|
|
13350
13551
|
let total = 0n;
|
|
@@ -13386,13 +13587,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13386
13587
|
}
|
|
13387
13588
|
function composeFacadeParts(deps, hooks) {
|
|
13388
13589
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13590
|
+
const ledger = new ReservationLedger();
|
|
13389
13591
|
const view = new InventoryView({
|
|
13390
13592
|
port: deps.storagePort,
|
|
13391
13593
|
kv: deps.kv,
|
|
13392
13594
|
emit: (event) => deps.emit(event, {}),
|
|
13595
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13596
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13597
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13598
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13393
13599
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13394
13600
|
});
|
|
13395
|
-
const ledger = new ReservationLedger();
|
|
13396
13601
|
const queue = new SpendQueue({
|
|
13397
13602
|
ledger,
|
|
13398
13603
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13413,6 +13618,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13413
13618
|
ownPubkeyBytes,
|
|
13414
13619
|
view,
|
|
13415
13620
|
ledger,
|
|
13621
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13416
13622
|
queue,
|
|
13417
13623
|
historyStore,
|
|
13418
13624
|
machineStores,
|
|
@@ -13424,6 +13630,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13424
13630
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13425
13631
|
};
|
|
13426
13632
|
}
|
|
13633
|
+
function buildPins(deps, hooks, parts) {
|
|
13634
|
+
return new IntentPins({
|
|
13635
|
+
ledger: parts.ledger,
|
|
13636
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13637
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13638
|
+
release: (tokenId) => {
|
|
13639
|
+
parts.view.release(tokenId);
|
|
13640
|
+
},
|
|
13641
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13642
|
+
});
|
|
13643
|
+
}
|
|
13427
13644
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13428
13645
|
return {
|
|
13429
13646
|
stores: machineStores,
|
|
@@ -13533,7 +13750,8 @@ var PaymentsFacade = class {
|
|
|
13533
13750
|
this.deps = deps;
|
|
13534
13751
|
const parts = composeFacadeParts(deps, {
|
|
13535
13752
|
engine: () => this.engine(),
|
|
13536
|
-
send: (request) => this.send(request)
|
|
13753
|
+
send: (request) => this.send(request),
|
|
13754
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13537
13755
|
});
|
|
13538
13756
|
this.view = parts.view;
|
|
13539
13757
|
this.ledger = parts.ledger;
|
|
@@ -13547,6 +13765,7 @@ var PaymentsFacade = class {
|
|
|
13547
13765
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13548
13766
|
this.restoreDeps = parts.restoreDeps;
|
|
13549
13767
|
this.requests = parts.requests;
|
|
13768
|
+
this.pins = parts.pins;
|
|
13550
13769
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13551
13770
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13552
13771
|
now: () => this.nowMs(),
|
|
@@ -13589,6 +13808,8 @@ var PaymentsFacade = class {
|
|
|
13589
13808
|
passChain = new SerialChain();
|
|
13590
13809
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13591
13810
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13811
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13812
|
+
pins;
|
|
13592
13813
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13593
13814
|
async start() {
|
|
13594
13815
|
if (this.started) return;
|
|
@@ -13611,6 +13832,7 @@ var PaymentsFacade = class {
|
|
|
13611
13832
|
});
|
|
13612
13833
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13613
13834
|
await this.deps.session.start();
|
|
13835
|
+
await this.pins.sync().catch(() => void 0);
|
|
13614
13836
|
this.trackTail(this.runConvergencePass());
|
|
13615
13837
|
this.trackTail(this.seedHeldStates());
|
|
13616
13838
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13681,6 +13903,7 @@ var PaymentsFacade = class {
|
|
|
13681
13903
|
}
|
|
13682
13904
|
async convergeBody() {
|
|
13683
13905
|
const outcome = await this.converger.convergeOnce();
|
|
13906
|
+
await this.pins.sync().catch(() => void 0);
|
|
13684
13907
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13685
13908
|
this.heartbeat.settle(outcome, pending);
|
|
13686
13909
|
}
|
|
@@ -13920,6 +14143,7 @@ var PaymentsFacade = class {
|
|
|
13920
14143
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
13921
14144
|
settleKeepOpen(ctx) {
|
|
13922
14145
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14146
|
+
this.pins.adopt(ctx.transferId);
|
|
13923
14147
|
this.heartbeat.arm();
|
|
13924
14148
|
}
|
|
13925
14149
|
settlePartial(ctx, partial) {
|