From 5322edc9bc755309c89ed19ebadac7ea31781b7f Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 18 Feb 2021 16:37:43 +0300 Subject: [PATCH] ADD: psbt cosign --- Navigation.js | 2 +- class/wallets/abstract-hd-electrum-wallet.js | 47 +++++ class/wallets/abstract-wallet.js | 4 + class/wallets/hd-legacy-p2pkh-wallet.js | 4 + class/wallets/hd-segwit-bech32-wallet.js | 4 + class/wallets/hd-segwit-p2sh-wallet.js | 4 + components/DynamicQRCode.js | 1 + helpers/scan-qr.js | 29 +++ loc/en.json | 3 +- screen/send/ScanQRCode.js | 3 +- screen/send/create.js | 5 +- screen/send/details.js | 52 +++++ screen/wallets/transactions.js | 2 +- tests/e2e/bluewallet.spec.js | 24 ++- tests/unit/cosign.test.js | 202 +++++++++++++++++++ 15 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 helpers/scan-qr.js create mode 100644 tests/unit/cosign.test.js diff --git a/Navigation.js b/Navigation.js index d36f7c939..773030164 100644 --- a/Navigation.js +++ b/Navigation.js @@ -60,6 +60,7 @@ import SendCreate from './screen/send/create'; import Confirm from './screen/send/confirm'; import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet'; import PsbtMultisig from './screen/send/psbtMultisig'; +import PsbtMultisigQRCode from './screen/send/psbtMultisigQRCode'; import Success from './screen/send/success'; import Broadcast from './screen/send/broadcast'; import IsItMyAddress from './screen/send/isItMyAddress'; @@ -78,7 +79,6 @@ import DrawerList from './screen/wallets/drawerList'; import { isTablet } from 'react-native-device-info'; import SettingsPrivacy from './screen/settings/SettingsPrivacy'; import LNDViewAdditionalInvoicePreImage from './screen/lnd/lndViewAdditionalInvoicePreImage'; -import PsbtMultisigQRCode from './screen/send/psbtMultisigQRCode'; const defaultScreenOptions = Platform.OS === 'ios' diff --git a/class/wallets/abstract-hd-electrum-wallet.js b/class/wallets/abstract-hd-electrum-wallet.js index 66fabc176..e77267483 100644 --- a/class/wallets/abstract-hd-electrum-wallet.js +++ b/class/wallets/abstract-hd-electrum-wallet.js @@ -1048,4 +1048,51 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { } return false; } + + calculateHowManySignaturesWeHaveFromPsbt(psbt) { + let sigsHave = 0; + for (const inp of psbt.data.inputs) { + if (inp.finalScriptSig || inp.finalScriptWitness || inp.partialSig) sigsHave++; + } + return sigsHave; + } + + /** + * Tries to signs passed psbt object (by reference). If there are enough signatures - tries to finalize psbt + * and returns Transaction (ready to extract hex) + * + * @param psbt {Psbt} + * @returns {{ tx: Transaction }} + */ + cosignPsbt(psbt) { + const mnemonic = this.secret; + const seed = bip39.mnemonicToSeed(mnemonic); + const hdRoot = HDNode.fromSeed(seed); + + for (let cc = 0; cc < psbt.inputCount; cc++) { + try { + psbt.signInputHD(cc, hdRoot); + } catch (e) {} // protects agains duplicate cosignings + + if (!psbt.inputHasHDKey(cc, hdRoot)) { + for (const derivation of psbt.data.inputs[cc].bip32Derivation || []) { + const splt = derivation.path.split('/'); + const internal = +splt[splt.length - 2]; + const index = +splt[splt.length - 1]; + const wif = this._getWIFByIndex(internal, index); + const keyPair = bitcoin.ECPair.fromWIF(wif); + try { + psbt.signInput(cc, keyPair); + } catch (e) {} // protects agains duplicate cosignings or if this output can't be signed with current wallet + } + } + } + + let tx = false; + if (this.calculateHowManySignaturesWeHaveFromPsbt(psbt) === psbt.inputCount) { + tx = psbt.finalizeAllInputs().extractTransaction(); + } + + return { tx }; + } } diff --git a/class/wallets/abstract-wallet.js b/class/wallets/abstract-wallet.js index 2cb077d9d..ada46c277 100644 --- a/class/wallets/abstract-wallet.js +++ b/class/wallets/abstract-wallet.js @@ -119,6 +119,10 @@ export class AbstractWallet { return false; } + allowCosignPsbt() { + return false; + } + weOwnAddress(address) { throw Error('not implemented'); } diff --git a/class/wallets/hd-legacy-p2pkh-wallet.js b/class/wallets/hd-legacy-p2pkh-wallet.js index 5032c72dd..a385062db 100644 --- a/class/wallets/hd-legacy-p2pkh-wallet.js +++ b/class/wallets/hd-legacy-p2pkh-wallet.js @@ -21,6 +21,10 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet { return true; } + allowCosignPsbt() { + return true; + } + getXpub() { if (this._xpub) { return this._xpub; // cache hit diff --git a/class/wallets/hd-segwit-bech32-wallet.js b/class/wallets/hd-segwit-bech32-wallet.js index dd81412c6..73eaec7f5 100644 --- a/class/wallets/hd-segwit-bech32-wallet.js +++ b/class/wallets/hd-segwit-bech32-wallet.js @@ -32,4 +32,8 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet { allowPayJoin() { return true; } + + allowCosignPsbt() { + return true; + } } diff --git a/class/wallets/hd-segwit-p2sh-wallet.js b/class/wallets/hd-segwit-p2sh-wallet.js index 50aef1b8a..132fe7408 100644 --- a/class/wallets/hd-segwit-p2sh-wallet.js +++ b/class/wallets/hd-segwit-p2sh-wallet.js @@ -21,6 +21,10 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet { return true; } + allowCosignPsbt() { + return true; + } + /** * Get internal/external WIF by wallet index * @param {Boolean} internal diff --git a/components/DynamicQRCode.js b/components/DynamicQRCode.js index 7a5d0dff4..b24c021c8 100644 --- a/components/DynamicQRCode.js +++ b/components/DynamicQRCode.js @@ -105,6 +105,7 @@ export class DynamicQRCode extends Component { return ( { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); diff --git a/helpers/scan-qr.js b/helpers/scan-qr.js new file mode 100644 index 000000000..0d899e5a0 --- /dev/null +++ b/helpers/scan-qr.js @@ -0,0 +1,29 @@ +/** + * Helper function that navigates to ScanQR screen, and returns promise that will resolve with the result of a scan, + * and then navigates back. If QRCode scan was closed, promise resolves to null. + * + * @param navigateFunc {function} + * @param currentScreenName {string} + * + * @return {Promise} + */ +module.exports = function (navigateFunc, currentScreenName) { + return new Promise(resolve => { + const params = {}; + params.showFileImportButton = true; + + params.onBarScanned = function (data) { + setTimeout(() => resolve(data.data || data), 1); + navigateFunc(currentScreenName); + }; + + params.onDismiss = function () { + setTimeout(() => resolve(null), 1); + }; + + navigateFunc('ScanQRCodeRoot', { + screen: 'ScanQRCode', + params, + }); + }); +}; diff --git a/loc/en.json b/loc/en.json index 6f6f72ac4..3f73d00fa 100644 --- a/loc/en.json +++ b/loc/en.json @@ -212,6 +212,7 @@ "input_total": "Total:", "permission_camera_message": "We need your permission to use your camera.", "permission_camera_title": "Permission to use camera", + "psbt_sign": "Sign a transaction", "open_settings": "Open Settings", "permission_storage_later": "Ask me later", "permission_storage_message": "BlueWallet needs your permission to access your storage to save this file.", @@ -348,7 +349,7 @@ "details_title": "Transaction", "details_to": "Output", "details_transaction_details": "Transaction Details", - "enable_hw": "This wallet is not being used in conjunction with a hardware wallet. Would you like to enable hardware wallet use?", + "enable_offline_signing": "This wallet is not being used in conjunction with an offline signing. Would you wish to enable it now?", "list_conf": "Conf: {number}", "pending": "Pending", "list_title": "Transactions", diff --git a/screen/send/ScanQRCode.js b/screen/send/ScanQRCode.js index 1d9385098..157b1ce57 100644 --- a/screen/send/ScanQRCode.js +++ b/screen/send/ScanQRCode.js @@ -91,7 +91,7 @@ const ScanQRCode = () => { const navigation = useNavigation(); const route = useRoute(); const showFileImportButton = route.params.showFileImportButton || false; - const { launchedBy, onBarScanned } = route.params; + const { launchedBy, onBarScanned, onDismiss } = route.params; const scannedCache = {}; const { colors } = useTheme(); const isFocused = useIsFocused(); @@ -239,6 +239,7 @@ const ScanQRCode = () => { } else { navigation.goBack(); } + if (onDismiss) onDismiss(); }; const handleCameraStatusChange = event => { diff --git a/screen/send/create.js b/screen/send/create.js index 1cfe93cb2..2395f96d4 100644 --- a/screen/send/create.js +++ b/screen/send/create.js @@ -28,6 +28,7 @@ import Privacy from '../../blue_modules/Privacy'; import { BitcoinUnit } from '../../models/bitcoinUnits'; import loc from '../../loc'; import { BlueCurrentTheme } from '../../components/themes'; +import { DynamicQRCode } from '../../components/DynamicQRCode'; const currency = require('../../blue_modules/currency'); export default class SendCreate extends Component { @@ -45,6 +46,8 @@ export default class SendCreate extends Component { satoshiPerByte: props.route.params.satoshiPerByte, wallet: props.route.params.wallet, feeSatoshi: props.route.params.feeSatoshi, + showAnimatedQr: props.route.params.showAnimatedQr ?? false, + psbt: props.route.params.psbt, }; } @@ -137,6 +140,7 @@ export default class SendCreate extends Component { + {this.state.showAnimatedQr && this.state.psbt ? : null} {loc.send.create_this_is_hex} @@ -206,7 +210,6 @@ const styles = StyleSheet.create({ }, root: { flex: 1, - paddingTop: 19, backgroundColor: BlueCurrentTheme.colors.elevated, }, card: { diff --git a/screen/send/details.js b/screen/send/details.js index ce6e5abb8..23fd19274 100644 --- a/screen/send/details.js +++ b/screen/send/details.js @@ -51,6 +51,7 @@ import { BlueStorageContext } from '../../blue_modules/storage-context'; const currency = require('../../blue_modules/currency'); const prompt = require('../../blue_modules/prompt'); const fs = require('../../blue_modules/fs'); +const scanqr = require('../../helpers/scan-qr'); const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/; @@ -1109,6 +1110,48 @@ export default class SendDetails extends Component { ); }; + handlePsbtSign = async () => { + this.setState({ isAdvancedTransactionOptionsVisible: false, isLoading: true }); + await new Promise(resolve => setTimeout(resolve, 100)); // sleep for animations + const scannedData = await scanqr(this.props.navigation.navigate, this.props.route.name); + if (!scannedData) return this.setState({ isLoading: false }); + + /** @type {HDSegwitBech32Wallet} */ + const wallet = this.state.fromWallet; + + let tx; + let psbt; + try { + psbt = bitcoin.Psbt.fromBase64(scannedData); + tx = wallet.cosignPsbt(psbt).tx; + } catch (e) { + alert(e.message); + return; + } finally { + this.setState({ isLoading: false }); + } + + if (!tx) return this.setState({ isLoading: false }); + + // we need to remove change address from recipients, so that Confirm screen show more accurate info + const changeAddresses = []; + for (let c = 0; c < wallet.next_free_change_address_index + wallet.gap_limit; c++) { + changeAddresses.push(wallet._getInternalAddressByIndex(c)); + } + const recipients = psbt.txOutputs.filter(({ address }) => !changeAddresses.includes(address)); + + this.props.navigation.navigate('CreateTransaction', { + fee: new BigNumber(psbt.getFee()).dividedBy(100000000).toNumber(), + feeSatoshi: psbt.getFee(), + wallet, + tx: tx.toHex(), + recipients, + satoshiPerByte: psbt.getFeeRate(), + showAnimatedQr: true, + psbt, + }); + }; + hideAdvancedTransactionOptionsModal = () => { Keyboard.dismiss(); this.setState({ isAdvancedTransactionOptionsVisible: false }); @@ -1203,6 +1246,15 @@ export default class SendDetails extends Component { component={TouchableOpacity} onPress={this.handleCoinControl} /> + {this.state.fromWallet.allowCosignPsbt() && ( + + )} diff --git a/screen/wallets/transactions.js b/screen/wallets/transactions.js index 7f00bd471..a3ea055b6 100644 --- a/screen/wallets/transactions.js +++ b/screen/wallets/transactions.js @@ -492,7 +492,7 @@ const WalletTransactions = () => { } else { Alert.alert( loc.wallets.details_title, - loc.transactions.enable_hw, + loc.transactions.enable_offline_signing, [ { text: loc._.ok, diff --git a/tests/e2e/bluewallet.spec.js b/tests/e2e/bluewallet.spec.js index a37d528bc..e589851fc 100644 --- a/tests/e2e/bluewallet.spec.js +++ b/tests/e2e/bluewallet.spec.js @@ -361,7 +361,7 @@ describe('BlueWallet UI Tests', () => { process.env.TRAVIS && require('fs').writeFileSync(lockFile, '1'); }); - it('can import BIP84 mnemonic, fetch balance & transactions, then create a transaction', async () => { + it('can import BIP84 mnemonic, fetch balance & transactions, then create a transaction; then cosign', async () => { const lockFile = '/tmp/travislock.' + hashIt(jasmine.currentTest.fullName); if (process.env.TRAVIS) { if (require('fs').existsSync(lockFile)) @@ -486,6 +486,28 @@ describe('BlueWallet UI Tests', () => { assert.strictEqual(transaction.outs.length, 1, 'should be single output, no change'); assert.ok(transaction.outs[0].value > 100000); + // now, testing cosign psbt: + + await device.pressBack(); + await device.pressBack(); + await element(by.id('SendButton')).tap(); + await element(by.id('advancedOptionsMenuButton')).tap(); + await element(by.id('PsbtSign')).tap(); + + // tapping 10 times invisible button is a backdoor: + for (let c = 0; c <= 5; c++) { + await element(by.id('ScanQrBackdoorButton')).tap(); + await sleep(1000); + } + // 1 input, 2 outputs. wallet can fully sign this tx + const psbt = + 'cHNidP8BAFICAAAAAXYa7FEQBAQ2X0B48aHHKKgzkVuHfQ2yCOi3v9RR0IqlAQAAAAAAAACAAegDAAAAAAAAFgAUSnH40G+jiJfreeRb36cs641KFm8AAAAAAAEBH5YVAAAAAAAAFgAUTKHjDm4OJQSbvy9uzyLYi5i5XIoiBgMQcGrP5TIMrdvb73yB4WnZvkPzKr1EzJXJYBHWmlPJZRgAAAAAVAAAgAAAAIAAAACAAQAAAD4AAAAAAA=='; + await element(by.id('scanQrBackdoorInput')).replaceText(psbt); + await element(by.id('scanQrBackdoorOkButton')).tap(); + + // this is fully-signed tx, "this is tx hex" help text should appear + await yo('DynamicCode'); + process.env.TRAVIS && require('fs').writeFileSync(lockFile, '1'); }); diff --git a/tests/unit/cosign.test.js b/tests/unit/cosign.test.js new file mode 100644 index 000000000..a5ede6208 --- /dev/null +++ b/tests/unit/cosign.test.js @@ -0,0 +1,202 @@ +/* global it, describe */ +import assert from 'assert'; +import * as bitcoin from 'bitcoinjs-lib'; +import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet } from '../../class'; + +describe('AbstractHDElectrumWallet.cosign', () => { + it('different descendants of AbstractHDElectrumWallet can cosign one transaction', async () => { + if (!process.env.HD_MNEMONIC || !process.env.HD_MNEMONIC_BIP49) { + console.error('process.env.HD_MNEMONIC or HD_MNEMONIC_BIP49 not set, skipped'); + return; + } + + const w1 = new HDLegacyP2PKHWallet(); + w1.setSecret(process.env.HD_MNEMONIC); + assert.ok(w1.validateMnemonic()); + const w1Utxo = [ + { + height: 554830, + value: 10000, + address: '186FBQmCV5W1xY7ywaWtTZPAQNciVN8Por', + vout: 0, + txid: '4f65c8cb159585c00d4deba9c5b36a2bcdfb1399a561114dcf6f2d0c1174bc5f', + amount: 10000, + confirmations: 1, + txhex: + '01000000000101e8d98effbb4fba4f0a89bcf217eb5a7e2f8efcae44f32ecacbc5d8cc3ce683c301000000171600148ba6d02e74c0a6e000e8b174eb2ed44e5ea211a6ffffffff0510270000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac204e0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac30750000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac409c0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac204716000000000017a914e286d58e53f9247a4710e51232cce0686f16873c8702483045022100af3800cd8171f154785cf13f46c092f61c1668f97db432bb4e7ed7bc812a8c6d022051bddca1eaf1ad8b5f3bd0ccde7447e56fd3c8709e5906f02ec6326e9a5b2ff30121039a421d5eb7c9de6590ae2a471cb556b60de8c6b056beb907dbdc1f5e6092f58800000000', + }, + ]; + + const w2 = new HDSegwitBech32Wallet(); + w2.setSecret(process.env.HD_MNEMONIC); + assert.ok(w2.validateMnemonic()); + const w2Utxo = [ + { + height: 563077, + value: 50000, + address: 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh', + vout: 1, + txid: 'ad00a92409d8982a1d7f877056dbed0c4337d2ebab70b30463e2802279fb936d', + amount: 50000, + confirmations: 1, + }, + ]; + + const w3 = new HDSegwitP2SHWallet(); + w3.setSecret(process.env.HD_MNEMONIC_BIP49); + assert.ok(w3.validateMnemonic()); + const w3Utxo = [ + { + height: 591862, + value: 26000, + address: '3C5iv2Hp6nfuhkfTZibb7GJPkXj367eurD', + txid: 'fe9c4d1b240f270e9cda227c48e29b2983cb26aaab183b34454871d5d9acc987', + vout: 0, + amount: 26000, + confirmations: 1, + }, + ]; + + // now let's create transaction with 3 different inputs for each wallet and one output + // maybe in future bitcoin-js will support psbt.join() and this test can be simplified to: + // const { psbt } = w1.createTransaction(w1Utxo, [{address: w1._getExternalAddressByIndex(0)}], 1, w1._getInternalAddressByIndex(0), undefined, true) + // const { psbt:psbt2 } = w2.createTransaction(w2Utxo, [{address: w2._getExternalAddressByIndex(0)}], 1, w2._getInternalAddressByIndex(0), undefined, true) + // const { psbt:psbt3 } = w3.createTransaction(w3Utxo, [{address: w3._getExternalAddressByIndex(0)}], 1, w3._getInternalAddressByIndex(0), undefined, true) + // psbt.join(psbt2, psbt3) + // but for now, we will construct psbt by hand + + const sequence = HDSegwitBech32Wallet.defaultRBFSequence; + const masterFingerprintBuffer = Buffer.from([0x00, 0x00, 0x00, 0x00]); + const psbt = new bitcoin.Psbt(); + + // add one input from each wallet + { + // w1 + const input = w1Utxo[0]; + const pubkey = w1._getPubkeyByAddress(input.address); + const path = w1._getDerivationPathByAddress(input.address, 44); + + psbt.addInput({ + hash: input.txid, + index: input.vout, + sequence, + bip32Derivation: [ + { + masterFingerprint: masterFingerprintBuffer, + path, + pubkey, + }, + ], + // non-segwit inputs now require passing the whole previous tx as Buffer + nonWitnessUtxo: Buffer.from(input.txhex, 'hex'), + }); + } + + { + // w2 + const input = w2Utxo[0]; + const pubkey = w2._getPubkeyByAddress(input.address); + const path = w2._getDerivationPathByAddress(input.address); + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); + + psbt.addInput({ + hash: input.txid, + index: input.vout, + sequence, + bip32Derivation: [ + { + masterFingerprint: masterFingerprintBuffer, + path, + pubkey, + }, + ], + witnessUtxo: { + script: p2wpkh.output, + value: input.value, + }, + }); + } + + { + // w3 + const input = w3Utxo[0]; + const pubkey = w3._getPubkeyByAddress(input.address); + const path = w3._getDerivationPathByAddress(input.address, 49); + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); + const p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh }); + + psbt.addInput({ + hash: input.txid, + index: input.vout, + sequence, + bip32Derivation: [ + { + masterFingerprint: masterFingerprintBuffer, + path, + pubkey, + }, + ], + witnessUtxo: { + script: p2sh.output, + value: input.amount || input.value, + }, + redeemScript: p2wpkh.output, + }); + } + + // send all to the one output + psbt.addOutput({ + address: w1._getExternalAddressByIndex(0), + value: 10000, + }); + + assert.strictEqual( + psbt.toBase64(), + 'cHNidP8BAKcCAAAAA1+8dBEMLW/PTRFhpZkT+80rarPFqetNDcCFlRXLyGVPAAAAAAAAAACAbZP7eSKA4mMEs3Cr69I3Qwzt21Zwh38dKpjYCSSpAK0BAAAAAAAAAICHyazZ1XFIRTQ7GKuqJsuDKZviSHwi2pwOJw8kG02c/gAAAAAAAAAAgAEQJwAAAAAAABl2qRRNxsv2TfmrEGzugSx1AZYLk+khd4isAAAAAAABAP1gAQEAAAAAAQHo2Y7/u0+6TwqJvPIX61p+L478rkTzLsrLxdjMPOaDwwEAAAAXFgAUi6bQLnTApuAA6LF06y7UTl6iEab/////BRAnAAAAAAAAGXapFE3Gy/ZN+asQbO6BLHUBlguT6SF3iKwgTgAAAAAAABl2qRS8Lba3TI25sYhxHc7dUR5qMFYD9YisMHUAAAAAAAAZdqkUTcbL9k35qxBs7oEsdQGWC5PpIXeIrECcAAAAAAAAGXapFLwttrdMjbmxiHEdzt1RHmowVgP1iKwgRxYAAAAAABepFOKG1Y5T+SR6RxDlEjLM4GhvFoc8hwJIMEUCIQCvOADNgXHxVHhc8T9GwJL2HBZo+X20MrtOfte8gSqMbQIgUb3coerxrYtfO9DM3nRH5W/TyHCeWQbwLsYybppbL/MBIQOaQh1et8neZZCuKkcctVa2DejGsFa+uQfb3B9eYJL1iAAAAAAiBgMW6EolVvMKGZVBYz9d2meHcQzKsmdxtwhPTJ4RBPR2ZxgAAAAALAAAgAAAAIAAAACAAAAAAAAAAAAAAQEfUMMAAAAAAAAWABRdVlN9SNyYZGw0RlmtnzqBcHoXxSIGAnqv8b0nSBLQEkZL4l3AZYcoektXhnjljJSaEzufuTx/GAAAAABUAACAAAAAgAAAAIAAAAAAAQAAAAABASCQZQAAAAAAABepFHH8oGeDfo3SSYkgJqW15AVPiyXhhwEEFgAUojm2oMvHqtwud2Q942MGphZ/rRUiBgICrDvRWeVNwx5lhCrV+aELTrAk6DhkoxmyfeZe4IsqORgAAAAAMQAAgAAAAIAAAACAAAAAAAAAAAAAAA==', + ); + + // now signing this psbt usign wallets one by one + // because BW users will pass psbt from one device to another base64 encoded, let's do the same + + let tx; + + assert.strictEqual(w1.calculateHowManySignaturesWeHaveFromPsbt(psbt), 0); + tx = w1.cosignPsbt(psbt).tx; + assert.strictEqual(w1.calculateHowManySignaturesWeHaveFromPsbt(psbt), 1); + assert.strictEqual(tx, false); // not yet fully-signed + + tx = w2.cosignPsbt(psbt).tx; + assert.strictEqual(w2.calculateHowManySignaturesWeHaveFromPsbt(psbt), 2); + assert.strictEqual(tx, false); // not yet fully-signed + + tx = w3.cosignPsbt(psbt).tx; // GREAT SUCCESS! + assert.strictEqual(w3.calculateHowManySignaturesWeHaveFromPsbt(psbt), 3); + assert.ok(tx); + + assert.strictEqual( + tx.toHex(), + '020000000001035fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f000000006a473044022041df555e5f6a3769fafdbe23bfe29de84a1341b8fd85ffd279e238309c5df07702207cf1628b35ccacdb7d34e20fd46a3bc8adc0b1bd3b63249a3a4442b5a993d73501210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667000000806d93fb792280e26304b370abebd237430ceddb5670877f1d2a98d80924a900ad01000000000000008087c9acd9d5714845343b18abaa26cb83299be2487c22da9c0e270f241b4d9cfe0000000017160014a239b6a0cbc7aadc2e77643de36306a6167fad15000000800110270000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac0002483045022100efe66403aba1441041dfdeff1f24b5e89ab5728ae7ceb9edb264eee004d5883c02207bf03cb611c9322086ac75fa97c374e9540c911359ede4f62de3c94c429ea2320121027aaff1bd274812d012464be25dc06587287a4b578678e58c949a133b9fb93c7f0247304402207a99c115f0b372d151caf991bb5af9f880e7d87625eeb4233fefa671489ed8e702200e5675b92e4e22b2fe37f563b2a0e75fb81def5a6efb431c7ca3b654ef63fe5801210202ac3bd159e54dc31e65842ad5f9a10b4eb024e83864a319b27de65ee08b2a3900000000', + ); + }); + + it('HDSegwitBech32Wallet can cosign psbt with correct fingerprint', async () => { + if (!process.env.MNEMONICS_COBO) { + console.error('process.env.HD_MNEMONIC or HD_MNEMONIC_BIP49 not set, skipped'); + return; + } + + const w = new HDSegwitBech32Wallet(); + w.setSecret(process.env.MNEMONICS_COBO); + assert.ok(w.validateMnemonic()); + + const psbtWithCorrectFpBase64 = + 'cHNidP8BAFUCAAAAAfsmeQ1mJJqC9cD0DxDRFQoG2hvU6S4koB0jl+8TEDKjAAAAAAD/////AQpfAAAAAAAAGXapFBkSnVPmMZuvGdugWb6tFm35Crj1iKwAAAAAAAEBH8p3AAAAAAAAFgAUf8fcrCg92McSzWkmw+UAluC4IjsiBgLfsmddhS3oxlnlGrUPDBVoVHSMa8RcXlGsyhfc8CcGpRjTfq2IVAAAgAAAAIAAAACAAAAAAAQAAAAAAA=='; + const psbtWithCorrectFp = bitcoin.Psbt.fromBase64(psbtWithCorrectFpBase64); + + assert.strictEqual(w.calculateHowManySignaturesWeHaveFromPsbt(psbtWithCorrectFp), 0); + + const { tx } = w.cosignPsbt(psbtWithCorrectFp); + assert.ok(tx && tx.toHex()); + assert.strictEqual(w.calculateHowManySignaturesWeHaveFromPsbt(psbtWithCorrectFp), 1); + }); +});