@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.js
CHANGED
|
@@ -8146,6 +8146,8 @@ var FETCH_TIMEOUT_MS = 1e4;
|
|
|
8146
8146
|
var TokenRegistry = class _TokenRegistry {
|
|
8147
8147
|
static instance = null;
|
|
8148
8148
|
definitionsById;
|
|
8149
|
+
/** Non-fungible definitions keyed by TOKEN TYPE — a separate namespace (#147). */
|
|
8150
|
+
definitionsByType;
|
|
8149
8151
|
definitionsBySymbol;
|
|
8150
8152
|
definitionsByName;
|
|
8151
8153
|
// Remote refresh state
|
|
@@ -8164,6 +8166,7 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8164
8166
|
inFlight = null;
|
|
8165
8167
|
constructor() {
|
|
8166
8168
|
this.definitionsById = /* @__PURE__ */ new Map();
|
|
8169
|
+
this.definitionsByType = /* @__PURE__ */ new Map();
|
|
8167
8170
|
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
8168
8171
|
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
8169
8172
|
}
|
|
@@ -8391,9 +8394,11 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8391
8394
|
this.definitionsById.clear();
|
|
8392
8395
|
this.definitionsBySymbol.clear();
|
|
8393
8396
|
this.definitionsByName.clear();
|
|
8397
|
+
this.definitionsByType.clear();
|
|
8394
8398
|
for (const def of definitions) {
|
|
8395
8399
|
const idLower = def.id.toLowerCase();
|
|
8396
8400
|
this.definitionsById.set(idLower, def);
|
|
8401
|
+
if (def.assetKind === "non-fungible") this.definitionsByType.set(idLower, def);
|
|
8397
8402
|
if (def.symbol) {
|
|
8398
8403
|
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
8399
8404
|
}
|
|
@@ -8607,6 +8612,20 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
8607
8612
|
getAllDefinitions() {
|
|
8608
8613
|
return Array.from(this.definitionsById.values());
|
|
8609
8614
|
}
|
|
8615
|
+
/**
|
|
8616
|
+
* Definition for a coinless token's genesis TOKEN TYPE (#777). Never falls back
|
|
8617
|
+
* to `getDefinition`: a type and a coin id live in different namespaces, so a
|
|
8618
|
+
* type that collided with a coin id would otherwise render as that coin.
|
|
8619
|
+
*/
|
|
8620
|
+
getTypeMeta(tokenType) {
|
|
8621
|
+
const def = this.getTypeDefinition(tokenType);
|
|
8622
|
+
if (!def) return null;
|
|
8623
|
+
return { name: def.name, iconUrl: def.icons?.[0]?.url ?? null };
|
|
8624
|
+
}
|
|
8625
|
+
getTypeDefinition(tokenType) {
|
|
8626
|
+
if (!tokenType) return void 0;
|
|
8627
|
+
return this.definitionsByType.get(tokenType.toLowerCase());
|
|
8628
|
+
}
|
|
8610
8629
|
/**
|
|
8611
8630
|
* Get all fungible token definitions
|
|
8612
8631
|
* @returns Array of fungible token definitions
|
|
@@ -9441,6 +9460,8 @@ import { DataHasherFactory } from "@unicitylabs/state-transition-sdk/lib/crypto/
|
|
|
9441
9460
|
import { CborSerializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js";
|
|
9442
9461
|
import { CborDeserializer } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js";
|
|
9443
9462
|
import { CborError } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborError.js";
|
|
9463
|
+
import { CborReader } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborReader.js";
|
|
9464
|
+
import { MajorType } from "@unicitylabs/state-transition-sdk/lib/serialization/cbor/MajorType.js";
|
|
9444
9465
|
import { Asset } from "@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js";
|
|
9445
9466
|
import { AssetId } from "@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js";
|
|
9446
9467
|
import { PaymentAssetCollection } from "@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js";
|
|
@@ -9853,6 +9874,126 @@ async function burntTokenFromCheckpoint(deps, storedBytes, reDerivedBurnTx, sour
|
|
|
9853
9874
|
return burntToken;
|
|
9854
9875
|
}
|
|
9855
9876
|
|
|
9877
|
+
// token-engine/value-envelope.ts
|
|
9878
|
+
function isCoinlessEnvelope(envelope) {
|
|
9879
|
+
return envelope.startsWith("none_");
|
|
9880
|
+
}
|
|
9881
|
+
var SELF_DESCRIBED_CBOR_TAG = 55799n;
|
|
9882
|
+
var CBOR_MAJOR_TYPE_MASK = 224;
|
|
9883
|
+
var describe = (error) => error instanceof Error ? error.message : String(error);
|
|
9884
|
+
var invalid = (message) => new SphereError(message, "VALIDATION_ERROR");
|
|
9885
|
+
function majorTypeOf(bytes) {
|
|
9886
|
+
const first = bytes.at(0);
|
|
9887
|
+
return first === void 0 ? null : first & CBOR_MAJOR_TYPE_MASK;
|
|
9888
|
+
}
|
|
9889
|
+
function headTagNumber(bytes) {
|
|
9890
|
+
try {
|
|
9891
|
+
return new CborReader(bytes).readLength(MajorType.TAG);
|
|
9892
|
+
} catch {
|
|
9893
|
+
return null;
|
|
9894
|
+
}
|
|
9895
|
+
}
|
|
9896
|
+
function isAssetShaped(item) {
|
|
9897
|
+
try {
|
|
9898
|
+
return CborDeserializer.decodeArray(item, 2).every(
|
|
9899
|
+
(field) => majorTypeOf(field) === MajorType.BYTE_STRING
|
|
9900
|
+
);
|
|
9901
|
+
} catch {
|
|
9902
|
+
return false;
|
|
9903
|
+
}
|
|
9904
|
+
}
|
|
9905
|
+
function arrayItemsOrNull(bytes) {
|
|
9906
|
+
try {
|
|
9907
|
+
return CborDeserializer.decodeArray(bytes);
|
|
9908
|
+
} catch {
|
|
9909
|
+
return null;
|
|
9910
|
+
}
|
|
9911
|
+
}
|
|
9912
|
+
function decodeSphere(genesisData) {
|
|
9913
|
+
try {
|
|
9914
|
+
return SpherePaymentData.fromCBOR(genesisData).toValue();
|
|
9915
|
+
} catch (error) {
|
|
9916
|
+
throw invalid(`Failed to decode token payment data: ${describe(error)}`);
|
|
9917
|
+
}
|
|
9918
|
+
}
|
|
9919
|
+
function taggedItemBody(genesisData) {
|
|
9920
|
+
try {
|
|
9921
|
+
return CborDeserializer.decodeTag(genesisData).data;
|
|
9922
|
+
} catch (error) {
|
|
9923
|
+
throw invalid(
|
|
9924
|
+
`Token value payload is a CBOR tag whose item is malformed or carries trailing bytes: ${describe(error)}`
|
|
9925
|
+
);
|
|
9926
|
+
}
|
|
9927
|
+
}
|
|
9928
|
+
function selfDescribedBodyCarriesValue(body) {
|
|
9929
|
+
const major = majorTypeOf(body);
|
|
9930
|
+
if (major === MajorType.TAG) {
|
|
9931
|
+
const inner = headTagNumber(body);
|
|
9932
|
+
return inner === SpherePaymentData.CBOR_TAG || inner === SELF_DESCRIBED_CBOR_TAG;
|
|
9933
|
+
}
|
|
9934
|
+
if (major === MajorType.ARRAY) {
|
|
9935
|
+
const items = arrayItemsOrNull(body);
|
|
9936
|
+
return items !== null && items.some(isAssetShaped);
|
|
9937
|
+
}
|
|
9938
|
+
return false;
|
|
9939
|
+
}
|
|
9940
|
+
function classifyTagged(genesisData) {
|
|
9941
|
+
const tag = headTagNumber(genesisData);
|
|
9942
|
+
if (tag === null) {
|
|
9943
|
+
throw invalid(
|
|
9944
|
+
"Token value payload has an unreadable or non-canonically encoded CBOR tag head \u2014 it could be a malformed SpherePaymentData envelope"
|
|
9945
|
+
);
|
|
9946
|
+
}
|
|
9947
|
+
if (tag === SpherePaymentData.CBOR_TAG) {
|
|
9948
|
+
return { envelope: "sphere", value: decodeSphere(genesisData) };
|
|
9949
|
+
}
|
|
9950
|
+
const body = taggedItemBody(genesisData);
|
|
9951
|
+
if (tag === SELF_DESCRIBED_CBOR_TAG && selfDescribedBodyCarriesValue(body)) {
|
|
9952
|
+
throw invalid(
|
|
9953
|
+
"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"
|
|
9954
|
+
);
|
|
9955
|
+
}
|
|
9956
|
+
return { envelope: "none_tag", value: null };
|
|
9957
|
+
}
|
|
9958
|
+
function classifyArray(genesisData) {
|
|
9959
|
+
const items = arrayItemsOrNull(genesisData);
|
|
9960
|
+
if (items === null) {
|
|
9961
|
+
throw invalid("Token value payload is not PaymentAssetCollection: malformed CBOR array");
|
|
9962
|
+
}
|
|
9963
|
+
return items.some(isAssetShaped) ? { envelope: "bare_collection", value: null } : { envelope: "none_other", value: null };
|
|
9964
|
+
}
|
|
9965
|
+
function classifyValueEnvelope(genesisData) {
|
|
9966
|
+
if (genesisData === null) return { envelope: "none_absent", value: null };
|
|
9967
|
+
const major = majorTypeOf(genesisData);
|
|
9968
|
+
if (major === null) return { envelope: "none_absent", value: null };
|
|
9969
|
+
if (major === MajorType.TAG) return classifyTagged(genesisData);
|
|
9970
|
+
if (major === MajorType.ARRAY) return classifyArray(genesisData);
|
|
9971
|
+
return { envelope: "none_other", value: null };
|
|
9972
|
+
}
|
|
9973
|
+
function wrapToken(sdkToken) {
|
|
9974
|
+
const { envelope, value } = classifyValueEnvelope(sdkToken.genesis.data);
|
|
9975
|
+
const blob = {
|
|
9976
|
+
tokenId: HexConverter.encode(sdkToken.id.bytes),
|
|
9977
|
+
token: sdkToken.toCBOR()
|
|
9978
|
+
};
|
|
9979
|
+
return {
|
|
9980
|
+
sdkToken,
|
|
9981
|
+
blob,
|
|
9982
|
+
value,
|
|
9983
|
+
valueEnvelope: envelope,
|
|
9984
|
+
tokenType: HexConverter.encode(sdkToken.type.bytes)
|
|
9985
|
+
};
|
|
9986
|
+
}
|
|
9987
|
+
function assertMintableData(data) {
|
|
9988
|
+
try {
|
|
9989
|
+
classifyValueEnvelope(data);
|
|
9990
|
+
} catch (error) {
|
|
9991
|
+
throw invalid(
|
|
9992
|
+
`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.`
|
|
9993
|
+
);
|
|
9994
|
+
}
|
|
9995
|
+
}
|
|
9996
|
+
|
|
9856
9997
|
// token-engine/SphereTokenEngine.ts
|
|
9857
9998
|
var DEFAULT_PROOF_POLL_INTERVAL_MS = 300;
|
|
9858
9999
|
var MAX_MINT_CONCURRENCY = 8;
|
|
@@ -9909,7 +10050,7 @@ var SphereTokenEngine = class {
|
|
|
9909
10050
|
return sdkToken.latestTransaction.data;
|
|
9910
10051
|
}
|
|
9911
10052
|
const data = sdkToken.genesis.data;
|
|
9912
|
-
if (data &&
|
|
10053
|
+
if (data && classifyValueEnvelope(data).envelope === "sphere") {
|
|
9913
10054
|
return SpherePaymentData.fromCBOR(data).memo;
|
|
9914
10055
|
}
|
|
9915
10056
|
return null;
|
|
@@ -9939,9 +10080,10 @@ var SphereTokenEngine = class {
|
|
|
9939
10080
|
);
|
|
9940
10081
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
9941
10082
|
const token = await Token.mint(certified, this.deps.verificationContext);
|
|
9942
|
-
return
|
|
10083
|
+
return wrapToken(token);
|
|
9943
10084
|
}
|
|
9944
10085
|
async mintDataToken(params, options) {
|
|
10086
|
+
assertMintableData(params.data);
|
|
9945
10087
|
const recipient = SignaturePredicate.create(params.recipientPubkey);
|
|
9946
10088
|
const tokenType = params.tokenType ? new TokenType(params.tokenType) : TokenType.generate();
|
|
9947
10089
|
const salt = params.salt ? TokenSalt.fromBytes(params.salt) : TokenSalt.generate();
|
|
@@ -9956,7 +10098,7 @@ var SphereTokenEngine = class {
|
|
|
9956
10098
|
);
|
|
9957
10099
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
9958
10100
|
const token = await Token.mint(certified, this.deps.verificationContext);
|
|
9959
|
-
return
|
|
10101
|
+
return wrapToken(token);
|
|
9960
10102
|
}
|
|
9961
10103
|
async transfer(params, options) {
|
|
9962
10104
|
this.assertOwned(params.token);
|
|
@@ -9979,13 +10121,19 @@ var SphereTokenEngine = class {
|
|
|
9979
10121
|
);
|
|
9980
10122
|
const certified = await transferTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
9981
10123
|
const transferred = await params.token.sdkToken.transfer(certified, this.deps.verificationContext);
|
|
9982
|
-
return
|
|
10124
|
+
return wrapToken(transferred);
|
|
9983
10125
|
}
|
|
9984
10126
|
async split(params, options) {
|
|
9985
10127
|
this.assertOwned(params.token);
|
|
9986
10128
|
if (params.outputs.length === 0) {
|
|
9987
10129
|
throw new SphereError("Split requires at least one output", "VALIDATION_ERROR");
|
|
9988
10130
|
}
|
|
10131
|
+
if (params.token.value === null) {
|
|
10132
|
+
throw new SphereError(
|
|
10133
|
+
`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.`,
|
|
10134
|
+
"VALIDATION_ERROR"
|
|
10135
|
+
);
|
|
10136
|
+
}
|
|
9989
10137
|
const transferId = this.resolveTransferId(options);
|
|
9990
10138
|
const requests = params.outputs.map(
|
|
9991
10139
|
(o, i) => SplitTokenRequest.create(
|
|
@@ -10107,7 +10255,7 @@ var SphereTokenEngine = class {
|
|
|
10107
10255
|
const proof = await this.submitSplitMintLeg(certData, mintTx, options);
|
|
10108
10256
|
const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
|
|
10109
10257
|
const token = await Token.mint(certified, this.deps.verificationContext);
|
|
10110
|
-
return
|
|
10258
|
+
return wrapToken(token);
|
|
10111
10259
|
}
|
|
10112
10260
|
/**
|
|
10113
10261
|
* Submit one split-mint leg. A `TRANSACTION_HASH_MISMATCH` on a mint leg is NOT a foreign spend
|
|
@@ -10165,7 +10313,7 @@ var SphereTokenEngine = class {
|
|
|
10165
10313
|
"VALIDATION_ERROR"
|
|
10166
10314
|
);
|
|
10167
10315
|
}
|
|
10168
|
-
return
|
|
10316
|
+
return wrapToken(sdkToken);
|
|
10169
10317
|
}
|
|
10170
10318
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
10171
10319
|
/**
|
|
@@ -10277,34 +10425,6 @@ var SphereTokenEngine = class {
|
|
|
10277
10425
|
throw new SphereError("Cannot transfer a token not owned by this engine identity", "VALIDATION_ERROR");
|
|
10278
10426
|
}
|
|
10279
10427
|
}
|
|
10280
|
-
/** Wrap an SDK token into a SphereToken: cache its blob (incl. stable tokenId) + decoded value. */
|
|
10281
|
-
wrapToken(sdkToken) {
|
|
10282
|
-
const data = sdkToken.genesis.data;
|
|
10283
|
-
let value = null;
|
|
10284
|
-
if (data && this.isSpherePaymentData(data)) {
|
|
10285
|
-
try {
|
|
10286
|
-
value = SpherePaymentData.fromCBOR(data).toValue();
|
|
10287
|
-
} catch (err) {
|
|
10288
|
-
throw new SphereError(
|
|
10289
|
-
`Failed to decode token payment data: ${err instanceof Error ? err.message : String(err)}`,
|
|
10290
|
-
"VALIDATION_ERROR"
|
|
10291
|
-
);
|
|
10292
|
-
}
|
|
10293
|
-
}
|
|
10294
|
-
const blob = {
|
|
10295
|
-
tokenId: HexConverter.encode(sdkToken.id.bytes),
|
|
10296
|
-
token: sdkToken.toCBOR()
|
|
10297
|
-
};
|
|
10298
|
-
return { sdkToken, blob, value };
|
|
10299
|
-
}
|
|
10300
|
-
/** True if the bytes are a SpherePaymentData envelope (value token) vs a raw data token. */
|
|
10301
|
-
isSpherePaymentData(data) {
|
|
10302
|
-
try {
|
|
10303
|
-
return CborDeserializer.decodeTag(data).tag === SpherePaymentData.CBOR_TAG;
|
|
10304
|
-
} catch {
|
|
10305
|
-
return false;
|
|
10306
|
-
}
|
|
10307
|
-
}
|
|
10308
10428
|
/**
|
|
10309
10429
|
* Terminate the verification worker pool, if this engine was configured with
|
|
10310
10430
|
* one. Idempotent — the pool's dispose only touches workers it spawned.
|
|
@@ -11700,6 +11820,17 @@ function createMachineStores(kv) {
|
|
|
11700
11820
|
};
|
|
11701
11821
|
}
|
|
11702
11822
|
|
|
11823
|
+
// modules/payments-v2/machine/payload-view.ts
|
|
11824
|
+
function isWholeIntent(p) {
|
|
11825
|
+
return p.kind === "whole" || p.kind === "coinless";
|
|
11826
|
+
}
|
|
11827
|
+
function splitOf(p) {
|
|
11828
|
+
return p.kind === "coin" ? p.split : void 0;
|
|
11829
|
+
}
|
|
11830
|
+
function coinIdOf(p) {
|
|
11831
|
+
return p.kind === "coin" ? p.coinId : void 0;
|
|
11832
|
+
}
|
|
11833
|
+
|
|
11703
11834
|
// modules/payments-v2/machine/TransferMachine.ts
|
|
11704
11835
|
var CERTIFY_WIDTH = 20;
|
|
11705
11836
|
var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
|
|
@@ -11902,13 +12033,14 @@ var TransferMachine = class {
|
|
|
11902
12033
|
const finished = await engine.transfer({ token: source, recipientPubkey }, opts);
|
|
11903
12034
|
return { recipientBlob: finished.blob.token };
|
|
11904
12035
|
}
|
|
11905
|
-
const split = ctx.payload
|
|
12036
|
+
const split = splitOf(ctx.payload);
|
|
12037
|
+
const splitCoinId = coinIdOf(ctx.payload);
|
|
11906
12038
|
const { outputs } = await engine.split(
|
|
11907
12039
|
{
|
|
11908
12040
|
token: source,
|
|
11909
12041
|
outputs: [
|
|
11910
|
-
{ recipientPubkey, coinId:
|
|
11911
|
-
{ recipientPubkey: this.deps.ownPubkey, coinId:
|
|
12042
|
+
{ recipientPubkey, coinId: splitCoinId, amount: BigInt(split.splitAmount) },
|
|
12043
|
+
{ recipientPubkey: this.deps.ownPubkey, coinId: splitCoinId, amount: BigInt(split.remainderAmount) }
|
|
11912
12044
|
]
|
|
11913
12045
|
},
|
|
11914
12046
|
opts
|
|
@@ -12030,7 +12162,9 @@ var TransferMachine = class {
|
|
|
12030
12162
|
const entry = {
|
|
12031
12163
|
transferId: r.transferId,
|
|
12032
12164
|
remainingAmount: undelivered.toString(),
|
|
12033
|
-
|
|
12165
|
+
// '' only on a token-addressed intent, which is one leg: it either lands or
|
|
12166
|
+
// conflicts, so a shortfall row is unreachable there (settlePartial needs >1).
|
|
12167
|
+
coinId: coinIdOf(r.payload) ?? "",
|
|
12034
12168
|
recipient: r.payload.recipient,
|
|
12035
12169
|
committedTokenIds: committed.map((o) => o.op.sourceTokenId),
|
|
12036
12170
|
createdAt: this.deps.now()
|
|
@@ -12056,8 +12190,16 @@ var TransferMachine = class {
|
|
|
12056
12190
|
}
|
|
12057
12191
|
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
12058
12192
|
try {
|
|
12059
|
-
const
|
|
12060
|
-
await this.
|
|
12193
|
+
const coinId = coinIdOf(payload);
|
|
12194
|
+
const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
|
|
12195
|
+
const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
|
|
12196
|
+
await this.deps.recordHistory?.({
|
|
12197
|
+
transferId,
|
|
12198
|
+
payload,
|
|
12199
|
+
phase,
|
|
12200
|
+
committedAmount,
|
|
12201
|
+
...committedAssets !== void 0 ? { committedAssets } : {}
|
|
12202
|
+
});
|
|
12061
12203
|
} catch {
|
|
12062
12204
|
}
|
|
12063
12205
|
try {
|
|
@@ -12071,6 +12213,18 @@ var TransferMachine = class {
|
|
|
12071
12213
|
* a journal-rehydrated resume leg has no source token, but every committed
|
|
12072
12214
|
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
12073
12215
|
*/
|
|
12216
|
+
/** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
|
|
12217
|
+
async settledAssets(engine, committed) {
|
|
12218
|
+
const total = /* @__PURE__ */ new Map();
|
|
12219
|
+
for (const outcome of committed) {
|
|
12220
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
12221
|
+
const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
|
|
12222
|
+
for (const asset of engine.readValue(token)?.assets ?? []) {
|
|
12223
|
+
total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
|
|
12224
|
+
}
|
|
12225
|
+
}
|
|
12226
|
+
return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
|
|
12227
|
+
}
|
|
12074
12228
|
async settledAmount(engine, coinId, committed) {
|
|
12075
12229
|
let total = 0n;
|
|
12076
12230
|
for (const outcome of committed) {
|
|
@@ -12099,8 +12253,11 @@ var TransferMachine = class {
|
|
|
12099
12253
|
}
|
|
12100
12254
|
};
|
|
12101
12255
|
function conflictAmount(engine, payload, op, source) {
|
|
12102
|
-
|
|
12103
|
-
|
|
12256
|
+
const split = splitOf(payload);
|
|
12257
|
+
if (op.kind === "split") return BigInt(split.splitAmount);
|
|
12258
|
+
const coinId = coinIdOf(payload);
|
|
12259
|
+
if (coinId === void 0) return 0n;
|
|
12260
|
+
return source !== void 0 ? engine.balanceOf(source, coinId) : 0n;
|
|
12104
12261
|
}
|
|
12105
12262
|
|
|
12106
12263
|
// modules/payments-v2/machine/resume.ts
|
|
@@ -12207,6 +12364,14 @@ async function runOne(ctx, job, report) {
|
|
|
12207
12364
|
report.failed.push(job.transferId);
|
|
12208
12365
|
return;
|
|
12209
12366
|
}
|
|
12367
|
+
if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
|
|
12368
|
+
logger.warn(
|
|
12369
|
+
"PaymentsV2",
|
|
12370
|
+
`resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
|
|
12371
|
+
);
|
|
12372
|
+
report.failed.push(job.transferId);
|
|
12373
|
+
return;
|
|
12374
|
+
}
|
|
12210
12375
|
mine.set(op.sourceTokenId, token);
|
|
12211
12376
|
}
|
|
12212
12377
|
try {
|
|
@@ -12237,6 +12402,28 @@ async function classifyResumeFailure(deps, stores, transferId, err, report) {
|
|
|
12237
12402
|
}
|
|
12238
12403
|
report.failed.push(transferId);
|
|
12239
12404
|
}
|
|
12405
|
+
function validateCoinPayload(c) {
|
|
12406
|
+
if (typeof c.coinId !== "string" || typeof c.amount !== "string") {
|
|
12407
|
+
throw new SphereError("intent payload is missing coinId/amount", "VALIDATION_ERROR");
|
|
12408
|
+
}
|
|
12409
|
+
const s = c.split;
|
|
12410
|
+
if (s !== void 0 && (typeof s.tokenId !== "string" || typeof s.splitAmount !== "string" || typeof s.remainderAmount !== "string")) {
|
|
12411
|
+
throw new SphereError("intent payload split spec is malformed", "VALIDATION_ERROR");
|
|
12412
|
+
}
|
|
12413
|
+
return { ...c, kind: "coin" };
|
|
12414
|
+
}
|
|
12415
|
+
function validateWholePayload(p) {
|
|
12416
|
+
if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
|
|
12417
|
+
throw new SphereError(
|
|
12418
|
+
"token intent payload must name exactly one source token",
|
|
12419
|
+
"VALIDATION_ERROR"
|
|
12420
|
+
);
|
|
12421
|
+
}
|
|
12422
|
+
if (p.split !== void 0) {
|
|
12423
|
+
throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
|
|
12424
|
+
}
|
|
12425
|
+
return { ...p, kind: "whole", direct: [p.direct[0]] };
|
|
12426
|
+
}
|
|
12240
12427
|
function validatePayload(raw) {
|
|
12241
12428
|
const p = raw !== null && typeof raw === "object" ? raw : null;
|
|
12242
12429
|
if (p === null || p.v !== 2 || !Array.isArray(p.direct) || p.direct.some((t) => typeof t !== "string")) {
|
|
@@ -12245,14 +12432,17 @@ function validatePayload(raw) {
|
|
|
12245
12432
|
"VALIDATION_ERROR"
|
|
12246
12433
|
);
|
|
12247
12434
|
}
|
|
12248
|
-
if (typeof p.recipient !== "string"
|
|
12249
|
-
throw new SphereError("intent payload is missing recipient
|
|
12435
|
+
if (typeof p.recipient !== "string") {
|
|
12436
|
+
throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
|
|
12250
12437
|
}
|
|
12251
|
-
const
|
|
12252
|
-
if (
|
|
12253
|
-
throw new SphereError(
|
|
12438
|
+
const kind = p.kind ?? "coin";
|
|
12439
|
+
if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
|
|
12440
|
+
throw new SphereError(
|
|
12441
|
+
`unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
|
|
12442
|
+
"VALIDATION_ERROR"
|
|
12443
|
+
);
|
|
12254
12444
|
}
|
|
12255
|
-
return p;
|
|
12445
|
+
return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
|
|
12256
12446
|
}
|
|
12257
12447
|
|
|
12258
12448
|
// modules/payments-v2/convergence.ts
|
|
@@ -12458,13 +12648,24 @@ async function openRow(entry, journal, decryptPayload) {
|
|
|
12458
12648
|
transferId: entry.transferId,
|
|
12459
12649
|
kind: "open",
|
|
12460
12650
|
recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
|
|
12461
|
-
|
|
12462
|
-
amount: typeof payload?.amount === "string" ? payload.amount : "",
|
|
12651
|
+
...subject(payload),
|
|
12463
12652
|
legs: { certified, total: Math.max(total, certified) },
|
|
12464
12653
|
deliveryPending: journal.length > 0,
|
|
12465
12654
|
createdAt: entry.createdAt
|
|
12466
12655
|
};
|
|
12467
12656
|
}
|
|
12657
|
+
function subject(payload) {
|
|
12658
|
+
const rawKind = payload?.kind;
|
|
12659
|
+
if (rawKind === "whole" || rawKind === "coinless") {
|
|
12660
|
+
const tokenId = payload?.direct?.[0];
|
|
12661
|
+
return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
|
|
12662
|
+
}
|
|
12663
|
+
const coin = payload;
|
|
12664
|
+
return {
|
|
12665
|
+
coinId: typeof coin?.coinId === "string" ? coin.coinId : "",
|
|
12666
|
+
amount: typeof coin?.amount === "string" ? coin.amount : ""
|
|
12667
|
+
};
|
|
12668
|
+
}
|
|
12468
12669
|
function journalOnlyRow(transferId, entries) {
|
|
12469
12670
|
const certified = distinctOpCount(entries);
|
|
12470
12671
|
return {
|
|
@@ -12494,6 +12695,263 @@ function distinctOpCount(entries) {
|
|
|
12494
12695
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
12495
12696
|
}
|
|
12496
12697
|
|
|
12698
|
+
// modules/payments-v2/inventory/token-data.ts
|
|
12699
|
+
async function readTokenData(deps, tokenId) {
|
|
12700
|
+
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
12701
|
+
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
12702
|
+
}
|
|
12703
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
12704
|
+
if (bytes === void 0) {
|
|
12705
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
12706
|
+
}
|
|
12707
|
+
return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
|
|
12708
|
+
}
|
|
12709
|
+
|
|
12710
|
+
// modules/payments-v2/machine/payload.ts
|
|
12711
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12712
|
+
return {
|
|
12713
|
+
v: 2,
|
|
12714
|
+
kind: "coin",
|
|
12715
|
+
recipient: recipientPubkey,
|
|
12716
|
+
coinId: request.coinId,
|
|
12717
|
+
amount: request.amount,
|
|
12718
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12719
|
+
direct: [...spend.plan.direct],
|
|
12720
|
+
...spend.plan.split !== void 0 ? {
|
|
12721
|
+
split: {
|
|
12722
|
+
tokenId: spend.plan.split.tokenId,
|
|
12723
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12724
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12725
|
+
}
|
|
12726
|
+
} : {},
|
|
12727
|
+
spentStates
|
|
12728
|
+
};
|
|
12729
|
+
}
|
|
12730
|
+
function messageOf(err) {
|
|
12731
|
+
return err instanceof Error ? err.message : String(err);
|
|
12732
|
+
}
|
|
12733
|
+
function buildWholePayload(recipientPubkey, request, spentStates) {
|
|
12734
|
+
return {
|
|
12735
|
+
v: 2,
|
|
12736
|
+
kind: "whole",
|
|
12737
|
+
recipient: recipientPubkey,
|
|
12738
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12739
|
+
direct: [request.tokenId],
|
|
12740
|
+
spentStates
|
|
12741
|
+
};
|
|
12742
|
+
}
|
|
12743
|
+
|
|
12744
|
+
// modules/payments-v2/mint-params.ts
|
|
12745
|
+
function mintParams(recipientPubkey, coinId, amount) {
|
|
12746
|
+
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
12747
|
+
}
|
|
12748
|
+
|
|
12749
|
+
// modules/payments-v2/mint.ts
|
|
12750
|
+
function runMintUnderJournal(deps, input) {
|
|
12751
|
+
return mintUnderJournal(deps, input);
|
|
12752
|
+
}
|
|
12753
|
+
async function mintUnderJournal(deps, input) {
|
|
12754
|
+
const { mintId, coinId, amount } = input;
|
|
12755
|
+
const engine = deps.engine;
|
|
12756
|
+
const entry = {
|
|
12757
|
+
mintId,
|
|
12758
|
+
coinId,
|
|
12759
|
+
amount: amount.toString(),
|
|
12760
|
+
tokenId: "",
|
|
12761
|
+
createdAt: deps.now()
|
|
12762
|
+
};
|
|
12763
|
+
await deps.mintJournal.upsert(entry);
|
|
12764
|
+
let token;
|
|
12765
|
+
try {
|
|
12766
|
+
token = await engine.mint(mintParams(deps.ownPubkeyBytes, coinId, amount), { transferId: mintId });
|
|
12767
|
+
} catch (err) {
|
|
12768
|
+
deps.armHeartbeat();
|
|
12769
|
+
return { success: false, error: messageOf(err) };
|
|
12770
|
+
}
|
|
12771
|
+
if (token.blob.tokenId !== entry.tokenId) {
|
|
12772
|
+
await deps.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
12773
|
+
}
|
|
12774
|
+
try {
|
|
12775
|
+
await finalizeMint(deps, mintId, token, coinId, amount.toString());
|
|
12776
|
+
} catch (err) {
|
|
12777
|
+
deps.armHeartbeat();
|
|
12778
|
+
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
12779
|
+
}
|
|
12780
|
+
return { success: true, tokenId: token.blob.tokenId };
|
|
12781
|
+
}
|
|
12782
|
+
async function finalizeMint(deps, mintId, token, coinId, amount) {
|
|
12783
|
+
const engine = deps.engine;
|
|
12784
|
+
const bytes = token.blob.token;
|
|
12785
|
+
const digest = bytesToHex3(sha256(bytes));
|
|
12786
|
+
const keys = await deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
|
|
12787
|
+
const key = keys.get(digest);
|
|
12788
|
+
if (key === void 0) {
|
|
12789
|
+
throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
|
|
12790
|
+
}
|
|
12791
|
+
await deps.storagePort.applyDelta({
|
|
12792
|
+
transferId: mintId,
|
|
12793
|
+
spent: [],
|
|
12794
|
+
added: [{ tokenId: token.blob.tokenId, key }]
|
|
12795
|
+
});
|
|
12796
|
+
await deps.recordMint({
|
|
12797
|
+
tokenId: token.blob.tokenId,
|
|
12798
|
+
assets: [{ coinId, amount }]
|
|
12799
|
+
});
|
|
12800
|
+
await deps.mintJournal.removeByKey(mintId);
|
|
12801
|
+
deps.noteHeldState(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
12802
|
+
deps.refreshView();
|
|
12803
|
+
}
|
|
12804
|
+
|
|
12805
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
12806
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12807
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12808
|
+
return {
|
|
12809
|
+
id: tokenId,
|
|
12810
|
+
coinId: asset.coinId,
|
|
12811
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
12812
|
+
name: registry.getName(asset.coinId),
|
|
12813
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
12814
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12815
|
+
amount: asset.amount,
|
|
12816
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
12817
|
+
createdAt: snapshot.createdAt,
|
|
12818
|
+
updatedAt: snapshot.updatedAt,
|
|
12819
|
+
lazy: true,
|
|
12820
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12821
|
+
};
|
|
12822
|
+
}
|
|
12823
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
12824
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12825
|
+
for (const [tokenId, entry] of entries) {
|
|
12826
|
+
const moving = isTransferring(tokenId);
|
|
12827
|
+
for (const asset of entry.assets) {
|
|
12828
|
+
let group = groups.get(asset.coinId);
|
|
12829
|
+
if (!group) {
|
|
12830
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12831
|
+
groups.set(asset.coinId, group);
|
|
12832
|
+
}
|
|
12833
|
+
if (moving) {
|
|
12834
|
+
group.transferring += BigInt(asset.amount);
|
|
12835
|
+
group.transferringCount += 1;
|
|
12836
|
+
} else {
|
|
12837
|
+
group.total += BigInt(asset.amount);
|
|
12838
|
+
group.count += 1;
|
|
12839
|
+
}
|
|
12840
|
+
}
|
|
12841
|
+
}
|
|
12842
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12843
|
+
}
|
|
12844
|
+
function toAsset(coinId, totals, registry) {
|
|
12845
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
12846
|
+
return {
|
|
12847
|
+
coinId,
|
|
12848
|
+
symbol: registry.getSymbol(coinId),
|
|
12849
|
+
name: registry.getName(coinId),
|
|
12850
|
+
decimals: registry.getDecimals(coinId),
|
|
12851
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12852
|
+
totalAmount: totals.total.toString(),
|
|
12853
|
+
tokenCount: totals.count,
|
|
12854
|
+
confirmedAmount: totals.total.toString(),
|
|
12855
|
+
unconfirmedAmount: "0",
|
|
12856
|
+
confirmedTokenCount: totals.count,
|
|
12857
|
+
unconfirmedTokenCount: 0,
|
|
12858
|
+
transferringTokenCount: totals.transferringCount,
|
|
12859
|
+
transferringAmount: totals.transferring.toString(),
|
|
12860
|
+
priceUsd: null,
|
|
12861
|
+
priceEur: null,
|
|
12862
|
+
change24h: null,
|
|
12863
|
+
fiatValueUsd: null,
|
|
12864
|
+
fiatValueEur: null
|
|
12865
|
+
};
|
|
12866
|
+
}
|
|
12867
|
+
function priceIdOf(registry, coinId) {
|
|
12868
|
+
return registry.getName(coinId).toLowerCase();
|
|
12869
|
+
}
|
|
12870
|
+
async function withPrices(raw, registry, price) {
|
|
12871
|
+
try {
|
|
12872
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12873
|
+
const quotes = await price.getPrices(ids);
|
|
12874
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12875
|
+
} catch {
|
|
12876
|
+
return raw;
|
|
12877
|
+
}
|
|
12878
|
+
}
|
|
12879
|
+
function priceOne(asset, quote) {
|
|
12880
|
+
if (!quote) return asset;
|
|
12881
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12882
|
+
return {
|
|
12883
|
+
...asset,
|
|
12884
|
+
priceUsd: quote.priceUsd,
|
|
12885
|
+
priceEur: quote.priceEur ?? null,
|
|
12886
|
+
change24h: quote.change24h ?? null,
|
|
12887
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
12888
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12889
|
+
};
|
|
12890
|
+
}
|
|
12891
|
+
function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
12892
|
+
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
12893
|
+
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
12894
|
+
transferring: true,
|
|
12895
|
+
suspectedSpent: false
|
|
12896
|
+
});
|
|
12897
|
+
}
|
|
12898
|
+
|
|
12899
|
+
// modules/payments-v2/send-whole.ts
|
|
12900
|
+
async function materializeWholeSpend(deps, input) {
|
|
12901
|
+
const { engine } = deps;
|
|
12902
|
+
const tokenId = input.request.tokenId;
|
|
12903
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
12904
|
+
if (bytes === void 0) {
|
|
12905
|
+
throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
12906
|
+
}
|
|
12907
|
+
const token = await engine.decodeToken({ tokenId, token: bytes });
|
|
12908
|
+
if (token.valueEnvelope === "bare_collection") {
|
|
12909
|
+
throw new SphereError(
|
|
12910
|
+
`Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
|
|
12911
|
+
"VALIDATION_ERROR"
|
|
12912
|
+
);
|
|
12913
|
+
}
|
|
12914
|
+
if (input.requireCoinless && token.value !== null) {
|
|
12915
|
+
throw new SphereError(
|
|
12916
|
+
`Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
|
|
12917
|
+
"VALIDATION_ERROR"
|
|
12918
|
+
);
|
|
12919
|
+
}
|
|
12920
|
+
const keys = await engine.deliveryKeys(bytes);
|
|
12921
|
+
const payload = buildWholePayload(input.recipientPubkey, input.request, {
|
|
12922
|
+
[tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
|
|
12923
|
+
});
|
|
12924
|
+
const plan = {
|
|
12925
|
+
transferId: input.transferId,
|
|
12926
|
+
recipientPubkey: input.recipientPubkey,
|
|
12927
|
+
payload,
|
|
12928
|
+
ops: buildOps(payload),
|
|
12929
|
+
sources: /* @__PURE__ */ new Map([[tokenId, token]]),
|
|
12930
|
+
...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
|
|
12931
|
+
};
|
|
12932
|
+
const sourceTokens = (token.value?.assets ?? []).map(
|
|
12933
|
+
(asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
|
|
12934
|
+
);
|
|
12935
|
+
return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
|
|
12936
|
+
}
|
|
12937
|
+
|
|
12938
|
+
// modules/payments-v2/send-errors.ts
|
|
12939
|
+
function partialize(err, run) {
|
|
12940
|
+
if (run.delivered.length === 0) return err;
|
|
12941
|
+
return new PartialSendConflictError(
|
|
12942
|
+
"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.",
|
|
12943
|
+
run.firstPartialId ?? "",
|
|
12944
|
+
[...run.delivered],
|
|
12945
|
+
run.amount,
|
|
12946
|
+
err
|
|
12947
|
+
);
|
|
12948
|
+
}
|
|
12949
|
+
function stampTransferId(err, transferId) {
|
|
12950
|
+
if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
|
|
12951
|
+
err.transferId = transferId;
|
|
12952
|
+
}
|
|
12953
|
+
}
|
|
12954
|
+
|
|
12497
12955
|
// modules/payments-v2/recipient.ts
|
|
12498
12956
|
var ATTENTION_RECIPIENT_NETWORK_UNVERIFIED = "recipient:network-unverified";
|
|
12499
12957
|
async function requireSameNetworkRecipient(deps, identifier) {
|
|
@@ -12542,11 +13000,6 @@ async function splitOpIndex(deps, envelope) {
|
|
|
12542
13000
|
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
12543
13001
|
}
|
|
12544
13002
|
|
|
12545
|
-
// modules/payments-v2/mint-params.ts
|
|
12546
|
-
function mintParams(recipientPubkey, coinId, amount) {
|
|
12547
|
-
return { recipientPubkey, value: { assets: [{ coinId, amount }] } };
|
|
12548
|
-
}
|
|
12549
|
-
|
|
12550
13003
|
// modules/payments-v2/prewarm-cache.ts
|
|
12551
13004
|
var PrewarmCache = class _PrewarmCache {
|
|
12552
13005
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -12626,124 +13079,15 @@ async function takeSourceBlobs(deps, sourceIds) {
|
|
|
12626
13079
|
return blobs;
|
|
12627
13080
|
}
|
|
12628
13081
|
|
|
12629
|
-
// modules/payments-v2/
|
|
12630
|
-
function
|
|
12631
|
-
|
|
12632
|
-
return {
|
|
12633
|
-
id: tokenId,
|
|
12634
|
-
coinId: asset.coinId,
|
|
12635
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
12636
|
-
name: registry.getName(asset.coinId),
|
|
12637
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
12638
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12639
|
-
amount: asset.amount,
|
|
12640
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
12641
|
-
createdAt: snapshot.createdAt,
|
|
12642
|
-
updatedAt: snapshot.updatedAt,
|
|
12643
|
-
lazy: true,
|
|
12644
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12645
|
-
};
|
|
12646
|
-
}
|
|
12647
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
12648
|
-
const groups = /* @__PURE__ */ new Map();
|
|
12649
|
-
for (const [tokenId, entry] of entries) {
|
|
12650
|
-
const moving = isTransferring(tokenId);
|
|
12651
|
-
for (const asset of entry.assets) {
|
|
12652
|
-
let group = groups.get(asset.coinId);
|
|
12653
|
-
if (!group) {
|
|
12654
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12655
|
-
groups.set(asset.coinId, group);
|
|
12656
|
-
}
|
|
12657
|
-
if (moving) {
|
|
12658
|
-
group.transferring += BigInt(asset.amount);
|
|
12659
|
-
group.transferringCount += 1;
|
|
12660
|
-
} else {
|
|
12661
|
-
group.total += BigInt(asset.amount);
|
|
12662
|
-
group.count += 1;
|
|
12663
|
-
}
|
|
12664
|
-
}
|
|
12665
|
-
}
|
|
12666
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12667
|
-
}
|
|
12668
|
-
function toAsset(coinId, totals, registry) {
|
|
12669
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
12670
|
-
return {
|
|
12671
|
-
coinId,
|
|
12672
|
-
symbol: registry.getSymbol(coinId),
|
|
12673
|
-
name: registry.getName(coinId),
|
|
12674
|
-
decimals: registry.getDecimals(coinId),
|
|
12675
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12676
|
-
totalAmount: totals.total.toString(),
|
|
12677
|
-
tokenCount: totals.count,
|
|
12678
|
-
confirmedAmount: totals.total.toString(),
|
|
12679
|
-
unconfirmedAmount: "0",
|
|
12680
|
-
confirmedTokenCount: totals.count,
|
|
12681
|
-
unconfirmedTokenCount: 0,
|
|
12682
|
-
transferringTokenCount: totals.transferringCount,
|
|
12683
|
-
transferringAmount: totals.transferring.toString(),
|
|
12684
|
-
priceUsd: null,
|
|
12685
|
-
priceEur: null,
|
|
12686
|
-
change24h: null,
|
|
12687
|
-
fiatValueUsd: null,
|
|
12688
|
-
fiatValueEur: null
|
|
12689
|
-
};
|
|
12690
|
-
}
|
|
12691
|
-
function priceIdOf(registry, coinId) {
|
|
12692
|
-
return registry.getName(coinId).toLowerCase();
|
|
12693
|
-
}
|
|
12694
|
-
async function withPrices(raw, registry, price) {
|
|
12695
|
-
try {
|
|
12696
|
-
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
12697
|
-
const quotes = await price.getPrices(ids);
|
|
12698
|
-
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
12699
|
-
} catch {
|
|
12700
|
-
return raw;
|
|
12701
|
-
}
|
|
12702
|
-
}
|
|
12703
|
-
function priceOne(asset, quote) {
|
|
12704
|
-
if (!quote) return asset;
|
|
12705
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12706
|
-
return {
|
|
12707
|
-
...asset,
|
|
12708
|
-
priceUsd: quote.priceUsd,
|
|
12709
|
-
priceEur: quote.priceEur ?? null,
|
|
12710
|
-
change24h: quote.change24h ?? null,
|
|
12711
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
12712
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12713
|
-
};
|
|
12714
|
-
}
|
|
12715
|
-
function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
12716
|
-
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
12717
|
-
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
12718
|
-
transferring: true,
|
|
12719
|
-
suspectedSpent: false
|
|
12720
|
-
});
|
|
12721
|
-
}
|
|
12722
|
-
|
|
12723
|
-
// modules/payments-v2/machine/payload.ts
|
|
12724
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12725
|
-
return {
|
|
12726
|
-
v: 2,
|
|
12727
|
-
recipient: recipientPubkey,
|
|
12728
|
-
coinId: request.coinId,
|
|
12729
|
-
amount: request.amount,
|
|
12730
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12731
|
-
direct: [...spend.plan.direct],
|
|
12732
|
-
...spend.plan.split !== void 0 ? {
|
|
12733
|
-
split: {
|
|
12734
|
-
tokenId: spend.plan.split.tokenId,
|
|
12735
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12736
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12737
|
-
}
|
|
12738
|
-
} : {},
|
|
12739
|
-
spentStates
|
|
12740
|
-
};
|
|
13082
|
+
// modules/payments-v2/history/History.ts
|
|
13083
|
+
function wireAssets(assets) {
|
|
13084
|
+
return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
|
|
12741
13085
|
}
|
|
12742
|
-
function
|
|
12743
|
-
|
|
13086
|
+
function isPermanentReject(err) {
|
|
13087
|
+
const status = err?.status;
|
|
13088
|
+
if (typeof status !== "number") return false;
|
|
13089
|
+
return status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
12744
13090
|
}
|
|
12745
|
-
|
|
12746
|
-
// modules/payments-v2/history/History.ts
|
|
12747
13091
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
12748
13092
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
12749
13093
|
var History = class {
|
|
@@ -12768,7 +13112,7 @@ var History = class {
|
|
|
12768
13112
|
dedupKey: input.transferId,
|
|
12769
13113
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12770
13114
|
type: "SENT",
|
|
12771
|
-
assets:
|
|
13115
|
+
assets: wireAssets(input.assets),
|
|
12772
13116
|
ts: this.ts(input.timestamp),
|
|
12773
13117
|
transferId: input.transferId,
|
|
12774
13118
|
...this.wireTokenId(input.tokenId),
|
|
@@ -12781,7 +13125,7 @@ var History = class {
|
|
|
12781
13125
|
dedupKey: `RECEIVED:${input.tokenId.toLowerCase()}:${input.stateHash.toLowerCase()}`,
|
|
12782
13126
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12783
13127
|
type: "RECEIVED",
|
|
12784
|
-
assets:
|
|
13128
|
+
assets: wireAssets(input.assets),
|
|
12785
13129
|
ts: this.ts(input.timestamp),
|
|
12786
13130
|
...input.transferId !== void 0 ? { transferId: input.transferId } : {},
|
|
12787
13131
|
...this.wireTokenId(input.tokenId),
|
|
@@ -12794,7 +13138,7 @@ var History = class {
|
|
|
12794
13138
|
dedupKey: `MINT:${input.tokenId.toLowerCase()}`,
|
|
12795
13139
|
id: (this.deps.newId ?? randomUUID)(),
|
|
12796
13140
|
type: "MINT",
|
|
12797
|
-
assets:
|
|
13141
|
+
assets: wireAssets(input.assets),
|
|
12798
13142
|
ts: this.ts(input.timestamp),
|
|
12799
13143
|
...this.wireTokenId(input.tokenId)
|
|
12800
13144
|
}));
|
|
@@ -12805,7 +13149,10 @@ var History = class {
|
|
|
12805
13149
|
try {
|
|
12806
13150
|
await this.deps.client.postHistory([record]);
|
|
12807
13151
|
} catch (err) {
|
|
12808
|
-
this.deps.log?.(
|
|
13152
|
+
this.deps.log?.(
|
|
13153
|
+
isPermanentReject(err) ? "history POST REJECTED \u2014 the record shape is refused; no retry can fix it" : "history POST failed (dedupKey makes retry safe)",
|
|
13154
|
+
err
|
|
13155
|
+
);
|
|
12809
13156
|
return;
|
|
12810
13157
|
}
|
|
12811
13158
|
this.deps.emit("history:updated", this.toEntry(record));
|
|
@@ -12872,6 +13219,13 @@ var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
|
12872
13219
|
function stateKey(tokenId, stateHash) {
|
|
12873
13220
|
return `${tokenId}:${stateHash}`;
|
|
12874
13221
|
}
|
|
13222
|
+
function unchanged(prev, item, tokenType) {
|
|
13223
|
+
return prev.seq === item.seq && prev.status === item.status && prev.stateHash === item.stateHash && tokenType === prev.tokenType;
|
|
13224
|
+
}
|
|
13225
|
+
function isCoinless(item, prev) {
|
|
13226
|
+
if (item.status !== "active") return prev?.coinless ?? false;
|
|
13227
|
+
return (item.assets?.length ?? 0) === 0;
|
|
13228
|
+
}
|
|
12875
13229
|
var InventoryView = class {
|
|
12876
13230
|
constructor(deps) {
|
|
12877
13231
|
this.deps = deps;
|
|
@@ -12965,14 +13319,26 @@ var InventoryView = class {
|
|
|
12965
13319
|
pool(coinId) {
|
|
12966
13320
|
const out = [];
|
|
12967
13321
|
for (const [tokenId, entry] of this.mirror) {
|
|
12968
|
-
if (
|
|
12969
|
-
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12970
|
-
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
13322
|
+
if (!this.isSpendable(tokenId, entry)) continue;
|
|
12971
13323
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12972
13324
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
12973
13325
|
}
|
|
12974
13326
|
return out;
|
|
12975
13327
|
}
|
|
13328
|
+
/**
|
|
13329
|
+
* The eligibility gates every spend shares, coin or token — ONE definition, so a
|
|
13330
|
+
* single probe covers both verbs and neither can drift from the other.
|
|
13331
|
+
*/
|
|
13332
|
+
isSpendable(tokenId, entry) {
|
|
13333
|
+
if (entry.status !== "active") return false;
|
|
13334
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
|
|
13335
|
+
return !this.suspected.has(stateKey(tokenId, entry.stateHash));
|
|
13336
|
+
}
|
|
13337
|
+
/** Is this NAMED token spendable whole — coinless or valued (#777)? */
|
|
13338
|
+
spendableToken(tokenId) {
|
|
13339
|
+
const entry = this.mirror.get(tokenId);
|
|
13340
|
+
return entry !== void 0 && this.isSpendable(tokenId, entry);
|
|
13341
|
+
}
|
|
12976
13342
|
/**
|
|
12977
13343
|
* Current mirrored state of an ACTIVE token, or undefined when it is unknown
|
|
12978
13344
|
* or tombstoned. Exists so a cached blob can be state-scoped (F6): a blob is
|
|
@@ -12998,6 +13364,29 @@ var InventoryView = class {
|
|
|
12998
13364
|
}
|
|
12999
13365
|
return out;
|
|
13000
13366
|
}
|
|
13367
|
+
/**
|
|
13368
|
+
* Coinless holdings (#777) — DISJOINT from tokens(): an entry is coinless
|
|
13369
|
+
* exactly when it is not, so no mirror row can appear in both reads.
|
|
13370
|
+
*/
|
|
13371
|
+
coinless(registry) {
|
|
13372
|
+
const out = [];
|
|
13373
|
+
for (const [tokenId, entry] of this.mirror) {
|
|
13374
|
+
if (entry.status !== "active" || !entry.coinless) continue;
|
|
13375
|
+
const meta = entry.tokenType !== void 0 ? registry.getTypeMeta?.(entry.tokenType) : null;
|
|
13376
|
+
out.push({
|
|
13377
|
+
tokenId,
|
|
13378
|
+
...entry.tokenType !== void 0 ? { tokenType: entry.tokenType } : {},
|
|
13379
|
+
...meta ? { name: meta.name } : {},
|
|
13380
|
+
...meta?.iconUrl != null ? { iconUrl: meta.iconUrl } : {},
|
|
13381
|
+
stateHash: entry.stateHash,
|
|
13382
|
+
transferring: this.held(tokenId),
|
|
13383
|
+
...this.suspected.has(stateKey(tokenId, entry.stateHash)) ? { suspectedSpent: true } : {},
|
|
13384
|
+
createdAt: entry.createdAt,
|
|
13385
|
+
updatedAt: entry.updatedAt
|
|
13386
|
+
});
|
|
13387
|
+
}
|
|
13388
|
+
return out;
|
|
13389
|
+
}
|
|
13001
13390
|
async assets(registry, price) {
|
|
13002
13391
|
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
13003
13392
|
if (!price || raw.length === 0) return raw;
|
|
@@ -13069,14 +13458,19 @@ var InventoryView = class {
|
|
|
13069
13458
|
applyOne(item, now) {
|
|
13070
13459
|
const prev = this.mirror.get(item.tokenId);
|
|
13071
13460
|
if (prev && item.seq < prev.seq) return false;
|
|
13072
|
-
|
|
13073
|
-
|
|
13074
|
-
}
|
|
13461
|
+
const tokenType = item.tokenType ?? prev?.tokenType;
|
|
13462
|
+
if (prev && unchanged(prev, item, tokenType)) return false;
|
|
13075
13463
|
this.mirror.set(item.tokenId, {
|
|
13076
13464
|
stateHash: item.stateHash,
|
|
13077
13465
|
seq: item.seq,
|
|
13078
13466
|
status: item.status,
|
|
13079
|
-
|
|
13467
|
+
// ONLY a tombstone inherits: it omits assets for an unrelated reason and
|
|
13468
|
+
// recoverRemoved must know the amount it restores. An ACTIVE row that omits
|
|
13469
|
+
// them is stating coinlessness, and inheriting there would keep stale assets
|
|
13470
|
+
// in tokens() while `coinless` is true — the row in BOTH reads (§16).
|
|
13471
|
+
assets: item.assets ?? (item.status === "removed" ? prev?.assets ?? [] : []),
|
|
13472
|
+
coinless: isCoinless(item, prev),
|
|
13473
|
+
...tokenType !== void 0 ? { tokenType } : {},
|
|
13080
13474
|
createdAt: prev?.createdAt ?? now,
|
|
13081
13475
|
updatedAt: now
|
|
13082
13476
|
});
|
|
@@ -13312,7 +13706,12 @@ async function screen(deps, engine, entry) {
|
|
|
13312
13706
|
}
|
|
13313
13707
|
return {
|
|
13314
13708
|
kind: "accept",
|
|
13315
|
-
record: {
|
|
13709
|
+
record: {
|
|
13710
|
+
tokenId: keys.tokenId,
|
|
13711
|
+
stateHash: keys.stateHash,
|
|
13712
|
+
assets: toAssetAmounts(token),
|
|
13713
|
+
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
|
|
13714
|
+
}
|
|
13316
13715
|
};
|
|
13317
13716
|
}
|
|
13318
13717
|
async function announce(deps, entry, record) {
|
|
@@ -13323,6 +13722,7 @@ async function announce(deps, entry, record) {
|
|
|
13323
13722
|
tokenId: record.tokenId,
|
|
13324
13723
|
stateHash: record.stateHash,
|
|
13325
13724
|
assets: record.assets,
|
|
13725
|
+
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
13326
13726
|
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
13327
13727
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13328
13728
|
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
@@ -13336,6 +13736,19 @@ async function announce(deps, entry, record) {
|
|
|
13336
13736
|
senderPubkey: entry.senderPubkey ?? "",
|
|
13337
13737
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
13338
13738
|
tokens: record.assets.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
|
|
13739
|
+
// #777: named here rather than mapped from assets, which announced an EMPTY list.
|
|
13740
|
+
...record.tokenType !== void 0 ? {
|
|
13741
|
+
coinless: [
|
|
13742
|
+
{
|
|
13743
|
+
tokenId: record.tokenId,
|
|
13744
|
+
tokenType: record.tokenType,
|
|
13745
|
+
stateHash: record.stateHash,
|
|
13746
|
+
transferring: false,
|
|
13747
|
+
createdAt: receivedAt,
|
|
13748
|
+
updatedAt: receivedAt
|
|
13749
|
+
}
|
|
13750
|
+
]
|
|
13751
|
+
} : {},
|
|
13339
13752
|
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
13340
13753
|
receivedAt
|
|
13341
13754
|
};
|
|
@@ -14001,6 +14414,34 @@ var SpendQueue = class {
|
|
|
14001
14414
|
}
|
|
14002
14415
|
return { kind: "queued", settled: this.enqueue(reservationId, request.coinId, amount) };
|
|
14003
14416
|
}
|
|
14417
|
+
/**
|
|
14418
|
+
* #777: reserve a NAMED coinless source. Never queues — there is nothing to wait
|
|
14419
|
+
* for. A coin plan waits because some OTHER combination may free up; a named
|
|
14420
|
+
* token is either free now or held by a reservation that only its own transfer
|
|
14421
|
+
* can release, so queueing would block until a timeout on a spend that cannot
|
|
14422
|
+
* become possible.
|
|
14423
|
+
*/
|
|
14424
|
+
planWhole(reservationId, tokenId) {
|
|
14425
|
+
if (this.destroyed) {
|
|
14426
|
+
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
14427
|
+
}
|
|
14428
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
14429
|
+
if (unproven !== null) {
|
|
14430
|
+
throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
|
|
14431
|
+
}
|
|
14432
|
+
if (this.deps.spendableToken?.(tokenId) !== true) {
|
|
14433
|
+
throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
|
|
14434
|
+
}
|
|
14435
|
+
const holder = this.deps.ledger.holderOf(tokenId);
|
|
14436
|
+
if (holder !== void 0) {
|
|
14437
|
+
throw new SphereError(
|
|
14438
|
+
`Token ${tokenId} is already reserved by transfer ${holder}`,
|
|
14439
|
+
"VALIDATION_ERROR"
|
|
14440
|
+
);
|
|
14441
|
+
}
|
|
14442
|
+
this.deps.ledger.reserve(reservationId, [{ tokenId }]);
|
|
14443
|
+
return { reservationId, plan: { direct: [tokenId] } };
|
|
14444
|
+
}
|
|
14004
14445
|
/**
|
|
14005
14446
|
* The sources `plan()` WOULD pick right now, reserving nothing and queueing
|
|
14006
14447
|
* nothing — a read-only probe used to warm blobs while the user is still
|
|
@@ -14190,6 +14631,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
14190
14631
|
const queue = new SpendQueue({
|
|
14191
14632
|
ledger,
|
|
14192
14633
|
getPool: (coinId) => view.pool(coinId),
|
|
14634
|
+
spendableToken: (tokenId) => view.spendableToken(tokenId),
|
|
14193
14635
|
...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
|
|
14194
14636
|
});
|
|
14195
14637
|
const historyStore = new History({
|
|
@@ -14270,13 +14712,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
14270
14712
|
...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
|
|
14271
14713
|
emit: deps.emit,
|
|
14272
14714
|
now: deps.now ?? Date.now,
|
|
14273
|
-
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
14715
|
+
recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
|
|
14274
14716
|
await historyStore.recordSent({
|
|
14275
14717
|
transferId,
|
|
14276
|
-
|
|
14277
|
-
//
|
|
14278
|
-
|
|
14279
|
-
amount: committedAmount,
|
|
14718
|
+
// §5.9: the SETTLED amount, never payload.amount — the plan. A token
|
|
14719
|
+
// spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
|
|
14720
|
+
...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
|
|
14280
14721
|
recipientPubkey: payload.recipient,
|
|
14281
14722
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
14282
14723
|
});
|
|
@@ -14311,12 +14752,10 @@ function buildReceive(deps, hooks, historyStore, heldStates, refreshView) {
|
|
|
14311
14752
|
});
|
|
14312
14753
|
}
|
|
14313
14754
|
async function recordReceived(historyStore, record) {
|
|
14314
|
-
const first = record.assets[0];
|
|
14315
14755
|
await historyStore.recordReceived({
|
|
14316
14756
|
tokenId: record.tokenId,
|
|
14317
14757
|
stateHash: record.stateHash,
|
|
14318
|
-
|
|
14319
|
-
amount: first?.amount ?? "0",
|
|
14758
|
+
assets: record.assets,
|
|
14320
14759
|
...record.senderPubkey !== void 0 ? { senderPubkey: record.senderPubkey } : {},
|
|
14321
14760
|
...record.senderNametag !== void 0 ? { senderNametag: record.senderNametag } : {},
|
|
14322
14761
|
...record.memo !== void 0 ? { memo: record.memo } : {},
|
|
@@ -14464,6 +14903,13 @@ var PaymentsFacade = class {
|
|
|
14464
14903
|
tokens(filter) {
|
|
14465
14904
|
return this.view.tokens(this.deps.registry, filter);
|
|
14466
14905
|
}
|
|
14906
|
+
coinless() {
|
|
14907
|
+
return this.view.coinless(this.deps.registry);
|
|
14908
|
+
}
|
|
14909
|
+
tokenData(tokenId) {
|
|
14910
|
+
const deps = { engine: this.engine(), view: this.view, storagePort: this.deps.storagePort };
|
|
14911
|
+
return readTokenData(deps, tokenId);
|
|
14912
|
+
}
|
|
14467
14913
|
history(page) {
|
|
14468
14914
|
return this.historyStore.page(page ?? {});
|
|
14469
14915
|
}
|
|
@@ -14486,7 +14932,14 @@ var PaymentsFacade = class {
|
|
|
14486
14932
|
return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
|
|
14487
14933
|
}
|
|
14488
14934
|
send(request) {
|
|
14489
|
-
return this.track(this.
|
|
14935
|
+
return this.track(this.runJob({ kind: "coin", request }, request.amount));
|
|
14936
|
+
}
|
|
14937
|
+
sendWholeToken(request) {
|
|
14938
|
+
return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
|
|
14939
|
+
}
|
|
14940
|
+
/** NFT-scoped twin: refuses a valued source. */
|
|
14941
|
+
sendCoinless(request) {
|
|
14942
|
+
return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
|
|
14490
14943
|
}
|
|
14491
14944
|
async receive() {
|
|
14492
14945
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -14518,12 +14971,25 @@ var PaymentsFacade = class {
|
|
|
14518
14971
|
nowMs() {
|
|
14519
14972
|
return (this.deps.now ?? Date.now)();
|
|
14520
14973
|
}
|
|
14974
|
+
mintDeps() {
|
|
14975
|
+
return {
|
|
14976
|
+
engine: this.engine(),
|
|
14977
|
+
mintJournal: this.machineStores.mintJournal,
|
|
14978
|
+
storagePort: this.deps.storagePort,
|
|
14979
|
+
recordMint: (input) => this.historyStore.recordMint(input),
|
|
14980
|
+
armHeartbeat: () => this.heartbeat.arm(),
|
|
14981
|
+
noteHeldState: (tokenId, stateHash) => void this.heldStates.set(tokenId, stateHash),
|
|
14982
|
+
refreshView: () => this.trackTail(this.view.delta()),
|
|
14983
|
+
ownPubkeyBytes: this.ownPubkeyBytes,
|
|
14984
|
+
now: () => this.nowMs()
|
|
14985
|
+
};
|
|
14986
|
+
}
|
|
14521
14987
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
14522
14988
|
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
14523
14989
|
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
14524
|
-
async
|
|
14990
|
+
async runJob(job, amount) {
|
|
14525
14991
|
const run = {
|
|
14526
|
-
amount
|
|
14992
|
+
amount,
|
|
14527
14993
|
delivered: [],
|
|
14528
14994
|
sentTokens: [],
|
|
14529
14995
|
tokenTransfers: [],
|
|
@@ -14533,7 +14999,7 @@ var PaymentsFacade = class {
|
|
|
14533
14999
|
lastTransferId: ""
|
|
14534
15000
|
};
|
|
14535
15001
|
try {
|
|
14536
|
-
return await this.sendWithPolicy(
|
|
15002
|
+
return await this.sendWithPolicy(job, run);
|
|
14537
15003
|
} catch (err) {
|
|
14538
15004
|
this.emitCleanFailure(err, run.lastTransferId);
|
|
14539
15005
|
throw err;
|
|
@@ -14553,19 +15019,19 @@ var PaymentsFacade = class {
|
|
|
14553
15019
|
};
|
|
14554
15020
|
this.deps.emit("transfer:updated", failed);
|
|
14555
15021
|
}
|
|
14556
|
-
async sendWithPolicy(
|
|
14557
|
-
const recipient = await requireSameNetworkRecipient(this.deps, request.recipient);
|
|
15022
|
+
async sendWithPolicy(job, run) {
|
|
15023
|
+
const recipient = await requireSameNetworkRecipient(this.deps, job.request.recipient);
|
|
14558
15024
|
for (let attempt = 0; ; attempt++) {
|
|
14559
15025
|
let ctx;
|
|
14560
15026
|
try {
|
|
14561
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey,
|
|
15027
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, job, run);
|
|
14562
15028
|
} catch (err) {
|
|
14563
|
-
throw
|
|
15029
|
+
throw partialize(err, run);
|
|
14564
15030
|
}
|
|
14565
15031
|
const disposition = await this.runAttempt(ctx);
|
|
14566
|
-
if (disposition.kind === "rethrow") throw
|
|
15032
|
+
if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
|
|
14567
15033
|
if (disposition.kind === "retry-full") {
|
|
14568
|
-
if (attempt >= MAX_RESELECT) throw
|
|
15034
|
+
if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
|
|
14569
15035
|
continue;
|
|
14570
15036
|
}
|
|
14571
15037
|
if (disposition.kind === "success") {
|
|
@@ -14621,7 +15087,7 @@ var PaymentsFacade = class {
|
|
|
14621
15087
|
async disposeFailedAttempt(ctx, err) {
|
|
14622
15088
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
14623
15089
|
this.settleKeepOpen(ctx);
|
|
14624
|
-
|
|
15090
|
+
stampTransferId(err, ctx.transferId);
|
|
14625
15091
|
return { kind: "rethrow", error: err };
|
|
14626
15092
|
}
|
|
14627
15093
|
const backstop = await this.machineStores.backstop.getByKey(ctx.transferId);
|
|
@@ -14655,7 +15121,7 @@ var PaymentsFacade = class {
|
|
|
14655
15121
|
async disposeConvergeFailure(ctx, err) {
|
|
14656
15122
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
14657
15123
|
this.settleKeepOpen(ctx);
|
|
14658
|
-
|
|
15124
|
+
stampTransferId(err, ctx.transferId);
|
|
14659
15125
|
return { kind: "rethrow", error: err };
|
|
14660
15126
|
}
|
|
14661
15127
|
if (classifyError(err) === "conflict") {
|
|
@@ -14666,9 +15132,29 @@ var PaymentsFacade = class {
|
|
|
14666
15132
|
}
|
|
14667
15133
|
return { kind: "rethrow", error: err };
|
|
14668
15134
|
}
|
|
14669
|
-
async planAndMaterialize(recipientPubkey,
|
|
15135
|
+
async planAndMaterialize(recipientPubkey, job, run) {
|
|
14670
15136
|
const transferId = this.newId();
|
|
14671
15137
|
run.lastTransferId = transferId;
|
|
15138
|
+
if (job.kind === "whole") {
|
|
15139
|
+
const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
|
|
15140
|
+
const sourceIds2 = this.markPlanned(transferId, "", spend2);
|
|
15141
|
+
try {
|
|
15142
|
+
return await materializeWholeSpend(
|
|
15143
|
+
{ engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
|
|
15144
|
+
{
|
|
15145
|
+
transferId,
|
|
15146
|
+
recipientPubkey,
|
|
15147
|
+
request: job.request,
|
|
15148
|
+
sourceIds: sourceIds2,
|
|
15149
|
+
requireCoinless: job.requireCoinless
|
|
15150
|
+
}
|
|
15151
|
+
);
|
|
15152
|
+
} catch (err) {
|
|
15153
|
+
this.settleFailure(transferId, "", sourceIds2);
|
|
15154
|
+
throw err;
|
|
15155
|
+
}
|
|
15156
|
+
}
|
|
15157
|
+
const request = { ...job.request, amount: run.amount };
|
|
14672
15158
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
14673
15159
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
14674
15160
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -14802,22 +15288,6 @@ var PaymentsFacade = class {
|
|
|
14802
15288
|
return result;
|
|
14803
15289
|
}
|
|
14804
15290
|
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
14805
|
-
partialize(err, run) {
|
|
14806
|
-
if (run.delivered.length === 0) return err;
|
|
14807
|
-
return new PartialSendConflictError(
|
|
14808
|
-
"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.",
|
|
14809
|
-
run.firstPartialId ?? "",
|
|
14810
|
-
run.delivered,
|
|
14811
|
-
run.amount,
|
|
14812
|
-
err
|
|
14813
|
-
);
|
|
14814
|
-
}
|
|
14815
|
-
/** #441: possibly-committed errors must carry the transferId for the settling journal. */
|
|
14816
|
-
stampTransferId(err, transferId) {
|
|
14817
|
-
if (err instanceof SphereError && isPossiblyCommittedSendOutcome(err) && err.transferId === void 0) {
|
|
14818
|
-
err.transferId = transferId;
|
|
14819
|
-
}
|
|
14820
|
-
}
|
|
14821
15291
|
async softAbort(transferId) {
|
|
14822
15292
|
try {
|
|
14823
15293
|
await this.deps.client.abortIntent(transferId);
|
|
@@ -14833,57 +15303,11 @@ var PaymentsFacade = class {
|
|
|
14833
15303
|
const mintId = this.newId();
|
|
14834
15304
|
this.activeMoneyOps.add(mintId);
|
|
14835
15305
|
try {
|
|
14836
|
-
return await this.
|
|
15306
|
+
return await runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount });
|
|
14837
15307
|
} finally {
|
|
14838
15308
|
this.activeMoneyOps.delete(mintId);
|
|
14839
15309
|
}
|
|
14840
15310
|
}
|
|
14841
|
-
async mintUnderJournal(mintId, coinId, amount) {
|
|
14842
|
-
const engine = this.engine();
|
|
14843
|
-
const entry = {
|
|
14844
|
-
mintId,
|
|
14845
|
-
coinId,
|
|
14846
|
-
amount: amount.toString(),
|
|
14847
|
-
tokenId: "",
|
|
14848
|
-
createdAt: this.nowMs()
|
|
14849
|
-
};
|
|
14850
|
-
await this.machineStores.mintJournal.upsert(entry);
|
|
14851
|
-
let token;
|
|
14852
|
-
try {
|
|
14853
|
-
token = await engine.mint(mintParams(this.ownPubkeyBytes, coinId, amount), { transferId: mintId });
|
|
14854
|
-
} catch (err) {
|
|
14855
|
-
this.heartbeat.arm();
|
|
14856
|
-
return { success: false, error: messageOf(err) };
|
|
14857
|
-
}
|
|
14858
|
-
if (token.blob.tokenId !== entry.tokenId) {
|
|
14859
|
-
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
14860
|
-
}
|
|
14861
|
-
try {
|
|
14862
|
-
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
14863
|
-
} catch (err) {
|
|
14864
|
-
this.heartbeat.arm();
|
|
14865
|
-
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
14866
|
-
}
|
|
14867
|
-
return { success: true, tokenId: token.blob.tokenId };
|
|
14868
|
-
}
|
|
14869
|
-
async finalizeMint(engine, mintId, token, coinId, amount) {
|
|
14870
|
-
const bytes = token.blob.token;
|
|
14871
|
-
const digest = bytesToHex3(sha256(bytes));
|
|
14872
|
-
const keys = await this.deps.storagePort.uploadBlobs([{ sha256: digest, bytes }]);
|
|
14873
|
-
const key = keys.get(digest);
|
|
14874
|
-
if (key === void 0) {
|
|
14875
|
-
throw new SphereError(`no upload key returned for mint blob ${digest}`, "STORAGE_ERROR");
|
|
14876
|
-
}
|
|
14877
|
-
await this.deps.storagePort.applyDelta({
|
|
14878
|
-
transferId: mintId,
|
|
14879
|
-
spent: [],
|
|
14880
|
-
added: [{ tokenId: token.blob.tokenId, key }]
|
|
14881
|
-
});
|
|
14882
|
-
await this.historyStore.recordMint({ tokenId: token.blob.tokenId, coinId, amount });
|
|
14883
|
-
await this.machineStores.mintJournal.removeByKey(mintId);
|
|
14884
|
-
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
14885
|
-
this.trackTail(this.view.delta());
|
|
14886
|
-
}
|
|
14887
15311
|
/** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
|
|
14888
15312
|
async replayMints() {
|
|
14889
15313
|
let resolved = 0;
|
|
@@ -14915,7 +15339,7 @@ var PaymentsFacade = class {
|
|
|
14915
15339
|
if (token.blob.tokenId !== entry.tokenId) {
|
|
14916
15340
|
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
14917
15341
|
}
|
|
14918
|
-
await this.
|
|
15342
|
+
await finalizeMint(this.mintDeps(), entry.mintId, token, entry.coinId, entry.amount);
|
|
14919
15343
|
return true;
|
|
14920
15344
|
}
|
|
14921
15345
|
async tokenInServerInventory(tokenId) {
|