@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.cjs
CHANGED
|
@@ -9597,6 +9597,150 @@ async function deriveDirectAddress(publicKey) {
|
|
|
9597
9597
|
return `DIRECT://${import_HexConverter.HexConverter.encode(imprint)}${import_HexConverter.HexConverter.encode(checksum)}`;
|
|
9598
9598
|
}
|
|
9599
9599
|
|
|
9600
|
+
// token-engine/claims.ts
|
|
9601
|
+
var CoinClaims = class _CoinClaims {
|
|
9602
|
+
issuers = /* @__PURE__ */ new Map();
|
|
9603
|
+
policies = [];
|
|
9604
|
+
static fromPlugins(plugins) {
|
|
9605
|
+
const claims = new _CoinClaims();
|
|
9606
|
+
for (const plugin of plugins ?? []) {
|
|
9607
|
+
for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
|
|
9608
|
+
}
|
|
9609
|
+
return claims;
|
|
9610
|
+
}
|
|
9611
|
+
add(policy) {
|
|
9612
|
+
const tokenType = import_HexConverter.HexConverter.encode(policy.tokenType.bytes);
|
|
9613
|
+
const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
|
|
9614
|
+
const taken = coinIds.find((coinId) => this.issuers.has(coinId));
|
|
9615
|
+
if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
|
|
9616
|
+
for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
|
|
9617
|
+
this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
|
|
9618
|
+
}
|
|
9619
|
+
fingerprint() {
|
|
9620
|
+
return [...this.policies].sort().join("\n");
|
|
9621
|
+
}
|
|
9622
|
+
issuerOf(coinId) {
|
|
9623
|
+
return this.issuers.get(coinId.toLowerCase()) ?? null;
|
|
9624
|
+
}
|
|
9625
|
+
};
|
|
9626
|
+
|
|
9627
|
+
// token-engine/errors.ts
|
|
9628
|
+
var TransferConflictError = class extends SphereError {
|
|
9629
|
+
/**
|
|
9630
|
+
* #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
|
|
9631
|
+
* Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
|
|
9632
|
+
*/
|
|
9633
|
+
conflictedSourceId;
|
|
9634
|
+
constructor(message, cause) {
|
|
9635
|
+
super(message, "TRANSFER_CONFLICT", cause);
|
|
9636
|
+
this.name = "TransferConflictError";
|
|
9637
|
+
}
|
|
9638
|
+
};
|
|
9639
|
+
var MintReasonUnverifiableError = class extends SphereError {
|
|
9640
|
+
constructor(message, cause) {
|
|
9641
|
+
super(message, "MINT_REASON_UNVERIFIABLE", cause);
|
|
9642
|
+
this.name = "MintReasonUnverifiableError";
|
|
9643
|
+
}
|
|
9644
|
+
};
|
|
9645
|
+
var ProofUnconfirmedError = class extends SphereError {
|
|
9646
|
+
mayHaveCertified = true;
|
|
9647
|
+
constructor(message, cause) {
|
|
9648
|
+
super(message, "CERTIFICATION_UNCONFIRMED", cause);
|
|
9649
|
+
this.name = "ProofUnconfirmedError";
|
|
9650
|
+
}
|
|
9651
|
+
};
|
|
9652
|
+
var CheckpointPersistFailedError = class extends SphereError {
|
|
9653
|
+
mayHaveCertified = true;
|
|
9654
|
+
constructor(message, cause) {
|
|
9655
|
+
super(message, "CHECKPOINT_PERSIST_FAILED", cause);
|
|
9656
|
+
this.name = "CheckpointPersistFailedError";
|
|
9657
|
+
}
|
|
9658
|
+
};
|
|
9659
|
+
var SplitCheckpointLostError = class extends SphereError {
|
|
9660
|
+
constructor(message, cause) {
|
|
9661
|
+
super(message, "SPLIT_CHECKPOINT_LOST", cause);
|
|
9662
|
+
this.name = "SplitCheckpointLostError";
|
|
9663
|
+
}
|
|
9664
|
+
};
|
|
9665
|
+
var CheckpointTrustbaseMismatchError = class extends SphereError {
|
|
9666
|
+
constructor(message, cause) {
|
|
9667
|
+
super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
|
|
9668
|
+
this.name = "CheckpointTrustbaseMismatchError";
|
|
9669
|
+
}
|
|
9670
|
+
};
|
|
9671
|
+
function isKeepOpenSplitError(err) {
|
|
9672
|
+
return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
|
|
9673
|
+
}
|
|
9674
|
+
|
|
9675
|
+
// token-engine/mint-reasons.ts
|
|
9676
|
+
var RULE = "MintJustificationVerification";
|
|
9677
|
+
var MintReasonRegistry = class _MintReasonRegistry extends import_MintJustificationVerifierService.MintJustificationVerifierService {
|
|
9678
|
+
byTag = /* @__PURE__ */ new Map();
|
|
9679
|
+
pluginTags = /* @__PURE__ */ new Set();
|
|
9680
|
+
register(verifier) {
|
|
9681
|
+
super.register(verifier);
|
|
9682
|
+
this.byTag.set(verifier.tag, verifier);
|
|
9683
|
+
return this;
|
|
9684
|
+
}
|
|
9685
|
+
registerPlugin(verifier) {
|
|
9686
|
+
this.register(verifier);
|
|
9687
|
+
this.pluginTags.add(verifier.tag);
|
|
9688
|
+
return this;
|
|
9689
|
+
}
|
|
9690
|
+
async verify(transaction, nestedTokenCollector) {
|
|
9691
|
+
const tag = transaction.justification ? reasonTag(transaction.justification) : null;
|
|
9692
|
+
if (tag === null) return super.verify(transaction, nestedTokenCollector);
|
|
9693
|
+
const verifier = this.byTag.get(tag);
|
|
9694
|
+
if (verifier === void 0) throw new MintReasonUnverifiableError(`No verifier is registered for mint-reason tag ${tag}`);
|
|
9695
|
+
if (!this.pluginTags.has(tag)) return super.verify(transaction, nestedTokenCollector);
|
|
9696
|
+
return verifyPlugin(verifier, transaction, nestedTokenCollector);
|
|
9697
|
+
}
|
|
9698
|
+
knows(tag) {
|
|
9699
|
+
return this.byTag.has(tag);
|
|
9700
|
+
}
|
|
9701
|
+
/** `verifiers` replace the registered ones of their tags; a tag this registry lacks is refused. */
|
|
9702
|
+
overlaid(verifiers) {
|
|
9703
|
+
for (const verifier of verifiers) assertKnown(this, verifier.tag);
|
|
9704
|
+
const replaced = new Map(verifiers.map((verifier) => [verifier.tag, verifier]));
|
|
9705
|
+
const overlay = new _MintReasonRegistry();
|
|
9706
|
+
for (const [tag, verifier] of this.byTag) {
|
|
9707
|
+
const perCall = replaced.get(tag);
|
|
9708
|
+
if (perCall !== void 0 || this.pluginTags.has(tag)) overlay.registerPlugin(perCall ?? verifier);
|
|
9709
|
+
else overlay.register(verifier);
|
|
9710
|
+
}
|
|
9711
|
+
return overlay;
|
|
9712
|
+
}
|
|
9713
|
+
assertVerifiable(justification) {
|
|
9714
|
+
const tag = reasonTag(justification);
|
|
9715
|
+
if (tag === null) throw new SphereError("The mint reason is not tagged CBOR", "VALIDATION_ERROR");
|
|
9716
|
+
assertKnown(this, tag);
|
|
9717
|
+
}
|
|
9718
|
+
};
|
|
9719
|
+
function reasonTag(justification) {
|
|
9720
|
+
try {
|
|
9721
|
+
return BigInt(import_CborDeserializer.CborDeserializer.decodeTag(justification).tag);
|
|
9722
|
+
} catch {
|
|
9723
|
+
return null;
|
|
9724
|
+
}
|
|
9725
|
+
}
|
|
9726
|
+
async function verifyPlugin(verifier, transaction, nestedTokenCollector) {
|
|
9727
|
+
let result;
|
|
9728
|
+
try {
|
|
9729
|
+
result = await verifier.verify(transaction, nestedTokenCollector);
|
|
9730
|
+
} catch (err) {
|
|
9731
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
9732
|
+
throw new MintReasonUnverifiableError(`Mint-reason tag ${verifier.tag} cannot be verified yet: ${why}`, err);
|
|
9733
|
+
}
|
|
9734
|
+
return result.status === import_VerificationStatus.VerificationStatus.OK ? new import_VerificationResult.VerificationResult(RULE, import_VerificationStatus.VerificationStatus.OK, "", [result]) : new import_VerificationResult.VerificationResult(RULE, import_VerificationStatus.VerificationStatus.FAIL, `Verification failed for tag ${verifier.tag}.`, [result]);
|
|
9735
|
+
}
|
|
9736
|
+
function assertKnown(registry, tag) {
|
|
9737
|
+
if (registry.knows(tag)) return;
|
|
9738
|
+
throw new SphereError(
|
|
9739
|
+
`No verifier is registered on the engine for mint-reason tag ${tag}, so a replay of this mint could never verify`,
|
|
9740
|
+
"VALIDATION_ERROR"
|
|
9741
|
+
);
|
|
9742
|
+
}
|
|
9743
|
+
|
|
9600
9744
|
// token-engine/SpherePaymentData.ts
|
|
9601
9745
|
var COIN_ID_PATTERN = /^([0-9a-f]{2})+$/;
|
|
9602
9746
|
function assertAsset(coinId, amount) {
|
|
@@ -9773,45 +9917,6 @@ async function awaitProofBounded(deps, transaction, signal) {
|
|
|
9773
9917
|
return value;
|
|
9774
9918
|
}
|
|
9775
9919
|
|
|
9776
|
-
// token-engine/errors.ts
|
|
9777
|
-
var TransferConflictError = class extends SphereError {
|
|
9778
|
-
/**
|
|
9779
|
-
* #625: the source token id (the caller's id, e.g. `v2_<genesis>`) whose state was already spent.
|
|
9780
|
-
* Set by PaymentsModule at the engine call site so the self-healing retry can demote it and re-plan.
|
|
9781
|
-
*/
|
|
9782
|
-
conflictedSourceId;
|
|
9783
|
-
constructor(message, cause) {
|
|
9784
|
-
super(message, "TRANSFER_CONFLICT", cause);
|
|
9785
|
-
this.name = "TransferConflictError";
|
|
9786
|
-
}
|
|
9787
|
-
};
|
|
9788
|
-
var ProofUnconfirmedError = class extends SphereError {
|
|
9789
|
-
mayHaveCertified = true;
|
|
9790
|
-
constructor(message, cause) {
|
|
9791
|
-
super(message, "CERTIFICATION_UNCONFIRMED", cause);
|
|
9792
|
-
this.name = "ProofUnconfirmedError";
|
|
9793
|
-
}
|
|
9794
|
-
};
|
|
9795
|
-
var CheckpointPersistFailedError = class extends SphereError {
|
|
9796
|
-
mayHaveCertified = true;
|
|
9797
|
-
constructor(message, cause) {
|
|
9798
|
-
super(message, "CHECKPOINT_PERSIST_FAILED", cause);
|
|
9799
|
-
this.name = "CheckpointPersistFailedError";
|
|
9800
|
-
}
|
|
9801
|
-
};
|
|
9802
|
-
var SplitCheckpointLostError = class extends SphereError {
|
|
9803
|
-
constructor(message, cause) {
|
|
9804
|
-
super(message, "SPLIT_CHECKPOINT_LOST", cause);
|
|
9805
|
-
this.name = "SplitCheckpointLostError";
|
|
9806
|
-
}
|
|
9807
|
-
};
|
|
9808
|
-
var CheckpointTrustbaseMismatchError = class extends SphereError {
|
|
9809
|
-
constructor(message, cause) {
|
|
9810
|
-
super(message, "CHECKPOINT_TRUSTBASE_MISMATCH", cause);
|
|
9811
|
-
this.name = "CheckpointTrustbaseMismatchError";
|
|
9812
|
-
}
|
|
9813
|
-
};
|
|
9814
|
-
|
|
9815
9920
|
// token-engine/certification-outcome.ts
|
|
9816
9921
|
function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
|
|
9817
9922
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -10374,15 +10479,17 @@ async function readTokenNft(token) {
|
|
|
10374
10479
|
}
|
|
10375
10480
|
|
|
10376
10481
|
// token-engine/plugin-ops.ts
|
|
10377
|
-
function
|
|
10378
|
-
|
|
10379
|
-
|
|
10482
|
+
function mintContext(deps, params) {
|
|
10483
|
+
assertMintableData(params.data);
|
|
10484
|
+
if (params.justification !== void 0) deps.mintJustificationVerifier.assertVerifiable(params.justification);
|
|
10485
|
+
const verifiers = params.mintJustificationVerifiers;
|
|
10486
|
+
if (verifiers === void 0) return deps.verificationContext;
|
|
10380
10487
|
return new import_VerificationContext.VerificationContext(
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10488
|
+
deps.trustBase,
|
|
10489
|
+
deps.predicateVerifier,
|
|
10490
|
+
deps.unicityCertificateVerifier,
|
|
10491
|
+
deps.mintJustificationVerifier.overlaid(verifiers),
|
|
10492
|
+
deps.verificationContext.tokenIssuanceVerifier
|
|
10386
10493
|
);
|
|
10387
10494
|
}
|
|
10388
10495
|
|
|
@@ -10470,9 +10577,6 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
|
|
|
10470
10577
|
// token-engine/SphereTokenEngine.ts
|
|
10471
10578
|
var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
|
|
10472
10579
|
var MAX_MINT_CONCURRENCY = 8;
|
|
10473
|
-
function isKeepOpenSplitError(err) {
|
|
10474
|
-
return err instanceof ProofUnconfirmedError || err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError;
|
|
10475
|
-
}
|
|
10476
10580
|
var TRANSFER_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
10477
10581
|
var CLEAN_REJECT_STATUSES = /* @__PURE__ */ new Set([
|
|
10478
10582
|
import_CertificationResponse.CertificationStatus.STATE_ID_MISMATCH,
|
|
@@ -10562,8 +10666,9 @@ var SphereTokenEngine = class {
|
|
|
10562
10666
|
const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
|
|
10563
10667
|
return wrapToken(token);
|
|
10564
10668
|
}
|
|
10669
|
+
assertMintable = (params) => void mintContext(this.deps, params);
|
|
10565
10670
|
async mintDataToken(params, options) {
|
|
10566
|
-
|
|
10671
|
+
const context = mintContext(this.deps, params);
|
|
10567
10672
|
const recipient = import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey);
|
|
10568
10673
|
const tokenType = params.tokenType ? new import_TokenType.TokenType(params.tokenType) : import_TokenType.TokenType.generate();
|
|
10569
10674
|
const salt = params.salt ? import_TokenSalt.TokenSalt.fromBytes(params.salt) : import_TokenSalt.TokenSalt.generate();
|
|
@@ -10577,8 +10682,7 @@ var SphereTokenEngine = class {
|
|
|
10577
10682
|
options
|
|
10578
10683
|
);
|
|
10579
10684
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10580
|
-
|
|
10581
|
-
return wrapToken(await import_Token.Token.mint(certified, verifiers ? contextWithMintVerifiers(this.deps, verifiers) : this.deps.verificationContext));
|
|
10685
|
+
return wrapToken(await import_Token.Token.mint(certified, context));
|
|
10582
10686
|
}
|
|
10583
10687
|
buildNftMint(params) {
|
|
10584
10688
|
return planNftMint(this.deps, params);
|
|
@@ -10915,20 +11019,30 @@ var SphereTokenEngine = class {
|
|
|
10915
11019
|
|
|
10916
11020
|
// token-engine/factory.ts
|
|
10917
11021
|
var DEFAULT_VERIFICATION_POOL_SIZE = 4;
|
|
10918
|
-
function
|
|
11022
|
+
function registerPlugins(registries, plugins) {
|
|
10919
11023
|
for (const plugin of plugins ?? []) {
|
|
10920
11024
|
for (const verifier of plugin.mintJustificationVerifiers ?? []) {
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
);
|
|
10928
|
-
}
|
|
11025
|
+
claim(plugin, `mint-reason tag ${verifier.tag}`, () => registries.reasons.registerPlugin(verifier));
|
|
11026
|
+
}
|
|
11027
|
+
for (const policy of plugin.tokenIssuancePolicies ?? []) {
|
|
11028
|
+
const tokenType = import_HexConverter.HexConverter.encode(policy.tokenType.bytes);
|
|
11029
|
+
claim(plugin, `an issuance policy for token type ${tokenType}`, () => {
|
|
11030
|
+
registries.claims.add(policy);
|
|
11031
|
+
registries.policies.register(policy);
|
|
11032
|
+
});
|
|
10929
11033
|
}
|
|
10930
11034
|
}
|
|
10931
11035
|
}
|
|
11036
|
+
function claim(plugin, what, register) {
|
|
11037
|
+
try {
|
|
11038
|
+
register();
|
|
11039
|
+
} catch (err) {
|
|
11040
|
+
throw new SphereError(
|
|
11041
|
+
`Token plugin '${plugin.id}' registers ${what} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
|
|
11042
|
+
"INVALID_CONFIG"
|
|
11043
|
+
);
|
|
11044
|
+
}
|
|
11045
|
+
}
|
|
10932
11046
|
function disposedError() {
|
|
10933
11047
|
return new SphereError(
|
|
10934
11048
|
"Verification worker pool disposed \u2014 the in-flight verification was cancelled",
|
|
@@ -11006,11 +11120,15 @@ async function createSphereTokenEngine(config) {
|
|
|
11006
11120
|
const unicityCertificateVerifier = new import_UnicityCertificateVerifier.UnicityCertificateVerifier(
|
|
11007
11121
|
new import_UnicitySealQuorumSignaturesVerificationRule.UnicitySealQuorumSignaturesVerificationRule(new import_Secp256k1SignatureVerifier.Secp256k1SignatureVerifier(), new import_VerifiedSealCache.VerifiedSealCache(256))
|
|
11008
11122
|
);
|
|
11009
|
-
const mintJustificationVerifier = new
|
|
11123
|
+
const mintJustificationVerifier = new MintReasonRegistry();
|
|
11010
11124
|
mintJustificationVerifier.register(
|
|
11011
11125
|
new import_SplitMintJustificationVerifier.SplitMintJustificationVerifier(decodeSpherePaymentData)
|
|
11012
11126
|
);
|
|
11013
|
-
|
|
11127
|
+
const tokenIssuanceVerifier = new import_TokenIssuanceVerifierService.TokenIssuanceVerifierService(false);
|
|
11128
|
+
registerPlugins(
|
|
11129
|
+
{ reasons: mintJustificationVerifier, policies: tokenIssuanceVerifier, claims: new CoinClaims() },
|
|
11130
|
+
config.plugins
|
|
11131
|
+
);
|
|
11014
11132
|
const deps = {
|
|
11015
11133
|
client: new import_StateTransitionClient.StateTransitionClient(new import_AggregatorClient.AggregatorClient(config.aggregatorUrl, config.apiKey ?? null)),
|
|
11016
11134
|
trustBase,
|
|
@@ -11022,7 +11140,7 @@ async function createSphereTokenEngine(config) {
|
|
|
11022
11140
|
predicateVerifier,
|
|
11023
11141
|
unicityCertificateVerifier,
|
|
11024
11142
|
mintJustificationVerifier,
|
|
11025
|
-
|
|
11143
|
+
tokenIssuanceVerifier
|
|
11026
11144
|
),
|
|
11027
11145
|
signingService: new import_SigningService.SigningService(config.privateKey),
|
|
11028
11146
|
// Also the HKDF ikm for deterministic realization (Part E.1) — the
|
|
@@ -12122,6 +12240,9 @@ function createScopedKV(storage, network, chainPubkey) {
|
|
|
12122
12240
|
}
|
|
12123
12241
|
};
|
|
12124
12242
|
}
|
|
12243
|
+
function isLiveBurn(entry) {
|
|
12244
|
+
return !entry.settled && entry.failure === void 0;
|
|
12245
|
+
}
|
|
12125
12246
|
var STORE_KEYS = {
|
|
12126
12247
|
intentBackstop: "intents",
|
|
12127
12248
|
checkpointCache: "checkpoints",
|
|
@@ -12136,7 +12257,8 @@ var STORE_KEYS = {
|
|
|
12136
12257
|
epochLatch: "epoch-latch",
|
|
12137
12258
|
// §5.2 InventoryView durable overlays (#625/#679).
|
|
12138
12259
|
suspectedSpent: "suspected-spent",
|
|
12139
|
-
knownSpends: "known-spends"
|
|
12260
|
+
knownSpends: "known-spends",
|
|
12261
|
+
verifiedTokens: "verified-tokens"
|
|
12140
12262
|
};
|
|
12141
12263
|
|
|
12142
12264
|
// modules/payments-v2/machine/journal.ts
|
|
@@ -12653,6 +12775,7 @@ var TransferMachine = class {
|
|
|
12653
12775
|
const key = keys.get(digest);
|
|
12654
12776
|
if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
|
|
12655
12777
|
const { tokenId } = await engine.deliveryKeys(o.changeBlob);
|
|
12778
|
+
await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
|
|
12656
12779
|
added.push({ tokenId, key });
|
|
12657
12780
|
}
|
|
12658
12781
|
return added;
|
|
@@ -13048,7 +13171,7 @@ var Converger = class {
|
|
|
13048
13171
|
this.deps.stores.burnJournal.list()
|
|
13049
13172
|
]);
|
|
13050
13173
|
const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
|
|
13051
|
-
const burnsOpen = burns.some(
|
|
13174
|
+
const burnsOpen = burns.some(isLiveBurn);
|
|
13052
13175
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
|
|
13053
13176
|
}
|
|
13054
13177
|
async resumePass() {
|
|
@@ -13135,6 +13258,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
|
13135
13258
|
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
13136
13259
|
holds.set(entry.transferId, [...direct, ...split]);
|
|
13137
13260
|
}
|
|
13261
|
+
for (const burn of await stores.burnJournal.list()) {
|
|
13262
|
+
if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
|
|
13263
|
+
}
|
|
13138
13264
|
return { open: holds, complete };
|
|
13139
13265
|
}
|
|
13140
13266
|
function groupJournal(journal) {
|
|
@@ -13445,18 +13571,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
|
13445
13571
|
createdAt: snapshot.createdAt,
|
|
13446
13572
|
updatedAt: snapshot.updatedAt,
|
|
13447
13573
|
lazy: true,
|
|
13448
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
13574
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {},
|
|
13575
|
+
...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
|
|
13449
13576
|
};
|
|
13450
13577
|
}
|
|
13451
|
-
function aggregateAssets(entries,
|
|
13578
|
+
function aggregateAssets(entries, reading, registry) {
|
|
13452
13579
|
const groups = /* @__PURE__ */ new Map();
|
|
13453
13580
|
for (const [tokenId, entry] of entries) {
|
|
13454
|
-
const moving = isTransferring(tokenId);
|
|
13581
|
+
const moving = reading.isTransferring(tokenId);
|
|
13455
13582
|
for (const asset of entry.assets) {
|
|
13456
|
-
|
|
13583
|
+
const standing = reading.standing(tokenId, asset.coinId);
|
|
13584
|
+
const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
|
|
13585
|
+
let group = groups.get(key);
|
|
13457
13586
|
if (!group) {
|
|
13458
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
13459
|
-
groups.set(
|
|
13587
|
+
group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
13588
|
+
groups.set(key, group);
|
|
13460
13589
|
}
|
|
13461
13590
|
if (moving) {
|
|
13462
13591
|
group.transferring += BigInt(asset.amount);
|
|
@@ -13467,9 +13596,10 @@ function aggregateAssets(entries, isTransferring, registry) {
|
|
|
13467
13596
|
}
|
|
13468
13597
|
}
|
|
13469
13598
|
}
|
|
13470
|
-
return [...groups].map((
|
|
13599
|
+
return [...groups.values()].map((totals) => toAsset(totals, registry));
|
|
13471
13600
|
}
|
|
13472
|
-
function toAsset(
|
|
13601
|
+
function toAsset(totals, registry) {
|
|
13602
|
+
const coinId = totals.coinId;
|
|
13473
13603
|
const iconUrl = registry.getIconUrl(coinId);
|
|
13474
13604
|
return {
|
|
13475
13605
|
coinId,
|
|
@@ -13489,7 +13619,8 @@ function toAsset(coinId, totals, registry) {
|
|
|
13489
13619
|
priceEur: null,
|
|
13490
13620
|
change24h: null,
|
|
13491
13621
|
fiatValueUsd: null,
|
|
13492
|
-
fiatValueEur: null
|
|
13622
|
+
fiatValueEur: null,
|
|
13623
|
+
...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
|
|
13493
13624
|
};
|
|
13494
13625
|
}
|
|
13495
13626
|
function priceIdOf(registry, coinId) {
|
|
@@ -13497,9 +13628,9 @@ function priceIdOf(registry, coinId) {
|
|
|
13497
13628
|
}
|
|
13498
13629
|
async function withPrices(raw, registry, price) {
|
|
13499
13630
|
try {
|
|
13500
|
-
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
13631
|
+
const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
|
|
13501
13632
|
const quotes = await price.getPrices(ids);
|
|
13502
|
-
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
13633
|
+
return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
13503
13634
|
} catch {
|
|
13504
13635
|
return raw;
|
|
13505
13636
|
}
|
|
@@ -13520,11 +13651,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
|
13520
13651
|
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
13521
13652
|
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
13522
13653
|
transferring: true,
|
|
13523
|
-
suspectedSpent: false
|
|
13654
|
+
suspectedSpent: false,
|
|
13655
|
+
standing: "trusted"
|
|
13524
13656
|
});
|
|
13525
13657
|
}
|
|
13526
13658
|
|
|
13527
13659
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
13660
|
+
var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
|
|
13528
13661
|
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
13529
13662
|
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
13530
13663
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
@@ -13618,6 +13751,12 @@ var InventoryView = class {
|
|
|
13618
13751
|
for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
|
|
13619
13752
|
if (changed) this.deps.emit("inventory:updated");
|
|
13620
13753
|
}
|
|
13754
|
+
get trust() {
|
|
13755
|
+
return this.deps.trust ?? TRUST_ALL;
|
|
13756
|
+
}
|
|
13757
|
+
trusts(tokenId, coinId) {
|
|
13758
|
+
return this.trust.standing(tokenId, coinId) === "trusted";
|
|
13759
|
+
}
|
|
13621
13760
|
pinned(tokenId) {
|
|
13622
13761
|
return this.deps.isPinned?.(tokenId) ?? false;
|
|
13623
13762
|
}
|
|
@@ -13644,6 +13783,7 @@ var InventoryView = class {
|
|
|
13644
13783
|
isSpendable(tokenId, entry) {
|
|
13645
13784
|
if (entry.status !== "active") return false;
|
|
13646
13785
|
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
|
|
13786
|
+
if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
|
|
13647
13787
|
return !this.suspected.has(stateKey(tokenId, entry.stateHash));
|
|
13648
13788
|
}
|
|
13649
13789
|
/** Is this NAMED token spendable whole — coinless or valued (#777)? */
|
|
@@ -13670,7 +13810,8 @@ var InventoryView = class {
|
|
|
13670
13810
|
out.push(
|
|
13671
13811
|
toToken(tokenId, entry, asset, registry, {
|
|
13672
13812
|
transferring: this.held(tokenId),
|
|
13673
|
-
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
13813
|
+
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
|
|
13814
|
+
standing: this.trust.standing(tokenId, asset.coinId)
|
|
13674
13815
|
})
|
|
13675
13816
|
);
|
|
13676
13817
|
}
|
|
@@ -13700,10 +13841,21 @@ var InventoryView = class {
|
|
|
13700
13841
|
return out;
|
|
13701
13842
|
}
|
|
13702
13843
|
async assets(registry, price) {
|
|
13703
|
-
const
|
|
13844
|
+
const reading = {
|
|
13845
|
+
isTransferring: (tokenId) => this.held(tokenId),
|
|
13846
|
+
standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
|
|
13847
|
+
};
|
|
13848
|
+
const raw = aggregateAssets(this.activeEntries(), reading, registry);
|
|
13704
13849
|
if (!price || raw.length === 0) return raw;
|
|
13705
13850
|
return withPrices(raw, registry, price);
|
|
13706
13851
|
}
|
|
13852
|
+
claimHolders() {
|
|
13853
|
+
const out = [];
|
|
13854
|
+
for (const [tokenId, entry] of this.activeEntries()) {
|
|
13855
|
+
if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
|
|
13856
|
+
}
|
|
13857
|
+
return out;
|
|
13858
|
+
}
|
|
13707
13859
|
*activeEntries() {
|
|
13708
13860
|
for (const [tokenId, entry] of this.mirror) {
|
|
13709
13861
|
if (entry.status === "active") yield [tokenId, entry];
|
|
@@ -13873,6 +14025,171 @@ var InventoryView = class {
|
|
|
13873
14025
|
}
|
|
13874
14026
|
};
|
|
13875
14027
|
|
|
14028
|
+
// modules/payments-v2/inventory/verdicts.ts
|
|
14029
|
+
var VERDICT_RETRY_MS = 3e4;
|
|
14030
|
+
var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
|
|
14031
|
+
function honoursClaims(claims, token) {
|
|
14032
|
+
const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
|
|
14033
|
+
return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
|
|
14034
|
+
}
|
|
14035
|
+
var TokenVerdicts = class {
|
|
14036
|
+
constructor(deps) {
|
|
14037
|
+
this.deps = deps;
|
|
14038
|
+
}
|
|
14039
|
+
verified = /* @__PURE__ */ new Set();
|
|
14040
|
+
refused = /* @__PURE__ */ new Set();
|
|
14041
|
+
retries = /* @__PURE__ */ new Map();
|
|
14042
|
+
writes = new SerialChain();
|
|
14043
|
+
hydration = null;
|
|
14044
|
+
queued = null;
|
|
14045
|
+
running = null;
|
|
14046
|
+
held = [];
|
|
14047
|
+
timer = null;
|
|
14048
|
+
live = false;
|
|
14049
|
+
isClaimed(coinId) {
|
|
14050
|
+
return this.deps.claims.issuerOf(coinId) !== null;
|
|
14051
|
+
}
|
|
14052
|
+
standing(tokenId, coinId) {
|
|
14053
|
+
if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
|
|
14054
|
+
return this.refused.has(tokenId) ? "refused" : "pending";
|
|
14055
|
+
}
|
|
14056
|
+
trusts(tokenId, coinId) {
|
|
14057
|
+
return this.standing(tokenId, coinId) === "trusted";
|
|
14058
|
+
}
|
|
14059
|
+
hydrate() {
|
|
14060
|
+
this.hydration ??= this.load().catch((error) => {
|
|
14061
|
+
this.hydration = null;
|
|
14062
|
+
throw error;
|
|
14063
|
+
});
|
|
14064
|
+
return this.hydration;
|
|
14065
|
+
}
|
|
14066
|
+
start() {
|
|
14067
|
+
this.live = true;
|
|
14068
|
+
return this.hydrate();
|
|
14069
|
+
}
|
|
14070
|
+
stop() {
|
|
14071
|
+
this.live = false;
|
|
14072
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
14073
|
+
this.timer = null;
|
|
14074
|
+
}
|
|
14075
|
+
async accept(token) {
|
|
14076
|
+
if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
|
|
14077
|
+
}
|
|
14078
|
+
async recordSplit(sourceTokenId, outputTokenId) {
|
|
14079
|
+
await this.hydrate();
|
|
14080
|
+
if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
|
|
14081
|
+
}
|
|
14082
|
+
review(holders) {
|
|
14083
|
+
this.queued = holders;
|
|
14084
|
+
this.running ??= this.drain();
|
|
14085
|
+
return this.running;
|
|
14086
|
+
}
|
|
14087
|
+
async drain() {
|
|
14088
|
+
try {
|
|
14089
|
+
while (this.queued !== null) {
|
|
14090
|
+
const holders = this.queued;
|
|
14091
|
+
this.queued = null;
|
|
14092
|
+
await this.reviewOnce(holders);
|
|
14093
|
+
}
|
|
14094
|
+
} finally {
|
|
14095
|
+
this.running = null;
|
|
14096
|
+
}
|
|
14097
|
+
}
|
|
14098
|
+
async reviewOnce(holders) {
|
|
14099
|
+
await this.hydrate();
|
|
14100
|
+
this.held = holders;
|
|
14101
|
+
const due = holders.filter((tokenId) => this.isDue(tokenId));
|
|
14102
|
+
const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
|
|
14103
|
+
for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
|
|
14104
|
+
if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
|
|
14105
|
+
await this.persist();
|
|
14106
|
+
this.deps.changed();
|
|
14107
|
+
}
|
|
14108
|
+
this.scheduleRetry();
|
|
14109
|
+
}
|
|
14110
|
+
isDue(tokenId) {
|
|
14111
|
+
if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
|
|
14112
|
+
return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
|
|
14113
|
+
}
|
|
14114
|
+
async check(due) {
|
|
14115
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
14116
|
+
let blobs;
|
|
14117
|
+
try {
|
|
14118
|
+
blobs = await this.deps.getBlobs([...due]);
|
|
14119
|
+
} catch {
|
|
14120
|
+
return verdicts;
|
|
14121
|
+
}
|
|
14122
|
+
for (const tokenId of due) {
|
|
14123
|
+
const bytes = blobs.get(tokenId);
|
|
14124
|
+
if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
|
|
14125
|
+
}
|
|
14126
|
+
return verdicts;
|
|
14127
|
+
}
|
|
14128
|
+
async verdictOf(tokenId, bytes) {
|
|
14129
|
+
let engine;
|
|
14130
|
+
try {
|
|
14131
|
+
engine = this.deps.engine();
|
|
14132
|
+
} catch {
|
|
14133
|
+
return "retry";
|
|
14134
|
+
}
|
|
14135
|
+
let token;
|
|
14136
|
+
try {
|
|
14137
|
+
token = await engine.decodeToken({ tokenId, token: bytes });
|
|
14138
|
+
} catch {
|
|
14139
|
+
return "refused";
|
|
14140
|
+
}
|
|
14141
|
+
if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
|
|
14142
|
+
try {
|
|
14143
|
+
return (await engine.verify(token)).ok ? "verified" : "refused";
|
|
14144
|
+
} catch {
|
|
14145
|
+
return "retry";
|
|
14146
|
+
}
|
|
14147
|
+
}
|
|
14148
|
+
settle(tokenId, verdict) {
|
|
14149
|
+
if (verdict === "retry") {
|
|
14150
|
+
this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
|
|
14151
|
+
return;
|
|
14152
|
+
}
|
|
14153
|
+
this.retries.delete(tokenId);
|
|
14154
|
+
(verdict === "verified" ? this.verified : this.refused).add(tokenId);
|
|
14155
|
+
}
|
|
14156
|
+
nextRetry(previous) {
|
|
14157
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
14158
|
+
const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
|
|
14159
|
+
return { attempts, atMs: this.now() + delay };
|
|
14160
|
+
}
|
|
14161
|
+
scheduleRetry() {
|
|
14162
|
+
if (!this.live || this.timer !== null) return;
|
|
14163
|
+
const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
|
|
14164
|
+
if (due.length === 0) return;
|
|
14165
|
+
const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
|
|
14166
|
+
this.timer = setTimeout(() => {
|
|
14167
|
+
this.timer = null;
|
|
14168
|
+
void this.review(this.held).catch(() => void 0);
|
|
14169
|
+
}, delay);
|
|
14170
|
+
}
|
|
14171
|
+
async remember(tokenId) {
|
|
14172
|
+
await this.hydrate();
|
|
14173
|
+
if (this.verified.has(tokenId)) return;
|
|
14174
|
+
this.verified.add(tokenId);
|
|
14175
|
+
this.refused.delete(tokenId);
|
|
14176
|
+
this.retries.delete(tokenId);
|
|
14177
|
+
await this.persist();
|
|
14178
|
+
}
|
|
14179
|
+
persist() {
|
|
14180
|
+
const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
|
|
14181
|
+
return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
|
|
14182
|
+
}
|
|
14183
|
+
async load() {
|
|
14184
|
+
const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
|
|
14185
|
+
if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
|
|
14186
|
+
for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
|
|
14187
|
+
}
|
|
14188
|
+
now() {
|
|
14189
|
+
return (this.deps.now ?? Date.now)();
|
|
14190
|
+
}
|
|
14191
|
+
};
|
|
14192
|
+
|
|
13876
14193
|
// modules/payments-v2/ports.ts
|
|
13877
14194
|
function isRetryableAckError(err) {
|
|
13878
14195
|
return err !== null && typeof err === "object" && err.retryable === true;
|
|
@@ -13882,6 +14199,8 @@ function isRetryableAckError(err) {
|
|
|
13882
14199
|
var ACK_BATCH_SIZE = 200;
|
|
13883
14200
|
var REFRESH_INTERVAL_MS = 2500;
|
|
13884
14201
|
var POLL_INTERVAL_MS = 3e4;
|
|
14202
|
+
var RECHECK_BASE_MS = POLL_INTERVAL_MS;
|
|
14203
|
+
var RECHECK_MAX_MS = 60 * 60 * 1e3;
|
|
13885
14204
|
var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
|
|
13886
14205
|
function receivedDedupKey(tokenId, stateHash) {
|
|
13887
14206
|
return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
|
|
@@ -13898,6 +14217,7 @@ var Receive = class {
|
|
|
13898
14217
|
pollTimer = null;
|
|
13899
14218
|
unsubscribeWake = null;
|
|
13900
14219
|
lastRefreshAt = 0;
|
|
14220
|
+
rechecks = /* @__PURE__ */ new Map();
|
|
13901
14221
|
drainOnce() {
|
|
13902
14222
|
return this.drainFlight.run(() => this.doDrain());
|
|
13903
14223
|
}
|
|
@@ -13956,6 +14276,7 @@ var Receive = class {
|
|
|
13956
14276
|
const pending = [];
|
|
13957
14277
|
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
13958
14278
|
const claimable = { n: 0 };
|
|
14279
|
+
const deferred = { any: false };
|
|
13959
14280
|
const finish = () => {
|
|
13960
14281
|
if (claimable.n > 0) deps.refreshView?.();
|
|
13961
14282
|
return stored;
|
|
@@ -13964,7 +14285,7 @@ var Receive = class {
|
|
|
13964
14285
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
13965
14286
|
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
13966
14287
|
for (let pass = 0; pass < 2; pass++) {
|
|
13967
|
-
await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
|
|
14288
|
+
await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
|
|
13968
14289
|
const served = deps.delivery.incomingEpoch();
|
|
13969
14290
|
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
13970
14291
|
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
@@ -13981,20 +14302,41 @@ var Receive = class {
|
|
|
13981
14302
|
return finish();
|
|
13982
14303
|
}
|
|
13983
14304
|
async processEntry(ctx, entry) {
|
|
13984
|
-
const { deps, engine,
|
|
13985
|
-
const screened = await screen(deps, engine, entry);
|
|
14305
|
+
const { deps, engine, stored, claimable } = ctx;
|
|
14306
|
+
const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
|
|
14307
|
+
this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
|
|
14308
|
+
if (screened.kind === "defer") {
|
|
14309
|
+
ctx.deferred.any = true;
|
|
14310
|
+
return;
|
|
14311
|
+
}
|
|
13986
14312
|
if (screened.kind === "ack") {
|
|
13987
|
-
|
|
14313
|
+
queueAck(ctx, screened.ack);
|
|
13988
14314
|
if (screened.ack.disposition === "claimed") claimable.n += 1;
|
|
13989
14315
|
return;
|
|
13990
14316
|
}
|
|
14317
|
+
await deps.accepted?.(screened.token);
|
|
13991
14318
|
await deps.view.store(screened.record);
|
|
13992
|
-
|
|
14319
|
+
queueAck(ctx, claimAck(entry));
|
|
13993
14320
|
claimable.n += 1;
|
|
13994
14321
|
const transfer = await announce(deps, entry, screened.record);
|
|
13995
14322
|
stored.push(transfer);
|
|
13996
14323
|
deps.emit("transfer:incoming", transfer);
|
|
13997
14324
|
}
|
|
14325
|
+
recheckDue(deliveryId) {
|
|
14326
|
+
const recheck = this.rechecks.get(deliveryId);
|
|
14327
|
+
return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
|
|
14328
|
+
}
|
|
14329
|
+
scheduleRecheck(deliveryId, deferred) {
|
|
14330
|
+
if (!deferred) {
|
|
14331
|
+
this.rechecks.delete(deliveryId);
|
|
14332
|
+
return;
|
|
14333
|
+
}
|
|
14334
|
+
const previous = this.rechecks.get(deliveryId);
|
|
14335
|
+
if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
|
|
14336
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
14337
|
+
const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
|
|
14338
|
+
this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
|
|
14339
|
+
}
|
|
13998
14340
|
};
|
|
13999
14341
|
async function screen(deps, engine, entry) {
|
|
14000
14342
|
const blobBytes = await entry.fetchBlob();
|
|
@@ -14005,7 +14347,8 @@ async function screen(deps, engine, entry) {
|
|
|
14005
14347
|
logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
|
|
14006
14348
|
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
14007
14349
|
}
|
|
14008
|
-
const verdict = await engine
|
|
14350
|
+
const verdict = await verdictOf(engine, token);
|
|
14351
|
+
if (verdict === null) return { kind: "defer" };
|
|
14009
14352
|
if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
14010
14353
|
if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
|
|
14011
14354
|
return { kind: "ack", ack: rejectAck(entry, "not-owned") };
|
|
@@ -14016,38 +14359,43 @@ async function screen(deps, engine, entry) {
|
|
|
14016
14359
|
if (held !== null && await engine.isSpent(token)) {
|
|
14017
14360
|
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
14018
14361
|
}
|
|
14362
|
+
const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
|
|
14019
14363
|
return {
|
|
14020
14364
|
kind: "accept",
|
|
14365
|
+
token,
|
|
14021
14366
|
record: {
|
|
14022
14367
|
tokenId: keys.tokenId,
|
|
14023
14368
|
stateHash: keys.stateHash,
|
|
14024
14369
|
assets: toAssetAmounts(token),
|
|
14025
|
-
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
|
|
14370
|
+
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
|
|
14371
|
+
...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
|
|
14026
14372
|
}
|
|
14027
14373
|
};
|
|
14028
14374
|
}
|
|
14029
|
-
|
|
14030
|
-
const
|
|
14375
|
+
function unverifiedCoinsOf(deps, token) {
|
|
14376
|
+
const claims = deps.claims;
|
|
14377
|
+
if (claims === void 0 || honoursClaims(claims, token)) return [];
|
|
14378
|
+
return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
|
|
14379
|
+
}
|
|
14380
|
+
async function verdictOf(engine, token) {
|
|
14031
14381
|
try {
|
|
14032
|
-
await
|
|
14033
|
-
dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
|
|
14034
|
-
tokenId: record.tokenId,
|
|
14035
|
-
stateHash: record.stateHash,
|
|
14036
|
-
assets: record.assets,
|
|
14037
|
-
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
14038
|
-
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
14039
|
-
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
14040
|
-
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
14041
|
-
receivedAt
|
|
14042
|
-
});
|
|
14382
|
+
return await engine.verify(token);
|
|
14043
14383
|
} catch (err) {
|
|
14044
|
-
|
|
14384
|
+
if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
|
|
14385
|
+
throw err;
|
|
14045
14386
|
}
|
|
14387
|
+
}
|
|
14388
|
+
async function announce(deps, entry, record) {
|
|
14389
|
+
const receivedAt = (deps.now ?? Date.now)();
|
|
14390
|
+
await recordArrival(deps, entry, record, receivedAt);
|
|
14391
|
+
const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
|
|
14392
|
+
const counted = record.assets.filter((asset) => !refused2.includes(asset));
|
|
14046
14393
|
return {
|
|
14047
14394
|
id: record.tokenId,
|
|
14048
14395
|
senderPubkey: entry.senderPubkey ?? "",
|
|
14049
14396
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
14050
|
-
tokens:
|
|
14397
|
+
tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
|
|
14398
|
+
...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
|
|
14051
14399
|
// #777: named here rather than mapped from assets, which announced an EMPTY list.
|
|
14052
14400
|
...record.tokenType !== void 0 ? {
|
|
14053
14401
|
coinless: [
|
|
@@ -14065,6 +14413,25 @@ async function announce(deps, entry, record) {
|
|
|
14065
14413
|
receivedAt
|
|
14066
14414
|
};
|
|
14067
14415
|
}
|
|
14416
|
+
async function recordArrival(deps, entry, record, receivedAt) {
|
|
14417
|
+
const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
|
|
14418
|
+
if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
|
|
14419
|
+
try {
|
|
14420
|
+
await deps.recordReceived({
|
|
14421
|
+
dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
|
|
14422
|
+
tokenId: record.tokenId,
|
|
14423
|
+
stateHash: record.stateHash,
|
|
14424
|
+
assets,
|
|
14425
|
+
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
14426
|
+
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
14427
|
+
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
14428
|
+
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
14429
|
+
receivedAt
|
|
14430
|
+
});
|
|
14431
|
+
} catch (err) {
|
|
14432
|
+
logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
|
|
14433
|
+
}
|
|
14434
|
+
}
|
|
14068
14435
|
async function flushAcks(deps, pending, epochOf) {
|
|
14069
14436
|
if (pending.length === 0) return { progressed: true };
|
|
14070
14437
|
const before = pending.length;
|
|
@@ -14072,7 +14439,7 @@ async function flushAcks(deps, pending, epochOf) {
|
|
|
14072
14439
|
try {
|
|
14073
14440
|
const settled = await settleAcks(deps, pending);
|
|
14074
14441
|
let i = 0;
|
|
14075
|
-
while (i < pending.length && settled.has(pending[i].deliveryId)) {
|
|
14442
|
+
while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
|
|
14076
14443
|
lastAcked = pending[i].cursor;
|
|
14077
14444
|
i += 1;
|
|
14078
14445
|
}
|
|
@@ -14148,6 +14515,9 @@ async function ackOne(deps, ack) {
|
|
|
14148
14515
|
await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
|
|
14149
14516
|
}
|
|
14150
14517
|
}
|
|
14518
|
+
function queueAck(ctx, ack) {
|
|
14519
|
+
ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
|
|
14520
|
+
}
|
|
14151
14521
|
function claimAck(entry) {
|
|
14152
14522
|
return {
|
|
14153
14523
|
deliveryId: entry.deliveryId,
|
|
@@ -14856,72 +15226,276 @@ var SpendQueue = class {
|
|
|
14856
15226
|
queue = [];
|
|
14857
15227
|
this.queues.set(coinId, queue);
|
|
14858
15228
|
}
|
|
14859
|
-
queue.push(entry);
|
|
14860
|
-
return settled;
|
|
15229
|
+
queue.push(entry);
|
|
15230
|
+
return settled;
|
|
15231
|
+
}
|
|
15232
|
+
expire(id, coinId) {
|
|
15233
|
+
const entries = this.queues.get(coinId);
|
|
15234
|
+
if (!entries) return;
|
|
15235
|
+
const index = entries.findIndex((e) => e.id === id);
|
|
15236
|
+
if (index === -1) return;
|
|
15237
|
+
const [entry] = entries.splice(index, 1);
|
|
15238
|
+
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
15239
|
+
if (entries.length === 0) this.queues.delete(coinId);
|
|
15240
|
+
}
|
|
15241
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
15242
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
15243
|
+
freeView(coinId) {
|
|
15244
|
+
const freeView = [];
|
|
15245
|
+
let freeTotal = 0n;
|
|
15246
|
+
let pinnedTotal = 0n;
|
|
15247
|
+
const holders = /* @__PURE__ */ new Set();
|
|
15248
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
15249
|
+
for (const entry of this.deps.getPool(coinId)) {
|
|
15250
|
+
if (entry.amount <= 0n) continue;
|
|
15251
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
15252
|
+
if (holder === void 0) {
|
|
15253
|
+
freeView.push(entry);
|
|
15254
|
+
freeTotal += entry.amount;
|
|
15255
|
+
} else {
|
|
15256
|
+
pinnedTotal += entry.amount;
|
|
15257
|
+
holders.add(holder);
|
|
15258
|
+
}
|
|
15259
|
+
}
|
|
15260
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
15261
|
+
}
|
|
15262
|
+
expectedChangeTotal(coinId) {
|
|
15263
|
+
let total = 0n;
|
|
15264
|
+
for (const change of this.expectedChange.values()) {
|
|
15265
|
+
if (change.coinId === coinId) total += change.amount;
|
|
15266
|
+
}
|
|
15267
|
+
return total;
|
|
15268
|
+
}
|
|
15269
|
+
reservePlan(reservationId, plan, freeView) {
|
|
15270
|
+
const pool = new Set(freeView.map((e) => e.tokenId));
|
|
15271
|
+
const tokenIds = [...plan.direct, ...plan.split ? [plan.split.tokenId] : []];
|
|
15272
|
+
for (const tokenId of tokenIds) {
|
|
15273
|
+
if (!pool.has(tokenId)) {
|
|
15274
|
+
throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
|
|
15275
|
+
}
|
|
15276
|
+
}
|
|
15277
|
+
this.deps.ledger.reserve(reservationId, tokenIds.map((tokenId) => ({ tokenId })));
|
|
15278
|
+
}
|
|
15279
|
+
totalQueued() {
|
|
15280
|
+
let total = 0;
|
|
15281
|
+
for (const entries of this.queues.values()) total += entries.length;
|
|
15282
|
+
return total;
|
|
15283
|
+
}
|
|
15284
|
+
};
|
|
15285
|
+
function parseAmount(raw) {
|
|
15286
|
+
if (!/^\d+$/.test(raw)) {
|
|
15287
|
+
throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
|
|
15288
|
+
}
|
|
15289
|
+
const amount = BigInt(raw);
|
|
15290
|
+
if (amount <= 0n) {
|
|
15291
|
+
throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
|
|
15292
|
+
}
|
|
15293
|
+
return amount;
|
|
15294
|
+
}
|
|
15295
|
+
|
|
15296
|
+
// modules/payments-v2/machine/payload.ts
|
|
15297
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
15298
|
+
return {
|
|
15299
|
+
v: 2,
|
|
15300
|
+
kind: "coin",
|
|
15301
|
+
recipient: recipientPubkey,
|
|
15302
|
+
coinId: request.coinId,
|
|
15303
|
+
amount: request.amount,
|
|
15304
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
15305
|
+
direct: [...spend.plan.direct],
|
|
15306
|
+
...spend.plan.split !== void 0 ? {
|
|
15307
|
+
split: {
|
|
15308
|
+
tokenId: spend.plan.split.tokenId,
|
|
15309
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
15310
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
15311
|
+
}
|
|
15312
|
+
} : {},
|
|
15313
|
+
spentStates
|
|
15314
|
+
};
|
|
15315
|
+
}
|
|
15316
|
+
function messageOf(err) {
|
|
15317
|
+
return err instanceof Error ? err.message : String(err);
|
|
15318
|
+
}
|
|
15319
|
+
function buildWholePayload(recipientPubkey, request, spentStates) {
|
|
15320
|
+
return {
|
|
15321
|
+
v: 2,
|
|
15322
|
+
kind: "whole",
|
|
15323
|
+
recipient: recipientPubkey,
|
|
15324
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
15325
|
+
direct: [request.tokenId],
|
|
15326
|
+
spentStates
|
|
15327
|
+
};
|
|
15328
|
+
}
|
|
15329
|
+
|
|
15330
|
+
// modules/payments-v2/burn.ts
|
|
15331
|
+
var HEX32 = /^[0-9a-f]{64}$/;
|
|
15332
|
+
async function heldToken2(deps, tokenId) {
|
|
15333
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
15334
|
+
if (bytes === void 0) {
|
|
15335
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
|
|
15336
|
+
}
|
|
15337
|
+
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
15338
|
+
}
|
|
15339
|
+
function assertBurnable(engine, token, tokenId) {
|
|
15340
|
+
if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
|
|
15341
|
+
throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
|
|
15342
|
+
}
|
|
15343
|
+
if (token.valueEnvelope === "bare_collection") {
|
|
15344
|
+
throw new SphereError(
|
|
15345
|
+
`Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
|
|
15346
|
+
"VALIDATION_ERROR"
|
|
15347
|
+
);
|
|
15348
|
+
}
|
|
15349
|
+
}
|
|
15350
|
+
function assetsOf(token) {
|
|
15351
|
+
return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
|
|
15352
|
+
}
|
|
15353
|
+
async function runBurnUnderJournal(deps, input) {
|
|
15354
|
+
const { burnId } = input;
|
|
15355
|
+
const { tokenId, reasonBytes } = input.request;
|
|
15356
|
+
if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
|
|
15357
|
+
return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
|
|
15358
|
+
}
|
|
15359
|
+
let token;
|
|
15360
|
+
try {
|
|
15361
|
+
token = await heldToken2(deps, tokenId);
|
|
15362
|
+
assertBurnable(deps.engine, token, tokenId);
|
|
15363
|
+
} catch (err) {
|
|
15364
|
+
return refused(burnId, tokenId, err);
|
|
15365
|
+
}
|
|
15366
|
+
const entry = {
|
|
15367
|
+
burnId,
|
|
15368
|
+
tokenId,
|
|
15369
|
+
reasonHex: bytesToHex3(reasonBytes),
|
|
15370
|
+
burnedTokenHex: null,
|
|
15371
|
+
settled: false,
|
|
15372
|
+
assets: assetsOf(token),
|
|
15373
|
+
createdAt: deps.now()
|
|
15374
|
+
};
|
|
15375
|
+
await deps.burnJournal.upsert(entry);
|
|
15376
|
+
try {
|
|
15377
|
+
const settled = await burnJournaled(deps, entry, token);
|
|
15378
|
+
return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
|
|
15379
|
+
} catch (err) {
|
|
15380
|
+
const failure = failureOf(err);
|
|
15381
|
+
if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
|
|
15382
|
+
else deps.armHeartbeat();
|
|
15383
|
+
return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
|
|
15384
|
+
}
|
|
15385
|
+
}
|
|
15386
|
+
async function replayBurns(deps) {
|
|
15387
|
+
let progressed = 0;
|
|
15388
|
+
for (const entry of await deps.burnJournal.list()) {
|
|
15389
|
+
if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
|
|
15390
|
+
try {
|
|
15391
|
+
const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
|
|
15392
|
+
await burnJournaled(deps, entry, token);
|
|
15393
|
+
progressed += 1;
|
|
15394
|
+
} catch (err) {
|
|
15395
|
+
if (!isTerminal(err)) continue;
|
|
15396
|
+
await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
|
|
15397
|
+
progressed += 1;
|
|
15398
|
+
}
|
|
15399
|
+
}
|
|
15400
|
+
return progressed;
|
|
15401
|
+
}
|
|
15402
|
+
async function pendingBurns(deps) {
|
|
15403
|
+
return (await deps.burnJournal.list()).map((entry) => ({
|
|
15404
|
+
burnId: entry.burnId,
|
|
15405
|
+
tokenId: entry.tokenId,
|
|
15406
|
+
reasonBytes: hexToBytes4(entry.reasonHex),
|
|
15407
|
+
burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
|
|
15408
|
+
settled: entry.settled,
|
|
15409
|
+
createdAt: entry.createdAt,
|
|
15410
|
+
...entry.failure !== void 0 ? { failure: entry.failure } : {}
|
|
15411
|
+
}));
|
|
15412
|
+
}
|
|
15413
|
+
async function acknowledgeBurn(deps, burnId) {
|
|
15414
|
+
const entry = await deps.burnJournal.getByKey(burnId);
|
|
15415
|
+
if (entry === void 0) return;
|
|
15416
|
+
if (isLiveBurn(entry)) {
|
|
15417
|
+
throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
|
|
15418
|
+
}
|
|
15419
|
+
await deps.burnJournal.removeByKey(burnId);
|
|
15420
|
+
}
|
|
15421
|
+
function refused(burnId, tokenId, err) {
|
|
15422
|
+
const failure = failureOf(err);
|
|
15423
|
+
return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
|
|
15424
|
+
}
|
|
15425
|
+
function isTerminal(err) {
|
|
15426
|
+
return classifyError(err) === "conflict";
|
|
15427
|
+
}
|
|
15428
|
+
function failureOf(err) {
|
|
15429
|
+
const code = err !== null && typeof err === "object" ? err.code : void 0;
|
|
15430
|
+
return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
|
|
15431
|
+
}
|
|
15432
|
+
async function burnJournaled(deps, entry, token) {
|
|
15433
|
+
let current = entry;
|
|
15434
|
+
if (current.burnedTokenHex === null) {
|
|
15435
|
+
if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
|
|
15436
|
+
const burned = await deps.engine.burn(
|
|
15437
|
+
{ token, reasonBytes: hexToBytes4(current.reasonHex) },
|
|
15438
|
+
{ transferId: current.burnId }
|
|
15439
|
+
);
|
|
15440
|
+
if (burned.blob.tokenId !== current.tokenId) {
|
|
15441
|
+
throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
|
|
15442
|
+
}
|
|
15443
|
+
current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
|
|
15444
|
+
await deps.burnJournal.upsert(current);
|
|
14861
15445
|
}
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
15446
|
+
await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
|
|
15447
|
+
await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
|
|
15448
|
+
current = { ...current, settled: true };
|
|
15449
|
+
await deps.burnJournal.upsert(current);
|
|
15450
|
+
deps.refreshView();
|
|
15451
|
+
return current;
|
|
15452
|
+
}
|
|
15453
|
+
|
|
15454
|
+
// modules/payments-v2/burn-hold.ts
|
|
15455
|
+
var BurnHold = class {
|
|
15456
|
+
constructor(deps) {
|
|
15457
|
+
this.deps = deps;
|
|
14870
15458
|
}
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
let pinnedTotal = 0n;
|
|
14877
|
-
const holders = /* @__PURE__ */ new Set();
|
|
14878
|
-
const unproven = this.deps.ledger.unprovenReason();
|
|
14879
|
-
for (const entry of this.deps.getPool(coinId)) {
|
|
14880
|
-
if (entry.amount <= 0n) continue;
|
|
14881
|
-
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
14882
|
-
if (holder === void 0) {
|
|
14883
|
-
freeView.push(entry);
|
|
14884
|
-
freeTotal += entry.amount;
|
|
14885
|
-
} else {
|
|
14886
|
-
pinnedTotal += entry.amount;
|
|
14887
|
-
holders.add(holder);
|
|
14888
|
-
}
|
|
15459
|
+
async run(burnId, tokenId, burn) {
|
|
15460
|
+
try {
|
|
15461
|
+
this.deps.queue.planWhole(burnId, tokenId);
|
|
15462
|
+
} catch (err) {
|
|
15463
|
+
return refused(burnId, tokenId, err);
|
|
14889
15464
|
}
|
|
14890
|
-
|
|
14891
|
-
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
|
|
15465
|
+
this.deps.view.markInFlightMany([tokenId]);
|
|
15466
|
+
let burned = false;
|
|
15467
|
+
try {
|
|
15468
|
+
const result = await burn();
|
|
15469
|
+
burned = result.success;
|
|
15470
|
+
return result;
|
|
15471
|
+
} finally {
|
|
15472
|
+
await this.settle(burnId, tokenId, burned);
|
|
14896
15473
|
}
|
|
14897
|
-
return total;
|
|
14898
15474
|
}
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
throw new SphereError("Planned token missing from pool", "VALIDATION_ERROR");
|
|
14905
|
-
}
|
|
15475
|
+
async settle(burnId, tokenId, burned) {
|
|
15476
|
+
if (burned) {
|
|
15477
|
+
this.deps.ledger.commit(burnId);
|
|
15478
|
+
this.deps.track(this.refreshThenRelease(tokenId));
|
|
15479
|
+
return;
|
|
14906
15480
|
}
|
|
14907
|
-
this.deps.
|
|
15481
|
+
const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
|
|
15482
|
+
if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
|
|
15483
|
+
this.deps.ledger.cancel(burnId);
|
|
15484
|
+
this.deps.view.releaseMany([tokenId]);
|
|
15485
|
+
this.deps.queue.notifyChange("");
|
|
15486
|
+
return;
|
|
15487
|
+
}
|
|
15488
|
+
this.deps.pins.adopt(burnId);
|
|
14908
15489
|
}
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
15490
|
+
async refreshThenRelease(tokenId) {
|
|
15491
|
+
try {
|
|
15492
|
+
await this.deps.view.delta();
|
|
15493
|
+
this.deps.view.releaseMany([tokenId]);
|
|
15494
|
+
} finally {
|
|
15495
|
+
this.deps.queue.notifyChange("");
|
|
15496
|
+
}
|
|
14913
15497
|
}
|
|
14914
15498
|
};
|
|
14915
|
-
function parseAmount(raw) {
|
|
14916
|
-
if (!/^\d+$/.test(raw)) {
|
|
14917
|
-
throw new SphereError("Invalid send amount", "VALIDATION_ERROR");
|
|
14918
|
-
}
|
|
14919
|
-
const amount = BigInt(raw);
|
|
14920
|
-
if (amount <= 0n) {
|
|
14921
|
-
throw new SphereError("Send amount must be positive", "VALIDATION_ERROR");
|
|
14922
|
-
}
|
|
14923
|
-
return amount;
|
|
14924
|
-
}
|
|
14925
15499
|
|
|
14926
15500
|
// modules/payments-v2/compose.ts
|
|
14927
15501
|
function supportsDeterministicMint(engine) {
|
|
@@ -14930,16 +15504,8 @@ function supportsDeterministicMint(engine) {
|
|
|
14930
15504
|
function composeFacadeParts(deps, hooks) {
|
|
14931
15505
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
14932
15506
|
const ledger = new ReservationLedger();
|
|
14933
|
-
const
|
|
14934
|
-
|
|
14935
|
-
kv: deps.kv,
|
|
14936
|
-
emit: (event) => deps.emit(event, {}),
|
|
14937
|
-
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
14938
|
-
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
14939
|
-
// must not call a token spendable that the queue is about to refuse to spend.
|
|
14940
|
-
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
14941
|
-
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
14942
|
-
});
|
|
15507
|
+
const verdicts = buildVerdicts(deps, hooks);
|
|
15508
|
+
const view = buildView(deps, hooks, ledger, verdicts);
|
|
14943
15509
|
const queue = new SpendQueue({
|
|
14944
15510
|
ledger,
|
|
14945
15511
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -14954,27 +15520,57 @@ function composeFacadeParts(deps, hooks) {
|
|
|
14954
15520
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
14955
15521
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
14956
15522
|
});
|
|
14957
|
-
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
15523
|
+
const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
|
|
14958
15524
|
const machineStores = createMachineStores(deps.kv);
|
|
14959
15525
|
const heldStates = /* @__PURE__ */ new Map();
|
|
14960
15526
|
const refreshView = coalesced(() => view.delta(), hooks.track);
|
|
15527
|
+
const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
|
|
14961
15528
|
return {
|
|
14962
15529
|
ownPubkeyBytes,
|
|
14963
15530
|
view,
|
|
15531
|
+
verdicts,
|
|
14964
15532
|
refreshView,
|
|
14965
15533
|
ledger,
|
|
14966
|
-
pins
|
|
15534
|
+
pins,
|
|
14967
15535
|
queue,
|
|
15536
|
+
burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
|
|
14968
15537
|
historyStore,
|
|
14969
15538
|
machineStores,
|
|
14970
15539
|
machineDeps,
|
|
14971
15540
|
machine: new TransferMachine(machineDeps),
|
|
14972
15541
|
heldStates,
|
|
14973
|
-
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
|
|
15542
|
+
receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
|
|
14974
15543
|
requests: buildRequests(deps, hooks),
|
|
14975
15544
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
14976
15545
|
};
|
|
14977
15546
|
}
|
|
15547
|
+
function buildView(deps, hooks, ledger, verdicts) {
|
|
15548
|
+
const view = new InventoryView({
|
|
15549
|
+
port: deps.storagePort,
|
|
15550
|
+
kv: deps.kv,
|
|
15551
|
+
trust: verdicts,
|
|
15552
|
+
emit: (event) => {
|
|
15553
|
+
deps.emit(event, {});
|
|
15554
|
+
hooks.track(verdicts.review(view.claimHolders()));
|
|
15555
|
+
},
|
|
15556
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
15557
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
15558
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
15559
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
15560
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
15561
|
+
});
|
|
15562
|
+
return view;
|
|
15563
|
+
}
|
|
15564
|
+
function buildVerdicts(deps, hooks) {
|
|
15565
|
+
return new TokenVerdicts({
|
|
15566
|
+
kv: deps.kv,
|
|
15567
|
+
claims: deps.claims ?? new CoinClaims(),
|
|
15568
|
+
engine: () => hooks.engine(),
|
|
15569
|
+
getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
|
|
15570
|
+
changed: () => deps.emit("inventory:updated", {}),
|
|
15571
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
15572
|
+
});
|
|
15573
|
+
}
|
|
14978
15574
|
function buildPins(deps, hooks, parts) {
|
|
14979
15575
|
return new IntentPins({
|
|
14980
15576
|
ledger: parts.ledger,
|
|
@@ -15002,8 +15598,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
|
15002
15598
|
}
|
|
15003
15599
|
};
|
|
15004
15600
|
}
|
|
15005
|
-
function buildMachineDeps(deps, hooks, historyStore) {
|
|
15601
|
+
function buildMachineDeps(deps, hooks, historyStore, verdicts) {
|
|
15006
15602
|
return {
|
|
15603
|
+
recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
|
|
15007
15604
|
engine: () => hooks.engine(),
|
|
15008
15605
|
storage: deps.storagePort,
|
|
15009
15606
|
delivery: deps.deliveryPort,
|
|
@@ -15036,10 +15633,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
15036
15633
|
}
|
|
15037
15634
|
};
|
|
15038
15635
|
}
|
|
15039
|
-
function buildReceive(deps, hooks,
|
|
15636
|
+
function buildReceive(deps, hooks, parts, refreshView) {
|
|
15637
|
+
const { historyStore, heldStates, verdicts } = parts;
|
|
15040
15638
|
return new Receive({
|
|
15041
15639
|
delivery: deps.deliveryPort,
|
|
15042
15640
|
engine: () => hooks.engine(),
|
|
15641
|
+
claims: deps.claims ?? new CoinClaims(),
|
|
15642
|
+
accepted: (token) => verdicts.accept(token),
|
|
15043
15643
|
view: {
|
|
15044
15644
|
heldState: (tokenId) => heldStates.get(tokenId) ?? null,
|
|
15045
15645
|
store: async (entry) => {
|
|
@@ -15089,40 +15689,6 @@ function buildRequests(deps, hooks) {
|
|
|
15089
15689
|
});
|
|
15090
15690
|
}
|
|
15091
15691
|
|
|
15092
|
-
// modules/payments-v2/machine/payload.ts
|
|
15093
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
15094
|
-
return {
|
|
15095
|
-
v: 2,
|
|
15096
|
-
kind: "coin",
|
|
15097
|
-
recipient: recipientPubkey,
|
|
15098
|
-
coinId: request.coinId,
|
|
15099
|
-
amount: request.amount,
|
|
15100
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
15101
|
-
direct: [...spend.plan.direct],
|
|
15102
|
-
...spend.plan.split !== void 0 ? {
|
|
15103
|
-
split: {
|
|
15104
|
-
tokenId: spend.plan.split.tokenId,
|
|
15105
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
15106
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
15107
|
-
}
|
|
15108
|
-
} : {},
|
|
15109
|
-
spentStates
|
|
15110
|
-
};
|
|
15111
|
-
}
|
|
15112
|
-
function messageOf(err) {
|
|
15113
|
-
return err instanceof Error ? err.message : String(err);
|
|
15114
|
-
}
|
|
15115
|
-
function buildWholePayload(recipientPubkey, request, spentStates) {
|
|
15116
|
-
return {
|
|
15117
|
-
v: 2,
|
|
15118
|
-
kind: "whole",
|
|
15119
|
-
recipient: recipientPubkey,
|
|
15120
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
15121
|
-
direct: [request.tokenId],
|
|
15122
|
-
spentStates
|
|
15123
|
-
};
|
|
15124
|
-
}
|
|
15125
|
-
|
|
15126
15692
|
// modules/payments-v2/mint-params.ts
|
|
15127
15693
|
function mintParams(recipientPubkey, coinId, amount) {
|
|
15128
15694
|
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
@@ -15192,6 +15758,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
|
|
|
15192
15758
|
}
|
|
15193
15759
|
return false;
|
|
15194
15760
|
}
|
|
15761
|
+
async function seedHeldStates(storagePort, heldStates) {
|
|
15762
|
+
let page = await storagePort.listInventory();
|
|
15763
|
+
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
15764
|
+
for (const item of page.items) {
|
|
15765
|
+
if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
|
|
15766
|
+
}
|
|
15767
|
+
if (!page.more) return;
|
|
15768
|
+
page = await storagePort.listInventory(page.cursor);
|
|
15769
|
+
}
|
|
15770
|
+
}
|
|
15195
15771
|
async function replayCoinMints(deps) {
|
|
15196
15772
|
let resolved = 0;
|
|
15197
15773
|
for (const entry of await deps.mintJournal.list()) {
|
|
@@ -15296,7 +15872,8 @@ async function mintJournaled(deps, entry) {
|
|
|
15296
15872
|
}
|
|
15297
15873
|
|
|
15298
15874
|
// modules/payments-v2/mint-custom.ts
|
|
15299
|
-
var
|
|
15875
|
+
var HEX322 = /^[0-9a-f]{64}$/;
|
|
15876
|
+
var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
15300
15877
|
function assertRequest(request) {
|
|
15301
15878
|
if (request.tokenType.length !== 32) {
|
|
15302
15879
|
throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
|
|
@@ -15307,19 +15884,27 @@ function assertRequest(request) {
|
|
|
15307
15884
|
if (request.data.length === 0) {
|
|
15308
15885
|
throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
|
|
15309
15886
|
}
|
|
15887
|
+
if (request.justification !== void 0 && request.justification.length === 0) {
|
|
15888
|
+
throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
|
|
15889
|
+
}
|
|
15890
|
+
if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
|
|
15891
|
+
throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
|
|
15892
|
+
}
|
|
15310
15893
|
for (const asset of request.assets) {
|
|
15311
|
-
if (!
|
|
15894
|
+
if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
|
|
15312
15895
|
throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
|
|
15313
15896
|
}
|
|
15314
15897
|
}
|
|
15315
15898
|
}
|
|
15316
15899
|
async function runCustomMintUnderJournal(deps, input) {
|
|
15900
|
+
let entry;
|
|
15317
15901
|
try {
|
|
15318
15902
|
assertRequest(input.request);
|
|
15903
|
+
entry = journalEntryOf2(input.mintId, input.request, deps.now());
|
|
15904
|
+
deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
|
|
15319
15905
|
} catch (err) {
|
|
15320
15906
|
return { success: false, error: messageOf(err) };
|
|
15321
15907
|
}
|
|
15322
|
-
const entry = journalEntryOf2(input.mintId, input.request, deps.now());
|
|
15323
15908
|
await deps.customMintJournal.upsert(entry);
|
|
15324
15909
|
let tokenId;
|
|
15325
15910
|
try {
|
|
@@ -15359,24 +15944,25 @@ function journalEntryOf2(mintId, request, createdAt) {
|
|
|
15359
15944
|
createdAt
|
|
15360
15945
|
};
|
|
15361
15946
|
}
|
|
15947
|
+
function mintParamsOf(deps, entry, mintJustificationVerifiers) {
|
|
15948
|
+
return {
|
|
15949
|
+
recipientPubkey: deps.ownPubkeyBytes,
|
|
15950
|
+
data: hexToBytes4(entry.dataHex),
|
|
15951
|
+
tokenType: hexToBytes4(entry.tokenTypeHex),
|
|
15952
|
+
salt: hexToBytes4(entry.saltHex),
|
|
15953
|
+
...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
|
|
15954
|
+
...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
|
|
15955
|
+
};
|
|
15956
|
+
}
|
|
15362
15957
|
async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
|
|
15363
|
-
const token = await deps.engine.mintDataToken(
|
|
15364
|
-
{
|
|
15365
|
-
recipientPubkey: deps.ownPubkeyBytes,
|
|
15366
|
-
data: hexToBytes4(entry.dataHex),
|
|
15367
|
-
tokenType: hexToBytes4(entry.tokenTypeHex),
|
|
15368
|
-
salt: hexToBytes4(entry.saltHex),
|
|
15369
|
-
...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
|
|
15370
|
-
...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
|
|
15371
|
-
},
|
|
15372
|
-
{ transferId: entry.mintId }
|
|
15373
|
-
);
|
|
15958
|
+
const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
|
|
15374
15959
|
if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
|
|
15375
15960
|
throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
|
|
15376
15961
|
}
|
|
15377
15962
|
if (entry.tokenId === "") {
|
|
15378
15963
|
await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
15379
15964
|
}
|
|
15965
|
+
if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
|
|
15380
15966
|
await finalizeMint(deps, {
|
|
15381
15967
|
mintId: entry.mintId,
|
|
15382
15968
|
token,
|
|
@@ -15386,108 +15972,6 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
|
|
|
15386
15972
|
return token.blob.tokenId;
|
|
15387
15973
|
}
|
|
15388
15974
|
|
|
15389
|
-
// modules/payments-v2/burn.ts
|
|
15390
|
-
var HEX322 = /^[0-9a-f]{64}$/;
|
|
15391
|
-
async function heldToken2(deps, tokenId) {
|
|
15392
|
-
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
15393
|
-
if (bytes === void 0) {
|
|
15394
|
-
throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
|
|
15395
|
-
}
|
|
15396
|
-
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
15397
|
-
}
|
|
15398
|
-
function assetsOf(token) {
|
|
15399
|
-
return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
|
|
15400
|
-
}
|
|
15401
|
-
async function runBurnUnderJournal(deps, input) {
|
|
15402
|
-
const { burnId } = input;
|
|
15403
|
-
const { tokenId, reasonBytes } = input.request;
|
|
15404
|
-
if (!HEX322.test(tokenId) || reasonBytes.length === 0) {
|
|
15405
|
-
return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty" };
|
|
15406
|
-
}
|
|
15407
|
-
let token;
|
|
15408
|
-
try {
|
|
15409
|
-
token = await heldToken2(deps, tokenId);
|
|
15410
|
-
if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
|
|
15411
|
-
throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
|
|
15412
|
-
}
|
|
15413
|
-
} catch (err) {
|
|
15414
|
-
return { success: false, burnId, tokenId, error: messageOf(err) };
|
|
15415
|
-
}
|
|
15416
|
-
const entry = {
|
|
15417
|
-
burnId,
|
|
15418
|
-
tokenId,
|
|
15419
|
-
reasonHex: bytesToHex3(reasonBytes),
|
|
15420
|
-
burnedTokenHex: null,
|
|
15421
|
-
settled: false,
|
|
15422
|
-
assets: assetsOf(token),
|
|
15423
|
-
createdAt: deps.now()
|
|
15424
|
-
};
|
|
15425
|
-
await deps.burnJournal.upsert(entry);
|
|
15426
|
-
try {
|
|
15427
|
-
const settled = await burnJournaled(deps, entry, token, { applied: false });
|
|
15428
|
-
return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
|
|
15429
|
-
} catch (err) {
|
|
15430
|
-
deps.armHeartbeat();
|
|
15431
|
-
return { success: false, burnId, tokenId, error: messageOf(err) };
|
|
15432
|
-
}
|
|
15433
|
-
}
|
|
15434
|
-
async function replayBurns(deps) {
|
|
15435
|
-
let progressed = 0;
|
|
15436
|
-
for (const entry of await deps.burnJournal.list()) {
|
|
15437
|
-
if (entry.settled || deps.isActiveOp(entry.burnId)) continue;
|
|
15438
|
-
try {
|
|
15439
|
-
const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
|
|
15440
|
-
const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
|
|
15441
|
-
await burnJournaled(deps, entry, token, { applied });
|
|
15442
|
-
progressed += 1;
|
|
15443
|
-
} catch {
|
|
15444
|
-
continue;
|
|
15445
|
-
}
|
|
15446
|
-
}
|
|
15447
|
-
return progressed;
|
|
15448
|
-
}
|
|
15449
|
-
async function pendingBurns(deps) {
|
|
15450
|
-
return (await deps.burnJournal.list()).map((entry) => ({
|
|
15451
|
-
burnId: entry.burnId,
|
|
15452
|
-
tokenId: entry.tokenId,
|
|
15453
|
-
reasonBytes: hexToBytes4(entry.reasonHex),
|
|
15454
|
-
burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
|
|
15455
|
-
settled: entry.settled,
|
|
15456
|
-
createdAt: entry.createdAt
|
|
15457
|
-
}));
|
|
15458
|
-
}
|
|
15459
|
-
async function acknowledgeBurn(deps, burnId) {
|
|
15460
|
-
const entry = await deps.burnJournal.getByKey(burnId);
|
|
15461
|
-
if (entry === void 0) return;
|
|
15462
|
-
if (!entry.settled) {
|
|
15463
|
-
throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
|
|
15464
|
-
}
|
|
15465
|
-
await deps.burnJournal.removeByKey(burnId);
|
|
15466
|
-
}
|
|
15467
|
-
async function burnJournaled(deps, entry, token, state) {
|
|
15468
|
-
let current = entry;
|
|
15469
|
-
if (current.burnedTokenHex === null) {
|
|
15470
|
-
if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
|
|
15471
|
-
const burned = await deps.engine.burn(
|
|
15472
|
-
{ token, reasonBytes: hexToBytes4(current.reasonHex) },
|
|
15473
|
-
{ transferId: current.burnId }
|
|
15474
|
-
);
|
|
15475
|
-
if (burned.blob.tokenId !== current.tokenId) {
|
|
15476
|
-
throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
|
|
15477
|
-
}
|
|
15478
|
-
current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
|
|
15479
|
-
await deps.burnJournal.upsert(current);
|
|
15480
|
-
}
|
|
15481
|
-
if (!state.applied) {
|
|
15482
|
-
await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [] });
|
|
15483
|
-
}
|
|
15484
|
-
await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
|
|
15485
|
-
current = { ...current, settled: true };
|
|
15486
|
-
await deps.burnJournal.upsert(current);
|
|
15487
|
-
deps.refreshView();
|
|
15488
|
-
return current;
|
|
15489
|
-
}
|
|
15490
|
-
|
|
15491
15975
|
// modules/payments-v2/send-whole.ts
|
|
15492
15976
|
async function materializeWholeSpend(deps, input) {
|
|
15493
15977
|
const { engine } = deps;
|
|
@@ -15683,6 +16167,7 @@ var PaymentsFacade = class {
|
|
|
15683
16167
|
track: (op) => this.trackTail(op)
|
|
15684
16168
|
});
|
|
15685
16169
|
this.view = parts.view;
|
|
16170
|
+
this.verdicts = parts.verdicts;
|
|
15686
16171
|
this.wakeRefresh = parts.refreshView;
|
|
15687
16172
|
this.ledger = parts.ledger;
|
|
15688
16173
|
this.queue = parts.queue;
|
|
@@ -15696,6 +16181,7 @@ var PaymentsFacade = class {
|
|
|
15696
16181
|
this.restoreDeps = parts.restoreDeps;
|
|
15697
16182
|
this.requests = parts.requests;
|
|
15698
16183
|
this.pins = parts.pins;
|
|
16184
|
+
this.burnHold = parts.burnHold;
|
|
15699
16185
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
15700
16186
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
15701
16187
|
now: () => this.nowMs(),
|
|
@@ -15719,6 +16205,7 @@ var PaymentsFacade = class {
|
|
|
15719
16205
|
wakeRefresh;
|
|
15720
16206
|
prewarmed = new PrewarmCache();
|
|
15721
16207
|
view;
|
|
16208
|
+
verdicts;
|
|
15722
16209
|
ledger;
|
|
15723
16210
|
queue;
|
|
15724
16211
|
machine;
|
|
@@ -15745,6 +16232,7 @@ var PaymentsFacade = class {
|
|
|
15745
16232
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
15746
16233
|
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
15747
16234
|
pins;
|
|
16235
|
+
burnHold;
|
|
15748
16236
|
/** Per facade, so per address: an NFT reading is never served to another wallet. */
|
|
15749
16237
|
nftCache = new NftCache();
|
|
15750
16238
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -15769,7 +16257,8 @@ var PaymentsFacade = class {
|
|
|
15769
16257
|
await this.deps.session.start();
|
|
15770
16258
|
await this.pins.sync().catch(() => void 0);
|
|
15771
16259
|
this.trackTail(this.runConvergencePass());
|
|
15772
|
-
this.trackTail(this.
|
|
16260
|
+
this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
|
|
16261
|
+
await this.verdicts.start().catch(() => void 0);
|
|
15773
16262
|
await this.view.fullPull().catch(() => void 0);
|
|
15774
16263
|
this.receiveLoop.start(this.deps.receivePollMs);
|
|
15775
16264
|
}
|
|
@@ -15778,6 +16267,7 @@ var PaymentsFacade = class {
|
|
|
15778
16267
|
this.started = false;
|
|
15779
16268
|
this.heartbeat.stop();
|
|
15780
16269
|
this.receiveLoop.stop();
|
|
16270
|
+
this.verdicts.stop();
|
|
15781
16271
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
15782
16272
|
this.unsubscribers = [];
|
|
15783
16273
|
await this.deps.session.stop();
|
|
@@ -15794,12 +16284,17 @@ var PaymentsFacade = class {
|
|
|
15794
16284
|
}
|
|
15795
16285
|
// ── reads ──────────────────────────────────────────────────────────────────
|
|
15796
16286
|
async assets(coinId) {
|
|
16287
|
+
return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
|
|
16288
|
+
}
|
|
16289
|
+
unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
|
|
16290
|
+
async heldAssets(coinId) {
|
|
15797
16291
|
const all = await this.view.assets(this.deps.registry, this.deps.price);
|
|
15798
16292
|
return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
|
|
15799
16293
|
}
|
|
15800
16294
|
tokens(filter) {
|
|
15801
|
-
return this.view.tokens(this.deps.registry, filter);
|
|
16295
|
+
return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
|
|
15802
16296
|
}
|
|
16297
|
+
unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
|
|
15803
16298
|
coinless() {
|
|
15804
16299
|
return this.view.coinless(this.deps.registry);
|
|
15805
16300
|
}
|
|
@@ -15860,9 +16355,9 @@ var PaymentsFacade = class {
|
|
|
15860
16355
|
return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
|
|
15861
16356
|
}
|
|
15862
16357
|
mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
|
|
15863
|
-
burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
|
|
15864
|
-
pendingBurns = () => this.track(pendingBurns(this.
|
|
15865
|
-
acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.
|
|
16358
|
+
burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
|
|
16359
|
+
pendingBurns = () => this.track(pendingBurns(this.machineStores));
|
|
16360
|
+
acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
|
|
15866
16361
|
// §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
|
|
15867
16362
|
async resumeNow() {
|
|
15868
16363
|
if (!this.started) return;
|
|
@@ -15905,7 +16400,8 @@ var PaymentsFacade = class {
|
|
|
15905
16400
|
isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
|
|
15906
16401
|
tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
|
|
15907
16402
|
emit: this.deps.emit,
|
|
15908
|
-
now: () => this.nowMs()
|
|
16403
|
+
now: () => this.nowMs(),
|
|
16404
|
+
accepted: (token) => this.verdicts.accept(token)
|
|
15909
16405
|
};
|
|
15910
16406
|
}
|
|
15911
16407
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
@@ -16243,16 +16739,6 @@ var PaymentsFacade = class {
|
|
|
16243
16739
|
newId() {
|
|
16244
16740
|
return (this.deps.newId ?? randomUUID)();
|
|
16245
16741
|
}
|
|
16246
|
-
async seedHeldStates() {
|
|
16247
|
-
let page = await this.deps.storagePort.listInventory();
|
|
16248
|
-
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
16249
|
-
for (const item of page.items) {
|
|
16250
|
-
if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
|
|
16251
|
-
}
|
|
16252
|
-
if (!page.more) return;
|
|
16253
|
-
page = await this.deps.storagePort.listInventory(page.cursor);
|
|
16254
|
-
}
|
|
16255
|
-
}
|
|
16256
16742
|
track(op) {
|
|
16257
16743
|
const settled = op.then(() => void 0, () => void 0);
|
|
16258
16744
|
this.pendingOps.add(settled);
|
|
@@ -17496,6 +17982,7 @@ function composePaymentsV2(spec) {
|
|
|
17496
17982
|
engineRef: spec.engineRef,
|
|
17497
17983
|
kv,
|
|
17498
17984
|
registry: host.registry,
|
|
17985
|
+
claims: CoinClaims.fromPlugins(spec.plugins),
|
|
17499
17986
|
...host.price !== null ? { price: host.price } : {},
|
|
17500
17987
|
emit: host.emit,
|
|
17501
17988
|
resolveRecipient: (identifier) => resolveRecipientInfo(identifier, network, host.resolvePeer),
|
|
@@ -20544,7 +21031,8 @@ var Sphere = class _Sphere {
|
|
|
20544
21031
|
// deliver the NEXT address's Unicity ID — a name the sending key does
|
|
20545
21032
|
// not own (sphere#487 review).
|
|
20546
21033
|
nametag: () => this.getNametagForAddress(facadeAddressId)
|
|
20547
|
-
}
|
|
21034
|
+
},
|
|
21035
|
+
...this._plugins ? { plugins: this._plugins } : {}
|
|
20548
21036
|
});
|
|
20549
21037
|
this._paymentsV2Active = { index, facade };
|
|
20550
21038
|
await facade.start();
|