diff --git a/class/deeplink-schema-match.js b/class/deeplink-schema-match.js index 8111d42de..d98325840 100644 --- a/class/deeplink-schema-match.js +++ b/class/deeplink-schema-match.js @@ -41,7 +41,7 @@ class DeeplinkSchemaMatch { event.url = event.url.substring(11); } - if (DeeplinkSchemaMatch.isPossiblyPSBTFile(event.url)) { + if (DeeplinkSchemaMatch.isPossiblySignedPSBTFile(event.url)) { RNFS.readFile(event.url) .then(file => { if (file) { @@ -203,13 +203,23 @@ class DeeplinkSchemaMatch { } static isTXNFile(filePath) { - return filePath.toLowerCase().startsWith('file:') && filePath.toLowerCase().endsWith('.txn'); + return ( + (filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) && + filePath.toLowerCase().endsWith('.txn') + ); + } + + static isPossiblySignedPSBTFile(filePath) { + return ( + (filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) && + filePath.toLowerCase().endsWith('-signed.psbt') + ); } static isPossiblyPSBTFile(filePath) { return ( (filePath.toLowerCase().startsWith('file:') || filePath.toLowerCase().startsWith('content:')) && - filePath.toLowerCase().endsWith('-signed.psbt') + filePath.toLowerCase().endsWith('.psbt') ); } diff --git a/loc/en.json b/loc/en.json index fc0bebd74..5be7915cf 100644 --- a/loc/en.json +++ b/loc/en.json @@ -170,7 +170,7 @@ "details_next": "Next", "details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?", "details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?", - "details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.", + "details_no_signed_tx": "The selected file does not contain a transaction that can be imported.", "details_note_placeholder": "note to self", "details_scan": "Scan", "details_total_exceeds_balance": "The sending amount exceeds the available balance.", diff --git a/loc/id_id.json b/loc/id_id.json index b96a81596..1ea5fac5f 100644 --- a/loc/id_id.json +++ b/loc/id_id.json @@ -170,7 +170,7 @@ "details_next": "Next", "details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?", "details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?", - "details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.", + "details_no_signed_tx": "The selected file does not contain a transaction that can be imported.", "details_note_placeholder": "catatan pribadi", "details_scan": "Pindai", "details_total_exceeds_balance": "Jumlah yang dikirim melebihi saldo.", diff --git a/loc/jp_jp.json b/loc/jp_jp.json index aa19a3570..7dea8499b 100644 --- a/loc/jp_jp.json +++ b/loc/jp_jp.json @@ -170,7 +170,7 @@ "details_next": "次", "details_no_maximum": "選択したウォレットは、最大残高の自動計算に対応していません。このウォレットを選択してもよろしいですか?", "details_no_multiple": "選択したウォレットは、複数の受信者へのビットコインの送信をサポートしていません。このウォレットを選択してもよろしいですか?", - "details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.", + "details_no_signed_tx": "The selected file does not contain a transaction that can be imported.", "details_note_placeholder": "ラベル", "details_scan": "読取り", "details_total_exceeds_balance": "送金額が利用可能残額を超えています。", diff --git a/loc/nl_nl.json b/loc/nl_nl.json index e76ff5a4f..540283eff 100644 --- a/loc/nl_nl.json +++ b/loc/nl_nl.json @@ -170,7 +170,7 @@ "details_next": "Volgende", "details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?", "details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?", - "details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.", + "details_no_signed_tx": "The selected file does not contain a transaction that can be imported.", "details_note_placeholder": "notitie voor mezelf", "details_scan": "Scan", "details_total_exceeds_balance": "Het verzendingsbedrag overschrijdt het beschikbare saldo.", diff --git a/loc/sk_sk.json b/loc/sk_sk.json index 2f02181be..ec84214e9 100644 --- a/loc/sk_sk.json +++ b/loc/sk_sk.json @@ -170,7 +170,7 @@ "details_next": "Ďalej", "details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?", "details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?", - "details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.", + "details_no_signed_tx": "The selected file does not contain a transaction that can be imported.", "details_note_placeholder": "poznámka pre seba", "details_scan": "Skenovať", "details_total_exceeds_balance": "Čiastka, ktorú chcete poslať, presahuje dostupný zostatok.", diff --git a/loc/sv_se.json b/loc/sv_se.json index 869b41091..585dd9c30 100644 --- a/loc/sv_se.json +++ b/loc/sv_se.json @@ -170,7 +170,7 @@ "details_next": "Nästa", "details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?", "details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?", - "details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.", + "details_no_signed_tx": "The selected file does not contain a transaction that can be imported.", "details_note_placeholder": "egen notering", "details_scan": "Skanna", "details_total_exceeds_balance": "Beloppet överstiger plånbokens tillgängliga belopp", diff --git a/loc/th_th.json b/loc/th_th.json index f0bebc954..6d6b7417f 100644 --- a/loc/th_th.json +++ b/loc/th_th.json @@ -170,7 +170,7 @@ "details_next": "ถัดไป", "details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?", "details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?", - "details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.", + "details_no_signed_tx": "The selected file does not contain a transaction that can be imported.", "details_note_placeholder": "หมายเหตุถึงตัวท่านเอง", "details_scan": "สแกน", "details_total_exceeds_balance": "จำนวนเงินที่จะส่งเกินเงินที่มี.", diff --git a/loc/tr_tr.json b/loc/tr_tr.json index 7d32ccfdc..3e8023f30 100644 --- a/loc/tr_tr.json +++ b/loc/tr_tr.json @@ -170,7 +170,7 @@ "details_next": "Next", "details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?", "details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?", - "details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.", + "details_no_signed_tx": "The selected file does not contain a transaction that can be imported.", "details_note_placeholder": "kendime not", "details_scan": "Tara", "details_total_exceeds_balance": "Gönderme miktarı mevcut bakiyeyi aşıyor.", diff --git a/loc/zh_cn.json b/loc/zh_cn.json index a53e07549..afb436cde 100644 --- a/loc/zh_cn.json +++ b/loc/zh_cn.json @@ -170,7 +170,7 @@ "details_next": "Next", "details_no_maximum": "The selected wallet does not support automatic maximum balance calculation. Are you sure to want to select this wallet?", "details_no_multiple": "The selected wallet does not support sending Bitcoin to multiple recipients. Are you sure to want to select this wallet?", - "details_no_signed_tx": "The selected file does not contain a signed transaction that can be imported.", + "details_no_signed_tx": "The selected file does not contain a transaction that can be imported.", "details_note_placeholder": "消息", "details_scan": "扫描", "details_total_exceeds_balance": "余额不足", diff --git a/screen/send/details.js b/screen/send/details.js index 2612ba6b7..ddef5b4e4 100644 --- a/screen/send/details.js +++ b/screen/send/details.js @@ -772,32 +772,51 @@ export default class SendDetails extends Component { ); }; + /** + * watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code + * so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask + * user whether he wants to broadcast it. + * alternatively, user can export psbt file, sign it externally and then import it + * + * @returns {Promise} + */ importTransaction = async () => { + if (this.state.fromWallet.type !== WatchOnlyWallet.type) { + alert('Error: importing transaction in non-watchonly wallet (this should never happen)'); + return; + } + try { const res = await DocumentPicker.pick({ type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles], }); - if (DeeplinkSchemaMatch.isPossiblyPSBTFile(res.uri)) { + + if (DeeplinkSchemaMatch.isPossiblySignedPSBTFile(res.uri)) { + // we assume that transaction is already signed, so all we have to do is get txhex and pass it to next screen + // so user can broadcast: const file = await RNFS.readFile(res.uri, 'ascii'); - const bufferDecoded = Buffer.from(file, 'ascii').toString('base64'); - if (bufferDecoded) { - if (this.state.fromWallet.type === WatchOnlyWallet.type) { - // watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code - // so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask - // user whether he wants to broadcast it. - // alternatively, user can export psbt file, sign it externally and then import it - this.props.navigation.navigate('PsbtWithHardwareWallet', { - memo: this.state.memo, - fromWallet: this.state.fromWallet, - psbt: file, - }); - this.setState({ isLoading: false }); - return; - } - } else { - throw new Error(); - } + const psbt = bitcoin.Psbt.fromBase64(file); + const txhex = psbt.extractTransaction().toHex(); + + this.props.navigation.navigate('PsbtWithHardwareWallet', { + memo: this.state.memo, + fromWallet: this.state.fromWallet, + txhex, + }); + this.setState({ isLoading: false, isAdvancedTransactionOptionsVisible: false }); + } else if (DeeplinkSchemaMatch.isPossiblyPSBTFile(res.uri)) { + // looks like transaction is UNsigned, so we construct PSBT object and pass to next screen + // so user can do smth with it: + const file = await RNFS.readFile(res.uri, 'ascii'); + const psbt = bitcoin.Psbt.fromBase64(file); + this.props.navigation.navigate('PsbtWithHardwareWallet', { + memo: this.state.memo, + fromWallet: this.state.fromWallet, + psbt, + }); + this.setState({ isLoading: false, isAdvancedTransactionOptionsVisible: false }); } else if (DeeplinkSchemaMatch.isTXNFile(res.uri)) { + // plain text file with txhex ready to broadcast const file = await RNFS.readFile(res.uri, 'ascii'); this.props.navigation.navigate('PsbtWithHardwareWallet', { memo: this.state.memo, @@ -805,7 +824,8 @@ export default class SendDetails extends Component { txhex: file, }); this.setState({ isLoading: false, isAdvancedTransactionOptionsVisible: false }); - return; + } else { + alert('Unrecognized file format'); } } catch (err) { if (!DocumentPicker.isCancel(err)) { diff --git a/screen/send/psbtWithHardwareWallet.js b/screen/send/psbtWithHardwareWallet.js index 3fc3e89c0..5d74f3204 100644 --- a/screen/send/psbtWithHardwareWallet.js +++ b/screen/send/psbtWithHardwareWallet.js @@ -173,7 +173,7 @@ export default class PsbtWithHardwareWallet extends Component { } if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { // this looks like NOT base64, so maybe its transaction's hex - this.setState({ txhex: ret.data }, () => this.props.navigation.dangerouslyGetParent().pop()); + this.setState({ txhex: ret.data }); return; } try { @@ -201,11 +201,20 @@ export default class PsbtWithHardwareWallet extends Component { } static getDerivedStateFromProps(nextProps, prevState) { + if (!prevState.psbt && !nextProps.route.params.txhex) { + alert('There is no transaction signing in progress'); + return { + ...prevState, + isLoading: true, + }; + } + const deepLinkPSBT = nextProps.route.params.deepLinkPSBT; const txhex = nextProps.route.params.txhex; if (deepLinkPSBT) { + const psbt = bitcoin.Psbt.fromBase64(deepLinkPSBT); try { - const Tx = prevState.fromWallet.combinePsbt(prevState.psbt, deepLinkPSBT); + const Tx = prevState.fromWallet.combinePsbt(prevState.psbt, psbt); return { ...prevState, txhex: Tx.toHex(), diff --git a/tests/unit/deeplink-schema-match.test.js b/tests/unit/deeplink-schema-match.test.js index ee5280b4a..e4348621e 100644 --- a/tests/unit/deeplink-schema-match.test.js +++ b/tests/unit/deeplink-schema-match.test.js @@ -220,4 +220,32 @@ describe('unit - DeepLinkSchemaMatch', function () { }); assert.strictEqual(encoded, 'bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar'); }); + + it('recognizes files', () => { + // txn files: + assert.ok(DeeplinkSchemaMatch.isTXNFile('file://com.android.externalstorage.documents/document/081D-1403%3Atxhex.txn')); + assert.ok(!DeeplinkSchemaMatch.isPossiblySignedPSBTFile('file://com.android.externalstorage.documents/document/081D-1403%3Atxhex.txn')); + + assert.ok(DeeplinkSchemaMatch.isTXNFile('content://com.android.externalstorage.documents/document/081D-1403%3Atxhex.txn')); + assert.ok( + !DeeplinkSchemaMatch.isPossiblySignedPSBTFile('content://com.android.externalstorage.documents/document/081D-1403%3Atxhex.txn'), + ); + + // psbt files (signed): + assert.ok( + DeeplinkSchemaMatch.isPossiblySignedPSBTFile( + 'content://com.android.externalstorage.documents/document/081D-1403%3Atxhex-signed.psbt', + ), + ); + assert.ok( + DeeplinkSchemaMatch.isPossiblySignedPSBTFile('file://com.android.externalstorage.documents/document/081D-1403%3Atxhex-signed.psbt'), + ); + + assert.ok(!DeeplinkSchemaMatch.isTXNFile('content://com.android.externalstorage.documents/document/081D-1403%3Atxhex-signed.psbt')); + assert.ok(!DeeplinkSchemaMatch.isTXNFile('file://com.android.externalstorage.documents/document/081D-1403%3Atxhex-signed.psbt')); + + // psbt files (unsigned): + assert.ok(DeeplinkSchemaMatch.isPossiblyPSBTFile('content://com.android.externalstorage.documents/document/081D-1403%3Atxhex.psbt')); + assert.ok(DeeplinkSchemaMatch.isPossiblyPSBTFile('file://com.android.externalstorage.documents/document/081D-1403%3Atxhex.psbt')); + }); });