@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/index.cjs
CHANGED
|
@@ -8273,6 +8273,8 @@ var FETCH_TIMEOUT_MS = 1e4;
|
|
|
8273
8273
|
var TokenRegistry = class _TokenRegistry {
|
|
8274
8274
|
static instance = null;
|
|
8275
8275
|
definitionsById;
|
|
8276
|
+
/** Non-fungible definitions keyed by TOKEN TYPE — a separate namespace (#147). */
|
|
8277
|
+
definitionsByType;
|
|
8276
8278
|
definitionsBySymbol;
|
|
8277
8279
|
definitionsByName;
|
|
8278
8280
|
// Remote refresh state
|
|
@@ -8291,6 +8293,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8291
8293
|
inFlight = null;
|
|
8292
8294
|
constructor() {
|
|
8293
8295
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
8296
|
+
this.definitionsByType = /* @__PURE__ */ new Map();
|
|
8294
8297
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
8295
8298
|
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
8296
8299
|
}
|
|
@@ -8518,9 +8521,11 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8518
8521
|
this.definitionsById.clear();
|
|
8519
8522
|
this.definitionsBySymbol.clear();
|
|
8520
8523
|
this.definitionsByName.clear();
|
|
8524
|
+
this.definitionsByType.clear();
|
|
8521
8525
|
for (const def of definitions) {
|
|
8522
8526
|
const idLower = def.id.toLowerCase();
|
|
8523
8527
|
this.definitionsById.set(idLower, def);
|
|
8528
|
+
if (def.assetKind === "non-fungible") this.definitionsByType.set(idLower, def);
|
|
8524
8529
|
if (def.symbol) {
|
|
8525
8530
|
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
8526
8531
|
}
|
|
@@ -8734,6 +8739,20 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8734
8739
|
getAllDefinitions() {
|
|
8735
8740
|
return Array.from(this.definitionsById.values());
|
|
8736
8741
|
}
|
|
8742
|
+
/**
|
|
8743
|
+
* Definition for a coinless token's genesis TOKEN TYPE (#777). Never falls back
|
|
8744
|
+
* to `getDefinition`: a type and a coin id live in different namespaces, so a
|
|
8745
|
+
* type that collided with a coin id would otherwise render as that coin.
|
|
8746
|
+
*/
|
|
8747
|
+
getTypeMeta(tokenType) {
|
|
8748
|
+
const def = this.getTypeDefinition(tokenType);
|
|
8749
|
+
if (!def) return null;
|
|
8750
|
+
return { name: def.name, iconUrl: def.icons?.[0]?.url ?? null };
|
|
8751
|
+
}
|
|
8752
|
+
getTypeDefinition(tokenType) {
|
|
8753
|
+
if (!tokenType) return void 0;
|
|
8754
|
+
return this.definitionsByType.get(tokenType.toLowerCase());
|
|
8755
|
+
}
|
|
8737
8756
|
/**
|
|
8738
8757
|
* Get all fungible token definitions
|
|
8739
8758
|
* @returns Array of fungible token definitions
|
|
@@ -9568,6 +9587,8 @@ var import_DataHasherFactory = require("@unicitylabs/state-transition-sdk/lib/cr
|
|
|
9568
9587
|
var import_CborSerializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js");
|
|
9569
9588
|
var import_CborDeserializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js");
|
|
9570
9589
|
var import_CborError = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js");
|
|
9590
|
+
var import_CborReader = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js");
|
|
9591
|
+
var import_MajorType = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js");
|
|
9571
9592
|
var import_Asset = require("@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js");
|
|
9572
9593
|
var import_AssetId = require("@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js");
|
|
9573
9594
|
var import_PaymentAssetCollection = require("@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js");
|
|
@@ -9980,6 +10001,126 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
|
|
|
9980
10001
|
return burntToken;
|
|
9981
10002
|
}
|
|
9982
10003
|
|
|
10004
|
+
// token-engine/value-envelope.ts
|
|
10005
|
+
function isCoinlessEnvelope(envelope) {
|
|
10006
|
+
return envelope.startsWith("none_");
|
|
10007
|
+
}
|
|
10008
|
+
var SELF_DESCRIBED_CBOR_TAG = 55799n;
|
|
10009
|
+
var CBOR_MAJOR_TYPE_MASK = 224;
|
|
10010
|
+
var describe = (error) => error instanceof Error ? error.message : String(error);
|
|
10011
|
+
var invalid = (message) => new SphereError(message, "VALIDATION_ERROR");
|
|
10012
|
+
function majorTypeOf(bytes) {
|
|
10013
|
+
const first = bytes.at(0);
|
|
10014
|
+
return first === void 0 ? null : first & CBOR_MAJOR_TYPE_MASK;
|
|
10015
|
+
}
|
|
10016
|
+
function headTagNumber(bytes) {
|
|
10017
|
+
try {
|
|
10018
|
+
return new import_CborReader.CborReader(bytes).readLength(import_MajorType.MajorType.TAG);
|
|
10019
|
+
} catch {
|
|
10020
|
+
return null;
|
|
10021
|
+
}
|
|
10022
|
+
}
|
|
10023
|
+
function isAssetShaped(item) {
|
|
10024
|
+
try {
|
|
10025
|
+
return import_CborDeserializer.CborDeserializer.decodeArray(item, 2).every(
|
|
10026
|
+
(field) => majorTypeOf(field) === import_MajorType.MajorType.BYTE_STRING
|
|
10027
|
+
);
|
|
10028
|
+
} catch {
|
|
10029
|
+
return false;
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
function arrayItemsOrNull(bytes) {
|
|
10033
|
+
try {
|
|
10034
|
+
return import_CborDeserializer.CborDeserializer.decodeArray(bytes);
|
|
10035
|
+
} catch {
|
|
10036
|
+
return null;
|
|
10037
|
+
}
|
|
10038
|
+
}
|
|
10039
|
+
function decodeSphere(genesisData) {
|
|
10040
|
+
try {
|
|
10041
|
+
return SpherePaymentData.fromCBOR(genesisData).toValue();
|
|
10042
|
+
} catch (error) {
|
|
10043
|
+
throw invalid(`Failed to decode token payment data: ${describe(error)}`);
|
|
10044
|
+
}
|
|
10045
|
+
}
|
|
10046
|
+
function taggedItemBody(genesisData) {
|
|
10047
|
+
try {
|
|
10048
|
+
return import_CborDeserializer.CborDeserializer.decodeTag(genesisData).data;
|
|
10049
|
+
} catch (error) {
|
|
10050
|
+
throw invalid(
|
|
10051
|
+
`Token value payload is a CBOR tag whose item is malformed or carries trailing bytes: ${describe(error)}`
|
|
10052
|
+
);
|
|
10053
|
+
}
|
|
10054
|
+
}
|
|
10055
|
+
function selfDescribedBodyCarriesValue(body) {
|
|
10056
|
+
const major = majorTypeOf(body);
|
|
10057
|
+
if (major === import_MajorType.MajorType.TAG) {
|
|
10058
|
+
const inner = headTagNumber(body);
|
|
10059
|
+
return inner === SpherePaymentData.CBOR_TAG || inner === SELF_DESCRIBED_CBOR_TAG;
|
|
10060
|
+
}
|
|
10061
|
+
if (major === import_MajorType.MajorType.ARRAY) {
|
|
10062
|
+
const items = arrayItemsOrNull(body);
|
|
10063
|
+
return items !== null && items.some(isAssetShaped);
|
|
10064
|
+
}
|
|
10065
|
+
return false;
|
|
10066
|
+
}
|
|
10067
|
+
function classifyTagged(genesisData) {
|
|
10068
|
+
const tag = headTagNumber(genesisData);
|
|
10069
|
+
if (tag === null) {
|
|
10070
|
+
throw invalid(
|
|
10071
|
+
"Token value payload has an unreadable or non-canonically encoded CBOR tag head \u2014 it could be a malformed SpherePaymentData envelope"
|
|
10072
|
+
);
|
|
10073
|
+
}
|
|
10074
|
+
if (tag === SpherePaymentData.CBOR_TAG) {
|
|
10075
|
+
return { envelope: "sphere", value: decodeSphere(genesisData) };
|
|
10076
|
+
}
|
|
10077
|
+
const body = taggedItemBody(genesisData);
|
|
10078
|
+
if (tag === SELF_DESCRIBED_CBOR_TAG && selfDescribedBodyCarriesValue(body)) {
|
|
10079
|
+
throw invalid(
|
|
10080
|
+
"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"
|
|
10081
|
+
);
|
|
10082
|
+
}
|
|
10083
|
+
return { envelope: "none_tag", value: null };
|
|
10084
|
+
}
|
|
10085
|
+
function classifyArray(genesisData) {
|
|
10086
|
+
const items = arrayItemsOrNull(genesisData);
|
|
10087
|
+
if (items === null) {
|
|
10088
|
+
throw invalid("Token value payload is not PaymentAssetCollection: malformed CBOR array");
|
|
10089
|
+
}
|
|
10090
|
+
return items.some(isAssetShaped) ? { envelope: "bare_collection", value: null } : { envelope: "none_other", value: null };
|
|
10091
|
+
}
|
|
10092
|
+
function classifyValueEnvelope(genesisData) {
|
|
10093
|
+
if (genesisData === null) return { envelope: "none_absent", value: null };
|
|
10094
|
+
const major = majorTypeOf(genesisData);
|
|
10095
|
+
if (major === null) return { envelope: "none_absent", value: null };
|
|
10096
|
+
if (major === import_MajorType.MajorType.TAG) return classifyTagged(genesisData);
|
|
10097
|
+
if (major === import_MajorType.MajorType.ARRAY) return classifyArray(genesisData);
|
|
10098
|
+
return { envelope: "none_other", value: null };
|
|
10099
|
+
}
|
|
10100
|
+
function wrapToken(sdkToken) {
|
|
10101
|
+
const { envelope, value } = classifyValueEnvelope(sdkToken.genesis.data);
|
|
10102
|
+
const blob = {
|
|
10103
|
+
tokenId: import_HexConverter.HexConverter.encode(sdkToken.id.bytes),
|
|
10104
|
+
token: sdkToken.toCBOR()
|
|
10105
|
+
};
|
|
10106
|
+
return {
|
|
10107
|
+
sdkToken,
|
|
10108
|
+
blob,
|
|
10109
|
+
value,
|
|
10110
|
+
valueEnvelope: envelope,
|
|
10111
|
+
tokenType: import_HexConverter.HexConverter.encode(sdkToken.type.bytes)
|
|
10112
|
+
};
|
|
10113
|
+
}
|
|
10114
|
+
function assertMintableData(data) {
|
|
10115
|
+
try {
|
|
10116
|
+
classifyValueEnvelope(data);
|
|
10117
|
+
} catch (error) {
|
|
10118
|
+
throw invalid(
|
|
10119
|
+
`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.`
|
|
10120
|
+
);
|
|
10121
|
+
}
|
|
10122
|
+
}
|
|
10123
|
+
|
|
9983
10124
|
// token-engine/SphereTokenEngine.ts
|
|
9984
10125
|
var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
|
|
9985
10126
|
var MAX_MINT_CONCURRENCY = 8;
|
|
@@ -10036,7 +10177,7 @@ var SphereTokenEngine = class {
|
|
|
10036
10177
|
return sdkToken.latestTransaction.data;
|
|
10037
10178
|
}
|
|
10038
10179
|
const data = sdkToken.genesis.data;
|
|
10039
|
-
if (data &&
|
|
10180
|
+
if (data && classifyValueEnvelope(data).envelope === "sphere") {
|
|
10040
10181
|
return SpherePaymentData.fromCBOR(data).memo;
|
|
10041
10182
|
}
|
|
10042
10183
|
return null;
|
|
@@ -10066,9 +10207,10 @@ var SphereTokenEngine = class {
|
|
|
10066
10207
|
);
|
|
10067
10208
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10068
10209
|
const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
|
|
10069
|
-
return
|
|
10210
|
+
return wrapToken(token);
|
|
10070
10211
|
}
|
|
10071
10212
|
async mintDataToken(params, options) {
|
|
10213
|
+
assertMintableData(params.data);
|
|
10072
10214
|
const recipient = import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey);
|
|
10073
10215
|
const tokenType = params.tokenType ? new import_TokenType.TokenType(params.tokenType) : import_TokenType.TokenType.generate();
|
|
10074
10216
|
const salt = params.salt ? import_TokenSalt.TokenSalt.fromBytes(params.salt) : import_TokenSalt.TokenSalt.generate();
|
|
@@ -10083,7 +10225,7 @@ var SphereTokenEngine = class {
|
|
|
10083
10225
|
);
|
|
10084
10226
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10085
10227
|
const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
|
|
10086
|
-
return
|
|
10228
|
+
return wrapToken(token);
|
|
10087
10229
|
}
|
|
10088
10230
|
async transfer(params, options) {
|
|
10089
10231
|
this.assertOwned(params.token);
|
|
@@ -10106,13 +10248,19 @@ var SphereTokenEngine = class {
|
|
|
10106
10248
|
);
|
|
10107
10249
|
const certified = await transferTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10108
10250
|
const transferred = await params.token.sdkToken.transfer(certified, this.deps.verificationContext);
|
|
10109
|
-
return
|
|
10251
|
+
return wrapToken(transferred);
|
|
10110
10252
|
}
|
|
10111
10253
|
async split(params, options) {
|
|
10112
10254
|
this.assertOwned(params.token);
|
|
10113
10255
|
if (params.outputs.length === 0) {
|
|
10114
10256
|
throw new SphereError("Split requires at least one output", "VALIDATION_ERROR");
|
|
10115
10257
|
}
|
|
10258
|
+
if (params.token.value === null) {
|
|
10259
|
+
throw new SphereError(
|
|
10260
|
+
`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.`,
|
|
10261
|
+
"VALIDATION_ERROR"
|
|
10262
|
+
);
|
|
10263
|
+
}
|
|
10116
10264
|
const transferId = this.resolveTransferId(options);
|
|
10117
10265
|
const requests = params.outputs.map(
|
|
10118
10266
|
(o, i) => import_SplitTokenRequest.SplitTokenRequest.create(
|
|
@@ -10234,7 +10382,7 @@ var SphereTokenEngine = class {
|
|
|
10234
10382
|
const proof = await this.submitSplitMintLeg(certData, mintTx, options);
|
|
10235
10383
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10236
10384
|
const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
|
|
10237
|
-
return
|
|
10385
|
+
return wrapToken(token);
|
|
10238
10386
|
}
|
|
10239
10387
|
/**
|
|
10240
10388
|
* Submit one split-mint leg. A `TRANSACTION_HASH_MISMATCH` on a mint leg is NOT a foreign spend
|
|
@@ -10292,7 +10440,7 @@ var SphereTokenEngine = class {
|
|
|
10292
10440
|
"VALIDATION_ERROR"
|
|
10293
10441
|
);
|
|
10294
10442
|
}
|
|
10295
|
-
return
|
|
10443
|
+
return wrapToken(sdkToken);
|
|
10296
10444
|
}
|
|
10297
10445
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
10298
10446
|
/**
|
|
@@ -10404,34 +10552,6 @@ var SphereTokenEngine = class {
|
|
|
10404
10552
|
throw new SphereError("Cannot transfer a token not owned by this engine identity", "VALIDATION_ERROR");
|
|
10405
10553
|
}
|
|
10406
10554
|
}
|
|
10407
|
-
/** Wrap an SDK token into a SphereToken: cache its blob (incl. stable tokenId) + decoded value. */
|
|
10408
|
-
wrapToken(sdkToken) {
|
|
10409
|
-
const data = sdkToken.genesis.data;
|
|
10410
|
-
let value = null;
|
|
10411
|
-
if (data && this.isSpherePaymentData(data)) {
|
|
10412
|
-
try {
|
|
10413
|
-
value = SpherePaymentData.fromCBOR(data).toValue();
|
|
10414
|
-
} catch (err) {
|
|
10415
|
-
throw new SphereError(
|
|
10416
|
-
`Failed to decode token payment data: ${err instanceof Error ? err.message : String(err)}`,
|
|
10417
|
-
"VALIDATION_ERROR"
|
|
10418
|
-
);
|
|
10419
|
-
}
|
|
10420
|
-
}
|
|
10421
|
-
const blob = {
|
|
10422
|
-
tokenId: import_HexConverter.HexConverter.encode(sdkToken.id.bytes),
|
|
10423
|
-
token: sdkToken.toCBOR()
|
|
10424
|
-
};
|
|
10425
|
-
return { sdkToken, blob, value };
|
|
10426
|
-
}
|
|
10427
|
-
/** True if the bytes are a SpherePaymentData envelope (value token) vs a raw data token. */
|
|
10428
|
-
isSpherePaymentData(data) {
|
|
10429
|
-
try {
|
|
10430
|
-
return import_CborDeserializer.CborDeserializer.decodeTag(data).tag === SpherePaymentData.CBOR_TAG;
|
|
10431
|
-
} catch {
|
|
10432
|
-
return false;
|
|
10433
|
-
}
|
|
10434
|
-
}
|
|
10435
10555
|
/**
|
|
10436
10556
|
* Terminate the verification worker pool, if this engine was configured with
|
|
10437
10557
|
* one. Idempotent — the pool's dispose only touches workers it spawned.
|
|
@@ -11827,6 +11947,17 @@ function createMachineStores(kv) {
|
|
|
11827
11947
|
};
|
|
11828
11948
|
}
|
|
11829
11949
|
|
|
11950
|
+
// modules/payments-v2/machine/payload-view.ts
|
|
11951
|
+
function isWholeIntent(p) {
|
|
11952
|
+
return p.kind === "whole" || p.kind === "coinless";
|
|
11953
|
+
}
|
|
11954
|
+
function splitOf(p) {
|
|
11955
|
+
return p.kind === "coin" ? p.split : void 0;
|
|
11956
|
+
}
|
|
11957
|
+
function coinIdOf(p) {
|
|
11958
|
+
return p.kind === "coin" ? p.coinId : void 0;
|
|
11959
|
+
}
|
|
11960
|
+
|
|
11830
11961
|
// modules/payments-v2/machine/TransferMachine.ts
|
|
11831
11962
|
var CERTIFY_WIDTH = 20;
|
|
11832
11963
|
var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
|
|
@@ -12029,13 +12160,14 @@ var TransferMachine = class {
|
|
|
12029
12160
|
const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
|
|
12030
12161
|
return { recipientBlob: finished.blob.token };
|
|
12031
12162
|
}
|
|
12032
|
-
const split = ctx.payload
|
|
12163
|
+
const split = splitOf(ctx.payload);
|
|
12164
|
+
const splitCoinId = coinIdOf(ctx.payload);
|
|
12033
12165
|
const { outputs } = await engine.split(
|
|
12034
12166
|
{
|
|
12035
12167
|
token: source,
|
|
12036
12168
|
outputs: [
|
|
12037
|
-
{ recipientPubkey, coinId:
|
|
12038
|
-
{ recipientPubkey: this.deps.ownPubkey, coinId:
|
|
12169
|
+
{ recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
|
|
12170
|
+
{ recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
|
|
12039
12171
|
]
|
|
12040
12172
|
},
|
|
12041
12173
|
opts
|
|
@@ -12157,7 +12289,9 @@ var TransferMachine = class {
|
|
|
12157
12289
|
const entry = {
|
|
12158
12290
|
transferId: r.transferId,
|
|
12159
12291
|
remainingAmount: undelivered.toString(),
|
|
12160
|
-
|
|
12292
|
+
// '' only on a token-addressed intent, which is one leg: it either lands or
|
|
12293
|
+
// conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
|
|
12294
|
+
coinId: coinIdOf(r.payload) ?? "",
|
|
12161
12295
|
recipient: r.payload.recipient,
|
|
12162
12296
|
committedTokenIds: committed.map((o) => o.op.sourceTokenId),
|
|
12163
12297
|
createdAt: this.deps.now()
|
|
@@ -12183,8 +12317,16 @@ var TransferMachine = class {
|
|
|
12183
12317
|
}
|
|
12184
12318
|
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
12185
12319
|
try {
|
|
12186
|
-
const
|
|
12187
|
-
await this.
|
|
12320
|
+
const coinId = coinIdOf(payload);
|
|
12321
|
+
const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
|
|
12322
|
+
const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
|
|
12323
|
+
await this.deps.recordHistory?.({
|
|
12324
|
+
transferId,
|
|
12325
|
+
payload,
|
|
12326
|
+
phase,
|
|
12327
|
+
committedAmount,
|
|
12328
|
+
...committedAssets !== void 0 ? { committedAssets } : {}
|
|
12329
|
+
});
|
|
12188
12330
|
} catch {
|
|
12189
12331
|
}
|
|
12190
12332
|
try {
|
|
@@ -12198,6 +12340,18 @@ var TransferMachine = class {
|
|
|
12198
12340
|
* a journal-rehydrated resume leg has no source token, but every committed
|
|
12199
12341
|
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
12200
12342
|
*/
|
|
12343
|
+
/** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
|
|
12344
|
+
async settledAssets(engine, committed) {
|
|
12345
|
+
const total = /* @__PURE__ */ new Map();
|
|
12346
|
+
for (const outcome of committed) {
|
|
12347
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
12348
|
+
const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
|
|
12349
|
+
for (const asset of engine.readValue(token)?.assets ?? []) {
|
|
12350
|
+
total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
|
|
12351
|
+
}
|
|
12352
|
+
}
|
|
12353
|
+
return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
|
|
12354
|
+
}
|
|
12201
12355
|
async settledAmount(engine, coinId, committed) {
|
|
12202
12356
|
let total = 0n;
|
|
12203
12357
|
for (const outcome of committed) {
|
|
@@ -12226,8 +12380,11 @@ var TransferMachine = class {
|
|
|
12226
12380
|
}
|
|
12227
12381
|
};
|
|
12228
12382
|
function conflictAmount(engine, payload, op, source) {
|
|
12229
|
-
|
|
12230
|
-
|
|
12383
|
+
const split = splitOf(payload);
|
|
12384
|
+
if (op.kind === "split") return BigInt(split.splitAmount);
|
|
12385
|
+
const coinId = coinIdOf(payload);
|
|
12386
|
+
if (coinId === void 0) return 0n;
|
|
12387
|
+
return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
|
|
12231
12388
|
}
|
|
12232
12389
|
|
|
12233
12390
|
// modules/payments-v2/machine/resume.ts
|
|
@@ -12334,6 +12491,14 @@ async function runOne(ctx, job, report) {
|
|
|
12334
12491
|
report.failed.push(job.transferId);
|
|
12335
12492
|
return;
|
|
12336
12493
|
}
|
|
12494
|
+
if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
|
|
12495
|
+
logger.warn(
|
|
12496
|
+
"PaymentsV2",
|
|
12497
|
+
`resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
|
|
12498
|
+
);
|
|
12499
|
+
report.failed.push(job.transferId);
|
|
12500
|
+
return;
|
|
12501
|
+
}
|
|
12337
12502
|
mine.set(op.sourceTokenId, token);
|
|
12338
12503
|
}
|
|
12339
12504
|
try {
|
|
@@ -12364,6 +12529,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
|
|
|
12364
12529
|
}
|
|
12365
12530
|
report.failed.push(transferId);
|
|
12366
12531
|
}
|
|
12532
|
+
function validateCoinPayload(c) {
|
|
12533
|
+
if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
|
|
12534
|
+
throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
|
|
12535
|
+
}
|
|
12536
|
+
const s = c.split;
|
|
12537
|
+
if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
|
|
12538
|
+
throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
|
|
12539
|
+
}
|
|
12540
|
+
return { ...c, kind: "coin" };
|
|
12541
|
+
}
|
|
12542
|
+
function validateWholePayload(p) {
|
|
12543
|
+
if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
|
|
12544
|
+
throw new SphereError(
|
|
12545
|
+
"token intent payload must name exactly one source token",
|
|
12546
|
+
"VALIDATION_ERROR"
|
|
12547
|
+
);
|
|
12548
|
+
}
|
|
12549
|
+
if (p.split !== void 0) {
|
|
12550
|
+
throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
|
|
12551
|
+
}
|
|
12552
|
+
return { ...p, kind: "whole", direct: [p.direct[0]] };
|
|
12553
|
+
}
|
|
12367
12554
|
function validatePayload(raw) {
|
|
12368
12555
|
const p = raw !== null && typeof raw === "object" ? raw : null;
|
|
12369
12556
|
if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
|
|
@@ -12372,14 +12559,17 @@ function validatePayload(raw) {
|
|
|
12372
12559
|
"VALIDATION_ERROR"
|
|
12373
12560
|
);
|
|
12374
12561
|
}
|
|
12375
|
-
if (typeof p.recipient !== "string"
|
|
12376
|
-
throw new SphereError("intent payload is missing recipient
|
|
12562
|
+
if (typeof p.recipient !== "string") {
|
|
12563
|
+
throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
|
|
12377
12564
|
}
|
|
12378
|
-
const
|
|
12379
|
-
if (
|
|
12380
|
-
throw new SphereError(
|
|
12565
|
+
const kind = p.kind ?? "coin";
|
|
12566
|
+
if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
|
|
12567
|
+
throw new SphereError(
|
|
12568
|
+
`unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
|
|
12569
|
+
"VALIDATION_ERROR"
|
|
12570
|
+
);
|
|
12381
12571
|
}
|
|
12382
|
-
return p;
|
|
12572
|
+
return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
|
|
12383
12573
|
}
|
|
12384
12574
|
|
|
12385
12575
|
// modules/payments-v2/convergence.ts
|
|
@@ -12585,13 +12775,24 @@ async function openRow(entry, journal, decryptPayload) {
|
|
|
12585
12775
|
transferId: entry.transferId,
|
|
12586
12776
|
kind: "open",
|
|
12587
12777
|
recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
|
|
12588
|
-
|
|
12589
|
-
amount: typeof payload?.amount === "string" ? payload.amount : "",
|
|
12778
|
+
...subject(payload),
|
|
12590
12779
|
legs: { certified, total: Math.max(total, certified) },
|
|
12591
12780
|
deliveryPending: journal.length > 0,
|
|
12592
12781
|
createdAt: entry.createdAt
|
|
12593
12782
|
};
|
|
12594
12783
|
}
|
|
12784
|
+
function subject(payload) {
|
|
12785
|
+
const rawKind = payload?.kind;
|
|
12786
|
+
if (rawKind === "whole" || rawKind === "coinless") {
|
|
12787
|
+
const tokenId = payload?.direct?.[0];
|
|
12788
|
+
return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
|
|
12789
|
+
}
|
|
12790
|
+
const coin = payload;
|
|
12791
|
+
return {
|
|
12792
|
+
coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
|
|
12793
|
+
amount: typeof coin?.amount === "string" ? coin.amount : ""
|
|
12794
|
+
};
|
|
12795
|
+
}
|
|
12595
12796
|
function journalOnlyRow(transferId, entries) {
|
|
12596
12797
|
const certified = distinctOpCount(entries);
|
|
12597
12798
|
return {
|
|
@@ -12621,6 +12822,263 @@ function distinctOpCount(entries) {
|
|
|
12621
12822
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
12622
12823
|
}
|
|
12623
12824
|
|
|
12825
|
+
// modules/payments-v2/inventory/token-data.ts
|
|
12826
|
+
async function readTokenData(deps, tokenId) {
|
|
12827
|
+
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
12828
|
+
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
12829
|
+
}
|
|
12830
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
12831
|
+
if (bytes === void 0) {
|
|
12832
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
12833
|
+
}
|
|
12834
|
+
return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
|
|
12835
|
+
}
|
|
12836
|
+
|
|
12837
|
+
// modules/payments-v2/machine/payload.ts
|
|
12838
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12839
|
+
return {
|
|
12840
|
+
v: 2,
|
|
12841
|
+
kind: "coin",
|
|
12842
|
+
recipient: recipientPubkey,
|
|
12843
|
+
coinId: request.coinId,
|
|
12844
|
+
amount: request.amount,
|
|
12845
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12846
|
+
direct: [...spend.plan.direct],
|
|
12847
|
+
...spend.plan.split !== void 0 ? {
|
|
12848
|
+
split: {
|
|
12849
|
+
tokenId: spend.plan.split.tokenId,
|
|
12850
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12851
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12852
|
+
}
|
|
12853
|
+
} : {},
|
|
12854
|
+
spentStates
|
|
12855
|
+
};
|
|
12856
|
+
}
|
|
12857
|
+
function messageOf(err) {
|
|
12858
|
+
return err instanceof Error ? err.message : String(err);
|
|
12859
|
+
}
|
|
12860
|
+
function buildWholePayload(recipientPubkey, request, spentStates) {
|
|
12861
|
+
return {
|
|
12862
|
+
v: 2,
|
|
12863
|
+
kind: "whole",
|
|
12864
|
+
recipient: recipientPubkey,
|
|
12865
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12866
|
+
direct: [request.tokenId],
|
|
12867
|
+
spentStates
|
|
12868
|
+
};
|
|
12869
|
+
}
|
|
12870
|
+
|
|
12871
|
+
// modules/payments-v2/mint-params.ts
|
|
12872
|
+
function mintParams(recipientPubkey, coinId, amount) {
|
|
12873
|
+
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
12874
|
+
}
|
|
12875
|
+
|
|
12876
|
+
// modules/payments-v2/mint.ts
|
|
12877
|
+
function runMintUnderJournal(deps, input) {
|
|
12878
|
+
return mintUnderJournal(deps, input);
|
|
12879
|
+
}
|
|
12880
|
+
async function mintUnderJournal(deps, input) {
|
|
12881
|
+
const { mintId, coinId, amount } = input;
|
|
12882
|
+
const engine = deps.engine;
|
|
12883
|
+
const entry = {
|
|
12884
|
+
mintId,
|
|
12885
|
+
coinId,
|
|
12886
|
+
amount: amount.toString(),
|
|
12887
|
+
tokenId: "",
|
|
12888
|
+
createdAt: deps.now()
|
|
12889
|
+
};
|
|
12890
|
+
await deps.mintJournal.upsert(entry);
|
|
12891
|
+
let token;
|
|
12892
|
+
try {
|
|
12893
|
+
token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
|
|
12894
|
+
} catch (err) {
|
|
12895
|
+
deps.armHeartbeat();
|
|
12896
|
+
return { success: false, error: messageOf(err) };
|
|
12897
|
+
}
|
|
12898
|
+
if (token.blob.tokenId !== entry.tokenId) {
|
|
12899
|
+
await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
12900
|
+
}
|
|
12901
|
+
try {
|
|
12902
|
+
await finalizeMint(deps, mintId, token, coinId, amount.toString());
|
|
12903
|
+
} catch (err) {
|
|
12904
|
+
deps.armHeartbeat();
|
|
12905
|
+
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
12906
|
+
}
|
|
12907
|
+
return { success: true, tokenId: token.blob.tokenId };
|
|
12908
|
+
}
|
|
12909
|
+
async function finalizeMint(deps, mintId, token, coinId, amount) {
|
|
12910
|
+
const engine = deps.engine;
|
|
12911
|
+
const bytes = token.blob.token;
|
|
12912
|
+
const digest = bytesToHex3(sha256(bytes));
|
|
12913
|
+
const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
|
|
12914
|
+
const key = keys.get(digest);
|
|
12915
|
+
if (key === void 0) {
|
|
12916
|
+
throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
|
|
12917
|
+
}
|
|
12918
|
+
await deps.storagePort.applyDelta({
|
|
12919
|
+
transferId: mintId,
|
|
12920
|
+
spent: [],
|
|
12921
|
+
added: [{ tokenId: token.blob.tokenId, key }]
|
|
12922
|
+
});
|
|
12923
|
+
await deps.recordMint({
|
|
12924
|
+
tokenId: token.blob.tokenId,
|
|
12925
|
+
assets: [{ coinId, amount }]
|
|
12926
|
+
});
|
|
12927
|
+
await deps.mintJournal.removeByKey(mintId);
|
|
12928
|
+
deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
12929
|
+
deps.refreshView();
|
|
12930
|
+
}
|
|
12931
|
+
|
|
12932
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
12933
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12934
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12935
|
+
return {
|
|
12936
|
+
id: tokenId,
|
|
12937
|
+
coinId: asset.coinId,
|
|
12938
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
12939
|
+
name: registry.getName(asset.coinId),
|
|
12940
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
12941
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12942
|
+
amount: asset.amount,
|
|
12943
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
12944
|
+
createdAt: snapshot.createdAt,
|
|
12945
|
+
updatedAt: snapshot.updatedAt,
|
|
12946
|
+
lazy: true,
|
|
12947
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12948
|
+
};
|
|
12949
|
+
}
|
|
12950
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
12951
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12952
|
+
for (const [tokenId, entry] of entries) {
|
|
12953
|
+
const moving = isTransferring(tokenId);
|
|
12954
|
+
for (const asset of entry.assets) {
|
|
12955
|
+
let group = groups.get(asset.coinId);
|
|
12956
|
+
if (!group) {
|
|
12957
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12958
|
+
groups.set(asset.coinId, group);
|
|
12959
|
+
}
|
|
12960
|
+
if (moving) {
|
|
12961
|
+
group.transferring += BigInt(asset.amount);
|
|
12962
|
+
group.transferringCount += 1;
|
|
12963
|
+
} else {
|
|
12964
|
+
group.total += BigInt(asset.amount);
|
|
12965
|
+
group.count += 1;
|
|
12966
|
+
}
|
|
12967
|
+
}
|
|
12968
|
+
}
|
|
12969
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12970
|
+
}
|
|
12971
|
+
function toAsset(coinId, totals, registry) {
|
|
12972
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
12973
|
+
return {
|
|
12974
|
+
coinId,
|
|
12975
|
+
symbol: registry.getSymbol(coinId),
|
|
12976
|
+
name: registry.getName(coinId),
|
|
12977
|
+
decimals: registry.getDecimals(coinId),
|
|
12978
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12979
|
+
totalAmount: totals.total.toString(),
|
|
12980
|
+
tokenCount: totals.count,
|
|
12981
|
+
confirmedAmount: totals.total.toString(),
|
|
12982
|
+
unconfirmedAmount: "0",
|
|
12983
|
+
confirmedTokenCount: totals.count,
|
|
12984
|
+
unconfirmedTokenCount: 0,
|
|
12985
|
+
transferringTokenCount: totals.transferringCount,
|
|
12986
|
+
transferringAmount: totals.transferring.toString(),
|
|
12987
|
+
priceUsd: null,
|
|
12988
|
+
priceEur: null,
|
|
12989
|
+
change24h: null,
|
|
12990
|
+
fiatValueUsd: null,
|
|
12991
|
+
fiatValueEur: null
|
|
12992
|
+
};
|
|
12993
|
+
}
|
|
12994
|
+
function priceIdOf(registry, coinId) {
|
|
12995
|
+
return registry.getName(coinId).toLowerCase();
|
|
12996
|
+
}
|
|
12997
|
+
async function withPrices(raw, registry, price) {
|
|
12998
|
+
try {
|
|
12999
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
13000
|
+
const quotes = await price.getPrices(ids);
|
|
13001
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
13002
|
+
} catch {
|
|
13003
|
+
return raw;
|
|
13004
|
+
}
|
|
13005
|
+
}
|
|
13006
|
+
function priceOne(asset, quote) {
|
|
13007
|
+
if (!quote) return asset;
|
|
13008
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
13009
|
+
return {
|
|
13010
|
+
...asset,
|
|
13011
|
+
priceUsd: quote.priceUsd,
|
|
13012
|
+
priceEur: quote.priceEur ?? null,
|
|
13013
|
+
change24h: quote.change24h ?? null,
|
|
13014
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
13015
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
13016
|
+
};
|
|
13017
|
+
}
|
|
13018
|
+
function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
13019
|
+
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
13020
|
+
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
13021
|
+
transferring: true,
|
|
13022
|
+
suspectedSpent: false
|
|
13023
|
+
});
|
|
13024
|
+
}
|
|
13025
|
+
|
|
13026
|
+
// modules/payments-v2/send-whole.ts
|
|
13027
|
+
async function materializeWholeSpend(deps, input) {
|
|
13028
|
+
const { engine } = deps;
|
|
13029
|
+
const tokenId = input.request.tokenId;
|
|
13030
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
13031
|
+
if (bytes === void 0) {
|
|
13032
|
+
throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
13033
|
+
}
|
|
13034
|
+
const token = await engine.decodeToken({ tokenId, token: bytes });
|
|
13035
|
+
if (token.valueEnvelope === "bare_collection") {
|
|
13036
|
+
throw new SphereError(
|
|
13037
|
+
`Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
|
|
13038
|
+
"VALIDATION_ERROR"
|
|
13039
|
+
);
|
|
13040
|
+
}
|
|
13041
|
+
if (input.requireCoinless && token.value !== null) {
|
|
13042
|
+
throw new SphereError(
|
|
13043
|
+
`Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
|
|
13044
|
+
"VALIDATION_ERROR"
|
|
13045
|
+
);
|
|
13046
|
+
}
|
|
13047
|
+
const keys = await engine.deliveryKeys(bytes);
|
|
13048
|
+
const payload = buildWholePayload(input.recipientPubkey, input.request, {
|
|
13049
|
+
[tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
|
|
13050
|
+
});
|
|
13051
|
+
const plan = {
|
|
13052
|
+
transferId: input.transferId,
|
|
13053
|
+
recipientPubkey: input.recipientPubkey,
|
|
13054
|
+
payload,
|
|
13055
|
+
ops: buildOps(payload),
|
|
13056
|
+
sources: /* @__PURE__ */ new Map([[tokenId, token]]),
|
|
13057
|
+
...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
|
|
13058
|
+
};
|
|
13059
|
+
const sourceTokens = (token.value?.assets ?? []).map(
|
|
13060
|
+
(asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
|
|
13061
|
+
);
|
|
13062
|
+
return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
|
|
13063
|
+
}
|
|
13064
|
+
|
|
13065
|
+
// modules/payments-v2/send-errors.ts
|
|
13066
|
+
function partialize(err, run) {
|
|
13067
|
+
if (run.delivered.length === 0) return err;
|
|
13068
|
+
return new PartialSendConflictError(
|
|
13069
|
+
"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.",
|
|
13070
|
+
run.firstPartialId ?? "",
|
|
13071
|
+
[...run.delivered],
|
|
13072
|
+
run.amount,
|
|
13073
|
+
err
|
|
13074
|
+
);
|
|
13075
|
+
}
|
|
13076
|
+
function stampTransferId(err, transferId) {
|
|
13077
|
+
if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
|
|
13078
|
+
err.transferId = transferId;
|
|
13079
|
+
}
|
|
13080
|
+
}
|
|
13081
|
+
|
|
12624
13082
|
// modules/payments-v2/recipient.ts
|
|
12625
13083
|
var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
|
|
12626
13084
|
async function requireSameNetworkRecipient(deps, identifier) {
|
|
@@ -12669,11 +13127,6 @@ async function splitOpIndex(deps, envelope) {
|
|
|
12669
13127
|
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
12670
13128
|
}
|
|
12671
13129
|
|
|
12672
|
-
// modules/payments-v2/mint-params.ts
|
|
12673
|
-
function mintParams(recipientPubkey, coinId, amount) {
|
|
12674
|
-
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
12675
|
-
}
|
|
12676
|
-
|
|
12677
13130
|
// modules/payments-v2/prewarm-cache.ts
|
|
12678
13131
|
var PrewarmCache = class _PrewarmCache {
|
|
12679
13132
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -12753,124 +13206,15 @@ async function takeSourceBlobs(deps, sourceIds) {
|
|
|
12753
13206
|
return blobs;
|
|
12754
13207
|
}
|
|
12755
13208
|
|
|
12756
|
-
// modules/payments-v2/
|
|
12757
|
-
function
|
|
12758
|
-
|
|
12759
|
-
return {
|
|
12760
|
-
id: tokenId,
|
|
12761
|
-
coinId: asset.coinId,
|
|
12762
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
12763
|
-
name: registry.getName(asset.coinId),
|
|
12764
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
12765
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12766
|
-
amount: asset.amount,
|
|
12767
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
12768
|
-
createdAt: snapshot.createdAt,
|
|
12769
|
-
updatedAt: snapshot.updatedAt,
|
|
12770
|
-
lazy: true,
|
|
12771
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12772
|
-
};
|
|
12773
|
-
}
|
|
12774
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
12775
|
-
const groups = /* @__PURE__ */ new Map();
|
|
12776
|
-
for (const [tokenId, entry] of entries) {
|
|
12777
|
-
const moving = isTransferring(tokenId);
|
|
12778
|
-
for (const asset of entry.assets) {
|
|
12779
|
-
let group = groups.get(asset.coinId);
|
|
12780
|
-
if (!group) {
|
|
12781
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12782
|
-
groups.set(asset.coinId, group);
|
|
12783
|
-
}
|
|
12784
|
-
if (moving) {
|
|
12785
|
-
group.transferring += BigInt(asset.amount);
|
|
12786
|
-
group.transferringCount += 1;
|
|
12787
|
-
} else {
|
|
12788
|
-
group.total += BigInt(asset.amount);
|
|
12789
|
-
group.count += 1;
|
|
12790
|
-
}
|
|
12791
|
-
}
|
|
12792
|
-
}
|
|
12793
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12794
|
-
}
|
|
12795
|
-
function toAsset(coinId, totals, registry) {
|
|
12796
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
12797
|
-
return {
|
|
12798
|
-
coinId,
|
|
12799
|
-
symbol: registry.getSymbol(coinId),
|
|
12800
|
-
name: registry.getName(coinId),
|
|
12801
|
-
decimals: registry.getDecimals(coinId),
|
|
12802
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12803
|
-
totalAmount: totals.total.toString(),
|
|
12804
|
-
tokenCount: totals.count,
|
|
12805
|
-
confirmedAmount: totals.total.toString(),
|
|
12806
|
-
unconfirmedAmount: "0",
|
|
12807
|
-
confirmedTokenCount: totals.count,
|
|
12808
|
-
unconfirmedTokenCount: 0,
|
|
12809
|
-
transferringTokenCount: totals.transferringCount,
|
|
12810
|
-
transferringAmount: totals.transferring.toString(),
|
|
12811
|
-
priceUsd: null,
|
|
12812
|
-
priceEur: null,
|
|
12813
|
-
change24h: null,
|
|
12814
|
-
fiatValueUsd: null,
|
|
12815
|
-
fiatValueEur: null
|
|
12816
|
-
};
|
|
12817
|
-
}
|
|
12818
|
-
function priceIdOf(registry, coinId) {
|
|
12819
|
-
return registry.getName(coinId).toLowerCase();
|
|
12820
|
-
}
|
|
12821
|
-
async function withPrices(raw, registry, price) {
|
|
12822
|
-
try {
|
|
12823
|
-
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12824
|
-
const quotes = await price.getPrices(ids);
|
|
12825
|
-
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12826
|
-
} catch {
|
|
12827
|
-
return raw;
|
|
12828
|
-
}
|
|
12829
|
-
}
|
|
12830
|
-
function priceOne(asset, quote) {
|
|
12831
|
-
if (!quote) return asset;
|
|
12832
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12833
|
-
return {
|
|
12834
|
-
...asset,
|
|
12835
|
-
priceUsd: quote.priceUsd,
|
|
12836
|
-
priceEur: quote.priceEur ?? null,
|
|
12837
|
-
change24h: quote.change24h ?? null,
|
|
12838
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
12839
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12840
|
-
};
|
|
12841
|
-
}
|
|
12842
|
-
function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
12843
|
-
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
12844
|
-
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
12845
|
-
transferring: true,
|
|
12846
|
-
suspectedSpent: false
|
|
12847
|
-
});
|
|
12848
|
-
}
|
|
12849
|
-
|
|
12850
|
-
// modules/payments-v2/machine/payload.ts
|
|
12851
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12852
|
-
return {
|
|
12853
|
-
v: 2,
|
|
12854
|
-
recipient: recipientPubkey,
|
|
12855
|
-
coinId: request.coinId,
|
|
12856
|
-
amount: request.amount,
|
|
12857
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12858
|
-
direct: [...spend.plan.direct],
|
|
12859
|
-
...spend.plan.split !== void 0 ? {
|
|
12860
|
-
split: {
|
|
12861
|
-
tokenId: spend.plan.split.tokenId,
|
|
12862
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12863
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12864
|
-
}
|
|
12865
|
-
} : {},
|
|
12866
|
-
spentStates
|
|
12867
|
-
};
|
|
13209
|
+
// modules/payments-v2/history/History.ts
|
|
13210
|
+
function wireAssets(assets) {
|
|
13211
|
+
return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
|
|
12868
13212
|
}
|
|
12869
|
-
function
|
|
12870
|
-
|
|
13213
|
+
function isPermanentReject(err) {
|
|
13214
|
+
const status = err?.status;
|
|
13215
|
+
if (typeof status !== "number") return false;
|
|
13216
|
+
return status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
12871
13217
|
}
|
|
12872
|
-
|
|
12873
|
-
// modules/payments-v2/history/History.ts
|
|
12874
13218
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
12875
13219
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
12876
13220
|
var History = class {
|
|
@@ -12895,7 +13239,7 @@ var History = class {
|
|
|
12895
13239
|
dedupKey: input.transferId,
|
|
12896
13240
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12897
13241
|
type: "SENT",
|
|
12898
|
-
assets:
|
|
13242
|
+
assets: wireAssets(input.assets),
|
|
12899
13243
|
ts: this.ts(input.timestamp),
|
|
12900
13244
|
transferId: input.transferId,
|
|
12901
13245
|
...this.wireTokenId(input.tokenId),
|
|
@@ -12908,7 +13252,7 @@ var History = class {
|
|
|
12908
13252
|
dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
|
|
12909
13253
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12910
13254
|
type: "RECEIVED",
|
|
12911
|
-
assets:
|
|
13255
|
+
assets: wireAssets(input.assets),
|
|
12912
13256
|
ts: this.ts(input.timestamp),
|
|
12913
13257
|
...input.transferId !== void 0 ? { transferId: input.transferId } : {},
|
|
12914
13258
|
...this.wireTokenId(input.tokenId),
|
|
@@ -12921,7 +13265,7 @@ var History = class {
|
|
|
12921
13265
|
dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
|
|
12922
13266
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12923
13267
|
type: "MINT",
|
|
12924
|
-
assets:
|
|
13268
|
+
assets: wireAssets(input.assets),
|
|
12925
13269
|
ts: this.ts(input.timestamp),
|
|
12926
13270
|
...this.wireTokenId(input.tokenId)
|
|
12927
13271
|
}));
|
|
@@ -12932,7 +13276,10 @@ var History = class {
|
|
|
12932
13276
|
try {
|
|
12933
13277
|
await this.deps.client.postHistory([record]);
|
|
12934
13278
|
} catch (err) {
|
|
12935
|
-
this.deps.log?.(
|
|
13279
|
+
this.deps.log?.(
|
|
13280
|
+
isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
|
|
13281
|
+
err
|
|
13282
|
+
);
|
|
12936
13283
|
return;
|
|
12937
13284
|
}
|
|
12938
13285
|
this.deps.emit("history:updated", this.toEntry(record));
|
|
@@ -12999,6 +13346,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
|
12999
13346
|
function stateKey(tokenId, stateHash) {
|
|
13000
13347
|
return `${tokenId}:${stateHash}`;
|
|
13001
13348
|
}
|
|
13349
|
+
function unchanged(prev, item, tokenType) {
|
|
13350
|
+
return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
|
|
13351
|
+
}
|
|
13352
|
+
function isCoinless(item, prev) {
|
|
13353
|
+
if (item.status !== "active") return prev?.coinless ?? false;
|
|
13354
|
+
return (item.assets?.length ?? 0) === 0;
|
|
13355
|
+
}
|
|
13002
13356
|
var InventoryView = class {
|
|
13003
13357
|
constructor(deps) {
|
|
13004
13358
|
this.deps = deps;
|
|
@@ -13092,14 +13446,26 @@ var InventoryView = class {
|
|
|
13092
13446
|
pool(coinId) {
|
|
13093
13447
|
const out = [];
|
|
13094
13448
|
for (const [tokenId, entry] of this.mirror) {
|
|
13095
|
-
if (
|
|
13096
|
-
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
13097
|
-
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
13449
|
+
if (!this.isSpendable(tokenId, entry)) continue;
|
|
13098
13450
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
13099
13451
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
13100
13452
|
}
|
|
13101
13453
|
return out;
|
|
13102
13454
|
}
|
|
13455
|
+
/**
|
|
13456
|
+
* The eligibility gates every spend shares, coin or token — ONE definition, so a
|
|
13457
|
+
* single probe covers both verbs and neither can drift from the other.
|
|
13458
|
+
*/
|
|
13459
|
+
isSpendable(tokenId, entry) {
|
|
13460
|
+
if (entry.status !== "active") return false;
|
|
13461
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
|
|
13462
|
+
return !this.suspected.has(stateKey(tokenId, entry.stateHash));
|
|
13463
|
+
}
|
|
13464
|
+
/** Is this NAMED token spendable whole — coinless or valued (#777)? */
|
|
13465
|
+
spendableToken(tokenId) {
|
|
13466
|
+
const entry = this.mirror.get(tokenId);
|
|
13467
|
+
return entry !== void 0 && this.isSpendable(tokenId, entry);
|
|
13468
|
+
}
|
|
13103
13469
|
/**
|
|
13104
13470
|
* Current mirrored state of an ACTIVE token, or undefined when it is unknown
|
|
13105
13471
|
* or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
|
|
@@ -13125,6 +13491,29 @@ var InventoryView = class {
|
|
|
13125
13491
|
}
|
|
13126
13492
|
return out;
|
|
13127
13493
|
}
|
|
13494
|
+
/**
|
|
13495
|
+
* Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
|
|
13496
|
+
* exactly when it is not, so no mirror row can appear in both reads.
|
|
13497
|
+
*/
|
|
13498
|
+
coinless(registry) {
|
|
13499
|
+
const out = [];
|
|
13500
|
+
for (const [tokenId, entry] of this.mirror) {
|
|
13501
|
+
if (entry.status !== "active" || !entry.coinless) continue;
|
|
13502
|
+
const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
|
|
13503
|
+
out.push({
|
|
13504
|
+
tokenId,
|
|
13505
|
+
...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
|
|
13506
|
+
...meta ? { name: meta.name } : {},
|
|
13507
|
+
...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
|
|
13508
|
+
stateHash: entry.stateHash,
|
|
13509
|
+
transferring: this.held(tokenId),
|
|
13510
|
+
...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
|
|
13511
|
+
createdAt: entry.createdAt,
|
|
13512
|
+
updatedAt: entry.updatedAt
|
|
13513
|
+
});
|
|
13514
|
+
}
|
|
13515
|
+
return out;
|
|
13516
|
+
}
|
|
13128
13517
|
async assets(registry, price) {
|
|
13129
13518
|
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
13130
13519
|
if (!price || raw.length === 0) return raw;
|
|
@@ -13196,14 +13585,19 @@ var InventoryView = class {
|
|
|
13196
13585
|
applyOne(item, now) {
|
|
13197
13586
|
const prev = this.mirror.get(item.tokenId);
|
|
13198
13587
|
if (prev && item.seq < prev.seq) return false;
|
|
13199
|
-
|
|
13200
|
-
|
|
13201
|
-
}
|
|
13588
|
+
const tokenType = item.tokenType ?? prev?.tokenType;
|
|
13589
|
+
if (prev && unchanged(prev, item, tokenType)) return false;
|
|
13202
13590
|
this.mirror.set(item.tokenId, {
|
|
13203
13591
|
stateHash: item.stateHash,
|
|
13204
13592
|
seq: item.seq,
|
|
13205
13593
|
status: item.status,
|
|
13206
|
-
|
|
13594
|
+
// ONLY a tombstone inherits: it omits assets for an unrelated reason and
|
|
13595
|
+
// recoverRemoved must know the amount it restores. An ACTIVE row that omits
|
|
13596
|
+
// them is stating coinlessness, and inheriting there would keep stale assets
|
|
13597
|
+
// in tokens() while `coinless` is true — the row in BOTH reads (§16).
|
|
13598
|
+
assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
|
|
13599
|
+
coinless: isCoinless(item, prev),
|
|
13600
|
+
...tokenType !== void 0 ? { tokenType } : {},
|
|
13207
13601
|
createdAt: prev?.createdAt ?? now,
|
|
13208
13602
|
updatedAt: now
|
|
13209
13603
|
});
|
|
@@ -13439,7 +13833,12 @@ async function screen(deps, engine, entry) {
|
|
|
13439
13833
|
}
|
|
13440
13834
|
return {
|
|
13441
13835
|
kind: "accept",
|
|
13442
|
-
record: {
|
|
13836
|
+
record: {
|
|
13837
|
+
tokenId: keys.tokenId,
|
|
13838
|
+
stateHash: keys.stateHash,
|
|
13839
|
+
assets: toAssetAmounts(token),
|
|
13840
|
+
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
|
|
13841
|
+
}
|
|
13443
13842
|
};
|
|
13444
13843
|
}
|
|
13445
13844
|
async function announce(deps, entry, record) {
|
|
@@ -13450,6 +13849,7 @@ async function announce(deps, entry, record) {
|
|
|
13450
13849
|
tokenId: record.tokenId,
|
|
13451
13850
|
stateHash: record.stateHash,
|
|
13452
13851
|
assets: record.assets,
|
|
13852
|
+
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
13453
13853
|
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
13454
13854
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13455
13855
|
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
@@ -13463,6 +13863,19 @@ async function announce(deps, entry, record) {
|
|
|
13463
13863
|
senderPubkey: entry.senderPubkey ?? "",
|
|
13464
13864
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13465
13865
|
tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
|
|
13866
|
+
// #777: named here rather than mapped from assets, which announced an EMPTY list.
|
|
13867
|
+
...record.tokenType !== void 0 ? {
|
|
13868
|
+
coinless: [
|
|
13869
|
+
{
|
|
13870
|
+
tokenId: record.tokenId,
|
|
13871
|
+
tokenType: record.tokenType,
|
|
13872
|
+
stateHash: record.stateHash,
|
|
13873
|
+
transferring: false,
|
|
13874
|
+
createdAt: receivedAt,
|
|
13875
|
+
updatedAt: receivedAt
|
|
13876
|
+
}
|
|
13877
|
+
]
|
|
13878
|
+
} : {},
|
|
13466
13879
|
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
13467
13880
|
receivedAt
|
|
13468
13881
|
};
|
|
@@ -14128,6 +14541,34 @@ var SpendQueue = class {
|
|
|
14128
14541
|
}
|
|
14129
14542
|
return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
|
|
14130
14543
|
}
|
|
14544
|
+
/**
|
|
14545
|
+
* #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
|
|
14546
|
+
* for. A coin plan waits because some OTHER combination may free up; a named
|
|
14547
|
+
* token is either free now or held by a reservation that only its own transfer
|
|
14548
|
+
* can release, so queueing would block until a timeout on a spend that cannot
|
|
14549
|
+
* become possible.
|
|
14550
|
+
*/
|
|
14551
|
+
planWhole(reservationId, tokenId) {
|
|
14552
|
+
if (this.destroyed) {
|
|
14553
|
+
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
14554
|
+
}
|
|
14555
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
14556
|
+
if (unproven !== null) {
|
|
14557
|
+
throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
|
|
14558
|
+
}
|
|
14559
|
+
if (this.deps.spendableToken?.(tokenId) !== true) {
|
|
14560
|
+
throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
|
|
14561
|
+
}
|
|
14562
|
+
const holder = this.deps.ledger.holderOf(tokenId);
|
|
14563
|
+
if (holder !== void 0) {
|
|
14564
|
+
throw new SphereError(
|
|
14565
|
+
`Token ${tokenId} is already reserved by transfer ${holder}`,
|
|
14566
|
+
"VALIDATION_ERROR"
|
|
14567
|
+
);
|
|
14568
|
+
}
|
|
14569
|
+
this.deps.ledger.reserve(reservationId, [{ tokenId }]);
|
|
14570
|
+
return { reservationId, plan: { direct: [tokenId] } };
|
|
14571
|
+
}
|
|
14131
14572
|
/**
|
|
14132
14573
|
* The sources `plan()` WOULD pick right now, reserving nothing and queueing
|
|
14133
14574
|
* nothing — a read-only probe used to warm blobs while the user is still
|
|
@@ -14317,6 +14758,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
14317
14758
|
const queue = new SpendQueue({
|
|
14318
14759
|
ledger,
|
|
14319
14760
|
getPool: (coinId) => view.pool(coinId),
|
|
14761
|
+
spendableToken: (tokenId) => view.spendableToken(tokenId),
|
|
14320
14762
|
...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
|
|
14321
14763
|
});
|
|
14322
14764
|
const historyStore = new History({
|
|
@@ -14397,13 +14839,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
14397
14839
|
...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
|
|
14398
14840
|
emit: deps.emit,
|
|
14399
14841
|
now: deps.now ?? Date.now,
|
|
14400
|
-
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
14842
|
+
recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
|
|
14401
14843
|
await historyStore.recordSent({
|
|
14402
14844
|
transferId,
|
|
14403
|
-
|
|
14404
|
-
//
|
|
14405
|
-
|
|
14406
|
-
amount: committedAmount,
|
|
14845
|
+
// §5.9: the SETTLED amount, never payload.amount — the plan. A token
|
|
14846
|
+
// spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
|
|
14847
|
+
...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
|
|
14407
14848
|
recipientPubkey: payload.recipient,
|
|
14408
14849
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
14409
14850
|
});
|
|
@@ -14438,12 +14879,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
|
|
|
14438
14879
|
});
|
|
14439
14880
|
}
|
|
14440
14881
|
async function recordReceived(historyStore, record) {
|
|
14441
|
-
const first = record.assets[0];
|
|
14442
14882
|
await historyStore.recordReceived({
|
|
14443
14883
|
tokenId: record.tokenId,
|
|
14444
14884
|
stateHash: record.stateHash,
|
|
14445
|
-
|
|
14446
|
-
amount: first?.amount ?? "0",
|
|
14885
|
+
assets: record.assets,
|
|
14447
14886
|
...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
|
|
14448
14887
|
...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
|
|
14449
14888
|
...record.memo !== void 0 ? { memo: record.memo } : {},
|
|
@@ -14591,6 +15030,13 @@ var PaymentsFacade = class {
|
|
|
14591
15030
|
tokens(filter) {
|
|
14592
15031
|
return this.view.tokens(this.deps.registry, filter);
|
|
14593
15032
|
}
|
|
15033
|
+
coinless() {
|
|
15034
|
+
return this.view.coinless(this.deps.registry);
|
|
15035
|
+
}
|
|
15036
|
+
tokenData(tokenId) {
|
|
15037
|
+
const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
|
|
15038
|
+
return readTokenData(deps, tokenId);
|
|
15039
|
+
}
|
|
14594
15040
|
history(page) {
|
|
14595
15041
|
return this.historyStore.page(page ?? {});
|
|
14596
15042
|
}
|
|
@@ -14613,7 +15059,14 @@ var PaymentsFacade = class {
|
|
|
14613
15059
|
return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
|
|
14614
15060
|
}
|
|
14615
15061
|
send(request) {
|
|
14616
|
-
return this.track(this.
|
|
15062
|
+
return this.track(this.runJob({ kind: "coin", request }, request.amount));
|
|
15063
|
+
}
|
|
15064
|
+
sendWholeToken(request) {
|
|
15065
|
+
return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
|
|
15066
|
+
}
|
|
15067
|
+
/** NFT-scoped twin: refuses a valued source. */
|
|
15068
|
+
sendCoinless(request) {
|
|
15069
|
+
return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
|
|
14617
15070
|
}
|
|
14618
15071
|
async receive() {
|
|
14619
15072
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -14645,12 +15098,25 @@ var PaymentsFacade = class {
|
|
|
14645
15098
|
nowMs() {
|
|
14646
15099
|
return (this.deps.now ?? Date.now)();
|
|
14647
15100
|
}
|
|
15101
|
+
mintDeps() {
|
|
15102
|
+
return {
|
|
15103
|
+
engine: this.engine(),
|
|
15104
|
+
mintJournal: this.machineStores.mintJournal,
|
|
15105
|
+
storagePort: this.deps.storagePort,
|
|
15106
|
+
recordMint: (input) => this.historyStore.recordMint(input),
|
|
15107
|
+
armHeartbeat: () => this.heartbeat.arm(),
|
|
15108
|
+
noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
|
|
15109
|
+
refreshView: () => this.trackTail(this.view.delta()),
|
|
15110
|
+
ownPubkeyBytes: this.ownPubkeyBytes,
|
|
15111
|
+
now: () => this.nowMs()
|
|
15112
|
+
};
|
|
15113
|
+
}
|
|
14648
15114
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
14649
15115
|
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
14650
15116
|
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
14651
|
-
async
|
|
15117
|
+
async runJob(job, amount) {
|
|
14652
15118
|
const run = {
|
|
14653
|
-
amount
|
|
15119
|
+
amount,
|
|
14654
15120
|
delivered: [],
|
|
14655
15121
|
sentTokens: [],
|
|
14656
15122
|
tokenTransfers: [],
|
|
@@ -14660,7 +15126,7 @@ var PaymentsFacade = class {
|
|
|
14660
15126
|
lastTransferId: ""
|
|
14661
15127
|
};
|
|
14662
15128
|
try {
|
|
14663
|
-
return await this.sendWithPolicy(
|
|
15129
|
+
return await this.sendWithPolicy(job, run);
|
|
14664
15130
|
} catch (err) {
|
|
14665
15131
|
this.emitCleanFailure(err, run.lastTransferId);
|
|
14666
15132
|
throw err;
|
|
@@ -14680,19 +15146,19 @@ var PaymentsFacade = class {
|
|
|
14680
15146
|
};
|
|
14681
15147
|
this.deps.emit("transfer:updated", failed);
|
|
14682
15148
|
}
|
|
14683
|
-
async sendWithPolicy(
|
|
14684
|
-
const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
|
|
15149
|
+
async sendWithPolicy(job, run) {
|
|
15150
|
+
const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
|
|
14685
15151
|
for (let attempt = 0; ; attempt++) {
|
|
14686
15152
|
let ctx;
|
|
14687
15153
|
try {
|
|
14688
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey,
|
|
15154
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
|
|
14689
15155
|
} catch (err) {
|
|
14690
|
-
throw
|
|
15156
|
+
throw partialize(err, run);
|
|
14691
15157
|
}
|
|
14692
15158
|
const disposition = await this.runAttempt(ctx);
|
|
14693
|
-
if (disposition.kind === "rethrow") throw
|
|
15159
|
+
if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
|
|
14694
15160
|
if (disposition.kind === "retry-full") {
|
|
14695
|
-
if (attempt >= MAX_RESELECT) throw
|
|
15161
|
+
if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
|
|
14696
15162
|
continue;
|
|
14697
15163
|
}
|
|
14698
15164
|
if (disposition.kind === "success") {
|
|
@@ -14748,7 +15214,7 @@ var PaymentsFacade = class {
|
|
|
14748
15214
|
async disposeFailedAttempt(ctx, err) {
|
|
14749
15215
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
14750
15216
|
this.settleKeepOpen(ctx);
|
|
14751
|
-
|
|
15217
|
+
stampTransferId(err, ctx.transferId);
|
|
14752
15218
|
return { kind: "rethrow", error: err };
|
|
14753
15219
|
}
|
|
14754
15220
|
const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
|
|
@@ -14782,7 +15248,7 @@ var PaymentsFacade = class {
|
|
|
14782
15248
|
async disposeConvergeFailure(ctx, err) {
|
|
14783
15249
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
14784
15250
|
this.settleKeepOpen(ctx);
|
|
14785
|
-
|
|
15251
|
+
stampTransferId(err, ctx.transferId);
|
|
14786
15252
|
return { kind: "rethrow", error: err };
|
|
14787
15253
|
}
|
|
14788
15254
|
if (classifyError(err) === "conflict") {
|
|
@@ -14793,9 +15259,29 @@ var PaymentsFacade = class {
|
|
|
14793
15259
|
}
|
|
14794
15260
|
return { kind: "rethrow", error: err };
|
|
14795
15261
|
}
|
|
14796
|
-
async planAndMaterialize(recipientPubkey,
|
|
15262
|
+
async planAndMaterialize(recipientPubkey, job, run) {
|
|
14797
15263
|
const transferId = this.newId();
|
|
14798
15264
|
run.lastTransferId = transferId;
|
|
15265
|
+
if (job.kind === "whole") {
|
|
15266
|
+
const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
|
|
15267
|
+
const sourceIds2 = this.markPlanned(transferId, "", spend2);
|
|
15268
|
+
try {
|
|
15269
|
+
return await materializeWholeSpend(
|
|
15270
|
+
{ engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
|
|
15271
|
+
{
|
|
15272
|
+
transferId,
|
|
15273
|
+
recipientPubkey,
|
|
15274
|
+
request: job.request,
|
|
15275
|
+
sourceIds: sourceIds2,
|
|
15276
|
+
requireCoinless: job.requireCoinless
|
|
15277
|
+
}
|
|
15278
|
+
);
|
|
15279
|
+
} catch (err) {
|
|
15280
|
+
this.settleFailure(transferId, "", sourceIds2);
|
|
15281
|
+
throw err;
|
|
15282
|
+
}
|
|
15283
|
+
}
|
|
15284
|
+
const request = { ...job.request, amount: run.amount };
|
|
14799
15285
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
14800
15286
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
14801
15287
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -14929,22 +15415,6 @@ var PaymentsFacade = class {
|
|
|
14929
15415
|
return result;
|
|
14930
15416
|
}
|
|
14931
15417
|
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
14932
|
-
partialize(err, run) {
|
|
14933
|
-
if (run.delivered.length === 0) return err;
|
|
14934
|
-
return new PartialSendConflictError(
|
|
14935
|
-
"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.",
|
|
14936
|
-
run.firstPartialId ?? "",
|
|
14937
|
-
run.delivered,
|
|
14938
|
-
run.amount,
|
|
14939
|
-
err
|
|
14940
|
-
);
|
|
14941
|
-
}
|
|
14942
|
-
/** #441: possibly-committed errors must carry the transferId for the settling journal. */
|
|
14943
|
-
stampTransferId(err, transferId) {
|
|
14944
|
-
if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
|
|
14945
|
-
err.transferId = transferId;
|
|
14946
|
-
}
|
|
14947
|
-
}
|
|
14948
15418
|
async softAbort(transferId) {
|
|
14949
15419
|
try {
|
|
14950
15420
|
await this.deps.client.abortIntent(transferId);
|
|
@@ -14960,57 +15430,11 @@ var PaymentsFacade = class {
|
|
|
14960
15430
|
const mintId = this.newId();
|
|
14961
15431
|
this.activeMoneyOps.add(mintId);
|
|
14962
15432
|
try {
|
|
14963
|
-
return await this.
|
|
15433
|
+
return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
|
|
14964
15434
|
} finally {
|
|
14965
15435
|
this.activeMoneyOps.delete(mintId);
|
|
14966
15436
|
}
|
|
14967
15437
|
}
|
|
14968
|
-
async mintUnderJournal(mintId, coinId, amount) {
|
|
14969
|
-
const engine = this.engine();
|
|
14970
|
-
const entry = {
|
|
14971
|
-
mintId,
|
|
14972
|
-
coinId,
|
|
14973
|
-
amount: amount.toString(),
|
|
14974
|
-
tokenId: "",
|
|
14975
|
-
createdAt: this.nowMs()
|
|
14976
|
-
};
|
|
14977
|
-
await this.machineStores.mintJournal.upsert(entry);
|
|
14978
|
-
let token;
|
|
14979
|
-
try {
|
|
14980
|
-
token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
|
|
14981
|
-
} catch (err) {
|
|
14982
|
-
this.heartbeat.arm();
|
|
14983
|
-
return { success: false, error: messageOf(err) };
|
|
14984
|
-
}
|
|
14985
|
-
if (token.blob.tokenId !== entry.tokenId) {
|
|
14986
|
-
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
14987
|
-
}
|
|
14988
|
-
try {
|
|
14989
|
-
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
14990
|
-
} catch (err) {
|
|
14991
|
-
this.heartbeat.arm();
|
|
14992
|
-
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
14993
|
-
}
|
|
14994
|
-
return { success: true, tokenId: token.blob.tokenId };
|
|
14995
|
-
}
|
|
14996
|
-
async finalizeMint(engine, mintId, token, coinId, amount) {
|
|
14997
|
-
const bytes = token.blob.token;
|
|
14998
|
-
const digest = bytesToHex3(sha256(bytes));
|
|
14999
|
-
const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
|
|
15000
|
-
const key = keys.get(digest);
|
|
15001
|
-
if (key === void 0) {
|
|
15002
|
-
throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
|
|
15003
|
-
}
|
|
15004
|
-
await this.deps.storagePort.applyDelta({
|
|
15005
|
-
transferId: mintId,
|
|
15006
|
-
spent: [],
|
|
15007
|
-
added: [{ tokenId: token.blob.tokenId, key }]
|
|
15008
|
-
});
|
|
15009
|
-
await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
|
|
15010
|
-
await this.machineStores.mintJournal.removeByKey(mintId);
|
|
15011
|
-
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
15012
|
-
this.trackTail(this.view.delta());
|
|
15013
|
-
}
|
|
15014
15438
|
/** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
|
|
15015
15439
|
async replayMints() {
|
|
15016
15440
|
let resolved = 0;
|
|
@@ -15042,7 +15466,7 @@ var PaymentsFacade = class {
|
|
|
15042
15466
|
if (token.blob.tokenId !== entry.tokenId) {
|
|
15043
15467
|
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
15044
15468
|
}
|
|
15045
|
-
await this.
|
|
15469
|
+
await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
|
|
15046
15470
|
return true;
|
|
15047
15471
|
}
|
|
15048
15472
|
async tokenInServerInventory(tokenId) {
|