@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/CHANGELOG.md +255 -0
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/index.d.ts +50 -76
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +216 -271
- package/dist/index.js.map +1 -1
- package/package.json +24 -6
package/dist/index.js
CHANGED
|
@@ -1,146 +1,69 @@
|
|
|
1
|
-
import { buildAccount,
|
|
2
|
-
import {
|
|
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
|
-
* `
|
|
26
|
-
* `signTransaction
|
|
27
|
-
*
|
|
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
|
-
|
|
13
|
+
chains: BITCOIN_CHAINS_LIST,
|
|
32
14
|
id: slugify("btc", wallet.name),
|
|
33
15
|
label: "Bitcoin",
|
|
34
16
|
namespace: "bip122",
|
|
35
|
-
|
|
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
|
|
43
|
-
const
|
|
44
|
-
const
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
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:
|
|
107
|
-
chain: core.
|
|
108
|
-
psbt
|
|
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,
|
|
57
|
+
const discoverBitcoinAdapters = (onAdapter) => discoverWalletStandard(onAdapter, buildBitcoinAdapter);
|
|
115
58
|
|
|
116
59
|
//#endregion
|
|
117
|
-
//#region src/
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
|
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 =
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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(
|
|
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(
|
|
209
|
+
message: new TextDecoder().decode(message)
|
|
256
210
|
});
|
|
257
211
|
return {
|
|
258
212
|
signature: base64ToBytes(result.signature),
|
|
259
|
-
signedMessage:
|
|
213
|
+
signedMessage: message
|
|
260
214
|
};
|
|
261
215
|
},
|
|
262
|
-
async signTransaction(
|
|
263
|
-
|
|
264
|
-
const result = await callRequest("signPsbt", { psbt: bytesToBase64(
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
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
|
|
304
|
-
|
|
259
|
+
const readAccounts = async () => {
|
|
260
|
+
const addresses = await provider.getAccounts();
|
|
261
|
+
if (addresses.length === 0) return [];
|
|
305
262
|
try {
|
|
306
|
-
|
|
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(
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
await refreshChain();
|
|
287
|
+
async connect(options) {
|
|
288
|
+
if (options?.silent !== true) {
|
|
289
|
+
await provider.requestAccounts();
|
|
319
290
|
return;
|
|
320
291
|
}
|
|
321
|
-
await provider.
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
349
|
-
await
|
|
350
|
-
await
|
|
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(
|
|
362
|
-
signedMessage:
|
|
306
|
+
signature: base64ToBytes(signature),
|
|
307
|
+
signedMessage: message
|
|
363
308
|
};
|
|
364
309
|
},
|
|
365
|
-
async signTransaction(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return hexToBytes(signedHex);
|
|
310
|
+
async signTransaction(psbt, options) {
|
|
311
|
+
await prepare(options);
|
|
312
|
+
return hexToBytes(await provider.signPsbt(bytesToHex(psbt)));
|
|
369
313
|
},
|
|
370
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
336
|
+
reemitAccounts();
|
|
388
337
|
};
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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 {
|
|
415
|
+
export { GENERIC_BITCOIN_ICON, bitcoinDiscoverer, buildBitcoinAdapter, discoverBitcoinAdapters, discoverInjectedBitcoinAdapter };
|
|
471
416
|
//# sourceMappingURL=index.js.map
|