bitcoin-wallet-connector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +208 -0
  2. package/lib/BitcoinConnectionProvider.d.ts +23 -0
  3. package/lib/BitcoinWalletAdapterConnector-Bq835yj0.mjs +123 -0
  4. package/lib/BitcoinWalletAdapterConnector-Bq835yj0.mjs.map +1 -0
  5. package/lib/BitcoinWalletAdapterConnector-DMef0iHV.js +2 -0
  6. package/lib/BitcoinWalletAdapterConnector-DMef0iHV.js.map +1 -0
  7. package/lib/BitcoinWalletAdapterConnector.d.ts +30 -0
  8. package/lib/BitgetWalletAdapter.impl-C_HLO7Oi.mjs +10 -0
  9. package/lib/BitgetWalletAdapter.impl-C_HLO7Oi.mjs.map +1 -0
  10. package/lib/BitgetWalletAdapter.impl-CxnKMf7U.js +2 -0
  11. package/lib/BitgetWalletAdapter.impl-CxnKMf7U.js.map +1 -0
  12. package/lib/LeatherWalletAdapter.impl-B2QgX_tO.js +2 -0
  13. package/lib/LeatherWalletAdapter.impl-B2QgX_tO.js.map +1 -0
  14. package/lib/LeatherWalletAdapter.impl-RUYx555r.mjs +184 -0
  15. package/lib/LeatherWalletAdapter.impl-RUYx555r.mjs.map +1 -0
  16. package/lib/MagicEdenWalletAdapter.impl-CrA6SGvG.mjs +235 -0
  17. package/lib/MagicEdenWalletAdapter.impl-CrA6SGvG.mjs.map +1 -0
  18. package/lib/MagicEdenWalletAdapter.impl-Di3Nu2S5.js +2 -0
  19. package/lib/MagicEdenWalletAdapter.impl-Di3Nu2S5.js.map +1 -0
  20. package/lib/OkxWalletAdapter.impl-BepoUL1B.mjs +67 -0
  21. package/lib/OkxWalletAdapter.impl-BepoUL1B.mjs.map +1 -0
  22. package/lib/OkxWalletAdapter.impl-C8kesjGu.js +2 -0
  23. package/lib/OkxWalletAdapter.impl-C8kesjGu.js.map +1 -0
  24. package/lib/UnisatCompatibleWalletAdapterImpl-Cq2Oqk1b.js +2 -0
  25. package/lib/UnisatCompatibleWalletAdapterImpl-Cq2Oqk1b.js.map +1 -0
  26. package/lib/UnisatCompatibleWalletAdapterImpl-M38FqkZI.mjs +137 -0
  27. package/lib/UnisatCompatibleWalletAdapterImpl-M38FqkZI.mjs.map +1 -0
  28. package/lib/UnisatWalletAdapter.impl-CJB22se8.mjs +14 -0
  29. package/lib/UnisatWalletAdapter.impl-CJB22se8.mjs.map +1 -0
  30. package/lib/UnisatWalletAdapter.impl-EISvxdpc.js +2 -0
  31. package/lib/UnisatWalletAdapter.impl-EISvxdpc.js.map +1 -0
  32. package/lib/WalletAdapters.types-CnvOqHFH.mjs +32 -0
  33. package/lib/WalletAdapters.types-CnvOqHFH.mjs.map +1 -0
  34. package/lib/WalletAdapters.types-De_x1lzr.js +2 -0
  35. package/lib/WalletAdapters.types-De_x1lzr.js.map +1 -0
  36. package/lib/WalletAdapters.types.d.ts +110 -0
  37. package/lib/XverseCompatibleWalletAdapterImpl-Bf-BK5VK.js +2 -0
  38. package/lib/XverseCompatibleWalletAdapterImpl-Bf-BK5VK.js.map +1 -0
  39. package/lib/XverseCompatibleWalletAdapterImpl-DXKnO_-V.mjs +151 -0
  40. package/lib/XverseCompatibleWalletAdapterImpl-DXKnO_-V.mjs.map +1 -0
  41. package/lib/XverseWalletAdapter.impl-CZO0RQva.mjs +105 -0
  42. package/lib/XverseWalletAdapter.impl-CZO0RQva.mjs.map +1 -0
  43. package/lib/XverseWalletAdapter.impl-lJwMi-Iv.js +2 -0
  44. package/lib/XverseWalletAdapter.impl-lJwMi-Iv.js.map +1 -0
  45. package/lib/adapters/BitgetWalletAdapter.d.ts +2 -0
  46. package/lib/adapters/BitgetWalletAdapter.impl.d.ts +8 -0
  47. package/lib/adapters/LeatherWalletAdapter.d.ts +2 -0
  48. package/lib/adapters/LeatherWalletAdapter.impl.d.ts +41 -0
  49. package/lib/adapters/MagicEdenWalletAdapter.d.ts +11 -0
  50. package/lib/adapters/MagicEdenWalletAdapter.impl.d.ts +22 -0
  51. package/lib/adapters/MockAddressWalletAdapter.d.ts +33 -0
  52. package/lib/adapters/OkxWalletAdapter.d.ts +2 -0
  53. package/lib/adapters/OkxWalletAdapter.impl.d.ts +51 -0
  54. package/lib/adapters/UnisatWalletAdapter.d.ts +2 -0
  55. package/lib/adapters/UnisatWalletAdapter.impl.d.ts +14 -0
  56. package/lib/adapters/XverseWalletAdapter.d.ts +3 -0
  57. package/lib/adapters/XverseWalletAdapter.impl.d.ts +14 -0
  58. package/lib/adapters/index.d.ts +7 -0
  59. package/lib/adapters.js +2 -0
  60. package/lib/adapters.js.map +1 -0
  61. package/lib/adapters.mjs +11 -0
  62. package/lib/adapters.mjs.map +1 -0
  63. package/lib/bitget-C7oB4Ffq.mjs +5 -0
  64. package/lib/bitget-C7oB4Ffq.mjs.map +1 -0
  65. package/lib/bitget-DXnsxx_y.js +2 -0
  66. package/lib/bitget-DXnsxx_y.js.map +1 -0
  67. package/lib/index-CaV3F1Nm.js +424 -0
  68. package/lib/index-CaV3F1Nm.js.map +1 -0
  69. package/lib/index-CcQUdePc.mjs +12224 -0
  70. package/lib/index-CcQUdePc.mjs.map +1 -0
  71. package/lib/index-D7YwhNAG.mjs +3946 -0
  72. package/lib/index-D7YwhNAG.mjs.map +1 -0
  73. package/lib/index-Zx0KcpYx.js +2 -0
  74. package/lib/index-Zx0KcpYx.js.map +1 -0
  75. package/lib/index.d.ts +3 -0
  76. package/lib/index.js +2 -0
  77. package/lib/index.js.map +1 -0
  78. package/lib/index.mjs +20 -0
  79. package/lib/index.mjs.map +1 -0
  80. package/lib/leather-BoQG_CPn.mjs +5 -0
  81. package/lib/leather-BoQG_CPn.mjs.map +1 -0
  82. package/lib/leather-DJ8nWmM8.js +2 -0
  83. package/lib/leather-DJ8nWmM8.js.map +1 -0
  84. package/lib/magiceden-B36CEQa6.js +2 -0
  85. package/lib/magiceden-B36CEQa6.js.map +1 -0
  86. package/lib/magiceden-Cg7d3agI.mjs +5 -0
  87. package/lib/magiceden-Cg7d3agI.mjs.map +1 -0
  88. package/lib/misc-B5EWO_dn.mjs +10 -0
  89. package/lib/misc-B5EWO_dn.mjs.map +1 -0
  90. package/lib/misc-CigR0RqC.js +2 -0
  91. package/lib/misc-CigR0RqC.js.map +1 -0
  92. package/lib/okx-ChwzM0dK.js +2 -0
  93. package/lib/okx-ChwzM0dK.js.map +1 -0
  94. package/lib/okx-DWbHwazu.mjs +5 -0
  95. package/lib/okx-DWbHwazu.mjs.map +1 -0
  96. package/lib/react.d.ts +2 -0
  97. package/lib/react.js +2 -0
  98. package/lib/react.js.map +1 -0
  99. package/lib/react.mjs +128 -0
  100. package/lib/react.mjs.map +1 -0
  101. package/lib/transaction-CiLOYSE_.mjs +1063 -0
  102. package/lib/transaction-CiLOYSE_.mjs.map +1 -0
  103. package/lib/transaction-CzdnbXSo.js +2 -0
  104. package/lib/transaction-CzdnbXSo.js.map +1 -0
  105. package/lib/unisat-BvZW5h0U.js +2 -0
  106. package/lib/unisat-BvZW5h0U.js.map +1 -0
  107. package/lib/unisat-pLgab4nG.mjs +5 -0
  108. package/lib/unisat-pLgab4nG.mjs.map +1 -0
  109. package/lib/utils/StateChannel.d.ts +14 -0
  110. package/lib/utils/UnisatCompatibleWalletAdapterImpl.d.ts +99 -0
  111. package/lib/utils/XverseCompatibleWalletAdapterImpl.d.ts +80 -0
  112. package/lib/utils/XverseCompatibleWalletAdapterImpl_legacy.d.ts +44 -0
  113. package/lib/utils/bitcoinAddressHelpers.d.ts +14 -0
  114. package/lib/utils/bitcoinNetworkHelpers.d.ts +4 -0
  115. package/lib/utils/createAdapterAvailability.d.ts +15 -0
  116. package/lib/utils/error.d.ts +6 -0
  117. package/lib/utils/misc.d.ts +3 -0
  118. package/lib/xverse-IKOHyGi-.js +2 -0
  119. package/lib/xverse-IKOHyGi-.js.map +1 -0
  120. package/lib/xverse-iHLNanCB.mjs +5 -0
  121. package/lib/xverse-iHLNanCB.mjs.map +1 -0
  122. package/package.json +86 -0
  123. package/src/BitcoinConnectionProvider.stories.tsx +329 -0
  124. package/src/BitcoinConnectionProvider.tsx +234 -0
  125. package/src/BitcoinWalletAdapterConnector.ts +166 -0
  126. package/src/WalletAdapters.types.ts +154 -0
  127. package/src/_/bitget.png +0 -0
  128. package/src/_/leather.svg +4 -0
  129. package/src/_/magiceden.png +0 -0
  130. package/src/_/okx.png +0 -0
  131. package/src/_/unisat.svg +31 -0
  132. package/src/_/xverse.png +0 -0
  133. package/src/adapters/BitgetWalletAdapter.impl.ts +22 -0
  134. package/src/adapters/BitgetWalletAdapter.ts +44 -0
  135. package/src/adapters/LeatherWalletAdapter.impl.ts +324 -0
  136. package/src/adapters/LeatherWalletAdapter.ts +35 -0
  137. package/src/adapters/MagicEdenWalletAdapter.impl.ts +139 -0
  138. package/src/adapters/MagicEdenWalletAdapter.ts +51 -0
  139. package/src/adapters/MockAddressWalletAdapter.ts +199 -0
  140. package/src/adapters/OkxWalletAdapter.impl.ts +168 -0
  141. package/src/adapters/OkxWalletAdapter.ts +37 -0
  142. package/src/adapters/UnisatWalletAdapter.impl.ts +32 -0
  143. package/src/adapters/UnisatWalletAdapter.ts +50 -0
  144. package/src/adapters/XverseWalletAdapter.impl.ts +150 -0
  145. package/src/adapters/XverseWalletAdapter.ts +37 -0
  146. package/src/adapters/index.ts +7 -0
  147. package/src/env.d.ts +9 -0
  148. package/src/index.ts +3 -0
  149. package/src/react.ts +9 -0
  150. package/src/utils/StateChannel.ts +39 -0
  151. package/src/utils/UnisatCompatibleWalletAdapterImpl.ts +342 -0
  152. package/src/utils/XverseCompatibleWalletAdapterImpl.ts +288 -0
  153. package/src/utils/XverseCompatibleWalletAdapterImpl_legacy.ts +278 -0
  154. package/src/utils/bitcoinAddressHelpers.ts +132 -0
  155. package/src/utils/bitcoinNetworkHelpers.ts +17 -0
  156. package/src/utils/createAdapterAvailability.ts +92 -0
  157. package/src/utils/error.ts +13 -0
  158. package/src/utils/misc.ts +10 -0
package/src/react.ts ADDED
@@ -0,0 +1,9 @@
1
+ export {
2
+ BitcoinConnectionProvider,
3
+ useBitcoinConnectionContext,
4
+ } from "./BitcoinConnectionProvider"
5
+
6
+ export type {
7
+ WalletSession,
8
+ BitcoinConnectionContextValue,
9
+ } from "./BitcoinConnectionProvider"
@@ -0,0 +1,39 @@
1
+ export type StateChannelListener<T> = (value: T) => void
2
+
3
+ export interface StateChannelSubscription {
4
+ unsubscribe: () => void
5
+ }
6
+
7
+ export class StateChannel<T> {
8
+ private listeners = new Set<StateChannelListener<T>>()
9
+ constructor(private value: T) {}
10
+
11
+ getValue(): T {
12
+ return this.value
13
+ }
14
+
15
+ setValue(value: T): void {
16
+ this.value = value
17
+ this.emit()
18
+ }
19
+
20
+ update(updater: (current: T) => T): void {
21
+ this.setValue(updater(this.value))
22
+ }
23
+
24
+ subscribe(listener: StateChannelListener<T>): StateChannelSubscription {
25
+ this.listeners.add(listener)
26
+ listener(this.value)
27
+ return {
28
+ unsubscribe: () => {
29
+ this.listeners.delete(listener)
30
+ },
31
+ }
32
+ }
33
+
34
+ private emit(): void {
35
+ for (const listener of this.listeners) {
36
+ listener(this.value)
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,342 @@
1
+ import { hex } from "@scure/base"
2
+ import * as btc from "@scure/btc-signer"
3
+ import { hasAny } from "./misc"
4
+ import {
5
+ addressToScriptPubKey,
6
+ getAddressType,
7
+ getRedeemScriptOf_P2SH_P2WPKH_publicKey,
8
+ getTapInternalKeyOf_P2TR_publicKey,
9
+ } from "./bitcoinAddressHelpers"
10
+ import { getBitcoinNetwork } from "./bitcoinNetworkHelpers"
11
+ import { UserRejectError, BitcoinWalletAdapterError } from "../utils/error"
12
+ import {
13
+ SignMessageAlgorithm,
14
+ SignMessageResult,
15
+ WalletAdapter,
16
+ WalletAdapter_onAddressesChanged_callback,
17
+ WalletAdapterAddress,
18
+ WalletAdapterAddressPurpose,
19
+ WalletAdapterAddressType,
20
+ WalletAdapterBitcoinNetwork,
21
+ WalletAdapterNotConnectedError,
22
+ } from "../WalletAdapters.types"
23
+
24
+ /**
25
+ * https://docs.unisat.io/dev-center/open-api-documentation/unisat-wallet#events
26
+ */
27
+ export interface UniSatEvents {
28
+ accountsChanged: [accounts: Array<string>]
29
+ networkChanged: [network: string]
30
+ }
31
+
32
+ export interface UnisatCompatibleProviderAPI {
33
+ /**
34
+ * https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#getaccounts
35
+ */
36
+ getAccounts(): Promise<string[]>
37
+
38
+ /**
39
+ * https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#requestaccounts
40
+ */
41
+ requestAccounts(): Promise<string[]>
42
+
43
+ /**
44
+ * https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#getpublickey
45
+ */
46
+ getPublicKey(): Promise<string>
47
+
48
+ /**
49
+ * https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#getnetwork
50
+ */
51
+ getNetwork(): Promise<"livenet" | "testnet">
52
+
53
+ /**
54
+ * https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#signmessage
55
+ */
56
+ signMessage(
57
+ message: string,
58
+ algorithm: "ecdsa" | "bip322-simple",
59
+ ): Promise<string>
60
+
61
+ /**
62
+ * https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#sendbitcoin
63
+ */
64
+ sendBitcoin(
65
+ receiverAddress: string,
66
+ satoshiAmount: number,
67
+ options?: { feeRate?: number },
68
+ ): Promise<string>
69
+
70
+ /**
71
+ * https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#sendInscription
72
+ */
73
+ sendInscription(
74
+ receiverAddress: string,
75
+ inscriptionId: string,
76
+ options?: { feeRate?: number },
77
+ ): Promise<string>
78
+
79
+ /**
80
+ * https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#signpsbt
81
+ */
82
+ signPsbt(
83
+ psbtHex: string,
84
+ options?: {
85
+ autoFinalized?: boolean
86
+ toSignInputs?: {
87
+ index: number
88
+ address?: string
89
+ publicKey?: string
90
+ sighashTypes?: number[]
91
+ disableTweakSigner?: boolean
92
+ }[]
93
+ },
94
+ ): Promise<string>
95
+
96
+ /**
97
+ * https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#events
98
+ */
99
+ on<K extends keyof UniSatEvents>(
100
+ event: K,
101
+ handler: (...args: UniSatEvents[K]) => void,
102
+ ): void
103
+ removeListener(event: keyof UniSatEvents, handler: () => void): void
104
+ }
105
+
106
+ export type UnisatCompatibleWalletAdapterAddresses = (WalletAdapterAddress & {
107
+ publicKey: string
108
+ })[]
109
+
110
+ export class UnisatCompatibleWalletAdapterImpl implements WalletAdapter {
111
+ constructor(
112
+ private unisat: UnisatCompatibleProviderAPI,
113
+ private walletDisplayName: string,
114
+ ) {}
115
+
116
+ async connect(): Promise<void> {
117
+ if ((await this.unisat.getAccounts()).length === 0) {
118
+ await this.unisat.requestAccounts()
119
+ }
120
+ }
121
+
122
+ disconnect(): Promise<void> {
123
+ // do nothing
124
+ return Promise.resolve()
125
+ }
126
+
127
+ async getAddresses(): Promise<UnisatCompatibleWalletAdapterAddresses> {
128
+ const addresses: string[] = await this.unisat.getAccounts()
129
+
130
+ if (addresses.length === 0) {
131
+ throw new WalletAdapterNotConnectedError(this.walletDisplayName)
132
+ }
133
+
134
+ const publicKey: string = await this.unisat.getPublicKey()
135
+
136
+ const _network: "livenet" | "testnet" = await this.unisat.getNetwork()
137
+ const network = _network === "livenet" ? "mainnet" : "testnet"
138
+
139
+ if (!hasAny(addresses)) {
140
+ throw new BitcoinWalletAdapterError("Request wallet addresses failed")
141
+ }
142
+
143
+ const address = addresses[0]
144
+ const addrType = getAddressType(getBitcoinNetwork(network), address)
145
+ // prettier-ignore
146
+ const addressType =
147
+ addrType === "p2tr" ? WalletAdapterAddressType.P2TR :
148
+ addrType === "p2sh" ? WalletAdapterAddressType.P2SH_P2WPKH :
149
+ addrType === "p2wpkh" ? WalletAdapterAddressType.P2WPKH :
150
+ undefined
151
+
152
+ if (addressType == null) {
153
+ throw new BitcoinWalletAdapterError("Please select a SegWit address")
154
+ }
155
+
156
+ const scriptPubKey = hex.encode(
157
+ addressToScriptPubKey(getBitcoinNetwork(network), address),
158
+ )
159
+
160
+ const tapInternalKey =
161
+ addressType !== WalletAdapterAddressType.P2TR
162
+ ? undefined
163
+ : hex.encode(
164
+ getTapInternalKeyOf_P2TR_publicKey(
165
+ getBitcoinNetwork(network),
166
+ hex.decode(publicKey),
167
+ ),
168
+ )
169
+
170
+ const redeemScript =
171
+ addressType !== WalletAdapterAddressType.P2SH_P2WPKH
172
+ ? undefined
173
+ : hex.encode(
174
+ getRedeemScriptOf_P2SH_P2WPKH_publicKey(
175
+ getBitcoinNetwork(network),
176
+ hex.decode(publicKey),
177
+ ),
178
+ )
179
+
180
+ return [
181
+ {
182
+ addressType,
183
+ address,
184
+ scriptPubKey,
185
+ redeemScript,
186
+ tapInternalKey,
187
+ publicKey,
188
+ network:
189
+ network === "mainnet"
190
+ ? WalletAdapterBitcoinNetwork.MAINNET
191
+ : WalletAdapterBitcoinNetwork.TESTNET,
192
+ purposes: [
193
+ WalletAdapterAddressPurpose.Bitcoin,
194
+ WalletAdapterAddressPurpose.Ordinals,
195
+ WalletAdapterAddressPurpose.BRC20,
196
+ WalletAdapterAddressPurpose.Runes,
197
+ ],
198
+ },
199
+ ]
200
+ }
201
+
202
+ async signMessage(
203
+ address: string,
204
+ message: string,
205
+ ): Promise<SignMessageResult> {
206
+ const result = await handleRpcError(
207
+ this.unisat.signMessage(message, "bip322-simple"),
208
+ )
209
+
210
+ return {
211
+ result,
212
+ address,
213
+ algorithm: SignMessageAlgorithm.BIP322,
214
+ }
215
+ }
216
+
217
+ sendBitcoinFeeRateCapability: WalletAdapter["sendBitcoinFeeRateCapability"] =
218
+ "available" as const
219
+ async sendBitcoin(
220
+ fromAddress: string,
221
+ receiverAddress: string,
222
+ satoshiAmount: bigint,
223
+ options?: { feeRate?: number },
224
+ ): Promise<{ txid: string }> {
225
+ const txid = await handleRpcError(
226
+ this.unisat.sendBitcoin(receiverAddress, Number(satoshiAmount), {
227
+ feeRate: options?.feeRate,
228
+ }),
229
+ )
230
+ return { txid }
231
+ }
232
+
233
+ sendInscriptionFeeRateCapability: WalletAdapter["sendInscriptionFeeRateCapability"] =
234
+ "available" as const
235
+ async sendInscription(
236
+ fromAddress: string,
237
+ receiverAddress: string,
238
+ inscriptionId: string,
239
+ options?: { feeRate?: number },
240
+ ): Promise<{ txid: string }> {
241
+ const txid = await handleRpcError(
242
+ this.unisat.sendInscription(receiverAddress, inscriptionId, {
243
+ feeRate: options?.feeRate,
244
+ }),
245
+ )
246
+ return { txid }
247
+ }
248
+
249
+ async signAndFinalizePsbt(
250
+ psbtHex: string,
251
+ signIndices: [address: string, signIndex: number][],
252
+ ): Promise<{
253
+ signedPsbtHex: string
254
+ }> {
255
+ const addr = await this.getAddresses()
256
+
257
+ const signedPsbtHex = await handleRpcError(
258
+ this.unisat.signPsbt(psbtHex, {
259
+ autoFinalized: false,
260
+ toSignInputs: signIndices.map(([address, signIndex]) => ({
261
+ index: signIndex,
262
+ address,
263
+ })),
264
+ }),
265
+ )
266
+
267
+ /**
268
+ * Some version of unisat's signPsbt API does not working well with the autoFinalized option,
269
+ * so we finalize the PSBT manually.
270
+ */
271
+ const tx = btc.Transaction.fromPSBT(hex.decode(signedPsbtHex), {
272
+ allowUnknownInputs: true,
273
+ allowUnknownOutputs: true,
274
+ disableScriptCheck: true,
275
+ allowLegacyWitnessUtxo: true,
276
+ })
277
+ tx.finalize()
278
+
279
+ return { signedPsbtHex: hex.encode(tx.toPSBT()) }
280
+ }
281
+
282
+ onAddressesChanged(callback: WalletAdapter_onAddressesChanged_callback): {
283
+ unsubscribe: () => void
284
+ } {
285
+ const handler = async (): Promise<void> => {
286
+ try {
287
+ const addresses = await this.getAddresses()
288
+ callback({ addresses })
289
+ } catch (error) {
290
+ // Ignore errors from getAddresses (e.g., wallet disconnected)
291
+ console.warn(
292
+ `[${this.walletDisplayName}] Failed to get addresses on change:`,
293
+ error,
294
+ )
295
+ }
296
+ }
297
+
298
+ // Listen to both account and network changes
299
+ this.unisat.on("accountsChanged", handler)
300
+ this.unisat.on("networkChanged", handler)
301
+
302
+ return {
303
+ unsubscribe: () => {
304
+ this.unisat.removeListener("accountsChanged", handler)
305
+ this.unisat.removeListener("networkChanged", handler)
306
+ },
307
+ }
308
+ }
309
+ }
310
+
311
+ interface UnisatCompatibleProviderAPIThrownError {
312
+ code: number
313
+ message: string
314
+ }
315
+ function isUnisatCompatibleProviderAPIThrownError(
316
+ err: any,
317
+ ): err is UnisatCompatibleProviderAPIThrownError {
318
+ return err != null && "code" in err && "message" in err
319
+ }
320
+
321
+ export class UnisatCompatibleProviderError extends Error {
322
+ readonly code: number
323
+ constructor(err: UnisatCompatibleProviderAPIThrownError) {
324
+ super(err.message)
325
+ this.code = err.code
326
+ this.cause = err
327
+ }
328
+ }
329
+
330
+ const handleRpcError = async <T>(promise: Promise<T>): Promise<T> => {
331
+ try {
332
+ return await promise
333
+ } catch (e: any) {
334
+ if (isUnisatCompatibleProviderAPIThrownError(e)) {
335
+ if (e.code === 4001) {
336
+ throw new UserRejectError()
337
+ }
338
+ throw new UnisatCompatibleProviderError(e)
339
+ }
340
+ throw e
341
+ }
342
+ }
@@ -0,0 +1,288 @@
1
+ import { base64, hex } from "@scure/base"
2
+ import * as btc from "@scure/btc-signer"
3
+ import { UserRejectError } from "../utils/error"
4
+ import {
5
+ SignMessageAlgorithm,
6
+ SignMessageResult,
7
+ WalletAdapter,
8
+ WalletAdapter_onAddressesChanged_callback,
9
+ WalletAdapterAddress,
10
+ WalletAdapterAddressPurpose,
11
+ WalletAdapterNotConnectedError,
12
+ } from "../WalletAdapters.types"
13
+
14
+ export type XverseCompatibleWalletAdapterGetProviderIdFn = () => Promise<string>
15
+
16
+ export type XverseCompatibleWalletAdapterParsedAddressesFn = (info: {
17
+ sdk: typeof import("sats-connect")
18
+ network: import("sats-connect").BitcoinNetworkType
19
+ addresses: import("sats-connect").Address[]
20
+ }) => Promise<XverseCompatibleWalletAdapterImplAddress[]>
21
+
22
+ export type XverseCompatibleWalletAdapterImplAddress = WalletAdapterAddress & {
23
+ publicKey: string
24
+ }
25
+
26
+ export class XverseCompatibleWalletAdapterImpl implements WalletAdapter {
27
+ protected readonly walletDisplayName: string
28
+ protected readonly getProviderId: XverseCompatibleWalletAdapterGetProviderIdFn
29
+ protected readonly parseAddresses: XverseCompatibleWalletAdapterParsedAddressesFn
30
+
31
+ constructor(info: {
32
+ walletDisplayName: string
33
+ getProviderId: XverseCompatibleWalletAdapterGetProviderIdFn
34
+ parseAddresses: XverseCompatibleWalletAdapterParsedAddressesFn
35
+ }) {
36
+ this.walletDisplayName = info.walletDisplayName
37
+ this.getProviderId = info.getProviderId
38
+ this.parseAddresses = info.parseAddresses
39
+ }
40
+
41
+ private async getSdk(): Promise<typeof import("sats-connect")> {
42
+ return import("sats-connect")
43
+ }
44
+
45
+ async connect(): Promise<void> {
46
+ await (await this.getSdk()).request("wallet_connect", null)
47
+ }
48
+
49
+ async disconnect(): Promise<void> {
50
+ return Promise.resolve()
51
+ }
52
+
53
+ async getAddresses(): Promise<XverseCompatibleWalletAdapterImplAddress[]> {
54
+ const sdk = await this.getSdk()
55
+
56
+ const resp = await handleRpcError(
57
+ sdk.request(
58
+ "getAddresses",
59
+ {
60
+ purposes: [sdk.AddressPurpose.Ordinals, sdk.AddressPurpose.Payment],
61
+ },
62
+ await this.getProviderId(),
63
+ ),
64
+ )
65
+
66
+ if (resp == null) {
67
+ throw new WalletAdapterNotConnectedError(this.walletDisplayName)
68
+ }
69
+
70
+ return this.parseAddresses({
71
+ sdk,
72
+ network: resp.network.bitcoin.name,
73
+ addresses: resp.addresses,
74
+ })
75
+ }
76
+
77
+ async signMessage(
78
+ address: string,
79
+ message: string,
80
+ ): Promise<SignMessageResult> {
81
+ const sdk = await this.getSdk()
82
+
83
+ const result = await handleRpcError(
84
+ sdk.request(
85
+ "signMessage",
86
+ {
87
+ address,
88
+ message,
89
+ protocol: sdk.MessageSigningProtocols.BIP322,
90
+ },
91
+ await this.getProviderId(),
92
+ ),
93
+ )
94
+
95
+ return {
96
+ result: result.signature,
97
+ address,
98
+ algorithm: SignMessageAlgorithm.BIP322,
99
+ }
100
+ }
101
+
102
+ sendBitcoinFeeRateCapability = "unavailable" as const
103
+ async sendBitcoin(
104
+ fromAddress: string,
105
+ receiverAddress: string,
106
+ satoshiAmount: bigint,
107
+ ): Promise<{ txid: string }> {
108
+ const senderAddress = (await this.getAddresses()).find(a =>
109
+ a.purposes.includes(WalletAdapterAddressPurpose.Bitcoin),
110
+ )
111
+ if (senderAddress == null) {
112
+ throw new XverseCompatibleProviderError({
113
+ code: XverseRpcErrorCode.INVALID_PARAMS,
114
+ message: "Bitcoin address not found",
115
+ })
116
+ }
117
+
118
+ const sdk = await this.getSdk()
119
+
120
+ return await handleRpcError(
121
+ sdk.request(
122
+ "sendTransfer",
123
+ {
124
+ recipients: [
125
+ {
126
+ address: receiverAddress,
127
+ amount: Number(satoshiAmount),
128
+ },
129
+ ],
130
+ },
131
+ await this.getProviderId(),
132
+ ),
133
+ )
134
+ }
135
+
136
+ sendInscriptionFeeRateCapability = "unavailable" as const
137
+
138
+ async signAndFinalizePsbt(
139
+ psbtHex: string,
140
+ signIndices: [address: string, signIndex: number][],
141
+ ): Promise<{
142
+ signedPsbtHex: string
143
+ }> {
144
+ const sdk = await this.getSdk()
145
+
146
+ const psbtBase64 = base64.encode(hex.decode(psbtHex))
147
+
148
+ const result = await handleRpcError(
149
+ sdk.request(
150
+ "signPsbt",
151
+ {
152
+ psbt: psbtBase64,
153
+ signInputs: signIndices.reduce(
154
+ (acc, [address, signIndex]) => {
155
+ acc[address] = [signIndex]
156
+ return acc
157
+ },
158
+ {} as Record<string, number[]>,
159
+ ),
160
+ broadcast: false,
161
+ },
162
+ await this.getProviderId(),
163
+ ),
164
+ )
165
+
166
+ const tx = btc.Transaction.fromPSBT(base64.decode(result.psbt), {
167
+ allowUnknownInputs: true,
168
+ allowUnknownOutputs: true,
169
+ disableScriptCheck: true,
170
+ allowLegacyWitnessUtxo: true,
171
+ })
172
+ tx.finalize()
173
+
174
+ return { signedPsbtHex: hex.encode(tx.toPSBT()) }
175
+ }
176
+
177
+ onAddressesChanged(callback: WalletAdapter_onAddressesChanged_callback): {
178
+ unsubscribe: () => void
179
+ } {
180
+ // Xverse uses sats-connect Wallet.addListener for events
181
+ // https://docs.xverse.app/sats-connect/xverse-wallet-events
182
+ const removeListeners: Array<() => void> = []
183
+
184
+ void (async () => {
185
+ const sdk = await this.getSdk()
186
+
187
+ const handler = async (): Promise<void> => {
188
+ try {
189
+ const addresses = await this.getAddresses()
190
+ callback({ addresses })
191
+ } catch (error) {
192
+ console.warn(
193
+ `[${this.walletDisplayName}] Failed to get addresses on change:`,
194
+ error,
195
+ )
196
+ }
197
+ }
198
+
199
+ // Listen to account and network changes
200
+ const removeAccountListener = sdk.addListener("accountChange", handler)
201
+ const removeNetworkListener = sdk.addListener("networkChange", handler)
202
+
203
+ removeListeners.push(removeAccountListener, removeNetworkListener)
204
+ })()
205
+
206
+ return {
207
+ unsubscribe: () => {
208
+ removeListeners.forEach(remove => remove())
209
+ },
210
+ }
211
+ }
212
+ }
213
+
214
+ export enum XverseRpcErrorCode {
215
+ /**
216
+ * Parse error Invalid JSON
217
+ **/
218
+ PARSE_ERROR = -32700,
219
+ /**
220
+ * The JSON sent is not a valid Request object.
221
+ **/
222
+ INVALID_REQUEST = -32600,
223
+ /**
224
+ * The method does not exist/is not available.
225
+ **/
226
+ METHOD_NOT_FOUND = -32601,
227
+ /**
228
+ * Invalid method parameter(s).
229
+ */
230
+ INVALID_PARAMS = -32602,
231
+ /**
232
+ * Internal JSON-RPC error.
233
+ * This is a generic error, used when the server encounters an error in performing the request.
234
+ **/
235
+ INTERNAL_ERROR = -32603,
236
+ /**
237
+ * user rejected/canceled the request
238
+ */
239
+ USER_REJECTION = -32000,
240
+ /**
241
+ * method is not supported for the address provided
242
+ */
243
+ METHOD_NOT_SUPPORTED = -32001,
244
+ /**
245
+ * The client does not have permission to access the requested resource.
246
+ */
247
+ ACCESS_DENIED = -32002,
248
+ }
249
+ export interface XverseCompatibleProviderAPIThrownError {
250
+ code: XverseRpcErrorCode
251
+ message: string
252
+ }
253
+ export function isXverseCompatibleProviderAPIThrownError(
254
+ err: any,
255
+ ): err is XverseCompatibleProviderAPIThrownError {
256
+ return err != null && "code" in err && "message" in err
257
+ }
258
+
259
+ export class XverseCompatibleProviderError extends Error {
260
+ readonly code: number
261
+ constructor(err: XverseCompatibleProviderAPIThrownError) {
262
+ super(err.message)
263
+ this.code = err.code
264
+ this.cause = err
265
+ }
266
+ }
267
+
268
+ const handleRpcError = async <T>(
269
+ promise: Promise<
270
+ { status: "success"; result: T } | { status: "error"; error: unknown }
271
+ >,
272
+ ): Promise<T> => {
273
+ try {
274
+ const res = await promise
275
+ if (res.status === "error") {
276
+ throw res.error
277
+ }
278
+ return res.result
279
+ } catch (e: any) {
280
+ if (isXverseCompatibleProviderAPIThrownError(e)) {
281
+ if (e.code === XverseRpcErrorCode.USER_REJECTION) {
282
+ throw new UserRejectError()
283
+ }
284
+ throw new XverseCompatibleProviderError(e)
285
+ }
286
+ throw e
287
+ }
288
+ }