@exodus/bitcoin-api 2.3.2 → 2.3.4
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/package.json +4 -2
- package/src/btc-like-address.js +1 -1
- package/src/btc-like-keys.js +1 -1
- package/src/move-funds.js +63 -47
- package/src/tx-send/index.js +35 -5
- package/src/tx-sign/default-create-tx.js +21 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.4",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@exodus/asset-lib": "^3.7.2",
|
|
22
|
+
"@exodus/basic-utils": "^2.0.1",
|
|
22
23
|
"@exodus/bip44-constants": "^195.0.0",
|
|
23
24
|
"@exodus/bitcoinjs-lib": "6.0.2-beta.5",
|
|
24
25
|
"@exodus/models": "^8.10.4",
|
|
@@ -35,10 +36,11 @@
|
|
|
35
36
|
"url-join": "4.0.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
39
|
+
"@exodus/asset-lib": "^3.7.1",
|
|
38
40
|
"@exodus/bcash-meta": "^1.0.0",
|
|
39
41
|
"@exodus/bip-schnorr": "0.6.6-fork-1",
|
|
40
42
|
"@exodus/bitcoin-meta": "^1.0.1",
|
|
41
43
|
"@noble/secp256k1": "~1.5.3"
|
|
42
44
|
},
|
|
43
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "ae212da85b29d90a3656bcfa034608950f883df1"
|
|
44
46
|
}
|
package/src/btc-like-address.js
CHANGED
package/src/btc-like-keys.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import bs58check from 'bs58check'
|
|
2
2
|
import wif from 'wif'
|
|
3
|
-
import bech32 from 'bech32'
|
|
3
|
+
import * as bech32 from 'bech32'
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
5
|
import { identity, pickBy } from 'lodash'
|
|
6
6
|
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
|
package/src/move-funds.js
CHANGED
|
@@ -41,13 +41,20 @@ export const moveFundsFactory = ({
|
|
|
41
41
|
assert(address, 'address is required')
|
|
42
42
|
assert(signTx, 'signTx is required')
|
|
43
43
|
assert(getAddressesFromPrivateKey, 'getAddressesFromPrivateKey is required')
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
|
|
45
|
+
async function prepareSendFundsTx({
|
|
46
|
+
assetName,
|
|
47
|
+
walletAccount,
|
|
48
|
+
input,
|
|
49
|
+
toAddress,
|
|
50
|
+
assetClientInterface,
|
|
51
|
+
MoveFundsError,
|
|
52
|
+
}) {
|
|
53
|
+
assert(asset.name === assetName, `expected asset ${asset.name} but got assetName ${assetName}`)
|
|
54
|
+
assert(walletAccount, 'walletAccount is required')
|
|
47
55
|
assert(toAddress, 'toAddress is required')
|
|
48
56
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
49
|
-
assert(
|
|
50
|
-
assert(asset.name === assetName, `expected asset ${asset.name} but got assetName ${assetName}`)
|
|
57
|
+
assert(MoveFundsError, 'MoveFundsError is required') // should we move MoveFundsError to asset libs?
|
|
51
58
|
|
|
52
59
|
const formatProps = {
|
|
53
60
|
asset,
|
|
@@ -69,71 +76,78 @@ export const moveFundsFactory = ({
|
|
|
69
76
|
multiAddressMode: true,
|
|
70
77
|
})
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const findFromAddress = async () => {
|
|
80
|
+
for (const currentAddress of addresses) {
|
|
81
|
+
const selfSend = receiveAddresses.some(
|
|
82
|
+
(receiveAddress) => String(receiveAddress) === String(currentAddress)
|
|
83
|
+
)
|
|
84
|
+
if (selfSend) {
|
|
85
|
+
throw new MoveFundsError('private-key-own-key', formatProps)
|
|
86
|
+
}
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
const utxos = await getUtxos({ asset, address: currentAddress })
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
if (!utxos.value.isZero) {
|
|
91
|
+
return { fromAddress: currentAddress, utxos }
|
|
92
|
+
}
|
|
86
93
|
}
|
|
94
|
+
throw new MoveFundsError('balance-zero', {
|
|
95
|
+
...formatProps,
|
|
96
|
+
fromAddress: `${addresses.slice(0, -1).join(', ')}, or ${addresses[addresses.length - 1]}`,
|
|
97
|
+
})
|
|
87
98
|
}
|
|
88
|
-
if (!found) {
|
|
89
|
-
formatProps.fromAddress = `${addresses.slice(0, -1).join(', ')}, or ${
|
|
90
|
-
addresses[addresses.length - 1]
|
|
91
|
-
}`
|
|
92
|
-
throw new MoveFundsError('balance-zero', formatProps)
|
|
93
|
-
}
|
|
94
|
-
const fromAddress = address
|
|
95
|
-
formatProps.fromAddress = fromAddress
|
|
96
99
|
|
|
100
|
+
const { fromAddress, utxos } = await findFromAddress()
|
|
101
|
+
formatProps.fromAddress = fromAddress
|
|
97
102
|
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
98
|
-
const fee = getFee({ asset, feeData, utxos, compressed })
|
|
103
|
+
const { fee, sizeKB } = getFee({ asset, feeData, utxos, compressed })
|
|
99
104
|
|
|
100
105
|
let amount = utxos.value.sub(fee)
|
|
101
106
|
if (amount.isNegative) {
|
|
102
107
|
throw new MoveFundsError('balance-negative', formatProps)
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
return { fromAddress, toAddress, amount, fee, utxos, privateKey }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const sendFunds = async (
|
|
109
|
-
assetName,
|
|
110
|
-
{ fromAddress, toAddress, amount, fee, utxos, privateKey }
|
|
111
|
-
) => {
|
|
112
|
-
assert(fromAddress, 'fromAddress is required')
|
|
113
|
-
assert(toAddress, 'toAddress is required')
|
|
114
|
-
assert(fee, 'fee is required')
|
|
115
|
-
assert(utxos, 'utxos is required')
|
|
116
|
-
assert(privateKey, 'privateKey is required')
|
|
117
|
-
const selected = utxos
|
|
118
|
-
const privateKeysAddressMap = {
|
|
119
|
-
[fromAddress]: privateKey,
|
|
120
|
-
}
|
|
121
110
|
const unsignedTx = {
|
|
122
111
|
txData: {
|
|
123
|
-
inputs: createInputs(assetName,
|
|
112
|
+
inputs: createInputs(assetName, utxos.toArray()),
|
|
124
113
|
outputs: [createOutput(assetName, toAddress, amount)],
|
|
125
114
|
},
|
|
126
115
|
txMeta: {
|
|
127
|
-
addressPathsMap:
|
|
116
|
+
addressPathsMap: utxos.getAddressPathsMap(),
|
|
128
117
|
},
|
|
129
118
|
}
|
|
130
119
|
const nonWitnessTxs = await getNonWitnessTxs(
|
|
131
120
|
{ name: assetName, address }, // pretty ugly hack!
|
|
132
|
-
|
|
121
|
+
utxos,
|
|
133
122
|
insightClient
|
|
134
123
|
)
|
|
135
124
|
Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
|
|
136
125
|
|
|
126
|
+
return { fromAddress, toAddress, amount, fee, utxos, unsignedTx, sizeKB, privateKey }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sendFunds = async ({
|
|
130
|
+
assetName,
|
|
131
|
+
fromAddress,
|
|
132
|
+
toAddress,
|
|
133
|
+
amount,
|
|
134
|
+
fee,
|
|
135
|
+
unsignedTx,
|
|
136
|
+
privateKey,
|
|
137
|
+
}) => {
|
|
138
|
+
// the response from prepareSendFundsTx
|
|
139
|
+
assert(assetName, 'assetName is required')
|
|
140
|
+
assert(fromAddress, 'fromAddress is required')
|
|
141
|
+
assert(toAddress, 'toAddress is required')
|
|
142
|
+
assert(amount, 'amount is required')
|
|
143
|
+
assert(fee, 'fee is required')
|
|
144
|
+
assert(unsignedTx, 'unsignedTx is required')
|
|
145
|
+
assert(privateKey, 'privateKey is required')
|
|
146
|
+
|
|
147
|
+
const privateKeysAddressMap = {
|
|
148
|
+
[fromAddress]: privateKey,
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
const { rawTx, txId } = await signTx({ unsignedTx, privateKeysAddressMap })
|
|
138
152
|
|
|
139
153
|
await insightClient.broadcastTx(rawTx.toString('hex'))
|
|
@@ -158,7 +172,9 @@ export const moveFundsFactory = ({
|
|
|
158
172
|
function getFee({ asset, feeData, utxos, compressed }) {
|
|
159
173
|
const { feePerKB } = feeData
|
|
160
174
|
const feeEstimator = getFeeEstimator(asset, feePerKB, { compressed })
|
|
161
|
-
|
|
175
|
+
const fee = feeEstimator({ inputs: utxos, outputs: [null] })
|
|
176
|
+
const sizeKB = fee.toDefaultNumber() / feePerKB
|
|
177
|
+
return { fee, sizeKB }
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
function isValidPrivateKey(privateKey) {
|
|
@@ -171,7 +187,7 @@ export const moveFundsFactory = ({
|
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
return {
|
|
174
|
-
|
|
190
|
+
prepareSendFundsTx,
|
|
175
191
|
sendFunds,
|
|
176
192
|
}
|
|
177
193
|
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
17
17
|
import { getUsableUtxos, getUtxos } from '../utxos-utils'
|
|
18
18
|
|
|
19
|
+
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
|
|
20
|
+
|
|
19
21
|
const ASSETS_SUPPORTED_BIP_174 = [
|
|
20
22
|
'bitcoin',
|
|
21
23
|
'bitcoinregtest',
|
|
@@ -79,12 +81,40 @@ export async function getNonWitnessTxs(asset, utxos, insightClient) {
|
|
|
79
81
|
return rawTxs
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
export const getSizeAndChangeScriptFactory = ({ bitcoinJsLib = defaultBitcoinjsLib } = {}) => ({
|
|
85
|
+
assetName,
|
|
86
|
+
tx,
|
|
87
|
+
rawTx,
|
|
88
|
+
changeUtxoIndex,
|
|
89
|
+
txId,
|
|
90
|
+
}) => {
|
|
91
|
+
assert(assetName, 'assetName is required')
|
|
92
|
+
assert(rawTx, 'rawTx is required')
|
|
93
|
+
assert(typeof changeUtxoIndex === 'number', 'changeUtxoIndex must be a number')
|
|
94
|
+
if (tx) {
|
|
95
|
+
return { script: tx.outs?.[changeUtxoIndex]?.script.toString('hex'), size: tx.virtualSize }
|
|
96
|
+
}
|
|
97
|
+
// Trezor doesn't return tx!! we need to reparse it!
|
|
98
|
+
const parsedTx = bitcoinJsLib.Transaction.fromBuffer(Buffer.from(rawTx, 'hex'))
|
|
99
|
+
try {
|
|
100
|
+
return {
|
|
101
|
+
script: parsedTx.outs?.[changeUtxoIndex]?.script.toString('hex'),
|
|
102
|
+
size: parsedTx.virtualSize(),
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.warn(
|
|
106
|
+
`tx-send warning: ${assetName} cannot extract script and size from tx ${txId}. ${e}`
|
|
107
|
+
)
|
|
108
|
+
return {}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
// not ported from Exodus; but this demos signing / broadcasting
|
|
83
113
|
// NOTE: this will be ripped out in the coming weeks
|
|
84
114
|
|
|
85
115
|
export const createAndBroadcastTXFactory = ({
|
|
86
116
|
getFeeEstimator,
|
|
87
|
-
|
|
117
|
+
getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
|
|
88
118
|
allowUnconfirmedRbfEnabledUtxos,
|
|
89
119
|
}) => async ({ asset, walletAccount, address, amount, options }, { assetClientInterface }) => {
|
|
90
120
|
const {
|
|
@@ -308,6 +338,8 @@ export const createAndBroadcastTXFactory = ({
|
|
|
308
338
|
}
|
|
309
339
|
}
|
|
310
340
|
|
|
341
|
+
const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
|
|
342
|
+
|
|
311
343
|
let remainingUtxos = usableUtxos.difference(selectedUtxos)
|
|
312
344
|
if (changeUtxoIndex !== -1) {
|
|
313
345
|
const address = Address.create(ourAddress.address, ourAddress.meta)
|
|
@@ -315,7 +347,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
315
347
|
txId,
|
|
316
348
|
address,
|
|
317
349
|
vout: changeUtxoIndex,
|
|
318
|
-
script
|
|
350
|
+
script,
|
|
319
351
|
value: change,
|
|
320
352
|
confirmations: 0,
|
|
321
353
|
rbfEnabled,
|
|
@@ -364,9 +396,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
364
396
|
data: {
|
|
365
397
|
sent: selfSend ? [] : receivers,
|
|
366
398
|
rbfEnabled,
|
|
367
|
-
feePerKB:
|
|
368
|
-
? fee.div(tx.virtualSize / 1000).toBaseNumber()
|
|
369
|
-
: undefined,
|
|
399
|
+
feePerKB: size ? fee.div(size / 1000).toBaseNumber() : undefined,
|
|
370
400
|
changeAddress: changeOutput ? ourAddress : undefined,
|
|
371
401
|
blockHeight,
|
|
372
402
|
blocksSeen: 0,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
import lodash from 'lodash'
|
|
3
3
|
import ECPairFactory from 'ecpair'
|
|
4
|
-
import { Psbt, Transaction } from '@exodus/bitcoinjs-lib'
|
|
4
|
+
import { payments, Psbt, Transaction } from '@exodus/bitcoinjs-lib'
|
|
5
|
+
import { getOwnProperty } from '@exodus/basic-utils'
|
|
6
|
+
|
|
7
|
+
import secp256k1 from 'secp256k1'
|
|
5
8
|
|
|
6
9
|
import { toAsyncSigner, tweakSigner } from './taproot'
|
|
7
10
|
|
|
@@ -45,10 +48,11 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
|
|
|
45
48
|
ECPair = ECPair || ECPairFactory(ecc)
|
|
46
49
|
|
|
47
50
|
const getKeyAndPurpose = lodash.memoize((address) => {
|
|
48
|
-
// TODO: Consider using privateKeysAddressMap for other assets
|
|
49
51
|
const purpose = resolvePurpose(address)
|
|
50
52
|
if (privateKeysAddressMap) {
|
|
51
|
-
const
|
|
53
|
+
const privateKey = getOwnProperty(privateKeysAddressMap, address, 'string')
|
|
54
|
+
assert(privateKey, `there is no private key for address ${address}`)
|
|
55
|
+
const key = ECPair.fromWIF(privateKey, networkInfo)
|
|
52
56
|
return { key, purpose }
|
|
53
57
|
}
|
|
54
58
|
const path = addressPathsMap[address]
|
|
@@ -96,6 +100,20 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
|
|
|
96
100
|
for (let index = 0; index < inputs.length; index++) {
|
|
97
101
|
const { address } = inputs[index]
|
|
98
102
|
const { key, purpose } = getKeyAndPurpose(address)
|
|
103
|
+
|
|
104
|
+
if (purpose === 49) {
|
|
105
|
+
// If spending from a P2SH address, we assume the address is P2SH wrapping
|
|
106
|
+
// P2WPKH. Exodus doesn't use P2SH addresses so we should only ever be
|
|
107
|
+
// signing a P2SH input if we are importing a private key
|
|
108
|
+
// BIP143: As a default policy, only compressed public keys are accepted in P2WPKH and P2WSH
|
|
109
|
+
const publicKey = secp256k1.publicKeyCreate(key.privateKey, true)
|
|
110
|
+
const p2wpkh = payments.p2wpkh({ pubkey: publicKey })
|
|
111
|
+
const p2sh = payments.p2sh({ redeem: p2wpkh })
|
|
112
|
+
psbt.updateInput(index, {
|
|
113
|
+
redeemScript: p2sh.redeem.output,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
if (ecc.signSchnorrAsync) {
|
|
100
118
|
// desktop / BE / mobile with bip-schnorr signing
|
|
101
119
|
const isTaprootAddress = purpose === 86
|