@dynamic-labs/embedded-wallet-bitcoin 4.18.7
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 +5475 -0
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/_virtual/_tslib.cjs +36 -0
- package/_virtual/_tslib.js +32 -0
- package/package.cjs +8 -0
- package/package.js +4 -0
- package/package.json +35 -0
- package/src/TurnkeyBitcoinWalletConnectors.cjs +23 -0
- package/src/TurnkeyBitcoinWalletConnectors.d.ts +2 -0
- package/src/TurnkeyBitcoinWalletConnectors.js +19 -0
- package/src/index.cjs +12 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +6 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/TurnkeyBitcoinWalletConnector.cjs +360 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/TurnkeyBitcoinWalletConnector.d.ts +47 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/TurnkeyBitcoinWalletConnector.js +352 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/fees.cjs +50 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/fees.d.ts +15 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/fees.js +46 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/index.d.ts +1 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/signer.cjs +51 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/signer.d.ts +23 -0
- package/src/lib/TurnkeyBitcoinWalletConnector/signer.js +47 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { __awaiter } from '../../../_virtual/_tslib.js';
|
|
3
|
+
import ecc from '@bitcoinerlab/secp256k1';
|
|
4
|
+
import { ECPairFactory } from 'ecpair';
|
|
5
|
+
import { initEccLib, networks, Psbt, payments, address } from 'bitcoinjs-lib';
|
|
6
|
+
import { BitcoinWallet, getMempoolApiUrl, satoshisToBtc } from '@dynamic-labs/bitcoin';
|
|
7
|
+
import { isSameAddress } from '@dynamic-labs/wallet-connector-core';
|
|
8
|
+
import { DynamicError } from '@dynamic-labs/utils';
|
|
9
|
+
import { TurnkeyWalletConnectorBase, findTurnkeyVerifiedCredentials, logger } from '@dynamic-labs/embedded-wallet';
|
|
10
|
+
import { estimateFees } from './fees.js';
|
|
11
|
+
import { TurnkeySigner } from './signer.js';
|
|
12
|
+
|
|
13
|
+
initEccLib(ecc);
|
|
14
|
+
class TurnkeyBitcoinWalletConnector extends TurnkeyWalletConnectorBase {
|
|
15
|
+
constructor(nameAndKey, props) {
|
|
16
|
+
super(nameAndKey, props);
|
|
17
|
+
this.ChainWallet = BitcoinWallet;
|
|
18
|
+
this.connectedChain = 'BTC';
|
|
19
|
+
this.supportedChains = ['BTC'];
|
|
20
|
+
this.canFetchConnectedAccounts = false;
|
|
21
|
+
this.isHardwareWalletEnabled = false;
|
|
22
|
+
}
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
24
|
+
isLedgerAddress(_address) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
clearConnectedAccounts() {
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
setVerifiedCredentials(verifiedCredentials) {
|
|
31
|
+
const turnkeyVerifiedCredentials = findTurnkeyVerifiedCredentials(verifiedCredentials, 'bip122');
|
|
32
|
+
const [turnkeyVerifiedCredential] = turnkeyVerifiedCredentials;
|
|
33
|
+
const didTurnkeyVerifiedCredentialsChanged = JSON.stringify(this.verifiedCredentials) !==
|
|
34
|
+
JSON.stringify(turnkeyVerifiedCredentials);
|
|
35
|
+
if (!didTurnkeyVerifiedCredentialsChanged) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.verifiedCredential = turnkeyVerifiedCredential;
|
|
39
|
+
this.verifiedCredentials = turnkeyVerifiedCredentials;
|
|
40
|
+
this.refreshTurnkeyAccount();
|
|
41
|
+
}
|
|
42
|
+
validateActiveWallet(expectedAddress) {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
var _a, _b;
|
|
45
|
+
const activeAddress = ((_a = this.verifiedCredential) === null || _a === void 0 ? void 0 : _a.address) || '';
|
|
46
|
+
const isWalletActive = isSameAddress(activeAddress, expectedAddress, this.connectedChain);
|
|
47
|
+
if (!isWalletActive) {
|
|
48
|
+
const targetActiveAccount = (_b = this.verifiedCredentials) === null || _b === void 0 ? void 0 : _b.find((vc) => (vc === null || vc === void 0 ? void 0 : vc.address) === expectedAddress);
|
|
49
|
+
if (!targetActiveAccount) {
|
|
50
|
+
throw new DynamicError('Account not found');
|
|
51
|
+
}
|
|
52
|
+
this.verifiedCredential = targetActiveAccount;
|
|
53
|
+
this.refreshTurnkeyAccount();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
refreshTurnkeyAccount() {
|
|
58
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
this._turnkeyAccount = undefined;
|
|
60
|
+
return this.getTurnkeyAccount();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
createTurnkeyAccount() {
|
|
64
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
const turnkeyClient = yield this.getTurnkeyClient();
|
|
66
|
+
const account = turnkeyClient;
|
|
67
|
+
return account;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
getTurnkeyAccount() {
|
|
71
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
var _a, _b;
|
|
73
|
+
const { turnkeySubOrganizationId } = (_a = this.walletProperties) !== null && _a !== void 0 ? _a : {};
|
|
74
|
+
const { address } = (_b = this.verifiedCredential) !== null && _b !== void 0 ? _b : {};
|
|
75
|
+
if (!turnkeySubOrganizationId || !address) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this._turnkeyAccount = yield this.createTurnkeyAccount();
|
|
79
|
+
this.setLoggerMetadata();
|
|
80
|
+
return this._turnkeyAccount;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
getUTXOs(address) {
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
try {
|
|
86
|
+
const url = getMempoolApiUrl(address);
|
|
87
|
+
const response = yield fetch(`${url}/address/${address}/utxo`);
|
|
88
|
+
return yield response.json();
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
logger.error('Error fetching UTXOs:', error);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
getBitcoinNetwork(accountAddress) {
|
|
97
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
return accountAddress.startsWith('t') ? networks.testnet : networks.bitcoin;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
sendRawTransaction(rawTransaction) {
|
|
102
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
103
|
+
if (!rawTransaction) {
|
|
104
|
+
throw new DynamicError('No transaction specified!');
|
|
105
|
+
}
|
|
106
|
+
const accountAddress = this.getAccount();
|
|
107
|
+
if (!accountAddress) {
|
|
108
|
+
throw new DynamicError('No account address found!');
|
|
109
|
+
}
|
|
110
|
+
const API_URL = getMempoolApiUrl(accountAddress);
|
|
111
|
+
const response = yield fetch(`${API_URL}/tx`, {
|
|
112
|
+
body: rawTransaction,
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
115
|
+
},
|
|
116
|
+
method: 'POST',
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
if (response.status === 429) {
|
|
120
|
+
throw new DynamicError('sendRawTransaction - mempool api rate limit exceeded');
|
|
121
|
+
}
|
|
122
|
+
const error = yield response.text();
|
|
123
|
+
logger.debug(`sendRawTransaction - response not ok: ${JSON.stringify(error)}`);
|
|
124
|
+
throw new DynamicError('sendRawTransaction - failed to send transaction');
|
|
125
|
+
}
|
|
126
|
+
// this endpoint returns the transaction ID
|
|
127
|
+
return response.text();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
sendBitcoin(_a) {
|
|
131
|
+
return __awaiter(this, arguments, void 0, function* ({ recipientAddress, amount, }) {
|
|
132
|
+
var _b, _c;
|
|
133
|
+
yield this.createOrRestoreSession();
|
|
134
|
+
const accountAddress = this.getAccount();
|
|
135
|
+
const { walletAdditionalAddresses } = (_b = this.verifiedCredential) !== null && _b !== void 0 ? _b : {};
|
|
136
|
+
if (!accountAddress || !walletAdditionalAddresses) {
|
|
137
|
+
throw new Error('Failed to get address or wallet additional addresses');
|
|
138
|
+
}
|
|
139
|
+
const additionalAddress = walletAdditionalAddresses.find((additionalAddress) => additionalAddress.address === accountAddress);
|
|
140
|
+
const publicKeyCompressed = additionalAddress === null || additionalAddress === void 0 ? void 0 : additionalAddress.publicKey;
|
|
141
|
+
if (!publicKeyCompressed) {
|
|
142
|
+
throw new Error('Failed to get public key');
|
|
143
|
+
}
|
|
144
|
+
const ECPair = ECPairFactory(ecc);
|
|
145
|
+
const pair = ECPair.fromPublicKey(Buffer.from(publicKeyCompressed, 'hex'));
|
|
146
|
+
const network = yield this.getBitcoinNetwork(accountAddress);
|
|
147
|
+
const psbt = new Psbt({ network });
|
|
148
|
+
const utxos = yield this.getUTXOs(accountAddress);
|
|
149
|
+
const feeEstimate = yield estimateFees({
|
|
150
|
+
network,
|
|
151
|
+
numInputs: utxos.length,
|
|
152
|
+
numOutputs: 2,
|
|
153
|
+
});
|
|
154
|
+
const totalToSpend = utxos.reduce((total, utxo) => total + utxo.value, 0);
|
|
155
|
+
const maxToSpend = totalToSpend - feeEstimate;
|
|
156
|
+
const changeAmount = BigInt(maxToSpend) - BigInt(amount);
|
|
157
|
+
for (const utxo of utxos) {
|
|
158
|
+
if (accountAddress.startsWith('tb1p') ||
|
|
159
|
+
accountAddress.startsWith('bc1p')) {
|
|
160
|
+
const xOnlyPublicKey = pair.publicKey.slice(1, 33);
|
|
161
|
+
const outputScript = payments.p2tr({
|
|
162
|
+
internalPubkey: xOnlyPublicKey,
|
|
163
|
+
network: network,
|
|
164
|
+
}).output;
|
|
165
|
+
if (!outputScript) {
|
|
166
|
+
throw new Error('Failed to create output script');
|
|
167
|
+
}
|
|
168
|
+
psbt.addInput({
|
|
169
|
+
hash: utxo.txid,
|
|
170
|
+
index: utxo.vout,
|
|
171
|
+
tapInternalKey: xOnlyPublicKey,
|
|
172
|
+
witnessUtxo: {
|
|
173
|
+
script: outputScript,
|
|
174
|
+
value: utxo.value,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// P2WPKH (SegWit)
|
|
180
|
+
const witnessUtxo = payments.p2wpkh({
|
|
181
|
+
network,
|
|
182
|
+
pubkey: pair.publicKey,
|
|
183
|
+
}).output;
|
|
184
|
+
if (!witnessUtxo) {
|
|
185
|
+
throw new Error('Failed to create witness utxo');
|
|
186
|
+
}
|
|
187
|
+
psbt.addInput({
|
|
188
|
+
hash: utxo.txid,
|
|
189
|
+
index: utxo.vout,
|
|
190
|
+
witnessUtxo: {
|
|
191
|
+
script: witnessUtxo,
|
|
192
|
+
value: utxo.value,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const DUST_LIMIT = 546; // Bitcoin's dust limit in satoshis
|
|
198
|
+
if (Number(amount) < DUST_LIMIT) {
|
|
199
|
+
throw new Error('Amount is below dust limit of 546 satoshis');
|
|
200
|
+
}
|
|
201
|
+
psbt.addOutput({
|
|
202
|
+
script: address.toOutputScript(recipientAddress, network),
|
|
203
|
+
value: Number(amount),
|
|
204
|
+
});
|
|
205
|
+
if (changeAmount > 0) {
|
|
206
|
+
psbt.addOutput({
|
|
207
|
+
script: address.toOutputScript(accountAddress, network),
|
|
208
|
+
value: Number(changeAmount),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const turnkeyClient = yield this.getTurnkeyAccount();
|
|
212
|
+
if (!turnkeyClient) {
|
|
213
|
+
throw new Error('Failed to get turnkey client');
|
|
214
|
+
}
|
|
215
|
+
const signer = new TurnkeySigner({
|
|
216
|
+
address: accountAddress,
|
|
217
|
+
client: turnkeyClient,
|
|
218
|
+
organizationId: (_c = this.walletProperties) === null || _c === void 0 ? void 0 : _c.turnkeySubOrganizationId,
|
|
219
|
+
publicKey: address.fromBech32(accountAddress).data,
|
|
220
|
+
});
|
|
221
|
+
yield Promise.all(utxos.map((_utxo, i) => __awaiter(this, void 0, void 0, function* () {
|
|
222
|
+
yield psbt.signInputAsync(i, signer);
|
|
223
|
+
})));
|
|
224
|
+
psbt.finalizeAllInputs();
|
|
225
|
+
const txHex = psbt.extractTransaction().toHex();
|
|
226
|
+
return this.sendRawTransaction(txHex);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
signPsbt(_a) {
|
|
230
|
+
return __awaiter(this, arguments, void 0, function* ({ allowedSighash, unsignedPsbtBase64, signature, }) {
|
|
231
|
+
var _b, _c, _d;
|
|
232
|
+
yield this.createOrRestoreSession();
|
|
233
|
+
const accountAddress = this.getAccount();
|
|
234
|
+
if (!accountAddress) {
|
|
235
|
+
throw new Error('Failed to get address');
|
|
236
|
+
}
|
|
237
|
+
const psbt = Psbt.fromBase64(unsignedPsbtBase64);
|
|
238
|
+
const turnkeyClient = yield this.getTurnkeyAccount();
|
|
239
|
+
if (!turnkeyClient) {
|
|
240
|
+
throw new Error('Failed to get turnkey client');
|
|
241
|
+
}
|
|
242
|
+
const signer = new TurnkeySigner({
|
|
243
|
+
address: accountAddress,
|
|
244
|
+
client: turnkeyClient,
|
|
245
|
+
organizationId: (_b = this.walletProperties) === null || _b === void 0 ? void 0 : _b.turnkeySubOrganizationId,
|
|
246
|
+
publicKey: address.fromBech32(accountAddress).data,
|
|
247
|
+
});
|
|
248
|
+
if (signature) {
|
|
249
|
+
for (let i = 0; i < signature.length; i++) {
|
|
250
|
+
const sigIdx = (_d = (_c = signature[i]) === null || _c === void 0 ? void 0 : _c.signingIndexes) === null || _d === void 0 ? void 0 : _d[0];
|
|
251
|
+
if (sigIdx !== undefined) {
|
|
252
|
+
yield psbt.signInputAsync(sigIdx, signer, allowedSighash);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
yield psbt.signAllInputsAsync(signer, allowedSighash);
|
|
258
|
+
}
|
|
259
|
+
psbt.finalizeAllInputs();
|
|
260
|
+
return {
|
|
261
|
+
signedPsbt: psbt.toBase64(),
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
signPsbts(requests) {
|
|
266
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
267
|
+
const signedPsbts = yield Promise.all(requests.map((request) => this.signPsbt(request)));
|
|
268
|
+
return signedPsbts.map((response) => response.signedPsbt || '');
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
272
|
+
createUiTransaction(from) {
|
|
273
|
+
throw new Error('Method not implemented.');
|
|
274
|
+
}
|
|
275
|
+
canConnectWithHardwareWallet() {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
isInstalledOnBrowser() {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
apiGetBalance(address) {
|
|
282
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
283
|
+
const API_URL = getMempoolApiUrl(address);
|
|
284
|
+
const response = yield fetch(`${API_URL}/address/${address}`);
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
// if the request fails due to rate limits, return cached value
|
|
287
|
+
if (response.status === 429) {
|
|
288
|
+
return '0';
|
|
289
|
+
}
|
|
290
|
+
// new accounts not yet indexed will return a 404
|
|
291
|
+
if (response.status === 404) {
|
|
292
|
+
return '0';
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const addressInfo = yield response.json();
|
|
297
|
+
if (!(addressInfo === null || addressInfo === void 0 ? void 0 : addressInfo.chain_stats) || !(addressInfo === null || addressInfo === void 0 ? void 0 : addressInfo.mempool_stats)) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
const confirmedBalanceInSats = Number(addressInfo.chain_stats.funded_txo_sum) -
|
|
301
|
+
Number(addressInfo.chain_stats.spent_txo_sum);
|
|
302
|
+
const unconfirmedBalanceInSats = Number(addressInfo.mempool_stats.funded_txo_sum) -
|
|
303
|
+
Number(addressInfo.mempool_stats.spent_txo_sum);
|
|
304
|
+
const balance = satoshisToBtc(confirmedBalanceInSats + unconfirmedBalanceInSats);
|
|
305
|
+
return balance.toString();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
getBalance(address) {
|
|
309
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
310
|
+
return this.apiGetBalance(address);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
getAccount() {
|
|
314
|
+
return this.turnkeyAddress;
|
|
315
|
+
}
|
|
316
|
+
signMessage(messageToSign, options) {
|
|
317
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
318
|
+
var _a, _b;
|
|
319
|
+
yield this.createOrRestoreSession();
|
|
320
|
+
const turnkeyAccount = yield this.getTurnkeyAccount();
|
|
321
|
+
const accountAddress = this.getAccount();
|
|
322
|
+
if (!turnkeyAccount || !accountAddress) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const msgBuffer = new TextEncoder().encode(messageToSign);
|
|
326
|
+
const hashBuffer = yield crypto.subtle.digest('SHA-256', msgBuffer);
|
|
327
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
328
|
+
const payload = hashArray
|
|
329
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
330
|
+
.join('');
|
|
331
|
+
const response = yield (turnkeyAccount === null || turnkeyAccount === void 0 ? void 0 : turnkeyAccount.signRawPayload({
|
|
332
|
+
organizationId: (_a = this.walletProperties) === null || _a === void 0 ? void 0 : _a.turnkeySubOrganizationId,
|
|
333
|
+
parameters: {
|
|
334
|
+
encoding: 'PAYLOAD_ENCODING_HEXADECIMAL',
|
|
335
|
+
hashFunction: 'HASH_FUNCTION_SHA256',
|
|
336
|
+
payload,
|
|
337
|
+
signWith: (_b = options === null || options === void 0 ? void 0 : options.address) !== null && _b !== void 0 ? _b : accountAddress, // default to ordinals address
|
|
338
|
+
},
|
|
339
|
+
timestampMs: new Date().getTime().toString(),
|
|
340
|
+
type: 'ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2',
|
|
341
|
+
}));
|
|
342
|
+
const result = response.activity.result.signRawPayloadResult;
|
|
343
|
+
if (!result) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const { r, s } = result;
|
|
347
|
+
return Buffer.from(r + s).toString('hex');
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export { TurnkeyBitcoinWalletConnector };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
6
|
+
var _tslib = require('../../../_virtual/_tslib.cjs');
|
|
7
|
+
var embeddedWallet = require('@dynamic-labs/embedded-wallet');
|
|
8
|
+
|
|
9
|
+
// These constants helps us estimate an upper bound for fees.
|
|
10
|
+
// Source: https://x.com/murchandamus/status/1262062602298916865
|
|
11
|
+
const INPUT_BYTE_SIZE_UPPER_BOUND = 90;
|
|
12
|
+
const OUTPUT_BYTE_SIZE_UPPER_BOUND = 45;
|
|
13
|
+
/**
|
|
14
|
+
* Estimate fees for a transaction with N inputs and M output. This function provides an UPPER BOUND.
|
|
15
|
+
* The "proper" approach to estimate fees tighter would be:
|
|
16
|
+
* - make a transaction "for real", sign it
|
|
17
|
+
* - finalize with `psbt.finalizeAllInputs()`
|
|
18
|
+
* - get the transaction byte size with `psbt.extractTransaction().toHex()`;
|
|
19
|
+
* - then modify the transaction (adjust fees/change) and re-sign
|
|
20
|
+
* Given this is a test-only script we're comfortable over-paying a bit. Miners, rejoice!
|
|
21
|
+
*/
|
|
22
|
+
const estimateFees = (feeParams) => _tslib.__awaiter(void 0, void 0, void 0, function* () {
|
|
23
|
+
const { numInputs, numOutputs, network } = feeParams;
|
|
24
|
+
const feePerByteResponse = yield getFeePerByte(network);
|
|
25
|
+
const feePerByte = feePerByteResponse.hourFee;
|
|
26
|
+
const minRelayFee = 111;
|
|
27
|
+
const estimatedFee = feePerByte *
|
|
28
|
+
(INPUT_BYTE_SIZE_UPPER_BOUND * numInputs +
|
|
29
|
+
OUTPUT_BYTE_SIZE_UPPER_BOUND * numOutputs) +
|
|
30
|
+
minRelayFee;
|
|
31
|
+
return estimatedFee;
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Fetch the right fee-per-byte for the passed in network.
|
|
35
|
+
*/
|
|
36
|
+
const getFeePerByte = (network) => _tslib.__awaiter(void 0, void 0, void 0, function* () {
|
|
37
|
+
try {
|
|
38
|
+
const url = network.bech32 === 'bc'
|
|
39
|
+
? 'https://mempool.space/api/v1/fees/recommended'
|
|
40
|
+
: 'https://mempool.space/testnet/api/v1/fees/recommended';
|
|
41
|
+
const response = yield fetch(url);
|
|
42
|
+
return yield response.json();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
embeddedWallet.logger.error('Error fetching fee estimate:', error);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
exports.estimateFees = estimateFees;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Network } from 'bitcoinjs-lib';
|
|
2
|
+
/**
|
|
3
|
+
* Estimate fees for a transaction with N inputs and M output. This function provides an UPPER BOUND.
|
|
4
|
+
* The "proper" approach to estimate fees tighter would be:
|
|
5
|
+
* - make a transaction "for real", sign it
|
|
6
|
+
* - finalize with `psbt.finalizeAllInputs()`
|
|
7
|
+
* - get the transaction byte size with `psbt.extractTransaction().toHex()`;
|
|
8
|
+
* - then modify the transaction (adjust fees/change) and re-sign
|
|
9
|
+
* Given this is a test-only script we're comfortable over-paying a bit. Miners, rejoice!
|
|
10
|
+
*/
|
|
11
|
+
export declare const estimateFees: (feeParams: {
|
|
12
|
+
numInputs: number;
|
|
13
|
+
numOutputs: number;
|
|
14
|
+
network: Network;
|
|
15
|
+
}) => Promise<number>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { __awaiter } from '../../../_virtual/_tslib.js';
|
|
3
|
+
import { logger } from '@dynamic-labs/embedded-wallet';
|
|
4
|
+
|
|
5
|
+
// These constants helps us estimate an upper bound for fees.
|
|
6
|
+
// Source: https://x.com/murchandamus/status/1262062602298916865
|
|
7
|
+
const INPUT_BYTE_SIZE_UPPER_BOUND = 90;
|
|
8
|
+
const OUTPUT_BYTE_SIZE_UPPER_BOUND = 45;
|
|
9
|
+
/**
|
|
10
|
+
* Estimate fees for a transaction with N inputs and M output. This function provides an UPPER BOUND.
|
|
11
|
+
* The "proper" approach to estimate fees tighter would be:
|
|
12
|
+
* - make a transaction "for real", sign it
|
|
13
|
+
* - finalize with `psbt.finalizeAllInputs()`
|
|
14
|
+
* - get the transaction byte size with `psbt.extractTransaction().toHex()`;
|
|
15
|
+
* - then modify the transaction (adjust fees/change) and re-sign
|
|
16
|
+
* Given this is a test-only script we're comfortable over-paying a bit. Miners, rejoice!
|
|
17
|
+
*/
|
|
18
|
+
const estimateFees = (feeParams) => __awaiter(void 0, void 0, void 0, function* () {
|
|
19
|
+
const { numInputs, numOutputs, network } = feeParams;
|
|
20
|
+
const feePerByteResponse = yield getFeePerByte(network);
|
|
21
|
+
const feePerByte = feePerByteResponse.hourFee;
|
|
22
|
+
const minRelayFee = 111;
|
|
23
|
+
const estimatedFee = feePerByte *
|
|
24
|
+
(INPUT_BYTE_SIZE_UPPER_BOUND * numInputs +
|
|
25
|
+
OUTPUT_BYTE_SIZE_UPPER_BOUND * numOutputs) +
|
|
26
|
+
minRelayFee;
|
|
27
|
+
return estimatedFee;
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Fetch the right fee-per-byte for the passed in network.
|
|
31
|
+
*/
|
|
32
|
+
const getFeePerByte = (network) => __awaiter(void 0, void 0, void 0, function* () {
|
|
33
|
+
try {
|
|
34
|
+
const url = network.bech32 === 'bc'
|
|
35
|
+
? 'https://mempool.space/api/v1/fees/recommended'
|
|
36
|
+
: 'https://mempool.space/testnet/api/v1/fees/recommended';
|
|
37
|
+
const response = yield fetch(url);
|
|
38
|
+
return yield response.json();
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.error('Error fetching fee estimate:', error);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export { estimateFees };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TurnkeyBitcoinWalletConnector } from './TurnkeyBitcoinWalletConnector';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
6
|
+
var _tslib = require('../../../_virtual/_tslib.cjs');
|
|
7
|
+
|
|
8
|
+
class TurnkeySigner {
|
|
9
|
+
/**
|
|
10
|
+
* @param client The turnkey SDK client
|
|
11
|
+
* @param address The Turnkey-derived address in bech32 format
|
|
12
|
+
* @param publicKey public key buffer. To sign P2TR outputs it needs to be
|
|
13
|
+
* the decoded address (`bitcoin.address.fromBech32(address).data`).
|
|
14
|
+
* For P2WPKH it should be the key pair's underlying public key buffer, uncompressed.
|
|
15
|
+
*/
|
|
16
|
+
constructor({ client, address, publicKey, organizationId, }) {
|
|
17
|
+
this.client = client;
|
|
18
|
+
this.address = address;
|
|
19
|
+
this.publicKey = publicKey;
|
|
20
|
+
this.organizationId = organizationId;
|
|
21
|
+
}
|
|
22
|
+
sign(hash) {
|
|
23
|
+
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
const response = yield this.client.signRawPayload({
|
|
25
|
+
organizationId: this.organizationId,
|
|
26
|
+
parameters: {
|
|
27
|
+
encoding: 'PAYLOAD_ENCODING_HEXADECIMAL',
|
|
28
|
+
hashFunction: 'HASH_FUNCTION_NO_OP',
|
|
29
|
+
payload: hash.toString('hex'),
|
|
30
|
+
signWith: this.address,
|
|
31
|
+
},
|
|
32
|
+
timestampMs: new Date().getTime().toString(),
|
|
33
|
+
type: 'ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2',
|
|
34
|
+
});
|
|
35
|
+
const result = response.activity.result.signRawPayloadResult;
|
|
36
|
+
if (!result) {
|
|
37
|
+
throw new Error('Failed to sign raw payload');
|
|
38
|
+
}
|
|
39
|
+
const { r, s } = result;
|
|
40
|
+
return Buffer.from(r + s, 'hex');
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
signSchnorr(hash) {
|
|
44
|
+
return _tslib.__awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
// Use the same signing logic for Schnorr
|
|
46
|
+
return this.sign(hash);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
exports.TurnkeySigner = TurnkeySigner;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { TurnkeyClient } from '@turnkey/http';
|
|
3
|
+
export declare class TurnkeySigner {
|
|
4
|
+
client: TurnkeyClient;
|
|
5
|
+
publicKey: Buffer;
|
|
6
|
+
address: string;
|
|
7
|
+
organizationId: string;
|
|
8
|
+
/**
|
|
9
|
+
* @param client The turnkey SDK client
|
|
10
|
+
* @param address The Turnkey-derived address in bech32 format
|
|
11
|
+
* @param publicKey public key buffer. To sign P2TR outputs it needs to be
|
|
12
|
+
* the decoded address (`bitcoin.address.fromBech32(address).data`).
|
|
13
|
+
* For P2WPKH it should be the key pair's underlying public key buffer, uncompressed.
|
|
14
|
+
*/
|
|
15
|
+
constructor({ client, address, publicKey, organizationId, }: {
|
|
16
|
+
client: TurnkeyClient;
|
|
17
|
+
address: string;
|
|
18
|
+
publicKey: Buffer;
|
|
19
|
+
organizationId: string;
|
|
20
|
+
});
|
|
21
|
+
sign(hash: Buffer): Promise<Buffer>;
|
|
22
|
+
signSchnorr(hash: Buffer): Promise<Buffer>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { __awaiter } from '../../../_virtual/_tslib.js';
|
|
3
|
+
|
|
4
|
+
class TurnkeySigner {
|
|
5
|
+
/**
|
|
6
|
+
* @param client The turnkey SDK client
|
|
7
|
+
* @param address The Turnkey-derived address in bech32 format
|
|
8
|
+
* @param publicKey public key buffer. To sign P2TR outputs it needs to be
|
|
9
|
+
* the decoded address (`bitcoin.address.fromBech32(address).data`).
|
|
10
|
+
* For P2WPKH it should be the key pair's underlying public key buffer, uncompressed.
|
|
11
|
+
*/
|
|
12
|
+
constructor({ client, address, publicKey, organizationId, }) {
|
|
13
|
+
this.client = client;
|
|
14
|
+
this.address = address;
|
|
15
|
+
this.publicKey = publicKey;
|
|
16
|
+
this.organizationId = organizationId;
|
|
17
|
+
}
|
|
18
|
+
sign(hash) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
const response = yield this.client.signRawPayload({
|
|
21
|
+
organizationId: this.organizationId,
|
|
22
|
+
parameters: {
|
|
23
|
+
encoding: 'PAYLOAD_ENCODING_HEXADECIMAL',
|
|
24
|
+
hashFunction: 'HASH_FUNCTION_NO_OP',
|
|
25
|
+
payload: hash.toString('hex'),
|
|
26
|
+
signWith: this.address,
|
|
27
|
+
},
|
|
28
|
+
timestampMs: new Date().getTime().toString(),
|
|
29
|
+
type: 'ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2',
|
|
30
|
+
});
|
|
31
|
+
const result = response.activity.result.signRawPayloadResult;
|
|
32
|
+
if (!result) {
|
|
33
|
+
throw new Error('Failed to sign raw payload');
|
|
34
|
+
}
|
|
35
|
+
const { r, s } = result;
|
|
36
|
+
return Buffer.from(r + s, 'hex');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
signSchnorr(hash) {
|
|
40
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
// Use the same signing logic for Schnorr
|
|
42
|
+
return this.sign(hash);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { TurnkeySigner };
|