-
Notifications
You must be signed in to change notification settings - Fork 272
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5068 from BitGo/BTC-1578-descriptor
feat(utxo-coredao): descriptor for staking output
- Loading branch information
Showing
17 changed files
with
306 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { BIP32Interface } from '@bitgo/utxo-lib'; | ||
|
||
/** | ||
* Script type for a descriptor. This is either a p2sh (sh) or a p2sh-p2wsh (sh-wsh) script. | ||
*/ | ||
export type ScriptType = 'sh' | 'sh-wsh'; | ||
|
||
/** | ||
* Create a multi-sig descriptor to produce a coredao staking address | ||
* @param scriptType segwit or legacy | ||
* @param locktime locktime for CLTV | ||
* @param m Total number of keys required to unlock | ||
* @param orderedKeys | ||
* @param neutered If true, neuter the keys. Default to true | ||
*/ | ||
export function createMultiSigDescriptor( | ||
scriptType: ScriptType, | ||
locktime: number, | ||
m: number, | ||
orderedKeys: BIP32Interface[], | ||
neutered = true | ||
): string { | ||
if (m > orderedKeys.length || m < 1) { | ||
throw new Error( | ||
`m (${m}) must be less than or equal to the number of keys (${orderedKeys.length}) and greater than 0` | ||
); | ||
} | ||
if (locktime <= 0) { | ||
throw new Error(`locktime (${locktime}) must be greater than 0`); | ||
} | ||
|
||
const xpubs = orderedKeys.map((key) => (neutered ? key.neutered() : key).toBase58() + '/*'); | ||
const inner = `and_v(r:after(${locktime}),multi(${m},${xpubs.join(',')}))`; | ||
return scriptType === 'sh' ? `sh(${inner})` : `sh(wsh(${inner}))`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
OP_PUSHBYTES_2 0008 OP_CLTV OP_DROP OP_PUSHNUM_2 OP_PUSHBYTES_33 03b6ca82c5298cba26c76a9cc5a9bb08c68b125263924ac9d58a15a60d49cf3ef3 OP_PUSHBYTES_33 02e6a4442a7efbc0f53fad6ff2a7c0940ea18c751a0b6644c03675babd731015cc OP_PUSHNUM_2 OP_CHECKMULTISIG |
26 changes: 26 additions & 0 deletions
26
modules/utxo-coredao/test/fixtures/descriptor/sh-2of2-ast.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"Sh": { | ||
"Ms": { | ||
"AndV": [ | ||
{ | ||
"Drop": { | ||
"After": { | ||
"absLockTime": 2048 | ||
} | ||
} | ||
}, | ||
{ | ||
"Multi": [ | ||
2, | ||
{ | ||
"XPub": "xpub661MyMwAqRbcGZfgedgcQiBJpkHoZ37k6uotUVLoC6Px4Y46Yrdmy1CUogcJMoAsosY381gPjhGe9jFx1uYAcxy2gTHh9YFP32tUWycqHnV/*" | ||
}, | ||
{ | ||
"XPub": "xpub661MyMwAqRbcF8FoYWukS8eTn2gVEojzwf5DYETpB8uqC8t5sqDCEFnuJ39DaPjLRerBDK9QqSMvSYpT4WSugCVbUK5HEevSKAu1wUkVWsS/*" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
modules/utxo-coredao/test/fixtures/descriptor/sh-2of2-string.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sh(and_v(r:after(2048),multi(2,xprv9s21ZrQH143K45bDYc9c3aEaGiTK9aPtjgtHg6wBdkryBjix1KKXRCszxPcFPejLT9tdLgNe8E8AuQXK2fy8KhNPeLAZsGoX8w9KS2PkacL/*,xprv9s21ZrQH143K2eBLSVNk4zhjDzqzqM29aS9cjr4CcoNrKLYwLHtwgTURSk7RPV3cH9zNZQeR1zGw3MEwSjvARSfWEGpxfaBmduhW3TKsH5g/*))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
02000000013ccb3b820130a360a33cc4b8afa063d6c6c2cf04680d6ac3b728db0a82cbec6700000000e100483045022100bdf153f069124649bd6053bc5134b8ee49769fd91bebd38a1d366dfa6aca837c02201c78e9a8804339bef63c2a8250e86faced3feab246f092b460a2289070ff1e8301483045022100cf53ecb0baa76b98e66a2b110a28267f72dff6b243d799013e20561696dfa61d0220067ecaa7fbc50026a56234fe61a25995c4a2b2f6c29c9ab55241cfd5ffc33183014c4c020008b175522103b6ca82c5298cba26c76a9cc5a9bb08c68b125263924ac9d58a15a60d49cf3ef32102e6a4442a7efbc0f53fad6ff2a7c0940ea18c751a0b6644c03675babd731015cc52aefeffffff01804a5d050000000017a914199ff00d994285c5fa0c08a4becd8973945e06c28700080000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
OP_PUSHBYTES_2 0008 OP_CLTV OP_DROP OP_PUSHNUM_3 OP_PUSHBYTES_33 03b6ca82c5298cba26c76a9cc5a9bb08c68b125263924ac9d58a15a60d49cf3ef3 OP_PUSHBYTES_33 02e6a4442a7efbc0f53fad6ff2a7c0940ea18c751a0b6644c03675babd731015cc OP_PUSHBYTES_33 038ce8843d1085927fdb26a26e9869d6c10dcd37aa45fdd9773ff544a7f3514c82 OP_PUSHNUM_3 OP_CHECKMULTISIG |
29 changes: 29 additions & 0 deletions
29
modules/utxo-coredao/test/fixtures/descriptor/sh-3of3-ast.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"Sh": { | ||
"Ms": { | ||
"AndV": [ | ||
{ | ||
"Drop": { | ||
"After": { | ||
"absLockTime": 2048 | ||
} | ||
} | ||
}, | ||
{ | ||
"Multi": [ | ||
3, | ||
{ | ||
"XPub": "xpub661MyMwAqRbcGZfgedgcQiBJpkHoZ37k6uotUVLoC6Px4Y46Yrdmy1CUogcJMoAsosY381gPjhGe9jFx1uYAcxy2gTHh9YFP32tUWycqHnV/*" | ||
}, | ||
{ | ||
"XPub": "xpub661MyMwAqRbcF8FoYWukS8eTn2gVEojzwf5DYETpB8uqC8t5sqDCEFnuJ39DaPjLRerBDK9QqSMvSYpT4WSugCVbUK5HEevSKAu1wUkVWsS/*" | ||
}, | ||
{ | ||
"XPub": "xpub661MyMwAqRbcFdScyinA4JpCViqkKsd37MQ6fwuZQQ4shdaGRRX9a8bWvR9QC1AFqKongweJJfyrm7uCoWmCw7UixwGZkZnCT2mchFr7cQb/*" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
modules/utxo-coredao/test/fixtures/descriptor/sh-3of3-string.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sh(and_v(r:after(2048),multi(3,xprv9s21ZrQH143K45bDYc9c3aEaGiTK9aPtjgtHg6wBdkryBjix1KKXRCszxPcFPejLT9tdLgNe8E8AuQXK2fy8KhNPeLAZsGoX8w9KS2PkacL/*,xprv9s21ZrQH143K2eBLSVNk4zhjDzqzqM29aS9cjr4CcoNrKLYwLHtwgTURSk7RPV3cH9zNZQeR1zGw3MEwSjvARSfWEGpxfaBmduhW3TKsH5g/*,xprv9s21ZrQH143K39N9shF9hAsTwh1FvQuBk8UVsZVwr4XtpqF7stCu2LH358NLuqkkK6pu1Af7TJHr5FZERQoLLtnC7wkoM9sdFo1HuP7dWuv/*))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
02000000012864beb49a42260574c7fc2d89a97dba04073353eb5bae5c08b2bde845db371300000000fd49010047304402201f27a9ed1572e5cf4be3c09c48c4df8cbebb7553af584dd437ef541f7c9edc9a02204fe6c5d4bdbd8094f20c7e78160686243a664f69dd41256a4eb6aa97c1f3765b01473044022023d1e8dd29365a384553889b7101071bf1bd265978032155734a65ef7fb240c1022073d45ea7090e31798ebd3e3b41d552706f6bec78ca5fa129c3f3d3a2690478fc01473044022053ad461ec6173e4699ece94345f3aedaa758d7b4070d6f3c75567ab07092013e02204d77ece097fa2f7283dd5e3e4256695c29f3abee7e59d47fc4334c15277f2334014c6e020008b175532103b6ca82c5298cba26c76a9cc5a9bb08c68b125263924ac9d58a15a60d49cf3ef32102e6a4442a7efbc0f53fad6ff2a7c0940ea18c751a0b6644c03675babd731015cc21038ce8843d1085927fdb26a26e9869d6c10dcd37aa45fdd9773ff544a7f3514c8253aefeffffff01804a5d050000000017a914bf73c6a0f7b8e8ed8ad854cc2d65ffe21ad276298700080000 |
1 change: 1 addition & 0 deletions
1
modules/utxo-coredao/test/fixtures/descriptor/sh-wsh-2of2-asm.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
OP_PUSHBYTES_2 0008 OP_CLTV OP_DROP OP_PUSHNUM_2 OP_PUSHBYTES_33 03b6ca82c5298cba26c76a9cc5a9bb08c68b125263924ac9d58a15a60d49cf3ef3 OP_PUSHBYTES_33 02e6a4442a7efbc0f53fad6ff2a7c0940ea18c751a0b6644c03675babd731015cc OP_PUSHNUM_2 OP_CHECKMULTISIG |
28 changes: 28 additions & 0 deletions
28
modules/utxo-coredao/test/fixtures/descriptor/sh-wsh-2of2-ast.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"Sh": { | ||
"Wsh": { | ||
"Ms": { | ||
"AndV": [ | ||
{ | ||
"Drop": { | ||
"After": { | ||
"absLockTime": 2048 | ||
} | ||
} | ||
}, | ||
{ | ||
"Multi": [ | ||
2, | ||
{ | ||
"XPub": "xpub661MyMwAqRbcGZfgedgcQiBJpkHoZ37k6uotUVLoC6Px4Y46Yrdmy1CUogcJMoAsosY381gPjhGe9jFx1uYAcxy2gTHh9YFP32tUWycqHnV/*" | ||
}, | ||
{ | ||
"XPub": "xpub661MyMwAqRbcF8FoYWukS8eTn2gVEojzwf5DYETpB8uqC8t5sqDCEFnuJ39DaPjLRerBDK9QqSMvSYpT4WSugCVbUK5HEevSKAu1wUkVWsS/*" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
} | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
modules/utxo-coredao/test/fixtures/descriptor/sh-wsh-2of2-string.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sh(wsh(and_v(r:after(2048),multi(2,xprv9s21ZrQH143K45bDYc9c3aEaGiTK9aPtjgtHg6wBdkryBjix1KKXRCszxPcFPejLT9tdLgNe8E8AuQXK2fy8KhNPeLAZsGoX8w9KS2PkacL/*,xprv9s21ZrQH143K2eBLSVNk4zhjDzqzqM29aS9cjr4CcoNrKLYwLHtwgTURSk7RPV3cH9zNZQeR1zGw3MEwSjvARSfWEGpxfaBmduhW3TKsH5g/*)))) |
1 change: 1 addition & 0 deletions
1
modules/utxo-coredao/test/fixtures/descriptor/sh-wsh-2of2-tx.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
020000000001016529f61c633614899fb44d3feef56796225b9010803b44e463334bacf3927af200000000232200206e9093852388246c2aeceaff64ebb2a3753eb5cb15079e91aee0ffa4d62e5530feffffff01804a5d050000000017a914afbde2b1755e3bec5ba23355408f07cbf20d8d53870400473044022039b22b53980cc018ec079a611567229d0e5b73d5822b9d999e65d554b4650fc702205de919715d3b2a0f619f69cf7fff0ebc6a393a6388264051cb2626440f7229d2014730440220353ffb65f59a8d7c9f46a96c23c2cb82f520e356700148fc720ac36d51ff3bf5022026363c67bc038705c5f4dfc5cdd00b8e392f66e70c0390e2825ec1ab1fbd93d8014c020008b175522103b6ca82c5298cba26c76a9cc5a9bb08c68b125263924ac9d58a15a60d49cf3ef32102e6a4442a7efbc0f53fad6ff2a7c0940ea18c751a0b6644c03675babd731015cc52ae00080000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import * as assert from 'assert'; | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
import { Descriptor } from '@bitgo/wasm-miniscript'; | ||
|
||
import { createMultiSigDescriptor } from '../../src/descriptor'; | ||
import { finalizePsbt, getFixture, updateInputWithDescriptor } from './utils'; | ||
|
||
describe('descriptor', function () { | ||
const baseFixturePath = 'test/fixtures/descriptor/'; | ||
const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys(); | ||
const key1 = rootWalletKeys.triple[0]; | ||
const key2 = rootWalletKeys.triple[1]; | ||
const key3 = rootWalletKeys.triple[2]; | ||
const validLocktime = 2048; | ||
|
||
it('should fail if m is longer than the number of keys or not at least 1', function () { | ||
assert.throws(() => { | ||
createMultiSigDescriptor('sh', validLocktime, 3, [key1, key2], false); | ||
}); | ||
|
||
assert.throws(() => { | ||
createMultiSigDescriptor('sh', validLocktime, 0, [key1, key2], false); | ||
}); | ||
}); | ||
|
||
it('should fail if locktime is invalid', function () { | ||
assert.throws(() => { | ||
createMultiSigDescriptor('sh', 0, 2, [key1, key2], false); | ||
}); | ||
}); | ||
|
||
async function runTestForParams(scriptType: 'sh' | 'sh-wsh', m: number, keys: utxolib.BIP32Interface[]) { | ||
const fixturePath = baseFixturePath + `${scriptType}-${m}of${keys.length}`; | ||
describe(`should create a ${m} of ${keys.length} multi-sig ${scriptType} descriptor`, function () { | ||
it('has expected descriptor string', async function () { | ||
const descriptorString = createMultiSigDescriptor(scriptType, validLocktime, m, keys, false); | ||
assert.strictEqual( | ||
descriptorString, | ||
await getFixture(fixturePath + `-string.txt`, descriptorString), | ||
descriptorString | ||
); | ||
}); | ||
|
||
it('has expected AST', async function () { | ||
const descriptor = Descriptor.fromString( | ||
createMultiSigDescriptor(scriptType, validLocktime, m, keys, false), | ||
'derivable' | ||
); | ||
|
||
assert.deepStrictEqual(descriptor.node(), await getFixture(fixturePath + '-ast.json', descriptor.node())); | ||
}); | ||
|
||
it('has expected asm', async function () { | ||
const descriptor = Descriptor.fromString( | ||
createMultiSigDescriptor(scriptType, validLocktime, m, keys, false), | ||
'derivable' | ||
); | ||
const asmString = descriptor.atDerivationIndex(0).toAsmString(); | ||
assert.strictEqual(asmString, await getFixture(fixturePath + '-asm.txt', asmString), asmString); | ||
}); | ||
|
||
it('can be signed', async function () { | ||
// Derive the script from the descriptor | ||
const descriptor = Descriptor.fromString( | ||
createMultiSigDescriptor(scriptType, validLocktime, m, keys, false), | ||
'derivable' | ||
); | ||
const descriptorAt0 = descriptor.atDerivationIndex(0); | ||
const script = Buffer.from(descriptorAt0.scriptPubkey()); | ||
|
||
// Make the prevTx | ||
const prevPsbt = utxolib.testutil.constructPsbt( | ||
[{ scriptType: 'p2wsh', value: BigInt(1.1e8) }], | ||
[{ script: script.toString('hex'), value: BigInt(1e8) }], | ||
utxolib.networks.bitcoin, | ||
rootWalletKeys, | ||
'fullsigned' | ||
); | ||
const prevTx = prevPsbt.finalizeAllInputs().extractTransaction(); | ||
|
||
// Create the PSBT and sign | ||
const psbt = Object.assign(new utxolib.Psbt({ network: utxolib.networks.bitcoin }), { | ||
locktime: validLocktime, | ||
}); | ||
psbt.addInput({ | ||
hash: prevTx.getId(), | ||
index: 0, | ||
sequence: 0xfffffffe, | ||
}); | ||
if (scriptType === 'sh-wsh') { | ||
psbt.updateInput(0, { witnessUtxo: { script, value: BigInt(1e8) } }); | ||
} else { | ||
psbt.updateInput(0, { nonWitnessUtxo: prevTx.toBuffer() }); | ||
} | ||
psbt.addOutput({ script, value: BigInt(0.9e8) }); | ||
updateInputWithDescriptor(psbt, 0, descriptorAt0); | ||
keys.forEach((signer, i) => { | ||
if (i >= m) { | ||
return; | ||
} | ||
psbt.signAllInputsHD(signer); | ||
}); | ||
|
||
// Get the fully signed transaction and check | ||
const signedTx = finalizePsbt(psbt).extractTransaction().toBuffer(); | ||
assert.strictEqual( | ||
signedTx.toString('hex'), | ||
await getFixture(fixturePath + '-tx.txt', signedTx.toString('hex')) | ||
); | ||
}); | ||
}); | ||
} | ||
|
||
runTestForParams('sh', 2, [key1, key2]); | ||
runTestForParams('sh-wsh', 2, [key1, key2]); | ||
runTestForParams('sh', 3, [key1, key2, key3]); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { promises as fs } from 'fs'; | ||
import { Descriptor, Psbt } from '@bitgo/wasm-miniscript'; | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
|
||
function encode(path: string, defaultValue: unknown): string { | ||
if (path.endsWith('.txt')) { | ||
return String(defaultValue); | ||
} | ||
if (path.endsWith('.json')) { | ||
return JSON.stringify(defaultValue, null, 2); | ||
} | ||
throw new Error(`unrecognized path ${path}`); | ||
} | ||
|
||
function decode(path: string, v: string): unknown { | ||
if (path.endsWith('.txt')) { | ||
return v; | ||
} | ||
if (path.endsWith('.json')) { | ||
return JSON.parse(v); | ||
} | ||
throw new Error(`unrecognized path ${path}`); | ||
} | ||
|
||
export async function getFixture(path: string, defaultValue: unknown): Promise<unknown> { | ||
try { | ||
return decode(path, await fs.readFile(path, 'utf8')); | ||
} catch (e) { | ||
if (e.code === 'ENOENT') { | ||
await fs.writeFile(path, encode(path, defaultValue), 'utf8'); | ||
throw new Error(`wrote default value for ${path}`); | ||
} | ||
throw e; | ||
} | ||
} | ||
|
||
export function updateInputWithDescriptor(psbt: utxolib.Psbt, inputIndex: number, descriptor: Descriptor): void { | ||
const wrappedPsbt = Psbt.deserialize(psbt.toBuffer()); | ||
wrappedPsbt.updateInputWithDescriptor(inputIndex, descriptor); | ||
psbt.data.inputs[inputIndex] = utxolib.bitgo.UtxoPsbt.fromBuffer(Buffer.from(wrappedPsbt.serialize()), { | ||
network: utxolib.networks.bitcoin, | ||
}).data.inputs[inputIndex]; | ||
} | ||
|
||
export function finalizePsbt(psbt: utxolib.Psbt): utxolib.bitgo.UtxoPsbt { | ||
const wrappedPsbt = Psbt.deserialize(psbt.toBuffer()); | ||
wrappedPsbt.finalize(); | ||
return utxolib.bitgo.UtxoPsbt.fromBuffer(Buffer.from(wrappedPsbt.serialize()), { | ||
network: utxolib.networks.bitcoin, | ||
}); | ||
} |
Oops, something went wrong.