@unicitylabs/sphere-sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +1112 -0
- package/dist/core/index.cjs +7042 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +2548 -0
- package/dist/core/index.d.ts +2548 -0
- package/dist/core/index.js +6946 -0
- package/dist/core/index.js.map +1 -0
- package/dist/impl/browser/index.cjs +7853 -0
- package/dist/impl/browser/index.cjs.map +1 -0
- package/dist/impl/browser/index.d.cts +3016 -0
- package/dist/impl/browser/index.d.ts +3016 -0
- package/dist/impl/browser/index.js +7841 -0
- package/dist/impl/browser/index.js.map +1 -0
- package/dist/impl/nodejs/index.cjs +1767 -0
- package/dist/impl/nodejs/index.cjs.map +1 -0
- package/dist/impl/nodejs/index.d.cts +1091 -0
- package/dist/impl/nodejs/index.d.ts +1091 -0
- package/dist/impl/nodejs/index.js +1722 -0
- package/dist/impl/nodejs/index.js.map +1 -0
- package/dist/index.cjs +7647 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10533 -0
- package/dist/index.d.ts +10533 -0
- package/dist/index.js +7523 -0
- package/dist/index.js.map +1 -0
- package/dist/l1/index.cjs +1545 -0
- package/dist/l1/index.cjs.map +1 -0
- package/dist/l1/index.d.cts +705 -0
- package/dist/l1/index.d.ts +705 -0
- package/dist/l1/index.js +1455 -0
- package/dist/l1/index.js.map +1 -0
- package/package.json +140 -0
|
@@ -0,0 +1,1767 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// impl/nodejs/index.ts
|
|
31
|
+
var nodejs_exports = {};
|
|
32
|
+
__export(nodejs_exports, {
|
|
33
|
+
FileStorageProvider: () => FileStorageProvider,
|
|
34
|
+
FileTokenStorageProvider: () => FileTokenStorageProvider,
|
|
35
|
+
NodeTrustBaseLoader: () => NodeTrustBaseLoader,
|
|
36
|
+
NostrTransportProvider: () => NostrTransportProvider,
|
|
37
|
+
UnicityAggregatorProvider: () => UnicityAggregatorProvider,
|
|
38
|
+
UnicityOracleProvider: () => UnicityOracleProvider,
|
|
39
|
+
createFileStorageProvider: () => createFileStorageProvider,
|
|
40
|
+
createFileTokenStorageProvider: () => createFileTokenStorageProvider,
|
|
41
|
+
createNodeProviders: () => createNodeProviders,
|
|
42
|
+
createNodeTrustBaseLoader: () => createNodeTrustBaseLoader,
|
|
43
|
+
createNodeWebSocketFactory: () => createNodeWebSocketFactory,
|
|
44
|
+
createNostrTransportProvider: () => createNostrTransportProvider,
|
|
45
|
+
createUnicityAggregatorProvider: () => createUnicityAggregatorProvider,
|
|
46
|
+
createUnicityOracleProvider: () => createUnicityOracleProvider
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(nodejs_exports);
|
|
49
|
+
|
|
50
|
+
// impl/nodejs/storage/FileStorageProvider.ts
|
|
51
|
+
var fs = __toESM(require("fs"), 1);
|
|
52
|
+
var path = __toESM(require("path"), 1);
|
|
53
|
+
var FileStorageProvider = class {
|
|
54
|
+
id = "file-storage";
|
|
55
|
+
name = "File Storage";
|
|
56
|
+
type = "local";
|
|
57
|
+
dataDir;
|
|
58
|
+
filePath;
|
|
59
|
+
data = {};
|
|
60
|
+
status = "disconnected";
|
|
61
|
+
_identity = null;
|
|
62
|
+
constructor(config) {
|
|
63
|
+
if (typeof config === "string") {
|
|
64
|
+
this.dataDir = config;
|
|
65
|
+
this.filePath = path.join(config, "wallet.json");
|
|
66
|
+
} else {
|
|
67
|
+
this.dataDir = config.dataDir;
|
|
68
|
+
this.filePath = path.join(config.dataDir, config.fileName ?? "wallet.json");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
setIdentity(identity) {
|
|
72
|
+
this._identity = identity;
|
|
73
|
+
}
|
|
74
|
+
getIdentity() {
|
|
75
|
+
return this._identity;
|
|
76
|
+
}
|
|
77
|
+
async connect() {
|
|
78
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
79
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
if (fs.existsSync(this.filePath)) {
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(this.filePath, "utf-8");
|
|
84
|
+
this.data = JSON.parse(content);
|
|
85
|
+
} catch {
|
|
86
|
+
this.data = {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.status = "connected";
|
|
90
|
+
}
|
|
91
|
+
async disconnect() {
|
|
92
|
+
await this.save();
|
|
93
|
+
this.status = "disconnected";
|
|
94
|
+
}
|
|
95
|
+
isConnected() {
|
|
96
|
+
return this.status === "connected";
|
|
97
|
+
}
|
|
98
|
+
getStatus() {
|
|
99
|
+
return this.status;
|
|
100
|
+
}
|
|
101
|
+
async get(key) {
|
|
102
|
+
return this.data[key] ?? null;
|
|
103
|
+
}
|
|
104
|
+
async set(key, value) {
|
|
105
|
+
this.data[key] = value;
|
|
106
|
+
await this.save();
|
|
107
|
+
}
|
|
108
|
+
async remove(key) {
|
|
109
|
+
delete this.data[key];
|
|
110
|
+
await this.save();
|
|
111
|
+
}
|
|
112
|
+
async has(key) {
|
|
113
|
+
return key in this.data;
|
|
114
|
+
}
|
|
115
|
+
async keys(prefix) {
|
|
116
|
+
const allKeys = Object.keys(this.data);
|
|
117
|
+
if (prefix) {
|
|
118
|
+
return allKeys.filter((k) => k.startsWith(prefix));
|
|
119
|
+
}
|
|
120
|
+
return allKeys;
|
|
121
|
+
}
|
|
122
|
+
async clear(prefix) {
|
|
123
|
+
if (prefix) {
|
|
124
|
+
const keysToDelete = Object.keys(this.data).filter((k) => k.startsWith(prefix));
|
|
125
|
+
for (const key of keysToDelete) {
|
|
126
|
+
delete this.data[key];
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
this.data = {};
|
|
130
|
+
}
|
|
131
|
+
await this.save();
|
|
132
|
+
}
|
|
133
|
+
async save() {
|
|
134
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
135
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
function createFileStorageProvider(config) {
|
|
141
|
+
return new FileStorageProvider(config);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// impl/nodejs/storage/FileTokenStorageProvider.ts
|
|
145
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
146
|
+
var path2 = __toESM(require("path"), 1);
|
|
147
|
+
var FileTokenStorageProvider = class {
|
|
148
|
+
id = "file-token-storage";
|
|
149
|
+
name = "File Token Storage";
|
|
150
|
+
type = "local";
|
|
151
|
+
tokensDir;
|
|
152
|
+
status = "disconnected";
|
|
153
|
+
identity = null;
|
|
154
|
+
constructor(config) {
|
|
155
|
+
this.tokensDir = typeof config === "string" ? config : config.tokensDir;
|
|
156
|
+
}
|
|
157
|
+
setIdentity(identity) {
|
|
158
|
+
this.identity = identity;
|
|
159
|
+
}
|
|
160
|
+
async initialize() {
|
|
161
|
+
if (!fs2.existsSync(this.tokensDir)) {
|
|
162
|
+
fs2.mkdirSync(this.tokensDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
this.status = "connected";
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
async shutdown() {
|
|
168
|
+
this.status = "disconnected";
|
|
169
|
+
}
|
|
170
|
+
async connect() {
|
|
171
|
+
await this.initialize();
|
|
172
|
+
}
|
|
173
|
+
async disconnect() {
|
|
174
|
+
this.status = "disconnected";
|
|
175
|
+
}
|
|
176
|
+
isConnected() {
|
|
177
|
+
return this.status === "connected";
|
|
178
|
+
}
|
|
179
|
+
getStatus() {
|
|
180
|
+
return this.status;
|
|
181
|
+
}
|
|
182
|
+
async load() {
|
|
183
|
+
const data = {
|
|
184
|
+
_meta: {
|
|
185
|
+
version: 1,
|
|
186
|
+
address: this.identity?.address ?? "",
|
|
187
|
+
formatVersion: "2.0",
|
|
188
|
+
updatedAt: Date.now()
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
const files = fs2.readdirSync(this.tokensDir).filter((f) => f.endsWith(".json") && f !== "_meta.json");
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
try {
|
|
195
|
+
const content = fs2.readFileSync(path2.join(this.tokensDir, file), "utf-8");
|
|
196
|
+
const token = JSON.parse(content);
|
|
197
|
+
const key = `_${path2.basename(file, ".json")}`;
|
|
198
|
+
data[key] = token;
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
success: true,
|
|
204
|
+
data,
|
|
205
|
+
source: "local",
|
|
206
|
+
timestamp: Date.now()
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
212
|
+
source: "local",
|
|
213
|
+
timestamp: Date.now()
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async save(data) {
|
|
218
|
+
try {
|
|
219
|
+
fs2.writeFileSync(
|
|
220
|
+
path2.join(this.tokensDir, "_meta.json"),
|
|
221
|
+
JSON.stringify(data._meta, null, 2)
|
|
222
|
+
);
|
|
223
|
+
for (const [key, value] of Object.entries(data)) {
|
|
224
|
+
if (key.startsWith("_") && key !== "_meta" && key !== "_tombstones" && key !== "_outbox" && key !== "_sent" && key !== "_invalid") {
|
|
225
|
+
const tokenId = key.slice(1);
|
|
226
|
+
fs2.writeFileSync(
|
|
227
|
+
path2.join(this.tokensDir, `${tokenId}.json`),
|
|
228
|
+
JSON.stringify(value, null, 2)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (data._tombstones) {
|
|
233
|
+
for (const tombstone of data._tombstones) {
|
|
234
|
+
const filePath = path2.join(this.tokensDir, `${tombstone.tokenId}.json`);
|
|
235
|
+
if (fs2.existsSync(filePath)) {
|
|
236
|
+
fs2.unlinkSync(filePath);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
success: true,
|
|
242
|
+
timestamp: Date.now()
|
|
243
|
+
};
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
248
|
+
timestamp: Date.now()
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async sync(localData) {
|
|
253
|
+
const saveResult = await this.save(localData);
|
|
254
|
+
return {
|
|
255
|
+
success: saveResult.success,
|
|
256
|
+
merged: localData,
|
|
257
|
+
added: 0,
|
|
258
|
+
removed: 0,
|
|
259
|
+
conflicts: 0,
|
|
260
|
+
error: saveResult.error
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async deleteToken(tokenId) {
|
|
264
|
+
const filePath = path2.join(this.tokensDir, `${tokenId}.json`);
|
|
265
|
+
if (fs2.existsSync(filePath)) {
|
|
266
|
+
fs2.unlinkSync(filePath);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async saveToken(tokenId, tokenData) {
|
|
270
|
+
fs2.writeFileSync(
|
|
271
|
+
path2.join(this.tokensDir, `${tokenId}.json`),
|
|
272
|
+
JSON.stringify(tokenData, null, 2)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
async getToken(tokenId) {
|
|
276
|
+
const filePath = path2.join(this.tokensDir, `${tokenId}.json`);
|
|
277
|
+
if (!fs2.existsSync(filePath)) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
282
|
+
return JSON.parse(content);
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async listTokenIds() {
|
|
288
|
+
const files = fs2.readdirSync(this.tokensDir).filter((f) => f.endsWith(".json") && f !== "_meta.json");
|
|
289
|
+
return files.map((f) => path2.basename(f, ".json"));
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
function createFileTokenStorageProvider(config) {
|
|
293
|
+
return new FileTokenStorageProvider(config);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// impl/nodejs/transport/index.ts
|
|
297
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
298
|
+
|
|
299
|
+
// transport/NostrTransportProvider.ts
|
|
300
|
+
var import_buffer = require("buffer");
|
|
301
|
+
var import_nostr_js_sdk = require("@unicitylabs/nostr-js-sdk");
|
|
302
|
+
|
|
303
|
+
// transport/websocket.ts
|
|
304
|
+
var WebSocketReadyState = {
|
|
305
|
+
CONNECTING: 0,
|
|
306
|
+
OPEN: 1,
|
|
307
|
+
CLOSING: 2,
|
|
308
|
+
CLOSED: 3
|
|
309
|
+
};
|
|
310
|
+
function defaultUUIDGenerator() {
|
|
311
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
312
|
+
return crypto.randomUUID();
|
|
313
|
+
}
|
|
314
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
315
|
+
const r = Math.random() * 16 | 0;
|
|
316
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
317
|
+
return v.toString(16);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// constants.ts
|
|
322
|
+
var STORAGE_PREFIX = "sphere_";
|
|
323
|
+
var STORAGE_KEYS = {
|
|
324
|
+
/** Encrypted BIP39 mnemonic */
|
|
325
|
+
MNEMONIC: `${STORAGE_PREFIX}mnemonic`,
|
|
326
|
+
/** Encrypted master private key */
|
|
327
|
+
MASTER_KEY: `${STORAGE_PREFIX}master_key`,
|
|
328
|
+
/** BIP32 chain code */
|
|
329
|
+
CHAIN_CODE: `${STORAGE_PREFIX}chain_code`,
|
|
330
|
+
/** HD derivation path (full path like m/44'/0'/0'/0/0) */
|
|
331
|
+
DERIVATION_PATH: `${STORAGE_PREFIX}derivation_path`,
|
|
332
|
+
/** Base derivation path (like m/44'/0'/0' without chain/index) */
|
|
333
|
+
BASE_PATH: `${STORAGE_PREFIX}base_path`,
|
|
334
|
+
/** Derivation mode: bip32, wif_hmac, legacy_hmac */
|
|
335
|
+
DERIVATION_MODE: `${STORAGE_PREFIX}derivation_mode`,
|
|
336
|
+
/** Wallet source: mnemonic, file, unknown */
|
|
337
|
+
WALLET_SOURCE: `${STORAGE_PREFIX}wallet_source`,
|
|
338
|
+
/** Wallet existence flag */
|
|
339
|
+
WALLET_EXISTS: `${STORAGE_PREFIX}wallet_exists`,
|
|
340
|
+
/** Registered nametag (legacy - single address) */
|
|
341
|
+
NAMETAG: `${STORAGE_PREFIX}nametag`,
|
|
342
|
+
/** Current active address index */
|
|
343
|
+
CURRENT_ADDRESS_INDEX: `${STORAGE_PREFIX}current_address_index`,
|
|
344
|
+
/** Address nametags map (JSON: { "0": "alice", "1": "bob" }) */
|
|
345
|
+
ADDRESS_NAMETAGS: `${STORAGE_PREFIX}address_nametags`,
|
|
346
|
+
/** Token data */
|
|
347
|
+
TOKENS: `${STORAGE_PREFIX}tokens`,
|
|
348
|
+
/** Pending transfers */
|
|
349
|
+
PENDING_TRANSFERS: `${STORAGE_PREFIX}pending_transfers`,
|
|
350
|
+
/** Transfer outbox */
|
|
351
|
+
OUTBOX: `${STORAGE_PREFIX}outbox`,
|
|
352
|
+
/** Conversations */
|
|
353
|
+
CONVERSATIONS: `${STORAGE_PREFIX}conversations`,
|
|
354
|
+
/** Messages */
|
|
355
|
+
MESSAGES: `${STORAGE_PREFIX}messages`,
|
|
356
|
+
/** Transaction history */
|
|
357
|
+
TRANSACTION_HISTORY: `${STORAGE_PREFIX}transaction_history`,
|
|
358
|
+
/** Archived tokens (spent token history) */
|
|
359
|
+
ARCHIVED_TOKENS: `${STORAGE_PREFIX}archived_tokens`,
|
|
360
|
+
/** Tombstones (records of deleted/spent tokens) */
|
|
361
|
+
TOMBSTONES: `${STORAGE_PREFIX}tombstones`,
|
|
362
|
+
/** Forked tokens (alternative histories) */
|
|
363
|
+
FORKED_TOKENS: `${STORAGE_PREFIX}forked_tokens`
|
|
364
|
+
};
|
|
365
|
+
var DEFAULT_NOSTR_RELAYS = [
|
|
366
|
+
"wss://relay.unicity.network",
|
|
367
|
+
"wss://relay.damus.io",
|
|
368
|
+
"wss://nos.lol",
|
|
369
|
+
"wss://relay.nostr.band"
|
|
370
|
+
];
|
|
371
|
+
var NOSTR_EVENT_KINDS = {
|
|
372
|
+
/** NIP-04 encrypted direct message */
|
|
373
|
+
DIRECT_MESSAGE: 4,
|
|
374
|
+
/** Token transfer (Unicity custom - 31113) */
|
|
375
|
+
TOKEN_TRANSFER: 31113,
|
|
376
|
+
/** Payment request (Unicity custom - 31115) */
|
|
377
|
+
PAYMENT_REQUEST: 31115,
|
|
378
|
+
/** Payment request response (Unicity custom - 31116) */
|
|
379
|
+
PAYMENT_REQUEST_RESPONSE: 31116,
|
|
380
|
+
/** Nametag binding (NIP-78 app-specific data) */
|
|
381
|
+
NAMETAG_BINDING: 30078,
|
|
382
|
+
/** Public broadcast */
|
|
383
|
+
BROADCAST: 1
|
|
384
|
+
};
|
|
385
|
+
var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
|
|
386
|
+
var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
|
|
387
|
+
var TEST_AGGREGATOR_URL = "https://goggregator-test.unicity.network";
|
|
388
|
+
var DEFAULT_AGGREGATOR_TIMEOUT = 3e4;
|
|
389
|
+
var DEFAULT_IPFS_GATEWAYS = [
|
|
390
|
+
"https://ipfs.unicity.network",
|
|
391
|
+
"https://dweb.link",
|
|
392
|
+
"https://ipfs.io"
|
|
393
|
+
];
|
|
394
|
+
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
395
|
+
var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
|
|
396
|
+
var DEFAULT_ELECTRUM_URL = "wss://fulcrum.alpha.unicity.network:50004";
|
|
397
|
+
var TEST_ELECTRUM_URL = "wss://fulcrum.alpha.testnet.unicity.network:50004";
|
|
398
|
+
var TEST_NOSTR_RELAYS = [
|
|
399
|
+
"wss://nostr-relay.testnet.unicity.network"
|
|
400
|
+
];
|
|
401
|
+
var NETWORKS = {
|
|
402
|
+
mainnet: {
|
|
403
|
+
name: "Mainnet",
|
|
404
|
+
aggregatorUrl: DEFAULT_AGGREGATOR_URL,
|
|
405
|
+
nostrRelays: DEFAULT_NOSTR_RELAYS,
|
|
406
|
+
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
407
|
+
electrumUrl: DEFAULT_ELECTRUM_URL
|
|
408
|
+
},
|
|
409
|
+
testnet: {
|
|
410
|
+
name: "Testnet",
|
|
411
|
+
aggregatorUrl: TEST_AGGREGATOR_URL,
|
|
412
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
413
|
+
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
414
|
+
electrumUrl: TEST_ELECTRUM_URL
|
|
415
|
+
},
|
|
416
|
+
dev: {
|
|
417
|
+
name: "Development",
|
|
418
|
+
aggregatorUrl: DEV_AGGREGATOR_URL,
|
|
419
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
420
|
+
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
421
|
+
electrumUrl: TEST_ELECTRUM_URL
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
var TIMEOUTS = {
|
|
425
|
+
/** WebSocket connection timeout */
|
|
426
|
+
WEBSOCKET_CONNECT: 1e4,
|
|
427
|
+
/** Nostr relay reconnect delay */
|
|
428
|
+
NOSTR_RECONNECT_DELAY: 3e3,
|
|
429
|
+
/** Max reconnect attempts */
|
|
430
|
+
MAX_RECONNECT_ATTEMPTS: 5,
|
|
431
|
+
/** Proof polling interval */
|
|
432
|
+
PROOF_POLL_INTERVAL: 1e3,
|
|
433
|
+
/** Sync interval */
|
|
434
|
+
SYNC_INTERVAL: 6e4
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// transport/NostrTransportProvider.ts
|
|
438
|
+
var EVENT_KINDS = NOSTR_EVENT_KINDS;
|
|
439
|
+
var NostrTransportProvider = class {
|
|
440
|
+
id = "nostr";
|
|
441
|
+
name = "Nostr Transport";
|
|
442
|
+
type = "p2p";
|
|
443
|
+
description = "P2P messaging via Nostr protocol";
|
|
444
|
+
config;
|
|
445
|
+
identity = null;
|
|
446
|
+
keyManager = null;
|
|
447
|
+
status = "disconnected";
|
|
448
|
+
// WebSocket connections to relays
|
|
449
|
+
connections = /* @__PURE__ */ new Map();
|
|
450
|
+
reconnectAttempts = /* @__PURE__ */ new Map();
|
|
451
|
+
// Event handlers
|
|
452
|
+
messageHandlers = /* @__PURE__ */ new Set();
|
|
453
|
+
transferHandlers = /* @__PURE__ */ new Set();
|
|
454
|
+
paymentRequestHandlers = /* @__PURE__ */ new Set();
|
|
455
|
+
paymentRequestResponseHandlers = /* @__PURE__ */ new Set();
|
|
456
|
+
broadcastHandlers = /* @__PURE__ */ new Map();
|
|
457
|
+
eventCallbacks = /* @__PURE__ */ new Set();
|
|
458
|
+
// Subscriptions
|
|
459
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
460
|
+
// subId -> relays
|
|
461
|
+
constructor(config) {
|
|
462
|
+
this.config = {
|
|
463
|
+
relays: config.relays ?? [...DEFAULT_NOSTR_RELAYS],
|
|
464
|
+
timeout: config.timeout ?? TIMEOUTS.WEBSOCKET_CONNECT,
|
|
465
|
+
autoReconnect: config.autoReconnect ?? true,
|
|
466
|
+
reconnectDelay: config.reconnectDelay ?? TIMEOUTS.NOSTR_RECONNECT_DELAY,
|
|
467
|
+
maxReconnectAttempts: config.maxReconnectAttempts ?? TIMEOUTS.MAX_RECONNECT_ATTEMPTS,
|
|
468
|
+
debug: config.debug ?? false,
|
|
469
|
+
createWebSocket: config.createWebSocket,
|
|
470
|
+
generateUUID: config.generateUUID ?? defaultUUIDGenerator
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
// ===========================================================================
|
|
474
|
+
// BaseProvider Implementation
|
|
475
|
+
// ===========================================================================
|
|
476
|
+
async connect() {
|
|
477
|
+
if (this.status === "connected") return;
|
|
478
|
+
this.status = "connecting";
|
|
479
|
+
try {
|
|
480
|
+
const connectPromises = this.config.relays.map(
|
|
481
|
+
(relay) => this.connectToRelay(relay)
|
|
482
|
+
);
|
|
483
|
+
await Promise.allSettled(connectPromises);
|
|
484
|
+
if (this.connections.size === 0) {
|
|
485
|
+
throw new Error("Failed to connect to any relay");
|
|
486
|
+
}
|
|
487
|
+
this.status = "connected";
|
|
488
|
+
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
489
|
+
this.log("Connected to", this.connections.size, "relays");
|
|
490
|
+
if (this.identity) {
|
|
491
|
+
this.subscribeToEvents();
|
|
492
|
+
}
|
|
493
|
+
} catch (error) {
|
|
494
|
+
this.status = "error";
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async disconnect() {
|
|
499
|
+
for (const [url, ws] of this.connections) {
|
|
500
|
+
ws.close();
|
|
501
|
+
this.connections.delete(url);
|
|
502
|
+
}
|
|
503
|
+
this.subscriptions.clear();
|
|
504
|
+
this.status = "disconnected";
|
|
505
|
+
this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
|
|
506
|
+
this.log("Disconnected from all relays");
|
|
507
|
+
}
|
|
508
|
+
isConnected() {
|
|
509
|
+
return this.status === "connected" && this.connections.size > 0;
|
|
510
|
+
}
|
|
511
|
+
getStatus() {
|
|
512
|
+
return this.status;
|
|
513
|
+
}
|
|
514
|
+
// ===========================================================================
|
|
515
|
+
// Dynamic Relay Management
|
|
516
|
+
// ===========================================================================
|
|
517
|
+
/**
|
|
518
|
+
* Get list of configured relay URLs
|
|
519
|
+
*/
|
|
520
|
+
getRelays() {
|
|
521
|
+
return [...this.config.relays];
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Get list of currently connected relay URLs
|
|
525
|
+
*/
|
|
526
|
+
getConnectedRelays() {
|
|
527
|
+
return Array.from(this.connections.keys());
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Add a new relay dynamically
|
|
531
|
+
* Will connect immediately if provider is already connected
|
|
532
|
+
*/
|
|
533
|
+
async addRelay(relayUrl) {
|
|
534
|
+
if (this.config.relays.includes(relayUrl)) {
|
|
535
|
+
this.log("Relay already configured:", relayUrl);
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
this.config.relays.push(relayUrl);
|
|
539
|
+
if (this.status === "connected") {
|
|
540
|
+
try {
|
|
541
|
+
await this.connectToRelay(relayUrl);
|
|
542
|
+
this.log("Added and connected to relay:", relayUrl);
|
|
543
|
+
this.emitEvent({
|
|
544
|
+
type: "transport:relay_added",
|
|
545
|
+
timestamp: Date.now(),
|
|
546
|
+
data: { relay: relayUrl, connected: true }
|
|
547
|
+
});
|
|
548
|
+
return true;
|
|
549
|
+
} catch (error) {
|
|
550
|
+
this.log("Failed to connect to new relay:", relayUrl, error);
|
|
551
|
+
this.emitEvent({
|
|
552
|
+
type: "transport:relay_added",
|
|
553
|
+
timestamp: Date.now(),
|
|
554
|
+
data: { relay: relayUrl, connected: false, error: String(error) }
|
|
555
|
+
});
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
this.emitEvent({
|
|
560
|
+
type: "transport:relay_added",
|
|
561
|
+
timestamp: Date.now(),
|
|
562
|
+
data: { relay: relayUrl, connected: false }
|
|
563
|
+
});
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Remove a relay dynamically
|
|
568
|
+
* Will disconnect from the relay if connected
|
|
569
|
+
*/
|
|
570
|
+
async removeRelay(relayUrl) {
|
|
571
|
+
const index = this.config.relays.indexOf(relayUrl);
|
|
572
|
+
if (index === -1) {
|
|
573
|
+
this.log("Relay not found:", relayUrl);
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
this.config.relays.splice(index, 1);
|
|
577
|
+
const ws = this.connections.get(relayUrl);
|
|
578
|
+
if (ws) {
|
|
579
|
+
ws.close();
|
|
580
|
+
this.connections.delete(relayUrl);
|
|
581
|
+
this.reconnectAttempts.delete(relayUrl);
|
|
582
|
+
this.log("Removed and disconnected from relay:", relayUrl);
|
|
583
|
+
}
|
|
584
|
+
this.emitEvent({
|
|
585
|
+
type: "transport:relay_removed",
|
|
586
|
+
timestamp: Date.now(),
|
|
587
|
+
data: { relay: relayUrl }
|
|
588
|
+
});
|
|
589
|
+
if (this.connections.size === 0 && this.status === "connected") {
|
|
590
|
+
this.status = "error";
|
|
591
|
+
this.emitEvent({
|
|
592
|
+
type: "transport:error",
|
|
593
|
+
timestamp: Date.now(),
|
|
594
|
+
data: { error: "No connected relays remaining" }
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Check if a relay is configured
|
|
601
|
+
*/
|
|
602
|
+
hasRelay(relayUrl) {
|
|
603
|
+
return this.config.relays.includes(relayUrl);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Check if a relay is currently connected
|
|
607
|
+
*/
|
|
608
|
+
isRelayConnected(relayUrl) {
|
|
609
|
+
const ws = this.connections.get(relayUrl);
|
|
610
|
+
return ws !== void 0 && ws.readyState === WebSocketReadyState.OPEN;
|
|
611
|
+
}
|
|
612
|
+
// ===========================================================================
|
|
613
|
+
// TransportProvider Implementation
|
|
614
|
+
// ===========================================================================
|
|
615
|
+
setIdentity(identity) {
|
|
616
|
+
this.identity = identity;
|
|
617
|
+
const secretKey = import_buffer.Buffer.from(identity.privateKey, "hex");
|
|
618
|
+
this.keyManager = import_nostr_js_sdk.NostrKeyManager.fromPrivateKey(secretKey);
|
|
619
|
+
const nostrPubkey = this.keyManager.getPublicKeyHex();
|
|
620
|
+
this.log("Identity set, Nostr pubkey:", nostrPubkey.slice(0, 16) + "...");
|
|
621
|
+
if (this.isConnected()) {
|
|
622
|
+
this.subscribeToEvents();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Get the Nostr-format public key (32 bytes / 64 hex chars)
|
|
627
|
+
* This is the x-coordinate only, without the 02/03 prefix.
|
|
628
|
+
*/
|
|
629
|
+
getNostrPubkey() {
|
|
630
|
+
if (!this.keyManager) {
|
|
631
|
+
throw new Error("KeyManager not initialized - call setIdentity first");
|
|
632
|
+
}
|
|
633
|
+
return this.keyManager.getPublicKeyHex();
|
|
634
|
+
}
|
|
635
|
+
async sendMessage(recipientPubkey, content) {
|
|
636
|
+
this.ensureReady();
|
|
637
|
+
const event = await this.createEncryptedEvent(
|
|
638
|
+
EVENT_KINDS.DIRECT_MESSAGE,
|
|
639
|
+
content,
|
|
640
|
+
[["p", recipientPubkey]]
|
|
641
|
+
);
|
|
642
|
+
await this.publishEvent(event);
|
|
643
|
+
this.emitEvent({
|
|
644
|
+
type: "message:sent",
|
|
645
|
+
timestamp: Date.now(),
|
|
646
|
+
data: { recipient: recipientPubkey }
|
|
647
|
+
});
|
|
648
|
+
return event.id;
|
|
649
|
+
}
|
|
650
|
+
onMessage(handler) {
|
|
651
|
+
this.messageHandlers.add(handler);
|
|
652
|
+
return () => this.messageHandlers.delete(handler);
|
|
653
|
+
}
|
|
654
|
+
async sendTokenTransfer(recipientPubkey, payload) {
|
|
655
|
+
this.ensureReady();
|
|
656
|
+
const content = "token_transfer:" + JSON.stringify(payload);
|
|
657
|
+
const event = await this.createEncryptedEvent(
|
|
658
|
+
EVENT_KINDS.TOKEN_TRANSFER,
|
|
659
|
+
content,
|
|
660
|
+
[
|
|
661
|
+
["p", recipientPubkey],
|
|
662
|
+
["d", "token-transfer"],
|
|
663
|
+
["type", "token_transfer"]
|
|
664
|
+
]
|
|
665
|
+
);
|
|
666
|
+
await this.publishEvent(event);
|
|
667
|
+
this.emitEvent({
|
|
668
|
+
type: "transfer:sent",
|
|
669
|
+
timestamp: Date.now(),
|
|
670
|
+
data: { recipient: recipientPubkey }
|
|
671
|
+
});
|
|
672
|
+
return event.id;
|
|
673
|
+
}
|
|
674
|
+
onTokenTransfer(handler) {
|
|
675
|
+
this.transferHandlers.add(handler);
|
|
676
|
+
return () => this.transferHandlers.delete(handler);
|
|
677
|
+
}
|
|
678
|
+
async sendPaymentRequest(recipientPubkey, payload) {
|
|
679
|
+
this.ensureReady();
|
|
680
|
+
const requestId = this.config.generateUUID();
|
|
681
|
+
const amount = typeof payload.amount === "bigint" ? payload.amount.toString() : payload.amount;
|
|
682
|
+
const requestContent = {
|
|
683
|
+
requestId,
|
|
684
|
+
amount,
|
|
685
|
+
coinId: payload.coinId,
|
|
686
|
+
message: payload.message,
|
|
687
|
+
recipientNametag: payload.recipientNametag,
|
|
688
|
+
deadline: Date.now() + 5 * 60 * 1e3
|
|
689
|
+
// 5 minutes default
|
|
690
|
+
};
|
|
691
|
+
const content = "payment_request:" + JSON.stringify(requestContent);
|
|
692
|
+
const tags = [
|
|
693
|
+
["p", recipientPubkey],
|
|
694
|
+
["type", "payment_request"],
|
|
695
|
+
["amount", amount]
|
|
696
|
+
];
|
|
697
|
+
if (payload.recipientNametag) {
|
|
698
|
+
tags.push(["recipient", payload.recipientNametag]);
|
|
699
|
+
}
|
|
700
|
+
const event = await this.createEncryptedEvent(
|
|
701
|
+
EVENT_KINDS.PAYMENT_REQUEST,
|
|
702
|
+
content,
|
|
703
|
+
tags
|
|
704
|
+
);
|
|
705
|
+
await this.publishEvent(event);
|
|
706
|
+
this.log("Sent payment request:", event.id);
|
|
707
|
+
return event.id;
|
|
708
|
+
}
|
|
709
|
+
onPaymentRequest(handler) {
|
|
710
|
+
this.paymentRequestHandlers.add(handler);
|
|
711
|
+
return () => this.paymentRequestHandlers.delete(handler);
|
|
712
|
+
}
|
|
713
|
+
async sendPaymentRequestResponse(recipientPubkey, payload) {
|
|
714
|
+
this.ensureReady();
|
|
715
|
+
const responseContent = {
|
|
716
|
+
requestId: payload.requestId,
|
|
717
|
+
responseType: payload.responseType,
|
|
718
|
+
message: payload.message,
|
|
719
|
+
transferId: payload.transferId
|
|
720
|
+
};
|
|
721
|
+
const content = "payment_response:" + JSON.stringify(responseContent);
|
|
722
|
+
const event = await this.createEncryptedEvent(
|
|
723
|
+
EVENT_KINDS.PAYMENT_REQUEST_RESPONSE,
|
|
724
|
+
content,
|
|
725
|
+
[
|
|
726
|
+
["p", recipientPubkey],
|
|
727
|
+
["e", payload.requestId],
|
|
728
|
+
// Reference to original request
|
|
729
|
+
["d", "payment-request-response"],
|
|
730
|
+
["type", "payment_response"]
|
|
731
|
+
]
|
|
732
|
+
);
|
|
733
|
+
await this.publishEvent(event);
|
|
734
|
+
this.log("Sent payment request response:", event.id, "type:", payload.responseType);
|
|
735
|
+
return event.id;
|
|
736
|
+
}
|
|
737
|
+
onPaymentRequestResponse(handler) {
|
|
738
|
+
this.paymentRequestResponseHandlers.add(handler);
|
|
739
|
+
return () => this.paymentRequestResponseHandlers.delete(handler);
|
|
740
|
+
}
|
|
741
|
+
async resolveNametag(nametag) {
|
|
742
|
+
this.ensureReady();
|
|
743
|
+
const hashedNametag = (0, import_nostr_js_sdk.hashNametag)(nametag);
|
|
744
|
+
let events = await this.queryEvents({
|
|
745
|
+
kinds: [EVENT_KINDS.NAMETAG_BINDING],
|
|
746
|
+
"#t": [hashedNametag],
|
|
747
|
+
limit: 1
|
|
748
|
+
});
|
|
749
|
+
if (events.length === 0) {
|
|
750
|
+
events = await this.queryEvents({
|
|
751
|
+
kinds: [EVENT_KINDS.NAMETAG_BINDING],
|
|
752
|
+
"#d": [hashedNametag],
|
|
753
|
+
limit: 1
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
if (events.length === 0) return null;
|
|
757
|
+
const bindingEvent = events[0];
|
|
758
|
+
if (bindingEvent.pubkey) {
|
|
759
|
+
return bindingEvent.pubkey;
|
|
760
|
+
}
|
|
761
|
+
const pubkeyTag = bindingEvent.tags.find((t) => t[0] === "p");
|
|
762
|
+
if (pubkeyTag?.[1]) return pubkeyTag[1];
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
async publishNametag(nametag, address) {
|
|
766
|
+
this.ensureReady();
|
|
767
|
+
const hashedNametag = (0, import_nostr_js_sdk.hashNametag)(nametag);
|
|
768
|
+
const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, address, [
|
|
769
|
+
["d", hashedNametag],
|
|
770
|
+
["a", address]
|
|
771
|
+
]);
|
|
772
|
+
await this.publishEvent(event);
|
|
773
|
+
this.log("Published nametag binding:", nametag);
|
|
774
|
+
}
|
|
775
|
+
async registerNametag(nametag, _publicKey) {
|
|
776
|
+
this.ensureReady();
|
|
777
|
+
const nostrPubkey = this.getNostrPubkey();
|
|
778
|
+
const existing = await this.resolveNametag(nametag);
|
|
779
|
+
this.log("registerNametag:", nametag, "existing:", existing, "myPubkey:", nostrPubkey);
|
|
780
|
+
if (existing && existing !== nostrPubkey) {
|
|
781
|
+
this.log("Nametag already taken:", nametag, "- owner:", existing);
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
const hashedNametag = (0, import_nostr_js_sdk.hashNametag)(nametag);
|
|
785
|
+
const content = JSON.stringify({
|
|
786
|
+
nametag_hash: hashedNametag,
|
|
787
|
+
address: nostrPubkey,
|
|
788
|
+
verified: Date.now()
|
|
789
|
+
});
|
|
790
|
+
const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, [
|
|
791
|
+
["d", hashedNametag],
|
|
792
|
+
["nametag", hashedNametag],
|
|
793
|
+
["t", hashedNametag],
|
|
794
|
+
["address", nostrPubkey]
|
|
795
|
+
]);
|
|
796
|
+
await this.publishEvent(event);
|
|
797
|
+
this.log("Registered nametag:", nametag, "for pubkey:", nostrPubkey.slice(0, 16) + "...");
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
subscribeToBroadcast(tags, handler) {
|
|
801
|
+
const key = tags.sort().join(":");
|
|
802
|
+
if (!this.broadcastHandlers.has(key)) {
|
|
803
|
+
this.broadcastHandlers.set(key, /* @__PURE__ */ new Set());
|
|
804
|
+
if (this.isConnected()) {
|
|
805
|
+
this.subscribeToTags(tags);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
this.broadcastHandlers.get(key).add(handler);
|
|
809
|
+
return () => {
|
|
810
|
+
this.broadcastHandlers.get(key)?.delete(handler);
|
|
811
|
+
if (this.broadcastHandlers.get(key)?.size === 0) {
|
|
812
|
+
this.broadcastHandlers.delete(key);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
async publishBroadcast(content, tags) {
|
|
817
|
+
this.ensureReady();
|
|
818
|
+
const eventTags = tags?.map((t) => ["t", t]) ?? [];
|
|
819
|
+
const event = await this.createEvent(EVENT_KINDS.BROADCAST, content, eventTags);
|
|
820
|
+
await this.publishEvent(event);
|
|
821
|
+
return event.id;
|
|
822
|
+
}
|
|
823
|
+
// ===========================================================================
|
|
824
|
+
// Event Subscription
|
|
825
|
+
// ===========================================================================
|
|
826
|
+
onEvent(callback) {
|
|
827
|
+
this.eventCallbacks.add(callback);
|
|
828
|
+
return () => this.eventCallbacks.delete(callback);
|
|
829
|
+
}
|
|
830
|
+
// ===========================================================================
|
|
831
|
+
// Private: Connection Management
|
|
832
|
+
// ===========================================================================
|
|
833
|
+
async connectToRelay(url) {
|
|
834
|
+
return new Promise((resolve, reject) => {
|
|
835
|
+
const ws = this.config.createWebSocket(url);
|
|
836
|
+
const timeout = setTimeout(() => {
|
|
837
|
+
ws.close();
|
|
838
|
+
reject(new Error(`Connection timeout: ${url}`));
|
|
839
|
+
}, this.config.timeout);
|
|
840
|
+
ws.onopen = () => {
|
|
841
|
+
clearTimeout(timeout);
|
|
842
|
+
this.connections.set(url, ws);
|
|
843
|
+
this.reconnectAttempts.set(url, 0);
|
|
844
|
+
this.log("Connected to relay:", url);
|
|
845
|
+
resolve();
|
|
846
|
+
};
|
|
847
|
+
ws.onerror = (error) => {
|
|
848
|
+
clearTimeout(timeout);
|
|
849
|
+
this.log("Relay error:", url, error);
|
|
850
|
+
reject(error);
|
|
851
|
+
};
|
|
852
|
+
ws.onclose = () => {
|
|
853
|
+
this.connections.delete(url);
|
|
854
|
+
if (this.config.autoReconnect && this.status === "connected") {
|
|
855
|
+
this.scheduleReconnect(url);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
ws.onmessage = (event) => {
|
|
859
|
+
this.handleRelayMessage(url, event.data);
|
|
860
|
+
};
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
scheduleReconnect(url) {
|
|
864
|
+
const attempts = this.reconnectAttempts.get(url) ?? 0;
|
|
865
|
+
if (attempts >= this.config.maxReconnectAttempts) {
|
|
866
|
+
this.log("Max reconnect attempts reached for:", url);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
this.reconnectAttempts.set(url, attempts + 1);
|
|
870
|
+
const delay = this.config.reconnectDelay * Math.pow(2, attempts);
|
|
871
|
+
this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
|
|
872
|
+
setTimeout(() => {
|
|
873
|
+
this.connectToRelay(url).catch(() => {
|
|
874
|
+
});
|
|
875
|
+
}, delay);
|
|
876
|
+
}
|
|
877
|
+
// ===========================================================================
|
|
878
|
+
// Private: Message Handling
|
|
879
|
+
// ===========================================================================
|
|
880
|
+
handleRelayMessage(relay, data) {
|
|
881
|
+
try {
|
|
882
|
+
const message = JSON.parse(data);
|
|
883
|
+
const [type, ...args] = message;
|
|
884
|
+
switch (type) {
|
|
885
|
+
case "EVENT":
|
|
886
|
+
this.handleEvent(args[1]);
|
|
887
|
+
break;
|
|
888
|
+
case "EOSE":
|
|
889
|
+
break;
|
|
890
|
+
case "OK":
|
|
891
|
+
break;
|
|
892
|
+
case "NOTICE":
|
|
893
|
+
this.log("Relay notice:", relay, args[0]);
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
} catch (error) {
|
|
897
|
+
this.log("Failed to parse relay message:", error);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
async handleEvent(event) {
|
|
901
|
+
try {
|
|
902
|
+
switch (event.kind) {
|
|
903
|
+
case EVENT_KINDS.DIRECT_MESSAGE:
|
|
904
|
+
await this.handleDirectMessage(event);
|
|
905
|
+
break;
|
|
906
|
+
case EVENT_KINDS.TOKEN_TRANSFER:
|
|
907
|
+
await this.handleTokenTransfer(event);
|
|
908
|
+
break;
|
|
909
|
+
case EVENT_KINDS.PAYMENT_REQUEST:
|
|
910
|
+
await this.handlePaymentRequest(event);
|
|
911
|
+
break;
|
|
912
|
+
case EVENT_KINDS.PAYMENT_REQUEST_RESPONSE:
|
|
913
|
+
await this.handlePaymentRequestResponse(event);
|
|
914
|
+
break;
|
|
915
|
+
case EVENT_KINDS.BROADCAST:
|
|
916
|
+
this.handleBroadcast(event);
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
} catch (error) {
|
|
920
|
+
this.log("Failed to handle event:", error);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async handleDirectMessage(event) {
|
|
924
|
+
if (!this.identity || !this.keyManager) return;
|
|
925
|
+
if (event.pubkey === this.keyManager.getPublicKeyHex()) return;
|
|
926
|
+
const content = await this.decryptContent(event.content, event.pubkey);
|
|
927
|
+
const message = {
|
|
928
|
+
id: event.id,
|
|
929
|
+
senderPubkey: event.pubkey,
|
|
930
|
+
content,
|
|
931
|
+
timestamp: event.created_at * 1e3,
|
|
932
|
+
encrypted: true
|
|
933
|
+
};
|
|
934
|
+
this.emitEvent({ type: "message:received", timestamp: Date.now() });
|
|
935
|
+
for (const handler of this.messageHandlers) {
|
|
936
|
+
try {
|
|
937
|
+
handler(message);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
this.log("Message handler error:", error);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async handleTokenTransfer(event) {
|
|
944
|
+
if (!this.identity) return;
|
|
945
|
+
const content = await this.decryptContent(event.content, event.pubkey);
|
|
946
|
+
const payload = JSON.parse(content);
|
|
947
|
+
const transfer = {
|
|
948
|
+
id: event.id,
|
|
949
|
+
senderPubkey: event.pubkey,
|
|
950
|
+
payload,
|
|
951
|
+
timestamp: event.created_at * 1e3
|
|
952
|
+
};
|
|
953
|
+
this.emitEvent({ type: "transfer:received", timestamp: Date.now() });
|
|
954
|
+
for (const handler of this.transferHandlers) {
|
|
955
|
+
try {
|
|
956
|
+
handler(transfer);
|
|
957
|
+
} catch (error) {
|
|
958
|
+
this.log("Transfer handler error:", error);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async handlePaymentRequest(event) {
|
|
963
|
+
if (!this.identity) return;
|
|
964
|
+
try {
|
|
965
|
+
const content = await this.decryptContent(event.content, event.pubkey);
|
|
966
|
+
const requestData = JSON.parse(content);
|
|
967
|
+
const request = {
|
|
968
|
+
id: event.id,
|
|
969
|
+
senderPubkey: event.pubkey,
|
|
970
|
+
request: {
|
|
971
|
+
requestId: requestData.requestId,
|
|
972
|
+
amount: requestData.amount,
|
|
973
|
+
coinId: requestData.coinId,
|
|
974
|
+
message: requestData.message,
|
|
975
|
+
recipientNametag: requestData.recipientNametag,
|
|
976
|
+
metadata: requestData.metadata
|
|
977
|
+
},
|
|
978
|
+
timestamp: event.created_at * 1e3
|
|
979
|
+
};
|
|
980
|
+
this.log("Received payment request:", request.id);
|
|
981
|
+
for (const handler of this.paymentRequestHandlers) {
|
|
982
|
+
try {
|
|
983
|
+
handler(request);
|
|
984
|
+
} catch (error) {
|
|
985
|
+
this.log("Payment request handler error:", error);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
} catch (error) {
|
|
989
|
+
this.log("Failed to handle payment request:", error);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
async handlePaymentRequestResponse(event) {
|
|
993
|
+
if (!this.identity) return;
|
|
994
|
+
try {
|
|
995
|
+
const content = await this.decryptContent(event.content, event.pubkey);
|
|
996
|
+
const responseData = JSON.parse(content);
|
|
997
|
+
const response = {
|
|
998
|
+
id: event.id,
|
|
999
|
+
responderPubkey: event.pubkey,
|
|
1000
|
+
response: {
|
|
1001
|
+
requestId: responseData.requestId,
|
|
1002
|
+
responseType: responseData.responseType,
|
|
1003
|
+
message: responseData.message,
|
|
1004
|
+
transferId: responseData.transferId
|
|
1005
|
+
},
|
|
1006
|
+
timestamp: event.created_at * 1e3
|
|
1007
|
+
};
|
|
1008
|
+
this.log("Received payment request response:", response.id, "type:", responseData.responseType);
|
|
1009
|
+
for (const handler of this.paymentRequestResponseHandlers) {
|
|
1010
|
+
try {
|
|
1011
|
+
handler(response);
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
this.log("Payment request response handler error:", error);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
this.log("Failed to handle payment request response:", error);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
handleBroadcast(event) {
|
|
1021
|
+
const tags = event.tags.filter((t) => t[0] === "t").map((t) => t[1]);
|
|
1022
|
+
const broadcast = {
|
|
1023
|
+
id: event.id,
|
|
1024
|
+
authorPubkey: event.pubkey,
|
|
1025
|
+
content: event.content,
|
|
1026
|
+
tags,
|
|
1027
|
+
timestamp: event.created_at * 1e3
|
|
1028
|
+
};
|
|
1029
|
+
for (const [key, handlers] of this.broadcastHandlers) {
|
|
1030
|
+
const subscribedTags = key.split(":");
|
|
1031
|
+
if (tags.some((t) => subscribedTags.includes(t))) {
|
|
1032
|
+
for (const handler of handlers) {
|
|
1033
|
+
try {
|
|
1034
|
+
handler(broadcast);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
this.log("Broadcast handler error:", error);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// ===========================================================================
|
|
1043
|
+
// Private: Event Creation & Publishing
|
|
1044
|
+
// ===========================================================================
|
|
1045
|
+
async createEvent(kind, content, tags) {
|
|
1046
|
+
if (!this.identity) throw new Error("Identity not set");
|
|
1047
|
+
if (!this.keyManager) throw new Error("KeyManager not initialized");
|
|
1048
|
+
const signedEvent = import_nostr_js_sdk.Event.create(this.keyManager, {
|
|
1049
|
+
kind,
|
|
1050
|
+
content,
|
|
1051
|
+
tags
|
|
1052
|
+
});
|
|
1053
|
+
const event = {
|
|
1054
|
+
id: signedEvent.id,
|
|
1055
|
+
kind: signedEvent.kind,
|
|
1056
|
+
content: signedEvent.content,
|
|
1057
|
+
tags: signedEvent.tags,
|
|
1058
|
+
pubkey: signedEvent.pubkey,
|
|
1059
|
+
created_at: signedEvent.created_at,
|
|
1060
|
+
sig: signedEvent.sig
|
|
1061
|
+
};
|
|
1062
|
+
return event;
|
|
1063
|
+
}
|
|
1064
|
+
async createEncryptedEvent(kind, content, tags) {
|
|
1065
|
+
if (!this.keyManager) throw new Error("KeyManager not initialized");
|
|
1066
|
+
const recipientTag = tags.find((t) => t[0] === "p");
|
|
1067
|
+
if (!recipientTag || !recipientTag[1]) {
|
|
1068
|
+
throw new Error("No recipient pubkey in tags for encryption");
|
|
1069
|
+
}
|
|
1070
|
+
const recipientPubkey = recipientTag[1];
|
|
1071
|
+
const encrypted = await import_nostr_js_sdk.NIP04.encryptHex(
|
|
1072
|
+
content,
|
|
1073
|
+
this.keyManager.getPrivateKeyHex(),
|
|
1074
|
+
recipientPubkey
|
|
1075
|
+
);
|
|
1076
|
+
return this.createEvent(kind, encrypted, tags);
|
|
1077
|
+
}
|
|
1078
|
+
async publishEvent(event) {
|
|
1079
|
+
const message = JSON.stringify(["EVENT", event]);
|
|
1080
|
+
const publishPromises = Array.from(this.connections.values()).map((ws) => {
|
|
1081
|
+
return new Promise((resolve, reject) => {
|
|
1082
|
+
if (ws.readyState !== WebSocketReadyState.OPEN) {
|
|
1083
|
+
reject(new Error("WebSocket not open"));
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
ws.send(message);
|
|
1087
|
+
resolve();
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
1090
|
+
await Promise.any(publishPromises);
|
|
1091
|
+
}
|
|
1092
|
+
async queryEvents(filter) {
|
|
1093
|
+
if (this.connections.size === 0) {
|
|
1094
|
+
throw new Error("No connected relays");
|
|
1095
|
+
}
|
|
1096
|
+
const queryPromises = Array.from(this.connections.values()).map(
|
|
1097
|
+
(ws) => this.queryEventsFromRelay(ws, filter)
|
|
1098
|
+
);
|
|
1099
|
+
const results = await Promise.allSettled(queryPromises);
|
|
1100
|
+
for (const result of results) {
|
|
1101
|
+
if (result.status === "fulfilled" && result.value.length > 0) {
|
|
1102
|
+
return result.value;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return [];
|
|
1106
|
+
}
|
|
1107
|
+
async queryEventsFromRelay(ws, filter) {
|
|
1108
|
+
const subId = this.config.generateUUID().slice(0, 8);
|
|
1109
|
+
const events = [];
|
|
1110
|
+
return new Promise((resolve) => {
|
|
1111
|
+
const timeout = setTimeout(() => {
|
|
1112
|
+
this.unsubscribeFromRelay(ws, subId);
|
|
1113
|
+
resolve(events);
|
|
1114
|
+
}, 5e3);
|
|
1115
|
+
const originalHandler = ws.onmessage;
|
|
1116
|
+
ws.onmessage = (event) => {
|
|
1117
|
+
const message = JSON.parse(event.data);
|
|
1118
|
+
const [type, sid, data] = message;
|
|
1119
|
+
if (sid !== subId) {
|
|
1120
|
+
originalHandler?.call(ws, event);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (type === "EVENT") {
|
|
1124
|
+
events.push(data);
|
|
1125
|
+
} else if (type === "EOSE") {
|
|
1126
|
+
clearTimeout(timeout);
|
|
1127
|
+
ws.onmessage = originalHandler;
|
|
1128
|
+
this.unsubscribeFromRelay(ws, subId);
|
|
1129
|
+
resolve(events);
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
ws.send(JSON.stringify(["REQ", subId, filter]));
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
unsubscribeFromRelay(ws, subId) {
|
|
1136
|
+
if (ws.readyState === WebSocketReadyState.OPEN) {
|
|
1137
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
// ===========================================================================
|
|
1141
|
+
// Private: Subscriptions
|
|
1142
|
+
// ===========================================================================
|
|
1143
|
+
subscribeToEvents() {
|
|
1144
|
+
if (!this.identity || !this.keyManager) return;
|
|
1145
|
+
const subId = "main";
|
|
1146
|
+
const nostrPubkey = this.keyManager.getPublicKeyHex();
|
|
1147
|
+
const filter = {
|
|
1148
|
+
kinds: [
|
|
1149
|
+
EVENT_KINDS.DIRECT_MESSAGE,
|
|
1150
|
+
EVENT_KINDS.TOKEN_TRANSFER,
|
|
1151
|
+
EVENT_KINDS.PAYMENT_REQUEST,
|
|
1152
|
+
EVENT_KINDS.PAYMENT_REQUEST_RESPONSE
|
|
1153
|
+
],
|
|
1154
|
+
"#p": [nostrPubkey],
|
|
1155
|
+
since: Math.floor(Date.now() / 1e3) - 86400
|
|
1156
|
+
// Last 24h
|
|
1157
|
+
};
|
|
1158
|
+
const message = JSON.stringify(["REQ", subId, filter]);
|
|
1159
|
+
for (const ws of this.connections.values()) {
|
|
1160
|
+
if (ws.readyState === WebSocketReadyState.OPEN) {
|
|
1161
|
+
ws.send(message);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
this.subscriptions.set(subId, Array.from(this.connections.keys()));
|
|
1165
|
+
this.log("Subscribed to events");
|
|
1166
|
+
}
|
|
1167
|
+
subscribeToTags(tags) {
|
|
1168
|
+
const subId = `tags:${tags.join(":")}`;
|
|
1169
|
+
const filter = {
|
|
1170
|
+
kinds: [EVENT_KINDS.BROADCAST],
|
|
1171
|
+
"#t": tags,
|
|
1172
|
+
since: Math.floor(Date.now() / 1e3) - 3600
|
|
1173
|
+
// Last hour
|
|
1174
|
+
};
|
|
1175
|
+
const message = JSON.stringify(["REQ", subId, filter]);
|
|
1176
|
+
for (const ws of this.connections.values()) {
|
|
1177
|
+
if (ws.readyState === WebSocketReadyState.OPEN) {
|
|
1178
|
+
ws.send(message);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
this.subscriptions.set(subId, Array.from(this.connections.keys()));
|
|
1182
|
+
}
|
|
1183
|
+
// ===========================================================================
|
|
1184
|
+
// Private: Encryption
|
|
1185
|
+
// ===========================================================================
|
|
1186
|
+
async decryptContent(content, senderPubkey) {
|
|
1187
|
+
if (!this.keyManager) throw new Error("KeyManager not initialized");
|
|
1188
|
+
const decrypted = await import_nostr_js_sdk.NIP04.decryptHex(
|
|
1189
|
+
content,
|
|
1190
|
+
this.keyManager.getPrivateKeyHex(),
|
|
1191
|
+
senderPubkey
|
|
1192
|
+
);
|
|
1193
|
+
return this.stripContentPrefix(decrypted);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Strip known content prefixes (nostr-js-sdk compatibility)
|
|
1197
|
+
* Handles: payment_request:, token_transfer:, etc.
|
|
1198
|
+
*/
|
|
1199
|
+
stripContentPrefix(content) {
|
|
1200
|
+
const prefixes = [
|
|
1201
|
+
"payment_request:",
|
|
1202
|
+
"token_transfer:",
|
|
1203
|
+
"payment_response:"
|
|
1204
|
+
];
|
|
1205
|
+
for (const prefix of prefixes) {
|
|
1206
|
+
if (content.startsWith(prefix)) {
|
|
1207
|
+
return content.slice(prefix.length);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return content;
|
|
1211
|
+
}
|
|
1212
|
+
// ===========================================================================
|
|
1213
|
+
// Private: Helpers
|
|
1214
|
+
// ===========================================================================
|
|
1215
|
+
ensureReady() {
|
|
1216
|
+
if (!this.isConnected()) {
|
|
1217
|
+
throw new Error("NostrTransportProvider not connected");
|
|
1218
|
+
}
|
|
1219
|
+
if (!this.identity) {
|
|
1220
|
+
throw new Error("Identity not set");
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
emitEvent(event) {
|
|
1224
|
+
for (const callback of this.eventCallbacks) {
|
|
1225
|
+
try {
|
|
1226
|
+
callback(event);
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
this.log("Event callback error:", error);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
log(...args) {
|
|
1233
|
+
if (this.config.debug) {
|
|
1234
|
+
console.log("[NostrTransportProvider]", ...args);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
// impl/nodejs/transport/index.ts
|
|
1240
|
+
function createNodeWebSocketFactory() {
|
|
1241
|
+
return (url) => {
|
|
1242
|
+
return new import_ws.default(url);
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function createNostrTransportProvider(config) {
|
|
1246
|
+
return new NostrTransportProvider({
|
|
1247
|
+
...config,
|
|
1248
|
+
createWebSocket: createNodeWebSocketFactory()
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// impl/nodejs/oracle/index.ts
|
|
1253
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
1254
|
+
|
|
1255
|
+
// oracle/UnicityAggregatorProvider.ts
|
|
1256
|
+
var import_StateTransitionClient = require("@unicitylabs/state-transition-sdk/lib/StateTransitionClient");
|
|
1257
|
+
var import_AggregatorClient = require("@unicitylabs/state-transition-sdk/lib/api/AggregatorClient");
|
|
1258
|
+
var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/bft/RootTrustBase");
|
|
1259
|
+
var import_Token = require("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
1260
|
+
var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
1261
|
+
var UnicityAggregatorProvider = class {
|
|
1262
|
+
id = "unicity-aggregator";
|
|
1263
|
+
name = "Unicity Aggregator";
|
|
1264
|
+
type = "network";
|
|
1265
|
+
description = "Unicity state transition aggregator (oracle implementation)";
|
|
1266
|
+
config;
|
|
1267
|
+
status = "disconnected";
|
|
1268
|
+
eventCallbacks = /* @__PURE__ */ new Set();
|
|
1269
|
+
// SDK clients
|
|
1270
|
+
aggregatorClient = null;
|
|
1271
|
+
stateTransitionClient = null;
|
|
1272
|
+
trustBase = null;
|
|
1273
|
+
/** Get the current trust base */
|
|
1274
|
+
getTrustBase() {
|
|
1275
|
+
return this.trustBase;
|
|
1276
|
+
}
|
|
1277
|
+
/** Get the state transition client */
|
|
1278
|
+
getStateTransitionClient() {
|
|
1279
|
+
return this.stateTransitionClient;
|
|
1280
|
+
}
|
|
1281
|
+
/** Get the aggregator client */
|
|
1282
|
+
getAggregatorClient() {
|
|
1283
|
+
return this.aggregatorClient;
|
|
1284
|
+
}
|
|
1285
|
+
// Cache for spent states (immutable)
|
|
1286
|
+
spentCache = /* @__PURE__ */ new Map();
|
|
1287
|
+
constructor(config) {
|
|
1288
|
+
this.config = {
|
|
1289
|
+
url: config.url,
|
|
1290
|
+
apiKey: config.apiKey ?? "",
|
|
1291
|
+
timeout: config.timeout ?? DEFAULT_AGGREGATOR_TIMEOUT,
|
|
1292
|
+
skipVerification: config.skipVerification ?? false,
|
|
1293
|
+
debug: config.debug ?? false,
|
|
1294
|
+
trustBaseLoader: config.trustBaseLoader
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
// ===========================================================================
|
|
1298
|
+
// BaseProvider Implementation
|
|
1299
|
+
// ===========================================================================
|
|
1300
|
+
async connect() {
|
|
1301
|
+
if (this.status === "connected") return;
|
|
1302
|
+
this.status = "connecting";
|
|
1303
|
+
this.status = "connected";
|
|
1304
|
+
this.emitEvent({ type: "oracle:connected", timestamp: Date.now() });
|
|
1305
|
+
this.log("Connected to oracle:", this.config.url);
|
|
1306
|
+
}
|
|
1307
|
+
async disconnect() {
|
|
1308
|
+
this.status = "disconnected";
|
|
1309
|
+
this.emitEvent({ type: "oracle:disconnected", timestamp: Date.now() });
|
|
1310
|
+
this.log("Disconnected from oracle");
|
|
1311
|
+
}
|
|
1312
|
+
isConnected() {
|
|
1313
|
+
return this.status === "connected";
|
|
1314
|
+
}
|
|
1315
|
+
getStatus() {
|
|
1316
|
+
return this.status;
|
|
1317
|
+
}
|
|
1318
|
+
// ===========================================================================
|
|
1319
|
+
// OracleProvider Implementation
|
|
1320
|
+
// ===========================================================================
|
|
1321
|
+
async initialize(trustBase) {
|
|
1322
|
+
this.aggregatorClient = new import_AggregatorClient.AggregatorClient(
|
|
1323
|
+
this.config.url,
|
|
1324
|
+
this.config.apiKey || null
|
|
1325
|
+
);
|
|
1326
|
+
this.stateTransitionClient = new import_StateTransitionClient.StateTransitionClient(this.aggregatorClient);
|
|
1327
|
+
if (trustBase) {
|
|
1328
|
+
this.trustBase = trustBase;
|
|
1329
|
+
} else if (!this.config.skipVerification && this.config.trustBaseLoader) {
|
|
1330
|
+
try {
|
|
1331
|
+
const trustBaseJson = await this.config.trustBaseLoader.load();
|
|
1332
|
+
if (trustBaseJson) {
|
|
1333
|
+
this.trustBase = import_RootTrustBase.RootTrustBase.fromJSON(trustBaseJson);
|
|
1334
|
+
}
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
this.log("Failed to load trust base:", error);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
await this.connect();
|
|
1340
|
+
this.log("Initialized with trust base:", !!this.trustBase);
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Submit a transfer commitment to the aggregator.
|
|
1344
|
+
* Accepts either an SDK TransferCommitment or a simple commitment object.
|
|
1345
|
+
*/
|
|
1346
|
+
async submitCommitment(commitment) {
|
|
1347
|
+
this.ensureConnected();
|
|
1348
|
+
try {
|
|
1349
|
+
let requestId;
|
|
1350
|
+
if (this.isSdkTransferCommitment(commitment)) {
|
|
1351
|
+
const response = await this.stateTransitionClient.submitTransferCommitment(commitment);
|
|
1352
|
+
requestId = commitment.requestId?.toString() ?? response.status;
|
|
1353
|
+
} else {
|
|
1354
|
+
const response = await this.rpcCall("submitCommitment", {
|
|
1355
|
+
sourceToken: commitment.sourceToken,
|
|
1356
|
+
recipient: commitment.recipient,
|
|
1357
|
+
salt: Array.from(commitment.salt),
|
|
1358
|
+
data: commitment.data
|
|
1359
|
+
});
|
|
1360
|
+
requestId = response.requestId ?? "";
|
|
1361
|
+
}
|
|
1362
|
+
this.emitEvent({
|
|
1363
|
+
type: "commitment:submitted",
|
|
1364
|
+
timestamp: Date.now(),
|
|
1365
|
+
data: { requestId }
|
|
1366
|
+
});
|
|
1367
|
+
return {
|
|
1368
|
+
success: true,
|
|
1369
|
+
requestId,
|
|
1370
|
+
timestamp: Date.now()
|
|
1371
|
+
};
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1374
|
+
return {
|
|
1375
|
+
success: false,
|
|
1376
|
+
error: errorMsg,
|
|
1377
|
+
timestamp: Date.now()
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Submit a mint commitment to the aggregator (SDK only)
|
|
1383
|
+
* @param commitment - SDK MintCommitment instance
|
|
1384
|
+
*/
|
|
1385
|
+
async submitMintCommitment(commitment) {
|
|
1386
|
+
this.ensureConnected();
|
|
1387
|
+
try {
|
|
1388
|
+
const response = await this.stateTransitionClient.submitMintCommitment(commitment);
|
|
1389
|
+
const requestId = commitment.requestId?.toString() ?? response.status;
|
|
1390
|
+
this.emitEvent({
|
|
1391
|
+
type: "commitment:submitted",
|
|
1392
|
+
timestamp: Date.now(),
|
|
1393
|
+
data: { requestId }
|
|
1394
|
+
});
|
|
1395
|
+
return {
|
|
1396
|
+
success: true,
|
|
1397
|
+
requestId,
|
|
1398
|
+
timestamp: Date.now()
|
|
1399
|
+
};
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1402
|
+
return {
|
|
1403
|
+
success: false,
|
|
1404
|
+
error: errorMsg,
|
|
1405
|
+
timestamp: Date.now()
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
isSdkTransferCommitment(commitment) {
|
|
1410
|
+
return commitment !== null && typeof commitment === "object" && "requestId" in commitment && typeof commitment.requestId?.toString === "function";
|
|
1411
|
+
}
|
|
1412
|
+
async getProof(requestId) {
|
|
1413
|
+
this.ensureConnected();
|
|
1414
|
+
try {
|
|
1415
|
+
const response = await this.rpcCall("getInclusionProof", { requestId });
|
|
1416
|
+
if (!response.proof) {
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
return {
|
|
1420
|
+
requestId,
|
|
1421
|
+
roundNumber: response.roundNumber ?? 0,
|
|
1422
|
+
proof: response.proof,
|
|
1423
|
+
timestamp: Date.now()
|
|
1424
|
+
};
|
|
1425
|
+
} catch {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async waitForProof(requestId, options) {
|
|
1430
|
+
const timeout = options?.timeout ?? this.config.timeout;
|
|
1431
|
+
const pollInterval = options?.pollInterval ?? TIMEOUTS.PROOF_POLL_INTERVAL;
|
|
1432
|
+
const startTime = Date.now();
|
|
1433
|
+
let attempt = 0;
|
|
1434
|
+
while (Date.now() - startTime < timeout) {
|
|
1435
|
+
options?.onPoll?.(++attempt);
|
|
1436
|
+
const proof = await this.getProof(requestId);
|
|
1437
|
+
if (proof) {
|
|
1438
|
+
this.emitEvent({
|
|
1439
|
+
type: "proof:received",
|
|
1440
|
+
timestamp: Date.now(),
|
|
1441
|
+
data: { requestId, roundNumber: proof.roundNumber }
|
|
1442
|
+
});
|
|
1443
|
+
return proof;
|
|
1444
|
+
}
|
|
1445
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
1446
|
+
}
|
|
1447
|
+
throw new Error(`Timeout waiting for proof: ${requestId}`);
|
|
1448
|
+
}
|
|
1449
|
+
async validateToken(tokenData) {
|
|
1450
|
+
this.ensureConnected();
|
|
1451
|
+
try {
|
|
1452
|
+
if (this.trustBase && !this.config.skipVerification) {
|
|
1453
|
+
try {
|
|
1454
|
+
const sdkToken = await import_Token.Token.fromJSON(tokenData);
|
|
1455
|
+
const verifyResult = await sdkToken.verify(this.trustBase);
|
|
1456
|
+
const stateHash = await sdkToken.state.calculateHash();
|
|
1457
|
+
const stateHashStr = stateHash.toJSON();
|
|
1458
|
+
const valid2 = verifyResult.isSuccessful;
|
|
1459
|
+
this.emitEvent({
|
|
1460
|
+
type: "validation:completed",
|
|
1461
|
+
timestamp: Date.now(),
|
|
1462
|
+
data: { valid: valid2 }
|
|
1463
|
+
});
|
|
1464
|
+
return {
|
|
1465
|
+
valid: valid2,
|
|
1466
|
+
spent: false,
|
|
1467
|
+
// Spend check is separate
|
|
1468
|
+
stateHash: stateHashStr,
|
|
1469
|
+
error: valid2 ? void 0 : "SDK verification failed"
|
|
1470
|
+
};
|
|
1471
|
+
} catch (sdkError) {
|
|
1472
|
+
this.log("SDK validation failed, falling back to RPC:", sdkError);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
const response = await this.rpcCall("validateToken", { token: tokenData });
|
|
1476
|
+
const valid = response.valid ?? false;
|
|
1477
|
+
const spent = response.spent ?? false;
|
|
1478
|
+
this.emitEvent({
|
|
1479
|
+
type: "validation:completed",
|
|
1480
|
+
timestamp: Date.now(),
|
|
1481
|
+
data: { valid }
|
|
1482
|
+
});
|
|
1483
|
+
if (response.stateHash && spent) {
|
|
1484
|
+
this.spentCache.set(response.stateHash, true);
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
valid,
|
|
1488
|
+
spent,
|
|
1489
|
+
stateHash: response.stateHash,
|
|
1490
|
+
error: response.error
|
|
1491
|
+
};
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
return {
|
|
1494
|
+
valid: false,
|
|
1495
|
+
spent: false,
|
|
1496
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Wait for inclusion proof using SDK (for SDK commitments)
|
|
1502
|
+
*/
|
|
1503
|
+
async waitForProofSdk(commitment, signal) {
|
|
1504
|
+
this.ensureConnected();
|
|
1505
|
+
if (!this.trustBase) {
|
|
1506
|
+
throw new Error("Trust base not initialized");
|
|
1507
|
+
}
|
|
1508
|
+
return await (0, import_InclusionProofUtils.waitInclusionProof)(
|
|
1509
|
+
this.trustBase,
|
|
1510
|
+
this.stateTransitionClient,
|
|
1511
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1512
|
+
commitment,
|
|
1513
|
+
signal
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
async isSpent(stateHash) {
|
|
1517
|
+
if (this.spentCache.has(stateHash)) {
|
|
1518
|
+
return this.spentCache.get(stateHash);
|
|
1519
|
+
}
|
|
1520
|
+
this.ensureConnected();
|
|
1521
|
+
try {
|
|
1522
|
+
const response = await this.rpcCall("isSpent", { stateHash });
|
|
1523
|
+
const spent = response.spent ?? false;
|
|
1524
|
+
if (spent) {
|
|
1525
|
+
this.spentCache.set(stateHash, true);
|
|
1526
|
+
}
|
|
1527
|
+
return spent;
|
|
1528
|
+
} catch {
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
async getTokenState(tokenId) {
|
|
1533
|
+
this.ensureConnected();
|
|
1534
|
+
try {
|
|
1535
|
+
const response = await this.rpcCall("getTokenState", { tokenId });
|
|
1536
|
+
if (!response.state) {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
return {
|
|
1540
|
+
tokenId,
|
|
1541
|
+
stateHash: response.state.stateHash ?? "",
|
|
1542
|
+
spent: response.state.spent ?? false,
|
|
1543
|
+
roundNumber: response.state.roundNumber,
|
|
1544
|
+
lastUpdated: Date.now()
|
|
1545
|
+
};
|
|
1546
|
+
} catch {
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
async getCurrentRound() {
|
|
1551
|
+
if (this.aggregatorClient) {
|
|
1552
|
+
const blockHeight = await this.aggregatorClient.getBlockHeight();
|
|
1553
|
+
return Number(blockHeight);
|
|
1554
|
+
}
|
|
1555
|
+
return 0;
|
|
1556
|
+
}
|
|
1557
|
+
async mint(params) {
|
|
1558
|
+
this.ensureConnected();
|
|
1559
|
+
try {
|
|
1560
|
+
const response = await this.rpcCall("mint", {
|
|
1561
|
+
coinId: params.coinId,
|
|
1562
|
+
amount: params.amount,
|
|
1563
|
+
recipientAddress: params.recipientAddress,
|
|
1564
|
+
recipientPubkey: params.recipientPubkey
|
|
1565
|
+
});
|
|
1566
|
+
return {
|
|
1567
|
+
success: true,
|
|
1568
|
+
requestId: response.requestId,
|
|
1569
|
+
tokenId: response.tokenId
|
|
1570
|
+
};
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
return {
|
|
1573
|
+
success: false,
|
|
1574
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
// ===========================================================================
|
|
1579
|
+
// Event Subscription
|
|
1580
|
+
// ===========================================================================
|
|
1581
|
+
onEvent(callback) {
|
|
1582
|
+
this.eventCallbacks.add(callback);
|
|
1583
|
+
return () => this.eventCallbacks.delete(callback);
|
|
1584
|
+
}
|
|
1585
|
+
// ===========================================================================
|
|
1586
|
+
// Private: RPC
|
|
1587
|
+
// ===========================================================================
|
|
1588
|
+
async rpcCall(method, params) {
|
|
1589
|
+
const controller = new AbortController();
|
|
1590
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
|
1591
|
+
try {
|
|
1592
|
+
const response = await fetch(this.config.url, {
|
|
1593
|
+
method: "POST",
|
|
1594
|
+
headers: {
|
|
1595
|
+
"Content-Type": "application/json"
|
|
1596
|
+
},
|
|
1597
|
+
body: JSON.stringify({
|
|
1598
|
+
jsonrpc: "2.0",
|
|
1599
|
+
id: Date.now(),
|
|
1600
|
+
method,
|
|
1601
|
+
params
|
|
1602
|
+
}),
|
|
1603
|
+
signal: controller.signal
|
|
1604
|
+
});
|
|
1605
|
+
if (!response.ok) {
|
|
1606
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1607
|
+
}
|
|
1608
|
+
const result = await response.json();
|
|
1609
|
+
if (result.error) {
|
|
1610
|
+
throw new Error(result.error.message ?? "RPC error");
|
|
1611
|
+
}
|
|
1612
|
+
return result.result ?? {};
|
|
1613
|
+
} finally {
|
|
1614
|
+
clearTimeout(timeout);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
// ===========================================================================
|
|
1618
|
+
// Private: Helpers
|
|
1619
|
+
// ===========================================================================
|
|
1620
|
+
ensureConnected() {
|
|
1621
|
+
if (this.status !== "connected") {
|
|
1622
|
+
throw new Error("UnicityAggregatorProvider not connected");
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
emitEvent(event) {
|
|
1626
|
+
for (const callback of this.eventCallbacks) {
|
|
1627
|
+
try {
|
|
1628
|
+
callback(event);
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
this.log("Event callback error:", error);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
log(...args) {
|
|
1635
|
+
if (this.config.debug) {
|
|
1636
|
+
console.log("[UnicityAggregatorProvider]", ...args);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
var UnicityOracleProvider = UnicityAggregatorProvider;
|
|
1641
|
+
|
|
1642
|
+
// impl/nodejs/oracle/index.ts
|
|
1643
|
+
var NodeTrustBaseLoader = class {
|
|
1644
|
+
filePath;
|
|
1645
|
+
constructor(filePath = "./trustbase-testnet.json") {
|
|
1646
|
+
this.filePath = filePath;
|
|
1647
|
+
}
|
|
1648
|
+
async load() {
|
|
1649
|
+
try {
|
|
1650
|
+
if (fs3.existsSync(this.filePath)) {
|
|
1651
|
+
const content = fs3.readFileSync(this.filePath, "utf-8");
|
|
1652
|
+
return JSON.parse(content);
|
|
1653
|
+
}
|
|
1654
|
+
} catch {
|
|
1655
|
+
}
|
|
1656
|
+
return null;
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
function createNodeTrustBaseLoader(filePath) {
|
|
1660
|
+
return new NodeTrustBaseLoader(filePath);
|
|
1661
|
+
}
|
|
1662
|
+
function createUnicityAggregatorProvider(config) {
|
|
1663
|
+
const { trustBasePath, ...restConfig } = config;
|
|
1664
|
+
return new UnicityAggregatorProvider({
|
|
1665
|
+
...restConfig,
|
|
1666
|
+
trustBaseLoader: createNodeTrustBaseLoader(trustBasePath)
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
var createUnicityOracleProvider = createUnicityAggregatorProvider;
|
|
1670
|
+
|
|
1671
|
+
// impl/shared/resolvers.ts
|
|
1672
|
+
function getNetworkConfig(network = "mainnet") {
|
|
1673
|
+
return NETWORKS[network];
|
|
1674
|
+
}
|
|
1675
|
+
function resolveTransportConfig(network, config) {
|
|
1676
|
+
const networkConfig = getNetworkConfig(network);
|
|
1677
|
+
let relays;
|
|
1678
|
+
if (config?.relays) {
|
|
1679
|
+
relays = config.relays;
|
|
1680
|
+
} else {
|
|
1681
|
+
relays = [...networkConfig.nostrRelays];
|
|
1682
|
+
if (config?.additionalRelays) {
|
|
1683
|
+
relays = [...relays, ...config.additionalRelays];
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return {
|
|
1687
|
+
relays,
|
|
1688
|
+
timeout: config?.timeout,
|
|
1689
|
+
autoReconnect: config?.autoReconnect,
|
|
1690
|
+
debug: config?.debug,
|
|
1691
|
+
// Browser-specific
|
|
1692
|
+
reconnectDelay: config?.reconnectDelay,
|
|
1693
|
+
maxReconnectAttempts: config?.maxReconnectAttempts
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function resolveOracleConfig(network, config) {
|
|
1697
|
+
const networkConfig = getNetworkConfig(network);
|
|
1698
|
+
return {
|
|
1699
|
+
url: config?.url ?? networkConfig.aggregatorUrl,
|
|
1700
|
+
apiKey: config?.apiKey,
|
|
1701
|
+
timeout: config?.timeout,
|
|
1702
|
+
skipVerification: config?.skipVerification,
|
|
1703
|
+
debug: config?.debug,
|
|
1704
|
+
// Node.js-specific
|
|
1705
|
+
trustBasePath: config?.trustBasePath
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function resolveL1Config(network, config) {
|
|
1709
|
+
if (config === void 0) {
|
|
1710
|
+
return void 0;
|
|
1711
|
+
}
|
|
1712
|
+
const networkConfig = getNetworkConfig(network);
|
|
1713
|
+
return {
|
|
1714
|
+
electrumUrl: config.electrumUrl ?? networkConfig.electrumUrl,
|
|
1715
|
+
defaultFeeRate: config.defaultFeeRate,
|
|
1716
|
+
enableVesting: config.enableVesting
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// impl/nodejs/index.ts
|
|
1721
|
+
function createNodeProviders(config) {
|
|
1722
|
+
const network = config?.network ?? "mainnet";
|
|
1723
|
+
const transportConfig = resolveTransportConfig(network, config?.transport);
|
|
1724
|
+
const oracleConfig = resolveOracleConfig(network, config?.oracle);
|
|
1725
|
+
const l1Config = resolveL1Config(network, config?.l1);
|
|
1726
|
+
return {
|
|
1727
|
+
storage: createFileStorageProvider({
|
|
1728
|
+
dataDir: config?.dataDir ?? "./sphere-data"
|
|
1729
|
+
}),
|
|
1730
|
+
tokenStorage: createFileTokenStorageProvider({
|
|
1731
|
+
tokensDir: config?.tokensDir ?? "./sphere-tokens"
|
|
1732
|
+
}),
|
|
1733
|
+
transport: createNostrTransportProvider({
|
|
1734
|
+
relays: transportConfig.relays,
|
|
1735
|
+
timeout: transportConfig.timeout,
|
|
1736
|
+
autoReconnect: transportConfig.autoReconnect,
|
|
1737
|
+
debug: transportConfig.debug
|
|
1738
|
+
}),
|
|
1739
|
+
oracle: createUnicityAggregatorProvider({
|
|
1740
|
+
url: oracleConfig.url,
|
|
1741
|
+
apiKey: oracleConfig.apiKey,
|
|
1742
|
+
timeout: oracleConfig.timeout,
|
|
1743
|
+
trustBasePath: oracleConfig.trustBasePath,
|
|
1744
|
+
skipVerification: oracleConfig.skipVerification,
|
|
1745
|
+
debug: oracleConfig.debug
|
|
1746
|
+
}),
|
|
1747
|
+
l1: l1Config
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1751
|
+
0 && (module.exports = {
|
|
1752
|
+
FileStorageProvider,
|
|
1753
|
+
FileTokenStorageProvider,
|
|
1754
|
+
NodeTrustBaseLoader,
|
|
1755
|
+
NostrTransportProvider,
|
|
1756
|
+
UnicityAggregatorProvider,
|
|
1757
|
+
UnicityOracleProvider,
|
|
1758
|
+
createFileStorageProvider,
|
|
1759
|
+
createFileTokenStorageProvider,
|
|
1760
|
+
createNodeProviders,
|
|
1761
|
+
createNodeTrustBaseLoader,
|
|
1762
|
+
createNodeWebSocketFactory,
|
|
1763
|
+
createNostrTransportProvider,
|
|
1764
|
+
createUnicityAggregatorProvider,
|
|
1765
|
+
createUnicityOracleProvider
|
|
1766
|
+
});
|
|
1767
|
+
//# sourceMappingURL=index.cjs.map
|