@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
package/dist/core/index.js
CHANGED
|
@@ -9511,6 +9511,150 @@ async function deriveDirectAddress(publicKey) {
|
|
|
9511
9511
|
return `DIRECT://${HexConverter.encode(imprint)}${HexConverter.encode(checksum)}`;
|
|
9512
9512
|
}
|
|
9513
9513
|
|
|
9514
|
+
// token-engine/claims.ts
|
|
9515
|
+
var CoinClaims = class _CoinClaims {
|
|
9516
|
+
issuers = /* @__PURE__ */ new Map();
|
|
9517
|
+
policies = [];
|
|
9518
|
+
static fromPlugins(plugins) {
|
|
9519
|
+
const claims = new _CoinClaims();
|
|
9520
|
+
for (const plugin of plugins ?? []) {
|
|
9521
|
+
for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
|
|
9522
|
+
}
|
|
9523
|
+
return claims;
|
|
9524
|
+
}
|
|
9525
|
+
add(policy) {
|
|
9526
|
+
const tokenType = HexConverter.encode(policy.tokenType.bytes);
|
|
9527
|
+
const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
|
|
9528
|
+
const taken = coinIds.find((coinId) => this.issuers.has(coinId));
|
|
9529
|
+
if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
|
|
9530
|
+
for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
|
|
9531
|
+
this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
|
|
9532
|
+
}
|
|
9533
|
+
fingerprint() {
|
|
9534
|
+
return [...this.policies].sort().join("\n");
|
|
9535
|
+
}
|
|
9536
|
+
issuerOf(coinId) {
|
|
9537
|
+
return this.issuers.get(coinId.toLowerCase()) ?? null;
|
|
9538
|
+
}
|
|
9539
|
+
};
|
|
9540
|
+
|
|
9541
|
+
// token-engine/errors.ts
|
|
9542
|
+
var TransferConflictError = class extends SphereError {
|
|
9543
|
+
/**
|
|
9544
|
+
* #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
|
|
9545
|
+
* Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
|
|
9546
|
+
*/
|
|
9547
|
+
conflictedSourceId;
|
|
9548
|
+
constructor(message, cause) {
|
|
9549
|
+
super(message, "TRANSFER_CONFLICT", cause);
|
|
9550
|
+
this.name = "TransferConflictError";
|
|
9551
|
+
}
|
|
9552
|
+
};
|
|
9553
|
+
var MintReasonUnverifiableError = class extends SphereError {
|
|
9554
|
+
constructor(message, cause) {
|
|
9555
|
+
super(message, "MINT_REASON_UNVERIFIABLE", cause);
|
|
9556
|
+
this.name = "MintReasonUnverifiableError";
|
|
9557
|
+
}
|
|
9558
|
+
};
|
|
9559
|
+
var ProofUnconfirmedError = class extends SphereError {
|
|
9560
|
+
mayHaveCertified = true;
|
|
9561
|
+
constructor(message, cause) {
|
|
9562
|
+
super(message, "CERTIFICATION_UNCONFIRMED", cause);
|
|
9563
|
+
this.name = "ProofUnconfirmedError";
|
|
9564
|
+
}
|
|
9565
|
+
};
|
|
9566
|
+
var CheckpointPersistFailedError = class extends SphereError {
|
|
9567
|
+
mayHaveCertified = true;
|
|
9568
|
+
constructor(message, cause) {
|
|
9569
|
+
super(message, "CHECKPOINT_PERSIST_FAILED", cause);
|
|
9570
|
+
this.name = "CheckpointPersistFailedError";
|
|
9571
|
+
}
|
|
9572
|
+
};
|
|
9573
|
+
var SplitCheckpointLostError = class extends SphereError {
|
|
9574
|
+
constructor(message, cause) {
|
|
9575
|
+
super(message, "SPLIT_CHECKPOINT_LOST", cause);
|
|
9576
|
+
this.name = "SplitCheckpointLostError";
|
|
9577
|
+
}
|
|
9578
|
+
};
|
|
9579
|
+
var CheckpointTrustbaseMismatchError = class extends SphereError {
|
|
9580
|
+
constructor(message, cause) {
|
|
9581
|
+
super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
|
|
9582
|
+
this.name = "CheckpointTrustbaseMismatchError";
|
|
9583
|
+
}
|
|
9584
|
+
};
|
|
9585
|
+
function isKeepOpenSplitError(err) {
|
|
9586
|
+
return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
|
|
9587
|
+
}
|
|
9588
|
+
|
|
9589
|
+
// token-engine/mint-reasons.ts
|
|
9590
|
+
var RULE = "MintJustificationVerification";
|
|
9591
|
+
var MintReasonRegistry = class _MintReasonRegistry extends MintJustificationVerifierService {
|
|
9592
|
+
byTag = /* @__PURE__ */ new Map();
|
|
9593
|
+
pluginTags = /* @__PURE__ */ new Set();
|
|
9594
|
+
register(verifier) {
|
|
9595
|
+
super.register(verifier);
|
|
9596
|
+
this.byTag.set(verifier.tag, verifier);
|
|
9597
|
+
return this;
|
|
9598
|
+
}
|
|
9599
|
+
registerPlugin(verifier) {
|
|
9600
|
+
this.register(verifier);
|
|
9601
|
+
this.pluginTags.add(verifier.tag);
|
|
9602
|
+
return this;
|
|
9603
|
+
}
|
|
9604
|
+
async verify(transaction, nestedTokenCollector) {
|
|
9605
|
+
const tag = transaction.justification ? reasonTag(transaction.justification) : null;
|
|
9606
|
+
if (tag === null) return super.verify(transaction, nestedTokenCollector);
|
|
9607
|
+
const verifier = this.byTag.get(tag);
|
|
9608
|
+
if (verifier === void 0) throw new MintReasonUnverifiableError(`No verifier is registered for mint-reason tag ${tag}`);
|
|
9609
|
+
if (!this.pluginTags.has(tag)) return super.verify(transaction, nestedTokenCollector);
|
|
9610
|
+
return verifyPlugin(verifier, transaction, nestedTokenCollector);
|
|
9611
|
+
}
|
|
9612
|
+
knows(tag) {
|
|
9613
|
+
return this.byTag.has(tag);
|
|
9614
|
+
}
|
|
9615
|
+
/** `verifiers` replace the registered ones of their tags; a tag this registry lacks is refused. */
|
|
9616
|
+
overlaid(verifiers) {
|
|
9617
|
+
for (const verifier of verifiers) assertKnown(this, verifier.tag);
|
|
9618
|
+
const replaced = new Map(verifiers.map((verifier) => [verifier.tag, verifier]));
|
|
9619
|
+
const overlay = new _MintReasonRegistry();
|
|
9620
|
+
for (const [tag, verifier] of this.byTag) {
|
|
9621
|
+
const perCall = replaced.get(tag);
|
|
9622
|
+
if (perCall !== void 0 || this.pluginTags.has(tag)) overlay.registerPlugin(perCall ?? verifier);
|
|
9623
|
+
else overlay.register(verifier);
|
|
9624
|
+
}
|
|
9625
|
+
return overlay;
|
|
9626
|
+
}
|
|
9627
|
+
assertVerifiable(justification) {
|
|
9628
|
+
const tag = reasonTag(justification);
|
|
9629
|
+
if (tag === null) throw new SphereError("The mint reason is not tagged CBOR", "VALIDATION_ERROR");
|
|
9630
|
+
assertKnown(this, tag);
|
|
9631
|
+
}
|
|
9632
|
+
};
|
|
9633
|
+
function reasonTag(justification) {
|
|
9634
|
+
try {
|
|
9635
|
+
return BigInt(CborDeserializer.decodeTag(justification).tag);
|
|
9636
|
+
} catch {
|
|
9637
|
+
return null;
|
|
9638
|
+
}
|
|
9639
|
+
}
|
|
9640
|
+
async function verifyPlugin(verifier, transaction, nestedTokenCollector) {
|
|
9641
|
+
let result;
|
|
9642
|
+
try {
|
|
9643
|
+
result = await verifier.verify(transaction, nestedTokenCollector);
|
|
9644
|
+
} catch (err) {
|
|
9645
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
9646
|
+
throw new MintReasonUnverifiableError(`Mint-reason tag ${verifier.tag} cannot be verified yet: ${why}`, err);
|
|
9647
|
+
}
|
|
9648
|
+
return result.status === VerificationStatus.OK ? new VerificationResult(RULE, VerificationStatus.OK, "", [result]) : new VerificationResult(RULE, VerificationStatus.FAIL, `Verification failed for tag ${verifier.tag}.`, [result]);
|
|
9649
|
+
}
|
|
9650
|
+
function assertKnown(registry, tag) {
|
|
9651
|
+
if (registry.knows(tag)) return;
|
|
9652
|
+
throw new SphereError(
|
|
9653
|
+
`No verifier is registered on the engine for mint-reason tag ${tag}, so a replay of this mint could never verify`,
|
|
9654
|
+
"VALIDATION_ERROR"
|
|
9655
|
+
);
|
|
9656
|
+
}
|
|
9657
|
+
|
|
9514
9658
|
// token-engine/SpherePaymentData.ts
|
|
9515
9659
|
var COIN_ID_PATTERN = /^([0-9a-f]{2})+$/;
|
|
9516
9660
|
function assertAsset(coinId, amount) {
|
|
@@ -9687,45 +9831,6 @@ async function awaitProofBounded(deps, transaction, signal) {
|
|
|
9687
9831
|
return value;
|
|
9688
9832
|
}
|
|
9689
9833
|
|
|
9690
|
-
// token-engine/errors.ts
|
|
9691
|
-
var TransferConflictError = class extends SphereError {
|
|
9692
|
-
/**
|
|
9693
|
-
* #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
|
|
9694
|
-
* Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
|
|
9695
|
-
*/
|
|
9696
|
-
conflictedSourceId;
|
|
9697
|
-
constructor(message, cause) {
|
|
9698
|
-
super(message, "TRANSFER_CONFLICT", cause);
|
|
9699
|
-
this.name = "TransferConflictError";
|
|
9700
|
-
}
|
|
9701
|
-
};
|
|
9702
|
-
var ProofUnconfirmedError = class extends SphereError {
|
|
9703
|
-
mayHaveCertified = true;
|
|
9704
|
-
constructor(message, cause) {
|
|
9705
|
-
super(message, "CERTIFICATION_UNCONFIRMED", cause);
|
|
9706
|
-
this.name = "ProofUnconfirmedError";
|
|
9707
|
-
}
|
|
9708
|
-
};
|
|
9709
|
-
var CheckpointPersistFailedError = class extends SphereError {
|
|
9710
|
-
mayHaveCertified = true;
|
|
9711
|
-
constructor(message, cause) {
|
|
9712
|
-
super(message, "CHECKPOINT_PERSIST_FAILED", cause);
|
|
9713
|
-
this.name = "CheckpointPersistFailedError";
|
|
9714
|
-
}
|
|
9715
|
-
};
|
|
9716
|
-
var SplitCheckpointLostError = class extends SphereError {
|
|
9717
|
-
constructor(message, cause) {
|
|
9718
|
-
super(message, "SPLIT_CHECKPOINT_LOST", cause);
|
|
9719
|
-
this.name = "SplitCheckpointLostError";
|
|
9720
|
-
}
|
|
9721
|
-
};
|
|
9722
|
-
var CheckpointTrustbaseMismatchError = class extends SphereError {
|
|
9723
|
-
constructor(message, cause) {
|
|
9724
|
-
super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
|
|
9725
|
-
this.name = "CheckpointTrustbaseMismatchError";
|
|
9726
|
-
}
|
|
9727
|
-
};
|
|
9728
|
-
|
|
9729
9834
|
// token-engine/certification-outcome.ts
|
|
9730
9835
|
function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
|
|
9731
9836
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -10288,15 +10393,17 @@ async function readTokenNft(token) {
|
|
|
10288
10393
|
}
|
|
10289
10394
|
|
|
10290
10395
|
// token-engine/plugin-ops.ts
|
|
10291
|
-
function
|
|
10292
|
-
|
|
10293
|
-
|
|
10396
|
+
function mintContext(deps, params) {
|
|
10397
|
+
assertMintableData(params.data);
|
|
10398
|
+
if (params.justification !== void 0) deps.mintJustificationVerifier.assertVerifiable(params.justification);
|
|
10399
|
+
const verifiers = params.mintJustificationVerifiers;
|
|
10400
|
+
if (verifiers === void 0) return deps.verificationContext;
|
|
10294
10401
|
return new VerificationContext(
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10402
|
+
deps.trustBase,
|
|
10403
|
+
deps.predicateVerifier,
|
|
10404
|
+
deps.unicityCertificateVerifier,
|
|
10405
|
+
deps.mintJustificationVerifier.overlaid(verifiers),
|
|
10406
|
+
deps.verificationContext.tokenIssuanceVerifier
|
|
10300
10407
|
);
|
|
10301
10408
|
}
|
|
10302
10409
|
|
|
@@ -10384,9 +10491,6 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
|
|
|
10384
10491
|
// token-engine/SphereTokenEngine.ts
|
|
10385
10492
|
var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
|
|
10386
10493
|
var MAX_MINT_CONCURRENCY = 8;
|
|
10387
|
-
function isKeepOpenSplitError(err) {
|
|
10388
|
-
return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
|
|
10389
|
-
}
|
|
10390
10494
|
var TRANSFER_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
10391
10495
|
var CLEAN_REJECT_STATUSES = /* @__PURE__ */ new Set([
|
|
10392
10496
|
CertificationStatus.STATE_ID_MISMATCH,
|
|
@@ -10476,8 +10580,9 @@ var SphereTokenEngine = class {
|
|
|
10476
10580
|
const token = await Token.mint(certified, this.deps.verificationContext);
|
|
10477
10581
|
return wrapToken(token);
|
|
10478
10582
|
}
|
|
10583
|
+
assertMintable = (params) => void mintContext(this.deps, params);
|
|
10479
10584
|
async mintDataToken(params, options) {
|
|
10480
|
-
|
|
10585
|
+
const context = mintContext(this.deps, params);
|
|
10481
10586
|
const recipient = SignaturePredicate.create(params.recipientPubkey);
|
|
10482
10587
|
const tokenType = params.tokenType ? new TokenType(params.tokenType) : TokenType.generate();
|
|
10483
10588
|
const salt = params.salt ? TokenSalt.fromBytes(params.salt) : TokenSalt.generate();
|
|
@@ -10491,8 +10596,7 @@ var SphereTokenEngine = class {
|
|
|
10491
10596
|
options
|
|
10492
10597
|
);
|
|
10493
10598
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10494
|
-
|
|
10495
|
-
return wrapToken(await Token.mint(certified, verifiers ? contextWithMintVerifiers(this.deps, verifiers) : this.deps.verificationContext));
|
|
10599
|
+
return wrapToken(await Token.mint(certified, context));
|
|
10496
10600
|
}
|
|
10497
10601
|
buildNftMint(params) {
|
|
10498
10602
|
return planNftMint(this.deps, params);
|
|
@@ -10829,20 +10933,30 @@ var SphereTokenEngine = class {
|
|
|
10829
10933
|
|
|
10830
10934
|
// token-engine/factory.ts
|
|
10831
10935
|
var DEFAULT_VERIFICATION_POOL_SIZE = 4;
|
|
10832
|
-
function
|
|
10936
|
+
function registerPlugins(registries, plugins) {
|
|
10833
10937
|
for (const plugin of plugins ?? []) {
|
|
10834
10938
|
for (const verifier of plugin.mintJustificationVerifiers ?? []) {
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
);
|
|
10842
|
-
}
|
|
10939
|
+
claim(plugin, `mint-reason tag ${verifier.tag}`, () => registries.reasons.registerPlugin(verifier));
|
|
10940
|
+
}
|
|
10941
|
+
for (const policy of plugin.tokenIssuancePolicies ?? []) {
|
|
10942
|
+
const tokenType = HexConverter.encode(policy.tokenType.bytes);
|
|
10943
|
+
claim(plugin, `an issuance policy for token type ${tokenType}`, () => {
|
|
10944
|
+
registries.claims.add(policy);
|
|
10945
|
+
registries.policies.register(policy);
|
|
10946
|
+
});
|
|
10843
10947
|
}
|
|
10844
10948
|
}
|
|
10845
10949
|
}
|
|
10950
|
+
function claim(plugin, what, register) {
|
|
10951
|
+
try {
|
|
10952
|
+
register();
|
|
10953
|
+
} catch (err) {
|
|
10954
|
+
throw new SphereError(
|
|
10955
|
+
`Token plugin '${plugin.id}' registers ${what} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
|
|
10956
|
+
"INVALID_CONFIG"
|
|
10957
|
+
);
|
|
10958
|
+
}
|
|
10959
|
+
}
|
|
10846
10960
|
function disposedError() {
|
|
10847
10961
|
return new SphereError(
|
|
10848
10962
|
"Verification worker pool disposed \u2014 the in-flight verification was cancelled",
|
|
@@ -10920,11 +11034,15 @@ async function createSphereTokenEngine(config) {
|
|
|
10920
11034
|
const unicityCertificateVerifier = new UnicityCertificateVerifier(
|
|
10921
11035
|
new UnicitySealQuorumSignaturesVerificationRule(new Secp256k1SignatureVerifier(), new VerifiedSealCache(256))
|
|
10922
11036
|
);
|
|
10923
|
-
const mintJustificationVerifier = new
|
|
11037
|
+
const mintJustificationVerifier = new MintReasonRegistry();
|
|
10924
11038
|
mintJustificationVerifier.register(
|
|
10925
11039
|
new SplitMintJustificationVerifier(decodeSpherePaymentData)
|
|
10926
11040
|
);
|
|
10927
|
-
|
|
11041
|
+
const tokenIssuanceVerifier = new TokenIssuanceVerifierService(false);
|
|
11042
|
+
registerPlugins(
|
|
11043
|
+
{ reasons: mintJustificationVerifier, policies: tokenIssuanceVerifier, claims: new CoinClaims() },
|
|
11044
|
+
config.plugins
|
|
11045
|
+
);
|
|
10928
11046
|
const deps = {
|
|
10929
11047
|
client: new StateTransitionClient(new AggregatorClient(config.aggregatorUrl, config.apiKey ?? null)),
|
|
10930
11048
|
trustBase,
|
|
@@ -10936,7 +11054,7 @@ async function createSphereTokenEngine(config) {
|
|
|
10936
11054
|
predicateVerifier,
|
|
10937
11055
|
unicityCertificateVerifier,
|
|
10938
11056
|
mintJustificationVerifier,
|
|
10939
|
-
|
|
11057
|
+
tokenIssuanceVerifier
|
|
10940
11058
|
),
|
|
10941
11059
|
signingService: new SigningService(config.privateKey),
|
|
10942
11060
|
// Also the HKDF ikm for deterministic realization (Part E.1) — the
|
|
@@ -12036,6 +12154,9 @@ function createScopedKV(storage, network, chainPubkey) {
|
|
|
12036
12154
|
}
|
|
12037
12155
|
};
|
|
12038
12156
|
}
|
|
12157
|
+
function isLiveBurn(entry) {
|
|
12158
|
+
return !entry.settled && entry.failure === void 0;
|
|
12159
|
+
}
|
|
12039
12160
|
var STORE_KEYS = {
|
|
12040
12161
|
intentBackstop: "intents",
|
|
12041
12162
|
checkpointCache: "checkpoints",
|
|
@@ -12050,7 +12171,8 @@ var STORE_KEYS = {
|
|
|
12050
12171
|
epochLatch: "epoch-latch",
|
|
12051
12172
|
// §5.2 InventoryView durable overlays (#625/#679).
|
|
12052
12173
|
suspectedSpent: "suspected-spent",
|
|
12053
|
-
knownSpends: "known-spends"
|
|
12174
|
+
knownSpends: "known-spends",
|
|
12175
|
+
verifiedTokens: "verified-tokens"
|
|
12054
12176
|
};
|
|
12055
12177
|
|
|
12056
12178
|
// modules/payments-v2/machine/journal.ts
|
|
@@ -12567,6 +12689,7 @@ var TransferMachine = class {
|
|
|
12567
12689
|
const key = keys.get(digest);
|
|
12568
12690
|
if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
|
|
12569
12691
|
const { tokenId } = await engine.deliveryKeys(o.changeBlob);
|
|
12692
|
+
await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
|
|
12570
12693
|
added.push({ tokenId, key });
|
|
12571
12694
|
}
|
|
12572
12695
|
return added;
|
|
@@ -12962,7 +13085,7 @@ var Converger = class {
|
|
|
12962
13085
|
this.deps.stores.burnJournal.list()
|
|
12963
13086
|
]);
|
|
12964
13087
|
const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
|
|
12965
|
-
const burnsOpen = burns.some(
|
|
13088
|
+
const burnsOpen = burns.some(isLiveBurn);
|
|
12966
13089
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
|
|
12967
13090
|
}
|
|
12968
13091
|
async resumePass() {
|
|
@@ -13049,6 +13172,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
|
13049
13172
|
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
13050
13173
|
holds.set(entry.transferId, [...direct, ...split]);
|
|
13051
13174
|
}
|
|
13175
|
+
for (const burn of await stores.burnJournal.list()) {
|
|
13176
|
+
if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
|
|
13177
|
+
}
|
|
13052
13178
|
return { open: holds, complete };
|
|
13053
13179
|
}
|
|
13054
13180
|
function groupJournal(journal) {
|
|
@@ -13359,18 +13485,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
|
13359
13485
|
createdAt: snapshot.createdAt,
|
|
13360
13486
|
updatedAt: snapshot.updatedAt,
|
|
13361
13487
|
lazy: true,
|
|
13362
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
13488
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {},
|
|
13489
|
+
...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
|
|
13363
13490
|
};
|
|
13364
13491
|
}
|
|
13365
|
-
function aggregateAssets(entries,
|
|
13492
|
+
function aggregateAssets(entries, reading, registry) {
|
|
13366
13493
|
const groups = /* @__PURE__ */ new Map();
|
|
13367
13494
|
for (const [tokenId, entry] of entries) {
|
|
13368
|
-
const moving = isTransferring(tokenId);
|
|
13495
|
+
const moving = reading.isTransferring(tokenId);
|
|
13369
13496
|
for (const asset of entry.assets) {
|
|
13370
|
-
|
|
13497
|
+
const standing = reading.standing(tokenId, asset.coinId);
|
|
13498
|
+
const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
|
|
13499
|
+
let group = groups.get(key);
|
|
13371
13500
|
if (!group) {
|
|
13372
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
13373
|
-
groups.set(
|
|
13501
|
+
group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
13502
|
+
groups.set(key, group);
|
|
13374
13503
|
}
|
|
13375
13504
|
if (moving) {
|
|
13376
13505
|
group.transferring += BigInt(asset.amount);
|
|
@@ -13381,9 +13510,10 @@ function aggregateAssets(entries, isTransferring, registry) {
|
|
|
13381
13510
|
}
|
|
13382
13511
|
}
|
|
13383
13512
|
}
|
|
13384
|
-
return [...groups].map((
|
|
13513
|
+
return [...groups.values()].map((totals) => toAsset(totals, registry));
|
|
13385
13514
|
}
|
|
13386
|
-
function toAsset(
|
|
13515
|
+
function toAsset(totals, registry) {
|
|
13516
|
+
const coinId = totals.coinId;
|
|
13387
13517
|
const iconUrl = registry.getIconUrl(coinId);
|
|
13388
13518
|
return {
|
|
13389
13519
|
coinId,
|
|
@@ -13403,7 +13533,8 @@ function toAsset(coinId, totals, registry) {
|
|
|
13403
13533
|
priceEur: null,
|
|
13404
13534
|
change24h: null,
|
|
13405
13535
|
fiatValueUsd: null,
|
|
13406
|
-
fiatValueEur: null
|
|
13536
|
+
fiatValueEur: null,
|
|
13537
|
+
...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
|
|
13407
13538
|
};
|
|
13408
13539
|
}
|
|
13409
13540
|
function priceIdOf(registry, coinId) {
|
|
@@ -13411,9 +13542,9 @@ function priceIdOf(registry, coinId) {
|
|
|
13411
13542
|
}
|
|
13412
13543
|
async function withPrices(raw, registry, price) {
|
|
13413
13544
|
try {
|
|
13414
|
-
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
13545
|
+
const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
|
|
13415
13546
|
const quotes = await price.getPrices(ids);
|
|
13416
|
-
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
13547
|
+
return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
13417
13548
|
} catch {
|
|
13418
13549
|
return raw;
|
|
13419
13550
|
}
|
|
@@ -13434,11 +13565,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
|
13434
13565
|
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
13435
13566
|
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
13436
13567
|
transferring: true,
|
|
13437
|
-
suspectedSpent: false
|
|
13568
|
+
suspectedSpent: false,
|
|
13569
|
+
standing: "trusted"
|
|
13438
13570
|
});
|
|
13439
13571
|
}
|
|
13440
13572
|
|
|
13441
13573
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
13574
|
+
var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
|
|
13442
13575
|
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
13443
13576
|
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
13444
13577
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
@@ -13532,6 +13665,12 @@ var InventoryView = class {
|
|
|
13532
13665
|
for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
|
|
13533
13666
|
if (changed) this.deps.emit("inventory:updated");
|
|
13534
13667
|
}
|
|
13668
|
+
get trust() {
|
|
13669
|
+
return this.deps.trust ?? TRUST_ALL;
|
|
13670
|
+
}
|
|
13671
|
+
trusts(tokenId, coinId) {
|
|
13672
|
+
return this.trust.standing(tokenId, coinId) === "trusted";
|
|
13673
|
+
}
|
|
13535
13674
|
pinned(tokenId) {
|
|
13536
13675
|
return this.deps.isPinned?.(tokenId) ?? false;
|
|
13537
13676
|
}
|
|
@@ -13558,6 +13697,7 @@ var InventoryView = class {
|
|
|
13558
13697
|
isSpendable(tokenId, entry) {
|
|
13559
13698
|
if (entry.status !== "active") return false;
|
|
13560
13699
|
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
|
|
13700
|
+
if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
|
|
13561
13701
|
return !this.suspected.has(stateKey(tokenId, entry.stateHash));
|
|
13562
13702
|
}
|
|
13563
13703
|
/** Is this NAMED token spendable whole — coinless or valued (#777)? */
|
|
@@ -13584,7 +13724,8 @@ var InventoryView = class {
|
|
|
13584
13724
|
out.push(
|
|
13585
13725
|
toToken(tokenId, entry, asset, registry, {
|
|
13586
13726
|
transferring: this.held(tokenId),
|
|
13587
|
-
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
13727
|
+
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
|
|
13728
|
+
standing: this.trust.standing(tokenId, asset.coinId)
|
|
13588
13729
|
})
|
|
13589
13730
|
);
|
|
13590
13731
|
}
|
|
@@ -13614,10 +13755,21 @@ var InventoryView = class {
|
|
|
13614
13755
|
return out;
|
|
13615
13756
|
}
|
|
13616
13757
|
async assets(registry, price) {
|
|
13617
|
-
const
|
|
13758
|
+
const reading = {
|
|
13759
|
+
isTransferring: (tokenId) => this.held(tokenId),
|
|
13760
|
+
standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
|
|
13761
|
+
};
|
|
13762
|
+
const raw = aggregateAssets(this.activeEntries(), reading, registry);
|
|
13618
13763
|
if (!price || raw.length === 0) return raw;
|
|
13619
13764
|
return withPrices(raw, registry, price);
|
|
13620
13765
|
}
|
|
13766
|
+
claimHolders() {
|
|
13767
|
+
const out = [];
|
|
13768
|
+
for (const [tokenId, entry] of this.activeEntries()) {
|
|
13769
|
+
if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
|
|
13770
|
+
}
|
|
13771
|
+
return out;
|
|
13772
|
+
}
|
|
13621
13773
|
*activeEntries() {
|
|
13622
13774
|
for (const [tokenId, entry] of this.mirror) {
|
|
13623
13775
|
if (entry.status === "active") yield [tokenId, entry];
|
|
@@ -13787,6 +13939,171 @@ var InventoryView = class {
|
|
|
13787
13939
|
}
|
|
13788
13940
|
};
|
|
13789
13941
|
|
|
13942
|
+
// modules/payments-v2/inventory/verdicts.ts
|
|
13943
|
+
var VERDICT_RETRY_MS = 3e4;
|
|
13944
|
+
var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
|
|
13945
|
+
function honoursClaims(claims, token) {
|
|
13946
|
+
const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
|
|
13947
|
+
return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
|
|
13948
|
+
}
|
|
13949
|
+
var TokenVerdicts = class {
|
|
13950
|
+
constructor(deps) {
|
|
13951
|
+
this.deps = deps;
|
|
13952
|
+
}
|
|
13953
|
+
verified = /* @__PURE__ */ new Set();
|
|
13954
|
+
refused = /* @__PURE__ */ new Set();
|
|
13955
|
+
retries = /* @__PURE__ */ new Map();
|
|
13956
|
+
writes = new SerialChain();
|
|
13957
|
+
hydration = null;
|
|
13958
|
+
queued = null;
|
|
13959
|
+
running = null;
|
|
13960
|
+
held = [];
|
|
13961
|
+
timer = null;
|
|
13962
|
+
live = false;
|
|
13963
|
+
isClaimed(coinId) {
|
|
13964
|
+
return this.deps.claims.issuerOf(coinId) !== null;
|
|
13965
|
+
}
|
|
13966
|
+
standing(tokenId, coinId) {
|
|
13967
|
+
if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
|
|
13968
|
+
return this.refused.has(tokenId) ? "refused" : "pending";
|
|
13969
|
+
}
|
|
13970
|
+
trusts(tokenId, coinId) {
|
|
13971
|
+
return this.standing(tokenId, coinId) === "trusted";
|
|
13972
|
+
}
|
|
13973
|
+
hydrate() {
|
|
13974
|
+
this.hydration ??= this.load().catch((error) => {
|
|
13975
|
+
this.hydration = null;
|
|
13976
|
+
throw error;
|
|
13977
|
+
});
|
|
13978
|
+
return this.hydration;
|
|
13979
|
+
}
|
|
13980
|
+
start() {
|
|
13981
|
+
this.live = true;
|
|
13982
|
+
return this.hydrate();
|
|
13983
|
+
}
|
|
13984
|
+
stop() {
|
|
13985
|
+
this.live = false;
|
|
13986
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
13987
|
+
this.timer = null;
|
|
13988
|
+
}
|
|
13989
|
+
async accept(token) {
|
|
13990
|
+
if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
|
|
13991
|
+
}
|
|
13992
|
+
async recordSplit(sourceTokenId, outputTokenId) {
|
|
13993
|
+
await this.hydrate();
|
|
13994
|
+
if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
|
|
13995
|
+
}
|
|
13996
|
+
review(holders) {
|
|
13997
|
+
this.queued = holders;
|
|
13998
|
+
this.running ??= this.drain();
|
|
13999
|
+
return this.running;
|
|
14000
|
+
}
|
|
14001
|
+
async drain() {
|
|
14002
|
+
try {
|
|
14003
|
+
while (this.queued !== null) {
|
|
14004
|
+
const holders = this.queued;
|
|
14005
|
+
this.queued = null;
|
|
14006
|
+
await this.reviewOnce(holders);
|
|
14007
|
+
}
|
|
14008
|
+
} finally {
|
|
14009
|
+
this.running = null;
|
|
14010
|
+
}
|
|
14011
|
+
}
|
|
14012
|
+
async reviewOnce(holders) {
|
|
14013
|
+
await this.hydrate();
|
|
14014
|
+
this.held = holders;
|
|
14015
|
+
const due = holders.filter((tokenId) => this.isDue(tokenId));
|
|
14016
|
+
const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
|
|
14017
|
+
for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
|
|
14018
|
+
if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
|
|
14019
|
+
await this.persist();
|
|
14020
|
+
this.deps.changed();
|
|
14021
|
+
}
|
|
14022
|
+
this.scheduleRetry();
|
|
14023
|
+
}
|
|
14024
|
+
isDue(tokenId) {
|
|
14025
|
+
if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
|
|
14026
|
+
return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
|
|
14027
|
+
}
|
|
14028
|
+
async check(due) {
|
|
14029
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
14030
|
+
let blobs;
|
|
14031
|
+
try {
|
|
14032
|
+
blobs = await this.deps.getBlobs([...due]);
|
|
14033
|
+
} catch {
|
|
14034
|
+
return verdicts;
|
|
14035
|
+
}
|
|
14036
|
+
for (const tokenId of due) {
|
|
14037
|
+
const bytes = blobs.get(tokenId);
|
|
14038
|
+
if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
|
|
14039
|
+
}
|
|
14040
|
+
return verdicts;
|
|
14041
|
+
}
|
|
14042
|
+
async verdictOf(tokenId, bytes) {
|
|
14043
|
+
let engine;
|
|
14044
|
+
try {
|
|
14045
|
+
engine = this.deps.engine();
|
|
14046
|
+
} catch {
|
|
14047
|
+
return "retry";
|
|
14048
|
+
}
|
|
14049
|
+
let token;
|
|
14050
|
+
try {
|
|
14051
|
+
token = await engine.decodeToken({ tokenId, token: bytes });
|
|
14052
|
+
} catch {
|
|
14053
|
+
return "refused";
|
|
14054
|
+
}
|
|
14055
|
+
if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
|
|
14056
|
+
try {
|
|
14057
|
+
return (await engine.verify(token)).ok ? "verified" : "refused";
|
|
14058
|
+
} catch {
|
|
14059
|
+
return "retry";
|
|
14060
|
+
}
|
|
14061
|
+
}
|
|
14062
|
+
settle(tokenId, verdict) {
|
|
14063
|
+
if (verdict === "retry") {
|
|
14064
|
+
this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
|
|
14065
|
+
return;
|
|
14066
|
+
}
|
|
14067
|
+
this.retries.delete(tokenId);
|
|
14068
|
+
(verdict === "verified" ? this.verified : this.refused).add(tokenId);
|
|
14069
|
+
}
|
|
14070
|
+
nextRetry(previous) {
|
|
14071
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
14072
|
+
const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
|
|
14073
|
+
return { attempts, atMs: this.now() + delay };
|
|
14074
|
+
}
|
|
14075
|
+
scheduleRetry() {
|
|
14076
|
+
if (!this.live || this.timer !== null) return;
|
|
14077
|
+
const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
|
|
14078
|
+
if (due.length === 0) return;
|
|
14079
|
+
const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
|
|
14080
|
+
this.timer = setTimeout(() => {
|
|
14081
|
+
this.timer = null;
|
|
14082
|
+
void this.review(this.held).catch(() => void 0);
|
|
14083
|
+
}, delay);
|
|
14084
|
+
}
|
|
14085
|
+
async remember(tokenId) {
|
|
14086
|
+
await this.hydrate();
|
|
14087
|
+
if (this.verified.has(tokenId)) return;
|
|
14088
|
+
this.verified.add(tokenId);
|
|
14089
|
+
this.refused.delete(tokenId);
|
|
14090
|
+
this.retries.delete(tokenId);
|
|
14091
|
+
await this.persist();
|
|
14092
|
+
}
|
|
14093
|
+
persist() {
|
|
14094
|
+
const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
|
|
14095
|
+
return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
|
|
14096
|
+
}
|
|
14097
|
+
async load() {
|
|
14098
|
+
const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
|
|
14099
|
+
if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
|
|
14100
|
+
for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
|
|
14101
|
+
}
|
|
14102
|
+
now() {
|
|
14103
|
+
return (this.deps.now ?? Date.now)();
|
|
14104
|
+
}
|
|
14105
|
+
};
|
|
14106
|
+
|
|
13790
14107
|
// modules/payments-v2/ports.ts
|
|
13791
14108
|
function isRetryableAckError(err) {
|
|
13792
14109
|
return err !== null && typeof err === "object" && err.retryable === true;
|
|
@@ -13796,6 +14113,8 @@ function isRetryableAckError(err) {
|
|
|
13796
14113
|
var ACK_BATCH_SIZE = 200;
|
|
13797
14114
|
var REFRESH_INTERVAL_MS = 2500;
|
|
13798
14115
|
var POLL_INTERVAL_MS = 3e4;
|
|
14116
|
+
var RECHECK_BASE_MS = POLL_INTERVAL_MS;
|
|
14117
|
+
var RECHECK_MAX_MS = 60 * 60 * 1e3;
|
|
13799
14118
|
var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
|
|
13800
14119
|
function receivedDedupKey(tokenId, stateHash) {
|
|
13801
14120
|
return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
|
|
@@ -13812,6 +14131,7 @@ var Receive = class {
|
|
|
13812
14131
|
pollTimer = null;
|
|
13813
14132
|
unsubscribeWake = null;
|
|
13814
14133
|
lastRefreshAt = 0;
|
|
14134
|
+
rechecks = /* @__PURE__ */ new Map();
|
|
13815
14135
|
drainOnce() {
|
|
13816
14136
|
return this.drainFlight.run(() => this.doDrain());
|
|
13817
14137
|
}
|
|
@@ -13870,6 +14190,7 @@ var Receive = class {
|
|
|
13870
14190
|
const pending = [];
|
|
13871
14191
|
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
13872
14192
|
const claimable = { n: 0 };
|
|
14193
|
+
const deferred = { any: false };
|
|
13873
14194
|
const finish = () => {
|
|
13874
14195
|
if (claimable.n > 0) deps.refreshView?.();
|
|
13875
14196
|
return stored;
|
|
@@ -13878,7 +14199,7 @@ var Receive = class {
|
|
|
13878
14199
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
13879
14200
|
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
13880
14201
|
for (let pass = 0; pass < 2; pass++) {
|
|
13881
|
-
await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
|
|
14202
|
+
await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
|
|
13882
14203
|
const served = deps.delivery.incomingEpoch();
|
|
13883
14204
|
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
13884
14205
|
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
@@ -13895,20 +14216,41 @@ var Receive = class {
|
|
|
13895
14216
|
return finish();
|
|
13896
14217
|
}
|
|
13897
14218
|
async processEntry(ctx, entry) {
|
|
13898
|
-
const { deps, engine,
|
|
13899
|
-
const screened = await screen(deps, engine, entry);
|
|
14219
|
+
const { deps, engine, stored, claimable } = ctx;
|
|
14220
|
+
const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
|
|
14221
|
+
this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
|
|
14222
|
+
if (screened.kind === "defer") {
|
|
14223
|
+
ctx.deferred.any = true;
|
|
14224
|
+
return;
|
|
14225
|
+
}
|
|
13900
14226
|
if (screened.kind === "ack") {
|
|
13901
|
-
|
|
14227
|
+
queueAck(ctx, screened.ack);
|
|
13902
14228
|
if (screened.ack.disposition === "claimed") claimable.n += 1;
|
|
13903
14229
|
return;
|
|
13904
14230
|
}
|
|
14231
|
+
await deps.accepted?.(screened.token);
|
|
13905
14232
|
await deps.view.store(screened.record);
|
|
13906
|
-
|
|
14233
|
+
queueAck(ctx, claimAck(entry));
|
|
13907
14234
|
claimable.n += 1;
|
|
13908
14235
|
const transfer = await announce(deps, entry, screened.record);
|
|
13909
14236
|
stored.push(transfer);
|
|
13910
14237
|
deps.emit("transfer:incoming", transfer);
|
|
13911
14238
|
}
|
|
14239
|
+
recheckDue(deliveryId) {
|
|
14240
|
+
const recheck = this.rechecks.get(deliveryId);
|
|
14241
|
+
return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
|
|
14242
|
+
}
|
|
14243
|
+
scheduleRecheck(deliveryId, deferred) {
|
|
14244
|
+
if (!deferred) {
|
|
14245
|
+
this.rechecks.delete(deliveryId);
|
|
14246
|
+
return;
|
|
14247
|
+
}
|
|
14248
|
+
const previous = this.rechecks.get(deliveryId);
|
|
14249
|
+
if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
|
|
14250
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
14251
|
+
const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
|
|
14252
|
+
this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
|
|
14253
|
+
}
|
|
13912
14254
|
};
|
|
13913
14255
|
async function screen(deps, engine, entry) {
|
|
13914
14256
|
const blobBytes = await entry.fetchBlob();
|
|
@@ -13919,7 +14261,8 @@ async function screen(deps, engine, entry) {
|
|
|
13919
14261
|
logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
|
|
13920
14262
|
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
13921
14263
|
}
|
|
13922
|
-
const verdict = await engine
|
|
14264
|
+
const verdict = await verdictOf(engine, token);
|
|
14265
|
+
if (verdict === null) return { kind: "defer" };
|
|
13923
14266
|
if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
13924
14267
|
if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
|
|
13925
14268
|
return { kind: "ack", ack: rejectAck(entry, "not-owned") };
|
|
@@ -13930,38 +14273,43 @@ async function screen(deps, engine, entry) {
|
|
|
13930
14273
|
if (held !== null && await engine.isSpent(token)) {
|
|
13931
14274
|
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
13932
14275
|
}
|
|
14276
|
+
const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
|
|
13933
14277
|
return {
|
|
13934
14278
|
kind: "accept",
|
|
14279
|
+
token,
|
|
13935
14280
|
record: {
|
|
13936
14281
|
tokenId: keys.tokenId,
|
|
13937
14282
|
stateHash: keys.stateHash,
|
|
13938
14283
|
assets: toAssetAmounts(token),
|
|
13939
|
-
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
|
|
14284
|
+
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
|
|
14285
|
+
...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
|
|
13940
14286
|
}
|
|
13941
14287
|
};
|
|
13942
14288
|
}
|
|
13943
|
-
|
|
13944
|
-
const
|
|
14289
|
+
function unverifiedCoinsOf(deps, token) {
|
|
14290
|
+
const claims = deps.claims;
|
|
14291
|
+
if (claims === void 0 || honoursClaims(claims, token)) return [];
|
|
14292
|
+
return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
|
|
14293
|
+
}
|
|
14294
|
+
async function verdictOf(engine, token) {
|
|
13945
14295
|
try {
|
|
13946
|
-
await
|
|
13947
|
-
dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
|
|
13948
|
-
tokenId: record.tokenId,
|
|
13949
|
-
stateHash: record.stateHash,
|
|
13950
|
-
assets: record.assets,
|
|
13951
|
-
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
13952
|
-
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
13953
|
-
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13954
|
-
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
13955
|
-
receivedAt
|
|
13956
|
-
});
|
|
14296
|
+
return await engine.verify(token);
|
|
13957
14297
|
} catch (err) {
|
|
13958
|
-
|
|
14298
|
+
if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
|
|
14299
|
+
throw err;
|
|
13959
14300
|
}
|
|
14301
|
+
}
|
|
14302
|
+
async function announce(deps, entry, record) {
|
|
14303
|
+
const receivedAt = (deps.now ?? Date.now)();
|
|
14304
|
+
await recordArrival(deps, entry, record, receivedAt);
|
|
14305
|
+
const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
|
|
14306
|
+
const counted = record.assets.filter((asset) => !refused2.includes(asset));
|
|
13960
14307
|
return {
|
|
13961
14308
|
id: record.tokenId,
|
|
13962
14309
|
senderPubkey: entry.senderPubkey ?? "",
|
|
13963
14310
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13964
|
-
tokens:
|
|
14311
|
+
tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
|
|
14312
|
+
...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
|
|
13965
14313
|
// #777: named here rather than mapped from assets, which announced an EMPTY list.
|
|
13966
14314
|
...record.tokenType !== void 0 ? {
|
|
13967
14315
|
coinless: [
|
|
@@ -13979,6 +14327,25 @@ async function announce(deps, entry, record) {
|
|
|
13979
14327
|
receivedAt
|
|
13980
14328
|
};
|
|
13981
14329
|
}
|
|
14330
|
+
async function recordArrival(deps, entry, record, receivedAt) {
|
|
14331
|
+
const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
|
|
14332
|
+
if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
|
|
14333
|
+
try {
|
|
14334
|
+
await deps.recordReceived({
|
|
14335
|
+
dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
|
|
14336
|
+
tokenId: record.tokenId,
|
|
14337
|
+
stateHash: record.stateHash,
|
|
14338
|
+
assets,
|
|
14339
|
+
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
14340
|
+
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
14341
|
+
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
14342
|
+
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
14343
|
+
receivedAt
|
|
14344
|
+
});
|
|
14345
|
+
} catch (err) {
|
|
14346
|
+
logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
|
|
14347
|
+
}
|
|
14348
|
+
}
|
|
13982
14349
|
async function flushAcks(deps, pending, epochOf) {
|
|
13983
14350
|
if (pending.length === 0) return { progressed: true };
|
|
13984
14351
|
const before = pending.length;
|
|
@@ -13986,7 +14353,7 @@ async function flushAcks(deps, pending, epochOf) {
|
|
|
13986
14353
|
try {
|
|
13987
14354
|
const settled = await settleAcks(deps, pending);
|
|
13988
14355
|
let i = 0;
|
|
13989
|
-
while (i < pending.length && settled.has(pending[i].deliveryId)) {
|
|
14356
|
+
while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
|
|
13990
14357
|
lastAcked = pending[i].cursor;
|
|
13991
14358
|
i += 1;
|
|
13992
14359
|
}
|
|
@@ -14062,6 +14429,9 @@ async function ackOne(deps, ack) {
|
|
|
14062
14429
|
await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
|
|
14063
14430
|
}
|
|
14064
14431
|
}
|
|
14432
|
+
function queueAck(ctx, ack) {
|
|
14433
|
+
ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
|
|
14434
|
+
}
|
|
14065
14435
|
function claimAck(entry) {
|
|
14066
14436
|
return {
|
|
14067
14437
|
deliveryId: entry.deliveryId,
|
|
@@ -14770,72 +15140,276 @@ var SpendQueue = class {
|
|
|
14770
15140
|
queue = [];
|
|
14771
15141
|
this.queues.set(coinId, queue);
|
|
14772
15142
|
}
|
|
14773
|
-
queue.push(entry);
|
|
14774
|
-
return settled;
|
|
15143
|
+
queue.push(entry);
|
|
15144
|
+
return settled;
|
|
15145
|
+
}
|
|
15146
|
+
expire(id, coinId) {
|
|
15147
|
+
const entries = this.queues.get(coinId);
|
|
15148
|
+
if (!entries) return;
|
|
15149
|
+
const index = entries.findIndex((e) => e.id === id);
|
|
15150
|
+
if (index === -1) return;
|
|
15151
|
+
const [entry] = entries.splice(index, 1);
|
|
15152
|
+
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
15153
|
+
if (entries.length === 0) this.queues.delete(coinId);
|
|
15154
|
+
}
|
|
15155
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
15156
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
15157
|
+
freeView(coinId) {
|
|
15158
|
+
const freeView = [];
|
|
15159
|
+
let freeTotal = 0n;
|
|
15160
|
+
let pinnedTotal = 0n;
|
|
15161
|
+
const holders = /* @__PURE__ */ new Set();
|
|
15162
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
15163
|
+
for (const entry of this.deps.getPool(coinId)) {
|
|
15164
|
+
if (entry.amount <= 0n) continue;
|
|
15165
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
15166
|
+
if (holder === void 0) {
|
|
15167
|
+
freeView.push(entry);
|
|
15168
|
+
freeTotal += entry.amount;
|
|
15169
|
+
} else {
|
|
15170
|
+
pinnedTotal += entry.amount;
|
|
15171
|
+
holders.add(holder);
|
|
15172
|
+
}
|
|
15173
|
+
}
|
|
15174
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
15175
|
+
}
|
|
15176
|
+
expectedChangeTotal(coinId) {
|
|
15177
|
+
let total = 0n;
|
|
15178
|
+
for (const change of this.expectedChange.values()) {
|
|
15179
|
+
if (change.coinId === coinId) total += change.amount;
|
|
15180
|
+
}
|
|
15181
|
+
return total;
|
|
15182
|
+
}
|
|
15183
|
+
reservePlan(reservationId, plan, freeView) {
|
|
15184
|
+
const pool = new Set(freeView.map((e) => e.tokenId));
|
|
15185
|
+
const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
|
|
15186
|
+
for (const tokenId of tokenIds) {
|
|
15187
|
+
if (!pool.has(tokenId)) {
|
|
15188
|
+
throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
|
|
15189
|
+
}
|
|
15190
|
+
}
|
|
15191
|
+
this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
|
|
15192
|
+
}
|
|
15193
|
+
totalQueued() {
|
|
15194
|
+
let total = 0;
|
|
15195
|
+
for (const entries of this.queues.values()) total += entries.length;
|
|
15196
|
+
return total;
|
|
15197
|
+
}
|
|
15198
|
+
};
|
|
15199
|
+
function parseAmount(raw) {
|
|
15200
|
+
if (!/^\d+$/.test(raw)) {
|
|
15201
|
+
throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
|
|
15202
|
+
}
|
|
15203
|
+
const amount = BigInt(raw);
|
|
15204
|
+
if (amount <= 0n) {
|
|
15205
|
+
throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
|
|
15206
|
+
}
|
|
15207
|
+
return amount;
|
|
15208
|
+
}
|
|
15209
|
+
|
|
15210
|
+
// modules/payments-v2/machine/payload.ts
|
|
15211
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
15212
|
+
return {
|
|
15213
|
+
v: 2,
|
|
15214
|
+
kind: "coin",
|
|
15215
|
+
recipient: recipientPubkey,
|
|
15216
|
+
coinId: request.coinId,
|
|
15217
|
+
amount: request.amount,
|
|
15218
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
15219
|
+
direct: [...spend.plan.direct],
|
|
15220
|
+
...spend.plan.split !== void 0 ? {
|
|
15221
|
+
split: {
|
|
15222
|
+
tokenId: spend.plan.split.tokenId,
|
|
15223
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
15224
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
15225
|
+
}
|
|
15226
|
+
} : {},
|
|
15227
|
+
spentStates
|
|
15228
|
+
};
|
|
15229
|
+
}
|
|
15230
|
+
function messageOf(err) {
|
|
15231
|
+
return err instanceof Error ? err.message : String(err);
|
|
15232
|
+
}
|
|
15233
|
+
function buildWholePayload(recipientPubkey, request, spentStates) {
|
|
15234
|
+
return {
|
|
15235
|
+
v: 2,
|
|
15236
|
+
kind: "whole",
|
|
15237
|
+
recipient: recipientPubkey,
|
|
15238
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
15239
|
+
direct: [request.tokenId],
|
|
15240
|
+
spentStates
|
|
15241
|
+
};
|
|
15242
|
+
}
|
|
15243
|
+
|
|
15244
|
+
// modules/payments-v2/burn.ts
|
|
15245
|
+
var HEX32 = /^[0-9a-f]{64}$/;
|
|
15246
|
+
async function heldToken2(deps, tokenId) {
|
|
15247
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
15248
|
+
if (bytes === void 0) {
|
|
15249
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
|
|
15250
|
+
}
|
|
15251
|
+
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
15252
|
+
}
|
|
15253
|
+
function assertBurnable(engine, token, tokenId) {
|
|
15254
|
+
if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
|
|
15255
|
+
throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
|
|
15256
|
+
}
|
|
15257
|
+
if (token.valueEnvelope === "bare_collection") {
|
|
15258
|
+
throw new SphereError(
|
|
15259
|
+
`Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
|
|
15260
|
+
"VALIDATION_ERROR"
|
|
15261
|
+
);
|
|
15262
|
+
}
|
|
15263
|
+
}
|
|
15264
|
+
function assetsOf(token) {
|
|
15265
|
+
return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
|
|
15266
|
+
}
|
|
15267
|
+
async function runBurnUnderJournal(deps, input) {
|
|
15268
|
+
const { burnId } = input;
|
|
15269
|
+
const { tokenId, reasonBytes } = input.request;
|
|
15270
|
+
if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
|
|
15271
|
+
return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
|
|
15272
|
+
}
|
|
15273
|
+
let token;
|
|
15274
|
+
try {
|
|
15275
|
+
token = await heldToken2(deps, tokenId);
|
|
15276
|
+
assertBurnable(deps.engine, token, tokenId);
|
|
15277
|
+
} catch (err) {
|
|
15278
|
+
return refused(burnId, tokenId, err);
|
|
15279
|
+
}
|
|
15280
|
+
const entry = {
|
|
15281
|
+
burnId,
|
|
15282
|
+
tokenId,
|
|
15283
|
+
reasonHex: bytesToHex3(reasonBytes),
|
|
15284
|
+
burnedTokenHex: null,
|
|
15285
|
+
settled: false,
|
|
15286
|
+
assets: assetsOf(token),
|
|
15287
|
+
createdAt: deps.now()
|
|
15288
|
+
};
|
|
15289
|
+
await deps.burnJournal.upsert(entry);
|
|
15290
|
+
try {
|
|
15291
|
+
const settled = await burnJournaled(deps, entry, token);
|
|
15292
|
+
return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
|
|
15293
|
+
} catch (err) {
|
|
15294
|
+
const failure = failureOf(err);
|
|
15295
|
+
if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
|
|
15296
|
+
else deps.armHeartbeat();
|
|
15297
|
+
return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
|
|
15298
|
+
}
|
|
15299
|
+
}
|
|
15300
|
+
async function replayBurns(deps) {
|
|
15301
|
+
let progressed = 0;
|
|
15302
|
+
for (const entry of await deps.burnJournal.list()) {
|
|
15303
|
+
if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
|
|
15304
|
+
try {
|
|
15305
|
+
const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
|
|
15306
|
+
await burnJournaled(deps, entry, token);
|
|
15307
|
+
progressed += 1;
|
|
15308
|
+
} catch (err) {
|
|
15309
|
+
if (!isTerminal(err)) continue;
|
|
15310
|
+
await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
|
|
15311
|
+
progressed += 1;
|
|
15312
|
+
}
|
|
15313
|
+
}
|
|
15314
|
+
return progressed;
|
|
15315
|
+
}
|
|
15316
|
+
async function pendingBurns(deps) {
|
|
15317
|
+
return (await deps.burnJournal.list()).map((entry) => ({
|
|
15318
|
+
burnId: entry.burnId,
|
|
15319
|
+
tokenId: entry.tokenId,
|
|
15320
|
+
reasonBytes: hexToBytes4(entry.reasonHex),
|
|
15321
|
+
burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
|
|
15322
|
+
settled: entry.settled,
|
|
15323
|
+
createdAt: entry.createdAt,
|
|
15324
|
+
...entry.failure !== void 0 ? { failure: entry.failure } : {}
|
|
15325
|
+
}));
|
|
15326
|
+
}
|
|
15327
|
+
async function acknowledgeBurn(deps, burnId) {
|
|
15328
|
+
const entry = await deps.burnJournal.getByKey(burnId);
|
|
15329
|
+
if (entry === void 0) return;
|
|
15330
|
+
if (isLiveBurn(entry)) {
|
|
15331
|
+
throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
|
|
15332
|
+
}
|
|
15333
|
+
await deps.burnJournal.removeByKey(burnId);
|
|
15334
|
+
}
|
|
15335
|
+
function refused(burnId, tokenId, err) {
|
|
15336
|
+
const failure = failureOf(err);
|
|
15337
|
+
return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
|
|
15338
|
+
}
|
|
15339
|
+
function isTerminal(err) {
|
|
15340
|
+
return classifyError(err) === "conflict";
|
|
15341
|
+
}
|
|
15342
|
+
function failureOf(err) {
|
|
15343
|
+
const code = err !== null && typeof err === "object" ? err.code : void 0;
|
|
15344
|
+
return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
|
|
15345
|
+
}
|
|
15346
|
+
async function burnJournaled(deps, entry, token) {
|
|
15347
|
+
let current = entry;
|
|
15348
|
+
if (current.burnedTokenHex === null) {
|
|
15349
|
+
if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
|
|
15350
|
+
const burned = await deps.engine.burn(
|
|
15351
|
+
{ token, reasonBytes: hexToBytes4(current.reasonHex) },
|
|
15352
|
+
{ transferId: current.burnId }
|
|
15353
|
+
);
|
|
15354
|
+
if (burned.blob.tokenId !== current.tokenId) {
|
|
15355
|
+
throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
|
|
15356
|
+
}
|
|
15357
|
+
current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
|
|
15358
|
+
await deps.burnJournal.upsert(current);
|
|
14775
15359
|
}
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
15360
|
+
await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
|
|
15361
|
+
await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
|
|
15362
|
+
current = { ...current, settled: true };
|
|
15363
|
+
await deps.burnJournal.upsert(current);
|
|
15364
|
+
deps.refreshView();
|
|
15365
|
+
return current;
|
|
15366
|
+
}
|
|
15367
|
+
|
|
15368
|
+
// modules/payments-v2/burn-hold.ts
|
|
15369
|
+
var BurnHold = class {
|
|
15370
|
+
constructor(deps) {
|
|
15371
|
+
this.deps = deps;
|
|
14784
15372
|
}
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
let pinnedTotal = 0n;
|
|
14791
|
-
const holders = /* @__PURE__ */ new Set();
|
|
14792
|
-
const unproven = this.deps.ledger.unprovenReason();
|
|
14793
|
-
for (const entry of this.deps.getPool(coinId)) {
|
|
14794
|
-
if (entry.amount <= 0n) continue;
|
|
14795
|
-
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
14796
|
-
if (holder === void 0) {
|
|
14797
|
-
freeView.push(entry);
|
|
14798
|
-
freeTotal += entry.amount;
|
|
14799
|
-
} else {
|
|
14800
|
-
pinnedTotal += entry.amount;
|
|
14801
|
-
holders.add(holder);
|
|
14802
|
-
}
|
|
15373
|
+
async run(burnId, tokenId, burn) {
|
|
15374
|
+
try {
|
|
15375
|
+
this.deps.queue.planWhole(burnId, tokenId);
|
|
15376
|
+
} catch (err) {
|
|
15377
|
+
return refused(burnId, tokenId, err);
|
|
14803
15378
|
}
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
15379
|
+
this.deps.view.markInFlightMany([tokenId]);
|
|
15380
|
+
let burned = false;
|
|
15381
|
+
try {
|
|
15382
|
+
const result = await burn();
|
|
15383
|
+
burned = result.success;
|
|
15384
|
+
return result;
|
|
15385
|
+
} finally {
|
|
15386
|
+
await this.settle(burnId, tokenId, burned);
|
|
14810
15387
|
}
|
|
14811
|
-
return total;
|
|
14812
15388
|
}
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
|
|
14819
|
-
}
|
|
15389
|
+
async settle(burnId, tokenId, burned) {
|
|
15390
|
+
if (burned) {
|
|
15391
|
+
this.deps.ledger.commit(burnId);
|
|
15392
|
+
this.deps.track(this.refreshThenRelease(tokenId));
|
|
15393
|
+
return;
|
|
14820
15394
|
}
|
|
14821
|
-
this.deps.
|
|
15395
|
+
const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
|
|
15396
|
+
if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
|
|
15397
|
+
this.deps.ledger.cancel(burnId);
|
|
15398
|
+
this.deps.view.releaseMany([tokenId]);
|
|
15399
|
+
this.deps.queue.notifyChange("");
|
|
15400
|
+
return;
|
|
15401
|
+
}
|
|
15402
|
+
this.deps.pins.adopt(burnId);
|
|
14822
15403
|
}
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
15404
|
+
async refreshThenRelease(tokenId) {
|
|
15405
|
+
try {
|
|
15406
|
+
await this.deps.view.delta();
|
|
15407
|
+
this.deps.view.releaseMany([tokenId]);
|
|
15408
|
+
} finally {
|
|
15409
|
+
this.deps.queue.notifyChange("");
|
|
15410
|
+
}
|
|
14827
15411
|
}
|
|
14828
15412
|
};
|
|
14829
|
-
function parseAmount(raw) {
|
|
14830
|
-
if (!/^\d+$/.test(raw)) {
|
|
14831
|
-
throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
|
|
14832
|
-
}
|
|
14833
|
-
const amount = BigInt(raw);
|
|
14834
|
-
if (amount <= 0n) {
|
|
14835
|
-
throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
|
|
14836
|
-
}
|
|
14837
|
-
return amount;
|
|
14838
|
-
}
|
|
14839
15413
|
|
|
14840
15414
|
// modules/payments-v2/compose.ts
|
|
14841
15415
|
function supportsDeterministicMint(engine) {
|
|
@@ -14844,16 +15418,8 @@ function supportsDeterministicMint(engine) {
|
|
|
14844
15418
|
function composeFacadeParts(deps, hooks) {
|
|
14845
15419
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
14846
15420
|
const ledger = new ReservationLedger();
|
|
14847
|
-
const
|
|
14848
|
-
|
|
14849
|
-
kv: deps.kv,
|
|
14850
|
-
emit: (event) => deps.emit(event, {}),
|
|
14851
|
-
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
14852
|
-
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
14853
|
-
// must not call a token spendable that the queue is about to refuse to spend.
|
|
14854
|
-
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
14855
|
-
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
14856
|
-
});
|
|
15421
|
+
const verdicts = buildVerdicts(deps, hooks);
|
|
15422
|
+
const view = buildView(deps, hooks, ledger, verdicts);
|
|
14857
15423
|
const queue = new SpendQueue({
|
|
14858
15424
|
ledger,
|
|
14859
15425
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -14868,27 +15434,57 @@ function composeFacadeParts(deps, hooks) {
|
|
|
14868
15434
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
14869
15435
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
14870
15436
|
});
|
|
14871
|
-
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
15437
|
+
const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
|
|
14872
15438
|
const machineStores = createMachineStores(deps.kv);
|
|
14873
15439
|
const heldStates = /* @__PURE__ */ new Map();
|
|
14874
15440
|
const refreshView = coalesced(() => view.delta(), hooks.track);
|
|
15441
|
+
const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
|
|
14875
15442
|
return {
|
|
14876
15443
|
ownPubkeyBytes,
|
|
14877
15444
|
view,
|
|
15445
|
+
verdicts,
|
|
14878
15446
|
refreshView,
|
|
14879
15447
|
ledger,
|
|
14880
|
-
pins
|
|
15448
|
+
pins,
|
|
14881
15449
|
queue,
|
|
15450
|
+
burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
|
|
14882
15451
|
historyStore,
|
|
14883
15452
|
machineStores,
|
|
14884
15453
|
machineDeps,
|
|
14885
15454
|
machine: new TransferMachine(machineDeps),
|
|
14886
15455
|
heldStates,
|
|
14887
|
-
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
|
|
15456
|
+
receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
|
|
14888
15457
|
requests: buildRequests(deps, hooks),
|
|
14889
15458
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
14890
15459
|
};
|
|
14891
15460
|
}
|
|
15461
|
+
function buildView(deps, hooks, ledger, verdicts) {
|
|
15462
|
+
const view = new InventoryView({
|
|
15463
|
+
port: deps.storagePort,
|
|
15464
|
+
kv: deps.kv,
|
|
15465
|
+
trust: verdicts,
|
|
15466
|
+
emit: (event) => {
|
|
15467
|
+
deps.emit(event, {});
|
|
15468
|
+
hooks.track(verdicts.review(view.claimHolders()));
|
|
15469
|
+
},
|
|
15470
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
15471
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
15472
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
15473
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
15474
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
15475
|
+
});
|
|
15476
|
+
return view;
|
|
15477
|
+
}
|
|
15478
|
+
function buildVerdicts(deps, hooks) {
|
|
15479
|
+
return new TokenVerdicts({
|
|
15480
|
+
kv: deps.kv,
|
|
15481
|
+
claims: deps.claims ?? new CoinClaims(),
|
|
15482
|
+
engine: () => hooks.engine(),
|
|
15483
|
+
getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
|
|
15484
|
+
changed: () => deps.emit("inventory:updated", {}),
|
|
15485
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
15486
|
+
});
|
|
15487
|
+
}
|
|
14892
15488
|
function buildPins(deps, hooks, parts) {
|
|
14893
15489
|
return new IntentPins({
|
|
14894
15490
|
ledger: parts.ledger,
|
|
@@ -14916,8 +15512,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
|
14916
15512
|
}
|
|
14917
15513
|
};
|
|
14918
15514
|
}
|
|
14919
|
-
function buildMachineDeps(deps, hooks, historyStore) {
|
|
15515
|
+
function buildMachineDeps(deps, hooks, historyStore, verdicts) {
|
|
14920
15516
|
return {
|
|
15517
|
+
recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
|
|
14921
15518
|
engine: () => hooks.engine(),
|
|
14922
15519
|
storage: deps.storagePort,
|
|
14923
15520
|
delivery: deps.deliveryPort,
|
|
@@ -14950,10 +15547,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
14950
15547
|
}
|
|
14951
15548
|
};
|
|
14952
15549
|
}
|
|
14953
|
-
function buildReceive(deps, hooks,
|
|
15550
|
+
function buildReceive(deps, hooks, parts, refreshView) {
|
|
15551
|
+
const { historyStore, heldStates, verdicts } = parts;
|
|
14954
15552
|
return new Receive({
|
|
14955
15553
|
delivery: deps.deliveryPort,
|
|
14956
15554
|
engine: () => hooks.engine(),
|
|
15555
|
+
claims: deps.claims ?? new CoinClaims(),
|
|
15556
|
+
accepted: (token) => verdicts.accept(token),
|
|
14957
15557
|
view: {
|
|
14958
15558
|
heldState: (tokenId) => heldStates.get(tokenId) ?? null,
|
|
14959
15559
|
store: async (entry) => {
|
|
@@ -15003,40 +15603,6 @@ function buildRequests(deps, hooks) {
|
|
|
15003
15603
|
});
|
|
15004
15604
|
}
|
|
15005
15605
|
|
|
15006
|
-
// modules/payments-v2/machine/payload.ts
|
|
15007
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
15008
|
-
return {
|
|
15009
|
-
v: 2,
|
|
15010
|
-
kind: "coin",
|
|
15011
|
-
recipient: recipientPubkey,
|
|
15012
|
-
coinId: request.coinId,
|
|
15013
|
-
amount: request.amount,
|
|
15014
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
15015
|
-
direct: [...spend.plan.direct],
|
|
15016
|
-
...spend.plan.split !== void 0 ? {
|
|
15017
|
-
split: {
|
|
15018
|
-
tokenId: spend.plan.split.tokenId,
|
|
15019
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
15020
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
15021
|
-
}
|
|
15022
|
-
} : {},
|
|
15023
|
-
spentStates
|
|
15024
|
-
};
|
|
15025
|
-
}
|
|
15026
|
-
function messageOf(err) {
|
|
15027
|
-
return err instanceof Error ? err.message : String(err);
|
|
15028
|
-
}
|
|
15029
|
-
function buildWholePayload(recipientPubkey, request, spentStates) {
|
|
15030
|
-
return {
|
|
15031
|
-
v: 2,
|
|
15032
|
-
kind: "whole",
|
|
15033
|
-
recipient: recipientPubkey,
|
|
15034
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
15035
|
-
direct: [request.tokenId],
|
|
15036
|
-
spentStates
|
|
15037
|
-
};
|
|
15038
|
-
}
|
|
15039
|
-
|
|
15040
15606
|
// modules/payments-v2/mint-params.ts
|
|
15041
15607
|
function mintParams(recipientPubkey, coinId, amount) {
|
|
15042
15608
|
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
@@ -15106,6 +15672,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
|
|
|
15106
15672
|
}
|
|
15107
15673
|
return false;
|
|
15108
15674
|
}
|
|
15675
|
+
async function seedHeldStates(storagePort, heldStates) {
|
|
15676
|
+
let page = await storagePort.listInventory();
|
|
15677
|
+
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
15678
|
+
for (const item of page.items) {
|
|
15679
|
+
if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
|
|
15680
|
+
}
|
|
15681
|
+
if (!page.more) return;
|
|
15682
|
+
page = await storagePort.listInventory(page.cursor);
|
|
15683
|
+
}
|
|
15684
|
+
}
|
|
15109
15685
|
async function replayCoinMints(deps) {
|
|
15110
15686
|
let resolved = 0;
|
|
15111
15687
|
for (const entry of await deps.mintJournal.list()) {
|
|
@@ -15210,7 +15786,8 @@ async function mintJournaled(deps, entry) {
|
|
|
15210
15786
|
}
|
|
15211
15787
|
|
|
15212
15788
|
// modules/payments-v2/mint-custom.ts
|
|
15213
|
-
var
|
|
15789
|
+
var HEX322 = /^[0-9a-f]{64}$/;
|
|
15790
|
+
var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
15214
15791
|
function assertRequest(request) {
|
|
15215
15792
|
if (request.tokenType.length !== 32) {
|
|
15216
15793
|
throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
|
|
@@ -15221,19 +15798,27 @@ function assertRequest(request) {
|
|
|
15221
15798
|
if (request.data.length === 0) {
|
|
15222
15799
|
throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
|
|
15223
15800
|
}
|
|
15801
|
+
if (request.justification !== void 0 && request.justification.length === 0) {
|
|
15802
|
+
throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
|
|
15803
|
+
}
|
|
15804
|
+
if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
|
|
15805
|
+
throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
|
|
15806
|
+
}
|
|
15224
15807
|
for (const asset of request.assets) {
|
|
15225
|
-
if (!
|
|
15808
|
+
if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
|
|
15226
15809
|
throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
|
|
15227
15810
|
}
|
|
15228
15811
|
}
|
|
15229
15812
|
}
|
|
15230
15813
|
async function runCustomMintUnderJournal(deps, input) {
|
|
15814
|
+
let entry;
|
|
15231
15815
|
try {
|
|
15232
15816
|
assertRequest(input.request);
|
|
15817
|
+
entry = journalEntryOf2(input.mintId, input.request, deps.now());
|
|
15818
|
+
deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
|
|
15233
15819
|
} catch (err) {
|
|
15234
15820
|
return { success: false, error: messageOf(err) };
|
|
15235
15821
|
}
|
|
15236
|
-
const entry = journalEntryOf2(input.mintId, input.request, deps.now());
|
|
15237
15822
|
await deps.customMintJournal.upsert(entry);
|
|
15238
15823
|
let tokenId;
|
|
15239
15824
|
try {
|
|
@@ -15273,24 +15858,25 @@ function journalEntryOf2(mintId, request, createdAt) {
|
|
|
15273
15858
|
createdAt
|
|
15274
15859
|
};
|
|
15275
15860
|
}
|
|
15861
|
+
function mintParamsOf(deps, entry, mintJustificationVerifiers) {
|
|
15862
|
+
return {
|
|
15863
|
+
recipientPubkey: deps.ownPubkeyBytes,
|
|
15864
|
+
data: hexToBytes4(entry.dataHex),
|
|
15865
|
+
tokenType: hexToBytes4(entry.tokenTypeHex),
|
|
15866
|
+
salt: hexToBytes4(entry.saltHex),
|
|
15867
|
+
...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
|
|
15868
|
+
...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
|
|
15869
|
+
};
|
|
15870
|
+
}
|
|
15276
15871
|
async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
|
|
15277
|
-
const token = await deps.engine.mintDataToken(
|
|
15278
|
-
{
|
|
15279
|
-
recipientPubkey: deps.ownPubkeyBytes,
|
|
15280
|
-
data: hexToBytes4(entry.dataHex),
|
|
15281
|
-
tokenType: hexToBytes4(entry.tokenTypeHex),
|
|
15282
|
-
salt: hexToBytes4(entry.saltHex),
|
|
15283
|
-
...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
|
|
15284
|
-
...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
|
|
15285
|
-
},
|
|
15286
|
-
{ transferId: entry.mintId }
|
|
15287
|
-
);
|
|
15872
|
+
const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
|
|
15288
15873
|
if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
|
|
15289
15874
|
throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
|
|
15290
15875
|
}
|
|
15291
15876
|
if (entry.tokenId === "") {
|
|
15292
15877
|
await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
15293
15878
|
}
|
|
15879
|
+
if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
|
|
15294
15880
|
await finalizeMint(deps, {
|
|
15295
15881
|
mintId: entry.mintId,
|
|
15296
15882
|
token,
|
|
@@ -15300,108 +15886,6 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
|
|
|
15300
15886
|
return token.blob.tokenId;
|
|
15301
15887
|
}
|
|
15302
15888
|
|
|
15303
|
-
// modules/payments-v2/burn.ts
|
|
15304
|
-
var HEX322 = /^[0-9a-f]{64}$/;
|
|
15305
|
-
async function heldToken2(deps, tokenId) {
|
|
15306
|
-
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
15307
|
-
if (bytes === void 0) {
|
|
15308
|
-
throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
|
|
15309
|
-
}
|
|
15310
|
-
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
15311
|
-
}
|
|
15312
|
-
function assetsOf(token) {
|
|
15313
|
-
return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
|
|
15314
|
-
}
|
|
15315
|
-
async function runBurnUnderJournal(deps, input) {
|
|
15316
|
-
const { burnId } = input;
|
|
15317
|
-
const { tokenId, reasonBytes } = input.request;
|
|
15318
|
-
if (!HEX322.test(tokenId) || reasonBytes.length === 0) {
|
|
15319
|
-
return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty" };
|
|
15320
|
-
}
|
|
15321
|
-
let token;
|
|
15322
|
-
try {
|
|
15323
|
-
token = await heldToken2(deps, tokenId);
|
|
15324
|
-
if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
|
|
15325
|
-
throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
|
|
15326
|
-
}
|
|
15327
|
-
} catch (err) {
|
|
15328
|
-
return { success: false, burnId, tokenId, error: messageOf(err) };
|
|
15329
|
-
}
|
|
15330
|
-
const entry = {
|
|
15331
|
-
burnId,
|
|
15332
|
-
tokenId,
|
|
15333
|
-
reasonHex: bytesToHex3(reasonBytes),
|
|
15334
|
-
burnedTokenHex: null,
|
|
15335
|
-
settled: false,
|
|
15336
|
-
assets: assetsOf(token),
|
|
15337
|
-
createdAt: deps.now()
|
|
15338
|
-
};
|
|
15339
|
-
await deps.burnJournal.upsert(entry);
|
|
15340
|
-
try {
|
|
15341
|
-
const settled = await burnJournaled(deps, entry, token, { applied: false });
|
|
15342
|
-
return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
|
|
15343
|
-
} catch (err) {
|
|
15344
|
-
deps.armHeartbeat();
|
|
15345
|
-
return { success: false, burnId, tokenId, error: messageOf(err) };
|
|
15346
|
-
}
|
|
15347
|
-
}
|
|
15348
|
-
async function replayBurns(deps) {
|
|
15349
|
-
let progressed = 0;
|
|
15350
|
-
for (const entry of await deps.burnJournal.list()) {
|
|
15351
|
-
if (entry.settled || deps.isActiveOp(entry.burnId)) continue;
|
|
15352
|
-
try {
|
|
15353
|
-
const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
|
|
15354
|
-
const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
|
|
15355
|
-
await burnJournaled(deps, entry, token, { applied });
|
|
15356
|
-
progressed += 1;
|
|
15357
|
-
} catch {
|
|
15358
|
-
continue;
|
|
15359
|
-
}
|
|
15360
|
-
}
|
|
15361
|
-
return progressed;
|
|
15362
|
-
}
|
|
15363
|
-
async function pendingBurns(deps) {
|
|
15364
|
-
return (await deps.burnJournal.list()).map((entry) => ({
|
|
15365
|
-
burnId: entry.burnId,
|
|
15366
|
-
tokenId: entry.tokenId,
|
|
15367
|
-
reasonBytes: hexToBytes4(entry.reasonHex),
|
|
15368
|
-
burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
|
|
15369
|
-
settled: entry.settled,
|
|
15370
|
-
createdAt: entry.createdAt
|
|
15371
|
-
}));
|
|
15372
|
-
}
|
|
15373
|
-
async function acknowledgeBurn(deps, burnId) {
|
|
15374
|
-
const entry = await deps.burnJournal.getByKey(burnId);
|
|
15375
|
-
if (entry === void 0) return;
|
|
15376
|
-
if (!entry.settled) {
|
|
15377
|
-
throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
|
|
15378
|
-
}
|
|
15379
|
-
await deps.burnJournal.removeByKey(burnId);
|
|
15380
|
-
}
|
|
15381
|
-
async function burnJournaled(deps, entry, token, state) {
|
|
15382
|
-
let current = entry;
|
|
15383
|
-
if (current.burnedTokenHex === null) {
|
|
15384
|
-
if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
|
|
15385
|
-
const burned = await deps.engine.burn(
|
|
15386
|
-
{ token, reasonBytes: hexToBytes4(current.reasonHex) },
|
|
15387
|
-
{ transferId: current.burnId }
|
|
15388
|
-
);
|
|
15389
|
-
if (burned.blob.tokenId !== current.tokenId) {
|
|
15390
|
-
throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
|
|
15391
|
-
}
|
|
15392
|
-
current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
|
|
15393
|
-
await deps.burnJournal.upsert(current);
|
|
15394
|
-
}
|
|
15395
|
-
if (!state.applied) {
|
|
15396
|
-
await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [] });
|
|
15397
|
-
}
|
|
15398
|
-
await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
|
|
15399
|
-
current = { ...current, settled: true };
|
|
15400
|
-
await deps.burnJournal.upsert(current);
|
|
15401
|
-
deps.refreshView();
|
|
15402
|
-
return current;
|
|
15403
|
-
}
|
|
15404
|
-
|
|
15405
15889
|
// modules/payments-v2/send-whole.ts
|
|
15406
15890
|
async function materializeWholeSpend(deps, input) {
|
|
15407
15891
|
const { engine } = deps;
|
|
@@ -15597,6 +16081,7 @@ var PaymentsFacade = class {
|
|
|
15597
16081
|
track: (op) => this.trackTail(op)
|
|
15598
16082
|
});
|
|
15599
16083
|
this.view = parts.view;
|
|
16084
|
+
this.verdicts = parts.verdicts;
|
|
15600
16085
|
this.wakeRefresh = parts.refreshView;
|
|
15601
16086
|
this.ledger = parts.ledger;
|
|
15602
16087
|
this.queue = parts.queue;
|
|
@@ -15610,6 +16095,7 @@ var PaymentsFacade = class {
|
|
|
15610
16095
|
this.restoreDeps = parts.restoreDeps;
|
|
15611
16096
|
this.requests = parts.requests;
|
|
15612
16097
|
this.pins = parts.pins;
|
|
16098
|
+
this.burnHold = parts.burnHold;
|
|
15613
16099
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
15614
16100
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
15615
16101
|
now: () => this.nowMs(),
|
|
@@ -15633,6 +16119,7 @@ var PaymentsFacade = class {
|
|
|
15633
16119
|
wakeRefresh;
|
|
15634
16120
|
prewarmed = new PrewarmCache();
|
|
15635
16121
|
view;
|
|
16122
|
+
verdicts;
|
|
15636
16123
|
ledger;
|
|
15637
16124
|
queue;
|
|
15638
16125
|
machine;
|
|
@@ -15659,6 +16146,7 @@ var PaymentsFacade = class {
|
|
|
15659
16146
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
15660
16147
|
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
15661
16148
|
pins;
|
|
16149
|
+
burnHold;
|
|
15662
16150
|
/** Per facade, so per address: an NFT reading is never served to another wallet. */
|
|
15663
16151
|
nftCache = new NftCache();
|
|
15664
16152
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -15683,7 +16171,8 @@ var PaymentsFacade = class {
|
|
|
15683
16171
|
await this.deps.session.start();
|
|
15684
16172
|
await this.pins.sync().catch(() => void 0);
|
|
15685
16173
|
this.trackTail(this.runConvergencePass());
|
|
15686
|
-
this.trackTail(this.
|
|
16174
|
+
this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
|
|
16175
|
+
await this.verdicts.start().catch(() => void 0);
|
|
15687
16176
|
await this.view.fullPull().catch(() => void 0);
|
|
15688
16177
|
this.receiveLoop.start(this.deps.receivePollMs);
|
|
15689
16178
|
}
|
|
@@ -15692,6 +16181,7 @@ var PaymentsFacade = class {
|
|
|
15692
16181
|
this.started = false;
|
|
15693
16182
|
this.heartbeat.stop();
|
|
15694
16183
|
this.receiveLoop.stop();
|
|
16184
|
+
this.verdicts.stop();
|
|
15695
16185
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
15696
16186
|
this.unsubscribers = [];
|
|
15697
16187
|
await this.deps.session.stop();
|
|
@@ -15708,12 +16198,17 @@ var PaymentsFacade = class {
|
|
|
15708
16198
|
}
|
|
15709
16199
|
// ── reads ──────────────────────────────────────────────────────────────────
|
|
15710
16200
|
async assets(coinId) {
|
|
16201
|
+
return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
|
|
16202
|
+
}
|
|
16203
|
+
unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
|
|
16204
|
+
async heldAssets(coinId) {
|
|
15711
16205
|
const all = await this.view.assets(this.deps.registry, this.deps.price);
|
|
15712
16206
|
return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
|
|
15713
16207
|
}
|
|
15714
16208
|
tokens(filter) {
|
|
15715
|
-
return this.view.tokens(this.deps.registry, filter);
|
|
16209
|
+
return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
|
|
15716
16210
|
}
|
|
16211
|
+
unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
|
|
15717
16212
|
coinless() {
|
|
15718
16213
|
return this.view.coinless(this.deps.registry);
|
|
15719
16214
|
}
|
|
@@ -15774,9 +16269,9 @@ var PaymentsFacade = class {
|
|
|
15774
16269
|
return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
|
|
15775
16270
|
}
|
|
15776
16271
|
mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
|
|
15777
|
-
burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
|
|
15778
|
-
pendingBurns = () => this.track(pendingBurns(this.
|
|
15779
|
-
acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.
|
|
16272
|
+
burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
|
|
16273
|
+
pendingBurns = () => this.track(pendingBurns(this.machineStores));
|
|
16274
|
+
acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
|
|
15780
16275
|
// §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
|
|
15781
16276
|
async resumeNow() {
|
|
15782
16277
|
if (!this.started) return;
|
|
@@ -15819,7 +16314,8 @@ var PaymentsFacade = class {
|
|
|
15819
16314
|
isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
|
|
15820
16315
|
tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
|
|
15821
16316
|
emit: this.deps.emit,
|
|
15822
|
-
now: () => this.nowMs()
|
|
16317
|
+
now: () => this.nowMs(),
|
|
16318
|
+
accepted: (token) => this.verdicts.accept(token)
|
|
15823
16319
|
};
|
|
15824
16320
|
}
|
|
15825
16321
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
@@ -16157,16 +16653,6 @@ var PaymentsFacade = class {
|
|
|
16157
16653
|
newId() {
|
|
16158
16654
|
return (this.deps.newId ?? randomUUID)();
|
|
16159
16655
|
}
|
|
16160
|
-
async seedHeldStates() {
|
|
16161
|
-
let page = await this.deps.storagePort.listInventory();
|
|
16162
|
-
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
16163
|
-
for (const item of page.items) {
|
|
16164
|
-
if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
|
|
16165
|
-
}
|
|
16166
|
-
if (!page.more) return;
|
|
16167
|
-
page = await this.deps.storagePort.listInventory(page.cursor);
|
|
16168
|
-
}
|
|
16169
|
-
}
|
|
16170
16656
|
track(op) {
|
|
16171
16657
|
const settled = op.then(() => void 0, () => void 0);
|
|
16172
16658
|
this.pendingOps.add(settled);
|
|
@@ -17410,6 +17896,7 @@ function composePaymentsV2(spec) {
|
|
|
17410
17896
|
engineRef: spec.engineRef,
|
|
17411
17897
|
kv,
|
|
17412
17898
|
registry: host.registry,
|
|
17899
|
+
claims: CoinClaims.fromPlugins(spec.plugins),
|
|
17413
17900
|
...host.price !== null ? { price: host.price } : {},
|
|
17414
17901
|
emit: host.emit,
|
|
17415
17902
|
resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
|
|
@@ -20458,7 +20945,8 @@ var Sphere = class _Sphere {
|
|
|
20458
20945
|
// deliver the NEXT address's Unicity ID — a name the sending key does
|
|
20459
20946
|
// not own (sphere#487 review).
|
|
20460
20947
|
nametag: () => this.getNametagForAddress(facadeAddressId)
|
|
20461
|
-
}
|
|
20948
|
+
},
|
|
20949
|
+
...this._plugins ? { plugins: this._plugins } : {}
|
|
20462
20950
|
});
|
|
20463
20951
|
this._paymentsV2Active = { index, facade };
|
|
20464
20952
|
await facade.start();
|