Add external (psbt) invoice

This commit is contained in:
finned-palmer 2021-07-21 14:10:18 -05:00 committed by ixv
parent b93e2a15e3
commit 216e5b19ac
3 changed files with 352 additions and 41 deletions

View File

@ -0,0 +1,287 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import { Sigil } from './sigil.js';
import * as bitcoin from 'bitcoinjs-lib';
import { isValidPatp } from 'urbit-ob';
import Sent from './sent.js';
import Error from './error.js';
import { copyToClipboard, satsToCurrency } from '../../lib/util.js';
import { useSettings } from '../../hooks/useSettings.js';
import { api } from '../../api';
const ExternalInvoice = ({ payee, stopSending, satsAmount }) => {
const { error, currencyRates, fee, broadcastSuccess, denomination, psbt } =
useSettings();
const [txHex, setTxHex] = useState('');
const [ready, setReady] = useState(false);
const [copiedPsbt, setCopiedPsbt] = useState(false);
const [downloadedButton, setDownloadedButton] = useState(false);
const [copiedButton, setCopiedButton] = useState(false);
const [localError, setLocalError] = useState('');
const [broadcasting, setBroadcasting] = useState(false);
const invoiceRef = useRef();
useEffect(() => {
if (broadcasting && localError !== '') {
setBroadcasting(false);
}
if (error !== '') {
setLocalError(error);
}
}, [error, broadcasting, setBroadcasting]);
const broadCastTx = (hex) => {
let command = {
'broadcast-tx': hex,
};
return api.btcWalletCommand(command);
};
const sendBitcoin = (hex) => {
try {
bitcoin.Transaction.fromHex(hex);
broadCastTx(hex);
setBroadcasting(true);
} catch (e) {
setLocalError('invalid-signed');
setBroadcasting(false);
}
};
const checkTxHex = (e) => {
setTxHex(e.target.value);
setReady(txHex.length > 0);
setLocalError('');
};
const copyPsbt = () => {
copyToClipboard(psbt);
setCopiedPsbt(true);
setCopiedButton(true);
setTimeout(() => {
setCopiedPsbt(false);
setCopiedButton(false);
}, 2000);
};
const downloadPsbtFile = () => {
setDownloadedButton(true);
const blob = new Blob([psbt]);
const downloadURL = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadURL;
link.setAttribute('download', 'urbit.psbt');
document.body.appendChild(link);
link.click();
link.parentNode.removeChild(link);
setTimeout(() => {
setDownloadedButton(false);
}, 1000);
};
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (localError !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
const isShip = isValidPatp(payee);
const icon = isShip ? (
<Sigil ship={payee} size={24} color="black" classes={''} icon padding={5} />
) : (
<Box
backgroundColor="lighterGray"
width="24px"
height="24px"
textAlign="center"
alignItems="center"
borderRadius="2px"
p={1}
>
<Icon icon="Bitcoin" color="gray" />
</Box>
);
return (
<>
{broadcastSuccess ? (
<Sent payee={payee} stopSending={stopSending} satsAmount={satsAmount} />
) : (
<Col
ref={invoiceRef}
width="100%"
backgroundColor="white"
borderRadius="48px"
mb={5}
p={5}
>
<Col
p={5}
mt={4}
backgroundColor="veryLightGreen"
borderRadius="24px"
alignItems="center"
>
<Row>
<Text color="green" fontSize="40px">
{satsToCurrency(satsAmount, denomination, currencyRates)}
</Text>
</Row>
<Row>
<Text
fontWeight="bold"
fontSize="16px"
color="midGreen"
>{`${satsAmount} sats`}</Text>
</Row>
<Row mt={2}>
<Text fontSize="14px" color="midGreen">{`Fee: ${satsToCurrency(
fee,
denomination,
currencyRates
)} (${fee} sats)`}</Text>
</Row>
<Row mt={4}>
<Text fontSize="16px" fontWeight="bold" color="gray">
You are paying
</Text>
</Row>
<Row mt={2} alignItems="center">
{icon}
<Text
ml={2}
mono
color="gray"
fontSize="14px"
style={{ display: 'block', overflowWrap: 'anywhere' }}
>
{payee}
</Text>
</Row>
</Col>
<Box mt={3}>
<Text fontSize="14px" fontWeight="500">
Partially-signed Bitcoin Transaction (PSBT)
</Text>
</Box>
<Box mt={3}>
<Text
mono
color="lightGray"
fontSize="14px"
style={{ overflowWrap: 'anywhere', cursor: 'pointer' }}
onClick={() => copyPsbt()}
>
{copiedPsbt ? 'copied' : psbt}
</Text>
</Box>
<Box mt={3} mb={2}>
<Text gray fontSize="14px">
Paste the signed transaction from your external wallet:
</Text>
</Box>
<Input
value={txHex}
fontSize="14px"
placeholder="..."
autoCapitalize="none"
autoCorrect="off"
color={inputColor}
backgroundColor={inputBg}
borderColor={inputBorder}
style={{ lineHeight: '4' }}
onChange={(e) => checkTxHex(e)}
/>
{localError !== '' && (
<Row>
<Error error={localError} fontSize="14px" mt={2} />
</Row>
)}
<Row
flexDirection="row"
mt={4}
alignItems="center"
justifyContent="center"
>
<Button
mr={3}
disabled={downloadedButton}
fontSize={1}
fontWeight="bold"
color={downloadedButton ? 'green' : 'orange'}
backgroundColor={
downloadedButton ? 'veryLightGreen' : 'midOrange'
}
style={{
cursor: downloadedButton ? 'default' : 'pointer',
}}
borderColor="none"
borderRadius="24px"
height="48px"
onClick={() => downloadPsbtFile()}
>
{downloadedButton ? 'PSBT Downloading' : 'Download PSBT'}
</Button>
<Button
mr={3}
disabled={copiedButton}
fontSize={1}
fontWeight="bold"
color={copiedButton ? 'green' : 'orange'}
backgroundColor={copiedButton ? 'veryLightGreen' : 'midOrange'}
style={{
cursor: copiedButton ? 'default' : 'pointer',
}}
borderColor="none"
borderRadius="24px"
height="48px"
onClick={() => copyPsbt()}
>
{copiedButton ? 'PSBT Copied!' : 'Copy PSBT'}
</Button>
<Button
primary
mr={3}
fontSize={1}
borderRadius="24px"
border="none"
height="48px"
onClick={() => sendBitcoin(txHex)}
disabled={!ready || localError || broadcasting}
color={
ready && !localError && !broadcasting ? 'white' : 'lighterGray'
}
backgroundColor={
ready && !localError && !broadcasting
? 'green'
: 'veryLightGray'
}
style={{
cursor: ready && !localError ? 'pointer' : 'default',
}}
>
Send BTC
</Button>
{broadcasting ? <LoadingSpinner mr={3} /> : null}
</Row>
</Col>
)}
</>
);
};
export default ExternalInvoice;

View File

@ -19,6 +19,7 @@ import * as ob from 'urbit-ob';
import { useSettings } from '../../hooks/useSettings.js';
import { api } from '../../api';
import { deSig } from '../../lib/util.js';
import ExternalInvoice from './externalInvoice.js';
const focusFields = {
empty: '',
@ -34,6 +35,12 @@ export const feeLevels = {
high: 'high',
};
export const signMethods = {
bridge: 'bridge',
masterTicket: 'masterTicket',
external: 'external',
};
const Send = ({ stopSending, value, conversion }) => {
const { error, setError, network, psbt, denomination, shipWallets } =
useSettings();
@ -55,7 +62,7 @@ const Send = ({ stopSending, value, conversion }) => {
const [showModal, setShowModal] = useState(false);
const [note, setNote] = useState('');
const [choosingSignMethod, setChoosingSignMethod] = useState(false);
const [signMethod, setSignMethod] = useState('bridge');
const [signMethod, setSignMethod] = useState(signMethods.bridge);
const feeDismiss = () => {
setShowModal(false);
@ -190,22 +197,40 @@ const Send = ({ stopSending, value, conversion }) => {
const signReady = ready && parseInt(satsAmount) > 0 && !signing;
let invoice = null;
if (signMethod === 'masterTicket') {
invoice = (
<Invoice
stopSending={stopSending}
payee={payee}
satsAmount={satsAmount}
/>
);
} else if (signMethod === 'bridge') {
invoice = (
<BridgeInvoice
stopSending={stopSending}
payee={payee}
satsAmount={satsAmount}
/>
);
switch (signMethod) {
case signMethods.masterTicket: {
invoice = (
<Invoice
stopSending={stopSending}
payee={payee}
satsAmount={satsAmount}
/>
);
break;
}
case signMethods.bridge: {
invoice = (
<BridgeInvoice
stopSending={stopSending}
payee={payee}
satsAmount={satsAmount}
/>
);
break;
}
case signMethods.external: {
invoice = (
<ExternalInvoice
stopSending={stopSending}
payee={payee}
satsAmount={satsAmount}
/>
);
break;
}
default:
break;
}
return (
@ -435,7 +460,7 @@ const Send = ({ stopSending, value, conversion }) => {
/>
</Button>
</Row>
{signMethod === 'masterTicket' && (
{signMethod === signMethod.masterTicket && (
<Row mt={4} alignItems="center">
<Icon icon="Info" color="yellow" height={4} width={4} />
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>

View File

@ -1,5 +1,12 @@
import React from 'react';
import { Box, Button } from '@tlon/indigo-react';
import { signMethods } from './send';
const signMethodLabels = {
bridge: 'Sign with Bridge',
masterTicket: 'Sign with Master Ticket',
external: 'Sign Externally',
};
const Signer = ({
signReady,
@ -10,28 +17,20 @@ const Signer = ({
}) => {
return choosingSignMethod ? (
<Box borderRadius="24px" backgroundColor="rgba(33, 157, 255, 0.2)">
<Button
border="none"
backgroundColor="transparent"
fontWeight="bold"
cursor="pointer"
color={signMethod === 'masterTicket' ? 'blue' : 'lightBlue'}
height="48px"
onClick={() => setSignMethod('masterTicket')}
>
Sign with Master Ticket
</Button>
<Button
border="none"
backgroundColor="transparent"
fontWeight="bold"
cursor="pointer"
color={signMethod === 'bridge' ? 'blue' : 'lightBlue'}
height="48px"
onClick={() => setSignMethod('bridge')}
>
Sign with Bridge
</Button>
{Object.keys(signMethods).map((method) => (
<Button
key={method}
border="none"
backgroundColor="transparent"
fontWeight="bold"
cursor="pointer"
color={signMethod === signMethods[method] ? 'blue' : 'lightBlue'}
height="48px"
onClick={() => setSignMethod(signMethods[method])}
>
{signMethodLabels[method]}
</Button>
))}
</Box>
) : (
<Button
@ -47,7 +46,7 @@ const Signer = ({
border="none"
style={{ cursor: signReady ? 'pointer' : 'default' }}
>
{signMethod === 'bridge' ? 'Sign with Bridge' : 'Sign with Master Ticket'}
{signMethodLabels[signMethod]}
</Button>
);
};