@qntx/wallet-engine 0.6.0 → 0.7.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/{contract-D5pguqiZ.mjs → contract-CEsgLme3.mjs} +27 -34
- package/dist/contract-CEsgLme3.mjs.map +1 -0
- package/dist/gateway-contract.d.mts +1 -13
- package/dist/gateway-contract.mjs +1 -1
- package/dist/idb.d.mts +0 -4
- package/dist/idb.mjs +3 -6
- package/dist/idb.mjs.map +1 -1
- package/dist/index.d.mts +310 -163
- package/dist/index.mjs +1469 -641
- package/dist/index.mjs.map +1 -1
- package/dist/{registry-9oeWs_yZ.d.mts → registry-LcFSPrE1.d.mts} +1 -3
- package/package.json +8 -8
- package/dist/contract-D5pguqiZ.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as rpcPath, C as EVM_RPC_METHODS, I as parseWith, c as networkById, i as servedNetworksResponse, t as networksPath, u as esploraBasePath, v as INSTALL_ID_HEADER } from "./contract-CEsgLme3.mjs";
|
|
2
2
|
import { createStore } from "zustand/vanilla";
|
|
3
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
4
|
+
import { ChainError } from "@qntx/wallet-chains";
|
|
5
|
+
import { WalletError, parseBackupHeader } from "@qntx/wallet";
|
|
3
6
|
import { Address, OutScript, Transaction } from "@scure/btc-signer";
|
|
4
7
|
import { SparkInputError, bitcoinAddressScript, createSparkClient, decodeBolt11, decodeSparkAddress, encodeSparkAddress, parseSparkInvoice, uuidv7 } from "@qntx/spark";
|
|
5
8
|
import { BITCOIN_NETWORKS, bitcoinAddressFromXpub, bitcoinNetwork, bitcoinPubkeyFromXpub, decodeBitcoinPsbt } from "@qntx/wallet-chains/bitcoin";
|
|
6
|
-
import { checksumEvmAddress, decodeEvm, isEvmAddress } from "@qntx/wallet-chains/evm";
|
|
9
|
+
import { checksumEvmAddress, decodeEvm, isEvmAddress, normalizeEvm } from "@qntx/wallet-chains/evm";
|
|
7
10
|
import { PROGRAM_IDS, decodeSolana, isSolanaAddress, netAfterTransferFee, parseMintExtensions } from "@qntx/wallet-chains/solana";
|
|
8
|
-
import { WalletError, parseBackupHeader } from "@qntx/wallet";
|
|
9
|
-
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
10
11
|
import { decodeLnurl, encodeLnurl, lightningAddressUrl, registerLightningAddress, requestInvoice, resolveLnurl, submitWithdraw, unregisterLightningAddress } from "@qntx/spark/lnurl";
|
|
11
12
|
import { signedMatchesUnsigned, verifyEvent } from "@qntx/nostr";
|
|
12
13
|
import { makeZapRequest } from "@qntx/nostr/nips/nip57";
|
|
14
|
+
import { chains } from "@qntx/wallet/chains";
|
|
13
15
|
import { nostr } from "@qntx/wallet/nostr";
|
|
14
16
|
import { spark } from "@qntx/wallet/spark";
|
|
15
17
|
import { createPublicClient, decodeFunctionResult, encodeFunctionData, fallback, http, isAddress, isHex, numberToHex } from "viem";
|
|
16
|
-
import { chains } from "@qntx/wallet/chains";
|
|
17
18
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
18
19
|
import { base64, hex } from "@scure/base";
|
|
19
20
|
import { p2wpkh } from "@scure/btc-signer/payment.js";
|
|
@@ -22,7 +23,7 @@ import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction }
|
|
|
22
23
|
import { getTransferSolInstruction } from "@solana-program/system";
|
|
23
24
|
import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, getCreateAssociatedTokenIdempotentInstruction, getTransferCheckedInstruction } from "@solana-program/token";
|
|
24
25
|
import { address, appendTransactionMessageInstructions, blockhash, compileTransactionMessage, createNoopSigner, createTransactionMessage, getCompiledTransactionMessageEncoder, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash } from "@solana/kit";
|
|
25
|
-
|
|
26
|
+
// src/controller.ts
|
|
26
27
|
/**
|
|
27
28
|
* Controller base: a zustand vanilla store plus lifecycle. `update` applies a recipe that must
|
|
28
29
|
* return a NEW state object; returning the same reference is a no-op (no set, no notification).
|
|
@@ -42,8 +43,7 @@ var Controller = class {
|
|
|
42
43
|
this.store.setState((state) => recipe(state));
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
|
-
|
|
46
|
-
//#region src/events.ts
|
|
46
|
+
// src/events.ts
|
|
47
47
|
function createEventBus(logger) {
|
|
48
48
|
const listeners = /* @__PURE__ */ new Map();
|
|
49
49
|
return {
|
|
@@ -70,8 +70,7 @@ function createEventBus(logger) {
|
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
//#region src/errors.ts
|
|
73
|
+
// src/errors.ts
|
|
75
74
|
/** Typed engine errors: store codes are `closed` and `integrity` (decrypt or bind failure). */
|
|
76
75
|
var EngineStoreError = class extends Error {
|
|
77
76
|
code;
|
|
@@ -81,14 +80,12 @@ var EngineStoreError = class extends Error {
|
|
|
81
80
|
this.code = code;
|
|
82
81
|
}
|
|
83
82
|
};
|
|
84
|
-
|
|
85
|
-
//#region src/store/guard.ts
|
|
83
|
+
// src/store/guard.ts
|
|
86
84
|
/** NUL is the composite-key/AAD separator — a name or key containing it collides. */
|
|
87
85
|
function checkName(kind, value) {
|
|
88
86
|
if (value.includes("\0")) throw new EngineStoreError("invalid-input", `${kind} must not contain NUL`);
|
|
89
87
|
}
|
|
90
|
-
|
|
91
|
-
//#region src/store/memory.ts
|
|
88
|
+
// src/store/memory.ts
|
|
92
89
|
/** In-memory EngineStore: the Node/test implementation — same contract, no durability. */
|
|
93
90
|
function createMemoryEngineStore(backing) {
|
|
94
91
|
const collections = backing?.collections ?? /* @__PURE__ */ new Map();
|
|
@@ -133,318 +130,357 @@ function createMemoryEngineStore(backing) {
|
|
|
133
130
|
}
|
|
134
131
|
};
|
|
135
132
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
var
|
|
139
|
-
|
|
140
|
-
constructor(
|
|
133
|
+
// src/connect/errors.ts
|
|
134
|
+
/** EIP-1193 / EIP-1474 / EIP-3326 error: `code` is what the page's promise rejects with. */
|
|
135
|
+
var ProviderError = class extends Error {
|
|
136
|
+
code;
|
|
137
|
+
constructor(code, message) {
|
|
141
138
|
super(message);
|
|
142
|
-
this.name = "
|
|
143
|
-
this.
|
|
139
|
+
this.name = "ProviderError";
|
|
140
|
+
this.code = code;
|
|
144
141
|
}
|
|
145
142
|
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
if (family === "evm") return isEvmAddress(address) ? checksumEvmAddress(address) : void 0;
|
|
156
|
-
if (family === "solana") return isSolanaAddress(address) ? address : void 0;
|
|
157
|
-
if (family === "spark") {
|
|
158
|
-
const decoded = decodeSparkAddress(address);
|
|
159
|
-
return encodeSparkAddress({
|
|
160
|
-
network: decoded.network,
|
|
161
|
-
identityPublicKey: decoded.identityPublicKey
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
for (const network of Object.values(BITCOIN_NETWORKS)) try {
|
|
165
|
-
const codec = Address(network.params);
|
|
166
|
-
return codec.encode(codec.decode(address));
|
|
167
|
-
} catch {}
|
|
168
|
-
return;
|
|
169
|
-
} catch {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
//#endregion
|
|
174
|
-
//#region src/addresses/record.ts
|
|
143
|
+
const userRejectedRequest = (message = "user rejected the request") => new ProviderError(4001, message);
|
|
144
|
+
const unauthorized = (message = "not authorized") => new ProviderError(4100, message);
|
|
145
|
+
const unsupportedMethod = (method) => new ProviderError(4200, `${method} is not supported`);
|
|
146
|
+
const unrecognizedChain = (message = "unrecognized chain") => new ProviderError(4902, message);
|
|
147
|
+
const invalidParams = (message) => new ProviderError(-32602, message);
|
|
148
|
+
const requestPending = (message) => new ProviderError(-32002, message);
|
|
149
|
+
const internalError = (message) => new ProviderError(-32603, message);
|
|
150
|
+
// src/connect/controller.ts
|
|
175
151
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
152
|
+
* DApp connections (CAIP-25 style sessions): one session per origin, authorization by network,
|
|
153
|
+
* account and method, persisted in the engine store and cleared from memory on lock. A connection
|
|
154
|
+
* is approved from a pending queue, by id, like any other request; locking rejects all of it.
|
|
178
155
|
*/
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
]);
|
|
195
|
-
const isObject$12 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
196
|
-
const isNonNegativeInt$2 = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
197
|
-
/** Trimmed name, or `undefined` when it is not 1 to {@link NAME_MAX} characters. */
|
|
198
|
-
const cleanName = (raw) => {
|
|
199
|
-
const name = raw.trim();
|
|
200
|
-
return name.length > 0 && name.length <= 32 ? name : void 0;
|
|
156
|
+
const COLLECTION$4 = "connections";
|
|
157
|
+
/** `lastUsedAt` is rewritten at most this often. */
|
|
158
|
+
const TOUCH_INTERVAL_MS = 6e4;
|
|
159
|
+
/** Granted for every network a session covers. */
|
|
160
|
+
const SESSION_METHODS = [
|
|
161
|
+
"eth_sendTransaction",
|
|
162
|
+
"personal_sign",
|
|
163
|
+
"eth_signTypedData_v4"
|
|
164
|
+
];
|
|
165
|
+
const SESSION_NOTIFICATIONS = ["accountsChanged", "chainChanged"];
|
|
166
|
+
/** `eip155:1` → 1; `undefined` for anything that is not an EVM network. */
|
|
167
|
+
const evmChainId = (network) => {
|
|
168
|
+
const match = /^eip155:(\d+)$/.exec(network);
|
|
169
|
+
const id = match?.[1] === void 0 ? void 0 : Number(match[1]);
|
|
170
|
+
return id !== void 0 && Number.isSafeInteger(id) ? id : void 0;
|
|
201
171
|
};
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
};
|
|
209
|
-
return note.length <= 120 ? {
|
|
210
|
-
ok: true,
|
|
211
|
-
note
|
|
212
|
-
} : { ok: false };
|
|
172
|
+
const caip10 = (network, address) => `${network}:${address}`;
|
|
173
|
+
const addressOf$1 = (account) => account.slice(account.lastIndexOf(":") + 1);
|
|
174
|
+
const isObject$14 = (v) => typeof v === "object" && v !== null;
|
|
175
|
+
const isSession = (value) => {
|
|
176
|
+
if (!isObject$14(value) || !isObject$14(value["origin"]) || !isObject$14(value["scopes"])) return false;
|
|
177
|
+
return typeof value["id"] === "string" && value["origin"]["kind"] === "browser" && typeof value["origin"]["origin"] === "string" && typeof value["currentNetwork"] === "string" && typeof value["createdAt"] === "number" && typeof value["lastUsedAt"] === "number";
|
|
213
178
|
};
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Strict decode with the controller's rules; the address is re-normalized. Anything else is
|
|
217
|
-
* `undefined`.
|
|
218
|
-
*/
|
|
219
|
-
function decodeRecord(value) {
|
|
220
|
-
if (!isObject$12(value) || typeof value["id"] !== "string" || !isNonNegativeInt$2(value["updatedAt"])) return;
|
|
221
|
-
if ("deleted" in value) return value["deleted"] === true && hasOnly(value, TOMBSTONE_KEYS) ? {
|
|
222
|
-
id: value["id"],
|
|
223
|
-
deleted: true,
|
|
224
|
-
updatedAt: value["updatedAt"]
|
|
225
|
-
} : void 0;
|
|
226
|
-
const { family, name, address, note } = value;
|
|
227
|
-
if (!hasOnly(value, ENTRY_KEYS) || !isFamily(family) || typeof name !== "string" || typeof address !== "string" || !isNonNegativeInt$2(value["createdAt"]) || note !== void 0 && typeof note !== "string") return;
|
|
228
|
-
const cleanedName = cleanName(name);
|
|
229
|
-
const normalized = normalizeAddress(family, address);
|
|
230
|
-
const cleanedNote = cleanNote(note);
|
|
231
|
-
if (cleanedName === void 0 || normalized === void 0 || !cleanedNote.ok || note !== void 0 && cleanedNote.note === void 0) return;
|
|
232
|
-
return {
|
|
233
|
-
id: value["id"],
|
|
234
|
-
name: cleanedName,
|
|
235
|
-
family,
|
|
236
|
-
address: normalized,
|
|
237
|
-
...cleanedNote.note === void 0 ? {} : { note: cleanedNote.note },
|
|
238
|
-
createdAt: value["createdAt"],
|
|
239
|
-
updatedAt: value["updatedAt"]
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
const serialized = (record) => JSON.stringify(record);
|
|
243
|
-
/** The winner of two versions of one id: larger `updatedAt`, then the larger serialization. */
|
|
244
|
-
function pickWinner(a, b) {
|
|
245
|
-
if (a.updatedAt !== b.updatedAt) return a.updatedAt > b.updatedAt ? a : b;
|
|
246
|
-
return serialized(a) >= serialized(b) ? a : b;
|
|
247
|
-
}
|
|
248
|
-
const pruneTombstones = (records, now) => records.filter((r) => !isTombstone(r) || now - r.updatedAt <= 7776e6);
|
|
249
|
-
/** Newest first, id as the tiebreak, so every device orders (and truncates) the same way. */
|
|
250
|
-
const newestFirst = (a, b) => b.updatedAt - a.updatedAt || (a.id < b.id ? -1 : 1);
|
|
251
|
-
/** Drops the oldest tombstones until at most `max` records remain, or no tombstone is left. */
|
|
252
|
-
function evictTombstones(records, max) {
|
|
253
|
-
const excess = records.length - max;
|
|
254
|
-
if (excess <= 0) return records;
|
|
255
|
-
const oldest = new Set(records.filter(isTombstone).sort((a, b) => a.updatedAt - b.updatedAt || (a.id < b.id ? -1 : 1)).slice(0, excess).map((r) => r.id));
|
|
256
|
-
return records.filter((r) => !oldest.has(r.id));
|
|
257
|
-
}
|
|
258
|
-
/** {@link mergeRecords}, also counting the live entries the cap forced out. */
|
|
259
|
-
function mergeWithReport(local, remote, now) {
|
|
260
|
-
const byId = /* @__PURE__ */ new Map();
|
|
261
|
-
for (const record of [...local, ...remote]) {
|
|
262
|
-
const existing = byId.get(record.id);
|
|
263
|
-
byId.set(record.id, existing === void 0 ? record : pickWinner(existing, record));
|
|
264
|
-
}
|
|
265
|
-
const live = /* @__PURE__ */ new Map();
|
|
266
|
-
const kept = [];
|
|
267
|
-
for (const record of [...pruneTombstones([...byId.values()], now)].sort(newestFirst)) {
|
|
268
|
-
if (isTombstone(record)) {
|
|
269
|
-
kept.push(record);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const key = `${record.family}:${record.address}`;
|
|
273
|
-
if (!live.has(key)) {
|
|
274
|
-
live.set(key, record);
|
|
275
|
-
kept.push(record);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const evicted = evictTombstones(kept, 128);
|
|
279
|
-
return {
|
|
280
|
-
records: [...evicted.slice(0, 128)].sort((a, b) => a.id < b.id ? -1 : 1),
|
|
281
|
-
droppedLive: evicted.slice(128).filter((r) => !isTombstone(r)).length
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
//#endregion
|
|
285
|
-
//#region src/addresses/controller.ts
|
|
286
|
-
/**
|
|
287
|
-
* Address book: named recipients, validated and normalized per chain family, persisted in the
|
|
288
|
-
* engine store and cleared from memory on lock. Deletions are kept as tombstones so they reach the
|
|
289
|
-
* other devices; `syncEntries`/`merge` are the whole surface the metadata sync needs. Request risk
|
|
290
|
-
* checks treat its addresses as known.
|
|
291
|
-
*/
|
|
292
|
-
const COLLECTION$3 = "contacts";
|
|
293
|
-
const entriesOf = (records) => Object.fromEntries(records.flatMap((r) => isTombstone(r) ? [] : [[r.id, r]]));
|
|
294
|
-
var AddressBookController = class extends Controller {
|
|
179
|
+
var ConnectionController = class extends Controller {
|
|
295
180
|
options;
|
|
296
181
|
engineStore;
|
|
297
|
-
|
|
298
|
-
|
|
182
|
+
waiters = /* @__PURE__ */ new Map();
|
|
183
|
+
group;
|
|
299
184
|
constructor(options) {
|
|
300
185
|
super({
|
|
301
|
-
name: "
|
|
302
|
-
defaultState: () => ({
|
|
186
|
+
name: "connections",
|
|
187
|
+
defaultState: () => ({
|
|
188
|
+
sessions: {},
|
|
189
|
+
pending: {}
|
|
190
|
+
})
|
|
303
191
|
});
|
|
304
192
|
this.options = options;
|
|
305
193
|
}
|
|
306
194
|
async init() {
|
|
307
195
|
const store = await this.options.openStore();
|
|
308
196
|
this.engineStore = store;
|
|
309
|
-
|
|
197
|
+
this.group = void 0;
|
|
198
|
+
const collection = store.collection(COLLECTION$4);
|
|
310
199
|
const keys = await collection.list();
|
|
311
200
|
const rows = await Promise.all(keys.map(async (key) => collection.get(key)));
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
201
|
+
const now = this.options.now();
|
|
202
|
+
const sessions = {};
|
|
203
|
+
const expired = [];
|
|
204
|
+
for (const [index, raw] of rows.entries()) {
|
|
205
|
+
const key = keys[index];
|
|
206
|
+
if (raw === void 0 || key === void 0) continue;
|
|
207
|
+
const parsed = JSON.parse(new TextDecoder().decode(raw));
|
|
208
|
+
if (!isSession(parsed)) {
|
|
209
|
+
this.options.logger.warn("connection session skipped: malformed");
|
|
210
|
+
expired.push(key);
|
|
211
|
+
} else if (now - parsed.lastUsedAt > 7776e6) expired.push(key);
|
|
212
|
+
else sessions[parsed.id] = parsed;
|
|
318
213
|
}
|
|
319
|
-
|
|
320
|
-
this.update(() => ({
|
|
214
|
+
await Promise.all(expired.map(async (key) => collection.delete(key)));
|
|
215
|
+
this.update(() => ({
|
|
216
|
+
sessions,
|
|
217
|
+
pending: {}
|
|
218
|
+
}));
|
|
321
219
|
}
|
|
322
220
|
async destroy() {
|
|
323
221
|
this.lock();
|
|
324
222
|
await Promise.resolve();
|
|
325
223
|
}
|
|
224
|
+
/** Locking is not consent: every pending connection is rejected. */
|
|
326
225
|
lock() {
|
|
327
226
|
this.engineStore = void 0;
|
|
328
|
-
this.
|
|
329
|
-
this.
|
|
227
|
+
this.group = void 0;
|
|
228
|
+
for (const waiter of this.waiters.values()) waiter.reject(userRejectedRequest("the wallet was locked"));
|
|
229
|
+
this.waiters.clear();
|
|
230
|
+
this.update(() => ({
|
|
231
|
+
sessions: {},
|
|
232
|
+
pending: {}
|
|
233
|
+
}));
|
|
330
234
|
}
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
|
|
235
|
+
/** Throws `4100` unless `origin` is `https://host[:port]` (or localhost in development). */
|
|
236
|
+
checkOrigin(origin) {
|
|
237
|
+
let url;
|
|
238
|
+
try {
|
|
239
|
+
url = new URL(origin);
|
|
240
|
+
} catch {
|
|
241
|
+
throw unauthorized("invalid origin");
|
|
242
|
+
}
|
|
243
|
+
const localhost = url.protocol === "http:" && url.hostname === "localhost";
|
|
244
|
+
if (url.origin !== origin || url.protocol !== "https:" && !(localhost && this.options.allowLocalhost)) throw unauthorized("origin not allowed");
|
|
245
|
+
return {
|
|
246
|
+
kind: "browser",
|
|
247
|
+
origin
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
session(origin) {
|
|
251
|
+
return Object.values(this.getState().sessions).find((s) => s.origin.origin === origin);
|
|
252
|
+
}
|
|
253
|
+
pending() {
|
|
254
|
+
return Object.values(this.getState().pending);
|
|
255
|
+
}
|
|
256
|
+
/** The group sessions are bound to: the host's last `setActiveGroup`, else the app's selection. */
|
|
257
|
+
activeGroup() {
|
|
258
|
+
return this.group ?? this.options.selectedGroup();
|
|
259
|
+
}
|
|
260
|
+
/** Whether `network` is an EVM network the wallet has enabled. */
|
|
261
|
+
isEnabled(network) {
|
|
262
|
+
return this.options.enabledNetworks().includes(network);
|
|
263
|
+
}
|
|
264
|
+
/** The origin's authorized addresses on its current network; empty when not connected. */
|
|
265
|
+
accounts(origin) {
|
|
266
|
+
const session = this.session(origin);
|
|
267
|
+
return session?.scopes[session.currentNetwork]?.accounts.map(addressOf$1) ?? [];
|
|
268
|
+
}
|
|
269
|
+
/** Throws `4100` unless the origin's session covers `method` on `network`. */
|
|
270
|
+
authorize(origin, network, method) {
|
|
271
|
+
const session = this.session(origin);
|
|
272
|
+
const scope = session?.scopes[network];
|
|
273
|
+
if (session === void 0 || scope === void 0 || !scope.methods.includes(method)) throw unauthorized();
|
|
274
|
+
return session;
|
|
275
|
+
}
|
|
276
|
+
/** Resolves with the session once the host approves; rejects with `4001` when it refuses. */
|
|
277
|
+
async request(origin, kind, network) {
|
|
278
|
+
if (this.engineStore === void 0) throw userRejectedRequest("the wallet is locked");
|
|
279
|
+
if (this.pending().some((p) => p.origin.origin === origin)) throw requestPending("a connection request is already pending");
|
|
280
|
+
const id = `conn-${[...this.options.random(8)].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
281
|
+
const entry = {
|
|
282
|
+
id,
|
|
283
|
+
origin: {
|
|
284
|
+
kind: "browser",
|
|
285
|
+
origin
|
|
286
|
+
},
|
|
287
|
+
kind,
|
|
288
|
+
...network === void 0 ? {} : { network },
|
|
289
|
+
createdAt: this.options.now()
|
|
290
|
+
};
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
this.waiters.set(id, {
|
|
293
|
+
resolve,
|
|
294
|
+
reject
|
|
295
|
+
});
|
|
296
|
+
this.update((s) => ({
|
|
297
|
+
...s,
|
|
298
|
+
pending: {
|
|
299
|
+
...s.pending,
|
|
300
|
+
[id]: entry
|
|
301
|
+
}
|
|
302
|
+
}));
|
|
303
|
+
});
|
|
334
304
|
}
|
|
335
305
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* lacks, i.e. whether the remote copy needs a newer publish.
|
|
306
|
+
* The host's approval, by id: the accounts granted (EVM addresses of the active group) and the
|
|
307
|
+
* network.
|
|
339
308
|
*/
|
|
340
|
-
async
|
|
309
|
+
async approveConnection(id, grant) {
|
|
341
310
|
const store = this.requireStore();
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
311
|
+
const entry = this.getState().pending[id];
|
|
312
|
+
const waiter = this.waiters.get(id);
|
|
313
|
+
if (entry === void 0 || waiter === void 0) throw new Error(`unknown connection request ${id}`);
|
|
314
|
+
if (entry.kind === "network" && entry.network !== grant.network) throw new Error(`connection request ${id} is for ${String(entry.network)}`);
|
|
315
|
+
if (evmChainId(grant.network) === void 0 || !this.options.enabledNetworks().includes(grant.network)) throw invalidParams(`${grant.network} is not an enabled EVM network`);
|
|
316
|
+
const group = this.activeGroup();
|
|
317
|
+
if (group === void 0) throw new Error("no account group is selected");
|
|
318
|
+
const own = (await this.options.evmAddress(group, grant.network)).toLowerCase();
|
|
319
|
+
if (grant.accounts.length === 0 || grant.accounts.some((a) => a.toLowerCase() !== own)) throw invalidParams("only the active group's account can be granted");
|
|
320
|
+
if (this.getState().pending[id] === void 0) throw new Error(`connection request ${id} is no longer pending`);
|
|
321
|
+
const now = this.options.now();
|
|
322
|
+
const existing = this.session(entry.origin.origin);
|
|
323
|
+
const scope = {
|
|
324
|
+
accounts: [caip10(grant.network, own)],
|
|
325
|
+
methods: SESSION_METHODS,
|
|
326
|
+
notifications: SESSION_NOTIFICATIONS
|
|
350
327
|
};
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
328
|
+
const session = {
|
|
329
|
+
id: existing?.id ?? `session-${[...this.options.random(8)].map((b) => b.toString(16).padStart(2, "0")).join("")}`,
|
|
330
|
+
origin: entry.origin,
|
|
331
|
+
scopes: {
|
|
332
|
+
...existing?.scopes,
|
|
333
|
+
[grant.network]: scope
|
|
334
|
+
},
|
|
335
|
+
currentNetwork: grant.network,
|
|
336
|
+
createdAt: existing?.createdAt ?? now,
|
|
337
|
+
lastUsedAt: now
|
|
359
338
|
};
|
|
360
|
-
|
|
361
|
-
|
|
339
|
+
await this.persist(store, session);
|
|
340
|
+
this.dropPending(id);
|
|
341
|
+
if (existing !== void 0 && existing.currentNetwork !== grant.network) this.options.invalidate(entry.origin.origin);
|
|
342
|
+
waiter.resolve(session);
|
|
343
|
+
return session;
|
|
344
|
+
}
|
|
345
|
+
rejectConnection(id) {
|
|
346
|
+
const waiter = this.waiters.get(id);
|
|
347
|
+
if (this.getState().pending[id] === void 0 || waiter === void 0) throw new Error(`unknown connection request ${id}`);
|
|
348
|
+
this.dropPending(id);
|
|
349
|
+
waiter.reject(userRejectedRequest());
|
|
350
|
+
}
|
|
351
|
+
/** Ends the origin's session; its pending connection and unfinished requests are void. */
|
|
352
|
+
async revoke(origin) {
|
|
362
353
|
const store = this.requireStore();
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (address === void 0) throw new AddressBookError("invalid-input", `not a valid ${params.family} address`);
|
|
368
|
-
if (Object.values(this.getState().entries).some((e) => e.family === params.family && e.address === address)) throw new AddressBookError("duplicate", "this address is already in the address book");
|
|
369
|
-
if (this.records.size >= 128) {
|
|
370
|
-
const kept = evictTombstones([...this.records.values()], 127);
|
|
371
|
-
if (kept.length >= 128) throw new AddressBookError("limit", `the address book holds at most 128 contacts`);
|
|
372
|
-
const keptIds = new Set(kept.map((r) => r.id));
|
|
373
|
-
await Promise.all([...this.records.keys()].filter((id) => !keptIds.has(id)).map(async (id) => store.collection(COLLECTION$3).delete(id)));
|
|
374
|
-
this.records = new Map(kept.map((r) => [r.id, r]));
|
|
354
|
+
for (const p of this.pending().filter((x) => x.origin.origin === origin)) {
|
|
355
|
+
const waiter = this.waiters.get(p.id);
|
|
356
|
+
this.dropPending(p.id);
|
|
357
|
+
waiter?.reject(userRejectedRequest("the connection was revoked"));
|
|
375
358
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
359
|
+
const session = this.session(origin);
|
|
360
|
+
if (session !== void 0) {
|
|
361
|
+
await store.collection(COLLECTION$4).delete(session.id);
|
|
362
|
+
this.update((s) => {
|
|
363
|
+
const { [session.id]: _gone, ...sessions } = s.sessions;
|
|
364
|
+
return {
|
|
365
|
+
...s,
|
|
366
|
+
sessions
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
this.options.invalidate(origin);
|
|
388
371
|
}
|
|
389
|
-
|
|
372
|
+
/** Switches within the session's scope; the origin's unfinished requests are void. */
|
|
373
|
+
async setCurrentNetwork(origin, network) {
|
|
390
374
|
const store = this.requireStore();
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
};
|
|
399
|
-
await this.persist(store, entry);
|
|
400
|
-
return entry;
|
|
375
|
+
const session = this.session(origin);
|
|
376
|
+
if (session === void 0 || session.scopes[network] === void 0) throw unauthorized();
|
|
377
|
+
await this.persist(store, {
|
|
378
|
+
...session,
|
|
379
|
+
currentNetwork: network
|
|
380
|
+
});
|
|
381
|
+
this.options.invalidate(origin);
|
|
401
382
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
const { note: _previous, ...rest } = existing;
|
|
408
|
-
const entry = {
|
|
409
|
-
...rest,
|
|
410
|
-
...cleaned === void 0 ? {} : { note: cleaned },
|
|
411
|
-
updatedAt: this.options.now()
|
|
412
|
-
};
|
|
413
|
-
await this.persist(store, entry);
|
|
414
|
-
return entry;
|
|
415
|
-
}
|
|
416
|
-
/** Removes the entry, leaving a tombstone so other devices drop it too. */
|
|
417
|
-
async remove(id) {
|
|
418
|
-
const store = this.requireStore();
|
|
419
|
-
this.requireEntry(id);
|
|
383
|
+
async touch(origin) {
|
|
384
|
+
const store = this.engineStore;
|
|
385
|
+
const session = this.session(origin);
|
|
386
|
+
const now = this.options.now();
|
|
387
|
+
if (store === void 0 || session === void 0 || now - session.lastUsedAt < TOUCH_INTERVAL_MS) return;
|
|
420
388
|
await this.persist(store, {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
updatedAt: this.options.now()
|
|
389
|
+
...session,
|
|
390
|
+
lastUsedAt: now
|
|
424
391
|
});
|
|
425
392
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
393
|
+
/**
|
|
394
|
+
* The host switched account group: every session's accounts become the new group's addresses. A
|
|
395
|
+
* session that cannot get one (no address derivable) is left with an empty list and has to be
|
|
396
|
+
* authorized again.
|
|
397
|
+
*/
|
|
398
|
+
async setActiveGroup(walletId, groupIndex) {
|
|
399
|
+
const store = this.requireStore();
|
|
400
|
+
const group = {
|
|
401
|
+
walletId,
|
|
402
|
+
groupIndex
|
|
403
|
+
};
|
|
404
|
+
this.group = group;
|
|
405
|
+
await Promise.all(Object.values(this.getState().sessions).map(async (session) => {
|
|
406
|
+
const entries = await Promise.all(Object.entries(session.scopes).map(async ([network, scope]) => {
|
|
407
|
+
if (scope.accounts.length === 0) return [network, scope];
|
|
408
|
+
try {
|
|
409
|
+
const address = await this.options.evmAddress(group, network);
|
|
410
|
+
return [network, {
|
|
411
|
+
...scope,
|
|
412
|
+
accounts: [caip10(network, address.toLowerCase())]
|
|
413
|
+
}];
|
|
414
|
+
} catch (error) {
|
|
415
|
+
this.options.logger.warn("connection account unavailable in the new group", error);
|
|
416
|
+
return [network, {
|
|
417
|
+
...scope,
|
|
418
|
+
accounts: []
|
|
419
|
+
}];
|
|
420
|
+
}
|
|
421
|
+
}));
|
|
422
|
+
await this.persist(store, {
|
|
423
|
+
...session,
|
|
424
|
+
scopes: Object.fromEntries(entries)
|
|
425
|
+
});
|
|
426
|
+
}));
|
|
430
427
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
428
|
+
dropPending(id) {
|
|
429
|
+
this.waiters.delete(id);
|
|
430
|
+
this.update((s) => {
|
|
431
|
+
const { [id]: _gone, ...pending } = s.pending;
|
|
432
|
+
return {
|
|
433
|
+
...s,
|
|
434
|
+
pending
|
|
435
|
+
};
|
|
436
|
+
});
|
|
435
437
|
}
|
|
436
|
-
async persist(store,
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
async persist(store, session) {
|
|
439
|
+
try {
|
|
440
|
+
await store.collection(COLLECTION$4).put(session.id, new TextEncoder().encode(JSON.stringify(session)));
|
|
441
|
+
} catch (error) {
|
|
442
|
+
throw internalError(error instanceof Error ? error.message : String(error));
|
|
443
|
+
}
|
|
444
|
+
this.update((s) => ({
|
|
445
|
+
...s,
|
|
446
|
+
sessions: {
|
|
447
|
+
...s.sessions,
|
|
448
|
+
[session.id]: session
|
|
449
|
+
}
|
|
450
|
+
}));
|
|
440
451
|
}
|
|
441
452
|
requireStore() {
|
|
442
|
-
if (this.engineStore === void 0) throw
|
|
453
|
+
if (this.engineStore === void 0) throw userRejectedRequest("the wallet is locked");
|
|
443
454
|
return this.engineStore;
|
|
444
455
|
}
|
|
445
456
|
};
|
|
446
|
-
|
|
447
|
-
|
|
457
|
+
// src/addresses/normalize.ts
|
|
458
|
+
/**
|
|
459
|
+
* The canonical form of `address` for its family — EIP-55 for EVM, the re-encoded form for bitcoin
|
|
460
|
+
* (accepted on any known bitcoin network, since a contact is not tied to the wallet's network) and
|
|
461
|
+
* Spark (identity key only, no invoice fields) — or `undefined` when it is not a valid address.
|
|
462
|
+
*/
|
|
463
|
+
function normalizeAddress(family, address) {
|
|
464
|
+
try {
|
|
465
|
+
if (family === "evm") return isEvmAddress(address) ? checksumEvmAddress(address) : void 0;
|
|
466
|
+
if (family === "solana") return isSolanaAddress(address) ? address : void 0;
|
|
467
|
+
if (family === "spark") {
|
|
468
|
+
const decoded = decodeSparkAddress(address);
|
|
469
|
+
return encodeSparkAddress({
|
|
470
|
+
network: decoded.network,
|
|
471
|
+
identityPublicKey: decoded.identityPublicKey
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
for (const network of Object.values(BITCOIN_NETWORKS)) try {
|
|
475
|
+
const codec = Address(network.params);
|
|
476
|
+
return codec.encode(codec.decode(address));
|
|
477
|
+
} catch {}
|
|
478
|
+
return;
|
|
479
|
+
} catch {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// src/spark/errors.ts
|
|
448
484
|
var SparkControllerError = class extends Error {
|
|
449
485
|
category;
|
|
450
486
|
constructor(category, message, cause) {
|
|
@@ -463,8 +499,7 @@ function categorize(error) {
|
|
|
463
499
|
return "internal";
|
|
464
500
|
}
|
|
465
501
|
const feeCheck = (amountSats, maxFeeSats) => maxFeeSats !== void 0 && maxFeeSats >= amountSats ? ["fee-exceeds-amount"] : [];
|
|
466
|
-
|
|
467
|
-
//#region src/payments/errors.ts
|
|
502
|
+
// src/payments/errors.ts
|
|
468
503
|
var PaymentError = class extends Error {
|
|
469
504
|
category;
|
|
470
505
|
constructor(category, message, cause) {
|
|
@@ -473,8 +508,7 @@ var PaymentError = class extends Error {
|
|
|
473
508
|
this.category = category;
|
|
474
509
|
}
|
|
475
510
|
};
|
|
476
|
-
|
|
477
|
-
//#region src/payments/pay.ts
|
|
511
|
+
// src/payments/pay.ts
|
|
478
512
|
const lightningDetails = (invoice) => {
|
|
479
513
|
const decoded = decodeBolt11(invoice);
|
|
480
514
|
return {
|
|
@@ -688,8 +722,7 @@ async function withdrawLnurl(context, params) {
|
|
|
688
722
|
invoice
|
|
689
723
|
};
|
|
690
724
|
}
|
|
691
|
-
|
|
692
|
-
//#region src/payments/zap.ts
|
|
725
|
+
// src/payments/zap.ts
|
|
693
726
|
/** Everything up to payment: resolve LNURL, sign the 9734, request the invoice. */
|
|
694
727
|
async function prepareZap(params, context) {
|
|
695
728
|
const amountMsat = params.amountSats * 1e3;
|
|
@@ -793,8 +826,13 @@ const lightningInvoiceDetails = (invoice) => ({ payee: bytesToHex(decodeBolt11(i
|
|
|
793
826
|
function categorizePayment(error) {
|
|
794
827
|
return categorize(error) === "network" ? "network" : "rejected";
|
|
795
828
|
}
|
|
796
|
-
|
|
797
|
-
|
|
829
|
+
// src/spark/l1.ts
|
|
830
|
+
/** The bitcoin network each Spark network settles on; deposits and exits must agree with it. */
|
|
831
|
+
const SPARK_L1_NETWORK = {
|
|
832
|
+
mainnet: "bip122:000000000019d6689c085ae165831e93",
|
|
833
|
+
regtest: "bip122:0f9188f13cb7b2c71f2a335e3a4fc328"
|
|
834
|
+
};
|
|
835
|
+
// src/transactions/route.ts
|
|
798
836
|
const hexId = (prefix, random) => `${prefix}-${[...random(8)].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
799
837
|
const newId = (random) => hexId("tx", random);
|
|
800
838
|
const newRequestId = (random) => hexId("req", random);
|
|
@@ -815,8 +853,23 @@ const replacementStaleReason = (ctx, originalId) => {
|
|
|
815
853
|
if (original?.state !== "submitted" && original?.state !== "pending") return "original transaction already finished — nothing to replace";
|
|
816
854
|
return Object.values(txs).some((t) => t.replaces === originalId && LIVE_REPLACEMENT_STATES.has(t.state)) ? "original transaction was already replaced — start over" : void 0;
|
|
817
855
|
};
|
|
818
|
-
|
|
819
|
-
|
|
856
|
+
const submitTx = async (ctx, tx, hash) => {
|
|
857
|
+
const tracked = await ctx.transition(tx, {
|
|
858
|
+
state: "submitted",
|
|
859
|
+
hash,
|
|
860
|
+
submittedAt: ctx.options.now()
|
|
861
|
+
});
|
|
862
|
+
ctx.track(tracked.id, hash);
|
|
863
|
+
return tracked;
|
|
864
|
+
};
|
|
865
|
+
const failTx = async (ctx, tx, error) => {
|
|
866
|
+
const failed = tx.state === "signed" ? "failed" : "rejected";
|
|
867
|
+
await ctx.transition(tx, {
|
|
868
|
+
state: failed,
|
|
869
|
+
error: errorMessage$1(error)
|
|
870
|
+
});
|
|
871
|
+
};
|
|
872
|
+
// src/requests/checks.ts
|
|
820
873
|
/** Two reads of one fee differ by more than this share of the smaller one. */
|
|
821
874
|
const FEE_MISMATCH_PERCENT = 25n;
|
|
822
875
|
/** A fee ceiling above this multiple of the independent estimate is flagged. */
|
|
@@ -913,18 +966,20 @@ function assessRecipients(family, recipients, known) {
|
|
|
913
966
|
}
|
|
914
967
|
return [...found.values()].sort((a, b) => Number(b.level === "danger") - Number(a.level === "danger"));
|
|
915
968
|
}
|
|
916
|
-
|
|
917
|
-
//#region src/requests/sign-log.ts
|
|
969
|
+
// src/requests/sign-log.ts
|
|
918
970
|
const SIGN_LOG_LIMIT = 1e3;
|
|
919
|
-
const COLLECTION$
|
|
920
|
-
const isObject$
|
|
971
|
+
const COLLECTION$3 = "sign-log";
|
|
972
|
+
const isObject$13 = (v) => typeof v === "object" && v !== null;
|
|
921
973
|
const METHODS = /* @__PURE__ */ new Set([
|
|
922
974
|
"send",
|
|
923
975
|
"speed-up",
|
|
924
976
|
"cancel",
|
|
925
977
|
"cpfp",
|
|
926
978
|
"pay",
|
|
927
|
-
"zap"
|
|
979
|
+
"zap",
|
|
980
|
+
"evm-sign",
|
|
981
|
+
"spark-deposit",
|
|
982
|
+
"spark-exit"
|
|
928
983
|
]);
|
|
929
984
|
const OUTCOMES = /* @__PURE__ */ new Set([
|
|
930
985
|
"submitted",
|
|
@@ -939,12 +994,13 @@ const FAMILIES = /* @__PURE__ */ new Set([
|
|
|
939
994
|
"bitcoin"
|
|
940
995
|
]);
|
|
941
996
|
const isAccount = (value) => {
|
|
942
|
-
if (!isObject$
|
|
997
|
+
if (!isObject$13(value)) return false;
|
|
943
998
|
if (value["kind"] === "hd") return typeof value["walletId"] === "string" && typeof value["groupIndex"] === "number" && typeof value["template"] === "string";
|
|
944
999
|
if (value["kind"] === "imported") return typeof value["keyId"] === "string" && FAMILIES.has(value["family"]);
|
|
945
1000
|
return value["kind"] === void 0 && typeof value["walletId"] === "string" && typeof value["groupIndex"] === "number" && typeof value["network"] === "string";
|
|
946
1001
|
};
|
|
947
|
-
const
|
|
1002
|
+
const isOrigin = (value) => isObject$13(value) && (value["kind"] === "internal" || value["kind"] === "browser" && typeof value["origin"] === "string");
|
|
1003
|
+
const isEntry = (value) => isObject$13(value) && typeof value["id"] === "string" && typeof value["at"] === "number" && isOrigin(value["origin"]) && METHODS.has(value["method"]) && OUTCOMES.has(value["outcome"]) && isAccount(value["account"]);
|
|
948
1004
|
/** Sortable key: time first, request id as the tiebreak. */
|
|
949
1005
|
const keyOf = (entry) => `${String(entry.at).padStart(16, "0")}-${entry.id}`;
|
|
950
1006
|
function createSignLog(logger) {
|
|
@@ -962,14 +1018,14 @@ function createSignLog(logger) {
|
|
|
962
1018
|
logger.warn(`sign log entry for request ${entry.id} (${entry.outcome}) not written: vault locked`);
|
|
963
1019
|
return;
|
|
964
1020
|
}
|
|
965
|
-
const collection = current.collection(COLLECTION$
|
|
1021
|
+
const collection = current.collection(COLLECTION$3);
|
|
966
1022
|
await collection.put(keyOf(entry), new TextEncoder().encode(JSON.stringify(entry)));
|
|
967
1023
|
const keys = [...await collection.list()].sort();
|
|
968
1024
|
await Promise.all(keys.slice(0, Math.max(0, keys.length - SIGN_LOG_LIMIT)).map(async (k) => collection.delete(k)));
|
|
969
1025
|
},
|
|
970
1026
|
list: async () => {
|
|
971
1027
|
if (store === void 0) return [];
|
|
972
|
-
const collection = store.collection(COLLECTION$
|
|
1028
|
+
const collection = store.collection(COLLECTION$3);
|
|
973
1029
|
const keys = [...await collection.list()].sort();
|
|
974
1030
|
const rows = await Promise.all(keys.map(async (k) => collection.get(k)));
|
|
975
1031
|
const entries = [];
|
|
@@ -982,30 +1038,39 @@ function createSignLog(logger) {
|
|
|
982
1038
|
},
|
|
983
1039
|
clear: async () => {
|
|
984
1040
|
if (store === void 0) return;
|
|
985
|
-
const collection = store.collection(COLLECTION$
|
|
1041
|
+
const collection = store.collection(COLLECTION$3);
|
|
986
1042
|
const keys = [...await collection.list()].sort();
|
|
987
1043
|
await Promise.all(keys.map(async (k) => collection.delete(k)));
|
|
988
1044
|
}
|
|
989
1045
|
};
|
|
990
1046
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1047
|
+
// src/requests/controller.ts
|
|
1048
|
+
/** A request that ended without a result: `stage` says how, `message` is the failure if any. */
|
|
1049
|
+
var RequestEndedError = class extends Error {
|
|
1050
|
+
stage;
|
|
1051
|
+
constructor(stage, message) {
|
|
1052
|
+
super(message ?? `request ${stage}`);
|
|
1053
|
+
this.name = "RequestEndedError";
|
|
1054
|
+
this.stage = stage;
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
/** The origin already has a request in flight; a dApp gets one at a time. */
|
|
1058
|
+
var OriginBusyError = class extends Error {
|
|
1059
|
+
constructor(origin) {
|
|
1060
|
+
super(`${origin} already has an unfinished request`);
|
|
1061
|
+
this.name = "OriginBusyError";
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1000
1064
|
/** Requests expire this long after creation. */
|
|
1001
1065
|
const REQUEST_TTL_MS = 6e5;
|
|
1002
|
-
const
|
|
1066
|
+
const originKey = (origin) => `origin:${origin.origin}`;
|
|
1003
1067
|
const LIVE = /* @__PURE__ */ new Set(["awaiting-approval", "signing"]);
|
|
1004
1068
|
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
1005
1069
|
var RequestController = class extends Controller {
|
|
1006
1070
|
options;
|
|
1007
1071
|
entries = /* @__PURE__ */ new Map();
|
|
1008
1072
|
log;
|
|
1073
|
+
settlers = /* @__PURE__ */ new Map();
|
|
1009
1074
|
/** Original transaction id → the unfinished request replacing or accelerating it. */
|
|
1010
1075
|
targets = /* @__PURE__ */ new Map();
|
|
1011
1076
|
locked = true;
|
|
@@ -1024,6 +1089,7 @@ var RequestController = class extends Controller {
|
|
|
1024
1089
|
this.locked = false;
|
|
1025
1090
|
this.entries.clear();
|
|
1026
1091
|
this.targets.clear();
|
|
1092
|
+
this.settlers.clear();
|
|
1027
1093
|
this.update(() => ({ requests: {} }));
|
|
1028
1094
|
this.log.open(await this.options.openStore());
|
|
1029
1095
|
}
|
|
@@ -1038,6 +1104,10 @@ var RequestController = class extends Controller {
|
|
|
1038
1104
|
for (const [id, request] of Object.entries(this.getState().requests)) if (LIVE.has(request.stage)) this.finish(id, "invalidated");
|
|
1039
1105
|
this.log.close();
|
|
1040
1106
|
}
|
|
1107
|
+
/** A page navigated away, was revoked or changed network: its unfinished requests are void. */
|
|
1108
|
+
invalidateOrigin(origin) {
|
|
1109
|
+
for (const [id, request] of Object.entries(this.getState().requests)) if (LIVE.has(request.stage) && request.origin.kind === "browser" && request.origin.origin === origin) this.finish(id, "invalidated");
|
|
1110
|
+
}
|
|
1041
1111
|
/** The signature record, newest first. */
|
|
1042
1112
|
async signLog() {
|
|
1043
1113
|
return this.log.list();
|
|
@@ -1049,7 +1119,12 @@ var RequestController = class extends Controller {
|
|
|
1049
1119
|
if (this.locked) throw new Error("requests require an unlocked vault");
|
|
1050
1120
|
const { session } = this;
|
|
1051
1121
|
const id = newRequestId(this.options.random);
|
|
1052
|
-
const target =
|
|
1122
|
+
const target = "txId" in input ? input.txId : void 0;
|
|
1123
|
+
if (input.method === "evm-sign") {
|
|
1124
|
+
const key = originKey(input.origin);
|
|
1125
|
+
if (this.targets.has(key)) throw new OriginBusyError(input.origin.origin);
|
|
1126
|
+
this.targets.set(key, id);
|
|
1127
|
+
}
|
|
1053
1128
|
if (target !== void 0) {
|
|
1054
1129
|
if (this.targets.has(target)) throw new Error(`transaction ${target} already has an unfinished replacement request`);
|
|
1055
1130
|
this.targets.set(target, id);
|
|
@@ -1070,7 +1145,7 @@ var RequestController = class extends Controller {
|
|
|
1070
1145
|
const createdAt = this.options.now();
|
|
1071
1146
|
const request = {
|
|
1072
1147
|
id,
|
|
1073
|
-
origin: { kind: "internal" },
|
|
1148
|
+
origin: input.method === "evm-sign" ? input.origin : { kind: "internal" },
|
|
1074
1149
|
method: input.method,
|
|
1075
1150
|
account: built.account,
|
|
1076
1151
|
network: built.network,
|
|
@@ -1091,8 +1166,57 @@ var RequestController = class extends Controller {
|
|
|
1091
1166
|
stopTimer
|
|
1092
1167
|
});
|
|
1093
1168
|
this.put(request);
|
|
1169
|
+
this.openSettler(id);
|
|
1094
1170
|
return request;
|
|
1095
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Resolves with what `approve` returned, or rejects with `RequestEndedError` when the request
|
|
1174
|
+
* ends any other way — how a dApp's pending call learns the outcome.
|
|
1175
|
+
*/
|
|
1176
|
+
async outcome(id) {
|
|
1177
|
+
const settler = this.settlers.get(id);
|
|
1178
|
+
if (settler === void 0) throw new Error(`unknown request ${id}`);
|
|
1179
|
+
return settler.promise;
|
|
1180
|
+
}
|
|
1181
|
+
openSettler(id) {
|
|
1182
|
+
const holder = {};
|
|
1183
|
+
const promise = new Promise((resolve, reject) => {
|
|
1184
|
+
holder.resolve = resolve;
|
|
1185
|
+
holder.reject = reject;
|
|
1186
|
+
});
|
|
1187
|
+
Promise.allSettled([promise]);
|
|
1188
|
+
this.settlers.set(id, {
|
|
1189
|
+
promise,
|
|
1190
|
+
resolve: (value) => holder.resolve?.(value),
|
|
1191
|
+
reject: (error) => holder.reject?.(error)
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
settle(id, result) {
|
|
1195
|
+
const settler = this.settlers.get(id);
|
|
1196
|
+
if (settler === void 0) return;
|
|
1197
|
+
if (result instanceof Error) settler.reject(result);
|
|
1198
|
+
else settler.resolve(result);
|
|
1199
|
+
}
|
|
1200
|
+
/** The bitcoin network `account`'s Spark network settles on; any other pairing is refused. */
|
|
1201
|
+
l1Network(account) {
|
|
1202
|
+
const network = SPARK_L1_NETWORK[account.network];
|
|
1203
|
+
if (network === void 0) throw new Error(`unknown Spark network ${account.network}`);
|
|
1204
|
+
return network;
|
|
1205
|
+
}
|
|
1206
|
+
sparkBuilt(account, action) {
|
|
1207
|
+
return {
|
|
1208
|
+
account,
|
|
1209
|
+
network: action.display.network,
|
|
1210
|
+
display: action.display,
|
|
1211
|
+
checks: collectChecks(action.checks),
|
|
1212
|
+
risk: this.sparkRisk(action.display),
|
|
1213
|
+
run: {
|
|
1214
|
+
staleReason: () => action.staleReason(this.options.now()),
|
|
1215
|
+
submit: async (verify) => action.execute(this.options.spark, verify),
|
|
1216
|
+
release: () => void 0
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1096
1220
|
async build(input, id) {
|
|
1097
1221
|
if (input.method === "pay" || input.method === "zap") {
|
|
1098
1222
|
const action = input.method === "pay" ? await preparePay({
|
|
@@ -1102,17 +1226,60 @@ var RequestController = class extends Controller {
|
|
|
1102
1226
|
maxFeeSats: input.maxFeeSats,
|
|
1103
1227
|
now: this.options.now
|
|
1104
1228
|
});
|
|
1229
|
+
return this.sparkBuilt(input.account, action);
|
|
1230
|
+
}
|
|
1231
|
+
if (input.method === "spark-deposit") {
|
|
1232
|
+
const l1 = this.l1Network(input.account);
|
|
1233
|
+
if (input.from.kind !== "hd" || input.from.walletId !== input.account.walletId) throw new Error("a Spark deposit must come from the same wallet");
|
|
1234
|
+
const to = await this.options.spark.newDepositAddress(input.account);
|
|
1235
|
+
const prepared = await this.options.transactions.prepareSend({
|
|
1236
|
+
account: input.from,
|
|
1237
|
+
network: l1,
|
|
1238
|
+
to,
|
|
1239
|
+
amount: BigInt(input.amountSats),
|
|
1240
|
+
fee: input.fee
|
|
1241
|
+
}, id, {
|
|
1242
|
+
walletId: input.account.walletId,
|
|
1243
|
+
groupIndex: input.account.groupIndex,
|
|
1244
|
+
network: input.account.network
|
|
1245
|
+
});
|
|
1105
1246
|
return {
|
|
1247
|
+
account: prepared.account,
|
|
1248
|
+
network: prepared.network,
|
|
1249
|
+
display: prepared.intent,
|
|
1250
|
+
checks: collectChecks([prepared.intent.verifiability === "full" ? void 0 : "decode-partial", ...prepared.checks]),
|
|
1251
|
+
risk: [],
|
|
1252
|
+
run: prepared
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
if (input.method === "spark-exit") {
|
|
1256
|
+
const l1 = this.l1Network(input.account);
|
|
1257
|
+
const address = await this.options.transactions.nextReceiveAddress(input.account.walletId, input.account.groupIndex, l1);
|
|
1258
|
+
const action = await preparePay({
|
|
1259
|
+
fetch: this.options.fetch,
|
|
1260
|
+
now: this.options.now
|
|
1261
|
+
}, {
|
|
1106
1262
|
account: input.account,
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1263
|
+
option: {
|
|
1264
|
+
kind: "bitcoin",
|
|
1265
|
+
address
|
|
1266
|
+
},
|
|
1267
|
+
amountSats: input.amountSats,
|
|
1268
|
+
maxFeeSats: input.maxFeeSats,
|
|
1269
|
+
...input.speed === void 0 ? {} : { speed: input.speed }
|
|
1270
|
+
});
|
|
1271
|
+
return this.sparkBuilt(input.account, action);
|
|
1272
|
+
}
|
|
1273
|
+
if (input.method === "evm-sign") {
|
|
1274
|
+
const prepared = await this.options.transactions.prepareDapp(input.account, input.request);
|
|
1275
|
+
const isTransaction = input.request.kind === "evm.transaction";
|
|
1276
|
+
return {
|
|
1277
|
+
account: prepared.account,
|
|
1278
|
+
network: prepared.network,
|
|
1279
|
+
display: prepared.intent,
|
|
1280
|
+
checks: collectChecks([isTransaction && prepared.intent.verifiability !== "full" ? "decode-partial" : void 0, ...prepared.checks]),
|
|
1281
|
+
risk: isTransaction ? this.chainRisk(prepared.intent) : [],
|
|
1282
|
+
run: prepared
|
|
1116
1283
|
};
|
|
1117
1284
|
}
|
|
1118
1285
|
const prepared = input.method === "send" ? await this.options.transactions.prepareSend(input, id) : await this.options.transactions.prepareReplacement(input, id);
|
|
@@ -1165,6 +1332,7 @@ var RequestController = class extends Controller {
|
|
|
1165
1332
|
this.put(done);
|
|
1166
1333
|
this.record(done, "submitted");
|
|
1167
1334
|
} else this.record(request, "submitted");
|
|
1335
|
+
this.settle(id, result);
|
|
1168
1336
|
return result;
|
|
1169
1337
|
} catch (error) {
|
|
1170
1338
|
if (session === this.session) this.finish(id, this.failureStage(entry.prepared, error), errorMessage(error));
|
|
@@ -1226,6 +1394,7 @@ var RequestController = class extends Controller {
|
|
|
1226
1394
|
};
|
|
1227
1395
|
this.put(ended);
|
|
1228
1396
|
this.record(ended, stage, error);
|
|
1397
|
+
this.settle(id, new RequestEndedError(stage, error));
|
|
1229
1398
|
}
|
|
1230
1399
|
}
|
|
1231
1400
|
/** Appends the final stage to the signature record; a failed write is a warning, not an error. */
|
|
@@ -1245,36 +1414,592 @@ var RequestController = class extends Controller {
|
|
|
1245
1414
|
outcome,
|
|
1246
1415
|
...error === void 0 ? {} : { error }
|
|
1247
1416
|
};
|
|
1248
|
-
this.appendLog(entry);
|
|
1417
|
+
this.appendLog(entry);
|
|
1418
|
+
}
|
|
1419
|
+
async appendLog(entry) {
|
|
1420
|
+
try {
|
|
1421
|
+
await this.log.append(entry);
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
this.options.logger.warn("sign log write failed", error);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Addresses the wallet already knows for a family: contacts, plus recipients of tracked chain
|
|
1428
|
+
* sends (Spark payments are not tracked).
|
|
1429
|
+
*/
|
|
1430
|
+
knownAddresses(family) {
|
|
1431
|
+
const contacts = Object.values(this.options.addressBook.getState().entries).filter((e) => e.family === family).map((e) => e.address);
|
|
1432
|
+
if (family === "spark") return contacts;
|
|
1433
|
+
const paid = Object.values(this.options.transactions.getState().txs).filter((tx) => tx.family === family && (tx.state === "submitted" || tx.state === "pending" || tx.state === "confirmed")).flatMap((tx) => tx.intent?.outflows.map((o) => o.to) ?? []);
|
|
1434
|
+
return [...contacts, ...paid];
|
|
1435
|
+
}
|
|
1436
|
+
chainRisk(intent) {
|
|
1437
|
+
return assessRecipients(intent.family, intent.outflows.map((o) => o.to), this.knownAddresses(intent.family));
|
|
1438
|
+
}
|
|
1439
|
+
/** Only a Spark transfer has a recipient to check; Lightning and exits are not assessed. */
|
|
1440
|
+
sparkRisk(display) {
|
|
1441
|
+
if (display.kind !== "transfer") return [];
|
|
1442
|
+
return assessRecipients("spark", [normalizeAddress("spark", display.receiver) ?? display.receiver], this.knownAddresses("spark"));
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
// src/connect/eip1193.ts
|
|
1446
|
+
/**
|
|
1447
|
+
* Platform-neutral EIP-1193 provider logic: it knows nothing about WebViews. The host hands it a
|
|
1448
|
+
* page's origin (computed from the WebView's top-level URL, never from the page) and a JSON-RPC
|
|
1449
|
+
* call; signing goes through the request pipeline and waits for the host's approval by id.
|
|
1450
|
+
*/
|
|
1451
|
+
/** Refused outright, whatever the session. */
|
|
1452
|
+
const REFUSED = /* @__PURE__ */ new Set([
|
|
1453
|
+
"eth_sign",
|
|
1454
|
+
"eth_signTypedData",
|
|
1455
|
+
"eth_signTypedData_v3",
|
|
1456
|
+
"eth_signTransaction",
|
|
1457
|
+
"eth_sendRawTransaction"
|
|
1458
|
+
]);
|
|
1459
|
+
/** Planned for later layers; answered `4200` until then. */
|
|
1460
|
+
const DEFERRED = /* @__PURE__ */ new Set([
|
|
1461
|
+
"wallet_addEthereumChain",
|
|
1462
|
+
"wallet_watchAsset",
|
|
1463
|
+
"wallet_sendCalls",
|
|
1464
|
+
"wallet_getCallsStatus",
|
|
1465
|
+
"wallet_getCapabilities",
|
|
1466
|
+
"wallet_showCallsStatus"
|
|
1467
|
+
]);
|
|
1468
|
+
const READ_ONLY = new Set(EVM_RPC_METHODS.filter((m) => m !== "eth_sendRawTransaction" && m !== "eth_chainId"));
|
|
1469
|
+
const hexChainId = (network) => `0x${(evmChainId(network) ?? 0).toString(16)}`;
|
|
1470
|
+
const isObject$12 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1471
|
+
const paramList = (params) => {
|
|
1472
|
+
if (params === void 0) return [];
|
|
1473
|
+
if (!Array.isArray(params)) throw invalidParams("params must be an array");
|
|
1474
|
+
return params;
|
|
1475
|
+
};
|
|
1476
|
+
const textBytes = (value) => {
|
|
1477
|
+
if (typeof value !== "string") throw invalidParams("message must be a string");
|
|
1478
|
+
if (/^0x(?:[0-9a-fA-F]{2})*$/.test(value)) {
|
|
1479
|
+
const bytes = hexToBytes(value.slice(2));
|
|
1480
|
+
try {
|
|
1481
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1482
|
+
} catch {
|
|
1483
|
+
throw new ProviderError(4200, "blind signing is not enabled: the message is not UTF-8 text");
|
|
1484
|
+
}
|
|
1485
|
+
return bytes;
|
|
1486
|
+
}
|
|
1487
|
+
return new TextEncoder().encode(value);
|
|
1488
|
+
};
|
|
1489
|
+
const quantityOf = (value, field, fallback) => {
|
|
1490
|
+
if (value === void 0) return fallback;
|
|
1491
|
+
if (typeof value !== "string" || !/^0x[0-9a-fA-F]+$/.test(value)) throw invalidParams(`${field} must be a 0x quantity`);
|
|
1492
|
+
return value;
|
|
1493
|
+
};
|
|
1494
|
+
var Eip1193Handler = class {
|
|
1495
|
+
options;
|
|
1496
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1497
|
+
unsubscribe;
|
|
1498
|
+
constructor(options) {
|
|
1499
|
+
this.options = options;
|
|
1500
|
+
}
|
|
1501
|
+
/** Events for the host to forward to pages; derived from changes to the sessions. */
|
|
1502
|
+
subscribe(listener) {
|
|
1503
|
+
this.listeners.add(listener);
|
|
1504
|
+
if (this.unsubscribe === void 0) {
|
|
1505
|
+
let previous = this.options.connections.getState().sessions;
|
|
1506
|
+
this.unsubscribe = this.options.connections.store.subscribe((state) => {
|
|
1507
|
+
for (const event of diffSessions(previous, state.sessions)) for (const l of this.listeners) l(event);
|
|
1508
|
+
previous = state.sessions;
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
return () => {
|
|
1512
|
+
this.listeners.delete(listener);
|
|
1513
|
+
if (this.listeners.size === 0) {
|
|
1514
|
+
this.unsubscribe?.();
|
|
1515
|
+
this.unsubscribe = void 0;
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
async handle(origin, request) {
|
|
1520
|
+
const { connections } = this.options;
|
|
1521
|
+
connections.checkOrigin(origin);
|
|
1522
|
+
const { method } = request;
|
|
1523
|
+
const params = paramList(request.params);
|
|
1524
|
+
if (REFUSED.has(method) || DEFERRED.has(method)) throw unsupportedMethod(method);
|
|
1525
|
+
switch (method) {
|
|
1526
|
+
case "eth_accounts": return connections.accounts(origin);
|
|
1527
|
+
case "eth_chainId": return hexChainId(connections.session(origin)?.currentNetwork ?? this.options.defaultNetwork());
|
|
1528
|
+
case "eth_requestAccounts": return this.requestAccounts(origin);
|
|
1529
|
+
case "wallet_requestPermissions":
|
|
1530
|
+
await this.requestAccounts(origin);
|
|
1531
|
+
return this.permissions(origin);
|
|
1532
|
+
case "wallet_getPermissions": return this.permissions(origin);
|
|
1533
|
+
case "wallet_revokePermissions":
|
|
1534
|
+
await connections.revoke(origin);
|
|
1535
|
+
return null;
|
|
1536
|
+
case "wallet_switchEthereumChain": return this.switchChain(origin, params[0]);
|
|
1537
|
+
case "personal_sign": return this.personalSign(origin, params);
|
|
1538
|
+
case "eth_signTypedData_v4": return this.typedData(origin, params);
|
|
1539
|
+
case "eth_sendTransaction": return this.sendTransaction(origin, params[0]);
|
|
1540
|
+
}
|
|
1541
|
+
if (READ_ONLY.has(method)) return this.read(origin, method, params);
|
|
1542
|
+
throw new ProviderError(-32601, `${method} does not exist`);
|
|
1543
|
+
}
|
|
1544
|
+
async requestAccounts(origin) {
|
|
1545
|
+
const { connections } = this.options;
|
|
1546
|
+
if (connections.accounts(origin).length === 0) await connections.request(origin, "connect");
|
|
1547
|
+
await connections.touch(origin);
|
|
1548
|
+
return connections.accounts(origin);
|
|
1549
|
+
}
|
|
1550
|
+
permissions(origin) {
|
|
1551
|
+
const accounts = this.options.connections.accounts(origin);
|
|
1552
|
+
if (accounts.length === 0) return [];
|
|
1553
|
+
return [{
|
|
1554
|
+
parentCapability: "eth_accounts",
|
|
1555
|
+
invoker: origin,
|
|
1556
|
+
caveats: [{
|
|
1557
|
+
type: "restrictReturnedAccounts",
|
|
1558
|
+
value: accounts
|
|
1559
|
+
}]
|
|
1560
|
+
}];
|
|
1561
|
+
}
|
|
1562
|
+
async switchChain(origin, arg) {
|
|
1563
|
+
const { connections } = this.options;
|
|
1564
|
+
const chainId = isObject$12(arg) ? arg["chainId"] : void 0;
|
|
1565
|
+
if (typeof chainId !== "string" || !/^0x[0-9a-fA-F]+$/.test(chainId)) throw invalidParams("chainId must be a 0x quantity");
|
|
1566
|
+
const network = `eip155:${Number.parseInt(chainId, 16)}`;
|
|
1567
|
+
if (!connections.isEnabled(network)) throw unrecognizedChain(`chain ${chainId} is not enabled`);
|
|
1568
|
+
const session = connections.session(origin);
|
|
1569
|
+
if (session === void 0) throw unauthorized();
|
|
1570
|
+
if (session.scopes[network] === void 0) await connections.request(origin, "network", network);
|
|
1571
|
+
else if (session.currentNetwork !== network) await connections.setCurrentNetwork(origin, network);
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
authorizedAccount(session, address) {
|
|
1575
|
+
const accounts = this.options.connections.accounts(session.origin.origin);
|
|
1576
|
+
const wanted = typeof address === "string" ? address.toLowerCase() : accounts[0];
|
|
1577
|
+
if (wanted === void 0 || !accounts.some((a) => a.toLowerCase() === wanted.toLowerCase())) throw unauthorized("account not authorized");
|
|
1578
|
+
return wanted;
|
|
1579
|
+
}
|
|
1580
|
+
async personalSign(origin, params) {
|
|
1581
|
+
const session = this.signingSession(origin, "personal_sign");
|
|
1582
|
+
this.authorizedAccount(session, params[1]);
|
|
1583
|
+
const message = textBytes(params[0]);
|
|
1584
|
+
return this.sign(origin, session, (account) => ({
|
|
1585
|
+
kind: "evm.personal-message",
|
|
1586
|
+
network: session.currentNetwork,
|
|
1587
|
+
account,
|
|
1588
|
+
message
|
|
1589
|
+
}));
|
|
1590
|
+
}
|
|
1591
|
+
async typedData(origin, params) {
|
|
1592
|
+
const session = this.signingSession(origin, "eth_signTypedData_v4");
|
|
1593
|
+
this.authorizedAccount(session, params[0]);
|
|
1594
|
+
const [, source] = params;
|
|
1595
|
+
const typedData = typeof source === "string" ? source : JSON.stringify(source);
|
|
1596
|
+
return this.sign(origin, session, (account) => ({
|
|
1597
|
+
kind: "evm.typed-data",
|
|
1598
|
+
network: session.currentNetwork,
|
|
1599
|
+
account,
|
|
1600
|
+
typedData
|
|
1601
|
+
}));
|
|
1602
|
+
}
|
|
1603
|
+
async sendTransaction(origin, arg) {
|
|
1604
|
+
const session = this.signingSession(origin, "eth_sendTransaction");
|
|
1605
|
+
if (!isObject$12(arg)) throw invalidParams("transaction must be an object");
|
|
1606
|
+
this.authorizedAccount(session, arg["from"]);
|
|
1607
|
+
const chainId = evmChainId(session.currentNetwork);
|
|
1608
|
+
if (chainId === void 0) throw internalError("session network is not an EVM network");
|
|
1609
|
+
if (arg["chainId"] !== void 0 && Number(arg["chainId"]) !== chainId) throw invalidParams("transaction chainId does not match the session network");
|
|
1610
|
+
const data = arg["data"] ?? arg["input"] ?? "0x";
|
|
1611
|
+
if (typeof data !== "string") throw invalidParams("data must be a hex string");
|
|
1612
|
+
const { to } = arg;
|
|
1613
|
+
if (to !== void 0 && typeof to !== "string") throw invalidParams("to must be an address");
|
|
1614
|
+
return this.sign(origin, session, (account) => ({
|
|
1615
|
+
kind: "evm.transaction",
|
|
1616
|
+
network: session.currentNetwork,
|
|
1617
|
+
account,
|
|
1618
|
+
tx: {
|
|
1619
|
+
type: "eip1559",
|
|
1620
|
+
chainId,
|
|
1621
|
+
nonce: 0,
|
|
1622
|
+
gas: quantityOf(arg["gas"], "gas", "0x0"),
|
|
1623
|
+
value: quantityOf(arg["value"], "value", "0x0"),
|
|
1624
|
+
data,
|
|
1625
|
+
...to === void 0 ? {} : { to },
|
|
1626
|
+
accessList: [],
|
|
1627
|
+
maxFeePerGas: "0x0",
|
|
1628
|
+
maxPriorityFeePerGas: "0x0"
|
|
1629
|
+
}
|
|
1630
|
+
}));
|
|
1631
|
+
}
|
|
1632
|
+
signingSession(origin, method) {
|
|
1633
|
+
const { connections } = this.options;
|
|
1634
|
+
const session = connections.session(origin);
|
|
1635
|
+
if (session === void 0) throw unauthorized();
|
|
1636
|
+
return connections.authorize(origin, session.currentNetwork, method);
|
|
1637
|
+
}
|
|
1638
|
+
async sign(origin, session, build) {
|
|
1639
|
+
const { connections, requests } = this.options;
|
|
1640
|
+
const group = connections.activeGroup();
|
|
1641
|
+
if (group === void 0) throw internalError("no account group is selected");
|
|
1642
|
+
const account = {
|
|
1643
|
+
kind: "hd",
|
|
1644
|
+
...group,
|
|
1645
|
+
template: "evm.bip44"
|
|
1646
|
+
};
|
|
1647
|
+
try {
|
|
1648
|
+
const pending = await requests.prepare({
|
|
1649
|
+
method: "evm-sign",
|
|
1650
|
+
origin: {
|
|
1651
|
+
kind: "browser",
|
|
1652
|
+
origin
|
|
1653
|
+
},
|
|
1654
|
+
account,
|
|
1655
|
+
network: session.currentNetwork,
|
|
1656
|
+
request: build(account)
|
|
1657
|
+
});
|
|
1658
|
+
const result = await requests.outcome(pending.id);
|
|
1659
|
+
await connections.touch(origin);
|
|
1660
|
+
if ("signature" in result && result.signature instanceof Uint8Array) return `0x${bytesToHex(result.signature)}`;
|
|
1661
|
+
if (!("hash" in result) || typeof result.hash !== "string") throw internalError("the transaction has no hash");
|
|
1662
|
+
return result.hash;
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
throw toProviderError(error);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
async read(origin, method, params) {
|
|
1668
|
+
const { connections, networks } = this.options;
|
|
1669
|
+
const network = connections.session(origin)?.currentNetwork ?? this.options.defaultNetwork();
|
|
1670
|
+
try {
|
|
1671
|
+
return await networks.client(network).transport.request({
|
|
1672
|
+
method,
|
|
1673
|
+
params: [...params]
|
|
1674
|
+
});
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
throw toProviderError(error);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
function toProviderError(error) {
|
|
1681
|
+
if (error instanceof ProviderError) return error;
|
|
1682
|
+
if (error instanceof OriginBusyError) return userRejectedRequest(error.message);
|
|
1683
|
+
if (error instanceof RequestEndedError) return error.stage === "failed" ? internalError(error.message) : userRejectedRequest(error.message);
|
|
1684
|
+
if (error instanceof ChainError) return invalidParams(error.message);
|
|
1685
|
+
return internalError(error instanceof Error ? error.message : String(error));
|
|
1686
|
+
}
|
|
1687
|
+
function currentAccounts(session) {
|
|
1688
|
+
return session.scopes[session.currentNetwork]?.accounts.map((a) => a.slice(a.lastIndexOf(":") + 1)) ?? [];
|
|
1689
|
+
}
|
|
1690
|
+
const byOrigin = (sessions) => new Map(Object.values(sessions).map((s) => [s.origin.origin, s]));
|
|
1691
|
+
function diffSessions(before, after) {
|
|
1692
|
+
const events = [];
|
|
1693
|
+
const prev = byOrigin(before);
|
|
1694
|
+
const next = byOrigin(after);
|
|
1695
|
+
for (const [origin, session] of prev) {
|
|
1696
|
+
const now = next.get(origin);
|
|
1697
|
+
if (now === void 0) {
|
|
1698
|
+
events.push({
|
|
1699
|
+
name: "disconnect",
|
|
1700
|
+
origin
|
|
1701
|
+
});
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
if (now.currentNetwork !== session.currentNetwork) events.push({
|
|
1705
|
+
name: "chainChanged",
|
|
1706
|
+
origin,
|
|
1707
|
+
chainId: hexChainId(now.currentNetwork)
|
|
1708
|
+
});
|
|
1709
|
+
if (currentAccounts(session).join(",") !== currentAccounts(now).join(",")) events.push({
|
|
1710
|
+
name: "accountsChanged",
|
|
1711
|
+
origin,
|
|
1712
|
+
accounts: currentAccounts(now)
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
for (const [origin, session] of next) if (!prev.has(origin)) events.push({
|
|
1716
|
+
name: "connect",
|
|
1717
|
+
origin,
|
|
1718
|
+
chainId: hexChainId(session.currentNetwork)
|
|
1719
|
+
});
|
|
1720
|
+
return events;
|
|
1721
|
+
}
|
|
1722
|
+
// src/addresses/errors.ts
|
|
1723
|
+
var AddressBookError = class extends Error {
|
|
1724
|
+
category;
|
|
1725
|
+
constructor(category, message) {
|
|
1726
|
+
super(message);
|
|
1727
|
+
this.name = "AddressBookError";
|
|
1728
|
+
this.category = category;
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
// src/addresses/record.ts
|
|
1732
|
+
/**
|
|
1733
|
+
* The address-book record shared by the controller, the sync merge and the metadata document: a
|
|
1734
|
+
* live entry, or a tombstone that carries a deletion to the other devices.
|
|
1735
|
+
*/
|
|
1736
|
+
const isTombstone = (record) => "deleted" in record;
|
|
1737
|
+
const isFamily = (v) => v === "evm" || v === "solana" || v === "bitcoin" || v === "spark";
|
|
1738
|
+
const ENTRY_KEYS = /* @__PURE__ */ new Set([
|
|
1739
|
+
"id",
|
|
1740
|
+
"name",
|
|
1741
|
+
"family",
|
|
1742
|
+
"address",
|
|
1743
|
+
"note",
|
|
1744
|
+
"createdAt",
|
|
1745
|
+
"updatedAt"
|
|
1746
|
+
]);
|
|
1747
|
+
const TOMBSTONE_KEYS = /* @__PURE__ */ new Set([
|
|
1748
|
+
"id",
|
|
1749
|
+
"deleted",
|
|
1750
|
+
"updatedAt"
|
|
1751
|
+
]);
|
|
1752
|
+
const isObject$11 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1753
|
+
const isNonNegativeInt$2 = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
1754
|
+
/** Trimmed name, or `undefined` when it is not 1 to {@link NAME_MAX} characters. */
|
|
1755
|
+
const cleanName = (raw) => {
|
|
1756
|
+
const name = raw.trim();
|
|
1757
|
+
return name.length > 0 && name.length <= 32 ? name : void 0;
|
|
1758
|
+
};
|
|
1759
|
+
/** Trimmed note; an empty one means no note. `undefined` return with `ok: false` when too long. */
|
|
1760
|
+
const cleanNote = (raw) => {
|
|
1761
|
+
const note = raw?.trim();
|
|
1762
|
+
if (note === void 0 || note.length === 0) return {
|
|
1763
|
+
ok: true,
|
|
1764
|
+
note: void 0
|
|
1765
|
+
};
|
|
1766
|
+
return note.length <= 120 ? {
|
|
1767
|
+
ok: true,
|
|
1768
|
+
note
|
|
1769
|
+
} : { ok: false };
|
|
1770
|
+
};
|
|
1771
|
+
const hasOnly = (value, keys) => Object.keys(value).every((k) => keys.has(k));
|
|
1772
|
+
/**
|
|
1773
|
+
* Strict decode with the controller's rules; the address is re-normalized. Anything else is
|
|
1774
|
+
* `undefined`.
|
|
1775
|
+
*/
|
|
1776
|
+
function decodeRecord(value) {
|
|
1777
|
+
if (!isObject$11(value) || typeof value["id"] !== "string" || !isNonNegativeInt$2(value["updatedAt"])) return;
|
|
1778
|
+
if ("deleted" in value) return value["deleted"] === true && hasOnly(value, TOMBSTONE_KEYS) ? {
|
|
1779
|
+
id: value["id"],
|
|
1780
|
+
deleted: true,
|
|
1781
|
+
updatedAt: value["updatedAt"]
|
|
1782
|
+
} : void 0;
|
|
1783
|
+
const { family, name, address, note } = value;
|
|
1784
|
+
if (!hasOnly(value, ENTRY_KEYS) || !isFamily(family) || typeof name !== "string" || typeof address !== "string" || !isNonNegativeInt$2(value["createdAt"]) || note !== void 0 && typeof note !== "string") return;
|
|
1785
|
+
const cleanedName = cleanName(name);
|
|
1786
|
+
const normalized = normalizeAddress(family, address);
|
|
1787
|
+
const cleanedNote = cleanNote(note);
|
|
1788
|
+
if (cleanedName === void 0 || normalized === void 0 || !cleanedNote.ok || note !== void 0 && cleanedNote.note === void 0) return;
|
|
1789
|
+
return {
|
|
1790
|
+
id: value["id"],
|
|
1791
|
+
name: cleanedName,
|
|
1792
|
+
family,
|
|
1793
|
+
address: normalized,
|
|
1794
|
+
...cleanedNote.note === void 0 ? {} : { note: cleanedNote.note },
|
|
1795
|
+
createdAt: value["createdAt"],
|
|
1796
|
+
updatedAt: value["updatedAt"]
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const serialized = (record) => JSON.stringify(record);
|
|
1800
|
+
/** The winner of two versions of one id: larger `updatedAt`, then the larger serialization. */
|
|
1801
|
+
function pickWinner(a, b) {
|
|
1802
|
+
if (a.updatedAt !== b.updatedAt) return a.updatedAt > b.updatedAt ? a : b;
|
|
1803
|
+
return serialized(a) >= serialized(b) ? a : b;
|
|
1804
|
+
}
|
|
1805
|
+
const pruneTombstones = (records, now) => records.filter((r) => !isTombstone(r) || now - r.updatedAt <= 7776e6);
|
|
1806
|
+
/** Newest first, id as the tiebreak, so every device orders (and truncates) the same way. */
|
|
1807
|
+
const newestFirst = (a, b) => b.updatedAt - a.updatedAt || (a.id < b.id ? -1 : 1);
|
|
1808
|
+
/** Drops the oldest tombstones until at most `max` records remain, or no tombstone is left. */
|
|
1809
|
+
function evictTombstones(records, max) {
|
|
1810
|
+
const excess = records.length - max;
|
|
1811
|
+
if (excess <= 0) return records;
|
|
1812
|
+
const oldest = new Set(records.filter(isTombstone).sort((a, b) => a.updatedAt - b.updatedAt || (a.id < b.id ? -1 : 1)).slice(0, excess).map((r) => r.id));
|
|
1813
|
+
return records.filter((r) => !oldest.has(r.id));
|
|
1814
|
+
}
|
|
1815
|
+
/** {@link mergeRecords}, also counting the live entries the cap forced out. */
|
|
1816
|
+
function mergeWithReport(local, remote, now) {
|
|
1817
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1818
|
+
for (const record of [...local, ...remote]) {
|
|
1819
|
+
const existing = byId.get(record.id);
|
|
1820
|
+
byId.set(record.id, existing === void 0 ? record : pickWinner(existing, record));
|
|
1821
|
+
}
|
|
1822
|
+
const live = /* @__PURE__ */ new Map();
|
|
1823
|
+
const kept = [];
|
|
1824
|
+
for (const record of [...pruneTombstones([...byId.values()], now)].sort(newestFirst)) {
|
|
1825
|
+
if (isTombstone(record)) {
|
|
1826
|
+
kept.push(record);
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
const key = `${record.family}:${record.address}`;
|
|
1830
|
+
if (!live.has(key)) {
|
|
1831
|
+
live.set(key, record);
|
|
1832
|
+
kept.push(record);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const evicted = evictTombstones(kept, 128);
|
|
1836
|
+
return {
|
|
1837
|
+
records: [...evicted.slice(0, 128)].sort((a, b) => a.id < b.id ? -1 : 1),
|
|
1838
|
+
droppedLive: evicted.slice(128).filter((r) => !isTombstone(r)).length
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
// src/addresses/controller.ts
|
|
1842
|
+
/**
|
|
1843
|
+
* Address book: named recipients, validated and normalized per chain family, persisted in the
|
|
1844
|
+
* engine store and cleared from memory on lock. Deletions are kept as tombstones so they reach the
|
|
1845
|
+
* other devices; `syncEntries`/`merge` are the whole surface the metadata sync needs. Request risk
|
|
1846
|
+
* checks treat its addresses as known.
|
|
1847
|
+
*/
|
|
1848
|
+
const COLLECTION$2 = "contacts";
|
|
1849
|
+
const entriesOf = (records) => Object.fromEntries(records.flatMap((r) => isTombstone(r) ? [] : [[r.id, r]]));
|
|
1850
|
+
var AddressBookController = class extends Controller {
|
|
1851
|
+
options;
|
|
1852
|
+
engineStore;
|
|
1853
|
+
/** Every record, tombstones included, by id. */
|
|
1854
|
+
records = /* @__PURE__ */ new Map();
|
|
1855
|
+
constructor(options) {
|
|
1856
|
+
super({
|
|
1857
|
+
name: "address-book",
|
|
1858
|
+
defaultState: () => ({ entries: {} })
|
|
1859
|
+
});
|
|
1860
|
+
this.options = options;
|
|
1861
|
+
}
|
|
1862
|
+
async init() {
|
|
1863
|
+
const store = await this.options.openStore();
|
|
1864
|
+
this.engineStore = store;
|
|
1865
|
+
const collection = store.collection(COLLECTION$2);
|
|
1866
|
+
const keys = await collection.list();
|
|
1867
|
+
const rows = await Promise.all(keys.map(async (key) => collection.get(key)));
|
|
1868
|
+
const records = /* @__PURE__ */ new Map();
|
|
1869
|
+
for (const raw of rows) {
|
|
1870
|
+
if (raw === void 0) continue;
|
|
1871
|
+
const record = decodeRecord(JSON.parse(new TextDecoder().decode(raw)));
|
|
1872
|
+
if (record === void 0) this.options.logger.warn("address book record skipped: malformed");
|
|
1873
|
+
else records.set(record.id, record);
|
|
1874
|
+
}
|
|
1875
|
+
this.records = records;
|
|
1876
|
+
this.update(() => ({ entries: entriesOf([...records.values()]) }));
|
|
1877
|
+
}
|
|
1878
|
+
async destroy() {
|
|
1879
|
+
this.lock();
|
|
1880
|
+
await Promise.resolve();
|
|
1881
|
+
}
|
|
1882
|
+
lock() {
|
|
1883
|
+
this.engineStore = void 0;
|
|
1884
|
+
this.records = /* @__PURE__ */ new Map();
|
|
1885
|
+
this.update(() => ({ entries: {} }));
|
|
1886
|
+
}
|
|
1887
|
+
/** Entries and tombstones, for the metadata document. */
|
|
1888
|
+
syncEntries() {
|
|
1889
|
+
return [...this.records.values()].sort((a, b) => a.id < b.id ? -1 : 1);
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Merges records from another device (last writer wins, tombstones win over older entries).
|
|
1893
|
+
* Returns whether anything local changed and whether the merged set holds something `remote`
|
|
1894
|
+
* lacks, i.e. whether the remote copy needs a newer publish.
|
|
1895
|
+
*/
|
|
1896
|
+
async merge(remote) {
|
|
1897
|
+
const store = this.requireStore();
|
|
1898
|
+
const before = JSON.stringify(this.syncEntries());
|
|
1899
|
+
const { records: merged, droppedLive } = mergeWithReport(this.syncEntries(), remote, this.options.now());
|
|
1900
|
+
if (droppedLive > 0) this.options.logger.warn(`address book sync: ${droppedLive} contacts dropped, over the 128-record cap`);
|
|
1901
|
+
const remoteSorted = JSON.stringify([...remote].sort((a, b) => a.id < b.id ? -1 : 1));
|
|
1902
|
+
const localNewer = JSON.stringify(merged) !== remoteSorted;
|
|
1903
|
+
if (JSON.stringify(merged) === before) return {
|
|
1904
|
+
changed: false,
|
|
1905
|
+
localNewer
|
|
1906
|
+
};
|
|
1907
|
+
const collection = store.collection(COLLECTION$2);
|
|
1908
|
+
const next = new Map(merged.map((r) => [r.id, r]));
|
|
1909
|
+
await Promise.all([...merged.map(async (r) => collection.put(r.id, new TextEncoder().encode(JSON.stringify(r)))), ...[...this.records.keys()].filter((id) => !next.has(id)).map(async (id) => collection.delete(id))]);
|
|
1910
|
+
this.records = next;
|
|
1911
|
+
this.update(() => ({ entries: entriesOf(merged) }));
|
|
1912
|
+
return {
|
|
1913
|
+
changed: true,
|
|
1914
|
+
localNewer
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
async add(params) {
|
|
1918
|
+
const store = this.requireStore();
|
|
1919
|
+
const name = cleanName(params.name);
|
|
1920
|
+
if (name === void 0) throw new AddressBookError("invalid-input", `name must be 1 to 32 characters`);
|
|
1921
|
+
const note = this.checkedNote(params.note);
|
|
1922
|
+
const address = normalizeAddress(params.family, params.address.trim());
|
|
1923
|
+
if (address === void 0) throw new AddressBookError("invalid-input", `not a valid ${params.family} address`);
|
|
1924
|
+
if (Object.values(this.getState().entries).some((e) => e.family === params.family && e.address === address)) throw new AddressBookError("duplicate", "this address is already in the address book");
|
|
1925
|
+
if (this.records.size >= 128) {
|
|
1926
|
+
const kept = evictTombstones([...this.records.values()], 127);
|
|
1927
|
+
if (kept.length >= 128) throw new AddressBookError("limit", `the address book holds at most 128 contacts`);
|
|
1928
|
+
const keptIds = new Set(kept.map((r) => r.id));
|
|
1929
|
+
await Promise.all([...this.records.keys()].filter((id) => !keptIds.has(id)).map(async (id) => store.collection(COLLECTION$2).delete(id)));
|
|
1930
|
+
this.records = new Map(kept.map((r) => [r.id, r]));
|
|
1931
|
+
}
|
|
1932
|
+
const now = this.options.now();
|
|
1933
|
+
const entry = {
|
|
1934
|
+
id: `contact-${[...this.options.random(8)].map((b) => b.toString(16).padStart(2, "0")).join("")}`,
|
|
1935
|
+
name,
|
|
1936
|
+
family: params.family,
|
|
1937
|
+
address,
|
|
1938
|
+
...note === void 0 ? {} : { note },
|
|
1939
|
+
createdAt: now,
|
|
1940
|
+
updatedAt: now
|
|
1941
|
+
};
|
|
1942
|
+
await this.persist(store, entry);
|
|
1943
|
+
return entry;
|
|
1944
|
+
}
|
|
1945
|
+
async rename(id, name) {
|
|
1946
|
+
const store = this.requireStore();
|
|
1947
|
+
const existing = this.requireEntry(id);
|
|
1948
|
+
const cleaned = cleanName(name);
|
|
1949
|
+
if (cleaned === void 0) throw new AddressBookError("invalid-input", `name must be 1 to 32 characters`);
|
|
1950
|
+
const entry = {
|
|
1951
|
+
...existing,
|
|
1952
|
+
name: cleaned,
|
|
1953
|
+
updatedAt: this.options.now()
|
|
1954
|
+
};
|
|
1955
|
+
await this.persist(store, entry);
|
|
1956
|
+
return entry;
|
|
1957
|
+
}
|
|
1958
|
+
/** Sets or clears (empty or `undefined`) the note. */
|
|
1959
|
+
async setNote(id, note) {
|
|
1960
|
+
const store = this.requireStore();
|
|
1961
|
+
const existing = this.requireEntry(id);
|
|
1962
|
+
const cleaned = this.checkedNote(note);
|
|
1963
|
+
const { note: _previous, ...rest } = existing;
|
|
1964
|
+
const entry = {
|
|
1965
|
+
...rest,
|
|
1966
|
+
...cleaned === void 0 ? {} : { note: cleaned },
|
|
1967
|
+
updatedAt: this.options.now()
|
|
1968
|
+
};
|
|
1969
|
+
await this.persist(store, entry);
|
|
1970
|
+
return entry;
|
|
1249
1971
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1972
|
+
/** Removes the entry, leaving a tombstone so other devices drop it too. */
|
|
1973
|
+
async remove(id) {
|
|
1974
|
+
const store = this.requireStore();
|
|
1975
|
+
this.requireEntry(id);
|
|
1976
|
+
await this.persist(store, {
|
|
1977
|
+
id,
|
|
1978
|
+
deleted: true,
|
|
1979
|
+
updatedAt: this.options.now()
|
|
1980
|
+
});
|
|
1256
1981
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
knownAddresses(family) {
|
|
1262
|
-
const contacts = Object.values(this.options.addressBook.getState().entries).filter((e) => e.family === family).map((e) => e.address);
|
|
1263
|
-
if (family === "spark") return contacts;
|
|
1264
|
-
const paid = Object.values(this.options.transactions.getState().txs).filter((tx) => tx.family === family && (tx.state === "submitted" || tx.state === "pending" || tx.state === "confirmed")).flatMap((tx) => tx.intent?.outflows.map((o) => o.to) ?? []);
|
|
1265
|
-
return [...contacts, ...paid];
|
|
1982
|
+
checkedNote(note) {
|
|
1983
|
+
const cleaned = cleanNote(note);
|
|
1984
|
+
if (!cleaned.ok) throw new AddressBookError("invalid-input", `note must be at most 120 characters`);
|
|
1985
|
+
return cleaned.note;
|
|
1266
1986
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1987
|
+
requireEntry(id) {
|
|
1988
|
+
const entry = this.getState().entries[id];
|
|
1989
|
+
if (entry === void 0) throw new AddressBookError("not-found", `unknown contact ${id}`);
|
|
1990
|
+
return entry;
|
|
1269
1991
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1992
|
+
async persist(store, record) {
|
|
1993
|
+
await store.collection(COLLECTION$2).put(record.id, new TextEncoder().encode(JSON.stringify(record)));
|
|
1994
|
+
this.records.set(record.id, record);
|
|
1995
|
+
this.update(() => ({ entries: entriesOf([...this.records.values()]) }));
|
|
1996
|
+
}
|
|
1997
|
+
requireStore() {
|
|
1998
|
+
if (this.engineStore === void 0) throw new AddressBookError("locked", "the address book requires an unlocked vault");
|
|
1999
|
+
return this.engineStore;
|
|
1274
2000
|
}
|
|
1275
2001
|
};
|
|
1276
|
-
|
|
1277
|
-
//#region src/chains/bitcoin-scan.ts
|
|
2002
|
+
// src/chains/bitcoin-scan.ts
|
|
1278
2003
|
/**
|
|
1279
2004
|
* Gap-20 BIP-44/86 account scanning over Esplora — derives /0/* and /1/* from the account xpub,
|
|
1280
2005
|
* walks each chain in windows until 20 consecutive unused addresses, persists the discovered
|
|
@@ -1282,6 +2007,7 @@ var RequestController = class extends Controller {
|
|
|
1282
2007
|
* receive/change address.
|
|
1283
2008
|
*/
|
|
1284
2009
|
const GAP = 20;
|
|
2010
|
+
const COLL = "btc-scan";
|
|
1285
2011
|
const isObject$10 = (v) => typeof v === "object" && v !== null;
|
|
1286
2012
|
const isUnknownArray = (v) => Array.isArray(v);
|
|
1287
2013
|
const asArray = (v) => isUnknownArray(v) ? v : [];
|
|
@@ -1334,8 +2060,45 @@ const scanUtxos = async (es, addresses, params) => {
|
|
|
1334
2060
|
const candidates = addresses.filter((a) => a.used || a.chain === 1 || a.index < GAP);
|
|
1335
2061
|
return Promise.all(candidates.map(async (a) => scanAddressUtxos(es, a, params))).then((r) => r.flat());
|
|
1336
2062
|
};
|
|
2063
|
+
var BitcoinScanStore = class {
|
|
2064
|
+
engineStore;
|
|
2065
|
+
async unlock(store) {
|
|
2066
|
+
this.engineStore = store;
|
|
2067
|
+
const raw = await store.collection(COLL).get("state");
|
|
2068
|
+
const parsed = raw === void 0 ? void 0 : JSON.parse(new TextDecoder().decode(raw));
|
|
2069
|
+
if (!isObject$10(parsed) || !Array.isArray(parsed["addresses"]) || !Array.isArray(parsed["utxos"])) return;
|
|
2070
|
+
return {
|
|
2071
|
+
addresses: asArray(parsed["addresses"]).flatMap((a) => toScanAddress(a)),
|
|
2072
|
+
utxos: asArray(parsed["utxos"]).flatMap((u) => toScanUtxo(u)),
|
|
2073
|
+
scannedAt: typeof parsed["scannedAt"] === "number" ? parsed["scannedAt"] : 0
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
async persist(state) {
|
|
2077
|
+
const store = this.engineStore;
|
|
2078
|
+
if (store !== void 0) await store.collection(COLL).put("state", new TextEncoder().encode(JSON.stringify(state)));
|
|
2079
|
+
}
|
|
2080
|
+
nextUnused(state, chain) {
|
|
2081
|
+
return state.addresses.find((a) => a.chain === chain && !a.used);
|
|
2082
|
+
}
|
|
2083
|
+
lock() {
|
|
2084
|
+
this.engineStore = void 0;
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
1337
2087
|
const str$1 = (v) => typeof v === "string" ? v : void 0;
|
|
1338
2088
|
const num = (v) => typeof v === "number" ? v : void 0;
|
|
2089
|
+
const toScanAddress = (v) => {
|
|
2090
|
+
if (!isObject$10(v)) return [];
|
|
2091
|
+
const chain = num(v["chain"]);
|
|
2092
|
+
const index = num(v["index"]);
|
|
2093
|
+
const address = str$1(v["address"]);
|
|
2094
|
+
const used = v["used"] === true;
|
|
2095
|
+
return chain !== void 0 && index !== void 0 && address !== void 0 && (chain === 0 || chain === 1) ? [{
|
|
2096
|
+
chain: chain === 0 ? 0 : 1,
|
|
2097
|
+
index,
|
|
2098
|
+
address,
|
|
2099
|
+
used
|
|
2100
|
+
}] : [];
|
|
2101
|
+
};
|
|
1339
2102
|
const toScanUtxo = (v) => {
|
|
1340
2103
|
if (!isObject$10(v)) return [];
|
|
1341
2104
|
const txid = str$1(v["txid"]);
|
|
@@ -1452,8 +2215,7 @@ var BtcUtxoStore = class BtcUtxoStore {
|
|
|
1452
2215
|
this.engineStore = void 0;
|
|
1453
2216
|
}
|
|
1454
2217
|
};
|
|
1455
|
-
|
|
1456
|
-
//#region src/accounts/errors.ts
|
|
2218
|
+
// src/accounts/errors.ts
|
|
1457
2219
|
var AccountTreeError = class extends Error {
|
|
1458
2220
|
category;
|
|
1459
2221
|
constructor(category, message, cause) {
|
|
@@ -1462,8 +2224,7 @@ var AccountTreeError = class extends Error {
|
|
|
1462
2224
|
this.category = category;
|
|
1463
2225
|
}
|
|
1464
2226
|
};
|
|
1465
|
-
|
|
1466
|
-
//#region src/accounts/controller.ts
|
|
2227
|
+
// src/accounts/controller.ts
|
|
1467
2228
|
const EMPTY_TREE = {
|
|
1468
2229
|
wallets: {},
|
|
1469
2230
|
groups: {},
|
|
@@ -2278,8 +3039,7 @@ function persistedToState(tree) {
|
|
|
2278
3039
|
totals: tree.totals
|
|
2279
3040
|
};
|
|
2280
3041
|
}
|
|
2281
|
-
|
|
2282
|
-
//#region src/chains/evm.ts
|
|
3042
|
+
// src/chains/evm.ts
|
|
2283
3043
|
/**
|
|
2284
3044
|
* EVM chain adapter: native transfers through a viem PublicClient injected by NetworkController.
|
|
2285
3045
|
* Fee tiers come from eth_feeHistory (20 blocks, p10/p50/p90 → slow/normal/fast, maxFee = 2×baseFee +
|
|
@@ -2344,6 +3104,15 @@ const checkedAddress = (v) => {
|
|
|
2344
3104
|
return v;
|
|
2345
3105
|
};
|
|
2346
3106
|
const hex$1 = (v) => numberToHex(v);
|
|
3107
|
+
const evmRpc = async (client, method, params) => client.transport.request({
|
|
3108
|
+
method,
|
|
3109
|
+
params
|
|
3110
|
+
});
|
|
3111
|
+
const evmRpcHex = async (client, method, params) => {
|
|
3112
|
+
const result = await evmRpc(client, method, params);
|
|
3113
|
+
if (typeof result !== "string") throw new TypeError(`${method} returned a malformed result`);
|
|
3114
|
+
return result;
|
|
3115
|
+
};
|
|
2347
3116
|
var EvmChainAdapter = class {
|
|
2348
3117
|
family = "evm";
|
|
2349
3118
|
nonces = /* @__PURE__ */ new Map();
|
|
@@ -2359,20 +3128,9 @@ var EvmChainAdapter = class {
|
|
|
2359
3128
|
this.nonces.set(key, next);
|
|
2360
3129
|
return next;
|
|
2361
3130
|
}
|
|
2362
|
-
async rpc(client, method, params) {
|
|
2363
|
-
return client.transport.request({
|
|
2364
|
-
method,
|
|
2365
|
-
params
|
|
2366
|
-
});
|
|
2367
|
-
}
|
|
2368
|
-
async rpcHex(client, method, params) {
|
|
2369
|
-
const result = await this.rpc(client, method, params);
|
|
2370
|
-
if (typeof result !== "string") throw new TypeError(`${method} returned a malformed result`);
|
|
2371
|
-
return result;
|
|
2372
|
-
}
|
|
2373
3131
|
/** The endpoint's pending transaction count for `from`. */
|
|
2374
3132
|
async pendingCount(client, from) {
|
|
2375
|
-
return Number(await
|
|
3133
|
+
return Number(await evmRpcHex(client, "eth_getTransactionCount", [from, "pending"]));
|
|
2376
3134
|
}
|
|
2377
3135
|
/**
|
|
2378
3136
|
* The next nonce for (network, from): the larger of the endpoint's pending count and one past the
|
|
@@ -2406,14 +3164,16 @@ var EvmChainAdapter = class {
|
|
|
2406
3164
|
abi: ERC20_ABI,
|
|
2407
3165
|
functionName: "transfer",
|
|
2408
3166
|
args: [checkedAddress(intent.to), intent.value]
|
|
2409
|
-
}) : "0x";
|
|
2410
|
-
const estimate = BigInt(await
|
|
3167
|
+
}) : intent.kind === "call" ? intent.data : "0x";
|
|
3168
|
+
const estimate = BigInt(await evmRpcHex(client, "eth_estimateGas", [{
|
|
2411
3169
|
from,
|
|
2412
3170
|
to,
|
|
2413
3171
|
value: hex$1(value),
|
|
2414
3172
|
data
|
|
2415
3173
|
}]));
|
|
2416
3174
|
const scale = erc20 ? 11n : 12n;
|
|
3175
|
+
const buffered = estimate * scale / 10n + (estimate * scale % 10n === 0n ? 0n : 1n);
|
|
3176
|
+
const minGas = intent.kind === "call" ? intent.minGas ?? 0n : 0n;
|
|
2417
3177
|
return {
|
|
2418
3178
|
family: "evm",
|
|
2419
3179
|
network,
|
|
@@ -2422,12 +3182,12 @@ var EvmChainAdapter = class {
|
|
|
2422
3182
|
value,
|
|
2423
3183
|
data,
|
|
2424
3184
|
nonce,
|
|
2425
|
-
gas:
|
|
3185
|
+
gas: buffered > minGas ? buffered : minGas
|
|
2426
3186
|
};
|
|
2427
3187
|
}
|
|
2428
3188
|
async estimateFee(client) {
|
|
2429
3189
|
try {
|
|
2430
|
-
const history = await
|
|
3190
|
+
const history = await evmRpc(client, "eth_feeHistory", [
|
|
2431
3191
|
"0x14",
|
|
2432
3192
|
"latest",
|
|
2433
3193
|
[
|
|
@@ -2457,7 +3217,7 @@ var EvmChainAdapter = class {
|
|
|
2457
3217
|
tier(2, "fast")
|
|
2458
3218
|
];
|
|
2459
3219
|
} catch {
|
|
2460
|
-
const price = BigInt(await
|
|
3220
|
+
const price = BigInt(await evmRpcHex(client, "eth_gasPrice", []));
|
|
2461
3221
|
return [
|
|
2462
3222
|
"slow",
|
|
2463
3223
|
"normal",
|
|
@@ -2514,21 +3274,20 @@ var EvmChainAdapter = class {
|
|
|
2514
3274
|
return `0x${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
2515
3275
|
}
|
|
2516
3276
|
async broadcast(client, raw) {
|
|
2517
|
-
return
|
|
3277
|
+
return evmRpcHex(client, "eth_sendRawTransaction", [raw]);
|
|
2518
3278
|
}
|
|
2519
3279
|
async status(client, hash) {
|
|
2520
|
-
const receipt = await
|
|
3280
|
+
const receipt = await evmRpc(client, "eth_getTransactionReceipt", [hash]);
|
|
2521
3281
|
if (receipt === null || typeof receipt !== "object") return "pending";
|
|
2522
3282
|
if ("status" in receipt && typeof receipt.status === "string" && BigInt(receipt.status) === 0n) return "failed";
|
|
2523
3283
|
try {
|
|
2524
|
-
const finalized = await
|
|
3284
|
+
const finalized = await evmRpc(client, "eth_getBlockByNumber", ["finalized", false]);
|
|
2525
3285
|
if (finalized !== null && typeof finalized === "object" && "number" in finalized && typeof finalized.number === "string" && "blockNumber" in receipt && typeof receipt.blockNumber === "string" && BigInt(receipt.blockNumber) <= BigInt(finalized.number)) return "finalized";
|
|
2526
3286
|
} catch {}
|
|
2527
3287
|
return "confirmed";
|
|
2528
3288
|
}
|
|
2529
3289
|
};
|
|
2530
|
-
|
|
2531
|
-
//#region src/assets/controller.ts
|
|
3290
|
+
// src/assets/controller.ts
|
|
2532
3291
|
/**
|
|
2533
3292
|
* EVM balances per (account address, network). Native via eth_getBalance; token balanceOf calls go
|
|
2534
3293
|
* through the network's Multicall3 (one aggregate eth_call) when the registry declares it,
|
|
@@ -2660,7 +3419,7 @@ var AssetsController = class extends Controller {
|
|
|
2660
3419
|
return;
|
|
2661
3420
|
}
|
|
2662
3421
|
const client = this.options.networks.client(network);
|
|
2663
|
-
const native = BigInt(await
|
|
3422
|
+
const native = BigInt(await evmRpcHex(client, "eth_getBalance", [address, "latest"])).toString();
|
|
2664
3423
|
const custom = this.customTokens.get(network) ?? [];
|
|
2665
3424
|
const tokens = await this.readTokens(client, info.multicall3, address, custom.map((t) => ({
|
|
2666
3425
|
address: t.address,
|
|
@@ -2712,8 +3471,8 @@ var AssetsController = class extends Controller {
|
|
|
2712
3471
|
const balances = multicall3 === void 0 ? await Promise.all(tokens.map(async (t) => this.balanceOf(client, t.address, owner))) : await this.balanceOfBatch(client, multicall3, owner, tokens.map((t) => t.address));
|
|
2713
3472
|
return Promise.all(tokens.map(async (t, i) => ({
|
|
2714
3473
|
address: t.address,
|
|
2715
|
-
symbol: await this.
|
|
2716
|
-
decimals: await this.
|
|
3474
|
+
symbol: await this.callToken(client, t.address, "symbol") ?? "?",
|
|
3475
|
+
decimals: await this.callToken(client, t.address, "decimals") ?? 18,
|
|
2717
3476
|
balance: balances[i]?.toString() ?? "0",
|
|
2718
3477
|
verified: t.verified
|
|
2719
3478
|
})));
|
|
@@ -2732,7 +3491,7 @@ var AssetsController = class extends Controller {
|
|
|
2732
3491
|
})
|
|
2733
3492
|
}))]
|
|
2734
3493
|
});
|
|
2735
|
-
const result = await
|
|
3494
|
+
const result = await evmRpcHex(client, "eth_call", [{
|
|
2736
3495
|
to: multicall3,
|
|
2737
3496
|
data
|
|
2738
3497
|
}, "latest"]);
|
|
@@ -2753,7 +3512,7 @@ var AssetsController = class extends Controller {
|
|
|
2753
3512
|
functionName: "balanceOf",
|
|
2754
3513
|
args: [checked(owner)]
|
|
2755
3514
|
});
|
|
2756
|
-
const result = await
|
|
3515
|
+
const result = await evmRpcHex(client, "eth_call", [{
|
|
2757
3516
|
to: token,
|
|
2758
3517
|
data
|
|
2759
3518
|
}, "latest"]);
|
|
@@ -2764,32 +3523,12 @@ var AssetsController = class extends Controller {
|
|
|
2764
3523
|
data: result
|
|
2765
3524
|
});
|
|
2766
3525
|
}
|
|
2767
|
-
async
|
|
2768
|
-
const data = encodeFunctionData({
|
|
2769
|
-
abi: ERC20_ABI,
|
|
2770
|
-
functionName: fn
|
|
2771
|
-
});
|
|
2772
|
-
const result = await this.rpcHex(client, "eth_call", [{
|
|
2773
|
-
to: token,
|
|
2774
|
-
data
|
|
2775
|
-
}, "latest"]);
|
|
2776
|
-
if (!isHex(result)) return;
|
|
2777
|
-
try {
|
|
2778
|
-
return decodeFunctionResult({
|
|
2779
|
-
abi: ERC20_ABI,
|
|
2780
|
-
functionName: fn,
|
|
2781
|
-
data: result
|
|
2782
|
-
});
|
|
2783
|
-
} catch {
|
|
2784
|
-
return;
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
async callNumber(client, token, fn) {
|
|
3526
|
+
async callToken(client, token, fn) {
|
|
2788
3527
|
const data = encodeFunctionData({
|
|
2789
3528
|
abi: ERC20_ABI,
|
|
2790
3529
|
functionName: fn
|
|
2791
3530
|
});
|
|
2792
|
-
const result = await
|
|
3531
|
+
const result = await evmRpcHex(client, "eth_call", [{
|
|
2793
3532
|
to: token,
|
|
2794
3533
|
data
|
|
2795
3534
|
}, "latest"]);
|
|
@@ -2815,8 +3554,8 @@ var AssetsController = class extends Controller {
|
|
|
2815
3554
|
return;
|
|
2816
3555
|
}
|
|
2817
3556
|
const client = this.options.networks.client(network);
|
|
2818
|
-
await this.
|
|
2819
|
-
await this.
|
|
3557
|
+
await this.callToken(client, address, "symbol");
|
|
3558
|
+
await this.callToken(client, address, "decimals");
|
|
2820
3559
|
await this.persistToken(network, address);
|
|
2821
3560
|
}
|
|
2822
3561
|
async persistToken(network, address) {
|
|
@@ -2833,17 +3572,6 @@ var AssetsController = class extends Controller {
|
|
|
2833
3572
|
await this.engineStore.collection(COLLECTION$1).put("customTokens", new TextEncoder().encode(JSON.stringify(merged)));
|
|
2834
3573
|
}
|
|
2835
3574
|
}
|
|
2836
|
-
async rpc(client, method, params) {
|
|
2837
|
-
return client.transport.request({
|
|
2838
|
-
method,
|
|
2839
|
-
params
|
|
2840
|
-
});
|
|
2841
|
-
}
|
|
2842
|
-
async rpcHex(client, method, params) {
|
|
2843
|
-
const result = await this.rpc(client, method, params);
|
|
2844
|
-
if (typeof result !== "string") throw new TypeError(`${method} returned a malformed result`);
|
|
2845
|
-
return result;
|
|
2846
|
-
}
|
|
2847
3575
|
/** BTC balance = confirmed spendable + own unconfirmed change; frozen 546/330 shown separately. */
|
|
2848
3576
|
async refreshBitcoin(xpub, network) {
|
|
2849
3577
|
const es = this.options.networks.esplora(network);
|
|
@@ -2878,8 +3606,7 @@ var AssetsController = class extends Controller {
|
|
|
2878
3606
|
}));
|
|
2879
3607
|
}
|
|
2880
3608
|
};
|
|
2881
|
-
|
|
2882
|
-
//#region src/backup/errors.ts
|
|
3609
|
+
// src/backup/errors.ts
|
|
2883
3610
|
var BackupControllerError = class extends Error {
|
|
2884
3611
|
category;
|
|
2885
3612
|
constructor(category, message) {
|
|
@@ -2888,8 +3615,7 @@ var BackupControllerError = class extends Error {
|
|
|
2888
3615
|
this.category = category;
|
|
2889
3616
|
}
|
|
2890
3617
|
};
|
|
2891
|
-
|
|
2892
|
-
//#region src/backup/controller.ts
|
|
3618
|
+
// src/backup/controller.ts
|
|
2893
3619
|
/**
|
|
2894
3620
|
* The backup reconcile loop (design/backup.mdx "同步"): keeps the cloud file
|
|
2895
3621
|
* `qntx-wallet-backup-<id>.json` in step with the vault. Single-flight — a trigger during a run
|
|
@@ -2928,17 +3654,15 @@ function parseRecord(bytes) {
|
|
|
2928
3654
|
const { digest } = o;
|
|
2929
3655
|
const { uploadedAt } = o;
|
|
2930
3656
|
const { walletIds } = o;
|
|
2931
|
-
const { nostrKeyIds } = o;
|
|
2932
3657
|
if (typeof id !== "string" || !/^[0-9a-f]{32}$/.test(id)) return;
|
|
2933
3658
|
if (digest !== void 0 && (typeof digest !== "string" || !/^[0-9a-f]{64}$/.test(digest))) return;
|
|
2934
3659
|
if (uploadedAt !== void 0 && (typeof uploadedAt !== "number" || !Number.isSafeInteger(uploadedAt))) return;
|
|
2935
|
-
if (!isStringArray(walletIds)
|
|
3660
|
+
if (!isStringArray(walletIds)) return;
|
|
2936
3661
|
return {
|
|
2937
3662
|
id,
|
|
2938
3663
|
digest,
|
|
2939
3664
|
uploadedAt,
|
|
2940
|
-
walletIds
|
|
2941
|
-
nostrKeyIds
|
|
3665
|
+
walletIds
|
|
2942
3666
|
};
|
|
2943
3667
|
} catch {
|
|
2944
3668
|
return;
|
|
@@ -3055,8 +3779,7 @@ var BackupController = class extends Controller {
|
|
|
3055
3779
|
id,
|
|
3056
3780
|
digest: digestOf(text),
|
|
3057
3781
|
uploadedAt: previous?.uploadedAt,
|
|
3058
|
-
walletIds: []
|
|
3059
|
-
nostrKeyIds: []
|
|
3782
|
+
walletIds: []
|
|
3060
3783
|
});
|
|
3061
3784
|
}
|
|
3062
3785
|
try {
|
|
@@ -3086,20 +3809,10 @@ var BackupController = class extends Controller {
|
|
|
3086
3809
|
}
|
|
3087
3810
|
unprotected(snapshot, record) {
|
|
3088
3811
|
const covered = snapshot.backup !== null && record !== void 0 && record.id === snapshot.backup.id && record.digest !== void 0;
|
|
3089
|
-
|
|
3090
|
-
const keys = snapshot.nostr.keys.filter((k) => k.ref.kind === "standalone" && k.backedUp === false);
|
|
3091
|
-
const walletIds = wallets.map((w) => w.walletId).filter((id) => !covered || !record.walletIds.includes(id));
|
|
3092
|
-
const nostrKeyIds = keys.map((k) => k.publicKey).filter((id) => !covered || !record.nostrKeyIds.includes(id));
|
|
3093
|
-
return {
|
|
3094
|
-
walletIds: sorted(walletIds),
|
|
3095
|
-
nostrKeyIds: sorted(nostrKeyIds)
|
|
3096
|
-
};
|
|
3812
|
+
return { walletIds: sorted(snapshot.wallets.filter((w) => w.backup !== "mnemonic").map((w) => w.walletId).filter((id) => !covered || !record.walletIds.includes(id))) };
|
|
3097
3813
|
}
|
|
3098
3814
|
coverage(snapshot) {
|
|
3099
|
-
return {
|
|
3100
|
-
walletIds: sorted(snapshot.wallets.map((w) => w.walletId)),
|
|
3101
|
-
nostrKeyIds: sorted(snapshot.nostr.keys.filter((k) => k.ref.kind === "standalone").map((k) => k.publicKey))
|
|
3102
|
-
};
|
|
3815
|
+
return { walletIds: sorted(snapshot.wallets.map((w) => w.walletId)) };
|
|
3103
3816
|
}
|
|
3104
3817
|
setStatus(generation, status, error, record) {
|
|
3105
3818
|
if (generation !== this.generation) return;
|
|
@@ -3151,8 +3864,7 @@ var BackupController = class extends Controller {
|
|
|
3151
3864
|
id: adoption.id,
|
|
3152
3865
|
digest: adoption.digest,
|
|
3153
3866
|
uploadedAt: void 0,
|
|
3154
|
-
walletIds: []
|
|
3155
|
-
nostrKeyIds: []
|
|
3867
|
+
walletIds: []
|
|
3156
3868
|
};
|
|
3157
3869
|
if (generation !== this.generation) return;
|
|
3158
3870
|
await this.writeRecord(record);
|
|
@@ -3175,7 +3887,7 @@ var BackupController = class extends Controller {
|
|
|
3175
3887
|
}
|
|
3176
3888
|
const baseline = record !== void 0 && record.id === backup.id ? record.digest : void 0;
|
|
3177
3889
|
const cov = this.coverage(snapshot);
|
|
3178
|
-
if (baseline !== void 0 && record !== void 0 && sameSet(cov.walletIds, record.walletIds)
|
|
3890
|
+
if (baseline !== void 0 && record !== void 0 && sameSet(cov.walletIds, record.walletIds)) {
|
|
3179
3891
|
this.setStatus(generation, "synced", void 0, record);
|
|
3180
3892
|
return;
|
|
3181
3893
|
}
|
|
@@ -3200,8 +3912,7 @@ var BackupController = class extends Controller {
|
|
|
3200
3912
|
id: backup.id,
|
|
3201
3913
|
digest,
|
|
3202
3914
|
uploadedAt: this.now(),
|
|
3203
|
-
walletIds: cov.walletIds
|
|
3204
|
-
nostrKeyIds: cov.nostrKeyIds
|
|
3915
|
+
walletIds: cov.walletIds
|
|
3205
3916
|
};
|
|
3206
3917
|
if (generation !== this.generation) return;
|
|
3207
3918
|
await this.writeRecord(next);
|
|
@@ -3224,10 +3935,7 @@ function defaultState() {
|
|
|
3224
3935
|
status: "locked",
|
|
3225
3936
|
backup: null,
|
|
3226
3937
|
uploadedAt: void 0,
|
|
3227
|
-
unprotected: {
|
|
3228
|
-
walletIds: [],
|
|
3229
|
-
nostrKeyIds: []
|
|
3230
|
-
},
|
|
3938
|
+
unprotected: { walletIds: [] },
|
|
3231
3939
|
error: void 0
|
|
3232
3940
|
};
|
|
3233
3941
|
}
|
|
@@ -3324,8 +4032,7 @@ function decodeMetaDocBytes(bytes) {
|
|
|
3324
4032
|
return;
|
|
3325
4033
|
}
|
|
3326
4034
|
}
|
|
3327
|
-
|
|
3328
|
-
//#region src/meta/errors.ts
|
|
4035
|
+
// src/meta/errors.ts
|
|
3329
4036
|
var MetaSyncError = class extends Error {
|
|
3330
4037
|
category;
|
|
3331
4038
|
constructor(category, message, cause) {
|
|
@@ -3334,15 +4041,15 @@ var MetaSyncError = class extends Error {
|
|
|
3334
4041
|
this.category = category;
|
|
3335
4042
|
}
|
|
3336
4043
|
};
|
|
3337
|
-
|
|
3338
|
-
//#region src/meta/controller.ts
|
|
4044
|
+
// src/meta/controller.ts
|
|
3339
4045
|
const META_SYNC_KIND = 30078;
|
|
3340
4046
|
const PUBLISH_DEBOUNCE_MS = 2e3;
|
|
3341
4047
|
/**
|
|
3342
4048
|
* Wallet-metadata sync over kind-30078 addressable events: one NIP-44-encrypted document per
|
|
3343
|
-
* wallet, authored by its
|
|
3344
|
-
* replaceable events would otherwise let a stale local tree overwrite a newer remote
|
|
3345
|
-
* Debounced publishes are per wallet; a generation counter cancels in-flight write-backs
|
|
4049
|
+
* wallet, authored by its dedicated sync key (WD-070). Pull always precedes publish in an unlock
|
|
4050
|
+
* session — replaceable events would otherwise let a stale local tree overwrite a newer remote
|
|
4051
|
+
* document. Debounced publishes are per wallet; a generation counter cancels in-flight write-backs
|
|
4052
|
+
* on lock.
|
|
3346
4053
|
*/
|
|
3347
4054
|
var MetaSyncController = class extends Controller {
|
|
3348
4055
|
options;
|
|
@@ -3414,19 +4121,6 @@ var MetaSyncController = class extends Controller {
|
|
|
3414
4121
|
const snap = this.options.wallet.getSnapshot();
|
|
3415
4122
|
await Promise.all(snap.wallets.map(async (w) => this.pullOne(w.walletId, gen)));
|
|
3416
4123
|
}
|
|
3417
|
-
async pubkeyFor(signer) {
|
|
3418
|
-
return signer.getPublicKey();
|
|
3419
|
-
}
|
|
3420
|
-
signerFor(walletId) {
|
|
3421
|
-
const snap = this.options.wallet.getSnapshot();
|
|
3422
|
-
const ref = {
|
|
3423
|
-
kind: "derived",
|
|
3424
|
-
walletId,
|
|
3425
|
-
groupIndex: 0
|
|
3426
|
-
};
|
|
3427
|
-
if (!snap.nostr.keys.some((k) => k.ref.kind === "derived" && k.ref.walletId === walletId && k.ref.groupIndex === 0)) return;
|
|
3428
|
-
return this.options.nostr.signer(ref);
|
|
3429
|
-
}
|
|
3430
4124
|
setStatus(walletId, status) {
|
|
3431
4125
|
this.update((s) => ({
|
|
3432
4126
|
...s,
|
|
@@ -3439,15 +4133,11 @@ var MetaSyncController = class extends Controller {
|
|
|
3439
4133
|
async pullOne(walletId, gen) {
|
|
3440
4134
|
const config = this.options.metaSync;
|
|
3441
4135
|
if (config === void 0 || this.inflight.has(walletId)) return;
|
|
3442
|
-
const signer = this.
|
|
3443
|
-
if (signer === void 0) {
|
|
3444
|
-
if (gen === this.generation) this.setStatus(walletId, { status: "unavailable" });
|
|
3445
|
-
return;
|
|
3446
|
-
}
|
|
4136
|
+
const signer = this.options.nostr.syncSigner(walletId);
|
|
3447
4137
|
this.inflight.add(walletId);
|
|
3448
4138
|
this.setStatus(walletId, { status: "syncing" });
|
|
3449
4139
|
try {
|
|
3450
|
-
const pubkey = await
|
|
4140
|
+
const pubkey = await signer.getPublicKey();
|
|
3451
4141
|
const events = await config.relays.fetch(this.filterFor(pubkey, config.tag));
|
|
3452
4142
|
let remote;
|
|
3453
4143
|
let remoteSkipped = 0;
|
|
@@ -3467,8 +4157,7 @@ var MetaSyncController = class extends Controller {
|
|
|
3467
4157
|
this.setStatus(walletId, { status: "synced" });
|
|
3468
4158
|
if (hasEdits) await this.publishNow(walletId, gen);
|
|
3469
4159
|
} else {
|
|
3470
|
-
const
|
|
3471
|
-
const plaintext = decrypt === void 0 ? void 0 : await decrypt.call(signer, pubkey, remote.content).catch(() => void 0);
|
|
4160
|
+
const plaintext = await signer.decrypt(remote.content).catch(() => void 0);
|
|
3472
4161
|
const doc = plaintext === void 0 ? void 0 : decodeMetaDocBytes(new TextEncoder().encode(plaintext));
|
|
3473
4162
|
if (doc === void 0) {
|
|
3474
4163
|
if (gen !== this.generation) return;
|
|
@@ -3551,18 +4240,12 @@ var MetaSyncController = class extends Controller {
|
|
|
3551
4240
|
async publishNow(walletId, gen) {
|
|
3552
4241
|
const config = this.options.metaSync;
|
|
3553
4242
|
if (config === void 0 || !this.pulled.has(walletId)) return;
|
|
3554
|
-
const signer = this.
|
|
3555
|
-
if (signer === void 0) {
|
|
3556
|
-
this.setStatus(walletId, { status: "unavailable" });
|
|
3557
|
-
return;
|
|
3558
|
-
}
|
|
4243
|
+
const signer = this.options.nostr.syncSigner(walletId);
|
|
3559
4244
|
try {
|
|
3560
4245
|
const bytes = encodeMetaDoc(this.exportDoc(walletId));
|
|
3561
4246
|
if (bytes === void 0) throw new MetaSyncError("internal", "meta doc exceeds 64 KB");
|
|
3562
|
-
const
|
|
3563
|
-
|
|
3564
|
-
const pubkey = await this.pubkeyFor(signer);
|
|
3565
|
-
const content = await encrypt.call(signer, pubkey, new TextDecoder().decode(bytes));
|
|
4247
|
+
const pubkey = await signer.getPublicKey();
|
|
4248
|
+
const content = await signer.encrypt(new TextDecoder().decode(bytes));
|
|
3566
4249
|
const lastRemote = this.lastRemoteCreatedAt.get(walletId) ?? 0;
|
|
3567
4250
|
const created_at = Math.max(Math.floor(this.options.now() / 1e3), lastRemote + 1);
|
|
3568
4251
|
const event = await signer.signEvent({
|
|
@@ -3597,8 +4280,7 @@ function isUnsupportedDoc(plaintext) {
|
|
|
3597
4280
|
return false;
|
|
3598
4281
|
}
|
|
3599
4282
|
}
|
|
3600
|
-
|
|
3601
|
-
//#region src/chains/bitcoin.ts
|
|
4283
|
+
// src/chains/bitcoin.ts
|
|
3602
4284
|
const DUST = {
|
|
3603
4285
|
p2wpkh: 294,
|
|
3604
4286
|
p2tr: 330,
|
|
@@ -3733,8 +4415,8 @@ var BitcoinChainAdapter = class {
|
|
|
3733
4415
|
entry.nonWitnessUtxo = raw;
|
|
3734
4416
|
}
|
|
3735
4417
|
if (template === "btc.bip86") {
|
|
3736
|
-
entry.tapInternalKey = pub
|
|
3737
|
-
entry.tapBip32Derivation = [[pub
|
|
4418
|
+
entry.tapInternalKey = pub;
|
|
4419
|
+
entry.tapBip32Derivation = [[pub, {
|
|
3738
4420
|
hashes: [],
|
|
3739
4421
|
der: {
|
|
3740
4422
|
fingerprint,
|
|
@@ -3790,8 +4472,7 @@ var BitcoinChainAdapter = class {
|
|
|
3790
4472
|
};
|
|
3791
4473
|
}
|
|
3792
4474
|
};
|
|
3793
|
-
|
|
3794
|
-
//#region src/networks/controller.ts
|
|
4475
|
+
// src/networks/controller.ts
|
|
3795
4476
|
/**
|
|
3796
4477
|
* EVM network controller: the built-in registry plus host-provided and user-added RPC endpoints.
|
|
3797
4478
|
* Each network gets a lazily-created viem PublicClient on a `fallback` transport — endpoints are
|
|
@@ -4064,8 +4745,7 @@ var NetworkController = class extends Controller {
|
|
|
4064
4745
|
if (store !== void 0) await store.collection("networks").put("customRpc", new TextEncoder().encode(JSON.stringify(customRpc)));
|
|
4065
4746
|
}
|
|
4066
4747
|
};
|
|
4067
|
-
|
|
4068
|
-
//#region src/payments/parse.ts
|
|
4748
|
+
// src/payments/parse.ts
|
|
4069
4749
|
/** Invoices with less than this much life left are treated as already expired. */
|
|
4070
4750
|
const BOLT11_EXPIRY_GRACE_MS = 6e4;
|
|
4071
4751
|
const invalid = (reason) => ({
|
|
@@ -4335,8 +5015,7 @@ function parsePayment(input, options) {
|
|
|
4335
5015
|
return invalid("malformed");
|
|
4336
5016
|
}
|
|
4337
5017
|
}
|
|
4338
|
-
|
|
4339
|
-
//#region src/payments/resolve.ts
|
|
5018
|
+
// src/payments/resolve.ts
|
|
4340
5019
|
/**
|
|
4341
5020
|
* `parsePayment` plus LNURL dereferencing: an `lnurl` result is fetched and turned into a concrete
|
|
4342
5021
|
* `lnurl-pay` / `lnurl-withdraw` request (`tag=login` endpoints report as unsupported). Network
|
|
@@ -4371,8 +5050,7 @@ async function resolvePayment(input, options) {
|
|
|
4371
5050
|
format: "lnurl-auth"
|
|
4372
5051
|
};
|
|
4373
5052
|
}
|
|
4374
|
-
|
|
4375
|
-
//#region src/spark/notes.ts
|
|
5053
|
+
// src/spark/notes.ts
|
|
4376
5054
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
4377
5055
|
const isObject$4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4378
5056
|
const NOTE_FIELDS = /* @__PURE__ */ new Set([
|
|
@@ -4403,8 +5081,7 @@ function parsePaymentNote(value) {
|
|
|
4403
5081
|
function encodePaymentNote(note) {
|
|
4404
5082
|
return new TextEncoder().encode(JSON.stringify(note));
|
|
4405
5083
|
}
|
|
4406
|
-
|
|
4407
|
-
//#region src/spark/exit-chain-codec.ts
|
|
5084
|
+
// src/spark/exit-chain-codec.ts
|
|
4408
5085
|
/**
|
|
4409
5086
|
* SparkExitChain persistence codec: JSON with Uint8Arrays tagged `{ $b64: "…" }` — small and
|
|
4410
5087
|
* explicit rather than schema-bound, so upstream field additions survive a round-trip.
|
|
@@ -4431,8 +5108,7 @@ function encodeExitChain(chain) {
|
|
|
4431
5108
|
function decodeExitChain(bytes) {
|
|
4432
5109
|
return decodeValue(JSON.parse(new TextDecoder().decode(bytes)));
|
|
4433
5110
|
}
|
|
4434
|
-
|
|
4435
|
-
//#region src/spark/store.ts
|
|
5111
|
+
// src/spark/store.ts
|
|
4436
5112
|
const ALL = "__all__";
|
|
4437
5113
|
/**
|
|
4438
5114
|
* SparkStore over one EngineCollection: chains keyed by leafId, plus an index record listing them
|
|
@@ -4476,8 +5152,7 @@ function sparkStore(col) {
|
|
|
4476
5152
|
}
|
|
4477
5153
|
};
|
|
4478
5154
|
}
|
|
4479
|
-
|
|
4480
|
-
//#region src/spark/controller.ts
|
|
5155
|
+
// src/spark/controller.ts
|
|
4481
5156
|
const key = (ref) => `${ref.walletId}:${ref.groupIndex}:${ref.network}`;
|
|
4482
5157
|
/** TransferId: the client and SO expect a UUIDv7 — mint it from the injected clock+random. */
|
|
4483
5158
|
const transferId = (random, now) => uuidv7(random, now);
|
|
@@ -4894,6 +5569,14 @@ var SparkController = class extends Controller {
|
|
|
4894
5569
|
...params.memo === void 0 ? {} : { memo: params.memo }
|
|
4895
5570
|
}));
|
|
4896
5571
|
}
|
|
5572
|
+
/** A fresh single-use L1 deposit address (with the SO's proof of possession). */
|
|
5573
|
+
async newDepositAddress(ref) {
|
|
5574
|
+
return this.guarded(async () => this.clientFor(ref).deposits.newAddress());
|
|
5575
|
+
}
|
|
5576
|
+
/** Claims a confirmed L1 deposit paying one of the account's unused deposit addresses. */
|
|
5577
|
+
async claimDeposit(ref, transaction) {
|
|
5578
|
+
await this.guarded(async () => this.clientFor(ref).deposits.claim({ transaction }));
|
|
5579
|
+
}
|
|
4897
5580
|
async receiveAddress(ref) {
|
|
4898
5581
|
const state = this.getState().accounts[key(ref)];
|
|
4899
5582
|
await Promise.resolve();
|
|
@@ -4941,10 +5624,77 @@ var SparkController = class extends Controller {
|
|
|
4941
5624
|
});
|
|
4942
5625
|
}
|
|
4943
5626
|
};
|
|
4944
|
-
|
|
4945
|
-
|
|
5627
|
+
// src/spark/deposit-claim.ts
|
|
5628
|
+
/**
|
|
5629
|
+
* Claims Spark deposits: once a transaction tagged `sparkDeposit` is confirmed, its raw bytes are
|
|
5630
|
+
* read from Esplora and handed to the Spark account's `deposits.claim`. Claiming too early (the SO
|
|
5631
|
+
* wants enough confirmations) just fails and is retried with backoff; locking stops everything.
|
|
5632
|
+
* Works only from the transaction state, so a restart resumes whatever is confirmed and unclaimed.
|
|
5633
|
+
*/
|
|
5634
|
+
const pending = (tx) => tx.sparkDeposit !== void 0 && tx.state === "confirmed" && tx.sparkDeposit.claimed !== true;
|
|
5635
|
+
var DepositClaimer = class {
|
|
5636
|
+
options;
|
|
5637
|
+
active = /* @__PURE__ */ new Map();
|
|
5638
|
+
unsubscribe;
|
|
5639
|
+
constructor(options) {
|
|
5640
|
+
this.options = options;
|
|
5641
|
+
}
|
|
5642
|
+
/** Starts watching; every confirmed, unclaimed deposit already in the state is picked up. */
|
|
5643
|
+
start() {
|
|
5644
|
+
this.stop();
|
|
5645
|
+
this.unsubscribe = this.options.transactions.store.subscribe(() => {
|
|
5646
|
+
this.scan();
|
|
5647
|
+
});
|
|
5648
|
+
this.scan();
|
|
5649
|
+
}
|
|
5650
|
+
stop() {
|
|
5651
|
+
this.unsubscribe?.();
|
|
5652
|
+
this.unsubscribe = void 0;
|
|
5653
|
+
for (const abort of this.active.values()) abort.abort();
|
|
5654
|
+
this.active.clear();
|
|
5655
|
+
}
|
|
5656
|
+
scan() {
|
|
5657
|
+
for (const tx of Object.values(this.options.transactions.getState().txs)) if (pending(tx) && !this.active.has(tx.id)) {
|
|
5658
|
+
const abort = new AbortController();
|
|
5659
|
+
this.active.set(tx.id, abort);
|
|
5660
|
+
this.claim(tx.id, abort.signal);
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
async claim(id, signal) {
|
|
5664
|
+
for (let failures = 0; !signal.aborted; failures += 1) {
|
|
5665
|
+
const tx = this.options.transactions.getState().txs[id];
|
|
5666
|
+
if (tx === void 0 || !pending(tx) || tx.sparkDeposit === void 0) break;
|
|
5667
|
+
const { walletId, groupIndex, network: sparkNetwork } = tx.sparkDeposit;
|
|
5668
|
+
try {
|
|
5669
|
+
if (tx.hash === void 0) throw new Error("the deposit has no transaction id");
|
|
5670
|
+
const raw = await this.options.networks.esplora(tx.network).get(`/tx/${tx.hash}/hex`);
|
|
5671
|
+
if (typeof raw !== "string") throw new TypeError("esplora returned a malformed transaction");
|
|
5672
|
+
await this.options.spark.claimDeposit({
|
|
5673
|
+
walletId,
|
|
5674
|
+
groupIndex,
|
|
5675
|
+
network: sparkNetwork
|
|
5676
|
+
}, hex.decode(raw.trim()));
|
|
5677
|
+
if (signal.aborted) break;
|
|
5678
|
+
await this.options.transactions.markDepositClaimed(id);
|
|
5679
|
+
break;
|
|
5680
|
+
} catch (error) {
|
|
5681
|
+
this.options.logger.warn(`spark deposit ${id} not claimed yet`, error);
|
|
5682
|
+
await eventsBackoff(failures, signal);
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
if (this.active.get(id)?.signal === signal) this.active.delete(id);
|
|
5686
|
+
}
|
|
5687
|
+
};
|
|
5688
|
+
// src/transactions/bitcoin.ts
|
|
4946
5689
|
/** Bitcoin route: scan → coin-select → PSBT → sign → broadcast, plus esplora status poll. */
|
|
4947
5690
|
const isObject$3 = (v) => typeof v === "object" && v !== null;
|
|
5691
|
+
const broadcastPsbt = async (es, result) => {
|
|
5692
|
+
if (result.kind !== "bitcoin.psbt" || !(result.signedPsbt instanceof Uint8Array)) throw new Error("unexpected bitcoin sign result");
|
|
5693
|
+
const signed = Transaction.fromPSBT(result.signedPsbt);
|
|
5694
|
+
signed.finalize();
|
|
5695
|
+
const raw = signed.extract();
|
|
5696
|
+
return String(await es.post("/tx", hex.encode(raw)));
|
|
5697
|
+
};
|
|
4948
5698
|
const fetchRawTxes = async (es, utxos) => {
|
|
4949
5699
|
const out = /* @__PURE__ */ new Map();
|
|
4950
5700
|
await Promise.all(utxos.map(async (u) => {
|
|
@@ -4968,6 +5718,23 @@ const poll = async (ctx, tx) => {
|
|
|
4968
5718
|
if (!(isObject$3(status) && status["confirmed"] === true)) return;
|
|
4969
5719
|
return "confirmed";
|
|
4970
5720
|
};
|
|
5721
|
+
const nextReceiveAddress = async (ctx, walletId, groupIndex, network) => {
|
|
5722
|
+
const account = await (ctx.options.chains ?? chains(ctx.options.wallet)).account({
|
|
5723
|
+
walletId,
|
|
5724
|
+
groupIndex,
|
|
5725
|
+
template: "btc.bip84",
|
|
5726
|
+
network
|
|
5727
|
+
});
|
|
5728
|
+
if (!("xpub" in account) || account.xpub === void 0) throw new Error("bitcoin account carries no xpub");
|
|
5729
|
+
const receive = await scanChain(ctx.options.networks.esplora(network), account.xpub, "btc.bip84", network, bitcoinNetwork(network).params, 0);
|
|
5730
|
+
const next = new BitcoinScanStore().nextUnused({
|
|
5731
|
+
addresses: receive,
|
|
5732
|
+
utxos: [],
|
|
5733
|
+
scannedAt: ctx.options.now()
|
|
5734
|
+
}, 0);
|
|
5735
|
+
if (next === void 0) throw new Error("no unused receive address");
|
|
5736
|
+
return next.address;
|
|
5737
|
+
};
|
|
4971
5738
|
/** Per-controller route — the UTXO store and the build stash are engine state. */
|
|
4972
5739
|
const createBitcoinRoute = () => {
|
|
4973
5740
|
const bitcoin = new BitcoinChainAdapter();
|
|
@@ -4989,7 +5756,7 @@ const createBitcoinRoute = () => {
|
|
|
4989
5756
|
const releaseReservation = (requestId) => {
|
|
4990
5757
|
for (const [key, holder] of reserved) if (holder === requestId) reserved.delete(key);
|
|
4991
5758
|
};
|
|
4992
|
-
const prepare = async (ctx, params, requestId) => {
|
|
5759
|
+
const prepare = async (ctx, params, requestId, sparkDeposit) => {
|
|
4993
5760
|
const { account, network, to, amount, fee } = params;
|
|
4994
5761
|
if (account.kind !== "hd") throw new Error("imported accounts are not yet supported");
|
|
4995
5762
|
const es = ctx.options.networks.esplora(network);
|
|
@@ -5147,6 +5914,7 @@ const createBitcoinRoute = () => {
|
|
|
5147
5914
|
intent,
|
|
5148
5915
|
nonce: 0,
|
|
5149
5916
|
btc: record,
|
|
5917
|
+
...sparkDeposit === void 0 ? {} : { sparkDeposit },
|
|
5150
5918
|
submittedAt: ctx.options.now(),
|
|
5151
5919
|
error: void 0
|
|
5152
5920
|
};
|
|
@@ -5157,26 +5925,12 @@ const createBitcoinRoute = () => {
|
|
|
5157
5925
|
verify
|
|
5158
5926
|
});
|
|
5159
5927
|
tx = await ctx.transition(tx, { state: "signed" });
|
|
5160
|
-
|
|
5161
|
-
const signed = Transaction.fromPSBT(result.signedPsbt);
|
|
5162
|
-
signed.finalize();
|
|
5163
|
-
const raw = signed.extract();
|
|
5164
|
-
const hash = String(await es.post("/tx", hex.encode(raw)));
|
|
5928
|
+
const hash = await broadcastPsbt(es, result);
|
|
5165
5929
|
bitcoinUtxos.lock(selectedUtxos, tx.id);
|
|
5166
5930
|
releaseReservation(requestId);
|
|
5167
|
-
|
|
5168
|
-
state: "submitted",
|
|
5169
|
-
hash,
|
|
5170
|
-
submittedAt: ctx.options.now()
|
|
5171
|
-
});
|
|
5172
|
-
ctx.track(tx.id, hash);
|
|
5173
|
-
return tx;
|
|
5931
|
+
return await submitTx(ctx, tx, hash);
|
|
5174
5932
|
} catch (error) {
|
|
5175
|
-
|
|
5176
|
-
await ctx.transition(tx, {
|
|
5177
|
-
state: failed,
|
|
5178
|
-
error: errorMessage$1(error)
|
|
5179
|
-
});
|
|
5933
|
+
await failTx(ctx, tx, error);
|
|
5180
5934
|
throw error;
|
|
5181
5935
|
}
|
|
5182
5936
|
};
|
|
@@ -5235,11 +5989,7 @@ const createBitcoinRoute = () => {
|
|
|
5235
5989
|
verify
|
|
5236
5990
|
});
|
|
5237
5991
|
tx = await ctx.transition(tx, { state: "signed" });
|
|
5238
|
-
|
|
5239
|
-
const signed = Transaction.fromPSBT(result.signedPsbt);
|
|
5240
|
-
signed.finalize();
|
|
5241
|
-
const raw = signed.extract();
|
|
5242
|
-
const hash = String(await es.post("/tx", hex.encode(raw)));
|
|
5992
|
+
const hash = await broadcastPsbt(es, result);
|
|
5243
5993
|
if (link.replaces !== void 0) bitcoinUtxos.endTx(old.id, "dropped");
|
|
5244
5994
|
bitcoinUtxos.lock(record.inputs.map((i) => ({
|
|
5245
5995
|
...i,
|
|
@@ -5247,19 +5997,9 @@ const createBitcoinRoute = () => {
|
|
|
5247
5997
|
height: 0
|
|
5248
5998
|
})), tx.id);
|
|
5249
5999
|
releaseReservation(requestId);
|
|
5250
|
-
|
|
5251
|
-
state: "submitted",
|
|
5252
|
-
hash,
|
|
5253
|
-
submittedAt: ctx.options.now()
|
|
5254
|
-
});
|
|
5255
|
-
ctx.track(tx.id, hash);
|
|
5256
|
-
return tx;
|
|
6000
|
+
return await submitTx(ctx, tx, hash);
|
|
5257
6001
|
} catch (error) {
|
|
5258
|
-
|
|
5259
|
-
await ctx.transition(tx, {
|
|
5260
|
-
state: failed,
|
|
5261
|
-
error: errorMessage$1(error)
|
|
5262
|
-
});
|
|
6002
|
+
await failTx(ctx, tx, error);
|
|
5263
6003
|
throw error;
|
|
5264
6004
|
}
|
|
5265
6005
|
};
|
|
@@ -5471,6 +6211,7 @@ const createBitcoinRoute = () => {
|
|
|
5471
6211
|
};
|
|
5472
6212
|
return {
|
|
5473
6213
|
prepare,
|
|
6214
|
+
nextReceiveAddress,
|
|
5474
6215
|
poll: pollAndRelease,
|
|
5475
6216
|
prepareReplace,
|
|
5476
6217
|
prepareCpfp,
|
|
@@ -5481,17 +6222,50 @@ const createBitcoinRoute = () => {
|
|
|
5481
6222
|
}
|
|
5482
6223
|
};
|
|
5483
6224
|
};
|
|
5484
|
-
|
|
5485
|
-
//#region src/transactions/evm.ts
|
|
6225
|
+
// src/transactions/evm.ts
|
|
5486
6226
|
/** EVM route: native/ERC-20 send build → fee → decode → sign → broadcast, plus status poll. */
|
|
5487
6227
|
const bump = (v) => (v * 11n + 9n) / 10n;
|
|
6228
|
+
const signSubmit = async (ctx, tx, apis, request, verify, broadcast) => {
|
|
6229
|
+
let signed;
|
|
6230
|
+
let tracked = tx;
|
|
6231
|
+
try {
|
|
6232
|
+
const result = await apis.sign({
|
|
6233
|
+
request,
|
|
6234
|
+
verify
|
|
6235
|
+
});
|
|
6236
|
+
tracked = await ctx.transition(tracked, { state: "signed" });
|
|
6237
|
+
signed = result;
|
|
6238
|
+
} catch (error) {
|
|
6239
|
+
await ctx.transition(tracked, {
|
|
6240
|
+
state: "rejected",
|
|
6241
|
+
error: errorMessage$1(error)
|
|
6242
|
+
});
|
|
6243
|
+
throw error;
|
|
6244
|
+
}
|
|
6245
|
+
try {
|
|
6246
|
+
const hash = await broadcast(signed);
|
|
6247
|
+
tracked = await ctx.transition(tracked, {
|
|
6248
|
+
state: "submitted",
|
|
6249
|
+
hash,
|
|
6250
|
+
submittedAt: ctx.options.now()
|
|
6251
|
+
});
|
|
6252
|
+
ctx.track(tracked.id, hash);
|
|
6253
|
+
return tracked;
|
|
6254
|
+
} catch (error) {
|
|
6255
|
+
await ctx.transition(tracked, {
|
|
6256
|
+
state: "failed",
|
|
6257
|
+
error: errorMessage$1(error)
|
|
6258
|
+
});
|
|
6259
|
+
throw error;
|
|
6260
|
+
}
|
|
6261
|
+
};
|
|
5488
6262
|
/** Per-controller route — the adapter's nonce queue and the replace stash are engine state. */
|
|
5489
6263
|
const createEvmRoute = () => {
|
|
5490
6264
|
const evm = new EvmChainAdapter();
|
|
5491
6265
|
/** In-memory build context keyed by tx id — the inputs a replace rebuilds from. */
|
|
5492
6266
|
const pending = /* @__PURE__ */ new Map();
|
|
5493
|
-
const
|
|
5494
|
-
const { account, network,
|
|
6267
|
+
const prepareSpec = async (ctx, spec) => {
|
|
6268
|
+
const { account, network, fee } = spec;
|
|
5495
6269
|
const { networks } = ctx.options;
|
|
5496
6270
|
if (account.kind !== "hd") throw new Error("imported accounts are not yet supported");
|
|
5497
6271
|
const info = networks.info(network);
|
|
@@ -5514,16 +6288,7 @@ const createEvmRoute = () => {
|
|
|
5514
6288
|
} catch {
|
|
5515
6289
|
flags.push("reads-unverified");
|
|
5516
6290
|
}
|
|
5517
|
-
const unsigned = await evm.build(client, network, from,
|
|
5518
|
-
kind: "native",
|
|
5519
|
-
to,
|
|
5520
|
-
value: amount
|
|
5521
|
-
} : {
|
|
5522
|
-
kind: "erc20",
|
|
5523
|
-
token: params.token,
|
|
5524
|
-
to,
|
|
5525
|
-
value: amount
|
|
5526
|
-
}, nonce);
|
|
6291
|
+
const unsigned = await evm.build(client, network, from, spec.intent, nonce);
|
|
5527
6292
|
const quotes = await evm.estimateFee(client);
|
|
5528
6293
|
let quote = quotes.find((q) => q.tier === fee) ?? quotes[1];
|
|
5529
6294
|
if (quote === void 0) throw new Error("no fee quote");
|
|
@@ -5539,7 +6304,7 @@ const createEvmRoute = () => {
|
|
|
5539
6304
|
flags.push("reads-unverified");
|
|
5540
6305
|
}
|
|
5541
6306
|
const feed = evm.applyFee(unsigned, quote);
|
|
5542
|
-
if (
|
|
6307
|
+
if (spec.nativeValue !== void 0 && feeReachesAmount(feed.gas * (feed.maxFeePerGas ?? feed.gasPrice ?? 0n), spec.nativeValue)) flags.push("fee-exceeds-amount");
|
|
5543
6308
|
const request = evm.toSignRequest(feed, account, info.chainId);
|
|
5544
6309
|
const intent = decodeEvm(request);
|
|
5545
6310
|
const txId = newId(ctx.options.random);
|
|
@@ -5564,39 +6329,12 @@ const createEvmRoute = () => {
|
|
|
5564
6329
|
error: void 0
|
|
5565
6330
|
};
|
|
5566
6331
|
tx = await ctx.transition(tx, {});
|
|
5567
|
-
|
|
5568
|
-
try {
|
|
5569
|
-
const result = await apis.sign({
|
|
5570
|
-
request,
|
|
5571
|
-
verify
|
|
5572
|
-
});
|
|
5573
|
-
tx = await ctx.transition(tx, { state: "signed" });
|
|
5574
|
-
signed = result;
|
|
5575
|
-
} catch (error) {
|
|
5576
|
-
await ctx.transition(tx, {
|
|
5577
|
-
state: "rejected",
|
|
5578
|
-
error: errorMessage$1(error)
|
|
5579
|
-
});
|
|
5580
|
-
throw error;
|
|
5581
|
-
}
|
|
5582
|
-
try {
|
|
6332
|
+
return signSubmit(ctx, tx, apis, request, verify, async (signed) => {
|
|
5583
6333
|
const raw = evm.finalize(feed, signed);
|
|
5584
6334
|
const hash = await evm.broadcast(client, raw);
|
|
5585
6335
|
evm.commitNonce(network, from, unsigned.nonce);
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
hash,
|
|
5589
|
-
submittedAt: ctx.options.now()
|
|
5590
|
-
});
|
|
5591
|
-
ctx.track(tx.id, hash);
|
|
5592
|
-
return tx;
|
|
5593
|
-
} catch (error) {
|
|
5594
|
-
await ctx.transition(tx, {
|
|
5595
|
-
state: "failed",
|
|
5596
|
-
error: errorMessage$1(error)
|
|
5597
|
-
});
|
|
5598
|
-
throw error;
|
|
5599
|
-
}
|
|
6336
|
+
return hash;
|
|
6337
|
+
});
|
|
5600
6338
|
};
|
|
5601
6339
|
return {
|
|
5602
6340
|
txId,
|
|
@@ -5609,6 +6347,44 @@ const createEvmRoute = () => {
|
|
|
5609
6347
|
release: () => void 0
|
|
5610
6348
|
};
|
|
5611
6349
|
};
|
|
6350
|
+
const prepare = async (ctx, params) => prepareSpec(ctx, {
|
|
6351
|
+
account: params.account,
|
|
6352
|
+
network: params.network,
|
|
6353
|
+
fee: params.fee,
|
|
6354
|
+
intent: params.token === void 0 ? {
|
|
6355
|
+
kind: "native",
|
|
6356
|
+
to: params.to,
|
|
6357
|
+
value: params.amount
|
|
6358
|
+
} : {
|
|
6359
|
+
kind: "erc20",
|
|
6360
|
+
token: params.token,
|
|
6361
|
+
to: params.to,
|
|
6362
|
+
value: params.amount
|
|
6363
|
+
},
|
|
6364
|
+
nativeValue: params.token === void 0 ? params.amount : void 0
|
|
6365
|
+
});
|
|
6366
|
+
/**
|
|
6367
|
+
* A dApp transaction, already normalized: only `to`, `value` and `data` are kept; nonce and fees
|
|
6368
|
+
* are read again and the dApp's `gas` only raises the floor under our own estimate.
|
|
6369
|
+
*/
|
|
6370
|
+
const prepareCall = async (ctx, request) => {
|
|
6371
|
+
const { tx } = request;
|
|
6372
|
+
if (tx.to === void 0) throw new Error("contract creation is not supported");
|
|
6373
|
+
const value = BigInt(tx.value);
|
|
6374
|
+
return prepareSpec(ctx, {
|
|
6375
|
+
account: request.account,
|
|
6376
|
+
network: request.network,
|
|
6377
|
+
fee: "normal",
|
|
6378
|
+
intent: {
|
|
6379
|
+
kind: "call",
|
|
6380
|
+
to: tx.to,
|
|
6381
|
+
value,
|
|
6382
|
+
data: tx.data,
|
|
6383
|
+
minGas: BigInt(tx.gas)
|
|
6384
|
+
},
|
|
6385
|
+
nativeValue: value > 0n ? value : void 0
|
|
6386
|
+
});
|
|
6387
|
+
};
|
|
5612
6388
|
/**
|
|
5613
6389
|
* EVM speed-up/cancel — replaces a pending tx at the same nonce with a bumped fee (cancel = a
|
|
5614
6390
|
* 0-value transfer to self). The fee is cross-checked and merged first, then the replacement
|
|
@@ -5685,38 +6461,7 @@ const createEvmRoute = () => {
|
|
|
5685
6461
|
error: void 0
|
|
5686
6462
|
};
|
|
5687
6463
|
tx = await ctx.transition(tx, {});
|
|
5688
|
-
|
|
5689
|
-
try {
|
|
5690
|
-
const result = await apis.sign({
|
|
5691
|
-
request,
|
|
5692
|
-
verify
|
|
5693
|
-
});
|
|
5694
|
-
tx = await ctx.transition(tx, { state: "signed" });
|
|
5695
|
-
signed = result;
|
|
5696
|
-
} catch (error) {
|
|
5697
|
-
await ctx.transition(tx, {
|
|
5698
|
-
state: "rejected",
|
|
5699
|
-
error: errorMessage$1(error)
|
|
5700
|
-
});
|
|
5701
|
-
throw error;
|
|
5702
|
-
}
|
|
5703
|
-
try {
|
|
5704
|
-
const raw = evm.finalize(unsigned, signed);
|
|
5705
|
-
const hash = await evm.broadcast(client, raw);
|
|
5706
|
-
tx = await ctx.transition(tx, {
|
|
5707
|
-
state: "submitted",
|
|
5708
|
-
hash,
|
|
5709
|
-
submittedAt: ctx.options.now()
|
|
5710
|
-
});
|
|
5711
|
-
ctx.track(tx.id, hash);
|
|
5712
|
-
return tx;
|
|
5713
|
-
} catch (error) {
|
|
5714
|
-
await ctx.transition(tx, {
|
|
5715
|
-
state: "failed",
|
|
5716
|
-
error: errorMessage$1(error)
|
|
5717
|
-
});
|
|
5718
|
-
throw error;
|
|
5719
|
-
}
|
|
6464
|
+
return signSubmit(ctx, tx, apis, request, verify, async (signed) => evm.broadcast(client, evm.finalize(unsigned, signed)));
|
|
5720
6465
|
};
|
|
5721
6466
|
return {
|
|
5722
6467
|
txId,
|
|
@@ -5740,6 +6485,7 @@ const createEvmRoute = () => {
|
|
|
5740
6485
|
};
|
|
5741
6486
|
return {
|
|
5742
6487
|
prepare,
|
|
6488
|
+
prepareCall,
|
|
5743
6489
|
poll,
|
|
5744
6490
|
prepareReplace
|
|
5745
6491
|
};
|
|
@@ -5928,8 +6674,7 @@ var SolanaChainAdapter = class {
|
|
|
5928
6674
|
return BigInt(str(result, "0"));
|
|
5929
6675
|
}
|
|
5930
6676
|
};
|
|
5931
|
-
|
|
5932
|
-
//#region src/transactions/solana.ts
|
|
6677
|
+
// src/transactions/solana.ts
|
|
5933
6678
|
/** Solana route: SOL/SPL send → sign → broadcast, plus status poll with rebroadcast/drop. */
|
|
5934
6679
|
/** The network's base signature fee, lamports per signature. */
|
|
5935
6680
|
const BASE_FEE_LAMPORTS = 5000n;
|
|
@@ -6038,19 +6783,9 @@ const prepareSolana = async (ctx, params) => {
|
|
|
6038
6783
|
const wire = solana.finalize(messageBytes, [result.signature]);
|
|
6039
6784
|
const hash = await solana.broadcast(rpc, wire);
|
|
6040
6785
|
wires.set(id, wire);
|
|
6041
|
-
|
|
6042
|
-
state: "submitted",
|
|
6043
|
-
hash,
|
|
6044
|
-
submittedAt: ctx.options.now()
|
|
6045
|
-
});
|
|
6046
|
-
ctx.track(tx.id, hash);
|
|
6047
|
-
return tx;
|
|
6786
|
+
return await submitTx(ctx, tx, hash);
|
|
6048
6787
|
} catch (error) {
|
|
6049
|
-
|
|
6050
|
-
await ctx.transition(tx, {
|
|
6051
|
-
state: failed,
|
|
6052
|
-
error: errorMessage$1(error)
|
|
6053
|
-
});
|
|
6788
|
+
await failTx(ctx, tx, error);
|
|
6054
6789
|
throw error;
|
|
6055
6790
|
}
|
|
6056
6791
|
};
|
|
@@ -6123,13 +6858,7 @@ const token2022Intent = (intent, ext, mint, amount) => {
|
|
|
6123
6858
|
warnings
|
|
6124
6859
|
};
|
|
6125
6860
|
};
|
|
6126
|
-
|
|
6127
|
-
//#region src/transactions/controller.ts
|
|
6128
|
-
/**
|
|
6129
|
-
* Transaction controller: orchestrates lifecycle, persistence and polling; each chain family routes
|
|
6130
|
-
* through its own module (`evm.ts`, `solana.ts`, `bitcoin.ts`). A rejected signature (wrong
|
|
6131
|
-
* password, cancelled presence) never broadcasts.
|
|
6132
|
-
*/
|
|
6861
|
+
// src/transactions/controller.ts
|
|
6133
6862
|
const encode = (tx) => new TextEncoder().encode(JSON.stringify(tx));
|
|
6134
6863
|
const isObject = (v) => typeof v === "object" && v !== null;
|
|
6135
6864
|
const TX_STATES = /* @__PURE__ */ new Set([
|
|
@@ -6149,11 +6878,12 @@ const isBtcRecord = (v) => {
|
|
|
6149
6878
|
const { inputs, outputs, vsize, fee, feeRate } = v;
|
|
6150
6879
|
return Array.isArray(inputs) && Array.isArray(outputs) && typeof vsize === "number" && typeof fee === "number" && typeof feeRate === "number" && inputs.every((i) => isObject(i) && typeof i["txid"] === "string" && typeof i["vout"] === "number" && typeof i["value"] === "number" && typeof i["address"] === "string") && outputs.every((o) => isObject(o) && typeof o["address"] === "string" && typeof o["value"] === "number" && typeof o["own"] === "boolean");
|
|
6151
6880
|
};
|
|
6881
|
+
const isSparkDeposit = (v) => isObject(v) && typeof v["walletId"] === "string" && typeof v["groupIndex"] === "number" && (v["network"] === "mainnet" || v["network"] === "regtest") && (v["claimed"] === void 0 || typeof v["claimed"] === "boolean");
|
|
6152
6882
|
const isIntent = (v) => isObject(v) && typeof v["summary"] === "string" && Array.isArray(v["outflows"]) && typeof v["verifiability"] === "string";
|
|
6153
6883
|
const decode = (raw) => {
|
|
6154
6884
|
const parsed = JSON.parse(new TextDecoder().decode(raw));
|
|
6155
6885
|
if (!isObject(parsed)) return;
|
|
6156
|
-
const { id, family, network, account, hash, state, intent, nonce, replaces, parentId, btc, submittedAt, error } = parsed;
|
|
6886
|
+
const { id, family, network, account, hash, state, intent, nonce, replaces, parentId, btc, sparkDeposit, submittedAt, error } = parsed;
|
|
6157
6887
|
if (typeof id !== "string" || family !== "evm" && family !== "solana" && family !== "bitcoin" || typeof network !== "string" || typeof account !== "string" || !isTxState(state) || typeof nonce !== "number" || typeof submittedAt !== "number") return;
|
|
6158
6888
|
return {
|
|
6159
6889
|
id,
|
|
@@ -6167,6 +6897,7 @@ const decode = (raw) => {
|
|
|
6167
6897
|
...typeof replaces === "string" ? { replaces } : {},
|
|
6168
6898
|
...typeof parentId === "string" ? { parentId } : {},
|
|
6169
6899
|
...isBtcRecord(btc) ? { btc } : {},
|
|
6900
|
+
...isSparkDeposit(sparkDeposit) ? { sparkDeposit } : {},
|
|
6170
6901
|
submittedAt,
|
|
6171
6902
|
error: typeof error === "string" ? error : void 0
|
|
6172
6903
|
};
|
|
@@ -6255,16 +6986,77 @@ var TransactionController = class extends Controller {
|
|
|
6255
6986
|
* signing it. Bitcoin outputs it selects stay locked under `requestId` until the returned
|
|
6256
6987
|
* `release` runs or the broadcast moves them to the transaction.
|
|
6257
6988
|
*/
|
|
6258
|
-
async prepareSend(params, requestId) {
|
|
6989
|
+
async prepareSend(params, requestId, sparkDeposit) {
|
|
6259
6990
|
if (this.locked) throw new Error("transactions require an unlocked vault");
|
|
6260
6991
|
const { account, network } = params;
|
|
6261
6992
|
const info = this.options.networks.info(network);
|
|
6262
6993
|
if (account.kind !== "hd") throw new Error("imported accounts are not yet supported");
|
|
6263
6994
|
const ctx = this.routeCtx();
|
|
6264
6995
|
if (info.family === "solana") return prepareSolana(ctx, params);
|
|
6265
|
-
if (info.family === "bitcoin") return this.bitcoinRoute.prepare(ctx, params, requestId);
|
|
6996
|
+
if (info.family === "bitcoin") return this.bitcoinRoute.prepare(ctx, params, requestId, sparkDeposit);
|
|
6266
6997
|
return this.evmRoute.prepare(ctx, params);
|
|
6267
6998
|
}
|
|
6999
|
+
/** The group's next unused `btc.bip84` receive address on a bitcoin `network`. */
|
|
7000
|
+
async nextReceiveAddress(walletId, groupIndex, network) {
|
|
7001
|
+
if (this.locked) throw new Error("transactions require an unlocked vault");
|
|
7002
|
+
return this.bitcoinRoute.nextReceiveAddress(this.routeCtx(), walletId, groupIndex, network);
|
|
7003
|
+
}
|
|
7004
|
+
/** Records that a Spark deposit was credited; persisted with the transaction. */
|
|
7005
|
+
async markDepositClaimed(id) {
|
|
7006
|
+
const tx = this.getState().txs[id];
|
|
7007
|
+
if (tx?.sparkDeposit === void 0) throw new Error(`transaction ${id} is not a Spark deposit`);
|
|
7008
|
+
await this.transition(tx, { sparkDeposit: {
|
|
7009
|
+
...tx.sparkDeposit,
|
|
7010
|
+
claimed: true
|
|
7011
|
+
} });
|
|
7012
|
+
}
|
|
7013
|
+
/**
|
|
7014
|
+
* Internal entry for a dApp's EVM signature request: the request is normalized first, a
|
|
7015
|
+
* transaction is rebuilt with fresh nonce and fees (the dApp's own are discarded), and a message
|
|
7016
|
+
* is decoded and signed as is.
|
|
7017
|
+
*/
|
|
7018
|
+
async prepareDapp(account, request) {
|
|
7019
|
+
if (this.locked) throw new Error("transactions require an unlocked vault");
|
|
7020
|
+
const normalized = normalizeEvm(request);
|
|
7021
|
+
if (account.kind !== "hd") throw new Error("imported accounts are not yet supported");
|
|
7022
|
+
if (normalized.kind === "evm.transaction") return this.evmRoute.prepareCall(this.routeCtx(), {
|
|
7023
|
+
account,
|
|
7024
|
+
network: normalized.network,
|
|
7025
|
+
tx: normalized.tx
|
|
7026
|
+
});
|
|
7027
|
+
if (normalized.kind !== "evm.personal-message" && normalized.kind !== "evm.typed-data") throw new Error(`unsupported dApp request ${normalized.kind}`);
|
|
7028
|
+
const { kind, network } = normalized;
|
|
7029
|
+
const apis = this.options.chains ?? chains(this.options.wallet);
|
|
7030
|
+
return {
|
|
7031
|
+
account,
|
|
7032
|
+
network,
|
|
7033
|
+
intent: decodeEvm(normalized),
|
|
7034
|
+
checks: [],
|
|
7035
|
+
staleReason: () => void 0,
|
|
7036
|
+
release: () => void 0,
|
|
7037
|
+
submit: async (verify) => {
|
|
7038
|
+
const result = await apis.sign({
|
|
7039
|
+
request: normalized.kind === "evm.personal-message" ? {
|
|
7040
|
+
kind: normalized.kind,
|
|
7041
|
+
network,
|
|
7042
|
+
account,
|
|
7043
|
+
message: normalized.message
|
|
7044
|
+
} : {
|
|
7045
|
+
kind: normalized.kind,
|
|
7046
|
+
network,
|
|
7047
|
+
account,
|
|
7048
|
+
typedData: normalized.typedData
|
|
7049
|
+
},
|
|
7050
|
+
verify
|
|
7051
|
+
});
|
|
7052
|
+
if (result.kind !== kind || !("signature" in result)) throw new Error("unexpected sign result");
|
|
7053
|
+
return {
|
|
7054
|
+
kind,
|
|
7055
|
+
signature: result.signature
|
|
7056
|
+
};
|
|
7057
|
+
}
|
|
7058
|
+
};
|
|
7059
|
+
}
|
|
6268
7060
|
/**
|
|
6269
7061
|
* Internal entry for replacements and CPFP: builds, cross-checks and reserves what the
|
|
6270
7062
|
* replacement needs without signing it. The original must be `submitted` or `pending`.
|
|
@@ -6315,8 +7107,7 @@ var TransactionController = class extends Controller {
|
|
|
6315
7107
|
this.timers.set(id, stop);
|
|
6316
7108
|
}
|
|
6317
7109
|
};
|
|
6318
|
-
|
|
6319
|
-
//#region src/engine.ts
|
|
7110
|
+
// src/engine.ts
|
|
6320
7111
|
/**
|
|
6321
7112
|
* Composition root: wires the Wallet's lock state to the SparkController's lifecycle. Unlocked →
|
|
6322
7113
|
* open the EngineStore and start every declared account; locked → abort event loops, close every
|
|
@@ -6409,11 +7200,44 @@ function createWalletEngine(options) {
|
|
|
6409
7200
|
wallet: options.wallet,
|
|
6410
7201
|
accounts,
|
|
6411
7202
|
addressBook,
|
|
6412
|
-
nostr: {
|
|
7203
|
+
nostr: { syncSigner: (walletId) => nostr(options.wallet).syncSigner(walletId) },
|
|
6413
7204
|
metaSync: options.metaSync,
|
|
6414
7205
|
logger: options.logger,
|
|
6415
7206
|
now: options.now
|
|
6416
7207
|
});
|
|
7208
|
+
const depositClaimer = new DepositClaimer({
|
|
7209
|
+
transactions,
|
|
7210
|
+
spark: controller,
|
|
7211
|
+
networks,
|
|
7212
|
+
logger: options.logger
|
|
7213
|
+
});
|
|
7214
|
+
const connections = new ConnectionController({
|
|
7215
|
+
openStore: options.openStore,
|
|
7216
|
+
random: options.random,
|
|
7217
|
+
now: options.now,
|
|
7218
|
+
logger: options.logger,
|
|
7219
|
+
allowLocalhost: options.allowLocalhostOrigins ?? false,
|
|
7220
|
+
selectedGroup: () => accounts.getState().selected ?? void 0,
|
|
7221
|
+
evmAddress: async (group, network) => {
|
|
7222
|
+
const account = await chains(options.wallet).account({
|
|
7223
|
+
...group,
|
|
7224
|
+
template: "evm.bip44",
|
|
7225
|
+
network
|
|
7226
|
+
});
|
|
7227
|
+
if (!("address" in account)) throw new Error("no EVM address for the group");
|
|
7228
|
+
return account.address;
|
|
7229
|
+
},
|
|
7230
|
+
enabledNetworks: () => networks.enabled("evm"),
|
|
7231
|
+
invalidate: (origin) => {
|
|
7232
|
+
requests.invalidateOrigin(origin);
|
|
7233
|
+
}
|
|
7234
|
+
});
|
|
7235
|
+
const eip1193 = new Eip1193Handler({
|
|
7236
|
+
connections,
|
|
7237
|
+
requests,
|
|
7238
|
+
networks,
|
|
7239
|
+
defaultNetwork: () => networks.enabled("evm")[0] ?? "eip155:1"
|
|
7240
|
+
});
|
|
6417
7241
|
let chain = Promise.resolve();
|
|
6418
7242
|
let store;
|
|
6419
7243
|
let started = false;
|
|
@@ -6441,8 +7265,12 @@ function createWalletEngine(options) {
|
|
|
6441
7265
|
await assets.init();
|
|
6442
7266
|
await transactions.init();
|
|
6443
7267
|
await requests.init();
|
|
7268
|
+
await connections.init();
|
|
7269
|
+
depositClaimer.start();
|
|
6444
7270
|
} else if (!unlocked && started) {
|
|
6445
7271
|
started = false;
|
|
7272
|
+
depositClaimer.stop();
|
|
7273
|
+
connections.lock();
|
|
6446
7274
|
requests.lock();
|
|
6447
7275
|
addressBook.lock();
|
|
6448
7276
|
transactions.lock();
|
|
@@ -6486,6 +7314,8 @@ function createWalletEngine(options) {
|
|
|
6486
7314
|
transactions,
|
|
6487
7315
|
requests,
|
|
6488
7316
|
addressBook,
|
|
7317
|
+
connections,
|
|
7318
|
+
eip1193,
|
|
6489
7319
|
spark: controller,
|
|
6490
7320
|
accounts,
|
|
6491
7321
|
metaSync,
|
|
@@ -6513,8 +7343,7 @@ function createWalletEngine(options) {
|
|
|
6513
7343
|
unsubscribe
|
|
6514
7344
|
};
|
|
6515
7345
|
}
|
|
6516
|
-
|
|
6517
|
-
//#region src/backup/memory.ts
|
|
7346
|
+
// src/backup/memory.ts
|
|
6518
7347
|
const tick = Promise.resolve();
|
|
6519
7348
|
/** In-memory CloudStore for tests and the playground; swap the Map for cross-engine sharing. */
|
|
6520
7349
|
function createMemoryCloudStore(backing) {
|
|
@@ -6546,7 +7375,6 @@ function createMemoryCloudStore(backing) {
|
|
|
6546
7375
|
}
|
|
6547
7376
|
};
|
|
6548
7377
|
}
|
|
6549
|
-
|
|
6550
|
-
export { AccountTreeController, AccountTreeError, AddressBookController, AddressBookError, BackupController, BackupControllerError, Controller, EngineStoreError, MetaSyncController, MetaSyncError, PaymentError, REQUEST_TTL_MS, RequestController, SparkController, SparkControllerError, createEventBus, createMemoryCloudStore, createMemoryEngineStore, createWalletEngine, decodeExitChain, encodeExitChain, parsePayment, prepareZap, resolvePayment, sparkStore };
|
|
7378
|
+
export { AccountTreeController, AccountTreeError, AddressBookController, AddressBookError, BackupController, BackupControllerError, ConnectionController, Controller, Eip1193Handler, EngineStoreError, MetaSyncController, MetaSyncError, PaymentError, ProviderError, REQUEST_TTL_MS, RequestController, SparkController, SparkControllerError, createEventBus, createMemoryCloudStore, createMemoryEngineStore, createWalletEngine, decodeExitChain, encodeExitChain, parsePayment, prepareZap, resolvePayment, sparkStore };
|
|
6551
7379
|
|
|
6552
7380
|
//# sourceMappingURL=index.mjs.map
|