@usebutr/bitcoin 1.1.0 → 2.0.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/index.js CHANGED
@@ -1,146 +1,69 @@
1
- import { buildAccount, buildWalletCapabilities, createWalletStandardCore, discoverWalletStandard, getFeature, slugify } from "@usebutr/wallet-standard-shared";
2
- import { base64ToBytes, bytesToBase64, bytesToHex, hexToBytes } from "@usebutr/core";
1
+ import { BITCOIN_CHAINS, BITCOIN_CHAINS_LIST, ConnectionError, base64ToBytes, buildAccount, bytesToBase64, bytesToHex, hexToBytes, logWarn, resolveChain } from "@usebutr/core";
2
+ import { createWalletStandardCore, discoverWalletStandard, getFeature, slugify } from "@usebutr/wallet-standard-shared";
3
+ import { z } from "zod";
3
4
 
4
- //#region src/capabilities.ts
5
- /**
6
- * `signIn` is false: there is no Sign-In-With-Bitcoin standard, and BIP-322
7
- * is message-signing oriented rather than auth. `sendTransfer` leaves UTXO
8
- * selection, fees and broadcast to the wallet; `signPsbt` is sign-only.
9
- */
10
- const resolveBitcoinCapabilities = (input) => buildWalletCapabilities({
11
- chainCount: input.chainCount,
12
- events: input.features.events,
13
- sendTransaction: input.features.sendTransfer,
14
- signIn: false,
15
- signMessage: input.features.signMessage,
16
- signTransaction: input.features.signPsbt
17
- });
18
-
19
- //#endregion
20
5
  //#region src/wallet-standard-adapter.ts
21
- const BITCOIN_PREFIX = "bip122:";
22
- const BITCOIN_DECIMALS = 8;
23
- const BITCOIN_MAINNET_ID = "bip122:000000000019d6689c085ae165831e93";
24
6
  /**
25
- * `sendTx` takes `{ amount, recipient }` for `bitcoin:sendTransfer` and
26
- * `signTransaction` takes raw PSBT bytes for `bitcoin:signPsbt`. Balance
27
- * and receipt reads would need an Esplora/Electrum client butr doesn't ship.
7
+ * `bitcoin:sendTransfer` backs `sendTx` and `bitcoin:signPsbt` backs
8
+ * `signTransaction`, both routing `options.chain` per call. Balance and
9
+ * receipt reads would need an Esplora/Electrum client butr doesn't ship.
28
10
  */
29
11
  const buildBitcoinAdapter = (wallet, registerDisconnector) => {
30
12
  const core = createWalletStandardCore({
31
- chainPrefix: BITCOIN_PREFIX,
13
+ chains: BITCOIN_CHAINS_LIST,
32
14
  id: slugify("btc", wallet.name),
33
15
  label: "Bitcoin",
34
16
  namespace: "bip122",
35
- platform: "Bitcoin",
36
- preferredChainIds: [BITCOIN_MAINNET_ID],
17
+ preferredChainIds: [BITCOIN_CHAINS.mainnet.id],
37
18
  registerDisconnector,
38
19
  trackChainChanges: true,
39
20
  wallet
40
21
  });
41
22
  if (core === null) return null;
42
- const signMessage = getFeature(wallet, "bitcoin:signMessage");
43
- const signPsbt = getFeature(wallet, "bitcoin:signPsbt");
44
- const sendTransfer = getFeature(wallet, "bitcoin:sendTransfer");
45
- /** Resolve a caller-supplied target into a chain the wallet advertises,
46
- * accepting either a full CAIP-2 id or a bare genesis-hash reference. */
47
- const resolveTargetChain = (targetChainId) => {
48
- const candidate = targetChainId.startsWith(BITCOIN_PREFIX) ? targetChainId : `${BITCOIN_PREFIX}${targetChainId}`;
49
- if (!wallet.chains.includes(candidate)) throw new Error(`Wallet ${wallet.name} does not advertise chain "${candidate}". Available: ${wallet.chains.join(", ")}`);
50
- return candidate;
51
- };
52
- const sendTransferTx = async (tx, account, chain) => {
53
- if (sendTransfer === void 0) throw new Error(`Wallet ${wallet.name} does not advertise bitcoin:sendTransfer`);
54
- if (typeof tx !== "object" || tx === null || !("amount" in tx) || typeof tx.amount !== "bigint" || !("recipient" in tx) || typeof tx.recipient !== "string") throw new TypeError("Bitcoin sendTx expects { amount: bigint, recipient: string }: pass the recipient address and an amount in satoshis");
55
- const { amount, recipient } = tx;
56
- return (await sendTransfer.sendTransfer({
57
- account: core.resolveAccount(account),
58
- amount,
59
- chain: chain ?? core.currentChainId(),
60
- recipient
61
- })).txid;
62
- };
23
+ const sendTransfer = getFeature(wallet, "bitcoin:sendTransfer", "sendTransfer");
24
+ const signMessage = getFeature(wallet, "bitcoin:signMessage", "signMessage");
25
+ const signPsbt = getFeature(wallet, "bitcoin:signPsbt", "signPsbt");
63
26
  return {
64
- ...core,
65
- capabilities: resolveBitcoinCapabilities({
66
- chainCount: core.chainCount,
67
- features: {
68
- events: core.hasEvents,
69
- sendTransfer: Boolean(sendTransfer),
70
- signMessage: Boolean(signMessage),
71
- signPsbt: Boolean(signPsbt)
72
- }
73
- }),
27
+ ...core.base,
74
28
  chainPlatform: "bitcoin",
75
- getBalance: () => Promise.resolve({
76
- decimals: BITCOIN_DECIMALS,
77
- formatted: "0",
78
- symbol: "BTC",
79
- value: 0n
80
- }),
81
- getTransactionReceipt: () => Promise.resolve({ status: "Pending" }),
82
- async requestAccounts() {
83
- await core.connect();
84
- },
85
- sendTx: (tx, account) => sendTransferTx(tx, account),
86
- async sendTxToChain(tx, targetChainId, account, cb) {
87
- const target = resolveTargetChain(targetChainId);
88
- if (target !== core.currentChainId()) cb?.();
89
- return await sendTransferTx(tx, account, target);
90
- },
91
- async signMessage(msg, account) {
92
- if (signMessage === void 0) throw new Error(`Wallet ${wallet.name} does not advertise bitcoin:signMessage`);
29
+ ...sendTransfer !== void 0 && { async sendTx({ amount, recipient }, options) {
30
+ return (await sendTransfer.sendTransfer({
31
+ account: core.resolveAccount(options?.account),
32
+ amount,
33
+ chain: core.resolveChainId(options?.chain),
34
+ recipient
35
+ })).txid;
36
+ } },
37
+ ...signMessage !== void 0 && { async signMessage(message, options) {
93
38
  const output = await signMessage.signMessage({
94
- account: core.resolveAccount(account),
95
- message: msg
39
+ account: core.resolveAccount(options?.account),
40
+ message
96
41
  });
97
42
  return {
98
43
  signature: output.signature,
99
44
  signedMessage: output.signedMessage
100
45
  };
101
- },
102
- ...signPsbt === void 0 ? {} : { async signTransaction(tx, account) {
103
- const wsAccount = core.resolveAccount(account);
104
- if (!(tx instanceof Uint8Array)) throw new TypeError("Bitcoin signTransaction expects a PSBT as Uint8Array (e.g. psbt.toBuffer())");
46
+ } },
47
+ ...signPsbt !== void 0 && { async signTransaction(psbt, options) {
105
48
  return (await signPsbt.signPsbt({
106
- account: wsAccount,
107
- chain: core.currentChainId(),
108
- psbt: tx
49
+ account: core.resolveAccount(options?.account),
50
+ chain: core.resolveChainId(options?.chain),
51
+ psbt
109
52
  })).signedPsbt;
110
53
  } }
111
54
  };
112
55
  };
113
56
  /** Requires the optional `@wallet-standard/app` peer dep. */
114
- const discoverBitcoinAdapters = (onAdapter) => discoverWalletStandard(onAdapter, (wallet, registerDisconnector) => buildBitcoinAdapter(wallet, registerDisconnector));
57
+ const discoverBitcoinAdapters = (onAdapter) => discoverWalletStandard(onAdapter, buildBitcoinAdapter);
115
58
 
116
59
  //#endregion
117
- //#region src/chains.ts
118
- /**
119
- * CAIP-2 Bitcoin references are the first 8 bytes of each network's
120
- * genesis block hash, which is what wallets advertise in `wallet.chains`.
121
- * No major wallet exposes a switch-chain RPC, so chain stays local state.
122
- */
123
- const BITCOIN_CHAINS = {
124
- mainnet: {
125
- id: "bip122:000000000019d6689c085ae165831e93",
126
- name: "Bitcoin",
127
- namespace: "bip122",
128
- reference: "000000000019d6689c085ae165831e93"
129
- },
130
- signet: {
131
- id: "bip122:00000008819873e925422c1ff0f99f7c",
132
- name: "Bitcoin Signet",
133
- namespace: "bip122",
134
- reference: "00000008819873e925422c1ff0f99f7c"
135
- },
136
- testnet: {
137
- id: "bip122:000000000933ea01ad0ee984209779ba",
138
- name: "Bitcoin Testnet",
139
- namespace: "bip122",
140
- reference: "000000000933ea01ad0ee984209779ba"
141
- }
60
+ //#region src/injected/chain.ts
61
+ const assertBitcoinChain = (chain) => {
62
+ if (chain.namespace !== "bip122") throw new Error(`Bitcoin adapter received non-Bitcoin chain "${chain.id}". Pass a chain with namespace "bip122".`);
142
63
  };
143
- const BITCOIN_CHAINS_LIST = Object.values(BITCOIN_CHAINS);
64
+ /** An injected wallet has one network for every call, so a target it cannot
65
+ * move to is refused rather than sent on the network it happens to be on. */
66
+ const chainMismatch = (wallet, current, target) => new ConnectionError("ChainMismatch", `Wallet ${wallet} is on ${current.name} and cannot switch to ${target.name}`);
144
67
 
145
68
  //#endregion
146
69
  //#region src/injected/icon.ts
@@ -152,36 +75,56 @@ const GENERIC_BITCOIN_ICON = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDov
152
75
 
153
76
  //#endregion
154
77
  //#region src/injected/sats-connect.ts
155
- const CAPS_SATS_CONNECT = {
156
- getBalance: false,
157
- getTransactionReceipt: false,
158
- requestAccounts: true,
159
- sendTransaction: true,
160
- signIn: false,
161
- signMessage: true,
162
- signTransaction: true,
163
- subscribe: false,
164
- switchAccount: false,
165
- switchChain: false
166
- };
167
78
  const ACCOUNT_PURPOSES = ["payment", "ordinals"];
168
79
  const CONNECT_MESSAGE = "Connect to butr";
80
+ const networkNameSchema = z.enum([
81
+ "Mainnet",
82
+ "Regtest",
83
+ "Signet",
84
+ "Testnet",
85
+ "Testnet4"
86
+ ]);
87
+ /** sats-connect names networks, not chains. Regtest sits outside butr's
88
+ * registry, so `resolveChain` names it by its CAIP-2 id. */
89
+ const NETWORK_CHAIN_IDS = {
90
+ Mainnet: BITCOIN_CHAINS.mainnet.id,
91
+ Regtest: "bip122:0f9188f13cb7b2c71f2a335e3a4fc328",
92
+ Signet: BITCOIN_CHAINS.signet.id,
93
+ Testnet: BITCOIN_CHAINS.testnet.id,
94
+ Testnet4: BITCOIN_CHAINS.testnet4.id
95
+ };
96
+ const satsAddressSchema = z.object({
97
+ address: z.string(),
98
+ publicKey: z.string().optional(),
99
+ purpose: z.string().optional()
100
+ });
101
+ const accountResultSchema = z.union([z.array(satsAddressSchema), z.object({ addresses: z.array(satsAddressSchema) }).transform((result) => result.addresses)]);
102
+ const networkResultSchema = z.object({ bitcoin: z.object({ name: networkNameSchema }) });
103
+ const sendTransferResultSchema = z.object({ txid: z.string() });
104
+ const signMessageResultSchema = z.object({
105
+ messageHash: z.string().optional(),
106
+ signature: z.string()
107
+ });
108
+ const signPsbtResultSchema = z.object({ psbt: z.string() });
109
+ const changeNetworkResultSchema = z.unknown();
169
110
  const pickPaymentAddress = (addresses) => addresses.find((a) => a.purpose === "payment") ?? addresses[0];
111
+ const networkFor = (chain) => Object.entries(NETWORK_CHAIN_IDS).find(([, chainId]) => chainId === chain.id)?.[0];
170
112
  /**
171
- * Only the payment address is exposed as an Account: sats-connect's
172
- * `sendTransfer` takes no sender and always debits it. The taproot ordinals
173
- * address stays reachable through `getSigner()` and `signMessage`'s account.
113
+ * Only the payment address is an Account (`sendTransfer` always debits it); `signMessage` also
114
+ * takes the ordinals address. The network is wallet-wide, so a call targeting another chain
115
+ * moves it with `wallet_changeNetwork` first.
174
116
  */
175
117
  const buildSatsConnectAdapter = (id, name, provider) => {
176
118
  let chain = BITCOIN_CHAINS.mainnet;
177
119
  let session = { status: "disconnected" };
178
- const callRequest = async (method, params) => {
120
+ const listeners = /* @__PURE__ */ new Set();
121
+ const callRequest = async (method, schema, params) => {
179
122
  const response = await provider.request(method, params);
180
123
  if (response.error) throw new Error(`[butr/bitcoin] sats-connect ${method} failed: ${response.error.message}`);
181
- return response.result;
124
+ return schema.parse(response.result);
182
125
  };
183
126
  const startSession = async (method, params) => {
184
- const addresses = (await callRequest(method, params))?.addresses ?? [];
127
+ const addresses = await callRequest(method, accountResultSchema, params);
185
128
  const payment = pickPaymentAddress(addresses);
186
129
  if (payment === void 0) throw new Error(`Wallet ${name} returned no addresses from ${method}`);
187
130
  session = {
@@ -190,10 +133,12 @@ const buildSatsConnectAdapter = (id, name, provider) => {
190
133
  status: "connected"
191
134
  };
192
135
  };
193
- const requestSession = () => startSession("getAccounts", {
194
- message: CONNECT_MESSAGE,
195
- purposes: ACCOUNT_PURPOSES
196
- });
136
+ const readSession = () => startSession("wallet_getAccount", { addresses: ACCOUNT_PURPOSES });
137
+ const readChain = async () => {
138
+ const result = await callRequest("wallet_getNetwork", networkResultSchema);
139
+ return resolveChain(NETWORK_CHAIN_IDS[result.bitcoin.name], BITCOIN_CHAINS_LIST);
140
+ };
141
+ const exposedAccounts = () => session.status === "connected" ? [buildAccount(session.payment, chain)] : [];
197
142
  const requireSession = () => {
198
143
  if (session.status !== "connected") throw new Error("No connected account");
199
144
  return session;
@@ -205,199 +150,199 @@ const buildSatsConnectAdapter = (id, name, provider) => {
205
150
  if (match === void 0) throw new Error(`Account ${account.walletAddress} is not exposed by ${name}`);
206
151
  return match.address;
207
152
  };
208
- const sendTransferTx = async (tx, account) => {
209
- const sender = resolveAddress(account);
210
- if (sender !== requireSession().payment) throw new Error(`Wallet ${name} always debits its payment address; ${sender} cannot be used as the sender.`);
211
- if (typeof tx !== "object" || tx === null || !("recipient" in tx) || typeof tx.recipient !== "string" || !("amount" in tx) || typeof tx.amount !== "bigint") throw new TypeError("Bitcoin sendTx expects { amount: bigint, recipient: string }: amount in satoshis");
212
- const { amount, recipient } = tx;
213
- return (await callRequest("sendTransfer", { recipients: [{
214
- address: recipient,
215
- amount: amount.toString()
216
- }] })).txid;
153
+ const moveTo = async (target) => {
154
+ assertBitcoinChain(target);
155
+ const current = await readChain();
156
+ if (current.id !== target.id) {
157
+ const network = networkFor(target);
158
+ if (network === void 0) throw chainMismatch(name, current, target);
159
+ await callRequest("wallet_changeNetwork", changeNetworkResultSchema, { name: network });
160
+ }
161
+ if (chain.id === target.id) return;
162
+ await readSession();
163
+ chain = resolveChain(target.id, BITCOIN_CHAINS_LIST);
164
+ const accounts = exposedAccounts();
165
+ for (const listener of listeners) listener({
166
+ accounts,
167
+ type: "accountsChanged"
168
+ });
169
+ };
170
+ const prepare = async (options) => {
171
+ if (options?.chain !== void 0) await moveTo(options.chain);
172
+ return resolveAddress(options?.account);
217
173
  };
218
174
  return {
219
- capabilities: CAPS_SATS_CONNECT,
220
175
  chainPlatform: "bitcoin",
221
- async connect(opts) {
222
- if (opts?.silent === true) {
223
- await startSession("wallet_getAccount", { addresses: ACCOUNT_PURPOSES });
224
- return;
225
- }
226
- await requestSession();
176
+ async connect(options) {
177
+ await (options?.silent === true ? readSession() : startSession("getAccounts", {
178
+ message: CONNECT_MESSAGE,
179
+ purposes: ACCOUNT_PURPOSES
180
+ }));
181
+ try {
182
+ chain = await readChain();
183
+ } catch {}
227
184
  },
228
185
  disconnect: () => {
229
186
  session = { status: "disconnected" };
230
187
  return Promise.resolve();
231
188
  },
232
- getAccount: () => Promise.resolve(session.status === "connected" ? buildAccount(session.payment, chain) : null),
233
- getAccounts: () => Promise.resolve(session.status === "connected" ? [buildAccount(session.payment, chain)] : []),
234
- getBalance: () => Promise.resolve({
235
- decimals: 8,
236
- formatted: "0",
237
- symbol: "BTC",
238
- value: 0n
189
+ getAccounts: () => Promise.resolve(exposedAccounts()),
190
+ getSigner: () => Promise.resolve({
191
+ kind: "sats-connect",
192
+ provider
239
193
  }),
240
- getSigner: () => Promise.resolve(provider),
241
- getTransactionReceipt: () => Promise.resolve({ status: "Pending" }),
242
194
  icon: GENERIC_BITCOIN_ICON,
243
195
  id,
244
196
  name,
245
- requestAccounts: () => requestSession(),
246
- sendTx: (tx, account) => sendTransferTx(tx, account),
247
- sendTxToChain: (tx, _targetChainId, account, cb) => {
248
- cb?.();
249
- return sendTransferTx(tx, account);
197
+ async sendTx({ amount, recipient }, options) {
198
+ const sender = await prepare(options);
199
+ if (sender !== requireSession().payment) throw new Error(`Wallet ${name} always debits its payment address; ${sender} cannot be used as the sender.`);
200
+ return (await callRequest("sendTransfer", sendTransferResultSchema, { recipients: [{
201
+ address: recipient,
202
+ amount: Number(amount)
203
+ }] })).txid;
250
204
  },
251
- async signMessage(msg, account) {
252
- const address = resolveAddress(account);
253
- const result = await callRequest("signMessage", {
205
+ async signMessage(message, options) {
206
+ const address = resolveAddress(options?.account);
207
+ const result = await callRequest("signMessage", signMessageResultSchema, {
254
208
  address,
255
- message: new TextDecoder().decode(msg)
209
+ message: new TextDecoder().decode(message)
256
210
  });
257
211
  return {
258
212
  signature: base64ToBytes(result.signature),
259
- signedMessage: msg
213
+ signedMessage: message
260
214
  };
261
215
  },
262
- async signTransaction(tx) {
263
- if (!(tx instanceof Uint8Array)) throw new TypeError("Bitcoin signTransaction expects a PSBT as Uint8Array (e.g. psbt.toBuffer())");
264
- const result = await callRequest("signPsbt", { psbt: bytesToBase64(tx) });
216
+ async signTransaction(psbt, options) {
217
+ await prepare(options);
218
+ const result = await callRequest("signPsbt", signPsbtResultSchema, { psbt: bytesToBase64(psbt) });
265
219
  return base64ToBytes(result.psbt);
266
220
  },
267
- switchChain: (target) => {
268
- if (target.namespace !== "bip122") throw new Error(`Bitcoin adapter received non-Bitcoin chain "${target.id}". Pass a chain with namespace "bip122".`);
269
- chain = target;
270
- return Promise.resolve();
271
- }
221
+ subscribe: (listener) => {
222
+ listeners.add(listener);
223
+ return () => {
224
+ listeners.delete(listener);
225
+ };
226
+ },
227
+ switchChain: moveTo
272
228
  };
273
229
  };
274
230
 
275
231
  //#endregion
276
232
  //#region src/injected/unisat.ts
277
- const CAPS_UNISAT = {
278
- getBalance: false,
279
- getTransactionReceipt: false,
280
- requestAccounts: true,
281
- sendTransaction: true,
282
- signIn: false,
283
- signMessage: true,
284
- signTransaction: true,
285
- subscribe: true,
286
- switchAccount: false,
287
- switchChain: false
288
- };
233
+ const NETWORK_CHAINS = /* @__PURE__ */ new Map([
234
+ ["livenet", BITCOIN_CHAINS.mainnet],
235
+ ["mainnet", BITCOIN_CHAINS.mainnet],
236
+ ["signet", BITCOIN_CHAINS.signet],
237
+ ["testnet", BITCOIN_CHAINS.testnet]
238
+ ]);
239
+ const SWITCH_NETWORKS = /* @__PURE__ */ new Map([[BITCOIN_CHAINS.mainnet.id, "livenet"], [BITCOIN_CHAINS.testnet.id, "testnet"]]);
289
240
  const toStringArray = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
290
241
  /**
291
- * The UniSat shape is consistent enough across `window.unisat`,
292
- * `window.okxwallet.bitcoin` and `window.btc` that one adapter covers all
293
- * three; the differences (network names, `sendBitcoin`) are probed per call.
242
+ * One adapter covers `window.unisat`, `window.okxwallet.bitcoin` and `window.btc`.
243
+ * The network is wallet-wide, so a call targeting another chain switches it
244
+ * first, or rejects when the provider cannot switch.
294
245
  */
295
246
  const buildUnisatAdapter = (id, name, provider) => {
247
+ const getNetwork = provider.getNetwork?.bind(provider);
248
+ const on = provider.on?.bind(provider);
249
+ const sendBitcoin = provider.sendBitcoin?.bind(provider);
250
+ const switchNetwork = provider.switchNetwork?.bind(provider);
296
251
  let chain = BITCOIN_CHAINS.mainnet;
297
- const sendBitcoinTx = async (tx) => {
298
- if (typeof provider.sendBitcoin !== "function") throw new TypeError(`Wallet ${name} does not expose sendBitcoin`);
299
- if (typeof tx !== "object" || tx === null || !("recipient" in tx) || typeof tx.recipient !== "string" || !("amount" in tx) || typeof tx.amount !== "bigint") throw new TypeError("Bitcoin sendTx expects { amount: bigint, recipient: string }: amount in satoshis");
300
- const { amount, recipient } = tx;
301
- return await provider.sendBitcoin(recipient, Number(amount));
252
+ const readChain = async () => {
253
+ if (getNetwork === void 0) return chain;
254
+ const network = await getNetwork();
255
+ const match = NETWORK_CHAINS.get(network);
256
+ if (match === void 0) throw new Error(`Wallet ${name} reported an unknown network "${network}"`);
257
+ return match;
302
258
  };
303
- const refreshChain = async () => {
304
- if (typeof provider.getNetwork !== "function") return;
259
+ const readAccounts = async () => {
260
+ const addresses = await provider.getAccounts();
261
+ if (addresses.length === 0) return [];
305
262
  try {
306
- const network = await provider.getNetwork();
307
- if (network === "testnet") chain = BITCOIN_CHAINS.testnet;
308
- else if (network === "signet") chain = BITCOIN_CHAINS.signet;
309
- else chain = BITCOIN_CHAINS.mainnet;
263
+ chain = await readChain();
310
264
  } catch {}
265
+ return addresses.map((address) => buildAccount(address, chain));
266
+ };
267
+ const assertActive = async (account) => {
268
+ if (account === void 0) return;
269
+ const [active] = await provider.getAccounts();
270
+ if (account.walletAddress !== active) throw new Error(`Wallet ${name} signs only with its active account, and ${account.walletAddress} is not it`);
271
+ };
272
+ const moveTo = async (target) => {
273
+ assertBitcoinChain(target);
274
+ chain = await readChain();
275
+ if (chain.id === target.id) return;
276
+ const network = SWITCH_NETWORKS.get(target.id);
277
+ if (switchNetwork === void 0 || network === void 0) throw chainMismatch(name, chain, target);
278
+ await switchNetwork(network);
279
+ chain = resolveChain(target.id, BITCOIN_CHAINS_LIST);
280
+ };
281
+ const prepare = async (options) => {
282
+ if (options?.chain !== void 0) await moveTo(options.chain);
283
+ await assertActive(options?.account);
311
284
  };
312
285
  return {
313
- capabilities: CAPS_UNISAT,
314
286
  chainPlatform: "bitcoin",
315
- async connect(opts) {
316
- if (opts?.silent === true) {
317
- if ((await provider.getAccounts()).length === 0) throw new Error("No authorized accounts for silent reconnect");
318
- await refreshChain();
287
+ async connect(options) {
288
+ if (options?.silent !== true) {
289
+ await provider.requestAccounts();
319
290
  return;
320
291
  }
321
- await provider.requestAccounts();
322
- await refreshChain();
323
- },
324
- disconnect: () => Promise.resolve(),
325
- async getAccount() {
326
- const first = (await provider.getAccounts())[0];
327
- if (first === void 0) return null;
328
- await refreshChain();
329
- return buildAccount(first, chain);
292
+ if ((await provider.getAccounts()).length === 0) throw new Error("No authorized accounts for silent reconnect");
330
293
  },
331
- async getAccounts() {
332
- const accounts = await provider.getAccounts();
333
- if (accounts.length === 0) return [];
334
- await refreshChain();
335
- return accounts.map((a) => buildAccount(a, chain));
336
- },
337
- getBalance: () => Promise.resolve({
338
- decimals: 8,
339
- formatted: "0",
340
- symbol: "BTC",
341
- value: 0n
294
+ getAccounts: readAccounts,
295
+ getSigner: () => Promise.resolve({
296
+ kind: "unisat",
297
+ provider
342
298
  }),
343
- getSigner: () => Promise.resolve(provider),
344
- getTransactionReceipt: () => Promise.resolve({ status: "Pending" }),
345
299
  icon: GENERIC_BITCOIN_ICON,
346
300
  id,
347
301
  name,
348
- async requestAccounts() {
349
- await provider.requestAccounts();
350
- await refreshChain();
351
- },
352
- sendTx: (tx) => sendBitcoinTx(tx),
353
- sendTxToChain: (tx, _targetChainIdDecimal, _account, cb) => {
354
- cb?.();
355
- return sendBitcoinTx(tx);
356
- },
357
- async signMessage(msg) {
358
- const text = new TextDecoder().decode(msg);
359
- const signatureB64 = await provider.signMessage(text);
302
+ async signMessage(message, options) {
303
+ await assertActive(options?.account);
304
+ const signature = await provider.signMessage(new TextDecoder().decode(message));
360
305
  return {
361
- signature: base64ToBytes(signatureB64),
362
- signedMessage: msg
306
+ signature: base64ToBytes(signature),
307
+ signedMessage: message
363
308
  };
364
309
  },
365
- async signTransaction(tx) {
366
- if (!(tx instanceof Uint8Array)) throw new TypeError("Bitcoin signTransaction expects a PSBT as Uint8Array (e.g. psbt.toBuffer())");
367
- const signedHex = await provider.signPsbt(bytesToHex(tx));
368
- return hexToBytes(signedHex);
310
+ async signTransaction(psbt, options) {
311
+ await prepare(options);
312
+ return hexToBytes(await provider.signPsbt(bytesToHex(psbt)));
369
313
  },
370
- subscribe(listener) {
314
+ ...sendBitcoin !== void 0 && { async sendTx({ amount, recipient }, options) {
315
+ await prepare(options);
316
+ return sendBitcoin(recipient, Number(amount));
317
+ } },
318
+ ...on !== void 0 && { subscribe: (listener) => {
319
+ const emit = (accounts) => {
320
+ listener(accounts.length === 0 ? { type: "disconnected" } : {
321
+ accounts,
322
+ type: "accountsChanged"
323
+ });
324
+ };
371
325
  const onAccountsChanged = (...args) => {
372
- const accounts = toStringArray(args[0]);
373
- if (accounts.length === 0) {
374
- listener({ type: "disconnected" });
375
- return;
326
+ emit(toStringArray(args[0]).map((address) => buildAccount(address, chain)));
327
+ };
328
+ const reemitAccounts = async () => {
329
+ try {
330
+ emit(await readAccounts());
331
+ } catch (error) {
332
+ logWarn(`[butr] ${name} accounts could not be re-read after a network change:`, error);
376
333
  }
377
- const built = accounts.map((a) => buildAccount(a, chain));
378
- const first = built[0];
379
- if (first === void 0) return;
380
- listener({
381
- account: first,
382
- accounts: built,
383
- type: "accountChanged"
384
- });
385
334
  };
386
335
  const onNetworkChanged = () => {
387
- refreshChain();
336
+ reemitAccounts();
388
337
  };
389
- provider.on?.("accountsChanged", onAccountsChanged);
390
- provider.on?.("networkChanged", onNetworkChanged);
338
+ on("accountsChanged", onAccountsChanged);
339
+ on("networkChanged", onNetworkChanged);
391
340
  return () => {
392
341
  provider.removeListener?.("accountsChanged", onAccountsChanged);
393
342
  provider.removeListener?.("networkChanged", onNetworkChanged);
394
343
  };
395
- },
396
- async switchChain(target) {
397
- if (target.namespace !== "bip122") throw new Error(`Bitcoin adapter received non-Bitcoin chain "${target.id}". Pass a chain with namespace "bip122".`);
398
- chain = target;
399
- await refreshChain();
400
- }
344
+ } },
345
+ ...switchNetwork !== void 0 && { switchChain: moveTo }
401
346
  };
402
347
  };
403
348
 
@@ -467,5 +412,5 @@ const bitcoinDiscoverer = {
467
412
  };
468
413
 
469
414
  //#endregion
470
- export { BITCOIN_CHAINS, BITCOIN_CHAINS_LIST, GENERIC_BITCOIN_ICON, bitcoinDiscoverer, buildBitcoinAdapter, discoverBitcoinAdapters, discoverInjectedBitcoinAdapter, resolveBitcoinCapabilities };
415
+ export { GENERIC_BITCOIN_ICON, bitcoinDiscoverer, buildBitcoinAdapter, discoverBitcoinAdapters, discoverInjectedBitcoinAdapter };
471
416
  //# sourceMappingURL=index.js.map