@unicitylabs/sphere-sdk 0.1.9 → 0.2.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/index.js CHANGED
@@ -1539,7 +1539,6 @@ var L1PaymentsModule = class {
1539
1539
  _initialized = false;
1540
1540
  _config;
1541
1541
  _identity;
1542
- _chainCode;
1543
1542
  _addresses = [];
1544
1543
  _wallet;
1545
1544
  _transport;
@@ -1553,7 +1552,6 @@ var L1PaymentsModule = class {
1553
1552
  }
1554
1553
  async initialize(deps) {
1555
1554
  this._identity = deps.identity;
1556
- this._chainCode = deps.chainCode;
1557
1555
  this._addresses = deps.addresses ?? [];
1558
1556
  this._transport = deps.transport;
1559
1557
  this._wallet = {
@@ -1589,7 +1587,6 @@ var L1PaymentsModule = class {
1589
1587
  }
1590
1588
  this._initialized = false;
1591
1589
  this._identity = void 0;
1592
- this._chainCode = void 0;
1593
1590
  this._addresses = [];
1594
1591
  this._wallet = void 0;
1595
1592
  }
@@ -1624,10 +1621,10 @@ var L1PaymentsModule = class {
1624
1621
  * Resolve nametag to L1 address using transport provider
1625
1622
  */
1626
1623
  async resolveNametagToL1Address(nametag) {
1627
- if (!this._transport?.resolveNametagInfo) {
1628
- throw new Error("Transport provider does not support nametag resolution");
1624
+ if (!this._transport?.resolve) {
1625
+ throw new Error("Transport provider does not support resolution");
1629
1626
  }
1630
- const info = await this._transport.resolveNametagInfo(nametag);
1627
+ const info = await this._transport.resolve(nametag);
1631
1628
  if (!info) {
1632
1629
  throw new Error(`Nametag not found: ${nametag}`);
1633
1630
  }
@@ -1904,25 +1901,11 @@ var TokenSplitCalculator = class {
1904
1901
  * 3. If no exact match, determine which token to split
1905
1902
  */
1906
1903
  async calculateOptimalSplit(availableTokens, targetAmount, targetCoinIdHex) {
1907
- console.log(
1908
- `[SplitCalculator] Calculating split for ${targetAmount} of ${targetCoinIdHex}`
1909
- );
1910
- console.log(`[SplitCalculator] Available tokens: ${availableTokens.length}`);
1911
1904
  const candidates = [];
1912
1905
  for (const t of availableTokens) {
1913
- console.log(`[SplitCalculator] Token ${t.id}: coinId=${t.coinId}, status=${t.status}, hasSdkData=${!!t.sdkData}`);
1914
- if (t.coinId !== targetCoinIdHex) {
1915
- console.log(`[SplitCalculator] Skipping token ${t.id}: coinId mismatch (${t.coinId} !== ${targetCoinIdHex})`);
1916
- continue;
1917
- }
1918
- if (t.status !== "confirmed") {
1919
- console.log(`[SplitCalculator] Skipping token ${t.id}: status is ${t.status}`);
1920
- continue;
1921
- }
1922
- if (!t.sdkData) {
1923
- console.log(`[SplitCalculator] Skipping token ${t.id}: no sdkData`);
1924
- continue;
1925
- }
1906
+ if (t.coinId !== targetCoinIdHex) continue;
1907
+ if (t.status !== "confirmed") continue;
1908
+ if (!t.sdkData) continue;
1926
1909
  try {
1927
1910
  const parsed = JSON.parse(t.sdkData);
1928
1911
  const sdkToken = await SdkToken.fromJSON(parsed);
@@ -1950,14 +1933,12 @@ var TokenSplitCalculator = class {
1950
1933
  }
1951
1934
  const exactMatch = candidates.find((t) => t.amount === targetAmount);
1952
1935
  if (exactMatch) {
1953
- console.log("[SplitCalculator] Found exact match token");
1954
1936
  return this.createDirectPlan([exactMatch], targetAmount, targetCoinIdHex);
1955
1937
  }
1956
1938
  const maxCombinationSize = Math.min(5, candidates.length);
1957
1939
  for (let size = 2; size <= maxCombinationSize; size++) {
1958
1940
  const combo = this.findCombinationOfSize(candidates, targetAmount, size);
1959
1941
  if (combo) {
1960
- console.log(`[SplitCalculator] Found exact combination of ${size} tokens`);
1961
1942
  return this.createDirectPlan(combo, targetAmount, targetCoinIdHex);
1962
1943
  }
1963
1944
  }
@@ -1974,9 +1955,6 @@ var TokenSplitCalculator = class {
1974
1955
  } else {
1975
1956
  const neededFromThisToken = targetAmount - currentSum;
1976
1957
  const remainderForSender = candidate.amount - neededFromThisToken;
1977
- console.log(
1978
- `[SplitCalculator] Split required. Sending: ${neededFromThisToken}, Remainder: ${remainderForSender}`
1979
- );
1980
1958
  return {
1981
1959
  tokensToTransferDirectly: toTransfer,
1982
1960
  tokenToSplit: candidate,
@@ -1995,16 +1973,10 @@ var TokenSplitCalculator = class {
1995
1973
  */
1996
1974
  getTokenBalance(sdkToken, coinIdHex) {
1997
1975
  try {
1998
- if (!sdkToken.coins) {
1999
- console.log("[SplitCalculator] Token has no coins");
2000
- return 0n;
2001
- }
1976
+ if (!sdkToken.coins) return 0n;
2002
1977
  const coinId = CoinId.fromJSON(coinIdHex);
2003
- const balance = sdkToken.coins.get(coinId);
2004
- console.log(`[SplitCalculator] Token balance for ${coinIdHex.slice(0, 8)}...: ${balance ?? 0n}`);
2005
- return balance ?? 0n;
2006
- } catch (e) {
2007
- console.error("[SplitCalculator] Error getting token balance:", e);
1978
+ return sdkToken.coins.get(coinId) ?? 0n;
1979
+ } catch {
2008
1980
  return 0n;
2009
1981
  }
2010
1982
  }
@@ -2233,20 +2205,18 @@ var NametagMinter = class {
2233
2205
  const cleanNametag = nametag.replace("@", "").trim();
2234
2206
  this.log(`Starting mint for nametag: ${cleanNametag}`);
2235
2207
  try {
2236
- const isAvailable = await this.isNametagAvailable(cleanNametag);
2237
- if (!isAvailable) {
2238
- return {
2239
- success: false,
2240
- error: `Nametag "${cleanNametag}" is already taken`
2241
- };
2242
- }
2243
2208
  const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
2244
2209
  const nametagTokenType = new TokenType(
2245
2210
  Buffer.from(UNICITY_TOKEN_TYPE_HEX, "hex")
2246
2211
  );
2247
- const salt = new Uint8Array(32);
2248
- crypto.getRandomValues(salt);
2249
- this.log("Generated salt");
2212
+ const nametagBytes = new TextEncoder().encode(cleanNametag);
2213
+ const pubKey = this.signingService.publicKey;
2214
+ const saltInput = new Uint8Array(pubKey.length + nametagBytes.length);
2215
+ saltInput.set(pubKey, 0);
2216
+ saltInput.set(nametagBytes, pubKey.length);
2217
+ const saltBuffer = await crypto.subtle.digest("SHA-256", saltInput);
2218
+ const salt = new Uint8Array(saltBuffer);
2219
+ this.log("Generated deterministic salt");
2250
2220
  const mintData = await MintTransactionData.createFromNametag(
2251
2221
  cleanNametag,
2252
2222
  nametagTokenType,
@@ -2369,8 +2339,10 @@ var STORAGE_KEYS_GLOBAL = {
2369
2339
  WALLET_EXISTS: "wallet_exists",
2370
2340
  /** Current active address index */
2371
2341
  CURRENT_ADDRESS_INDEX: "current_address_index",
2372
- /** Index of address nametags (JSON: { "0": "alice", "1": "bob" }) - for discovery */
2373
- ADDRESS_NAMETAGS: "address_nametags"
2342
+ /** Nametag cache per address (separate from tracked addresses registry) */
2343
+ ADDRESS_NAMETAGS: "address_nametags",
2344
+ /** Active addresses registry (JSON: TrackedAddressesStorage) */
2345
+ TRACKED_ADDRESSES: "tracked_addresses"
2374
2346
  };
2375
2347
  var STORAGE_KEYS_ADDRESS = {
2376
2348
  /** Pending transfers for this address */
@@ -2819,11 +2791,16 @@ function getCurrentStateHash(txf) {
2819
2791
  if (lastTx?.newStateHash) {
2820
2792
  return lastTx.newStateHash;
2821
2793
  }
2822
- return void 0;
2794
+ if (lastTx?.inclusionProof?.authenticator?.stateHash) {
2795
+ return lastTx.inclusionProof.authenticator.stateHash;
2796
+ }
2823
2797
  }
2824
2798
  if (txf._integrity?.currentStateHash) {
2825
2799
  return txf._integrity.currentStateHash;
2826
2800
  }
2801
+ if (txf.genesis?.inclusionProof?.authenticator?.stateHash) {
2802
+ return txf.genesis.inclusionProof.authenticator.stateHash;
2803
+ }
2827
2804
  return void 0;
2828
2805
  }
2829
2806
  function hasValidTxfData(token) {
@@ -3184,16 +3161,733 @@ function getCoinIdByName(name) {
3184
3161
  return TokenRegistry.getInstance().getCoinIdByName(name);
3185
3162
  }
3186
3163
 
3187
- // modules/payments/PaymentsModule.ts
3188
- import { Token as SdkToken2 } from "@unicitylabs/state-transition-sdk/lib/token/Token";
3164
+ // modules/payments/InstantSplitExecutor.ts
3165
+ import { Token as Token3 } from "@unicitylabs/state-transition-sdk/lib/token/Token";
3166
+ import { TokenId as TokenId3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenId";
3167
+ import { TokenState as TokenState3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
3189
3168
  import { CoinId as CoinId3 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
3169
+ import { TokenCoinData as TokenCoinData2 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData";
3170
+ import { TokenSplitBuilder as TokenSplitBuilder2 } from "@unicitylabs/state-transition-sdk/lib/transaction/split/TokenSplitBuilder";
3171
+ import { HashAlgorithm as HashAlgorithm3 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
3172
+ import { UnmaskedPredicate as UnmaskedPredicate3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
3173
+ import { UnmaskedPredicateReference as UnmaskedPredicateReference2 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
3190
3174
  import { TransferCommitment as TransferCommitment2 } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment";
3175
+ import { waitInclusionProof as waitInclusionProof3 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
3176
+ async function sha2563(input) {
3177
+ const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
3178
+ const buffer = new ArrayBuffer(data.length);
3179
+ new Uint8Array(buffer).set(data);
3180
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
3181
+ return new Uint8Array(hashBuffer);
3182
+ }
3183
+ function toHex2(bytes) {
3184
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3185
+ }
3186
+ function fromHex2(hex) {
3187
+ const bytes = new Uint8Array(hex.length / 2);
3188
+ for (let i = 0; i < hex.length; i += 2) {
3189
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
3190
+ }
3191
+ return bytes;
3192
+ }
3193
+ var InstantSplitExecutor = class {
3194
+ client;
3195
+ trustBase;
3196
+ signingService;
3197
+ devMode;
3198
+ constructor(config) {
3199
+ this.client = config.stateTransitionClient;
3200
+ this.trustBase = config.trustBase;
3201
+ this.signingService = config.signingService;
3202
+ this.devMode = config.devMode ?? false;
3203
+ }
3204
+ /**
3205
+ * Execute an instant split transfer with V5 optimized flow.
3206
+ *
3207
+ * Critical path (~2.3s):
3208
+ * 1. Create and submit burn commitment
3209
+ * 2. Wait for burn proof
3210
+ * 3. Create mint commitments with SplitMintReason
3211
+ * 4. Create transfer commitment (no mint proof needed)
3212
+ * 5. Send bundle via transport
3213
+ *
3214
+ * @param tokenToSplit - The SDK token to split
3215
+ * @param splitAmount - Amount to send to recipient
3216
+ * @param remainderAmount - Amount to keep as change
3217
+ * @param coinIdHex - Coin ID in hex format
3218
+ * @param recipientAddress - Recipient's address (PROXY or DIRECT)
3219
+ * @param transport - Transport provider for sending the bundle
3220
+ * @param recipientPubkey - Recipient's transport public key
3221
+ * @param options - Optional configuration
3222
+ * @returns InstantSplitResult with success status and timing info
3223
+ */
3224
+ async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
3225
+ const startTime = performance.now();
3226
+ const splitGroupId = crypto.randomUUID();
3227
+ const tokenIdHex = toHex2(tokenToSplit.id.bytes);
3228
+ console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
3229
+ try {
3230
+ const coinId = new CoinId3(fromHex2(coinIdHex));
3231
+ const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
3232
+ const recipientTokenId = new TokenId3(await sha2563(seedString));
3233
+ const senderTokenId = new TokenId3(await sha2563(seedString + "_sender"));
3234
+ const recipientSalt = await sha2563(seedString + "_recipient_salt");
3235
+ const senderSalt = await sha2563(seedString + "_sender_salt");
3236
+ const senderAddressRef = await UnmaskedPredicateReference2.create(
3237
+ tokenToSplit.type,
3238
+ this.signingService.algorithm,
3239
+ this.signingService.publicKey,
3240
+ HashAlgorithm3.SHA256
3241
+ );
3242
+ const senderAddress = await senderAddressRef.toAddress();
3243
+ const builder = new TokenSplitBuilder2();
3244
+ const coinDataA = TokenCoinData2.create([[coinId, splitAmount]]);
3245
+ builder.createToken(
3246
+ recipientTokenId,
3247
+ tokenToSplit.type,
3248
+ new Uint8Array(0),
3249
+ coinDataA,
3250
+ senderAddress,
3251
+ // Mint to sender first, then transfer
3252
+ recipientSalt,
3253
+ null
3254
+ );
3255
+ const coinDataB = TokenCoinData2.create([[coinId, remainderAmount]]);
3256
+ builder.createToken(
3257
+ senderTokenId,
3258
+ tokenToSplit.type,
3259
+ new Uint8Array(0),
3260
+ coinDataB,
3261
+ senderAddress,
3262
+ senderSalt,
3263
+ null
3264
+ );
3265
+ const split = await builder.build(tokenToSplit);
3266
+ console.log("[InstantSplit] Step 1: Creating and submitting burn...");
3267
+ const burnSalt = await sha2563(seedString + "_burn_salt");
3268
+ const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
3269
+ const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
3270
+ if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
3271
+ throw new Error(`Burn submission failed: ${burnResponse.status}`);
3272
+ }
3273
+ console.log("[InstantSplit] Step 2: Waiting for burn proof...");
3274
+ const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await waitInclusionProof3(this.trustBase, this.client, burnCommitment);
3275
+ const burnTransaction = burnCommitment.toTransaction(burnProof);
3276
+ const burnDuration = performance.now() - startTime;
3277
+ console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
3278
+ options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
3279
+ console.log("[InstantSplit] Step 3: Creating mint commitments...");
3280
+ const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
3281
+ const recipientIdHex = toHex2(recipientTokenId.bytes);
3282
+ const senderIdHex = toHex2(senderTokenId.bytes);
3283
+ const recipientMintCommitment = mintCommitments.find(
3284
+ (c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
3285
+ );
3286
+ const senderMintCommitment = mintCommitments.find(
3287
+ (c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
3288
+ );
3289
+ if (!recipientMintCommitment || !senderMintCommitment) {
3290
+ throw new Error("Failed to find expected mint commitments");
3291
+ }
3292
+ console.log("[InstantSplit] Step 4: Creating transfer commitment...");
3293
+ const transferSalt = await sha2563(seedString + "_transfer_salt");
3294
+ const transferCommitment = await this.createTransferCommitmentFromMintData(
3295
+ recipientMintCommitment.transactionData,
3296
+ recipientAddress,
3297
+ transferSalt,
3298
+ this.signingService
3299
+ );
3300
+ const mintedPredicate = await UnmaskedPredicate3.create(
3301
+ recipientTokenId,
3302
+ tokenToSplit.type,
3303
+ this.signingService,
3304
+ HashAlgorithm3.SHA256,
3305
+ recipientSalt
3306
+ );
3307
+ const mintedState = new TokenState3(mintedPredicate, null);
3308
+ console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
3309
+ const senderPubkey = toHex2(this.signingService.publicKey);
3310
+ let nametagTokenJson;
3311
+ const recipientAddressStr = recipientAddress.toString();
3312
+ if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
3313
+ nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
3314
+ }
3315
+ const bundle = {
3316
+ version: "5.0",
3317
+ type: "INSTANT_SPLIT",
3318
+ burnTransaction: JSON.stringify(burnTransaction.toJSON()),
3319
+ recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
3320
+ transferCommitment: JSON.stringify(transferCommitment.toJSON()),
3321
+ amount: splitAmount.toString(),
3322
+ coinId: coinIdHex,
3323
+ tokenTypeHex: toHex2(tokenToSplit.type.bytes),
3324
+ splitGroupId,
3325
+ senderPubkey,
3326
+ recipientSaltHex: toHex2(recipientSalt),
3327
+ transferSaltHex: toHex2(transferSalt),
3328
+ mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
3329
+ finalRecipientStateJson: "",
3330
+ // Recipient creates their own
3331
+ recipientAddressJson: recipientAddressStr,
3332
+ nametagTokenJson
3333
+ };
3334
+ console.log("[InstantSplit] Step 6: Sending via transport...");
3335
+ const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
3336
+ token: JSON.stringify(bundle),
3337
+ proof: null,
3338
+ // Proof is included in the bundle
3339
+ memo: "INSTANT_SPLIT_V5",
3340
+ sender: {
3341
+ transportPubkey: senderPubkey
3342
+ }
3343
+ });
3344
+ const criticalPathDuration = performance.now() - startTime;
3345
+ console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
3346
+ options?.onNostrDelivered?.(nostrEventId);
3347
+ if (!options?.skipBackground) {
3348
+ this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
3349
+ signingService: this.signingService,
3350
+ tokenType: tokenToSplit.type,
3351
+ coinId,
3352
+ senderTokenId,
3353
+ senderSalt,
3354
+ onProgress: options?.onBackgroundProgress,
3355
+ onChangeTokenCreated: options?.onChangeTokenCreated,
3356
+ onStorageSync: options?.onStorageSync
3357
+ });
3358
+ }
3359
+ return {
3360
+ success: true,
3361
+ nostrEventId,
3362
+ splitGroupId,
3363
+ criticalPathDurationMs: criticalPathDuration,
3364
+ backgroundStarted: !options?.skipBackground
3365
+ };
3366
+ } catch (error) {
3367
+ const duration = performance.now() - startTime;
3368
+ const errorMessage = error instanceof Error ? error.message : String(error);
3369
+ console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
3370
+ return {
3371
+ success: false,
3372
+ splitGroupId,
3373
+ criticalPathDurationMs: duration,
3374
+ error: errorMessage,
3375
+ backgroundStarted: false
3376
+ };
3377
+ }
3378
+ }
3379
+ /**
3380
+ * Create a TransferCommitment from MintTransactionData WITHOUT waiting for mint proof.
3381
+ *
3382
+ * Key insight: TransferCommitment.create() only needs token.state and token.nametagTokens.
3383
+ * It does NOT need the genesis transaction or mint proof.
3384
+ */
3385
+ async createTransferCommitmentFromMintData(mintData, recipientAddress, transferSalt, signingService, nametagTokens) {
3386
+ const predicate = await UnmaskedPredicate3.create(
3387
+ mintData.tokenId,
3388
+ mintData.tokenType,
3389
+ signingService,
3390
+ HashAlgorithm3.SHA256,
3391
+ mintData.salt
3392
+ );
3393
+ const state = new TokenState3(predicate, null);
3394
+ const minimalToken = {
3395
+ state,
3396
+ nametagTokens: nametagTokens || [],
3397
+ id: mintData.tokenId,
3398
+ type: mintData.tokenType
3399
+ };
3400
+ const transferCommitment = await TransferCommitment2.create(
3401
+ minimalToken,
3402
+ recipientAddress,
3403
+ transferSalt,
3404
+ null,
3405
+ // recipientData
3406
+ null,
3407
+ // recipientDataHash
3408
+ signingService
3409
+ );
3410
+ return transferCommitment;
3411
+ }
3412
+ /**
3413
+ * V5 background submission.
3414
+ *
3415
+ * Submits mint commitments to aggregator in PARALLEL after transport delivery.
3416
+ * Then waits for sender's mint proof, reconstructs change token, and saves it.
3417
+ */
3418
+ submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, context) {
3419
+ console.log("[InstantSplit] Background: Starting parallel mint submission...");
3420
+ const startTime = performance.now();
3421
+ const submissions = Promise.all([
3422
+ this.client.submitMintCommitment(senderMintCommitment).then((res) => ({ type: "senderMint", status: res.status })).catch((err) => ({ type: "senderMint", status: "ERROR", error: err })),
3423
+ this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
3424
+ this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
3425
+ ]);
3426
+ submissions.then(async (results) => {
3427
+ const submitDuration = performance.now() - startTime;
3428
+ console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
3429
+ context.onProgress?.({
3430
+ stage: "MINTS_SUBMITTED",
3431
+ message: `All commitments submitted in ${submitDuration.toFixed(0)}ms`
3432
+ });
3433
+ const senderMintResult = results.find((r) => r.type === "senderMint");
3434
+ if (senderMintResult?.status !== "SUCCESS" && senderMintResult?.status !== "REQUEST_ID_EXISTS") {
3435
+ console.error("[InstantSplit] Background: Sender mint failed - cannot save change token");
3436
+ context.onProgress?.({
3437
+ stage: "FAILED",
3438
+ message: "Sender mint submission failed",
3439
+ error: String(senderMintResult?.error)
3440
+ });
3441
+ return;
3442
+ }
3443
+ console.log("[InstantSplit] Background: Waiting for sender mint proof...");
3444
+ const proofStartTime = performance.now();
3445
+ try {
3446
+ const senderMintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(senderMintCommitment) : await waitInclusionProof3(this.trustBase, this.client, senderMintCommitment);
3447
+ const proofDuration = performance.now() - proofStartTime;
3448
+ console.log(`[InstantSplit] Background: Sender mint proof received in ${proofDuration.toFixed(0)}ms`);
3449
+ context.onProgress?.({
3450
+ stage: "MINTS_PROVEN",
3451
+ message: `Mint proof received in ${proofDuration.toFixed(0)}ms`
3452
+ });
3453
+ const mintTransaction = senderMintCommitment.toTransaction(senderMintProof);
3454
+ const predicate = await UnmaskedPredicate3.create(
3455
+ context.senderTokenId,
3456
+ context.tokenType,
3457
+ context.signingService,
3458
+ HashAlgorithm3.SHA256,
3459
+ context.senderSalt
3460
+ );
3461
+ const state = new TokenState3(predicate, null);
3462
+ const changeToken = await Token3.mint(this.trustBase, state, mintTransaction);
3463
+ if (!this.devMode) {
3464
+ const verification = await changeToken.verify(this.trustBase);
3465
+ if (!verification.isSuccessful) {
3466
+ throw new Error(`Change token verification failed`);
3467
+ }
3468
+ }
3469
+ console.log("[InstantSplit] Background: Change token created");
3470
+ context.onProgress?.({
3471
+ stage: "CHANGE_TOKEN_SAVED",
3472
+ message: "Change token created and verified"
3473
+ });
3474
+ if (context.onChangeTokenCreated) {
3475
+ await context.onChangeTokenCreated(changeToken);
3476
+ console.log("[InstantSplit] Background: Change token saved");
3477
+ }
3478
+ if (context.onStorageSync) {
3479
+ try {
3480
+ const syncSuccess = await context.onStorageSync();
3481
+ console.log(`[InstantSplit] Background: Storage sync ${syncSuccess ? "completed" : "deferred"}`);
3482
+ context.onProgress?.({
3483
+ stage: "STORAGE_SYNCED",
3484
+ message: syncSuccess ? "Storage synchronized" : "Sync deferred"
3485
+ });
3486
+ } catch (syncError) {
3487
+ console.warn("[InstantSplit] Background: Storage sync error:", syncError);
3488
+ }
3489
+ }
3490
+ const totalDuration = performance.now() - startTime;
3491
+ console.log(`[InstantSplit] Background: Complete in ${totalDuration.toFixed(0)}ms`);
3492
+ context.onProgress?.({
3493
+ stage: "COMPLETED",
3494
+ message: `Background processing complete in ${totalDuration.toFixed(0)}ms`
3495
+ });
3496
+ } catch (proofError) {
3497
+ console.error("[InstantSplit] Background: Failed to get sender mint proof:", proofError);
3498
+ context.onProgress?.({
3499
+ stage: "FAILED",
3500
+ message: "Failed to get mint proof",
3501
+ error: String(proofError)
3502
+ });
3503
+ }
3504
+ }).catch((err) => {
3505
+ console.error("[InstantSplit] Background: Submission batch failed:", err);
3506
+ context.onProgress?.({
3507
+ stage: "FAILED",
3508
+ message: "Background submission failed",
3509
+ error: String(err)
3510
+ });
3511
+ });
3512
+ }
3513
+ /**
3514
+ * Dev mode bypass for waitInclusionProof.
3515
+ * In dev mode, we create a mock proof for testing.
3516
+ */
3517
+ async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
3518
+ if (this.devMode) {
3519
+ try {
3520
+ return await Promise.race([
3521
+ waitInclusionProof3(this.trustBase, this.client, commitment),
3522
+ new Promise(
3523
+ (_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
3524
+ )
3525
+ ]);
3526
+ } catch {
3527
+ console.log("[InstantSplit] Dev mode: Using mock proof");
3528
+ return {
3529
+ toJSON: () => ({ mock: true })
3530
+ };
3531
+ }
3532
+ }
3533
+ return waitInclusionProof3(this.trustBase, this.client, commitment);
3534
+ }
3535
+ };
3536
+
3537
+ // modules/payments/InstantSplitProcessor.ts
3538
+ import { Token as Token4 } from "@unicitylabs/state-transition-sdk/lib/token/Token";
3539
+ import { TokenState as TokenState4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
3540
+ import { TokenType as TokenType2 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
3541
+ import { HashAlgorithm as HashAlgorithm4 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
3542
+ import { UnmaskedPredicate as UnmaskedPredicate4 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
3543
+ import { TransferCommitment as TransferCommitment3 } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment";
3191
3544
  import { TransferTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction";
3545
+ import { MintCommitment as MintCommitment2 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment";
3546
+ import { MintTransactionData as MintTransactionData2 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData";
3547
+ import { waitInclusionProof as waitInclusionProof4 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
3548
+
3549
+ // types/instant-split.ts
3550
+ function isInstantSplitBundle(obj) {
3551
+ if (typeof obj !== "object" || obj === null) {
3552
+ return false;
3553
+ }
3554
+ const bundle = obj;
3555
+ if (bundle.type !== "INSTANT_SPLIT") return false;
3556
+ if (typeof bundle.recipientMintData !== "string") return false;
3557
+ if (typeof bundle.transferCommitment !== "string") return false;
3558
+ if (typeof bundle.amount !== "string") return false;
3559
+ if (typeof bundle.coinId !== "string") return false;
3560
+ if (typeof bundle.splitGroupId !== "string") return false;
3561
+ if (typeof bundle.senderPubkey !== "string") return false;
3562
+ if (typeof bundle.recipientSaltHex !== "string") return false;
3563
+ if (typeof bundle.transferSaltHex !== "string") return false;
3564
+ if (bundle.version === "4.0") {
3565
+ return typeof bundle.burnCommitment === "string";
3566
+ } else if (bundle.version === "5.0") {
3567
+ return typeof bundle.burnTransaction === "string" && typeof bundle.mintedTokenStateJson === "string" && typeof bundle.finalRecipientStateJson === "string" && typeof bundle.recipientAddressJson === "string";
3568
+ }
3569
+ return false;
3570
+ }
3571
+ function isInstantSplitBundleV4(obj) {
3572
+ return isInstantSplitBundle(obj) && obj.version === "4.0";
3573
+ }
3574
+ function isInstantSplitBundleV5(obj) {
3575
+ return isInstantSplitBundle(obj) && obj.version === "5.0";
3576
+ }
3577
+
3578
+ // modules/payments/InstantSplitProcessor.ts
3579
+ function fromHex3(hex) {
3580
+ const bytes = new Uint8Array(hex.length / 2);
3581
+ for (let i = 0; i < hex.length; i += 2) {
3582
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
3583
+ }
3584
+ return bytes;
3585
+ }
3586
+ var InstantSplitProcessor = class {
3587
+ client;
3588
+ trustBase;
3589
+ devMode;
3590
+ constructor(config) {
3591
+ this.client = config.stateTransitionClient;
3592
+ this.trustBase = config.trustBase;
3593
+ this.devMode = config.devMode ?? false;
3594
+ }
3595
+ /**
3596
+ * Process a received INSTANT_SPLIT bundle.
3597
+ *
3598
+ * @param bundle - The received bundle (V4 or V5)
3599
+ * @param signingService - Recipient's signing service
3600
+ * @param senderPubkey - Sender's public key (for verification)
3601
+ * @param options - Processing options
3602
+ * @returns Processing result with finalized token if successful
3603
+ */
3604
+ async processReceivedBundle(bundle, signingService, senderPubkey, options) {
3605
+ if (isInstantSplitBundleV5(bundle)) {
3606
+ return this.processV5Bundle(bundle, signingService, senderPubkey, options);
3607
+ } else if (isInstantSplitBundleV4(bundle)) {
3608
+ return this.processV4Bundle(bundle, signingService, senderPubkey, options);
3609
+ }
3610
+ return {
3611
+ success: false,
3612
+ error: `Unknown bundle version: ${bundle.version}`,
3613
+ durationMs: 0
3614
+ };
3615
+ }
3616
+ /**
3617
+ * Process a V5 bundle (production mode).
3618
+ *
3619
+ * V5 Flow:
3620
+ * 1. Burn transaction already has proof (just validate)
3621
+ * 2. Submit mint commitment -> wait for proof
3622
+ * 3. Reconstruct minted token (use sender's state from bundle)
3623
+ * 4. Submit transfer commitment -> wait for proof
3624
+ * 5. Create recipient's final state and finalize token
3625
+ */
3626
+ async processV5Bundle(bundle, signingService, senderPubkey, options) {
3627
+ console.log("[InstantSplitProcessor] Processing V5 bundle...");
3628
+ const startTime = performance.now();
3629
+ try {
3630
+ if (bundle.senderPubkey !== senderPubkey) {
3631
+ console.warn("[InstantSplitProcessor] Sender pubkey mismatch (non-fatal)");
3632
+ }
3633
+ const burnTxJson = JSON.parse(bundle.burnTransaction);
3634
+ const burnTransaction = await TransferTransaction.fromJSON(burnTxJson);
3635
+ console.log("[InstantSplitProcessor] Burn transaction validated");
3636
+ const mintDataJson = JSON.parse(bundle.recipientMintData);
3637
+ const mintData = await MintTransactionData2.fromJSON(mintDataJson);
3638
+ const mintCommitment = await MintCommitment2.create(mintData);
3639
+ console.log("[InstantSplitProcessor] Mint commitment recreated");
3640
+ const mintResponse = await this.client.submitMintCommitment(mintCommitment);
3641
+ if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
3642
+ throw new Error(`Mint submission failed: ${mintResponse.status}`);
3643
+ }
3644
+ console.log(`[InstantSplitProcessor] Mint submitted: ${mintResponse.status}`);
3645
+ const mintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(mintCommitment, options?.proofTimeoutMs) : await waitInclusionProof4(this.trustBase, this.client, mintCommitment);
3646
+ const mintTransaction = mintCommitment.toTransaction(mintProof);
3647
+ console.log("[InstantSplitProcessor] Mint proof received");
3648
+ const tokenType = new TokenType2(fromHex3(bundle.tokenTypeHex));
3649
+ const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
3650
+ const tokenJson = {
3651
+ version: "2.0",
3652
+ state: senderMintedStateJson,
3653
+ genesis: mintTransaction.toJSON(),
3654
+ transactions: [],
3655
+ nametags: []
3656
+ };
3657
+ const mintedToken = await Token4.fromJSON(tokenJson);
3658
+ console.log("[InstantSplitProcessor] Minted token reconstructed from sender state");
3659
+ const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
3660
+ const transferCommitment = await TransferCommitment3.fromJSON(transferCommitmentJson);
3661
+ const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
3662
+ if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
3663
+ throw new Error(`Transfer submission failed: ${transferResponse.status}`);
3664
+ }
3665
+ console.log(`[InstantSplitProcessor] Transfer submitted: ${transferResponse.status}`);
3666
+ const transferProof = this.devMode ? await this.waitInclusionProofWithDevBypass(transferCommitment, options?.proofTimeoutMs) : await waitInclusionProof4(this.trustBase, this.client, transferCommitment);
3667
+ const transferTransaction = transferCommitment.toTransaction(transferProof);
3668
+ console.log("[InstantSplitProcessor] Transfer proof received");
3669
+ const transferSalt = fromHex3(bundle.transferSaltHex);
3670
+ const finalRecipientPredicate = await UnmaskedPredicate4.create(
3671
+ mintData.tokenId,
3672
+ tokenType,
3673
+ signingService,
3674
+ HashAlgorithm4.SHA256,
3675
+ transferSalt
3676
+ );
3677
+ const finalRecipientState = new TokenState4(finalRecipientPredicate, null);
3678
+ console.log("[InstantSplitProcessor] Final recipient state created");
3679
+ let nametagTokens = [];
3680
+ const recipientAddressStr = bundle.recipientAddressJson;
3681
+ if (recipientAddressStr.startsWith("PROXY://")) {
3682
+ console.log("[InstantSplitProcessor] PROXY address detected, finding nametag token...");
3683
+ if (bundle.nametagTokenJson) {
3684
+ try {
3685
+ const nametagToken = await Token4.fromJSON(JSON.parse(bundle.nametagTokenJson));
3686
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
3687
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
3688
+ if (proxy.address !== recipientAddressStr) {
3689
+ console.warn("[InstantSplitProcessor] Nametag PROXY address mismatch, ignoring bundle token");
3690
+ } else {
3691
+ nametagTokens = [nametagToken];
3692
+ console.log("[InstantSplitProcessor] Using nametag token from bundle (address validated)");
3693
+ }
3694
+ } catch (err) {
3695
+ console.warn("[InstantSplitProcessor] Failed to parse nametag token from bundle:", err);
3696
+ }
3697
+ }
3698
+ if (nametagTokens.length === 0 && options?.findNametagToken) {
3699
+ const token = await options.findNametagToken(recipientAddressStr);
3700
+ if (token) {
3701
+ nametagTokens = [token];
3702
+ console.log("[InstantSplitProcessor] Found nametag token via callback");
3703
+ }
3704
+ }
3705
+ if (nametagTokens.length === 0 && !this.devMode) {
3706
+ throw new Error(
3707
+ `PROXY address transfer requires nametag token for verification. Address: ${recipientAddressStr}`
3708
+ );
3709
+ }
3710
+ }
3711
+ let finalToken;
3712
+ if (this.devMode) {
3713
+ console.log("[InstantSplitProcessor] Dev mode: finalizing without verification");
3714
+ const tokenJson2 = mintedToken.toJSON();
3715
+ tokenJson2.state = finalRecipientState.toJSON();
3716
+ tokenJson2.transactions = [transferTransaction.toJSON()];
3717
+ finalToken = await Token4.fromJSON(tokenJson2);
3718
+ } else {
3719
+ finalToken = await this.client.finalizeTransaction(
3720
+ this.trustBase,
3721
+ mintedToken,
3722
+ finalRecipientState,
3723
+ transferTransaction,
3724
+ nametagTokens
3725
+ );
3726
+ }
3727
+ console.log("[InstantSplitProcessor] Token finalized");
3728
+ if (!this.devMode) {
3729
+ const verification = await finalToken.verify(this.trustBase);
3730
+ if (!verification.isSuccessful) {
3731
+ throw new Error(`Token verification failed`);
3732
+ }
3733
+ console.log("[InstantSplitProcessor] Token verified");
3734
+ }
3735
+ const duration = performance.now() - startTime;
3736
+ console.log(`[InstantSplitProcessor] V5 bundle processed in ${duration.toFixed(0)}ms`);
3737
+ return {
3738
+ success: true,
3739
+ token: finalToken,
3740
+ durationMs: duration
3741
+ };
3742
+ } catch (error) {
3743
+ const duration = performance.now() - startTime;
3744
+ const errorMessage = error instanceof Error ? error.message : String(error);
3745
+ console.error(`[InstantSplitProcessor] V5 processing failed:`, error);
3746
+ return {
3747
+ success: false,
3748
+ error: errorMessage,
3749
+ durationMs: duration
3750
+ };
3751
+ }
3752
+ }
3753
+ /**
3754
+ * Process a V4 bundle (dev mode only).
3755
+ *
3756
+ * V4 Flow:
3757
+ * 1. Submit burn commitment -> wait for proof
3758
+ * 2. Submit mint commitment -> wait for proof
3759
+ * 3. Reconstruct minted token
3760
+ * 4. Submit transfer commitment -> wait for proof
3761
+ * 5. Finalize token
3762
+ */
3763
+ async processV4Bundle(bundle, signingService, _senderPubkey, options) {
3764
+ if (!this.devMode) {
3765
+ return {
3766
+ success: false,
3767
+ error: "INSTANT_SPLIT V4 is only supported in dev mode",
3768
+ durationMs: 0
3769
+ };
3770
+ }
3771
+ console.log("[InstantSplitProcessor] Processing V4 bundle (dev mode)...");
3772
+ const startTime = performance.now();
3773
+ try {
3774
+ const burnCommitmentJson = JSON.parse(bundle.burnCommitment);
3775
+ const burnCommitment = await TransferCommitment3.fromJSON(burnCommitmentJson);
3776
+ const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
3777
+ if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
3778
+ throw new Error(`Burn submission failed: ${burnResponse.status}`);
3779
+ }
3780
+ await this.waitInclusionProofWithDevBypass(burnCommitment, options?.proofTimeoutMs);
3781
+ console.log("[InstantSplitProcessor] V4: Burn proof received");
3782
+ const mintDataJson = JSON.parse(bundle.recipientMintData);
3783
+ const mintData = await MintTransactionData2.fromJSON(mintDataJson);
3784
+ const mintCommitment = await MintCommitment2.create(mintData);
3785
+ const mintResponse = await this.client.submitMintCommitment(mintCommitment);
3786
+ if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
3787
+ throw new Error(`Mint submission failed: ${mintResponse.status}`);
3788
+ }
3789
+ const mintProof = await this.waitInclusionProofWithDevBypass(
3790
+ mintCommitment,
3791
+ options?.proofTimeoutMs
3792
+ );
3793
+ const mintTransaction = mintCommitment.toTransaction(mintProof);
3794
+ console.log("[InstantSplitProcessor] V4: Mint proof received");
3795
+ const tokenType = new TokenType2(fromHex3(bundle.tokenTypeHex));
3796
+ const recipientSalt = fromHex3(bundle.recipientSaltHex);
3797
+ const recipientPredicate = await UnmaskedPredicate4.create(
3798
+ mintData.tokenId,
3799
+ tokenType,
3800
+ signingService,
3801
+ HashAlgorithm4.SHA256,
3802
+ recipientSalt
3803
+ );
3804
+ const recipientState = new TokenState4(recipientPredicate, null);
3805
+ const tokenJson = {
3806
+ version: "2.0",
3807
+ state: recipientState.toJSON(),
3808
+ genesis: mintTransaction.toJSON(),
3809
+ transactions: [],
3810
+ nametags: []
3811
+ };
3812
+ const mintedToken = await Token4.fromJSON(tokenJson);
3813
+ console.log("[InstantSplitProcessor] V4: Minted token reconstructed");
3814
+ const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
3815
+ const transferCommitment = await TransferCommitment3.fromJSON(transferCommitmentJson);
3816
+ const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
3817
+ if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
3818
+ throw new Error(`Transfer submission failed: ${transferResponse.status}`);
3819
+ }
3820
+ const transferProof = await this.waitInclusionProofWithDevBypass(
3821
+ transferCommitment,
3822
+ options?.proofTimeoutMs
3823
+ );
3824
+ const transferTransaction = transferCommitment.toTransaction(transferProof);
3825
+ console.log("[InstantSplitProcessor] V4: Transfer proof received");
3826
+ const transferSalt = fromHex3(bundle.transferSaltHex);
3827
+ const finalPredicate = await UnmaskedPredicate4.create(
3828
+ mintData.tokenId,
3829
+ tokenType,
3830
+ signingService,
3831
+ HashAlgorithm4.SHA256,
3832
+ transferSalt
3833
+ );
3834
+ const finalState = new TokenState4(finalPredicate, null);
3835
+ const finalTokenJson = mintedToken.toJSON();
3836
+ finalTokenJson.state = finalState.toJSON();
3837
+ finalTokenJson.transactions = [transferTransaction.toJSON()];
3838
+ const finalToken = await Token4.fromJSON(finalTokenJson);
3839
+ console.log("[InstantSplitProcessor] V4: Token finalized");
3840
+ const duration = performance.now() - startTime;
3841
+ console.log(`[InstantSplitProcessor] V4 bundle processed in ${duration.toFixed(0)}ms`);
3842
+ return {
3843
+ success: true,
3844
+ token: finalToken,
3845
+ durationMs: duration
3846
+ };
3847
+ } catch (error) {
3848
+ const duration = performance.now() - startTime;
3849
+ const errorMessage = error instanceof Error ? error.message : String(error);
3850
+ console.error(`[InstantSplitProcessor] V4 processing failed:`, error);
3851
+ return {
3852
+ success: false,
3853
+ error: errorMessage,
3854
+ durationMs: duration
3855
+ };
3856
+ }
3857
+ }
3858
+ /**
3859
+ * Dev mode bypass for waitInclusionProof.
3860
+ */
3861
+ async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
3862
+ if (this.devMode) {
3863
+ try {
3864
+ return await Promise.race([
3865
+ waitInclusionProof4(this.trustBase, this.client, commitment),
3866
+ new Promise(
3867
+ (_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
3868
+ )
3869
+ ]);
3870
+ } catch {
3871
+ console.log("[InstantSplitProcessor] Dev mode: Using mock proof");
3872
+ return {
3873
+ toJSON: () => ({ mock: true })
3874
+ };
3875
+ }
3876
+ }
3877
+ return waitInclusionProof4(this.trustBase, this.client, commitment);
3878
+ }
3879
+ };
3880
+
3881
+ // modules/payments/PaymentsModule.ts
3882
+ import { Token as SdkToken2 } from "@unicitylabs/state-transition-sdk/lib/token/Token";
3883
+ import { CoinId as CoinId4 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
3884
+ import { TransferCommitment as TransferCommitment4 } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment";
3885
+ import { TransferTransaction as TransferTransaction2 } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction";
3192
3886
  import { SigningService } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
3193
3887
  import { AddressScheme } from "@unicitylabs/state-transition-sdk/lib/address/AddressScheme";
3194
- import { UnmaskedPredicate as UnmaskedPredicate3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
3195
- import { TokenState as TokenState3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
3196
- import { HashAlgorithm as HashAlgorithm3 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
3888
+ import { UnmaskedPredicate as UnmaskedPredicate5 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
3889
+ import { TokenState as TokenState5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
3890
+ import { HashAlgorithm as HashAlgorithm5 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
3197
3891
  function enrichWithRegistry(info) {
3198
3892
  const registry = TokenRegistry.getInstance();
3199
3893
  const def = registry.getDefinition(info.coinId);
@@ -3232,7 +3926,7 @@ async function parseTokenInfo(tokenData) {
3232
3926
  if (Array.isArray(firstCoin) && firstCoin.length === 2) {
3233
3927
  [coinIdObj, amount] = firstCoin;
3234
3928
  }
3235
- if (coinIdObj instanceof CoinId3) {
3929
+ if (coinIdObj instanceof CoinId4) {
3236
3930
  const coinIdHex = coinIdObj.toJSON();
3237
3931
  return enrichWithRegistry({
3238
3932
  coinId: coinIdHex,
@@ -3365,21 +4059,48 @@ function extractStateHashFromSdkData(sdkData) {
3365
4059
  if (!sdkData) return "";
3366
4060
  try {
3367
4061
  const txf = JSON.parse(sdkData);
3368
- return getCurrentStateHash(txf) || "";
4062
+ const stateHash = getCurrentStateHash(txf);
4063
+ if (!stateHash) {
4064
+ if (txf.state?.hash) {
4065
+ return txf.state.hash;
4066
+ }
4067
+ if (txf.stateHash) {
4068
+ return txf.stateHash;
4069
+ }
4070
+ if (txf.currentStateHash) {
4071
+ return txf.currentStateHash;
4072
+ }
4073
+ }
4074
+ return stateHash || "";
3369
4075
  } catch {
3370
4076
  return "";
3371
4077
  }
3372
4078
  }
3373
- function isSameToken(t1, t2) {
3374
- if (t1.id === t2.id) return true;
4079
+ function createTokenStateKey(tokenId, stateHash) {
4080
+ return `${tokenId}_${stateHash}`;
4081
+ }
4082
+ function extractTokenStateKey(token) {
4083
+ const tokenId = extractTokenIdFromSdkData(token.sdkData);
4084
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
4085
+ if (!tokenId || !stateHash) return null;
4086
+ return createTokenStateKey(tokenId, stateHash);
4087
+ }
4088
+ function hasSameGenesisTokenId(t1, t2) {
3375
4089
  const id1 = extractTokenIdFromSdkData(t1.sdkData);
3376
4090
  const id2 = extractTokenIdFromSdkData(t2.sdkData);
3377
4091
  return !!(id1 && id2 && id1 === id2);
3378
4092
  }
4093
+ function isSameTokenState(t1, t2) {
4094
+ const key1 = extractTokenStateKey(t1);
4095
+ const key2 = extractTokenStateKey(t2);
4096
+ return !!(key1 && key2 && key1 === key2);
4097
+ }
3379
4098
  function createTombstoneFromToken(token) {
3380
4099
  const tokenId = extractTokenIdFromSdkData(token.sdkData);
3381
- if (!tokenId) return null;
3382
4100
  const stateHash = extractStateHashFromSdkData(token.sdkData);
4101
+ if (!tokenId || !stateHash) {
4102
+ return null;
4103
+ }
3383
4104
  return {
3384
4105
  tokenId,
3385
4106
  stateHash,
@@ -3445,7 +4166,7 @@ function findBestTokenVersion(tokenId, archivedTokens, forkedTokens) {
3445
4166
  candidates.sort((a, b) => countCommittedTxns(b) - countCommittedTxns(a));
3446
4167
  return candidates[0];
3447
4168
  }
3448
- var PaymentsModule = class {
4169
+ var PaymentsModule = class _PaymentsModule {
3449
4170
  moduleConfig;
3450
4171
  deps = null;
3451
4172
  /** L1 (ALPHA blockchain) payments sub-module (null if disabled) */
@@ -3470,6 +4191,13 @@ var PaymentsModule = class {
3470
4191
  unsubscribeTransfers = null;
3471
4192
  unsubscribePaymentRequests = null;
3472
4193
  unsubscribePaymentRequestResponses = null;
4194
+ // NOSTR-FIRST proof polling (background proof verification)
4195
+ proofPollingJobs = /* @__PURE__ */ new Map();
4196
+ proofPollingInterval = null;
4197
+ static PROOF_POLLING_INTERVAL_MS = 2e3;
4198
+ // Poll every 2s
4199
+ static PROOF_POLLING_MAX_ATTEMPTS = 30;
4200
+ // Max 30 attempts (~60s)
3473
4201
  constructor(config) {
3474
4202
  this.moduleConfig = {
3475
4203
  autoSync: config?.autoSync ?? true,
@@ -3485,6 +4213,8 @@ var PaymentsModule = class {
3485
4213
  getConfig() {
3486
4214
  return this.moduleConfig;
3487
4215
  }
4216
+ /** Price provider (optional) */
4217
+ priceProvider = null;
3488
4218
  log(...args) {
3489
4219
  if (this.moduleConfig.debug) {
3490
4220
  console.log("[PaymentsModule]", ...args);
@@ -3497,7 +4227,21 @@ var PaymentsModule = class {
3497
4227
  * Initialize module with dependencies
3498
4228
  */
3499
4229
  initialize(deps) {
4230
+ this.unsubscribeTransfers?.();
4231
+ this.unsubscribeTransfers = null;
4232
+ this.unsubscribePaymentRequests?.();
4233
+ this.unsubscribePaymentRequests = null;
4234
+ this.unsubscribePaymentRequestResponses?.();
4235
+ this.unsubscribePaymentRequestResponses = null;
4236
+ this.tokens.clear();
4237
+ this.pendingTransfers.clear();
4238
+ this.tombstones = [];
4239
+ this.archivedTokens.clear();
4240
+ this.forkedTokens.clear();
4241
+ this.transactionHistory = [];
4242
+ this.nametag = null;
3500
4243
  this.deps = deps;
4244
+ this.priceProvider = deps.price ?? null;
3501
4245
  if (this.l1) {
3502
4246
  this.l1.initialize({
3503
4247
  identity: deps.identity,
@@ -3568,6 +4312,8 @@ var PaymentsModule = class {
3568
4312
  this.unsubscribePaymentRequestResponses = null;
3569
4313
  this.paymentRequestHandlers.clear();
3570
4314
  this.paymentRequestResponseHandlers.clear();
4315
+ this.stopProofPolling();
4316
+ this.proofPollingJobs.clear();
3571
4317
  for (const [, resolver] of this.pendingResponseResolvers) {
3572
4318
  clearTimeout(resolver.timeout);
3573
4319
  resolver.reject(new Error("Module destroyed"));
@@ -3592,8 +4338,9 @@ var PaymentsModule = class {
3592
4338
  tokens: []
3593
4339
  };
3594
4340
  try {
3595
- const recipientPubkey = await this.resolveRecipient(request.recipient);
3596
- const recipientAddress = await this.resolveRecipientAddress(request.recipient);
4341
+ const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
4342
+ const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
4343
+ const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
3597
4344
  const signingService = await this.createSigningService();
3598
4345
  const stClient = this.deps.oracle.getStateTransitionClient?.();
3599
4346
  if (!stClient) {
@@ -3613,7 +4360,6 @@ var PaymentsModule = class {
3613
4360
  if (!splitPlan) {
3614
4361
  throw new Error("Insufficient balance");
3615
4362
  }
3616
- this.log(`Split plan: requiresSplit=${splitPlan.requiresSplit}, directTokens=${splitPlan.tokensToTransferDirectly.length}`);
3617
4363
  const tokensToSend = splitPlan.tokensToTransferDirectly.map((t) => t.uiToken);
3618
4364
  if (splitPlan.tokenToSplit) {
3619
4365
  tokensToSend.push(splitPlan.tokenToSplit.uiToken);
@@ -3656,11 +4402,13 @@ var PaymentsModule = class {
3656
4402
  };
3657
4403
  await this.addToken(changeToken, true);
3658
4404
  this.log(`Change token saved: ${changeToken.id}, amount: ${changeToken.amount}`);
4405
+ console.log(`[Payments] Sending split token to ${recipientPubkey.slice(0, 8)}... via Nostr`);
3659
4406
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
3660
4407
  sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
3661
4408
  transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
3662
4409
  memo: request.memo
3663
4410
  });
4411
+ console.log(`[Payments] Split token sent successfully`);
3664
4412
  await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
3665
4413
  result.txHash = "split-" + Date.now().toString(16);
3666
4414
  this.log(`Split transfer completed`);
@@ -3679,11 +4427,13 @@ var PaymentsModule = class {
3679
4427
  const transferTx = commitment.toTransaction(inclusionProof);
3680
4428
  const requestIdBytes = commitment.requestId;
3681
4429
  result.txHash = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
4430
+ console.log(`[Payments] Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}... via Nostr`);
3682
4431
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
3683
4432
  sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
3684
4433
  transferTx: JSON.stringify(transferTx.toJSON()),
3685
4434
  memo: request.memo
3686
4435
  });
4436
+ console.log(`[Payments] Direct token sent successfully`);
3687
4437
  this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
3688
4438
  await this.removeToken(token.id, recipientNametag);
3689
4439
  }
@@ -3737,26 +4487,255 @@ var PaymentsModule = class {
3737
4487
  return TokenRegistry.getInstance().getIconUrl(coinId) ?? void 0;
3738
4488
  }
3739
4489
  // ===========================================================================
3740
- // Public API - Payment Requests
4490
+ // Public API - Instant Split (V5 Optimized)
3741
4491
  // ===========================================================================
3742
4492
  /**
3743
- * Send a payment request to someone
3744
- * @param recipientPubkeyOrNametag - Recipient's pubkey or @nametag
3745
- * @param request - Payment request details
3746
- * @returns Result with event ID
4493
+ * Send tokens using INSTANT_SPLIT V5 optimized flow.
4494
+ *
4495
+ * This achieves ~2.3s critical path latency instead of ~42s by:
4496
+ * 1. Waiting only for burn proof (required)
4497
+ * 2. Creating transfer commitment from mint data (no mint proof needed)
4498
+ * 3. Sending bundle via Nostr immediately
4499
+ * 4. Processing mints in background
4500
+ *
4501
+ * @param request - Transfer request with recipient, amount, and coinId
4502
+ * @param options - Optional instant split configuration
4503
+ * @returns InstantSplitResult with timing info
3747
4504
  */
3748
- async sendPaymentRequest(recipientPubkeyOrNametag, request) {
4505
+ async sendInstant(request, options) {
3749
4506
  this.ensureInitialized();
3750
- if (!this.deps.transport.sendPaymentRequest) {
3751
- return {
3752
- success: false,
3753
- error: "Transport provider does not support payment requests"
3754
- };
3755
- }
4507
+ const startTime = performance.now();
3756
4508
  try {
3757
- const recipientPubkey = await this.resolveRecipient(recipientPubkeyOrNametag);
3758
- const payload = {
3759
- amount: request.amount,
4509
+ const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
4510
+ const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
4511
+ const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
4512
+ const signingService = await this.createSigningService();
4513
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
4514
+ if (!stClient) {
4515
+ throw new Error("State transition client not available");
4516
+ }
4517
+ const trustBase = this.deps.oracle.getTrustBase?.();
4518
+ if (!trustBase) {
4519
+ throw new Error("Trust base not available");
4520
+ }
4521
+ const calculator = new TokenSplitCalculator();
4522
+ const availableTokens = Array.from(this.tokens.values());
4523
+ const splitPlan = await calculator.calculateOptimalSplit(
4524
+ availableTokens,
4525
+ BigInt(request.amount),
4526
+ request.coinId
4527
+ );
4528
+ if (!splitPlan) {
4529
+ throw new Error("Insufficient balance");
4530
+ }
4531
+ if (!splitPlan.requiresSplit || !splitPlan.tokenToSplit) {
4532
+ this.log("No split required, falling back to standard send()");
4533
+ const result2 = await this.send(request);
4534
+ return {
4535
+ success: result2.status === "completed",
4536
+ criticalPathDurationMs: performance.now() - startTime,
4537
+ error: result2.error
4538
+ };
4539
+ }
4540
+ this.log(`InstantSplit: amount=${splitPlan.splitAmount}, remainder=${splitPlan.remainderAmount}`);
4541
+ const tokenToSplit = splitPlan.tokenToSplit.uiToken;
4542
+ tokenToSplit.status = "transferring";
4543
+ this.tokens.set(tokenToSplit.id, tokenToSplit);
4544
+ const devMode = options?.devMode ?? this.deps.oracle.isDevMode?.() ?? false;
4545
+ const executor = new InstantSplitExecutor({
4546
+ stateTransitionClient: stClient,
4547
+ trustBase,
4548
+ signingService,
4549
+ devMode
4550
+ });
4551
+ const result = await executor.executeSplitInstant(
4552
+ splitPlan.tokenToSplit.sdkToken,
4553
+ splitPlan.splitAmount,
4554
+ splitPlan.remainderAmount,
4555
+ splitPlan.coinId,
4556
+ recipientAddress,
4557
+ this.deps.transport,
4558
+ recipientPubkey,
4559
+ {
4560
+ ...options,
4561
+ onChangeTokenCreated: async (changeToken) => {
4562
+ const changeTokenData = changeToken.toJSON();
4563
+ const uiToken = {
4564
+ id: crypto.randomUUID(),
4565
+ coinId: request.coinId,
4566
+ symbol: this.getCoinSymbol(request.coinId),
4567
+ name: this.getCoinName(request.coinId),
4568
+ decimals: this.getCoinDecimals(request.coinId),
4569
+ iconUrl: this.getCoinIconUrl(request.coinId),
4570
+ amount: splitPlan.remainderAmount.toString(),
4571
+ status: "confirmed",
4572
+ createdAt: Date.now(),
4573
+ updatedAt: Date.now(),
4574
+ sdkData: JSON.stringify(changeTokenData)
4575
+ };
4576
+ await this.addToken(uiToken, true);
4577
+ this.log(`Change token saved via background: ${uiToken.id}`);
4578
+ },
4579
+ onStorageSync: async () => {
4580
+ await this.save();
4581
+ return true;
4582
+ }
4583
+ }
4584
+ );
4585
+ if (result.success) {
4586
+ const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4587
+ await this.removeToken(tokenToSplit.id, recipientNametag);
4588
+ await this.addToHistory({
4589
+ type: "SENT",
4590
+ amount: request.amount,
4591
+ coinId: request.coinId,
4592
+ symbol: this.getCoinSymbol(request.coinId),
4593
+ timestamp: Date.now(),
4594
+ recipientNametag
4595
+ });
4596
+ await this.save();
4597
+ } else {
4598
+ tokenToSplit.status = "confirmed";
4599
+ this.tokens.set(tokenToSplit.id, tokenToSplit);
4600
+ }
4601
+ return result;
4602
+ } catch (error) {
4603
+ const errorMessage = error instanceof Error ? error.message : String(error);
4604
+ return {
4605
+ success: false,
4606
+ criticalPathDurationMs: performance.now() - startTime,
4607
+ error: errorMessage
4608
+ };
4609
+ }
4610
+ }
4611
+ /**
4612
+ * Process a received INSTANT_SPLIT bundle.
4613
+ *
4614
+ * This should be called when receiving an instant split bundle via transport.
4615
+ * It handles the recipient-side processing:
4616
+ * 1. Validate burn transaction
4617
+ * 2. Submit and wait for mint proof
4618
+ * 3. Submit and wait for transfer proof
4619
+ * 4. Finalize and save the token
4620
+ *
4621
+ * @param bundle - The received InstantSplitBundle (V4 or V5)
4622
+ * @param senderPubkey - Sender's public key for verification
4623
+ * @returns Processing result with finalized token
4624
+ */
4625
+ async processInstantSplitBundle(bundle, senderPubkey) {
4626
+ this.ensureInitialized();
4627
+ try {
4628
+ const signingService = await this.createSigningService();
4629
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
4630
+ if (!stClient) {
4631
+ throw new Error("State transition client not available");
4632
+ }
4633
+ const trustBase = this.deps.oracle.getTrustBase?.();
4634
+ if (!trustBase) {
4635
+ throw new Error("Trust base not available");
4636
+ }
4637
+ const devMode = this.deps.oracle.isDevMode?.() ?? false;
4638
+ const processor = new InstantSplitProcessor({
4639
+ stateTransitionClient: stClient,
4640
+ trustBase,
4641
+ devMode
4642
+ });
4643
+ const result = await processor.processReceivedBundle(
4644
+ bundle,
4645
+ signingService,
4646
+ senderPubkey,
4647
+ {
4648
+ findNametagToken: async (proxyAddress) => {
4649
+ if (this.nametag?.token) {
4650
+ try {
4651
+ const nametagToken = await SdkToken2.fromJSON(this.nametag.token);
4652
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
4653
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
4654
+ if (proxy.address === proxyAddress) {
4655
+ return nametagToken;
4656
+ }
4657
+ this.log(`Nametag PROXY address mismatch: ${proxy.address} !== ${proxyAddress}`);
4658
+ return null;
4659
+ } catch (err) {
4660
+ this.log("Failed to parse nametag token:", err);
4661
+ return null;
4662
+ }
4663
+ }
4664
+ return null;
4665
+ }
4666
+ }
4667
+ );
4668
+ if (result.success && result.token) {
4669
+ const tokenData = result.token.toJSON();
4670
+ const info = await parseTokenInfo(tokenData);
4671
+ const uiToken = {
4672
+ id: crypto.randomUUID(),
4673
+ coinId: info.coinId,
4674
+ symbol: info.symbol,
4675
+ name: info.name,
4676
+ decimals: info.decimals,
4677
+ iconUrl: info.iconUrl,
4678
+ amount: bundle.amount,
4679
+ status: "confirmed",
4680
+ createdAt: Date.now(),
4681
+ updatedAt: Date.now(),
4682
+ sdkData: JSON.stringify(tokenData)
4683
+ };
4684
+ await this.addToken(uiToken);
4685
+ await this.addToHistory({
4686
+ type: "RECEIVED",
4687
+ amount: bundle.amount,
4688
+ coinId: info.coinId,
4689
+ symbol: info.symbol,
4690
+ timestamp: Date.now(),
4691
+ senderPubkey
4692
+ });
4693
+ await this.save();
4694
+ this.deps.emitEvent("transfer:incoming", {
4695
+ id: bundle.splitGroupId,
4696
+ senderPubkey,
4697
+ tokens: [uiToken],
4698
+ receivedAt: Date.now()
4699
+ });
4700
+ }
4701
+ return result;
4702
+ } catch (error) {
4703
+ const errorMessage = error instanceof Error ? error.message : String(error);
4704
+ return {
4705
+ success: false,
4706
+ error: errorMessage,
4707
+ durationMs: 0
4708
+ };
4709
+ }
4710
+ }
4711
+ /**
4712
+ * Check if a payload is an instant split bundle
4713
+ */
4714
+ isInstantSplitBundle(payload) {
4715
+ return isInstantSplitBundle(payload);
4716
+ }
4717
+ // ===========================================================================
4718
+ // Public API - Payment Requests
4719
+ // ===========================================================================
4720
+ /**
4721
+ * Send a payment request to someone
4722
+ * @param recipientPubkeyOrNametag - Recipient's pubkey or @nametag
4723
+ * @param request - Payment request details
4724
+ * @returns Result with event ID
4725
+ */
4726
+ async sendPaymentRequest(recipientPubkeyOrNametag, request) {
4727
+ this.ensureInitialized();
4728
+ if (!this.deps.transport.sendPaymentRequest) {
4729
+ return {
4730
+ success: false,
4731
+ error: "Transport provider does not support payment requests"
4732
+ };
4733
+ }
4734
+ try {
4735
+ const peerInfo = await this.deps.transport.resolve?.(recipientPubkeyOrNametag) ?? null;
4736
+ const recipientPubkey = this.resolveTransportPubkey(recipientPubkeyOrNametag, peerInfo);
4737
+ const payload = {
4738
+ amount: request.amount,
3760
4739
  coinId: request.coinId,
3761
4740
  message: request.message,
3762
4741
  recipientNametag: request.recipientNametag,
@@ -4058,47 +5037,46 @@ var PaymentsModule = class {
4058
5037
  // Public API - Balance & Tokens
4059
5038
  // ===========================================================================
4060
5039
  /**
4061
- * Get balance for coin type
5040
+ * Set or update price provider
4062
5041
  */
4063
- getBalance(coinId) {
4064
- const balances = /* @__PURE__ */ new Map();
4065
- for (const token of this.tokens.values()) {
4066
- if (token.status !== "confirmed") continue;
4067
- if (coinId && token.coinId !== coinId) continue;
4068
- const key = token.coinId;
4069
- const existing = balances.get(key);
4070
- if (existing) {
4071
- existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
4072
- existing.tokenCount++;
4073
- } else {
4074
- balances.set(key, {
4075
- coinId: token.coinId,
4076
- symbol: token.symbol,
4077
- name: token.name,
4078
- totalAmount: token.amount,
4079
- tokenCount: 1,
4080
- decimals: 8
4081
- });
5042
+ setPriceProvider(provider) {
5043
+ this.priceProvider = provider;
5044
+ }
5045
+ /**
5046
+ * Get total portfolio value in USD
5047
+ * Returns null if PriceProvider is not configured
5048
+ */
5049
+ async getBalance() {
5050
+ const assets = await this.getAssets();
5051
+ if (!this.priceProvider) {
5052
+ return null;
5053
+ }
5054
+ let total = 0;
5055
+ let hasAnyPrice = false;
5056
+ for (const asset of assets) {
5057
+ if (asset.fiatValueUsd != null) {
5058
+ total += asset.fiatValueUsd;
5059
+ hasAnyPrice = true;
4082
5060
  }
4083
5061
  }
4084
- return Array.from(balances.values());
5062
+ return hasAnyPrice ? total : null;
4085
5063
  }
4086
5064
  /**
4087
- * Get aggregated assets (tokens grouped by coinId)
5065
+ * Get aggregated assets (tokens grouped by coinId) with price data
4088
5066
  * Only includes confirmed tokens
4089
5067
  */
4090
- getAssets(coinId) {
4091
- const assets = /* @__PURE__ */ new Map();
5068
+ async getAssets(coinId) {
5069
+ const assetsMap = /* @__PURE__ */ new Map();
4092
5070
  for (const token of this.tokens.values()) {
4093
5071
  if (token.status !== "confirmed") continue;
4094
5072
  if (coinId && token.coinId !== coinId) continue;
4095
5073
  const key = token.coinId;
4096
- const existing = assets.get(key);
5074
+ const existing = assetsMap.get(key);
4097
5075
  if (existing) {
4098
5076
  existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
4099
5077
  existing.tokenCount++;
4100
5078
  } else {
4101
- assets.set(key, {
5079
+ assetsMap.set(key, {
4102
5080
  coinId: token.coinId,
4103
5081
  symbol: token.symbol,
4104
5082
  name: token.name,
@@ -4109,7 +5087,66 @@ var PaymentsModule = class {
4109
5087
  });
4110
5088
  }
4111
5089
  }
4112
- return Array.from(assets.values());
5090
+ const rawAssets = Array.from(assetsMap.values());
5091
+ let priceMap = null;
5092
+ if (this.priceProvider && rawAssets.length > 0) {
5093
+ const registry = TokenRegistry.getInstance();
5094
+ const nameToCoins = /* @__PURE__ */ new Map();
5095
+ for (const asset of rawAssets) {
5096
+ const def = registry.getDefinition(asset.coinId);
5097
+ if (def?.name) {
5098
+ const existing = nameToCoins.get(def.name);
5099
+ if (existing) {
5100
+ existing.push(asset.coinId);
5101
+ } else {
5102
+ nameToCoins.set(def.name, [asset.coinId]);
5103
+ }
5104
+ }
5105
+ }
5106
+ if (nameToCoins.size > 0) {
5107
+ const tokenNames = Array.from(nameToCoins.keys());
5108
+ const prices = await this.priceProvider.getPrices(tokenNames);
5109
+ priceMap = /* @__PURE__ */ new Map();
5110
+ for (const [name, coinIds] of nameToCoins) {
5111
+ const price = prices.get(name);
5112
+ if (price) {
5113
+ for (const cid of coinIds) {
5114
+ priceMap.set(cid, {
5115
+ priceUsd: price.priceUsd,
5116
+ priceEur: price.priceEur,
5117
+ change24h: price.change24h
5118
+ });
5119
+ }
5120
+ }
5121
+ }
5122
+ }
5123
+ }
5124
+ return rawAssets.map((raw) => {
5125
+ const price = priceMap?.get(raw.coinId);
5126
+ let fiatValueUsd = null;
5127
+ let fiatValueEur = null;
5128
+ if (price) {
5129
+ const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
5130
+ fiatValueUsd = humanAmount * price.priceUsd;
5131
+ if (price.priceEur != null) {
5132
+ fiatValueEur = humanAmount * price.priceEur;
5133
+ }
5134
+ }
5135
+ return {
5136
+ coinId: raw.coinId,
5137
+ symbol: raw.symbol,
5138
+ name: raw.name,
5139
+ decimals: raw.decimals,
5140
+ iconUrl: raw.iconUrl,
5141
+ totalAmount: raw.totalAmount,
5142
+ tokenCount: raw.tokenCount,
5143
+ priceUsd: price?.priceUsd ?? null,
5144
+ priceEur: price?.priceEur ?? null,
5145
+ change24h: price?.change24h ?? null,
5146
+ fiatValueUsd,
5147
+ fiatValueEur
5148
+ };
5149
+ });
4113
5150
  }
4114
5151
  /**
4115
5152
  * Get all tokens
@@ -4135,14 +5172,52 @@ var PaymentsModule = class {
4135
5172
  // ===========================================================================
4136
5173
  /**
4137
5174
  * Add a token
4138
- * @returns false if duplicate
5175
+ * Tokens are uniquely identified by (tokenId, stateHash) composite key.
5176
+ * Multiple historic states of the same token can coexist.
5177
+ * @returns false if exact duplicate (same tokenId AND same stateHash)
4139
5178
  */
4140
5179
  async addToken(token, skipHistory = false) {
4141
5180
  this.ensureInitialized();
4142
- for (const existing of this.tokens.values()) {
4143
- if (isSameToken(existing, token)) {
4144
- this.log(`Duplicate token detected: ${token.id}`);
4145
- return false;
5181
+ const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
5182
+ const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
5183
+ const incomingStateKey = incomingTokenId && incomingStateHash ? createTokenStateKey(incomingTokenId, incomingStateHash) : null;
5184
+ if (incomingTokenId && incomingStateHash && this.isStateTombstoned(incomingTokenId, incomingStateHash)) {
5185
+ this.log(`Rejecting tombstoned token: ${incomingTokenId.slice(0, 8)}..._${incomingStateHash.slice(0, 8)}...`);
5186
+ return false;
5187
+ }
5188
+ if (incomingStateKey) {
5189
+ for (const [existingId, existing] of this.tokens) {
5190
+ if (isSameTokenState(existing, token)) {
5191
+ this.log(`Duplicate token state ignored: ${incomingTokenId?.slice(0, 8)}..._${incomingStateHash?.slice(0, 8)}...`);
5192
+ return false;
5193
+ }
5194
+ }
5195
+ }
5196
+ for (const [existingId, existing] of this.tokens) {
5197
+ if (hasSameGenesisTokenId(existing, token)) {
5198
+ const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
5199
+ if (incomingStateHash && existingStateHash && incomingStateHash === existingStateHash) {
5200
+ continue;
5201
+ }
5202
+ if (existing.status === "spent" || existing.status === "invalid") {
5203
+ this.log(`Replacing spent/invalid token ${incomingTokenId?.slice(0, 8)}...`);
5204
+ this.tokens.delete(existingId);
5205
+ break;
5206
+ }
5207
+ if (incomingStateHash && existingStateHash && incomingStateHash !== existingStateHash) {
5208
+ this.log(`Token ${incomingTokenId?.slice(0, 8)}... state updated: ${existingStateHash.slice(0, 8)}... -> ${incomingStateHash.slice(0, 8)}...`);
5209
+ await this.archiveToken(existing);
5210
+ this.tokens.delete(existingId);
5211
+ break;
5212
+ }
5213
+ if (!incomingStateHash || !existingStateHash) {
5214
+ if (existingId !== token.id) {
5215
+ this.log(`Token ${incomingTokenId?.slice(0, 8)}... .id changed, replacing`);
5216
+ await this.archiveToken(existing);
5217
+ this.tokens.delete(existingId);
5218
+ break;
5219
+ }
5220
+ }
4146
5221
  }
4147
5222
  }
4148
5223
  this.tokens.set(token.id, token);
@@ -4297,8 +5372,10 @@ var PaymentsModule = class {
4297
5372
  );
4298
5373
  if (!alreadyTombstoned) {
4299
5374
  this.tombstones.push(tombstone);
4300
- this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}...`);
5375
+ this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}..._${tombstone.stateHash.slice(0, 8)}...`);
4301
5376
  }
5377
+ } else {
5378
+ this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
4302
5379
  }
4303
5380
  this.tokens.delete(tokenId);
4304
5381
  if (!skipHistory && token.coinId && token.amount) {
@@ -4616,15 +5693,15 @@ var PaymentsModule = class {
4616
5693
  }
4617
5694
  try {
4618
5695
  const signingService = await this.createSigningService();
4619
- const { UnmaskedPredicateReference: UnmaskedPredicateReference3 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
4620
- const { TokenType: TokenType3 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
5696
+ const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
5697
+ const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
4621
5698
  const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
4622
- const tokenType = new TokenType3(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
4623
- const addressRef = await UnmaskedPredicateReference3.create(
5699
+ const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
5700
+ const addressRef = await UnmaskedPredicateReference4.create(
4624
5701
  tokenType,
4625
5702
  signingService.algorithm,
4626
5703
  signingService.publicKey,
4627
- HashAlgorithm3.SHA256
5704
+ HashAlgorithm5.SHA256
4628
5705
  );
4629
5706
  const ownerAddress = await addressRef.toAddress();
4630
5707
  const minter = new NametagMinter({
@@ -4791,40 +5868,22 @@ var PaymentsModule = class {
4791
5868
  * Detect if a string is an L3 address (not a nametag)
4792
5869
  * Returns true for: hex pubkeys (64+ chars), PROXY:, DIRECT: prefixed addresses
4793
5870
  */
4794
- isL3Address(value) {
4795
- if (value.startsWith("PROXY:") || value.startsWith("DIRECT:")) {
4796
- return true;
4797
- }
4798
- if (value.length >= 64 && /^[0-9a-fA-F]+$/.test(value)) {
4799
- return true;
4800
- }
4801
- return false;
4802
- }
4803
5871
  /**
4804
- * Resolve recipient to Nostr pubkey for messaging
4805
- * Supports: nametag (with or without @), hex pubkey
5872
+ * Resolve recipient to transport pubkey for messaging.
5873
+ * Uses pre-resolved PeerInfo if available, otherwise resolves via transport.
4806
5874
  */
4807
- async resolveRecipient(recipient) {
4808
- if (recipient.startsWith("@")) {
4809
- const nametag = recipient.slice(1);
4810
- const pubkey = await this.deps.transport.resolveNametag?.(nametag);
4811
- if (!pubkey) {
4812
- throw new Error(`Nametag not found: ${nametag}`);
4813
- }
4814
- return pubkey;
5875
+ resolveTransportPubkey(recipient, peerInfo) {
5876
+ if (peerInfo?.transportPubkey) {
5877
+ return peerInfo.transportPubkey;
4815
5878
  }
4816
- if (this.isL3Address(recipient)) {
4817
- return recipient;
4818
- }
4819
- if (this.deps?.transport.resolveNametag) {
4820
- const pubkey = await this.deps.transport.resolveNametag(recipient);
4821
- if (pubkey) {
4822
- this.log(`Resolved "${recipient}" as nametag to pubkey`);
4823
- return pubkey;
5879
+ if (recipient.length >= 64 && /^[0-9a-fA-F]+$/.test(recipient)) {
5880
+ if (recipient.length === 66 && (recipient.startsWith("02") || recipient.startsWith("03"))) {
5881
+ return recipient.slice(2);
4824
5882
  }
5883
+ return recipient;
4825
5884
  }
4826
5885
  throw new Error(
4827
- `Recipient "${recipient}" is not a valid nametag or address. Use @nametag for explicit nametag or a valid hex pubkey/PROXY:/DIRECT: address.`
5886
+ `Cannot resolve transport pubkey for "${recipient}". No binding event found. The recipient must publish their identity first.`
4828
5887
  );
4829
5888
  }
4830
5889
  /**
@@ -4834,7 +5893,7 @@ var PaymentsModule = class {
4834
5893
  const tokenData = token.sdkData ? typeof token.sdkData === "string" ? JSON.parse(token.sdkData) : token.sdkData : token;
4835
5894
  const sdkToken = await SdkToken2.fromJSON(tokenData);
4836
5895
  const salt = crypto.getRandomValues(new Uint8Array(32));
4837
- const commitment = await TransferCommitment2.create(
5896
+ const commitment = await TransferCommitment4.create(
4838
5897
  sdkToken,
4839
5898
  recipientAddress,
4840
5899
  salt,
@@ -4860,75 +5919,264 @@ var PaymentsModule = class {
4860
5919
  * Create DirectAddress from a public key using UnmaskedPredicateReference
4861
5920
  */
4862
5921
  async createDirectAddressFromPubkey(pubkeyHex) {
4863
- const { UnmaskedPredicateReference: UnmaskedPredicateReference3 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
4864
- const { TokenType: TokenType3 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
5922
+ const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
5923
+ const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
4865
5924
  const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
4866
- const tokenType = new TokenType3(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
5925
+ const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
4867
5926
  const pubkeyBytes = new Uint8Array(
4868
5927
  pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
4869
5928
  );
4870
- const addressRef = await UnmaskedPredicateReference3.create(
5929
+ const addressRef = await UnmaskedPredicateReference4.create(
4871
5930
  tokenType,
4872
5931
  "secp256k1",
4873
5932
  pubkeyBytes,
4874
- HashAlgorithm3.SHA256
5933
+ HashAlgorithm5.SHA256
4875
5934
  );
4876
5935
  return addressRef.toAddress();
4877
5936
  }
4878
5937
  /**
4879
- * Resolve nametag to 33-byte compressed public key using resolveNametagInfo
4880
- * Returns null if nametag not found or publicKey not available
5938
+ * Resolve recipient to IAddress for L3 transfers.
5939
+ * Uses pre-resolved PeerInfo when available to avoid redundant network queries.
4881
5940
  */
4882
- async resolveNametagToPublicKey(nametag) {
4883
- if (!this.deps?.transport.resolveNametagInfo) {
4884
- this.log("resolveNametagInfo not available on transport");
4885
- return null;
5941
+ async resolveRecipientAddress(recipient, addressMode = "auto", peerInfo) {
5942
+ const { AddressFactory } = await import("@unicitylabs/state-transition-sdk/lib/address/AddressFactory");
5943
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
5944
+ if (recipient.startsWith("PROXY:") || recipient.startsWith("DIRECT:")) {
5945
+ return AddressFactory.createAddress(recipient);
4886
5946
  }
4887
- const info = await this.deps.transport.resolveNametagInfo(nametag);
5947
+ if (recipient.length === 66 && /^[0-9a-fA-F]+$/.test(recipient)) {
5948
+ this.log(`Creating DirectAddress from 33-byte compressed pubkey`);
5949
+ return this.createDirectAddressFromPubkey(recipient);
5950
+ }
5951
+ const info = peerInfo ?? await this.deps?.transport.resolve?.(recipient) ?? null;
4888
5952
  if (!info) {
4889
- this.log(`Nametag "${nametag}" not found`);
4890
- return null;
5953
+ throw new Error(
5954
+ `Recipient "${recipient}" not found. Use @nametag, a valid PROXY:/DIRECT: address, or a 33-byte hex pubkey.`
5955
+ );
4891
5956
  }
4892
- if (!info.chainPubkey) {
4893
- this.log(`Nametag "${nametag}" has no 33-byte chainPubkey (legacy event)`);
4894
- return null;
5957
+ const nametag = recipient.startsWith("@") ? recipient.slice(1) : info.nametag || recipient;
5958
+ if (addressMode === "proxy") {
5959
+ console.log(`[Payments] Using PROXY address for "${nametag}" (forced)`);
5960
+ return ProxyAddress.fromNameTag(nametag);
4895
5961
  }
4896
- return info.chainPubkey;
5962
+ if (addressMode === "direct") {
5963
+ if (!info.directAddress) {
5964
+ throw new Error(`"${nametag}" has no DirectAddress stored. It may be a legacy registration.`);
5965
+ }
5966
+ console.log(`[Payments] Using DirectAddress for "${nametag}" (forced): ${info.directAddress.slice(0, 30)}...`);
5967
+ return AddressFactory.createAddress(info.directAddress);
5968
+ }
5969
+ if (info.directAddress) {
5970
+ this.log(`Using DirectAddress for "${nametag}": ${info.directAddress.slice(0, 30)}...`);
5971
+ return AddressFactory.createAddress(info.directAddress);
5972
+ }
5973
+ this.log(`Using PROXY address for legacy nametag "${nametag}"`);
5974
+ return ProxyAddress.fromNameTag(nametag);
4897
5975
  }
4898
5976
  /**
4899
- * Resolve recipient to IAddress for L3 transfers
4900
- * Supports: nametag (with or without @), PROXY:, DIRECT:, hex pubkey
5977
+ * Handle NOSTR-FIRST commitment-only transfer (recipient side)
5978
+ * This is called when receiving a transfer with only commitmentData and no proof yet.
5979
+ * We create the token as 'submitted', submit commitment (idempotent), and poll for proof.
4901
5980
  */
4902
- async resolveRecipientAddress(recipient) {
4903
- const { AddressFactory } = await import("@unicitylabs/state-transition-sdk/lib/address/AddressFactory");
4904
- if (recipient.startsWith("@")) {
4905
- const nametag = recipient.slice(1);
4906
- const publicKey2 = await this.resolveNametagToPublicKey(nametag);
4907
- if (publicKey2) {
4908
- this.log(`Resolved @${nametag} to 33-byte publicKey for DirectAddress`);
4909
- return this.createDirectAddressFromPubkey(publicKey2);
5981
+ async handleCommitmentOnlyTransfer(transfer, payload) {
5982
+ try {
5983
+ const sourceTokenInput = typeof payload.sourceToken === "string" ? JSON.parse(payload.sourceToken) : payload.sourceToken;
5984
+ const commitmentInput = typeof payload.commitmentData === "string" ? JSON.parse(payload.commitmentData) : payload.commitmentData;
5985
+ if (!sourceTokenInput || !commitmentInput) {
5986
+ console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
5987
+ return;
4910
5988
  }
4911
- throw new Error(`Nametag "${nametag}" not found or missing publicKey`);
4912
- }
4913
- if (recipient.startsWith("PROXY:") || recipient.startsWith("DIRECT:")) {
4914
- return AddressFactory.createAddress(recipient);
4915
- }
4916
- if (recipient.length === 66 && /^[0-9a-fA-F]+$/.test(recipient)) {
4917
- this.log(`Creating DirectAddress from 33-byte compressed pubkey`);
4918
- return this.createDirectAddressFromPubkey(recipient);
5989
+ const tokenInfo = await parseTokenInfo(sourceTokenInput);
5990
+ const token = {
5991
+ id: tokenInfo.tokenId ?? crypto.randomUUID(),
5992
+ coinId: tokenInfo.coinId,
5993
+ symbol: tokenInfo.symbol,
5994
+ name: tokenInfo.name,
5995
+ decimals: tokenInfo.decimals,
5996
+ iconUrl: tokenInfo.iconUrl,
5997
+ amount: tokenInfo.amount,
5998
+ status: "submitted",
5999
+ // NOSTR-FIRST: unconfirmed until proof
6000
+ createdAt: Date.now(),
6001
+ updatedAt: Date.now(),
6002
+ sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
6003
+ };
6004
+ const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
6005
+ const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
6006
+ if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
6007
+ this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
6008
+ return;
6009
+ }
6010
+ this.tokens.set(token.id, token);
6011
+ await this.save();
6012
+ this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
6013
+ const incomingTransfer = {
6014
+ id: transfer.id,
6015
+ senderPubkey: transfer.senderTransportPubkey,
6016
+ tokens: [token],
6017
+ memo: payload.memo,
6018
+ receivedAt: transfer.timestamp
6019
+ };
6020
+ this.deps.emitEvent("transfer:incoming", incomingTransfer);
6021
+ try {
6022
+ const commitment = await TransferCommitment4.fromJSON(commitmentInput);
6023
+ const requestIdBytes = commitment.requestId;
6024
+ const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
6025
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6026
+ if (stClient) {
6027
+ const response = await stClient.submitTransferCommitment(commitment);
6028
+ this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
6029
+ }
6030
+ this.addProofPollingJob({
6031
+ tokenId: token.id,
6032
+ requestIdHex,
6033
+ commitmentJson: JSON.stringify(commitmentInput),
6034
+ startedAt: Date.now(),
6035
+ attemptCount: 0,
6036
+ lastAttemptAt: 0,
6037
+ onProofReceived: async (tokenId) => {
6038
+ await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
6039
+ }
6040
+ });
6041
+ } catch (err) {
6042
+ console.error("[Payments] Failed to parse commitment for proof polling:", err);
6043
+ }
6044
+ } catch (error) {
6045
+ console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
4919
6046
  }
4920
- const publicKey = await this.resolveNametagToPublicKey(recipient);
4921
- if (publicKey) {
4922
- this.log(`Resolved "${recipient}" as nametag to 33-byte publicKey for DirectAddress`);
4923
- return this.createDirectAddressFromPubkey(publicKey);
6047
+ }
6048
+ /**
6049
+ * Shared finalization logic for received transfers.
6050
+ * Handles both PROXY (with nametag token + address validation) and DIRECT schemes.
6051
+ */
6052
+ async finalizeTransferToken(sourceToken, transferTx, stClient, trustBase) {
6053
+ const recipientAddress = transferTx.data.recipient;
6054
+ const addressScheme = recipientAddress.scheme;
6055
+ const signingService = await this.createSigningService();
6056
+ const transferSalt = transferTx.data.salt;
6057
+ const recipientPredicate = await UnmaskedPredicate5.create(
6058
+ sourceToken.id,
6059
+ sourceToken.type,
6060
+ signingService,
6061
+ HashAlgorithm5.SHA256,
6062
+ transferSalt
6063
+ );
6064
+ const recipientState = new TokenState5(recipientPredicate, null);
6065
+ let nametagTokens = [];
6066
+ if (addressScheme === AddressScheme.PROXY) {
6067
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
6068
+ if (!this.nametag?.token) {
6069
+ throw new Error("Cannot finalize PROXY transfer - no nametag token");
6070
+ }
6071
+ const nametagToken = await SdkToken2.fromJSON(this.nametag.token);
6072
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
6073
+ if (proxy.address !== recipientAddress.address) {
6074
+ throw new Error(
6075
+ `PROXY address mismatch: nametag resolves to ${proxy.address} but transfer targets ${recipientAddress.address}`
6076
+ );
6077
+ }
6078
+ nametagTokens = [nametagToken];
4924
6079
  }
4925
- throw new Error(
4926
- `Recipient "${recipient}" is not a valid nametag or L3 address. Use @nametag for explicit nametag or a valid 33-byte hex pubkey/PROXY:/DIRECT: address.`
6080
+ return stClient.finalizeTransaction(
6081
+ trustBase,
6082
+ sourceToken,
6083
+ recipientState,
6084
+ transferTx,
6085
+ nametagTokens
4927
6086
  );
4928
6087
  }
6088
+ /**
6089
+ * Finalize a received token after proof is available
6090
+ */
6091
+ async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
6092
+ try {
6093
+ const token = this.tokens.get(tokenId);
6094
+ if (!token) {
6095
+ this.log(`Token ${tokenId} not found for finalization`);
6096
+ return;
6097
+ }
6098
+ const commitment = await TransferCommitment4.fromJSON(commitmentInput);
6099
+ if (!this.deps.oracle.waitForProofSdk) {
6100
+ this.log("Cannot finalize - no waitForProofSdk");
6101
+ token.status = "confirmed";
6102
+ token.updatedAt = Date.now();
6103
+ await this.save();
6104
+ return;
6105
+ }
6106
+ const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
6107
+ const transferTx = commitment.toTransaction(inclusionProof);
6108
+ const sourceToken = await SdkToken2.fromJSON(sourceTokenInput);
6109
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6110
+ const trustBase = this.deps.oracle.getTrustBase?.();
6111
+ if (!stClient || !trustBase) {
6112
+ this.log("Cannot finalize - missing state transition client or trust base");
6113
+ token.status = "confirmed";
6114
+ token.updatedAt = Date.now();
6115
+ await this.save();
6116
+ return;
6117
+ }
6118
+ const finalizedSdkToken = await this.finalizeTransferToken(
6119
+ sourceToken,
6120
+ transferTx,
6121
+ stClient,
6122
+ trustBase
6123
+ );
6124
+ const finalizedToken = {
6125
+ ...token,
6126
+ status: "confirmed",
6127
+ updatedAt: Date.now(),
6128
+ sdkData: JSON.stringify(finalizedSdkToken.toJSON())
6129
+ };
6130
+ this.tokens.set(tokenId, finalizedToken);
6131
+ await this.save();
6132
+ await this.saveTokenToFileStorage(finalizedToken);
6133
+ this.log(`NOSTR-FIRST: Token ${tokenId.slice(0, 8)}... finalized and confirmed`);
6134
+ this.deps.emitEvent("transfer:confirmed", {
6135
+ id: crypto.randomUUID(),
6136
+ status: "completed",
6137
+ tokens: [finalizedToken]
6138
+ });
6139
+ await this.addToHistory({
6140
+ type: "RECEIVED",
6141
+ amount: finalizedToken.amount,
6142
+ coinId: finalizedToken.coinId,
6143
+ symbol: finalizedToken.symbol,
6144
+ timestamp: Date.now(),
6145
+ senderPubkey
6146
+ });
6147
+ } catch (error) {
6148
+ console.error("[Payments] Failed to finalize received token:", error);
6149
+ const token = this.tokens.get(tokenId);
6150
+ if (token && token.status === "submitted") {
6151
+ token.status = "confirmed";
6152
+ token.updatedAt = Date.now();
6153
+ await this.save();
6154
+ }
6155
+ }
6156
+ }
4929
6157
  async handleIncomingTransfer(transfer) {
4930
6158
  try {
4931
6159
  const payload = transfer.payload;
6160
+ if (isInstantSplitBundle(payload)) {
6161
+ this.log("Processing INSTANT_SPLIT bundle...");
6162
+ try {
6163
+ if (!this.nametag) {
6164
+ await this.loadNametagFromFileStorage();
6165
+ }
6166
+ const result = await this.processInstantSplitBundle(
6167
+ payload,
6168
+ transfer.senderTransportPubkey
6169
+ );
6170
+ if (result.success) {
6171
+ this.log("INSTANT_SPLIT processed successfully");
6172
+ } else {
6173
+ console.warn("[Payments] INSTANT_SPLIT processing failed:", result.error);
6174
+ }
6175
+ } catch (err) {
6176
+ console.error("[Payments] INSTANT_SPLIT processing error:", err);
6177
+ }
6178
+ return;
6179
+ }
4932
6180
  let tokenData;
4933
6181
  let finalizedSdkToken = null;
4934
6182
  if (payload.sourceToken && payload.transferTx) {
@@ -4939,82 +6187,71 @@ var PaymentsModule = class {
4939
6187
  console.warn("[Payments] Invalid Sphere wallet transfer format");
4940
6188
  return;
4941
6189
  }
4942
- const sourceToken = await SdkToken2.fromJSON(sourceTokenInput);
4943
- const transferTx = await TransferTransaction.fromJSON(transferTxInput);
4944
- const recipientAddress = transferTx.data.recipient;
4945
- const addressScheme = recipientAddress.scheme;
4946
- if (addressScheme === AddressScheme.PROXY) {
4947
- if (!this.nametag?.token) {
4948
- console.error("[Payments] Cannot finalize PROXY transfer - no nametag token. Token rejected.");
4949
- return;
4950
- }
4951
- {
4952
- try {
4953
- const nametagToken = await SdkToken2.fromJSON(this.nametag.token);
4954
- const signingService = await this.createSigningService();
4955
- const transferSalt = transferTx.data.salt;
4956
- const recipientPredicate = await UnmaskedPredicate3.create(
4957
- sourceToken.id,
4958
- sourceToken.type,
4959
- signingService,
4960
- HashAlgorithm3.SHA256,
4961
- transferSalt
4962
- );
4963
- const recipientState = new TokenState3(recipientPredicate, null);
4964
- const stClient = this.deps.oracle.getStateTransitionClient?.();
4965
- const trustBase = this.deps.oracle.getTrustBase?.();
4966
- if (!stClient || !trustBase) {
4967
- console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
4968
- return;
4969
- }
4970
- finalizedSdkToken = await stClient.finalizeTransaction(
4971
- trustBase,
4972
- sourceToken,
4973
- recipientState,
4974
- transferTx,
4975
- [nametagToken]
4976
- );
4977
- tokenData = finalizedSdkToken.toJSON();
4978
- this.log("Token finalized successfully");
4979
- } catch (finalizeError) {
4980
- console.error("[Payments] Finalization failed:", finalizeError);
6190
+ let sourceToken;
6191
+ let transferTx;
6192
+ try {
6193
+ sourceToken = await SdkToken2.fromJSON(sourceTokenInput);
6194
+ } catch (err) {
6195
+ console.error("[Payments] Failed to parse sourceToken:", err);
6196
+ return;
6197
+ }
6198
+ try {
6199
+ const hasInclusionProof = transferTxInput.inclusionProof !== void 0;
6200
+ const hasData = transferTxInput.data !== void 0;
6201
+ const hasTransactionData = transferTxInput.transactionData !== void 0;
6202
+ const hasAuthenticator = transferTxInput.authenticator !== void 0;
6203
+ if (hasData && hasInclusionProof) {
6204
+ transferTx = await TransferTransaction2.fromJSON(transferTxInput);
6205
+ } else if (hasTransactionData && hasAuthenticator) {
6206
+ const commitment = await TransferCommitment4.fromJSON(transferTxInput);
6207
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6208
+ if (!stClient) {
6209
+ console.error("[Payments] Cannot process commitment - no state transition client");
4981
6210
  return;
4982
6211
  }
4983
- }
4984
- } else {
4985
- this.log("Finalizing DIRECT address transfer for state tracking...");
4986
- try {
4987
- const signingService = await this.createSigningService();
4988
- const transferSalt = transferTx.data.salt;
4989
- const recipientPredicate = await UnmaskedPredicate3.create(
4990
- sourceToken.id,
4991
- sourceToken.type,
4992
- signingService,
4993
- HashAlgorithm3.SHA256,
4994
- transferSalt
4995
- );
4996
- const recipientState = new TokenState3(recipientPredicate, null);
4997
- const stClient = this.deps.oracle.getStateTransitionClient?.();
4998
- const trustBase = this.deps.oracle.getTrustBase?.();
4999
- if (!stClient || !trustBase) {
5000
- this.log("Cannot finalize DIRECT transfer - missing client, using source token");
5001
- tokenData = sourceTokenInput;
5002
- } else {
5003
- finalizedSdkToken = await stClient.finalizeTransaction(
5004
- trustBase,
5005
- sourceToken,
5006
- recipientState,
5007
- transferTx,
5008
- []
5009
- // No nametag tokens needed for DIRECT
5010
- );
5011
- tokenData = finalizedSdkToken.toJSON();
5012
- this.log("DIRECT transfer finalized successfully");
6212
+ const response = await stClient.submitTransferCommitment(commitment);
6213
+ if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
6214
+ console.error("[Payments] Transfer commitment submission failed:", response.status);
6215
+ return;
6216
+ }
6217
+ if (!this.deps.oracle.waitForProofSdk) {
6218
+ console.error("[Payments] Cannot wait for proof - missing oracle method");
6219
+ return;
6220
+ }
6221
+ const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
6222
+ transferTx = commitment.toTransaction(inclusionProof);
6223
+ } else {
6224
+ try {
6225
+ transferTx = await TransferTransaction2.fromJSON(transferTxInput);
6226
+ } catch {
6227
+ const commitment = await TransferCommitment4.fromJSON(transferTxInput);
6228
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6229
+ if (!stClient || !this.deps.oracle.waitForProofSdk) {
6230
+ throw new Error("Cannot submit commitment - missing oracle methods");
6231
+ }
6232
+ await stClient.submitTransferCommitment(commitment);
6233
+ const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
6234
+ transferTx = commitment.toTransaction(inclusionProof);
5013
6235
  }
5014
- } catch (finalizeError) {
5015
- this.log("DIRECT finalization failed, using source token:", finalizeError);
5016
- tokenData = sourceTokenInput;
5017
6236
  }
6237
+ } catch (err) {
6238
+ console.error("[Payments] Failed to parse transferTx:", err);
6239
+ return;
6240
+ }
6241
+ try {
6242
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6243
+ const trustBase = this.deps.oracle.getTrustBase?.();
6244
+ if (!stClient || !trustBase) {
6245
+ console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
6246
+ return;
6247
+ }
6248
+ finalizedSdkToken = await this.finalizeTransferToken(sourceToken, transferTx, stClient, trustBase);
6249
+ tokenData = finalizedSdkToken.toJSON();
6250
+ const addressScheme = transferTx.data.recipient.scheme;
6251
+ this.log(`${addressScheme === AddressScheme.PROXY ? "PROXY" : "DIRECT"} finalization successful`);
6252
+ } catch (finalizeError) {
6253
+ console.error(`[Payments] Finalization FAILED - token rejected:`, finalizeError);
6254
+ return;
5018
6255
  }
5019
6256
  } else if (payload.token) {
5020
6257
  tokenData = payload.token;
@@ -5041,12 +6278,6 @@ var PaymentsModule = class {
5041
6278
  updatedAt: Date.now(),
5042
6279
  sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
5043
6280
  };
5044
- const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
5045
- const stateHash = extractStateHashFromSdkData(token.sdkData);
5046
- if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
5047
- this.log(`Rejected tombstoned token ${sdkTokenId.slice(0, 8)}...`);
5048
- return;
5049
- }
5050
6281
  await this.addToken(token);
5051
6282
  const incomingTransfer = {
5052
6283
  id: transfer.id,
@@ -5134,14 +6365,159 @@ var PaymentsModule = class {
5134
6365
  }
5135
6366
  loadFromStorageData(data) {
5136
6367
  const parsed = parseTxfStorageData(data);
6368
+ this.tombstones = parsed.tombstones;
5137
6369
  this.tokens.clear();
5138
6370
  for (const token of parsed.tokens) {
6371
+ const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
6372
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
6373
+ if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
6374
+ this.log(`Skipping tombstoned token ${sdkTokenId.slice(0, 8)}... during load (exact state match)`);
6375
+ continue;
6376
+ }
5139
6377
  this.tokens.set(token.id, token);
5140
6378
  }
5141
- this.tombstones = parsed.tombstones;
5142
6379
  this.archivedTokens = parsed.archivedTokens;
5143
6380
  this.forkedTokens = parsed.forkedTokens;
5144
- this.nametag = parsed.nametag;
6381
+ if (parsed.nametag !== null) {
6382
+ this.nametag = parsed.nametag;
6383
+ }
6384
+ }
6385
+ // ===========================================================================
6386
+ // Private: NOSTR-FIRST Proof Polling
6387
+ // ===========================================================================
6388
+ /**
6389
+ * Submit commitment to aggregator and start background proof polling
6390
+ * (NOSTR-FIRST pattern: fire-and-forget submission)
6391
+ */
6392
+ async submitAndPollForProof(tokenId, commitment, requestIdHex, onProofReceived) {
6393
+ try {
6394
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6395
+ if (!stClient) {
6396
+ this.log("Cannot submit commitment - no state transition client");
6397
+ return;
6398
+ }
6399
+ const response = await stClient.submitTransferCommitment(commitment);
6400
+ if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
6401
+ this.log(`Transfer commitment submission failed: ${response.status}`);
6402
+ const token = this.tokens.get(tokenId);
6403
+ if (token) {
6404
+ token.status = "invalid";
6405
+ token.updatedAt = Date.now();
6406
+ this.tokens.set(tokenId, token);
6407
+ await this.save();
6408
+ }
6409
+ return;
6410
+ }
6411
+ this.addProofPollingJob({
6412
+ tokenId,
6413
+ requestIdHex,
6414
+ commitmentJson: JSON.stringify(commitment.toJSON()),
6415
+ startedAt: Date.now(),
6416
+ attemptCount: 0,
6417
+ lastAttemptAt: 0,
6418
+ onProofReceived
6419
+ });
6420
+ } catch (error) {
6421
+ this.log("submitAndPollForProof error:", error);
6422
+ }
6423
+ }
6424
+ /**
6425
+ * Add a proof polling job to the queue
6426
+ */
6427
+ addProofPollingJob(job) {
6428
+ this.proofPollingJobs.set(job.tokenId, job);
6429
+ this.log(`Added proof polling job for token ${job.tokenId.slice(0, 8)}...`);
6430
+ this.startProofPolling();
6431
+ }
6432
+ /**
6433
+ * Start the proof polling interval if not already running
6434
+ */
6435
+ startProofPolling() {
6436
+ if (this.proofPollingInterval) return;
6437
+ if (this.proofPollingJobs.size === 0) return;
6438
+ this.log("Starting proof polling...");
6439
+ this.proofPollingInterval = setInterval(
6440
+ () => this.processProofPollingQueue(),
6441
+ _PaymentsModule.PROOF_POLLING_INTERVAL_MS
6442
+ );
6443
+ }
6444
+ /**
6445
+ * Stop the proof polling interval
6446
+ */
6447
+ stopProofPolling() {
6448
+ if (this.proofPollingInterval) {
6449
+ clearInterval(this.proofPollingInterval);
6450
+ this.proofPollingInterval = null;
6451
+ this.log("Stopped proof polling");
6452
+ }
6453
+ }
6454
+ /**
6455
+ * Process all pending proof polling jobs
6456
+ */
6457
+ async processProofPollingQueue() {
6458
+ if (this.proofPollingJobs.size === 0) {
6459
+ this.stopProofPolling();
6460
+ return;
6461
+ }
6462
+ const completedJobs = [];
6463
+ for (const [tokenId, job] of this.proofPollingJobs) {
6464
+ try {
6465
+ job.attemptCount++;
6466
+ job.lastAttemptAt = Date.now();
6467
+ if (job.attemptCount >= _PaymentsModule.PROOF_POLLING_MAX_ATTEMPTS) {
6468
+ this.log(`Proof polling timeout for token ${tokenId.slice(0, 8)}...`);
6469
+ const token2 = this.tokens.get(tokenId);
6470
+ if (token2 && token2.status === "submitted") {
6471
+ token2.status = "invalid";
6472
+ token2.updatedAt = Date.now();
6473
+ this.tokens.set(tokenId, token2);
6474
+ }
6475
+ completedJobs.push(tokenId);
6476
+ continue;
6477
+ }
6478
+ const commitment = await TransferCommitment4.fromJSON(JSON.parse(job.commitmentJson));
6479
+ let inclusionProof = null;
6480
+ try {
6481
+ const abortController = new AbortController();
6482
+ const timeoutId = setTimeout(() => abortController.abort(), 500);
6483
+ if (this.deps.oracle.waitForProofSdk) {
6484
+ inclusionProof = await Promise.race([
6485
+ this.deps.oracle.waitForProofSdk(commitment, abortController.signal),
6486
+ new Promise((resolve) => setTimeout(() => resolve(null), 500))
6487
+ ]);
6488
+ } else {
6489
+ const proof = await this.deps.oracle.getProof(job.requestIdHex);
6490
+ if (proof) {
6491
+ inclusionProof = proof;
6492
+ }
6493
+ }
6494
+ clearTimeout(timeoutId);
6495
+ } catch (err) {
6496
+ continue;
6497
+ }
6498
+ if (!inclusionProof) {
6499
+ continue;
6500
+ }
6501
+ const token = this.tokens.get(tokenId);
6502
+ if (token) {
6503
+ token.status = "spent";
6504
+ token.updatedAt = Date.now();
6505
+ this.tokens.set(tokenId, token);
6506
+ await this.save();
6507
+ this.log(`Proof received for token ${tokenId.slice(0, 8)}..., status: spent`);
6508
+ }
6509
+ job.onProofReceived?.(tokenId);
6510
+ completedJobs.push(tokenId);
6511
+ } catch (error) {
6512
+ this.log(`Proof polling attempt ${job.attemptCount} for ${tokenId.slice(0, 8)}...: ${error}`);
6513
+ }
6514
+ }
6515
+ for (const tokenId of completedJobs) {
6516
+ this.proofPollingJobs.delete(tokenId);
6517
+ }
6518
+ if (this.proofPollingJobs.size === 0) {
6519
+ this.stopProofPolling();
6520
+ }
5145
6521
  }
5146
6522
  // ===========================================================================
5147
6523
  // Private: Helpers
@@ -5156,6 +6532,14 @@ function createPaymentsModule(config) {
5156
6532
  return new PaymentsModule(config);
5157
6533
  }
5158
6534
 
6535
+ // modules/payments/TokenRecoveryService.ts
6536
+ import { TokenId as TokenId4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenId";
6537
+ import { TokenState as TokenState6 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
6538
+ import { TokenType as TokenType3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
6539
+ import { CoinId as CoinId5 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
6540
+ import { HashAlgorithm as HashAlgorithm6 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
6541
+ import { UnmaskedPredicate as UnmaskedPredicate6 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
6542
+
5159
6543
  // modules/communications/CommunicationsModule.ts
5160
6544
  var CommunicationsModule = class {
5161
6545
  config;
@@ -6124,20 +7508,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
6124
7508
 
6125
7509
  // core/Sphere.ts
6126
7510
  import { SigningService as SigningService2 } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
6127
- import { TokenType as TokenType2 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
6128
- import { HashAlgorithm as HashAlgorithm4 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
6129
- import { UnmaskedPredicateReference as UnmaskedPredicateReference2 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
7511
+ import { TokenType as TokenType4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
7512
+ import { HashAlgorithm as HashAlgorithm7 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
7513
+ import { UnmaskedPredicateReference as UnmaskedPredicateReference3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
6130
7514
  var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
6131
7515
  async function deriveL3PredicateAddress(privateKey) {
6132
7516
  const secret = Buffer.from(privateKey, "hex");
6133
7517
  const signingService = await SigningService2.createFromSecret(secret);
6134
7518
  const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
6135
- const tokenType = new TokenType2(tokenTypeBytes);
6136
- const predicateRef = UnmaskedPredicateReference2.create(
7519
+ const tokenType = new TokenType4(tokenTypeBytes);
7520
+ const predicateRef = UnmaskedPredicateReference3.create(
6137
7521
  tokenType,
6138
7522
  signingService.algorithm,
6139
7523
  signingService.publicKey,
6140
- HashAlgorithm4.SHA256
7524
+ HashAlgorithm7.SHA256
6141
7525
  );
6142
7526
  return (await (await predicateRef).toAddress()).toString();
6143
7527
  }
@@ -6153,7 +7537,11 @@ var Sphere = class _Sphere {
6153
7537
  _derivationMode = "bip32";
6154
7538
  _basePath = DEFAULT_BASE_PATH;
6155
7539
  _currentAddressIndex = 0;
6156
- /** Map of addressId -> (nametagIndex -> nametag). Supports multiple nametags per address (e.g., from Nostr recovery) */
7540
+ /** Registry of all tracked (activated) addresses, keyed by HD index */
7541
+ _trackedAddresses = /* @__PURE__ */ new Map();
7542
+ /** Reverse lookup: addressId -> HD index */
7543
+ _addressIdToIndex = /* @__PURE__ */ new Map();
7544
+ /** Nametag cache: addressId -> (nametagIndex -> nametag). Separate from tracked addresses. */
6157
7545
  _addressNametags = /* @__PURE__ */ new Map();
6158
7546
  /** Cached PROXY address (computed once when nametag is set) */
6159
7547
  _cachedProxyAddress = void 0;
@@ -6162,6 +7550,7 @@ var Sphere = class _Sphere {
6162
7550
  _tokenStorageProviders = /* @__PURE__ */ new Map();
6163
7551
  _transport;
6164
7552
  _oracle;
7553
+ _priceProvider;
6165
7554
  // Modules
6166
7555
  _payments;
6167
7556
  _communications;
@@ -6170,10 +7559,11 @@ var Sphere = class _Sphere {
6170
7559
  // ===========================================================================
6171
7560
  // Constructor (private)
6172
7561
  // ===========================================================================
6173
- constructor(storage, transport, oracle, tokenStorage, l1Config) {
7562
+ constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider) {
6174
7563
  this._storage = storage;
6175
7564
  this._transport = transport;
6176
7565
  this._oracle = oracle;
7566
+ this._priceProvider = priceProvider ?? null;
6177
7567
  if (tokenStorage) {
6178
7568
  this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
6179
7569
  }
@@ -6233,7 +7623,8 @@ var Sphere = class _Sphere {
6233
7623
  transport: options.transport,
6234
7624
  oracle: options.oracle,
6235
7625
  tokenStorage: options.tokenStorage,
6236
- l1: options.l1
7626
+ l1: options.l1,
7627
+ price: options.price
6237
7628
  });
6238
7629
  return { sphere: sphere2, created: false };
6239
7630
  }
@@ -6257,7 +7648,8 @@ var Sphere = class _Sphere {
6257
7648
  tokenStorage: options.tokenStorage,
6258
7649
  derivationPath: options.derivationPath,
6259
7650
  nametag: options.nametag,
6260
- l1: options.l1
7651
+ l1: options.l1,
7652
+ price: options.price
6261
7653
  });
6262
7654
  return { sphere, created: true, generatedMnemonic };
6263
7655
  }
@@ -6276,7 +7668,8 @@ var Sphere = class _Sphere {
6276
7668
  options.transport,
6277
7669
  options.oracle,
6278
7670
  options.tokenStorage,
6279
- options.l1
7671
+ options.l1,
7672
+ options.price
6280
7673
  );
6281
7674
  await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
6282
7675
  await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
@@ -6285,10 +7678,12 @@ var Sphere = class _Sphere {
6285
7678
  await sphere.finalizeWalletCreation();
6286
7679
  sphere._initialized = true;
6287
7680
  _Sphere.instance = sphere;
7681
+ await sphere.ensureAddressTracked(0);
6288
7682
  if (options.nametag) {
6289
7683
  await sphere.registerNametag(options.nametag);
6290
7684
  } else {
6291
- await sphere.recoverNametagFromNostr();
7685
+ await sphere.syncIdentityWithTransport();
7686
+ await sphere.recoverNametagFromTransport();
6292
7687
  }
6293
7688
  return sphere;
6294
7689
  }
@@ -6304,14 +7699,28 @@ var Sphere = class _Sphere {
6304
7699
  options.transport,
6305
7700
  options.oracle,
6306
7701
  options.tokenStorage,
6307
- options.l1
7702
+ options.l1,
7703
+ options.price
6308
7704
  );
6309
7705
  await sphere.loadIdentityFromStorage();
6310
7706
  await sphere.initializeProviders();
6311
7707
  await sphere.initializeModules();
6312
- await sphere.syncNametagWithNostr();
7708
+ await sphere.syncIdentityWithTransport();
6313
7709
  sphere._initialized = true;
6314
7710
  _Sphere.instance = sphere;
7711
+ if (sphere._identity?.nametag && !sphere._payments.hasNametag()) {
7712
+ console.log(`[Sphere] Nametag @${sphere._identity.nametag} has no token, attempting to mint...`);
7713
+ try {
7714
+ const result = await sphere.mintNametag(sphere._identity.nametag);
7715
+ if (result.success) {
7716
+ console.log(`[Sphere] Nametag token minted successfully on load`);
7717
+ } else {
7718
+ console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
7719
+ }
7720
+ } catch (err) {
7721
+ console.warn(`[Sphere] Nametag token mint failed:`, err);
7722
+ }
7723
+ }
6315
7724
  return sphere;
6316
7725
  }
6317
7726
  /**
@@ -6327,7 +7736,8 @@ var Sphere = class _Sphere {
6327
7736
  options.transport,
6328
7737
  options.oracle,
6329
7738
  options.tokenStorage,
6330
- options.l1
7739
+ options.l1,
7740
+ options.price
6331
7741
  );
6332
7742
  if (options.mnemonic) {
6333
7743
  if (!_Sphere.validateMnemonic(options.mnemonic)) {
@@ -6352,11 +7762,12 @@ var Sphere = class _Sphere {
6352
7762
  await sphere.initializeProviders();
6353
7763
  await sphere.initializeModules();
6354
7764
  if (!options.nametag) {
6355
- await sphere.recoverNametagFromNostr();
7765
+ await sphere.recoverNametagFromTransport();
6356
7766
  }
6357
7767
  await sphere.finalizeWalletCreation();
6358
7768
  sphere._initialized = true;
6359
7769
  _Sphere.instance = sphere;
7770
+ await sphere.ensureAddressTracked(0);
6360
7771
  if (options.nametag) {
6361
7772
  await sphere.registerNametag(options.nametag);
6362
7773
  }
@@ -6392,6 +7803,7 @@ var Sphere = class _Sphere {
6392
7803
  await storage.remove(STORAGE_KEYS_GLOBAL.DERIVATION_MODE);
6393
7804
  await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_SOURCE);
6394
7805
  await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_EXISTS);
7806
+ await storage.remove(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
6395
7807
  await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
6396
7808
  await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
6397
7809
  await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
@@ -6516,6 +7928,13 @@ var Sphere = class _Sphere {
6516
7928
  hasTokenStorageProvider(providerId) {
6517
7929
  return this._tokenStorageProviders.has(providerId);
6518
7930
  }
7931
+ /**
7932
+ * Set or update the price provider after initialization
7933
+ */
7934
+ setPriceProvider(provider) {
7935
+ this._priceProvider = provider;
7936
+ this._payments.setPriceProvider(provider);
7937
+ }
6519
7938
  getTransport() {
6520
7939
  return this._transport;
6521
7940
  }
@@ -7003,10 +8422,9 @@ var Sphere = class _Sphere {
7003
8422
  * @returns Primary nametag (index 0) or undefined if not registered
7004
8423
  */
7005
8424
  getNametagForAddress(addressId) {
7006
- const id = addressId ?? this.getCurrentAddressId();
8425
+ const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
7007
8426
  if (!id) return void 0;
7008
- const nametagsMap = this._addressNametags.get(id);
7009
- return nametagsMap?.get(0);
8427
+ return this._addressNametags.get(id)?.get(0);
7010
8428
  }
7011
8429
  /**
7012
8430
  * Get all nametags for a specific address
@@ -7015,29 +8433,89 @@ var Sphere = class _Sphere {
7015
8433
  * @returns Map of nametagIndex to nametag, or undefined if no nametags
7016
8434
  */
7017
8435
  getNametagsForAddress(addressId) {
7018
- const id = addressId ?? this.getCurrentAddressId();
8436
+ const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
7019
8437
  if (!id) return void 0;
7020
- const nametagsMap = this._addressNametags.get(id);
7021
- return nametagsMap ? new Map(nametagsMap) : void 0;
8438
+ const nametags = this._addressNametags.get(id);
8439
+ return nametags && nametags.size > 0 ? new Map(nametags) : void 0;
7022
8440
  }
7023
8441
  /**
7024
8442
  * Get all registered address nametags
7025
- *
8443
+ * @deprecated Use getActiveAddresses() or getAllTrackedAddresses() instead
7026
8444
  * @returns Map of addressId to (nametagIndex -> nametag)
7027
8445
  */
7028
8446
  getAllAddressNametags() {
7029
8447
  const result = /* @__PURE__ */ new Map();
7030
- this._addressNametags.forEach((nametagsMap, addressId) => {
7031
- result.set(addressId, new Map(nametagsMap));
7032
- });
8448
+ for (const [addressId, nametags] of this._addressNametags.entries()) {
8449
+ if (nametags.size > 0) {
8450
+ result.set(addressId, new Map(nametags));
8451
+ }
8452
+ }
7033
8453
  return result;
7034
8454
  }
7035
8455
  /**
7036
- * Get current address identifier (DIRECT://xxx format)
8456
+ * Get all active (non-hidden) tracked addresses.
8457
+ * Returns addresses that have been activated through create, switchToAddress,
8458
+ * registerNametag, or nametag recovery.
8459
+ *
8460
+ * @returns Array of TrackedAddress entries sorted by index, excluding hidden ones
8461
+ */
8462
+ getActiveAddresses() {
8463
+ this.ensureReady();
8464
+ const result = [];
8465
+ for (const entry of this._trackedAddresses.values()) {
8466
+ if (!entry.hidden) {
8467
+ const nametag = this._addressNametags.get(entry.addressId)?.get(0);
8468
+ result.push({ ...entry, nametag });
8469
+ }
8470
+ }
8471
+ return result.sort((a, b) => a.index - b.index);
8472
+ }
8473
+ /**
8474
+ * Get all tracked addresses, including hidden ones.
8475
+ *
8476
+ * @returns Array of all TrackedAddress entries sorted by index
8477
+ */
8478
+ getAllTrackedAddresses() {
8479
+ this.ensureReady();
8480
+ const result = [];
8481
+ for (const entry of this._trackedAddresses.values()) {
8482
+ const nametag = this._addressNametags.get(entry.addressId)?.get(0);
8483
+ result.push({ ...entry, nametag });
8484
+ }
8485
+ return result.sort((a, b) => a.index - b.index);
8486
+ }
8487
+ /**
8488
+ * Get tracked address info by index.
8489
+ *
8490
+ * @param index - Address index
8491
+ * @returns TrackedAddress or undefined if not tracked
8492
+ */
8493
+ getTrackedAddress(index) {
8494
+ this.ensureReady();
8495
+ const entry = this._trackedAddresses.get(index);
8496
+ if (!entry) return void 0;
8497
+ const nametag = this._addressNametags.get(entry.addressId)?.get(0);
8498
+ return { ...entry, nametag };
8499
+ }
8500
+ /**
8501
+ * Set visibility of a tracked address.
8502
+ * Hidden addresses are not returned by getActiveAddresses() but remain tracked.
8503
+ *
8504
+ * @param index - Address index to hide/unhide
8505
+ * @param hidden - true to hide, false to show
8506
+ * @throws Error if address index is not tracked
7037
8507
  */
7038
- getCurrentAddressId() {
7039
- if (!this._identity?.directAddress) return void 0;
7040
- return getAddressId(this._identity.directAddress);
8508
+ async setAddressHidden(index, hidden) {
8509
+ this.ensureReady();
8510
+ const entry = this._trackedAddresses.get(index);
8511
+ if (!entry) {
8512
+ throw new Error(`Address at index ${index} is not tracked. Switch to it first.`);
8513
+ }
8514
+ if (entry.hidden === hidden) return;
8515
+ entry.hidden = hidden;
8516
+ await this.persistTrackedAddresses();
8517
+ const eventType = hidden ? "address:hidden" : "address:unhidden";
8518
+ this.emitEvent(eventType, { index, addressId: entry.addressId });
7041
8519
  }
7042
8520
  /**
7043
8521
  * Switch to a different address by index
@@ -7058,7 +8536,7 @@ var Sphere = class _Sphere {
7058
8536
  * await sphere.switchToAddress(0);
7059
8537
  * ```
7060
8538
  */
7061
- async switchToAddress(index) {
8539
+ async switchToAddress(index, options) {
7062
8540
  this.ensureReady();
7063
8541
  if (!this._masterKey) {
7064
8542
  throw new Error("HD derivation requires master key with chain code. Cannot switch addresses.");
@@ -7066,12 +8544,28 @@ var Sphere = class _Sphere {
7066
8544
  if (index < 0) {
7067
8545
  throw new Error("Address index must be non-negative");
7068
8546
  }
8547
+ const newNametag = options?.nametag?.startsWith("@") ? options.nametag.slice(1) : options?.nametag;
8548
+ if (newNametag && !this.validateNametag(newNametag)) {
8549
+ throw new Error("Invalid nametag format. Use alphanumeric characters, 3-20 chars.");
8550
+ }
7069
8551
  const addressInfo = this.deriveAddress(index, false);
7070
8552
  const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
7071
8553
  const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
8554
+ await this.ensureAddressTracked(index);
7072
8555
  const addressId = getAddressId(predicateAddress);
7073
- const nametagsMap = this._addressNametags.get(addressId);
7074
- const nametag = nametagsMap?.get(0);
8556
+ if (newNametag) {
8557
+ const existing = await this._transport.resolveNametag?.(newNametag);
8558
+ if (existing) {
8559
+ throw new Error(`Nametag @${newNametag} is already taken`);
8560
+ }
8561
+ let nametags = this._addressNametags.get(addressId);
8562
+ if (!nametags) {
8563
+ nametags = /* @__PURE__ */ new Map();
8564
+ this._addressNametags.set(addressId, nametags);
8565
+ }
8566
+ nametags.set(0, newNametag);
8567
+ }
8568
+ const nametag = this._addressNametags.get(addressId)?.get(0);
7075
8569
  this._identity = {
7076
8570
  privateKey: addressInfo.privateKey,
7077
8571
  chainPubkey: addressInfo.publicKey,
@@ -7084,11 +8578,47 @@ var Sphere = class _Sphere {
7084
8578
  await this._updateCachedProxyAddress();
7085
8579
  await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
7086
8580
  this._storage.setIdentity(this._identity);
7087
- this._transport.setIdentity(this._identity);
8581
+ await this._transport.setIdentity(this._identity);
7088
8582
  for (const provider of this._tokenStorageProviders.values()) {
7089
8583
  provider.setIdentity(this._identity);
8584
+ await provider.initialize();
7090
8585
  }
7091
8586
  await this.reinitializeModulesForNewAddress();
8587
+ if (this._identity.nametag) {
8588
+ await this.syncIdentityWithTransport();
8589
+ }
8590
+ if (newNametag) {
8591
+ await this.persistAddressNametags();
8592
+ if (!this._payments.hasNametag()) {
8593
+ console.log(`[Sphere] Minting nametag token for @${newNametag}...`);
8594
+ try {
8595
+ const result = await this.mintNametag(newNametag);
8596
+ if (result.success) {
8597
+ console.log(`[Sphere] Nametag token minted successfully`);
8598
+ } else {
8599
+ console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
8600
+ }
8601
+ } catch (err) {
8602
+ console.warn(`[Sphere] Nametag token mint failed:`, err);
8603
+ }
8604
+ }
8605
+ this.emitEvent("nametag:registered", {
8606
+ nametag: newNametag,
8607
+ addressIndex: index
8608
+ });
8609
+ } else if (this._identity.nametag && !this._payments.hasNametag()) {
8610
+ console.log(`[Sphere] Nametag @${this._identity.nametag} has no token after switch, minting...`);
8611
+ try {
8612
+ const result = await this.mintNametag(this._identity.nametag);
8613
+ if (result.success) {
8614
+ console.log(`[Sphere] Nametag token minted successfully after switch`);
8615
+ } else {
8616
+ console.warn(`[Sphere] Could not mint nametag token after switch: ${result.error}`);
8617
+ }
8618
+ } catch (err) {
8619
+ console.warn(`[Sphere] Nametag token mint failed after switch:`, err);
8620
+ }
8621
+ }
7092
8622
  this.emitEvent("identity:changed", {
7093
8623
  l1Address: this._identity.l1Address,
7094
8624
  directAddress: this._identity.directAddress,
@@ -7110,7 +8640,8 @@ var Sphere = class _Sphere {
7110
8640
  transport: this._transport,
7111
8641
  oracle: this._oracle,
7112
8642
  emitEvent,
7113
- chainCode: this._masterKey?.chainCode
8643
+ chainCode: this._masterKey?.chainCode,
8644
+ price: this._priceProvider ?? void 0
7114
8645
  });
7115
8646
  this._communications.initialize({
7116
8647
  identity: this._identity,
@@ -7143,6 +8674,14 @@ var Sphere = class _Sphere {
7143
8674
  */
7144
8675
  deriveAddress(index, isChange = false) {
7145
8676
  this.ensureReady();
8677
+ return this._deriveAddressInternal(index, isChange);
8678
+ }
8679
+ /**
8680
+ * Internal address derivation without ensureReady() check.
8681
+ * Used during initialization (loadTrackedAddresses, ensureAddressTracked)
8682
+ * when _initialized is still false.
8683
+ */
8684
+ _deriveAddressInternal(index, isChange = false) {
7146
8685
  if (!this._masterKey) {
7147
8686
  throw new Error("HD derivation requires master key with chain code");
7148
8687
  }
@@ -7281,6 +8820,22 @@ var Sphere = class _Sphere {
7281
8820
  getProxyAddress() {
7282
8821
  return this._cachedProxyAddress;
7283
8822
  }
8823
+ /**
8824
+ * Resolve any identifier to full peer information.
8825
+ * Accepts @nametag, bare nametag, DIRECT://, PROXY://, L1 address, or transport pubkey.
8826
+ *
8827
+ * @example
8828
+ * ```ts
8829
+ * const peer = await sphere.resolve('@alice');
8830
+ * const peer = await sphere.resolve('DIRECT://...');
8831
+ * const peer = await sphere.resolve('alpha1...');
8832
+ * const peer = await sphere.resolve('ab12cd...'); // 64-char hex transport pubkey
8833
+ * ```
8834
+ */
8835
+ async resolve(identifier) {
8836
+ this.ensureReady();
8837
+ return this._transport.resolve?.(identifier) ?? null;
8838
+ }
7284
8839
  /** Compute and cache the PROXY address from the current nametag */
7285
8840
  async _updateCachedProxyAddress() {
7286
8841
  const nametag = this._identity?.nametag;
@@ -7319,11 +8874,12 @@ var Sphere = class _Sphere {
7319
8874
  if (this._identity?.nametag) {
7320
8875
  throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
7321
8876
  }
7322
- if (this._transport.registerNametag) {
7323
- const success = await this._transport.registerNametag(
7324
- cleanNametag,
8877
+ if (this._transport.publishIdentityBinding) {
8878
+ const success = await this._transport.publishIdentityBinding(
7325
8879
  this._identity.chainPubkey,
7326
- this._identity.directAddress || ""
8880
+ this._identity.l1Address,
8881
+ this._identity.directAddress || "",
8882
+ cleanNametag
7327
8883
  );
7328
8884
  if (!success) {
7329
8885
  throw new Error("Failed to register nametag. It may already be taken.");
@@ -7331,14 +8887,14 @@ var Sphere = class _Sphere {
7331
8887
  }
7332
8888
  this._identity.nametag = cleanNametag;
7333
8889
  await this._updateCachedProxyAddress();
7334
- const addressId = this.getCurrentAddressId();
7335
- if (addressId) {
7336
- let nametagsMap = this._addressNametags.get(addressId);
7337
- if (!nametagsMap) {
7338
- nametagsMap = /* @__PURE__ */ new Map();
7339
- this._addressNametags.set(addressId, nametagsMap);
8890
+ const currentAddressId = this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
8891
+ if (currentAddressId) {
8892
+ let nametags = this._addressNametags.get(currentAddressId);
8893
+ if (!nametags) {
8894
+ nametags = /* @__PURE__ */ new Map();
8895
+ this._addressNametags.set(currentAddressId, nametags);
7340
8896
  }
7341
- nametagsMap.set(0, cleanNametag);
8897
+ nametags.set(0, cleanNametag);
7342
8898
  }
7343
8899
  await this.persistAddressNametags();
7344
8900
  if (!this._payments.hasNametag()) {
@@ -7357,19 +8913,19 @@ var Sphere = class _Sphere {
7357
8913
  console.log(`[Sphere] Nametag registered for address ${this._currentAddressIndex}:`, cleanNametag);
7358
8914
  }
7359
8915
  /**
7360
- * Persist address nametags to storage
7361
- * Format: { "DIRECT://abc...xyz": { "0": "alice", "1": "alice2" }, ... }
8916
+ * Persist tracked addresses to storage (only minimal fields via StorageProvider)
7362
8917
  */
7363
- async persistAddressNametags() {
7364
- const result = {};
7365
- this._addressNametags.forEach((nametagsMap, addressId) => {
7366
- const innerObj = {};
7367
- nametagsMap.forEach((nametag, index) => {
7368
- innerObj[index.toString()] = nametag;
8918
+ async persistTrackedAddresses() {
8919
+ const entries = [];
8920
+ for (const entry of this._trackedAddresses.values()) {
8921
+ entries.push({
8922
+ index: entry.index,
8923
+ hidden: entry.hidden,
8924
+ createdAt: entry.createdAt,
8925
+ updatedAt: entry.updatedAt
7369
8926
  });
7370
- result[addressId] = innerObj;
7371
- });
7372
- await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
8927
+ }
8928
+ await this._storage.saveTrackedAddresses(entries);
7373
8929
  }
7374
8930
  /**
7375
8931
  * Mint a nametag token on-chain (like Sphere wallet and lottery)
@@ -7403,63 +8959,184 @@ var Sphere = class _Sphere {
7403
8959
  return this._payments.isNametagAvailable(nametag);
7404
8960
  }
7405
8961
  /**
7406
- * Load address nametags from storage
7407
- * Supports new format: { "DIRECT://abc...xyz": { "0": "alice" } }
7408
- * And legacy format: { "0": "alice" } (migrates to new format on save)
8962
+ * Load tracked addresses from storage.
8963
+ * Falls back to migrating from old ADDRESS_NAMETAGS format.
7409
8964
  */
7410
- async loadAddressNametags() {
8965
+ async loadTrackedAddresses() {
8966
+ this._trackedAddresses.clear();
8967
+ this._addressIdToIndex.clear();
7411
8968
  try {
7412
- const saved = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
7413
- if (saved) {
7414
- const parsed = JSON.parse(saved);
7415
- this._addressNametags.clear();
7416
- for (const [key, value] of Object.entries(parsed)) {
7417
- if (typeof value === "object" && value !== null) {
7418
- const nametagsMap = /* @__PURE__ */ new Map();
7419
- for (const [indexStr, nametag] of Object.entries(value)) {
7420
- nametagsMap.set(parseInt(indexStr, 10), nametag);
7421
- }
7422
- this._addressNametags.set(key, nametagsMap);
7423
- } else if (typeof value === "string") {
7424
- }
8969
+ const entries = await this._storage.loadTrackedAddresses();
8970
+ if (entries.length > 0) {
8971
+ for (const stored of entries) {
8972
+ const addrInfo = this._deriveAddressInternal(stored.index, false);
8973
+ const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
8974
+ const addressId = getAddressId(directAddress);
8975
+ const entry = {
8976
+ ...stored,
8977
+ addressId,
8978
+ l1Address: addrInfo.address,
8979
+ directAddress,
8980
+ chainPubkey: addrInfo.publicKey
8981
+ };
8982
+ this._trackedAddresses.set(entry.index, entry);
8983
+ this._addressIdToIndex.set(addressId, entry.index);
7425
8984
  }
8985
+ return;
8986
+ }
8987
+ const oldData = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
8988
+ if (oldData) {
8989
+ const parsed = JSON.parse(oldData);
8990
+ await this.migrateFromOldNametagFormat(parsed);
8991
+ await this.persistTrackedAddresses();
7426
8992
  }
7427
8993
  } catch {
7428
8994
  }
7429
8995
  }
7430
8996
  /**
7431
- * Sync nametag with Nostr on wallet load
7432
- * If local nametag exists but not registered on Nostr, re-register it
8997
+ * Migrate from old ADDRESS_NAMETAGS format to tracked addresses.
8998
+ * Scans HD indices 0..19 to match addressIds from the old format.
8999
+ * Populates both _trackedAddresses and _addressNametags.
7433
9000
  */
7434
- async syncNametagWithNostr() {
7435
- const nametag = this._identity?.nametag;
7436
- if (!nametag) {
7437
- return;
9001
+ async migrateFromOldNametagFormat(parsed) {
9002
+ const addressIdToNametags = /* @__PURE__ */ new Map();
9003
+ for (const [key, value] of Object.entries(parsed)) {
9004
+ if (typeof value === "object" && value !== null) {
9005
+ addressIdToNametags.set(key, value);
9006
+ }
9007
+ }
9008
+ if (addressIdToNametags.size === 0 || !this._masterKey) return;
9009
+ const SCAN_LIMIT = 20;
9010
+ for (let i = 0; i < SCAN_LIMIT && addressIdToNametags.size > 0; i++) {
9011
+ try {
9012
+ const addrInfo = this._deriveAddressInternal(i, false);
9013
+ const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
9014
+ const addressId = getAddressId(directAddress);
9015
+ if (addressIdToNametags.has(addressId)) {
9016
+ const nametagsObj = addressIdToNametags.get(addressId);
9017
+ const nametagMap = /* @__PURE__ */ new Map();
9018
+ for (const [idx, tag] of Object.entries(nametagsObj)) {
9019
+ nametagMap.set(parseInt(idx, 10), tag);
9020
+ }
9021
+ if (nametagMap.size > 0) {
9022
+ this._addressNametags.set(addressId, nametagMap);
9023
+ }
9024
+ const now = Date.now();
9025
+ const entry = {
9026
+ index: i,
9027
+ addressId,
9028
+ l1Address: addrInfo.address,
9029
+ directAddress,
9030
+ chainPubkey: addrInfo.publicKey,
9031
+ nametag: nametagMap.get(0),
9032
+ hidden: false,
9033
+ createdAt: now,
9034
+ updatedAt: now
9035
+ };
9036
+ this._trackedAddresses.set(i, entry);
9037
+ this._addressIdToIndex.set(addressId, i);
9038
+ addressIdToNametags.delete(addressId);
9039
+ }
9040
+ } catch {
9041
+ }
9042
+ }
9043
+ await this.persistAddressNametags();
9044
+ }
9045
+ /**
9046
+ * Ensure an address is tracked in the registry.
9047
+ * If not yet tracked, derives full info and creates the entry.
9048
+ */
9049
+ async ensureAddressTracked(index) {
9050
+ const existing = this._trackedAddresses.get(index);
9051
+ if (existing) return existing;
9052
+ const addrInfo = this._deriveAddressInternal(index, false);
9053
+ const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
9054
+ const addressId = getAddressId(directAddress);
9055
+ const now = Date.now();
9056
+ const nametag = this._addressNametags.get(addressId)?.get(0);
9057
+ const entry = {
9058
+ index,
9059
+ addressId,
9060
+ l1Address: addrInfo.address,
9061
+ directAddress,
9062
+ chainPubkey: addrInfo.publicKey,
9063
+ nametag,
9064
+ hidden: false,
9065
+ createdAt: now,
9066
+ updatedAt: now
9067
+ };
9068
+ this._trackedAddresses.set(index, entry);
9069
+ this._addressIdToIndex.set(addressId, index);
9070
+ await this.persistTrackedAddresses();
9071
+ this.emitEvent("address:activated", { address: { ...entry } });
9072
+ return entry;
9073
+ }
9074
+ /**
9075
+ * Persist nametag cache to storage.
9076
+ * Format: { addressId: { "0": "alice", "1": "alice2" } }
9077
+ */
9078
+ async persistAddressNametags() {
9079
+ const result = {};
9080
+ for (const [addressId, nametags] of this._addressNametags.entries()) {
9081
+ const obj = {};
9082
+ for (const [idx, tag] of nametags.entries()) {
9083
+ obj[idx.toString()] = tag;
9084
+ }
9085
+ result[addressId] = obj;
9086
+ }
9087
+ await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
9088
+ }
9089
+ /**
9090
+ * Load nametag cache from storage.
9091
+ */
9092
+ async loadAddressNametags() {
9093
+ this._addressNametags.clear();
9094
+ try {
9095
+ const data = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
9096
+ if (!data) return;
9097
+ const parsed = JSON.parse(data);
9098
+ for (const [addressId, nametags] of Object.entries(parsed)) {
9099
+ const map = /* @__PURE__ */ new Map();
9100
+ for (const [idx, tag] of Object.entries(nametags)) {
9101
+ map.set(parseInt(idx, 10), tag);
9102
+ }
9103
+ this._addressNametags.set(addressId, map);
9104
+ }
9105
+ } catch {
7438
9106
  }
7439
- if (!this._transport.resolveNametag || !this._transport.registerNametag) {
9107
+ }
9108
+ /**
9109
+ * Publish identity binding via transport.
9110
+ * Always publishes base identity (chainPubkey, l1Address, directAddress).
9111
+ * If nametag is set, also publishes nametag hash, proxy address, encrypted nametag.
9112
+ */
9113
+ async syncIdentityWithTransport() {
9114
+ if (!this._transport.publishIdentityBinding) {
7440
9115
  return;
7441
9116
  }
7442
9117
  try {
7443
- const success = await this._transport.registerNametag(
7444
- nametag,
9118
+ const nametag = this._identity?.nametag;
9119
+ const success = await this._transport.publishIdentityBinding(
7445
9120
  this._identity.chainPubkey,
7446
- this._identity.directAddress || ""
9121
+ this._identity.l1Address,
9122
+ this._identity.directAddress || "",
9123
+ nametag || void 0
7447
9124
  );
7448
9125
  if (success) {
7449
- console.log(`[Sphere] Nametag @${nametag} synced with Nostr`);
7450
- } else {
9126
+ console.log(`[Sphere] Identity binding published${nametag ? ` with nametag @${nametag}` : ""}`);
9127
+ } else if (nametag) {
7451
9128
  console.warn(`[Sphere] Nametag @${nametag} is taken by another pubkey`);
7452
9129
  }
7453
9130
  } catch (error) {
7454
- console.warn(`[Sphere] Nametag sync failed:`, error);
9131
+ console.warn(`[Sphere] Identity binding sync failed:`, error);
7455
9132
  }
7456
9133
  }
7457
9134
  /**
7458
- * Recover nametag from Nostr after wallet import
9135
+ * Recover nametag from transport after wallet import.
7459
9136
  * Searches for encrypted nametag events authored by this wallet's pubkey
7460
- * and decrypts them to restore the nametag association
9137
+ * and decrypts them to restore the nametag association.
7461
9138
  */
7462
- async recoverNametagFromNostr() {
9139
+ async recoverNametagFromTransport() {
7463
9140
  if (this._identity?.nametag) {
7464
9141
  return;
7465
9142
  }
@@ -7473,22 +9150,21 @@ var Sphere = class _Sphere {
7473
9150
  this._identity.nametag = recoveredNametag;
7474
9151
  await this._updateCachedProxyAddress();
7475
9152
  }
7476
- const addressId = this.getCurrentAddressId();
7477
- if (addressId) {
7478
- let nametagsMap = this._addressNametags.get(addressId);
7479
- if (!nametagsMap) {
7480
- nametagsMap = /* @__PURE__ */ new Map();
7481
- this._addressNametags.set(addressId, nametagsMap);
7482
- }
7483
- const nextIndex = nametagsMap.size;
7484
- nametagsMap.set(nextIndex, recoveredNametag);
9153
+ const entry = await this.ensureAddressTracked(this._currentAddressIndex);
9154
+ let nametags = this._addressNametags.get(entry.addressId);
9155
+ if (!nametags) {
9156
+ nametags = /* @__PURE__ */ new Map();
9157
+ this._addressNametags.set(entry.addressId, nametags);
7485
9158
  }
9159
+ const nextIndex = nametags.size;
9160
+ nametags.set(nextIndex, recoveredNametag);
7486
9161
  await this.persistAddressNametags();
7487
- if (this._transport.registerNametag) {
7488
- await this._transport.registerNametag(
7489
- recoveredNametag,
9162
+ if (this._transport.publishIdentityBinding) {
9163
+ await this._transport.publishIdentityBinding(
7490
9164
  this._identity.chainPubkey,
7491
- this._identity.directAddress || ""
9165
+ this._identity.l1Address,
9166
+ this._identity.directAddress || "",
9167
+ recoveredNametag
7492
9168
  );
7493
9169
  }
7494
9170
  this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
@@ -7516,6 +9192,9 @@ var Sphere = class _Sphere {
7516
9192
  await this._oracle.disconnect();
7517
9193
  this._initialized = false;
7518
9194
  this._identity = null;
9195
+ this._trackedAddresses.clear();
9196
+ this._addressIdToIndex.clear();
9197
+ this._addressNametags.clear();
7519
9198
  this.eventHandlers.clear();
7520
9199
  if (_Sphere.instance === this) {
7521
9200
  _Sphere.instance = null;
@@ -7613,14 +9292,14 @@ var Sphere = class _Sphere {
7613
9292
  if (this._identity) {
7614
9293
  this._storage.setIdentity(this._identity);
7615
9294
  }
9295
+ await this.loadTrackedAddresses();
7616
9296
  await this.loadAddressNametags();
9297
+ const trackedEntry = await this.ensureAddressTracked(this._currentAddressIndex);
9298
+ const nametag = this._addressNametags.get(trackedEntry.addressId)?.get(0);
7617
9299
  if (this._currentAddressIndex > 0 && this._masterKey) {
7618
- const addressInfo = this.deriveAddress(this._currentAddressIndex, false);
9300
+ const addressInfo = this._deriveAddressInternal(this._currentAddressIndex, false);
7619
9301
  const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
7620
9302
  const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
7621
- const addressId = getAddressId(predicateAddress);
7622
- const nametagsMap = this._addressNametags.get(addressId);
7623
- const nametag = nametagsMap?.get(0);
7624
9303
  this._identity = {
7625
9304
  privateKey: addressInfo.privateKey,
7626
9305
  chainPubkey: addressInfo.publicKey,
@@ -7631,13 +9310,8 @@ var Sphere = class _Sphere {
7631
9310
  };
7632
9311
  this._storage.setIdentity(this._identity);
7633
9312
  console.log(`[Sphere] Restored to address ${this._currentAddressIndex}:`, this._identity.l1Address);
7634
- } else if (this._identity) {
7635
- const addressId = this.getCurrentAddressId();
7636
- const nametagsMap = addressId ? this._addressNametags.get(addressId) : void 0;
7637
- const nametag = nametagsMap?.get(0);
7638
- if (nametag) {
7639
- this._identity.nametag = nametag;
7640
- }
9313
+ } else if (this._identity && nametag) {
9314
+ this._identity.nametag = nametag;
7641
9315
  }
7642
9316
  await this._updateCachedProxyAddress();
7643
9317
  }
@@ -7695,7 +9369,7 @@ var Sphere = class _Sphere {
7695
9369
  // ===========================================================================
7696
9370
  async initializeProviders() {
7697
9371
  this._storage.setIdentity(this._identity);
7698
- this._transport.setIdentity(this._identity);
9372
+ await this._transport.setIdentity(this._identity);
7699
9373
  for (const provider of this._tokenStorageProviders.values()) {
7700
9374
  provider.setIdentity(this._identity);
7701
9375
  }
@@ -7716,7 +9390,8 @@ var Sphere = class _Sphere {
7716
9390
  oracle: this._oracle,
7717
9391
  emitEvent,
7718
9392
  // Pass chain code for L1 HD derivation
7719
- chainCode: this._masterKey?.chainCode
9393
+ chainCode: this._masterKey?.chainCode,
9394
+ price: this._priceProvider ?? void 0
7720
9395
  });
7721
9396
  this._communications.initialize({
7722
9397
  identity: this._identity,
@@ -7798,6 +9473,65 @@ function formatAmount(amount, options = {}) {
7798
9473
  return symbol ? `${readable} ${symbol}` : readable;
7799
9474
  }
7800
9475
 
9476
+ // types/payment-session.ts
9477
+ function createPaymentSession(params) {
9478
+ const now = Date.now();
9479
+ const deadlineMs = params.deadlineMs ?? 3e5;
9480
+ return {
9481
+ id: crypto.randomUUID(),
9482
+ direction: params.direction,
9483
+ status: "INITIATED",
9484
+ createdAt: now,
9485
+ updatedAt: now,
9486
+ deadline: now + deadlineMs,
9487
+ error: null,
9488
+ sourceTokenId: params.sourceTokenId,
9489
+ recipientNametag: params.recipientNametag,
9490
+ recipientPubkey: params.recipientPubkey,
9491
+ amount: params.amount,
9492
+ coinId: params.coinId,
9493
+ salt: params.salt
9494
+ };
9495
+ }
9496
+ function createSplitPaymentSession(params) {
9497
+ const now = Date.now();
9498
+ return {
9499
+ id: crypto.randomUUID(),
9500
+ direction: "SEND",
9501
+ sourceTokenId: params.sourceTokenId,
9502
+ paymentAmount: params.paymentAmount,
9503
+ changeAmount: params.changeAmount,
9504
+ recipientNametag: params.recipientNametag,
9505
+ recipientPubkey: params.recipientPubkey,
9506
+ splitGroupId: params.splitGroupId,
9507
+ phases: {
9508
+ burn: "PENDING",
9509
+ mints: "PENDING",
9510
+ transfer: "PENDING"
9511
+ },
9512
+ timing: {},
9513
+ createdAt: now,
9514
+ updatedAt: now,
9515
+ error: null
9516
+ };
9517
+ }
9518
+ function isPaymentSessionTimedOut(session) {
9519
+ if (!("deadline" in session) || !session.deadline) return false;
9520
+ return Date.now() > session.deadline;
9521
+ }
9522
+ function isPaymentSessionTerminal(session) {
9523
+ return session.status === "COMPLETED" || session.status === "FAILED" || session.status === "TIMED_OUT";
9524
+ }
9525
+ function createPaymentSessionError(code, message, recoverable = false, details) {
9526
+ return {
9527
+ code,
9528
+ message,
9529
+ timestamp: Date.now(),
9530
+ recoverable,
9531
+ details
9532
+ };
9533
+ }
9534
+
7801
9535
  // types/index.ts
7802
9536
  var SphereError = class extends Error {
7803
9537
  code;
@@ -7985,14 +9719,30 @@ var TokenValidator = class {
7985
9719
  }
7986
9720
  }
7987
9721
  /**
7988
- * Check which tokens are spent
9722
+ * Check which tokens are spent using SDK Token object to calculate state hash.
9723
+ *
9724
+ * Follows the same approach as the Sphere webgui TokenValidationService:
9725
+ * 1. Parse TXF using SDK's Token.fromJSON()
9726
+ * 2. Calculate CURRENT state hash via sdkToken.state.calculateHash()
9727
+ * 3. Create RequestId via RequestId.create(walletPubKey, calculatedHash)
9728
+ *
9729
+ * Uses wallet's own pubkey (not source state predicate key) because "spent" means
9730
+ * the CURRENT OWNER committed this state. Using the source state key would falsely
9731
+ * detect received tokens as "spent" (sender's commitment matches source state).
7989
9732
  */
7990
9733
  async checkSpentTokens(tokens, publicKey, options) {
7991
9734
  const spentTokens = [];
7992
9735
  const errors = [];
9736
+ if (!this.aggregatorClient) {
9737
+ errors.push("Aggregator client not available");
9738
+ return { spentTokens, errors };
9739
+ }
7993
9740
  const batchSize = options?.batchSize ?? 3;
7994
9741
  const total = tokens.length;
7995
9742
  let completed = 0;
9743
+ const { Token: SdkToken3 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
9744
+ const { RequestId } = await import("@unicitylabs/state-transition-sdk/lib/api/RequestId");
9745
+ const pubKeyBytes = Buffer.from(publicKey, "hex");
7996
9746
  for (let i = 0; i < tokens.length; i += batchSize) {
7997
9747
  const batch = tokens.slice(i, i + batchSize);
7998
9748
  const batchResults = await Promise.allSettled(
@@ -8002,13 +9752,39 @@ var TokenValidator = class {
8002
9752
  if (!txf) {
8003
9753
  return { tokenId: token.id, localId: token.id, stateHash: "", spent: false, error: "Invalid TXF" };
8004
9754
  }
8005
- const tokenId = txf.genesis.data.tokenId;
8006
- const stateHash = getCurrentStateHash(txf);
8007
- if (!stateHash) {
8008
- return { tokenId, localId: token.id, stateHash: "", spent: false, error: "No state hash" };
9755
+ const tokenId = txf.genesis?.data?.tokenId || token.id;
9756
+ const sdkToken = await SdkToken3.fromJSON(txf);
9757
+ const calculatedStateHash = await sdkToken.state.calculateHash();
9758
+ const calculatedStateHashStr = calculatedStateHash.toJSON();
9759
+ const cacheKey = `${tokenId}:${calculatedStateHashStr}:${publicKey}`;
9760
+ const cached = this.spentStateCache.get(cacheKey);
9761
+ if (cached !== void 0) {
9762
+ if (cached.isSpent) {
9763
+ return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: true };
9764
+ }
9765
+ if (Date.now() - cached.timestamp < this.UNSPENT_CACHE_TTL_MS) {
9766
+ return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: false };
9767
+ }
9768
+ }
9769
+ const { DataHash } = await import("@unicitylabs/state-transition-sdk/lib/hash/DataHash");
9770
+ const stateHashObj = DataHash.fromJSON(calculatedStateHashStr);
9771
+ const requestId2 = await RequestId.create(pubKeyBytes, stateHashObj);
9772
+ const response = await this.aggregatorClient.getInclusionProof(requestId2);
9773
+ let isSpent = false;
9774
+ if (response.inclusionProof) {
9775
+ const proof = response.inclusionProof;
9776
+ const pathResult = await proof.merkleTreePath.verify(
9777
+ requestId2.toBitString().toBigInt()
9778
+ );
9779
+ if (pathResult.isPathValid && pathResult.isPathIncluded && proof.authenticator !== null) {
9780
+ isSpent = true;
9781
+ }
8009
9782
  }
8010
- const spent = await this.isTokenStateSpent(tokenId, stateHash, publicKey);
8011
- return { tokenId, localId: token.id, stateHash, spent };
9783
+ this.spentStateCache.set(cacheKey, {
9784
+ isSpent,
9785
+ timestamp: Date.now()
9786
+ });
9787
+ return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: isSpent };
8012
9788
  } catch (err) {
8013
9789
  return {
8014
9790
  tokenId: token.id,
@@ -8067,8 +9843,8 @@ var TokenValidator = class {
8067
9843
  }
8068
9844
  async verifyWithSdk(txfToken) {
8069
9845
  try {
8070
- const { Token: Token3 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
8071
- const sdkToken = await Token3.fromJSON(txfToken);
9846
+ const { Token: Token5 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
9847
+ const sdkToken = await Token5.fromJSON(txfToken);
8072
9848
  if (!this.trustBase) {
8073
9849
  return { success: true };
8074
9850
  }
@@ -8091,8 +9867,116 @@ var TokenValidator = class {
8091
9867
  function createTokenValidator(options) {
8092
9868
  return new TokenValidator(options);
8093
9869
  }
9870
+
9871
+ // price/CoinGeckoPriceProvider.ts
9872
+ var CoinGeckoPriceProvider = class {
9873
+ platform = "coingecko";
9874
+ cache = /* @__PURE__ */ new Map();
9875
+ apiKey;
9876
+ cacheTtlMs;
9877
+ timeout;
9878
+ debug;
9879
+ baseUrl;
9880
+ constructor(config) {
9881
+ this.apiKey = config?.apiKey;
9882
+ this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
9883
+ this.timeout = config?.timeout ?? 1e4;
9884
+ this.debug = config?.debug ?? false;
9885
+ this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
9886
+ }
9887
+ async getPrices(tokenNames) {
9888
+ if (tokenNames.length === 0) {
9889
+ return /* @__PURE__ */ new Map();
9890
+ }
9891
+ const now = Date.now();
9892
+ const result = /* @__PURE__ */ new Map();
9893
+ const uncachedNames = [];
9894
+ for (const name of tokenNames) {
9895
+ const cached = this.cache.get(name);
9896
+ if (cached && cached.expiresAt > now) {
9897
+ if (cached.price !== null) {
9898
+ result.set(name, cached.price);
9899
+ }
9900
+ } else {
9901
+ uncachedNames.push(name);
9902
+ }
9903
+ }
9904
+ if (uncachedNames.length === 0) {
9905
+ return result;
9906
+ }
9907
+ try {
9908
+ const ids = uncachedNames.join(",");
9909
+ const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
9910
+ const headers = { Accept: "application/json" };
9911
+ if (this.apiKey) {
9912
+ headers["x-cg-pro-api-key"] = this.apiKey;
9913
+ }
9914
+ if (this.debug) {
9915
+ console.log(`[CoinGecko] Fetching prices for: ${uncachedNames.join(", ")}`);
9916
+ }
9917
+ const response = await fetch(url, {
9918
+ headers,
9919
+ signal: AbortSignal.timeout(this.timeout)
9920
+ });
9921
+ if (!response.ok) {
9922
+ throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
9923
+ }
9924
+ const data = await response.json();
9925
+ for (const [name, values] of Object.entries(data)) {
9926
+ if (values && typeof values === "object") {
9927
+ const price = {
9928
+ tokenName: name,
9929
+ priceUsd: values.usd ?? 0,
9930
+ priceEur: values.eur,
9931
+ change24h: values.usd_24h_change,
9932
+ timestamp: now
9933
+ };
9934
+ this.cache.set(name, { price, expiresAt: now + this.cacheTtlMs });
9935
+ result.set(name, price);
9936
+ }
9937
+ }
9938
+ for (const name of uncachedNames) {
9939
+ if (!result.has(name)) {
9940
+ this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
9941
+ }
9942
+ }
9943
+ if (this.debug) {
9944
+ console.log(`[CoinGecko] Fetched ${result.size} prices`);
9945
+ }
9946
+ } catch (error) {
9947
+ if (this.debug) {
9948
+ console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
9949
+ }
9950
+ for (const name of uncachedNames) {
9951
+ const stale = this.cache.get(name);
9952
+ if (stale?.price) {
9953
+ result.set(name, stale.price);
9954
+ }
9955
+ }
9956
+ }
9957
+ return result;
9958
+ }
9959
+ async getPrice(tokenName) {
9960
+ const prices = await this.getPrices([tokenName]);
9961
+ return prices.get(tokenName) ?? null;
9962
+ }
9963
+ clearCache() {
9964
+ this.cache.clear();
9965
+ }
9966
+ };
9967
+
9968
+ // price/index.ts
9969
+ function createPriceProvider(config) {
9970
+ switch (config.platform) {
9971
+ case "coingecko":
9972
+ return new CoinGeckoPriceProvider(config);
9973
+ default:
9974
+ throw new Error(`Unsupported price platform: ${String(config.platform)}`);
9975
+ }
9976
+ }
8094
9977
  export {
8095
9978
  COIN_TYPES,
9979
+ CoinGeckoPriceProvider,
8096
9980
  CommunicationsModule,
8097
9981
  DEFAULT_AGGREGATOR_TIMEOUT,
8098
9982
  DEFAULT_AGGREGATOR_URL,
@@ -8128,8 +10012,12 @@ export {
8128
10012
  createCommunicationsModule,
8129
10013
  createKeyPair,
8130
10014
  createL1PaymentsModule,
10015
+ createPaymentSession,
10016
+ createPaymentSessionError,
8131
10017
  createPaymentsModule,
10018
+ createPriceProvider,
8132
10019
  createSphere,
10020
+ createSplitPaymentSession,
8133
10021
  createTokenValidator,
8134
10022
  decodeBech32,
8135
10023
  decryptCMasterKey,
@@ -8167,7 +10055,12 @@ export {
8167
10055
  initSphere,
8168
10056
  isArchivedKey,
8169
10057
  isForkedKey,
10058
+ isInstantSplitBundle,
10059
+ isInstantSplitBundleV4,
10060
+ isInstantSplitBundleV5,
8170
10061
  isKnownToken,
10062
+ isPaymentSessionTerminal,
10063
+ isPaymentSessionTimedOut,
8171
10064
  isSQLiteDatabase,
8172
10065
  isTextWalletEncrypted,
8173
10066
  isTokenKey,