@unicitylabs/sphere-sdk 0.3.8 → 0.4.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/dist/connect/index.cjs +770 -0
- package/dist/connect/index.cjs.map +1 -0
- package/dist/connect/index.d.cts +312 -0
- package/dist/connect/index.d.ts +312 -0
- package/dist/connect/index.js +747 -0
- package/dist/connect/index.js.map +1 -0
- package/dist/core/index.cjs +2744 -56
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +277 -3
- package/dist/core/index.d.ts +277 -3
- package/dist/core/index.js +2740 -52
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +271 -0
- package/dist/impl/browser/connect/index.cjs.map +1 -0
- package/dist/impl/browser/connect/index.d.cts +137 -0
- package/dist/impl/browser/connect/index.d.ts +137 -0
- package/dist/impl/browser/connect/index.js +248 -0
- package/dist/impl/browser/connect/index.js.map +1 -0
- package/dist/impl/browser/index.cjs +583 -45
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +587 -46
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +372 -0
- package/dist/impl/nodejs/connect/index.cjs.map +1 -0
- package/dist/impl/nodejs/connect/index.d.cts +178 -0
- package/dist/impl/nodejs/connect/index.d.ts +178 -0
- package/dist/impl/nodejs/connect/index.js +333 -0
- package/dist/impl/nodejs/connect/index.js.map +1 -0
- package/dist/impl/nodejs/index.cjs +266 -12
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +96 -0
- package/dist/impl/nodejs/index.d.ts +96 -0
- package/dist/impl/nodejs/index.js +270 -13
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +2761 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +375 -5
- package/dist/index.d.ts +375 -5
- package/dist/index.js +2750 -52
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs +5 -1
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.d.cts +2 -1
- package/dist/l1/index.d.ts +2 -1
- package/dist/l1/index.js +5 -1
- package/dist/l1/index.js.map +1 -1
- package/package.json +31 -1
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
// constants.ts
|
|
2
|
+
var STORAGE_KEYS_GLOBAL = {
|
|
3
|
+
/** Encrypted BIP39 mnemonic */
|
|
4
|
+
MNEMONIC: "mnemonic",
|
|
5
|
+
/** Encrypted master private key */
|
|
6
|
+
MASTER_KEY: "master_key",
|
|
7
|
+
/** BIP32 chain code */
|
|
8
|
+
CHAIN_CODE: "chain_code",
|
|
9
|
+
/** HD derivation path (full path like m/44'/0'/0'/0/0) */
|
|
10
|
+
DERIVATION_PATH: "derivation_path",
|
|
11
|
+
/** Base derivation path (like m/44'/0'/0' without chain/index) */
|
|
12
|
+
BASE_PATH: "base_path",
|
|
13
|
+
/** Derivation mode: bip32, wif_hmac, legacy_hmac */
|
|
14
|
+
DERIVATION_MODE: "derivation_mode",
|
|
15
|
+
/** Wallet source: mnemonic, file, unknown */
|
|
16
|
+
WALLET_SOURCE: "wallet_source",
|
|
17
|
+
/** Wallet existence flag */
|
|
18
|
+
WALLET_EXISTS: "wallet_exists",
|
|
19
|
+
/** Current active address index */
|
|
20
|
+
CURRENT_ADDRESS_INDEX: "current_address_index",
|
|
21
|
+
/** Nametag cache per address (separate from tracked addresses registry) */
|
|
22
|
+
ADDRESS_NAMETAGS: "address_nametags",
|
|
23
|
+
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
24
|
+
TRACKED_ADDRESSES: "tracked_addresses",
|
|
25
|
+
/** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
|
|
26
|
+
LAST_WALLET_EVENT_TS: "last_wallet_event_ts",
|
|
27
|
+
/** Group chat: joined groups */
|
|
28
|
+
GROUP_CHAT_GROUPS: "group_chat_groups",
|
|
29
|
+
/** Group chat: messages */
|
|
30
|
+
GROUP_CHAT_MESSAGES: "group_chat_messages",
|
|
31
|
+
/** Group chat: members */
|
|
32
|
+
GROUP_CHAT_MEMBERS: "group_chat_members",
|
|
33
|
+
/** Group chat: processed event IDs for deduplication */
|
|
34
|
+
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
35
|
+
/** Group chat: last used relay URL (stale data detection) */
|
|
36
|
+
GROUP_CHAT_RELAY_URL: "group_chat_relay_url",
|
|
37
|
+
/** Cached token registry JSON (fetched from remote) */
|
|
38
|
+
TOKEN_REGISTRY_CACHE: "token_registry_cache",
|
|
39
|
+
/** Timestamp of last token registry cache update (ms since epoch) */
|
|
40
|
+
TOKEN_REGISTRY_CACHE_TS: "token_registry_cache_ts",
|
|
41
|
+
/** Cached price data JSON (from CoinGecko or other provider) */
|
|
42
|
+
PRICE_CACHE: "price_cache",
|
|
43
|
+
/** Timestamp of last price cache update (ms since epoch) */
|
|
44
|
+
PRICE_CACHE_TS: "price_cache_ts"
|
|
45
|
+
};
|
|
46
|
+
var STORAGE_KEYS_ADDRESS = {
|
|
47
|
+
/** Pending transfers for this address */
|
|
48
|
+
PENDING_TRANSFERS: "pending_transfers",
|
|
49
|
+
/** Transfer outbox for this address */
|
|
50
|
+
OUTBOX: "outbox",
|
|
51
|
+
/** Conversations for this address */
|
|
52
|
+
CONVERSATIONS: "conversations",
|
|
53
|
+
/** Messages for this address */
|
|
54
|
+
MESSAGES: "messages",
|
|
55
|
+
/** Transaction history for this address */
|
|
56
|
+
TRANSACTION_HISTORY: "transaction_history",
|
|
57
|
+
/** Pending V5 finalization tokens (unconfirmed instant split tokens) */
|
|
58
|
+
PENDING_V5_TOKENS: "pending_v5_tokens"
|
|
59
|
+
};
|
|
60
|
+
var STORAGE_KEYS = {
|
|
61
|
+
...STORAGE_KEYS_GLOBAL,
|
|
62
|
+
...STORAGE_KEYS_ADDRESS
|
|
63
|
+
};
|
|
64
|
+
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
65
|
+
var DEFAULT_DERIVATION_PATH = `${DEFAULT_BASE_PATH}/0/0`;
|
|
66
|
+
var HOST_READY_TYPE = "sphere-connect:host-ready";
|
|
67
|
+
var HOST_READY_TIMEOUT = 3e4;
|
|
68
|
+
|
|
69
|
+
// connect/protocol.ts
|
|
70
|
+
var SPHERE_CONNECT_NAMESPACE = "sphere-connect";
|
|
71
|
+
var SPHERE_CONNECT_VERSION = "1.0";
|
|
72
|
+
var RPC_METHODS = {
|
|
73
|
+
GET_IDENTITY: "sphere_getIdentity",
|
|
74
|
+
GET_BALANCE: "sphere_getBalance",
|
|
75
|
+
GET_ASSETS: "sphere_getAssets",
|
|
76
|
+
GET_FIAT_BALANCE: "sphere_getFiatBalance",
|
|
77
|
+
GET_TOKENS: "sphere_getTokens",
|
|
78
|
+
GET_HISTORY: "sphere_getHistory",
|
|
79
|
+
L1_GET_BALANCE: "sphere_l1GetBalance",
|
|
80
|
+
L1_GET_HISTORY: "sphere_l1GetHistory",
|
|
81
|
+
RESOLVE: "sphere_resolve",
|
|
82
|
+
SUBSCRIBE: "sphere_subscribe",
|
|
83
|
+
UNSUBSCRIBE: "sphere_unsubscribe",
|
|
84
|
+
DISCONNECT: "sphere_disconnect"
|
|
85
|
+
};
|
|
86
|
+
var INTENT_ACTIONS = {
|
|
87
|
+
SEND: "send",
|
|
88
|
+
L1_SEND: "l1_send",
|
|
89
|
+
DM: "dm",
|
|
90
|
+
PAYMENT_REQUEST: "payment_request",
|
|
91
|
+
RECEIVE: "receive",
|
|
92
|
+
SIGN_MESSAGE: "sign_message"
|
|
93
|
+
};
|
|
94
|
+
var ERROR_CODES = {
|
|
95
|
+
// Standard JSON-RPC
|
|
96
|
+
PARSE_ERROR: -32700,
|
|
97
|
+
INVALID_REQUEST: -32600,
|
|
98
|
+
METHOD_NOT_FOUND: -32601,
|
|
99
|
+
INVALID_PARAMS: -32602,
|
|
100
|
+
INTERNAL_ERROR: -32603,
|
|
101
|
+
// Sphere Connect (4xxx)
|
|
102
|
+
NOT_CONNECTED: 4001,
|
|
103
|
+
PERMISSION_DENIED: 4002,
|
|
104
|
+
USER_REJECTED: 4003,
|
|
105
|
+
SESSION_EXPIRED: 4004,
|
|
106
|
+
ORIGIN_BLOCKED: 4005,
|
|
107
|
+
RATE_LIMITED: 4006,
|
|
108
|
+
INSUFFICIENT_BALANCE: 4100,
|
|
109
|
+
INVALID_RECIPIENT: 4101,
|
|
110
|
+
TRANSFER_FAILED: 4102,
|
|
111
|
+
INTENT_CANCELLED: 4200
|
|
112
|
+
};
|
|
113
|
+
function isSphereConnectMessage(msg) {
|
|
114
|
+
if (!msg || typeof msg !== "object") return false;
|
|
115
|
+
const m = msg;
|
|
116
|
+
return m.ns === SPHERE_CONNECT_NAMESPACE && m.v === SPHERE_CONNECT_VERSION;
|
|
117
|
+
}
|
|
118
|
+
function createRequestId() {
|
|
119
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
120
|
+
return crypto.randomUUID();
|
|
121
|
+
}
|
|
122
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// connect/permissions.ts
|
|
126
|
+
var PERMISSION_SCOPES = {
|
|
127
|
+
IDENTITY_READ: "identity:read",
|
|
128
|
+
BALANCE_READ: "balance:read",
|
|
129
|
+
TOKENS_READ: "tokens:read",
|
|
130
|
+
HISTORY_READ: "history:read",
|
|
131
|
+
L1_READ: "l1:read",
|
|
132
|
+
EVENTS_SUBSCRIBE: "events:subscribe",
|
|
133
|
+
RESOLVE_PEER: "resolve:peer",
|
|
134
|
+
TRANSFER_REQUEST: "transfer:request",
|
|
135
|
+
L1_TRANSFER: "l1:transfer",
|
|
136
|
+
DM_REQUEST: "dm:request",
|
|
137
|
+
PAYMENT_REQUEST: "payment:request",
|
|
138
|
+
SIGN_REQUEST: "sign:request"
|
|
139
|
+
};
|
|
140
|
+
var ALL_PERMISSIONS = Object.values(PERMISSION_SCOPES);
|
|
141
|
+
var DEFAULT_PERMISSIONS = [
|
|
142
|
+
PERMISSION_SCOPES.IDENTITY_READ
|
|
143
|
+
];
|
|
144
|
+
var METHOD_PERMISSIONS = {
|
|
145
|
+
[RPC_METHODS.GET_IDENTITY]: PERMISSION_SCOPES.IDENTITY_READ,
|
|
146
|
+
[RPC_METHODS.GET_BALANCE]: PERMISSION_SCOPES.BALANCE_READ,
|
|
147
|
+
[RPC_METHODS.GET_ASSETS]: PERMISSION_SCOPES.BALANCE_READ,
|
|
148
|
+
[RPC_METHODS.GET_FIAT_BALANCE]: PERMISSION_SCOPES.BALANCE_READ,
|
|
149
|
+
[RPC_METHODS.GET_TOKENS]: PERMISSION_SCOPES.TOKENS_READ,
|
|
150
|
+
[RPC_METHODS.GET_HISTORY]: PERMISSION_SCOPES.HISTORY_READ,
|
|
151
|
+
[RPC_METHODS.L1_GET_BALANCE]: PERMISSION_SCOPES.L1_READ,
|
|
152
|
+
[RPC_METHODS.L1_GET_HISTORY]: PERMISSION_SCOPES.L1_READ,
|
|
153
|
+
[RPC_METHODS.RESOLVE]: PERMISSION_SCOPES.RESOLVE_PEER,
|
|
154
|
+
[RPC_METHODS.SUBSCRIBE]: PERMISSION_SCOPES.EVENTS_SUBSCRIBE,
|
|
155
|
+
[RPC_METHODS.UNSUBSCRIBE]: PERMISSION_SCOPES.EVENTS_SUBSCRIBE
|
|
156
|
+
};
|
|
157
|
+
var INTENT_PERMISSIONS = {
|
|
158
|
+
[INTENT_ACTIONS.SEND]: PERMISSION_SCOPES.TRANSFER_REQUEST,
|
|
159
|
+
[INTENT_ACTIONS.L1_SEND]: PERMISSION_SCOPES.L1_TRANSFER,
|
|
160
|
+
[INTENT_ACTIONS.DM]: PERMISSION_SCOPES.DM_REQUEST,
|
|
161
|
+
[INTENT_ACTIONS.PAYMENT_REQUEST]: PERMISSION_SCOPES.PAYMENT_REQUEST,
|
|
162
|
+
[INTENT_ACTIONS.RECEIVE]: PERMISSION_SCOPES.IDENTITY_READ,
|
|
163
|
+
[INTENT_ACTIONS.SIGN_MESSAGE]: PERMISSION_SCOPES.SIGN_REQUEST
|
|
164
|
+
};
|
|
165
|
+
function hasMethodPermission(granted, method) {
|
|
166
|
+
const required = METHOD_PERMISSIONS[method];
|
|
167
|
+
if (!required) return false;
|
|
168
|
+
return granted.has(required);
|
|
169
|
+
}
|
|
170
|
+
function hasIntentPermission(granted, action) {
|
|
171
|
+
const required = INTENT_PERMISSIONS[action];
|
|
172
|
+
if (!required) return false;
|
|
173
|
+
return granted.has(required);
|
|
174
|
+
}
|
|
175
|
+
function validatePermissions(permissions) {
|
|
176
|
+
const validScopes = new Set(ALL_PERMISSIONS);
|
|
177
|
+
return permissions.every((p) => validScopes.has(p));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// connect/host/ConnectHost.ts
|
|
181
|
+
var DEFAULT_SESSION_TTL_MS = 864e5;
|
|
182
|
+
var DEFAULT_MAX_RPS = 20;
|
|
183
|
+
var ConnectHost = class {
|
|
184
|
+
sphere;
|
|
185
|
+
transport;
|
|
186
|
+
config;
|
|
187
|
+
session = null;
|
|
188
|
+
grantedPermissions = /* @__PURE__ */ new Set();
|
|
189
|
+
// Event subscription management
|
|
190
|
+
eventSubscriptions = /* @__PURE__ */ new Map();
|
|
191
|
+
// eventName → unsub
|
|
192
|
+
// Rate limiting
|
|
193
|
+
rateLimitCounter = 0;
|
|
194
|
+
rateLimitResetAt = 0;
|
|
195
|
+
unsubscribeTransport = null;
|
|
196
|
+
constructor(config) {
|
|
197
|
+
this.sphere = config.sphere;
|
|
198
|
+
this.transport = config.transport;
|
|
199
|
+
this.config = config;
|
|
200
|
+
this.unsubscribeTransport = this.transport.onMessage(this.handleMessage.bind(this));
|
|
201
|
+
}
|
|
202
|
+
/** Get current active session */
|
|
203
|
+
getSession() {
|
|
204
|
+
return this.session;
|
|
205
|
+
}
|
|
206
|
+
/** Revoke the current session */
|
|
207
|
+
revokeSession() {
|
|
208
|
+
if (this.session) {
|
|
209
|
+
this.session.active = false;
|
|
210
|
+
this.cleanupEventSubscriptions();
|
|
211
|
+
this.session = null;
|
|
212
|
+
this.grantedPermissions.clear();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Destroy the host, clean up all resources */
|
|
216
|
+
destroy() {
|
|
217
|
+
this.revokeSession();
|
|
218
|
+
if (this.unsubscribeTransport) {
|
|
219
|
+
this.unsubscribeTransport();
|
|
220
|
+
this.unsubscribeTransport = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ===========================================================================
|
|
224
|
+
// Message Handling
|
|
225
|
+
// ===========================================================================
|
|
226
|
+
async handleMessage(msg) {
|
|
227
|
+
try {
|
|
228
|
+
if (msg.type === "handshake" && msg.direction === "request") {
|
|
229
|
+
await this.handleHandshake(msg);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (msg.type === "request") {
|
|
233
|
+
await this.handleRpcRequest(msg);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (msg.type === "intent") {
|
|
237
|
+
await this.handleIntentRequest(msg);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.warn("[ConnectHost] Error handling message:", error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ===========================================================================
|
|
245
|
+
// Handshake
|
|
246
|
+
// ===========================================================================
|
|
247
|
+
async handleHandshake(msg) {
|
|
248
|
+
const dapp = msg.dapp;
|
|
249
|
+
if (!dapp) {
|
|
250
|
+
this.sendHandshakeResponse([], void 0, void 0);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const requestedPermissions = msg.permissions;
|
|
254
|
+
const { approved, grantedPermissions } = await this.config.onConnectionRequest(
|
|
255
|
+
dapp,
|
|
256
|
+
requestedPermissions
|
|
257
|
+
);
|
|
258
|
+
if (!approved) {
|
|
259
|
+
this.sendHandshakeResponse([], void 0, void 0);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const sessionId = createRequestId();
|
|
263
|
+
const allPermissions = [.../* @__PURE__ */ new Set([...DEFAULT_PERMISSIONS, ...grantedPermissions])];
|
|
264
|
+
const ttl = this.config.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS;
|
|
265
|
+
this.session = {
|
|
266
|
+
id: sessionId,
|
|
267
|
+
dapp,
|
|
268
|
+
permissions: allPermissions,
|
|
269
|
+
createdAt: Date.now(),
|
|
270
|
+
expiresAt: ttl > 0 ? Date.now() + ttl : 0,
|
|
271
|
+
active: true
|
|
272
|
+
};
|
|
273
|
+
this.grantedPermissions = new Set(allPermissions);
|
|
274
|
+
const identity = this.getPublicIdentity();
|
|
275
|
+
this.sendHandshakeResponse(allPermissions, sessionId, identity);
|
|
276
|
+
}
|
|
277
|
+
sendHandshakeResponse(permissions, sessionId, identity) {
|
|
278
|
+
this.transport.send({
|
|
279
|
+
ns: SPHERE_CONNECT_NAMESPACE,
|
|
280
|
+
v: SPHERE_CONNECT_VERSION,
|
|
281
|
+
type: "handshake",
|
|
282
|
+
direction: "response",
|
|
283
|
+
permissions,
|
|
284
|
+
sessionId,
|
|
285
|
+
identity
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// ===========================================================================
|
|
289
|
+
// RPC Requests (query)
|
|
290
|
+
// ===========================================================================
|
|
291
|
+
async handleRpcRequest(msg) {
|
|
292
|
+
if (!this.session?.active) {
|
|
293
|
+
this.sendError(msg.id, ERROR_CODES.NOT_CONNECTED, "Not connected");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (this.session.expiresAt > 0 && Date.now() > this.session.expiresAt) {
|
|
297
|
+
this.revokeSession();
|
|
298
|
+
this.sendError(msg.id, ERROR_CODES.SESSION_EXPIRED, "Session expired");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!this.checkRateLimit()) {
|
|
302
|
+
this.sendError(msg.id, ERROR_CODES.RATE_LIMITED, "Too many requests");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (msg.method === RPC_METHODS.DISCONNECT) {
|
|
306
|
+
this.revokeSession();
|
|
307
|
+
this.sendResult(msg.id, { disconnected: true });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!hasMethodPermission(this.grantedPermissions, msg.method)) {
|
|
311
|
+
this.sendError(msg.id, ERROR_CODES.PERMISSION_DENIED, `Permission denied for ${msg.method}`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const result = await this.executeMethod(msg.method, msg.params ?? {});
|
|
316
|
+
this.sendResult(msg.id, result);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
this.sendError(msg.id, ERROR_CODES.INTERNAL_ERROR, error.message);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// ===========================================================================
|
|
322
|
+
// Intent Requests
|
|
323
|
+
// ===========================================================================
|
|
324
|
+
async handleIntentRequest(msg) {
|
|
325
|
+
if (!this.session?.active) {
|
|
326
|
+
this.sendIntentError(msg.id, ERROR_CODES.NOT_CONNECTED, "Not connected");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (this.session.expiresAt > 0 && Date.now() > this.session.expiresAt) {
|
|
330
|
+
this.revokeSession();
|
|
331
|
+
this.sendIntentError(msg.id, ERROR_CODES.SESSION_EXPIRED, "Session expired");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (!hasIntentPermission(this.grantedPermissions, msg.action)) {
|
|
335
|
+
this.sendIntentError(msg.id, ERROR_CODES.PERMISSION_DENIED, `Permission denied for intent: ${msg.action}`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const response = await this.config.onIntent(msg.action, msg.params, this.session);
|
|
339
|
+
if (response.error) {
|
|
340
|
+
this.sendIntentError(msg.id, response.error.code, response.error.message);
|
|
341
|
+
} else {
|
|
342
|
+
this.sendIntentResult(msg.id, response.result);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// ===========================================================================
|
|
346
|
+
// Method Router
|
|
347
|
+
// ===========================================================================
|
|
348
|
+
async executeMethod(method, params) {
|
|
349
|
+
switch (method) {
|
|
350
|
+
case RPC_METHODS.GET_IDENTITY:
|
|
351
|
+
return this.getPublicIdentity();
|
|
352
|
+
case RPC_METHODS.GET_BALANCE:
|
|
353
|
+
return this.sphere.payments.getBalance(params.coinId);
|
|
354
|
+
case RPC_METHODS.GET_ASSETS:
|
|
355
|
+
return this.sphere.payments.getAssets(params.coinId);
|
|
356
|
+
case RPC_METHODS.GET_FIAT_BALANCE:
|
|
357
|
+
return { fiatBalance: await this.sphere.payments.getFiatBalance() };
|
|
358
|
+
case RPC_METHODS.GET_TOKENS:
|
|
359
|
+
return this.stripTokenSdkData(
|
|
360
|
+
this.sphere.payments.getTokens(
|
|
361
|
+
params.coinId ? { coinId: params.coinId } : void 0
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
case RPC_METHODS.GET_HISTORY:
|
|
365
|
+
return this.sphere.payments.getHistory();
|
|
366
|
+
case RPC_METHODS.L1_GET_BALANCE:
|
|
367
|
+
if (!this.sphere.payments.l1) {
|
|
368
|
+
throw new Error("L1 module not available");
|
|
369
|
+
}
|
|
370
|
+
return this.sphere.payments.l1.getBalance();
|
|
371
|
+
case RPC_METHODS.L1_GET_HISTORY:
|
|
372
|
+
if (!this.sphere.payments.l1) {
|
|
373
|
+
throw new Error("L1 module not available");
|
|
374
|
+
}
|
|
375
|
+
return this.sphere.payments.l1.getHistory(params.limit);
|
|
376
|
+
case RPC_METHODS.RESOLVE:
|
|
377
|
+
if (!params.identifier) {
|
|
378
|
+
throw new Error("Missing required parameter: identifier");
|
|
379
|
+
}
|
|
380
|
+
return this.sphere.resolve(params.identifier);
|
|
381
|
+
case RPC_METHODS.SUBSCRIBE:
|
|
382
|
+
return this.handleSubscribe(params.event);
|
|
383
|
+
case RPC_METHODS.UNSUBSCRIBE:
|
|
384
|
+
return this.handleUnsubscribe(params.event);
|
|
385
|
+
default:
|
|
386
|
+
throw new Error(`Unknown method: ${method}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// ===========================================================================
|
|
390
|
+
// Event Subscriptions
|
|
391
|
+
// ===========================================================================
|
|
392
|
+
handleSubscribe(eventName) {
|
|
393
|
+
if (!eventName) throw new Error("Missing required parameter: event");
|
|
394
|
+
if (this.eventSubscriptions.has(eventName)) {
|
|
395
|
+
return { subscribed: true, event: eventName };
|
|
396
|
+
}
|
|
397
|
+
const unsub = this.sphere.on(eventName, (data) => {
|
|
398
|
+
this.transport.send({
|
|
399
|
+
ns: SPHERE_CONNECT_NAMESPACE,
|
|
400
|
+
v: SPHERE_CONNECT_VERSION,
|
|
401
|
+
type: "event",
|
|
402
|
+
event: eventName,
|
|
403
|
+
data
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
this.eventSubscriptions.set(eventName, unsub);
|
|
407
|
+
return { subscribed: true, event: eventName };
|
|
408
|
+
}
|
|
409
|
+
handleUnsubscribe(eventName) {
|
|
410
|
+
if (!eventName) throw new Error("Missing required parameter: event");
|
|
411
|
+
const unsub = this.eventSubscriptions.get(eventName);
|
|
412
|
+
if (unsub) {
|
|
413
|
+
unsub();
|
|
414
|
+
this.eventSubscriptions.delete(eventName);
|
|
415
|
+
}
|
|
416
|
+
return { unsubscribed: true, event: eventName };
|
|
417
|
+
}
|
|
418
|
+
cleanupEventSubscriptions() {
|
|
419
|
+
for (const [, unsub] of this.eventSubscriptions) {
|
|
420
|
+
unsub();
|
|
421
|
+
}
|
|
422
|
+
this.eventSubscriptions.clear();
|
|
423
|
+
}
|
|
424
|
+
// ===========================================================================
|
|
425
|
+
// Helpers
|
|
426
|
+
// ===========================================================================
|
|
427
|
+
getPublicIdentity() {
|
|
428
|
+
const id = this.sphere.identity;
|
|
429
|
+
if (!id) return void 0;
|
|
430
|
+
return {
|
|
431
|
+
chainPubkey: id.chainPubkey,
|
|
432
|
+
l1Address: id.l1Address,
|
|
433
|
+
directAddress: id.directAddress,
|
|
434
|
+
nametag: id.nametag
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
stripTokenSdkData(tokens) {
|
|
438
|
+
return tokens.map((t) => {
|
|
439
|
+
const token = t;
|
|
440
|
+
const { sdkData: _sdkData, ...publicFields } = token;
|
|
441
|
+
return publicFields;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
sendResult(id, result) {
|
|
445
|
+
this.transport.send({
|
|
446
|
+
ns: SPHERE_CONNECT_NAMESPACE,
|
|
447
|
+
v: SPHERE_CONNECT_VERSION,
|
|
448
|
+
type: "response",
|
|
449
|
+
id,
|
|
450
|
+
result
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
sendError(id, code, message) {
|
|
454
|
+
this.transport.send({
|
|
455
|
+
ns: SPHERE_CONNECT_NAMESPACE,
|
|
456
|
+
v: SPHERE_CONNECT_VERSION,
|
|
457
|
+
type: "response",
|
|
458
|
+
id,
|
|
459
|
+
error: { code, message }
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
sendIntentResult(id, result) {
|
|
463
|
+
this.transport.send({
|
|
464
|
+
ns: SPHERE_CONNECT_NAMESPACE,
|
|
465
|
+
v: SPHERE_CONNECT_VERSION,
|
|
466
|
+
type: "intent_result",
|
|
467
|
+
id,
|
|
468
|
+
result
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
sendIntentError(id, code, message) {
|
|
472
|
+
this.transport.send({
|
|
473
|
+
ns: SPHERE_CONNECT_NAMESPACE,
|
|
474
|
+
v: SPHERE_CONNECT_VERSION,
|
|
475
|
+
type: "intent_result",
|
|
476
|
+
id,
|
|
477
|
+
error: { code, message }
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
checkRateLimit() {
|
|
481
|
+
const maxRps = this.config.maxRequestsPerSecond ?? DEFAULT_MAX_RPS;
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
if (now > this.rateLimitResetAt) {
|
|
484
|
+
this.rateLimitCounter = 0;
|
|
485
|
+
this.rateLimitResetAt = now + 1e3;
|
|
486
|
+
}
|
|
487
|
+
this.rateLimitCounter++;
|
|
488
|
+
return this.rateLimitCounter <= maxRps;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// connect/client/ConnectClient.ts
|
|
493
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
494
|
+
var DEFAULT_INTENT_TIMEOUT = 12e4;
|
|
495
|
+
var ConnectClient = class {
|
|
496
|
+
transport;
|
|
497
|
+
dapp;
|
|
498
|
+
requestedPermissions;
|
|
499
|
+
timeout;
|
|
500
|
+
intentTimeout;
|
|
501
|
+
sessionId = null;
|
|
502
|
+
grantedPermissions = [];
|
|
503
|
+
identity = null;
|
|
504
|
+
connected = false;
|
|
505
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
506
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
507
|
+
unsubscribeTransport = null;
|
|
508
|
+
// Handshake resolver (one-shot)
|
|
509
|
+
handshakeResolver = null;
|
|
510
|
+
constructor(config) {
|
|
511
|
+
this.transport = config.transport;
|
|
512
|
+
this.dapp = config.dapp;
|
|
513
|
+
this.requestedPermissions = config.permissions ?? [...ALL_PERMISSIONS];
|
|
514
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
515
|
+
this.intentTimeout = config.intentTimeout ?? DEFAULT_INTENT_TIMEOUT;
|
|
516
|
+
}
|
|
517
|
+
// ===========================================================================
|
|
518
|
+
// Connection
|
|
519
|
+
// ===========================================================================
|
|
520
|
+
/** Connect to the wallet. Returns session info and public identity. */
|
|
521
|
+
async connect() {
|
|
522
|
+
this.unsubscribeTransport = this.transport.onMessage(this.handleMessage.bind(this));
|
|
523
|
+
return new Promise((resolve, reject) => {
|
|
524
|
+
const timer = setTimeout(() => {
|
|
525
|
+
this.handshakeResolver = null;
|
|
526
|
+
reject(new Error("Connection timeout"));
|
|
527
|
+
}, this.timeout);
|
|
528
|
+
this.handshakeResolver = { resolve, reject, timer };
|
|
529
|
+
this.transport.send({
|
|
530
|
+
ns: SPHERE_CONNECT_NAMESPACE,
|
|
531
|
+
v: SPHERE_CONNECT_VERSION,
|
|
532
|
+
type: "handshake",
|
|
533
|
+
direction: "request",
|
|
534
|
+
permissions: this.requestedPermissions,
|
|
535
|
+
dapp: this.dapp
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
/** Disconnect from the wallet */
|
|
540
|
+
async disconnect() {
|
|
541
|
+
if (this.connected) {
|
|
542
|
+
try {
|
|
543
|
+
await this.query(RPC_METHODS.DISCONNECT);
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
this.cleanup();
|
|
548
|
+
}
|
|
549
|
+
/** Whether currently connected */
|
|
550
|
+
get isConnected() {
|
|
551
|
+
return this.connected;
|
|
552
|
+
}
|
|
553
|
+
/** Granted permission scopes */
|
|
554
|
+
get permissions() {
|
|
555
|
+
return this.grantedPermissions;
|
|
556
|
+
}
|
|
557
|
+
/** Current session ID */
|
|
558
|
+
get session() {
|
|
559
|
+
return this.sessionId;
|
|
560
|
+
}
|
|
561
|
+
/** Public identity received during handshake */
|
|
562
|
+
get walletIdentity() {
|
|
563
|
+
return this.identity;
|
|
564
|
+
}
|
|
565
|
+
// ===========================================================================
|
|
566
|
+
// Query (read data)
|
|
567
|
+
// ===========================================================================
|
|
568
|
+
/** Send a query request and return the result */
|
|
569
|
+
async query(method, params) {
|
|
570
|
+
if (!this.connected) throw new Error("Not connected");
|
|
571
|
+
const id = createRequestId();
|
|
572
|
+
return new Promise((resolve, reject) => {
|
|
573
|
+
const timer = setTimeout(() => {
|
|
574
|
+
this.pendingRequests.delete(id);
|
|
575
|
+
reject(new Error(`Query timeout: ${method}`));
|
|
576
|
+
}, this.timeout);
|
|
577
|
+
this.pendingRequests.set(id, {
|
|
578
|
+
resolve,
|
|
579
|
+
reject,
|
|
580
|
+
timer
|
|
581
|
+
});
|
|
582
|
+
this.transport.send({
|
|
583
|
+
ns: SPHERE_CONNECT_NAMESPACE,
|
|
584
|
+
v: SPHERE_CONNECT_VERSION,
|
|
585
|
+
type: "request",
|
|
586
|
+
id,
|
|
587
|
+
method,
|
|
588
|
+
params
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
// ===========================================================================
|
|
593
|
+
// Intent (trigger wallet UI)
|
|
594
|
+
// ===========================================================================
|
|
595
|
+
/** Send an intent request. The wallet will open its UI for user confirmation. */
|
|
596
|
+
async intent(action, params) {
|
|
597
|
+
if (!this.connected) throw new Error("Not connected");
|
|
598
|
+
const id = createRequestId();
|
|
599
|
+
return new Promise((resolve, reject) => {
|
|
600
|
+
const timer = setTimeout(() => {
|
|
601
|
+
this.pendingRequests.delete(id);
|
|
602
|
+
reject(new Error(`Intent timeout: ${action}`));
|
|
603
|
+
}, this.intentTimeout);
|
|
604
|
+
this.pendingRequests.set(id, {
|
|
605
|
+
resolve,
|
|
606
|
+
reject,
|
|
607
|
+
timer
|
|
608
|
+
});
|
|
609
|
+
this.transport.send({
|
|
610
|
+
ns: SPHERE_CONNECT_NAMESPACE,
|
|
611
|
+
v: SPHERE_CONNECT_VERSION,
|
|
612
|
+
type: "intent",
|
|
613
|
+
id,
|
|
614
|
+
action,
|
|
615
|
+
params
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
// ===========================================================================
|
|
620
|
+
// Events
|
|
621
|
+
// ===========================================================================
|
|
622
|
+
/** Subscribe to a wallet event. Returns unsubscribe function. */
|
|
623
|
+
on(event, handler) {
|
|
624
|
+
if (!this.eventHandlers.has(event)) {
|
|
625
|
+
this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
626
|
+
if (this.connected) {
|
|
627
|
+
this.query(RPC_METHODS.SUBSCRIBE, { event }).catch(() => {
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
this.eventHandlers.get(event).add(handler);
|
|
632
|
+
return () => {
|
|
633
|
+
const handlers = this.eventHandlers.get(event);
|
|
634
|
+
if (handlers) {
|
|
635
|
+
handlers.delete(handler);
|
|
636
|
+
if (handlers.size === 0) {
|
|
637
|
+
this.eventHandlers.delete(event);
|
|
638
|
+
if (this.connected) {
|
|
639
|
+
this.query(RPC_METHODS.UNSUBSCRIBE, { event }).catch(() => {
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
// ===========================================================================
|
|
647
|
+
// Message Handling
|
|
648
|
+
// ===========================================================================
|
|
649
|
+
handleMessage(msg) {
|
|
650
|
+
if (msg.type === "handshake" && msg.direction === "response") {
|
|
651
|
+
this.handleHandshakeResponse(msg);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (msg.type === "response") {
|
|
655
|
+
this.handlePendingResponse(msg.id, msg.result, msg.error);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (msg.type === "intent_result") {
|
|
659
|
+
this.handlePendingResponse(msg.id, msg.result, msg.error);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (msg.type === "event") {
|
|
663
|
+
const handlers = this.eventHandlers.get(msg.event);
|
|
664
|
+
if (handlers) {
|
|
665
|
+
for (const handler of handlers) {
|
|
666
|
+
try {
|
|
667
|
+
handler(msg.data);
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
handleHandshakeResponse(msg) {
|
|
675
|
+
if (!this.handshakeResolver) return;
|
|
676
|
+
clearTimeout(this.handshakeResolver.timer);
|
|
677
|
+
if (msg.sessionId && msg.identity) {
|
|
678
|
+
this.sessionId = msg.sessionId;
|
|
679
|
+
this.grantedPermissions = msg.permissions;
|
|
680
|
+
this.identity = msg.identity;
|
|
681
|
+
this.connected = true;
|
|
682
|
+
this.handshakeResolver.resolve({
|
|
683
|
+
sessionId: msg.sessionId,
|
|
684
|
+
permissions: this.grantedPermissions,
|
|
685
|
+
identity: msg.identity
|
|
686
|
+
});
|
|
687
|
+
} else {
|
|
688
|
+
this.handshakeResolver.reject(new Error("Connection rejected by wallet"));
|
|
689
|
+
}
|
|
690
|
+
this.handshakeResolver = null;
|
|
691
|
+
}
|
|
692
|
+
handlePendingResponse(id, result, error) {
|
|
693
|
+
const pending = this.pendingRequests.get(id);
|
|
694
|
+
if (!pending) return;
|
|
695
|
+
clearTimeout(pending.timer);
|
|
696
|
+
this.pendingRequests.delete(id);
|
|
697
|
+
if (error) {
|
|
698
|
+
const err = new Error(error.message);
|
|
699
|
+
err.code = error.code;
|
|
700
|
+
err.data = error.data;
|
|
701
|
+
pending.reject(err);
|
|
702
|
+
} else {
|
|
703
|
+
pending.resolve(result);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// ===========================================================================
|
|
707
|
+
// Cleanup
|
|
708
|
+
// ===========================================================================
|
|
709
|
+
cleanup() {
|
|
710
|
+
if (this.unsubscribeTransport) {
|
|
711
|
+
this.unsubscribeTransport();
|
|
712
|
+
this.unsubscribeTransport = null;
|
|
713
|
+
}
|
|
714
|
+
for (const [, pending] of this.pendingRequests) {
|
|
715
|
+
clearTimeout(pending.timer);
|
|
716
|
+
pending.reject(new Error("Disconnected"));
|
|
717
|
+
}
|
|
718
|
+
this.pendingRequests.clear();
|
|
719
|
+
this.eventHandlers.clear();
|
|
720
|
+
this.connected = false;
|
|
721
|
+
this.sessionId = null;
|
|
722
|
+
this.grantedPermissions = [];
|
|
723
|
+
this.identity = null;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
export {
|
|
727
|
+
ALL_PERMISSIONS,
|
|
728
|
+
ConnectClient,
|
|
729
|
+
ConnectHost,
|
|
730
|
+
DEFAULT_PERMISSIONS,
|
|
731
|
+
ERROR_CODES,
|
|
732
|
+
HOST_READY_TIMEOUT,
|
|
733
|
+
HOST_READY_TYPE,
|
|
734
|
+
INTENT_ACTIONS,
|
|
735
|
+
INTENT_PERMISSIONS,
|
|
736
|
+
METHOD_PERMISSIONS,
|
|
737
|
+
PERMISSION_SCOPES,
|
|
738
|
+
RPC_METHODS,
|
|
739
|
+
SPHERE_CONNECT_NAMESPACE,
|
|
740
|
+
SPHERE_CONNECT_VERSION,
|
|
741
|
+
createRequestId,
|
|
742
|
+
hasIntentPermission,
|
|
743
|
+
hasMethodPermission,
|
|
744
|
+
isSphereConnectMessage,
|
|
745
|
+
validatePermissions
|
|
746
|
+
};
|
|
747
|
+
//# sourceMappingURL=index.js.map
|