btc: invoice error handling

This commit is contained in:
ixv 2021-05-26 12:19:33 -07:00
parent 768b47985d
commit 40d69b5a02
13 changed files with 170 additions and 64 deletions

View File

@ -334,7 +334,8 @@
?~ txbu.poym %.n
=((get-id:txu:bc (decode:txu:bc signed)) ~(get-txid txb:bl u.txbu.poym))
:- ?. tx-match
((slog leaf+"txid didn't match txid in wallet") ~)
%- (slog leaf+"txid didn't match txid in wallet")
[(give-update %error %broadcast-fail)]~
~[(poke-provider [%broadcast-tx signed])]
?. tx-match state
?~ txbu.poym state
@ -486,7 +487,7 @@
=+ fee=~(fee txb:bl u.txbu.poym)
~& >> "{<vb>} vbytes, {<(div fee vb)>} sats/byte, {<fee>} sats fee"
%- (slog [%leaf "PSBT: {<u.pb>}"]~)
[(give-update [%psbt u.pb])]~
[(give-update [%psbt u.pb fee])]~
:: update outgoing payment with a rawtx, if the txid is in poym's txis
::
++ update-poym-txis
@ -629,12 +630,14 @@
%fail-broadcast-tx
?> =(src.bowl our.bowl)
~& >>> "%fail-broadcast-tx"
`state(poym [~ ~])
:_ state(poym [~ ~])
[(give-update %error %broadcast-fail)]~
::
%succeed-broadcast-tx
?> =(src.bowl our.bowl)
~& > "%succeed-broadcast-tx"
:_ state
:- (give-update %broadcast-success ~)
?~ prov ~
:- (poke-provider [%tx-info txid.intr])
?~ txbu.poym ~

View File

@ -104,13 +104,14 @@
%initial (initial upd)
%change-provider (change-provider upd)
%change-wallet (change-wallet upd)
%psbt s+pb.upd
%psbt (psbt upd)
%btc-state (btc-state btc-state.upd)
%new-tx (hest hest.upd)
%cancel-tx (hexb txid.upd)
%new-address (address address.upd)
%balance (balance balance.upd)
%error s+error.upd
%broadcast-success ~
==
::
++ initial
@ -142,6 +143,15 @@
history+(history history.upd)
==
::
++ psbt
|= upd=update:btc-wallet
?> ?=(%psbt -.upd)
^- json
%- pairs
:~ pb+s+pb.upd
fee+(numb fee.upd)
==
::
++ balance
|= b=(unit [p=@ q=@])
^- json

View File

@ -124,6 +124,7 @@
%no-dust
%tx-being-signed
%insufficient-balance
%broadcast-fail
==
:: data to send to the frontend
::
@ -136,9 +137,10 @@
=btc-state
address=(unit address)
==
[%broadcast-success ~]
[%change-provider provider=(unit provider)]
[%change-wallet wallet=(unit xpub) balance=(unit [p=sats q=sats]) =history]
[%psbt pb=@t]
[%psbt pb=@t fee=sats]
[%btc-state =btc-state]
[%new-tx =hest]
[%cancel-tx =txid]

View File

@ -68,6 +68,7 @@ export default class Balance extends Component {
<>
{this.state.sending ?
<Send
state={this.props.state}
api={api}
psbt={this.props.state.psbt}
currencyRates={this.props.state.currencyRates}
@ -80,7 +81,7 @@ export default class Balance extends Component {
error={this.props.state.error}
stopSending={() => {
this.setState({sending: false});
store.handleEvent({data: {psbt: '', error: ''}});
store.handleEvent({data: {psbt: '', fee: 0, error: '', "broadcast-fail": null}});
}}
/> :
<Col

View File

@ -7,12 +7,14 @@ import {
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import * as bitcoin from 'bitcoinjs-lib';
import * as kg from 'urbit-key-generation';
import Sent from './sent.js'
import Error from './error.js'
import { satsToCurrency } from '../../lib/util.js';
@ -24,10 +26,10 @@ export default class BridgeInvoice extends Component {
super(props);
this.state = {
txHex: 'm',
txHex: '',
ready: false,
error: false,
sent: false,
error: this.props.state.error,
broadcasting: false,
};
this.checkTxHex = this.checkTxHex.bind(this);
@ -46,42 +48,56 @@ export default class BridgeInvoice extends Component {
window.open('https://bridge.urbit.org/?kind=btc&utx=' + this.props.psbt);
}
sendBitcoin(hex) {
componentDidUpdate(prevProps){
if (this.state.broadcasting) {
if (this.state.error !== '') {
this.setState({broadcasting: false});
}
}
if (prevProps.state.error !== this.props.state.error) {
this.setState({error: this.props.state.error});
}
}
sendBitcoin(hex) {
try {
bitcoin.Transaction.fromHex(hex)
this.broadCastTx(hex).then(res => this.setState({sent: true}));
this.broadCastTx(hex)
this.setState({broadcasting: true});
}
catch(e) {
this.setState({error: true});
this.setState({error: 'invalid-signed', broadcasting: false});
}
}
checkTxHex(e){
let txHex = e.target.value;
let ready = (txHex.length > 0);
let error = false;
let error = '';
this.setState({txHex, ready, error});
}
render() {
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates } = this.props;
const { sent, error, txHex } = this.state;
const { error, txHex } = this.state;
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (error) {
if (error !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
console.log('bridge invoice', error);
return (
<>
{ sent ?
{ this.props.state.broadcastSuccess ?
<Sent
payee={payee}
stopSending={stopSending}
@ -166,19 +182,18 @@ export default class BridgeInvoice extends Component {
style={{'line-height': '4'}}
onChange={this.checkTxHex}
/>
{error &&
{ (error !== '') &&
<Row>
<Text
<Error
error={error}
fontSize='14px'
color='red'
mt={2}>
Invalid signed bitcoin transaction
</Text>
mt={2}/>
</Row>
}
<Row
flexDirection='row-reverse'
mt={4}
alignItems="center"
>
<Button
primary
@ -192,6 +207,7 @@ export default class BridgeInvoice extends Component {
disabled={!this.state.ready || error}
style={{cursor: (this.state.ready && !error) ? "pointer" : "default"}}
/>
{this.state.broadcasting ? <LoadingSpinner mr={3}/> : null}
</Row>
</Col>
}

View File

@ -17,6 +17,15 @@ const errorToString = (error) => {
if (error === 'insufficient-balance') {
return 'Insufficient confirmed balance';
}
if (error === 'broadcast-fail') {
return 'Transaction broadcast failed';
}
if (error === 'invalid-master-ticket') {
return 'Invalid master ticket';
}
if (error === 'invalid-signed') {
return 'Invalid signed bitcoin transaction';
}
}
export default function Error(props) {

View File

@ -7,6 +7,7 @@ import {
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import * as bitcoin from 'bitcoinjs-lib';
@ -14,13 +15,39 @@ import * as kg from 'urbit-key-generation';
import * as bip39 from 'bip39';
import Sent from './sent.js'
import { patp2dec, isValidPatq } from 'urbit-ob';
import { satsToCurrency } from '../../lib/util.js';
import Error from './error.js';
window.bitcoin = bitcoin;
window.kg = kg;
window.bip39 = bip39;
const BITCOIN_MAINNET_INFO = {
messagePrefix: '\x18Bitcoin Signed Message:\n',
bech32: 'bc',
bip32: {
public: 0x04b24746,
private: 0x04b2430c,
},
pubKeyHash: 0x00,
scriptHash: 0x05,
wif: 0x80,
};
const BITCOIN_TESTNET_INFO = {
messagePrefix: '\x18Bitcoin Signed Message:\n',
bech32: 'tb',
bip32: {
public: 0x045f1cf6,
private: 0x045f18bc,
},
pubKeyHash: 0x6f,
scriptHash: 0xc4,
wif: 0xef,
};
export default class Invoice extends Component {
constructor(props) {
super(props);
@ -28,8 +55,9 @@ export default class Invoice extends Component {
this.state = {
masterTicket: '',
ready: false,
error: false,
error: this.props.state.error,
sent: false,
broadcasting: false,
};
this.checkTicket = this.checkTicket.bind(this);
@ -37,6 +65,14 @@ export default class Invoice extends Component {
this.sendBitcoin = this.sendBitcoin.bind(this);
}
componentDidUpdate(prevProps, prevState) {
if (this.state.broadcasting) {
if (this.state.error !== '') {
this.setState({broadcasting: false});
}
}
}
broadCastTx(psbtHex) {
let command = {
'broadcast-tx': psbtHex
@ -45,42 +81,58 @@ export default class Invoice extends Component {
}
sendBitcoin(ticket, psbt) {
const mnemonic = kg.deriveNodeSeed(ticket, 'bitcoin');
const seed = bip39.mnemonicToSeed(mnemonic);
const hd = bitcoin.bip32.fromSeed(seed);
const newPsbt = bitcoin.Psbt.fromBase64(psbt);
this.setState({broadcasting: true});
kg.generateWallet({ ticket, ship: parseInt(patp2dec('~' + window.ship)) })
.then(urbitWallet => {
const { xpub } = this.props.network === 'testnet'
? urbitWallet.bitcoinTestnet.keys
: urbitWallet.bitcoinMainnet.keys;
try {
const hex =
newPsbt.data.inputs
.reduce((psbt, input, idx) => {
const path = input.bip32Derivation[0].path
const prv = hd.derivePath(path).privateKey;
return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
const { xprv: zprv } = urbitWallet.bitcoinMainnet.keys;
const { xprv: vprv } = urbitWallet.bitcoinTestnet.keys;
}, newPsbt)
.finalizeAllInputs()
.extractTransaction()
.toHex();
const isTestnet = (this.props.network === 'testnet');
const derivationPrefix = isTestnet ? "m/84'/1'/0'/" : "m/84'/0'/0'/";
const btcWallet = (isTestnet)
? bitcoin.bip32.fromBase58(vprv, BITCOIN_TESTNET_INFO)
: bitcoin.bip32.fromBase58(zprv, BITCOIN_MAINNET_INFO);
try {
const hex = newPsbt.data.inputs
.reduce((psbt, input, idx) => {
// removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
const path = input.bip32Derivation[0].path
.split(derivationPrefix)
.join('');
const prv = btcWallet.derivePath(path).privateKey;
return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
}, newPsbt)
.finalizeAllInputs()
.extractTransaction()
.toHex();
this.broadCastTx(hex);
}
catch(e) {
this.setState({error: 'invalid-master-ticket', broadcasting: false});
}
});
this.broadCastTx(hex).then(res => this.setState({sent: true}));
}
catch(e) {
this.setState({error: true});
}
}
checkTicket(e){
// TODO: port over bridge ticket validation logic
let masterTicket = e.target.value;
let ready = (masterTicket.length > 0);
let error = false;
let ready = isValidPatq(masterTicket);
let error = (ready) ? '' : 'invalid-master-ticket';
this.setState({masterTicket, ready, error});
}
render() {
const broadcastSuccess = this.props.state.broadcastSuccess;
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates } = this.props;
const { sent, error } = this.state;
@ -88,7 +140,7 @@ export default class Invoice extends Component {
let inputBg = 'white';
let inputBorder = 'lightGray';
if (error) {
if (error !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
@ -96,7 +148,7 @@ export default class Invoice extends Component {
return (
<>
{ sent ?
{ broadcastSuccess ?
<Sent
payee={payee}
stopSending={stopSending}
@ -178,19 +230,19 @@ export default class Invoice extends Component {
borderColor={inputBorder}
onChange={this.checkTicket}
/>
{error &&
{(error !== '') &&
<Row>
<Text
<Error
fontSize='14px'
color='red'
mt={2}>
Invalid master ticket
</Text>
error={error}
mt={2}/>
</Row>
}
<Row
flexDirection='row-reverse'
mt={4}
alignItems="center"
>
<Button
primary
@ -201,9 +253,10 @@ export default class Invoice extends Component {
py='24px'
px='24px'
onClick={() => this.sendBitcoin(this.state.masterTicket, psbt)}
disabled={!this.state.ready || error}
style={{cursor: (this.state.ready && !error) ? "pointer" : "default"}}
disabled={!this.state.ready || error || this.state.broadcasting}
style={{cursor: (this.state.ready && !error && !this.state.broadcasting) ? "pointer" : "default"}}
/>
{ (this.state.broadcasting) ? <LoadingSpinner mr={3}/> : null}
</Row>
</Col>
}

View File

@ -128,7 +128,8 @@ export default class Send extends Component {
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.error !== this.props.error && this.props.error !== '') {
if ((prevProps.error !== this.props.error) &&
(this.props.error !== '') && (this.props.error !== 'broadcast-fail')) {
this.setState({signing: false});
}
@ -197,7 +198,7 @@ export default class Send extends Component {
}
const { api, value, conversion, stopSending, denomination, psbt, currencyRates, error } = this.props;
const { api, value, conversion, stopSending, denomination, psbt, currencyRates, error, network } = this.props;
const { denomAmount, satsAmount, signing, payee, choosingSignMethod, signMethod } = this.state;
const signReady = (this.state.ready && (parseInt(this.state.satsAmount) > 0)) && !signing;
@ -206,6 +207,7 @@ export default class Send extends Component {
if (signMethod === 'Sign Transaction') {
invoice =
<Invoice
network={network}
api={api}
psbt={psbt}
currencyRates={currencyRates}
@ -213,10 +215,12 @@ export default class Send extends Component {
payee={payee}
denomination={denomination}
satsAmount={satsAmount}
state={this.props.state}
/>
} else if (signMethod === 'Sign with Bridge') {
invoice =
<BridgeInvoice
state={this.props.state}
api={api}
psbt={psbt}
currencyRates={currencyRates}

View File

@ -44,7 +44,7 @@ export default function Sent(props) {
>
<Text
color='white'
fontSize='52px'
fontSize='40px'
>
{satsToCurrency(satsAmount, denomination, currencyRates)}
</Text>

View File

@ -10,7 +10,7 @@ import {
LoadingSpinner,
} from '@tlon/indigo-react';
import { patp2dec } from 'urbit-ob';
import { patp2dec, isValidPatq } from 'urbit-ob';
const kg = require('urbit-key-generation');
const bitcoin = require('bitcoinjs-lib');
@ -44,11 +44,11 @@ export default class WalletModal extends Component {
// TODO: port over bridge ticket validation logic
if (this.state.confirmingMasterTicket) {
let confirmedMasterTicket = e.target.value;
let readyToSubmit = (confirmedMasterTicket.length > 0);
let readyToSubmit = isValidPatq(confirmedMasterTicket);
this.setState({confirmedMasterTicket, readyToSubmit});
} else {
let masterTicket = e.target.value;
let readyToSubmit = (masterTicket.length > 0);
let readyToSubmit = isValidPatq(masterTicket);
this.setState({masterTicket, readyToSubmit});
}
}
@ -60,7 +60,6 @@ export default class WalletModal extends Component {
}
submitMasterTicket(ticket){
this.setState({processingSubmission: true});
kg.generateWallet({ ticket, ship: parseInt(patp2dec('~' + window.ship)) })
.then(urbitWallet => {

View File

@ -89,7 +89,7 @@ export function satsToCurrency(sats, denomination, rates){
denomination = "BTC";
}
let rate = rates[denomination];
let val = (sats * rate.last) * 0.00000001;
let val = parseFloat(((sats * rate.last) * 0.00000001).toFixed(8));
let text;
if (denomination === 'BTC'){
text = val + ' ' + rate.symbol

View File

@ -6,6 +6,7 @@ export class UpdateReducer {
if (!json) {
return;
}
console.log('reduce', json);
if (json.providerStatus) {
this.reduceProviderStatus(json.providerStatus, state);
}
@ -39,6 +40,12 @@ export class UpdateReducer {
if (json.hasOwnProperty('error')) {
this.reduceError(json.error, state);
}
if (json.hasOwnProperty('broadcast-success')){
state.broadcastSuccess = true;
}
if (json.hasOwnProperty('broadcast-fail')){
state.broadcastSuccess = false;
}
}
reduceProviderStatus(json, state) {
@ -58,7 +65,8 @@ export class UpdateReducer {
}
reducePsbt(json, state) {
state.psbt = json;
state.psbt = json.pb;
state.fee = json.fee;
}
reduceBtcState(json, state) {

View File

@ -25,6 +25,7 @@ class Store {
denomination: 'BTC',
showWarning: true,
error: '',
broadcastSuccess: false,
};
this.initialReducer = new InitialReducer();