@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/core/index.cjs
CHANGED
|
@@ -881,6 +881,7 @@ __export(core_exports, {
|
|
|
881
881
|
base58Encode: () => base58Encode,
|
|
882
882
|
bytesToHex: () => bytesToHex3,
|
|
883
883
|
checkNetworkHealth: () => checkNetworkHealth,
|
|
884
|
+
computeDirectAddressFromChainPubkey: () => computeDirectAddressFromChainPubkey,
|
|
884
885
|
computeHash160: () => computeHash160,
|
|
885
886
|
convertBits: () => convertBits,
|
|
886
887
|
createAddress: () => createAddress,
|
|
@@ -1445,6 +1446,17 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1445
1446
|
getStorageAdapter() {
|
|
1446
1447
|
return this.storage;
|
|
1447
1448
|
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Get the underlying NostrClient (or null if not yet connected).
|
|
1451
|
+
*
|
|
1452
|
+
* Exposed so {@link MultiAddressTransportMux} can share the same
|
|
1453
|
+
* client/socket pair instead of opening a duplicate WebSocket per
|
|
1454
|
+
* relay (#123). The transport owns the client's lifecycle — callers
|
|
1455
|
+
* MUST NOT call {@code disconnect()} on the returned instance.
|
|
1456
|
+
*/
|
|
1457
|
+
getNostrClient() {
|
|
1458
|
+
return this.nostrClient;
|
|
1459
|
+
}
|
|
1448
1460
|
/**
|
|
1449
1461
|
* Suppress event subscriptions — unsubscribe wallet/chat filters
|
|
1450
1462
|
* but keep the connection alive for resolve/identity-binding operations.
|
|
@@ -3083,9 +3095,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3083
3095
|
chatSubscriptionId = null;
|
|
3084
3096
|
chatEoseFired = false;
|
|
3085
3097
|
resubscribeTimer = null;
|
|
3086
|
-
lastWalletEventAt = Date.now();
|
|
3087
|
-
lastChatEventAt = Date.now();
|
|
3088
|
-
healthCheckTimer = null;
|
|
3089
3098
|
chatEoseHandlers = [];
|
|
3090
3099
|
// Dedup — bounded to prevent memory leak in long-running sessions.
|
|
3091
3100
|
// Set preserves insertion order; evict oldest entries when cap is reached.
|
|
@@ -3096,6 +3105,19 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3096
3105
|
// Identity key for the Mux's NostrClient — relays may filter gift-wrap
|
|
3097
3106
|
// delivery to the recipient's subscription key.
|
|
3098
3107
|
identityPrivateKey;
|
|
3108
|
+
// Resolves the shared NostrClient at use-time (the source provider may
|
|
3109
|
+
// create its client lazily, after the Mux is constructed). null means
|
|
3110
|
+
// "no shared client; create our own."
|
|
3111
|
+
sharedNostrClientGetter;
|
|
3112
|
+
// True when this Mux is using a shared NostrClient and therefore must
|
|
3113
|
+
// not call connect()/disconnect() on it.
|
|
3114
|
+
usingSharedClient = false;
|
|
3115
|
+
// Listener registered on the underlying NostrClient. Tracked so we can
|
|
3116
|
+
// remove it on disconnect / rebind — otherwise a long-lived shared
|
|
3117
|
+
// client accumulates listeners across address switches and (worse)
|
|
3118
|
+
// a "disconnected" Mux still sees onReconnected callbacks fire and
|
|
3119
|
+
// re-establish subscriptions it shouldn't have.
|
|
3120
|
+
connectionListener = null;
|
|
3099
3121
|
constructor(config) {
|
|
3100
3122
|
this.identityPrivateKey = config.identityPrivateKey;
|
|
3101
3123
|
this.config = {
|
|
@@ -3108,6 +3130,14 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3108
3130
|
generateUUID: config.generateUUID ?? defaultUUIDGenerator
|
|
3109
3131
|
};
|
|
3110
3132
|
this.storage = config.storage ?? null;
|
|
3133
|
+
if (typeof config.sharedNostrClient === "function") {
|
|
3134
|
+
this.sharedNostrClientGetter = config.sharedNostrClient;
|
|
3135
|
+
} else if (config.sharedNostrClient) {
|
|
3136
|
+
const c = config.sharedNostrClient;
|
|
3137
|
+
this.sharedNostrClientGetter = () => c;
|
|
3138
|
+
} else {
|
|
3139
|
+
this.sharedNostrClientGetter = null;
|
|
3140
|
+
}
|
|
3111
3141
|
}
|
|
3112
3142
|
// ===========================================================================
|
|
3113
3143
|
// Address Management
|
|
@@ -3198,53 +3228,49 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3198
3228
|
if (this.status === "connected") return;
|
|
3199
3229
|
this.status = "connecting";
|
|
3200
3230
|
try {
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3231
|
+
const shared = this.sharedNostrClientGetter ? this.sharedNostrClientGetter() : null;
|
|
3232
|
+
if (shared) {
|
|
3233
|
+
if (!shared.isConnected()) {
|
|
3234
|
+
throw new SphereError(
|
|
3235
|
+
"sharedNostrClient is not connected; the Mux cannot share a closed socket",
|
|
3236
|
+
"TRANSPORT_ERROR"
|
|
3205
3237
|
);
|
|
3206
|
-
} else {
|
|
3207
|
-
const tempKey = import_buffer2.Buffer.alloc(32);
|
|
3208
|
-
crypto.getRandomValues(tempKey);
|
|
3209
|
-
this.primaryKeyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(tempKey);
|
|
3210
3238
|
}
|
|
3239
|
+
this.nostrClient = shared;
|
|
3240
|
+
this.usingSharedClient = true;
|
|
3241
|
+
} else {
|
|
3242
|
+
if (!this.primaryKeyManager) {
|
|
3243
|
+
if (this.identityPrivateKey) {
|
|
3244
|
+
this.primaryKeyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(
|
|
3245
|
+
import_buffer2.Buffer.from(this.identityPrivateKey)
|
|
3246
|
+
);
|
|
3247
|
+
} else {
|
|
3248
|
+
const tempKey = import_buffer2.Buffer.alloc(32);
|
|
3249
|
+
crypto.getRandomValues(tempKey);
|
|
3250
|
+
this.primaryKeyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(tempKey);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
this.nostrClient = new import_nostr_js_sdk2.NostrClient(this.primaryKeyManager, {
|
|
3254
|
+
autoReconnect: this.config.autoReconnect,
|
|
3255
|
+
reconnectIntervalMs: this.config.reconnectDelay,
|
|
3256
|
+
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
3257
|
+
pingIntervalMs: 15e3
|
|
3258
|
+
});
|
|
3211
3259
|
}
|
|
3212
|
-
this.
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
},
|
|
3226
|
-
onReconnecting: (url, attempt) => {
|
|
3227
|
-
logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
|
|
3228
|
-
this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
|
|
3229
|
-
},
|
|
3230
|
-
onReconnected: (url) => {
|
|
3231
|
-
logger.debug("Mux", "Reconnected to relay:", url);
|
|
3232
|
-
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
3233
|
-
this.updateSubscriptions().catch((err) => {
|
|
3234
|
-
logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
|
|
3235
|
-
});
|
|
3260
|
+
this.connectionListener = this.buildConnectionListener();
|
|
3261
|
+
this.nostrClient.addConnectionListener(this.connectionListener);
|
|
3262
|
+
if (!this.usingSharedClient) {
|
|
3263
|
+
await Promise.race([
|
|
3264
|
+
this.nostrClient.connect(...this.config.relays),
|
|
3265
|
+
new Promise(
|
|
3266
|
+
(_, reject) => setTimeout(() => reject(new Error(
|
|
3267
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
3268
|
+
)), this.config.timeout)
|
|
3269
|
+
)
|
|
3270
|
+
]);
|
|
3271
|
+
if (!this.nostrClient.isConnected()) {
|
|
3272
|
+
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
3236
3273
|
}
|
|
3237
|
-
});
|
|
3238
|
-
await Promise.race([
|
|
3239
|
-
this.nostrClient.connect(...this.config.relays),
|
|
3240
|
-
new Promise(
|
|
3241
|
-
(_, reject) => setTimeout(() => reject(new Error(
|
|
3242
|
-
`Transport connection timed out after ${this.config.timeout}ms`
|
|
3243
|
-
)), this.config.timeout)
|
|
3244
|
-
)
|
|
3245
|
-
]);
|
|
3246
|
-
if (!this.nostrClient.isConnected()) {
|
|
3247
|
-
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
3248
3274
|
}
|
|
3249
3275
|
this.status = "connected";
|
|
3250
3276
|
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
@@ -3253,6 +3279,21 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3253
3279
|
}
|
|
3254
3280
|
} catch (error) {
|
|
3255
3281
|
this.status = "error";
|
|
3282
|
+
if (this.connectionListener && this.nostrClient) {
|
|
3283
|
+
try {
|
|
3284
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3285
|
+
} catch {
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
this.connectionListener = null;
|
|
3289
|
+
if (this.nostrClient && !this.usingSharedClient) {
|
|
3290
|
+
try {
|
|
3291
|
+
this.nostrClient.disconnect();
|
|
3292
|
+
} catch {
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
this.nostrClient = null;
|
|
3296
|
+
this.usingSharedClient = false;
|
|
3256
3297
|
throw error;
|
|
3257
3298
|
}
|
|
3258
3299
|
}
|
|
@@ -3261,25 +3302,153 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3261
3302
|
clearTimeout(this.resubscribeTimer);
|
|
3262
3303
|
this.resubscribeTimer = null;
|
|
3263
3304
|
}
|
|
3264
|
-
if (this.healthCheckTimer) {
|
|
3265
|
-
clearInterval(this.healthCheckTimer);
|
|
3266
|
-
this.healthCheckTimer = null;
|
|
3267
|
-
}
|
|
3268
3305
|
if (this.nostrClient) {
|
|
3269
|
-
this.
|
|
3306
|
+
if (this.walletSubscriptionId) {
|
|
3307
|
+
try {
|
|
3308
|
+
this.nostrClient.unsubscribe(this.walletSubscriptionId);
|
|
3309
|
+
} catch {
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
if (this.chatSubscriptionId) {
|
|
3313
|
+
try {
|
|
3314
|
+
this.nostrClient.unsubscribe(this.chatSubscriptionId);
|
|
3315
|
+
} catch {
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
if (this.connectionListener) {
|
|
3319
|
+
try {
|
|
3320
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3321
|
+
} catch {
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
if (!this.usingSharedClient) {
|
|
3325
|
+
this.nostrClient.disconnect();
|
|
3326
|
+
}
|
|
3270
3327
|
this.nostrClient = null;
|
|
3271
3328
|
}
|
|
3329
|
+
this.connectionListener = null;
|
|
3330
|
+
this.usingSharedClient = false;
|
|
3272
3331
|
this.walletSubscriptionId = null;
|
|
3273
3332
|
this.chatSubscriptionId = null;
|
|
3274
3333
|
this.chatEoseFired = false;
|
|
3275
|
-
this.lastWalletEventAt = Date.now();
|
|
3276
|
-
this.lastChatEventAt = Date.now();
|
|
3277
3334
|
this.status = "disconnected";
|
|
3278
3335
|
this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
|
|
3279
3336
|
}
|
|
3280
3337
|
isConnected() {
|
|
3281
3338
|
return this.status === "connected" && this.nostrClient?.isConnected() === true;
|
|
3282
3339
|
}
|
|
3340
|
+
/**
|
|
3341
|
+
* Build the connection listener used by both {@link connect} and
|
|
3342
|
+
* {@link rebindToSharedClient}.
|
|
3343
|
+
*
|
|
3344
|
+
* Behavioral notes:
|
|
3345
|
+
* - When the Mux is sharing a {@link NostrClient} with the host
|
|
3346
|
+
* transport (#123), we deliberately do NOT emit
|
|
3347
|
+
* {@code transport:connected} / {@code transport:reconnecting} here
|
|
3348
|
+
* — the host transport's own listener already emits those for the
|
|
3349
|
+
* same socket event. Re-subscribing after a reconnect IS still our
|
|
3350
|
+
* responsibility, since the host has
|
|
3351
|
+
* {@code suppressSubscriptions()}'d its own filters.
|
|
3352
|
+
* - {@code onConnect} does not emit {@code transport:connected}.
|
|
3353
|
+
* The SDK only fires {@code onConnect} on the initial socket
|
|
3354
|
+
* connection (subsequent reconnects use {@code onReconnected}),
|
|
3355
|
+
* and {@link connect()}'s bottom already emits
|
|
3356
|
+
* {@code transport:connected} once that returns. Emitting here too
|
|
3357
|
+
* would double-fire on every initial connect.
|
|
3358
|
+
* - Each callback bails out early when the Mux is not in an active
|
|
3359
|
+
* state ({@code disconnected} / {@code error}). Listeners are
|
|
3360
|
+
* removed on {@code disconnect()} before the callback can fire,
|
|
3361
|
+
* so this guard is mainly defense-in-depth against any in-flight
|
|
3362
|
+
* callback that lands during teardown — but having it at the top
|
|
3363
|
+
* means we never emit a misleading {@code transport:connected}
|
|
3364
|
+
* from a Mux that has already torn down.
|
|
3365
|
+
*/
|
|
3366
|
+
buildConnectionListener() {
|
|
3367
|
+
const isInactive = () => this.status === "disconnected" || this.status === "error";
|
|
3368
|
+
return {
|
|
3369
|
+
onConnect: (url) => {
|
|
3370
|
+
if (isInactive()) return;
|
|
3371
|
+
logger.debug("Mux", "Connected to relay:", url);
|
|
3372
|
+
},
|
|
3373
|
+
onDisconnect: (url, reason) => {
|
|
3374
|
+
logger.debug("Mux", "Disconnected from relay:", url, "reason:", reason);
|
|
3375
|
+
},
|
|
3376
|
+
onReconnecting: (url, attempt) => {
|
|
3377
|
+
if (isInactive()) return;
|
|
3378
|
+
logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
|
|
3379
|
+
if (!this.usingSharedClient) {
|
|
3380
|
+
this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
|
|
3381
|
+
}
|
|
3382
|
+
},
|
|
3383
|
+
onReconnected: (url) => {
|
|
3384
|
+
if (isInactive()) return;
|
|
3385
|
+
logger.debug("Mux", "Reconnected to relay:", url);
|
|
3386
|
+
if (!this.usingSharedClient) {
|
|
3387
|
+
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
3388
|
+
}
|
|
3389
|
+
this.updateSubscriptions().catch((err) => {
|
|
3390
|
+
logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
/**
|
|
3396
|
+
* Re-attach to a freshly-created shared NostrClient.
|
|
3397
|
+
*
|
|
3398
|
+
* Call this after the host (e.g. {@link NostrTransportProvider}) has
|
|
3399
|
+
* recreated its NostrClient — typically because the wallet's active
|
|
3400
|
+
* identity changed and the SDK's NostrClient does not support
|
|
3401
|
+
* changing identity at runtime. The previous client has already
|
|
3402
|
+
* been disconnected by the host, so its server-side subscriptions
|
|
3403
|
+
* are gone — we just adopt the new client and re-issue our own.
|
|
3404
|
+
*
|
|
3405
|
+
* The caller is responsible for ordering: by the time rebind runs,
|
|
3406
|
+
* the host transport's new NostrClient must already be created and
|
|
3407
|
+
* connected. In Sphere this is guaranteed because we await
|
|
3408
|
+
* {@code transport.setIdentity()} before calling rebind.
|
|
3409
|
+
*
|
|
3410
|
+
* Returns silently in two cases that are not caller errors:
|
|
3411
|
+
* - the Mux owns its own client (not sharing) — nothing to rebind
|
|
3412
|
+
* - the shared client reference hasn't changed (rebind is a no-op)
|
|
3413
|
+
*
|
|
3414
|
+
* Throws otherwise (rather than silently no-op'ing) so a wiring
|
|
3415
|
+
* mistake — for instance, calling rebind before the host's new
|
|
3416
|
+
* client is ready — surfaces immediately instead of leaving the
|
|
3417
|
+
* Mux pinned to a stale client.
|
|
3418
|
+
*/
|
|
3419
|
+
async rebindToSharedClient() {
|
|
3420
|
+
if (!this.usingSharedClient) return;
|
|
3421
|
+
if (!this.sharedNostrClientGetter) return;
|
|
3422
|
+
const newClient = this.sharedNostrClientGetter();
|
|
3423
|
+
if (!newClient) {
|
|
3424
|
+
throw new SphereError(
|
|
3425
|
+
"rebindToSharedClient: shared client getter returned null. The host transport must finish (re)creating its NostrClient before rebind is called.",
|
|
3426
|
+
"TRANSPORT_ERROR"
|
|
3427
|
+
);
|
|
3428
|
+
}
|
|
3429
|
+
if (this.nostrClient === newClient) return;
|
|
3430
|
+
if (!newClient.isConnected()) {
|
|
3431
|
+
throw new SphereError(
|
|
3432
|
+
"rebindToSharedClient: new shared client is not connected. Await transport.setIdentity() / transport.connect() before rebinding.",
|
|
3433
|
+
"TRANSPORT_ERROR"
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
3436
|
+
if (this.nostrClient && this.connectionListener && this.nostrClient !== newClient) {
|
|
3437
|
+
try {
|
|
3438
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3439
|
+
} catch {
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
this.nostrClient = newClient;
|
|
3443
|
+
this.walletSubscriptionId = null;
|
|
3444
|
+
this.chatSubscriptionId = null;
|
|
3445
|
+
this.chatEoseFired = false;
|
|
3446
|
+
this.connectionListener = this.buildConnectionListener();
|
|
3447
|
+
this.nostrClient.addConnectionListener(this.connectionListener);
|
|
3448
|
+
if (this.addresses.size > 0) {
|
|
3449
|
+
await this.updateSubscriptions();
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3283
3452
|
/**
|
|
3284
3453
|
* One-shot fetch of pending events from the relay.
|
|
3285
3454
|
* Creates a temporary subscription, waits for EOSE (or timeout),
|
|
@@ -3408,8 +3577,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3408
3577
|
this.nostrClient.unsubscribe(this.chatSubscriptionId);
|
|
3409
3578
|
this.chatSubscriptionId = null;
|
|
3410
3579
|
}
|
|
3411
|
-
this.lastWalletEventAt = Date.now();
|
|
3412
|
-
this.lastChatEventAt = Date.now();
|
|
3413
3580
|
if (this.addresses.size === 0) return;
|
|
3414
3581
|
const allPubkeys = [];
|
|
3415
3582
|
for (const entry of this.addresses.values()) {
|
|
@@ -3496,25 +3663,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3496
3663
|
}
|
|
3497
3664
|
});
|
|
3498
3665
|
logger.debug("Mux", `updateSubscriptions: walletSub=${this.walletSubscriptionId} chatSub=${this.chatSubscriptionId}`);
|
|
3499
|
-
this.startHealthCheck();
|
|
3500
|
-
}
|
|
3501
|
-
startHealthCheck() {
|
|
3502
|
-
if (this.healthCheckTimer) return;
|
|
3503
|
-
this.healthCheckTimer = setInterval(() => {
|
|
3504
|
-
if (!this.isConnected()) return;
|
|
3505
|
-
const chatElapsed = Date.now() - this.lastChatEventAt;
|
|
3506
|
-
const walletElapsed = Date.now() - this.lastWalletEventAt;
|
|
3507
|
-
const needResubscribe = chatElapsed > 6e4 || walletElapsed > 3e5;
|
|
3508
|
-
if (needResubscribe) {
|
|
3509
|
-
const reason = chatElapsed > 6e4 ? `No chat events for ${Math.round(chatElapsed / 1e3)}s` : `No wallet events for ${Math.round(walletElapsed / 1e3)}s`;
|
|
3510
|
-
logger.warn("Mux", `${reason} \u2014 re-subscribing`);
|
|
3511
|
-
this.lastChatEventAt = Date.now();
|
|
3512
|
-
this.lastWalletEventAt = Date.now();
|
|
3513
|
-
this.updateSubscriptions().catch((err) => {
|
|
3514
|
-
logger.warn("Mux", "Health check re-subscription failed:", err);
|
|
3515
|
-
});
|
|
3516
|
-
}
|
|
3517
|
-
}, 3e4);
|
|
3518
3666
|
}
|
|
3519
3667
|
/**
|
|
3520
3668
|
* Schedule a re-subscription after a relay-initiated subscription closure.
|
|
@@ -3575,12 +3723,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3575
3723
|
}
|
|
3576
3724
|
}
|
|
3577
3725
|
}
|
|
3578
|
-
if (event.kind !== import_nostr_js_sdk2.EventKinds.GIFT_WRAP) {
|
|
3579
|
-
this.lastWalletEventAt = Date.now();
|
|
3580
|
-
}
|
|
3581
|
-
if (event.kind === import_nostr_js_sdk2.EventKinds.GIFT_WRAP) {
|
|
3582
|
-
this.lastChatEventAt = Date.now();
|
|
3583
|
-
}
|
|
3584
3726
|
try {
|
|
3585
3727
|
if (event.kind === import_nostr_js_sdk2.EventKinds.GIFT_WRAP) {
|
|
3586
3728
|
await this.routeGiftWrap(event);
|
|
@@ -12023,6 +12165,132 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
12023
12165
|
};
|
|
12024
12166
|
}
|
|
12025
12167
|
}
|
|
12168
|
+
/**
|
|
12169
|
+
* Mint a fungible token directly to this wallet (genesis mint).
|
|
12170
|
+
*
|
|
12171
|
+
* Useful for test setups that need to seed a wallet with specific token
|
|
12172
|
+
* balances WITHOUT depending on the testnet faucet HTTP service. The
|
|
12173
|
+
* resulting token has the canonical CoinId bytes (passed in `coinIdHex`)
|
|
12174
|
+
* — when those bytes match a registered symbol in the TokenRegistry,
|
|
12175
|
+
* the token shows up under the symbol's name (e.g. "UCT"). There is no
|
|
12176
|
+
* cryptographic restriction on which key may issue a given CoinId; the
|
|
12177
|
+
* aggregator records the mint regardless of issuer identity.
|
|
12178
|
+
*
|
|
12179
|
+
* The flow:
|
|
12180
|
+
* 1. Generate a random TokenId.
|
|
12181
|
+
* 2. Build TokenCoinData with [(coinId, amount)].
|
|
12182
|
+
* 3. Build MintTransactionData with recipient = self (UnmaskedPredicate
|
|
12183
|
+
* from this wallet's signing service).
|
|
12184
|
+
* 4. Submit MintCommitment to the aggregator.
|
|
12185
|
+
* 5. Wait for the inclusion proof.
|
|
12186
|
+
* 6. Construct an SDK Token via Token.mint().
|
|
12187
|
+
* 7. Convert to wallet Token format and call addToken().
|
|
12188
|
+
*
|
|
12189
|
+
* @param coinIdHex - 64-char lowercase hex CoinId. Must match the bytes
|
|
12190
|
+
* used by the registered symbol if you want the wallet to recognize
|
|
12191
|
+
* the token as that symbol (e.g. UCT's coinId from the public registry).
|
|
12192
|
+
* @param amount - Amount in smallest units (multiply by 10^decimals
|
|
12193
|
+
* when converting from human values).
|
|
12194
|
+
* @returns Result with the resulting wallet Token and its on-chain id.
|
|
12195
|
+
*/
|
|
12196
|
+
async mintFungibleToken(coinIdHex, amount) {
|
|
12197
|
+
this.ensureInitialized();
|
|
12198
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
12199
|
+
if (!stClient) {
|
|
12200
|
+
return { success: false, error: "State transition client not available" };
|
|
12201
|
+
}
|
|
12202
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
12203
|
+
if (!trustBase) {
|
|
12204
|
+
return { success: false, error: "Trust base not available" };
|
|
12205
|
+
}
|
|
12206
|
+
try {
|
|
12207
|
+
const signingService = await this.createSigningService();
|
|
12208
|
+
const { TokenId: TokenId5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
12209
|
+
const { TokenCoinData: TokenCoinData3 } = await import("@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData");
|
|
12210
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
12211
|
+
const tokenTypeBytes = fromHex4("f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509");
|
|
12212
|
+
const tokenType = new import_TokenType3.TokenType(tokenTypeBytes);
|
|
12213
|
+
const tokenIdBytes = new Uint8Array(32);
|
|
12214
|
+
crypto.getRandomValues(tokenIdBytes);
|
|
12215
|
+
const tokenId = new TokenId5(tokenIdBytes);
|
|
12216
|
+
const coinIdBytes = fromHex4(coinIdHex);
|
|
12217
|
+
const coinId = new import_CoinId4.CoinId(coinIdBytes);
|
|
12218
|
+
const coinData = TokenCoinData3.create([[coinId, amount]]);
|
|
12219
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
12220
|
+
tokenType,
|
|
12221
|
+
signingService.algorithm,
|
|
12222
|
+
signingService.publicKey,
|
|
12223
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256
|
|
12224
|
+
);
|
|
12225
|
+
const ownerAddress = await addressRef.toAddress();
|
|
12226
|
+
const salt = new Uint8Array(32);
|
|
12227
|
+
crypto.getRandomValues(salt);
|
|
12228
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.create(
|
|
12229
|
+
tokenId,
|
|
12230
|
+
tokenType,
|
|
12231
|
+
null,
|
|
12232
|
+
// tokenData: no metadata
|
|
12233
|
+
coinData,
|
|
12234
|
+
// fungible coin data
|
|
12235
|
+
ownerAddress,
|
|
12236
|
+
// recipient = self
|
|
12237
|
+
salt,
|
|
12238
|
+
null,
|
|
12239
|
+
// recipientDataHash
|
|
12240
|
+
null
|
|
12241
|
+
// reason: null (genesis, no burn predecessor)
|
|
12242
|
+
);
|
|
12243
|
+
const commitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
12244
|
+
const MAX_RETRIES = 3;
|
|
12245
|
+
let lastStatus;
|
|
12246
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
12247
|
+
const response = await stClient.submitMintCommitment(commitment);
|
|
12248
|
+
lastStatus = response.status;
|
|
12249
|
+
if (response.status === "SUCCESS" || response.status === "REQUEST_ID_EXISTS") break;
|
|
12250
|
+
if (attempt === MAX_RETRIES) {
|
|
12251
|
+
return { success: false, error: `Mint submit failed after ${MAX_RETRIES} attempts: ${response.status}` };
|
|
12252
|
+
}
|
|
12253
|
+
await new Promise((r) => setTimeout(r, 1e3 * attempt));
|
|
12254
|
+
}
|
|
12255
|
+
if (lastStatus !== "SUCCESS" && lastStatus !== "REQUEST_ID_EXISTS") {
|
|
12256
|
+
return { success: false, error: `Mint submit failed: ${lastStatus}` };
|
|
12257
|
+
}
|
|
12258
|
+
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
12259
|
+
const genesisTransaction = commitment.toTransaction(inclusionProof);
|
|
12260
|
+
const predicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
|
|
12261
|
+
tokenId,
|
|
12262
|
+
tokenType,
|
|
12263
|
+
signingService,
|
|
12264
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256,
|
|
12265
|
+
salt
|
|
12266
|
+
);
|
|
12267
|
+
const tokenState = new import_TokenState5.TokenState(predicate, null);
|
|
12268
|
+
const sdkToken = await import_Token6.Token.mint(trustBase, tokenState, genesisTransaction);
|
|
12269
|
+
const tokenIdHex = tokenId.toJSON();
|
|
12270
|
+
const symbol = this.getCoinSymbol(coinIdHex);
|
|
12271
|
+
const name = this.getCoinName(coinIdHex);
|
|
12272
|
+
const decimals = this.getCoinDecimals(coinIdHex);
|
|
12273
|
+
const iconUrl = this.getCoinIconUrl(coinIdHex);
|
|
12274
|
+
const uiToken = {
|
|
12275
|
+
id: tokenIdHex,
|
|
12276
|
+
coinId: coinIdHex,
|
|
12277
|
+
symbol,
|
|
12278
|
+
name,
|
|
12279
|
+
decimals,
|
|
12280
|
+
...iconUrl !== void 0 ? { iconUrl } : {},
|
|
12281
|
+
amount: amount.toString(),
|
|
12282
|
+
status: "confirmed",
|
|
12283
|
+
createdAt: Date.now(),
|
|
12284
|
+
updatedAt: Date.now(),
|
|
12285
|
+
sdkData: JSON.stringify(sdkToken.toJSON())
|
|
12286
|
+
};
|
|
12287
|
+
await this.addToken(uiToken);
|
|
12288
|
+
return { success: true, token: uiToken, tokenId: tokenIdHex };
|
|
12289
|
+
} catch (err) {
|
|
12290
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12291
|
+
return { success: false, error: `Local mint failed: ${msg}` };
|
|
12292
|
+
}
|
|
12293
|
+
}
|
|
12026
12294
|
/**
|
|
12027
12295
|
* Check if a nametag is available for minting
|
|
12028
12296
|
* @param nametag - The nametag to check (e.g., "alice" or "@alice")
|
|
@@ -18229,6 +18497,24 @@ var AccountingModule = class _AccountingModule {
|
|
|
18229
18497
|
dirtyLedgerEntries = /* @__PURE__ */ new Set();
|
|
18230
18498
|
/** Count of unknown (not in invoiceTermsCache) invoice IDs in the ledger. */
|
|
18231
18499
|
unknownLedgerCount = 0;
|
|
18500
|
+
/**
|
|
18501
|
+
* Per-unknown-invoice first-seen timestamp for TTL eviction.
|
|
18502
|
+
*
|
|
18503
|
+
* W1 (steelman round-4): without TTL, an attacker who can deliver 500
|
|
18504
|
+
* inbound transfers with synthesized memo invoiceIds permanently exhausts
|
|
18505
|
+
* the unknown-ledger cap, after which legitimate orphan transfers (out-of-
|
|
18506
|
+
* order delivery for real swaps) are silently dropped at the cap-check.
|
|
18507
|
+
*
|
|
18508
|
+
* Round-5 perf: gated by `unknownLedgerNextSweepMs` to amortize the
|
|
18509
|
+
* sweep cost. The naive every-call sweep is O(N) where N=cap=500;
|
|
18510
|
+
* combined with the per-token cleanup loop inside the sweep it became
|
|
18511
|
+
* O(N×M) on every transfer under flood. Now we sweep at most every
|
|
18512
|
+
* `UNKNOWN_LEDGER_SWEEP_INTERVAL_MS` (60s) UNLESS the cap is currently
|
|
18513
|
+
* full, in which case we sweep on each call (the only path that can
|
|
18514
|
+
* actually drop a legitimate orphan).
|
|
18515
|
+
*/
|
|
18516
|
+
unknownLedgerFirstSeen = /* @__PURE__ */ new Map();
|
|
18517
|
+
unknownLedgerNextSweepMs = 0;
|
|
18232
18518
|
/** W17: Tracks whether tokenScanState has been mutated since last flush. */
|
|
18233
18519
|
tokenScanDirty = false;
|
|
18234
18520
|
/** W2 fix: Serialization guard for _flushDirtyLedgerEntries. */
|
|
@@ -19219,6 +19505,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
19219
19505
|
}
|
|
19220
19506
|
if (this.invoiceLedger.has(tokenId) && !this.invoiceTermsCache.has(tokenId)) {
|
|
19221
19507
|
this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
|
|
19508
|
+
this.unknownLedgerFirstSeen.delete(tokenId);
|
|
19222
19509
|
}
|
|
19223
19510
|
this.invoiceTermsCache.set(tokenId, terms);
|
|
19224
19511
|
this._addToHashIndex(tokenId);
|
|
@@ -19487,6 +19774,29 @@ var AccountingModule = class _AccountingModule {
|
|
|
19487
19774
|
closed: this.closedInvoices.has(invoiceId)
|
|
19488
19775
|
};
|
|
19489
19776
|
}
|
|
19777
|
+
/**
|
|
19778
|
+
* Return the set of token IDs that are currently linked to the given
|
|
19779
|
+
* invoice. Populated by both the on-chain `_processTokenTransactions`
|
|
19780
|
+
* path (tokens with `inv:` references) and the transport-memo orphan
|
|
19781
|
+
* buffering path in `_handleIncomingTransfer`.
|
|
19782
|
+
*
|
|
19783
|
+
* Used by callers that want to scope per-invoice operations (e.g.
|
|
19784
|
+
* SwapModule.verifyPayout's L3 validation) to only the tokens that
|
|
19785
|
+
* cover this invoice — avoiding false negatives when the wallet
|
|
19786
|
+
* contains unrelated tokens of the same currency in unconfirmed or
|
|
19787
|
+
* spent state.
|
|
19788
|
+
*
|
|
19789
|
+
* Returns an empty set if no tokens are currently linked.
|
|
19790
|
+
*/
|
|
19791
|
+
getTokenIdsForInvoice(invoiceId) {
|
|
19792
|
+
const result = /* @__PURE__ */ new Set();
|
|
19793
|
+
for (const [tokenId, invoiceIds] of this.tokenInvoiceMap) {
|
|
19794
|
+
if (invoiceIds.has(invoiceId)) {
|
|
19795
|
+
result.add(tokenId);
|
|
19796
|
+
}
|
|
19797
|
+
}
|
|
19798
|
+
return result;
|
|
19799
|
+
}
|
|
19490
19800
|
/**
|
|
19491
19801
|
* Explicitly close an invoice. Only target parties may close (§8.3).
|
|
19492
19802
|
*
|
|
@@ -19806,6 +20116,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
19806
20116
|
ledger.set(entryKey, forwardRef);
|
|
19807
20117
|
this.dirtyLedgerEntries.add(invoiceId);
|
|
19808
20118
|
this.balanceCache.delete(invoiceId);
|
|
20119
|
+
await this._persistProvisionalAndVerify(invoiceId, "payInvoice");
|
|
19809
20120
|
}
|
|
19810
20121
|
return result;
|
|
19811
20122
|
} finally {
|
|
@@ -19973,6 +20284,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
19973
20284
|
this.dirtyLedgerEntries.add(invoiceId);
|
|
19974
20285
|
}
|
|
19975
20286
|
this.balanceCache.delete(invoiceId);
|
|
20287
|
+
await this._persistProvisionalAndVerify(invoiceId, "returnInvoicePayment");
|
|
19976
20288
|
}
|
|
19977
20289
|
return result;
|
|
19978
20290
|
} finally {
|
|
@@ -21052,9 +21364,30 @@ var AccountingModule = class _AccountingModule {
|
|
|
21052
21364
|
continue;
|
|
21053
21365
|
}
|
|
21054
21366
|
innerMap.set(entryKey, ref);
|
|
21055
|
-
|
|
21367
|
+
const HEX_64 = /^[a-f0-9]{64}$/i;
|
|
21368
|
+
if (entryKey.startsWith("mt:")) {
|
|
21369
|
+
const firstColon = entryKey.indexOf(":");
|
|
21370
|
+
const secondColon = entryKey.indexOf(":", firstColon + 1);
|
|
21371
|
+
if (secondColon > firstColon + 1) {
|
|
21372
|
+
const tokenIdFromKey2 = entryKey.slice(firstColon + 1, secondColon);
|
|
21373
|
+
if (HEX_64.test(tokenIdFromKey2)) {
|
|
21374
|
+
this._addToTokenInvoiceMap(tokenIdFromKey2, invoiceId);
|
|
21375
|
+
}
|
|
21376
|
+
}
|
|
21377
|
+
} else if (entryKey.startsWith("synthetic:")) {
|
|
21378
|
+
const afterPrefix = entryKey.slice("synthetic:".length);
|
|
21379
|
+
const tokenIdEnd = afterPrefix.indexOf(":");
|
|
21380
|
+
if (tokenIdEnd > 0) {
|
|
21381
|
+
const tokenId = afterPrefix.slice(0, tokenIdEnd);
|
|
21382
|
+
if (HEX_64.test(tokenId) && ref.transferId !== tokenId) {
|
|
21383
|
+
this._addToTokenInvoiceMap(tokenId, invoiceId);
|
|
21384
|
+
}
|
|
21385
|
+
}
|
|
21386
|
+
} else if (!ref.transferId.startsWith("provisional:") && ref.transferId.includes(":")) {
|
|
21056
21387
|
const tokenIdFromRef = ref.transferId.slice(0, ref.transferId.indexOf(":"));
|
|
21057
|
-
|
|
21388
|
+
if (HEX_64.test(tokenIdFromRef)) {
|
|
21389
|
+
this._addToTokenInvoiceMap(tokenIdFromRef, invoiceId);
|
|
21390
|
+
}
|
|
21058
21391
|
}
|
|
21059
21392
|
}
|
|
21060
21393
|
} catch (err) {
|
|
@@ -21255,7 +21588,14 @@ var AccountingModule = class _AccountingModule {
|
|
|
21255
21588
|
}
|
|
21256
21589
|
}
|
|
21257
21590
|
for (const [existingKey, existingRef] of ledger) {
|
|
21258
|
-
if (existingKey.startsWith("synthetic:") && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21591
|
+
if ((existingKey.startsWith("synthetic:") || existingKey.startsWith("synthetic-tx:")) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21592
|
+
keysToDelete.push(existingKey);
|
|
21593
|
+
break;
|
|
21594
|
+
}
|
|
21595
|
+
}
|
|
21596
|
+
const mtPrefix = `mt:${tokenId}:`;
|
|
21597
|
+
for (const [existingKey, existingRef] of ledger) {
|
|
21598
|
+
if (existingKey.startsWith(mtPrefix) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21259
21599
|
keysToDelete.push(existingKey);
|
|
21260
21600
|
break;
|
|
21261
21601
|
}
|
|
@@ -21372,6 +21712,49 @@ var AccountingModule = class _AccountingModule {
|
|
|
21372
21712
|
});
|
|
21373
21713
|
}
|
|
21374
21714
|
}
|
|
21715
|
+
/**
|
|
21716
|
+
* Synchronously persist any pending provisional ledger entry for `invoiceId`
|
|
21717
|
+
* before returning to the caller. Used by `payInvoice` and
|
|
21718
|
+
* `returnInvoicePayment` to make the in-memory provisional entry durable
|
|
21719
|
+
* inside the same per-invoice gate that wrote it, closing the
|
|
21720
|
+
* crash-mid-conclude race that produces over-coverage on receivers.
|
|
21721
|
+
*
|
|
21722
|
+
* Implementation:
|
|
21723
|
+
* 1. Schedule a flush via the existing `_flushPromise` chain (so
|
|
21724
|
+
* concurrent `_handleTokenChange` callers waiting on the chain
|
|
21725
|
+
* observe ours as part of the sequence).
|
|
21726
|
+
* 2. Await OUR flush directly — NOT `_drainFlushPromise()`, which would
|
|
21727
|
+
* spin while concurrent token changes keep extending the chain and
|
|
21728
|
+
* hold the per-invoice gate for an unbounded number of additional
|
|
21729
|
+
* flushes. We only need OUR provisional entry durable.
|
|
21730
|
+
* 3. `_flushDirtyLedgerEntries` swallows per-invoice `storage.set`
|
|
21731
|
+
* rejections internally (sets a local `step1Failed` flag), leaving
|
|
21732
|
+
* the dirty entry on the set without re-throwing. So we post-check
|
|
21733
|
+
* `dirtyLedgerEntries.has(invoiceId)` and throw a `STORAGE_ERROR`
|
|
21734
|
+
* `SphereError` if our entry is still dirty — propagating to the
|
|
21735
|
+
* caller so they learn about the durability failure rather than
|
|
21736
|
+
* receiving a silent "success" return that lies on disk.
|
|
21737
|
+
*
|
|
21738
|
+
* @param invoiceId The invoice whose provisional entry must be durable.
|
|
21739
|
+
* @param callContext Used in the error message so the caller is named
|
|
21740
|
+
* ('payInvoice' / 'returnInvoicePayment') without
|
|
21741
|
+
* forcing a stack-trace inspection.
|
|
21742
|
+
*/
|
|
21743
|
+
async _persistProvisionalAndVerify(invoiceId, callContext) {
|
|
21744
|
+
const flushTrigger = (this._flushPromise ?? Promise.resolve()).then(() => this._flushDirtyLedgerEntries());
|
|
21745
|
+
const tracked = flushTrigger.catch(() => {
|
|
21746
|
+
}).finally(() => {
|
|
21747
|
+
if (this._flushPromise === tracked) this._flushPromise = null;
|
|
21748
|
+
});
|
|
21749
|
+
this._flushPromise = tracked;
|
|
21750
|
+
await flushTrigger;
|
|
21751
|
+
if (this.dirtyLedgerEntries.has(invoiceId)) {
|
|
21752
|
+
throw new SphereError(
|
|
21753
|
+
`${callContext}: provisional ledger entry for invoice ${invoiceId} failed to persist \u2014 caller should retry`,
|
|
21754
|
+
"STORAGE_ERROR"
|
|
21755
|
+
);
|
|
21756
|
+
}
|
|
21757
|
+
}
|
|
21375
21758
|
// ===========================================================================
|
|
21376
21759
|
// Internal: Event handlers
|
|
21377
21760
|
// ===========================================================================
|
|
@@ -21432,13 +21815,96 @@ var AccountingModule = class _AccountingModule {
|
|
|
21432
21815
|
}
|
|
21433
21816
|
}
|
|
21434
21817
|
if (!this.invoiceTermsCache.has(invoiceId)) {
|
|
21435
|
-
|
|
21436
|
-
|
|
21437
|
-
invoiceId
|
|
21438
|
-
|
|
21439
|
-
|
|
21440
|
-
|
|
21441
|
-
|
|
21818
|
+
let gracefullyGraduated = false;
|
|
21819
|
+
await this.withInvoiceGate(invoiceId, async () => {
|
|
21820
|
+
if (this.invoiceTermsCache.has(invoiceId)) {
|
|
21821
|
+
gracefullyGraduated = true;
|
|
21822
|
+
return;
|
|
21823
|
+
}
|
|
21824
|
+
const syntheticRef = this._buildSyntheticTransferRef(
|
|
21825
|
+
transfer,
|
|
21826
|
+
invoiceId,
|
|
21827
|
+
paymentDirection,
|
|
21828
|
+
confirmed
|
|
21829
|
+
);
|
|
21830
|
+
deps.emitEvent("invoice:unknown_reference", { invoiceId, transfer: syntheticRef });
|
|
21831
|
+
const MAX_UNKNOWN_INVOICE_IDS = 500;
|
|
21832
|
+
const UNKNOWN_LEDGER_TTL_MS = 30 * 60 * 1e3;
|
|
21833
|
+
const MAX_ORPHAN_ENTRIES_PER_INVOICE = 50;
|
|
21834
|
+
const UNKNOWN_LEDGER_SWEEP_INTERVAL_MS = 6e4;
|
|
21835
|
+
const nowMs = Date.now();
|
|
21836
|
+
const capFull = this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS;
|
|
21837
|
+
const sweepDue = nowMs >= this.unknownLedgerNextSweepMs;
|
|
21838
|
+
if (this.unknownLedgerFirstSeen.size > 0 && (capFull || sweepDue)) {
|
|
21839
|
+
this.unknownLedgerNextSweepMs = nowMs + UNKNOWN_LEDGER_SWEEP_INTERVAL_MS;
|
|
21840
|
+
const expiredIds = [];
|
|
21841
|
+
for (const [unkId, firstSeen] of this.unknownLedgerFirstSeen) {
|
|
21842
|
+
if (nowMs - firstSeen > UNKNOWN_LEDGER_TTL_MS) {
|
|
21843
|
+
expiredIds.push(unkId);
|
|
21844
|
+
}
|
|
21845
|
+
}
|
|
21846
|
+
for (const expiredId of expiredIds) {
|
|
21847
|
+
if (!this.invoiceTermsCache.has(expiredId) && this.invoiceLedger.has(expiredId)) {
|
|
21848
|
+
this.invoiceLedger.delete(expiredId);
|
|
21849
|
+
this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
|
|
21850
|
+
for (const [tokenId, invoiceSet] of this.tokenInvoiceMap) {
|
|
21851
|
+
if (invoiceSet.has(expiredId)) {
|
|
21852
|
+
invoiceSet.delete(expiredId);
|
|
21853
|
+
if (invoiceSet.size === 0) this.tokenInvoiceMap.delete(tokenId);
|
|
21854
|
+
}
|
|
21855
|
+
}
|
|
21856
|
+
}
|
|
21857
|
+
this.unknownLedgerFirstSeen.delete(expiredId);
|
|
21858
|
+
}
|
|
21859
|
+
}
|
|
21860
|
+
if (!this.invoiceLedger.has(invoiceId)) {
|
|
21861
|
+
if (this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS) {
|
|
21862
|
+
return;
|
|
21863
|
+
}
|
|
21864
|
+
this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
|
|
21865
|
+
this.unknownLedgerCount++;
|
|
21866
|
+
this.unknownLedgerFirstSeen.set(invoiceId, nowMs);
|
|
21867
|
+
}
|
|
21868
|
+
const orphanLedger = this.invoiceLedger.get(invoiceId);
|
|
21869
|
+
let mtEntryCount = 0;
|
|
21870
|
+
for (const k of orphanLedger.keys()) {
|
|
21871
|
+
if (k.startsWith("mt:")) mtEntryCount++;
|
|
21872
|
+
}
|
|
21873
|
+
if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
|
|
21874
|
+
return;
|
|
21875
|
+
}
|
|
21876
|
+
for (const token of transfer.tokens) {
|
|
21877
|
+
if (!token.id) continue;
|
|
21878
|
+
let onChainAttributed = false;
|
|
21879
|
+
const tokenKeyPrefix = `${token.id}:`;
|
|
21880
|
+
for (const existingKey of orphanLedger.keys()) {
|
|
21881
|
+
if (existingKey.startsWith(tokenKeyPrefix) && !existingKey.startsWith("mt:")) {
|
|
21882
|
+
onChainAttributed = true;
|
|
21883
|
+
break;
|
|
21884
|
+
}
|
|
21885
|
+
}
|
|
21886
|
+
if (!onChainAttributed) {
|
|
21887
|
+
if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
|
|
21888
|
+
break;
|
|
21889
|
+
}
|
|
21890
|
+
const orphanKey = `mt:${token.id}:${transfer.id}`;
|
|
21891
|
+
if (!orphanLedger.has(orphanKey)) {
|
|
21892
|
+
orphanLedger.set(orphanKey, syntheticRef);
|
|
21893
|
+
mtEntryCount++;
|
|
21894
|
+
}
|
|
21895
|
+
}
|
|
21896
|
+
if (!this.tokenInvoiceMap.has(token.id)) {
|
|
21897
|
+
this.tokenInvoiceMap.set(token.id, /* @__PURE__ */ new Set());
|
|
21898
|
+
}
|
|
21899
|
+
this.tokenInvoiceMap.get(token.id).add(invoiceId);
|
|
21900
|
+
}
|
|
21901
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
21902
|
+
this.balanceCache.delete(invoiceId);
|
|
21903
|
+
await this._flushDirtyLedgerEntries();
|
|
21904
|
+
});
|
|
21905
|
+
if (gracefullyGraduated) {
|
|
21906
|
+
await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
|
|
21907
|
+
}
|
|
21442
21908
|
return;
|
|
21443
21909
|
}
|
|
21444
21910
|
await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
|
|
@@ -21874,7 +22340,8 @@ var AccountingModule = class _AccountingModule {
|
|
|
21874
22340
|
}
|
|
21875
22341
|
const existingLedger = this.invoiceLedger.get(invoiceId);
|
|
21876
22342
|
const firstTokenId = transfer.tokens.find((t) => t.id)?.id;
|
|
21877
|
-
const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22343
|
+
const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22344
|
+
let mutated = false;
|
|
21878
22345
|
if (!existingLedger.has(syntheticKey)) {
|
|
21879
22346
|
let hasRealEntry = false;
|
|
21880
22347
|
for (const tok of transfer.tokens) {
|
|
@@ -21889,8 +22356,25 @@ var AccountingModule = class _AccountingModule {
|
|
|
21889
22356
|
}
|
|
21890
22357
|
if (!hasRealEntry) {
|
|
21891
22358
|
existingLedger.set(syntheticKey, { ...syntheticRef });
|
|
22359
|
+
mutated = true;
|
|
21892
22360
|
}
|
|
21893
22361
|
}
|
|
22362
|
+
for (const tok of transfer.tokens) {
|
|
22363
|
+
if (!tok.id) continue;
|
|
22364
|
+
if (!this.tokenInvoiceMap.has(tok.id)) {
|
|
22365
|
+
this.tokenInvoiceMap.set(tok.id, /* @__PURE__ */ new Set());
|
|
22366
|
+
mutated = true;
|
|
22367
|
+
}
|
|
22368
|
+
const beforeSize = this.tokenInvoiceMap.get(tok.id).size;
|
|
22369
|
+
this.tokenInvoiceMap.get(tok.id).add(invoiceId);
|
|
22370
|
+
if (this.tokenInvoiceMap.get(tok.id).size !== beforeSize) {
|
|
22371
|
+
mutated = true;
|
|
22372
|
+
}
|
|
22373
|
+
}
|
|
22374
|
+
if (mutated) {
|
|
22375
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22376
|
+
this.balanceCache.delete(invoiceId);
|
|
22377
|
+
}
|
|
21894
22378
|
deps.emitEvent("invoice:payment", {
|
|
21895
22379
|
invoiceId,
|
|
21896
22380
|
transfer: syntheticRef,
|
|
@@ -22040,7 +22524,8 @@ var AccountingModule = class _AccountingModule {
|
|
|
22040
22524
|
this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
|
|
22041
22525
|
}
|
|
22042
22526
|
const hLedger = this.invoiceLedger.get(invoiceId);
|
|
22043
|
-
const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22527
|
+
const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22528
|
+
let hMutated = false;
|
|
22044
22529
|
if (!hLedger.has(hKey)) {
|
|
22045
22530
|
let hasRealEntry = false;
|
|
22046
22531
|
if (entry.tokenId) {
|
|
@@ -22053,7 +22538,23 @@ var AccountingModule = class _AccountingModule {
|
|
|
22053
22538
|
}
|
|
22054
22539
|
if (!hasRealEntry) {
|
|
22055
22540
|
hLedger.set(hKey, { ...syntheticRef });
|
|
22541
|
+
hMutated = true;
|
|
22542
|
+
}
|
|
22543
|
+
}
|
|
22544
|
+
if (entry.tokenId) {
|
|
22545
|
+
if (!this.tokenInvoiceMap.has(entry.tokenId)) {
|
|
22546
|
+
this.tokenInvoiceMap.set(entry.tokenId, /* @__PURE__ */ new Set());
|
|
22547
|
+
hMutated = true;
|
|
22056
22548
|
}
|
|
22549
|
+
const beforeSize = this.tokenInvoiceMap.get(entry.tokenId).size;
|
|
22550
|
+
this.tokenInvoiceMap.get(entry.tokenId).add(invoiceId);
|
|
22551
|
+
if (this.tokenInvoiceMap.get(entry.tokenId).size !== beforeSize) {
|
|
22552
|
+
hMutated = true;
|
|
22553
|
+
}
|
|
22554
|
+
}
|
|
22555
|
+
if (hMutated) {
|
|
22556
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22557
|
+
this.balanceCache.delete(invoiceId);
|
|
22057
22558
|
}
|
|
22058
22559
|
deps.emitEvent("invoice:payment", {
|
|
22059
22560
|
invoiceId,
|
|
@@ -24585,17 +25086,63 @@ var SwapModule = class {
|
|
|
24585
25086
|
for (const addr of allAddresses) {
|
|
24586
25087
|
myDirectAddresses.add(addr.directAddress);
|
|
24587
25088
|
}
|
|
24588
|
-
|
|
24589
|
-
|
|
24590
|
-
|
|
24591
|
-
|
|
24592
|
-
|
|
25089
|
+
const matchesPartyA = myDirectAddresses.has(swap.manifest.party_a_address);
|
|
25090
|
+
const matchesPartyB = myDirectAddresses.has(swap.manifest.party_b_address);
|
|
25091
|
+
if (matchesPartyA && matchesPartyB) {
|
|
25092
|
+
throw new SphereError(
|
|
25093
|
+
"Ambiguous party identity: local wallet matches both party_a_address and party_b_address",
|
|
25094
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25095
|
+
);
|
|
25096
|
+
}
|
|
25097
|
+
let myExpectedCurrency;
|
|
25098
|
+
if (matchesPartyA) {
|
|
25099
|
+
myExpectedCurrency = swap.manifest.party_a_currency_to_change;
|
|
25100
|
+
} else if (matchesPartyB) {
|
|
25101
|
+
myExpectedCurrency = swap.manifest.party_b_currency_to_change;
|
|
24593
25102
|
} else {
|
|
24594
25103
|
throw new SphereError(
|
|
24595
25104
|
"Local wallet address does not match either party in the swap manifest",
|
|
24596
25105
|
"SWAP_DEPOSIT_FAILED"
|
|
24597
25106
|
);
|
|
24598
25107
|
}
|
|
25108
|
+
if (!myExpectedCurrency || myExpectedCurrency === "") {
|
|
25109
|
+
throw new SphereError(
|
|
25110
|
+
"Manifest currency_to_change is empty for this party",
|
|
25111
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25112
|
+
);
|
|
25113
|
+
}
|
|
25114
|
+
const invoiceRefForAssetLookup = deps.accounting.getInvoice(swap.depositInvoiceId);
|
|
25115
|
+
if (!invoiceRefForAssetLookup) {
|
|
25116
|
+
throw new SphereError(
|
|
25117
|
+
"Deposit invoice not yet imported into accounting module",
|
|
25118
|
+
"SWAP_WRONG_STATE"
|
|
25119
|
+
);
|
|
25120
|
+
}
|
|
25121
|
+
const depositTarget = invoiceRefForAssetLookup.terms.targets[0];
|
|
25122
|
+
if (!depositTarget) {
|
|
25123
|
+
throw new SphereError(
|
|
25124
|
+
"Deposit invoice has no targets",
|
|
25125
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25126
|
+
);
|
|
25127
|
+
}
|
|
25128
|
+
const assetIndex = depositTarget.assets.findIndex(
|
|
25129
|
+
(a) => a.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)
|
|
25130
|
+
);
|
|
25131
|
+
if (assetIndex < 0) {
|
|
25132
|
+
throw new SphereError(
|
|
25133
|
+
`No asset matching expected currency ${myExpectedCurrency} found in deposit invoice`,
|
|
25134
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25135
|
+
);
|
|
25136
|
+
}
|
|
25137
|
+
for (let i = assetIndex + 1; i < depositTarget.assets.length; i += 1) {
|
|
25138
|
+
const a = depositTarget.assets[i];
|
|
25139
|
+
if (a?.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)) {
|
|
25140
|
+
throw new SphereError(
|
|
25141
|
+
`Ambiguous asset match in deposit invoice: slots ${assetIndex} and ${i} both match currency ${myExpectedCurrency}`,
|
|
25142
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25143
|
+
);
|
|
25144
|
+
}
|
|
25145
|
+
}
|
|
24599
25146
|
return this.withSwapGate(swapId, async () => {
|
|
24600
25147
|
if (swap.progress !== "announced") {
|
|
24601
25148
|
throw new SphereError(
|
|
@@ -24701,7 +25248,6 @@ var SwapModule = class {
|
|
|
24701
25248
|
swap.updatedAt = Date.now();
|
|
24702
25249
|
this.clearLocalTimer(swap.swapId);
|
|
24703
25250
|
this.terminalSwapIds.add(swap.swapId);
|
|
24704
|
-
const entryIdx = this._storedTerminalEntries.length;
|
|
24705
25251
|
this._storedTerminalEntries.push({
|
|
24706
25252
|
swapId: swap.swapId,
|
|
24707
25253
|
progress: "failed",
|
|
@@ -24716,7 +25262,13 @@ var SwapModule = class {
|
|
|
24716
25262
|
swap.error = prevError;
|
|
24717
25263
|
swap.updatedAt = prevUpdatedAt;
|
|
24718
25264
|
this.terminalSwapIds.delete(swap.swapId);
|
|
24719
|
-
this._storedTerminalEntries.
|
|
25265
|
+
for (let i = this._storedTerminalEntries.length - 1; i >= 0; i--) {
|
|
25266
|
+
const entry = this._storedTerminalEntries[i];
|
|
25267
|
+
if (entry.swapId === swap.swapId && entry.progress === "failed") {
|
|
25268
|
+
this._storedTerminalEntries.splice(i, 1);
|
|
25269
|
+
break;
|
|
25270
|
+
}
|
|
25271
|
+
}
|
|
24720
25272
|
logger.warn(LOG_TAG3, `failPayout: persistSwap failed for ${swapId}; fraud detection will retry on next load:`, persistErr);
|
|
24721
25273
|
throw persistErr;
|
|
24722
25274
|
}
|
|
@@ -24760,9 +25312,25 @@ var SwapModule = class {
|
|
|
24760
25312
|
if (!targetStatus.coinAssets[0].isCovered) {
|
|
24761
25313
|
return returnFalse();
|
|
24762
25314
|
}
|
|
24763
|
-
|
|
25315
|
+
let netCoveredAmount;
|
|
25316
|
+
let expectedAmountBigInt;
|
|
25317
|
+
try {
|
|
25318
|
+
netCoveredAmount = BigInt(targetStatus.coinAssets[0].netCoveredAmount);
|
|
25319
|
+
expectedAmountBigInt = BigInt(expectedAmount);
|
|
25320
|
+
} catch (parseErr) {
|
|
25321
|
+
return failPayout(
|
|
25322
|
+
`MALFORMED_AMOUNT: failed to parse coverage amounts (netCoveredAmount=${targetStatus.coinAssets[0].netCoveredAmount}, expectedAmount=${expectedAmount}): ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`
|
|
25323
|
+
);
|
|
25324
|
+
}
|
|
25325
|
+
if (netCoveredAmount < expectedAmountBigInt) {
|
|
24764
25326
|
return returnFalse();
|
|
24765
25327
|
}
|
|
25328
|
+
if (netCoveredAmount > expectedAmountBigInt) {
|
|
25329
|
+
const surplus = netCoveredAmount - expectedAmountBigInt;
|
|
25330
|
+
return failPayout(
|
|
25331
|
+
`OVER_COVERAGE: net=${netCoveredAmount.toString()}, expected=${expectedAmount}, surplus=${surplus.toString()} \u2014 surplus refund expected via auto-return; settlement halted`
|
|
25332
|
+
);
|
|
25333
|
+
}
|
|
24766
25334
|
const escrowAddr = swap.deal.escrowAddress ?? this.config.defaultEscrowAddress;
|
|
24767
25335
|
if (escrowAddr) {
|
|
24768
25336
|
const escrowPeer = await deps.resolve(escrowAddr);
|
|
@@ -24772,8 +25340,28 @@ var SwapModule = class {
|
|
|
24772
25340
|
}
|
|
24773
25341
|
const validationResult = await deps.payments.validate();
|
|
24774
25342
|
if (validationResult.invalid.length > 0) {
|
|
24775
|
-
|
|
24776
|
-
|
|
25343
|
+
const payoutTokenIds = deps.accounting.getTokenIdsForInvoice?.(swap.payoutInvoiceId) ?? /* @__PURE__ */ new Set();
|
|
25344
|
+
if (payoutTokenIds.size === 0) {
|
|
25345
|
+
logger.warn(
|
|
25346
|
+
LOG_TAG3,
|
|
25347
|
+
`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`
|
|
25348
|
+
);
|
|
25349
|
+
return returnFalse();
|
|
25350
|
+
}
|
|
25351
|
+
const relevantInvalid = validationResult.invalid.filter(
|
|
25352
|
+
(t) => payoutTokenIds.has(t.id)
|
|
25353
|
+
);
|
|
25354
|
+
if (relevantInvalid.length > 0) {
|
|
25355
|
+
logger.warn(
|
|
25356
|
+
LOG_TAG3,
|
|
25357
|
+
`verifyPayout for ${swapId.slice(0, 12)}: L3 validation found ${relevantInvalid.length} invalid token(s) covering this payout invoice \u2014 retry after wallet sync`
|
|
25358
|
+
);
|
|
25359
|
+
return returnFalse();
|
|
25360
|
+
}
|
|
25361
|
+
logger.debug(
|
|
25362
|
+
LOG_TAG3,
|
|
25363
|
+
`verifyPayout for ${swapId.slice(0, 12)}: ${validationResult.invalid.length} unrelated invalid token(s) ignored (not linked to this payout invoice)`
|
|
25364
|
+
);
|
|
24777
25365
|
}
|
|
24778
25366
|
if (swap.progress === "completed") {
|
|
24779
25367
|
swap.payoutVerified = true;
|
|
@@ -24952,8 +25540,22 @@ var SwapModule = class {
|
|
|
24952
25540
|
* @param dm - The incoming direct message.
|
|
24953
25541
|
*/
|
|
24954
25542
|
handleIncomingDM(dm) {
|
|
25543
|
+
if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
|
|
25544
|
+
logger.warn(
|
|
25545
|
+
LOG_TAG3,
|
|
25546
|
+
`diag_swap_dm_arrived sender=${dm.senderPubkey.slice(0, 16)} length=${dm.content.length}`
|
|
25547
|
+
);
|
|
25548
|
+
}
|
|
24955
25549
|
const parsed = parseSwapDM(dm.content);
|
|
24956
|
-
if (!parsed)
|
|
25550
|
+
if (!parsed) {
|
|
25551
|
+
if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
|
|
25552
|
+
logger.warn(
|
|
25553
|
+
LOG_TAG3,
|
|
25554
|
+
`diag_swap_dm_parse_rejected sender=${dm.senderPubkey.slice(0, 16)} prefix=${dm.content.slice(0, 80)}`
|
|
25555
|
+
);
|
|
25556
|
+
}
|
|
25557
|
+
return;
|
|
25558
|
+
}
|
|
24957
25559
|
void (async () => {
|
|
24958
25560
|
try {
|
|
24959
25561
|
switch (parsed.kind) {
|
|
@@ -25319,16 +25921,43 @@ var SwapModule = class {
|
|
|
25319
25921
|
// invoice_delivery (§12.4.2 + §12.4.3)
|
|
25320
25922
|
// ---------------------------------------------------------------
|
|
25321
25923
|
case "invoice_delivery": {
|
|
25322
|
-
|
|
25924
|
+
logger.warn(
|
|
25925
|
+
LOG_TAG3,
|
|
25926
|
+
`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)}`
|
|
25927
|
+
);
|
|
25928
|
+
if (!swapId) {
|
|
25929
|
+
logger.warn(LOG_TAG3, "diag_invoice_delivery_dropped reason=no_swap_id");
|
|
25930
|
+
return;
|
|
25931
|
+
}
|
|
25323
25932
|
const swap = this.swaps.get(swapId);
|
|
25324
|
-
if (!swap)
|
|
25325
|
-
|
|
25933
|
+
if (!swap) {
|
|
25934
|
+
logger.warn(
|
|
25935
|
+
LOG_TAG3,
|
|
25936
|
+
`diag_invoice_delivery_dropped reason=swap_not_in_map swap_id=${swapId.slice(0, 16)} known_swap_ids_count=${this.swaps.size}`
|
|
25937
|
+
);
|
|
25938
|
+
return;
|
|
25939
|
+
}
|
|
25940
|
+
if (!this.isFromExpectedEscrow(dm.senderPubkey, swap)) {
|
|
25941
|
+
logger.warn(
|
|
25942
|
+
LOG_TAG3,
|
|
25943
|
+
`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)}`
|
|
25944
|
+
);
|
|
25945
|
+
return;
|
|
25946
|
+
}
|
|
25326
25947
|
const deps = this.deps;
|
|
25327
25948
|
if (msg.invoice_type === "deposit") {
|
|
25949
|
+
logger.warn(
|
|
25950
|
+
LOG_TAG3,
|
|
25951
|
+
`diag_invoice_delivery_proceeding_to_import swap_id=${swapId.slice(0, 16)} progress=${swap.progress} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)}`
|
|
25952
|
+
);
|
|
25328
25953
|
await this.withSwapGate(swapId, async () => {
|
|
25329
25954
|
if (isTerminalProgress(swap.progress)) return;
|
|
25330
25955
|
try {
|
|
25331
25956
|
await deps.accounting.importInvoice(msg.invoice_token);
|
|
25957
|
+
logger.warn(
|
|
25958
|
+
LOG_TAG3,
|
|
25959
|
+
`diag_invoice_imported swap_id=${swapId.slice(0, 16)} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)} type=deposit`
|
|
25960
|
+
);
|
|
25332
25961
|
} catch (err) {
|
|
25333
25962
|
if (err instanceof SphereError && err.code === "INVOICE_ALREADY_EXISTS") {
|
|
25334
25963
|
logger.debug(LOG_TAG3, `Deposit invoice for swap ${swapId} already imported \u2014 relay re-delivery, continuing`);
|
|
@@ -26821,27 +27450,42 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
26821
27450
|
|
|
26822
27451
|
// core/Sphere.ts
|
|
26823
27452
|
var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
|
|
27453
|
+
var import_nostr_js_sdk5 = require("@unicitylabs/nostr-js-sdk");
|
|
27454
|
+
|
|
27455
|
+
// core/address-derivation.ts
|
|
26824
27456
|
var import_TokenType5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
26825
27457
|
var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
26826
27458
|
var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
26827
|
-
var
|
|
27459
|
+
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
27460
|
+
var COMPRESSED_PUBKEY_RE = /^(02|03)[0-9a-fA-F]{64}$/;
|
|
27461
|
+
async function computeDirectAddressFromChainPubkey(chainPubkey) {
|
|
27462
|
+
if (typeof chainPubkey !== "string" || !COMPRESSED_PUBKEY_RE.test(chainPubkey)) {
|
|
27463
|
+
throw new Error(
|
|
27464
|
+
`computeDirectAddressFromChainPubkey: chainPubkey must be 66-char hex with 02/03 prefix, got "${String(chainPubkey).slice(0, 12)}..."`
|
|
27465
|
+
);
|
|
27466
|
+
}
|
|
27467
|
+
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
27468
|
+
const tokenType = new import_TokenType5.TokenType(tokenTypeBytes);
|
|
27469
|
+
const publicKeyBytes = Buffer.from(chainPubkey, "hex");
|
|
27470
|
+
const predicateRef = await import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
|
|
27471
|
+
tokenType,
|
|
27472
|
+
"secp256k1",
|
|
27473
|
+
publicKeyBytes,
|
|
27474
|
+
import_HashAlgorithm7.HashAlgorithm.SHA256
|
|
27475
|
+
);
|
|
27476
|
+
return (await predicateRef.toAddress()).toString();
|
|
27477
|
+
}
|
|
27478
|
+
|
|
27479
|
+
// core/Sphere.ts
|
|
26828
27480
|
function isValidNametag2(nametag) {
|
|
26829
27481
|
if ((0, import_nostr_js_sdk5.isPhoneNumber)(nametag)) return true;
|
|
26830
27482
|
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
26831
27483
|
}
|
|
26832
|
-
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
26833
27484
|
async function deriveL3PredicateAddress(privateKey) {
|
|
26834
27485
|
const secret = Buffer.from(privateKey, "hex");
|
|
26835
27486
|
const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
|
|
26836
|
-
const
|
|
26837
|
-
|
|
26838
|
-
const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
|
|
26839
|
-
tokenType,
|
|
26840
|
-
signingService.algorithm,
|
|
26841
|
-
signingService.publicKey,
|
|
26842
|
-
import_HashAlgorithm7.HashAlgorithm.SHA256
|
|
26843
|
-
);
|
|
26844
|
-
return (await (await predicateRef).toAddress()).toString();
|
|
27487
|
+
const pubkeyHex = Buffer.from(signingService.publicKey).toString("hex");
|
|
27488
|
+
return computeDirectAddressFromChainPubkey(pubkeyHex);
|
|
26845
27489
|
}
|
|
26846
27490
|
var Sphere = class _Sphere {
|
|
26847
27491
|
// Singleton
|
|
@@ -28295,6 +28939,9 @@ var Sphere = class _Sphere {
|
|
|
28295
28939
|
this._transport.setFallbackSince(fallbackTs);
|
|
28296
28940
|
}
|
|
28297
28941
|
await this._transport.setIdentity(this._identity);
|
|
28942
|
+
if (this._transportMux && typeof this._transportMux.rebindToSharedClient === "function") {
|
|
28943
|
+
await this._transportMux.rebindToSharedClient();
|
|
28944
|
+
}
|
|
28298
28945
|
this.emitEvent("identity:changed", {
|
|
28299
28946
|
l1Address: this._identity.l1Address,
|
|
28300
28947
|
directAddress: this._identity.directAddress,
|
|
@@ -28505,7 +29152,12 @@ var Sphere = class _Sphere {
|
|
|
28505
29152
|
this._transportMux = new MultiAddressTransportMux({
|
|
28506
29153
|
relays: nostrTransport.getConfiguredRelays(),
|
|
28507
29154
|
createWebSocket: nostrTransport.getWebSocketFactory(),
|
|
28508
|
-
storage: nostrTransport.getStorageAdapter() ?? void 0
|
|
29155
|
+
storage: nostrTransport.getStorageAdapter() ?? void 0,
|
|
29156
|
+
// #123: share the original transport's NostrClient instead of
|
|
29157
|
+
// opening a second WebSocket per relay. Pass a getter so the
|
|
29158
|
+
// Mux resolves it at connect-time (after the transport finishes
|
|
29159
|
+
// its own connect()).
|
|
29160
|
+
sharedNostrClient: typeof nostrTransport.getNostrClient === "function" ? () => nostrTransport.getNostrClient() : void 0
|
|
28509
29161
|
});
|
|
28510
29162
|
await this._transportMux.connect();
|
|
28511
29163
|
if (typeof nostrTransport.suppressSubscriptions === "function") {
|
|
@@ -30218,6 +30870,7 @@ async function runCustomCheck(name, checkFn, timeoutMs) {
|
|
|
30218
30870
|
base58Encode,
|
|
30219
30871
|
bytesToHex,
|
|
30220
30872
|
checkNetworkHealth,
|
|
30873
|
+
computeDirectAddressFromChainPubkey,
|
|
30221
30874
|
computeHash160,
|
|
30222
30875
|
convertBits,
|
|
30223
30876
|
createAddress,
|