@unicitylabs/sphere-sdk 0.18.0-dev.2 → 0.18.0-dev.3
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/chunks/{chunk-OAAY2JEA.js → chunk-7BZL7HE2.js} +3 -3
- package/dist/connect/chunks/{chunk-MQQ3TMHM.js → chunk-CJ4D6M6W.js} +2 -2
- package/dist/connect/chunks/{chunk-FGQ6CKKK.js → chunk-LDEOQNG2.js} +2 -2
- package/dist/connect/chunks/{chunk-FGQ6CKKK.js.map → chunk-LDEOQNG2.js.map} +1 -1
- package/dist/connect/chunks/{chunk-3CYK5SIU.js → chunk-WCKTUB4V.js} +1 -1
- package/dist/connect/chunks/chunk-WCKTUB4V.js.map +1 -0
- package/dist/connect/chunks/{chunk-JYBI36GK.js → chunk-WL6VYP7G.js} +2 -2
- package/dist/connect/index.cjs +1 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +5 -5
- package/dist/connect/internal/host.js +3 -3
- package/dist/connect/internal/nft-wire.js +2 -2
- package/dist/core/index.cjs +831 -343
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +30 -1
- package/dist/core/index.d.ts +30 -1
- package/dist/core/index.js +831 -343
- 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 +2 -2
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +1 -0
- package/dist/impl/nodejs/index.d.ts +1 -0
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/impl/shared/wallet-api/index.d.cts +1 -0
- package/dist/impl/shared/wallet-api/index.d.ts +1 -0
- package/dist/impl/wallet-api-v2/index.cjs +2 -1
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.d.cts +3 -0
- package/dist/impl/wallet-api-v2/index.d.ts +3 -0
- package/dist/impl/wallet-api-v2/index.js +2 -1
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +833 -343
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +832 -343
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +658 -259
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +65 -2
- package/dist/modules/payments-v2/index.d.ts +65 -2
- package/dist/modules/payments-v2/index.js +656 -259
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/dist/token-engine/index.cjs +185 -65
- package/dist/token-engine/index.cjs.map +1 -1
- package/dist/token-engine/index.d.cts +20 -3
- package/dist/token-engine/index.d.ts +20 -3
- package/dist/token-engine/index.js +184 -65
- package/dist/token-engine/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/connect/chunks/chunk-3CYK5SIU.js.map +0 -1
- /package/dist/connect/chunks/{chunk-OAAY2JEA.js.map → chunk-7BZL7HE2.js.map} +0 -0
- /package/dist/connect/chunks/{chunk-MQQ3TMHM.js.map → chunk-CJ4D6M6W.js.map} +0 -0
- /package/dist/connect/chunks/{chunk-JYBI36GK.js.map → chunk-WL6VYP7G.js.map} +0 -0
|
@@ -1695,6 +1695,33 @@ function decryptField(key, envelope) {
|
|
|
1695
1695
|
return new TextDecoder().decode(decryptFieldBytes(key, envelope));
|
|
1696
1696
|
}
|
|
1697
1697
|
|
|
1698
|
+
// token-engine/claims.ts
|
|
1699
|
+
var CoinClaims = class _CoinClaims {
|
|
1700
|
+
issuers = /* @__PURE__ */ new Map();
|
|
1701
|
+
policies = [];
|
|
1702
|
+
static fromPlugins(plugins) {
|
|
1703
|
+
const claims = new _CoinClaims();
|
|
1704
|
+
for (const plugin of plugins ?? []) {
|
|
1705
|
+
for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
|
|
1706
|
+
}
|
|
1707
|
+
return claims;
|
|
1708
|
+
}
|
|
1709
|
+
add(policy) {
|
|
1710
|
+
const tokenType = HexConverter.encode(policy.tokenType.bytes);
|
|
1711
|
+
const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
|
|
1712
|
+
const taken = coinIds.find((coinId) => this.issuers.has(coinId));
|
|
1713
|
+
if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
|
|
1714
|
+
for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
|
|
1715
|
+
this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
|
|
1716
|
+
}
|
|
1717
|
+
fingerprint() {
|
|
1718
|
+
return [...this.policies].sort().join("\n");
|
|
1719
|
+
}
|
|
1720
|
+
issuerOf(coinId) {
|
|
1721
|
+
return this.issuers.get(coinId.toLowerCase()) ?? null;
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1698
1725
|
// modules/payments-v2/machine/payload-view.ts
|
|
1699
1726
|
function isWholeIntent(p) {
|
|
1700
1727
|
return p.kind === "whole" || p.kind === "coinless";
|
|
@@ -1972,6 +1999,9 @@ function createScopedKV(storage, network, chainPubkey) {
|
|
|
1972
1999
|
}
|
|
1973
2000
|
};
|
|
1974
2001
|
}
|
|
2002
|
+
function isLiveBurn(entry) {
|
|
2003
|
+
return !entry.settled && entry.failure === void 0;
|
|
2004
|
+
}
|
|
1975
2005
|
var STORE_KEYS = {
|
|
1976
2006
|
intentBackstop: "intents",
|
|
1977
2007
|
checkpointCache: "checkpoints",
|
|
@@ -1986,7 +2016,8 @@ var STORE_KEYS = {
|
|
|
1986
2016
|
epochLatch: "epoch-latch",
|
|
1987
2017
|
// §5.2 InventoryView durable overlays (#625/#679).
|
|
1988
2018
|
suspectedSpent: "suspected-spent",
|
|
1989
|
-
knownSpends: "known-spends"
|
|
2019
|
+
knownSpends: "known-spends",
|
|
2020
|
+
verifiedTokens: "verified-tokens"
|
|
1990
2021
|
};
|
|
1991
2022
|
|
|
1992
2023
|
// modules/payments-v2/inventory/presentation.ts
|
|
@@ -2004,18 +2035,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
|
2004
2035
|
createdAt: snapshot.createdAt,
|
|
2005
2036
|
updatedAt: snapshot.updatedAt,
|
|
2006
2037
|
lazy: true,
|
|
2007
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
2038
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {},
|
|
2039
|
+
...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
|
|
2008
2040
|
};
|
|
2009
2041
|
}
|
|
2010
|
-
function aggregateAssets(entries,
|
|
2042
|
+
function aggregateAssets(entries, reading, registry) {
|
|
2011
2043
|
const groups = /* @__PURE__ */ new Map();
|
|
2012
2044
|
for (const [tokenId, entry] of entries) {
|
|
2013
|
-
const moving = isTransferring(tokenId);
|
|
2045
|
+
const moving = reading.isTransferring(tokenId);
|
|
2014
2046
|
for (const asset of entry.assets) {
|
|
2015
|
-
|
|
2047
|
+
const standing = reading.standing(tokenId, asset.coinId);
|
|
2048
|
+
const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
|
|
2049
|
+
let group = groups.get(key);
|
|
2016
2050
|
if (!group) {
|
|
2017
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
2018
|
-
groups.set(
|
|
2051
|
+
group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
2052
|
+
groups.set(key, group);
|
|
2019
2053
|
}
|
|
2020
2054
|
if (moving) {
|
|
2021
2055
|
group.transferring += BigInt(asset.amount);
|
|
@@ -2026,9 +2060,10 @@ function aggregateAssets(entries, isTransferring, registry) {
|
|
|
2026
2060
|
}
|
|
2027
2061
|
}
|
|
2028
2062
|
}
|
|
2029
|
-
return [...groups].map((
|
|
2063
|
+
return [...groups.values()].map((totals) => toAsset(totals, registry));
|
|
2030
2064
|
}
|
|
2031
|
-
function toAsset(
|
|
2065
|
+
function toAsset(totals, registry) {
|
|
2066
|
+
const coinId = totals.coinId;
|
|
2032
2067
|
const iconUrl = registry.getIconUrl(coinId);
|
|
2033
2068
|
return {
|
|
2034
2069
|
coinId,
|
|
@@ -2048,7 +2083,8 @@ function toAsset(coinId, totals, registry) {
|
|
|
2048
2083
|
priceEur: null,
|
|
2049
2084
|
change24h: null,
|
|
2050
2085
|
fiatValueUsd: null,
|
|
2051
|
-
fiatValueEur: null
|
|
2086
|
+
fiatValueEur: null,
|
|
2087
|
+
...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
|
|
2052
2088
|
};
|
|
2053
2089
|
}
|
|
2054
2090
|
function priceIdOf(registry, coinId) {
|
|
@@ -2056,9 +2092,9 @@ function priceIdOf(registry, coinId) {
|
|
|
2056
2092
|
}
|
|
2057
2093
|
async function withPrices(raw, registry, price) {
|
|
2058
2094
|
try {
|
|
2059
|
-
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
2095
|
+
const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
|
|
2060
2096
|
const quotes = await price.getPrices(ids);
|
|
2061
|
-
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
2097
|
+
return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
2062
2098
|
} catch {
|
|
2063
2099
|
return raw;
|
|
2064
2100
|
}
|
|
@@ -2079,11 +2115,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
|
2079
2115
|
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
2080
2116
|
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
2081
2117
|
transferring: true,
|
|
2082
|
-
suspectedSpent: false
|
|
2118
|
+
suspectedSpent: false,
|
|
2119
|
+
standing: "trusted"
|
|
2083
2120
|
});
|
|
2084
2121
|
}
|
|
2085
2122
|
|
|
2086
2123
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
2124
|
+
var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
|
|
2087
2125
|
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
2088
2126
|
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
2089
2127
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
@@ -2177,6 +2215,12 @@ var InventoryView = class {
|
|
|
2177
2215
|
for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
|
|
2178
2216
|
if (changed) this.deps.emit("inventory:updated");
|
|
2179
2217
|
}
|
|
2218
|
+
get trust() {
|
|
2219
|
+
return this.deps.trust ?? TRUST_ALL;
|
|
2220
|
+
}
|
|
2221
|
+
trusts(tokenId, coinId) {
|
|
2222
|
+
return this.trust.standing(tokenId, coinId) === "trusted";
|
|
2223
|
+
}
|
|
2180
2224
|
pinned(tokenId) {
|
|
2181
2225
|
return this.deps.isPinned?.(tokenId) ?? false;
|
|
2182
2226
|
}
|
|
@@ -2203,6 +2247,7 @@ var InventoryView = class {
|
|
|
2203
2247
|
isSpendable(tokenId, entry) {
|
|
2204
2248
|
if (entry.status !== "active") return false;
|
|
2205
2249
|
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
|
|
2250
|
+
if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
|
|
2206
2251
|
return !this.suspected.has(stateKey(tokenId, entry.stateHash));
|
|
2207
2252
|
}
|
|
2208
2253
|
/** Is this NAMED token spendable whole — coinless or valued (#777)? */
|
|
@@ -2229,7 +2274,8 @@ var InventoryView = class {
|
|
|
2229
2274
|
out.push(
|
|
2230
2275
|
toToken(tokenId, entry, asset, registry, {
|
|
2231
2276
|
transferring: this.held(tokenId),
|
|
2232
|
-
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
2277
|
+
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
|
|
2278
|
+
standing: this.trust.standing(tokenId, asset.coinId)
|
|
2233
2279
|
})
|
|
2234
2280
|
);
|
|
2235
2281
|
}
|
|
@@ -2259,10 +2305,21 @@ var InventoryView = class {
|
|
|
2259
2305
|
return out;
|
|
2260
2306
|
}
|
|
2261
2307
|
async assets(registry, price) {
|
|
2262
|
-
const
|
|
2308
|
+
const reading = {
|
|
2309
|
+
isTransferring: (tokenId) => this.held(tokenId),
|
|
2310
|
+
standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
|
|
2311
|
+
};
|
|
2312
|
+
const raw = aggregateAssets(this.activeEntries(), reading, registry);
|
|
2263
2313
|
if (!price || raw.length === 0) return raw;
|
|
2264
2314
|
return withPrices(raw, registry, price);
|
|
2265
2315
|
}
|
|
2316
|
+
claimHolders() {
|
|
2317
|
+
const out = [];
|
|
2318
|
+
for (const [tokenId, entry] of this.activeEntries()) {
|
|
2319
|
+
if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
|
|
2320
|
+
}
|
|
2321
|
+
return out;
|
|
2322
|
+
}
|
|
2266
2323
|
*activeEntries() {
|
|
2267
2324
|
for (const [tokenId, entry] of this.mirror) {
|
|
2268
2325
|
if (entry.status === "active") yield [tokenId, entry];
|
|
@@ -2432,6 +2489,171 @@ var InventoryView = class {
|
|
|
2432
2489
|
}
|
|
2433
2490
|
};
|
|
2434
2491
|
|
|
2492
|
+
// modules/payments-v2/inventory/verdicts.ts
|
|
2493
|
+
var VERDICT_RETRY_MS = 3e4;
|
|
2494
|
+
var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
|
|
2495
|
+
function honoursClaims(claims, token) {
|
|
2496
|
+
const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
|
|
2497
|
+
return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
|
|
2498
|
+
}
|
|
2499
|
+
var TokenVerdicts = class {
|
|
2500
|
+
constructor(deps) {
|
|
2501
|
+
this.deps = deps;
|
|
2502
|
+
}
|
|
2503
|
+
verified = /* @__PURE__ */ new Set();
|
|
2504
|
+
refused = /* @__PURE__ */ new Set();
|
|
2505
|
+
retries = /* @__PURE__ */ new Map();
|
|
2506
|
+
writes = new SerialChain();
|
|
2507
|
+
hydration = null;
|
|
2508
|
+
queued = null;
|
|
2509
|
+
running = null;
|
|
2510
|
+
held = [];
|
|
2511
|
+
timer = null;
|
|
2512
|
+
live = false;
|
|
2513
|
+
isClaimed(coinId) {
|
|
2514
|
+
return this.deps.claims.issuerOf(coinId) !== null;
|
|
2515
|
+
}
|
|
2516
|
+
standing(tokenId, coinId) {
|
|
2517
|
+
if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
|
|
2518
|
+
return this.refused.has(tokenId) ? "refused" : "pending";
|
|
2519
|
+
}
|
|
2520
|
+
trusts(tokenId, coinId) {
|
|
2521
|
+
return this.standing(tokenId, coinId) === "trusted";
|
|
2522
|
+
}
|
|
2523
|
+
hydrate() {
|
|
2524
|
+
this.hydration ??= this.load().catch((error) => {
|
|
2525
|
+
this.hydration = null;
|
|
2526
|
+
throw error;
|
|
2527
|
+
});
|
|
2528
|
+
return this.hydration;
|
|
2529
|
+
}
|
|
2530
|
+
start() {
|
|
2531
|
+
this.live = true;
|
|
2532
|
+
return this.hydrate();
|
|
2533
|
+
}
|
|
2534
|
+
stop() {
|
|
2535
|
+
this.live = false;
|
|
2536
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
2537
|
+
this.timer = null;
|
|
2538
|
+
}
|
|
2539
|
+
async accept(token) {
|
|
2540
|
+
if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
|
|
2541
|
+
}
|
|
2542
|
+
async recordSplit(sourceTokenId, outputTokenId) {
|
|
2543
|
+
await this.hydrate();
|
|
2544
|
+
if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
|
|
2545
|
+
}
|
|
2546
|
+
review(holders) {
|
|
2547
|
+
this.queued = holders;
|
|
2548
|
+
this.running ??= this.drain();
|
|
2549
|
+
return this.running;
|
|
2550
|
+
}
|
|
2551
|
+
async drain() {
|
|
2552
|
+
try {
|
|
2553
|
+
while (this.queued !== null) {
|
|
2554
|
+
const holders = this.queued;
|
|
2555
|
+
this.queued = null;
|
|
2556
|
+
await this.reviewOnce(holders);
|
|
2557
|
+
}
|
|
2558
|
+
} finally {
|
|
2559
|
+
this.running = null;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
async reviewOnce(holders) {
|
|
2563
|
+
await this.hydrate();
|
|
2564
|
+
this.held = holders;
|
|
2565
|
+
const due = holders.filter((tokenId) => this.isDue(tokenId));
|
|
2566
|
+
const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
|
|
2567
|
+
for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
|
|
2568
|
+
if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
|
|
2569
|
+
await this.persist();
|
|
2570
|
+
this.deps.changed();
|
|
2571
|
+
}
|
|
2572
|
+
this.scheduleRetry();
|
|
2573
|
+
}
|
|
2574
|
+
isDue(tokenId) {
|
|
2575
|
+
if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
|
|
2576
|
+
return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
|
|
2577
|
+
}
|
|
2578
|
+
async check(due) {
|
|
2579
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
2580
|
+
let blobs;
|
|
2581
|
+
try {
|
|
2582
|
+
blobs = await this.deps.getBlobs([...due]);
|
|
2583
|
+
} catch {
|
|
2584
|
+
return verdicts;
|
|
2585
|
+
}
|
|
2586
|
+
for (const tokenId of due) {
|
|
2587
|
+
const bytes = blobs.get(tokenId);
|
|
2588
|
+
if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
|
|
2589
|
+
}
|
|
2590
|
+
return verdicts;
|
|
2591
|
+
}
|
|
2592
|
+
async verdictOf(tokenId, bytes) {
|
|
2593
|
+
let engine;
|
|
2594
|
+
try {
|
|
2595
|
+
engine = this.deps.engine();
|
|
2596
|
+
} catch {
|
|
2597
|
+
return "retry";
|
|
2598
|
+
}
|
|
2599
|
+
let token;
|
|
2600
|
+
try {
|
|
2601
|
+
token = await engine.decodeToken({ tokenId, token: bytes });
|
|
2602
|
+
} catch {
|
|
2603
|
+
return "refused";
|
|
2604
|
+
}
|
|
2605
|
+
if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
|
|
2606
|
+
try {
|
|
2607
|
+
return (await engine.verify(token)).ok ? "verified" : "refused";
|
|
2608
|
+
} catch {
|
|
2609
|
+
return "retry";
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
settle(tokenId, verdict) {
|
|
2613
|
+
if (verdict === "retry") {
|
|
2614
|
+
this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
this.retries.delete(tokenId);
|
|
2618
|
+
(verdict === "verified" ? this.verified : this.refused).add(tokenId);
|
|
2619
|
+
}
|
|
2620
|
+
nextRetry(previous) {
|
|
2621
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
2622
|
+
const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
|
|
2623
|
+
return { attempts, atMs: this.now() + delay };
|
|
2624
|
+
}
|
|
2625
|
+
scheduleRetry() {
|
|
2626
|
+
if (!this.live || this.timer !== null) return;
|
|
2627
|
+
const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
|
|
2628
|
+
if (due.length === 0) return;
|
|
2629
|
+
const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
|
|
2630
|
+
this.timer = setTimeout(() => {
|
|
2631
|
+
this.timer = null;
|
|
2632
|
+
void this.review(this.held).catch(() => void 0);
|
|
2633
|
+
}, delay);
|
|
2634
|
+
}
|
|
2635
|
+
async remember(tokenId) {
|
|
2636
|
+
await this.hydrate();
|
|
2637
|
+
if (this.verified.has(tokenId)) return;
|
|
2638
|
+
this.verified.add(tokenId);
|
|
2639
|
+
this.refused.delete(tokenId);
|
|
2640
|
+
this.retries.delete(tokenId);
|
|
2641
|
+
await this.persist();
|
|
2642
|
+
}
|
|
2643
|
+
persist() {
|
|
2644
|
+
const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
|
|
2645
|
+
return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
|
|
2646
|
+
}
|
|
2647
|
+
async load() {
|
|
2648
|
+
const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
|
|
2649
|
+
if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
|
|
2650
|
+
for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
|
|
2651
|
+
}
|
|
2652
|
+
now() {
|
|
2653
|
+
return (this.deps.now ?? Date.now)();
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
|
|
2435
2657
|
// token-engine/value-envelope.ts
|
|
2436
2658
|
function isCoinlessEnvelope(envelope) {
|
|
2437
2659
|
return envelope.startsWith("none_");
|
|
@@ -2446,6 +2668,8 @@ function isRetryableAckError(err) {
|
|
|
2446
2668
|
var ACK_BATCH_SIZE = 200;
|
|
2447
2669
|
var REFRESH_INTERVAL_MS = 2500;
|
|
2448
2670
|
var POLL_INTERVAL_MS = 3e4;
|
|
2671
|
+
var RECHECK_BASE_MS = POLL_INTERVAL_MS;
|
|
2672
|
+
var RECHECK_MAX_MS = 60 * 60 * 1e3;
|
|
2449
2673
|
var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
|
|
2450
2674
|
function receivedDedupKey(tokenId, stateHash) {
|
|
2451
2675
|
return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
|
|
@@ -2462,6 +2686,7 @@ var Receive = class {
|
|
|
2462
2686
|
pollTimer = null;
|
|
2463
2687
|
unsubscribeWake = null;
|
|
2464
2688
|
lastRefreshAt = 0;
|
|
2689
|
+
rechecks = /* @__PURE__ */ new Map();
|
|
2465
2690
|
drainOnce() {
|
|
2466
2691
|
return this.drainFlight.run(() => this.doDrain());
|
|
2467
2692
|
}
|
|
@@ -2520,6 +2745,7 @@ var Receive = class {
|
|
|
2520
2745
|
const pending = [];
|
|
2521
2746
|
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
2522
2747
|
const claimable = { n: 0 };
|
|
2748
|
+
const deferred = { any: false };
|
|
2523
2749
|
const finish = () => {
|
|
2524
2750
|
if (claimable.n > 0) deps.refreshView?.();
|
|
2525
2751
|
return stored;
|
|
@@ -2528,7 +2754,7 @@ var Receive = class {
|
|
|
2528
2754
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
2529
2755
|
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
2530
2756
|
for (let pass = 0; pass < 2; pass++) {
|
|
2531
|
-
await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
|
|
2757
|
+
await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
|
|
2532
2758
|
const served = deps.delivery.incomingEpoch();
|
|
2533
2759
|
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
2534
2760
|
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
@@ -2545,20 +2771,41 @@ var Receive = class {
|
|
|
2545
2771
|
return finish();
|
|
2546
2772
|
}
|
|
2547
2773
|
async processEntry(ctx, entry) {
|
|
2548
|
-
const { deps, engine,
|
|
2549
|
-
const screened = await screen(deps, engine, entry);
|
|
2774
|
+
const { deps, engine, stored, claimable } = ctx;
|
|
2775
|
+
const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
|
|
2776
|
+
this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
|
|
2777
|
+
if (screened.kind === "defer") {
|
|
2778
|
+
ctx.deferred.any = true;
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2550
2781
|
if (screened.kind === "ack") {
|
|
2551
|
-
|
|
2782
|
+
queueAck(ctx, screened.ack);
|
|
2552
2783
|
if (screened.ack.disposition === "claimed") claimable.n += 1;
|
|
2553
2784
|
return;
|
|
2554
2785
|
}
|
|
2786
|
+
await deps.accepted?.(screened.token);
|
|
2555
2787
|
await deps.view.store(screened.record);
|
|
2556
|
-
|
|
2788
|
+
queueAck(ctx, claimAck(entry));
|
|
2557
2789
|
claimable.n += 1;
|
|
2558
2790
|
const transfer = await announce(deps, entry, screened.record);
|
|
2559
2791
|
stored.push(transfer);
|
|
2560
2792
|
deps.emit("transfer:incoming", transfer);
|
|
2561
2793
|
}
|
|
2794
|
+
recheckDue(deliveryId) {
|
|
2795
|
+
const recheck = this.rechecks.get(deliveryId);
|
|
2796
|
+
return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
|
|
2797
|
+
}
|
|
2798
|
+
scheduleRecheck(deliveryId, deferred) {
|
|
2799
|
+
if (!deferred) {
|
|
2800
|
+
this.rechecks.delete(deliveryId);
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
const previous = this.rechecks.get(deliveryId);
|
|
2804
|
+
if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
|
|
2805
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
2806
|
+
const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
|
|
2807
|
+
this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
|
|
2808
|
+
}
|
|
2562
2809
|
};
|
|
2563
2810
|
async function screen(deps, engine, entry) {
|
|
2564
2811
|
const blobBytes = await entry.fetchBlob();
|
|
@@ -2569,7 +2816,8 @@ async function screen(deps, engine, entry) {
|
|
|
2569
2816
|
logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
|
|
2570
2817
|
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
2571
2818
|
}
|
|
2572
|
-
const verdict = await engine
|
|
2819
|
+
const verdict = await verdictOf(engine, token);
|
|
2820
|
+
if (verdict === null) return { kind: "defer" };
|
|
2573
2821
|
if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
2574
2822
|
if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
|
|
2575
2823
|
return { kind: "ack", ack: rejectAck(entry, "not-owned") };
|
|
@@ -2580,38 +2828,43 @@ async function screen(deps, engine, entry) {
|
|
|
2580
2828
|
if (held !== null && await engine.isSpent(token)) {
|
|
2581
2829
|
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
2582
2830
|
}
|
|
2831
|
+
const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
|
|
2583
2832
|
return {
|
|
2584
2833
|
kind: "accept",
|
|
2834
|
+
token,
|
|
2585
2835
|
record: {
|
|
2586
2836
|
tokenId: keys.tokenId,
|
|
2587
2837
|
stateHash: keys.stateHash,
|
|
2588
2838
|
assets: toAssetAmounts(token),
|
|
2589
|
-
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
|
|
2839
|
+
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
|
|
2840
|
+
...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
|
|
2590
2841
|
}
|
|
2591
2842
|
};
|
|
2592
2843
|
}
|
|
2593
|
-
|
|
2594
|
-
const
|
|
2844
|
+
function unverifiedCoinsOf(deps, token) {
|
|
2845
|
+
const claims = deps.claims;
|
|
2846
|
+
if (claims === void 0 || honoursClaims(claims, token)) return [];
|
|
2847
|
+
return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
|
|
2848
|
+
}
|
|
2849
|
+
async function verdictOf(engine, token) {
|
|
2595
2850
|
try {
|
|
2596
|
-
await
|
|
2597
|
-
dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
|
|
2598
|
-
tokenId: record.tokenId,
|
|
2599
|
-
stateHash: record.stateHash,
|
|
2600
|
-
assets: record.assets,
|
|
2601
|
-
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
2602
|
-
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
2603
|
-
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
2604
|
-
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
2605
|
-
receivedAt
|
|
2606
|
-
});
|
|
2851
|
+
return await engine.verify(token);
|
|
2607
2852
|
} catch (err) {
|
|
2608
|
-
|
|
2853
|
+
if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
|
|
2854
|
+
throw err;
|
|
2609
2855
|
}
|
|
2856
|
+
}
|
|
2857
|
+
async function announce(deps, entry, record) {
|
|
2858
|
+
const receivedAt = (deps.now ?? Date.now)();
|
|
2859
|
+
await recordArrival(deps, entry, record, receivedAt);
|
|
2860
|
+
const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
|
|
2861
|
+
const counted = record.assets.filter((asset) => !refused2.includes(asset));
|
|
2610
2862
|
return {
|
|
2611
2863
|
id: record.tokenId,
|
|
2612
2864
|
senderPubkey: entry.senderPubkey ?? "",
|
|
2613
2865
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
2614
|
-
tokens:
|
|
2866
|
+
tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
|
|
2867
|
+
...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
|
|
2615
2868
|
// #777: named here rather than mapped from assets, which announced an EMPTY list.
|
|
2616
2869
|
...record.tokenType !== void 0 ? {
|
|
2617
2870
|
coinless: [
|
|
@@ -2629,6 +2882,25 @@ async function announce(deps, entry, record) {
|
|
|
2629
2882
|
receivedAt
|
|
2630
2883
|
};
|
|
2631
2884
|
}
|
|
2885
|
+
async function recordArrival(deps, entry, record, receivedAt) {
|
|
2886
|
+
const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
|
|
2887
|
+
if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
|
|
2888
|
+
try {
|
|
2889
|
+
await deps.recordReceived({
|
|
2890
|
+
dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
|
|
2891
|
+
tokenId: record.tokenId,
|
|
2892
|
+
stateHash: record.stateHash,
|
|
2893
|
+
assets,
|
|
2894
|
+
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
2895
|
+
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
2896
|
+
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
2897
|
+
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
2898
|
+
receivedAt
|
|
2899
|
+
});
|
|
2900
|
+
} catch (err) {
|
|
2901
|
+
logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2632
2904
|
async function flushAcks(deps, pending, epochOf) {
|
|
2633
2905
|
if (pending.length === 0) return { progressed: true };
|
|
2634
2906
|
const before = pending.length;
|
|
@@ -2636,7 +2908,7 @@ async function flushAcks(deps, pending, epochOf) {
|
|
|
2636
2908
|
try {
|
|
2637
2909
|
const settled = await settleAcks(deps, pending);
|
|
2638
2910
|
let i = 0;
|
|
2639
|
-
while (i < pending.length && settled.has(pending[i].deliveryId)) {
|
|
2911
|
+
while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
|
|
2640
2912
|
lastAcked = pending[i].cursor;
|
|
2641
2913
|
i += 1;
|
|
2642
2914
|
}
|
|
@@ -2712,6 +2984,9 @@ async function ackOne(deps, ack) {
|
|
|
2712
2984
|
await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
|
|
2713
2985
|
}
|
|
2714
2986
|
}
|
|
2987
|
+
function queueAck(ctx, ack) {
|
|
2988
|
+
ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
|
|
2989
|
+
}
|
|
2715
2990
|
function claimAck(entry) {
|
|
2716
2991
|
return {
|
|
2717
2992
|
deliveryId: entry.deliveryId,
|
|
@@ -3612,6 +3887,7 @@ var TransferMachine = class {
|
|
|
3612
3887
|
const key = keys.get(digest);
|
|
3613
3888
|
if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
|
|
3614
3889
|
const { tokenId } = await engine.deliveryKeys(o.changeBlob);
|
|
3890
|
+
await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
|
|
3615
3891
|
added.push({ tokenId, key });
|
|
3616
3892
|
}
|
|
3617
3893
|
return added;
|
|
@@ -4007,7 +4283,7 @@ var Converger = class {
|
|
|
4007
4283
|
this.deps.stores.burnJournal.list()
|
|
4008
4284
|
]);
|
|
4009
4285
|
const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
|
|
4010
|
-
const burnsOpen = burns.some(
|
|
4286
|
+
const burnsOpen = burns.some(isLiveBurn);
|
|
4011
4287
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
|
|
4012
4288
|
}
|
|
4013
4289
|
async resumePass() {
|
|
@@ -4094,6 +4370,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
|
4094
4370
|
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
4095
4371
|
holds.set(entry.transferId, [...direct, ...split]);
|
|
4096
4372
|
}
|
|
4373
|
+
for (const burn of await stores.burnJournal.list()) {
|
|
4374
|
+
if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
|
|
4375
|
+
}
|
|
4097
4376
|
return { open: holds, complete };
|
|
4098
4377
|
}
|
|
4099
4378
|
function groupJournal(journal) {
|
|
@@ -4583,6 +4862,176 @@ function parseAmount(raw) {
|
|
|
4583
4862
|
return amount;
|
|
4584
4863
|
}
|
|
4585
4864
|
|
|
4865
|
+
// modules/payments-v2/burn.ts
|
|
4866
|
+
var HEX32 = /^[0-9a-f]{64}$/;
|
|
4867
|
+
async function heldToken(deps, tokenId) {
|
|
4868
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
4869
|
+
if (bytes === void 0) {
|
|
4870
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
|
|
4871
|
+
}
|
|
4872
|
+
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
4873
|
+
}
|
|
4874
|
+
function assertBurnable(engine, token, tokenId) {
|
|
4875
|
+
if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
|
|
4876
|
+
throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
|
|
4877
|
+
}
|
|
4878
|
+
if (token.valueEnvelope === "bare_collection") {
|
|
4879
|
+
throw new SphereError(
|
|
4880
|
+
`Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
|
|
4881
|
+
"VALIDATION_ERROR"
|
|
4882
|
+
);
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
function assetsOf(token) {
|
|
4886
|
+
return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
|
|
4887
|
+
}
|
|
4888
|
+
async function runBurnUnderJournal(deps, input) {
|
|
4889
|
+
const { burnId } = input;
|
|
4890
|
+
const { tokenId, reasonBytes } = input.request;
|
|
4891
|
+
if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
|
|
4892
|
+
return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
|
|
4893
|
+
}
|
|
4894
|
+
let token;
|
|
4895
|
+
try {
|
|
4896
|
+
token = await heldToken(deps, tokenId);
|
|
4897
|
+
assertBurnable(deps.engine, token, tokenId);
|
|
4898
|
+
} catch (err) {
|
|
4899
|
+
return refused(burnId, tokenId, err);
|
|
4900
|
+
}
|
|
4901
|
+
const entry = {
|
|
4902
|
+
burnId,
|
|
4903
|
+
tokenId,
|
|
4904
|
+
reasonHex: bytesToHex(reasonBytes),
|
|
4905
|
+
burnedTokenHex: null,
|
|
4906
|
+
settled: false,
|
|
4907
|
+
assets: assetsOf(token),
|
|
4908
|
+
createdAt: deps.now()
|
|
4909
|
+
};
|
|
4910
|
+
await deps.burnJournal.upsert(entry);
|
|
4911
|
+
try {
|
|
4912
|
+
const settled = await burnJournaled(deps, entry, token);
|
|
4913
|
+
return { success: true, burnId, tokenId, burnedToken: hexToBytes(settled.burnedTokenHex ?? "") };
|
|
4914
|
+
} catch (err) {
|
|
4915
|
+
const failure = failureOf(err);
|
|
4916
|
+
if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
|
|
4917
|
+
else deps.armHeartbeat();
|
|
4918
|
+
return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
async function replayBurns(deps) {
|
|
4922
|
+
let progressed = 0;
|
|
4923
|
+
for (const entry of await deps.burnJournal.list()) {
|
|
4924
|
+
if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
|
|
4925
|
+
try {
|
|
4926
|
+
const token = entry.burnedTokenHex === null ? await heldToken(deps, entry.tokenId) : null;
|
|
4927
|
+
await burnJournaled(deps, entry, token);
|
|
4928
|
+
progressed += 1;
|
|
4929
|
+
} catch (err) {
|
|
4930
|
+
if (!isTerminal(err)) continue;
|
|
4931
|
+
await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
|
|
4932
|
+
progressed += 1;
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
return progressed;
|
|
4936
|
+
}
|
|
4937
|
+
async function pendingBurns(deps) {
|
|
4938
|
+
return (await deps.burnJournal.list()).map((entry) => ({
|
|
4939
|
+
burnId: entry.burnId,
|
|
4940
|
+
tokenId: entry.tokenId,
|
|
4941
|
+
reasonBytes: hexToBytes(entry.reasonHex),
|
|
4942
|
+
burnedToken: entry.burnedTokenHex === null ? null : hexToBytes(entry.burnedTokenHex),
|
|
4943
|
+
settled: entry.settled,
|
|
4944
|
+
createdAt: entry.createdAt,
|
|
4945
|
+
...entry.failure !== void 0 ? { failure: entry.failure } : {}
|
|
4946
|
+
}));
|
|
4947
|
+
}
|
|
4948
|
+
async function acknowledgeBurn(deps, burnId) {
|
|
4949
|
+
const entry = await deps.burnJournal.getByKey(burnId);
|
|
4950
|
+
if (entry === void 0) return;
|
|
4951
|
+
if (isLiveBurn(entry)) {
|
|
4952
|
+
throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
|
|
4953
|
+
}
|
|
4954
|
+
await deps.burnJournal.removeByKey(burnId);
|
|
4955
|
+
}
|
|
4956
|
+
function refused(burnId, tokenId, err) {
|
|
4957
|
+
const failure = failureOf(err);
|
|
4958
|
+
return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
|
|
4959
|
+
}
|
|
4960
|
+
function isTerminal(err) {
|
|
4961
|
+
return classifyError(err) === "conflict";
|
|
4962
|
+
}
|
|
4963
|
+
function failureOf(err) {
|
|
4964
|
+
const code = err !== null && typeof err === "object" ? err.code : void 0;
|
|
4965
|
+
return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
|
|
4966
|
+
}
|
|
4967
|
+
async function burnJournaled(deps, entry, token) {
|
|
4968
|
+
let current = entry;
|
|
4969
|
+
if (current.burnedTokenHex === null) {
|
|
4970
|
+
if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
|
|
4971
|
+
const burned = await deps.engine.burn(
|
|
4972
|
+
{ token, reasonBytes: hexToBytes(current.reasonHex) },
|
|
4973
|
+
{ transferId: current.burnId }
|
|
4974
|
+
);
|
|
4975
|
+
if (burned.blob.tokenId !== current.tokenId) {
|
|
4976
|
+
throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
|
|
4977
|
+
}
|
|
4978
|
+
current = { ...current, burnedTokenHex: bytesToHex(burned.blob.token) };
|
|
4979
|
+
await deps.burnJournal.upsert(current);
|
|
4980
|
+
}
|
|
4981
|
+
await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
|
|
4982
|
+
await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
|
|
4983
|
+
current = { ...current, settled: true };
|
|
4984
|
+
await deps.burnJournal.upsert(current);
|
|
4985
|
+
deps.refreshView();
|
|
4986
|
+
return current;
|
|
4987
|
+
}
|
|
4988
|
+
|
|
4989
|
+
// modules/payments-v2/burn-hold.ts
|
|
4990
|
+
var BurnHold = class {
|
|
4991
|
+
constructor(deps) {
|
|
4992
|
+
this.deps = deps;
|
|
4993
|
+
}
|
|
4994
|
+
async run(burnId, tokenId, burn) {
|
|
4995
|
+
try {
|
|
4996
|
+
this.deps.queue.planWhole(burnId, tokenId);
|
|
4997
|
+
} catch (err) {
|
|
4998
|
+
return refused(burnId, tokenId, err);
|
|
4999
|
+
}
|
|
5000
|
+
this.deps.view.markInFlightMany([tokenId]);
|
|
5001
|
+
let burned = false;
|
|
5002
|
+
try {
|
|
5003
|
+
const result = await burn();
|
|
5004
|
+
burned = result.success;
|
|
5005
|
+
return result;
|
|
5006
|
+
} finally {
|
|
5007
|
+
await this.settle(burnId, tokenId, burned);
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
async settle(burnId, tokenId, burned) {
|
|
5011
|
+
if (burned) {
|
|
5012
|
+
this.deps.ledger.commit(burnId);
|
|
5013
|
+
this.deps.track(this.refreshThenRelease(tokenId));
|
|
5014
|
+
return;
|
|
5015
|
+
}
|
|
5016
|
+
const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
|
|
5017
|
+
if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
|
|
5018
|
+
this.deps.ledger.cancel(burnId);
|
|
5019
|
+
this.deps.view.releaseMany([tokenId]);
|
|
5020
|
+
this.deps.queue.notifyChange("");
|
|
5021
|
+
return;
|
|
5022
|
+
}
|
|
5023
|
+
this.deps.pins.adopt(burnId);
|
|
5024
|
+
}
|
|
5025
|
+
async refreshThenRelease(tokenId) {
|
|
5026
|
+
try {
|
|
5027
|
+
await this.deps.view.delta();
|
|
5028
|
+
this.deps.view.releaseMany([tokenId]);
|
|
5029
|
+
} finally {
|
|
5030
|
+
this.deps.queue.notifyChange("");
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
};
|
|
5034
|
+
|
|
4586
5035
|
// modules/payments-v2/compose.ts
|
|
4587
5036
|
function supportsDeterministicMint(engine) {
|
|
4588
5037
|
return engine.deterministicMint === true;
|
|
@@ -4590,16 +5039,8 @@ function supportsDeterministicMint(engine) {
|
|
|
4590
5039
|
function composeFacadeParts(deps, hooks) {
|
|
4591
5040
|
const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
|
|
4592
5041
|
const ledger = new ReservationLedger();
|
|
4593
|
-
const
|
|
4594
|
-
|
|
4595
|
-
kv: deps.kv,
|
|
4596
|
-
emit: (event) => deps.emit(event, {}),
|
|
4597
|
-
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
4598
|
-
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
4599
|
-
// must not call a token spendable that the queue is about to refuse to spend.
|
|
4600
|
-
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
4601
|
-
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
4602
|
-
});
|
|
5042
|
+
const verdicts = buildVerdicts(deps, hooks);
|
|
5043
|
+
const view = buildView(deps, hooks, ledger, verdicts);
|
|
4603
5044
|
const queue = new SpendQueue({
|
|
4604
5045
|
ledger,
|
|
4605
5046
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -4614,27 +5055,57 @@ function composeFacadeParts(deps, hooks) {
|
|
|
4614
5055
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
4615
5056
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
4616
5057
|
});
|
|
4617
|
-
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
5058
|
+
const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
|
|
4618
5059
|
const machineStores = createMachineStores(deps.kv);
|
|
4619
5060
|
const heldStates = /* @__PURE__ */ new Map();
|
|
4620
5061
|
const refreshView = coalesced(() => view.delta(), hooks.track);
|
|
5062
|
+
const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
|
|
4621
5063
|
return {
|
|
4622
5064
|
ownPubkeyBytes,
|
|
4623
5065
|
view,
|
|
5066
|
+
verdicts,
|
|
4624
5067
|
refreshView,
|
|
4625
5068
|
ledger,
|
|
4626
|
-
pins
|
|
5069
|
+
pins,
|
|
4627
5070
|
queue,
|
|
5071
|
+
burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
|
|
4628
5072
|
historyStore,
|
|
4629
5073
|
machineStores,
|
|
4630
5074
|
machineDeps,
|
|
4631
5075
|
machine: new TransferMachine(machineDeps),
|
|
4632
5076
|
heldStates,
|
|
4633
|
-
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
|
|
5077
|
+
receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
|
|
4634
5078
|
requests: buildRequests(deps, hooks),
|
|
4635
5079
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
4636
5080
|
};
|
|
4637
5081
|
}
|
|
5082
|
+
function buildView(deps, hooks, ledger, verdicts) {
|
|
5083
|
+
const view = new InventoryView({
|
|
5084
|
+
port: deps.storagePort,
|
|
5085
|
+
kv: deps.kv,
|
|
5086
|
+
trust: verdicts,
|
|
5087
|
+
emit: (event) => {
|
|
5088
|
+
deps.emit(event, {});
|
|
5089
|
+
hooks.track(verdicts.review(view.claimHolders()));
|
|
5090
|
+
},
|
|
5091
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
5092
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
5093
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
5094
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
5095
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
5096
|
+
});
|
|
5097
|
+
return view;
|
|
5098
|
+
}
|
|
5099
|
+
function buildVerdicts(deps, hooks) {
|
|
5100
|
+
return new TokenVerdicts({
|
|
5101
|
+
kv: deps.kv,
|
|
5102
|
+
claims: deps.claims ?? new CoinClaims(),
|
|
5103
|
+
engine: () => hooks.engine(),
|
|
5104
|
+
getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
|
|
5105
|
+
changed: () => deps.emit("inventory:updated", {}),
|
|
5106
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
5107
|
+
});
|
|
5108
|
+
}
|
|
4638
5109
|
function buildPins(deps, hooks, parts) {
|
|
4639
5110
|
return new IntentPins({
|
|
4640
5111
|
ledger: parts.ledger,
|
|
@@ -4662,8 +5133,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
|
4662
5133
|
}
|
|
4663
5134
|
};
|
|
4664
5135
|
}
|
|
4665
|
-
function buildMachineDeps(deps, hooks, historyStore) {
|
|
5136
|
+
function buildMachineDeps(deps, hooks, historyStore, verdicts) {
|
|
4666
5137
|
return {
|
|
5138
|
+
recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
|
|
4667
5139
|
engine: () => hooks.engine(),
|
|
4668
5140
|
storage: deps.storagePort,
|
|
4669
5141
|
delivery: deps.deliveryPort,
|
|
@@ -4696,10 +5168,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
4696
5168
|
}
|
|
4697
5169
|
};
|
|
4698
5170
|
}
|
|
4699
|
-
function buildReceive(deps, hooks,
|
|
5171
|
+
function buildReceive(deps, hooks, parts, refreshView) {
|
|
5172
|
+
const { historyStore, heldStates, verdicts } = parts;
|
|
4700
5173
|
return new Receive({
|
|
4701
5174
|
delivery: deps.deliveryPort,
|
|
4702
5175
|
engine: () => hooks.engine(),
|
|
5176
|
+
claims: deps.claims ?? new CoinClaims(),
|
|
5177
|
+
accepted: (token) => verdicts.accept(token),
|
|
4703
5178
|
view: {
|
|
4704
5179
|
heldState: (tokenId) => heldStates.get(tokenId) ?? null,
|
|
4705
5180
|
store: async (entry) => {
|
|
@@ -4818,6 +5293,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
|
|
|
4818
5293
|
}
|
|
4819
5294
|
return false;
|
|
4820
5295
|
}
|
|
5296
|
+
async function seedHeldStates(storagePort, heldStates) {
|
|
5297
|
+
let page = await storagePort.listInventory();
|
|
5298
|
+
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
5299
|
+
for (const item of page.items) {
|
|
5300
|
+
if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
|
|
5301
|
+
}
|
|
5302
|
+
if (!page.more) return;
|
|
5303
|
+
page = await storagePort.listInventory(page.cursor);
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
4821
5306
|
async function replayCoinMints(deps) {
|
|
4822
5307
|
let resolved = 0;
|
|
4823
5308
|
for (const entry of await deps.mintJournal.list()) {
|
|
@@ -4921,98 +5406,9 @@ async function mintJournaled(deps, entry) {
|
|
|
4921
5406
|
await finalizeMint(deps, { mintId: entry.mintId, token, assets: [], journal: deps.nftMintJournal });
|
|
4922
5407
|
}
|
|
4923
5408
|
|
|
4924
|
-
// modules/payments-v2/inventory/nft-read.ts
|
|
4925
|
-
var NFT_CACHE_MAX = 256;
|
|
4926
|
-
var NftCache = class {
|
|
4927
|
-
constructor(max = NFT_CACHE_MAX) {
|
|
4928
|
-
this.max = max;
|
|
4929
|
-
}
|
|
4930
|
-
entries = /* @__PURE__ */ new Map();
|
|
4931
|
-
/** `undefined` = not cached. A hit becomes the most recently used entry. */
|
|
4932
|
-
get(tokenId) {
|
|
4933
|
-
const view = this.entries.get(tokenId);
|
|
4934
|
-
if (view === void 0) return void 0;
|
|
4935
|
-
this.entries.delete(tokenId);
|
|
4936
|
-
this.entries.set(tokenId, view);
|
|
4937
|
-
return view;
|
|
4938
|
-
}
|
|
4939
|
-
set(tokenId, view) {
|
|
4940
|
-
this.entries.delete(tokenId);
|
|
4941
|
-
this.entries.set(tokenId, view);
|
|
4942
|
-
const oldest = this.entries.keys().next().value;
|
|
4943
|
-
if (this.entries.size > this.max && oldest !== void 0) this.entries.delete(oldest);
|
|
4944
|
-
}
|
|
4945
|
-
};
|
|
4946
|
-
async function readNft(deps, tokenId) {
|
|
4947
|
-
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
4948
|
-
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
4949
|
-
}
|
|
4950
|
-
const cached = deps.cache.get(tokenId);
|
|
4951
|
-
if (cached !== void 0) return cached;
|
|
4952
|
-
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
4953
|
-
if (bytes === void 0) {
|
|
4954
|
-
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
4955
|
-
}
|
|
4956
|
-
return decodeView(deps, tokenId, bytes);
|
|
4957
|
-
}
|
|
4958
|
-
async function readNfts(deps, tokenIds) {
|
|
4959
|
-
const held = [...new Set(tokenIds)].filter((tokenId) => deps.view.stateHashOf(tokenId) !== void 0);
|
|
4960
|
-
const views = /* @__PURE__ */ new Map();
|
|
4961
|
-
const misses = [];
|
|
4962
|
-
for (const tokenId of held) {
|
|
4963
|
-
const cached = deps.cache.get(tokenId);
|
|
4964
|
-
if (cached === void 0) misses.push(tokenId);
|
|
4965
|
-
else views.set(tokenId, cached);
|
|
4966
|
-
}
|
|
4967
|
-
const blobs = misses.length > 0 ? await deps.storagePort.getBlobs(misses) : /* @__PURE__ */ new Map();
|
|
4968
|
-
for (const tokenId of misses) {
|
|
4969
|
-
const bytes = blobs.get(tokenId);
|
|
4970
|
-
if (bytes !== void 0) views.set(tokenId, await decodeView(deps, tokenId, bytes).catch(() => null));
|
|
4971
|
-
}
|
|
4972
|
-
const out = /* @__PURE__ */ new Map();
|
|
4973
|
-
for (const tokenId of held) {
|
|
4974
|
-
const view = views.get(tokenId);
|
|
4975
|
-
if (view) out.set(tokenId, view);
|
|
4976
|
-
}
|
|
4977
|
-
return out;
|
|
4978
|
-
}
|
|
4979
|
-
async function decodeView(deps, tokenId, bytes) {
|
|
4980
|
-
let view;
|
|
4981
|
-
try {
|
|
4982
|
-
const token = await deps.engine.decodeToken({ tokenId, token: bytes });
|
|
4983
|
-
if (token.blob.tokenId !== tokenId) {
|
|
4984
|
-
throw new SphereError(`the blob served for ${tokenId} decodes as ${token.blob.tokenId}`, "STORAGE_ERROR");
|
|
4985
|
-
}
|
|
4986
|
-
const reading = await deps.engine.readNft(token);
|
|
4987
|
-
view = reading === null ? null : { tokenId, ...reading };
|
|
4988
|
-
} catch (err) {
|
|
4989
|
-
logger.warn("PaymentsV2", `NFT read: token ${tokenId} did not decode \u2014 ${String(err)}`);
|
|
4990
|
-
throw err;
|
|
4991
|
-
}
|
|
4992
|
-
deps.cache.set(tokenId, view);
|
|
4993
|
-
return view;
|
|
4994
|
-
}
|
|
4995
|
-
|
|
4996
|
-
// modules/payments-v2/inventory/token-data.ts
|
|
4997
|
-
async function readTokenData(deps, tokenId) {
|
|
4998
|
-
return deps.engine.readTokenData(await heldToken(deps, tokenId));
|
|
4999
|
-
}
|
|
5000
|
-
async function readTokenJustification(deps, tokenId) {
|
|
5001
|
-
return deps.engine.readTokenJustification(await heldToken(deps, tokenId));
|
|
5002
|
-
}
|
|
5003
|
-
async function heldToken(deps, tokenId) {
|
|
5004
|
-
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
5005
|
-
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
5006
|
-
}
|
|
5007
|
-
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
5008
|
-
if (bytes === void 0) {
|
|
5009
|
-
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
5010
|
-
}
|
|
5011
|
-
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
5012
|
-
}
|
|
5013
|
-
|
|
5014
5409
|
// modules/payments-v2/mint-custom.ts
|
|
5015
|
-
var
|
|
5410
|
+
var HEX322 = /^[0-9a-f]{64}$/;
|
|
5411
|
+
var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
5016
5412
|
function assertRequest(request) {
|
|
5017
5413
|
if (request.tokenType.length !== 32) {
|
|
5018
5414
|
throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
|
|
@@ -5023,19 +5419,27 @@ function assertRequest(request) {
|
|
|
5023
5419
|
if (request.data.length === 0) {
|
|
5024
5420
|
throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
|
|
5025
5421
|
}
|
|
5422
|
+
if (request.justification !== void 0 && request.justification.length === 0) {
|
|
5423
|
+
throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
|
|
5424
|
+
}
|
|
5425
|
+
if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
|
|
5426
|
+
throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
|
|
5427
|
+
}
|
|
5026
5428
|
for (const asset of request.assets) {
|
|
5027
|
-
if (!
|
|
5429
|
+
if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
|
|
5028
5430
|
throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
|
|
5029
5431
|
}
|
|
5030
5432
|
}
|
|
5031
5433
|
}
|
|
5032
5434
|
async function runCustomMintUnderJournal(deps, input) {
|
|
5435
|
+
let entry;
|
|
5033
5436
|
try {
|
|
5034
5437
|
assertRequest(input.request);
|
|
5438
|
+
entry = journalEntryOf2(input.mintId, input.request, deps.now());
|
|
5439
|
+
deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
|
|
5035
5440
|
} catch (err) {
|
|
5036
5441
|
return { success: false, error: messageOf(err) };
|
|
5037
5442
|
}
|
|
5038
|
-
const entry = journalEntryOf2(input.mintId, input.request, deps.now());
|
|
5039
5443
|
await deps.customMintJournal.upsert(entry);
|
|
5040
5444
|
let tokenId;
|
|
5041
5445
|
try {
|
|
@@ -5075,24 +5479,25 @@ function journalEntryOf2(mintId, request, createdAt) {
|
|
|
5075
5479
|
createdAt
|
|
5076
5480
|
};
|
|
5077
5481
|
}
|
|
5482
|
+
function mintParamsOf(deps, entry, mintJustificationVerifiers) {
|
|
5483
|
+
return {
|
|
5484
|
+
recipientPubkey: deps.ownPubkeyBytes,
|
|
5485
|
+
data: hexToBytes(entry.dataHex),
|
|
5486
|
+
tokenType: hexToBytes(entry.tokenTypeHex),
|
|
5487
|
+
salt: hexToBytes(entry.saltHex),
|
|
5488
|
+
...entry.justificationHex !== null ? { justification: hexToBytes(entry.justificationHex) } : {},
|
|
5489
|
+
...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
|
|
5490
|
+
};
|
|
5491
|
+
}
|
|
5078
5492
|
async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
|
|
5079
|
-
const token = await deps.engine.mintDataToken(
|
|
5080
|
-
{
|
|
5081
|
-
recipientPubkey: deps.ownPubkeyBytes,
|
|
5082
|
-
data: hexToBytes(entry.dataHex),
|
|
5083
|
-
tokenType: hexToBytes(entry.tokenTypeHex),
|
|
5084
|
-
salt: hexToBytes(entry.saltHex),
|
|
5085
|
-
...entry.justificationHex !== null ? { justification: hexToBytes(entry.justificationHex) } : {},
|
|
5086
|
-
...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
|
|
5087
|
-
},
|
|
5088
|
-
{ transferId: entry.mintId }
|
|
5089
|
-
);
|
|
5493
|
+
const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
|
|
5090
5494
|
if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
|
|
5091
5495
|
throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
|
|
5092
5496
|
}
|
|
5093
5497
|
if (entry.tokenId === "") {
|
|
5094
5498
|
await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
5095
5499
|
}
|
|
5500
|
+
if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
|
|
5096
5501
|
await finalizeMint(deps, {
|
|
5097
5502
|
mintId: entry.mintId,
|
|
5098
5503
|
token,
|
|
@@ -5102,106 +5507,94 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
|
|
|
5102
5507
|
return token.blob.tokenId;
|
|
5103
5508
|
}
|
|
5104
5509
|
|
|
5105
|
-
// modules/payments-v2/
|
|
5106
|
-
var
|
|
5107
|
-
|
|
5510
|
+
// modules/payments-v2/inventory/nft-read.ts
|
|
5511
|
+
var NFT_CACHE_MAX = 256;
|
|
5512
|
+
var NftCache = class {
|
|
5513
|
+
constructor(max = NFT_CACHE_MAX) {
|
|
5514
|
+
this.max = max;
|
|
5515
|
+
}
|
|
5516
|
+
entries = /* @__PURE__ */ new Map();
|
|
5517
|
+
/** `undefined` = not cached. A hit becomes the most recently used entry. */
|
|
5518
|
+
get(tokenId) {
|
|
5519
|
+
const view = this.entries.get(tokenId);
|
|
5520
|
+
if (view === void 0) return void 0;
|
|
5521
|
+
this.entries.delete(tokenId);
|
|
5522
|
+
this.entries.set(tokenId, view);
|
|
5523
|
+
return view;
|
|
5524
|
+
}
|
|
5525
|
+
set(tokenId, view) {
|
|
5526
|
+
this.entries.delete(tokenId);
|
|
5527
|
+
this.entries.set(tokenId, view);
|
|
5528
|
+
const oldest = this.entries.keys().next().value;
|
|
5529
|
+
if (this.entries.size > this.max && oldest !== void 0) this.entries.delete(oldest);
|
|
5530
|
+
}
|
|
5531
|
+
};
|
|
5532
|
+
async function readNft(deps, tokenId) {
|
|
5533
|
+
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
5534
|
+
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
5535
|
+
}
|
|
5536
|
+
const cached = deps.cache.get(tokenId);
|
|
5537
|
+
if (cached !== void 0) return cached;
|
|
5108
5538
|
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
5109
5539
|
if (bytes === void 0) {
|
|
5110
|
-
throw new SphereError(`Token ${tokenId} has no blob in storage
|
|
5540
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
5111
5541
|
}
|
|
5112
|
-
return deps
|
|
5113
|
-
}
|
|
5114
|
-
function assetsOf(token) {
|
|
5115
|
-
return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
|
|
5542
|
+
return decodeView(deps, tokenId, bytes);
|
|
5116
5543
|
}
|
|
5117
|
-
async function
|
|
5118
|
-
const
|
|
5119
|
-
const
|
|
5120
|
-
|
|
5121
|
-
|
|
5544
|
+
async function readNfts(deps, tokenIds) {
|
|
5545
|
+
const held = [...new Set(tokenIds)].filter((tokenId) => deps.view.stateHashOf(tokenId) !== void 0);
|
|
5546
|
+
const views = /* @__PURE__ */ new Map();
|
|
5547
|
+
const misses = [];
|
|
5548
|
+
for (const tokenId of held) {
|
|
5549
|
+
const cached = deps.cache.get(tokenId);
|
|
5550
|
+
if (cached === void 0) misses.push(tokenId);
|
|
5551
|
+
else views.set(tokenId, cached);
|
|
5122
5552
|
}
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
if (
|
|
5127
|
-
throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
|
|
5128
|
-
}
|
|
5129
|
-
} catch (err) {
|
|
5130
|
-
return { success: false, burnId, tokenId, error: messageOf(err) };
|
|
5553
|
+
const blobs = misses.length > 0 ? await deps.storagePort.getBlobs(misses) : /* @__PURE__ */ new Map();
|
|
5554
|
+
for (const tokenId of misses) {
|
|
5555
|
+
const bytes = blobs.get(tokenId);
|
|
5556
|
+
if (bytes !== void 0) views.set(tokenId, await decodeView(deps, tokenId, bytes).catch(() => null));
|
|
5131
5557
|
}
|
|
5132
|
-
const
|
|
5133
|
-
|
|
5134
|
-
tokenId
|
|
5135
|
-
|
|
5136
|
-
burnedTokenHex: null,
|
|
5137
|
-
settled: false,
|
|
5138
|
-
assets: assetsOf(token),
|
|
5139
|
-
createdAt: deps.now()
|
|
5140
|
-
};
|
|
5141
|
-
await deps.burnJournal.upsert(entry);
|
|
5142
|
-
try {
|
|
5143
|
-
const settled = await burnJournaled(deps, entry, token, { applied: false });
|
|
5144
|
-
return { success: true, burnId, tokenId, burnedToken: hexToBytes(settled.burnedTokenHex ?? "") };
|
|
5145
|
-
} catch (err) {
|
|
5146
|
-
deps.armHeartbeat();
|
|
5147
|
-
return { success: false, burnId, tokenId, error: messageOf(err) };
|
|
5558
|
+
const out = /* @__PURE__ */ new Map();
|
|
5559
|
+
for (const tokenId of held) {
|
|
5560
|
+
const view = views.get(tokenId);
|
|
5561
|
+
if (view) out.set(tokenId, view);
|
|
5148
5562
|
}
|
|
5563
|
+
return out;
|
|
5149
5564
|
}
|
|
5150
|
-
async function
|
|
5151
|
-
let
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
|
|
5157
|
-
await burnJournaled(deps, entry, token, { applied });
|
|
5158
|
-
progressed += 1;
|
|
5159
|
-
} catch {
|
|
5160
|
-
continue;
|
|
5565
|
+
async function decodeView(deps, tokenId, bytes) {
|
|
5566
|
+
let view;
|
|
5567
|
+
try {
|
|
5568
|
+
const token = await deps.engine.decodeToken({ tokenId, token: bytes });
|
|
5569
|
+
if (token.blob.tokenId !== tokenId) {
|
|
5570
|
+
throw new SphereError(`the blob served for ${tokenId} decodes as ${token.blob.tokenId}`, "STORAGE_ERROR");
|
|
5161
5571
|
}
|
|
5572
|
+
const reading = await deps.engine.readNft(token);
|
|
5573
|
+
view = reading === null ? null : { tokenId, ...reading };
|
|
5574
|
+
} catch (err) {
|
|
5575
|
+
logger.warn("PaymentsV2", `NFT read: token ${tokenId} did not decode \u2014 ${String(err)}`);
|
|
5576
|
+
throw err;
|
|
5162
5577
|
}
|
|
5163
|
-
|
|
5578
|
+
deps.cache.set(tokenId, view);
|
|
5579
|
+
return view;
|
|
5164
5580
|
}
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
reasonBytes: hexToBytes(entry.reasonHex),
|
|
5170
|
-
burnedToken: entry.burnedTokenHex === null ? null : hexToBytes(entry.burnedTokenHex),
|
|
5171
|
-
settled: entry.settled,
|
|
5172
|
-
createdAt: entry.createdAt
|
|
5173
|
-
}));
|
|
5581
|
+
|
|
5582
|
+
// modules/payments-v2/inventory/token-data.ts
|
|
5583
|
+
async function readTokenData(deps, tokenId) {
|
|
5584
|
+
return deps.engine.readTokenData(await heldToken2(deps, tokenId));
|
|
5174
5585
|
}
|
|
5175
|
-
async function
|
|
5176
|
-
|
|
5177
|
-
if (entry === void 0) return;
|
|
5178
|
-
if (!entry.settled) {
|
|
5179
|
-
throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
|
|
5180
|
-
}
|
|
5181
|
-
await deps.burnJournal.removeByKey(burnId);
|
|
5586
|
+
async function readTokenJustification(deps, tokenId) {
|
|
5587
|
+
return deps.engine.readTokenJustification(await heldToken2(deps, tokenId));
|
|
5182
5588
|
}
|
|
5183
|
-
async function
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
|
|
5187
|
-
const burned = await deps.engine.burn(
|
|
5188
|
-
{ token, reasonBytes: hexToBytes(current.reasonHex) },
|
|
5189
|
-
{ transferId: current.burnId }
|
|
5190
|
-
);
|
|
5191
|
-
if (burned.blob.tokenId !== current.tokenId) {
|
|
5192
|
-
throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
|
|
5193
|
-
}
|
|
5194
|
-
current = { ...current, burnedTokenHex: bytesToHex(burned.blob.token) };
|
|
5195
|
-
await deps.burnJournal.upsert(current);
|
|
5589
|
+
async function heldToken2(deps, tokenId) {
|
|
5590
|
+
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
5591
|
+
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
5196
5592
|
}
|
|
5197
|
-
|
|
5198
|
-
|
|
5593
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
5594
|
+
if (bytes === void 0) {
|
|
5595
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
5199
5596
|
}
|
|
5200
|
-
|
|
5201
|
-
current = { ...current, settled: true };
|
|
5202
|
-
await deps.burnJournal.upsert(current);
|
|
5203
|
-
deps.refreshView();
|
|
5204
|
-
return current;
|
|
5597
|
+
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
5205
5598
|
}
|
|
5206
5599
|
|
|
5207
5600
|
// modules/payments-v2/send-whole.ts
|
|
@@ -5399,6 +5792,7 @@ var PaymentsFacade = class {
|
|
|
5399
5792
|
track: (op) => this.trackTail(op)
|
|
5400
5793
|
});
|
|
5401
5794
|
this.view = parts.view;
|
|
5795
|
+
this.verdicts = parts.verdicts;
|
|
5402
5796
|
this.wakeRefresh = parts.refreshView;
|
|
5403
5797
|
this.ledger = parts.ledger;
|
|
5404
5798
|
this.queue = parts.queue;
|
|
@@ -5412,6 +5806,7 @@ var PaymentsFacade = class {
|
|
|
5412
5806
|
this.restoreDeps = parts.restoreDeps;
|
|
5413
5807
|
this.requests = parts.requests;
|
|
5414
5808
|
this.pins = parts.pins;
|
|
5809
|
+
this.burnHold = parts.burnHold;
|
|
5415
5810
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
5416
5811
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
5417
5812
|
now: () => this.nowMs(),
|
|
@@ -5435,6 +5830,7 @@ var PaymentsFacade = class {
|
|
|
5435
5830
|
wakeRefresh;
|
|
5436
5831
|
prewarmed = new PrewarmCache();
|
|
5437
5832
|
view;
|
|
5833
|
+
verdicts;
|
|
5438
5834
|
ledger;
|
|
5439
5835
|
queue;
|
|
5440
5836
|
machine;
|
|
@@ -5461,6 +5857,7 @@ var PaymentsFacade = class {
|
|
|
5461
5857
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
5462
5858
|
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
5463
5859
|
pins;
|
|
5860
|
+
burnHold;
|
|
5464
5861
|
/** Per facade, so per address: an NFT reading is never served to another wallet. */
|
|
5465
5862
|
nftCache = new NftCache();
|
|
5466
5863
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -5485,7 +5882,8 @@ var PaymentsFacade = class {
|
|
|
5485
5882
|
await this.deps.session.start();
|
|
5486
5883
|
await this.pins.sync().catch(() => void 0);
|
|
5487
5884
|
this.trackTail(this.runConvergencePass());
|
|
5488
|
-
this.trackTail(this.
|
|
5885
|
+
this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
|
|
5886
|
+
await this.verdicts.start().catch(() => void 0);
|
|
5489
5887
|
await this.view.fullPull().catch(() => void 0);
|
|
5490
5888
|
this.receiveLoop.start(this.deps.receivePollMs);
|
|
5491
5889
|
}
|
|
@@ -5494,6 +5892,7 @@ var PaymentsFacade = class {
|
|
|
5494
5892
|
this.started = false;
|
|
5495
5893
|
this.heartbeat.stop();
|
|
5496
5894
|
this.receiveLoop.stop();
|
|
5895
|
+
this.verdicts.stop();
|
|
5497
5896
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
5498
5897
|
this.unsubscribers = [];
|
|
5499
5898
|
await this.deps.session.stop();
|
|
@@ -5510,12 +5909,17 @@ var PaymentsFacade = class {
|
|
|
5510
5909
|
}
|
|
5511
5910
|
// ── reads ──────────────────────────────────────────────────────────────────
|
|
5512
5911
|
async assets(coinId) {
|
|
5912
|
+
return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
|
|
5913
|
+
}
|
|
5914
|
+
unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
|
|
5915
|
+
async heldAssets(coinId) {
|
|
5513
5916
|
const all = await this.view.assets(this.deps.registry, this.deps.price);
|
|
5514
5917
|
return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
|
|
5515
5918
|
}
|
|
5516
5919
|
tokens(filter) {
|
|
5517
|
-
return this.view.tokens(this.deps.registry, filter);
|
|
5920
|
+
return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
|
|
5518
5921
|
}
|
|
5922
|
+
unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
|
|
5519
5923
|
coinless() {
|
|
5520
5924
|
return this.view.coinless(this.deps.registry);
|
|
5521
5925
|
}
|
|
@@ -5576,9 +5980,9 @@ var PaymentsFacade = class {
|
|
|
5576
5980
|
return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
|
|
5577
5981
|
}
|
|
5578
5982
|
mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
|
|
5579
|
-
burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
|
|
5580
|
-
pendingBurns = () => this.track(pendingBurns(this.
|
|
5581
|
-
acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.
|
|
5983
|
+
burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
|
|
5984
|
+
pendingBurns = () => this.track(pendingBurns(this.machineStores));
|
|
5985
|
+
acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
|
|
5582
5986
|
// §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
|
|
5583
5987
|
async resumeNow() {
|
|
5584
5988
|
if (!this.started) return;
|
|
@@ -5621,7 +6025,8 @@ var PaymentsFacade = class {
|
|
|
5621
6025
|
isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
|
|
5622
6026
|
tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
|
|
5623
6027
|
emit: this.deps.emit,
|
|
5624
|
-
now: () => this.nowMs()
|
|
6028
|
+
now: () => this.nowMs(),
|
|
6029
|
+
accepted: (token) => this.verdicts.accept(token)
|
|
5625
6030
|
};
|
|
5626
6031
|
}
|
|
5627
6032
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
@@ -5959,16 +6364,6 @@ var PaymentsFacade = class {
|
|
|
5959
6364
|
newId() {
|
|
5960
6365
|
return (this.deps.newId ?? randomUUID)();
|
|
5961
6366
|
}
|
|
5962
|
-
async seedHeldStates() {
|
|
5963
|
-
let page = await this.deps.storagePort.listInventory();
|
|
5964
|
-
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
5965
|
-
for (const item of page.items) {
|
|
5966
|
-
if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
|
|
5967
|
-
}
|
|
5968
|
-
if (!page.more) return;
|
|
5969
|
-
page = await this.deps.storagePort.listInventory(page.cursor);
|
|
5970
|
-
}
|
|
5971
|
-
}
|
|
5972
6367
|
track(op) {
|
|
5973
6368
|
const settled = op.then(() => void 0, () => void 0);
|
|
5974
6369
|
this.pendingOps.add(settled);
|
|
@@ -5983,6 +6378,7 @@ export {
|
|
|
5983
6378
|
ATTENTION_MINT_UNRESOLVED,
|
|
5984
6379
|
ATTENTION_RECIPIENT_NETWORK_UNVERIFIED,
|
|
5985
6380
|
ATTENTION_RESEED_REJECTED,
|
|
6381
|
+
CUSTOM_MINT_MAX_PAYLOAD_BYTES,
|
|
5986
6382
|
HEARTBEAT_CAP_MS,
|
|
5987
6383
|
HEARTBEAT_SEED_MS,
|
|
5988
6384
|
MAX_RESELECT,
|
|
@@ -5996,6 +6392,7 @@ export {
|
|
|
5996
6392
|
STORE_KEYS,
|
|
5997
6393
|
createScopedKV,
|
|
5998
6394
|
encodeNftContent,
|
|
6395
|
+
isLiveBurn,
|
|
5999
6396
|
isNftDocumentLink,
|
|
6000
6397
|
isRetryableAckError,
|
|
6001
6398
|
parseNftDocument,
|