@exodus/bitcoin-api 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -4
- package/src/bitcoinjs-lib/ecc/index.js +8 -2
- package/src/fee/get-fee-resolver.js +15 -2
- package/src/fee/utxo-selector.js +6 -1
- package/src/index.js +1 -0
- package/src/multisig-address.js +81 -0
- package/src/tx-send/index.js +9 -17
- package/src/tx-sign/create-sign-with-wallet.js +5 -3
- package/src/tx-sign/default-create-tx.js +1 -1
- package/src/tx-sign/default-prepare-for-signing.js +13 -5
- package/src/tx-sign/taproot.js +4 -4
- package/src/utxos-utils.js +4 -0
- package/src/bitcoinjs-lib/ecc/common.js +0 -81
- package/src/bitcoinjs-lib/ecc/desktop.js +0 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -23,14 +23,13 @@
|
|
|
23
23
|
"@exodus/bip322-js": "^1.1.0-exodus.4",
|
|
24
24
|
"@exodus/bip44-constants": "^195.0.0",
|
|
25
25
|
"@exodus/bitcoin-lib": "2.3.0",
|
|
26
|
+
"@exodus/bitcoinerlab-secp256k1": "^1.0.5-exodus.1",
|
|
26
27
|
"@exodus/bitcoinjs-lib": "^6.1.5-exodus.1",
|
|
27
28
|
"@exodus/currency": "^2.3.2",
|
|
28
29
|
"@exodus/fetch": "^1.3.0",
|
|
29
30
|
"@exodus/models": "^11.0.0",
|
|
30
|
-
"@exodus/secp256k1": "4.0.2-exodus.0",
|
|
31
31
|
"@exodus/simple-retry": "0.0.6",
|
|
32
32
|
"@exodus/timer": "^1.0.0",
|
|
33
|
-
"@noble/secp256k1": "~1.7.1",
|
|
34
33
|
"bech32": "^1.1.3",
|
|
35
34
|
"bip32-path": "^0.4.2",
|
|
36
35
|
"bn.js": "4.12.0",
|
|
@@ -58,5 +57,5 @@
|
|
|
58
57
|
"jest-when": "^3.5.1",
|
|
59
58
|
"safe-buffer": "^5.2.1"
|
|
60
59
|
},
|
|
61
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "9d296fc0875749545bc95b2a976e93739d63553b"
|
|
62
61
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { initEccLib } from '@exodus/bitcoinjs-lib'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import bitcoinerlab from '@exodus/bitcoinerlab-secp256k1'
|
|
4
|
+
|
|
5
|
+
export const ecc = bitcoinerlab
|
|
6
|
+
|
|
7
|
+
initEccLib(ecc)
|
|
8
|
+
|
|
9
|
+
export const eccFactory = () => ecc
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
import { getUtxosData } from './utxo-selector'
|
|
3
3
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getInscriptionIds,
|
|
6
|
+
getOrdinalsUtxos,
|
|
7
|
+
getTransferOrdinalsUtxos,
|
|
8
|
+
getUsableUtxos,
|
|
9
|
+
getUtxos,
|
|
10
|
+
} from '../utxos-utils'
|
|
5
11
|
import { canBumpTx } from './can-bump-tx'
|
|
6
12
|
import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data'
|
|
7
13
|
|
|
@@ -42,7 +48,7 @@ export class GetFeeResolver {
|
|
|
42
48
|
assert(!nft, 'nft must not be provided when brc20 is provided!!!')
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
const inscriptionIds =
|
|
51
|
+
const inscriptionIds = getInscriptionIds({ nft, brc20 })
|
|
46
52
|
|
|
47
53
|
const { resolvedFee, extraFee } = this.#getUtxosData({
|
|
48
54
|
asset,
|
|
@@ -100,6 +106,12 @@ export class GetFeeResolver {
|
|
|
100
106
|
const utxos = getUtxos({ accountState, asset })
|
|
101
107
|
const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
|
|
102
108
|
|
|
109
|
+
const ordinalsUtxos = getOrdinalsUtxos({ accountState, asset })
|
|
110
|
+
|
|
111
|
+
const transferOrdinalsUtxos = inscriptionIds
|
|
112
|
+
? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos })
|
|
113
|
+
: undefined
|
|
114
|
+
|
|
103
115
|
const usableUtxos = getUsableUtxos({
|
|
104
116
|
asset,
|
|
105
117
|
utxos,
|
|
@@ -117,6 +129,7 @@ export class GetFeeResolver {
|
|
|
117
129
|
amount,
|
|
118
130
|
feeRate: feePerKB,
|
|
119
131
|
receiveAddress,
|
|
132
|
+
transferOrdinalsUtxos,
|
|
120
133
|
inscriptionIds,
|
|
121
134
|
isSendAll,
|
|
122
135
|
getFeeEstimator: this.#getFeeEstimator,
|
package/src/fee/utxo-selector.js
CHANGED
|
@@ -34,6 +34,7 @@ export const selectUtxos = ({
|
|
|
34
34
|
allowUnconfirmedRbfEnabledUtxos,
|
|
35
35
|
unconfirmedTxAncestor,
|
|
36
36
|
inscriptionIds, // for each inscription transfer, we need to calculate one more input and one more output
|
|
37
|
+
transferOrdinalsUtxos, // to calculate the size of the input
|
|
37
38
|
}) => {
|
|
38
39
|
const resolvedReceiveAddresses = getBestReceiveAddresses({
|
|
39
40
|
asset,
|
|
@@ -197,7 +198,9 @@ export const selectUtxos = ({
|
|
|
197
198
|
selectedUtxosValue = selectedUtxosValue.add(newUtxo.value)
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
let selectedUtxos = UtxoCollection.
|
|
201
|
+
let selectedUtxos = (transferOrdinalsUtxos || UtxoCollection.createEmpty({ currency })).union(
|
|
202
|
+
UtxoCollection.fromArray(selectedUtxosArray, { currency })
|
|
203
|
+
) // extremelly important, orden must be kept!!! ordinals utxos go first!!!
|
|
201
204
|
|
|
202
205
|
// start figuring out fees
|
|
203
206
|
const outputs =
|
|
@@ -240,6 +243,7 @@ export const getUtxosData = ({
|
|
|
240
243
|
mustSpendUtxos,
|
|
241
244
|
allowUnconfirmedRbfEnabledUtxos,
|
|
242
245
|
inscriptionIds,
|
|
246
|
+
transferOrdinalsUtxos,
|
|
243
247
|
unconfirmedTxAncestor,
|
|
244
248
|
utxosDescendingOrder,
|
|
245
249
|
}) => {
|
|
@@ -257,6 +261,7 @@ export const getUtxosData = ({
|
|
|
257
261
|
allowUnconfirmedRbfEnabledUtxos,
|
|
258
262
|
unconfirmedTxAncestor,
|
|
259
263
|
inscriptionIds,
|
|
264
|
+
transferOrdinalsUtxos,
|
|
260
265
|
utxosDescendingOrder,
|
|
261
266
|
})
|
|
262
267
|
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from './unconfirmed-ancestor-data'
|
|
|
16
16
|
export * from './parse-unsigned-tx'
|
|
17
17
|
export * from './insight-api-client/util'
|
|
18
18
|
export * from './move-funds'
|
|
19
|
+
export { createEncodeMultisigContract } from './multisig-address'
|
|
19
20
|
export { toAsyncSigner } from './tx-sign/taproot'
|
|
20
21
|
export { toXOnly } from './bitcoinjs-lib/ecc-utils'
|
|
21
22
|
export * from './ordinals-utils'
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
|
|
2
|
+
import { toXOnly } from './bitcoinjs-lib/ecc-utils'
|
|
3
|
+
import { eccFactory } from './bitcoinjs-lib/ecc'
|
|
4
|
+
|
|
5
|
+
// Key to use when key path spending is disabled https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
|
|
6
|
+
const DUMMY_TAPROOT_PUBKEY = Buffer.from(
|
|
7
|
+
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0',
|
|
8
|
+
'hex'
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Leaf version for BIP342 is 0xc0 or 192 https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#specification
|
|
12
|
+
const LEAF_VERSION_TAPSCRIPT = 192
|
|
13
|
+
|
|
14
|
+
// Limit multisig keys to 20 for now
|
|
15
|
+
const MAX_PUBKEYS = 20
|
|
16
|
+
|
|
17
|
+
export const createEncodeMultisigContract =
|
|
18
|
+
({
|
|
19
|
+
bitcoinjsLib = defaultBitcoinjsLib,
|
|
20
|
+
network = bitcoinjsLib.Network.bitcoin,
|
|
21
|
+
ecc = eccFactory(),
|
|
22
|
+
}) =>
|
|
23
|
+
(publicKeys, { threshold = publicKeys.length, version = 0 } = Object.create(null)) => {
|
|
24
|
+
if (
|
|
25
|
+
!Array.isArray(publicKeys) ||
|
|
26
|
+
publicKeys.some((k) => !Buffer.isBuffer(k) || !ecc.isPointCompressed(k))
|
|
27
|
+
) {
|
|
28
|
+
throw new Error('publicKeys must be an Array of Buffers representing compressed public keys')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (publicKeys.length <= 0 || publicKeys.length > MAX_PUBKEYS) {
|
|
32
|
+
throw new Error(`asset.encodeMultisigContract supports from 1 to ${MAX_PUBKEYS} pubKeys`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (new Set(publicKeys.map((k) => k.toString('hex'))).size !== publicKeys.length) {
|
|
36
|
+
throw new Error('publicKeys must not contain any duplicates')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!Number.isSafeInteger(version)) {
|
|
40
|
+
throw new TypeError('asset.encodeMultisigContract requires meta.version to be an integer')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Only support version 0 for now
|
|
44
|
+
if (version !== 0) {
|
|
45
|
+
throw new Error(`asset.encodeMultisigContract does not support version ${version}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!Number.isSafeInteger(threshold) || threshold <= 0 || threshold > MAX_PUBKEYS) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`asset.encodeMultisigContract requires meta.threshold to be an integer between 1 and ${MAX_PUBKEYS}`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (threshold > publicKeys.length) {
|
|
55
|
+
throw new Error('threshold must be <= publicKeys.length')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Sort according to BIP67 https://github.com/bitcoin/bips/blob/master/bip-0067.mediawiki
|
|
59
|
+
publicKeys.sort((a, b) => Buffer.compare(a, b))
|
|
60
|
+
|
|
61
|
+
// Create multisig redeem script https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#cite_note-5
|
|
62
|
+
const OPS = bitcoinjsLib.script.OPS
|
|
63
|
+
const chunks = []
|
|
64
|
+
const keysIter = publicKeys[Symbol.iterator]()
|
|
65
|
+
|
|
66
|
+
chunks.push(toXOnly(keysIter.next().value), OPS.OP_CHECKSIG)
|
|
67
|
+
for (const key of keysIter) {
|
|
68
|
+
chunks.push(toXOnly(key), OPS.OP_CHECKSIGADD)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
chunks.push(Buffer.from([threshold]), OPS.OP_NUMEQUAL)
|
|
72
|
+
|
|
73
|
+
const output = bitcoinjsLib.script.compile(chunks)
|
|
74
|
+
|
|
75
|
+
return bitcoinjsLib.payments.p2tr({
|
|
76
|
+
internalPubkey: DUMMY_TAPROOT_PUBKEY,
|
|
77
|
+
scriptTree: { output },
|
|
78
|
+
redeem: { output, redeemVersion: LEAF_VERSION_TAPSCRIPT },
|
|
79
|
+
network,
|
|
80
|
+
})
|
|
81
|
+
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from './dogecoin'
|
|
16
16
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
17
17
|
import {
|
|
18
|
+
getInscriptionIds,
|
|
18
19
|
getOrdinalsUtxos,
|
|
19
20
|
getTransferOrdinalsUtxos,
|
|
20
21
|
getUsableUtxos,
|
|
@@ -221,11 +222,9 @@ export const getPrepareSendTransaction =
|
|
|
221
222
|
allowUnconfirmedRbfEnabledUtxos,
|
|
222
223
|
utxosDescendingOrder,
|
|
223
224
|
rbfEnabled: providedRbfEnabled,
|
|
225
|
+
assetClientInterface,
|
|
224
226
|
}) =>
|
|
225
|
-
async (
|
|
226
|
-
{ asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
|
|
227
|
-
{ assetClientInterface }
|
|
228
|
-
) => {
|
|
227
|
+
async ({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options }) => {
|
|
229
228
|
const {
|
|
230
229
|
multipleAddressesEnabled,
|
|
231
230
|
feePerKB,
|
|
@@ -258,7 +257,7 @@ export const getPrepareSendTransaction =
|
|
|
258
257
|
}
|
|
259
258
|
|
|
260
259
|
const amount = isToken ? asset.currency.ZERO : tokenAmount
|
|
261
|
-
const inscriptionIds =
|
|
260
|
+
const inscriptionIds = getInscriptionIds({ nft, brc20 })
|
|
262
261
|
|
|
263
262
|
assert(
|
|
264
263
|
ordinalsEnabled || !inscriptionIds,
|
|
@@ -351,6 +350,7 @@ export const getPrepareSendTransaction =
|
|
|
351
350
|
allowUnconfirmedRbfEnabledUtxos,
|
|
352
351
|
unconfirmedTxAncestor,
|
|
353
352
|
inscriptionIds,
|
|
353
|
+
transferOrdinalsUtxos,
|
|
354
354
|
utxosDescendingOrder,
|
|
355
355
|
})
|
|
356
356
|
|
|
@@ -365,10 +365,6 @@ export const getPrepareSendTransaction =
|
|
|
365
365
|
throw new Error(`Unable to bump ${bumpTxId}`)
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
if (transferOrdinalsUtxos) {
|
|
369
|
-
selectedUtxos = transferOrdinalsUtxos.union(selectedUtxos)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
368
|
if (replaceTx) {
|
|
373
369
|
replaceTx = replaceTx.clone()
|
|
374
370
|
replaceTx = replaceTx.update({ data: { ...replaceTx.data } })
|
|
@@ -474,11 +470,9 @@ export const createAndBroadcastTXFactory =
|
|
|
474
470
|
allowUnconfirmedRbfEnabledUtxos,
|
|
475
471
|
ordinalsEnabled = false,
|
|
476
472
|
utxosDescendingOrder,
|
|
473
|
+
assetClientInterface,
|
|
477
474
|
}) =>
|
|
478
|
-
async (
|
|
479
|
-
{ asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
|
|
480
|
-
{ assetClientInterface }
|
|
481
|
-
) => {
|
|
475
|
+
async ({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options }) => {
|
|
482
476
|
// Prepare transaction
|
|
483
477
|
const { bumpTxId, nft, isExchange, isBip70, isRbfAllowed = true, feeOpts } = options
|
|
484
478
|
|
|
@@ -503,10 +497,8 @@ export const createAndBroadcastTXFactory =
|
|
|
503
497
|
allowUnconfirmedRbfEnabledUtxos,
|
|
504
498
|
utxosDescendingOrder,
|
|
505
499
|
rbfEnabled,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
{ assetClientInterface }
|
|
509
|
-
)
|
|
500
|
+
assetClientInterface,
|
|
501
|
+
})({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options })
|
|
510
502
|
const {
|
|
511
503
|
amount,
|
|
512
504
|
change,
|
|
@@ -37,7 +37,9 @@ export function createSignWithWallet({
|
|
|
37
37
|
const { key, purpose, publicKey } = getKeyAndPurpose(address)
|
|
38
38
|
|
|
39
39
|
const isP2SH = purpose === 49
|
|
40
|
-
const
|
|
40
|
+
const hasTapLeafScript =
|
|
41
|
+
psbt.data.inputs[index].tapLeafScript && psbt.data.inputs[index].tapLeafScript.length > 0
|
|
42
|
+
const isTaprootKeySpend = purpose === 86 && !hasTapLeafScript
|
|
41
43
|
|
|
42
44
|
if (isP2SH) {
|
|
43
45
|
// If spending from a P2SH address, we assume the address is P2SH wrapping
|
|
@@ -56,7 +58,7 @@ export function createSignWithWallet({
|
|
|
56
58
|
} else {
|
|
57
59
|
throw new Error('Expected P2SH script to be a nested segwit input')
|
|
58
60
|
}
|
|
59
|
-
} else if (
|
|
61
|
+
} else if (isTaprootKeySpend && !Buffer.isBuffer(psbt.data.inputs[index].tapInternalKey)) {
|
|
60
62
|
// tapInternalKey is metadata for signing and not part of the hash to sign.
|
|
61
63
|
// so modifying it here is fine.
|
|
62
64
|
psbt.updateInput(index, {
|
|
@@ -67,7 +69,7 @@ export function createSignWithWallet({
|
|
|
67
69
|
// desktop / BE / mobile with bip-schnorr signing
|
|
68
70
|
await psbt.signInputAsync(
|
|
69
71
|
index,
|
|
70
|
-
toAsyncSigner({ keyPair: key,
|
|
72
|
+
toAsyncSigner({ keyPair: key, isTaprootKeySpend, network }),
|
|
71
73
|
allowedSigHashTypes
|
|
72
74
|
)
|
|
73
75
|
}
|
|
@@ -35,7 +35,7 @@ export const signTxFactory = ({ assetName, resolvePurpose, coinInfo, network })
|
|
|
35
35
|
|
|
36
36
|
await signWithWallet(psbt, inputsToSign)
|
|
37
37
|
|
|
38
|
-
const skipFinalize = !!unsignedTx.txData.psbtBuffer
|
|
38
|
+
const skipFinalize = !!unsignedTx.txData.psbtBuffer || unsignedTx.txMeta.returnPsbt
|
|
39
39
|
return extractTransaction({ psbt, skipFinalize })
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -17,17 +17,21 @@ export function createPrepareForSigning({ assetName, resolvePurpose, coinInfo })
|
|
|
17
17
|
assert(coinInfo, 'coinInfo is required')
|
|
18
18
|
|
|
19
19
|
return ({ unsignedTx }) => {
|
|
20
|
+
const networkInfo = coinInfo.toBitcoinJS()
|
|
21
|
+
|
|
20
22
|
const isPsbtBufferPassed =
|
|
21
23
|
unsignedTx.txData.psbtBuffer &&
|
|
22
24
|
unsignedTx.txMeta.addressPathsMap &&
|
|
23
25
|
unsignedTx.txMeta.inputsToSign
|
|
24
26
|
if (isPsbtBufferPassed) {
|
|
25
27
|
// PSBT created externally (Web3, etc..)
|
|
26
|
-
return createPsbtFromBuffer({
|
|
28
|
+
return createPsbtFromBuffer({
|
|
29
|
+
psbtBuffer: unsignedTx.txData.psbtBuffer,
|
|
30
|
+
networkInfo,
|
|
31
|
+
})
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
// Create PSBT based on internal Exodus data structure
|
|
30
|
-
const networkInfo = coinInfo.toBitcoinJS()
|
|
31
35
|
const psbt = createPsbtFromTxData({
|
|
32
36
|
...unsignedTx.txData,
|
|
33
37
|
...unsignedTx.txMeta,
|
|
@@ -41,8 +45,8 @@ export function createPrepareForSigning({ assetName, resolvePurpose, coinInfo })
|
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
// Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
|
|
44
|
-
function createPsbtFromBuffer({ psbtBuffer, ecc }) {
|
|
45
|
-
return Psbt.fromBuffer(psbtBuffer, { eccLib: ecc })
|
|
48
|
+
function createPsbtFromBuffer({ psbtBuffer, ecc, networkInfo }) {
|
|
49
|
+
return Psbt.fromBuffer(psbtBuffer, { eccLib: ecc, network: networkInfo })
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
// Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
|
|
@@ -54,7 +58,7 @@ function createPsbtFromTxData({ inputs, outputs, rawTxs, networkInfo, resolvePur
|
|
|
54
58
|
const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
|
|
55
59
|
|
|
56
60
|
// Fill tx
|
|
57
|
-
for (const { txId, vout, address, value, script, sequence } of inputs) {
|
|
61
|
+
for (const { txId, vout, address, value, script, sequence, tapLeafScript } of inputs) {
|
|
58
62
|
// TODO: don't use the purpose as intermediate variable
|
|
59
63
|
// see internals of `resolvePurposes`, just use `isP2TR, isP2SH etc directly
|
|
60
64
|
const purpose = resolvePurpose(address)
|
|
@@ -64,6 +68,10 @@ function createPsbtFromTxData({ inputs, outputs, rawTxs, networkInfo, resolvePur
|
|
|
64
68
|
|
|
65
69
|
const txIn = { hash: txId, index: vout, sequence }
|
|
66
70
|
if (isSegwitAddress || isTaprootAddress) {
|
|
71
|
+
if (isTaprootAddress && tapLeafScript) {
|
|
72
|
+
txIn.tapLeafScript = tapLeafScript
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
// witness outputs only require the value and the script, not the full transaction
|
|
68
76
|
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
69
77
|
} else {
|
package/src/tx-sign/taproot.js
CHANGED
|
@@ -40,22 +40,22 @@ function tapTweakHash(pubKey, h) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Take a sync signer and make it async.
|
|
42
42
|
*/
|
|
43
|
-
export function toAsyncSigner({ keyPair,
|
|
43
|
+
export function toAsyncSigner({ keyPair, isTaprootKeySpend, network }) {
|
|
44
44
|
assert(keyPair, 'keyPair is required')
|
|
45
45
|
|
|
46
|
-
if (
|
|
46
|
+
if (isTaprootKeySpend) {
|
|
47
47
|
keyPair = tweakSigner({ signer: keyPair, ECPair, network })
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
51
51
|
keyPair.sign = async (h) => {
|
|
52
|
-
const sig =
|
|
52
|
+
const sig = ecc.sign(h, keyPair.privateKey)
|
|
53
53
|
return Buffer.from(sig)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
57
57
|
keyPair.signSchnorr = async (h) => {
|
|
58
|
-
const sig =
|
|
58
|
+
const sig = ecc.signSchnorr(h, keyPair.privateKey, getSchnorrEntropy())
|
|
59
59
|
return Buffer.from(sig)
|
|
60
60
|
}
|
|
61
61
|
|
package/src/utxos-utils.js
CHANGED
|
@@ -9,6 +9,10 @@ export const getInscriptionTxId = (inscriptionId) => {
|
|
|
9
9
|
return inscriptionId.split('i')[0]
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function getInscriptionIds({ nft, brc20 }) {
|
|
13
|
+
return nft?.tokenId ? [nft?.tokenId] : brc20 ? brc20.inscriptionIds : undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
export function getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos }) {
|
|
13
17
|
const transferOrdinalsUtxos = ordinalsUtxos.filter((utxo) =>
|
|
14
18
|
utxo.inscriptions?.some((i) => inscriptionIds.includes(i.inscriptionId))
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import secp256k1 from '@exodus/secp256k1'
|
|
2
|
-
import { toXOnly } from '../ecc-utils'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Common ecc functions between mobile and desktop. Once mobile accepts @noble/secp256k1, we can unify both
|
|
6
|
-
*/
|
|
7
|
-
export const common = {
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
* @param msg32 {Uint8Array} the message
|
|
11
|
-
* @param seckey {Uint8Array} the private key
|
|
12
|
-
* @param data {Uint8Array} the data to sign
|
|
13
|
-
*/
|
|
14
|
-
sign: (msg32, seckey, data) => secp256k1.ecdsaSign(msg32, seckey, { data }).signature,
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
* @param msg32 {Uint8Array} the message to verify
|
|
19
|
-
* @param publicKey {Uint8Array} the public key
|
|
20
|
-
* @param signature {Uint8Array} the signature
|
|
21
|
-
* @returns {boolean}
|
|
22
|
-
*/
|
|
23
|
-
verify: (msg32, publicKey, signature) => secp256k1.ecdsaVerify(signature, msg32, publicKey),
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
* @param seckey {Uint8Array}
|
|
28
|
-
* @param tweak {Uint8Array}
|
|
29
|
-
*/
|
|
30
|
-
privateAdd: (seckey, tweak) =>
|
|
31
|
-
// cloning input. secp256k1 modifies it and it cannot be reused/cached
|
|
32
|
-
secp256k1.privateKeyTweakAdd(Buffer.from(seckey), tweak),
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @param seckey {Uint8Array} the public key
|
|
36
|
-
*/
|
|
37
|
-
privateNegate: (seckey) =>
|
|
38
|
-
// cloning input. secp256k1 modifies it and it cannot be reused/cached
|
|
39
|
-
secp256k1.privateKeyNegate(Buffer.from(seckey)),
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
43
|
-
* @param publicKey {Uint8Array}
|
|
44
|
-
* @param tweak {Uint8Array}
|
|
45
|
-
* @returns {{parity: (number), xOnlyPubkey}|null}
|
|
46
|
-
*/
|
|
47
|
-
xOnlyPointAddTweak: (publicKey, tweak) => {
|
|
48
|
-
try {
|
|
49
|
-
const t = secp256k1.publicKeyTweakAdd(toPubKey(publicKey), tweak)
|
|
50
|
-
return {
|
|
51
|
-
parity: t[0] === 0x02 ? 0 : 1,
|
|
52
|
-
xOnlyPubkey: toXOnly(t),
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
return null
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @param seckey {Uint8Array}
|
|
61
|
-
* @returns {boolean}
|
|
62
|
-
*/
|
|
63
|
-
isPrivate: (seckey) => secp256k1.privateKeyVerify(seckey),
|
|
64
|
-
/**
|
|
65
|
-
* @param seckey {Uint8Array}
|
|
66
|
-
* @param compressed {boolean}
|
|
67
|
-
*/
|
|
68
|
-
pointFromScalar: (seckey, compressed) => secp256k1.publicKeyCreate(seckey, compressed),
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
* @param xOnly {Uint8Array}
|
|
74
|
-
* @returns {Uint8Array}
|
|
75
|
-
*/
|
|
76
|
-
export const toPubKey = (xOnly) => {
|
|
77
|
-
const p = new Uint8Array(33)
|
|
78
|
-
p.set([0x02])
|
|
79
|
-
p.set(xOnly, 1)
|
|
80
|
-
return p
|
|
81
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { initEccLib } from '@exodus/bitcoinjs-lib'
|
|
2
|
-
import { Point, schnorr, sign } from '@noble/secp256k1'
|
|
3
|
-
import { common, toPubKey } from './common'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Wrapper around `secp256k1` in order to follow the bitcoinjs-lib `TinySecp256k1Interface`
|
|
7
|
-
* Schnorr signatures are offered by @noble/secp256k1
|
|
8
|
-
*/
|
|
9
|
-
export const desktopEcc = {
|
|
10
|
-
...common,
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @param message {Uint8Array}
|
|
14
|
-
* @param privateKey {Uint8Array}
|
|
15
|
-
* @param extraEntropy {Uint8Array}
|
|
16
|
-
* @returns {Promise<Uint8Array>}
|
|
17
|
-
*/
|
|
18
|
-
signAsync: async (message, privateKey, extraEntropy) =>
|
|
19
|
-
sign(message, privateKey, {
|
|
20
|
-
extraEntropy,
|
|
21
|
-
canonical: true,
|
|
22
|
-
der: false,
|
|
23
|
-
}),
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param message {Uint8Array}
|
|
27
|
-
* @param privateKey {Uint8Array}
|
|
28
|
-
* @param extraEntropy {Uint8Array}
|
|
29
|
-
* @returns {Promise<Uint8Array>}
|
|
30
|
-
*/
|
|
31
|
-
signSchnorrAsync: async (message, privateKey, extraEntropy) =>
|
|
32
|
-
schnorr.sign(message, privateKey, extraEntropy),
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @param message {Uint8Array}
|
|
36
|
-
* @param publicKey {Uint8Array}
|
|
37
|
-
* @param signature {Uint8Array}
|
|
38
|
-
* @returns {Promise<boolean>}
|
|
39
|
-
*/
|
|
40
|
-
verifySchnorrAsync: async (message, publicKey, signature) =>
|
|
41
|
-
schnorr.verify(signature, message, publicKey),
|
|
42
|
-
|
|
43
|
-
// The underlying library does not expose sync functions for Schnorr sign and verify.
|
|
44
|
-
// These function are explicitly defined here as `null` for documentation purposes.
|
|
45
|
-
// Update, latest version of https://github.com/paulmillr/noble-secp256k1 does support SYNC
|
|
46
|
-
signSchnorr: null,
|
|
47
|
-
verifySchnorr: null,
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @param publicKey {Uint8Array}
|
|
51
|
-
* @returns {boolean}
|
|
52
|
-
*/
|
|
53
|
-
isPoint: (publicKey) => {
|
|
54
|
-
try {
|
|
55
|
-
Point.fromHex(publicKey).assertValidity()
|
|
56
|
-
return true
|
|
57
|
-
} catch {
|
|
58
|
-
return false
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @param publicKey {Uint8Array}
|
|
64
|
-
* @returns {boolean}
|
|
65
|
-
*/
|
|
66
|
-
isXOnlyPoint: (publicKey) => {
|
|
67
|
-
try {
|
|
68
|
-
Point.fromHex(toPubKey(publicKey)).assertValidity()
|
|
69
|
-
return true
|
|
70
|
-
} catch {
|
|
71
|
-
return false
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @param publicKey {Uint8Array}
|
|
77
|
-
* @param compressed {boolean}
|
|
78
|
-
* @returns {Uint8Array}
|
|
79
|
-
*/
|
|
80
|
-
pointCompress: (publicKey, compressed) => Point.fromHex(publicKey).toRawBytes(compressed),
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
initEccLib(desktopEcc)
|