@unicitylabs/sphere-sdk 0.13.2 → 0.13.3-dev.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.
@@ -30,7 +30,6 @@ var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: tr
30
30
  // core/index.ts
31
31
  var core_exports = {};
32
32
  __export(core_exports, {
33
- CHARSET: () => CHARSET,
34
33
  CurrencyUtils: () => CurrencyUtils,
35
34
  DEFAULT_DERIVATION_PATH: () => DEFAULT_DERIVATION_PATH2,
36
35
  DEFAULT_TOKEN_DECIMALS: () => DEFAULT_TOKEN_DECIMALS,
@@ -48,11 +47,8 @@ __export(core_exports, {
48
47
  base58Encode: () => base58Encode,
49
48
  bytesToHex: () => bytesToHex4,
50
49
  checkNetworkHealth: () => checkNetworkHealth,
51
- convertBits: () => convertBits,
52
- createAddress: () => createAddress,
53
50
  createKeyPair: () => createKeyPair,
54
51
  createSphere: () => createSphere,
55
- decodeBech32: () => decodeBech32,
56
52
  decrypt: () => decrypt,
57
53
  decryptDeliveryBundle: () => decryptDeliveryBundle,
58
54
  decryptField: () => decryptField,
@@ -69,7 +65,6 @@ __export(core_exports, {
69
65
  deserializeEncrypted: () => deserializeEncrypted,
70
66
  discoverAddressesImpl: () => discoverAddressesImpl,
71
67
  doubleSha256: () => doubleSha256,
72
- encodeBech32: () => encodeBech32,
73
68
  encrypt: () => encrypt,
74
69
  encryptDeliveryBundle: () => encryptDeliveryBundle,
75
70
  encryptField: () => encryptField,
@@ -85,7 +80,6 @@ __export(core_exports, {
85
80
  generateMasterKey: () => generateMasterKey,
86
81
  generateMnemonic: () => generateMnemonic2,
87
82
  generateRandomKey: () => generateRandomKey,
88
- getAddressHrp: () => getAddressHrp,
89
83
  getPublicKey: () => getPublicKey,
90
84
  getSphere: () => getSphere,
91
85
  hash160: () => hash160,
@@ -98,7 +92,6 @@ __export(core_exports, {
98
92
  isEncryptedData: () => isEncryptedData,
99
93
  isPossiblyCommittedSendOutcome: () => isPossiblyCommittedSendOutcome,
100
94
  isSphereError: () => isSphereError,
101
- isValidBech32: () => isValidBech32,
102
95
  isValidNametag: () => isValidNametag2,
103
96
  isValidPrivateKey: () => isValidPrivateKey,
104
97
  loadSphere: () => loadSphere,
@@ -8930,17 +8923,9 @@ function decryptDeliveryBundle(key, envelope) {
8930
8923
  }
8931
8924
 
8932
8925
  // types/txf.ts
8933
- var ARCHIVED_PREFIX = "archived-";
8934
- var FORKED_PREFIX = "_forked_";
8935
8926
  var RESERVED_KEYS = ["_meta", "_nametag", "_nametags", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity", "_history"];
8936
8927
  function isTokenKey(key) {
8937
- return key.startsWith("_") && !key.startsWith(ARCHIVED_PREFIX) && !key.startsWith(FORKED_PREFIX) && !RESERVED_KEYS.includes(key);
8938
- }
8939
- function isArchivedKey(key) {
8940
- return key.startsWith(ARCHIVED_PREFIX);
8941
- }
8942
- function isForkedKey(key) {
8943
- return key.startsWith(FORKED_PREFIX);
8928
+ return key.startsWith("_") && !RESERVED_KEYS.includes(key);
8944
8929
  }
8945
8930
  function tokenIdFromKey(key) {
8946
8931
  return key.startsWith("_") ? key.substring(1) : key;
@@ -8948,24 +8933,109 @@ function tokenIdFromKey(key) {
8948
8933
  function keyFromTokenId(tokenId) {
8949
8934
  return `_${tokenId}`;
8950
8935
  }
8951
- function tokenIdFromArchivedKey(key) {
8952
- return key.startsWith(ARCHIVED_PREFIX) ? key.substring(ARCHIVED_PREFIX.length) : key;
8936
+
8937
+ // serialization/txf-serializer.ts
8938
+ function isV2TokenBlob(sdkData) {
8939
+ return typeof sdkData === "string" && sdkData.length >= 2 && sdkData.length % 2 === 0 && sdkData[0] !== "{" && /^[0-9a-f]+$/i.test(sdkData);
8953
8940
  }
8954
- function archivedKeyFromTokenId(tokenId) {
8955
- return `${ARCHIVED_PREFIX}${tokenId}`;
8941
+ function v2TokenId(token) {
8942
+ return token.id.startsWith("v2_") ? token.id.slice(3) : token.id;
8956
8943
  }
8957
- function forkedKeyFromTokenIdAndState(tokenId, stateHash) {
8958
- return `${FORKED_PREFIX}${tokenId}_${stateHash}`;
8944
+ function isV2TokenEntry(entry) {
8945
+ return typeof entry === "object" && entry !== null && !("genesis" in entry) && isV2TokenBlob(entry.sdkData);
8959
8946
  }
8960
- function parseForkedKey(key) {
8961
- if (!key.startsWith(FORKED_PREFIX)) return null;
8962
- const remainder = key.substring(FORKED_PREFIX.length);
8963
- const underscoreIndex = remainder.indexOf("_");
8964
- if (underscoreIndex === -1 || underscoreIndex < 64) return null;
8965
- return {
8966
- tokenId: remainder.substring(0, underscoreIndex),
8967
- stateHash: remainder.substring(underscoreIndex + 1)
8947
+ async function buildTxfStorageData(tokens, meta, options) {
8948
+ const storageData = {
8949
+ _meta: {
8950
+ ...meta,
8951
+ formatVersion: "2.0"
8952
+ }
8953
+ };
8954
+ if (options?.nametags && options.nametags.length > 0) {
8955
+ storageData._nametags = options.nametags;
8956
+ }
8957
+ if (options?.tombstones && options.tombstones.length > 0) {
8958
+ storageData._tombstones = options.tombstones;
8959
+ }
8960
+ if (options?.historyEntries && options.historyEntries.length > 0) {
8961
+ storageData._history = options.historyEntries;
8962
+ }
8963
+ for (const token of tokens) {
8964
+ if (isV2TokenBlob(token.sdkData)) {
8965
+ storageData[keyFromTokenId(v2TokenId(token))] = token;
8966
+ } else {
8967
+ logger.warn(
8968
+ "TXF",
8969
+ `Refusing to persist token ${token.id.slice(0, 16)}...: sdkData is not a v2 token blob \u2014 entry skipped`
8970
+ );
8971
+ }
8972
+ }
8973
+ return storageData;
8974
+ }
8975
+ function parseTxfStorageData(data) {
8976
+ const result = {
8977
+ tokens: [],
8978
+ meta: null,
8979
+ nametags: [],
8980
+ tombstones: [],
8981
+ historyEntries: [],
8982
+ validationErrors: []
8968
8983
  };
8984
+ if (!data || typeof data !== "object") {
8985
+ result.validationErrors.push("Storage data is not an object");
8986
+ return result;
8987
+ }
8988
+ const storageData = data;
8989
+ if (storageData._meta && typeof storageData._meta === "object") {
8990
+ result.meta = storageData._meta;
8991
+ }
8992
+ const seenNames = /* @__PURE__ */ new Set();
8993
+ if (Array.isArray(storageData._nametags)) {
8994
+ for (const entry of storageData._nametags) {
8995
+ if (entry && typeof entry === "object" && typeof entry.name === "string") {
8996
+ result.nametags.push(entry);
8997
+ seenNames.add(entry.name);
8998
+ }
8999
+ }
9000
+ }
9001
+ if (storageData._nametag && typeof storageData._nametag === "object") {
9002
+ const legacy = storageData._nametag;
9003
+ if (typeof legacy.name === "string" && !seenNames.has(legacy.name)) {
9004
+ result.nametags.push(legacy);
9005
+ }
9006
+ }
9007
+ if (storageData._tombstones && Array.isArray(storageData._tombstones)) {
9008
+ for (const entry of storageData._tombstones) {
9009
+ if (typeof entry === "object" && entry !== null && typeof entry.tokenId === "string" && typeof entry.stateHash === "string" && typeof entry.timestamp === "number") {
9010
+ result.tombstones.push(entry);
9011
+ }
9012
+ }
9013
+ }
9014
+ if (Array.isArray(storageData._history)) {
9015
+ for (const entry of storageData._history) {
9016
+ if (typeof entry === "object" && entry !== null && typeof entry.dedupKey === "string" && typeof entry.type === "string") {
9017
+ result.historyEntries.push(entry);
9018
+ }
9019
+ }
9020
+ }
9021
+ for (const key of Object.keys(storageData)) {
9022
+ if (isTokenKey(key)) {
9023
+ const tokenId = tokenIdFromKey(key);
9024
+ const entry = storageData[key];
9025
+ if (isV2TokenEntry(entry)) {
9026
+ result.tokens.push(entry);
9027
+ } else {
9028
+ const msg = `Token ${tokenId}: unrecognized storage entry (not a v2 token blob) \u2014 entry skipped`;
9029
+ logger.warn("TXF", msg);
9030
+ result.validationErrors.push(msg);
9031
+ }
9032
+ } else if (key.startsWith("token-")) {
9033
+ const msg = `Token ${key}: sdkData is not a v2 token blob \u2014 entry skipped`;
9034
+ logger.warn("TXF", msg);
9035
+ result.validationErrors.push(msg);
9036
+ }
9037
+ }
9038
+ return result;
8969
9039
  }
8970
9040
 
8971
9041
  // registry/TokenRegistry.ts
@@ -9395,334 +9465,6 @@ function coinIdsMatch(a, b) {
9395
9465
  return normalizeCoinId(a) === normalizeCoinId(b);
9396
9466
  }
9397
9467
 
9398
- // serialization/txf-serializer.ts
9399
- function bytesToHex5(bytes) {
9400
- const arr = Array.isArray(bytes) ? bytes : Array.from(bytes);
9401
- return arr.map((b) => b.toString(16).padStart(2, "0")).join("");
9402
- }
9403
- function normalizeToHex(value) {
9404
- if (typeof value === "string") {
9405
- return value;
9406
- }
9407
- if (value && typeof value === "object") {
9408
- const obj = value;
9409
- if ("bytes" in obj && (Array.isArray(obj.bytes) || obj.bytes instanceof Uint8Array)) {
9410
- return bytesToHex5(obj.bytes);
9411
- }
9412
- if (obj.type === "Buffer" && Array.isArray(obj.data)) {
9413
- return bytesToHex5(obj.data);
9414
- }
9415
- }
9416
- return String(value);
9417
- }
9418
- function normalizeSdkTokenToStorage(sdkTokenJson) {
9419
- const txf = structuredClone(sdkTokenJson);
9420
- if (txf.genesis?.data) {
9421
- const data = txf.genesis.data;
9422
- if (data.tokenId !== void 0) {
9423
- data.tokenId = normalizeToHex(data.tokenId);
9424
- }
9425
- if (data.tokenType !== void 0) {
9426
- data.tokenType = normalizeToHex(data.tokenType);
9427
- }
9428
- if (data.salt !== void 0) {
9429
- data.salt = normalizeToHex(data.salt);
9430
- }
9431
- }
9432
- if (txf.genesis?.inclusionProof?.authenticator) {
9433
- const auth = txf.genesis.inclusionProof.authenticator;
9434
- if (auth.publicKey !== void 0) {
9435
- auth.publicKey = normalizeToHex(auth.publicKey);
9436
- }
9437
- if (auth.signature !== void 0) {
9438
- auth.signature = normalizeToHex(auth.signature);
9439
- }
9440
- }
9441
- if (Array.isArray(txf.transactions)) {
9442
- for (const tx of txf.transactions) {
9443
- if (tx.inclusionProof?.authenticator) {
9444
- const auth = tx.inclusionProof.authenticator;
9445
- if (auth.publicKey !== void 0) {
9446
- auth.publicKey = normalizeToHex(auth.publicKey);
9447
- }
9448
- if (auth.signature !== void 0) {
9449
- auth.signature = normalizeToHex(auth.signature);
9450
- }
9451
- }
9452
- }
9453
- }
9454
- return txf;
9455
- }
9456
- function tokenToTxf(token) {
9457
- const jsonData = token.sdkData;
9458
- if (!jsonData) {
9459
- return null;
9460
- }
9461
- try {
9462
- const txfData = normalizeSdkTokenToStorage(JSON.parse(jsonData));
9463
- if (!txfData.genesis || !txfData.state) {
9464
- return null;
9465
- }
9466
- if (!txfData.version) {
9467
- txfData.version = "2.0";
9468
- }
9469
- if (!txfData.transactions) {
9470
- txfData.transactions = [];
9471
- }
9472
- if (!txfData.nametags) {
9473
- txfData.nametags = [];
9474
- }
9475
- if (!txfData._integrity) {
9476
- txfData._integrity = {
9477
- genesisDataJSONHash: "0000" + "0".repeat(60)
9478
- };
9479
- }
9480
- return txfData;
9481
- } catch {
9482
- return null;
9483
- }
9484
- }
9485
- function determineTokenStatus(txf) {
9486
- if (txf.transactions.length > 0) {
9487
- const lastTx = txf.transactions[txf.transactions.length - 1];
9488
- if (lastTx.inclusionProof === null) {
9489
- return "pending";
9490
- }
9491
- }
9492
- return "confirmed";
9493
- }
9494
- function txfToToken(tokenId, txf) {
9495
- const coinData = txf.genesis.data.coinData;
9496
- const totalAmount = coinData.reduce((sum, [, amt]) => {
9497
- return sum + BigInt(amt || "0");
9498
- }, BigInt(0));
9499
- let coinId = coinData[0]?.[0] || "";
9500
- for (const [cid, amt] of coinData) {
9501
- if (BigInt(amt || "0") > 0) {
9502
- coinId = cid;
9503
- break;
9504
- }
9505
- }
9506
- const tokenType = txf.genesis.data.tokenType;
9507
- const isNft = tokenType === "455ad8720656b08e8dbd5bac1f3c73eeea5431565f6c1c3af742b1aa12d41d89";
9508
- const now = Date.now();
9509
- const registry = TokenRegistry.getInstance();
9510
- const def = registry.getDefinition(coinId);
9511
- return {
9512
- id: tokenId,
9513
- coinId,
9514
- symbol: isNft ? "NFT" : def?.symbol || coinId.slice(0, 8),
9515
- name: isNft ? "NFT" : def?.name ? def.name.charAt(0).toUpperCase() + def.name.slice(1) : "Token",
9516
- decimals: isNft ? 0 : def?.decimals ?? 8,
9517
- amount: totalAmount.toString(),
9518
- status: determineTokenStatus(txf),
9519
- createdAt: now,
9520
- updatedAt: now,
9521
- sdkData: JSON.stringify(txf)
9522
- };
9523
- }
9524
- function isV2TokenBlob(sdkData) {
9525
- return typeof sdkData === "string" && sdkData.length >= 2 && sdkData.length % 2 === 0 && sdkData[0] !== "{" && /^[0-9a-f]+$/i.test(sdkData);
9526
- }
9527
- function v2TokenId(token) {
9528
- return token.id.startsWith("v2_") ? token.id.slice(3) : token.id;
9529
- }
9530
- function isV2TokenEntry(entry) {
9531
- return typeof entry === "object" && entry !== null && !("genesis" in entry) && isV2TokenBlob(entry.sdkData);
9532
- }
9533
- async function buildTxfStorageData(tokens, meta, options) {
9534
- const storageData = {
9535
- _meta: {
9536
- ...meta,
9537
- formatVersion: "2.0"
9538
- }
9539
- };
9540
- if (options?.nametags && options.nametags.length > 0) {
9541
- storageData._nametags = options.nametags;
9542
- }
9543
- if (options?.tombstones && options.tombstones.length > 0) {
9544
- storageData._tombstones = options.tombstones;
9545
- }
9546
- if (options?.outboxEntries && options.outboxEntries.length > 0) {
9547
- storageData._outbox = options.outboxEntries;
9548
- }
9549
- if (options?.mintOutboxEntries && options.mintOutboxEntries.length > 0) {
9550
- storageData._mintOutbox = options.mintOutboxEntries;
9551
- }
9552
- if (options?.invalidatedNametags && options.invalidatedNametags.length > 0) {
9553
- storageData._invalidatedNametags = options.invalidatedNametags;
9554
- }
9555
- if (options?.historyEntries && options.historyEntries.length > 0) {
9556
- storageData._history = options.historyEntries;
9557
- }
9558
- for (const token of tokens) {
9559
- const txf = tokenToTxf(token);
9560
- if (txf) {
9561
- const actualTokenId = txf.genesis.data.tokenId;
9562
- storageData[keyFromTokenId(actualTokenId)] = txf;
9563
- } else if (isV2TokenBlob(token.sdkData)) {
9564
- storageData[keyFromTokenId(v2TokenId(token))] = token;
9565
- }
9566
- }
9567
- if (options?.archivedTokens && options.archivedTokens.size > 0) {
9568
- for (const [tokenId, txf] of options.archivedTokens) {
9569
- storageData[archivedKeyFromTokenId(tokenId)] = txf;
9570
- }
9571
- }
9572
- if (options?.forkedTokens && options.forkedTokens.size > 0) {
9573
- for (const [key, txf] of options.forkedTokens) {
9574
- const [tokenId, stateHash] = key.split("_");
9575
- if (tokenId && stateHash) {
9576
- storageData[forkedKeyFromTokenIdAndState(tokenId, stateHash)] = txf;
9577
- }
9578
- }
9579
- }
9580
- return storageData;
9581
- }
9582
- function parseTxfStorageData(data) {
9583
- const result = {
9584
- tokens: [],
9585
- meta: null,
9586
- nametags: [],
9587
- tombstones: [],
9588
- archivedTokens: /* @__PURE__ */ new Map(),
9589
- forkedTokens: /* @__PURE__ */ new Map(),
9590
- outboxEntries: [],
9591
- mintOutboxEntries: [],
9592
- invalidatedNametags: [],
9593
- historyEntries: [],
9594
- validationErrors: []
9595
- };
9596
- if (!data || typeof data !== "object") {
9597
- result.validationErrors.push("Storage data is not an object");
9598
- return result;
9599
- }
9600
- const storageData = data;
9601
- if (storageData._meta && typeof storageData._meta === "object") {
9602
- result.meta = storageData._meta;
9603
- }
9604
- const seenNames = /* @__PURE__ */ new Set();
9605
- if (Array.isArray(storageData._nametags)) {
9606
- for (const entry of storageData._nametags) {
9607
- if (entry && typeof entry === "object" && typeof entry.name === "string") {
9608
- result.nametags.push(entry);
9609
- seenNames.add(entry.name);
9610
- }
9611
- }
9612
- }
9613
- if (storageData._nametag && typeof storageData._nametag === "object") {
9614
- const legacy = storageData._nametag;
9615
- if (typeof legacy.name === "string" && !seenNames.has(legacy.name)) {
9616
- result.nametags.push(legacy);
9617
- }
9618
- }
9619
- if (storageData._tombstones && Array.isArray(storageData._tombstones)) {
9620
- for (const entry of storageData._tombstones) {
9621
- if (typeof entry === "object" && entry !== null && typeof entry.tokenId === "string" && typeof entry.stateHash === "string" && typeof entry.timestamp === "number") {
9622
- result.tombstones.push(entry);
9623
- }
9624
- }
9625
- }
9626
- if (storageData._outbox && Array.isArray(storageData._outbox)) {
9627
- for (const entry of storageData._outbox) {
9628
- if (typeof entry === "object" && entry !== null && typeof entry.id === "string" && typeof entry.status === "string") {
9629
- result.outboxEntries.push(entry);
9630
- }
9631
- }
9632
- }
9633
- if (storageData._mintOutbox && Array.isArray(storageData._mintOutbox)) {
9634
- for (const entry of storageData._mintOutbox) {
9635
- if (typeof entry === "object" && entry !== null && typeof entry.id === "string" && typeof entry.status === "string") {
9636
- result.mintOutboxEntries.push(entry);
9637
- }
9638
- }
9639
- }
9640
- if (storageData._invalidatedNametags && Array.isArray(storageData._invalidatedNametags)) {
9641
- for (const entry of storageData._invalidatedNametags) {
9642
- if (typeof entry === "object" && entry !== null && typeof entry.name === "string") {
9643
- result.invalidatedNametags.push(entry);
9644
- }
9645
- }
9646
- }
9647
- if (Array.isArray(storageData._history)) {
9648
- for (const entry of storageData._history) {
9649
- if (typeof entry === "object" && entry !== null && typeof entry.dedupKey === "string" && typeof entry.type === "string") {
9650
- result.historyEntries.push(entry);
9651
- }
9652
- }
9653
- }
9654
- for (const key of Object.keys(storageData)) {
9655
- if (isTokenKey(key)) {
9656
- const tokenId = tokenIdFromKey(key);
9657
- try {
9658
- const txfToken = storageData[key];
9659
- if (txfToken?.genesis?.data?.tokenId) {
9660
- const token = txfToToken(tokenId, txfToken);
9661
- result.tokens.push(token);
9662
- } else if (isV2TokenEntry(storageData[key])) {
9663
- result.tokens.push(storageData[key]);
9664
- }
9665
- } catch (err) {
9666
- result.validationErrors.push(`Token ${tokenId}: ${err}`);
9667
- }
9668
- } else if (isArchivedKey(key)) {
9669
- const tokenId = tokenIdFromArchivedKey(key);
9670
- try {
9671
- const txfToken = storageData[key];
9672
- if (txfToken?.genesis?.data?.tokenId) {
9673
- result.archivedTokens.set(tokenId, txfToken);
9674
- }
9675
- } catch {
9676
- result.validationErrors.push(`Archived token ${tokenId}: invalid structure`);
9677
- }
9678
- } else if (isForkedKey(key)) {
9679
- const parsed = parseForkedKey(key);
9680
- if (parsed) {
9681
- try {
9682
- const txfToken = storageData[key];
9683
- if (txfToken?.genesis?.data?.tokenId) {
9684
- const mapKey = `${parsed.tokenId}_${parsed.stateHash}`;
9685
- result.forkedTokens.set(mapKey, txfToken);
9686
- }
9687
- } catch {
9688
- result.validationErrors.push(`Forked token ${parsed.tokenId}: invalid structure`);
9689
- }
9690
- }
9691
- } else if (key.startsWith("token-")) {
9692
- try {
9693
- const entry = storageData[key];
9694
- const txfToken = entry?.token;
9695
- if (txfToken?.genesis?.data?.tokenId) {
9696
- const tokenId = txfToken.genesis.data.tokenId;
9697
- const token = txfToToken(tokenId, txfToken);
9698
- result.tokens.push(token);
9699
- }
9700
- } catch (err) {
9701
- result.validationErrors.push(`Token ${key}: ${err}`);
9702
- }
9703
- }
9704
- }
9705
- return result;
9706
- }
9707
- function getCurrentStateHash(txf) {
9708
- if (txf.transactions && txf.transactions.length > 0) {
9709
- const lastTx = txf.transactions[txf.transactions.length - 1];
9710
- if (lastTx?.newStateHash) {
9711
- return lastTx.newStateHash;
9712
- }
9713
- if (lastTx?.inclusionProof?.authenticator?.stateHash) {
9714
- return lastTx.inclusionProof.authenticator.stateHash;
9715
- }
9716
- }
9717
- if (txf._integrity?.currentStateHash) {
9718
- return txf._integrity.currentStateHash;
9719
- }
9720
- if (txf.genesis?.inclusionProof?.authenticator?.stateHash) {
9721
- return txf.genesis.inclusionProof.authenticator.stateHash;
9722
- }
9723
- return void 0;
9724
- }
9725
-
9726
9468
  // core/utils.ts
9727
9469
  var SECP256K1_ORDER = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141");
9728
9470
  var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
@@ -9888,6 +9630,10 @@ function surfacedSendFailure(summary) {
9888
9630
  }
9889
9631
  return primaryError;
9890
9632
  }
9633
+ function currentStateHashOfTxf(txf) {
9634
+ const last = txf.transactions?.[txf.transactions.length - 1];
9635
+ return last?.newStateHash ?? last?.inclusionProof?.authenticator?.stateHash ?? txf._integrity?.currentStateHash ?? txf.genesis?.inclusionProof?.authenticator?.stateHash ?? "";
9636
+ }
9891
9637
  function genesisIdOf(token) {
9892
9638
  return extractTokenIdFromSdkData(token.sdkData) ?? token.id.replace(/^v2_/, "");
9893
9639
  }
@@ -9999,7 +9745,7 @@ function parseSdkDataCached(sdkData) {
9999
9745
  try {
10000
9746
  const txf = JSON.parse(sdkData);
10001
9747
  tokenId = txf.genesis?.data?.tokenId || null;
10002
- stateHash = getCurrentStateHash(txf) || "";
9748
+ stateHash = currentStateHashOfTxf(txf);
10003
9749
  if (!stateHash) {
10004
9750
  if (txf.state?.hash) {
10005
9751
  stateHash = txf.state.hash;
@@ -10745,7 +10491,6 @@ var PaymentsModule = class _PaymentsModule {
10745
10491
  if (op.kind === "direct") {
10746
10492
  finished = await ctx.engine.transfer({ token: op.sdkToken, recipientPubkey: ctx.recipientChainPubkey }, opts);
10747
10493
  } else {
10748
- const checkpointStore = this.getCheckpointStore();
10749
10494
  const { outputs } = await ctx.engine.split(
10750
10495
  {
10751
10496
  token: op.sdkToken,
@@ -10754,7 +10499,7 @@ var PaymentsModule = class _PaymentsModule {
10754
10499
  { recipientPubkey: ctx.selfChainPubkey, coinId: ctx.coinId, amount: op.remainderAmount }
10755
10500
  ]
10756
10501
  },
10757
- { ...opts, ...checkpointStore ? { checkpointStore } : {} }
10502
+ { ...opts, ...ctx.checkpointStore ? { checkpointStore: ctx.checkpointStore } : {} }
10758
10503
  );
10759
10504
  finished = outputs[0];
10760
10505
  changeOutput = outputs[1];
@@ -10941,7 +10686,8 @@ var PaymentsModule = class _PaymentsModule {
10941
10686
  recipientChainPubkeyHex,
10942
10687
  selfChainPubkey: hexToBytes4(this.deps.identity.chainPubkey),
10943
10688
  coinId: request.coinId,
10944
- ...request.memo !== void 0 ? { memo: request.memo } : {}
10689
+ ...request.memo !== void 0 ? { memo: request.memo } : {},
10690
+ ...this.getCheckpointStore() ? { checkpointStore: this.getCheckpointStore() } : {}
10945
10691
  })
10946
10692
  );
10947
10693
  attempt.summary = summary;
@@ -13458,8 +13204,11 @@ var PaymentsModule = class _PaymentsModule {
13458
13204
  try {
13459
13205
  const plain = decryptField(this.getFieldEncryptionKey(), intent.payload);
13460
13206
  payload = JSON.parse(plain);
13461
- if (payload.v !== 1 && payload.v !== 2 || !Array.isArray(payload.direct)) {
13462
- throw new SphereError("Unknown intent payload shape", "VALIDATION_ERROR");
13207
+ if (payload.v !== 2 || !Array.isArray(payload.direct)) {
13208
+ throw new SphereError(
13209
+ `Unsupported intent payload shape (v=${String(payload.v)}) \u2014 not resumable by this wallet version`,
13210
+ "VALIDATION_ERROR"
13211
+ );
13463
13212
  }
13464
13213
  } catch (err) {
13465
13214
  logger.warn("Payments", `Intent ${intent.transferId.slice(0, 8)}\u2026 payload undecodable \u2014 skipped:`, err);
@@ -13598,31 +13347,22 @@ var PaymentsModule = class _PaymentsModule {
13598
13347
  async resumeIntent(transferId, payload) {
13599
13348
  const engine = this.deps.tokenEngine;
13600
13349
  const walletApi = this.deps.walletApi;
13601
- const delivery = this.delivery;
13602
13350
  const provider = this.getActiveTokenStorageProvider();
13603
13351
  if (!provider) throw new SphereError("No token storage provider for intent resume", "STORAGE_ERROR");
13604
- const serverApply = delivery.custody === "inventory";
13605
- const recipientChainPubkey = hexToBytes4(payload.recipient);
13606
- const deliverBlob = async (tokenBlobHex, opIndex) => {
13607
- await this.savePendingV2Delivery({
13608
- transferId,
13609
- recipientPubkey: payload.recipient,
13610
- tokenBlob: tokenBlobHex,
13611
- memo: payload.memo,
13612
- opIndex,
13613
- createdAt: Date.now()
13614
- });
13615
- const nm = this.currentNametagName();
13616
- await this.tryDeliver(
13617
- () => delivery.deliver(payload.recipient, hexToBytes4(tokenBlobHex), {
13618
- transferId,
13619
- memo: payload.memo,
13620
- ...nm !== void 0 ? { senderNametag: nm } : {}
13621
- }),
13622
- tokenBlobHex
13623
- );
13352
+ const serverApply = this.delivery.custody === "inventory";
13353
+ const checkpointStore = this.getCheckpointStore();
13354
+ const ctx = {
13355
+ engine,
13356
+ transferId,
13357
+ recipientChainPubkey: hexToBytes4(payload.recipient),
13358
+ recipientChainPubkeyHex: payload.recipient,
13359
+ selfChainPubkey: hexToBytes4(this.deps.identity.chainPubkey),
13360
+ coinId: payload.coinId,
13361
+ ...payload.memo !== void 0 ? { memo: payload.memo } : {},
13362
+ ...checkpointStore ? { checkpointStore } : {}
13624
13363
  };
13625
13364
  const journaled = await this.journaledByOp(transferId);
13365
+ const blobs = [];
13626
13366
  const spent = [];
13627
13367
  let conflicted = false;
13628
13368
  let firstConflict;
@@ -13630,91 +13370,71 @@ var PaymentsModule = class _PaymentsModule {
13630
13370
  for (const [opIndex, genesisId] of payload.direct.entries()) {
13631
13371
  const existing = journaled.get(opIndex);
13632
13372
  if (existing) {
13633
- await deliverBlob(existing.tokenBlob, opIndex);
13373
+ blobs.push(existing.tokenBlob);
13634
13374
  spent.push(genesisId);
13635
- } else {
13636
- const source = await engine.decodeToken(await provider.getToken(genesisId));
13637
- try {
13638
- const finished = await engine.transfer(
13639
- { token: source, recipientPubkey: recipientChainPubkey },
13640
- // (transferId, opIndex) pairing replayed from the intent's persisted order (§8.1)
13641
- { signal: timeoutSignal(SEND_ENGINE_OP_TIMEOUT_MS), transferId, opIndex }
13642
- );
13643
- await deliverBlob(bytesToHex4(encodeTokenBlob(engine.encodeToken(finished))), opIndex);
13644
- spent.push(genesisId);
13645
- } catch (err) {
13646
- if (!(err instanceof TransferConflictError)) throw err;
13647
- conflicted = true;
13648
- firstConflict ??= err;
13649
- undeliveredAmount += engine.balanceOf(source, payload.coinId);
13650
- logger.warn("Payments", `Resume op ${opIndex} lost its source to a concurrent transfer \u2014 not delivered (\xA78.1/#4):`, err);
13651
- }
13375
+ continue;
13376
+ }
13377
+ const source = await engine.decodeToken(await provider.getToken(genesisId));
13378
+ const outcome = await this.certifyOperation(
13379
+ {
13380
+ kind: "direct",
13381
+ opIndex,
13382
+ uiTokenId: `v2_${genesisId}`,
13383
+ genesisId,
13384
+ sourceSdkData: "",
13385
+ sdkToken: source,
13386
+ deliveredAmount: engine.balanceOf(source, payload.coinId)
13387
+ },
13388
+ ctx
13389
+ );
13390
+ if (outcome.error !== void 0) {
13391
+ if (!(outcome.error instanceof TransferConflictError)) throw outcome.error;
13392
+ conflicted = true;
13393
+ firstConflict ??= outcome.error;
13394
+ undeliveredAmount += engine.balanceOf(source, payload.coinId);
13395
+ logger.warn("Payments", `Resume op ${opIndex} lost its source to a concurrent transfer \u2014 not delivered (\xA78.1/#4):`, outcome.error);
13396
+ continue;
13652
13397
  }
13398
+ blobs.push(outcome.tokenBlob);
13399
+ spent.push(genesisId);
13653
13400
  }
13654
13401
  let changeOutput = null;
13655
13402
  if (payload.split) {
13656
13403
  const splitOpIndex = payload.direct.length;
13657
- const existingSplit = journaled.get(splitOpIndex);
13658
- const checkpointStore = payload.v === 2 ? this.getCheckpointStore() : void 0;
13659
13404
  let splitDelivered = false;
13660
- if (checkpointStore === void 0 && existingSplit) {
13661
- await deliverBlob(existingSplit.tokenBlob, splitOpIndex);
13662
- splitDelivered = true;
13663
- } else {
13664
- try {
13665
- const blob = await provider.getToken(payload.split.tokenId);
13666
- const source = await engine.decodeToken(blob);
13667
- const selfChainPubkey = hexToBytes4(this.deps.identity.chainPubkey);
13668
- const { outputs } = await engine.split(
13669
- {
13670
- token: source,
13671
- outputs: [
13672
- {
13673
- recipientPubkey: recipientChainPubkey,
13674
- coinId: payload.coinId,
13675
- amount: BigInt(payload.split.splitAmount)
13676
- },
13677
- {
13678
- recipientPubkey: selfChainPubkey,
13679
- coinId: payload.coinId,
13680
- amount: BigInt(payload.split.remainderAmount)
13681
- }
13682
- ]
13683
- },
13684
- {
13685
- signal: timeoutSignal(SEND_ENGINE_OP_TIMEOUT_MS),
13686
- transferId,
13687
- opIndex: splitOpIndex,
13688
- ...checkpointStore ? { checkpointStore } : {}
13689
- }
13690
- );
13691
- changeOutput = outputs[1];
13692
- if (!serverApply) await this.storeEngineToken(engine, changeOutput, { criticalSave: true });
13693
- await deliverBlob(bytesToHex4(encodeTokenBlob(engine.encodeToken(outputs[0]))), splitOpIndex);
13694
- splitDelivered = true;
13695
- } catch (err) {
13696
- if (err instanceof ProofUnconfirmedError) throw err;
13697
- if (checkpointStore !== void 0 && (err instanceof CheckpointPersistFailedError || err instanceof SplitCheckpointLostError || err instanceof CheckpointTrustbaseMismatchError)) {
13698
- throw err;
13405
+ {
13406
+ const source = await engine.decodeToken(await provider.getToken(payload.split.tokenId));
13407
+ const outcome = await this.certifyOperation(
13408
+ {
13409
+ kind: "split",
13410
+ opIndex: splitOpIndex,
13411
+ uiTokenId: `v2_${payload.split.tokenId}`,
13412
+ genesisId: payload.split.tokenId,
13413
+ sourceSdkData: "",
13414
+ sdkToken: source,
13415
+ deliveredAmount: BigInt(payload.split.splitAmount),
13416
+ remainderAmount: BigInt(payload.split.remainderAmount)
13417
+ },
13418
+ ctx
13419
+ );
13420
+ if (outcome.error === void 0) {
13421
+ changeOutput = outcome.changeOutput ?? null;
13422
+ if (!serverApply && changeOutput) {
13423
+ await this.storeEngineToken(engine, changeOutput, { criticalSave: true });
13699
13424
  }
13700
- if (err instanceof TransferConflictError) {
13425
+ blobs.push(outcome.tokenBlob);
13426
+ splitDelivered = true;
13427
+ } else {
13428
+ splitDelivered = this.classifyResumeSplitFailure(outcome.error, (err) => {
13701
13429
  conflicted = true;
13702
13430
  firstConflict ??= err;
13703
13431
  undeliveredAmount += BigInt(payload.split.splitAmount);
13704
- logger.warn("Payments", `Resume split: source lost to a concurrent transfer \u2014 not delivered (\xA78.1/#4):`, err);
13705
- } else if (err instanceof SplitCheckpointLostError) {
13706
- logger.warn("Payments", `Resume split (legacy no-checkpoint): recording the spend; change recovers on the next inventory resync (\xA73.1):`, err);
13707
- this.deps.emitEvent("inventory:conflict", { transferId, coinId: payload.coinId, error: err.message });
13708
- splitDelivered = true;
13709
- } else {
13710
- throw err;
13711
- }
13432
+ });
13712
13433
  }
13713
13434
  }
13714
- if (splitDelivered) {
13715
- spent.push(payload.split.tokenId);
13716
- }
13435
+ if (splitDelivered) spent.push(payload.split.tokenId);
13717
13436
  }
13437
+ await this.deliverCommittedBlobs(blobs, payload.recipient, transferId, payload.memo);
13718
13438
  if (conflicted && spent.length === 0) throw firstConflict;
13719
13439
  if (serverApply) {
13720
13440
  await this.applyInventoryDelta(
@@ -13726,6 +13446,55 @@ var PaymentsModule = class _PaymentsModule {
13726
13446
  // the one the sources were read from, captured for the whole resume
13727
13447
  );
13728
13448
  }
13449
+ await this.dropResumedSources(spent, transferId, payload);
13450
+ if (conflicted) {
13451
+ const remaining = undeliveredAmount > 0n ? undeliveredAmount : 0n;
13452
+ const delivered = BigInt(payload.amount) - remaining;
13453
+ await walletApi.completeIntent(transferId);
13454
+ await this.recordResumedSentHistory(payload, transferId, delivered > 0n ? delivered.toString() : "0", spent[0]);
13455
+ this.deps.emitEvent("send:partial-remainder", {
13456
+ transferId,
13457
+ remainingAmount: remaining.toString(),
13458
+ coinId: payload.coinId,
13459
+ recipientPubkey: payload.recipient
13460
+ });
13461
+ throw new PartialSendConflictError(
13462
+ "Resume: part of the payment was delivered before a source was consumed by a concurrent transfer \u2014 the delivered legs are final; re-plan ONLY the remainder.",
13463
+ transferId,
13464
+ spent,
13465
+ // the DELIVERED genesisIds — never the conflicted leg
13466
+ remaining.toString(),
13467
+ firstConflict
13468
+ );
13469
+ }
13470
+ await walletApi.completeIntent(transferId);
13471
+ await this.recordResumedSentHistory(payload, transferId, payload.amount, payload.direct[0]);
13472
+ }
13473
+ /**
13474
+ * A resumed split that produced no output: a lost source means the leg delivered
13475
+ * nothing (returns false), and every checkpoint failure keeps the intent open by
13476
+ * throwing. There is no path here that records the spend.
13477
+ */
13478
+ classifyResumeSplitFailure(error, onConflict) {
13479
+ if (error instanceof ProofUnconfirmedError) throw error;
13480
+ if (error instanceof CheckpointPersistFailedError || error instanceof SplitCheckpointLostError || error instanceof CheckpointTrustbaseMismatchError) {
13481
+ throw error;
13482
+ }
13483
+ if (error instanceof TransferConflictError) {
13484
+ onConflict(error);
13485
+ logger.warn("Payments", "Resume split: source lost to a concurrent transfer \u2014 not delivered (\xA78.1/#4):", error);
13486
+ return false;
13487
+ }
13488
+ throw error;
13489
+ }
13490
+ /**
13491
+ * Drop the local records of the sources a resume consumed (present when
13492
+ * resuming on the originating device). M7: each removal carries the LOCAL state
13493
+ * we spent, so a source a concurrent claim reactivated to a NEW state (a
13494
+ * self-send round-trip) is KEPT, not destroyed.
13495
+ */
13496
+ async dropResumedSources(spent, transferId, payload) {
13497
+ const engine = this.deps.tokenEngine;
13729
13498
  for (const genesisId of spent) {
13730
13499
  const local = this.tokens.get(`v2_${genesisId}`);
13731
13500
  if (!local) continue;
@@ -13737,8 +13506,7 @@ var PaymentsModule = class _PaymentsModule {
13737
13506
  let spentOnChain = false;
13738
13507
  try {
13739
13508
  if (local.sdkData) {
13740
- const decoded = await engine.decodeToken(decodeTokenBlob(hexToBytes4(local.sdkData)));
13741
- spentOnChain = await engine.isSpent(decoded);
13509
+ spentOnChain = await engine.isSpent(await engine.decodeToken(decodeTokenBlob(hexToBytes4(local.sdkData))));
13742
13510
  }
13743
13511
  } catch {
13744
13512
  spentOnChain = false;
@@ -13752,47 +13520,19 @@ var PaymentsModule = class _PaymentsModule {
13752
13520
  );
13753
13521
  }
13754
13522
  }
13755
- if (conflicted) {
13756
- const remaining = undeliveredAmount;
13757
- const delivered = BigInt(payload.amount) - remaining;
13758
- await walletApi.completeIntent(transferId);
13759
- await this.addToHistory({
13760
- type: "SENT",
13761
- amount: (delivered > 0n ? delivered : 0n).toString(),
13762
- coinId: payload.coinId,
13763
- symbol: this.getCoinSymbol(payload.coinId),
13764
- timestamp: Date.now(),
13765
- recipientPubkey: payload.recipient,
13766
- memo: payload.memo,
13767
- transferId,
13768
- tokenId: spent[0] ? `v2_${spent[0]}` : void 0
13769
- });
13770
- this.deps.emitEvent("send:partial-remainder", {
13771
- transferId,
13772
- remainingAmount: (remaining > 0n ? remaining : 0n).toString(),
13773
- coinId: payload.coinId,
13774
- recipientPubkey: payload.recipient
13775
- });
13776
- throw new PartialSendConflictError(
13777
- "Resume: part of the payment was delivered before a source was consumed by a concurrent transfer \u2014 the delivered legs are final; re-plan ONLY the remainder.",
13778
- transferId,
13779
- spent,
13780
- // the DELIVERED genesisIds — never the conflicted leg
13781
- (remaining > 0n ? remaining : 0n).toString(),
13782
- firstConflict
13783
- );
13784
- }
13785
- await walletApi.completeIntent(transferId);
13523
+ }
13524
+ /** §10: the SENT record — same dedupKey as the original attempt, so a resume after the history POST is a server-side no-op. */
13525
+ async recordResumedSentHistory(payload, transferId, amount, tokenGenesisId) {
13786
13526
  await this.addToHistory({
13787
13527
  type: "SENT",
13788
- amount: payload.amount,
13528
+ amount,
13789
13529
  coinId: payload.coinId,
13790
13530
  symbol: this.getCoinSymbol(payload.coinId),
13791
13531
  timestamp: Date.now(),
13792
13532
  recipientPubkey: payload.recipient,
13793
13533
  memo: payload.memo,
13794
13534
  transferId,
13795
- tokenId: payload.direct[0] ? `v2_${payload.direct[0]}` : void 0
13535
+ tokenId: tokenGenesisId ? `v2_${tokenGenesisId}` : void 0
13796
13536
  });
13797
13537
  }
13798
13538
  // ===========================================================================
@@ -13814,11 +13554,7 @@ var PaymentsModule = class _PaymentsModule {
13814
13554
  */
13815
13555
  async save(opts = {}) {
13816
13556
  const providers = this.getTokenStorageProviders();
13817
- const tokenStats = Array.from(this.tokens.values()).map((t) => {
13818
- const txf = tokenToTxf(t);
13819
- return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
13820
- });
13821
- logger.debug("Payments", `save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
13557
+ logger.debug("Payments", `save(): providers=${providers.size}, tokens=${this.tokens.size}`);
13822
13558
  if (providers.size === 0) {
13823
13559
  logger.debug("Payments", "save(): No token storage providers - TXF not persisted");
13824
13560
  return;
@@ -22488,7 +22224,6 @@ var import_canonicalize = __toESM(require("canonicalize"), 1);
22488
22224
 
22489
22225
  // core/address.ts
22490
22226
  var DIRECT_PREFIX = "DIRECT://";
22491
- var PROXY_PREFIX = "PROXY://";
22492
22227
  var NAMETAG_PREFIX = "@";
22493
22228
  function parseAddress(address) {
22494
22229
  if (!address || typeof address !== "string") return null;
@@ -22500,9 +22235,6 @@ function parseAddress(address) {
22500
22235
  }
22501
22236
  return null;
22502
22237
  }
22503
- if (trimmed.startsWith(PROXY_PREFIX) && trimmed.length > PROXY_PREFIX.length) {
22504
- return { type: "PROXY", raw: trimmed, value: trimmed.slice(PROXY_PREFIX.length) };
22505
- }
22506
22238
  if (trimmed.startsWith(NAMETAG_PREFIX)) {
22507
22239
  const value = trimmed.slice(NAMETAG_PREFIX.length);
22508
22240
  if (value.length > 0) {
@@ -25749,447 +25481,97 @@ function parseWalletText(content) {
25749
25481
  /ENCRYPTED MASTER KEY \(password protected\):\s*([^\n]+)/
25750
25482
  );
25751
25483
  if (!encryptedKey) {
25752
- return {
25753
- success: false,
25754
- error: "Could not find encrypted master key in backup file"
25755
- };
25756
- }
25757
- return {
25758
- success: false,
25759
- needsPassword: true,
25760
- error: "Password required for encrypted wallet"
25761
- };
25762
- }
25763
- const masterKey = extractFromText(
25764
- content,
25765
- /MASTER PRIVATE KEY \(keep secret!\):\s*([^\n]+)/
25766
- );
25767
- if (!masterKey) {
25768
- return {
25769
- success: false,
25770
- error: "Could not find master private key in backup file"
25771
- };
25772
- }
25773
- const chainCode = extractFromText(
25774
- content,
25775
- /MASTER CHAIN CODE \(for (?:BIP32 HD|Alpha) wallet compatibility\):\s*([^\n]+)/
25776
- );
25777
- const descriptorPath = extractFromText(content, /DESCRIPTOR PATH:\s*([^\n]+)/);
25778
- const isBIP32 = content.includes("WALLET TYPE: BIP32 hierarchical deterministic wallet") || content.includes("WALLET TYPE: Alpha descriptor wallet") || !!chainCode;
25779
- const effectiveDescriptorPath = descriptorPath ?? (isBIP32 ? "84'/1'/0'" : void 0);
25780
- const data = {
25781
- masterKey,
25782
- chainCode: chainCode ?? void 0,
25783
- descriptorPath: effectiveDescriptorPath,
25784
- derivationMode: isBIP32 ? "bip32" : "wif_hmac"
25785
- };
25786
- return {
25787
- success: true,
25788
- data
25789
- };
25790
- } catch (e) {
25791
- return {
25792
- success: false,
25793
- error: `Failed to parse wallet text: ${e instanceof Error ? e.message : String(e)}`
25794
- };
25795
- }
25796
- }
25797
- function parseAndDecryptWalletText(content, password) {
25798
- try {
25799
- const isEncrypted = isTextWalletEncrypted(content);
25800
- if (!isEncrypted) {
25801
- return parseWalletText(content);
25802
- }
25803
- const encryptedKey = extractFromText(
25804
- content,
25805
- /ENCRYPTED MASTER KEY \(password protected\):\s*([^\n]+)/
25806
- );
25807
- if (!encryptedKey) {
25808
- return {
25809
- success: false,
25810
- error: "Could not find encrypted master key in backup file"
25811
- };
25812
- }
25813
- const masterKey = decryptTextFormatKey(encryptedKey, password);
25814
- if (!masterKey) {
25815
- return {
25816
- success: false,
25817
- error: "Failed to decrypt - incorrect password?"
25818
- };
25819
- }
25820
- const chainCode = extractFromText(
25821
- content,
25822
- /MASTER CHAIN CODE \(for (?:BIP32 HD|Alpha) wallet compatibility\):\s*([^\n]+)/
25823
- );
25824
- const descriptorPath = extractFromText(content, /DESCRIPTOR PATH:\s*([^\n]+)/);
25825
- const isBIP32 = content.includes("WALLET TYPE: BIP32 hierarchical deterministic wallet") || content.includes("WALLET TYPE: Alpha descriptor wallet") || !!chainCode;
25826
- const effectiveDescriptorPath = descriptorPath ?? (isBIP32 ? "84'/1'/0'" : void 0);
25827
- const data = {
25828
- masterKey,
25829
- chainCode: chainCode ?? void 0,
25830
- descriptorPath: effectiveDescriptorPath,
25831
- derivationMode: isBIP32 ? "bip32" : "wif_hmac"
25832
- };
25833
- return {
25834
- success: true,
25835
- data
25836
- };
25837
- } catch (e) {
25838
- return {
25839
- success: false,
25840
- error: `Failed to parse/decrypt wallet text: ${e instanceof Error ? e.message : String(e)}`
25841
- };
25842
- }
25843
- }
25844
-
25845
- // serialization/wallet-dat.ts
25846
- var import_crypto_js4 = __toESM(require("crypto-js"), 1);
25847
- function uint8ArrayToWordArray(u8arr) {
25848
- const hex = bytesToHex4(u8arr);
25849
- return import_crypto_js4.default.enc.Hex.parse(hex);
25850
- }
25851
- function isSQLiteDatabase(data) {
25852
- if (data.length < 16) return false;
25853
- const header = new TextDecoder().decode(data.slice(0, 16));
25854
- return header.startsWith("SQLite format 3");
25855
- }
25856
- function isWalletDatEncrypted(data) {
25857
- const mkeyPattern = new TextEncoder().encode("mkey");
25858
- return findPattern(data, mkeyPattern, 0) !== -1;
25859
- }
25860
- function findAllCMasterKeys(data) {
25861
- const results = [];
25862
- for (let pos = 0; pos < data.length - 70; pos++) {
25863
- if (data[pos] === 48) {
25864
- const saltLenPos = pos + 1 + 48;
25865
- if (saltLenPos < data.length && data[saltLenPos] === 8) {
25866
- const iterPos = saltLenPos + 1 + 8 + 4;
25867
- if (iterPos + 4 <= data.length) {
25868
- const iterations = data[iterPos] | data[iterPos + 1] << 8 | data[iterPos + 2] << 16 | data[iterPos + 3] << 24;
25869
- if (iterations >= 1e3 && iterations <= 1e7) {
25870
- const encryptedKey = data.slice(pos + 1, pos + 1 + 48);
25871
- const salt = data.slice(saltLenPos + 1, saltLenPos + 1 + 8);
25872
- const derivationMethod = data[saltLenPos + 1 + 8] | data[saltLenPos + 1 + 8 + 1] << 8 | data[saltLenPos + 1 + 8 + 2] << 16 | data[saltLenPos + 1 + 8 + 3] << 24;
25873
- results.push({ encryptedKey, salt, derivationMethod, iterations, position: pos });
25874
- }
25875
- }
25876
- }
25877
- }
25878
- }
25879
- return results;
25880
- }
25881
- function findWpkhDescriptor(data) {
25882
- const descriptorPattern = new TextEncoder().encode("walletdescriptor");
25883
- let descriptorIndex = 0;
25884
- while ((descriptorIndex = findPattern(data, descriptorPattern, descriptorIndex)) !== -1) {
25885
- let scanPos = descriptorIndex + descriptorPattern.length + 32;
25886
- const descLen = data[scanPos];
25887
- scanPos++;
25888
- const descBytes = data.slice(scanPos, scanPos + Math.min(descLen, 200));
25889
- let descStr = "";
25890
- for (let i = 0; i < descBytes.length && descBytes[i] >= 32 && descBytes[i] <= 126; i++) {
25891
- descStr += String.fromCharCode(descBytes[i]);
25892
- }
25893
- if (descStr.startsWith("wpkh(xpub") && descStr.includes("/0/*)")) {
25894
- const xpubMatch = descStr.match(/xpub[1-9A-HJ-NP-Za-km-z]{100,}/);
25895
- if (xpubMatch) {
25896
- const descIdStart = descriptorIndex + descriptorPattern.length;
25897
- const descriptorId = data.slice(descIdStart, descIdStart + 32);
25898
- const pathMatch = descStr.match(/\[[\da-f]+\/(\d+'\/\d+'\/\d+')\]/);
25899
- const descriptorPath = pathMatch ? pathMatch[1] : "84'/1'/0'";
25900
- return {
25901
- descriptorId,
25902
- xpubString: xpubMatch[0],
25903
- descriptorPath
25904
- };
25905
- }
25906
- }
25907
- descriptorIndex++;
25908
- }
25909
- return { descriptorId: null, xpubString: null, descriptorPath: null };
25910
- }
25911
- function extractChainCodeFromXpub(xpubString) {
25912
- const decoded = base58Decode(xpubString);
25913
- return bytesToHex4(decoded.slice(13, 45));
25914
- }
25915
- function findMasterChainCode(data) {
25916
- const xpubPattern = new TextEncoder().encode("xpub");
25917
- const base58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
25918
- let searchPos = 0;
25919
- while (searchPos < data.length) {
25920
- const xpubIndex = findPattern(data, xpubPattern, searchPos);
25921
- if (xpubIndex === -1) break;
25922
- let xpubStr = "xpub";
25923
- let pos = xpubIndex + 4;
25924
- while (pos < data.length && xpubStr.length < 120) {
25925
- const char = String.fromCharCode(data[pos]);
25926
- if (base58Chars.includes(char)) {
25927
- xpubStr += char;
25928
- pos++;
25929
- } else {
25930
- break;
25931
- }
25932
- }
25933
- if (xpubStr.length > 100) {
25934
- try {
25935
- const decoded = base58Decode(xpubStr);
25936
- const depth = decoded[4];
25937
- if (depth === 0) {
25938
- return bytesToHex4(decoded.slice(13, 45));
25939
- }
25940
- } catch {
25941
- }
25942
- }
25943
- searchPos = xpubIndex + 4;
25944
- }
25945
- return null;
25946
- }
25947
- function extractDescriptorKeys(data) {
25948
- const keys = [];
25949
- const descriptorKeyPattern = new TextEncoder().encode("walletdescriptorkey");
25950
- let index = 0;
25951
- while ((index = findPattern(data, descriptorKeyPattern, index)) !== -1) {
25952
- for (let checkPos = index + descriptorKeyPattern.length; checkPos < Math.min(index + descriptorKeyPattern.length + 200, data.length - 40); checkPos++) {
25953
- if (data[checkPos] === 211 && data[checkPos + 1] === 2 && data[checkPos + 2] === 1 && data[checkPos + 3] === 1 && data[checkPos + 4] === 4 && data[checkPos + 5] === 32) {
25954
- const privKey = data.slice(checkPos + 6, checkPos + 38);
25955
- const privKeyHex = bytesToHex4(privKey);
25956
- if (isValidPrivateKey(privKeyHex)) {
25957
- keys.push(privKeyHex);
25958
- break;
25959
- }
25960
- }
25961
- }
25962
- index++;
25963
- }
25964
- return keys;
25965
- }
25966
- function extractLegacyKeys(data) {
25967
- const keys = [];
25968
- const keyPattern = new TextEncoder().encode("key");
25969
- let index = 0;
25970
- while ((index = findPattern(data, keyPattern, index)) !== -1) {
25971
- const searchPattern = new Uint8Array([4, 32]);
25972
- for (let i = index; i < Math.min(index + 200, data.length - 34); i++) {
25973
- if (data[i] === searchPattern[0] && data[i + 1] === searchPattern[1]) {
25974
- const privKey = data.slice(i + 2, i + 34);
25975
- const privKeyHex = bytesToHex4(privKey);
25976
- if (isValidPrivateKey(privKeyHex)) {
25977
- keys.push(privKeyHex);
25978
- break;
25979
- }
25980
- }
25981
- }
25982
- index++;
25983
- }
25984
- return keys;
25985
- }
25986
- function findEncryptedKeyForDescriptor(data, descriptorId) {
25987
- const ckeyPattern = new TextEncoder().encode("walletdescriptorckey");
25988
- let ckeyIndex = findPattern(data, ckeyPattern, 0);
25989
- while (ckeyIndex !== -1) {
25990
- const recordDescId = data.slice(ckeyIndex + ckeyPattern.length, ckeyIndex + ckeyPattern.length + 32);
25991
- if (Array.from(recordDescId).every((b, i) => b === descriptorId[i])) {
25992
- let keyPos = ckeyIndex + ckeyPattern.length + 32;
25993
- const pubkeyLen = data[keyPos];
25994
- keyPos++;
25995
- const pubkey = data.slice(keyPos, keyPos + pubkeyLen);
25996
- for (let searchPos = keyPos + pubkeyLen; searchPos < Math.min(keyPos + pubkeyLen + 100, data.length - 50); searchPos++) {
25997
- const valueLen = data[searchPos];
25998
- if (valueLen >= 32 && valueLen <= 64) {
25999
- const encryptedKey = data.slice(searchPos + 1, searchPos + 1 + valueLen);
26000
- return { pubkey, encryptedKey };
26001
- }
26002
- }
26003
- }
26004
- ckeyIndex = findPattern(data, ckeyPattern, ckeyIndex + 1);
26005
- }
26006
- return null;
26007
- }
26008
- async function decryptCMasterKey(cmk, password, onProgress) {
26009
- const { encryptedKey, salt, iterations } = cmk;
26010
- const passwordHex = bytesToHex4(new TextEncoder().encode(password));
26011
- const saltHex = bytesToHex4(salt);
26012
- const inputHex = passwordHex + saltHex;
26013
- let hash = import_crypto_js4.default.SHA512(import_crypto_js4.default.enc.Hex.parse(inputHex));
26014
- const BATCH_SIZE = 1e3;
26015
- for (let i = 0; i < iterations - 1; i++) {
26016
- hash = import_crypto_js4.default.SHA512(hash);
26017
- if (onProgress && i % BATCH_SIZE === 0) {
26018
- await onProgress(i, iterations);
26019
- }
26020
- }
26021
- const derivedKey = import_crypto_js4.default.lib.WordArray.create(hash.words.slice(0, 8));
26022
- const derivedIv = import_crypto_js4.default.lib.WordArray.create(hash.words.slice(8, 12));
26023
- const encryptedWords = uint8ArrayToWordArray(encryptedKey);
26024
- const decrypted = import_crypto_js4.default.AES.decrypt(
26025
- { ciphertext: encryptedWords },
26026
- derivedKey,
26027
- { iv: derivedIv, padding: import_crypto_js4.default.pad.Pkcs7, mode: import_crypto_js4.default.mode.CBC }
26028
- );
26029
- const result = import_crypto_js4.default.enc.Hex.stringify(decrypted);
26030
- if (!result || result.length !== 64) {
26031
- throw new SphereError("Master key decryption failed - incorrect password", "DECRYPTION_ERROR");
26032
- }
26033
- return result;
26034
- }
26035
- function decryptPrivateKey(encryptedKey, pubkey, masterKeyHex) {
26036
- const pubkeyWords = uint8ArrayToWordArray(pubkey);
26037
- const pubkeyHashWords = import_crypto_js4.default.SHA256(import_crypto_js4.default.SHA256(pubkeyWords));
26038
- const ivWords = import_crypto_js4.default.lib.WordArray.create(pubkeyHashWords.words.slice(0, 4));
26039
- const masterKeyWords = import_crypto_js4.default.enc.Hex.parse(masterKeyHex);
26040
- const encryptedWords = uint8ArrayToWordArray(encryptedKey);
26041
- const decrypted = import_crypto_js4.default.AES.decrypt(
26042
- { ciphertext: encryptedWords },
26043
- masterKeyWords,
26044
- { iv: ivWords, padding: import_crypto_js4.default.pad.Pkcs7, mode: import_crypto_js4.default.mode.CBC }
26045
- );
26046
- return import_crypto_js4.default.enc.Hex.stringify(decrypted);
26047
- }
26048
- function parseWalletDat(data) {
26049
- if (!isSQLiteDatabase(data)) {
26050
- return {
26051
- success: false,
26052
- error: "Invalid wallet.dat file - not an SQLite database"
26053
- };
26054
- }
26055
- const isEncrypted = isWalletDatEncrypted(data);
26056
- const cmasterKeys = isEncrypted ? findAllCMasterKeys(data) : [];
26057
- const descriptorKeys = isEncrypted ? [] : extractDescriptorKeys(data);
26058
- const legacyKeys = isEncrypted ? [] : extractLegacyKeys(data);
26059
- const { descriptorId: _descriptorId, xpubString, descriptorPath } = findWpkhDescriptor(data);
26060
- void _descriptorId;
26061
- let chainCode = null;
26062
- if (xpubString) {
26063
- try {
26064
- chainCode = extractChainCodeFromXpub(xpubString);
26065
- } catch {
26066
- chainCode = findMasterChainCode(data);
26067
- }
26068
- } else {
26069
- chainCode = findMasterChainCode(data);
26070
- }
26071
- if (!isEncrypted) {
26072
- let masterKey = null;
26073
- if (descriptorKeys.length > 0) {
26074
- masterKey = descriptorKeys[0];
26075
- } else if (legacyKeys.length > 0) {
26076
- masterKey = legacyKeys[0];
26077
- }
26078
- if (masterKey) {
26079
- const parsedData = {
26080
- masterKey,
26081
- chainCode: chainCode ?? void 0,
26082
- descriptorPath: descriptorPath ?? "84'/1'/0'",
26083
- derivationMode: chainCode ? "bip32" : "wif_hmac"
25484
+ return {
25485
+ success: false,
25486
+ error: "Could not find encrypted master key in backup file"
25487
+ };
25488
+ }
25489
+ return {
25490
+ success: false,
25491
+ needsPassword: true,
25492
+ error: "Password required for encrypted wallet"
26084
25493
  };
25494
+ }
25495
+ const masterKey = extractFromText(
25496
+ content,
25497
+ /MASTER PRIVATE KEY \(keep secret!\):\s*([^\n]+)/
25498
+ );
25499
+ if (!masterKey) {
26085
25500
  return {
26086
- success: true,
26087
- data: parsedData
25501
+ success: false,
25502
+ error: "Could not find master private key in backup file"
26088
25503
  };
26089
25504
  }
26090
- return {
26091
- success: false,
26092
- error: "No valid private keys found in wallet.dat file"
26093
- };
26094
- }
26095
- if (cmasterKeys.length === 0) {
26096
- return {
26097
- success: false,
26098
- error: "Encrypted wallet but no CMasterKey structures found"
25505
+ const chainCode = extractFromText(
25506
+ content,
25507
+ /MASTER CHAIN CODE \(for (?:BIP32 HD|Alpha) wallet compatibility\):\s*([^\n]+)/
25508
+ );
25509
+ const descriptorPath = extractFromText(content, /DESCRIPTOR PATH:\s*([^\n]+)/);
25510
+ const isBIP32 = content.includes("WALLET TYPE: BIP32 hierarchical deterministic wallet") || content.includes("WALLET TYPE: Alpha descriptor wallet") || !!chainCode;
25511
+ const effectiveDescriptorPath = descriptorPath ?? (isBIP32 ? "84'/1'/0'" : void 0);
25512
+ const data = {
25513
+ masterKey,
25514
+ chainCode: chainCode ?? void 0,
25515
+ descriptorPath: effectiveDescriptorPath,
25516
+ derivationMode: isBIP32 ? "bip32" : "wif_hmac"
26099
25517
  };
26100
- }
26101
- const firstCmk = cmasterKeys[0];
26102
- return {
26103
- success: false,
26104
- needsPassword: true,
26105
- encryptionInfo: {
26106
- iterations: firstCmk.iterations,
26107
- salt: firstCmk.salt,
26108
- encryptedKey: firstCmk.encryptedKey
26109
- },
26110
- error: "Password required for encrypted wallet"
26111
- };
26112
- }
26113
- async function parseAndDecryptWalletDat(data, password, onProgress) {
26114
- if (!isSQLiteDatabase(data)) {
26115
25518
  return {
26116
- success: false,
26117
- error: "Invalid wallet.dat file - not an SQLite database"
25519
+ success: true,
25520
+ data
26118
25521
  };
26119
- }
26120
- const isEncrypted = isWalletDatEncrypted(data);
26121
- if (!isEncrypted) {
26122
- return parseWalletDat(data);
26123
- }
26124
- const cmasterKeys = findAllCMasterKeys(data);
26125
- if (cmasterKeys.length === 0) {
25522
+ } catch (e) {
26126
25523
  return {
26127
25524
  success: false,
26128
- error: "Encrypted wallet but no CMasterKey structures found"
25525
+ error: `Failed to parse wallet text: ${e instanceof Error ? e.message : String(e)}`
26129
25526
  };
26130
25527
  }
26131
- let masterKeyHex = null;
26132
- for (const cmk of cmasterKeys) {
26133
- try {
26134
- masterKeyHex = await decryptCMasterKey(cmk, password, onProgress);
26135
- if (masterKeyHex && masterKeyHex.length === 64) {
26136
- break;
26137
- }
26138
- } catch {
26139
- continue;
25528
+ }
25529
+ function parseAndDecryptWalletText(content, password) {
25530
+ try {
25531
+ const isEncrypted = isTextWalletEncrypted(content);
25532
+ if (!isEncrypted) {
25533
+ return parseWalletText(content);
26140
25534
  }
26141
- }
26142
- if (!masterKeyHex || masterKeyHex.length !== 64) {
26143
- return {
26144
- success: false,
26145
- error: "Master key decryption failed - incorrect password"
26146
- };
26147
- }
26148
- const { descriptorId, xpubString, descriptorPath } = findWpkhDescriptor(data);
26149
- let chainCode = null;
26150
- if (xpubString) {
26151
- try {
26152
- chainCode = extractChainCodeFromXpub(xpubString);
26153
- } catch {
26154
- chainCode = findMasterChainCode(data);
25535
+ const encryptedKey = extractFromText(
25536
+ content,
25537
+ /ENCRYPTED MASTER KEY \(password protected\):\s*([^\n]+)/
25538
+ );
25539
+ if (!encryptedKey) {
25540
+ return {
25541
+ success: false,
25542
+ error: "Could not find encrypted master key in backup file"
25543
+ };
26155
25544
  }
26156
- } else {
26157
- chainCode = findMasterChainCode(data);
26158
- }
26159
- if (!descriptorId) {
26160
- return {
26161
- success: false,
26162
- error: "Could not find native SegWit receive descriptor"
25545
+ const masterKey = decryptTextFormatKey(encryptedKey, password);
25546
+ if (!masterKey) {
25547
+ return {
25548
+ success: false,
25549
+ error: "Failed to decrypt - incorrect password?"
25550
+ };
25551
+ }
25552
+ const chainCode = extractFromText(
25553
+ content,
25554
+ /MASTER CHAIN CODE \(for (?:BIP32 HD|Alpha) wallet compatibility\):\s*([^\n]+)/
25555
+ );
25556
+ const descriptorPath = extractFromText(content, /DESCRIPTOR PATH:\s*([^\n]+)/);
25557
+ const isBIP32 = content.includes("WALLET TYPE: BIP32 hierarchical deterministic wallet") || content.includes("WALLET TYPE: Alpha descriptor wallet") || !!chainCode;
25558
+ const effectiveDescriptorPath = descriptorPath ?? (isBIP32 ? "84'/1'/0'" : void 0);
25559
+ const data = {
25560
+ masterKey,
25561
+ chainCode: chainCode ?? void 0,
25562
+ descriptorPath: effectiveDescriptorPath,
25563
+ derivationMode: isBIP32 ? "bip32" : "wif_hmac"
26163
25564
  };
26164
- }
26165
- const encryptedKeyData = findEncryptedKeyForDescriptor(data, descriptorId);
26166
- if (!encryptedKeyData) {
26167
25565
  return {
26168
- success: false,
26169
- error: "Could not find encrypted private key for descriptor"
25566
+ success: true,
25567
+ data
26170
25568
  };
26171
- }
26172
- const bip32MasterKey = decryptPrivateKey(
26173
- encryptedKeyData.encryptedKey,
26174
- encryptedKeyData.pubkey,
26175
- masterKeyHex
26176
- );
26177
- if (!bip32MasterKey || bip32MasterKey.length !== 64) {
25569
+ } catch (e) {
26178
25570
  return {
26179
25571
  success: false,
26180
- error: "Could not decrypt BIP32 master private key"
25572
+ error: `Failed to parse/decrypt wallet text: ${e instanceof Error ? e.message : String(e)}`
26181
25573
  };
26182
25574
  }
26183
- const parsedData = {
26184
- masterKey: bip32MasterKey,
26185
- chainCode: chainCode ?? void 0,
26186
- descriptorPath: descriptorPath ?? "84'/1'/0'",
26187
- derivationMode: "bip32"
26188
- };
26189
- return {
26190
- success: true,
26191
- data: parsedData
26192
- };
26193
25575
  }
26194
25576
 
26195
25577
  // core/Sphere.ts
@@ -27022,32 +26404,196 @@ var Sphere = class _Sphere {
27022
26404
  if (this._transport.isConnected() && this._transport.fetchPendingEvents) {
27023
26405
  await this._transport.fetchPendingEvents();
27024
26406
  }
27025
- }
27026
- getAggregator() {
27027
- return this._oracle;
26407
+ }
26408
+ getAggregator() {
26409
+ return this._oracle;
26410
+ }
26411
+ /**
26412
+ * Apply a new gateway API key to the LIVE oracle + token engine WITHOUT a full
26413
+ * Sphere rebuild (transport / realtime socket / discovery stay up). Use this to
26414
+ * inject a per-wallet subscription key provisioned AFTER init, or a per-address
26415
+ * key on an address switch, so the next money operation authenticates with it.
26416
+ *
26417
+ * Money-safety: the running token engine is snapshotted per-operation (the
26418
+ * payments module reads `deps.tokenEngine` into a local at op start), so an
26419
+ * in-flight send/mint completes on the OLD key (submit + proof stay paired on
26420
+ * one client) and only operations started AFTER this call use the new key.
26421
+ * Only the ACTIVE address's engine is rebuilt — other tracked addresses re-key
26422
+ * on their next switch/build. No-op-safe if the engine can't be built (the
26423
+ * modules fall back to their legacy path, same as init).
26424
+ */
26425
+ async setOracleApiKey(apiKey) {
26426
+ this._oracle.setApiKey?.(apiKey);
26427
+ const active = this._addressModules.get(this._currentAddressIndex);
26428
+ const replaced = active?.tokenEngine ?? this._tokenEngine;
26429
+ this._tokenEngine = await this.buildTokenEngine();
26430
+ if (active) active.tokenEngine = this._tokenEngine;
26431
+ this._payments.setTokenEngine(this._tokenEngine);
26432
+ if (replaced && replaced !== this._tokenEngine) replaced.dispose?.();
26433
+ }
26434
+ /**
26435
+ * Import a wallet from a backup file: a `UNICITY WALLET DETAILS` text backup
26436
+ * (the format `exportToTxt` writes, optionally password-encrypted), a legacy
26437
+ * flat-JSON webwallet export, or a bare mnemonic in a text file.
26438
+ *
26439
+ * @example
26440
+ * const result = await Sphere.importFromLegacyFile({
26441
+ * fileContent: await file.text(),
26442
+ * fileName: file.name,
26443
+ * password, // when the backup is encrypted
26444
+ * ...providers,
26445
+ * });
26446
+ */
26447
+ static async importFromLegacyFile(options) {
26448
+ const { fileContent, fileName, password, onDecryptProgress, ...baseOptions } = options;
26449
+ const fileType = _Sphere.detectLegacyFileType(fileName, fileContent);
26450
+ if (fileType === "unknown") {
26451
+ return { success: false, error: "Unknown file format" };
26452
+ }
26453
+ if (fileType === "mnemonic") {
26454
+ const mnemonic = fileContent.trim().toLowerCase().split(/\s+/).join(" ");
26455
+ if (!_Sphere.validateMnemonic(mnemonic)) {
26456
+ return { success: false, error: "Invalid mnemonic phrase" };
26457
+ }
26458
+ const sphere = await _Sphere.import({ ...baseOptions, mnemonic });
26459
+ return { success: true, sphere, mnemonic };
26460
+ }
26461
+ if (fileType === "txt") {
26462
+ const content = fileContent;
26463
+ let parseResult;
26464
+ if (password) {
26465
+ parseResult = parseAndDecryptWalletText(content, password);
26466
+ } else if (isTextWalletEncrypted(content)) {
26467
+ return { success: false, needsPassword: true, error: "Password required for encrypted wallet" };
26468
+ } else {
26469
+ parseResult = parseWalletText(content);
26470
+ }
26471
+ if (parseResult.needsPassword && !password) {
26472
+ return { success: false, needsPassword: true, error: "Password required for encrypted wallet" };
26473
+ }
26474
+ if (!parseResult.success || !parseResult.data) {
26475
+ return { success: false, error: parseResult.error };
26476
+ }
26477
+ const { masterKey, chainCode, descriptorPath, derivationMode } = parseResult.data;
26478
+ const basePath = descriptorPath ? `m/${descriptorPath}` : DEFAULT_BASE_PATH;
26479
+ const sphere = await _Sphere.import({
26480
+ ...baseOptions,
26481
+ masterKey,
26482
+ chainCode,
26483
+ basePath,
26484
+ derivationMode: derivationMode || (chainCode ? "bip32" : "wif_hmac")
26485
+ });
26486
+ return { success: true, sphere };
26487
+ }
26488
+ if (fileType === "json") {
26489
+ const content = fileContent;
26490
+ let parsed;
26491
+ try {
26492
+ parsed = JSON.parse(content);
26493
+ } catch {
26494
+ return { success: false, error: "Invalid JSON file" };
26495
+ }
26496
+ if (parsed.type === "sphere-wallet") {
26497
+ const result = await _Sphere.importFromJSON({
26498
+ ...baseOptions,
26499
+ jsonContent: content,
26500
+ password
26501
+ });
26502
+ if (result.success) {
26503
+ const sphere2 = _Sphere.getInstance();
26504
+ return { success: true, sphere: sphere2, mnemonic: result.mnemonic };
26505
+ }
26506
+ if (!password && result.error?.includes("Password required")) {
26507
+ return { success: false, needsPassword: true, error: result.error };
26508
+ }
26509
+ return { success: false, error: result.error };
26510
+ }
26511
+ let masterKey;
26512
+ let mnemonic;
26513
+ if (parsed.encrypted && typeof parsed.encrypted === "object") {
26514
+ if (!password) {
26515
+ return { success: false, needsPassword: true, error: "Password required for encrypted wallet" };
26516
+ }
26517
+ const enc = parsed.encrypted;
26518
+ if (!enc.salt || !enc.masterPrivateKey) {
26519
+ return { success: false, error: "Invalid encrypted wallet format" };
26520
+ }
26521
+ const decryptedKey = decryptWithSalt(enc.masterPrivateKey, password, enc.salt);
26522
+ if (!decryptedKey) {
26523
+ return { success: false, error: "Failed to decrypt - incorrect password?" };
26524
+ }
26525
+ masterKey = decryptedKey;
26526
+ if (enc.mnemonic) {
26527
+ mnemonic = decryptWithSalt(enc.mnemonic, password, enc.salt) ?? void 0;
26528
+ }
26529
+ } else {
26530
+ masterKey = parsed.masterPrivateKey;
26531
+ mnemonic = parsed.mnemonic;
26532
+ }
26533
+ if (!masterKey) {
26534
+ return { success: false, error: "No master key found in wallet JSON" };
26535
+ }
26536
+ const chainCode = parsed.chainCode;
26537
+ const descriptorPath = parsed.descriptorPath;
26538
+ const derivationMode = parsed.derivationMode;
26539
+ const isBIP32 = derivationMode === "bip32" || !!chainCode;
26540
+ const basePath = descriptorPath ? `m/${descriptorPath}` : isBIP32 ? "m/84'/1'/0'" : DEFAULT_BASE_PATH;
26541
+ if (mnemonic) {
26542
+ const sphere2 = await _Sphere.import({ ...baseOptions, mnemonic, basePath });
26543
+ return { success: true, sphere: sphere2, mnemonic };
26544
+ }
26545
+ const sphere = await _Sphere.import({
26546
+ ...baseOptions,
26547
+ masterKey,
26548
+ chainCode,
26549
+ basePath,
26550
+ derivationMode: derivationMode || (chainCode ? "bip32" : "wif_hmac")
26551
+ });
26552
+ return { success: true, sphere };
26553
+ }
26554
+ return { success: false, error: "Unsupported file type" };
26555
+ }
26556
+ /**
26557
+ * Detect legacy file type from filename and content
26558
+ */
26559
+ static detectLegacyFileType(fileName, content) {
26560
+ if (fileName.endsWith(".json")) {
26561
+ return "json";
26562
+ }
26563
+ try {
26564
+ const trimmed = content.trim();
26565
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
26566
+ JSON.parse(trimmed);
26567
+ return "json";
26568
+ }
26569
+ } catch {
26570
+ }
26571
+ const words = content.trim().split(/\s+/);
26572
+ if ((words.length === 12 || words.length === 24) && words.every((w) => /^[a-z]+$/.test(w.toLowerCase()))) {
26573
+ return "mnemonic";
26574
+ }
26575
+ if (isWalletTextFormat(content)) {
26576
+ return "txt";
26577
+ }
26578
+ return "unknown";
27028
26579
  }
27029
26580
  /**
27030
- * Apply a new gateway API key to the LIVE oracle + token engine WITHOUT a full
27031
- * Sphere rebuild (transport / realtime socket / discovery stay up). Use this to
27032
- * inject a per-wallet subscription key provisioned AFTER init, or a per-address
27033
- * key on an address switch, so the next money operation authenticates with it.
27034
- *
27035
- * Money-safety: the running token engine is snapshotted per-operation (the
27036
- * payments module reads `deps.tokenEngine` into a local at op start), so an
27037
- * in-flight send/mint completes on the OLD key (submit + proof stay paired on
27038
- * one client) and only operations started AFTER this call use the new key.
27039
- * Only the ACTIVE address's engine is rebuilt — other tracked addresses re-key
27040
- * on their next switch/build. No-op-safe if the engine can't be built (the
27041
- * modules fall back to their legacy path, same as init).
26581
+ * Check if a legacy file is encrypted
27042
26582
  */
27043
- async setOracleApiKey(apiKey) {
27044
- this._oracle.setApiKey?.(apiKey);
27045
- const active = this._addressModules.get(this._currentAddressIndex);
27046
- const replaced = active?.tokenEngine ?? this._tokenEngine;
27047
- this._tokenEngine = await this.buildTokenEngine();
27048
- if (active) active.tokenEngine = this._tokenEngine;
27049
- this._payments.setTokenEngine(this._tokenEngine);
27050
- if (replaced && replaced !== this._tokenEngine) replaced.dispose?.();
26583
+ static isLegacyFileEncrypted(fileName, content) {
26584
+ const fileType = _Sphere.detectLegacyFileType(fileName, content);
26585
+ if (fileType === "txt") {
26586
+ return isTextWalletEncrypted(content);
26587
+ }
26588
+ if (fileType === "json") {
26589
+ try {
26590
+ const data = JSON.parse(content);
26591
+ return !!data.encrypted;
26592
+ } catch {
26593
+ return false;
26594
+ }
26595
+ }
26596
+ return false;
27051
26597
  }
27052
26598
  /**
27053
26599
  * Check if wallet has BIP32 master key for HD derivation
@@ -27319,224 +26865,6 @@ var Sphere = class _Sphere {
27319
26865
  };
27320
26866
  }
27321
26867
  }
27322
- /**
27323
- * Import wallet from legacy file (.dat, .txt, or mnemonic text)
27324
- *
27325
- * Supports:
27326
- * - Bitcoin Core wallet.dat files (SQLite format, encrypted or unencrypted)
27327
- * - Text backup files (UNICITY WALLET DETAILS format)
27328
- * - Plain mnemonic text (12 or 24 words)
27329
- *
27330
- * @returns Object with success status, created Sphere instance, and optionally recovered mnemonic
27331
- *
27332
- * @example
27333
- * ```ts
27334
- * // Import from .dat file
27335
- * const fileBuffer = await file.arrayBuffer();
27336
- * const result = await Sphere.importFromLegacyFile({
27337
- * fileContent: new Uint8Array(fileBuffer),
27338
- * fileName: 'wallet.dat',
27339
- * password: 'wallet-password', // if encrypted
27340
- * storage, transport, oracle,
27341
- * });
27342
- *
27343
- * // Import from .txt file
27344
- * const textContent = await file.text();
27345
- * const result = await Sphere.importFromLegacyFile({
27346
- * fileContent: textContent,
27347
- * fileName: 'backup.txt',
27348
- * storage, transport, oracle,
27349
- * });
27350
- * ```
27351
- */
27352
- static async importFromLegacyFile(options) {
27353
- const { fileContent, fileName, password, onDecryptProgress, ...baseOptions } = options;
27354
- const fileType = _Sphere.detectLegacyFileType(fileName, fileContent);
27355
- if (fileType === "unknown") {
27356
- return { success: false, error: "Unknown file format" };
27357
- }
27358
- if (fileType === "mnemonic") {
27359
- const mnemonic = fileContent.trim().toLowerCase().split(/\s+/).join(" ");
27360
- if (!_Sphere.validateMnemonic(mnemonic)) {
27361
- return { success: false, error: "Invalid mnemonic phrase" };
27362
- }
27363
- const sphere = await _Sphere.import({ ...baseOptions, mnemonic });
27364
- return { success: true, sphere, mnemonic };
27365
- }
27366
- if (fileType === "dat") {
27367
- const data = fileContent instanceof Uint8Array ? fileContent : new TextEncoder().encode(fileContent);
27368
- let parseResult;
27369
- if (password) {
27370
- parseResult = await parseAndDecryptWalletDat(data, password, onDecryptProgress);
27371
- } else {
27372
- parseResult = parseWalletDat(data);
27373
- }
27374
- if (parseResult.needsPassword && !password) {
27375
- return { success: false, needsPassword: true, error: "Password required for encrypted wallet" };
27376
- }
27377
- if (!parseResult.success || !parseResult.data) {
27378
- return { success: false, error: parseResult.error };
27379
- }
27380
- const { masterKey, chainCode, descriptorPath, derivationMode } = parseResult.data;
27381
- const basePath = descriptorPath ? `m/${descriptorPath}` : DEFAULT_BASE_PATH;
27382
- const sphere = await _Sphere.import({
27383
- ...baseOptions,
27384
- masterKey,
27385
- chainCode,
27386
- basePath,
27387
- derivationMode: derivationMode || (chainCode ? "bip32" : "wif_hmac")
27388
- });
27389
- return { success: true, sphere };
27390
- }
27391
- if (fileType === "txt") {
27392
- const content = typeof fileContent === "string" ? fileContent : new TextDecoder().decode(fileContent);
27393
- let parseResult;
27394
- if (password) {
27395
- parseResult = parseAndDecryptWalletText(content, password);
27396
- } else if (isTextWalletEncrypted(content)) {
27397
- return { success: false, needsPassword: true, error: "Password required for encrypted wallet" };
27398
- } else {
27399
- parseResult = parseWalletText(content);
27400
- }
27401
- if (parseResult.needsPassword && !password) {
27402
- return { success: false, needsPassword: true, error: "Password required for encrypted wallet" };
27403
- }
27404
- if (!parseResult.success || !parseResult.data) {
27405
- return { success: false, error: parseResult.error };
27406
- }
27407
- const { masterKey, chainCode, descriptorPath, derivationMode } = parseResult.data;
27408
- const basePath = descriptorPath ? `m/${descriptorPath}` : DEFAULT_BASE_PATH;
27409
- const sphere = await _Sphere.import({
27410
- ...baseOptions,
27411
- masterKey,
27412
- chainCode,
27413
- basePath,
27414
- derivationMode: derivationMode || (chainCode ? "bip32" : "wif_hmac")
27415
- });
27416
- return { success: true, sphere };
27417
- }
27418
- if (fileType === "json") {
27419
- const content = typeof fileContent === "string" ? fileContent : new TextDecoder().decode(fileContent);
27420
- let parsed;
27421
- try {
27422
- parsed = JSON.parse(content);
27423
- } catch {
27424
- return { success: false, error: "Invalid JSON file" };
27425
- }
27426
- if (parsed.type === "sphere-wallet") {
27427
- const result = await _Sphere.importFromJSON({
27428
- ...baseOptions,
27429
- jsonContent: content,
27430
- password
27431
- });
27432
- if (result.success) {
27433
- const sphere2 = _Sphere.getInstance();
27434
- return { success: true, sphere: sphere2, mnemonic: result.mnemonic };
27435
- }
27436
- if (!password && result.error?.includes("Password required")) {
27437
- return { success: false, needsPassword: true, error: result.error };
27438
- }
27439
- return { success: false, error: result.error };
27440
- }
27441
- let masterKey;
27442
- let mnemonic;
27443
- if (parsed.encrypted && typeof parsed.encrypted === "object") {
27444
- if (!password) {
27445
- return { success: false, needsPassword: true, error: "Password required for encrypted wallet" };
27446
- }
27447
- const enc = parsed.encrypted;
27448
- if (!enc.salt || !enc.masterPrivateKey) {
27449
- return { success: false, error: "Invalid encrypted wallet format" };
27450
- }
27451
- const decryptedKey = decryptWithSalt(enc.masterPrivateKey, password, enc.salt);
27452
- if (!decryptedKey) {
27453
- return { success: false, error: "Failed to decrypt - incorrect password?" };
27454
- }
27455
- masterKey = decryptedKey;
27456
- if (enc.mnemonic) {
27457
- mnemonic = decryptWithSalt(enc.mnemonic, password, enc.salt) ?? void 0;
27458
- }
27459
- } else {
27460
- masterKey = parsed.masterPrivateKey;
27461
- mnemonic = parsed.mnemonic;
27462
- }
27463
- if (!masterKey) {
27464
- return { success: false, error: "No master key found in wallet JSON" };
27465
- }
27466
- const chainCode = parsed.chainCode;
27467
- const descriptorPath = parsed.descriptorPath;
27468
- const derivationMode = parsed.derivationMode;
27469
- const isBIP32 = derivationMode === "bip32" || !!chainCode;
27470
- const basePath = descriptorPath ? `m/${descriptorPath}` : isBIP32 ? "m/84'/1'/0'" : DEFAULT_BASE_PATH;
27471
- if (mnemonic) {
27472
- const sphere2 = await _Sphere.import({ ...baseOptions, mnemonic, basePath });
27473
- return { success: true, sphere: sphere2, mnemonic };
27474
- }
27475
- const sphere = await _Sphere.import({
27476
- ...baseOptions,
27477
- masterKey,
27478
- chainCode,
27479
- basePath,
27480
- derivationMode: derivationMode || (chainCode ? "bip32" : "wif_hmac")
27481
- });
27482
- return { success: true, sphere };
27483
- }
27484
- return { success: false, error: "Unsupported file type" };
27485
- }
27486
- /**
27487
- * Detect legacy file type from filename and content
27488
- */
27489
- static detectLegacyFileType(fileName, content) {
27490
- if (fileName.endsWith(".dat")) {
27491
- return "dat";
27492
- }
27493
- const textContent = typeof content === "string" ? content : content.length < 1e3 ? new TextDecoder().decode(content) : "";
27494
- if (fileName.endsWith(".json")) {
27495
- return "json";
27496
- }
27497
- try {
27498
- const trimmed = textContent.trim();
27499
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
27500
- JSON.parse(trimmed);
27501
- return "json";
27502
- }
27503
- } catch {
27504
- }
27505
- const words = textContent.trim().split(/\s+/);
27506
- if ((words.length === 12 || words.length === 24) && words.every((w) => /^[a-z]+$/.test(w.toLowerCase()))) {
27507
- return "mnemonic";
27508
- }
27509
- if (isWalletTextFormat(textContent)) {
27510
- return "txt";
27511
- }
27512
- if (content instanceof Uint8Array && isSQLiteDatabase(content)) {
27513
- return "dat";
27514
- }
27515
- return "unknown";
27516
- }
27517
- /**
27518
- * Check if a legacy file is encrypted
27519
- */
27520
- static isLegacyFileEncrypted(fileName, content) {
27521
- const fileType = _Sphere.detectLegacyFileType(fileName, content);
27522
- if (fileType === "dat" && content instanceof Uint8Array) {
27523
- return isWalletDatEncrypted(content);
27524
- }
27525
- if (fileType === "txt") {
27526
- const textContent = typeof content === "string" ? content : new TextDecoder().decode(content);
27527
- return isTextWalletEncrypted(textContent);
27528
- }
27529
- if (fileType === "json") {
27530
- try {
27531
- const textContent = typeof content === "string" ? content : new TextDecoder().decode(content);
27532
- const data = JSON.parse(textContent);
27533
- return !!data.encrypted;
27534
- } catch {
27535
- return false;
27536
- }
27537
- }
27538
- return false;
27539
- }
27540
26868
  /**
27541
26869
  * Get the current active address index
27542
26870
  *
@@ -29447,116 +28775,6 @@ var CurrencyUtils = {
29447
28775
  format: formatAmount
29448
28776
  };
29449
28777
 
29450
- // core/bech32.ts
29451
- var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
29452
- var GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
29453
- function convertBits(data, fromBits, toBits, pad) {
29454
- let acc = 0;
29455
- let bits = 0;
29456
- const ret = [];
29457
- const maxv = (1 << toBits) - 1;
29458
- for (let i = 0; i < data.length; i++) {
29459
- const value = data[i];
29460
- if (value < 0 || value >> fromBits !== 0) return null;
29461
- acc = acc << fromBits | value;
29462
- bits += fromBits;
29463
- while (bits >= toBits) {
29464
- bits -= toBits;
29465
- ret.push(acc >> bits & maxv);
29466
- }
29467
- }
29468
- if (pad) {
29469
- if (bits > 0) {
29470
- ret.push(acc << toBits - bits & maxv);
29471
- }
29472
- } else if (bits >= fromBits || acc << toBits - bits & maxv) {
29473
- return null;
29474
- }
29475
- return ret;
29476
- }
29477
- function hrpExpand(hrp) {
29478
- const ret = [];
29479
- for (let i = 0; i < hrp.length; i++) ret.push(hrp.charCodeAt(i) >> 5);
29480
- ret.push(0);
29481
- for (let i = 0; i < hrp.length; i++) ret.push(hrp.charCodeAt(i) & 31);
29482
- return ret;
29483
- }
29484
- function bech32Polymod(values) {
29485
- let chk = 1;
29486
- for (let p = 0; p < values.length; p++) {
29487
- const top = chk >> 25;
29488
- chk = (chk & 33554431) << 5 ^ values[p];
29489
- for (let i = 0; i < 5; i++) {
29490
- if (top >> i & 1) chk ^= GENERATOR[i];
29491
- }
29492
- }
29493
- return chk;
29494
- }
29495
- function bech32Checksum(hrp, data) {
29496
- const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
29497
- const mod2 = bech32Polymod(values) ^ 1;
29498
- const ret = [];
29499
- for (let p = 0; p < 6; p++) {
29500
- ret.push(mod2 >> 5 * (5 - p) & 31);
29501
- }
29502
- return ret;
29503
- }
29504
- function encodeBech32(hrp, version, program) {
29505
- if (version < 0 || version > 16) {
29506
- throw new SphereError("Invalid witness version", "VALIDATION_ERROR");
29507
- }
29508
- const converted = convertBits(Array.from(program), 8, 5, true);
29509
- if (!converted) {
29510
- throw new SphereError("Failed to convert bits", "VALIDATION_ERROR");
29511
- }
29512
- const data = [version].concat(converted);
29513
- const checksum = bech32Checksum(hrp, data);
29514
- const combined = data.concat(checksum);
29515
- let out = hrp + "1";
29516
- for (let i = 0; i < combined.length; i++) {
29517
- out += CHARSET[combined[i]];
29518
- }
29519
- return out;
29520
- }
29521
- function decodeBech32(addr) {
29522
- addr = addr.toLowerCase();
29523
- const pos = addr.lastIndexOf("1");
29524
- if (pos < 1) return null;
29525
- const hrp = addr.substring(0, pos);
29526
- const dataStr = addr.substring(pos + 1);
29527
- const data = [];
29528
- for (let i = 0; i < dataStr.length; i++) {
29529
- const val = CHARSET.indexOf(dataStr[i]);
29530
- if (val === -1) return null;
29531
- data.push(val);
29532
- }
29533
- const checksum = bech32Checksum(hrp, data.slice(0, -6));
29534
- for (let i = 0; i < 6; i++) {
29535
- if (checksum[i] !== data[data.length - 6 + i]) {
29536
- return null;
29537
- }
29538
- }
29539
- const version = data[0];
29540
- const program = convertBits(data.slice(1, -6), 5, 8, false);
29541
- if (!program) return null;
29542
- return {
29543
- hrp,
29544
- witnessVersion: version,
29545
- data: Uint8Array.from(program)
29546
- };
29547
- }
29548
- function createAddress(hrp, pubkeyHash) {
29549
- const hashBytes = typeof pubkeyHash === "string" ? Uint8Array.from(Buffer.from(pubkeyHash, "hex")) : pubkeyHash;
29550
- return encodeBech32(hrp, 1, hashBytes);
29551
- }
29552
- function isValidBech32(addr) {
29553
- return decodeBech32(addr) !== null;
29554
- }
29555
- function getAddressHrp(addr) {
29556
- const result = decodeBech32(addr);
29557
- return result?.hrp ?? null;
29558
- }
29559
-
29560
28778
  // core/network-health.ts
29561
28779
  var DEFAULT_TIMEOUT_MS = 5e3;
29562
28780
  async function checkNetworkHealth(network = "testnet", options) {
@@ -29740,7 +28958,6 @@ async function runCustomCheck(name, checkFn, timeoutMs) {
29740
28958
  }
29741
28959
  // Annotate the CommonJS export names for ESM import in node:
29742
28960
  0 && (module.exports = {
29743
- CHARSET,
29744
28961
  CurrencyUtils,
29745
28962
  DEFAULT_DERIVATION_PATH,
29746
28963
  DEFAULT_TOKEN_DECIMALS,
@@ -29758,11 +28975,8 @@ async function runCustomCheck(name, checkFn, timeoutMs) {
29758
28975
  base58Encode,
29759
28976
  bytesToHex,
29760
28977
  checkNetworkHealth,
29761
- convertBits,
29762
- createAddress,
29763
28978
  createKeyPair,
29764
28979
  createSphere,
29765
- decodeBech32,
29766
28980
  decrypt,
29767
28981
  decryptDeliveryBundle,
29768
28982
  decryptField,
@@ -29779,7 +28993,6 @@ async function runCustomCheck(name, checkFn, timeoutMs) {
29779
28993
  deserializeEncrypted,
29780
28994
  discoverAddressesImpl,
29781
28995
  doubleSha256,
29782
- encodeBech32,
29783
28996
  encrypt,
29784
28997
  encryptDeliveryBundle,
29785
28998
  encryptField,
@@ -29795,7 +29008,6 @@ async function runCustomCheck(name, checkFn, timeoutMs) {
29795
29008
  generateMasterKey,
29796
29009
  generateMnemonic,
29797
29010
  generateRandomKey,
29798
- getAddressHrp,
29799
29011
  getPublicKey,
29800
29012
  getSphere,
29801
29013
  hash160,
@@ -29808,7 +29020,6 @@ async function runCustomCheck(name, checkFn, timeoutMs) {
29808
29020
  isEncryptedData,
29809
29021
  isPossiblyCommittedSendOutcome,
29810
29022
  isSphereError,
29811
- isValidBech32,
29812
29023
  isValidNametag,
29813
29024
  isValidPrivateKey,
29814
29025
  loadSphere,