@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.js
CHANGED
|
@@ -1365,6 +1365,17 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1365
1365
|
getStorageAdapter() {
|
|
1366
1366
|
return this.storage;
|
|
1367
1367
|
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Get the underlying NostrClient (or null if not yet connected).
|
|
1370
|
+
*
|
|
1371
|
+
* Exposed so {@link MultiAddressTransportMux} can share the same
|
|
1372
|
+
* client/socket pair instead of opening a duplicate WebSocket per
|
|
1373
|
+
* relay (#123). The transport owns the client's lifecycle — callers
|
|
1374
|
+
* MUST NOT call {@code disconnect()} on the returned instance.
|
|
1375
|
+
*/
|
|
1376
|
+
getNostrClient() {
|
|
1377
|
+
return this.nostrClient;
|
|
1378
|
+
}
|
|
1368
1379
|
/**
|
|
1369
1380
|
* Suppress event subscriptions — unsubscribe wallet/chat filters
|
|
1370
1381
|
* but keep the connection alive for resolve/identity-binding operations.
|
|
@@ -3003,9 +3014,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3003
3014
|
chatSubscriptionId = null;
|
|
3004
3015
|
chatEoseFired = false;
|
|
3005
3016
|
resubscribeTimer = null;
|
|
3006
|
-
lastWalletEventAt = Date.now();
|
|
3007
|
-
lastChatEventAt = Date.now();
|
|
3008
|
-
healthCheckTimer = null;
|
|
3009
3017
|
chatEoseHandlers = [];
|
|
3010
3018
|
// Dedup — bounded to prevent memory leak in long-running sessions.
|
|
3011
3019
|
// Set preserves insertion order; evict oldest entries when cap is reached.
|
|
@@ -3016,6 +3024,19 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3016
3024
|
// Identity key for the Mux's NostrClient — relays may filter gift-wrap
|
|
3017
3025
|
// delivery to the recipient's subscription key.
|
|
3018
3026
|
identityPrivateKey;
|
|
3027
|
+
// Resolves the shared NostrClient at use-time (the source provider may
|
|
3028
|
+
// create its client lazily, after the Mux is constructed). null means
|
|
3029
|
+
// "no shared client; create our own."
|
|
3030
|
+
sharedNostrClientGetter;
|
|
3031
|
+
// True when this Mux is using a shared NostrClient and therefore must
|
|
3032
|
+
// not call connect()/disconnect() on it.
|
|
3033
|
+
usingSharedClient = false;
|
|
3034
|
+
// Listener registered on the underlying NostrClient. Tracked so we can
|
|
3035
|
+
// remove it on disconnect / rebind — otherwise a long-lived shared
|
|
3036
|
+
// client accumulates listeners across address switches and (worse)
|
|
3037
|
+
// a "disconnected" Mux still sees onReconnected callbacks fire and
|
|
3038
|
+
// re-establish subscriptions it shouldn't have.
|
|
3039
|
+
connectionListener = null;
|
|
3019
3040
|
constructor(config) {
|
|
3020
3041
|
this.identityPrivateKey = config.identityPrivateKey;
|
|
3021
3042
|
this.config = {
|
|
@@ -3028,6 +3049,14 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3028
3049
|
generateUUID: config.generateUUID ?? defaultUUIDGenerator
|
|
3029
3050
|
};
|
|
3030
3051
|
this.storage = config.storage ?? null;
|
|
3052
|
+
if (typeof config.sharedNostrClient === "function") {
|
|
3053
|
+
this.sharedNostrClientGetter = config.sharedNostrClient;
|
|
3054
|
+
} else if (config.sharedNostrClient) {
|
|
3055
|
+
const c = config.sharedNostrClient;
|
|
3056
|
+
this.sharedNostrClientGetter = () => c;
|
|
3057
|
+
} else {
|
|
3058
|
+
this.sharedNostrClientGetter = null;
|
|
3059
|
+
}
|
|
3031
3060
|
}
|
|
3032
3061
|
// ===========================================================================
|
|
3033
3062
|
// Address Management
|
|
@@ -3118,53 +3147,49 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3118
3147
|
if (this.status === "connected") return;
|
|
3119
3148
|
this.status = "connecting";
|
|
3120
3149
|
try {
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3150
|
+
const shared = this.sharedNostrClientGetter ? this.sharedNostrClientGetter() : null;
|
|
3151
|
+
if (shared) {
|
|
3152
|
+
if (!shared.isConnected()) {
|
|
3153
|
+
throw new SphereError(
|
|
3154
|
+
"sharedNostrClient is not connected; the Mux cannot share a closed socket",
|
|
3155
|
+
"TRANSPORT_ERROR"
|
|
3125
3156
|
);
|
|
3126
|
-
} else {
|
|
3127
|
-
const tempKey = Buffer3.alloc(32);
|
|
3128
|
-
crypto.getRandomValues(tempKey);
|
|
3129
|
-
this.primaryKeyManager = NostrKeyManager2.fromPrivateKey(tempKey);
|
|
3130
3157
|
}
|
|
3158
|
+
this.nostrClient = shared;
|
|
3159
|
+
this.usingSharedClient = true;
|
|
3160
|
+
} else {
|
|
3161
|
+
if (!this.primaryKeyManager) {
|
|
3162
|
+
if (this.identityPrivateKey) {
|
|
3163
|
+
this.primaryKeyManager = NostrKeyManager2.fromPrivateKey(
|
|
3164
|
+
Buffer3.from(this.identityPrivateKey)
|
|
3165
|
+
);
|
|
3166
|
+
} else {
|
|
3167
|
+
const tempKey = Buffer3.alloc(32);
|
|
3168
|
+
crypto.getRandomValues(tempKey);
|
|
3169
|
+
this.primaryKeyManager = NostrKeyManager2.fromPrivateKey(tempKey);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
this.nostrClient = new NostrClient2(this.primaryKeyManager, {
|
|
3173
|
+
autoReconnect: this.config.autoReconnect,
|
|
3174
|
+
reconnectIntervalMs: this.config.reconnectDelay,
|
|
3175
|
+
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
3176
|
+
pingIntervalMs: 15e3
|
|
3177
|
+
});
|
|
3131
3178
|
}
|
|
3132
|
-
this.
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
},
|
|
3146
|
-
onReconnecting: (url, attempt) => {
|
|
3147
|
-
logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
|
|
3148
|
-
this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
|
|
3149
|
-
},
|
|
3150
|
-
onReconnected: (url) => {
|
|
3151
|
-
logger.debug("Mux", "Reconnected to relay:", url);
|
|
3152
|
-
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
3153
|
-
this.updateSubscriptions().catch((err) => {
|
|
3154
|
-
logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
|
|
3155
|
-
});
|
|
3179
|
+
this.connectionListener = this.buildConnectionListener();
|
|
3180
|
+
this.nostrClient.addConnectionListener(this.connectionListener);
|
|
3181
|
+
if (!this.usingSharedClient) {
|
|
3182
|
+
await Promise.race([
|
|
3183
|
+
this.nostrClient.connect(...this.config.relays),
|
|
3184
|
+
new Promise(
|
|
3185
|
+
(_, reject) => setTimeout(() => reject(new Error(
|
|
3186
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
3187
|
+
)), this.config.timeout)
|
|
3188
|
+
)
|
|
3189
|
+
]);
|
|
3190
|
+
if (!this.nostrClient.isConnected()) {
|
|
3191
|
+
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
3156
3192
|
}
|
|
3157
|
-
});
|
|
3158
|
-
await Promise.race([
|
|
3159
|
-
this.nostrClient.connect(...this.config.relays),
|
|
3160
|
-
new Promise(
|
|
3161
|
-
(_, reject) => setTimeout(() => reject(new Error(
|
|
3162
|
-
`Transport connection timed out after ${this.config.timeout}ms`
|
|
3163
|
-
)), this.config.timeout)
|
|
3164
|
-
)
|
|
3165
|
-
]);
|
|
3166
|
-
if (!this.nostrClient.isConnected()) {
|
|
3167
|
-
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
3168
3193
|
}
|
|
3169
3194
|
this.status = "connected";
|
|
3170
3195
|
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
@@ -3173,6 +3198,21 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3173
3198
|
}
|
|
3174
3199
|
} catch (error) {
|
|
3175
3200
|
this.status = "error";
|
|
3201
|
+
if (this.connectionListener && this.nostrClient) {
|
|
3202
|
+
try {
|
|
3203
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3204
|
+
} catch {
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
this.connectionListener = null;
|
|
3208
|
+
if (this.nostrClient && !this.usingSharedClient) {
|
|
3209
|
+
try {
|
|
3210
|
+
this.nostrClient.disconnect();
|
|
3211
|
+
} catch {
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
this.nostrClient = null;
|
|
3215
|
+
this.usingSharedClient = false;
|
|
3176
3216
|
throw error;
|
|
3177
3217
|
}
|
|
3178
3218
|
}
|
|
@@ -3181,25 +3221,153 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3181
3221
|
clearTimeout(this.resubscribeTimer);
|
|
3182
3222
|
this.resubscribeTimer = null;
|
|
3183
3223
|
}
|
|
3184
|
-
if (this.healthCheckTimer) {
|
|
3185
|
-
clearInterval(this.healthCheckTimer);
|
|
3186
|
-
this.healthCheckTimer = null;
|
|
3187
|
-
}
|
|
3188
3224
|
if (this.nostrClient) {
|
|
3189
|
-
this.
|
|
3225
|
+
if (this.walletSubscriptionId) {
|
|
3226
|
+
try {
|
|
3227
|
+
this.nostrClient.unsubscribe(this.walletSubscriptionId);
|
|
3228
|
+
} catch {
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
if (this.chatSubscriptionId) {
|
|
3232
|
+
try {
|
|
3233
|
+
this.nostrClient.unsubscribe(this.chatSubscriptionId);
|
|
3234
|
+
} catch {
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
if (this.connectionListener) {
|
|
3238
|
+
try {
|
|
3239
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3240
|
+
} catch {
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
if (!this.usingSharedClient) {
|
|
3244
|
+
this.nostrClient.disconnect();
|
|
3245
|
+
}
|
|
3190
3246
|
this.nostrClient = null;
|
|
3191
3247
|
}
|
|
3248
|
+
this.connectionListener = null;
|
|
3249
|
+
this.usingSharedClient = false;
|
|
3192
3250
|
this.walletSubscriptionId = null;
|
|
3193
3251
|
this.chatSubscriptionId = null;
|
|
3194
3252
|
this.chatEoseFired = false;
|
|
3195
|
-
this.lastWalletEventAt = Date.now();
|
|
3196
|
-
this.lastChatEventAt = Date.now();
|
|
3197
3253
|
this.status = "disconnected";
|
|
3198
3254
|
this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
|
|
3199
3255
|
}
|
|
3200
3256
|
isConnected() {
|
|
3201
3257
|
return this.status === "connected" && this.nostrClient?.isConnected() === true;
|
|
3202
3258
|
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Build the connection listener used by both {@link connect} and
|
|
3261
|
+
* {@link rebindToSharedClient}.
|
|
3262
|
+
*
|
|
3263
|
+
* Behavioral notes:
|
|
3264
|
+
* - When the Mux is sharing a {@link NostrClient} with the host
|
|
3265
|
+
* transport (#123), we deliberately do NOT emit
|
|
3266
|
+
* {@code transport:connected} / {@code transport:reconnecting} here
|
|
3267
|
+
* — the host transport's own listener already emits those for the
|
|
3268
|
+
* same socket event. Re-subscribing after a reconnect IS still our
|
|
3269
|
+
* responsibility, since the host has
|
|
3270
|
+
* {@code suppressSubscriptions()}'d its own filters.
|
|
3271
|
+
* - {@code onConnect} does not emit {@code transport:connected}.
|
|
3272
|
+
* The SDK only fires {@code onConnect} on the initial socket
|
|
3273
|
+
* connection (subsequent reconnects use {@code onReconnected}),
|
|
3274
|
+
* and {@link connect()}'s bottom already emits
|
|
3275
|
+
* {@code transport:connected} once that returns. Emitting here too
|
|
3276
|
+
* would double-fire on every initial connect.
|
|
3277
|
+
* - Each callback bails out early when the Mux is not in an active
|
|
3278
|
+
* state ({@code disconnected} / {@code error}). Listeners are
|
|
3279
|
+
* removed on {@code disconnect()} before the callback can fire,
|
|
3280
|
+
* so this guard is mainly defense-in-depth against any in-flight
|
|
3281
|
+
* callback that lands during teardown — but having it at the top
|
|
3282
|
+
* means we never emit a misleading {@code transport:connected}
|
|
3283
|
+
* from a Mux that has already torn down.
|
|
3284
|
+
*/
|
|
3285
|
+
buildConnectionListener() {
|
|
3286
|
+
const isInactive = () => this.status === "disconnected" || this.status === "error";
|
|
3287
|
+
return {
|
|
3288
|
+
onConnect: (url) => {
|
|
3289
|
+
if (isInactive()) return;
|
|
3290
|
+
logger.debug("Mux", "Connected to relay:", url);
|
|
3291
|
+
},
|
|
3292
|
+
onDisconnect: (url, reason) => {
|
|
3293
|
+
logger.debug("Mux", "Disconnected from relay:", url, "reason:", reason);
|
|
3294
|
+
},
|
|
3295
|
+
onReconnecting: (url, attempt) => {
|
|
3296
|
+
if (isInactive()) return;
|
|
3297
|
+
logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
|
|
3298
|
+
if (!this.usingSharedClient) {
|
|
3299
|
+
this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
|
|
3300
|
+
}
|
|
3301
|
+
},
|
|
3302
|
+
onReconnected: (url) => {
|
|
3303
|
+
if (isInactive()) return;
|
|
3304
|
+
logger.debug("Mux", "Reconnected to relay:", url);
|
|
3305
|
+
if (!this.usingSharedClient) {
|
|
3306
|
+
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
3307
|
+
}
|
|
3308
|
+
this.updateSubscriptions().catch((err) => {
|
|
3309
|
+
logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Re-attach to a freshly-created shared NostrClient.
|
|
3316
|
+
*
|
|
3317
|
+
* Call this after the host (e.g. {@link NostrTransportProvider}) has
|
|
3318
|
+
* recreated its NostrClient — typically because the wallet's active
|
|
3319
|
+
* identity changed and the SDK's NostrClient does not support
|
|
3320
|
+
* changing identity at runtime. The previous client has already
|
|
3321
|
+
* been disconnected by the host, so its server-side subscriptions
|
|
3322
|
+
* are gone — we just adopt the new client and re-issue our own.
|
|
3323
|
+
*
|
|
3324
|
+
* The caller is responsible for ordering: by the time rebind runs,
|
|
3325
|
+
* the host transport's new NostrClient must already be created and
|
|
3326
|
+
* connected. In Sphere this is guaranteed because we await
|
|
3327
|
+
* {@code transport.setIdentity()} before calling rebind.
|
|
3328
|
+
*
|
|
3329
|
+
* Returns silently in two cases that are not caller errors:
|
|
3330
|
+
* - the Mux owns its own client (not sharing) — nothing to rebind
|
|
3331
|
+
* - the shared client reference hasn't changed (rebind is a no-op)
|
|
3332
|
+
*
|
|
3333
|
+
* Throws otherwise (rather than silently no-op'ing) so a wiring
|
|
3334
|
+
* mistake — for instance, calling rebind before the host's new
|
|
3335
|
+
* client is ready — surfaces immediately instead of leaving the
|
|
3336
|
+
* Mux pinned to a stale client.
|
|
3337
|
+
*/
|
|
3338
|
+
async rebindToSharedClient() {
|
|
3339
|
+
if (!this.usingSharedClient) return;
|
|
3340
|
+
if (!this.sharedNostrClientGetter) return;
|
|
3341
|
+
const newClient = this.sharedNostrClientGetter();
|
|
3342
|
+
if (!newClient) {
|
|
3343
|
+
throw new SphereError(
|
|
3344
|
+
"rebindToSharedClient: shared client getter returned null. The host transport must finish (re)creating its NostrClient before rebind is called.",
|
|
3345
|
+
"TRANSPORT_ERROR"
|
|
3346
|
+
);
|
|
3347
|
+
}
|
|
3348
|
+
if (this.nostrClient === newClient) return;
|
|
3349
|
+
if (!newClient.isConnected()) {
|
|
3350
|
+
throw new SphereError(
|
|
3351
|
+
"rebindToSharedClient: new shared client is not connected. Await transport.setIdentity() / transport.connect() before rebinding.",
|
|
3352
|
+
"TRANSPORT_ERROR"
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3355
|
+
if (this.nostrClient && this.connectionListener && this.nostrClient !== newClient) {
|
|
3356
|
+
try {
|
|
3357
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3358
|
+
} catch {
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
this.nostrClient = newClient;
|
|
3362
|
+
this.walletSubscriptionId = null;
|
|
3363
|
+
this.chatSubscriptionId = null;
|
|
3364
|
+
this.chatEoseFired = false;
|
|
3365
|
+
this.connectionListener = this.buildConnectionListener();
|
|
3366
|
+
this.nostrClient.addConnectionListener(this.connectionListener);
|
|
3367
|
+
if (this.addresses.size > 0) {
|
|
3368
|
+
await this.updateSubscriptions();
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3203
3371
|
/**
|
|
3204
3372
|
* One-shot fetch of pending events from the relay.
|
|
3205
3373
|
* Creates a temporary subscription, waits for EOSE (or timeout),
|
|
@@ -3328,8 +3496,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3328
3496
|
this.nostrClient.unsubscribe(this.chatSubscriptionId);
|
|
3329
3497
|
this.chatSubscriptionId = null;
|
|
3330
3498
|
}
|
|
3331
|
-
this.lastWalletEventAt = Date.now();
|
|
3332
|
-
this.lastChatEventAt = Date.now();
|
|
3333
3499
|
if (this.addresses.size === 0) return;
|
|
3334
3500
|
const allPubkeys = [];
|
|
3335
3501
|
for (const entry of this.addresses.values()) {
|
|
@@ -3416,25 +3582,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3416
3582
|
}
|
|
3417
3583
|
});
|
|
3418
3584
|
logger.debug("Mux", `updateSubscriptions: walletSub=${this.walletSubscriptionId} chatSub=${this.chatSubscriptionId}`);
|
|
3419
|
-
this.startHealthCheck();
|
|
3420
|
-
}
|
|
3421
|
-
startHealthCheck() {
|
|
3422
|
-
if (this.healthCheckTimer) return;
|
|
3423
|
-
this.healthCheckTimer = setInterval(() => {
|
|
3424
|
-
if (!this.isConnected()) return;
|
|
3425
|
-
const chatElapsed = Date.now() - this.lastChatEventAt;
|
|
3426
|
-
const walletElapsed = Date.now() - this.lastWalletEventAt;
|
|
3427
|
-
const needResubscribe = chatElapsed > 6e4 || walletElapsed > 3e5;
|
|
3428
|
-
if (needResubscribe) {
|
|
3429
|
-
const reason = chatElapsed > 6e4 ? `No chat events for ${Math.round(chatElapsed / 1e3)}s` : `No wallet events for ${Math.round(walletElapsed / 1e3)}s`;
|
|
3430
|
-
logger.warn("Mux", `${reason} \u2014 re-subscribing`);
|
|
3431
|
-
this.lastChatEventAt = Date.now();
|
|
3432
|
-
this.lastWalletEventAt = Date.now();
|
|
3433
|
-
this.updateSubscriptions().catch((err) => {
|
|
3434
|
-
logger.warn("Mux", "Health check re-subscription failed:", err);
|
|
3435
|
-
});
|
|
3436
|
-
}
|
|
3437
|
-
}, 3e4);
|
|
3438
3585
|
}
|
|
3439
3586
|
/**
|
|
3440
3587
|
* Schedule a re-subscription after a relay-initiated subscription closure.
|
|
@@ -3495,12 +3642,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3495
3642
|
}
|
|
3496
3643
|
}
|
|
3497
3644
|
}
|
|
3498
|
-
if (event.kind !== EventKinds2.GIFT_WRAP) {
|
|
3499
|
-
this.lastWalletEventAt = Date.now();
|
|
3500
|
-
}
|
|
3501
|
-
if (event.kind === EventKinds2.GIFT_WRAP) {
|
|
3502
|
-
this.lastChatEventAt = Date.now();
|
|
3503
|
-
}
|
|
3504
3645
|
try {
|
|
3505
3646
|
if (event.kind === EventKinds2.GIFT_WRAP) {
|
|
3506
3647
|
await this.routeGiftWrap(event);
|
|
@@ -11943,6 +12084,132 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
11943
12084
|
};
|
|
11944
12085
|
}
|
|
11945
12086
|
}
|
|
12087
|
+
/**
|
|
12088
|
+
* Mint a fungible token directly to this wallet (genesis mint).
|
|
12089
|
+
*
|
|
12090
|
+
* Useful for test setups that need to seed a wallet with specific token
|
|
12091
|
+
* balances WITHOUT depending on the testnet faucet HTTP service. The
|
|
12092
|
+
* resulting token has the canonical CoinId bytes (passed in `coinIdHex`)
|
|
12093
|
+
* — when those bytes match a registered symbol in the TokenRegistry,
|
|
12094
|
+
* the token shows up under the symbol's name (e.g. "UCT"). There is no
|
|
12095
|
+
* cryptographic restriction on which key may issue a given CoinId; the
|
|
12096
|
+
* aggregator records the mint regardless of issuer identity.
|
|
12097
|
+
*
|
|
12098
|
+
* The flow:
|
|
12099
|
+
* 1. Generate a random TokenId.
|
|
12100
|
+
* 2. Build TokenCoinData with [(coinId, amount)].
|
|
12101
|
+
* 3. Build MintTransactionData with recipient = self (UnmaskedPredicate
|
|
12102
|
+
* from this wallet's signing service).
|
|
12103
|
+
* 4. Submit MintCommitment to the aggregator.
|
|
12104
|
+
* 5. Wait for the inclusion proof.
|
|
12105
|
+
* 6. Construct an SDK Token via Token.mint().
|
|
12106
|
+
* 7. Convert to wallet Token format and call addToken().
|
|
12107
|
+
*
|
|
12108
|
+
* @param coinIdHex - 64-char lowercase hex CoinId. Must match the bytes
|
|
12109
|
+
* used by the registered symbol if you want the wallet to recognize
|
|
12110
|
+
* the token as that symbol (e.g. UCT's coinId from the public registry).
|
|
12111
|
+
* @param amount - Amount in smallest units (multiply by 10^decimals
|
|
12112
|
+
* when converting from human values).
|
|
12113
|
+
* @returns Result with the resulting wallet Token and its on-chain id.
|
|
12114
|
+
*/
|
|
12115
|
+
async mintFungibleToken(coinIdHex, amount) {
|
|
12116
|
+
this.ensureInitialized();
|
|
12117
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
12118
|
+
if (!stClient) {
|
|
12119
|
+
return { success: false, error: "State transition client not available" };
|
|
12120
|
+
}
|
|
12121
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
12122
|
+
if (!trustBase) {
|
|
12123
|
+
return { success: false, error: "Trust base not available" };
|
|
12124
|
+
}
|
|
12125
|
+
try {
|
|
12126
|
+
const signingService = await this.createSigningService();
|
|
12127
|
+
const { TokenId: TokenId5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
12128
|
+
const { TokenCoinData: TokenCoinData3 } = await import("@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData");
|
|
12129
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
12130
|
+
const tokenTypeBytes = fromHex4("f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509");
|
|
12131
|
+
const tokenType = new TokenType3(tokenTypeBytes);
|
|
12132
|
+
const tokenIdBytes = new Uint8Array(32);
|
|
12133
|
+
crypto.getRandomValues(tokenIdBytes);
|
|
12134
|
+
const tokenId = new TokenId5(tokenIdBytes);
|
|
12135
|
+
const coinIdBytes = fromHex4(coinIdHex);
|
|
12136
|
+
const coinId = new CoinId4(coinIdBytes);
|
|
12137
|
+
const coinData = TokenCoinData3.create([[coinId, amount]]);
|
|
12138
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
12139
|
+
tokenType,
|
|
12140
|
+
signingService.algorithm,
|
|
12141
|
+
signingService.publicKey,
|
|
12142
|
+
HashAlgorithm5.SHA256
|
|
12143
|
+
);
|
|
12144
|
+
const ownerAddress = await addressRef.toAddress();
|
|
12145
|
+
const salt = new Uint8Array(32);
|
|
12146
|
+
crypto.getRandomValues(salt);
|
|
12147
|
+
const mintData = await MintTransactionData3.create(
|
|
12148
|
+
tokenId,
|
|
12149
|
+
tokenType,
|
|
12150
|
+
null,
|
|
12151
|
+
// tokenData: no metadata
|
|
12152
|
+
coinData,
|
|
12153
|
+
// fungible coin data
|
|
12154
|
+
ownerAddress,
|
|
12155
|
+
// recipient = self
|
|
12156
|
+
salt,
|
|
12157
|
+
null,
|
|
12158
|
+
// recipientDataHash
|
|
12159
|
+
null
|
|
12160
|
+
// reason: null (genesis, no burn predecessor)
|
|
12161
|
+
);
|
|
12162
|
+
const commitment = await MintCommitment3.create(mintData);
|
|
12163
|
+
const MAX_RETRIES = 3;
|
|
12164
|
+
let lastStatus;
|
|
12165
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
12166
|
+
const response = await stClient.submitMintCommitment(commitment);
|
|
12167
|
+
lastStatus = response.status;
|
|
12168
|
+
if (response.status === "SUCCESS" || response.status === "REQUEST_ID_EXISTS") break;
|
|
12169
|
+
if (attempt === MAX_RETRIES) {
|
|
12170
|
+
return { success: false, error: `Mint submit failed after ${MAX_RETRIES} attempts: ${response.status}` };
|
|
12171
|
+
}
|
|
12172
|
+
await new Promise((r) => setTimeout(r, 1e3 * attempt));
|
|
12173
|
+
}
|
|
12174
|
+
if (lastStatus !== "SUCCESS" && lastStatus !== "REQUEST_ID_EXISTS") {
|
|
12175
|
+
return { success: false, error: `Mint submit failed: ${lastStatus}` };
|
|
12176
|
+
}
|
|
12177
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
12178
|
+
const genesisTransaction = commitment.toTransaction(inclusionProof);
|
|
12179
|
+
const predicate = await UnmaskedPredicate5.create(
|
|
12180
|
+
tokenId,
|
|
12181
|
+
tokenType,
|
|
12182
|
+
signingService,
|
|
12183
|
+
HashAlgorithm5.SHA256,
|
|
12184
|
+
salt
|
|
12185
|
+
);
|
|
12186
|
+
const tokenState = new TokenState5(predicate, null);
|
|
12187
|
+
const sdkToken = await SdkToken2.mint(trustBase, tokenState, genesisTransaction);
|
|
12188
|
+
const tokenIdHex = tokenId.toJSON();
|
|
12189
|
+
const symbol = this.getCoinSymbol(coinIdHex);
|
|
12190
|
+
const name = this.getCoinName(coinIdHex);
|
|
12191
|
+
const decimals = this.getCoinDecimals(coinIdHex);
|
|
12192
|
+
const iconUrl = this.getCoinIconUrl(coinIdHex);
|
|
12193
|
+
const uiToken = {
|
|
12194
|
+
id: tokenIdHex,
|
|
12195
|
+
coinId: coinIdHex,
|
|
12196
|
+
symbol,
|
|
12197
|
+
name,
|
|
12198
|
+
decimals,
|
|
12199
|
+
...iconUrl !== void 0 ? { iconUrl } : {},
|
|
12200
|
+
amount: amount.toString(),
|
|
12201
|
+
status: "confirmed",
|
|
12202
|
+
createdAt: Date.now(),
|
|
12203
|
+
updatedAt: Date.now(),
|
|
12204
|
+
sdkData: JSON.stringify(sdkToken.toJSON())
|
|
12205
|
+
};
|
|
12206
|
+
await this.addToken(uiToken);
|
|
12207
|
+
return { success: true, token: uiToken, tokenId: tokenIdHex };
|
|
12208
|
+
} catch (err) {
|
|
12209
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12210
|
+
return { success: false, error: `Local mint failed: ${msg}` };
|
|
12211
|
+
}
|
|
12212
|
+
}
|
|
11946
12213
|
/**
|
|
11947
12214
|
* Check if a nametag is available for minting
|
|
11948
12215
|
* @param nametag - The nametag to check (e.g., "alice" or "@alice")
|
|
@@ -18153,6 +18420,24 @@ var AccountingModule = class _AccountingModule {
|
|
|
18153
18420
|
dirtyLedgerEntries = /* @__PURE__ */ new Set();
|
|
18154
18421
|
/** Count of unknown (not in invoiceTermsCache) invoice IDs in the ledger. */
|
|
18155
18422
|
unknownLedgerCount = 0;
|
|
18423
|
+
/**
|
|
18424
|
+
* Per-unknown-invoice first-seen timestamp for TTL eviction.
|
|
18425
|
+
*
|
|
18426
|
+
* W1 (steelman round-4): without TTL, an attacker who can deliver 500
|
|
18427
|
+
* inbound transfers with synthesized memo invoiceIds permanently exhausts
|
|
18428
|
+
* the unknown-ledger cap, after which legitimate orphan transfers (out-of-
|
|
18429
|
+
* order delivery for real swaps) are silently dropped at the cap-check.
|
|
18430
|
+
*
|
|
18431
|
+
* Round-5 perf: gated by `unknownLedgerNextSweepMs` to amortize the
|
|
18432
|
+
* sweep cost. The naive every-call sweep is O(N) where N=cap=500;
|
|
18433
|
+
* combined with the per-token cleanup loop inside the sweep it became
|
|
18434
|
+
* O(N×M) on every transfer under flood. Now we sweep at most every
|
|
18435
|
+
* `UNKNOWN_LEDGER_SWEEP_INTERVAL_MS` (60s) UNLESS the cap is currently
|
|
18436
|
+
* full, in which case we sweep on each call (the only path that can
|
|
18437
|
+
* actually drop a legitimate orphan).
|
|
18438
|
+
*/
|
|
18439
|
+
unknownLedgerFirstSeen = /* @__PURE__ */ new Map();
|
|
18440
|
+
unknownLedgerNextSweepMs = 0;
|
|
18156
18441
|
/** W17: Tracks whether tokenScanState has been mutated since last flush. */
|
|
18157
18442
|
tokenScanDirty = false;
|
|
18158
18443
|
/** W2 fix: Serialization guard for _flushDirtyLedgerEntries. */
|
|
@@ -19143,6 +19428,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
19143
19428
|
}
|
|
19144
19429
|
if (this.invoiceLedger.has(tokenId) && !this.invoiceTermsCache.has(tokenId)) {
|
|
19145
19430
|
this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
|
|
19431
|
+
this.unknownLedgerFirstSeen.delete(tokenId);
|
|
19146
19432
|
}
|
|
19147
19433
|
this.invoiceTermsCache.set(tokenId, terms);
|
|
19148
19434
|
this._addToHashIndex(tokenId);
|
|
@@ -19411,6 +19697,29 @@ var AccountingModule = class _AccountingModule {
|
|
|
19411
19697
|
closed: this.closedInvoices.has(invoiceId)
|
|
19412
19698
|
};
|
|
19413
19699
|
}
|
|
19700
|
+
/**
|
|
19701
|
+
* Return the set of token IDs that are currently linked to the given
|
|
19702
|
+
* invoice. Populated by both the on-chain `_processTokenTransactions`
|
|
19703
|
+
* path (tokens with `inv:` references) and the transport-memo orphan
|
|
19704
|
+
* buffering path in `_handleIncomingTransfer`.
|
|
19705
|
+
*
|
|
19706
|
+
* Used by callers that want to scope per-invoice operations (e.g.
|
|
19707
|
+
* SwapModule.verifyPayout's L3 validation) to only the tokens that
|
|
19708
|
+
* cover this invoice — avoiding false negatives when the wallet
|
|
19709
|
+
* contains unrelated tokens of the same currency in unconfirmed or
|
|
19710
|
+
* spent state.
|
|
19711
|
+
*
|
|
19712
|
+
* Returns an empty set if no tokens are currently linked.
|
|
19713
|
+
*/
|
|
19714
|
+
getTokenIdsForInvoice(invoiceId) {
|
|
19715
|
+
const result = /* @__PURE__ */ new Set();
|
|
19716
|
+
for (const [tokenId, invoiceIds] of this.tokenInvoiceMap) {
|
|
19717
|
+
if (invoiceIds.has(invoiceId)) {
|
|
19718
|
+
result.add(tokenId);
|
|
19719
|
+
}
|
|
19720
|
+
}
|
|
19721
|
+
return result;
|
|
19722
|
+
}
|
|
19414
19723
|
/**
|
|
19415
19724
|
* Explicitly close an invoice. Only target parties may close (§8.3).
|
|
19416
19725
|
*
|
|
@@ -19730,6 +20039,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
19730
20039
|
ledger.set(entryKey, forwardRef);
|
|
19731
20040
|
this.dirtyLedgerEntries.add(invoiceId);
|
|
19732
20041
|
this.balanceCache.delete(invoiceId);
|
|
20042
|
+
await this._persistProvisionalAndVerify(invoiceId, "payInvoice");
|
|
19733
20043
|
}
|
|
19734
20044
|
return result;
|
|
19735
20045
|
} finally {
|
|
@@ -19897,6 +20207,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
19897
20207
|
this.dirtyLedgerEntries.add(invoiceId);
|
|
19898
20208
|
}
|
|
19899
20209
|
this.balanceCache.delete(invoiceId);
|
|
20210
|
+
await this._persistProvisionalAndVerify(invoiceId, "returnInvoicePayment");
|
|
19900
20211
|
}
|
|
19901
20212
|
return result;
|
|
19902
20213
|
} finally {
|
|
@@ -20976,9 +21287,30 @@ var AccountingModule = class _AccountingModule {
|
|
|
20976
21287
|
continue;
|
|
20977
21288
|
}
|
|
20978
21289
|
innerMap.set(entryKey, ref);
|
|
20979
|
-
|
|
21290
|
+
const HEX_64 = /^[a-f0-9]{64}$/i;
|
|
21291
|
+
if (entryKey.startsWith("mt:")) {
|
|
21292
|
+
const firstColon = entryKey.indexOf(":");
|
|
21293
|
+
const secondColon = entryKey.indexOf(":", firstColon + 1);
|
|
21294
|
+
if (secondColon > firstColon + 1) {
|
|
21295
|
+
const tokenIdFromKey2 = entryKey.slice(firstColon + 1, secondColon);
|
|
21296
|
+
if (HEX_64.test(tokenIdFromKey2)) {
|
|
21297
|
+
this._addToTokenInvoiceMap(tokenIdFromKey2, invoiceId);
|
|
21298
|
+
}
|
|
21299
|
+
}
|
|
21300
|
+
} else if (entryKey.startsWith("synthetic:")) {
|
|
21301
|
+
const afterPrefix = entryKey.slice("synthetic:".length);
|
|
21302
|
+
const tokenIdEnd = afterPrefix.indexOf(":");
|
|
21303
|
+
if (tokenIdEnd > 0) {
|
|
21304
|
+
const tokenId = afterPrefix.slice(0, tokenIdEnd);
|
|
21305
|
+
if (HEX_64.test(tokenId) && ref.transferId !== tokenId) {
|
|
21306
|
+
this._addToTokenInvoiceMap(tokenId, invoiceId);
|
|
21307
|
+
}
|
|
21308
|
+
}
|
|
21309
|
+
} else if (!ref.transferId.startsWith("provisional:") && ref.transferId.includes(":")) {
|
|
20980
21310
|
const tokenIdFromRef = ref.transferId.slice(0, ref.transferId.indexOf(":"));
|
|
20981
|
-
|
|
21311
|
+
if (HEX_64.test(tokenIdFromRef)) {
|
|
21312
|
+
this._addToTokenInvoiceMap(tokenIdFromRef, invoiceId);
|
|
21313
|
+
}
|
|
20982
21314
|
}
|
|
20983
21315
|
}
|
|
20984
21316
|
} catch (err) {
|
|
@@ -21179,7 +21511,14 @@ var AccountingModule = class _AccountingModule {
|
|
|
21179
21511
|
}
|
|
21180
21512
|
}
|
|
21181
21513
|
for (const [existingKey, existingRef] of ledger) {
|
|
21182
|
-
if (existingKey.startsWith("synthetic:") && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21514
|
+
if ((existingKey.startsWith("synthetic:") || existingKey.startsWith("synthetic-tx:")) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21515
|
+
keysToDelete.push(existingKey);
|
|
21516
|
+
break;
|
|
21517
|
+
}
|
|
21518
|
+
}
|
|
21519
|
+
const mtPrefix = `mt:${tokenId}:`;
|
|
21520
|
+
for (const [existingKey, existingRef] of ledger) {
|
|
21521
|
+
if (existingKey.startsWith(mtPrefix) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21183
21522
|
keysToDelete.push(existingKey);
|
|
21184
21523
|
break;
|
|
21185
21524
|
}
|
|
@@ -21296,6 +21635,49 @@ var AccountingModule = class _AccountingModule {
|
|
|
21296
21635
|
});
|
|
21297
21636
|
}
|
|
21298
21637
|
}
|
|
21638
|
+
/**
|
|
21639
|
+
* Synchronously persist any pending provisional ledger entry for `invoiceId`
|
|
21640
|
+
* before returning to the caller. Used by `payInvoice` and
|
|
21641
|
+
* `returnInvoicePayment` to make the in-memory provisional entry durable
|
|
21642
|
+
* inside the same per-invoice gate that wrote it, closing the
|
|
21643
|
+
* crash-mid-conclude race that produces over-coverage on receivers.
|
|
21644
|
+
*
|
|
21645
|
+
* Implementation:
|
|
21646
|
+
* 1. Schedule a flush via the existing `_flushPromise` chain (so
|
|
21647
|
+
* concurrent `_handleTokenChange` callers waiting on the chain
|
|
21648
|
+
* observe ours as part of the sequence).
|
|
21649
|
+
* 2. Await OUR flush directly — NOT `_drainFlushPromise()`, which would
|
|
21650
|
+
* spin while concurrent token changes keep extending the chain and
|
|
21651
|
+
* hold the per-invoice gate for an unbounded number of additional
|
|
21652
|
+
* flushes. We only need OUR provisional entry durable.
|
|
21653
|
+
* 3. `_flushDirtyLedgerEntries` swallows per-invoice `storage.set`
|
|
21654
|
+
* rejections internally (sets a local `step1Failed` flag), leaving
|
|
21655
|
+
* the dirty entry on the set without re-throwing. So we post-check
|
|
21656
|
+
* `dirtyLedgerEntries.has(invoiceId)` and throw a `STORAGE_ERROR`
|
|
21657
|
+
* `SphereError` if our entry is still dirty — propagating to the
|
|
21658
|
+
* caller so they learn about the durability failure rather than
|
|
21659
|
+
* receiving a silent "success" return that lies on disk.
|
|
21660
|
+
*
|
|
21661
|
+
* @param invoiceId The invoice whose provisional entry must be durable.
|
|
21662
|
+
* @param callContext Used in the error message so the caller is named
|
|
21663
|
+
* ('payInvoice' / 'returnInvoicePayment') without
|
|
21664
|
+
* forcing a stack-trace inspection.
|
|
21665
|
+
*/
|
|
21666
|
+
async _persistProvisionalAndVerify(invoiceId, callContext) {
|
|
21667
|
+
const flushTrigger = (this._flushPromise ?? Promise.resolve()).then(() => this._flushDirtyLedgerEntries());
|
|
21668
|
+
const tracked = flushTrigger.catch(() => {
|
|
21669
|
+
}).finally(() => {
|
|
21670
|
+
if (this._flushPromise === tracked) this._flushPromise = null;
|
|
21671
|
+
});
|
|
21672
|
+
this._flushPromise = tracked;
|
|
21673
|
+
await flushTrigger;
|
|
21674
|
+
if (this.dirtyLedgerEntries.has(invoiceId)) {
|
|
21675
|
+
throw new SphereError(
|
|
21676
|
+
`${callContext}: provisional ledger entry for invoice ${invoiceId} failed to persist \u2014 caller should retry`,
|
|
21677
|
+
"STORAGE_ERROR"
|
|
21678
|
+
);
|
|
21679
|
+
}
|
|
21680
|
+
}
|
|
21299
21681
|
// ===========================================================================
|
|
21300
21682
|
// Internal: Event handlers
|
|
21301
21683
|
// ===========================================================================
|
|
@@ -21356,13 +21738,96 @@ var AccountingModule = class _AccountingModule {
|
|
|
21356
21738
|
}
|
|
21357
21739
|
}
|
|
21358
21740
|
if (!this.invoiceTermsCache.has(invoiceId)) {
|
|
21359
|
-
|
|
21360
|
-
|
|
21361
|
-
invoiceId
|
|
21362
|
-
|
|
21363
|
-
|
|
21364
|
-
|
|
21365
|
-
|
|
21741
|
+
let gracefullyGraduated = false;
|
|
21742
|
+
await this.withInvoiceGate(invoiceId, async () => {
|
|
21743
|
+
if (this.invoiceTermsCache.has(invoiceId)) {
|
|
21744
|
+
gracefullyGraduated = true;
|
|
21745
|
+
return;
|
|
21746
|
+
}
|
|
21747
|
+
const syntheticRef = this._buildSyntheticTransferRef(
|
|
21748
|
+
transfer,
|
|
21749
|
+
invoiceId,
|
|
21750
|
+
paymentDirection,
|
|
21751
|
+
confirmed
|
|
21752
|
+
);
|
|
21753
|
+
deps.emitEvent("invoice:unknown_reference", { invoiceId, transfer: syntheticRef });
|
|
21754
|
+
const MAX_UNKNOWN_INVOICE_IDS = 500;
|
|
21755
|
+
const UNKNOWN_LEDGER_TTL_MS = 30 * 60 * 1e3;
|
|
21756
|
+
const MAX_ORPHAN_ENTRIES_PER_INVOICE = 50;
|
|
21757
|
+
const UNKNOWN_LEDGER_SWEEP_INTERVAL_MS = 6e4;
|
|
21758
|
+
const nowMs = Date.now();
|
|
21759
|
+
const capFull = this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS;
|
|
21760
|
+
const sweepDue = nowMs >= this.unknownLedgerNextSweepMs;
|
|
21761
|
+
if (this.unknownLedgerFirstSeen.size > 0 && (capFull || sweepDue)) {
|
|
21762
|
+
this.unknownLedgerNextSweepMs = nowMs + UNKNOWN_LEDGER_SWEEP_INTERVAL_MS;
|
|
21763
|
+
const expiredIds = [];
|
|
21764
|
+
for (const [unkId, firstSeen] of this.unknownLedgerFirstSeen) {
|
|
21765
|
+
if (nowMs - firstSeen > UNKNOWN_LEDGER_TTL_MS) {
|
|
21766
|
+
expiredIds.push(unkId);
|
|
21767
|
+
}
|
|
21768
|
+
}
|
|
21769
|
+
for (const expiredId of expiredIds) {
|
|
21770
|
+
if (!this.invoiceTermsCache.has(expiredId) && this.invoiceLedger.has(expiredId)) {
|
|
21771
|
+
this.invoiceLedger.delete(expiredId);
|
|
21772
|
+
this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
|
|
21773
|
+
for (const [tokenId, invoiceSet] of this.tokenInvoiceMap) {
|
|
21774
|
+
if (invoiceSet.has(expiredId)) {
|
|
21775
|
+
invoiceSet.delete(expiredId);
|
|
21776
|
+
if (invoiceSet.size === 0) this.tokenInvoiceMap.delete(tokenId);
|
|
21777
|
+
}
|
|
21778
|
+
}
|
|
21779
|
+
}
|
|
21780
|
+
this.unknownLedgerFirstSeen.delete(expiredId);
|
|
21781
|
+
}
|
|
21782
|
+
}
|
|
21783
|
+
if (!this.invoiceLedger.has(invoiceId)) {
|
|
21784
|
+
if (this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS) {
|
|
21785
|
+
return;
|
|
21786
|
+
}
|
|
21787
|
+
this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
|
|
21788
|
+
this.unknownLedgerCount++;
|
|
21789
|
+
this.unknownLedgerFirstSeen.set(invoiceId, nowMs);
|
|
21790
|
+
}
|
|
21791
|
+
const orphanLedger = this.invoiceLedger.get(invoiceId);
|
|
21792
|
+
let mtEntryCount = 0;
|
|
21793
|
+
for (const k of orphanLedger.keys()) {
|
|
21794
|
+
if (k.startsWith("mt:")) mtEntryCount++;
|
|
21795
|
+
}
|
|
21796
|
+
if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
|
|
21797
|
+
return;
|
|
21798
|
+
}
|
|
21799
|
+
for (const token of transfer.tokens) {
|
|
21800
|
+
if (!token.id) continue;
|
|
21801
|
+
let onChainAttributed = false;
|
|
21802
|
+
const tokenKeyPrefix = `${token.id}:`;
|
|
21803
|
+
for (const existingKey of orphanLedger.keys()) {
|
|
21804
|
+
if (existingKey.startsWith(tokenKeyPrefix) && !existingKey.startsWith("mt:")) {
|
|
21805
|
+
onChainAttributed = true;
|
|
21806
|
+
break;
|
|
21807
|
+
}
|
|
21808
|
+
}
|
|
21809
|
+
if (!onChainAttributed) {
|
|
21810
|
+
if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
|
|
21811
|
+
break;
|
|
21812
|
+
}
|
|
21813
|
+
const orphanKey = `mt:${token.id}:${transfer.id}`;
|
|
21814
|
+
if (!orphanLedger.has(orphanKey)) {
|
|
21815
|
+
orphanLedger.set(orphanKey, syntheticRef);
|
|
21816
|
+
mtEntryCount++;
|
|
21817
|
+
}
|
|
21818
|
+
}
|
|
21819
|
+
if (!this.tokenInvoiceMap.has(token.id)) {
|
|
21820
|
+
this.tokenInvoiceMap.set(token.id, /* @__PURE__ */ new Set());
|
|
21821
|
+
}
|
|
21822
|
+
this.tokenInvoiceMap.get(token.id).add(invoiceId);
|
|
21823
|
+
}
|
|
21824
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
21825
|
+
this.balanceCache.delete(invoiceId);
|
|
21826
|
+
await this._flushDirtyLedgerEntries();
|
|
21827
|
+
});
|
|
21828
|
+
if (gracefullyGraduated) {
|
|
21829
|
+
await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
|
|
21830
|
+
}
|
|
21366
21831
|
return;
|
|
21367
21832
|
}
|
|
21368
21833
|
await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
|
|
@@ -21798,7 +22263,8 @@ var AccountingModule = class _AccountingModule {
|
|
|
21798
22263
|
}
|
|
21799
22264
|
const existingLedger = this.invoiceLedger.get(invoiceId);
|
|
21800
22265
|
const firstTokenId = transfer.tokens.find((t) => t.id)?.id;
|
|
21801
|
-
const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22266
|
+
const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22267
|
+
let mutated = false;
|
|
21802
22268
|
if (!existingLedger.has(syntheticKey)) {
|
|
21803
22269
|
let hasRealEntry = false;
|
|
21804
22270
|
for (const tok of transfer.tokens) {
|
|
@@ -21813,8 +22279,25 @@ var AccountingModule = class _AccountingModule {
|
|
|
21813
22279
|
}
|
|
21814
22280
|
if (!hasRealEntry) {
|
|
21815
22281
|
existingLedger.set(syntheticKey, { ...syntheticRef });
|
|
22282
|
+
mutated = true;
|
|
21816
22283
|
}
|
|
21817
22284
|
}
|
|
22285
|
+
for (const tok of transfer.tokens) {
|
|
22286
|
+
if (!tok.id) continue;
|
|
22287
|
+
if (!this.tokenInvoiceMap.has(tok.id)) {
|
|
22288
|
+
this.tokenInvoiceMap.set(tok.id, /* @__PURE__ */ new Set());
|
|
22289
|
+
mutated = true;
|
|
22290
|
+
}
|
|
22291
|
+
const beforeSize = this.tokenInvoiceMap.get(tok.id).size;
|
|
22292
|
+
this.tokenInvoiceMap.get(tok.id).add(invoiceId);
|
|
22293
|
+
if (this.tokenInvoiceMap.get(tok.id).size !== beforeSize) {
|
|
22294
|
+
mutated = true;
|
|
22295
|
+
}
|
|
22296
|
+
}
|
|
22297
|
+
if (mutated) {
|
|
22298
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22299
|
+
this.balanceCache.delete(invoiceId);
|
|
22300
|
+
}
|
|
21818
22301
|
deps.emitEvent("invoice:payment", {
|
|
21819
22302
|
invoiceId,
|
|
21820
22303
|
transfer: syntheticRef,
|
|
@@ -21964,7 +22447,8 @@ var AccountingModule = class _AccountingModule {
|
|
|
21964
22447
|
this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
|
|
21965
22448
|
}
|
|
21966
22449
|
const hLedger = this.invoiceLedger.get(invoiceId);
|
|
21967
|
-
const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22450
|
+
const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22451
|
+
let hMutated = false;
|
|
21968
22452
|
if (!hLedger.has(hKey)) {
|
|
21969
22453
|
let hasRealEntry = false;
|
|
21970
22454
|
if (entry.tokenId) {
|
|
@@ -21977,7 +22461,23 @@ var AccountingModule = class _AccountingModule {
|
|
|
21977
22461
|
}
|
|
21978
22462
|
if (!hasRealEntry) {
|
|
21979
22463
|
hLedger.set(hKey, { ...syntheticRef });
|
|
22464
|
+
hMutated = true;
|
|
22465
|
+
}
|
|
22466
|
+
}
|
|
22467
|
+
if (entry.tokenId) {
|
|
22468
|
+
if (!this.tokenInvoiceMap.has(entry.tokenId)) {
|
|
22469
|
+
this.tokenInvoiceMap.set(entry.tokenId, /* @__PURE__ */ new Set());
|
|
22470
|
+
hMutated = true;
|
|
21980
22471
|
}
|
|
22472
|
+
const beforeSize = this.tokenInvoiceMap.get(entry.tokenId).size;
|
|
22473
|
+
this.tokenInvoiceMap.get(entry.tokenId).add(invoiceId);
|
|
22474
|
+
if (this.tokenInvoiceMap.get(entry.tokenId).size !== beforeSize) {
|
|
22475
|
+
hMutated = true;
|
|
22476
|
+
}
|
|
22477
|
+
}
|
|
22478
|
+
if (hMutated) {
|
|
22479
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22480
|
+
this.balanceCache.delete(invoiceId);
|
|
21981
22481
|
}
|
|
21982
22482
|
deps.emitEvent("invoice:payment", {
|
|
21983
22483
|
invoiceId,
|
|
@@ -24509,17 +25009,63 @@ var SwapModule = class {
|
|
|
24509
25009
|
for (const addr of allAddresses) {
|
|
24510
25010
|
myDirectAddresses.add(addr.directAddress);
|
|
24511
25011
|
}
|
|
24512
|
-
|
|
24513
|
-
|
|
24514
|
-
|
|
24515
|
-
|
|
24516
|
-
|
|
25012
|
+
const matchesPartyA = myDirectAddresses.has(swap.manifest.party_a_address);
|
|
25013
|
+
const matchesPartyB = myDirectAddresses.has(swap.manifest.party_b_address);
|
|
25014
|
+
if (matchesPartyA && matchesPartyB) {
|
|
25015
|
+
throw new SphereError(
|
|
25016
|
+
"Ambiguous party identity: local wallet matches both party_a_address and party_b_address",
|
|
25017
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25018
|
+
);
|
|
25019
|
+
}
|
|
25020
|
+
let myExpectedCurrency;
|
|
25021
|
+
if (matchesPartyA) {
|
|
25022
|
+
myExpectedCurrency = swap.manifest.party_a_currency_to_change;
|
|
25023
|
+
} else if (matchesPartyB) {
|
|
25024
|
+
myExpectedCurrency = swap.manifest.party_b_currency_to_change;
|
|
24517
25025
|
} else {
|
|
24518
25026
|
throw new SphereError(
|
|
24519
25027
|
"Local wallet address does not match either party in the swap manifest",
|
|
24520
25028
|
"SWAP_DEPOSIT_FAILED"
|
|
24521
25029
|
);
|
|
24522
25030
|
}
|
|
25031
|
+
if (!myExpectedCurrency || myExpectedCurrency === "") {
|
|
25032
|
+
throw new SphereError(
|
|
25033
|
+
"Manifest currency_to_change is empty for this party",
|
|
25034
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25035
|
+
);
|
|
25036
|
+
}
|
|
25037
|
+
const invoiceRefForAssetLookup = deps.accounting.getInvoice(swap.depositInvoiceId);
|
|
25038
|
+
if (!invoiceRefForAssetLookup) {
|
|
25039
|
+
throw new SphereError(
|
|
25040
|
+
"Deposit invoice not yet imported into accounting module",
|
|
25041
|
+
"SWAP_WRONG_STATE"
|
|
25042
|
+
);
|
|
25043
|
+
}
|
|
25044
|
+
const depositTarget = invoiceRefForAssetLookup.terms.targets[0];
|
|
25045
|
+
if (!depositTarget) {
|
|
25046
|
+
throw new SphereError(
|
|
25047
|
+
"Deposit invoice has no targets",
|
|
25048
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25049
|
+
);
|
|
25050
|
+
}
|
|
25051
|
+
const assetIndex = depositTarget.assets.findIndex(
|
|
25052
|
+
(a) => a.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)
|
|
25053
|
+
);
|
|
25054
|
+
if (assetIndex < 0) {
|
|
25055
|
+
throw new SphereError(
|
|
25056
|
+
`No asset matching expected currency ${myExpectedCurrency} found in deposit invoice`,
|
|
25057
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25058
|
+
);
|
|
25059
|
+
}
|
|
25060
|
+
for (let i = assetIndex + 1; i < depositTarget.assets.length; i += 1) {
|
|
25061
|
+
const a = depositTarget.assets[i];
|
|
25062
|
+
if (a?.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)) {
|
|
25063
|
+
throw new SphereError(
|
|
25064
|
+
`Ambiguous asset match in deposit invoice: slots ${assetIndex} and ${i} both match currency ${myExpectedCurrency}`,
|
|
25065
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25066
|
+
);
|
|
25067
|
+
}
|
|
25068
|
+
}
|
|
24523
25069
|
return this.withSwapGate(swapId, async () => {
|
|
24524
25070
|
if (swap.progress !== "announced") {
|
|
24525
25071
|
throw new SphereError(
|
|
@@ -24625,7 +25171,6 @@ var SwapModule = class {
|
|
|
24625
25171
|
swap.updatedAt = Date.now();
|
|
24626
25172
|
this.clearLocalTimer(swap.swapId);
|
|
24627
25173
|
this.terminalSwapIds.add(swap.swapId);
|
|
24628
|
-
const entryIdx = this._storedTerminalEntries.length;
|
|
24629
25174
|
this._storedTerminalEntries.push({
|
|
24630
25175
|
swapId: swap.swapId,
|
|
24631
25176
|
progress: "failed",
|
|
@@ -24640,7 +25185,13 @@ var SwapModule = class {
|
|
|
24640
25185
|
swap.error = prevError;
|
|
24641
25186
|
swap.updatedAt = prevUpdatedAt;
|
|
24642
25187
|
this.terminalSwapIds.delete(swap.swapId);
|
|
24643
|
-
this._storedTerminalEntries.
|
|
25188
|
+
for (let i = this._storedTerminalEntries.length - 1; i >= 0; i--) {
|
|
25189
|
+
const entry = this._storedTerminalEntries[i];
|
|
25190
|
+
if (entry.swapId === swap.swapId && entry.progress === "failed") {
|
|
25191
|
+
this._storedTerminalEntries.splice(i, 1);
|
|
25192
|
+
break;
|
|
25193
|
+
}
|
|
25194
|
+
}
|
|
24644
25195
|
logger.warn(LOG_TAG3, `failPayout: persistSwap failed for ${swapId}; fraud detection will retry on next load:`, persistErr);
|
|
24645
25196
|
throw persistErr;
|
|
24646
25197
|
}
|
|
@@ -24684,9 +25235,25 @@ var SwapModule = class {
|
|
|
24684
25235
|
if (!targetStatus.coinAssets[0].isCovered) {
|
|
24685
25236
|
return returnFalse();
|
|
24686
25237
|
}
|
|
24687
|
-
|
|
25238
|
+
let netCoveredAmount;
|
|
25239
|
+
let expectedAmountBigInt;
|
|
25240
|
+
try {
|
|
25241
|
+
netCoveredAmount = BigInt(targetStatus.coinAssets[0].netCoveredAmount);
|
|
25242
|
+
expectedAmountBigInt = BigInt(expectedAmount);
|
|
25243
|
+
} catch (parseErr) {
|
|
25244
|
+
return failPayout(
|
|
25245
|
+
`MALFORMED_AMOUNT: failed to parse coverage amounts (netCoveredAmount=${targetStatus.coinAssets[0].netCoveredAmount}, expectedAmount=${expectedAmount}): ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`
|
|
25246
|
+
);
|
|
25247
|
+
}
|
|
25248
|
+
if (netCoveredAmount < expectedAmountBigInt) {
|
|
24688
25249
|
return returnFalse();
|
|
24689
25250
|
}
|
|
25251
|
+
if (netCoveredAmount > expectedAmountBigInt) {
|
|
25252
|
+
const surplus = netCoveredAmount - expectedAmountBigInt;
|
|
25253
|
+
return failPayout(
|
|
25254
|
+
`OVER_COVERAGE: net=${netCoveredAmount.toString()}, expected=${expectedAmount}, surplus=${surplus.toString()} \u2014 surplus refund expected via auto-return; settlement halted`
|
|
25255
|
+
);
|
|
25256
|
+
}
|
|
24690
25257
|
const escrowAddr = swap.deal.escrowAddress ?? this.config.defaultEscrowAddress;
|
|
24691
25258
|
if (escrowAddr) {
|
|
24692
25259
|
const escrowPeer = await deps.resolve(escrowAddr);
|
|
@@ -24696,8 +25263,28 @@ var SwapModule = class {
|
|
|
24696
25263
|
}
|
|
24697
25264
|
const validationResult = await deps.payments.validate();
|
|
24698
25265
|
if (validationResult.invalid.length > 0) {
|
|
24699
|
-
|
|
24700
|
-
|
|
25266
|
+
const payoutTokenIds = deps.accounting.getTokenIdsForInvoice?.(swap.payoutInvoiceId) ?? /* @__PURE__ */ new Set();
|
|
25267
|
+
if (payoutTokenIds.size === 0) {
|
|
25268
|
+
logger.warn(
|
|
25269
|
+
LOG_TAG3,
|
|
25270
|
+
`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`
|
|
25271
|
+
);
|
|
25272
|
+
return returnFalse();
|
|
25273
|
+
}
|
|
25274
|
+
const relevantInvalid = validationResult.invalid.filter(
|
|
25275
|
+
(t) => payoutTokenIds.has(t.id)
|
|
25276
|
+
);
|
|
25277
|
+
if (relevantInvalid.length > 0) {
|
|
25278
|
+
logger.warn(
|
|
25279
|
+
LOG_TAG3,
|
|
25280
|
+
`verifyPayout for ${swapId.slice(0, 12)}: L3 validation found ${relevantInvalid.length} invalid token(s) covering this payout invoice \u2014 retry after wallet sync`
|
|
25281
|
+
);
|
|
25282
|
+
return returnFalse();
|
|
25283
|
+
}
|
|
25284
|
+
logger.debug(
|
|
25285
|
+
LOG_TAG3,
|
|
25286
|
+
`verifyPayout for ${swapId.slice(0, 12)}: ${validationResult.invalid.length} unrelated invalid token(s) ignored (not linked to this payout invoice)`
|
|
25287
|
+
);
|
|
24701
25288
|
}
|
|
24702
25289
|
if (swap.progress === "completed") {
|
|
24703
25290
|
swap.payoutVerified = true;
|
|
@@ -24876,8 +25463,22 @@ var SwapModule = class {
|
|
|
24876
25463
|
* @param dm - The incoming direct message.
|
|
24877
25464
|
*/
|
|
24878
25465
|
handleIncomingDM(dm) {
|
|
25466
|
+
if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
|
|
25467
|
+
logger.warn(
|
|
25468
|
+
LOG_TAG3,
|
|
25469
|
+
`diag_swap_dm_arrived sender=${dm.senderPubkey.slice(0, 16)} length=${dm.content.length}`
|
|
25470
|
+
);
|
|
25471
|
+
}
|
|
24879
25472
|
const parsed = parseSwapDM(dm.content);
|
|
24880
|
-
if (!parsed)
|
|
25473
|
+
if (!parsed) {
|
|
25474
|
+
if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
|
|
25475
|
+
logger.warn(
|
|
25476
|
+
LOG_TAG3,
|
|
25477
|
+
`diag_swap_dm_parse_rejected sender=${dm.senderPubkey.slice(0, 16)} prefix=${dm.content.slice(0, 80)}`
|
|
25478
|
+
);
|
|
25479
|
+
}
|
|
25480
|
+
return;
|
|
25481
|
+
}
|
|
24881
25482
|
void (async () => {
|
|
24882
25483
|
try {
|
|
24883
25484
|
switch (parsed.kind) {
|
|
@@ -25243,16 +25844,43 @@ var SwapModule = class {
|
|
|
25243
25844
|
// invoice_delivery (§12.4.2 + §12.4.3)
|
|
25244
25845
|
// ---------------------------------------------------------------
|
|
25245
25846
|
case "invoice_delivery": {
|
|
25246
|
-
|
|
25847
|
+
logger.warn(
|
|
25848
|
+
LOG_TAG3,
|
|
25849
|
+
`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)}`
|
|
25850
|
+
);
|
|
25851
|
+
if (!swapId) {
|
|
25852
|
+
logger.warn(LOG_TAG3, "diag_invoice_delivery_dropped reason=no_swap_id");
|
|
25853
|
+
return;
|
|
25854
|
+
}
|
|
25247
25855
|
const swap = this.swaps.get(swapId);
|
|
25248
|
-
if (!swap)
|
|
25249
|
-
|
|
25856
|
+
if (!swap) {
|
|
25857
|
+
logger.warn(
|
|
25858
|
+
LOG_TAG3,
|
|
25859
|
+
`diag_invoice_delivery_dropped reason=swap_not_in_map swap_id=${swapId.slice(0, 16)} known_swap_ids_count=${this.swaps.size}`
|
|
25860
|
+
);
|
|
25861
|
+
return;
|
|
25862
|
+
}
|
|
25863
|
+
if (!this.isFromExpectedEscrow(dm.senderPubkey, swap)) {
|
|
25864
|
+
logger.warn(
|
|
25865
|
+
LOG_TAG3,
|
|
25866
|
+
`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)}`
|
|
25867
|
+
);
|
|
25868
|
+
return;
|
|
25869
|
+
}
|
|
25250
25870
|
const deps = this.deps;
|
|
25251
25871
|
if (msg.invoice_type === "deposit") {
|
|
25872
|
+
logger.warn(
|
|
25873
|
+
LOG_TAG3,
|
|
25874
|
+
`diag_invoice_delivery_proceeding_to_import swap_id=${swapId.slice(0, 16)} progress=${swap.progress} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)}`
|
|
25875
|
+
);
|
|
25252
25876
|
await this.withSwapGate(swapId, async () => {
|
|
25253
25877
|
if (isTerminalProgress(swap.progress)) return;
|
|
25254
25878
|
try {
|
|
25255
25879
|
await deps.accounting.importInvoice(msg.invoice_token);
|
|
25880
|
+
logger.warn(
|
|
25881
|
+
LOG_TAG3,
|
|
25882
|
+
`diag_invoice_imported swap_id=${swapId.slice(0, 16)} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)} type=deposit`
|
|
25883
|
+
);
|
|
25256
25884
|
} catch (err) {
|
|
25257
25885
|
if (err instanceof SphereError && err.code === "INVOICE_ALREADY_EXISTS") {
|
|
25258
25886
|
logger.debug(LOG_TAG3, `Deposit invoice for swap ${swapId} already imported \u2014 relay re-delivery, continuing`);
|
|
@@ -26745,27 +27373,42 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
26745
27373
|
|
|
26746
27374
|
// core/Sphere.ts
|
|
26747
27375
|
import { SigningService as SigningService2 } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
|
|
27376
|
+
import { normalizeNametag as normalizeNametag2, isPhoneNumber } from "@unicitylabs/nostr-js-sdk";
|
|
27377
|
+
|
|
27378
|
+
// core/address-derivation.ts
|
|
26748
27379
|
import { TokenType as TokenType5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
26749
27380
|
import { HashAlgorithm as HashAlgorithm7 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
26750
27381
|
import { UnmaskedPredicateReference as UnmaskedPredicateReference3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
|
|
26751
|
-
|
|
27382
|
+
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
27383
|
+
var COMPRESSED_PUBKEY_RE = /^(02|03)[0-9a-fA-F]{64}$/;
|
|
27384
|
+
async function computeDirectAddressFromChainPubkey(chainPubkey) {
|
|
27385
|
+
if (typeof chainPubkey !== "string" || !COMPRESSED_PUBKEY_RE.test(chainPubkey)) {
|
|
27386
|
+
throw new Error(
|
|
27387
|
+
`computeDirectAddressFromChainPubkey: chainPubkey must be 66-char hex with 02/03 prefix, got "${String(chainPubkey).slice(0, 12)}..."`
|
|
27388
|
+
);
|
|
27389
|
+
}
|
|
27390
|
+
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
27391
|
+
const tokenType = new TokenType5(tokenTypeBytes);
|
|
27392
|
+
const publicKeyBytes = Buffer.from(chainPubkey, "hex");
|
|
27393
|
+
const predicateRef = await UnmaskedPredicateReference3.create(
|
|
27394
|
+
tokenType,
|
|
27395
|
+
"secp256k1",
|
|
27396
|
+
publicKeyBytes,
|
|
27397
|
+
HashAlgorithm7.SHA256
|
|
27398
|
+
);
|
|
27399
|
+
return (await predicateRef.toAddress()).toString();
|
|
27400
|
+
}
|
|
27401
|
+
|
|
27402
|
+
// core/Sphere.ts
|
|
26752
27403
|
function isValidNametag2(nametag) {
|
|
26753
27404
|
if (isPhoneNumber(nametag)) return true;
|
|
26754
27405
|
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
26755
27406
|
}
|
|
26756
|
-
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
26757
27407
|
async function deriveL3PredicateAddress(privateKey) {
|
|
26758
27408
|
const secret = Buffer.from(privateKey, "hex");
|
|
26759
27409
|
const signingService = await SigningService2.createFromSecret(secret);
|
|
26760
|
-
const
|
|
26761
|
-
|
|
26762
|
-
const predicateRef = UnmaskedPredicateReference3.create(
|
|
26763
|
-
tokenType,
|
|
26764
|
-
signingService.algorithm,
|
|
26765
|
-
signingService.publicKey,
|
|
26766
|
-
HashAlgorithm7.SHA256
|
|
26767
|
-
);
|
|
26768
|
-
return (await (await predicateRef).toAddress()).toString();
|
|
27410
|
+
const pubkeyHex = Buffer.from(signingService.publicKey).toString("hex");
|
|
27411
|
+
return computeDirectAddressFromChainPubkey(pubkeyHex);
|
|
26769
27412
|
}
|
|
26770
27413
|
var Sphere = class _Sphere {
|
|
26771
27414
|
// Singleton
|
|
@@ -28219,6 +28862,9 @@ var Sphere = class _Sphere {
|
|
|
28219
28862
|
this._transport.setFallbackSince(fallbackTs);
|
|
28220
28863
|
}
|
|
28221
28864
|
await this._transport.setIdentity(this._identity);
|
|
28865
|
+
if (this._transportMux && typeof this._transportMux.rebindToSharedClient === "function") {
|
|
28866
|
+
await this._transportMux.rebindToSharedClient();
|
|
28867
|
+
}
|
|
28222
28868
|
this.emitEvent("identity:changed", {
|
|
28223
28869
|
l1Address: this._identity.l1Address,
|
|
28224
28870
|
directAddress: this._identity.directAddress,
|
|
@@ -28429,7 +29075,12 @@ var Sphere = class _Sphere {
|
|
|
28429
29075
|
this._transportMux = new MultiAddressTransportMux({
|
|
28430
29076
|
relays: nostrTransport.getConfiguredRelays(),
|
|
28431
29077
|
createWebSocket: nostrTransport.getWebSocketFactory(),
|
|
28432
|
-
storage: nostrTransport.getStorageAdapter() ?? void 0
|
|
29078
|
+
storage: nostrTransport.getStorageAdapter() ?? void 0,
|
|
29079
|
+
// #123: share the original transport's NostrClient instead of
|
|
29080
|
+
// opening a second WebSocket per relay. Pass a getter so the
|
|
29081
|
+
// Mux resolves it at connect-time (after the transport finishes
|
|
29082
|
+
// its own connect()).
|
|
29083
|
+
sharedNostrClient: typeof nostrTransport.getNostrClient === "function" ? () => nostrTransport.getNostrClient() : void 0
|
|
28433
29084
|
});
|
|
28434
29085
|
await this._transportMux.connect();
|
|
28435
29086
|
if (typeof nostrTransport.suppressSubscriptions === "function") {
|
|
@@ -30141,6 +30792,7 @@ export {
|
|
|
30141
30792
|
base58Encode,
|
|
30142
30793
|
bytesToHex3 as bytesToHex,
|
|
30143
30794
|
checkNetworkHealth,
|
|
30795
|
+
computeDirectAddressFromChainPubkey,
|
|
30144
30796
|
computeHash160,
|
|
30145
30797
|
convertBits,
|
|
30146
30798
|
createAddress,
|