@unicitylabs/sphere-sdk 0.7.1-dev.2 → 0.7.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.cjs CHANGED
@@ -894,6 +894,7 @@ var init_network = __esm({
894
894
  // index.ts
895
895
  var index_exports = {};
896
896
  __export(index_exports, {
897
+ AuthVerificationError: () => AuthVerificationError,
897
898
  COIN_TYPES: () => COIN_TYPES,
898
899
  CoinGeckoPriceProvider: () => CoinGeckoPriceProvider,
899
900
  CommunicationsModule: () => CommunicationsModule,
@@ -943,6 +944,8 @@ __export(index_exports, {
943
944
  buildTxfStorageData: () => buildTxfStorageData,
944
945
  bytesToHex: () => bytesToHex3,
945
946
  checkNetworkHealth: () => checkNetworkHealth,
947
+ coinIdsMatch: () => coinIdsMatch,
948
+ computeDirectAddressFromChainPubkey: () => computeDirectAddressFromChainPubkey,
946
949
  computeSwapId: () => computeSwapId,
947
950
  countCommittedTransactions: () => countCommittedTransactions,
948
951
  createAddress: () => createAddress,
@@ -961,22 +964,34 @@ __export(index_exports, {
961
964
  createSwapModule: () => createSwapModule,
962
965
  createTokenValidator: () => createTokenValidator,
963
966
  decodeBech32: () => decodeBech32,
967
+ decrypt: () => decrypt2,
964
968
  decryptCMasterKey: () => decryptCMasterKey,
969
+ decryptJson: () => decryptJson,
970
+ decryptMnemonic: () => decryptMnemonic,
965
971
  decryptNametag: () => import_nostr_js_sdk6.decryptNametag,
966
972
  decryptPrivateKey: () => decryptPrivateKey,
973
+ decryptSimple: () => decryptSimple,
967
974
  decryptTextFormatKey: () => decryptTextFormatKey,
975
+ decryptWallet: () => decryptWallet,
976
+ decryptWithSalt: () => decryptWithSalt,
968
977
  deriveAddressInfo: () => deriveAddressInfo,
969
978
  deriveChildKey: () => deriveChildKey,
970
979
  deriveKeyAtPath: () => deriveKeyAtPath,
971
980
  doubleSha256: () => doubleSha256,
972
981
  encodeBech32: () => encodeBech32,
982
+ encrypt: () => encrypt2,
983
+ encryptMnemonic: () => encryptMnemonic,
973
984
  encryptNametag: () => import_nostr_js_sdk6.encryptNametag,
985
+ encryptSimple: () => encryptSimple,
986
+ encryptWallet: () => encryptWallet,
974
987
  extractFromText: () => extractFromText,
975
988
  findPattern: () => findPattern,
976
989
  forkedKeyFromTokenIdAndState: () => forkedKeyFromTokenIdAndState,
977
990
  formatAmount: () => formatAmount,
991
+ generateAddressFromMasterKey: () => generateAddressFromMasterKey,
978
992
  generateMasterKey: () => generateMasterKey,
979
993
  generateMnemonic: () => generateMnemonic2,
994
+ generatePrivateKey: () => generatePrivateKey,
980
995
  getAddressHrp: () => getAddressHrp,
981
996
  getAddressId: () => getAddressId,
982
997
  getAddressStorageKey: () => getAddressStorageKey,
@@ -999,6 +1014,7 @@ __export(index_exports, {
999
1014
  hashNametag: () => import_nostr_js_sdk6.hashNametag,
1000
1015
  hashSignMessage: () => hashSignMessage,
1001
1016
  hexToBytes: () => hexToBytes2,
1017
+ hexToWIF: () => hexToWIF,
1002
1018
  identityFromMnemonicSync: () => identityFromMnemonicSync,
1003
1019
  initSphere: () => initSphere,
1004
1020
  isArchivedKey: () => isArchivedKey,
@@ -1028,6 +1044,7 @@ __export(index_exports, {
1028
1044
  logger: () => logger,
1029
1045
  mnemonicToSeedSync: () => mnemonicToSeedSync2,
1030
1046
  normalizeAddress: () => normalizeAddress,
1047
+ normalizeCoinId: () => normalizeCoinId,
1031
1048
  normalizeNametag: () => import_nostr_js_sdk6.normalizeNametag,
1032
1049
  normalizeSdkTokenToStorage: () => normalizeSdkTokenToStorage,
1033
1050
  objectToTxf: () => objectToTxf,
@@ -1058,6 +1075,7 @@ __export(index_exports, {
1058
1075
  verifyManifestIntegrity: () => verifyManifestIntegrity,
1059
1076
  verifyNametagBinding: () => verifyNametagBinding,
1060
1077
  verifySignedMessage: () => verifySignedMessage,
1078
+ verifySphereAuth: () => verifySphereAuth,
1061
1079
  verifySwapSignature: () => verifySwapSignature
1062
1080
  });
1063
1081
  module.exports = __toCommonJS(index_exports);
@@ -1555,6 +1573,17 @@ var NostrTransportProvider = class _NostrTransportProvider {
1555
1573
  getStorageAdapter() {
1556
1574
  return this.storage;
1557
1575
  }
1576
+ /**
1577
+ * Get the underlying NostrClient (or null if not yet connected).
1578
+ *
1579
+ * Exposed so {@link MultiAddressTransportMux} can share the same
1580
+ * client/socket pair instead of opening a duplicate WebSocket per
1581
+ * relay (#123). The transport owns the client's lifecycle — callers
1582
+ * MUST NOT call {@code disconnect()} on the returned instance.
1583
+ */
1584
+ getNostrClient() {
1585
+ return this.nostrClient;
1586
+ }
1558
1587
  /**
1559
1588
  * Suppress event subscriptions — unsubscribe wallet/chat filters
1560
1589
  * but keep the connection alive for resolve/identity-binding operations.
@@ -3193,9 +3222,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
3193
3222
  chatSubscriptionId = null;
3194
3223
  chatEoseFired = false;
3195
3224
  resubscribeTimer = null;
3196
- lastWalletEventAt = Date.now();
3197
- lastChatEventAt = Date.now();
3198
- healthCheckTimer = null;
3199
3225
  chatEoseHandlers = [];
3200
3226
  // Dedup — bounded to prevent memory leak in long-running sessions.
3201
3227
  // Set preserves insertion order; evict oldest entries when cap is reached.
@@ -3206,6 +3232,19 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
3206
3232
  // Identity key for the Mux's NostrClient — relays may filter gift-wrap
3207
3233
  // delivery to the recipient's subscription key.
3208
3234
  identityPrivateKey;
3235
+ // Resolves the shared NostrClient at use-time (the source provider may
3236
+ // create its client lazily, after the Mux is constructed). null means
3237
+ // "no shared client; create our own."
3238
+ sharedNostrClientGetter;
3239
+ // True when this Mux is using a shared NostrClient and therefore must
3240
+ // not call connect()/disconnect() on it.
3241
+ usingSharedClient = false;
3242
+ // Listener registered on the underlying NostrClient. Tracked so we can
3243
+ // remove it on disconnect / rebind — otherwise a long-lived shared
3244
+ // client accumulates listeners across address switches and (worse)
3245
+ // a "disconnected" Mux still sees onReconnected callbacks fire and
3246
+ // re-establish subscriptions it shouldn't have.
3247
+ connectionListener = null;
3209
3248
  constructor(config) {
3210
3249
  this.identityPrivateKey = config.identityPrivateKey;
3211
3250
  this.config = {
@@ -3218,6 +3257,14 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
3218
3257
  generateUUID: config.generateUUID ?? defaultUUIDGenerator
3219
3258
  };
3220
3259
  this.storage = config.storage ?? null;
3260
+ if (typeof config.sharedNostrClient === "function") {
3261
+ this.sharedNostrClientGetter = config.sharedNostrClient;
3262
+ } else if (config.sharedNostrClient) {
3263
+ const c = config.sharedNostrClient;
3264
+ this.sharedNostrClientGetter = () => c;
3265
+ } else {
3266
+ this.sharedNostrClientGetter = null;
3267
+ }
3221
3268
  }
3222
3269
  // ===========================================================================
3223
3270
  // Address Management
@@ -3308,53 +3355,49 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
3308
3355
  if (this.status === "connected") return;
3309
3356
  this.status = "connecting";
3310
3357
  try {
3311
- if (!this.primaryKeyManager) {
3312
- if (this.identityPrivateKey) {
3313
- this.primaryKeyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(
3314
- import_buffer2.Buffer.from(this.identityPrivateKey)
3358
+ const shared = this.sharedNostrClientGetter ? this.sharedNostrClientGetter() : null;
3359
+ if (shared) {
3360
+ if (!shared.isConnected()) {
3361
+ throw new SphereError(
3362
+ "sharedNostrClient is not connected; the Mux cannot share a closed socket",
3363
+ "TRANSPORT_ERROR"
3315
3364
  );
3316
- } else {
3317
- const tempKey = import_buffer2.Buffer.alloc(32);
3318
- crypto.getRandomValues(tempKey);
3319
- this.primaryKeyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(tempKey);
3320
3365
  }
3366
+ this.nostrClient = shared;
3367
+ this.usingSharedClient = true;
3368
+ } else {
3369
+ if (!this.primaryKeyManager) {
3370
+ if (this.identityPrivateKey) {
3371
+ this.primaryKeyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(
3372
+ import_buffer2.Buffer.from(this.identityPrivateKey)
3373
+ );
3374
+ } else {
3375
+ const tempKey = import_buffer2.Buffer.alloc(32);
3376
+ crypto.getRandomValues(tempKey);
3377
+ this.primaryKeyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(tempKey);
3378
+ }
3379
+ }
3380
+ this.nostrClient = new import_nostr_js_sdk2.NostrClient(this.primaryKeyManager, {
3381
+ autoReconnect: this.config.autoReconnect,
3382
+ reconnectIntervalMs: this.config.reconnectDelay,
3383
+ maxReconnectIntervalMs: this.config.reconnectDelay * 16,
3384
+ pingIntervalMs: 15e3
3385
+ });
3321
3386
  }
3322
- this.nostrClient = new import_nostr_js_sdk2.NostrClient(this.primaryKeyManager, {
3323
- autoReconnect: this.config.autoReconnect,
3324
- reconnectIntervalMs: this.config.reconnectDelay,
3325
- maxReconnectIntervalMs: this.config.reconnectDelay * 16,
3326
- pingIntervalMs: 15e3
3327
- });
3328
- this.nostrClient.addConnectionListener({
3329
- onConnect: (url) => {
3330
- logger.debug("Mux", "Connected to relay:", url);
3331
- this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
3332
- },
3333
- onDisconnect: (url, reason) => {
3334
- logger.debug("Mux", "Disconnected from relay:", url, "reason:", reason);
3335
- },
3336
- onReconnecting: (url, attempt) => {
3337
- logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
3338
- this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
3339
- },
3340
- onReconnected: (url) => {
3341
- logger.debug("Mux", "Reconnected to relay:", url);
3342
- this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
3343
- this.updateSubscriptions().catch((err) => {
3344
- logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
3345
- });
3387
+ this.connectionListener = this.buildConnectionListener();
3388
+ this.nostrClient.addConnectionListener(this.connectionListener);
3389
+ if (!this.usingSharedClient) {
3390
+ await Promise.race([
3391
+ this.nostrClient.connect(...this.config.relays),
3392
+ new Promise(
3393
+ (_, reject) => setTimeout(() => reject(new Error(
3394
+ `Transport connection timed out after ${this.config.timeout}ms`
3395
+ )), this.config.timeout)
3396
+ )
3397
+ ]);
3398
+ if (!this.nostrClient.isConnected()) {
3399
+ throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
3346
3400
  }
3347
- });
3348
- await Promise.race([
3349
- this.nostrClient.connect(...this.config.relays),
3350
- new Promise(
3351
- (_, reject) => setTimeout(() => reject(new Error(
3352
- `Transport connection timed out after ${this.config.timeout}ms`
3353
- )), this.config.timeout)
3354
- )
3355
- ]);
3356
- if (!this.nostrClient.isConnected()) {
3357
- throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
3358
3401
  }
3359
3402
  this.status = "connected";
3360
3403
  this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
@@ -3363,6 +3406,21 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
3363
3406
  }
3364
3407
  } catch (error) {
3365
3408
  this.status = "error";
3409
+ if (this.connectionListener && this.nostrClient) {
3410
+ try {
3411
+ this.nostrClient.removeConnectionListener(this.connectionListener);
3412
+ } catch {
3413
+ }
3414
+ }
3415
+ this.connectionListener = null;
3416
+ if (this.nostrClient && !this.usingSharedClient) {
3417
+ try {
3418
+ this.nostrClient.disconnect();
3419
+ } catch {
3420
+ }
3421
+ }
3422
+ this.nostrClient = null;
3423
+ this.usingSharedClient = false;
3366
3424
  throw error;
3367
3425
  }
3368
3426
  }
@@ -3371,25 +3429,153 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
3371
3429
  clearTimeout(this.resubscribeTimer);
3372
3430
  this.resubscribeTimer = null;
3373
3431
  }
3374
- if (this.healthCheckTimer) {
3375
- clearInterval(this.healthCheckTimer);
3376
- this.healthCheckTimer = null;
3377
- }
3378
3432
  if (this.nostrClient) {
3379
- this.nostrClient.disconnect();
3433
+ if (this.walletSubscriptionId) {
3434
+ try {
3435
+ this.nostrClient.unsubscribe(this.walletSubscriptionId);
3436
+ } catch {
3437
+ }
3438
+ }
3439
+ if (this.chatSubscriptionId) {
3440
+ try {
3441
+ this.nostrClient.unsubscribe(this.chatSubscriptionId);
3442
+ } catch {
3443
+ }
3444
+ }
3445
+ if (this.connectionListener) {
3446
+ try {
3447
+ this.nostrClient.removeConnectionListener(this.connectionListener);
3448
+ } catch {
3449
+ }
3450
+ }
3451
+ if (!this.usingSharedClient) {
3452
+ this.nostrClient.disconnect();
3453
+ }
3380
3454
  this.nostrClient = null;
3381
3455
  }
3456
+ this.connectionListener = null;
3457
+ this.usingSharedClient = false;
3382
3458
  this.walletSubscriptionId = null;
3383
3459
  this.chatSubscriptionId = null;
3384
3460
  this.chatEoseFired = false;
3385
- this.lastWalletEventAt = Date.now();
3386
- this.lastChatEventAt = Date.now();
3387
3461
  this.status = "disconnected";
3388
3462
  this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
3389
3463
  }
3390
3464
  isConnected() {
3391
3465
  return this.status === "connected" && this.nostrClient?.isConnected() === true;
3392
3466
  }
3467
+ /**
3468
+ * Build the connection listener used by both {@link connect} and
3469
+ * {@link rebindToSharedClient}.
3470
+ *
3471
+ * Behavioral notes:
3472
+ * - When the Mux is sharing a {@link NostrClient} with the host
3473
+ * transport (#123), we deliberately do NOT emit
3474
+ * {@code transport:connected} / {@code transport:reconnecting} here
3475
+ * — the host transport's own listener already emits those for the
3476
+ * same socket event. Re-subscribing after a reconnect IS still our
3477
+ * responsibility, since the host has
3478
+ * {@code suppressSubscriptions()}'d its own filters.
3479
+ * - {@code onConnect} does not emit {@code transport:connected}.
3480
+ * The SDK only fires {@code onConnect} on the initial socket
3481
+ * connection (subsequent reconnects use {@code onReconnected}),
3482
+ * and {@link connect()}'s bottom already emits
3483
+ * {@code transport:connected} once that returns. Emitting here too
3484
+ * would double-fire on every initial connect.
3485
+ * - Each callback bails out early when the Mux is not in an active
3486
+ * state ({@code disconnected} / {@code error}). Listeners are
3487
+ * removed on {@code disconnect()} before the callback can fire,
3488
+ * so this guard is mainly defense-in-depth against any in-flight
3489
+ * callback that lands during teardown — but having it at the top
3490
+ * means we never emit a misleading {@code transport:connected}
3491
+ * from a Mux that has already torn down.
3492
+ */
3493
+ buildConnectionListener() {
3494
+ const isInactive = () => this.status === "disconnected" || this.status === "error";
3495
+ return {
3496
+ onConnect: (url) => {
3497
+ if (isInactive()) return;
3498
+ logger.debug("Mux", "Connected to relay:", url);
3499
+ },
3500
+ onDisconnect: (url, reason) => {
3501
+ logger.debug("Mux", "Disconnected from relay:", url, "reason:", reason);
3502
+ },
3503
+ onReconnecting: (url, attempt) => {
3504
+ if (isInactive()) return;
3505
+ logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
3506
+ if (!this.usingSharedClient) {
3507
+ this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
3508
+ }
3509
+ },
3510
+ onReconnected: (url) => {
3511
+ if (isInactive()) return;
3512
+ logger.debug("Mux", "Reconnected to relay:", url);
3513
+ if (!this.usingSharedClient) {
3514
+ this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
3515
+ }
3516
+ this.updateSubscriptions().catch((err) => {
3517
+ logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
3518
+ });
3519
+ }
3520
+ };
3521
+ }
3522
+ /**
3523
+ * Re-attach to a freshly-created shared NostrClient.
3524
+ *
3525
+ * Call this after the host (e.g. {@link NostrTransportProvider}) has
3526
+ * recreated its NostrClient — typically because the wallet's active
3527
+ * identity changed and the SDK's NostrClient does not support
3528
+ * changing identity at runtime. The previous client has already
3529
+ * been disconnected by the host, so its server-side subscriptions
3530
+ * are gone — we just adopt the new client and re-issue our own.
3531
+ *
3532
+ * The caller is responsible for ordering: by the time rebind runs,
3533
+ * the host transport's new NostrClient must already be created and
3534
+ * connected. In Sphere this is guaranteed because we await
3535
+ * {@code transport.setIdentity()} before calling rebind.
3536
+ *
3537
+ * Returns silently in two cases that are not caller errors:
3538
+ * - the Mux owns its own client (not sharing) — nothing to rebind
3539
+ * - the shared client reference hasn't changed (rebind is a no-op)
3540
+ *
3541
+ * Throws otherwise (rather than silently no-op'ing) so a wiring
3542
+ * mistake — for instance, calling rebind before the host's new
3543
+ * client is ready — surfaces immediately instead of leaving the
3544
+ * Mux pinned to a stale client.
3545
+ */
3546
+ async rebindToSharedClient() {
3547
+ if (!this.usingSharedClient) return;
3548
+ if (!this.sharedNostrClientGetter) return;
3549
+ const newClient = this.sharedNostrClientGetter();
3550
+ if (!newClient) {
3551
+ throw new SphereError(
3552
+ "rebindToSharedClient: shared client getter returned null. The host transport must finish (re)creating its NostrClient before rebind is called.",
3553
+ "TRANSPORT_ERROR"
3554
+ );
3555
+ }
3556
+ if (this.nostrClient === newClient) return;
3557
+ if (!newClient.isConnected()) {
3558
+ throw new SphereError(
3559
+ "rebindToSharedClient: new shared client is not connected. Await transport.setIdentity() / transport.connect() before rebinding.",
3560
+ "TRANSPORT_ERROR"
3561
+ );
3562
+ }
3563
+ if (this.nostrClient && this.connectionListener && this.nostrClient !== newClient) {
3564
+ try {
3565
+ this.nostrClient.removeConnectionListener(this.connectionListener);
3566
+ } catch {
3567
+ }
3568
+ }
3569
+ this.nostrClient = newClient;
3570
+ this.walletSubscriptionId = null;
3571
+ this.chatSubscriptionId = null;
3572
+ this.chatEoseFired = false;
3573
+ this.connectionListener = this.buildConnectionListener();
3574
+ this.nostrClient.addConnectionListener(this.connectionListener);
3575
+ if (this.addresses.size > 0) {
3576
+ await this.updateSubscriptions();
3577
+ }
3578
+ }
3393
3579
  /**
3394
3580
  * One-shot fetch of pending events from the relay.
3395
3581
  * Creates a temporary subscription, waits for EOSE (or timeout),
@@ -3518,8 +3704,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
3518
3704
  this.nostrClient.unsubscribe(this.chatSubscriptionId);
3519
3705
  this.chatSubscriptionId = null;
3520
3706
  }
3521
- this.lastWalletEventAt = Date.now();
3522
- this.lastChatEventAt = Date.now();
3523
3707
  if (this.addresses.size === 0) return;
3524
3708
  const allPubkeys = [];
3525
3709
  for (const entry of this.addresses.values()) {
@@ -3606,25 +3790,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
3606
3790
  }
3607
3791
  });
3608
3792
  logger.debug("Mux", `updateSubscriptions: walletSub=${this.walletSubscriptionId} chatSub=${this.chatSubscriptionId}`);
3609
- this.startHealthCheck();
3610
- }
3611
- startHealthCheck() {
3612
- if (this.healthCheckTimer) return;
3613
- this.healthCheckTimer = setInterval(() => {
3614
- if (!this.isConnected()) return;
3615
- const chatElapsed = Date.now() - this.lastChatEventAt;
3616
- const walletElapsed = Date.now() - this.lastWalletEventAt;
3617
- const needResubscribe = chatElapsed > 6e4 || walletElapsed > 3e5;
3618
- if (needResubscribe) {
3619
- const reason = chatElapsed > 6e4 ? `No chat events for ${Math.round(chatElapsed / 1e3)}s` : `No wallet events for ${Math.round(walletElapsed / 1e3)}s`;
3620
- logger.warn("Mux", `${reason} \u2014 re-subscribing`);
3621
- this.lastChatEventAt = Date.now();
3622
- this.lastWalletEventAt = Date.now();
3623
- this.updateSubscriptions().catch((err) => {
3624
- logger.warn("Mux", "Health check re-subscription failed:", err);
3625
- });
3626
- }
3627
- }, 3e4);
3628
3793
  }
3629
3794
  /**
3630
3795
  * Schedule a re-subscription after a relay-initiated subscription closure.
@@ -3685,12 +3850,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
3685
3850
  }
3686
3851
  }
3687
3852
  }
3688
- if (event.kind !== import_nostr_js_sdk2.EventKinds.GIFT_WRAP) {
3689
- this.lastWalletEventAt = Date.now();
3690
- }
3691
- if (event.kind === import_nostr_js_sdk2.EventKinds.GIFT_WRAP) {
3692
- this.lastChatEventAt = Date.now();
3693
- }
3694
3853
  try {
3695
3854
  if (event.kind === import_nostr_js_sdk2.EventKinds.GIFT_WRAP) {
3696
3855
  await this.routeGiftWrap(event);
@@ -12372,6 +12531,132 @@ var PaymentsModule = class _PaymentsModule {
12372
12531
  };
12373
12532
  }
12374
12533
  }
12534
+ /**
12535
+ * Mint a fungible token directly to this wallet (genesis mint).
12536
+ *
12537
+ * Useful for test setups that need to seed a wallet with specific token
12538
+ * balances WITHOUT depending on the testnet faucet HTTP service. The
12539
+ * resulting token has the canonical CoinId bytes (passed in `coinIdHex`)
12540
+ * — when those bytes match a registered symbol in the TokenRegistry,
12541
+ * the token shows up under the symbol's name (e.g. "UCT"). There is no
12542
+ * cryptographic restriction on which key may issue a given CoinId; the
12543
+ * aggregator records the mint regardless of issuer identity.
12544
+ *
12545
+ * The flow:
12546
+ * 1. Generate a random TokenId.
12547
+ * 2. Build TokenCoinData with [(coinId, amount)].
12548
+ * 3. Build MintTransactionData with recipient = self (UnmaskedPredicate
12549
+ * from this wallet's signing service).
12550
+ * 4. Submit MintCommitment to the aggregator.
12551
+ * 5. Wait for the inclusion proof.
12552
+ * 6. Construct an SDK Token via Token.mint().
12553
+ * 7. Convert to wallet Token format and call addToken().
12554
+ *
12555
+ * @param coinIdHex - 64-char lowercase hex CoinId. Must match the bytes
12556
+ * used by the registered symbol if you want the wallet to recognize
12557
+ * the token as that symbol (e.g. UCT's coinId from the public registry).
12558
+ * @param amount - Amount in smallest units (multiply by 10^decimals
12559
+ * when converting from human values).
12560
+ * @returns Result with the resulting wallet Token and its on-chain id.
12561
+ */
12562
+ async mintFungibleToken(coinIdHex, amount) {
12563
+ this.ensureInitialized();
12564
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
12565
+ if (!stClient) {
12566
+ return { success: false, error: "State transition client not available" };
12567
+ }
12568
+ const trustBase = this.deps.oracle.getTrustBase?.();
12569
+ if (!trustBase) {
12570
+ return { success: false, error: "Trust base not available" };
12571
+ }
12572
+ try {
12573
+ const signingService = await this.createSigningService();
12574
+ const { TokenId: TokenId5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenId");
12575
+ const { TokenCoinData: TokenCoinData3 } = await import("@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData");
12576
+ const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
12577
+ const tokenTypeBytes = fromHex4("f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509");
12578
+ const tokenType = new import_TokenType3.TokenType(tokenTypeBytes);
12579
+ const tokenIdBytes = new Uint8Array(32);
12580
+ crypto.getRandomValues(tokenIdBytes);
12581
+ const tokenId = new TokenId5(tokenIdBytes);
12582
+ const coinIdBytes = fromHex4(coinIdHex);
12583
+ const coinId = new import_CoinId4.CoinId(coinIdBytes);
12584
+ const coinData = TokenCoinData3.create([[coinId, amount]]);
12585
+ const addressRef = await UnmaskedPredicateReference4.create(
12586
+ tokenType,
12587
+ signingService.algorithm,
12588
+ signingService.publicKey,
12589
+ import_HashAlgorithm5.HashAlgorithm.SHA256
12590
+ );
12591
+ const ownerAddress = await addressRef.toAddress();
12592
+ const salt = new Uint8Array(32);
12593
+ crypto.getRandomValues(salt);
12594
+ const mintData = await import_MintTransactionData3.MintTransactionData.create(
12595
+ tokenId,
12596
+ tokenType,
12597
+ null,
12598
+ // tokenData: no metadata
12599
+ coinData,
12600
+ // fungible coin data
12601
+ ownerAddress,
12602
+ // recipient = self
12603
+ salt,
12604
+ null,
12605
+ // recipientDataHash
12606
+ null
12607
+ // reason: null (genesis, no burn predecessor)
12608
+ );
12609
+ const commitment = await import_MintCommitment3.MintCommitment.create(mintData);
12610
+ const MAX_RETRIES = 3;
12611
+ let lastStatus;
12612
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
12613
+ const response = await stClient.submitMintCommitment(commitment);
12614
+ lastStatus = response.status;
12615
+ if (response.status === "SUCCESS" || response.status === "REQUEST_ID_EXISTS") break;
12616
+ if (attempt === MAX_RETRIES) {
12617
+ return { success: false, error: `Mint submit failed after ${MAX_RETRIES} attempts: ${response.status}` };
12618
+ }
12619
+ await new Promise((r) => setTimeout(r, 1e3 * attempt));
12620
+ }
12621
+ if (lastStatus !== "SUCCESS" && lastStatus !== "REQUEST_ID_EXISTS") {
12622
+ return { success: false, error: `Mint submit failed: ${lastStatus}` };
12623
+ }
12624
+ const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
12625
+ const genesisTransaction = commitment.toTransaction(inclusionProof);
12626
+ const predicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
12627
+ tokenId,
12628
+ tokenType,
12629
+ signingService,
12630
+ import_HashAlgorithm5.HashAlgorithm.SHA256,
12631
+ salt
12632
+ );
12633
+ const tokenState = new import_TokenState5.TokenState(predicate, null);
12634
+ const sdkToken = await import_Token6.Token.mint(trustBase, tokenState, genesisTransaction);
12635
+ const tokenIdHex = tokenId.toJSON();
12636
+ const symbol = this.getCoinSymbol(coinIdHex);
12637
+ const name = this.getCoinName(coinIdHex);
12638
+ const decimals = this.getCoinDecimals(coinIdHex);
12639
+ const iconUrl = this.getCoinIconUrl(coinIdHex);
12640
+ const uiToken = {
12641
+ id: tokenIdHex,
12642
+ coinId: coinIdHex,
12643
+ symbol,
12644
+ name,
12645
+ decimals,
12646
+ ...iconUrl !== void 0 ? { iconUrl } : {},
12647
+ amount: amount.toString(),
12648
+ status: "confirmed",
12649
+ createdAt: Date.now(),
12650
+ updatedAt: Date.now(),
12651
+ sdkData: JSON.stringify(sdkToken.toJSON())
12652
+ };
12653
+ await this.addToken(uiToken);
12654
+ return { success: true, token: uiToken, tokenId: tokenIdHex };
12655
+ } catch (err) {
12656
+ const msg = err instanceof Error ? err.message : String(err);
12657
+ return { success: false, error: `Local mint failed: ${msg}` };
12658
+ }
12659
+ }
12375
12660
  /**
12376
12661
  * Check if a nametag is available for minting
12377
12662
  * @param nametag - The nametag to check (e.g., "alice" or "@alice")
@@ -18578,6 +18863,24 @@ var AccountingModule = class _AccountingModule {
18578
18863
  dirtyLedgerEntries = /* @__PURE__ */ new Set();
18579
18864
  /** Count of unknown (not in invoiceTermsCache) invoice IDs in the ledger. */
18580
18865
  unknownLedgerCount = 0;
18866
+ /**
18867
+ * Per-unknown-invoice first-seen timestamp for TTL eviction.
18868
+ *
18869
+ * W1 (steelman round-4): without TTL, an attacker who can deliver 500
18870
+ * inbound transfers with synthesized memo invoiceIds permanently exhausts
18871
+ * the unknown-ledger cap, after which legitimate orphan transfers (out-of-
18872
+ * order delivery for real swaps) are silently dropped at the cap-check.
18873
+ *
18874
+ * Round-5 perf: gated by `unknownLedgerNextSweepMs` to amortize the
18875
+ * sweep cost. The naive every-call sweep is O(N) where N=cap=500;
18876
+ * combined with the per-token cleanup loop inside the sweep it became
18877
+ * O(N×M) on every transfer under flood. Now we sweep at most every
18878
+ * `UNKNOWN_LEDGER_SWEEP_INTERVAL_MS` (60s) UNLESS the cap is currently
18879
+ * full, in which case we sweep on each call (the only path that can
18880
+ * actually drop a legitimate orphan).
18881
+ */
18882
+ unknownLedgerFirstSeen = /* @__PURE__ */ new Map();
18883
+ unknownLedgerNextSweepMs = 0;
18581
18884
  /** W17: Tracks whether tokenScanState has been mutated since last flush. */
18582
18885
  tokenScanDirty = false;
18583
18886
  /** W2 fix: Serialization guard for _flushDirtyLedgerEntries. */
@@ -19568,6 +19871,7 @@ var AccountingModule = class _AccountingModule {
19568
19871
  }
19569
19872
  if (this.invoiceLedger.has(tokenId) && !this.invoiceTermsCache.has(tokenId)) {
19570
19873
  this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
19874
+ this.unknownLedgerFirstSeen.delete(tokenId);
19571
19875
  }
19572
19876
  this.invoiceTermsCache.set(tokenId, terms);
19573
19877
  this._addToHashIndex(tokenId);
@@ -19836,6 +20140,29 @@ var AccountingModule = class _AccountingModule {
19836
20140
  closed: this.closedInvoices.has(invoiceId)
19837
20141
  };
19838
20142
  }
20143
+ /**
20144
+ * Return the set of token IDs that are currently linked to the given
20145
+ * invoice. Populated by both the on-chain `_processTokenTransactions`
20146
+ * path (tokens with `inv:` references) and the transport-memo orphan
20147
+ * buffering path in `_handleIncomingTransfer`.
20148
+ *
20149
+ * Used by callers that want to scope per-invoice operations (e.g.
20150
+ * SwapModule.verifyPayout's L3 validation) to only the tokens that
20151
+ * cover this invoice — avoiding false negatives when the wallet
20152
+ * contains unrelated tokens of the same currency in unconfirmed or
20153
+ * spent state.
20154
+ *
20155
+ * Returns an empty set if no tokens are currently linked.
20156
+ */
20157
+ getTokenIdsForInvoice(invoiceId) {
20158
+ const result = /* @__PURE__ */ new Set();
20159
+ for (const [tokenId, invoiceIds] of this.tokenInvoiceMap) {
20160
+ if (invoiceIds.has(invoiceId)) {
20161
+ result.add(tokenId);
20162
+ }
20163
+ }
20164
+ return result;
20165
+ }
19839
20166
  /**
19840
20167
  * Explicitly close an invoice. Only target parties may close (§8.3).
19841
20168
  *
@@ -20155,6 +20482,7 @@ var AccountingModule = class _AccountingModule {
20155
20482
  ledger.set(entryKey, forwardRef);
20156
20483
  this.dirtyLedgerEntries.add(invoiceId);
20157
20484
  this.balanceCache.delete(invoiceId);
20485
+ await this._persistProvisionalAndVerify(invoiceId, "payInvoice");
20158
20486
  }
20159
20487
  return result;
20160
20488
  } finally {
@@ -20322,6 +20650,7 @@ var AccountingModule = class _AccountingModule {
20322
20650
  this.dirtyLedgerEntries.add(invoiceId);
20323
20651
  }
20324
20652
  this.balanceCache.delete(invoiceId);
20653
+ await this._persistProvisionalAndVerify(invoiceId, "returnInvoicePayment");
20325
20654
  }
20326
20655
  return result;
20327
20656
  } finally {
@@ -21401,9 +21730,30 @@ var AccountingModule = class _AccountingModule {
21401
21730
  continue;
21402
21731
  }
21403
21732
  innerMap.set(entryKey, ref);
21404
- if (!ref.transferId.startsWith("provisional:") && ref.transferId.includes(":")) {
21733
+ const HEX_64 = /^[a-f0-9]{64}$/i;
21734
+ if (entryKey.startsWith("mt:")) {
21735
+ const firstColon = entryKey.indexOf(":");
21736
+ const secondColon = entryKey.indexOf(":", firstColon + 1);
21737
+ if (secondColon > firstColon + 1) {
21738
+ const tokenIdFromKey2 = entryKey.slice(firstColon + 1, secondColon);
21739
+ if (HEX_64.test(tokenIdFromKey2)) {
21740
+ this._addToTokenInvoiceMap(tokenIdFromKey2, invoiceId);
21741
+ }
21742
+ }
21743
+ } else if (entryKey.startsWith("synthetic:")) {
21744
+ const afterPrefix = entryKey.slice("synthetic:".length);
21745
+ const tokenIdEnd = afterPrefix.indexOf(":");
21746
+ if (tokenIdEnd > 0) {
21747
+ const tokenId = afterPrefix.slice(0, tokenIdEnd);
21748
+ if (HEX_64.test(tokenId) && ref.transferId !== tokenId) {
21749
+ this._addToTokenInvoiceMap(tokenId, invoiceId);
21750
+ }
21751
+ }
21752
+ } else if (!ref.transferId.startsWith("provisional:") && ref.transferId.includes(":")) {
21405
21753
  const tokenIdFromRef = ref.transferId.slice(0, ref.transferId.indexOf(":"));
21406
- this._addToTokenInvoiceMap(tokenIdFromRef, invoiceId);
21754
+ if (HEX_64.test(tokenIdFromRef)) {
21755
+ this._addToTokenInvoiceMap(tokenIdFromRef, invoiceId);
21756
+ }
21407
21757
  }
21408
21758
  }
21409
21759
  } catch (err) {
@@ -21604,7 +21954,14 @@ var AccountingModule = class _AccountingModule {
21604
21954
  }
21605
21955
  }
21606
21956
  for (const [existingKey, existingRef] of ledger) {
21607
- if (existingKey.startsWith("synthetic:") && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
21957
+ if ((existingKey.startsWith("synthetic:") || existingKey.startsWith("synthetic-tx:")) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
21958
+ keysToDelete.push(existingKey);
21959
+ break;
21960
+ }
21961
+ }
21962
+ const mtPrefix = `mt:${tokenId}:`;
21963
+ for (const [existingKey, existingRef] of ledger) {
21964
+ if (existingKey.startsWith(mtPrefix) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
21608
21965
  keysToDelete.push(existingKey);
21609
21966
  break;
21610
21967
  }
@@ -21721,6 +22078,49 @@ var AccountingModule = class _AccountingModule {
21721
22078
  });
21722
22079
  }
21723
22080
  }
22081
+ /**
22082
+ * Synchronously persist any pending provisional ledger entry for `invoiceId`
22083
+ * before returning to the caller. Used by `payInvoice` and
22084
+ * `returnInvoicePayment` to make the in-memory provisional entry durable
22085
+ * inside the same per-invoice gate that wrote it, closing the
22086
+ * crash-mid-conclude race that produces over-coverage on receivers.
22087
+ *
22088
+ * Implementation:
22089
+ * 1. Schedule a flush via the existing `_flushPromise` chain (so
22090
+ * concurrent `_handleTokenChange` callers waiting on the chain
22091
+ * observe ours as part of the sequence).
22092
+ * 2. Await OUR flush directly — NOT `_drainFlushPromise()`, which would
22093
+ * spin while concurrent token changes keep extending the chain and
22094
+ * hold the per-invoice gate for an unbounded number of additional
22095
+ * flushes. We only need OUR provisional entry durable.
22096
+ * 3. `_flushDirtyLedgerEntries` swallows per-invoice `storage.set`
22097
+ * rejections internally (sets a local `step1Failed` flag), leaving
22098
+ * the dirty entry on the set without re-throwing. So we post-check
22099
+ * `dirtyLedgerEntries.has(invoiceId)` and throw a `STORAGE_ERROR`
22100
+ * `SphereError` if our entry is still dirty — propagating to the
22101
+ * caller so they learn about the durability failure rather than
22102
+ * receiving a silent "success" return that lies on disk.
22103
+ *
22104
+ * @param invoiceId The invoice whose provisional entry must be durable.
22105
+ * @param callContext Used in the error message so the caller is named
22106
+ * ('payInvoice' / 'returnInvoicePayment') without
22107
+ * forcing a stack-trace inspection.
22108
+ */
22109
+ async _persistProvisionalAndVerify(invoiceId, callContext) {
22110
+ const flushTrigger = (this._flushPromise ?? Promise.resolve()).then(() => this._flushDirtyLedgerEntries());
22111
+ const tracked = flushTrigger.catch(() => {
22112
+ }).finally(() => {
22113
+ if (this._flushPromise === tracked) this._flushPromise = null;
22114
+ });
22115
+ this._flushPromise = tracked;
22116
+ await flushTrigger;
22117
+ if (this.dirtyLedgerEntries.has(invoiceId)) {
22118
+ throw new SphereError(
22119
+ `${callContext}: provisional ledger entry for invoice ${invoiceId} failed to persist \u2014 caller should retry`,
22120
+ "STORAGE_ERROR"
22121
+ );
22122
+ }
22123
+ }
21724
22124
  // ===========================================================================
21725
22125
  // Internal: Event handlers
21726
22126
  // ===========================================================================
@@ -21781,13 +22181,96 @@ var AccountingModule = class _AccountingModule {
21781
22181
  }
21782
22182
  }
21783
22183
  if (!this.invoiceTermsCache.has(invoiceId)) {
21784
- const syntheticRef = this._buildSyntheticTransferRef(
21785
- transfer,
21786
- invoiceId,
21787
- paymentDirection,
21788
- confirmed
21789
- );
21790
- deps.emitEvent("invoice:unknown_reference", { invoiceId, transfer: syntheticRef });
22184
+ let gracefullyGraduated = false;
22185
+ await this.withInvoiceGate(invoiceId, async () => {
22186
+ if (this.invoiceTermsCache.has(invoiceId)) {
22187
+ gracefullyGraduated = true;
22188
+ return;
22189
+ }
22190
+ const syntheticRef = this._buildSyntheticTransferRef(
22191
+ transfer,
22192
+ invoiceId,
22193
+ paymentDirection,
22194
+ confirmed
22195
+ );
22196
+ deps.emitEvent("invoice:unknown_reference", { invoiceId, transfer: syntheticRef });
22197
+ const MAX_UNKNOWN_INVOICE_IDS = 500;
22198
+ const UNKNOWN_LEDGER_TTL_MS = 30 * 60 * 1e3;
22199
+ const MAX_ORPHAN_ENTRIES_PER_INVOICE = 50;
22200
+ const UNKNOWN_LEDGER_SWEEP_INTERVAL_MS = 6e4;
22201
+ const nowMs = Date.now();
22202
+ const capFull = this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS;
22203
+ const sweepDue = nowMs >= this.unknownLedgerNextSweepMs;
22204
+ if (this.unknownLedgerFirstSeen.size > 0 && (capFull || sweepDue)) {
22205
+ this.unknownLedgerNextSweepMs = nowMs + UNKNOWN_LEDGER_SWEEP_INTERVAL_MS;
22206
+ const expiredIds = [];
22207
+ for (const [unkId, firstSeen] of this.unknownLedgerFirstSeen) {
22208
+ if (nowMs - firstSeen > UNKNOWN_LEDGER_TTL_MS) {
22209
+ expiredIds.push(unkId);
22210
+ }
22211
+ }
22212
+ for (const expiredId of expiredIds) {
22213
+ if (!this.invoiceTermsCache.has(expiredId) && this.invoiceLedger.has(expiredId)) {
22214
+ this.invoiceLedger.delete(expiredId);
22215
+ this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
22216
+ for (const [tokenId, invoiceSet] of this.tokenInvoiceMap) {
22217
+ if (invoiceSet.has(expiredId)) {
22218
+ invoiceSet.delete(expiredId);
22219
+ if (invoiceSet.size === 0) this.tokenInvoiceMap.delete(tokenId);
22220
+ }
22221
+ }
22222
+ }
22223
+ this.unknownLedgerFirstSeen.delete(expiredId);
22224
+ }
22225
+ }
22226
+ if (!this.invoiceLedger.has(invoiceId)) {
22227
+ if (this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS) {
22228
+ return;
22229
+ }
22230
+ this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
22231
+ this.unknownLedgerCount++;
22232
+ this.unknownLedgerFirstSeen.set(invoiceId, nowMs);
22233
+ }
22234
+ const orphanLedger = this.invoiceLedger.get(invoiceId);
22235
+ let mtEntryCount = 0;
22236
+ for (const k of orphanLedger.keys()) {
22237
+ if (k.startsWith("mt:")) mtEntryCount++;
22238
+ }
22239
+ if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
22240
+ return;
22241
+ }
22242
+ for (const token of transfer.tokens) {
22243
+ if (!token.id) continue;
22244
+ let onChainAttributed = false;
22245
+ const tokenKeyPrefix = `${token.id}:`;
22246
+ for (const existingKey of orphanLedger.keys()) {
22247
+ if (existingKey.startsWith(tokenKeyPrefix) && !existingKey.startsWith("mt:")) {
22248
+ onChainAttributed = true;
22249
+ break;
22250
+ }
22251
+ }
22252
+ if (!onChainAttributed) {
22253
+ if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
22254
+ break;
22255
+ }
22256
+ const orphanKey = `mt:${token.id}:${transfer.id}`;
22257
+ if (!orphanLedger.has(orphanKey)) {
22258
+ orphanLedger.set(orphanKey, syntheticRef);
22259
+ mtEntryCount++;
22260
+ }
22261
+ }
22262
+ if (!this.tokenInvoiceMap.has(token.id)) {
22263
+ this.tokenInvoiceMap.set(token.id, /* @__PURE__ */ new Set());
22264
+ }
22265
+ this.tokenInvoiceMap.get(token.id).add(invoiceId);
22266
+ }
22267
+ this.dirtyLedgerEntries.add(invoiceId);
22268
+ this.balanceCache.delete(invoiceId);
22269
+ await this._flushDirtyLedgerEntries();
22270
+ });
22271
+ if (gracefullyGraduated) {
22272
+ await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
22273
+ }
21791
22274
  return;
21792
22275
  }
21793
22276
  await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
@@ -22223,7 +22706,8 @@ var AccountingModule = class _AccountingModule {
22223
22706
  }
22224
22707
  const existingLedger = this.invoiceLedger.get(invoiceId);
22225
22708
  const firstTokenId = transfer.tokens.find((t) => t.id)?.id;
22226
- const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
22709
+ const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
22710
+ let mutated = false;
22227
22711
  if (!existingLedger.has(syntheticKey)) {
22228
22712
  let hasRealEntry = false;
22229
22713
  for (const tok of transfer.tokens) {
@@ -22238,8 +22722,25 @@ var AccountingModule = class _AccountingModule {
22238
22722
  }
22239
22723
  if (!hasRealEntry) {
22240
22724
  existingLedger.set(syntheticKey, { ...syntheticRef });
22725
+ mutated = true;
22241
22726
  }
22242
22727
  }
22728
+ for (const tok of transfer.tokens) {
22729
+ if (!tok.id) continue;
22730
+ if (!this.tokenInvoiceMap.has(tok.id)) {
22731
+ this.tokenInvoiceMap.set(tok.id, /* @__PURE__ */ new Set());
22732
+ mutated = true;
22733
+ }
22734
+ const beforeSize = this.tokenInvoiceMap.get(tok.id).size;
22735
+ this.tokenInvoiceMap.get(tok.id).add(invoiceId);
22736
+ if (this.tokenInvoiceMap.get(tok.id).size !== beforeSize) {
22737
+ mutated = true;
22738
+ }
22739
+ }
22740
+ if (mutated) {
22741
+ this.dirtyLedgerEntries.add(invoiceId);
22742
+ this.balanceCache.delete(invoiceId);
22743
+ }
22243
22744
  deps.emitEvent("invoice:payment", {
22244
22745
  invoiceId,
22245
22746
  transfer: syntheticRef,
@@ -22389,7 +22890,8 @@ var AccountingModule = class _AccountingModule {
22389
22890
  this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
22390
22891
  }
22391
22892
  const hLedger = this.invoiceLedger.get(invoiceId);
22392
- const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
22893
+ const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
22894
+ let hMutated = false;
22393
22895
  if (!hLedger.has(hKey)) {
22394
22896
  let hasRealEntry = false;
22395
22897
  if (entry.tokenId) {
@@ -22402,8 +22904,24 @@ var AccountingModule = class _AccountingModule {
22402
22904
  }
22403
22905
  if (!hasRealEntry) {
22404
22906
  hLedger.set(hKey, { ...syntheticRef });
22907
+ hMutated = true;
22405
22908
  }
22406
22909
  }
22910
+ if (entry.tokenId) {
22911
+ if (!this.tokenInvoiceMap.has(entry.tokenId)) {
22912
+ this.tokenInvoiceMap.set(entry.tokenId, /* @__PURE__ */ new Set());
22913
+ hMutated = true;
22914
+ }
22915
+ const beforeSize = this.tokenInvoiceMap.get(entry.tokenId).size;
22916
+ this.tokenInvoiceMap.get(entry.tokenId).add(invoiceId);
22917
+ if (this.tokenInvoiceMap.get(entry.tokenId).size !== beforeSize) {
22918
+ hMutated = true;
22919
+ }
22920
+ }
22921
+ if (hMutated) {
22922
+ this.dirtyLedgerEntries.add(invoiceId);
22923
+ this.balanceCache.delete(invoiceId);
22924
+ }
22407
22925
  deps.emitEvent("invoice:payment", {
22408
22926
  invoiceId,
22409
22927
  transfer: syntheticRef,
@@ -24949,17 +25467,63 @@ var SwapModule = class {
24949
25467
  for (const addr of allAddresses) {
24950
25468
  myDirectAddresses.add(addr.directAddress);
24951
25469
  }
24952
- let assetIndex;
24953
- if (myDirectAddresses.has(swap.manifest.party_a_address)) {
24954
- assetIndex = 0;
24955
- } else if (myDirectAddresses.has(swap.manifest.party_b_address)) {
24956
- assetIndex = 1;
25470
+ const matchesPartyA = myDirectAddresses.has(swap.manifest.party_a_address);
25471
+ const matchesPartyB = myDirectAddresses.has(swap.manifest.party_b_address);
25472
+ if (matchesPartyA && matchesPartyB) {
25473
+ throw new SphereError(
25474
+ "Ambiguous party identity: local wallet matches both party_a_address and party_b_address",
25475
+ "SWAP_DEPOSIT_FAILED"
25476
+ );
25477
+ }
25478
+ let myExpectedCurrency;
25479
+ if (matchesPartyA) {
25480
+ myExpectedCurrency = swap.manifest.party_a_currency_to_change;
25481
+ } else if (matchesPartyB) {
25482
+ myExpectedCurrency = swap.manifest.party_b_currency_to_change;
24957
25483
  } else {
24958
25484
  throw new SphereError(
24959
25485
  "Local wallet address does not match either party in the swap manifest",
24960
25486
  "SWAP_DEPOSIT_FAILED"
24961
25487
  );
24962
25488
  }
25489
+ if (!myExpectedCurrency || myExpectedCurrency === "") {
25490
+ throw new SphereError(
25491
+ "Manifest currency_to_change is empty for this party",
25492
+ "SWAP_DEPOSIT_FAILED"
25493
+ );
25494
+ }
25495
+ const invoiceRefForAssetLookup = deps.accounting.getInvoice(swap.depositInvoiceId);
25496
+ if (!invoiceRefForAssetLookup) {
25497
+ throw new SphereError(
25498
+ "Deposit invoice not yet imported into accounting module",
25499
+ "SWAP_WRONG_STATE"
25500
+ );
25501
+ }
25502
+ const depositTarget = invoiceRefForAssetLookup.terms.targets[0];
25503
+ if (!depositTarget) {
25504
+ throw new SphereError(
25505
+ "Deposit invoice has no targets",
25506
+ "SWAP_DEPOSIT_FAILED"
25507
+ );
25508
+ }
25509
+ const assetIndex = depositTarget.assets.findIndex(
25510
+ (a) => a.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)
25511
+ );
25512
+ if (assetIndex < 0) {
25513
+ throw new SphereError(
25514
+ `No asset matching expected currency ${myExpectedCurrency} found in deposit invoice`,
25515
+ "SWAP_DEPOSIT_FAILED"
25516
+ );
25517
+ }
25518
+ for (let i = assetIndex + 1; i < depositTarget.assets.length; i += 1) {
25519
+ const a = depositTarget.assets[i];
25520
+ if (a?.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)) {
25521
+ throw new SphereError(
25522
+ `Ambiguous asset match in deposit invoice: slots ${assetIndex} and ${i} both match currency ${myExpectedCurrency}`,
25523
+ "SWAP_DEPOSIT_FAILED"
25524
+ );
25525
+ }
25526
+ }
24963
25527
  return this.withSwapGate(swapId, async () => {
24964
25528
  if (swap.progress !== "announced") {
24965
25529
  throw new SphereError(
@@ -25065,7 +25629,6 @@ var SwapModule = class {
25065
25629
  swap.updatedAt = Date.now();
25066
25630
  this.clearLocalTimer(swap.swapId);
25067
25631
  this.terminalSwapIds.add(swap.swapId);
25068
- const entryIdx = this._storedTerminalEntries.length;
25069
25632
  this._storedTerminalEntries.push({
25070
25633
  swapId: swap.swapId,
25071
25634
  progress: "failed",
@@ -25080,7 +25643,13 @@ var SwapModule = class {
25080
25643
  swap.error = prevError;
25081
25644
  swap.updatedAt = prevUpdatedAt;
25082
25645
  this.terminalSwapIds.delete(swap.swapId);
25083
- this._storedTerminalEntries.splice(entryIdx, 1);
25646
+ for (let i = this._storedTerminalEntries.length - 1; i >= 0; i--) {
25647
+ const entry = this._storedTerminalEntries[i];
25648
+ if (entry.swapId === swap.swapId && entry.progress === "failed") {
25649
+ this._storedTerminalEntries.splice(i, 1);
25650
+ break;
25651
+ }
25652
+ }
25084
25653
  logger.warn(LOG_TAG3, `failPayout: persistSwap failed for ${swapId}; fraud detection will retry on next load:`, persistErr);
25085
25654
  throw persistErr;
25086
25655
  }
@@ -25124,9 +25693,25 @@ var SwapModule = class {
25124
25693
  if (!targetStatus.coinAssets[0].isCovered) {
25125
25694
  return returnFalse();
25126
25695
  }
25127
- if (BigInt(targetStatus.coinAssets[0].netCoveredAmount) < BigInt(expectedAmount)) {
25696
+ let netCoveredAmount;
25697
+ let expectedAmountBigInt;
25698
+ try {
25699
+ netCoveredAmount = BigInt(targetStatus.coinAssets[0].netCoveredAmount);
25700
+ expectedAmountBigInt = BigInt(expectedAmount);
25701
+ } catch (parseErr) {
25702
+ return failPayout(
25703
+ `MALFORMED_AMOUNT: failed to parse coverage amounts (netCoveredAmount=${targetStatus.coinAssets[0].netCoveredAmount}, expectedAmount=${expectedAmount}): ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`
25704
+ );
25705
+ }
25706
+ if (netCoveredAmount < expectedAmountBigInt) {
25128
25707
  return returnFalse();
25129
25708
  }
25709
+ if (netCoveredAmount > expectedAmountBigInt) {
25710
+ const surplus = netCoveredAmount - expectedAmountBigInt;
25711
+ return failPayout(
25712
+ `OVER_COVERAGE: net=${netCoveredAmount.toString()}, expected=${expectedAmount}, surplus=${surplus.toString()} \u2014 surplus refund expected via auto-return; settlement halted`
25713
+ );
25714
+ }
25130
25715
  const escrowAddr = swap.deal.escrowAddress ?? this.config.defaultEscrowAddress;
25131
25716
  if (escrowAddr) {
25132
25717
  const escrowPeer = await deps.resolve(escrowAddr);
@@ -25136,8 +25721,28 @@ var SwapModule = class {
25136
25721
  }
25137
25722
  const validationResult = await deps.payments.validate();
25138
25723
  if (validationResult.invalid.length > 0) {
25139
- logger.warn(LOG_TAG3, `verifyPayout for ${swapId.slice(0, 12)}: L3 validation found ${validationResult.invalid.length} invalid token(s) \u2014 retry after wallet sync`);
25140
- return returnFalse();
25724
+ const payoutTokenIds = deps.accounting.getTokenIdsForInvoice?.(swap.payoutInvoiceId) ?? /* @__PURE__ */ new Set();
25725
+ if (payoutTokenIds.size === 0) {
25726
+ logger.warn(
25727
+ LOG_TAG3,
25728
+ `verifyPayout for ${swapId.slice(0, 12)}: ${validationResult.invalid.length} invalid token(s) but tokenInvoiceMap is empty for this payout invoice \u2014 failing closed until reverse index rebuilds`
25729
+ );
25730
+ return returnFalse();
25731
+ }
25732
+ const relevantInvalid = validationResult.invalid.filter(
25733
+ (t) => payoutTokenIds.has(t.id)
25734
+ );
25735
+ if (relevantInvalid.length > 0) {
25736
+ logger.warn(
25737
+ LOG_TAG3,
25738
+ `verifyPayout for ${swapId.slice(0, 12)}: L3 validation found ${relevantInvalid.length} invalid token(s) covering this payout invoice \u2014 retry after wallet sync`
25739
+ );
25740
+ return returnFalse();
25741
+ }
25742
+ logger.debug(
25743
+ LOG_TAG3,
25744
+ `verifyPayout for ${swapId.slice(0, 12)}: ${validationResult.invalid.length} unrelated invalid token(s) ignored (not linked to this payout invoice)`
25745
+ );
25141
25746
  }
25142
25747
  if (swap.progress === "completed") {
25143
25748
  swap.payoutVerified = true;
@@ -25316,8 +25921,22 @@ var SwapModule = class {
25316
25921
  * @param dm - The incoming direct message.
25317
25922
  */
25318
25923
  handleIncomingDM(dm) {
25924
+ if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
25925
+ logger.warn(
25926
+ LOG_TAG3,
25927
+ `diag_swap_dm_arrived sender=${dm.senderPubkey.slice(0, 16)} length=${dm.content.length}`
25928
+ );
25929
+ }
25319
25930
  const parsed = parseSwapDM(dm.content);
25320
- if (!parsed) return;
25931
+ if (!parsed) {
25932
+ if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
25933
+ logger.warn(
25934
+ LOG_TAG3,
25935
+ `diag_swap_dm_parse_rejected sender=${dm.senderPubkey.slice(0, 16)} prefix=${dm.content.slice(0, 80)}`
25936
+ );
25937
+ }
25938
+ return;
25939
+ }
25321
25940
  void (async () => {
25322
25941
  try {
25323
25942
  switch (parsed.kind) {
@@ -25683,16 +26302,43 @@ var SwapModule = class {
25683
26302
  // invoice_delivery (§12.4.2 + §12.4.3)
25684
26303
  // ---------------------------------------------------------------
25685
26304
  case "invoice_delivery": {
25686
- if (!swapId) return;
26305
+ logger.warn(
26306
+ LOG_TAG3,
26307
+ `diag_invoice_delivery_received swap_id=${swapId?.slice(0, 16)} sender=${dm.senderPubkey.slice(0, 16)} invoice_type=${msg.invoice_type} invoice_id=${msg.invoice_id?.slice(0, 16)}`
26308
+ );
26309
+ if (!swapId) {
26310
+ logger.warn(LOG_TAG3, "diag_invoice_delivery_dropped reason=no_swap_id");
26311
+ return;
26312
+ }
25687
26313
  const swap = this.swaps.get(swapId);
25688
- if (!swap) return;
25689
- if (!this.isFromExpectedEscrow(dm.senderPubkey, swap)) return;
26314
+ if (!swap) {
26315
+ logger.warn(
26316
+ LOG_TAG3,
26317
+ `diag_invoice_delivery_dropped reason=swap_not_in_map swap_id=${swapId.slice(0, 16)} known_swap_ids_count=${this.swaps.size}`
26318
+ );
26319
+ return;
26320
+ }
26321
+ if (!this.isFromExpectedEscrow(dm.senderPubkey, swap)) {
26322
+ logger.warn(
26323
+ LOG_TAG3,
26324
+ `diag_invoice_delivery_dropped reason=not_expected_escrow swap_id=${swapId.slice(0, 16)} sender=${dm.senderPubkey.slice(0, 16)} expected_escrow_pubkey=${swap.escrowPubkey?.slice(0, 16)} expected_escrow_addr=${swap.escrowDirectAddress?.slice(0, 24)}`
26325
+ );
26326
+ return;
26327
+ }
25690
26328
  const deps = this.deps;
25691
26329
  if (msg.invoice_type === "deposit") {
26330
+ logger.warn(
26331
+ LOG_TAG3,
26332
+ `diag_invoice_delivery_proceeding_to_import swap_id=${swapId.slice(0, 16)} progress=${swap.progress} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)}`
26333
+ );
25692
26334
  await this.withSwapGate(swapId, async () => {
25693
26335
  if (isTerminalProgress(swap.progress)) return;
25694
26336
  try {
25695
26337
  await deps.accounting.importInvoice(msg.invoice_token);
26338
+ logger.warn(
26339
+ LOG_TAG3,
26340
+ `diag_invoice_imported swap_id=${swapId.slice(0, 16)} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)} type=deposit`
26341
+ );
25696
26342
  } catch (err) {
25697
26343
  if (err instanceof SphereError && err.code === "INVOICE_ALREADY_EXISTS") {
25698
26344
  logger.debug(LOG_TAG3, `Deposit invoice for swap ${swapId} already imported \u2014 relay re-delivery, continuing`);
@@ -26331,6 +26977,65 @@ init_constants();
26331
26977
  var import_crypto_js6 = __toESM(require("crypto-js"), 1);
26332
26978
  init_errors();
26333
26979
  init_logger();
26980
+ var DEFAULT_ITERATIONS = 1e5;
26981
+ var KEY_SIZE = 256;
26982
+ var SALT_SIZE = 16;
26983
+ var IV_SIZE = 16;
26984
+ function deriveKey(password, salt, iterations) {
26985
+ return import_crypto_js6.default.PBKDF2(password, salt, {
26986
+ keySize: KEY_SIZE / 32,
26987
+ // WordArray uses 32-bit words
26988
+ iterations,
26989
+ hasher: import_crypto_js6.default.algo.SHA256
26990
+ });
26991
+ }
26992
+ function encrypt2(plaintext, password, options = {}) {
26993
+ const iterations = options.iterations ?? DEFAULT_ITERATIONS;
26994
+ const data = typeof plaintext === "string" ? plaintext : JSON.stringify(plaintext);
26995
+ const salt = import_crypto_js6.default.lib.WordArray.random(SALT_SIZE);
26996
+ const iv = import_crypto_js6.default.lib.WordArray.random(IV_SIZE);
26997
+ const key = deriveKey(password, salt, iterations);
26998
+ const encrypted = import_crypto_js6.default.AES.encrypt(data, key, {
26999
+ iv,
27000
+ mode: import_crypto_js6.default.mode.CBC,
27001
+ padding: import_crypto_js6.default.pad.Pkcs7
27002
+ });
27003
+ return {
27004
+ ciphertext: encrypted.ciphertext.toString(import_crypto_js6.default.enc.Base64),
27005
+ iv: iv.toString(import_crypto_js6.default.enc.Hex),
27006
+ salt: salt.toString(import_crypto_js6.default.enc.Hex),
27007
+ algorithm: "aes-256-cbc",
27008
+ kdf: "pbkdf2",
27009
+ iterations
27010
+ };
27011
+ }
27012
+ function decrypt2(encryptedData, password) {
27013
+ const salt = import_crypto_js6.default.enc.Hex.parse(encryptedData.salt);
27014
+ const iv = import_crypto_js6.default.enc.Hex.parse(encryptedData.iv);
27015
+ const key = deriveKey(password, salt, encryptedData.iterations);
27016
+ const ciphertext = import_crypto_js6.default.enc.Base64.parse(encryptedData.ciphertext);
27017
+ const cipherParams = import_crypto_js6.default.lib.CipherParams.create({
27018
+ ciphertext
27019
+ });
27020
+ const decrypted = import_crypto_js6.default.AES.decrypt(cipherParams, key, {
27021
+ iv,
27022
+ mode: import_crypto_js6.default.mode.CBC,
27023
+ padding: import_crypto_js6.default.pad.Pkcs7
27024
+ });
27025
+ const result = decrypted.toString(import_crypto_js6.default.enc.Utf8);
27026
+ if (!result) {
27027
+ throw new SphereError("Decryption failed: invalid password or corrupted data", "DECRYPTION_ERROR");
27028
+ }
27029
+ return result;
27030
+ }
27031
+ function decryptJson(encryptedData, password) {
27032
+ const decrypted = decrypt2(encryptedData, password);
27033
+ try {
27034
+ return JSON.parse(decrypted);
27035
+ } catch {
27036
+ throw new SphereError("Decryption failed: invalid JSON data", "DECRYPTION_ERROR");
27037
+ }
27038
+ }
26334
27039
  function encryptSimple(plaintext, password) {
26335
27040
  return import_crypto_js6.default.AES.encrypt(plaintext, password).toString();
26336
27041
  }
@@ -26357,6 +27062,12 @@ function decryptWithSalt(ciphertext, password, salt) {
26357
27062
  return null;
26358
27063
  }
26359
27064
  }
27065
+ function encryptMnemonic(mnemonic, password) {
27066
+ return encryptSimple(mnemonic, password);
27067
+ }
27068
+ function decryptMnemonic(encryptedMnemonic, password) {
27069
+ return decryptSimple(encryptedMnemonic, password);
27070
+ }
26360
27071
 
26361
27072
  // core/scan.ts
26362
27073
  init_logger();
@@ -27100,27 +27811,42 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
27100
27811
 
27101
27812
  // core/Sphere.ts
27102
27813
  var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
27814
+ var import_nostr_js_sdk5 = require("@unicitylabs/nostr-js-sdk");
27815
+
27816
+ // core/address-derivation.ts
27103
27817
  var import_TokenType5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
27104
27818
  var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
27105
27819
  var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
27106
- var import_nostr_js_sdk5 = require("@unicitylabs/nostr-js-sdk");
27820
+ var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
27821
+ var COMPRESSED_PUBKEY_RE = /^(02|03)[0-9a-fA-F]{64}$/;
27822
+ async function computeDirectAddressFromChainPubkey(chainPubkey) {
27823
+ if (typeof chainPubkey !== "string" || !COMPRESSED_PUBKEY_RE.test(chainPubkey)) {
27824
+ throw new Error(
27825
+ `computeDirectAddressFromChainPubkey: chainPubkey must be 66-char hex with 02/03 prefix, got "${String(chainPubkey).slice(0, 12)}..."`
27826
+ );
27827
+ }
27828
+ const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
27829
+ const tokenType = new import_TokenType5.TokenType(tokenTypeBytes);
27830
+ const publicKeyBytes = Buffer.from(chainPubkey, "hex");
27831
+ const predicateRef = await import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
27832
+ tokenType,
27833
+ "secp256k1",
27834
+ publicKeyBytes,
27835
+ import_HashAlgorithm7.HashAlgorithm.SHA256
27836
+ );
27837
+ return (await predicateRef.toAddress()).toString();
27838
+ }
27839
+
27840
+ // core/Sphere.ts
27107
27841
  function isValidNametag2(nametag) {
27108
27842
  if ((0, import_nostr_js_sdk5.isPhoneNumber)(nametag)) return true;
27109
27843
  return /^[a-z0-9_-]{3,20}$/.test(nametag);
27110
27844
  }
27111
- var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
27112
27845
  async function deriveL3PredicateAddress(privateKey) {
27113
27846
  const secret = Buffer.from(privateKey, "hex");
27114
27847
  const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
27115
- const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
27116
- const tokenType = new import_TokenType5.TokenType(tokenTypeBytes);
27117
- const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
27118
- tokenType,
27119
- signingService.algorithm,
27120
- signingService.publicKey,
27121
- import_HashAlgorithm7.HashAlgorithm.SHA256
27122
- );
27123
- return (await (await predicateRef).toAddress()).toString();
27848
+ const pubkeyHex = Buffer.from(signingService.publicKey).toString("hex");
27849
+ return computeDirectAddressFromChainPubkey(pubkeyHex);
27124
27850
  }
27125
27851
  var Sphere = class _Sphere {
27126
27852
  // Singleton
@@ -28574,6 +29300,9 @@ var Sphere = class _Sphere {
28574
29300
  this._transport.setFallbackSince(fallbackTs);
28575
29301
  }
28576
29302
  await this._transport.setIdentity(this._identity);
29303
+ if (this._transportMux && typeof this._transportMux.rebindToSharedClient === "function") {
29304
+ await this._transportMux.rebindToSharedClient();
29305
+ }
28577
29306
  this.emitEvent("identity:changed", {
28578
29307
  l1Address: this._identity.l1Address,
28579
29308
  directAddress: this._identity.directAddress,
@@ -28784,7 +29513,12 @@ var Sphere = class _Sphere {
28784
29513
  this._transportMux = new MultiAddressTransportMux({
28785
29514
  relays: nostrTransport.getConfiguredRelays(),
28786
29515
  createWebSocket: nostrTransport.getWebSocketFactory(),
28787
- storage: nostrTransport.getStorageAdapter() ?? void 0
29516
+ storage: nostrTransport.getStorageAdapter() ?? void 0,
29517
+ // #123: share the original transport's NostrClient instead of
29518
+ // opening a second WebSocket per relay. Pass a getter so the
29519
+ // Mux resolves it at connect-time (after the transport finishes
29520
+ // its own connect()).
29521
+ sharedNostrClient: typeof nostrTransport.getNostrClient === "function" ? () => nostrTransport.getNostrClient() : void 0
28788
29522
  });
28789
29523
  await this._transportMux.connect();
28790
29524
  if (typeof nostrTransport.suppressSubscriptions === "function") {
@@ -31123,8 +31857,38 @@ function createPriceProvider(config) {
31123
31857
  throw new SphereError(`Unsupported price platform: ${String(config.platform)}`, "INVALID_CONFIG");
31124
31858
  }
31125
31859
  }
31860
+
31861
+ // core/auth.ts
31862
+ var AuthVerificationError = class extends Error {
31863
+ code;
31864
+ constructor(message, code) {
31865
+ super(message);
31866
+ this.name = "AuthVerificationError";
31867
+ this.code = code;
31868
+ }
31869
+ };
31870
+ async function verifySphereAuth(input) {
31871
+ const { challenge, signature, chainPubkey } = input;
31872
+ let directAddress;
31873
+ try {
31874
+ directAddress = await computeDirectAddressFromChainPubkey(chainPubkey);
31875
+ } catch (err) {
31876
+ throw new AuthVerificationError(
31877
+ `chainPubkey is malformed: ${err.message}`,
31878
+ "PUBKEY_MALFORMED"
31879
+ );
31880
+ }
31881
+ if (!verifySignedMessage(challenge, signature, chainPubkey)) {
31882
+ throw new AuthVerificationError(
31883
+ "Signature does not verify against chainPubkey",
31884
+ "SIGNATURE_INVALID"
31885
+ );
31886
+ }
31887
+ return { chainPubkey, directAddress };
31888
+ }
31126
31889
  // Annotate the CommonJS export names for ESM import in node:
31127
31890
  0 && (module.exports = {
31891
+ AuthVerificationError,
31128
31892
  COIN_TYPES,
31129
31893
  CoinGeckoPriceProvider,
31130
31894
  CommunicationsModule,
@@ -31174,6 +31938,8 @@ function createPriceProvider(config) {
31174
31938
  buildTxfStorageData,
31175
31939
  bytesToHex,
31176
31940
  checkNetworkHealth,
31941
+ coinIdsMatch,
31942
+ computeDirectAddressFromChainPubkey,
31177
31943
  computeSwapId,
31178
31944
  countCommittedTransactions,
31179
31945
  createAddress,
@@ -31192,22 +31958,34 @@ function createPriceProvider(config) {
31192
31958
  createSwapModule,
31193
31959
  createTokenValidator,
31194
31960
  decodeBech32,
31961
+ decrypt,
31195
31962
  decryptCMasterKey,
31963
+ decryptJson,
31964
+ decryptMnemonic,
31196
31965
  decryptNametag,
31197
31966
  decryptPrivateKey,
31967
+ decryptSimple,
31198
31968
  decryptTextFormatKey,
31969
+ decryptWallet,
31970
+ decryptWithSalt,
31199
31971
  deriveAddressInfo,
31200
31972
  deriveChildKey,
31201
31973
  deriveKeyAtPath,
31202
31974
  doubleSha256,
31203
31975
  encodeBech32,
31976
+ encrypt,
31977
+ encryptMnemonic,
31204
31978
  encryptNametag,
31979
+ encryptSimple,
31980
+ encryptWallet,
31205
31981
  extractFromText,
31206
31982
  findPattern,
31207
31983
  forkedKeyFromTokenIdAndState,
31208
31984
  formatAmount,
31985
+ generateAddressFromMasterKey,
31209
31986
  generateMasterKey,
31210
31987
  generateMnemonic,
31988
+ generatePrivateKey,
31211
31989
  getAddressHrp,
31212
31990
  getAddressId,
31213
31991
  getAddressStorageKey,
@@ -31230,6 +32008,7 @@ function createPriceProvider(config) {
31230
32008
  hashNametag,
31231
32009
  hashSignMessage,
31232
32010
  hexToBytes,
32011
+ hexToWIF,
31233
32012
  identityFromMnemonicSync,
31234
32013
  initSphere,
31235
32014
  isArchivedKey,
@@ -31259,6 +32038,7 @@ function createPriceProvider(config) {
31259
32038
  logger,
31260
32039
  mnemonicToSeedSync,
31261
32040
  normalizeAddress,
32041
+ normalizeCoinId,
31262
32042
  normalizeNametag,
31263
32043
  normalizeSdkTokenToStorage,
31264
32044
  objectToTxf,
@@ -31289,6 +32069,7 @@ function createPriceProvider(config) {
31289
32069
  verifyManifestIntegrity,
31290
32070
  verifyNametagBinding,
31291
32071
  verifySignedMessage,
32072
+ verifySphereAuth,
31292
32073
  verifySwapSignature
31293
32074
  });
31294
32075
  /*! Bundled license information: