@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/README.md +63 -77
- package/dist/core/index.cjs +771 -118
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +187 -6
- package/dist/core/index.d.ts +187 -6
- package/dist/core/index.js +770 -118
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +11 -0
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +11 -0
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +11 -0
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +10 -1
- package/dist/impl/nodejs/index.d.ts +10 -1
- package/dist/impl/nodejs/index.js +11 -0
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +899 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +258 -1
- package/dist/index.d.ts +258 -1
- package/dist/index.js +881 -118
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
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.
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21785
|
-
|
|
21786
|
-
invoiceId
|
|
21787
|
-
|
|
21788
|
-
|
|
21789
|
-
|
|
21790
|
-
|
|
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
|
-
|
|
24953
|
-
|
|
24954
|
-
|
|
24955
|
-
|
|
24956
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
25140
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
25689
|
-
|
|
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
|
|
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
|
|
27116
|
-
|
|
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:
|