@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.cjs
CHANGED
|
@@ -8212,6 +8212,8 @@ var FETCH_TIMEOUT_MS = 1e4;
|
|
|
8212
8212
|
var TokenRegistry = class _TokenRegistry {
|
|
8213
8213
|
static instance = null;
|
|
8214
8214
|
definitionsById;
|
|
8215
|
+
/** Non-fungible definitions keyed by TOKEN TYPE — a separate namespace (#147). */
|
|
8216
|
+
definitionsByType;
|
|
8215
8217
|
definitionsBySymbol;
|
|
8216
8218
|
definitionsByName;
|
|
8217
8219
|
// Remote refresh state
|
|
@@ -8230,6 +8232,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8230
8232
|
inFlight = null;
|
|
8231
8233
|
constructor() {
|
|
8232
8234
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
8235
|
+
this.definitionsByType = /* @__PURE__ */ new Map();
|
|
8233
8236
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
8234
8237
|
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
8235
8238
|
}
|
|
@@ -8457,9 +8460,11 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8457
8460
|
this.definitionsById.clear();
|
|
8458
8461
|
this.definitionsBySymbol.clear();
|
|
8459
8462
|
this.definitionsByName.clear();
|
|
8463
|
+
this.definitionsByType.clear();
|
|
8460
8464
|
for (const def of definitions) {
|
|
8461
8465
|
const idLower = def.id.toLowerCase();
|
|
8462
8466
|
this.definitionsById.set(idLower, def);
|
|
8467
|
+
if (def.assetKind === "non-fungible") this.definitionsByType.set(idLower, def);
|
|
8463
8468
|
if (def.symbol) {
|
|
8464
8469
|
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
8465
8470
|
}
|
|
@@ -8673,6 +8678,20 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8673
8678
|
getAllDefinitions() {
|
|
8674
8679
|
return Array.from(this.definitionsById.values());
|
|
8675
8680
|
}
|
|
8681
|
+
/**
|
|
8682
|
+
* Definition for a coinless token's genesis TOKEN TYPE (#777). Never falls back
|
|
8683
|
+
* to `getDefinition`: a type and a coin id live in different namespaces, so a
|
|
8684
|
+
* type that collided with a coin id would otherwise render as that coin.
|
|
8685
|
+
*/
|
|
8686
|
+
getTypeMeta(tokenType) {
|
|
8687
|
+
const def = this.getTypeDefinition(tokenType);
|
|
8688
|
+
if (!def) return null;
|
|
8689
|
+
return { name: def.name, iconUrl: def.icons?.[0]?.url ?? null };
|
|
8690
|
+
}
|
|
8691
|
+
getTypeDefinition(tokenType) {
|
|
8692
|
+
if (!tokenType) return void 0;
|
|
8693
|
+
return this.definitionsByType.get(tokenType.toLowerCase());
|
|
8694
|
+
}
|
|
8676
8695
|
/**
|
|
8677
8696
|
* Get all fungible token definitions
|
|
8678
8697
|
* @returns Array of fungible token definitions
|
|
@@ -9508,6 +9527,8 @@ var import_DataHasherFactory = require("@unicitylabs/state-transition-sdk/lib/cr
|
|
|
9508
9527
|
var import_CborSerializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js");
|
|
9509
9528
|
var import_CborDeserializer = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js");
|
|
9510
9529
|
var import_CborError = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js");
|
|
9530
|
+
var import_CborReader = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js");
|
|
9531
|
+
var import_MajorType = require("@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js");
|
|
9511
9532
|
var import_Asset = require("@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js");
|
|
9512
9533
|
var import_AssetId = require("@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js");
|
|
9513
9534
|
var import_PaymentAssetCollection = require("@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js");
|
|
@@ -9920,6 +9941,126 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
|
|
|
9920
9941
|
return burntToken;
|
|
9921
9942
|
}
|
|
9922
9943
|
|
|
9944
|
+
// token-engine/value-envelope.ts
|
|
9945
|
+
function isCoinlessEnvelope(envelope) {
|
|
9946
|
+
return envelope.startsWith("none_");
|
|
9947
|
+
}
|
|
9948
|
+
var SELF_DESCRIBED_CBOR_TAG = 55799n;
|
|
9949
|
+
var CBOR_MAJOR_TYPE_MASK = 224;
|
|
9950
|
+
var describe = (error) => error instanceof Error ? error.message : String(error);
|
|
9951
|
+
var invalid = (message) => new SphereError(message, "VALIDATION_ERROR");
|
|
9952
|
+
function majorTypeOf(bytes) {
|
|
9953
|
+
const first = bytes.at(0);
|
|
9954
|
+
return first === void 0 ? null : first & CBOR_MAJOR_TYPE_MASK;
|
|
9955
|
+
}
|
|
9956
|
+
function headTagNumber(bytes) {
|
|
9957
|
+
try {
|
|
9958
|
+
return new import_CborReader.CborReader(bytes).readLength(import_MajorType.MajorType.TAG);
|
|
9959
|
+
} catch {
|
|
9960
|
+
return null;
|
|
9961
|
+
}
|
|
9962
|
+
}
|
|
9963
|
+
function isAssetShaped(item) {
|
|
9964
|
+
try {
|
|
9965
|
+
return import_CborDeserializer.CborDeserializer.decodeArray(item, 2).every(
|
|
9966
|
+
(field) => majorTypeOf(field) === import_MajorType.MajorType.BYTE_STRING
|
|
9967
|
+
);
|
|
9968
|
+
} catch {
|
|
9969
|
+
return false;
|
|
9970
|
+
}
|
|
9971
|
+
}
|
|
9972
|
+
function arrayItemsOrNull(bytes) {
|
|
9973
|
+
try {
|
|
9974
|
+
return import_CborDeserializer.CborDeserializer.decodeArray(bytes);
|
|
9975
|
+
} catch {
|
|
9976
|
+
return null;
|
|
9977
|
+
}
|
|
9978
|
+
}
|
|
9979
|
+
function decodeSphere(genesisData) {
|
|
9980
|
+
try {
|
|
9981
|
+
return SpherePaymentData.fromCBOR(genesisData).toValue();
|
|
9982
|
+
} catch (error) {
|
|
9983
|
+
throw invalid(`Failed to decode token payment data: ${describe(error)}`);
|
|
9984
|
+
}
|
|
9985
|
+
}
|
|
9986
|
+
function taggedItemBody(genesisData) {
|
|
9987
|
+
try {
|
|
9988
|
+
return import_CborDeserializer.CborDeserializer.decodeTag(genesisData).data;
|
|
9989
|
+
} catch (error) {
|
|
9990
|
+
throw invalid(
|
|
9991
|
+
`Token value payload is a CBOR tag whose item is malformed or carries trailing bytes: ${describe(error)}`
|
|
9992
|
+
);
|
|
9993
|
+
}
|
|
9994
|
+
}
|
|
9995
|
+
function selfDescribedBodyCarriesValue(body) {
|
|
9996
|
+
const major = majorTypeOf(body);
|
|
9997
|
+
if (major === import_MajorType.MajorType.TAG) {
|
|
9998
|
+
const inner = headTagNumber(body);
|
|
9999
|
+
return inner === SpherePaymentData.CBOR_TAG || inner === SELF_DESCRIBED_CBOR_TAG;
|
|
10000
|
+
}
|
|
10001
|
+
if (major === import_MajorType.MajorType.ARRAY) {
|
|
10002
|
+
const items = arrayItemsOrNull(body);
|
|
10003
|
+
return items !== null && items.some(isAssetShaped);
|
|
10004
|
+
}
|
|
10005
|
+
return false;
|
|
10006
|
+
}
|
|
10007
|
+
function classifyTagged(genesisData) {
|
|
10008
|
+
const tag = headTagNumber(genesisData);
|
|
10009
|
+
if (tag === null) {
|
|
10010
|
+
throw invalid(
|
|
10011
|
+
"Token value payload has an unreadable or non-canonically encoded CBOR tag head \u2014 it could be a malformed SpherePaymentData envelope"
|
|
10012
|
+
);
|
|
10013
|
+
}
|
|
10014
|
+
if (tag === SpherePaymentData.CBOR_TAG) {
|
|
10015
|
+
return { envelope: "sphere", value: decodeSphere(genesisData) };
|
|
10016
|
+
}
|
|
10017
|
+
const body = taggedItemBody(genesisData);
|
|
10018
|
+
if (tag === SELF_DESCRIBED_CBOR_TAG && selfDescribedBodyCarriesValue(body)) {
|
|
10019
|
+
throw invalid(
|
|
10020
|
+
"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"
|
|
10021
|
+
);
|
|
10022
|
+
}
|
|
10023
|
+
return { envelope: "none_tag", value: null };
|
|
10024
|
+
}
|
|
10025
|
+
function classifyArray(genesisData) {
|
|
10026
|
+
const items = arrayItemsOrNull(genesisData);
|
|
10027
|
+
if (items === null) {
|
|
10028
|
+
throw invalid("Token value payload is not PaymentAssetCollection: malformed CBOR array");
|
|
10029
|
+
}
|
|
10030
|
+
return items.some(isAssetShaped) ? { envelope: "bare_collection", value: null } : { envelope: "none_other", value: null };
|
|
10031
|
+
}
|
|
10032
|
+
function classifyValueEnvelope(genesisData) {
|
|
10033
|
+
if (genesisData === null) return { envelope: "none_absent", value: null };
|
|
10034
|
+
const major = majorTypeOf(genesisData);
|
|
10035
|
+
if (major === null) return { envelope: "none_absent", value: null };
|
|
10036
|
+
if (major === import_MajorType.MajorType.TAG) return classifyTagged(genesisData);
|
|
10037
|
+
if (major === import_MajorType.MajorType.ARRAY) return classifyArray(genesisData);
|
|
10038
|
+
return { envelope: "none_other", value: null };
|
|
10039
|
+
}
|
|
10040
|
+
function wrapToken(sdkToken) {
|
|
10041
|
+
const { envelope, value } = classifyValueEnvelope(sdkToken.genesis.data);
|
|
10042
|
+
const blob = {
|
|
10043
|
+
tokenId: import_HexConverter.HexConverter.encode(sdkToken.id.bytes),
|
|
10044
|
+
token: sdkToken.toCBOR()
|
|
10045
|
+
};
|
|
10046
|
+
return {
|
|
10047
|
+
sdkToken,
|
|
10048
|
+
blob,
|
|
10049
|
+
value,
|
|
10050
|
+
valueEnvelope: envelope,
|
|
10051
|
+
tokenType: import_HexConverter.HexConverter.encode(sdkToken.type.bytes)
|
|
10052
|
+
};
|
|
10053
|
+
}
|
|
10054
|
+
function assertMintableData(data) {
|
|
10055
|
+
try {
|
|
10056
|
+
classifyValueEnvelope(data);
|
|
10057
|
+
} catch (error) {
|
|
10058
|
+
throw invalid(
|
|
10059
|
+
`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.`
|
|
10060
|
+
);
|
|
10061
|
+
}
|
|
10062
|
+
}
|
|
10063
|
+
|
|
9923
10064
|
// token-engine/SphereTokenEngine.ts
|
|
9924
10065
|
var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
|
|
9925
10066
|
var MAX_MINT_CONCURRENCY = 8;
|
|
@@ -9976,7 +10117,7 @@ var SphereTokenEngine = class {
|
|
|
9976
10117
|
return sdkToken.latestTransaction.data;
|
|
9977
10118
|
}
|
|
9978
10119
|
const data = sdkToken.genesis.data;
|
|
9979
|
-
if (data &&
|
|
10120
|
+
if (data && classifyValueEnvelope(data).envelope === "sphere") {
|
|
9980
10121
|
return SpherePaymentData.fromCBOR(data).memo;
|
|
9981
10122
|
}
|
|
9982
10123
|
return null;
|
|
@@ -10006,9 +10147,10 @@ var SphereTokenEngine = class {
|
|
|
10006
10147
|
);
|
|
10007
10148
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10008
10149
|
const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
|
|
10009
|
-
return
|
|
10150
|
+
return wrapToken(token);
|
|
10010
10151
|
}
|
|
10011
10152
|
async mintDataToken(params, options) {
|
|
10153
|
+
assertMintableData(params.data);
|
|
10012
10154
|
const recipient = import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey);
|
|
10013
10155
|
const tokenType = params.tokenType ? new import_TokenType.TokenType(params.tokenType) : import_TokenType.TokenType.generate();
|
|
10014
10156
|
const salt = params.salt ? import_TokenSalt.TokenSalt.fromBytes(params.salt) : import_TokenSalt.TokenSalt.generate();
|
|
@@ -10023,7 +10165,7 @@ var SphereTokenEngine = class {
|
|
|
10023
10165
|
);
|
|
10024
10166
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10025
10167
|
const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
|
|
10026
|
-
return
|
|
10168
|
+
return wrapToken(token);
|
|
10027
10169
|
}
|
|
10028
10170
|
async transfer(params, options) {
|
|
10029
10171
|
this.assertOwned(params.token);
|
|
@@ -10046,13 +10188,19 @@ var SphereTokenEngine = class {
|
|
|
10046
10188
|
);
|
|
10047
10189
|
const certified = await transferTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10048
10190
|
const transferred = await params.token.sdkToken.transfer(certified, this.deps.verificationContext);
|
|
10049
|
-
return
|
|
10191
|
+
return wrapToken(transferred);
|
|
10050
10192
|
}
|
|
10051
10193
|
async split(params, options) {
|
|
10052
10194
|
this.assertOwned(params.token);
|
|
10053
10195
|
if (params.outputs.length === 0) {
|
|
10054
10196
|
throw new SphereError("Split requires at least one output", "VALIDATION_ERROR");
|
|
10055
10197
|
}
|
|
10198
|
+
if (params.token.value === null) {
|
|
10199
|
+
throw new SphereError(
|
|
10200
|
+
`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.`,
|
|
10201
|
+
"VALIDATION_ERROR"
|
|
10202
|
+
);
|
|
10203
|
+
}
|
|
10056
10204
|
const transferId = this.resolveTransferId(options);
|
|
10057
10205
|
const requests = params.outputs.map(
|
|
10058
10206
|
(o, i) => import_SplitTokenRequest.SplitTokenRequest.create(
|
|
@@ -10174,7 +10322,7 @@ var SphereTokenEngine = class {
|
|
|
10174
10322
|
const proof = await this.submitSplitMintLeg(certData, mintTx, options);
|
|
10175
10323
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10176
10324
|
const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
|
|
10177
|
-
return
|
|
10325
|
+
return wrapToken(token);
|
|
10178
10326
|
}
|
|
10179
10327
|
/**
|
|
10180
10328
|
* Submit one split-mint leg. A `TRANSACTION_HASH_MISMATCH` on a mint leg is NOT a foreign spend
|
|
@@ -10232,7 +10380,7 @@ var SphereTokenEngine = class {
|
|
|
10232
10380
|
"VALIDATION_ERROR"
|
|
10233
10381
|
);
|
|
10234
10382
|
}
|
|
10235
|
-
return
|
|
10383
|
+
return wrapToken(sdkToken);
|
|
10236
10384
|
}
|
|
10237
10385
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
10238
10386
|
/**
|
|
@@ -10344,34 +10492,6 @@ var SphereTokenEngine = class {
|
|
|
10344
10492
|
throw new SphereError("Cannot transfer a token not owned by this engine identity", "VALIDATION_ERROR");
|
|
10345
10493
|
}
|
|
10346
10494
|
}
|
|
10347
|
-
/** Wrap an SDK token into a SphereToken: cache its blob (incl. stable tokenId) + decoded value. */
|
|
10348
|
-
wrapToken(sdkToken) {
|
|
10349
|
-
const data = sdkToken.genesis.data;
|
|
10350
|
-
let value = null;
|
|
10351
|
-
if (data && this.isSpherePaymentData(data)) {
|
|
10352
|
-
try {
|
|
10353
|
-
value = SpherePaymentData.fromCBOR(data).toValue();
|
|
10354
|
-
} catch (err) {
|
|
10355
|
-
throw new SphereError(
|
|
10356
|
-
`Failed to decode token payment data: ${err instanceof Error ? err.message : String(err)}`,
|
|
10357
|
-
"VALIDATION_ERROR"
|
|
10358
|
-
);
|
|
10359
|
-
}
|
|
10360
|
-
}
|
|
10361
|
-
const blob = {
|
|
10362
|
-
tokenId: import_HexConverter.HexConverter.encode(sdkToken.id.bytes),
|
|
10363
|
-
token: sdkToken.toCBOR()
|
|
10364
|
-
};
|
|
10365
|
-
return { sdkToken, blob, value };
|
|
10366
|
-
}
|
|
10367
|
-
/** True if the bytes are a SpherePaymentData envelope (value token) vs a raw data token. */
|
|
10368
|
-
isSpherePaymentData(data) {
|
|
10369
|
-
try {
|
|
10370
|
-
return import_CborDeserializer.CborDeserializer.decodeTag(data).tag === SpherePaymentData.CBOR_TAG;
|
|
10371
|
-
} catch {
|
|
10372
|
-
return false;
|
|
10373
|
-
}
|
|
10374
|
-
}
|
|
10375
10495
|
/**
|
|
10376
10496
|
* Terminate the verification worker pool, if this engine was configured with
|
|
10377
10497
|
* one. Idempotent — the pool's dispose only touches workers it spawned.
|
|
@@ -11767,6 +11887,17 @@ function createMachineStores(kv) {
|
|
|
11767
11887
|
};
|
|
11768
11888
|
}
|
|
11769
11889
|
|
|
11890
|
+
// modules/payments-v2/machine/payload-view.ts
|
|
11891
|
+
function isWholeIntent(p) {
|
|
11892
|
+
return p.kind === "whole" || p.kind === "coinless";
|
|
11893
|
+
}
|
|
11894
|
+
function splitOf(p) {
|
|
11895
|
+
return p.kind === "coin" ? p.split : void 0;
|
|
11896
|
+
}
|
|
11897
|
+
function coinIdOf(p) {
|
|
11898
|
+
return p.kind === "coin" ? p.coinId : void 0;
|
|
11899
|
+
}
|
|
11900
|
+
|
|
11770
11901
|
// modules/payments-v2/machine/TransferMachine.ts
|
|
11771
11902
|
var CERTIFY_WIDTH = 20;
|
|
11772
11903
|
var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
|
|
@@ -11969,13 +12100,14 @@ var TransferMachine = class {
|
|
|
11969
12100
|
const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
|
|
11970
12101
|
return { recipientBlob: finished.blob.token };
|
|
11971
12102
|
}
|
|
11972
|
-
const split = ctx.payload
|
|
12103
|
+
const split = splitOf(ctx.payload);
|
|
12104
|
+
const splitCoinId = coinIdOf(ctx.payload);
|
|
11973
12105
|
const { outputs } = await engine.split(
|
|
11974
12106
|
{
|
|
11975
12107
|
token: source,
|
|
11976
12108
|
outputs: [
|
|
11977
|
-
{ recipientPubkey, coinId:
|
|
11978
|
-
{ recipientPubkey: this.deps.ownPubkey, coinId:
|
|
12109
|
+
{ recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
|
|
12110
|
+
{ recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
|
|
11979
12111
|
]
|
|
11980
12112
|
},
|
|
11981
12113
|
opts
|
|
@@ -12097,7 +12229,9 @@ var TransferMachine = class {
|
|
|
12097
12229
|
const entry = {
|
|
12098
12230
|
transferId: r.transferId,
|
|
12099
12231
|
remainingAmount: undelivered.toString(),
|
|
12100
|
-
|
|
12232
|
+
// '' only on a token-addressed intent, which is one leg: it either lands or
|
|
12233
|
+
// conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
|
|
12234
|
+
coinId: coinIdOf(r.payload) ?? "",
|
|
12101
12235
|
recipient: r.payload.recipient,
|
|
12102
12236
|
committedTokenIds: committed.map((o) => o.op.sourceTokenId),
|
|
12103
12237
|
createdAt: this.deps.now()
|
|
@@ -12123,8 +12257,16 @@ var TransferMachine = class {
|
|
|
12123
12257
|
}
|
|
12124
12258
|
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
12125
12259
|
try {
|
|
12126
|
-
const
|
|
12127
|
-
await this.
|
|
12260
|
+
const coinId = coinIdOf(payload);
|
|
12261
|
+
const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
|
|
12262
|
+
const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
|
|
12263
|
+
await this.deps.recordHistory?.({
|
|
12264
|
+
transferId,
|
|
12265
|
+
payload,
|
|
12266
|
+
phase,
|
|
12267
|
+
committedAmount,
|
|
12268
|
+
...committedAssets !== void 0 ? { committedAssets } : {}
|
|
12269
|
+
});
|
|
12128
12270
|
} catch {
|
|
12129
12271
|
}
|
|
12130
12272
|
try {
|
|
@@ -12138,6 +12280,18 @@ var TransferMachine = class {
|
|
|
12138
12280
|
* a journal-rehydrated resume leg has no source token, but every committed
|
|
12139
12281
|
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
12140
12282
|
*/
|
|
12283
|
+
/** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
|
|
12284
|
+
async settledAssets(engine, committed) {
|
|
12285
|
+
const total = /* @__PURE__ */ new Map();
|
|
12286
|
+
for (const outcome of committed) {
|
|
12287
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
12288
|
+
const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
|
|
12289
|
+
for (const asset of engine.readValue(token)?.assets ?? []) {
|
|
12290
|
+
total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
|
|
12291
|
+
}
|
|
12292
|
+
}
|
|
12293
|
+
return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
|
|
12294
|
+
}
|
|
12141
12295
|
async settledAmount(engine, coinId, committed) {
|
|
12142
12296
|
let total = 0n;
|
|
12143
12297
|
for (const outcome of committed) {
|
|
@@ -12166,8 +12320,11 @@ var TransferMachine = class {
|
|
|
12166
12320
|
}
|
|
12167
12321
|
};
|
|
12168
12322
|
function conflictAmount(engine, payload, op, source) {
|
|
12169
|
-
|
|
12170
|
-
|
|
12323
|
+
const split = splitOf(payload);
|
|
12324
|
+
if (op.kind === "split") return BigInt(split.splitAmount);
|
|
12325
|
+
const coinId = coinIdOf(payload);
|
|
12326
|
+
if (coinId === void 0) return 0n;
|
|
12327
|
+
return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
|
|
12171
12328
|
}
|
|
12172
12329
|
|
|
12173
12330
|
// modules/payments-v2/machine/resume.ts
|
|
@@ -12274,6 +12431,14 @@ async function runOne(ctx, job, report) {
|
|
|
12274
12431
|
report.failed.push(job.transferId);
|
|
12275
12432
|
return;
|
|
12276
12433
|
}
|
|
12434
|
+
if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
|
|
12435
|
+
logger.warn(
|
|
12436
|
+
"PaymentsV2",
|
|
12437
|
+
`resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
|
|
12438
|
+
);
|
|
12439
|
+
report.failed.push(job.transferId);
|
|
12440
|
+
return;
|
|
12441
|
+
}
|
|
12277
12442
|
mine.set(op.sourceTokenId, token);
|
|
12278
12443
|
}
|
|
12279
12444
|
try {
|
|
@@ -12304,6 +12469,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
|
|
|
12304
12469
|
}
|
|
12305
12470
|
report.failed.push(transferId);
|
|
12306
12471
|
}
|
|
12472
|
+
function validateCoinPayload(c) {
|
|
12473
|
+
if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
|
|
12474
|
+
throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
|
|
12475
|
+
}
|
|
12476
|
+
const s = c.split;
|
|
12477
|
+
if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
|
|
12478
|
+
throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
|
|
12479
|
+
}
|
|
12480
|
+
return { ...c, kind: "coin" };
|
|
12481
|
+
}
|
|
12482
|
+
function validateWholePayload(p) {
|
|
12483
|
+
if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
|
|
12484
|
+
throw new SphereError(
|
|
12485
|
+
"token intent payload must name exactly one source token",
|
|
12486
|
+
"VALIDATION_ERROR"
|
|
12487
|
+
);
|
|
12488
|
+
}
|
|
12489
|
+
if (p.split !== void 0) {
|
|
12490
|
+
throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
|
|
12491
|
+
}
|
|
12492
|
+
return { ...p, kind: "whole", direct: [p.direct[0]] };
|
|
12493
|
+
}
|
|
12307
12494
|
function validatePayload(raw) {
|
|
12308
12495
|
const p = raw !== null && typeof raw === "object" ? raw : null;
|
|
12309
12496
|
if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
|
|
@@ -12312,14 +12499,17 @@ function validatePayload(raw) {
|
|
|
12312
12499
|
"VALIDATION_ERROR"
|
|
12313
12500
|
);
|
|
12314
12501
|
}
|
|
12315
|
-
if (typeof p.recipient !== "string"
|
|
12316
|
-
throw new SphereError("intent payload is missing recipient
|
|
12502
|
+
if (typeof p.recipient !== "string") {
|
|
12503
|
+
throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
|
|
12317
12504
|
}
|
|
12318
|
-
const
|
|
12319
|
-
if (
|
|
12320
|
-
throw new SphereError(
|
|
12505
|
+
const kind = p.kind ?? "coin";
|
|
12506
|
+
if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
|
|
12507
|
+
throw new SphereError(
|
|
12508
|
+
`unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
|
|
12509
|
+
"VALIDATION_ERROR"
|
|
12510
|
+
);
|
|
12321
12511
|
}
|
|
12322
|
-
return p;
|
|
12512
|
+
return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
|
|
12323
12513
|
}
|
|
12324
12514
|
|
|
12325
12515
|
// modules/payments-v2/convergence.ts
|
|
@@ -12525,13 +12715,24 @@ async function openRow(entry, journal, decryptPayload) {
|
|
|
12525
12715
|
transferId: entry.transferId,
|
|
12526
12716
|
kind: "open",
|
|
12527
12717
|
recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
|
|
12528
|
-
|
|
12529
|
-
amount: typeof payload?.amount === "string" ? payload.amount : "",
|
|
12718
|
+
...subject(payload),
|
|
12530
12719
|
legs: { certified, total: Math.max(total, certified) },
|
|
12531
12720
|
deliveryPending: journal.length > 0,
|
|
12532
12721
|
createdAt: entry.createdAt
|
|
12533
12722
|
};
|
|
12534
12723
|
}
|
|
12724
|
+
function subject(payload) {
|
|
12725
|
+
const rawKind = payload?.kind;
|
|
12726
|
+
if (rawKind === "whole" || rawKind === "coinless") {
|
|
12727
|
+
const tokenId = payload?.direct?.[0];
|
|
12728
|
+
return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
|
|
12729
|
+
}
|
|
12730
|
+
const coin = payload;
|
|
12731
|
+
return {
|
|
12732
|
+
coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
|
|
12733
|
+
amount: typeof coin?.amount === "string" ? coin.amount : ""
|
|
12734
|
+
};
|
|
12735
|
+
}
|
|
12535
12736
|
function journalOnlyRow(transferId, entries) {
|
|
12536
12737
|
const certified = distinctOpCount(entries);
|
|
12537
12738
|
return {
|
|
@@ -12561,6 +12762,263 @@ function distinctOpCount(entries) {
|
|
|
12561
12762
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
12562
12763
|
}
|
|
12563
12764
|
|
|
12765
|
+
// modules/payments-v2/inventory/token-data.ts
|
|
12766
|
+
async function readTokenData(deps, tokenId) {
|
|
12767
|
+
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
12768
|
+
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
12769
|
+
}
|
|
12770
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
12771
|
+
if (bytes === void 0) {
|
|
12772
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
12773
|
+
}
|
|
12774
|
+
return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
|
|
12775
|
+
}
|
|
12776
|
+
|
|
12777
|
+
// modules/payments-v2/machine/payload.ts
|
|
12778
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12779
|
+
return {
|
|
12780
|
+
v: 2,
|
|
12781
|
+
kind: "coin",
|
|
12782
|
+
recipient: recipientPubkey,
|
|
12783
|
+
coinId: request.coinId,
|
|
12784
|
+
amount: request.amount,
|
|
12785
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12786
|
+
direct: [...spend.plan.direct],
|
|
12787
|
+
...spend.plan.split !== void 0 ? {
|
|
12788
|
+
split: {
|
|
12789
|
+
tokenId: spend.plan.split.tokenId,
|
|
12790
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12791
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12792
|
+
}
|
|
12793
|
+
} : {},
|
|
12794
|
+
spentStates
|
|
12795
|
+
};
|
|
12796
|
+
}
|
|
12797
|
+
function messageOf(err) {
|
|
12798
|
+
return err instanceof Error ? err.message : String(err);
|
|
12799
|
+
}
|
|
12800
|
+
function buildWholePayload(recipientPubkey, request, spentStates) {
|
|
12801
|
+
return {
|
|
12802
|
+
v: 2,
|
|
12803
|
+
kind: "whole",
|
|
12804
|
+
recipient: recipientPubkey,
|
|
12805
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12806
|
+
direct: [request.tokenId],
|
|
12807
|
+
spentStates
|
|
12808
|
+
};
|
|
12809
|
+
}
|
|
12810
|
+
|
|
12811
|
+
// modules/payments-v2/mint-params.ts
|
|
12812
|
+
function mintParams(recipientPubkey, coinId, amount) {
|
|
12813
|
+
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
12814
|
+
}
|
|
12815
|
+
|
|
12816
|
+
// modules/payments-v2/mint.ts
|
|
12817
|
+
function runMintUnderJournal(deps, input) {
|
|
12818
|
+
return mintUnderJournal(deps, input);
|
|
12819
|
+
}
|
|
12820
|
+
async function mintUnderJournal(deps, input) {
|
|
12821
|
+
const { mintId, coinId, amount } = input;
|
|
12822
|
+
const engine = deps.engine;
|
|
12823
|
+
const entry = {
|
|
12824
|
+
mintId,
|
|
12825
|
+
coinId,
|
|
12826
|
+
amount: amount.toString(),
|
|
12827
|
+
tokenId: "",
|
|
12828
|
+
createdAt: deps.now()
|
|
12829
|
+
};
|
|
12830
|
+
await deps.mintJournal.upsert(entry);
|
|
12831
|
+
let token;
|
|
12832
|
+
try {
|
|
12833
|
+
token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
|
|
12834
|
+
} catch (err) {
|
|
12835
|
+
deps.armHeartbeat();
|
|
12836
|
+
return { success: false, error: messageOf(err) };
|
|
12837
|
+
}
|
|
12838
|
+
if (token.blob.tokenId !== entry.tokenId) {
|
|
12839
|
+
await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
12840
|
+
}
|
|
12841
|
+
try {
|
|
12842
|
+
await finalizeMint(deps, mintId, token, coinId, amount.toString());
|
|
12843
|
+
} catch (err) {
|
|
12844
|
+
deps.armHeartbeat();
|
|
12845
|
+
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
12846
|
+
}
|
|
12847
|
+
return { success: true, tokenId: token.blob.tokenId };
|
|
12848
|
+
}
|
|
12849
|
+
async function finalizeMint(deps, mintId, token, coinId, amount) {
|
|
12850
|
+
const engine = deps.engine;
|
|
12851
|
+
const bytes = token.blob.token;
|
|
12852
|
+
const digest = bytesToHex3(sha256(bytes));
|
|
12853
|
+
const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
|
|
12854
|
+
const key = keys.get(digest);
|
|
12855
|
+
if (key === void 0) {
|
|
12856
|
+
throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
|
|
12857
|
+
}
|
|
12858
|
+
await deps.storagePort.applyDelta({
|
|
12859
|
+
transferId: mintId,
|
|
12860
|
+
spent: [],
|
|
12861
|
+
added: [{ tokenId: token.blob.tokenId, key }]
|
|
12862
|
+
});
|
|
12863
|
+
await deps.recordMint({
|
|
12864
|
+
tokenId: token.blob.tokenId,
|
|
12865
|
+
assets: [{ coinId, amount }]
|
|
12866
|
+
});
|
|
12867
|
+
await deps.mintJournal.removeByKey(mintId);
|
|
12868
|
+
deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
12869
|
+
deps.refreshView();
|
|
12870
|
+
}
|
|
12871
|
+
|
|
12872
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
12873
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12874
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12875
|
+
return {
|
|
12876
|
+
id: tokenId,
|
|
12877
|
+
coinId: asset.coinId,
|
|
12878
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
12879
|
+
name: registry.getName(asset.coinId),
|
|
12880
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
12881
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12882
|
+
amount: asset.amount,
|
|
12883
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
12884
|
+
createdAt: snapshot.createdAt,
|
|
12885
|
+
updatedAt: snapshot.updatedAt,
|
|
12886
|
+
lazy: true,
|
|
12887
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12888
|
+
};
|
|
12889
|
+
}
|
|
12890
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
12891
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12892
|
+
for (const [tokenId, entry] of entries) {
|
|
12893
|
+
const moving = isTransferring(tokenId);
|
|
12894
|
+
for (const asset of entry.assets) {
|
|
12895
|
+
let group = groups.get(asset.coinId);
|
|
12896
|
+
if (!group) {
|
|
12897
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12898
|
+
groups.set(asset.coinId, group);
|
|
12899
|
+
}
|
|
12900
|
+
if (moving) {
|
|
12901
|
+
group.transferring += BigInt(asset.amount);
|
|
12902
|
+
group.transferringCount += 1;
|
|
12903
|
+
} else {
|
|
12904
|
+
group.total += BigInt(asset.amount);
|
|
12905
|
+
group.count += 1;
|
|
12906
|
+
}
|
|
12907
|
+
}
|
|
12908
|
+
}
|
|
12909
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12910
|
+
}
|
|
12911
|
+
function toAsset(coinId, totals, registry) {
|
|
12912
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
12913
|
+
return {
|
|
12914
|
+
coinId,
|
|
12915
|
+
symbol: registry.getSymbol(coinId),
|
|
12916
|
+
name: registry.getName(coinId),
|
|
12917
|
+
decimals: registry.getDecimals(coinId),
|
|
12918
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12919
|
+
totalAmount: totals.total.toString(),
|
|
12920
|
+
tokenCount: totals.count,
|
|
12921
|
+
confirmedAmount: totals.total.toString(),
|
|
12922
|
+
unconfirmedAmount: "0",
|
|
12923
|
+
confirmedTokenCount: totals.count,
|
|
12924
|
+
unconfirmedTokenCount: 0,
|
|
12925
|
+
transferringTokenCount: totals.transferringCount,
|
|
12926
|
+
transferringAmount: totals.transferring.toString(),
|
|
12927
|
+
priceUsd: null,
|
|
12928
|
+
priceEur: null,
|
|
12929
|
+
change24h: null,
|
|
12930
|
+
fiatValueUsd: null,
|
|
12931
|
+
fiatValueEur: null
|
|
12932
|
+
};
|
|
12933
|
+
}
|
|
12934
|
+
function priceIdOf(registry, coinId) {
|
|
12935
|
+
return registry.getName(coinId).toLowerCase();
|
|
12936
|
+
}
|
|
12937
|
+
async function withPrices(raw, registry, price) {
|
|
12938
|
+
try {
|
|
12939
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12940
|
+
const quotes = await price.getPrices(ids);
|
|
12941
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12942
|
+
} catch {
|
|
12943
|
+
return raw;
|
|
12944
|
+
}
|
|
12945
|
+
}
|
|
12946
|
+
function priceOne(asset, quote) {
|
|
12947
|
+
if (!quote) return asset;
|
|
12948
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12949
|
+
return {
|
|
12950
|
+
...asset,
|
|
12951
|
+
priceUsd: quote.priceUsd,
|
|
12952
|
+
priceEur: quote.priceEur ?? null,
|
|
12953
|
+
change24h: quote.change24h ?? null,
|
|
12954
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
12955
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12956
|
+
};
|
|
12957
|
+
}
|
|
12958
|
+
function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
12959
|
+
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
12960
|
+
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
12961
|
+
transferring: true,
|
|
12962
|
+
suspectedSpent: false
|
|
12963
|
+
});
|
|
12964
|
+
}
|
|
12965
|
+
|
|
12966
|
+
// modules/payments-v2/send-whole.ts
|
|
12967
|
+
async function materializeWholeSpend(deps, input) {
|
|
12968
|
+
const { engine } = deps;
|
|
12969
|
+
const tokenId = input.request.tokenId;
|
|
12970
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
12971
|
+
if (bytes === void 0) {
|
|
12972
|
+
throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
12973
|
+
}
|
|
12974
|
+
const token = await engine.decodeToken({ tokenId, token: bytes });
|
|
12975
|
+
if (token.valueEnvelope === "bare_collection") {
|
|
12976
|
+
throw new SphereError(
|
|
12977
|
+
`Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
|
|
12978
|
+
"VALIDATION_ERROR"
|
|
12979
|
+
);
|
|
12980
|
+
}
|
|
12981
|
+
if (input.requireCoinless && token.value !== null) {
|
|
12982
|
+
throw new SphereError(
|
|
12983
|
+
`Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
|
|
12984
|
+
"VALIDATION_ERROR"
|
|
12985
|
+
);
|
|
12986
|
+
}
|
|
12987
|
+
const keys = await engine.deliveryKeys(bytes);
|
|
12988
|
+
const payload = buildWholePayload(input.recipientPubkey, input.request, {
|
|
12989
|
+
[tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
|
|
12990
|
+
});
|
|
12991
|
+
const plan = {
|
|
12992
|
+
transferId: input.transferId,
|
|
12993
|
+
recipientPubkey: input.recipientPubkey,
|
|
12994
|
+
payload,
|
|
12995
|
+
ops: buildOps(payload),
|
|
12996
|
+
sources: /* @__PURE__ */ new Map([[tokenId, token]]),
|
|
12997
|
+
...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
|
|
12998
|
+
};
|
|
12999
|
+
const sourceTokens = (token.value?.assets ?? []).map(
|
|
13000
|
+
(asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
|
|
13001
|
+
);
|
|
13002
|
+
return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
|
|
13003
|
+
}
|
|
13004
|
+
|
|
13005
|
+
// modules/payments-v2/send-errors.ts
|
|
13006
|
+
function partialize(err, run) {
|
|
13007
|
+
if (run.delivered.length === 0) return err;
|
|
13008
|
+
return new PartialSendConflictError(
|
|
13009
|
+
"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.",
|
|
13010
|
+
run.firstPartialId ?? "",
|
|
13011
|
+
[...run.delivered],
|
|
13012
|
+
run.amount,
|
|
13013
|
+
err
|
|
13014
|
+
);
|
|
13015
|
+
}
|
|
13016
|
+
function stampTransferId(err, transferId) {
|
|
13017
|
+
if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
|
|
13018
|
+
err.transferId = transferId;
|
|
13019
|
+
}
|
|
13020
|
+
}
|
|
13021
|
+
|
|
12564
13022
|
// modules/payments-v2/recipient.ts
|
|
12565
13023
|
var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
|
|
12566
13024
|
async function requireSameNetworkRecipient(deps, identifier) {
|
|
@@ -12609,11 +13067,6 @@ async function splitOpIndex(deps, envelope) {
|
|
|
12609
13067
|
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
12610
13068
|
}
|
|
12611
13069
|
|
|
12612
|
-
// modules/payments-v2/mint-params.ts
|
|
12613
|
-
function mintParams(recipientPubkey, coinId, amount) {
|
|
12614
|
-
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
12615
|
-
}
|
|
12616
|
-
|
|
12617
13070
|
// modules/payments-v2/prewarm-cache.ts
|
|
12618
13071
|
var PrewarmCache = class _PrewarmCache {
|
|
12619
13072
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -12693,124 +13146,15 @@ async function takeSourceBlobs(deps, sourceIds) {
|
|
|
12693
13146
|
return blobs;
|
|
12694
13147
|
}
|
|
12695
13148
|
|
|
12696
|
-
// modules/payments-v2/
|
|
12697
|
-
function
|
|
12698
|
-
|
|
12699
|
-
return {
|
|
12700
|
-
id: tokenId,
|
|
12701
|
-
coinId: asset.coinId,
|
|
12702
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
12703
|
-
name: registry.getName(asset.coinId),
|
|
12704
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
12705
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12706
|
-
amount: asset.amount,
|
|
12707
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
12708
|
-
createdAt: snapshot.createdAt,
|
|
12709
|
-
updatedAt: snapshot.updatedAt,
|
|
12710
|
-
lazy: true,
|
|
12711
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12712
|
-
};
|
|
12713
|
-
}
|
|
12714
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
12715
|
-
const groups = /* @__PURE__ */ new Map();
|
|
12716
|
-
for (const [tokenId, entry] of entries) {
|
|
12717
|
-
const moving = isTransferring(tokenId);
|
|
12718
|
-
for (const asset of entry.assets) {
|
|
12719
|
-
let group = groups.get(asset.coinId);
|
|
12720
|
-
if (!group) {
|
|
12721
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12722
|
-
groups.set(asset.coinId, group);
|
|
12723
|
-
}
|
|
12724
|
-
if (moving) {
|
|
12725
|
-
group.transferring += BigInt(asset.amount);
|
|
12726
|
-
group.transferringCount += 1;
|
|
12727
|
-
} else {
|
|
12728
|
-
group.total += BigInt(asset.amount);
|
|
12729
|
-
group.count += 1;
|
|
12730
|
-
}
|
|
12731
|
-
}
|
|
12732
|
-
}
|
|
12733
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12734
|
-
}
|
|
12735
|
-
function toAsset(coinId, totals, registry) {
|
|
12736
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
12737
|
-
return {
|
|
12738
|
-
coinId,
|
|
12739
|
-
symbol: registry.getSymbol(coinId),
|
|
12740
|
-
name: registry.getName(coinId),
|
|
12741
|
-
decimals: registry.getDecimals(coinId),
|
|
12742
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12743
|
-
totalAmount: totals.total.toString(),
|
|
12744
|
-
tokenCount: totals.count,
|
|
12745
|
-
confirmedAmount: totals.total.toString(),
|
|
12746
|
-
unconfirmedAmount: "0",
|
|
12747
|
-
confirmedTokenCount: totals.count,
|
|
12748
|
-
unconfirmedTokenCount: 0,
|
|
12749
|
-
transferringTokenCount: totals.transferringCount,
|
|
12750
|
-
transferringAmount: totals.transferring.toString(),
|
|
12751
|
-
priceUsd: null,
|
|
12752
|
-
priceEur: null,
|
|
12753
|
-
change24h: null,
|
|
12754
|
-
fiatValueUsd: null,
|
|
12755
|
-
fiatValueEur: null
|
|
12756
|
-
};
|
|
12757
|
-
}
|
|
12758
|
-
function priceIdOf(registry, coinId) {
|
|
12759
|
-
return registry.getName(coinId).toLowerCase();
|
|
12760
|
-
}
|
|
12761
|
-
async function withPrices(raw, registry, price) {
|
|
12762
|
-
try {
|
|
12763
|
-
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12764
|
-
const quotes = await price.getPrices(ids);
|
|
12765
|
-
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12766
|
-
} catch {
|
|
12767
|
-
return raw;
|
|
12768
|
-
}
|
|
12769
|
-
}
|
|
12770
|
-
function priceOne(asset, quote) {
|
|
12771
|
-
if (!quote) return asset;
|
|
12772
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12773
|
-
return {
|
|
12774
|
-
...asset,
|
|
12775
|
-
priceUsd: quote.priceUsd,
|
|
12776
|
-
priceEur: quote.priceEur ?? null,
|
|
12777
|
-
change24h: quote.change24h ?? null,
|
|
12778
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
12779
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12780
|
-
};
|
|
12781
|
-
}
|
|
12782
|
-
function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
12783
|
-
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
12784
|
-
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
12785
|
-
transferring: true,
|
|
12786
|
-
suspectedSpent: false
|
|
12787
|
-
});
|
|
12788
|
-
}
|
|
12789
|
-
|
|
12790
|
-
// modules/payments-v2/machine/payload.ts
|
|
12791
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12792
|
-
return {
|
|
12793
|
-
v: 2,
|
|
12794
|
-
recipient: recipientPubkey,
|
|
12795
|
-
coinId: request.coinId,
|
|
12796
|
-
amount: request.amount,
|
|
12797
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12798
|
-
direct: [...spend.plan.direct],
|
|
12799
|
-
...spend.plan.split !== void 0 ? {
|
|
12800
|
-
split: {
|
|
12801
|
-
tokenId: spend.plan.split.tokenId,
|
|
12802
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12803
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12804
|
-
}
|
|
12805
|
-
} : {},
|
|
12806
|
-
spentStates
|
|
12807
|
-
};
|
|
13149
|
+
// modules/payments-v2/history/History.ts
|
|
13150
|
+
function wireAssets(assets) {
|
|
13151
|
+
return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
|
|
12808
13152
|
}
|
|
12809
|
-
function
|
|
12810
|
-
|
|
13153
|
+
function isPermanentReject(err) {
|
|
13154
|
+
const status = err?.status;
|
|
13155
|
+
if (typeof status !== "number") return false;
|
|
13156
|
+
return status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
12811
13157
|
}
|
|
12812
|
-
|
|
12813
|
-
// modules/payments-v2/history/History.ts
|
|
12814
13158
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
12815
13159
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
12816
13160
|
var History = class {
|
|
@@ -12835,7 +13179,7 @@ var History = class {
|
|
|
12835
13179
|
dedupKey: input.transferId,
|
|
12836
13180
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12837
13181
|
type: "SENT",
|
|
12838
|
-
assets:
|
|
13182
|
+
assets: wireAssets(input.assets),
|
|
12839
13183
|
ts: this.ts(input.timestamp),
|
|
12840
13184
|
transferId: input.transferId,
|
|
12841
13185
|
...this.wireTokenId(input.tokenId),
|
|
@@ -12848,7 +13192,7 @@ var History = class {
|
|
|
12848
13192
|
dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
|
|
12849
13193
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12850
13194
|
type: "RECEIVED",
|
|
12851
|
-
assets:
|
|
13195
|
+
assets: wireAssets(input.assets),
|
|
12852
13196
|
ts: this.ts(input.timestamp),
|
|
12853
13197
|
...input.transferId !== void 0 ? { transferId: input.transferId } : {},
|
|
12854
13198
|
...this.wireTokenId(input.tokenId),
|
|
@@ -12861,7 +13205,7 @@ var History = class {
|
|
|
12861
13205
|
dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
|
|
12862
13206
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12863
13207
|
type: "MINT",
|
|
12864
|
-
assets:
|
|
13208
|
+
assets: wireAssets(input.assets),
|
|
12865
13209
|
ts: this.ts(input.timestamp),
|
|
12866
13210
|
...this.wireTokenId(input.tokenId)
|
|
12867
13211
|
}));
|
|
@@ -12872,7 +13216,10 @@ var History = class {
|
|
|
12872
13216
|
try {
|
|
12873
13217
|
await this.deps.client.postHistory([record]);
|
|
12874
13218
|
} catch (err) {
|
|
12875
|
-
this.deps.log?.(
|
|
13219
|
+
this.deps.log?.(
|
|
13220
|
+
isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
|
|
13221
|
+
err
|
|
13222
|
+
);
|
|
12876
13223
|
return;
|
|
12877
13224
|
}
|
|
12878
13225
|
this.deps.emit("history:updated", this.toEntry(record));
|
|
@@ -12939,6 +13286,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
|
12939
13286
|
function stateKey(tokenId, stateHash) {
|
|
12940
13287
|
return `${tokenId}:${stateHash}`;
|
|
12941
13288
|
}
|
|
13289
|
+
function unchanged(prev, item, tokenType) {
|
|
13290
|
+
return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
|
|
13291
|
+
}
|
|
13292
|
+
function isCoinless(item, prev) {
|
|
13293
|
+
if (item.status !== "active") return prev?.coinless ?? false;
|
|
13294
|
+
return (item.assets?.length ?? 0) === 0;
|
|
13295
|
+
}
|
|
12942
13296
|
var InventoryView = class {
|
|
12943
13297
|
constructor(deps) {
|
|
12944
13298
|
this.deps = deps;
|
|
@@ -13032,14 +13386,26 @@ var InventoryView = class {
|
|
|
13032
13386
|
pool(coinId) {
|
|
13033
13387
|
const out = [];
|
|
13034
13388
|
for (const [tokenId, entry] of this.mirror) {
|
|
13035
|
-
if (
|
|
13036
|
-
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
13037
|
-
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
13389
|
+
if (!this.isSpendable(tokenId, entry)) continue;
|
|
13038
13390
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
13039
13391
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
13040
13392
|
}
|
|
13041
13393
|
return out;
|
|
13042
13394
|
}
|
|
13395
|
+
/**
|
|
13396
|
+
* The eligibility gates every spend shares, coin or token — ONE definition, so a
|
|
13397
|
+
* single probe covers both verbs and neither can drift from the other.
|
|
13398
|
+
*/
|
|
13399
|
+
isSpendable(tokenId, entry) {
|
|
13400
|
+
if (entry.status !== "active") return false;
|
|
13401
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
|
|
13402
|
+
return !this.suspected.has(stateKey(tokenId, entry.stateHash));
|
|
13403
|
+
}
|
|
13404
|
+
/** Is this NAMED token spendable whole — coinless or valued (#777)? */
|
|
13405
|
+
spendableToken(tokenId) {
|
|
13406
|
+
const entry = this.mirror.get(tokenId);
|
|
13407
|
+
return entry !== void 0 && this.isSpendable(tokenId, entry);
|
|
13408
|
+
}
|
|
13043
13409
|
/**
|
|
13044
13410
|
* Current mirrored state of an ACTIVE token, or undefined when it is unknown
|
|
13045
13411
|
* or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
|
|
@@ -13065,6 +13431,29 @@ var InventoryView = class {
|
|
|
13065
13431
|
}
|
|
13066
13432
|
return out;
|
|
13067
13433
|
}
|
|
13434
|
+
/**
|
|
13435
|
+
* Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
|
|
13436
|
+
* exactly when it is not, so no mirror row can appear in both reads.
|
|
13437
|
+
*/
|
|
13438
|
+
coinless(registry) {
|
|
13439
|
+
const out = [];
|
|
13440
|
+
for (const [tokenId, entry] of this.mirror) {
|
|
13441
|
+
if (entry.status !== "active" || !entry.coinless) continue;
|
|
13442
|
+
const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
|
|
13443
|
+
out.push({
|
|
13444
|
+
tokenId,
|
|
13445
|
+
...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
|
|
13446
|
+
...meta ? { name: meta.name } : {},
|
|
13447
|
+
...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
|
|
13448
|
+
stateHash: entry.stateHash,
|
|
13449
|
+
transferring: this.held(tokenId),
|
|
13450
|
+
...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
|
|
13451
|
+
createdAt: entry.createdAt,
|
|
13452
|
+
updatedAt: entry.updatedAt
|
|
13453
|
+
});
|
|
13454
|
+
}
|
|
13455
|
+
return out;
|
|
13456
|
+
}
|
|
13068
13457
|
async assets(registry, price) {
|
|
13069
13458
|
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
13070
13459
|
if (!price || raw.length === 0) return raw;
|
|
@@ -13136,14 +13525,19 @@ var InventoryView = class {
|
|
|
13136
13525
|
applyOne(item, now) {
|
|
13137
13526
|
const prev = this.mirror.get(item.tokenId);
|
|
13138
13527
|
if (prev && item.seq < prev.seq) return false;
|
|
13139
|
-
|
|
13140
|
-
|
|
13141
|
-
}
|
|
13528
|
+
const tokenType = item.tokenType ?? prev?.tokenType;
|
|
13529
|
+
if (prev && unchanged(prev, item, tokenType)) return false;
|
|
13142
13530
|
this.mirror.set(item.tokenId, {
|
|
13143
13531
|
stateHash: item.stateHash,
|
|
13144
13532
|
seq: item.seq,
|
|
13145
13533
|
status: item.status,
|
|
13146
|
-
|
|
13534
|
+
// ONLY a tombstone inherits: it omits assets for an unrelated reason and
|
|
13535
|
+
// recoverRemoved must know the amount it restores. An ACTIVE row that omits
|
|
13536
|
+
// them is stating coinlessness, and inheriting there would keep stale assets
|
|
13537
|
+
// in tokens() while `coinless` is true — the row in BOTH reads (§16).
|
|
13538
|
+
assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
|
|
13539
|
+
coinless: isCoinless(item, prev),
|
|
13540
|
+
...tokenType !== void 0 ? { tokenType } : {},
|
|
13147
13541
|
createdAt: prev?.createdAt ?? now,
|
|
13148
13542
|
updatedAt: now
|
|
13149
13543
|
});
|
|
@@ -13379,7 +13773,12 @@ async function screen(deps, engine, entry) {
|
|
|
13379
13773
|
}
|
|
13380
13774
|
return {
|
|
13381
13775
|
kind: "accept",
|
|
13382
|
-
record: {
|
|
13776
|
+
record: {
|
|
13777
|
+
tokenId: keys.tokenId,
|
|
13778
|
+
stateHash: keys.stateHash,
|
|
13779
|
+
assets: toAssetAmounts(token),
|
|
13780
|
+
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
|
|
13781
|
+
}
|
|
13383
13782
|
};
|
|
13384
13783
|
}
|
|
13385
13784
|
async function announce(deps, entry, record) {
|
|
@@ -13390,6 +13789,7 @@ async function announce(deps, entry, record) {
|
|
|
13390
13789
|
tokenId: record.tokenId,
|
|
13391
13790
|
stateHash: record.stateHash,
|
|
13392
13791
|
assets: record.assets,
|
|
13792
|
+
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
13393
13793
|
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
13394
13794
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13395
13795
|
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
@@ -13403,6 +13803,19 @@ async function announce(deps, entry, record) {
|
|
|
13403
13803
|
senderPubkey: entry.senderPubkey ?? "",
|
|
13404
13804
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13405
13805
|
tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
|
|
13806
|
+
// #777: named here rather than mapped from assets, which announced an EMPTY list.
|
|
13807
|
+
...record.tokenType !== void 0 ? {
|
|
13808
|
+
coinless: [
|
|
13809
|
+
{
|
|
13810
|
+
tokenId: record.tokenId,
|
|
13811
|
+
tokenType: record.tokenType,
|
|
13812
|
+
stateHash: record.stateHash,
|
|
13813
|
+
transferring: false,
|
|
13814
|
+
createdAt: receivedAt,
|
|
13815
|
+
updatedAt: receivedAt
|
|
13816
|
+
}
|
|
13817
|
+
]
|
|
13818
|
+
} : {},
|
|
13406
13819
|
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
13407
13820
|
receivedAt
|
|
13408
13821
|
};
|
|
@@ -14068,6 +14481,34 @@ var SpendQueue = class {
|
|
|
14068
14481
|
}
|
|
14069
14482
|
return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
|
|
14070
14483
|
}
|
|
14484
|
+
/**
|
|
14485
|
+
* #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
|
|
14486
|
+
* for. A coin plan waits because some OTHER combination may free up; a named
|
|
14487
|
+
* token is either free now or held by a reservation that only its own transfer
|
|
14488
|
+
* can release, so queueing would block until a timeout on a spend that cannot
|
|
14489
|
+
* become possible.
|
|
14490
|
+
*/
|
|
14491
|
+
planWhole(reservationId, tokenId) {
|
|
14492
|
+
if (this.destroyed) {
|
|
14493
|
+
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
14494
|
+
}
|
|
14495
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
14496
|
+
if (unproven !== null) {
|
|
14497
|
+
throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
|
|
14498
|
+
}
|
|
14499
|
+
if (this.deps.spendableToken?.(tokenId) !== true) {
|
|
14500
|
+
throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
|
|
14501
|
+
}
|
|
14502
|
+
const holder = this.deps.ledger.holderOf(tokenId);
|
|
14503
|
+
if (holder !== void 0) {
|
|
14504
|
+
throw new SphereError(
|
|
14505
|
+
`Token ${tokenId} is already reserved by transfer ${holder}`,
|
|
14506
|
+
"VALIDATION_ERROR"
|
|
14507
|
+
);
|
|
14508
|
+
}
|
|
14509
|
+
this.deps.ledger.reserve(reservationId, [{ tokenId }]);
|
|
14510
|
+
return { reservationId, plan: { direct: [tokenId] } };
|
|
14511
|
+
}
|
|
14071
14512
|
/**
|
|
14072
14513
|
* The sources `plan()` WOULD pick right now, reserving nothing and queueing
|
|
14073
14514
|
* nothing — a read-only probe used to warm blobs while the user is still
|
|
@@ -14257,6 +14698,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
14257
14698
|
const queue = new SpendQueue({
|
|
14258
14699
|
ledger,
|
|
14259
14700
|
getPool: (coinId) => view.pool(coinId),
|
|
14701
|
+
spendableToken: (tokenId) => view.spendableToken(tokenId),
|
|
14260
14702
|
...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
|
|
14261
14703
|
});
|
|
14262
14704
|
const historyStore = new History({
|
|
@@ -14337,13 +14779,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
14337
14779
|
...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
|
|
14338
14780
|
emit: deps.emit,
|
|
14339
14781
|
now: deps.now ?? Date.now,
|
|
14340
|
-
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
14782
|
+
recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
|
|
14341
14783
|
await historyStore.recordSent({
|
|
14342
14784
|
transferId,
|
|
14343
|
-
|
|
14344
|
-
//
|
|
14345
|
-
|
|
14346
|
-
amount: committedAmount,
|
|
14785
|
+
// §5.9: the SETTLED amount, never payload.amount — the plan. A token
|
|
14786
|
+
// spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
|
|
14787
|
+
...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
|
|
14347
14788
|
recipientPubkey: payload.recipient,
|
|
14348
14789
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
14349
14790
|
});
|
|
@@ -14378,12 +14819,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
|
|
|
14378
14819
|
});
|
|
14379
14820
|
}
|
|
14380
14821
|
async function recordReceived(historyStore, record) {
|
|
14381
|
-
const first = record.assets[0];
|
|
14382
14822
|
await historyStore.recordReceived({
|
|
14383
14823
|
tokenId: record.tokenId,
|
|
14384
14824
|
stateHash: record.stateHash,
|
|
14385
|
-
|
|
14386
|
-
amount: first?.amount ?? "0",
|
|
14825
|
+
assets: record.assets,
|
|
14387
14826
|
...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
|
|
14388
14827
|
...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
|
|
14389
14828
|
...record.memo !== void 0 ? { memo: record.memo } : {},
|
|
@@ -14531,6 +14970,13 @@ var PaymentsFacade = class {
|
|
|
14531
14970
|
tokens(filter) {
|
|
14532
14971
|
return this.view.tokens(this.deps.registry, filter);
|
|
14533
14972
|
}
|
|
14973
|
+
coinless() {
|
|
14974
|
+
return this.view.coinless(this.deps.registry);
|
|
14975
|
+
}
|
|
14976
|
+
tokenData(tokenId) {
|
|
14977
|
+
const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
|
|
14978
|
+
return readTokenData(deps, tokenId);
|
|
14979
|
+
}
|
|
14534
14980
|
history(page) {
|
|
14535
14981
|
return this.historyStore.page(page ?? {});
|
|
14536
14982
|
}
|
|
@@ -14553,7 +14999,14 @@ var PaymentsFacade = class {
|
|
|
14553
14999
|
return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
|
|
14554
15000
|
}
|
|
14555
15001
|
send(request) {
|
|
14556
|
-
return this.track(this.
|
|
15002
|
+
return this.track(this.runJob({ kind: "coin", request }, request.amount));
|
|
15003
|
+
}
|
|
15004
|
+
sendWholeToken(request) {
|
|
15005
|
+
return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
|
|
15006
|
+
}
|
|
15007
|
+
/** NFT-scoped twin: refuses a valued source. */
|
|
15008
|
+
sendCoinless(request) {
|
|
15009
|
+
return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
|
|
14557
15010
|
}
|
|
14558
15011
|
async receive() {
|
|
14559
15012
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -14585,12 +15038,25 @@ var PaymentsFacade = class {
|
|
|
14585
15038
|
nowMs() {
|
|
14586
15039
|
return (this.deps.now ?? Date.now)();
|
|
14587
15040
|
}
|
|
15041
|
+
mintDeps() {
|
|
15042
|
+
return {
|
|
15043
|
+
engine: this.engine(),
|
|
15044
|
+
mintJournal: this.machineStores.mintJournal,
|
|
15045
|
+
storagePort: this.deps.storagePort,
|
|
15046
|
+
recordMint: (input) => this.historyStore.recordMint(input),
|
|
15047
|
+
armHeartbeat: () => this.heartbeat.arm(),
|
|
15048
|
+
noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
|
|
15049
|
+
refreshView: () => this.trackTail(this.view.delta()),
|
|
15050
|
+
ownPubkeyBytes: this.ownPubkeyBytes,
|
|
15051
|
+
now: () => this.nowMs()
|
|
15052
|
+
};
|
|
15053
|
+
}
|
|
14588
15054
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
14589
15055
|
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
14590
15056
|
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
14591
|
-
async
|
|
15057
|
+
async runJob(job, amount) {
|
|
14592
15058
|
const run = {
|
|
14593
|
-
amount
|
|
15059
|
+
amount,
|
|
14594
15060
|
delivered: [],
|
|
14595
15061
|
sentTokens: [],
|
|
14596
15062
|
tokenTransfers: [],
|
|
@@ -14600,7 +15066,7 @@ var PaymentsFacade = class {
|
|
|
14600
15066
|
lastTransferId: ""
|
|
14601
15067
|
};
|
|
14602
15068
|
try {
|
|
14603
|
-
return await this.sendWithPolicy(
|
|
15069
|
+
return await this.sendWithPolicy(job, run);
|
|
14604
15070
|
} catch (err) {
|
|
14605
15071
|
this.emitCleanFailure(err, run.lastTransferId);
|
|
14606
15072
|
throw err;
|
|
@@ -14620,19 +15086,19 @@ var PaymentsFacade = class {
|
|
|
14620
15086
|
};
|
|
14621
15087
|
this.deps.emit("transfer:updated", failed);
|
|
14622
15088
|
}
|
|
14623
|
-
async sendWithPolicy(
|
|
14624
|
-
const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
|
|
15089
|
+
async sendWithPolicy(job, run) {
|
|
15090
|
+
const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
|
|
14625
15091
|
for (let attempt = 0; ; attempt++) {
|
|
14626
15092
|
let ctx;
|
|
14627
15093
|
try {
|
|
14628
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey,
|
|
15094
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
|
|
14629
15095
|
} catch (err) {
|
|
14630
|
-
throw
|
|
15096
|
+
throw partialize(err, run);
|
|
14631
15097
|
}
|
|
14632
15098
|
const disposition = await this.runAttempt(ctx);
|
|
14633
|
-
if (disposition.kind === "rethrow") throw
|
|
15099
|
+
if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
|
|
14634
15100
|
if (disposition.kind === "retry-full") {
|
|
14635
|
-
if (attempt >= MAX_RESELECT) throw
|
|
15101
|
+
if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
|
|
14636
15102
|
continue;
|
|
14637
15103
|
}
|
|
14638
15104
|
if (disposition.kind === "success") {
|
|
@@ -14688,7 +15154,7 @@ var PaymentsFacade = class {
|
|
|
14688
15154
|
async disposeFailedAttempt(ctx, err) {
|
|
14689
15155
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
14690
15156
|
this.settleKeepOpen(ctx);
|
|
14691
|
-
|
|
15157
|
+
stampTransferId(err, ctx.transferId);
|
|
14692
15158
|
return { kind: "rethrow", error: err };
|
|
14693
15159
|
}
|
|
14694
15160
|
const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
|
|
@@ -14722,7 +15188,7 @@ var PaymentsFacade = class {
|
|
|
14722
15188
|
async disposeConvergeFailure(ctx, err) {
|
|
14723
15189
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
14724
15190
|
this.settleKeepOpen(ctx);
|
|
14725
|
-
|
|
15191
|
+
stampTransferId(err, ctx.transferId);
|
|
14726
15192
|
return { kind: "rethrow", error: err };
|
|
14727
15193
|
}
|
|
14728
15194
|
if (classifyError(err) === "conflict") {
|
|
@@ -14733,9 +15199,29 @@ var PaymentsFacade = class {
|
|
|
14733
15199
|
}
|
|
14734
15200
|
return { kind: "rethrow", error: err };
|
|
14735
15201
|
}
|
|
14736
|
-
async planAndMaterialize(recipientPubkey,
|
|
15202
|
+
async planAndMaterialize(recipientPubkey, job, run) {
|
|
14737
15203
|
const transferId = this.newId();
|
|
14738
15204
|
run.lastTransferId = transferId;
|
|
15205
|
+
if (job.kind === "whole") {
|
|
15206
|
+
const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
|
|
15207
|
+
const sourceIds2 = this.markPlanned(transferId, "", spend2);
|
|
15208
|
+
try {
|
|
15209
|
+
return await materializeWholeSpend(
|
|
15210
|
+
{ engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
|
|
15211
|
+
{
|
|
15212
|
+
transferId,
|
|
15213
|
+
recipientPubkey,
|
|
15214
|
+
request: job.request,
|
|
15215
|
+
sourceIds: sourceIds2,
|
|
15216
|
+
requireCoinless: job.requireCoinless
|
|
15217
|
+
}
|
|
15218
|
+
);
|
|
15219
|
+
} catch (err) {
|
|
15220
|
+
this.settleFailure(transferId, "", sourceIds2);
|
|
15221
|
+
throw err;
|
|
15222
|
+
}
|
|
15223
|
+
}
|
|
15224
|
+
const request = { ...job.request, amount: run.amount };
|
|
14739
15225
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
14740
15226
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
14741
15227
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -14869,22 +15355,6 @@ var PaymentsFacade = class {
|
|
|
14869
15355
|
return result;
|
|
14870
15356
|
}
|
|
14871
15357
|
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
14872
|
-
partialize(err, run) {
|
|
14873
|
-
if (run.delivered.length === 0) return err;
|
|
14874
|
-
return new PartialSendConflictError(
|
|
14875
|
-
"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.",
|
|
14876
|
-
run.firstPartialId ?? "",
|
|
14877
|
-
run.delivered,
|
|
14878
|
-
run.amount,
|
|
14879
|
-
err
|
|
14880
|
-
);
|
|
14881
|
-
}
|
|
14882
|
-
/** #441: possibly-committed errors must carry the transferId for the settling journal. */
|
|
14883
|
-
stampTransferId(err, transferId) {
|
|
14884
|
-
if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
|
|
14885
|
-
err.transferId = transferId;
|
|
14886
|
-
}
|
|
14887
|
-
}
|
|
14888
15358
|
async softAbort(transferId) {
|
|
14889
15359
|
try {
|
|
14890
15360
|
await this.deps.client.abortIntent(transferId);
|
|
@@ -14900,57 +15370,11 @@ var PaymentsFacade = class {
|
|
|
14900
15370
|
const mintId = this.newId();
|
|
14901
15371
|
this.activeMoneyOps.add(mintId);
|
|
14902
15372
|
try {
|
|
14903
|
-
return await this.
|
|
15373
|
+
return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
|
|
14904
15374
|
} finally {
|
|
14905
15375
|
this.activeMoneyOps.delete(mintId);
|
|
14906
15376
|
}
|
|
14907
15377
|
}
|
|
14908
|
-
async mintUnderJournal(mintId, coinId, amount) {
|
|
14909
|
-
const engine = this.engine();
|
|
14910
|
-
const entry = {
|
|
14911
|
-
mintId,
|
|
14912
|
-
coinId,
|
|
14913
|
-
amount: amount.toString(),
|
|
14914
|
-
tokenId: "",
|
|
14915
|
-
createdAt: this.nowMs()
|
|
14916
|
-
};
|
|
14917
|
-
await this.machineStores.mintJournal.upsert(entry);
|
|
14918
|
-
let token;
|
|
14919
|
-
try {
|
|
14920
|
-
token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
|
|
14921
|
-
} catch (err) {
|
|
14922
|
-
this.heartbeat.arm();
|
|
14923
|
-
return { success: false, error: messageOf(err) };
|
|
14924
|
-
}
|
|
14925
|
-
if (token.blob.tokenId !== entry.tokenId) {
|
|
14926
|
-
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
14927
|
-
}
|
|
14928
|
-
try {
|
|
14929
|
-
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
14930
|
-
} catch (err) {
|
|
14931
|
-
this.heartbeat.arm();
|
|
14932
|
-
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
14933
|
-
}
|
|
14934
|
-
return { success: true, tokenId: token.blob.tokenId };
|
|
14935
|
-
}
|
|
14936
|
-
async finalizeMint(engine, mintId, token, coinId, amount) {
|
|
14937
|
-
const bytes = token.blob.token;
|
|
14938
|
-
const digest = bytesToHex3(sha256(bytes));
|
|
14939
|
-
const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
|
|
14940
|
-
const key = keys.get(digest);
|
|
14941
|
-
if (key === void 0) {
|
|
14942
|
-
throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
|
|
14943
|
-
}
|
|
14944
|
-
await this.deps.storagePort.applyDelta({
|
|
14945
|
-
transferId: mintId,
|
|
14946
|
-
spent: [],
|
|
14947
|
-
added: [{ tokenId: token.blob.tokenId, key }]
|
|
14948
|
-
});
|
|
14949
|
-
await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
|
|
14950
|
-
await this.machineStores.mintJournal.removeByKey(mintId);
|
|
14951
|
-
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
14952
|
-
this.trackTail(this.view.delta());
|
|
14953
|
-
}
|
|
14954
15378
|
/** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
|
|
14955
15379
|
async replayMints() {
|
|
14956
15380
|
let resolved = 0;
|
|
@@ -14982,7 +15406,7 @@ var PaymentsFacade = class {
|
|
|
14982
15406
|
if (token.blob.tokenId !== entry.tokenId) {
|
|
14983
15407
|
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
14984
15408
|
}
|
|
14985
|
-
await this.
|
|
15409
|
+
await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
|
|
14986
15410
|
return true;
|
|
14987
15411
|
}
|
|
14988
15412
|
async tokenInServerInventory(tokenId) {
|