mirror of
https://github.com/urbit/shrub.git
synced 2025-01-03 01:54:43 +03:00
btc: invoice error handling
This commit is contained in:
parent
768b47985d
commit
40d69b5a02
@ -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 ~
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -44,7 +44,7 @@ export default function Sent(props) {
|
||||
>
|
||||
<Text
|
||||
color='white'
|
||||
fontSize='52px'
|
||||
fontSize='40px'
|
||||
>
|
||||
{satsToCurrency(satsAmount, denomination, currencyRates)}
|
||||
</Text>
|
||||
|
@ -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 => {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -25,6 +25,7 @@ class Store {
|
||||
denomination: 'BTC',
|
||||
showWarning: true,
|
||||
error: '',
|
||||
broadcastSuccess: false,
|
||||
};
|
||||
|
||||
this.initialReducer = new InitialReducer();
|
||||
|
Loading…
Reference in New Issue
Block a user