btc: add signing through bridge

This commit is contained in:
pkova 2021-05-17 15:51:21 +03:00 committed by ixv
parent aa7811c2f5
commit 4bd945ee6e
4 changed files with 340 additions and 42 deletions

View File

@ -0,0 +1,201 @@
import React, { Component } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import * as bitcoin from 'bitcoinjs-lib';
import * as kg from 'urbit-key-generation';
import Sent from './sent.js'
import { satsToCurrency } from '../../lib/util.js';
window.bitcoin = bitcoin;
window.kg = kg;
export default class BridgeInvoice extends Component {
constructor(props) {
super(props);
this.state = {
externalPsbt: '',
ready: false,
error: false,
sent: false,
};
this.checkExternalPsbt = this.checkExternalPsbt.bind(this);
this.broadCastTx = this.broadCastTx.bind(this);
this.sendBitcoin = this.sendBitcoin.bind(this);
}
broadCastTx(psbtHex) {
let command = {
'broadcast-tx': psbtHex
}
return this.props.api.btcWalletCommand(command)
}
componentDidMount() {
window.open('https://bridge.urbit.org/?kind=btc&utx=' + this.props.psbt);
}
sendBitcoin(psbt) {
try {
const hex = bitcoin.Psbt.fromBase64(psbt).validateSignaturesOfAllInputs().toHex();
this.broadCastTx(hex).then(res => this.setState({sent: true}));
}
catch(e) {
this.setState({error: true});
}
}
checkExternalPsbt(e){
let externalPsbt = e.target.value;
let ready = (externalPsbt.length > 0);
let error = false;
this.setState({externalPsbt, ready, error});
}
render() {
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates } = this.props;
const { sent, error, externalPsbt } = this.state;
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (error) {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
return (
<>
{ sent ?
<Sent
payee={payee}
stopSending={stopSending}
denomination={denomination}
currencyRates={currencyRates}
satsAmount={satsAmount}
/> :
<Col
height='400px'
width='100%'
backgroundColor='white'
borderRadius='48px'
mb={5}
p={5}
>
<Row
justifyContent='space-between'
alignItems='center'
>
<Text bold fontSize={1}>Invoice</Text>
<Icon
icon='X'
cursor='pointer'
onClick={() => stopSending()}
/>
</Row>
<Box
mt={4}
backgroundColor='rgba(0, 159, 101, 0.05)'
borderRadius='12px'
>
<Box
padding={4}
>
<Row>
<Text fontSize='14px' fontWeight='500'>You are sending</Text>
</Row>
<Row
mt={2}
>
<Text
color='green'
fontSize='14px'
>{satsToCurrency(satsAmount, denomination, currencyRates)}</Text>
<Text
ml={2}
fontSize='14px'
color='gray'
>{`${satsAmount} sats`}</Text>
</Row>
<Row
mt={2}
>
<Text fontSize='14px'>To:</Text>
<Text
ml={2}
fontSize='14px'
style={{'display': 'block', 'overflow-wrap': 'anywhere'}}
>{payee}</Text>
</Row>
</Box>
</Box>
<Box mt={3}>
<Text fontSize='14px' fontWeight='500'>
Bridge signed transaction
</Text>
</Box>
<Box mt={1} mb={2}>
<Text gray fontSize='14px'>
Copy the signed transaction from Bridge
</Text>
</Box>
<Input
value={this.state.externalPsbt}
fontSize='14px'
placeholder='cHNidP8BAHEBAAAAAXqmzdCZ4uv...'
autoCapitalize='none'
autoCorrect='off'
color={inputColor}
backgroundColor={inputBg}
borderColor={inputBorder}
style={{'line-height': '4'}}
onChange={this.checkExternalPsbt}
/>
{error &&
<Row>
<Text
fontSize='14px'
color='red'
mt={2}>
Invalid signed bitcoin transaction
</Text>
</Row>
}
<Row
flexDirection='row-reverse'
mt={4}
>
<Button
primary
children='Send BTC'
mr={3}
fontSize={1}
borderRadius='24px'
py='24px'
px='24px'
onClick={() => this.sendBitcoin(externalPsbt)}
disabled={!this.state.ready || error}
style={{cursor: (this.state.ready && !error) ? "pointer" : "default"}}
/>
</Row>
</Col>
}
</>
);
}
}

View File

@ -154,6 +154,7 @@ export default class Invoice extends Component {
<Text
ml={2}
fontSize='14px'
style={{'display': 'block', 'overflow-wrap': 'anywhere'}}
>{payee}</Text>
</Row>
</Box>
@ -201,7 +202,7 @@ export default class Invoice extends Component {
px='24px'
onClick={() => this.sendBitcoin(this.state.masterTicket, psbt)}
disabled={!this.state.ready || error}
style={{cursor: this.state.ready ? "pointer" : "default"}}
style={{cursor: (this.state.ready && !error) ? "pointer" : "default"}}
/>
</Row>
</Col>

View File

@ -12,8 +12,10 @@ import {
} from '@tlon/indigo-react';
import Invoice from './invoice.js'
import BridgeInvoice from './bridgeInvoice.js'
import FeePicker from './feePicker.js'
import Error from './error.js'
import Signer from './signer.js'
import { validate } from 'bitcoin-address-validation';
@ -45,11 +47,15 @@ export default class Send extends Component {
feeValue: "mid",
showModal: false,
note: '',
choosingSignMethod: false,
signMethod: 'Sign Transaction',
};
this.initPayment = this.initPayment.bind(this);
this.checkPayee = this.checkPayee.bind(this);
this.feeSelect = this.feeSelect.bind(this);
this.toggleSignMethod = this.toggleSignMethod.bind(this);
this.setSignMethod = this.setSignMethod.bind(this);
}
feeSelect(which) {
@ -57,24 +63,28 @@ export default class Send extends Component {
}
componentDidMount(){
if (this.props.network === 'bitcoin'){
// TODO switch this to bitcoin
if (this.props.network === 'testnet'){
let url = "https://bitcoiner.live/api/fees/estimates/latest";
fetch(url).then(res => res.json()).then(n => {
let estimates = Object.keys(n.estimates);
let mid = Math.floor(estimates.length/2)
let high = estimates.length - 1;
console.log(n);
this.setState({
feeChoices: {
high: [30, n.estimates[30]["sat_per_vbyte"]],
mid: [360, n.estimates[360]["sat_per_vbyte"]],
low: [1440, n.estimates[1440]["sat_per_vbyte"]],
low: [30, n.estimates[30]["sat_per_vbyte"]],
mid: [180, n.estimates[180]["sat_per_vbyte"]],
high: [360, n.estimates[360]["sat_per_vbyte"]],
}
});
})
}
}
setSignMethod(signMethod) {
this.setState({signMethod});
}
checkPayee(e){
store.handleEvent({data: {error: ''}});
@ -125,6 +135,10 @@ export default class Send extends Component {
}
}
toggleSignMethod(toggle) {
this.setState({choosingSignMethod: !toggle});
}
initPayment() {
if (this.state.payeeType === 'ship') {
let command = {
@ -180,22 +194,38 @@ export default class Send extends Component {
const { api, value, conversion, stopSending, denomination, psbt, currencyRates, error } = this.props;
const { denomAmount, satsAmount, signing, payee } = this.state;
const { denomAmount, satsAmount, signing, payee, choosingSignMethod, signMethod } = this.state;
const signReady = (this.state.ready && (parseInt(this.state.satsAmount) > 0)) && !signing;
let invoice = null;
if (signMethod === 'Sign Transaction') {
invoice =
<Invoice
api={api}
psbt={psbt}
currencyRates={currencyRates}
stopSending={stopSending}
payee={payee}
denomination={denomination}
satsAmount={satsAmount}
/>
} else if (signMethod === 'Sign with Bridge') {
invoice =
<BridgeInvoice
api={api}
psbt={psbt}
currencyRates={currencyRates}
stopSending={stopSending}
payee={payee}
denomination={denomination}
satsAmount={satsAmount}
/>
}
return (
<>
{ (signing && psbt) ?
<Invoice
api={api}
psbt={psbt}
currencyRates={currencyRates}
stopSending={stopSending}
payee={payee}
denomination={denomination}
satsAmount={satsAmount}
/> :
{ (signing && psbt) ? invoice :
<Col
width='100%'
backgroundColor='white'
@ -223,7 +253,7 @@ export default class Send extends Component {
<Row justifyContent="space-between" width='calc(40% - 30px)' alignItems="center">
<Text gray fontSize={1} fontWeight='600'>To</Text>
{this.state.checkingPatp ?
<LoadingSpinner background="midOrange" foreground="orange"/> : null
<LoadingSpinner background="midOrange" foreground="orange"/> : null
}
</Row>
<Input
@ -321,18 +351,18 @@ export default class Send extends Component {
{this.state.feeChoices[this.state.feeValue][1]} sats/vbyte
</Text>
<Icon icon="ChevronSouth"
fontSize="14px"
color="lightGray"
onClick={() => { this.setState({showModal: !this.state.showModal}); }}
cursor="pointer"/>
fontSize="14px"
color="lightGray"
onClick={() => { this.setState({showModal: !this.state.showModal}); }}
cursor="pointer"/>
</Row>
</Row>
<Col alignItems="center">
{!this.state.showModal ? null :
<FeePicker
feeChoices={this.state.feeChoices}
feeSelect={this.feeSelect}
/>
<FeePicker
feeChoices={this.state.feeChoices}
feeSelect={this.feeSelect}
/>
}
</Col>
<Row mt={4} width="100%"
@ -366,26 +396,37 @@ export default class Send extends Component {
<Row
flexDirection='row-reverse'
alignItems="center"
mt={4}
>
<Button
primary
children='Sign Transaction'
fontSize={1}
fontWeight='bold'
borderRadius='24px'
mt={4}
py='24px'
px='24px'
onClick={this.initPayment}
color={signReady ? "white" : "lighterGray"}
backgroundColor={signReady ? "blue" : "veryLightGray"}
disabled={!signReady}
border="none"
style={{cursor: signReady ? "pointer" : "default"}}
/>
<Signer
signReady={signReady}
choosingSignMethod={choosingSignMethod}
signMethod={signMethod}
setSignMethod={this.setSignMethod}
initPayment={this.initPayment} />
{ (!(signing && !error)) ? null :
<LoadingSpinner mr={2} background="midOrange" foreground="orange"/>
}
<Button
width='48px'
children={
<Icon
icon={choosingSignMethod ? 'X' : 'Ellipsis'}
color={signReady ? 'blue' : 'lighterGray'}
/>
}
fontSize={1}
fontWeight='bold'
borderRadius='24px'
mr={2}
py='24px'
px='24px'
onClick={() => this.toggleSignMethod(choosingSignMethod)}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={signReady ? 'rgba(33, 157, 255, 0.2)' : 'veryLightGray'}
disabled={!signReady}
border='none'
style={{cursor: signReady ? 'pointer' : 'default'}} />
</Row>
</Col>
}

View File

@ -0,0 +1,55 @@
import React, { Component } from 'react';
import {
Box,
Button,
} from '@tlon/indigo-react';
export default function Signer(props) {
const { signReady, initPayment, choosingSignMethod, signMethod, setSignMethod } = props;
return (
choosingSignMethod ?
<Box
borderRadius='24px'
backgroundColor='rgba(33, 157, 255, 0.2)'
>
<Button
border='none'
backgroundColor='transparent'
fontWeight='bold'
cursor='pointer'
color={(signMethod === 'Sign Transaction') ? 'blue' : 'lightBlue'}
py='24px'
px='24px'
onClick={() => setSignMethod('Sign Transaction')}
children='Sign Transaction' />
<Button
border='none'
backgroundColor='transparent'
fontWeight='bold'
cursor='pointer'
color={(signMethod === 'Sign with Bridge') ? 'blue' : 'lightBlue'}
py='24px'
px='24px'
onClick={() => setSignMethod('Sign with Bridge')}
children='Sign with Bridge' />
</Box>
:
<Button
primary
children={signMethod}
fontSize={1}
fontWeight='bold'
borderRadius='24px'
py='24px'
px='24px'
onClick={initPayment}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={signReady ? 'blue' : 'veryLightGray'}
disabled={!signReady}
border='none'
style={{cursor: signReady ? 'pointer' : 'default'}}
/>
)
}