@unicitylabs/sphere-sdk 0.7.1-dev.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -77
- package/dist/core/index.cjs +771 -118
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +187 -6
- package/dist/core/index.d.ts +187 -6
- package/dist/core/index.js +770 -118
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +11 -0
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +11 -0
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +11 -0
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +10 -1
- package/dist/impl/nodejs/index.d.ts +10 -1
- package/dist/impl/nodejs/index.js +11 -0
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +899 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +258 -1
- package/dist/index.d.ts +258 -1
- package/dist/index.js +881 -118
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1389,6 +1389,17 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1389
1389
|
getStorageAdapter() {
|
|
1390
1390
|
return this.storage;
|
|
1391
1391
|
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Get the underlying NostrClient (or null if not yet connected).
|
|
1394
|
+
*
|
|
1395
|
+
* Exposed so {@link MultiAddressTransportMux} can share the same
|
|
1396
|
+
* client/socket pair instead of opening a duplicate WebSocket per
|
|
1397
|
+
* relay (#123). The transport owns the client's lifecycle — callers
|
|
1398
|
+
* MUST NOT call {@code disconnect()} on the returned instance.
|
|
1399
|
+
*/
|
|
1400
|
+
getNostrClient() {
|
|
1401
|
+
return this.nostrClient;
|
|
1402
|
+
}
|
|
1392
1403
|
/**
|
|
1393
1404
|
* Suppress event subscriptions — unsubscribe wallet/chat filters
|
|
1394
1405
|
* but keep the connection alive for resolve/identity-binding operations.
|
|
@@ -3027,9 +3038,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3027
3038
|
chatSubscriptionId = null;
|
|
3028
3039
|
chatEoseFired = false;
|
|
3029
3040
|
resubscribeTimer = null;
|
|
3030
|
-
lastWalletEventAt = Date.now();
|
|
3031
|
-
lastChatEventAt = Date.now();
|
|
3032
|
-
healthCheckTimer = null;
|
|
3033
3041
|
chatEoseHandlers = [];
|
|
3034
3042
|
// Dedup — bounded to prevent memory leak in long-running sessions.
|
|
3035
3043
|
// Set preserves insertion order; evict oldest entries when cap is reached.
|
|
@@ -3040,6 +3048,19 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3040
3048
|
// Identity key for the Mux's NostrClient — relays may filter gift-wrap
|
|
3041
3049
|
// delivery to the recipient's subscription key.
|
|
3042
3050
|
identityPrivateKey;
|
|
3051
|
+
// Resolves the shared NostrClient at use-time (the source provider may
|
|
3052
|
+
// create its client lazily, after the Mux is constructed). null means
|
|
3053
|
+
// "no shared client; create our own."
|
|
3054
|
+
sharedNostrClientGetter;
|
|
3055
|
+
// True when this Mux is using a shared NostrClient and therefore must
|
|
3056
|
+
// not call connect()/disconnect() on it.
|
|
3057
|
+
usingSharedClient = false;
|
|
3058
|
+
// Listener registered on the underlying NostrClient. Tracked so we can
|
|
3059
|
+
// remove it on disconnect / rebind — otherwise a long-lived shared
|
|
3060
|
+
// client accumulates listeners across address switches and (worse)
|
|
3061
|
+
// a "disconnected" Mux still sees onReconnected callbacks fire and
|
|
3062
|
+
// re-establish subscriptions it shouldn't have.
|
|
3063
|
+
connectionListener = null;
|
|
3043
3064
|
constructor(config) {
|
|
3044
3065
|
this.identityPrivateKey = config.identityPrivateKey;
|
|
3045
3066
|
this.config = {
|
|
@@ -3052,6 +3073,14 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3052
3073
|
generateUUID: config.generateUUID ?? defaultUUIDGenerator
|
|
3053
3074
|
};
|
|
3054
3075
|
this.storage = config.storage ?? null;
|
|
3076
|
+
if (typeof config.sharedNostrClient === "function") {
|
|
3077
|
+
this.sharedNostrClientGetter = config.sharedNostrClient;
|
|
3078
|
+
} else if (config.sharedNostrClient) {
|
|
3079
|
+
const c = config.sharedNostrClient;
|
|
3080
|
+
this.sharedNostrClientGetter = () => c;
|
|
3081
|
+
} else {
|
|
3082
|
+
this.sharedNostrClientGetter = null;
|
|
3083
|
+
}
|
|
3055
3084
|
}
|
|
3056
3085
|
// ===========================================================================
|
|
3057
3086
|
// Address Management
|
|
@@ -3142,53 +3171,49 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3142
3171
|
if (this.status === "connected") return;
|
|
3143
3172
|
this.status = "connecting";
|
|
3144
3173
|
try {
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3174
|
+
const shared = this.sharedNostrClientGetter ? this.sharedNostrClientGetter() : null;
|
|
3175
|
+
if (shared) {
|
|
3176
|
+
if (!shared.isConnected()) {
|
|
3177
|
+
throw new SphereError(
|
|
3178
|
+
"sharedNostrClient is not connected; the Mux cannot share a closed socket",
|
|
3179
|
+
"TRANSPORT_ERROR"
|
|
3149
3180
|
);
|
|
3150
|
-
} else {
|
|
3151
|
-
const tempKey = Buffer3.alloc(32);
|
|
3152
|
-
crypto.getRandomValues(tempKey);
|
|
3153
|
-
this.primaryKeyManager = NostrKeyManager2.fromPrivateKey(tempKey);
|
|
3154
3181
|
}
|
|
3182
|
+
this.nostrClient = shared;
|
|
3183
|
+
this.usingSharedClient = true;
|
|
3184
|
+
} else {
|
|
3185
|
+
if (!this.primaryKeyManager) {
|
|
3186
|
+
if (this.identityPrivateKey) {
|
|
3187
|
+
this.primaryKeyManager = NostrKeyManager2.fromPrivateKey(
|
|
3188
|
+
Buffer3.from(this.identityPrivateKey)
|
|
3189
|
+
);
|
|
3190
|
+
} else {
|
|
3191
|
+
const tempKey = Buffer3.alloc(32);
|
|
3192
|
+
crypto.getRandomValues(tempKey);
|
|
3193
|
+
this.primaryKeyManager = NostrKeyManager2.fromPrivateKey(tempKey);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
this.nostrClient = new NostrClient2(this.primaryKeyManager, {
|
|
3197
|
+
autoReconnect: this.config.autoReconnect,
|
|
3198
|
+
reconnectIntervalMs: this.config.reconnectDelay,
|
|
3199
|
+
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
3200
|
+
pingIntervalMs: 15e3
|
|
3201
|
+
});
|
|
3155
3202
|
}
|
|
3156
|
-
this.
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
},
|
|
3170
|
-
onReconnecting: (url, attempt) => {
|
|
3171
|
-
logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
|
|
3172
|
-
this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
|
|
3173
|
-
},
|
|
3174
|
-
onReconnected: (url) => {
|
|
3175
|
-
logger.debug("Mux", "Reconnected to relay:", url);
|
|
3176
|
-
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
3177
|
-
this.updateSubscriptions().catch((err) => {
|
|
3178
|
-
logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
|
|
3179
|
-
});
|
|
3203
|
+
this.connectionListener = this.buildConnectionListener();
|
|
3204
|
+
this.nostrClient.addConnectionListener(this.connectionListener);
|
|
3205
|
+
if (!this.usingSharedClient) {
|
|
3206
|
+
await Promise.race([
|
|
3207
|
+
this.nostrClient.connect(...this.config.relays),
|
|
3208
|
+
new Promise(
|
|
3209
|
+
(_, reject) => setTimeout(() => reject(new Error(
|
|
3210
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
3211
|
+
)), this.config.timeout)
|
|
3212
|
+
)
|
|
3213
|
+
]);
|
|
3214
|
+
if (!this.nostrClient.isConnected()) {
|
|
3215
|
+
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
3180
3216
|
}
|
|
3181
|
-
});
|
|
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");
|
|
3192
3217
|
}
|
|
3193
3218
|
this.status = "connected";
|
|
3194
3219
|
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
@@ -3197,6 +3222,21 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3197
3222
|
}
|
|
3198
3223
|
} catch (error) {
|
|
3199
3224
|
this.status = "error";
|
|
3225
|
+
if (this.connectionListener && this.nostrClient) {
|
|
3226
|
+
try {
|
|
3227
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3228
|
+
} catch {
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
this.connectionListener = null;
|
|
3232
|
+
if (this.nostrClient && !this.usingSharedClient) {
|
|
3233
|
+
try {
|
|
3234
|
+
this.nostrClient.disconnect();
|
|
3235
|
+
} catch {
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
this.nostrClient = null;
|
|
3239
|
+
this.usingSharedClient = false;
|
|
3200
3240
|
throw error;
|
|
3201
3241
|
}
|
|
3202
3242
|
}
|
|
@@ -3205,25 +3245,153 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3205
3245
|
clearTimeout(this.resubscribeTimer);
|
|
3206
3246
|
this.resubscribeTimer = null;
|
|
3207
3247
|
}
|
|
3208
|
-
if (this.healthCheckTimer) {
|
|
3209
|
-
clearInterval(this.healthCheckTimer);
|
|
3210
|
-
this.healthCheckTimer = null;
|
|
3211
|
-
}
|
|
3212
3248
|
if (this.nostrClient) {
|
|
3213
|
-
this.
|
|
3249
|
+
if (this.walletSubscriptionId) {
|
|
3250
|
+
try {
|
|
3251
|
+
this.nostrClient.unsubscribe(this.walletSubscriptionId);
|
|
3252
|
+
} catch {
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
if (this.chatSubscriptionId) {
|
|
3256
|
+
try {
|
|
3257
|
+
this.nostrClient.unsubscribe(this.chatSubscriptionId);
|
|
3258
|
+
} catch {
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
if (this.connectionListener) {
|
|
3262
|
+
try {
|
|
3263
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3264
|
+
} catch {
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
if (!this.usingSharedClient) {
|
|
3268
|
+
this.nostrClient.disconnect();
|
|
3269
|
+
}
|
|
3214
3270
|
this.nostrClient = null;
|
|
3215
3271
|
}
|
|
3272
|
+
this.connectionListener = null;
|
|
3273
|
+
this.usingSharedClient = false;
|
|
3216
3274
|
this.walletSubscriptionId = null;
|
|
3217
3275
|
this.chatSubscriptionId = null;
|
|
3218
3276
|
this.chatEoseFired = false;
|
|
3219
|
-
this.lastWalletEventAt = Date.now();
|
|
3220
|
-
this.lastChatEventAt = Date.now();
|
|
3221
3277
|
this.status = "disconnected";
|
|
3222
3278
|
this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
|
|
3223
3279
|
}
|
|
3224
3280
|
isConnected() {
|
|
3225
3281
|
return this.status === "connected" && this.nostrClient?.isConnected() === true;
|
|
3226
3282
|
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Build the connection listener used by both {@link connect} and
|
|
3285
|
+
* {@link rebindToSharedClient}.
|
|
3286
|
+
*
|
|
3287
|
+
* Behavioral notes:
|
|
3288
|
+
* - When the Mux is sharing a {@link NostrClient} with the host
|
|
3289
|
+
* transport (#123), we deliberately do NOT emit
|
|
3290
|
+
* {@code transport:connected} / {@code transport:reconnecting} here
|
|
3291
|
+
* — the host transport's own listener already emits those for the
|
|
3292
|
+
* same socket event. Re-subscribing after a reconnect IS still our
|
|
3293
|
+
* responsibility, since the host has
|
|
3294
|
+
* {@code suppressSubscriptions()}'d its own filters.
|
|
3295
|
+
* - {@code onConnect} does not emit {@code transport:connected}.
|
|
3296
|
+
* The SDK only fires {@code onConnect} on the initial socket
|
|
3297
|
+
* connection (subsequent reconnects use {@code onReconnected}),
|
|
3298
|
+
* and {@link connect()}'s bottom already emits
|
|
3299
|
+
* {@code transport:connected} once that returns. Emitting here too
|
|
3300
|
+
* would double-fire on every initial connect.
|
|
3301
|
+
* - Each callback bails out early when the Mux is not in an active
|
|
3302
|
+
* state ({@code disconnected} / {@code error}). Listeners are
|
|
3303
|
+
* removed on {@code disconnect()} before the callback can fire,
|
|
3304
|
+
* so this guard is mainly defense-in-depth against any in-flight
|
|
3305
|
+
* callback that lands during teardown — but having it at the top
|
|
3306
|
+
* means we never emit a misleading {@code transport:connected}
|
|
3307
|
+
* from a Mux that has already torn down.
|
|
3308
|
+
*/
|
|
3309
|
+
buildConnectionListener() {
|
|
3310
|
+
const isInactive = () => this.status === "disconnected" || this.status === "error";
|
|
3311
|
+
return {
|
|
3312
|
+
onConnect: (url) => {
|
|
3313
|
+
if (isInactive()) return;
|
|
3314
|
+
logger.debug("Mux", "Connected to relay:", url);
|
|
3315
|
+
},
|
|
3316
|
+
onDisconnect: (url, reason) => {
|
|
3317
|
+
logger.debug("Mux", "Disconnected from relay:", url, "reason:", reason);
|
|
3318
|
+
},
|
|
3319
|
+
onReconnecting: (url, attempt) => {
|
|
3320
|
+
if (isInactive()) return;
|
|
3321
|
+
logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
|
|
3322
|
+
if (!this.usingSharedClient) {
|
|
3323
|
+
this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
|
|
3324
|
+
}
|
|
3325
|
+
},
|
|
3326
|
+
onReconnected: (url) => {
|
|
3327
|
+
if (isInactive()) return;
|
|
3328
|
+
logger.debug("Mux", "Reconnected to relay:", url);
|
|
3329
|
+
if (!this.usingSharedClient) {
|
|
3330
|
+
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
3331
|
+
}
|
|
3332
|
+
this.updateSubscriptions().catch((err) => {
|
|
3333
|
+
logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Re-attach to a freshly-created shared NostrClient.
|
|
3340
|
+
*
|
|
3341
|
+
* Call this after the host (e.g. {@link NostrTransportProvider}) has
|
|
3342
|
+
* recreated its NostrClient — typically because the wallet's active
|
|
3343
|
+
* identity changed and the SDK's NostrClient does not support
|
|
3344
|
+
* changing identity at runtime. The previous client has already
|
|
3345
|
+
* been disconnected by the host, so its server-side subscriptions
|
|
3346
|
+
* are gone — we just adopt the new client and re-issue our own.
|
|
3347
|
+
*
|
|
3348
|
+
* The caller is responsible for ordering: by the time rebind runs,
|
|
3349
|
+
* the host transport's new NostrClient must already be created and
|
|
3350
|
+
* connected. In Sphere this is guaranteed because we await
|
|
3351
|
+
* {@code transport.setIdentity()} before calling rebind.
|
|
3352
|
+
*
|
|
3353
|
+
* Returns silently in two cases that are not caller errors:
|
|
3354
|
+
* - the Mux owns its own client (not sharing) — nothing to rebind
|
|
3355
|
+
* - the shared client reference hasn't changed (rebind is a no-op)
|
|
3356
|
+
*
|
|
3357
|
+
* Throws otherwise (rather than silently no-op'ing) so a wiring
|
|
3358
|
+
* mistake — for instance, calling rebind before the host's new
|
|
3359
|
+
* client is ready — surfaces immediately instead of leaving the
|
|
3360
|
+
* Mux pinned to a stale client.
|
|
3361
|
+
*/
|
|
3362
|
+
async rebindToSharedClient() {
|
|
3363
|
+
if (!this.usingSharedClient) return;
|
|
3364
|
+
if (!this.sharedNostrClientGetter) return;
|
|
3365
|
+
const newClient = this.sharedNostrClientGetter();
|
|
3366
|
+
if (!newClient) {
|
|
3367
|
+
throw new SphereError(
|
|
3368
|
+
"rebindToSharedClient: shared client getter returned null. The host transport must finish (re)creating its NostrClient before rebind is called.",
|
|
3369
|
+
"TRANSPORT_ERROR"
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
if (this.nostrClient === newClient) return;
|
|
3373
|
+
if (!newClient.isConnected()) {
|
|
3374
|
+
throw new SphereError(
|
|
3375
|
+
"rebindToSharedClient: new shared client is not connected. Await transport.setIdentity() / transport.connect() before rebinding.",
|
|
3376
|
+
"TRANSPORT_ERROR"
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
if (this.nostrClient && this.connectionListener && this.nostrClient !== newClient) {
|
|
3380
|
+
try {
|
|
3381
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3382
|
+
} catch {
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
this.nostrClient = newClient;
|
|
3386
|
+
this.walletSubscriptionId = null;
|
|
3387
|
+
this.chatSubscriptionId = null;
|
|
3388
|
+
this.chatEoseFired = false;
|
|
3389
|
+
this.connectionListener = this.buildConnectionListener();
|
|
3390
|
+
this.nostrClient.addConnectionListener(this.connectionListener);
|
|
3391
|
+
if (this.addresses.size > 0) {
|
|
3392
|
+
await this.updateSubscriptions();
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3227
3395
|
/**
|
|
3228
3396
|
* One-shot fetch of pending events from the relay.
|
|
3229
3397
|
* Creates a temporary subscription, waits for EOSE (or timeout),
|
|
@@ -3352,8 +3520,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3352
3520
|
this.nostrClient.unsubscribe(this.chatSubscriptionId);
|
|
3353
3521
|
this.chatSubscriptionId = null;
|
|
3354
3522
|
}
|
|
3355
|
-
this.lastWalletEventAt = Date.now();
|
|
3356
|
-
this.lastChatEventAt = Date.now();
|
|
3357
3523
|
if (this.addresses.size === 0) return;
|
|
3358
3524
|
const allPubkeys = [];
|
|
3359
3525
|
for (const entry of this.addresses.values()) {
|
|
@@ -3440,25 +3606,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3440
3606
|
}
|
|
3441
3607
|
});
|
|
3442
3608
|
logger.debug("Mux", `updateSubscriptions: walletSub=${this.walletSubscriptionId} chatSub=${this.chatSubscriptionId}`);
|
|
3443
|
-
this.startHealthCheck();
|
|
3444
|
-
}
|
|
3445
|
-
startHealthCheck() {
|
|
3446
|
-
if (this.healthCheckTimer) return;
|
|
3447
|
-
this.healthCheckTimer = setInterval(() => {
|
|
3448
|
-
if (!this.isConnected()) return;
|
|
3449
|
-
const chatElapsed = Date.now() - this.lastChatEventAt;
|
|
3450
|
-
const walletElapsed = Date.now() - this.lastWalletEventAt;
|
|
3451
|
-
const needResubscribe = chatElapsed > 6e4 || walletElapsed > 3e5;
|
|
3452
|
-
if (needResubscribe) {
|
|
3453
|
-
const reason = chatElapsed > 6e4 ? `No chat events for ${Math.round(chatElapsed / 1e3)}s` : `No wallet events for ${Math.round(walletElapsed / 1e3)}s`;
|
|
3454
|
-
logger.warn("Mux", `${reason} \u2014 re-subscribing`);
|
|
3455
|
-
this.lastChatEventAt = Date.now();
|
|
3456
|
-
this.lastWalletEventAt = Date.now();
|
|
3457
|
-
this.updateSubscriptions().catch((err) => {
|
|
3458
|
-
logger.warn("Mux", "Health check re-subscription failed:", err);
|
|
3459
|
-
});
|
|
3460
|
-
}
|
|
3461
|
-
}, 3e4);
|
|
3462
3609
|
}
|
|
3463
3610
|
/**
|
|
3464
3611
|
* Schedule a re-subscription after a relay-initiated subscription closure.
|
|
@@ -3519,12 +3666,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3519
3666
|
}
|
|
3520
3667
|
}
|
|
3521
3668
|
}
|
|
3522
|
-
if (event.kind !== EventKinds2.GIFT_WRAP) {
|
|
3523
|
-
this.lastWalletEventAt = Date.now();
|
|
3524
|
-
}
|
|
3525
|
-
if (event.kind === EventKinds2.GIFT_WRAP) {
|
|
3526
|
-
this.lastChatEventAt = Date.now();
|
|
3527
|
-
}
|
|
3528
3669
|
try {
|
|
3529
3670
|
if (event.kind === EventKinds2.GIFT_WRAP) {
|
|
3530
3671
|
await this.routeGiftWrap(event);
|
|
@@ -12206,6 +12347,132 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
12206
12347
|
};
|
|
12207
12348
|
}
|
|
12208
12349
|
}
|
|
12350
|
+
/**
|
|
12351
|
+
* Mint a fungible token directly to this wallet (genesis mint).
|
|
12352
|
+
*
|
|
12353
|
+
* Useful for test setups that need to seed a wallet with specific token
|
|
12354
|
+
* balances WITHOUT depending on the testnet faucet HTTP service. The
|
|
12355
|
+
* resulting token has the canonical CoinId bytes (passed in `coinIdHex`)
|
|
12356
|
+
* — when those bytes match a registered symbol in the TokenRegistry,
|
|
12357
|
+
* the token shows up under the symbol's name (e.g. "UCT"). There is no
|
|
12358
|
+
* cryptographic restriction on which key may issue a given CoinId; the
|
|
12359
|
+
* aggregator records the mint regardless of issuer identity.
|
|
12360
|
+
*
|
|
12361
|
+
* The flow:
|
|
12362
|
+
* 1. Generate a random TokenId.
|
|
12363
|
+
* 2. Build TokenCoinData with [(coinId, amount)].
|
|
12364
|
+
* 3. Build MintTransactionData with recipient = self (UnmaskedPredicate
|
|
12365
|
+
* from this wallet's signing service).
|
|
12366
|
+
* 4. Submit MintCommitment to the aggregator.
|
|
12367
|
+
* 5. Wait for the inclusion proof.
|
|
12368
|
+
* 6. Construct an SDK Token via Token.mint().
|
|
12369
|
+
* 7. Convert to wallet Token format and call addToken().
|
|
12370
|
+
*
|
|
12371
|
+
* @param coinIdHex - 64-char lowercase hex CoinId. Must match the bytes
|
|
12372
|
+
* used by the registered symbol if you want the wallet to recognize
|
|
12373
|
+
* the token as that symbol (e.g. UCT's coinId from the public registry).
|
|
12374
|
+
* @param amount - Amount in smallest units (multiply by 10^decimals
|
|
12375
|
+
* when converting from human values).
|
|
12376
|
+
* @returns Result with the resulting wallet Token and its on-chain id.
|
|
12377
|
+
*/
|
|
12378
|
+
async mintFungibleToken(coinIdHex, amount) {
|
|
12379
|
+
this.ensureInitialized();
|
|
12380
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
12381
|
+
if (!stClient) {
|
|
12382
|
+
return { success: false, error: "State transition client not available" };
|
|
12383
|
+
}
|
|
12384
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
12385
|
+
if (!trustBase) {
|
|
12386
|
+
return { success: false, error: "Trust base not available" };
|
|
12387
|
+
}
|
|
12388
|
+
try {
|
|
12389
|
+
const signingService = await this.createSigningService();
|
|
12390
|
+
const { TokenId: TokenId5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
12391
|
+
const { TokenCoinData: TokenCoinData3 } = await import("@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData");
|
|
12392
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
12393
|
+
const tokenTypeBytes = fromHex4("f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509");
|
|
12394
|
+
const tokenType = new TokenType3(tokenTypeBytes);
|
|
12395
|
+
const tokenIdBytes = new Uint8Array(32);
|
|
12396
|
+
crypto.getRandomValues(tokenIdBytes);
|
|
12397
|
+
const tokenId = new TokenId5(tokenIdBytes);
|
|
12398
|
+
const coinIdBytes = fromHex4(coinIdHex);
|
|
12399
|
+
const coinId = new CoinId4(coinIdBytes);
|
|
12400
|
+
const coinData = TokenCoinData3.create([[coinId, amount]]);
|
|
12401
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
12402
|
+
tokenType,
|
|
12403
|
+
signingService.algorithm,
|
|
12404
|
+
signingService.publicKey,
|
|
12405
|
+
HashAlgorithm5.SHA256
|
|
12406
|
+
);
|
|
12407
|
+
const ownerAddress = await addressRef.toAddress();
|
|
12408
|
+
const salt = new Uint8Array(32);
|
|
12409
|
+
crypto.getRandomValues(salt);
|
|
12410
|
+
const mintData = await MintTransactionData3.create(
|
|
12411
|
+
tokenId,
|
|
12412
|
+
tokenType,
|
|
12413
|
+
null,
|
|
12414
|
+
// tokenData: no metadata
|
|
12415
|
+
coinData,
|
|
12416
|
+
// fungible coin data
|
|
12417
|
+
ownerAddress,
|
|
12418
|
+
// recipient = self
|
|
12419
|
+
salt,
|
|
12420
|
+
null,
|
|
12421
|
+
// recipientDataHash
|
|
12422
|
+
null
|
|
12423
|
+
// reason: null (genesis, no burn predecessor)
|
|
12424
|
+
);
|
|
12425
|
+
const commitment = await MintCommitment3.create(mintData);
|
|
12426
|
+
const MAX_RETRIES = 3;
|
|
12427
|
+
let lastStatus;
|
|
12428
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
12429
|
+
const response = await stClient.submitMintCommitment(commitment);
|
|
12430
|
+
lastStatus = response.status;
|
|
12431
|
+
if (response.status === "SUCCESS" || response.status === "REQUEST_ID_EXISTS") break;
|
|
12432
|
+
if (attempt === MAX_RETRIES) {
|
|
12433
|
+
return { success: false, error: `Mint submit failed after ${MAX_RETRIES} attempts: ${response.status}` };
|
|
12434
|
+
}
|
|
12435
|
+
await new Promise((r) => setTimeout(r, 1e3 * attempt));
|
|
12436
|
+
}
|
|
12437
|
+
if (lastStatus !== "SUCCESS" && lastStatus !== "REQUEST_ID_EXISTS") {
|
|
12438
|
+
return { success: false, error: `Mint submit failed: ${lastStatus}` };
|
|
12439
|
+
}
|
|
12440
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
12441
|
+
const genesisTransaction = commitment.toTransaction(inclusionProof);
|
|
12442
|
+
const predicate = await UnmaskedPredicate5.create(
|
|
12443
|
+
tokenId,
|
|
12444
|
+
tokenType,
|
|
12445
|
+
signingService,
|
|
12446
|
+
HashAlgorithm5.SHA256,
|
|
12447
|
+
salt
|
|
12448
|
+
);
|
|
12449
|
+
const tokenState = new TokenState5(predicate, null);
|
|
12450
|
+
const sdkToken = await SdkToken2.mint(trustBase, tokenState, genesisTransaction);
|
|
12451
|
+
const tokenIdHex = tokenId.toJSON();
|
|
12452
|
+
const symbol = this.getCoinSymbol(coinIdHex);
|
|
12453
|
+
const name = this.getCoinName(coinIdHex);
|
|
12454
|
+
const decimals = this.getCoinDecimals(coinIdHex);
|
|
12455
|
+
const iconUrl = this.getCoinIconUrl(coinIdHex);
|
|
12456
|
+
const uiToken = {
|
|
12457
|
+
id: tokenIdHex,
|
|
12458
|
+
coinId: coinIdHex,
|
|
12459
|
+
symbol,
|
|
12460
|
+
name,
|
|
12461
|
+
decimals,
|
|
12462
|
+
...iconUrl !== void 0 ? { iconUrl } : {},
|
|
12463
|
+
amount: amount.toString(),
|
|
12464
|
+
status: "confirmed",
|
|
12465
|
+
createdAt: Date.now(),
|
|
12466
|
+
updatedAt: Date.now(),
|
|
12467
|
+
sdkData: JSON.stringify(sdkToken.toJSON())
|
|
12468
|
+
};
|
|
12469
|
+
await this.addToken(uiToken);
|
|
12470
|
+
return { success: true, token: uiToken, tokenId: tokenIdHex };
|
|
12471
|
+
} catch (err) {
|
|
12472
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12473
|
+
return { success: false, error: `Local mint failed: ${msg}` };
|
|
12474
|
+
}
|
|
12475
|
+
}
|
|
12209
12476
|
/**
|
|
12210
12477
|
* Check if a nametag is available for minting
|
|
12211
12478
|
* @param nametag - The nametag to check (e.g., "alice" or "@alice")
|
|
@@ -18416,6 +18683,24 @@ var AccountingModule = class _AccountingModule {
|
|
|
18416
18683
|
dirtyLedgerEntries = /* @__PURE__ */ new Set();
|
|
18417
18684
|
/** Count of unknown (not in invoiceTermsCache) invoice IDs in the ledger. */
|
|
18418
18685
|
unknownLedgerCount = 0;
|
|
18686
|
+
/**
|
|
18687
|
+
* Per-unknown-invoice first-seen timestamp for TTL eviction.
|
|
18688
|
+
*
|
|
18689
|
+
* W1 (steelman round-4): without TTL, an attacker who can deliver 500
|
|
18690
|
+
* inbound transfers with synthesized memo invoiceIds permanently exhausts
|
|
18691
|
+
* the unknown-ledger cap, after which legitimate orphan transfers (out-of-
|
|
18692
|
+
* order delivery for real swaps) are silently dropped at the cap-check.
|
|
18693
|
+
*
|
|
18694
|
+
* Round-5 perf: gated by `unknownLedgerNextSweepMs` to amortize the
|
|
18695
|
+
* sweep cost. The naive every-call sweep is O(N) where N=cap=500;
|
|
18696
|
+
* combined with the per-token cleanup loop inside the sweep it became
|
|
18697
|
+
* O(N×M) on every transfer under flood. Now we sweep at most every
|
|
18698
|
+
* `UNKNOWN_LEDGER_SWEEP_INTERVAL_MS` (60s) UNLESS the cap is currently
|
|
18699
|
+
* full, in which case we sweep on each call (the only path that can
|
|
18700
|
+
* actually drop a legitimate orphan).
|
|
18701
|
+
*/
|
|
18702
|
+
unknownLedgerFirstSeen = /* @__PURE__ */ new Map();
|
|
18703
|
+
unknownLedgerNextSweepMs = 0;
|
|
18419
18704
|
/** W17: Tracks whether tokenScanState has been mutated since last flush. */
|
|
18420
18705
|
tokenScanDirty = false;
|
|
18421
18706
|
/** W2 fix: Serialization guard for _flushDirtyLedgerEntries. */
|
|
@@ -19406,6 +19691,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
19406
19691
|
}
|
|
19407
19692
|
if (this.invoiceLedger.has(tokenId) && !this.invoiceTermsCache.has(tokenId)) {
|
|
19408
19693
|
this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
|
|
19694
|
+
this.unknownLedgerFirstSeen.delete(tokenId);
|
|
19409
19695
|
}
|
|
19410
19696
|
this.invoiceTermsCache.set(tokenId, terms);
|
|
19411
19697
|
this._addToHashIndex(tokenId);
|
|
@@ -19674,6 +19960,29 @@ var AccountingModule = class _AccountingModule {
|
|
|
19674
19960
|
closed: this.closedInvoices.has(invoiceId)
|
|
19675
19961
|
};
|
|
19676
19962
|
}
|
|
19963
|
+
/**
|
|
19964
|
+
* Return the set of token IDs that are currently linked to the given
|
|
19965
|
+
* invoice. Populated by both the on-chain `_processTokenTransactions`
|
|
19966
|
+
* path (tokens with `inv:` references) and the transport-memo orphan
|
|
19967
|
+
* buffering path in `_handleIncomingTransfer`.
|
|
19968
|
+
*
|
|
19969
|
+
* Used by callers that want to scope per-invoice operations (e.g.
|
|
19970
|
+
* SwapModule.verifyPayout's L3 validation) to only the tokens that
|
|
19971
|
+
* cover this invoice — avoiding false negatives when the wallet
|
|
19972
|
+
* contains unrelated tokens of the same currency in unconfirmed or
|
|
19973
|
+
* spent state.
|
|
19974
|
+
*
|
|
19975
|
+
* Returns an empty set if no tokens are currently linked.
|
|
19976
|
+
*/
|
|
19977
|
+
getTokenIdsForInvoice(invoiceId) {
|
|
19978
|
+
const result = /* @__PURE__ */ new Set();
|
|
19979
|
+
for (const [tokenId, invoiceIds] of this.tokenInvoiceMap) {
|
|
19980
|
+
if (invoiceIds.has(invoiceId)) {
|
|
19981
|
+
result.add(tokenId);
|
|
19982
|
+
}
|
|
19983
|
+
}
|
|
19984
|
+
return result;
|
|
19985
|
+
}
|
|
19677
19986
|
/**
|
|
19678
19987
|
* Explicitly close an invoice. Only target parties may close (§8.3).
|
|
19679
19988
|
*
|
|
@@ -19993,6 +20302,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
19993
20302
|
ledger.set(entryKey, forwardRef);
|
|
19994
20303
|
this.dirtyLedgerEntries.add(invoiceId);
|
|
19995
20304
|
this.balanceCache.delete(invoiceId);
|
|
20305
|
+
await this._persistProvisionalAndVerify(invoiceId, "payInvoice");
|
|
19996
20306
|
}
|
|
19997
20307
|
return result;
|
|
19998
20308
|
} finally {
|
|
@@ -20160,6 +20470,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
20160
20470
|
this.dirtyLedgerEntries.add(invoiceId);
|
|
20161
20471
|
}
|
|
20162
20472
|
this.balanceCache.delete(invoiceId);
|
|
20473
|
+
await this._persistProvisionalAndVerify(invoiceId, "returnInvoicePayment");
|
|
20163
20474
|
}
|
|
20164
20475
|
return result;
|
|
20165
20476
|
} finally {
|
|
@@ -21239,9 +21550,30 @@ var AccountingModule = class _AccountingModule {
|
|
|
21239
21550
|
continue;
|
|
21240
21551
|
}
|
|
21241
21552
|
innerMap.set(entryKey, ref);
|
|
21242
|
-
|
|
21553
|
+
const HEX_64 = /^[a-f0-9]{64}$/i;
|
|
21554
|
+
if (entryKey.startsWith("mt:")) {
|
|
21555
|
+
const firstColon = entryKey.indexOf(":");
|
|
21556
|
+
const secondColon = entryKey.indexOf(":", firstColon + 1);
|
|
21557
|
+
if (secondColon > firstColon + 1) {
|
|
21558
|
+
const tokenIdFromKey2 = entryKey.slice(firstColon + 1, secondColon);
|
|
21559
|
+
if (HEX_64.test(tokenIdFromKey2)) {
|
|
21560
|
+
this._addToTokenInvoiceMap(tokenIdFromKey2, invoiceId);
|
|
21561
|
+
}
|
|
21562
|
+
}
|
|
21563
|
+
} else if (entryKey.startsWith("synthetic:")) {
|
|
21564
|
+
const afterPrefix = entryKey.slice("synthetic:".length);
|
|
21565
|
+
const tokenIdEnd = afterPrefix.indexOf(":");
|
|
21566
|
+
if (tokenIdEnd > 0) {
|
|
21567
|
+
const tokenId = afterPrefix.slice(0, tokenIdEnd);
|
|
21568
|
+
if (HEX_64.test(tokenId) && ref.transferId !== tokenId) {
|
|
21569
|
+
this._addToTokenInvoiceMap(tokenId, invoiceId);
|
|
21570
|
+
}
|
|
21571
|
+
}
|
|
21572
|
+
} else if (!ref.transferId.startsWith("provisional:") && ref.transferId.includes(":")) {
|
|
21243
21573
|
const tokenIdFromRef = ref.transferId.slice(0, ref.transferId.indexOf(":"));
|
|
21244
|
-
|
|
21574
|
+
if (HEX_64.test(tokenIdFromRef)) {
|
|
21575
|
+
this._addToTokenInvoiceMap(tokenIdFromRef, invoiceId);
|
|
21576
|
+
}
|
|
21245
21577
|
}
|
|
21246
21578
|
}
|
|
21247
21579
|
} catch (err) {
|
|
@@ -21442,7 +21774,14 @@ var AccountingModule = class _AccountingModule {
|
|
|
21442
21774
|
}
|
|
21443
21775
|
}
|
|
21444
21776
|
for (const [existingKey, existingRef] of ledger) {
|
|
21445
|
-
if (existingKey.startsWith("synthetic:") && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21777
|
+
if ((existingKey.startsWith("synthetic:") || existingKey.startsWith("synthetic-tx:")) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21778
|
+
keysToDelete.push(existingKey);
|
|
21779
|
+
break;
|
|
21780
|
+
}
|
|
21781
|
+
}
|
|
21782
|
+
const mtPrefix = `mt:${tokenId}:`;
|
|
21783
|
+
for (const [existingKey, existingRef] of ledger) {
|
|
21784
|
+
if (existingKey.startsWith(mtPrefix) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21446
21785
|
keysToDelete.push(existingKey);
|
|
21447
21786
|
break;
|
|
21448
21787
|
}
|
|
@@ -21559,6 +21898,49 @@ var AccountingModule = class _AccountingModule {
|
|
|
21559
21898
|
});
|
|
21560
21899
|
}
|
|
21561
21900
|
}
|
|
21901
|
+
/**
|
|
21902
|
+
* Synchronously persist any pending provisional ledger entry for `invoiceId`
|
|
21903
|
+
* before returning to the caller. Used by `payInvoice` and
|
|
21904
|
+
* `returnInvoicePayment` to make the in-memory provisional entry durable
|
|
21905
|
+
* inside the same per-invoice gate that wrote it, closing the
|
|
21906
|
+
* crash-mid-conclude race that produces over-coverage on receivers.
|
|
21907
|
+
*
|
|
21908
|
+
* Implementation:
|
|
21909
|
+
* 1. Schedule a flush via the existing `_flushPromise` chain (so
|
|
21910
|
+
* concurrent `_handleTokenChange` callers waiting on the chain
|
|
21911
|
+
* observe ours as part of the sequence).
|
|
21912
|
+
* 2. Await OUR flush directly — NOT `_drainFlushPromise()`, which would
|
|
21913
|
+
* spin while concurrent token changes keep extending the chain and
|
|
21914
|
+
* hold the per-invoice gate for an unbounded number of additional
|
|
21915
|
+
* flushes. We only need OUR provisional entry durable.
|
|
21916
|
+
* 3. `_flushDirtyLedgerEntries` swallows per-invoice `storage.set`
|
|
21917
|
+
* rejections internally (sets a local `step1Failed` flag), leaving
|
|
21918
|
+
* the dirty entry on the set without re-throwing. So we post-check
|
|
21919
|
+
* `dirtyLedgerEntries.has(invoiceId)` and throw a `STORAGE_ERROR`
|
|
21920
|
+
* `SphereError` if our entry is still dirty — propagating to the
|
|
21921
|
+
* caller so they learn about the durability failure rather than
|
|
21922
|
+
* receiving a silent "success" return that lies on disk.
|
|
21923
|
+
*
|
|
21924
|
+
* @param invoiceId The invoice whose provisional entry must be durable.
|
|
21925
|
+
* @param callContext Used in the error message so the caller is named
|
|
21926
|
+
* ('payInvoice' / 'returnInvoicePayment') without
|
|
21927
|
+
* forcing a stack-trace inspection.
|
|
21928
|
+
*/
|
|
21929
|
+
async _persistProvisionalAndVerify(invoiceId, callContext) {
|
|
21930
|
+
const flushTrigger = (this._flushPromise ?? Promise.resolve()).then(() => this._flushDirtyLedgerEntries());
|
|
21931
|
+
const tracked = flushTrigger.catch(() => {
|
|
21932
|
+
}).finally(() => {
|
|
21933
|
+
if (this._flushPromise === tracked) this._flushPromise = null;
|
|
21934
|
+
});
|
|
21935
|
+
this._flushPromise = tracked;
|
|
21936
|
+
await flushTrigger;
|
|
21937
|
+
if (this.dirtyLedgerEntries.has(invoiceId)) {
|
|
21938
|
+
throw new SphereError(
|
|
21939
|
+
`${callContext}: provisional ledger entry for invoice ${invoiceId} failed to persist \u2014 caller should retry`,
|
|
21940
|
+
"STORAGE_ERROR"
|
|
21941
|
+
);
|
|
21942
|
+
}
|
|
21943
|
+
}
|
|
21562
21944
|
// ===========================================================================
|
|
21563
21945
|
// Internal: Event handlers
|
|
21564
21946
|
// ===========================================================================
|
|
@@ -21619,13 +22001,96 @@ var AccountingModule = class _AccountingModule {
|
|
|
21619
22001
|
}
|
|
21620
22002
|
}
|
|
21621
22003
|
if (!this.invoiceTermsCache.has(invoiceId)) {
|
|
21622
|
-
|
|
21623
|
-
|
|
21624
|
-
invoiceId
|
|
21625
|
-
|
|
21626
|
-
|
|
21627
|
-
|
|
21628
|
-
|
|
22004
|
+
let gracefullyGraduated = false;
|
|
22005
|
+
await this.withInvoiceGate(invoiceId, async () => {
|
|
22006
|
+
if (this.invoiceTermsCache.has(invoiceId)) {
|
|
22007
|
+
gracefullyGraduated = true;
|
|
22008
|
+
return;
|
|
22009
|
+
}
|
|
22010
|
+
const syntheticRef = this._buildSyntheticTransferRef(
|
|
22011
|
+
transfer,
|
|
22012
|
+
invoiceId,
|
|
22013
|
+
paymentDirection,
|
|
22014
|
+
confirmed
|
|
22015
|
+
);
|
|
22016
|
+
deps.emitEvent("invoice:unknown_reference", { invoiceId, transfer: syntheticRef });
|
|
22017
|
+
const MAX_UNKNOWN_INVOICE_IDS = 500;
|
|
22018
|
+
const UNKNOWN_LEDGER_TTL_MS = 30 * 60 * 1e3;
|
|
22019
|
+
const MAX_ORPHAN_ENTRIES_PER_INVOICE = 50;
|
|
22020
|
+
const UNKNOWN_LEDGER_SWEEP_INTERVAL_MS = 6e4;
|
|
22021
|
+
const nowMs = Date.now();
|
|
22022
|
+
const capFull = this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS;
|
|
22023
|
+
const sweepDue = nowMs >= this.unknownLedgerNextSweepMs;
|
|
22024
|
+
if (this.unknownLedgerFirstSeen.size > 0 && (capFull || sweepDue)) {
|
|
22025
|
+
this.unknownLedgerNextSweepMs = nowMs + UNKNOWN_LEDGER_SWEEP_INTERVAL_MS;
|
|
22026
|
+
const expiredIds = [];
|
|
22027
|
+
for (const [unkId, firstSeen] of this.unknownLedgerFirstSeen) {
|
|
22028
|
+
if (nowMs - firstSeen > UNKNOWN_LEDGER_TTL_MS) {
|
|
22029
|
+
expiredIds.push(unkId);
|
|
22030
|
+
}
|
|
22031
|
+
}
|
|
22032
|
+
for (const expiredId of expiredIds) {
|
|
22033
|
+
if (!this.invoiceTermsCache.has(expiredId) && this.invoiceLedger.has(expiredId)) {
|
|
22034
|
+
this.invoiceLedger.delete(expiredId);
|
|
22035
|
+
this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
|
|
22036
|
+
for (const [tokenId, invoiceSet] of this.tokenInvoiceMap) {
|
|
22037
|
+
if (invoiceSet.has(expiredId)) {
|
|
22038
|
+
invoiceSet.delete(expiredId);
|
|
22039
|
+
if (invoiceSet.size === 0) this.tokenInvoiceMap.delete(tokenId);
|
|
22040
|
+
}
|
|
22041
|
+
}
|
|
22042
|
+
}
|
|
22043
|
+
this.unknownLedgerFirstSeen.delete(expiredId);
|
|
22044
|
+
}
|
|
22045
|
+
}
|
|
22046
|
+
if (!this.invoiceLedger.has(invoiceId)) {
|
|
22047
|
+
if (this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS) {
|
|
22048
|
+
return;
|
|
22049
|
+
}
|
|
22050
|
+
this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
|
|
22051
|
+
this.unknownLedgerCount++;
|
|
22052
|
+
this.unknownLedgerFirstSeen.set(invoiceId, nowMs);
|
|
22053
|
+
}
|
|
22054
|
+
const orphanLedger = this.invoiceLedger.get(invoiceId);
|
|
22055
|
+
let mtEntryCount = 0;
|
|
22056
|
+
for (const k of orphanLedger.keys()) {
|
|
22057
|
+
if (k.startsWith("mt:")) mtEntryCount++;
|
|
22058
|
+
}
|
|
22059
|
+
if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
|
|
22060
|
+
return;
|
|
22061
|
+
}
|
|
22062
|
+
for (const token of transfer.tokens) {
|
|
22063
|
+
if (!token.id) continue;
|
|
22064
|
+
let onChainAttributed = false;
|
|
22065
|
+
const tokenKeyPrefix = `${token.id}:`;
|
|
22066
|
+
for (const existingKey of orphanLedger.keys()) {
|
|
22067
|
+
if (existingKey.startsWith(tokenKeyPrefix) && !existingKey.startsWith("mt:")) {
|
|
22068
|
+
onChainAttributed = true;
|
|
22069
|
+
break;
|
|
22070
|
+
}
|
|
22071
|
+
}
|
|
22072
|
+
if (!onChainAttributed) {
|
|
22073
|
+
if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
|
|
22074
|
+
break;
|
|
22075
|
+
}
|
|
22076
|
+
const orphanKey = `mt:${token.id}:${transfer.id}`;
|
|
22077
|
+
if (!orphanLedger.has(orphanKey)) {
|
|
22078
|
+
orphanLedger.set(orphanKey, syntheticRef);
|
|
22079
|
+
mtEntryCount++;
|
|
22080
|
+
}
|
|
22081
|
+
}
|
|
22082
|
+
if (!this.tokenInvoiceMap.has(token.id)) {
|
|
22083
|
+
this.tokenInvoiceMap.set(token.id, /* @__PURE__ */ new Set());
|
|
22084
|
+
}
|
|
22085
|
+
this.tokenInvoiceMap.get(token.id).add(invoiceId);
|
|
22086
|
+
}
|
|
22087
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22088
|
+
this.balanceCache.delete(invoiceId);
|
|
22089
|
+
await this._flushDirtyLedgerEntries();
|
|
22090
|
+
});
|
|
22091
|
+
if (gracefullyGraduated) {
|
|
22092
|
+
await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
|
|
22093
|
+
}
|
|
21629
22094
|
return;
|
|
21630
22095
|
}
|
|
21631
22096
|
await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
|
|
@@ -22061,7 +22526,8 @@ var AccountingModule = class _AccountingModule {
|
|
|
22061
22526
|
}
|
|
22062
22527
|
const existingLedger = this.invoiceLedger.get(invoiceId);
|
|
22063
22528
|
const firstTokenId = transfer.tokens.find((t) => t.id)?.id;
|
|
22064
|
-
const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22529
|
+
const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22530
|
+
let mutated = false;
|
|
22065
22531
|
if (!existingLedger.has(syntheticKey)) {
|
|
22066
22532
|
let hasRealEntry = false;
|
|
22067
22533
|
for (const tok of transfer.tokens) {
|
|
@@ -22076,8 +22542,25 @@ var AccountingModule = class _AccountingModule {
|
|
|
22076
22542
|
}
|
|
22077
22543
|
if (!hasRealEntry) {
|
|
22078
22544
|
existingLedger.set(syntheticKey, { ...syntheticRef });
|
|
22545
|
+
mutated = true;
|
|
22546
|
+
}
|
|
22547
|
+
}
|
|
22548
|
+
for (const tok of transfer.tokens) {
|
|
22549
|
+
if (!tok.id) continue;
|
|
22550
|
+
if (!this.tokenInvoiceMap.has(tok.id)) {
|
|
22551
|
+
this.tokenInvoiceMap.set(tok.id, /* @__PURE__ */ new Set());
|
|
22552
|
+
mutated = true;
|
|
22553
|
+
}
|
|
22554
|
+
const beforeSize = this.tokenInvoiceMap.get(tok.id).size;
|
|
22555
|
+
this.tokenInvoiceMap.get(tok.id).add(invoiceId);
|
|
22556
|
+
if (this.tokenInvoiceMap.get(tok.id).size !== beforeSize) {
|
|
22557
|
+
mutated = true;
|
|
22079
22558
|
}
|
|
22080
22559
|
}
|
|
22560
|
+
if (mutated) {
|
|
22561
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22562
|
+
this.balanceCache.delete(invoiceId);
|
|
22563
|
+
}
|
|
22081
22564
|
deps.emitEvent("invoice:payment", {
|
|
22082
22565
|
invoiceId,
|
|
22083
22566
|
transfer: syntheticRef,
|
|
@@ -22227,7 +22710,8 @@ var AccountingModule = class _AccountingModule {
|
|
|
22227
22710
|
this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
|
|
22228
22711
|
}
|
|
22229
22712
|
const hLedger = this.invoiceLedger.get(invoiceId);
|
|
22230
|
-
const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22713
|
+
const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22714
|
+
let hMutated = false;
|
|
22231
22715
|
if (!hLedger.has(hKey)) {
|
|
22232
22716
|
let hasRealEntry = false;
|
|
22233
22717
|
if (entry.tokenId) {
|
|
@@ -22240,8 +22724,24 @@ var AccountingModule = class _AccountingModule {
|
|
|
22240
22724
|
}
|
|
22241
22725
|
if (!hasRealEntry) {
|
|
22242
22726
|
hLedger.set(hKey, { ...syntheticRef });
|
|
22727
|
+
hMutated = true;
|
|
22728
|
+
}
|
|
22729
|
+
}
|
|
22730
|
+
if (entry.tokenId) {
|
|
22731
|
+
if (!this.tokenInvoiceMap.has(entry.tokenId)) {
|
|
22732
|
+
this.tokenInvoiceMap.set(entry.tokenId, /* @__PURE__ */ new Set());
|
|
22733
|
+
hMutated = true;
|
|
22734
|
+
}
|
|
22735
|
+
const beforeSize = this.tokenInvoiceMap.get(entry.tokenId).size;
|
|
22736
|
+
this.tokenInvoiceMap.get(entry.tokenId).add(invoiceId);
|
|
22737
|
+
if (this.tokenInvoiceMap.get(entry.tokenId).size !== beforeSize) {
|
|
22738
|
+
hMutated = true;
|
|
22243
22739
|
}
|
|
22244
22740
|
}
|
|
22741
|
+
if (hMutated) {
|
|
22742
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22743
|
+
this.balanceCache.delete(invoiceId);
|
|
22744
|
+
}
|
|
22245
22745
|
deps.emitEvent("invoice:payment", {
|
|
22246
22746
|
invoiceId,
|
|
22247
22747
|
transfer: syntheticRef,
|
|
@@ -24787,17 +25287,63 @@ var SwapModule = class {
|
|
|
24787
25287
|
for (const addr of allAddresses) {
|
|
24788
25288
|
myDirectAddresses.add(addr.directAddress);
|
|
24789
25289
|
}
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
25290
|
+
const matchesPartyA = myDirectAddresses.has(swap.manifest.party_a_address);
|
|
25291
|
+
const matchesPartyB = myDirectAddresses.has(swap.manifest.party_b_address);
|
|
25292
|
+
if (matchesPartyA && matchesPartyB) {
|
|
25293
|
+
throw new SphereError(
|
|
25294
|
+
"Ambiguous party identity: local wallet matches both party_a_address and party_b_address",
|
|
25295
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25296
|
+
);
|
|
25297
|
+
}
|
|
25298
|
+
let myExpectedCurrency;
|
|
25299
|
+
if (matchesPartyA) {
|
|
25300
|
+
myExpectedCurrency = swap.manifest.party_a_currency_to_change;
|
|
25301
|
+
} else if (matchesPartyB) {
|
|
25302
|
+
myExpectedCurrency = swap.manifest.party_b_currency_to_change;
|
|
24795
25303
|
} else {
|
|
24796
25304
|
throw new SphereError(
|
|
24797
25305
|
"Local wallet address does not match either party in the swap manifest",
|
|
24798
25306
|
"SWAP_DEPOSIT_FAILED"
|
|
24799
25307
|
);
|
|
24800
25308
|
}
|
|
25309
|
+
if (!myExpectedCurrency || myExpectedCurrency === "") {
|
|
25310
|
+
throw new SphereError(
|
|
25311
|
+
"Manifest currency_to_change is empty for this party",
|
|
25312
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25313
|
+
);
|
|
25314
|
+
}
|
|
25315
|
+
const invoiceRefForAssetLookup = deps.accounting.getInvoice(swap.depositInvoiceId);
|
|
25316
|
+
if (!invoiceRefForAssetLookup) {
|
|
25317
|
+
throw new SphereError(
|
|
25318
|
+
"Deposit invoice not yet imported into accounting module",
|
|
25319
|
+
"SWAP_WRONG_STATE"
|
|
25320
|
+
);
|
|
25321
|
+
}
|
|
25322
|
+
const depositTarget = invoiceRefForAssetLookup.terms.targets[0];
|
|
25323
|
+
if (!depositTarget) {
|
|
25324
|
+
throw new SphereError(
|
|
25325
|
+
"Deposit invoice has no targets",
|
|
25326
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25327
|
+
);
|
|
25328
|
+
}
|
|
25329
|
+
const assetIndex = depositTarget.assets.findIndex(
|
|
25330
|
+
(a) => a.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)
|
|
25331
|
+
);
|
|
25332
|
+
if (assetIndex < 0) {
|
|
25333
|
+
throw new SphereError(
|
|
25334
|
+
`No asset matching expected currency ${myExpectedCurrency} found in deposit invoice`,
|
|
25335
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25336
|
+
);
|
|
25337
|
+
}
|
|
25338
|
+
for (let i = assetIndex + 1; i < depositTarget.assets.length; i += 1) {
|
|
25339
|
+
const a = depositTarget.assets[i];
|
|
25340
|
+
if (a?.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)) {
|
|
25341
|
+
throw new SphereError(
|
|
25342
|
+
`Ambiguous asset match in deposit invoice: slots ${assetIndex} and ${i} both match currency ${myExpectedCurrency}`,
|
|
25343
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25344
|
+
);
|
|
25345
|
+
}
|
|
25346
|
+
}
|
|
24801
25347
|
return this.withSwapGate(swapId, async () => {
|
|
24802
25348
|
if (swap.progress !== "announced") {
|
|
24803
25349
|
throw new SphereError(
|
|
@@ -24903,7 +25449,6 @@ var SwapModule = class {
|
|
|
24903
25449
|
swap.updatedAt = Date.now();
|
|
24904
25450
|
this.clearLocalTimer(swap.swapId);
|
|
24905
25451
|
this.terminalSwapIds.add(swap.swapId);
|
|
24906
|
-
const entryIdx = this._storedTerminalEntries.length;
|
|
24907
25452
|
this._storedTerminalEntries.push({
|
|
24908
25453
|
swapId: swap.swapId,
|
|
24909
25454
|
progress: "failed",
|
|
@@ -24918,7 +25463,13 @@ var SwapModule = class {
|
|
|
24918
25463
|
swap.error = prevError;
|
|
24919
25464
|
swap.updatedAt = prevUpdatedAt;
|
|
24920
25465
|
this.terminalSwapIds.delete(swap.swapId);
|
|
24921
|
-
this._storedTerminalEntries.
|
|
25466
|
+
for (let i = this._storedTerminalEntries.length - 1; i >= 0; i--) {
|
|
25467
|
+
const entry = this._storedTerminalEntries[i];
|
|
25468
|
+
if (entry.swapId === swap.swapId && entry.progress === "failed") {
|
|
25469
|
+
this._storedTerminalEntries.splice(i, 1);
|
|
25470
|
+
break;
|
|
25471
|
+
}
|
|
25472
|
+
}
|
|
24922
25473
|
logger.warn(LOG_TAG3, `failPayout: persistSwap failed for ${swapId}; fraud detection will retry on next load:`, persistErr);
|
|
24923
25474
|
throw persistErr;
|
|
24924
25475
|
}
|
|
@@ -24962,9 +25513,25 @@ var SwapModule = class {
|
|
|
24962
25513
|
if (!targetStatus.coinAssets[0].isCovered) {
|
|
24963
25514
|
return returnFalse();
|
|
24964
25515
|
}
|
|
24965
|
-
|
|
25516
|
+
let netCoveredAmount;
|
|
25517
|
+
let expectedAmountBigInt;
|
|
25518
|
+
try {
|
|
25519
|
+
netCoveredAmount = BigInt(targetStatus.coinAssets[0].netCoveredAmount);
|
|
25520
|
+
expectedAmountBigInt = BigInt(expectedAmount);
|
|
25521
|
+
} catch (parseErr) {
|
|
25522
|
+
return failPayout(
|
|
25523
|
+
`MALFORMED_AMOUNT: failed to parse coverage amounts (netCoveredAmount=${targetStatus.coinAssets[0].netCoveredAmount}, expectedAmount=${expectedAmount}): ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`
|
|
25524
|
+
);
|
|
25525
|
+
}
|
|
25526
|
+
if (netCoveredAmount < expectedAmountBigInt) {
|
|
24966
25527
|
return returnFalse();
|
|
24967
25528
|
}
|
|
25529
|
+
if (netCoveredAmount > expectedAmountBigInt) {
|
|
25530
|
+
const surplus = netCoveredAmount - expectedAmountBigInt;
|
|
25531
|
+
return failPayout(
|
|
25532
|
+
`OVER_COVERAGE: net=${netCoveredAmount.toString()}, expected=${expectedAmount}, surplus=${surplus.toString()} \u2014 surplus refund expected via auto-return; settlement halted`
|
|
25533
|
+
);
|
|
25534
|
+
}
|
|
24968
25535
|
const escrowAddr = swap.deal.escrowAddress ?? this.config.defaultEscrowAddress;
|
|
24969
25536
|
if (escrowAddr) {
|
|
24970
25537
|
const escrowPeer = await deps.resolve(escrowAddr);
|
|
@@ -24974,8 +25541,28 @@ var SwapModule = class {
|
|
|
24974
25541
|
}
|
|
24975
25542
|
const validationResult = await deps.payments.validate();
|
|
24976
25543
|
if (validationResult.invalid.length > 0) {
|
|
24977
|
-
|
|
24978
|
-
|
|
25544
|
+
const payoutTokenIds = deps.accounting.getTokenIdsForInvoice?.(swap.payoutInvoiceId) ?? /* @__PURE__ */ new Set();
|
|
25545
|
+
if (payoutTokenIds.size === 0) {
|
|
25546
|
+
logger.warn(
|
|
25547
|
+
LOG_TAG3,
|
|
25548
|
+
`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`
|
|
25549
|
+
);
|
|
25550
|
+
return returnFalse();
|
|
25551
|
+
}
|
|
25552
|
+
const relevantInvalid = validationResult.invalid.filter(
|
|
25553
|
+
(t) => payoutTokenIds.has(t.id)
|
|
25554
|
+
);
|
|
25555
|
+
if (relevantInvalid.length > 0) {
|
|
25556
|
+
logger.warn(
|
|
25557
|
+
LOG_TAG3,
|
|
25558
|
+
`verifyPayout for ${swapId.slice(0, 12)}: L3 validation found ${relevantInvalid.length} invalid token(s) covering this payout invoice \u2014 retry after wallet sync`
|
|
25559
|
+
);
|
|
25560
|
+
return returnFalse();
|
|
25561
|
+
}
|
|
25562
|
+
logger.debug(
|
|
25563
|
+
LOG_TAG3,
|
|
25564
|
+
`verifyPayout for ${swapId.slice(0, 12)}: ${validationResult.invalid.length} unrelated invalid token(s) ignored (not linked to this payout invoice)`
|
|
25565
|
+
);
|
|
24979
25566
|
}
|
|
24980
25567
|
if (swap.progress === "completed") {
|
|
24981
25568
|
swap.payoutVerified = true;
|
|
@@ -25154,8 +25741,22 @@ var SwapModule = class {
|
|
|
25154
25741
|
* @param dm - The incoming direct message.
|
|
25155
25742
|
*/
|
|
25156
25743
|
handleIncomingDM(dm) {
|
|
25744
|
+
if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
|
|
25745
|
+
logger.warn(
|
|
25746
|
+
LOG_TAG3,
|
|
25747
|
+
`diag_swap_dm_arrived sender=${dm.senderPubkey.slice(0, 16)} length=${dm.content.length}`
|
|
25748
|
+
);
|
|
25749
|
+
}
|
|
25157
25750
|
const parsed = parseSwapDM(dm.content);
|
|
25158
|
-
if (!parsed)
|
|
25751
|
+
if (!parsed) {
|
|
25752
|
+
if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
|
|
25753
|
+
logger.warn(
|
|
25754
|
+
LOG_TAG3,
|
|
25755
|
+
`diag_swap_dm_parse_rejected sender=${dm.senderPubkey.slice(0, 16)} prefix=${dm.content.slice(0, 80)}`
|
|
25756
|
+
);
|
|
25757
|
+
}
|
|
25758
|
+
return;
|
|
25759
|
+
}
|
|
25159
25760
|
void (async () => {
|
|
25160
25761
|
try {
|
|
25161
25762
|
switch (parsed.kind) {
|
|
@@ -25521,16 +26122,43 @@ var SwapModule = class {
|
|
|
25521
26122
|
// invoice_delivery (§12.4.2 + §12.4.3)
|
|
25522
26123
|
// ---------------------------------------------------------------
|
|
25523
26124
|
case "invoice_delivery": {
|
|
25524
|
-
|
|
26125
|
+
logger.warn(
|
|
26126
|
+
LOG_TAG3,
|
|
26127
|
+
`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)}`
|
|
26128
|
+
);
|
|
26129
|
+
if (!swapId) {
|
|
26130
|
+
logger.warn(LOG_TAG3, "diag_invoice_delivery_dropped reason=no_swap_id");
|
|
26131
|
+
return;
|
|
26132
|
+
}
|
|
25525
26133
|
const swap = this.swaps.get(swapId);
|
|
25526
|
-
if (!swap)
|
|
25527
|
-
|
|
26134
|
+
if (!swap) {
|
|
26135
|
+
logger.warn(
|
|
26136
|
+
LOG_TAG3,
|
|
26137
|
+
`diag_invoice_delivery_dropped reason=swap_not_in_map swap_id=${swapId.slice(0, 16)} known_swap_ids_count=${this.swaps.size}`
|
|
26138
|
+
);
|
|
26139
|
+
return;
|
|
26140
|
+
}
|
|
26141
|
+
if (!this.isFromExpectedEscrow(dm.senderPubkey, swap)) {
|
|
26142
|
+
logger.warn(
|
|
26143
|
+
LOG_TAG3,
|
|
26144
|
+
`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)}`
|
|
26145
|
+
);
|
|
26146
|
+
return;
|
|
26147
|
+
}
|
|
25528
26148
|
const deps = this.deps;
|
|
25529
26149
|
if (msg.invoice_type === "deposit") {
|
|
26150
|
+
logger.warn(
|
|
26151
|
+
LOG_TAG3,
|
|
26152
|
+
`diag_invoice_delivery_proceeding_to_import swap_id=${swapId.slice(0, 16)} progress=${swap.progress} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)}`
|
|
26153
|
+
);
|
|
25530
26154
|
await this.withSwapGate(swapId, async () => {
|
|
25531
26155
|
if (isTerminalProgress(swap.progress)) return;
|
|
25532
26156
|
try {
|
|
25533
26157
|
await deps.accounting.importInvoice(msg.invoice_token);
|
|
26158
|
+
logger.warn(
|
|
26159
|
+
LOG_TAG3,
|
|
26160
|
+
`diag_invoice_imported swap_id=${swapId.slice(0, 16)} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)} type=deposit`
|
|
26161
|
+
);
|
|
25534
26162
|
} catch (err) {
|
|
25535
26163
|
if (err instanceof SphereError && err.code === "INVOICE_ALREADY_EXISTS") {
|
|
25536
26164
|
logger.debug(LOG_TAG3, `Deposit invoice for swap ${swapId} already imported \u2014 relay re-delivery, continuing`);
|
|
@@ -26169,6 +26797,65 @@ init_constants();
|
|
|
26169
26797
|
init_errors();
|
|
26170
26798
|
init_logger();
|
|
26171
26799
|
import CryptoJS6 from "crypto-js";
|
|
26800
|
+
var DEFAULT_ITERATIONS = 1e5;
|
|
26801
|
+
var KEY_SIZE = 256;
|
|
26802
|
+
var SALT_SIZE = 16;
|
|
26803
|
+
var IV_SIZE = 16;
|
|
26804
|
+
function deriveKey(password, salt, iterations) {
|
|
26805
|
+
return CryptoJS6.PBKDF2(password, salt, {
|
|
26806
|
+
keySize: KEY_SIZE / 32,
|
|
26807
|
+
// WordArray uses 32-bit words
|
|
26808
|
+
iterations,
|
|
26809
|
+
hasher: CryptoJS6.algo.SHA256
|
|
26810
|
+
});
|
|
26811
|
+
}
|
|
26812
|
+
function encrypt2(plaintext, password, options = {}) {
|
|
26813
|
+
const iterations = options.iterations ?? DEFAULT_ITERATIONS;
|
|
26814
|
+
const data = typeof plaintext === "string" ? plaintext : JSON.stringify(plaintext);
|
|
26815
|
+
const salt = CryptoJS6.lib.WordArray.random(SALT_SIZE);
|
|
26816
|
+
const iv = CryptoJS6.lib.WordArray.random(IV_SIZE);
|
|
26817
|
+
const key = deriveKey(password, salt, iterations);
|
|
26818
|
+
const encrypted = CryptoJS6.AES.encrypt(data, key, {
|
|
26819
|
+
iv,
|
|
26820
|
+
mode: CryptoJS6.mode.CBC,
|
|
26821
|
+
padding: CryptoJS6.pad.Pkcs7
|
|
26822
|
+
});
|
|
26823
|
+
return {
|
|
26824
|
+
ciphertext: encrypted.ciphertext.toString(CryptoJS6.enc.Base64),
|
|
26825
|
+
iv: iv.toString(CryptoJS6.enc.Hex),
|
|
26826
|
+
salt: salt.toString(CryptoJS6.enc.Hex),
|
|
26827
|
+
algorithm: "aes-256-cbc",
|
|
26828
|
+
kdf: "pbkdf2",
|
|
26829
|
+
iterations
|
|
26830
|
+
};
|
|
26831
|
+
}
|
|
26832
|
+
function decrypt2(encryptedData, password) {
|
|
26833
|
+
const salt = CryptoJS6.enc.Hex.parse(encryptedData.salt);
|
|
26834
|
+
const iv = CryptoJS6.enc.Hex.parse(encryptedData.iv);
|
|
26835
|
+
const key = deriveKey(password, salt, encryptedData.iterations);
|
|
26836
|
+
const ciphertext = CryptoJS6.enc.Base64.parse(encryptedData.ciphertext);
|
|
26837
|
+
const cipherParams = CryptoJS6.lib.CipherParams.create({
|
|
26838
|
+
ciphertext
|
|
26839
|
+
});
|
|
26840
|
+
const decrypted = CryptoJS6.AES.decrypt(cipherParams, key, {
|
|
26841
|
+
iv,
|
|
26842
|
+
mode: CryptoJS6.mode.CBC,
|
|
26843
|
+
padding: CryptoJS6.pad.Pkcs7
|
|
26844
|
+
});
|
|
26845
|
+
const result = decrypted.toString(CryptoJS6.enc.Utf8);
|
|
26846
|
+
if (!result) {
|
|
26847
|
+
throw new SphereError("Decryption failed: invalid password or corrupted data", "DECRYPTION_ERROR");
|
|
26848
|
+
}
|
|
26849
|
+
return result;
|
|
26850
|
+
}
|
|
26851
|
+
function decryptJson(encryptedData, password) {
|
|
26852
|
+
const decrypted = decrypt2(encryptedData, password);
|
|
26853
|
+
try {
|
|
26854
|
+
return JSON.parse(decrypted);
|
|
26855
|
+
} catch {
|
|
26856
|
+
throw new SphereError("Decryption failed: invalid JSON data", "DECRYPTION_ERROR");
|
|
26857
|
+
}
|
|
26858
|
+
}
|
|
26172
26859
|
function encryptSimple(plaintext, password) {
|
|
26173
26860
|
return CryptoJS6.AES.encrypt(plaintext, password).toString();
|
|
26174
26861
|
}
|
|
@@ -26195,6 +26882,12 @@ function decryptWithSalt(ciphertext, password, salt) {
|
|
|
26195
26882
|
return null;
|
|
26196
26883
|
}
|
|
26197
26884
|
}
|
|
26885
|
+
function encryptMnemonic(mnemonic, password) {
|
|
26886
|
+
return encryptSimple(mnemonic, password);
|
|
26887
|
+
}
|
|
26888
|
+
function decryptMnemonic(encryptedMnemonic, password) {
|
|
26889
|
+
return decryptSimple(encryptedMnemonic, password);
|
|
26890
|
+
}
|
|
26198
26891
|
|
|
26199
26892
|
// core/scan.ts
|
|
26200
26893
|
init_logger();
|
|
@@ -26938,27 +27631,42 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
26938
27631
|
|
|
26939
27632
|
// core/Sphere.ts
|
|
26940
27633
|
import { SigningService as SigningService2 } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
|
|
27634
|
+
import { normalizeNametag as normalizeNametag2, isPhoneNumber } from "@unicitylabs/nostr-js-sdk";
|
|
27635
|
+
|
|
27636
|
+
// core/address-derivation.ts
|
|
26941
27637
|
import { TokenType as TokenType5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
26942
27638
|
import { HashAlgorithm as HashAlgorithm7 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
26943
27639
|
import { UnmaskedPredicateReference as UnmaskedPredicateReference3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
|
|
26944
|
-
|
|
27640
|
+
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
27641
|
+
var COMPRESSED_PUBKEY_RE = /^(02|03)[0-9a-fA-F]{64}$/;
|
|
27642
|
+
async function computeDirectAddressFromChainPubkey(chainPubkey) {
|
|
27643
|
+
if (typeof chainPubkey !== "string" || !COMPRESSED_PUBKEY_RE.test(chainPubkey)) {
|
|
27644
|
+
throw new Error(
|
|
27645
|
+
`computeDirectAddressFromChainPubkey: chainPubkey must be 66-char hex with 02/03 prefix, got "${String(chainPubkey).slice(0, 12)}..."`
|
|
27646
|
+
);
|
|
27647
|
+
}
|
|
27648
|
+
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
27649
|
+
const tokenType = new TokenType5(tokenTypeBytes);
|
|
27650
|
+
const publicKeyBytes = Buffer.from(chainPubkey, "hex");
|
|
27651
|
+
const predicateRef = await UnmaskedPredicateReference3.create(
|
|
27652
|
+
tokenType,
|
|
27653
|
+
"secp256k1",
|
|
27654
|
+
publicKeyBytes,
|
|
27655
|
+
HashAlgorithm7.SHA256
|
|
27656
|
+
);
|
|
27657
|
+
return (await predicateRef.toAddress()).toString();
|
|
27658
|
+
}
|
|
27659
|
+
|
|
27660
|
+
// core/Sphere.ts
|
|
26945
27661
|
function isValidNametag2(nametag) {
|
|
26946
27662
|
if (isPhoneNumber(nametag)) return true;
|
|
26947
27663
|
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
26948
27664
|
}
|
|
26949
|
-
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
26950
27665
|
async function deriveL3PredicateAddress(privateKey) {
|
|
26951
27666
|
const secret = Buffer.from(privateKey, "hex");
|
|
26952
27667
|
const signingService = await SigningService2.createFromSecret(secret);
|
|
26953
|
-
const
|
|
26954
|
-
|
|
26955
|
-
const predicateRef = UnmaskedPredicateReference3.create(
|
|
26956
|
-
tokenType,
|
|
26957
|
-
signingService.algorithm,
|
|
26958
|
-
signingService.publicKey,
|
|
26959
|
-
HashAlgorithm7.SHA256
|
|
26960
|
-
);
|
|
26961
|
-
return (await (await predicateRef).toAddress()).toString();
|
|
27668
|
+
const pubkeyHex = Buffer.from(signingService.publicKey).toString("hex");
|
|
27669
|
+
return computeDirectAddressFromChainPubkey(pubkeyHex);
|
|
26962
27670
|
}
|
|
26963
27671
|
var Sphere = class _Sphere {
|
|
26964
27672
|
// Singleton
|
|
@@ -28412,6 +29120,9 @@ var Sphere = class _Sphere {
|
|
|
28412
29120
|
this._transport.setFallbackSince(fallbackTs);
|
|
28413
29121
|
}
|
|
28414
29122
|
await this._transport.setIdentity(this._identity);
|
|
29123
|
+
if (this._transportMux && typeof this._transportMux.rebindToSharedClient === "function") {
|
|
29124
|
+
await this._transportMux.rebindToSharedClient();
|
|
29125
|
+
}
|
|
28415
29126
|
this.emitEvent("identity:changed", {
|
|
28416
29127
|
l1Address: this._identity.l1Address,
|
|
28417
29128
|
directAddress: this._identity.directAddress,
|
|
@@ -28622,7 +29333,12 @@ var Sphere = class _Sphere {
|
|
|
28622
29333
|
this._transportMux = new MultiAddressTransportMux({
|
|
28623
29334
|
relays: nostrTransport.getConfiguredRelays(),
|
|
28624
29335
|
createWebSocket: nostrTransport.getWebSocketFactory(),
|
|
28625
|
-
storage: nostrTransport.getStorageAdapter() ?? void 0
|
|
29336
|
+
storage: nostrTransport.getStorageAdapter() ?? void 0,
|
|
29337
|
+
// #123: share the original transport's NostrClient instead of
|
|
29338
|
+
// opening a second WebSocket per relay. Pass a getter so the
|
|
29339
|
+
// Mux resolves it at connect-time (after the transport finishes
|
|
29340
|
+
// its own connect()).
|
|
29341
|
+
sharedNostrClient: typeof nostrTransport.getNostrClient === "function" ? () => nostrTransport.getNostrClient() : void 0
|
|
28626
29342
|
});
|
|
28627
29343
|
await this._transportMux.connect();
|
|
28628
29344
|
if (typeof nostrTransport.suppressSubscriptions === "function") {
|
|
@@ -30969,7 +31685,37 @@ function createPriceProvider(config) {
|
|
|
30969
31685
|
throw new SphereError(`Unsupported price platform: ${String(config.platform)}`, "INVALID_CONFIG");
|
|
30970
31686
|
}
|
|
30971
31687
|
}
|
|
31688
|
+
|
|
31689
|
+
// core/auth.ts
|
|
31690
|
+
var AuthVerificationError = class extends Error {
|
|
31691
|
+
code;
|
|
31692
|
+
constructor(message, code) {
|
|
31693
|
+
super(message);
|
|
31694
|
+
this.name = "AuthVerificationError";
|
|
31695
|
+
this.code = code;
|
|
31696
|
+
}
|
|
31697
|
+
};
|
|
31698
|
+
async function verifySphereAuth(input) {
|
|
31699
|
+
const { challenge, signature, chainPubkey } = input;
|
|
31700
|
+
let directAddress;
|
|
31701
|
+
try {
|
|
31702
|
+
directAddress = await computeDirectAddressFromChainPubkey(chainPubkey);
|
|
31703
|
+
} catch (err) {
|
|
31704
|
+
throw new AuthVerificationError(
|
|
31705
|
+
`chainPubkey is malformed: ${err.message}`,
|
|
31706
|
+
"PUBKEY_MALFORMED"
|
|
31707
|
+
);
|
|
31708
|
+
}
|
|
31709
|
+
if (!verifySignedMessage(challenge, signature, chainPubkey)) {
|
|
31710
|
+
throw new AuthVerificationError(
|
|
31711
|
+
"Signature does not verify against chainPubkey",
|
|
31712
|
+
"SIGNATURE_INVALID"
|
|
31713
|
+
);
|
|
31714
|
+
}
|
|
31715
|
+
return { chainPubkey, directAddress };
|
|
31716
|
+
}
|
|
30972
31717
|
export {
|
|
31718
|
+
AuthVerificationError,
|
|
30973
31719
|
COIN_TYPES,
|
|
30974
31720
|
CoinGeckoPriceProvider,
|
|
30975
31721
|
CommunicationsModule,
|
|
@@ -31019,6 +31765,8 @@ export {
|
|
|
31019
31765
|
buildTxfStorageData,
|
|
31020
31766
|
bytesToHex3 as bytesToHex,
|
|
31021
31767
|
checkNetworkHealth,
|
|
31768
|
+
coinIdsMatch,
|
|
31769
|
+
computeDirectAddressFromChainPubkey,
|
|
31022
31770
|
computeSwapId,
|
|
31023
31771
|
countCommittedTransactions,
|
|
31024
31772
|
createAddress,
|
|
@@ -31037,22 +31785,34 @@ export {
|
|
|
31037
31785
|
createSwapModule,
|
|
31038
31786
|
createTokenValidator,
|
|
31039
31787
|
decodeBech32,
|
|
31788
|
+
decrypt2 as decrypt,
|
|
31040
31789
|
decryptCMasterKey,
|
|
31790
|
+
decryptJson,
|
|
31791
|
+
decryptMnemonic,
|
|
31041
31792
|
decryptNametag2 as decryptNametag,
|
|
31042
31793
|
decryptPrivateKey,
|
|
31794
|
+
decryptSimple,
|
|
31043
31795
|
decryptTextFormatKey,
|
|
31796
|
+
decryptWallet,
|
|
31797
|
+
decryptWithSalt,
|
|
31044
31798
|
deriveAddressInfo,
|
|
31045
31799
|
deriveChildKey,
|
|
31046
31800
|
deriveKeyAtPath,
|
|
31047
31801
|
doubleSha256,
|
|
31048
31802
|
encodeBech32,
|
|
31803
|
+
encrypt2 as encrypt,
|
|
31804
|
+
encryptMnemonic,
|
|
31049
31805
|
encryptNametag,
|
|
31806
|
+
encryptSimple,
|
|
31807
|
+
encryptWallet,
|
|
31050
31808
|
extractFromText,
|
|
31051
31809
|
findPattern,
|
|
31052
31810
|
forkedKeyFromTokenIdAndState,
|
|
31053
31811
|
formatAmount,
|
|
31812
|
+
generateAddressFromMasterKey,
|
|
31054
31813
|
generateMasterKey,
|
|
31055
31814
|
generateMnemonic2 as generateMnemonic,
|
|
31815
|
+
generatePrivateKey,
|
|
31056
31816
|
getAddressHrp,
|
|
31057
31817
|
getAddressId,
|
|
31058
31818
|
getAddressStorageKey,
|
|
@@ -31075,6 +31835,7 @@ export {
|
|
|
31075
31835
|
hashNametag,
|
|
31076
31836
|
hashSignMessage,
|
|
31077
31837
|
hexToBytes2 as hexToBytes,
|
|
31838
|
+
hexToWIF,
|
|
31078
31839
|
identityFromMnemonicSync,
|
|
31079
31840
|
initSphere,
|
|
31080
31841
|
isArchivedKey,
|
|
@@ -31104,6 +31865,7 @@ export {
|
|
|
31104
31865
|
logger,
|
|
31105
31866
|
mnemonicToSeedSync2 as mnemonicToSeedSync,
|
|
31106
31867
|
normalizeAddress,
|
|
31868
|
+
normalizeCoinId,
|
|
31107
31869
|
normalizeNametag3 as normalizeNametag,
|
|
31108
31870
|
normalizeSdkTokenToStorage,
|
|
31109
31871
|
objectToTxf,
|
|
@@ -31134,6 +31896,7 @@ export {
|
|
|
31134
31896
|
verifyManifestIntegrity,
|
|
31135
31897
|
verifyNametagBinding,
|
|
31136
31898
|
verifySignedMessage,
|
|
31899
|
+
verifySphereAuth,
|
|
31137
31900
|
verifySwapSignature
|
|
31138
31901
|
};
|
|
31139
31902
|
/*! Bundled license information:
|