@unicitylabs/sphere-sdk 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect/index.cjs +11 -5
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.d.cts +4 -1
- package/dist/connect/index.d.ts +4 -1
- package/dist/connect/index.js +11 -5
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +692 -268
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +69 -0
- package/dist/core/index.d.ts +69 -0
- package/dist/core/index.js +692 -268
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +11 -5
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.d.cts +3 -1
- package/dist/impl/browser/connect/index.d.ts +3 -1
- package/dist/impl/browser/connect/index.js +11 -5
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +10 -4
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.d.cts +1 -1
- package/dist/impl/nodejs/connect/index.d.ts +1 -1
- package/dist/impl/nodejs/connect/index.js +10 -4
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.d.cts +11 -0
- package/dist/impl/nodejs/index.d.ts +11 -0
- package/dist/impl/shared/wallet-api/index.d.cts +11 -0
- package/dist/impl/shared/wallet-api/index.d.ts +11 -0
- package/dist/impl/wallet-api-v2/index.cjs +2 -0
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.d.cts +13 -0
- package/dist/impl/wallet-api-v2/index.d.ts +13 -0
- package/dist/impl/wallet-api-v2/index.js +2 -0
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +692 -268
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -1
- package/dist/index.d.ts +63 -1
- package/dist/index.js +692 -268
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +767 -398
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +91 -14
- package/dist/modules/payments-v2/index.d.ts +91 -14
- package/dist/modules/payments-v2/index.js +767 -398
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/dist/token-engine/index.cjs +132 -34
- package/dist/token-engine/index.cjs.map +1 -1
- package/dist/token-engine/index.d.cts +19 -0
- package/dist/token-engine/index.d.ts +19 -0
- package/dist/token-engine/index.js +132 -34
- package/dist/token-engine/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.js
CHANGED
|
@@ -8127,6 +8127,8 @@ var FETCH_TIMEOUT_MS = 1e4;
|
|
|
8127
8127
|
var TokenRegistry = class _TokenRegistry {
|
|
8128
8128
|
static instance = null;
|
|
8129
8129
|
definitionsById;
|
|
8130
|
+
/** Non-fungible definitions keyed by TOKEN TYPE — a separate namespace (#147). */
|
|
8131
|
+
definitionsByType;
|
|
8130
8132
|
definitionsBySymbol;
|
|
8131
8133
|
definitionsByName;
|
|
8132
8134
|
// Remote refresh state
|
|
@@ -8145,6 +8147,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8145
8147
|
inFlight = null;
|
|
8146
8148
|
constructor() {
|
|
8147
8149
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
8150
|
+
this.definitionsByType = /* @__PURE__ */ new Map();
|
|
8148
8151
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
8149
8152
|
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
8150
8153
|
}
|
|
@@ -8372,9 +8375,11 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8372
8375
|
this.definitionsById.clear();
|
|
8373
8376
|
this.definitionsBySymbol.clear();
|
|
8374
8377
|
this.definitionsByName.clear();
|
|
8378
|
+
this.definitionsByType.clear();
|
|
8375
8379
|
for (const def of definitions) {
|
|
8376
8380
|
const idLower = def.id.toLowerCase();
|
|
8377
8381
|
this.definitionsById.set(idLower, def);
|
|
8382
|
+
if (def.assetKind === "non-fungible") this.definitionsByType.set(idLower, def);
|
|
8378
8383
|
if (def.symbol) {
|
|
8379
8384
|
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
8380
8385
|
}
|
|
@@ -8588,6 +8593,20 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8588
8593
|
getAllDefinitions() {
|
|
8589
8594
|
return Array.from(this.definitionsById.values());
|
|
8590
8595
|
}
|
|
8596
|
+
/**
|
|
8597
|
+
* Definition for a coinless token's genesis TOKEN TYPE (#777). Never falls back
|
|
8598
|
+
* to `getDefinition`: a type and a coin id live in different namespaces, so a
|
|
8599
|
+
* type that collided with a coin id would otherwise render as that coin.
|
|
8600
|
+
*/
|
|
8601
|
+
getTypeMeta(tokenType) {
|
|
8602
|
+
const def = this.getTypeDefinition(tokenType);
|
|
8603
|
+
if (!def) return null;
|
|
8604
|
+
return { name: def.name, iconUrl: def.icons?.[0]?.url ?? null };
|
|
8605
|
+
}
|
|
8606
|
+
getTypeDefinition(tokenType) {
|
|
8607
|
+
if (!tokenType) return void 0;
|
|
8608
|
+
return this.definitionsByType.get(tokenType.toLowerCase());
|
|
8609
|
+
}
|
|
8591
8610
|
/**
|
|
8592
8611
|
* Get all fungible token definitions
|
|
8593
8612
|
* @returns Array of fungible token definitions
|
|
@@ -9423,6 +9442,8 @@ import { DataHasherFactory } from "@unicitylabs/state-transition-sdk/lib/crypto/
|
|
|
9423
9442
|
import { CborSerializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js";
|
|
9424
9443
|
import { CborDeserializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js";
|
|
9425
9444
|
import { CborError } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js";
|
|
9445
|
+
import { CborReader } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js";
|
|
9446
|
+
import { MajorType } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js";
|
|
9426
9447
|
import { Asset } from "@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js";
|
|
9427
9448
|
import { AssetId } from "@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js";
|
|
9428
9449
|
import { PaymentAssetCollection } from "@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js";
|
|
@@ -9835,6 +9856,126 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
|
|
|
9835
9856
|
return burntToken;
|
|
9836
9857
|
}
|
|
9837
9858
|
|
|
9859
|
+
// token-engine/value-envelope.ts
|
|
9860
|
+
function isCoinlessEnvelope(envelope) {
|
|
9861
|
+
return envelope.startsWith("none_");
|
|
9862
|
+
}
|
|
9863
|
+
var SELF_DESCRIBED_CBOR_TAG = 55799n;
|
|
9864
|
+
var CBOR_MAJOR_TYPE_MASK = 224;
|
|
9865
|
+
var describe = (error) => error instanceof Error ? error.message : String(error);
|
|
9866
|
+
var invalid = (message) => new SphereError(message, "VALIDATION_ERROR");
|
|
9867
|
+
function majorTypeOf(bytes) {
|
|
9868
|
+
const first = bytes.at(0);
|
|
9869
|
+
return first === void 0 ? null : first & CBOR_MAJOR_TYPE_MASK;
|
|
9870
|
+
}
|
|
9871
|
+
function headTagNumber(bytes) {
|
|
9872
|
+
try {
|
|
9873
|
+
return new CborReader(bytes).readLength(MajorType.TAG);
|
|
9874
|
+
} catch {
|
|
9875
|
+
return null;
|
|
9876
|
+
}
|
|
9877
|
+
}
|
|
9878
|
+
function isAssetShaped(item) {
|
|
9879
|
+
try {
|
|
9880
|
+
return CborDeserializer.decodeArray(item, 2).every(
|
|
9881
|
+
(field) => majorTypeOf(field) === MajorType.BYTE_STRING
|
|
9882
|
+
);
|
|
9883
|
+
} catch {
|
|
9884
|
+
return false;
|
|
9885
|
+
}
|
|
9886
|
+
}
|
|
9887
|
+
function arrayItemsOrNull(bytes) {
|
|
9888
|
+
try {
|
|
9889
|
+
return CborDeserializer.decodeArray(bytes);
|
|
9890
|
+
} catch {
|
|
9891
|
+
return null;
|
|
9892
|
+
}
|
|
9893
|
+
}
|
|
9894
|
+
function decodeSphere(genesisData) {
|
|
9895
|
+
try {
|
|
9896
|
+
return SpherePaymentData.fromCBOR(genesisData).toValue();
|
|
9897
|
+
} catch (error) {
|
|
9898
|
+
throw invalid(`Failed to decode token payment data: ${describe(error)}`);
|
|
9899
|
+
}
|
|
9900
|
+
}
|
|
9901
|
+
function taggedItemBody(genesisData) {
|
|
9902
|
+
try {
|
|
9903
|
+
return CborDeserializer.decodeTag(genesisData).data;
|
|
9904
|
+
} catch (error) {
|
|
9905
|
+
throw invalid(
|
|
9906
|
+
`Token value payload is a CBOR tag whose item is malformed or carries trailing bytes: ${describe(error)}`
|
|
9907
|
+
);
|
|
9908
|
+
}
|
|
9909
|
+
}
|
|
9910
|
+
function selfDescribedBodyCarriesValue(body) {
|
|
9911
|
+
const major = majorTypeOf(body);
|
|
9912
|
+
if (major === MajorType.TAG) {
|
|
9913
|
+
const inner = headTagNumber(body);
|
|
9914
|
+
return inner === SpherePaymentData.CBOR_TAG || inner === SELF_DESCRIBED_CBOR_TAG;
|
|
9915
|
+
}
|
|
9916
|
+
if (major === MajorType.ARRAY) {
|
|
9917
|
+
const items = arrayItemsOrNull(body);
|
|
9918
|
+
return items !== null && items.some(isAssetShaped);
|
|
9919
|
+
}
|
|
9920
|
+
return false;
|
|
9921
|
+
}
|
|
9922
|
+
function classifyTagged(genesisData) {
|
|
9923
|
+
const tag = headTagNumber(genesisData);
|
|
9924
|
+
if (tag === null) {
|
|
9925
|
+
throw invalid(
|
|
9926
|
+
"Token value payload has an unreadable or non-canonically encoded CBOR tag head \u2014 it could be a malformed SpherePaymentData envelope"
|
|
9927
|
+
);
|
|
9928
|
+
}
|
|
9929
|
+
if (tag === SpherePaymentData.CBOR_TAG) {
|
|
9930
|
+
return { envelope: "sphere", value: decodeSphere(genesisData) };
|
|
9931
|
+
}
|
|
9932
|
+
const body = taggedItemBody(genesisData);
|
|
9933
|
+
if (tag === SELF_DESCRIBED_CBOR_TAG && selfDescribedBodyCarriesValue(body)) {
|
|
9934
|
+
throw invalid(
|
|
9935
|
+
"Token value payload is a value envelope wrapped in the self-described CBOR tag 55799 (RFC 8949 \xA73.4.6), which no codec here unwraps \u2014 its coins would be invisible. Emit the envelope without the self-describe prefix"
|
|
9936
|
+
);
|
|
9937
|
+
}
|
|
9938
|
+
return { envelope: "none_tag", value: null };
|
|
9939
|
+
}
|
|
9940
|
+
function classifyArray(genesisData) {
|
|
9941
|
+
const items = arrayItemsOrNull(genesisData);
|
|
9942
|
+
if (items === null) {
|
|
9943
|
+
throw invalid("Token value payload is not PaymentAssetCollection: malformed CBOR array");
|
|
9944
|
+
}
|
|
9945
|
+
return items.some(isAssetShaped) ? { envelope: "bare_collection", value: null } : { envelope: "none_other", value: null };
|
|
9946
|
+
}
|
|
9947
|
+
function classifyValueEnvelope(genesisData) {
|
|
9948
|
+
if (genesisData === null) return { envelope: "none_absent", value: null };
|
|
9949
|
+
const major = majorTypeOf(genesisData);
|
|
9950
|
+
if (major === null) return { envelope: "none_absent", value: null };
|
|
9951
|
+
if (major === MajorType.TAG) return classifyTagged(genesisData);
|
|
9952
|
+
if (major === MajorType.ARRAY) return classifyArray(genesisData);
|
|
9953
|
+
return { envelope: "none_other", value: null };
|
|
9954
|
+
}
|
|
9955
|
+
function wrapToken(sdkToken) {
|
|
9956
|
+
const { envelope, value } = classifyValueEnvelope(sdkToken.genesis.data);
|
|
9957
|
+
const blob = {
|
|
9958
|
+
tokenId: HexConverter.encode(sdkToken.id.bytes),
|
|
9959
|
+
token: sdkToken.toCBOR()
|
|
9960
|
+
};
|
|
9961
|
+
return {
|
|
9962
|
+
sdkToken,
|
|
9963
|
+
blob,
|
|
9964
|
+
value,
|
|
9965
|
+
valueEnvelope: envelope,
|
|
9966
|
+
tokenType: HexConverter.encode(sdkToken.type.bytes)
|
|
9967
|
+
};
|
|
9968
|
+
}
|
|
9969
|
+
function assertMintableData(data) {
|
|
9970
|
+
try {
|
|
9971
|
+
classifyValueEnvelope(data);
|
|
9972
|
+
} catch (error) {
|
|
9973
|
+
throw invalid(
|
|
9974
|
+
`Cannot mint a data token with this payload: ${describe(error)}. Raw bytes starting in the CBOR array (0x80-0x9f) or tag (0xc0-0xdf) range must be well-formed canonical CBOR; wrap them in a CBOR byte string, map, text string, or a tag other than 39050/55799.`
|
|
9975
|
+
);
|
|
9976
|
+
}
|
|
9977
|
+
}
|
|
9978
|
+
|
|
9838
9979
|
// token-engine/SphereTokenEngine.ts
|
|
9839
9980
|
var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
|
|
9840
9981
|
var MAX_MINT_CONCURRENCY = 8;
|
|
@@ -9891,7 +10032,7 @@ var SphereTokenEngine = class {
|
|
|
9891
10032
|
return sdkToken.latestTransaction.data;
|
|
9892
10033
|
}
|
|
9893
10034
|
const data = sdkToken.genesis.data;
|
|
9894
|
-
if (data &&
|
|
10035
|
+
if (data && classifyValueEnvelope(data).envelope === "sphere") {
|
|
9895
10036
|
return SpherePaymentData.fromCBOR(data).memo;
|
|
9896
10037
|
}
|
|
9897
10038
|
return null;
|
|
@@ -9921,9 +10062,10 @@ var SphereTokenEngine = class {
|
|
|
9921
10062
|
);
|
|
9922
10063
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
9923
10064
|
const token = await Token.mint(certified, this.deps.verificationContext);
|
|
9924
|
-
return
|
|
10065
|
+
return wrapToken(token);
|
|
9925
10066
|
}
|
|
9926
10067
|
async mintDataToken(params, options) {
|
|
10068
|
+
assertMintableData(params.data);
|
|
9927
10069
|
const recipient = SignaturePredicate.create(params.recipientPubkey);
|
|
9928
10070
|
const tokenType = params.tokenType ? new TokenType(params.tokenType) : TokenType.generate();
|
|
9929
10071
|
const salt = params.salt ? TokenSalt.fromBytes(params.salt) : TokenSalt.generate();
|
|
@@ -9938,7 +10080,7 @@ var SphereTokenEngine = class {
|
|
|
9938
10080
|
);
|
|
9939
10081
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
9940
10082
|
const token = await Token.mint(certified, this.deps.verificationContext);
|
|
9941
|
-
return
|
|
10083
|
+
return wrapToken(token);
|
|
9942
10084
|
}
|
|
9943
10085
|
async transfer(params, options) {
|
|
9944
10086
|
this.assertOwned(params.token);
|
|
@@ -9961,13 +10103,19 @@ var SphereTokenEngine = class {
|
|
|
9961
10103
|
);
|
|
9962
10104
|
const certified = await transferTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
9963
10105
|
const transferred = await params.token.sdkToken.transfer(certified, this.deps.verificationContext);
|
|
9964
|
-
return
|
|
10106
|
+
return wrapToken(transferred);
|
|
9965
10107
|
}
|
|
9966
10108
|
async split(params, options) {
|
|
9967
10109
|
this.assertOwned(params.token);
|
|
9968
10110
|
if (params.outputs.length === 0) {
|
|
9969
10111
|
throw new SphereError("Split requires at least one output", "VALIDATION_ERROR");
|
|
9970
10112
|
}
|
|
10113
|
+
if (params.token.value === null) {
|
|
10114
|
+
throw new SphereError(
|
|
10115
|
+
`Cannot split token ${params.token.blob.tokenId}: its genesis payload carries no value this SDK can read (envelope: ${params.token.valueEnvelope}). A coinless token can only be transferred whole.`,
|
|
10116
|
+
"VALIDATION_ERROR"
|
|
10117
|
+
);
|
|
10118
|
+
}
|
|
9971
10119
|
const transferId = this.resolveTransferId(options);
|
|
9972
10120
|
const requests = params.outputs.map(
|
|
9973
10121
|
(o, i) => SplitTokenRequest.create(
|
|
@@ -10089,7 +10237,7 @@ var SphereTokenEngine = class {
|
|
|
10089
10237
|
const proof = await this.submitSplitMintLeg(certData, mintTx, options);
|
|
10090
10238
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10091
10239
|
const token = await Token.mint(certified, this.deps.verificationContext);
|
|
10092
|
-
return
|
|
10240
|
+
return wrapToken(token);
|
|
10093
10241
|
}
|
|
10094
10242
|
/**
|
|
10095
10243
|
* Submit one split-mint leg. A `TRANSACTION_HASH_MISMATCH` on a mint leg is NOT a foreign spend
|
|
@@ -10147,7 +10295,7 @@ var SphereTokenEngine = class {
|
|
|
10147
10295
|
"VALIDATION_ERROR"
|
|
10148
10296
|
);
|
|
10149
10297
|
}
|
|
10150
|
-
return
|
|
10298
|
+
return wrapToken(sdkToken);
|
|
10151
10299
|
}
|
|
10152
10300
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
10153
10301
|
/**
|
|
@@ -10259,34 +10407,6 @@ var SphereTokenEngine = class {
|
|
|
10259
10407
|
throw new SphereError("Cannot transfer a token not owned by this engine identity", "VALIDATION_ERROR");
|
|
10260
10408
|
}
|
|
10261
10409
|
}
|
|
10262
|
-
/** Wrap an SDK token into a SphereToken: cache its blob (incl. stable tokenId) + decoded value. */
|
|
10263
|
-
wrapToken(sdkToken) {
|
|
10264
|
-
const data = sdkToken.genesis.data;
|
|
10265
|
-
let value = null;
|
|
10266
|
-
if (data && this.isSpherePaymentData(data)) {
|
|
10267
|
-
try {
|
|
10268
|
-
value = SpherePaymentData.fromCBOR(data).toValue();
|
|
10269
|
-
} catch (err) {
|
|
10270
|
-
throw new SphereError(
|
|
10271
|
-
`Failed to decode token payment data: ${err instanceof Error ? err.message : String(err)}`,
|
|
10272
|
-
"VALIDATION_ERROR"
|
|
10273
|
-
);
|
|
10274
|
-
}
|
|
10275
|
-
}
|
|
10276
|
-
const blob = {
|
|
10277
|
-
tokenId: HexConverter.encode(sdkToken.id.bytes),
|
|
10278
|
-
token: sdkToken.toCBOR()
|
|
10279
|
-
};
|
|
10280
|
-
return { sdkToken, blob, value };
|
|
10281
|
-
}
|
|
10282
|
-
/** True if the bytes are a SpherePaymentData envelope (value token) vs a raw data token. */
|
|
10283
|
-
isSpherePaymentData(data) {
|
|
10284
|
-
try {
|
|
10285
|
-
return CborDeserializer.decodeTag(data).tag === SpherePaymentData.CBOR_TAG;
|
|
10286
|
-
} catch {
|
|
10287
|
-
return false;
|
|
10288
|
-
}
|
|
10289
|
-
}
|
|
10290
10410
|
/**
|
|
10291
10411
|
* Terminate the verification worker pool, if this engine was configured with
|
|
10292
10412
|
* one. Idempotent — the pool's dispose only touches workers it spawned.
|
|
@@ -11682,6 +11802,17 @@ function createMachineStores(kv) {
|
|
|
11682
11802
|
};
|
|
11683
11803
|
}
|
|
11684
11804
|
|
|
11805
|
+
// modules/payments-v2/machine/payload-view.ts
|
|
11806
|
+
function isWholeIntent(p) {
|
|
11807
|
+
return p.kind === "whole" || p.kind === "coinless";
|
|
11808
|
+
}
|
|
11809
|
+
function splitOf(p) {
|
|
11810
|
+
return p.kind === "coin" ? p.split : void 0;
|
|
11811
|
+
}
|
|
11812
|
+
function coinIdOf(p) {
|
|
11813
|
+
return p.kind === "coin" ? p.coinId : void 0;
|
|
11814
|
+
}
|
|
11815
|
+
|
|
11685
11816
|
// modules/payments-v2/machine/TransferMachine.ts
|
|
11686
11817
|
var CERTIFY_WIDTH = 20;
|
|
11687
11818
|
var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
|
|
@@ -11884,13 +12015,14 @@ var TransferMachine = class {
|
|
|
11884
12015
|
const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
|
|
11885
12016
|
return { recipientBlob: finished.blob.token };
|
|
11886
12017
|
}
|
|
11887
|
-
const split = ctx.payload
|
|
12018
|
+
const split = splitOf(ctx.payload);
|
|
12019
|
+
const splitCoinId = coinIdOf(ctx.payload);
|
|
11888
12020
|
const { outputs } = await engine.split(
|
|
11889
12021
|
{
|
|
11890
12022
|
token: source,
|
|
11891
12023
|
outputs: [
|
|
11892
|
-
{ recipientPubkey, coinId:
|
|
11893
|
-
{ recipientPubkey: this.deps.ownPubkey, coinId:
|
|
12024
|
+
{ recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
|
|
12025
|
+
{ recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
|
|
11894
12026
|
]
|
|
11895
12027
|
},
|
|
11896
12028
|
opts
|
|
@@ -12012,7 +12144,9 @@ var TransferMachine = class {
|
|
|
12012
12144
|
const entry = {
|
|
12013
12145
|
transferId: r.transferId,
|
|
12014
12146
|
remainingAmount: undelivered.toString(),
|
|
12015
|
-
|
|
12147
|
+
// '' only on a token-addressed intent, which is one leg: it either lands or
|
|
12148
|
+
// conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
|
|
12149
|
+
coinId: coinIdOf(r.payload) ?? "",
|
|
12016
12150
|
recipient: r.payload.recipient,
|
|
12017
12151
|
committedTokenIds: committed.map((o) => o.op.sourceTokenId),
|
|
12018
12152
|
createdAt: this.deps.now()
|
|
@@ -12038,8 +12172,16 @@ var TransferMachine = class {
|
|
|
12038
12172
|
}
|
|
12039
12173
|
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
12040
12174
|
try {
|
|
12041
|
-
const
|
|
12042
|
-
await this.
|
|
12175
|
+
const coinId = coinIdOf(payload);
|
|
12176
|
+
const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
|
|
12177
|
+
const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
|
|
12178
|
+
await this.deps.recordHistory?.({
|
|
12179
|
+
transferId,
|
|
12180
|
+
payload,
|
|
12181
|
+
phase,
|
|
12182
|
+
committedAmount,
|
|
12183
|
+
...committedAssets !== void 0 ? { committedAssets } : {}
|
|
12184
|
+
});
|
|
12043
12185
|
} catch {
|
|
12044
12186
|
}
|
|
12045
12187
|
try {
|
|
@@ -12053,6 +12195,18 @@ var TransferMachine = class {
|
|
|
12053
12195
|
* a journal-rehydrated resume leg has no source token, but every committed
|
|
12054
12196
|
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
12055
12197
|
*/
|
|
12198
|
+
/** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
|
|
12199
|
+
async settledAssets(engine, committed) {
|
|
12200
|
+
const total = /* @__PURE__ */ new Map();
|
|
12201
|
+
for (const outcome of committed) {
|
|
12202
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
12203
|
+
const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
|
|
12204
|
+
for (const asset of engine.readValue(token)?.assets ?? []) {
|
|
12205
|
+
total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
|
|
12206
|
+
}
|
|
12207
|
+
}
|
|
12208
|
+
return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
|
|
12209
|
+
}
|
|
12056
12210
|
async settledAmount(engine, coinId, committed) {
|
|
12057
12211
|
let total = 0n;
|
|
12058
12212
|
for (const outcome of committed) {
|
|
@@ -12081,8 +12235,11 @@ var TransferMachine = class {
|
|
|
12081
12235
|
}
|
|
12082
12236
|
};
|
|
12083
12237
|
function conflictAmount(engine, payload, op, source) {
|
|
12084
|
-
|
|
12085
|
-
|
|
12238
|
+
const split = splitOf(payload);
|
|
12239
|
+
if (op.kind === "split") return BigInt(split.splitAmount);
|
|
12240
|
+
const coinId = coinIdOf(payload);
|
|
12241
|
+
if (coinId === void 0) return 0n;
|
|
12242
|
+
return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
|
|
12086
12243
|
}
|
|
12087
12244
|
|
|
12088
12245
|
// modules/payments-v2/machine/resume.ts
|
|
@@ -12189,6 +12346,14 @@ async function runOne(ctx, job, report) {
|
|
|
12189
12346
|
report.failed.push(job.transferId);
|
|
12190
12347
|
return;
|
|
12191
12348
|
}
|
|
12349
|
+
if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
|
|
12350
|
+
logger.warn(
|
|
12351
|
+
"PaymentsV2",
|
|
12352
|
+
`resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
|
|
12353
|
+
);
|
|
12354
|
+
report.failed.push(job.transferId);
|
|
12355
|
+
return;
|
|
12356
|
+
}
|
|
12192
12357
|
mine.set(op.sourceTokenId, token);
|
|
12193
12358
|
}
|
|
12194
12359
|
try {
|
|
@@ -12219,6 +12384,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
|
|
|
12219
12384
|
}
|
|
12220
12385
|
report.failed.push(transferId);
|
|
12221
12386
|
}
|
|
12387
|
+
function validateCoinPayload(c) {
|
|
12388
|
+
if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
|
|
12389
|
+
throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
|
|
12390
|
+
}
|
|
12391
|
+
const s = c.split;
|
|
12392
|
+
if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
|
|
12393
|
+
throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
|
|
12394
|
+
}
|
|
12395
|
+
return { ...c, kind: "coin" };
|
|
12396
|
+
}
|
|
12397
|
+
function validateWholePayload(p) {
|
|
12398
|
+
if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
|
|
12399
|
+
throw new SphereError(
|
|
12400
|
+
"token intent payload must name exactly one source token",
|
|
12401
|
+
"VALIDATION_ERROR"
|
|
12402
|
+
);
|
|
12403
|
+
}
|
|
12404
|
+
if (p.split !== void 0) {
|
|
12405
|
+
throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
|
|
12406
|
+
}
|
|
12407
|
+
return { ...p, kind: "whole", direct: [p.direct[0]] };
|
|
12408
|
+
}
|
|
12222
12409
|
function validatePayload(raw) {
|
|
12223
12410
|
const p = raw !== null && typeof raw === "object" ? raw : null;
|
|
12224
12411
|
if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
|
|
@@ -12227,14 +12414,17 @@ function validatePayload(raw) {
|
|
|
12227
12414
|
"VALIDATION_ERROR"
|
|
12228
12415
|
);
|
|
12229
12416
|
}
|
|
12230
|
-
if (typeof p.recipient !== "string"
|
|
12231
|
-
throw new SphereError("intent payload is missing recipient
|
|
12417
|
+
if (typeof p.recipient !== "string") {
|
|
12418
|
+
throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
|
|
12232
12419
|
}
|
|
12233
|
-
const
|
|
12234
|
-
if (
|
|
12235
|
-
throw new SphereError(
|
|
12420
|
+
const kind = p.kind ?? "coin";
|
|
12421
|
+
if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
|
|
12422
|
+
throw new SphereError(
|
|
12423
|
+
`unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
|
|
12424
|
+
"VALIDATION_ERROR"
|
|
12425
|
+
);
|
|
12236
12426
|
}
|
|
12237
|
-
return p;
|
|
12427
|
+
return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
|
|
12238
12428
|
}
|
|
12239
12429
|
|
|
12240
12430
|
// modules/payments-v2/convergence.ts
|
|
@@ -12440,13 +12630,24 @@ async function openRow(entry, journal, decryptPayload) {
|
|
|
12440
12630
|
transferId: entry.transferId,
|
|
12441
12631
|
kind: "open",
|
|
12442
12632
|
recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
|
|
12443
|
-
|
|
12444
|
-
amount: typeof payload?.amount === "string" ? payload.amount : "",
|
|
12633
|
+
...subject(payload),
|
|
12445
12634
|
legs: { certified, total: Math.max(total, certified) },
|
|
12446
12635
|
deliveryPending: journal.length > 0,
|
|
12447
12636
|
createdAt: entry.createdAt
|
|
12448
12637
|
};
|
|
12449
12638
|
}
|
|
12639
|
+
function subject(payload) {
|
|
12640
|
+
const rawKind = payload?.kind;
|
|
12641
|
+
if (rawKind === "whole" || rawKind === "coinless") {
|
|
12642
|
+
const tokenId = payload?.direct?.[0];
|
|
12643
|
+
return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
|
|
12644
|
+
}
|
|
12645
|
+
const coin = payload;
|
|
12646
|
+
return {
|
|
12647
|
+
coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
|
|
12648
|
+
amount: typeof coin?.amount === "string" ? coin.amount : ""
|
|
12649
|
+
};
|
|
12650
|
+
}
|
|
12450
12651
|
function journalOnlyRow(transferId, entries) {
|
|
12451
12652
|
const certified = distinctOpCount(entries);
|
|
12452
12653
|
return {
|
|
@@ -12476,6 +12677,263 @@ function distinctOpCount(entries) {
|
|
|
12476
12677
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
12477
12678
|
}
|
|
12478
12679
|
|
|
12680
|
+
// modules/payments-v2/inventory/token-data.ts
|
|
12681
|
+
async function readTokenData(deps, tokenId) {
|
|
12682
|
+
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
12683
|
+
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
12684
|
+
}
|
|
12685
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
12686
|
+
if (bytes === void 0) {
|
|
12687
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
12688
|
+
}
|
|
12689
|
+
return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
|
|
12690
|
+
}
|
|
12691
|
+
|
|
12692
|
+
// modules/payments-v2/machine/payload.ts
|
|
12693
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12694
|
+
return {
|
|
12695
|
+
v: 2,
|
|
12696
|
+
kind: "coin",
|
|
12697
|
+
recipient: recipientPubkey,
|
|
12698
|
+
coinId: request.coinId,
|
|
12699
|
+
amount: request.amount,
|
|
12700
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12701
|
+
direct: [...spend.plan.direct],
|
|
12702
|
+
...spend.plan.split !== void 0 ? {
|
|
12703
|
+
split: {
|
|
12704
|
+
tokenId: spend.plan.split.tokenId,
|
|
12705
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12706
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12707
|
+
}
|
|
12708
|
+
} : {},
|
|
12709
|
+
spentStates
|
|
12710
|
+
};
|
|
12711
|
+
}
|
|
12712
|
+
function messageOf(err) {
|
|
12713
|
+
return err instanceof Error ? err.message : String(err);
|
|
12714
|
+
}
|
|
12715
|
+
function buildWholePayload(recipientPubkey, request, spentStates) {
|
|
12716
|
+
return {
|
|
12717
|
+
v: 2,
|
|
12718
|
+
kind: "whole",
|
|
12719
|
+
recipient: recipientPubkey,
|
|
12720
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12721
|
+
direct: [request.tokenId],
|
|
12722
|
+
spentStates
|
|
12723
|
+
};
|
|
12724
|
+
}
|
|
12725
|
+
|
|
12726
|
+
// modules/payments-v2/mint-params.ts
|
|
12727
|
+
function mintParams(recipientPubkey, coinId, amount) {
|
|
12728
|
+
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
12729
|
+
}
|
|
12730
|
+
|
|
12731
|
+
// modules/payments-v2/mint.ts
|
|
12732
|
+
function runMintUnderJournal(deps, input) {
|
|
12733
|
+
return mintUnderJournal(deps, input);
|
|
12734
|
+
}
|
|
12735
|
+
async function mintUnderJournal(deps, input) {
|
|
12736
|
+
const { mintId, coinId, amount } = input;
|
|
12737
|
+
const engine = deps.engine;
|
|
12738
|
+
const entry = {
|
|
12739
|
+
mintId,
|
|
12740
|
+
coinId,
|
|
12741
|
+
amount: amount.toString(),
|
|
12742
|
+
tokenId: "",
|
|
12743
|
+
createdAt: deps.now()
|
|
12744
|
+
};
|
|
12745
|
+
await deps.mintJournal.upsert(entry);
|
|
12746
|
+
let token;
|
|
12747
|
+
try {
|
|
12748
|
+
token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
|
|
12749
|
+
} catch (err) {
|
|
12750
|
+
deps.armHeartbeat();
|
|
12751
|
+
return { success: false, error: messageOf(err) };
|
|
12752
|
+
}
|
|
12753
|
+
if (token.blob.tokenId !== entry.tokenId) {
|
|
12754
|
+
await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
12755
|
+
}
|
|
12756
|
+
try {
|
|
12757
|
+
await finalizeMint(deps, mintId, token, coinId, amount.toString());
|
|
12758
|
+
} catch (err) {
|
|
12759
|
+
deps.armHeartbeat();
|
|
12760
|
+
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
12761
|
+
}
|
|
12762
|
+
return { success: true, tokenId: token.blob.tokenId };
|
|
12763
|
+
}
|
|
12764
|
+
async function finalizeMint(deps, mintId, token, coinId, amount) {
|
|
12765
|
+
const engine = deps.engine;
|
|
12766
|
+
const bytes = token.blob.token;
|
|
12767
|
+
const digest = bytesToHex3(sha256(bytes));
|
|
12768
|
+
const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
|
|
12769
|
+
const key = keys.get(digest);
|
|
12770
|
+
if (key === void 0) {
|
|
12771
|
+
throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
|
|
12772
|
+
}
|
|
12773
|
+
await deps.storagePort.applyDelta({
|
|
12774
|
+
transferId: mintId,
|
|
12775
|
+
spent: [],
|
|
12776
|
+
added: [{ tokenId: token.blob.tokenId, key }]
|
|
12777
|
+
});
|
|
12778
|
+
await deps.recordMint({
|
|
12779
|
+
tokenId: token.blob.tokenId,
|
|
12780
|
+
assets: [{ coinId, amount }]
|
|
12781
|
+
});
|
|
12782
|
+
await deps.mintJournal.removeByKey(mintId);
|
|
12783
|
+
deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
12784
|
+
deps.refreshView();
|
|
12785
|
+
}
|
|
12786
|
+
|
|
12787
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
12788
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12789
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12790
|
+
return {
|
|
12791
|
+
id: tokenId,
|
|
12792
|
+
coinId: asset.coinId,
|
|
12793
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
12794
|
+
name: registry.getName(asset.coinId),
|
|
12795
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
12796
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12797
|
+
amount: asset.amount,
|
|
12798
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
12799
|
+
createdAt: snapshot.createdAt,
|
|
12800
|
+
updatedAt: snapshot.updatedAt,
|
|
12801
|
+
lazy: true,
|
|
12802
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12803
|
+
};
|
|
12804
|
+
}
|
|
12805
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
12806
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12807
|
+
for (const [tokenId, entry] of entries) {
|
|
12808
|
+
const moving = isTransferring(tokenId);
|
|
12809
|
+
for (const asset of entry.assets) {
|
|
12810
|
+
let group = groups.get(asset.coinId);
|
|
12811
|
+
if (!group) {
|
|
12812
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12813
|
+
groups.set(asset.coinId, group);
|
|
12814
|
+
}
|
|
12815
|
+
if (moving) {
|
|
12816
|
+
group.transferring += BigInt(asset.amount);
|
|
12817
|
+
group.transferringCount += 1;
|
|
12818
|
+
} else {
|
|
12819
|
+
group.total += BigInt(asset.amount);
|
|
12820
|
+
group.count += 1;
|
|
12821
|
+
}
|
|
12822
|
+
}
|
|
12823
|
+
}
|
|
12824
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12825
|
+
}
|
|
12826
|
+
function toAsset(coinId, totals, registry) {
|
|
12827
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
12828
|
+
return {
|
|
12829
|
+
coinId,
|
|
12830
|
+
symbol: registry.getSymbol(coinId),
|
|
12831
|
+
name: registry.getName(coinId),
|
|
12832
|
+
decimals: registry.getDecimals(coinId),
|
|
12833
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12834
|
+
totalAmount: totals.total.toString(),
|
|
12835
|
+
tokenCount: totals.count,
|
|
12836
|
+
confirmedAmount: totals.total.toString(),
|
|
12837
|
+
unconfirmedAmount: "0",
|
|
12838
|
+
confirmedTokenCount: totals.count,
|
|
12839
|
+
unconfirmedTokenCount: 0,
|
|
12840
|
+
transferringTokenCount: totals.transferringCount,
|
|
12841
|
+
transferringAmount: totals.transferring.toString(),
|
|
12842
|
+
priceUsd: null,
|
|
12843
|
+
priceEur: null,
|
|
12844
|
+
change24h: null,
|
|
12845
|
+
fiatValueUsd: null,
|
|
12846
|
+
fiatValueEur: null
|
|
12847
|
+
};
|
|
12848
|
+
}
|
|
12849
|
+
function priceIdOf(registry, coinId) {
|
|
12850
|
+
return registry.getName(coinId).toLowerCase();
|
|
12851
|
+
}
|
|
12852
|
+
async function withPrices(raw, registry, price) {
|
|
12853
|
+
try {
|
|
12854
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12855
|
+
const quotes = await price.getPrices(ids);
|
|
12856
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12857
|
+
} catch {
|
|
12858
|
+
return raw;
|
|
12859
|
+
}
|
|
12860
|
+
}
|
|
12861
|
+
function priceOne(asset, quote) {
|
|
12862
|
+
if (!quote) return asset;
|
|
12863
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12864
|
+
return {
|
|
12865
|
+
...asset,
|
|
12866
|
+
priceUsd: quote.priceUsd,
|
|
12867
|
+
priceEur: quote.priceEur ?? null,
|
|
12868
|
+
change24h: quote.change24h ?? null,
|
|
12869
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
12870
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12871
|
+
};
|
|
12872
|
+
}
|
|
12873
|
+
function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
12874
|
+
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
12875
|
+
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
12876
|
+
transferring: true,
|
|
12877
|
+
suspectedSpent: false
|
|
12878
|
+
});
|
|
12879
|
+
}
|
|
12880
|
+
|
|
12881
|
+
// modules/payments-v2/send-whole.ts
|
|
12882
|
+
async function materializeWholeSpend(deps, input) {
|
|
12883
|
+
const { engine } = deps;
|
|
12884
|
+
const tokenId = input.request.tokenId;
|
|
12885
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
12886
|
+
if (bytes === void 0) {
|
|
12887
|
+
throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
12888
|
+
}
|
|
12889
|
+
const token = await engine.decodeToken({ tokenId, token: bytes });
|
|
12890
|
+
if (token.valueEnvelope === "bare_collection") {
|
|
12891
|
+
throw new SphereError(
|
|
12892
|
+
`Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
|
|
12893
|
+
"VALIDATION_ERROR"
|
|
12894
|
+
);
|
|
12895
|
+
}
|
|
12896
|
+
if (input.requireCoinless && token.value !== null) {
|
|
12897
|
+
throw new SphereError(
|
|
12898
|
+
`Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
|
|
12899
|
+
"VALIDATION_ERROR"
|
|
12900
|
+
);
|
|
12901
|
+
}
|
|
12902
|
+
const keys = await engine.deliveryKeys(bytes);
|
|
12903
|
+
const payload = buildWholePayload(input.recipientPubkey, input.request, {
|
|
12904
|
+
[tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
|
|
12905
|
+
});
|
|
12906
|
+
const plan = {
|
|
12907
|
+
transferId: input.transferId,
|
|
12908
|
+
recipientPubkey: input.recipientPubkey,
|
|
12909
|
+
payload,
|
|
12910
|
+
ops: buildOps(payload),
|
|
12911
|
+
sources: /* @__PURE__ */ new Map([[tokenId, token]]),
|
|
12912
|
+
...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
|
|
12913
|
+
};
|
|
12914
|
+
const sourceTokens = (token.value?.assets ?? []).map(
|
|
12915
|
+
(asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
|
|
12916
|
+
);
|
|
12917
|
+
return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
|
|
12918
|
+
}
|
|
12919
|
+
|
|
12920
|
+
// modules/payments-v2/send-errors.ts
|
|
12921
|
+
function partialize(err, run) {
|
|
12922
|
+
if (run.delivered.length === 0) return err;
|
|
12923
|
+
return new PartialSendConflictError(
|
|
12924
|
+
"Part of your payment was sent; the remaining amount could not be completed (see cause). The delivered portion is final \u2014 re-plan only the shortfall, never the full amount.",
|
|
12925
|
+
run.firstPartialId ?? "",
|
|
12926
|
+
[...run.delivered],
|
|
12927
|
+
run.amount,
|
|
12928
|
+
err
|
|
12929
|
+
);
|
|
12930
|
+
}
|
|
12931
|
+
function stampTransferId(err, transferId) {
|
|
12932
|
+
if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
|
|
12933
|
+
err.transferId = transferId;
|
|
12934
|
+
}
|
|
12935
|
+
}
|
|
12936
|
+
|
|
12479
12937
|
// modules/payments-v2/recipient.ts
|
|
12480
12938
|
var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
|
|
12481
12939
|
async function requireSameNetworkRecipient(deps, identifier) {
|
|
@@ -12524,11 +12982,6 @@ async function splitOpIndex(deps, envelope) {
|
|
|
12524
12982
|
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
12525
12983
|
}
|
|
12526
12984
|
|
|
12527
|
-
// modules/payments-v2/mint-params.ts
|
|
12528
|
-
function mintParams(recipientPubkey, coinId, amount) {
|
|
12529
|
-
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
12530
|
-
}
|
|
12531
|
-
|
|
12532
12985
|
// modules/payments-v2/prewarm-cache.ts
|
|
12533
12986
|
var PrewarmCache = class _PrewarmCache {
|
|
12534
12987
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -12608,124 +13061,15 @@ async function takeSourceBlobs(deps, sourceIds) {
|
|
|
12608
13061
|
return blobs;
|
|
12609
13062
|
}
|
|
12610
13063
|
|
|
12611
|
-
// modules/payments-v2/
|
|
12612
|
-
function
|
|
12613
|
-
|
|
12614
|
-
return {
|
|
12615
|
-
id: tokenId,
|
|
12616
|
-
coinId: asset.coinId,
|
|
12617
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
12618
|
-
name: registry.getName(asset.coinId),
|
|
12619
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
12620
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12621
|
-
amount: asset.amount,
|
|
12622
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
12623
|
-
createdAt: snapshot.createdAt,
|
|
12624
|
-
updatedAt: snapshot.updatedAt,
|
|
12625
|
-
lazy: true,
|
|
12626
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12627
|
-
};
|
|
12628
|
-
}
|
|
12629
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
12630
|
-
const groups = /* @__PURE__ */ new Map();
|
|
12631
|
-
for (const [tokenId, entry] of entries) {
|
|
12632
|
-
const moving = isTransferring(tokenId);
|
|
12633
|
-
for (const asset of entry.assets) {
|
|
12634
|
-
let group = groups.get(asset.coinId);
|
|
12635
|
-
if (!group) {
|
|
12636
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12637
|
-
groups.set(asset.coinId, group);
|
|
12638
|
-
}
|
|
12639
|
-
if (moving) {
|
|
12640
|
-
group.transferring += BigInt(asset.amount);
|
|
12641
|
-
group.transferringCount += 1;
|
|
12642
|
-
} else {
|
|
12643
|
-
group.total += BigInt(asset.amount);
|
|
12644
|
-
group.count += 1;
|
|
12645
|
-
}
|
|
12646
|
-
}
|
|
12647
|
-
}
|
|
12648
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12649
|
-
}
|
|
12650
|
-
function toAsset(coinId, totals, registry) {
|
|
12651
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
12652
|
-
return {
|
|
12653
|
-
coinId,
|
|
12654
|
-
symbol: registry.getSymbol(coinId),
|
|
12655
|
-
name: registry.getName(coinId),
|
|
12656
|
-
decimals: registry.getDecimals(coinId),
|
|
12657
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12658
|
-
totalAmount: totals.total.toString(),
|
|
12659
|
-
tokenCount: totals.count,
|
|
12660
|
-
confirmedAmount: totals.total.toString(),
|
|
12661
|
-
unconfirmedAmount: "0",
|
|
12662
|
-
confirmedTokenCount: totals.count,
|
|
12663
|
-
unconfirmedTokenCount: 0,
|
|
12664
|
-
transferringTokenCount: totals.transferringCount,
|
|
12665
|
-
transferringAmount: totals.transferring.toString(),
|
|
12666
|
-
priceUsd: null,
|
|
12667
|
-
priceEur: null,
|
|
12668
|
-
change24h: null,
|
|
12669
|
-
fiatValueUsd: null,
|
|
12670
|
-
fiatValueEur: null
|
|
12671
|
-
};
|
|
12672
|
-
}
|
|
12673
|
-
function priceIdOf(registry, coinId) {
|
|
12674
|
-
return registry.getName(coinId).toLowerCase();
|
|
12675
|
-
}
|
|
12676
|
-
async function withPrices(raw, registry, price) {
|
|
12677
|
-
try {
|
|
12678
|
-
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12679
|
-
const quotes = await price.getPrices(ids);
|
|
12680
|
-
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12681
|
-
} catch {
|
|
12682
|
-
return raw;
|
|
12683
|
-
}
|
|
12684
|
-
}
|
|
12685
|
-
function priceOne(asset, quote) {
|
|
12686
|
-
if (!quote) return asset;
|
|
12687
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12688
|
-
return {
|
|
12689
|
-
...asset,
|
|
12690
|
-
priceUsd: quote.priceUsd,
|
|
12691
|
-
priceEur: quote.priceEur ?? null,
|
|
12692
|
-
change24h: quote.change24h ?? null,
|
|
12693
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
12694
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12695
|
-
};
|
|
12696
|
-
}
|
|
12697
|
-
function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
12698
|
-
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
12699
|
-
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
12700
|
-
transferring: true,
|
|
12701
|
-
suspectedSpent: false
|
|
12702
|
-
});
|
|
12703
|
-
}
|
|
12704
|
-
|
|
12705
|
-
// modules/payments-v2/machine/payload.ts
|
|
12706
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12707
|
-
return {
|
|
12708
|
-
v: 2,
|
|
12709
|
-
recipient: recipientPubkey,
|
|
12710
|
-
coinId: request.coinId,
|
|
12711
|
-
amount: request.amount,
|
|
12712
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12713
|
-
direct: [...spend.plan.direct],
|
|
12714
|
-
...spend.plan.split !== void 0 ? {
|
|
12715
|
-
split: {
|
|
12716
|
-
tokenId: spend.plan.split.tokenId,
|
|
12717
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12718
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12719
|
-
}
|
|
12720
|
-
} : {},
|
|
12721
|
-
spentStates
|
|
12722
|
-
};
|
|
13064
|
+
// modules/payments-v2/history/History.ts
|
|
13065
|
+
function wireAssets(assets) {
|
|
13066
|
+
return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
|
|
12723
13067
|
}
|
|
12724
|
-
function
|
|
12725
|
-
|
|
13068
|
+
function isPermanentReject(err) {
|
|
13069
|
+
const status = err?.status;
|
|
13070
|
+
if (typeof status !== "number") return false;
|
|
13071
|
+
return status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
12726
13072
|
}
|
|
12727
|
-
|
|
12728
|
-
// modules/payments-v2/history/History.ts
|
|
12729
13073
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
12730
13074
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
12731
13075
|
var History = class {
|
|
@@ -12750,7 +13094,7 @@ var History = class {
|
|
|
12750
13094
|
dedupKey: input.transferId,
|
|
12751
13095
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12752
13096
|
type: "SENT",
|
|
12753
|
-
assets:
|
|
13097
|
+
assets: wireAssets(input.assets),
|
|
12754
13098
|
ts: this.ts(input.timestamp),
|
|
12755
13099
|
transferId: input.transferId,
|
|
12756
13100
|
...this.wireTokenId(input.tokenId),
|
|
@@ -12763,7 +13107,7 @@ var History = class {
|
|
|
12763
13107
|
dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
|
|
12764
13108
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12765
13109
|
type: "RECEIVED",
|
|
12766
|
-
assets:
|
|
13110
|
+
assets: wireAssets(input.assets),
|
|
12767
13111
|
ts: this.ts(input.timestamp),
|
|
12768
13112
|
...input.transferId !== void 0 ? { transferId: input.transferId } : {},
|
|
12769
13113
|
...this.wireTokenId(input.tokenId),
|
|
@@ -12776,7 +13120,7 @@ var History = class {
|
|
|
12776
13120
|
dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
|
|
12777
13121
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12778
13122
|
type: "MINT",
|
|
12779
|
-
assets:
|
|
13123
|
+
assets: wireAssets(input.assets),
|
|
12780
13124
|
ts: this.ts(input.timestamp),
|
|
12781
13125
|
...this.wireTokenId(input.tokenId)
|
|
12782
13126
|
}));
|
|
@@ -12787,7 +13131,10 @@ var History = class {
|
|
|
12787
13131
|
try {
|
|
12788
13132
|
await this.deps.client.postHistory([record]);
|
|
12789
13133
|
} catch (err) {
|
|
12790
|
-
this.deps.log?.(
|
|
13134
|
+
this.deps.log?.(
|
|
13135
|
+
isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
|
|
13136
|
+
err
|
|
13137
|
+
);
|
|
12791
13138
|
return;
|
|
12792
13139
|
}
|
|
12793
13140
|
this.deps.emit("history:updated", this.toEntry(record));
|
|
@@ -12854,6 +13201,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
|
12854
13201
|
function stateKey(tokenId, stateHash) {
|
|
12855
13202
|
return `${tokenId}:${stateHash}`;
|
|
12856
13203
|
}
|
|
13204
|
+
function unchanged(prev, item, tokenType) {
|
|
13205
|
+
return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
|
|
13206
|
+
}
|
|
13207
|
+
function isCoinless(item, prev) {
|
|
13208
|
+
if (item.status !== "active") return prev?.coinless ?? false;
|
|
13209
|
+
return (item.assets?.length ?? 0) === 0;
|
|
13210
|
+
}
|
|
12857
13211
|
var InventoryView = class {
|
|
12858
13212
|
constructor(deps) {
|
|
12859
13213
|
this.deps = deps;
|
|
@@ -12947,14 +13301,26 @@ var InventoryView = class {
|
|
|
12947
13301
|
pool(coinId) {
|
|
12948
13302
|
const out = [];
|
|
12949
13303
|
for (const [tokenId, entry] of this.mirror) {
|
|
12950
|
-
if (
|
|
12951
|
-
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12952
|
-
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
13304
|
+
if (!this.isSpendable(tokenId, entry)) continue;
|
|
12953
13305
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12954
13306
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
12955
13307
|
}
|
|
12956
13308
|
return out;
|
|
12957
13309
|
}
|
|
13310
|
+
/**
|
|
13311
|
+
* The eligibility gates every spend shares, coin or token — ONE definition, so a
|
|
13312
|
+
* single probe covers both verbs and neither can drift from the other.
|
|
13313
|
+
*/
|
|
13314
|
+
isSpendable(tokenId, entry) {
|
|
13315
|
+
if (entry.status !== "active") return false;
|
|
13316
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
|
|
13317
|
+
return !this.suspected.has(stateKey(tokenId, entry.stateHash));
|
|
13318
|
+
}
|
|
13319
|
+
/** Is this NAMED token spendable whole — coinless or valued (#777)? */
|
|
13320
|
+
spendableToken(tokenId) {
|
|
13321
|
+
const entry = this.mirror.get(tokenId);
|
|
13322
|
+
return entry !== void 0 && this.isSpendable(tokenId, entry);
|
|
13323
|
+
}
|
|
12958
13324
|
/**
|
|
12959
13325
|
* Current mirrored state of an ACTIVE token, or undefined when it is unknown
|
|
12960
13326
|
* or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
|
|
@@ -12980,6 +13346,29 @@ var InventoryView = class {
|
|
|
12980
13346
|
}
|
|
12981
13347
|
return out;
|
|
12982
13348
|
}
|
|
13349
|
+
/**
|
|
13350
|
+
* Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
|
|
13351
|
+
* exactly when it is not, so no mirror row can appear in both reads.
|
|
13352
|
+
*/
|
|
13353
|
+
coinless(registry) {
|
|
13354
|
+
const out = [];
|
|
13355
|
+
for (const [tokenId, entry] of this.mirror) {
|
|
13356
|
+
if (entry.status !== "active" || !entry.coinless) continue;
|
|
13357
|
+
const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
|
|
13358
|
+
out.push({
|
|
13359
|
+
tokenId,
|
|
13360
|
+
...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
|
|
13361
|
+
...meta ? { name: meta.name } : {},
|
|
13362
|
+
...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
|
|
13363
|
+
stateHash: entry.stateHash,
|
|
13364
|
+
transferring: this.held(tokenId),
|
|
13365
|
+
...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
|
|
13366
|
+
createdAt: entry.createdAt,
|
|
13367
|
+
updatedAt: entry.updatedAt
|
|
13368
|
+
});
|
|
13369
|
+
}
|
|
13370
|
+
return out;
|
|
13371
|
+
}
|
|
12983
13372
|
async assets(registry, price) {
|
|
12984
13373
|
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12985
13374
|
if (!price || raw.length === 0) return raw;
|
|
@@ -13051,14 +13440,19 @@ var InventoryView = class {
|
|
|
13051
13440
|
applyOne(item, now) {
|
|
13052
13441
|
const prev = this.mirror.get(item.tokenId);
|
|
13053
13442
|
if (prev && item.seq < prev.seq) return false;
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
}
|
|
13443
|
+
const tokenType = item.tokenType ?? prev?.tokenType;
|
|
13444
|
+
if (prev && unchanged(prev, item, tokenType)) return false;
|
|
13057
13445
|
this.mirror.set(item.tokenId, {
|
|
13058
13446
|
stateHash: item.stateHash,
|
|
13059
13447
|
seq: item.seq,
|
|
13060
13448
|
status: item.status,
|
|
13061
|
-
|
|
13449
|
+
// ONLY a tombstone inherits: it omits assets for an unrelated reason and
|
|
13450
|
+
// recoverRemoved must know the amount it restores. An ACTIVE row that omits
|
|
13451
|
+
// them is stating coinlessness, and inheriting there would keep stale assets
|
|
13452
|
+
// in tokens() while `coinless` is true — the row in BOTH reads (§16).
|
|
13453
|
+
assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
|
|
13454
|
+
coinless: isCoinless(item, prev),
|
|
13455
|
+
...tokenType !== void 0 ? { tokenType } : {},
|
|
13062
13456
|
createdAt: prev?.createdAt ?? now,
|
|
13063
13457
|
updatedAt: now
|
|
13064
13458
|
});
|
|
@@ -13294,7 +13688,12 @@ async function screen(deps, engine, entry) {
|
|
|
13294
13688
|
}
|
|
13295
13689
|
return {
|
|
13296
13690
|
kind: "accept",
|
|
13297
|
-
record: {
|
|
13691
|
+
record: {
|
|
13692
|
+
tokenId: keys.tokenId,
|
|
13693
|
+
stateHash: keys.stateHash,
|
|
13694
|
+
assets: toAssetAmounts(token),
|
|
13695
|
+
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
|
|
13696
|
+
}
|
|
13298
13697
|
};
|
|
13299
13698
|
}
|
|
13300
13699
|
async function announce(deps, entry, record) {
|
|
@@ -13305,6 +13704,7 @@ async function announce(deps, entry, record) {
|
|
|
13305
13704
|
tokenId: record.tokenId,
|
|
13306
13705
|
stateHash: record.stateHash,
|
|
13307
13706
|
assets: record.assets,
|
|
13707
|
+
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
13308
13708
|
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
13309
13709
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13310
13710
|
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
@@ -13318,6 +13718,19 @@ async function announce(deps, entry, record) {
|
|
|
13318
13718
|
senderPubkey: entry.senderPubkey ?? "",
|
|
13319
13719
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13320
13720
|
tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
|
|
13721
|
+
// #777: named here rather than mapped from assets, which announced an EMPTY list.
|
|
13722
|
+
...record.tokenType !== void 0 ? {
|
|
13723
|
+
coinless: [
|
|
13724
|
+
{
|
|
13725
|
+
tokenId: record.tokenId,
|
|
13726
|
+
tokenType: record.tokenType,
|
|
13727
|
+
stateHash: record.stateHash,
|
|
13728
|
+
transferring: false,
|
|
13729
|
+
createdAt: receivedAt,
|
|
13730
|
+
updatedAt: receivedAt
|
|
13731
|
+
}
|
|
13732
|
+
]
|
|
13733
|
+
} : {},
|
|
13321
13734
|
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
13322
13735
|
receivedAt
|
|
13323
13736
|
};
|
|
@@ -13983,6 +14396,34 @@ var SpendQueue = class {
|
|
|
13983
14396
|
}
|
|
13984
14397
|
return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
|
|
13985
14398
|
}
|
|
14399
|
+
/**
|
|
14400
|
+
* #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
|
|
14401
|
+
* for. A coin plan waits because some OTHER combination may free up; a named
|
|
14402
|
+
* token is either free now or held by a reservation that only its own transfer
|
|
14403
|
+
* can release, so queueing would block until a timeout on a spend that cannot
|
|
14404
|
+
* become possible.
|
|
14405
|
+
*/
|
|
14406
|
+
planWhole(reservationId, tokenId) {
|
|
14407
|
+
if (this.destroyed) {
|
|
14408
|
+
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
14409
|
+
}
|
|
14410
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
14411
|
+
if (unproven !== null) {
|
|
14412
|
+
throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
|
|
14413
|
+
}
|
|
14414
|
+
if (this.deps.spendableToken?.(tokenId) !== true) {
|
|
14415
|
+
throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
|
|
14416
|
+
}
|
|
14417
|
+
const holder = this.deps.ledger.holderOf(tokenId);
|
|
14418
|
+
if (holder !== void 0) {
|
|
14419
|
+
throw new SphereError(
|
|
14420
|
+
`Token ${tokenId} is already reserved by transfer ${holder}`,
|
|
14421
|
+
"VALIDATION_ERROR"
|
|
14422
|
+
);
|
|
14423
|
+
}
|
|
14424
|
+
this.deps.ledger.reserve(reservationId, [{ tokenId }]);
|
|
14425
|
+
return { reservationId, plan: { direct: [tokenId] } };
|
|
14426
|
+
}
|
|
13986
14427
|
/**
|
|
13987
14428
|
* The sources `plan()` WOULD pick right now, reserving nothing and queueing
|
|
13988
14429
|
* nothing — a read-only probe used to warm blobs while the user is still
|
|
@@ -14172,6 +14613,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
14172
14613
|
const queue = new SpendQueue({
|
|
14173
14614
|
ledger,
|
|
14174
14615
|
getPool: (coinId) => view.pool(coinId),
|
|
14616
|
+
spendableToken: (tokenId) => view.spendableToken(tokenId),
|
|
14175
14617
|
...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
|
|
14176
14618
|
});
|
|
14177
14619
|
const historyStore = new History({
|
|
@@ -14252,13 +14694,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
14252
14694
|
...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
|
|
14253
14695
|
emit: deps.emit,
|
|
14254
14696
|
now: deps.now ?? Date.now,
|
|
14255
|
-
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
14697
|
+
recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
|
|
14256
14698
|
await historyStore.recordSent({
|
|
14257
14699
|
transferId,
|
|
14258
|
-
|
|
14259
|
-
//
|
|
14260
|
-
|
|
14261
|
-
amount: committedAmount,
|
|
14700
|
+
// §5.9: the SETTLED amount, never payload.amount — the plan. A token
|
|
14701
|
+
// spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
|
|
14702
|
+
...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
|
|
14262
14703
|
recipientPubkey: payload.recipient,
|
|
14263
14704
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
14264
14705
|
});
|
|
@@ -14293,12 +14734,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
|
|
|
14293
14734
|
});
|
|
14294
14735
|
}
|
|
14295
14736
|
async function recordReceived(historyStore, record) {
|
|
14296
|
-
const first = record.assets[0];
|
|
14297
14737
|
await historyStore.recordReceived({
|
|
14298
14738
|
tokenId: record.tokenId,
|
|
14299
14739
|
stateHash: record.stateHash,
|
|
14300
|
-
|
|
14301
|
-
amount: first?.amount ?? "0",
|
|
14740
|
+
assets: record.assets,
|
|
14302
14741
|
...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
|
|
14303
14742
|
...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
|
|
14304
14743
|
...record.memo !== void 0 ? { memo: record.memo } : {},
|
|
@@ -14446,6 +14885,13 @@ var PaymentsFacade = class {
|
|
|
14446
14885
|
tokens(filter) {
|
|
14447
14886
|
return this.view.tokens(this.deps.registry, filter);
|
|
14448
14887
|
}
|
|
14888
|
+
coinless() {
|
|
14889
|
+
return this.view.coinless(this.deps.registry);
|
|
14890
|
+
}
|
|
14891
|
+
tokenData(tokenId) {
|
|
14892
|
+
const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
|
|
14893
|
+
return readTokenData(deps, tokenId);
|
|
14894
|
+
}
|
|
14449
14895
|
history(page) {
|
|
14450
14896
|
return this.historyStore.page(page ?? {});
|
|
14451
14897
|
}
|
|
@@ -14468,7 +14914,14 @@ var PaymentsFacade = class {
|
|
|
14468
14914
|
return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
|
|
14469
14915
|
}
|
|
14470
14916
|
send(request) {
|
|
14471
|
-
return this.track(this.
|
|
14917
|
+
return this.track(this.runJob({ kind: "coin", request }, request.amount));
|
|
14918
|
+
}
|
|
14919
|
+
sendWholeToken(request) {
|
|
14920
|
+
return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
|
|
14921
|
+
}
|
|
14922
|
+
/** NFT-scoped twin: refuses a valued source. */
|
|
14923
|
+
sendCoinless(request) {
|
|
14924
|
+
return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
|
|
14472
14925
|
}
|
|
14473
14926
|
async receive() {
|
|
14474
14927
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -14500,12 +14953,25 @@ var PaymentsFacade = class {
|
|
|
14500
14953
|
nowMs() {
|
|
14501
14954
|
return (this.deps.now ?? Date.now)();
|
|
14502
14955
|
}
|
|
14956
|
+
mintDeps() {
|
|
14957
|
+
return {
|
|
14958
|
+
engine: this.engine(),
|
|
14959
|
+
mintJournal: this.machineStores.mintJournal,
|
|
14960
|
+
storagePort: this.deps.storagePort,
|
|
14961
|
+
recordMint: (input) => this.historyStore.recordMint(input),
|
|
14962
|
+
armHeartbeat: () => this.heartbeat.arm(),
|
|
14963
|
+
noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
|
|
14964
|
+
refreshView: () => this.trackTail(this.view.delta()),
|
|
14965
|
+
ownPubkeyBytes: this.ownPubkeyBytes,
|
|
14966
|
+
now: () => this.nowMs()
|
|
14967
|
+
};
|
|
14968
|
+
}
|
|
14503
14969
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
14504
14970
|
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
14505
14971
|
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
14506
|
-
async
|
|
14972
|
+
async runJob(job, amount) {
|
|
14507
14973
|
const run = {
|
|
14508
|
-
amount
|
|
14974
|
+
amount,
|
|
14509
14975
|
delivered: [],
|
|
14510
14976
|
sentTokens: [],
|
|
14511
14977
|
tokenTransfers: [],
|
|
@@ -14515,7 +14981,7 @@ var PaymentsFacade = class {
|
|
|
14515
14981
|
lastTransferId: ""
|
|
14516
14982
|
};
|
|
14517
14983
|
try {
|
|
14518
|
-
return await this.sendWithPolicy(
|
|
14984
|
+
return await this.sendWithPolicy(job, run);
|
|
14519
14985
|
} catch (err) {
|
|
14520
14986
|
this.emitCleanFailure(err, run.lastTransferId);
|
|
14521
14987
|
throw err;
|
|
@@ -14535,19 +15001,19 @@ var PaymentsFacade = class {
|
|
|
14535
15001
|
};
|
|
14536
15002
|
this.deps.emit("transfer:updated", failed);
|
|
14537
15003
|
}
|
|
14538
|
-
async sendWithPolicy(
|
|
14539
|
-
const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
|
|
15004
|
+
async sendWithPolicy(job, run) {
|
|
15005
|
+
const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
|
|
14540
15006
|
for (let attempt = 0; ; attempt++) {
|
|
14541
15007
|
let ctx;
|
|
14542
15008
|
try {
|
|
14543
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey,
|
|
15009
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
|
|
14544
15010
|
} catch (err) {
|
|
14545
|
-
throw
|
|
15011
|
+
throw partialize(err, run);
|
|
14546
15012
|
}
|
|
14547
15013
|
const disposition = await this.runAttempt(ctx);
|
|
14548
|
-
if (disposition.kind === "rethrow") throw
|
|
15014
|
+
if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
|
|
14549
15015
|
if (disposition.kind === "retry-full") {
|
|
14550
|
-
if (attempt >= MAX_RESELECT) throw
|
|
15016
|
+
if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
|
|
14551
15017
|
continue;
|
|
14552
15018
|
}
|
|
14553
15019
|
if (disposition.kind === "success") {
|
|
@@ -14603,7 +15069,7 @@ var PaymentsFacade = class {
|
|
|
14603
15069
|
async disposeFailedAttempt(ctx, err) {
|
|
14604
15070
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
14605
15071
|
this.settleKeepOpen(ctx);
|
|
14606
|
-
|
|
15072
|
+
stampTransferId(err, ctx.transferId);
|
|
14607
15073
|
return { kind: "rethrow", error: err };
|
|
14608
15074
|
}
|
|
14609
15075
|
const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
|
|
@@ -14637,7 +15103,7 @@ var PaymentsFacade = class {
|
|
|
14637
15103
|
async disposeConvergeFailure(ctx, err) {
|
|
14638
15104
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
14639
15105
|
this.settleKeepOpen(ctx);
|
|
14640
|
-
|
|
15106
|
+
stampTransferId(err, ctx.transferId);
|
|
14641
15107
|
return { kind: "rethrow", error: err };
|
|
14642
15108
|
}
|
|
14643
15109
|
if (classifyError(err) === "conflict") {
|
|
@@ -14648,9 +15114,29 @@ var PaymentsFacade = class {
|
|
|
14648
15114
|
}
|
|
14649
15115
|
return { kind: "rethrow", error: err };
|
|
14650
15116
|
}
|
|
14651
|
-
async planAndMaterialize(recipientPubkey,
|
|
15117
|
+
async planAndMaterialize(recipientPubkey, job, run) {
|
|
14652
15118
|
const transferId = this.newId();
|
|
14653
15119
|
run.lastTransferId = transferId;
|
|
15120
|
+
if (job.kind === "whole") {
|
|
15121
|
+
const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
|
|
15122
|
+
const sourceIds2 = this.markPlanned(transferId, "", spend2);
|
|
15123
|
+
try {
|
|
15124
|
+
return await materializeWholeSpend(
|
|
15125
|
+
{ engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
|
|
15126
|
+
{
|
|
15127
|
+
transferId,
|
|
15128
|
+
recipientPubkey,
|
|
15129
|
+
request: job.request,
|
|
15130
|
+
sourceIds: sourceIds2,
|
|
15131
|
+
requireCoinless: job.requireCoinless
|
|
15132
|
+
}
|
|
15133
|
+
);
|
|
15134
|
+
} catch (err) {
|
|
15135
|
+
this.settleFailure(transferId, "", sourceIds2);
|
|
15136
|
+
throw err;
|
|
15137
|
+
}
|
|
15138
|
+
}
|
|
15139
|
+
const request = { ...job.request, amount: run.amount };
|
|
14654
15140
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
14655
15141
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
14656
15142
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -14784,22 +15270,6 @@ var PaymentsFacade = class {
|
|
|
14784
15270
|
return result;
|
|
14785
15271
|
}
|
|
14786
15272
|
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
14787
|
-
partialize(err, run) {
|
|
14788
|
-
if (run.delivered.length === 0) return err;
|
|
14789
|
-
return new PartialSendConflictError(
|
|
14790
|
-
"Part of your payment was sent; the remaining amount could not be completed (see cause). The delivered portion is final \u2014 re-plan only the shortfall, never the full amount.",
|
|
14791
|
-
run.firstPartialId ?? "",
|
|
14792
|
-
run.delivered,
|
|
14793
|
-
run.amount,
|
|
14794
|
-
err
|
|
14795
|
-
);
|
|
14796
|
-
}
|
|
14797
|
-
/** #441: possibly-committed errors must carry the transferId for the settling journal. */
|
|
14798
|
-
stampTransferId(err, transferId) {
|
|
14799
|
-
if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
|
|
14800
|
-
err.transferId = transferId;
|
|
14801
|
-
}
|
|
14802
|
-
}
|
|
14803
15273
|
async softAbort(transferId) {
|
|
14804
15274
|
try {
|
|
14805
15275
|
await this.deps.client.abortIntent(transferId);
|
|
@@ -14815,57 +15285,11 @@ var PaymentsFacade = class {
|
|
|
14815
15285
|
const mintId = this.newId();
|
|
14816
15286
|
this.activeMoneyOps.add(mintId);
|
|
14817
15287
|
try {
|
|
14818
|
-
return await this.
|
|
15288
|
+
return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
|
|
14819
15289
|
} finally {
|
|
14820
15290
|
this.activeMoneyOps.delete(mintId);
|
|
14821
15291
|
}
|
|
14822
15292
|
}
|
|
14823
|
-
async mintUnderJournal(mintId, coinId, amount) {
|
|
14824
|
-
const engine = this.engine();
|
|
14825
|
-
const entry = {
|
|
14826
|
-
mintId,
|
|
14827
|
-
coinId,
|
|
14828
|
-
amount: amount.toString(),
|
|
14829
|
-
tokenId: "",
|
|
14830
|
-
createdAt: this.nowMs()
|
|
14831
|
-
};
|
|
14832
|
-
await this.machineStores.mintJournal.upsert(entry);
|
|
14833
|
-
let token;
|
|
14834
|
-
try {
|
|
14835
|
-
token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
|
|
14836
|
-
} catch (err) {
|
|
14837
|
-
this.heartbeat.arm();
|
|
14838
|
-
return { success: false, error: messageOf(err) };
|
|
14839
|
-
}
|
|
14840
|
-
if (token.blob.tokenId !== entry.tokenId) {
|
|
14841
|
-
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
14842
|
-
}
|
|
14843
|
-
try {
|
|
14844
|
-
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
14845
|
-
} catch (err) {
|
|
14846
|
-
this.heartbeat.arm();
|
|
14847
|
-
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
14848
|
-
}
|
|
14849
|
-
return { success: true, tokenId: token.blob.tokenId };
|
|
14850
|
-
}
|
|
14851
|
-
async finalizeMint(engine, mintId, token, coinId, amount) {
|
|
14852
|
-
const bytes = token.blob.token;
|
|
14853
|
-
const digest = bytesToHex3(sha256(bytes));
|
|
14854
|
-
const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
|
|
14855
|
-
const key = keys.get(digest);
|
|
14856
|
-
if (key === void 0) {
|
|
14857
|
-
throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
|
|
14858
|
-
}
|
|
14859
|
-
await this.deps.storagePort.applyDelta({
|
|
14860
|
-
transferId: mintId,
|
|
14861
|
-
spent: [],
|
|
14862
|
-
added: [{ tokenId: token.blob.tokenId, key }]
|
|
14863
|
-
});
|
|
14864
|
-
await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
|
|
14865
|
-
await this.machineStores.mintJournal.removeByKey(mintId);
|
|
14866
|
-
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
14867
|
-
this.trackTail(this.view.delta());
|
|
14868
|
-
}
|
|
14869
15293
|
/** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
|
|
14870
15294
|
async replayMints() {
|
|
14871
15295
|
let resolved = 0;
|
|
@@ -14897,7 +15321,7 @@ var PaymentsFacade = class {
|
|
|
14897
15321
|
if (token.blob.tokenId !== entry.tokenId) {
|
|
14898
15322
|
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
14899
15323
|
}
|
|
14900
|
-
await this.
|
|
15324
|
+
await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
|
|
14901
15325
|
return true;
|
|
14902
15326
|
}
|
|
14903
15327
|
async tokenInServerInventory(tokenId) {
|