otx-btc-wallet-connectors 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 (142) hide show
  1. package/README.md +554 -0
  2. package/dist/base-IAFq7sd8.d.mts +53 -0
  3. package/dist/base-IAFq7sd8.d.ts +53 -0
  4. package/dist/binance/index.d.mts +81 -0
  5. package/dist/binance/index.d.ts +81 -0
  6. package/dist/binance/index.js +13 -0
  7. package/dist/binance/index.js.map +1 -0
  8. package/dist/binance/index.mjs +4 -0
  9. package/dist/binance/index.mjs.map +1 -0
  10. package/dist/bitget/index.d.mts +84 -0
  11. package/dist/bitget/index.d.ts +84 -0
  12. package/dist/bitget/index.js +13 -0
  13. package/dist/bitget/index.js.map +1 -0
  14. package/dist/bitget/index.mjs +4 -0
  15. package/dist/bitget/index.mjs.map +1 -0
  16. package/dist/chunk-5Z5Q2Y75.mjs +91 -0
  17. package/dist/chunk-5Z5Q2Y75.mjs.map +1 -0
  18. package/dist/chunk-7KK2LZLZ.mjs +208 -0
  19. package/dist/chunk-7KK2LZLZ.mjs.map +1 -0
  20. package/dist/chunk-AW2JZIHR.mjs +753 -0
  21. package/dist/chunk-AW2JZIHR.mjs.map +1 -0
  22. package/dist/chunk-EIJOSZXZ.js +331 -0
  23. package/dist/chunk-EIJOSZXZ.js.map +1 -0
  24. package/dist/chunk-EQHR7P7G.js +541 -0
  25. package/dist/chunk-EQHR7P7G.js.map +1 -0
  26. package/dist/chunk-EWRXLZO4.mjs +539 -0
  27. package/dist/chunk-EWRXLZO4.mjs.map +1 -0
  28. package/dist/chunk-FISNQZZ7.js +802 -0
  29. package/dist/chunk-FISNQZZ7.js.map +1 -0
  30. package/dist/chunk-HL4WDMGS.js +200 -0
  31. package/dist/chunk-HL4WDMGS.js.map +1 -0
  32. package/dist/chunk-IPYWR76I.js +314 -0
  33. package/dist/chunk-IPYWR76I.js.map +1 -0
  34. package/dist/chunk-JYYNWR5G.js +142 -0
  35. package/dist/chunk-JYYNWR5G.js.map +1 -0
  36. package/dist/chunk-LNKTYZJM.js +701 -0
  37. package/dist/chunk-LNKTYZJM.js.map +1 -0
  38. package/dist/chunk-LVZMONQL.mjs +699 -0
  39. package/dist/chunk-LVZMONQL.mjs.map +1 -0
  40. package/dist/chunk-MFXLQWOE.js +93 -0
  41. package/dist/chunk-MFXLQWOE.js.map +1 -0
  42. package/dist/chunk-NBIA4TTE.mjs +204 -0
  43. package/dist/chunk-NBIA4TTE.mjs.map +1 -0
  44. package/dist/chunk-O4DD2XJ2.js +206 -0
  45. package/dist/chunk-O4DD2XJ2.js.map +1 -0
  46. package/dist/chunk-P7HVBU2B.mjs +140 -0
  47. package/dist/chunk-P7HVBU2B.mjs.map +1 -0
  48. package/dist/chunk-Q7QVQYEB.js +210 -0
  49. package/dist/chunk-Q7QVQYEB.js.map +1 -0
  50. package/dist/chunk-RLZEG6KL.mjs +329 -0
  51. package/dist/chunk-RLZEG6KL.mjs.map +1 -0
  52. package/dist/chunk-SYLDBJ75.mjs +246 -0
  53. package/dist/chunk-SYLDBJ75.mjs.map +1 -0
  54. package/dist/chunk-TTEUU3CI.mjs +198 -0
  55. package/dist/chunk-TTEUU3CI.mjs.map +1 -0
  56. package/dist/chunk-V66BXDTR.mjs +292 -0
  57. package/dist/chunk-V66BXDTR.mjs.map +1 -0
  58. package/dist/chunk-X77ZT4OI.js +268 -0
  59. package/dist/chunk-X77ZT4OI.js.map +1 -0
  60. package/dist/imtoken/index.d.mts +116 -0
  61. package/dist/imtoken/index.d.ts +116 -0
  62. package/dist/imtoken/index.js +14 -0
  63. package/dist/imtoken/index.js.map +1 -0
  64. package/dist/imtoken/index.mjs +5 -0
  65. package/dist/imtoken/index.mjs.map +1 -0
  66. package/dist/index.d.mts +14 -0
  67. package/dist/index.d.ts +14 -0
  68. package/dist/index.js +170 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/index.mjs +13 -0
  71. package/dist/index.mjs.map +1 -0
  72. package/dist/ledger/index.d.mts +290 -0
  73. package/dist/ledger/index.d.ts +290 -0
  74. package/dist/ledger/index.js +14 -0
  75. package/dist/ledger/index.js.map +1 -0
  76. package/dist/ledger/index.mjs +5 -0
  77. package/dist/ledger/index.mjs.map +1 -0
  78. package/dist/okx/index.d.mts +88 -0
  79. package/dist/okx/index.d.ts +88 -0
  80. package/dist/okx/index.js +13 -0
  81. package/dist/okx/index.js.map +1 -0
  82. package/dist/okx/index.mjs +4 -0
  83. package/dist/okx/index.mjs.map +1 -0
  84. package/dist/phantom/index.d.mts +96 -0
  85. package/dist/phantom/index.d.ts +96 -0
  86. package/dist/phantom/index.js +14 -0
  87. package/dist/phantom/index.js.map +1 -0
  88. package/dist/phantom/index.mjs +5 -0
  89. package/dist/phantom/index.mjs.map +1 -0
  90. package/dist/psbt-builder-CFOs69Z5.d.mts +131 -0
  91. package/dist/psbt-builder-CFOs69Z5.d.ts +131 -0
  92. package/dist/trezor/index.d.mts +155 -0
  93. package/dist/trezor/index.d.ts +155 -0
  94. package/dist/trezor/index.js +14 -0
  95. package/dist/trezor/index.js.map +1 -0
  96. package/dist/trezor/index.mjs +5 -0
  97. package/dist/trezor/index.mjs.map +1 -0
  98. package/dist/unisat/index.d.mts +75 -0
  99. package/dist/unisat/index.d.ts +75 -0
  100. package/dist/unisat/index.js +13 -0
  101. package/dist/unisat/index.js.map +1 -0
  102. package/dist/unisat/index.mjs +4 -0
  103. package/dist/unisat/index.mjs.map +1 -0
  104. package/dist/utils/index.d.mts +398 -0
  105. package/dist/utils/index.d.ts +398 -0
  106. package/dist/utils/index.js +120 -0
  107. package/dist/utils/index.js.map +1 -0
  108. package/dist/utils/index.mjs +3 -0
  109. package/dist/utils/index.mjs.map +1 -0
  110. package/dist/xverse/index.d.mts +79 -0
  111. package/dist/xverse/index.d.ts +79 -0
  112. package/dist/xverse/index.js +13 -0
  113. package/dist/xverse/index.js.map +1 -0
  114. package/dist/xverse/index.mjs +4 -0
  115. package/dist/xverse/index.mjs.map +1 -0
  116. package/package.json +108 -0
  117. package/src/base.ts +132 -0
  118. package/src/binance/BinanceConnector.ts +307 -0
  119. package/src/binance/index.ts +1 -0
  120. package/src/bitget/BitgetConnector.ts +301 -0
  121. package/src/bitget/index.ts +1 -0
  122. package/src/imtoken/ImTokenConnector.ts +420 -0
  123. package/src/imtoken/index.ts +2 -0
  124. package/src/index.ts +78 -0
  125. package/src/ledger/LedgerConnector.ts +1019 -0
  126. package/src/ledger/index.ts +8 -0
  127. package/src/okx/OKXConnector.ts +230 -0
  128. package/src/okx/index.ts +1 -0
  129. package/src/phantom/PhantomConnector.ts +381 -0
  130. package/src/phantom/index.ts +2 -0
  131. package/src/trezor/TrezorConnector.ts +824 -0
  132. package/src/trezor/index.ts +6 -0
  133. package/src/unisat/UnisatConnector.ts +312 -0
  134. package/src/unisat/index.ts +1 -0
  135. package/src/utils/blockstream.ts +230 -0
  136. package/src/utils/btc-service.ts +364 -0
  137. package/src/utils/index.ts +56 -0
  138. package/src/utils/mempool.ts +232 -0
  139. package/src/utils/psbt-builder.ts +492 -0
  140. package/src/utils/types.ts +183 -0
  141. package/src/xverse/XverseConnector.ts +479 -0
  142. package/src/xverse/index.ts +1 -0
@@ -0,0 +1,8 @@
1
+ export { LedgerConnector } from './LedgerConnector';
2
+ export type {
3
+ LedgerConnectorOptions,
4
+ LedgerAddressType,
5
+ LedgerUtxoInput,
6
+ LedgerTxOutput,
7
+ SendBitcoinOptions,
8
+ } from './LedgerConnector';
@@ -0,0 +1,230 @@
1
+ import type {
2
+ WalletAccount,
3
+ BitcoinNetwork,
4
+ SignPsbtOptions,
5
+ } from 'otx-btc-wallet-core';
6
+ import { BaseConnector } from '../base';
7
+
8
+ /**
9
+ * OKX sign PSBT options
10
+ * @see https://web3.okx.com/build/dev-docs/sdks/chains/bitcoin/provider
11
+ */
12
+ interface OKXSignPsbtOptions {
13
+ autoFinalized?: boolean;
14
+ toSignInputs?: Array<{
15
+ index: number;
16
+ address?: string;
17
+ publicKey?: string;
18
+ sighashTypes?: number[];
19
+ disableTweakSigner?: boolean;
20
+ }>;
21
+ }
22
+
23
+ /**
24
+ * OKX account changed event data
25
+ */
26
+ interface OKXAccountChangedData {
27
+ address: string;
28
+ publicKey: string;
29
+ compressedPublicKey: string;
30
+ }
31
+
32
+ /**
33
+ * OKX Bitcoin provider interface
34
+ * @see https://web3.okx.com/build/dev-docs/sdks/chains/bitcoin/provider
35
+ */
36
+ interface OKXBitcoinProvider {
37
+ connect(): Promise<{ address: string; publicKey: string }>;
38
+ requestAccounts(): Promise<string[]>;
39
+ getAccounts(): Promise<string[]>;
40
+ getPublicKey(): Promise<string>;
41
+ getNetwork(): Promise<string>;
42
+ getBalance(): Promise<{ confirmed: number; unconfirmed: number; total: number }>;
43
+ signMessage(message: string, type?: 'ecdsa' | 'bip322-simple'): Promise<string>;
44
+ signPsbt(psbtHex: string, options?: OKXSignPsbtOptions): Promise<string>;
45
+ signPsbts(psbtHexs: string[], options?: OKXSignPsbtOptions[]): Promise<string[]>;
46
+ pushPsbt(psbtHex: string): Promise<string>;
47
+ sendBitcoin(toAddress: string, satoshis: number, options?: { feeRate?: number }): Promise<string>;
48
+ on(event: 'accountChanged', callback: (data: OKXAccountChangedData) => void): void;
49
+ on(event: 'accountsChanged', callback: (addresses: string[]) => void): void;
50
+ on(event: 'networkChanged', callback: (network: string) => void): void;
51
+ removeListener(event: 'accountChanged', callback: (data: OKXAccountChangedData) => void): void;
52
+ removeListener(event: 'accountsChanged', callback: (addresses: string[]) => void): void;
53
+ removeListener(event: 'networkChanged', callback: (network: string) => void): void;
54
+ }
55
+
56
+ // Extend window type
57
+ declare global {
58
+ interface Window {
59
+ okxwallet?: {
60
+ bitcoin?: OKXBitcoinProvider;
61
+ };
62
+ }
63
+ }
64
+
65
+ // OKX wallet icon
66
+ const OKX_ICON =
67
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF83MDM1XzUxNDgpIj4KPG1hc2sgaWQ9Im1hc2swXzcwMzVfNTE0OCIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjAiIHk9IjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj4KPHBhdGggZD0iTTIwMCAwSDBWMjAwSDIwMFYwWiIgZmlsbD0id2hpdGUiLz4KPC9tYXNrPgo8ZyBtYXNrPSJ1cmwoI21hc2swXzcwMzVfNTE0OCkiPgo8cGF0aCBkPSJNMTU3LjA2OCAwSDQyLjkzMkMxOS4yMjEzIDAgMCAxOS4yMjEzIDAgNDIuOTMyVjE1Ny4wNjhDMCAxODAuNzc5IDE5LjIyMTMgMjAwIDQyLjkzMiAyMDBIMTU3LjA2OEMxODAuNzc5IDIwMCAyMDAgMTgwLjc3OSAyMDAgMTU3LjA2OFY0Mi45MzJDMjAwIDE5LjIyMTMgMTgwLjc3OSAwIDE1Ny4wNjggMFoiIGZpbGw9ImJsYWNrIi8+CjwvZz4KPHBhdGggZD0iTTExNi44MzYgODEuNjY3Mkg4NC4xOTI2QzgyLjgwNTggODEuNjY3MiA4MS42ODE0IDgyLjc5MTIgODEuNjgxNCA4NC4xNzhWMTE2LjgyMkM4MS42ODE0IDExOC4yMDkgODIuODA1OCAxMTkuMzMzIDg0LjE5MjYgMTE5LjMzM0gxMTYuODM2QzExOC4yMjMgMTE5LjMzMyAxMTkuMzQ4IDExOC4yMDkgMTE5LjM0OCAxMTYuODIyVjg0LjE3OEMxMTkuMzQ4IDgyLjc5MTIgMTE4LjIyMyA4MS42NjcyIDExNi44MzYgODEuNjY3MloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik03OS4xNTUgNDRINDYuNTExM0M0NS4xMjQ1IDQ0IDQ0IDQ1LjEyNCA0NCA0Ni41MTEyVjc5LjE1NUM0NCA4MC41NDE4IDQ1LjEyNDUgODEuNjY2MiA0Ni41MTEzIDgxLjY2NjJINzkuMTU1QzgwLjU0MjMgODEuNjY2MiA4MS42NjYyIDgwLjU0MTggODEuNjY2MiA3OS4xNTVWNDYuNTExMkM4MS42NjYyIDQ1LjEyNCA4MC41NDIzIDQ0IDc5LjE1NSA0NFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNTQuNDg3IDQ0SDEyMS44NDNDMTIwLjQ1NiA0NCAxMTkuMzMyIDQ1LjEyNCAxMTkuMzMyIDQ2LjUxMTJWNzkuMTU1QzExOS4zMzIgODAuNTQxOCAxMjAuNDU2IDgxLjY2NjIgMTIxLjg0MyA4MS42NjYySDE1NC40ODdDMTU1Ljg3NCA4MS42NjYyIDE1Ni45OTggODAuNTQxOCAxNTYuOTk4IDc5LjE1NVY0Ni41MTEyQzE1Ni45OTggNDUuMTI0IDE1NS44NzQgNDQgMTU0LjQ4NyA0NFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik03OS4xNTUgMTE5LjMzMUg0Ni41MTEzQzQ1LjEyNDUgMTE5LjMzMSA0NCAxMjAuNDU1IDQ0IDEyMS44NDJWMTU0LjQ4NkM0NCAxNTUuODczIDQ1LjEyNDUgMTU2Ljk5OCA0Ni41MTEzIDE1Ni45OThINzkuMTU1QzgwLjU0MjMgMTU2Ljk5OCA4MS42NjYyIDE1NS44NzMgODEuNjY2MiAxNTQuNDg2VjEyMS44NDJDODEuNjY2MiAxMjAuNDU1IDgwLjU0MjMgMTE5LjMzMSA3OS4xNTUgMTE5LjMzMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNTQuNDg3IDExOS4zMzFIMTIxLjg0M0MxMjAuNDU2IDExOS4zMzEgMTE5LjMzMiAxMjAuNDU1IDExOS4zMzIgMTIxLjg0MlYxNTQuNDg2QzExOS4zMzIgMTU1Ljg3MyAxMjAuNDU2IDE1Ni45OTggMTIxLjg0MyAxNTYuOTk4SDE1NC40ODdDMTU1Ljg3NCAxNTYuOTk4IDE1Ni45OTggMTU1Ljg3MyAxNTYuOTk4IDE1NC40ODZWMTIxLjg0MkMxNTYuOTk4IDEyMC40NTUgMTU1Ljg3NCAxMTkuMzMxIDE1NC40ODcgMTE5LjMzMVoiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNzAzNV81MTQ4Ij4KPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==';
68
+
69
+ /**
70
+ * OKX Wallet Connector
71
+ *
72
+ * @see https://www.okx.com/web3/build/docs/sdks/app-connect-overview
73
+ */
74
+ export class OKXConnector extends BaseConnector {
75
+ readonly id = 'okx';
76
+ readonly name = 'OKX Wallet';
77
+ readonly icon = OKX_ICON;
78
+
79
+ private _unsubscribeAccounts?: () => void;
80
+ private _unsubscribeNetwork?: () => void;
81
+
82
+ protected getProvider(): OKXBitcoinProvider | undefined {
83
+ if (typeof window === 'undefined') return undefined;
84
+ return window.okxwallet?.bitcoin;
85
+ }
86
+
87
+ async connect(): Promise<WalletAccount> {
88
+ this.ensureInstalled();
89
+ const provider = this.getProvider()!;
90
+
91
+ try {
92
+ const result = await provider.connect();
93
+
94
+ this.setupListeners();
95
+
96
+ return {
97
+ address: result.address,
98
+ publicKey: result.publicKey,
99
+ type: this.inferAddressType(result.address),
100
+ };
101
+ } catch (error) {
102
+ this.handleError(error);
103
+ }
104
+ }
105
+
106
+ async disconnect(): Promise<void> {
107
+ this._unsubscribeAccounts?.();
108
+ this._unsubscribeNetwork?.();
109
+ this.cleanup();
110
+ }
111
+
112
+ async getAccounts(): Promise<WalletAccount[]> {
113
+ this.ensureInstalled();
114
+ const provider = this.getProvider()!;
115
+
116
+ try {
117
+ const addresses = await provider.getAccounts();
118
+ const publicKey = await provider.getPublicKey();
119
+
120
+ return addresses.map((address) => ({
121
+ address,
122
+ publicKey,
123
+ type: this.inferAddressType(address),
124
+ }));
125
+ } catch (error) {
126
+ this.handleError(error);
127
+ }
128
+ }
129
+
130
+ async signMessage(message: string): Promise<string> {
131
+ this.ensureInstalled();
132
+ const provider = this.getProvider()!;
133
+
134
+ try {
135
+ return await provider.signMessage(message);
136
+ } catch (error) {
137
+ this.handleError(error);
138
+ }
139
+ }
140
+
141
+ async signPsbt(psbtHex: string, options?: SignPsbtOptions): Promise<string> {
142
+ this.ensureInstalled();
143
+ const provider = this.getProvider()!;
144
+
145
+ try {
146
+ const okxOptions: OKXSignPsbtOptions = {
147
+ autoFinalized: options?.autoFinalize ?? true,
148
+ };
149
+
150
+ if (options?.toSignInputs) {
151
+ okxOptions.toSignInputs = options.toSignInputs;
152
+ }
153
+
154
+ return await provider.signPsbt(psbtHex, okxOptions);
155
+ } catch (error) {
156
+ this.handleError(error);
157
+ }
158
+ }
159
+
160
+ async sendTransaction(to: string, satoshis: number): Promise<string> {
161
+ this.ensureInstalled();
162
+ const provider = this.getProvider()!;
163
+
164
+ try {
165
+ return await provider.sendBitcoin(to, satoshis);
166
+ } catch (error) {
167
+ this.handleError(error);
168
+ }
169
+ }
170
+
171
+ async getNetwork(): Promise<BitcoinNetwork> {
172
+ this.ensureInstalled();
173
+ const provider = this.getProvider()!;
174
+
175
+ try {
176
+ const network = await provider.getNetwork();
177
+ return this.mapNetwork(network);
178
+ } catch (error) {
179
+ this.handleError(error);
180
+ }
181
+ }
182
+
183
+ // OKX does not support switchNetwork via API - users must switch manually in extension
184
+
185
+ private setupListeners(): void {
186
+ const provider = this.getProvider();
187
+ if (!provider) return;
188
+
189
+ // accountChanged returns object with address, publicKey, compressedPublicKey
190
+ const handleAccountChanged = (data: OKXAccountChangedData) => {
191
+ const walletAccount: WalletAccount = {
192
+ address: data.address,
193
+ publicKey: data.publicKey,
194
+ type: this.inferAddressType(data.address),
195
+ };
196
+ this.emitAccountsChanged([walletAccount]);
197
+ };
198
+
199
+ const handleNetworkChanged = (network: string) => {
200
+ const btcNetwork = this.mapNetwork(network);
201
+ this.emitNetworkChanged(btcNetwork);
202
+ };
203
+
204
+ provider.on('accountChanged', handleAccountChanged);
205
+ provider.on('networkChanged', handleNetworkChanged);
206
+
207
+ this._unsubscribeAccounts = () => {
208
+ provider.removeListener('accountChanged', handleAccountChanged);
209
+ };
210
+ this._unsubscribeNetwork = () => {
211
+ provider.removeListener('networkChanged', handleNetworkChanged);
212
+ };
213
+ }
214
+
215
+ private mapNetwork(network: string): BitcoinNetwork {
216
+ switch (network.toLowerCase()) {
217
+ case 'livenet':
218
+ case 'mainnet':
219
+ return 'mainnet';
220
+ case 'testnet':
221
+ return 'testnet';
222
+ case 'testnet4':
223
+ return 'testnet4';
224
+ case 'signet':
225
+ return 'signet';
226
+ default:
227
+ return 'mainnet';
228
+ }
229
+ }
230
+ }
@@ -0,0 +1 @@
1
+ export { OKXConnector } from './OKXConnector';
@@ -0,0 +1,381 @@
1
+ import type {
2
+ WalletAccount,
3
+ BitcoinNetwork,
4
+ SignPsbtOptions,
5
+ } from 'otx-btc-wallet-core';
6
+ import * as bitcoin from 'bitcoinjs-lib';
7
+ import { BaseConnector } from '../base';
8
+ import { BtcService } from '../utils';
9
+ import {
10
+ generatePsbtForSend,
11
+ finalizeAllInputs,
12
+ getBitcoinJsNetwork,
13
+ type GeneratePsbtOptions,
14
+ } from '../utils';
15
+
16
+ /**
17
+ * Options for sendTransaction method
18
+ */
19
+ export interface PhantomSendOptions extends GeneratePsbtOptions {
20
+ // Additional Phantom-specific options can be added here
21
+ }
22
+
23
+ /**
24
+ * Phantom Bitcoin account type
25
+ * @see https://docs.phantom.com/bitcoin/establishing-a-connection
26
+ */
27
+ interface BtcAccount {
28
+ address: string;
29
+ addressType: 'p2tr' | 'p2wpkh' | 'p2sh' | 'p2pkh';
30
+ publicKey: string;
31
+ purpose: 'payment' | 'ordinals';
32
+ }
33
+
34
+ /**
35
+ * Phantom Bitcoin provider interface
36
+ * @see https://docs.phantom.com/bitcoin/provider-api-reference
37
+ */
38
+ interface PhantomBitcoinProvider {
39
+ isPhantom: boolean;
40
+ requestAccounts(): Promise<BtcAccount[]>;
41
+ signMessage(address: string, message: Uint8Array): Promise<{ signature: Uint8Array }>;
42
+ signPSBT(
43
+ psbt: Uint8Array,
44
+ options: {
45
+ inputsToSign: Array<{
46
+ address: string;
47
+ signingIndexes: number[];
48
+ sigHash?: number;
49
+ }>;
50
+ }
51
+ ): Promise<Uint8Array>;
52
+ on(event: 'accountsChanged', callback: (accounts: BtcAccount[]) => void): void;
53
+ off(event: 'accountsChanged', callback: (accounts: BtcAccount[]) => void): void;
54
+ }
55
+
56
+ // Extend window type
57
+ declare global {
58
+ interface Window {
59
+ phantom?: {
60
+ bitcoin?: PhantomBitcoinProvider;
61
+ };
62
+ }
63
+ }
64
+
65
+ // Phantom wallet icon
66
+ const PHANTOM_ICON =
67
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF83MDM1XzUxMjYpIj4KPG1hc2sgaWQ9Im1hc2swXzcwMzVfNTEyNiIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjAiIHk9IjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj4KPHBhdGggZD0iTTIwMCAwSDBWMjAwSDIwMFYwWiIgZmlsbD0id2hpdGUiLz4KPC9tYXNrPgo8ZyBtYXNrPSJ1cmwoI21hc2swXzcwMzVfNTEyNikiPgo8cGF0aCBkPSJNMTU3LjA2OCAwSDQyLjkzMkMxOS4yMjEzIDAgMCAxOS4yMjEzIDAgNDIuOTMyVjE1Ny4wNjhDMCAxODAuNzc5IDE5LjIyMTMgMjAwIDQyLjkzMiAyMDBIMTU3LjA2OEMxODAuNzc5IDIwMCAyMDAgMTgwLjc3OSAyMDAgMTU3LjA2OFY0Mi45MzJDMjAwIDE5LjIyMTMgMTgwLjc3OSAwIDE1Ny4wNjggMFoiIGZpbGw9IiNBQjlGRjIiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik04Ni4yMDMxIDEyOS45NjlDNzguMzUwMyAxNDIuMDAyIDY1LjE5MTMgMTU3LjIzIDQ3LjY4MTYgMTU3LjIzQzM5LjQwNDMgMTU3LjIzIDMxLjQ0NTMgMTUzLjgyMiAzMS40NDUzIDEzOS4wMkMzMS40NDUzIDEwMS4zMjQgODIuOTEzMyA0Mi45Njg4IDEzMC42NjcgNDIuOTY4OEMxNTcuODM0IDQyLjk2ODggMTY4LjY1OCA2MS44MTcgMTY4LjY1OCA4My4yMjFDMTY4LjY1OCAxMTAuNjk1IDE1MC44MyAxNDIuMTA4IDEzMy4xMDggMTQyLjEwOEMxMjcuNDg0IDE0Mi4xMDggMTI0LjcyNCAxMzkuMDIgMTI0LjcyNCAxMzQuMTIyQzEyNC43MjQgMTMyLjg0NCAxMjQuOTM3IDEzMS40NiAxMjUuMzYxIDEyOS45NjlDMTE5LjMxMiAxNDAuMjk4IDEwNy42MzkgMTQ5Ljg4MiA5Ni43MDkgMTQ5Ljg4MkM4OC43NSAxNDkuODgyIDg0LjcxNzUgMTQ0Ljg3NyA4NC43MTc1IDEzNy44NDlDODQuNzE3NSAxMzUuMjkzIDg1LjI0OCAxMzIuNjMxIDg2LjIwMzEgMTI5Ljk2OVpNMTUwLjcyNyA4Mi40NzgyQzE1MC43MjcgODguNzE1MiAxNDcuMDQ3IDkxLjgzMzcgMTQyLjkzMSA5MS44MzM3QzEzOC43NTIgOTEuODMzNyAxMzUuMTM1IDg4LjcxNTIgMTM1LjEzNSA4Mi40NzgyQzEzNS4xMzUgNzYuMjQxMyAxMzguNzUyIDczLjEyMjggMTQyLjkzMSA3My4xMjI4QzE0Ny4wNDcgNzMuMTIyOCAxNTAuNzI3IDc2LjI0MTMgMTUwLjcyNyA4Mi40NzgyWk0xMjcuMzM4IDgyLjQ3ODVDMTI3LjMzOCA4OC43MTU1IDEyMy42NTkgOTEuODM0IDExOS41NDIgOTEuODM0QzExNS4zNjQgOTEuODM0IDExMS43NDYgODguNzE1NSAxMTEuNzQ2IDgyLjQ3ODVDMTExLjc0NiA3Ni4yNDE1IDExNS4zNjQgNzMuMTIzMiAxMTkuNTQyIDczLjEyMzJDMTIzLjY1OSA3My4xMjMyIDEyNy4zMzggNzYuMjQxNSAxMjcuMzM4IDgyLjQ3ODVaIiBmaWxsPSIjRkZGREY4Ii8+CjwvZz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF83MDM1XzUxMjYiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K';
68
+
69
+ /**
70
+ * Phantom Wallet Connector (Bitcoin support)
71
+ *
72
+ * @see https://docs.phantom.com/bitcoin/detecting-the-provider
73
+ */
74
+ export class PhantomConnector extends BaseConnector {
75
+ readonly id = 'phantom';
76
+ readonly name = 'Phantom Wallet';
77
+ readonly icon = PHANTOM_ICON;
78
+
79
+ private _accounts: BtcAccount[] = [];
80
+ private _paymentAccount: WalletAccount | null = null;
81
+ private _ordinalsAccount: WalletAccount | null = null;
82
+ private _removeAccountChangeListener: (() => void) | null = null;
83
+
84
+ protected getProvider(): PhantomBitcoinProvider | undefined {
85
+ if (typeof window === 'undefined') return undefined;
86
+ const provider = window.phantom?.bitcoin;
87
+ if (provider?.isPhantom) {
88
+ return provider;
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ async connect(): Promise<WalletAccount> {
94
+ this.ensureInstalled();
95
+ const provider = this.getProvider()!;
96
+
97
+ try {
98
+ const accounts = await provider.requestAccounts();
99
+
100
+ if (!accounts || accounts.length === 0) {
101
+ throw new Error('No accounts returned');
102
+ }
103
+
104
+ this._accounts = accounts;
105
+ this.updateAccountsFromBtcAccounts(accounts);
106
+
107
+ // Setup event listeners
108
+ this.setupEventListeners();
109
+
110
+ if (!this._paymentAccount) {
111
+ throw new Error('No payment account found');
112
+ }
113
+
114
+ return this._paymentAccount;
115
+ } catch (error) {
116
+ this.handleError(error);
117
+ }
118
+ }
119
+
120
+ private updateAccountsFromBtcAccounts(accounts: BtcAccount[]): void {
121
+ const paymentAcc = accounts.find((a) => a.purpose === 'payment');
122
+ const ordinalsAcc = accounts.find((a) => a.purpose === 'ordinals');
123
+
124
+ if (paymentAcc) {
125
+ this._paymentAccount = {
126
+ address: paymentAcc.address,
127
+ publicKey: paymentAcc.publicKey,
128
+ type: this.mapAddressType(paymentAcc.addressType),
129
+ };
130
+ }
131
+
132
+ if (ordinalsAcc) {
133
+ this._ordinalsAccount = {
134
+ address: ordinalsAcc.address,
135
+ publicKey: ordinalsAcc.publicKey,
136
+ type: this.mapAddressType(ordinalsAcc.addressType),
137
+ };
138
+ }
139
+ }
140
+
141
+ private mapAddressType(addressType: BtcAccount['addressType']): WalletAccount['type'] {
142
+ switch (addressType) {
143
+ case 'p2tr':
144
+ return 'taproot';
145
+ case 'p2wpkh':
146
+ return 'segwit';
147
+ case 'p2sh':
148
+ return 'nested-segwit';
149
+ case 'p2pkh':
150
+ return 'legacy';
151
+ default:
152
+ return 'legacy';
153
+ }
154
+ }
155
+
156
+ private setupEventListeners(): void {
157
+ const provider = this.getProvider();
158
+ if (!provider) return;
159
+
160
+ // Remove existing listener
161
+ this.removeEventListeners();
162
+
163
+ // Account change listener
164
+ const handleAccountsChanged = (accounts: BtcAccount[]) => {
165
+ this._accounts = accounts;
166
+ this.updateAccountsFromBtcAccounts(accounts);
167
+
168
+ const walletAccounts: WalletAccount[] = accounts.map((acc) => ({
169
+ address: acc.address,
170
+ publicKey: acc.publicKey,
171
+ type: this.mapAddressType(acc.addressType),
172
+ }));
173
+
174
+ this.emitAccountsChanged(walletAccounts);
175
+ };
176
+
177
+ provider.on('accountsChanged', handleAccountsChanged);
178
+
179
+ this._removeAccountChangeListener = () => {
180
+ provider.off('accountsChanged', handleAccountsChanged);
181
+ };
182
+ }
183
+
184
+ private removeEventListeners(): void {
185
+ this._removeAccountChangeListener?.();
186
+ this._removeAccountChangeListener = null;
187
+ }
188
+
189
+ async disconnect(): Promise<void> {
190
+ this.removeEventListeners();
191
+ this._accounts = [];
192
+ this._paymentAccount = null;
193
+ this._ordinalsAccount = null;
194
+ this.cleanup();
195
+ }
196
+
197
+ async getAccounts(): Promise<WalletAccount[]> {
198
+ const accounts: WalletAccount[] = [];
199
+ if (this._paymentAccount) accounts.push(this._paymentAccount);
200
+ if (this._ordinalsAccount) accounts.push(this._ordinalsAccount);
201
+ return accounts;
202
+ }
203
+
204
+ async signMessage(message: string): Promise<string> {
205
+ this.ensureInstalled();
206
+ const provider = this.getProvider()!;
207
+
208
+ if (!this._paymentAccount) {
209
+ throw new Error('Not connected');
210
+ }
211
+
212
+ try {
213
+ // Convert message to Uint8Array
214
+ const messageBytes = new TextEncoder().encode(message);
215
+ const result = await provider.signMessage(this._paymentAccount.address, messageBytes);
216
+ // Convert Uint8Array signature to hex string
217
+ return bytesToHex(result.signature);
218
+ } catch (error) {
219
+ this.handleError(error);
220
+ }
221
+ }
222
+
223
+ async signPsbt(psbtHex: string, options?: SignPsbtOptions): Promise<string> {
224
+ this.ensureInstalled();
225
+ const provider = this.getProvider()!;
226
+
227
+ if (!this._paymentAccount) {
228
+ throw new Error('Not connected');
229
+ }
230
+
231
+ try {
232
+ // Convert hex to Uint8Array
233
+ const psbtBytes = hexToBytes(psbtHex);
234
+
235
+ // Build inputsToSign
236
+ const allAddresses = this._accounts.map((a) => a.address);
237
+ let inputsToSign: Array<{ address: string; signingIndexes: number[]; sigHash?: number }>;
238
+
239
+ if (options?.toSignInputs && options.toSignInputs.length > 0) {
240
+ inputsToSign = options.toSignInputs.map((input) => {
241
+ const addr = input.address && allAddresses.includes(input.address)
242
+ ? input.address
243
+ : this._paymentAccount!.address;
244
+
245
+ const item: { address: string; signingIndexes: number[]; sigHash?: number } = {
246
+ address: addr,
247
+ signingIndexes: [input.index],
248
+ };
249
+
250
+ if (input.sighashTypes && input.sighashTypes[0] !== undefined) {
251
+ item.sigHash = input.sighashTypes[0];
252
+ }
253
+
254
+ return item;
255
+ });
256
+ } else {
257
+ // Default: sign all inputs with payment address
258
+ inputsToSign = [{
259
+ address: this._paymentAccount.address,
260
+ signingIndexes: [0],
261
+ }];
262
+ }
263
+
264
+ const signedPsbt = await provider.signPSBT(psbtBytes, { inputsToSign });
265
+
266
+ // Convert back to hex
267
+ return bytesToHex(signedPsbt);
268
+ } catch (error) {
269
+ this.handleError(error);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Send Bitcoin transaction
275
+ * Builds a PSBT, signs it with Phantom, and broadcasts
276
+ *
277
+ * @param to - Recipient address
278
+ * @param satoshis - Amount to send in satoshis
279
+ * @param options - Send options (feeRate, etc.)
280
+ * @returns Transaction ID after broadcast
281
+ */
282
+ async sendTransaction(
283
+ to: string,
284
+ satoshis: number,
285
+ options?: PhantomSendOptions
286
+ ): Promise<string> {
287
+ this.ensureInstalled();
288
+ const provider = this.getProvider()!;
289
+
290
+ if (!this._paymentAccount) {
291
+ throw new Error('Not connected. Please call connect() first.');
292
+ }
293
+
294
+ const network: BitcoinNetwork = 'mainnet'; // Phantom only supports mainnet
295
+ const btcNetwork = getBitcoinJsNetwork(network);
296
+
297
+ // 1. Generate PSBT using the utility
298
+ const {
299
+ psbt,
300
+ selectedUtxos,
301
+ inputsToSign,
302
+ } = await generatePsbtForSend(
303
+ to,
304
+ satoshis,
305
+ this._paymentAccount.address,
306
+ this._paymentAccount.publicKey,
307
+ network,
308
+ options
309
+ );
310
+
311
+ // 2. Convert PSBT to Uint8Array for Phantom
312
+ const psbtBytes = psbt.toBuffer();
313
+
314
+ // 3. Build inputsToSign for Phantom API
315
+ const phantomInputsToSign = [{
316
+ address: this._paymentAccount.address,
317
+ signingIndexes: inputsToSign.map((input) => input.index),
318
+ }];
319
+
320
+ // 4. Sign PSBT with Phantom
321
+ const signedPsbtBytes = await provider.signPSBT(psbtBytes, {
322
+ inputsToSign: phantomInputsToSign,
323
+ });
324
+
325
+ // 5. Parse signed PSBT and finalize
326
+ const signedPsbt = bitcoin.Psbt.fromBuffer(
327
+ new Uint8Array(signedPsbtBytes),
328
+ { network: btcNetwork }
329
+ );
330
+ finalizeAllInputs(signedPsbt, selectedUtxos.length);
331
+
332
+ // 6. Extract transaction
333
+ const tx = signedPsbt.extractTransaction();
334
+ const txHex = tx.toHex();
335
+
336
+ // 7. Broadcast using BtcService
337
+ const btcService = new BtcService(network);
338
+ return await btcService.broadcastTransaction(txHex);
339
+ }
340
+
341
+ async getNetwork(): Promise<BitcoinNetwork> {
342
+ // Phantom Bitcoin only supports mainnet currently
343
+ return 'mainnet';
344
+ }
345
+
346
+ /**
347
+ * Get the ordinals address (if available)
348
+ */
349
+ getOrdinalsAddress(): WalletAccount | null {
350
+ return this._ordinalsAccount;
351
+ }
352
+
353
+ /**
354
+ * Get the payment address
355
+ */
356
+ getPaymentAddress(): WalletAccount | null {
357
+ return this._paymentAccount;
358
+ }
359
+
360
+ /**
361
+ * Get all connected addresses
362
+ */
363
+ getAllAddresses(): string[] {
364
+ return this._accounts.map((acc) => acc.address);
365
+ }
366
+ }
367
+
368
+ // Helper functions for hex/bytes conversion
369
+ function hexToBytes(hex: string): Uint8Array {
370
+ const bytes = new Uint8Array(hex.length / 2);
371
+ for (let i = 0; i < hex.length; i += 2) {
372
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
373
+ }
374
+ return bytes;
375
+ }
376
+
377
+ function bytesToHex(bytes: Uint8Array): string {
378
+ return Array.from(bytes)
379
+ .map((b) => b.toString(16).padStart(2, '0'))
380
+ .join('');
381
+ }
@@ -0,0 +1,2 @@
1
+ export { PhantomConnector } from './PhantomConnector';
2
+ export type { PhantomSendOptions } from './PhantomConnector';