mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-28 19:55:53 +03:00
Add external (psbt) invoice
This commit is contained in:
parent
b93e2a15e3
commit
216e5b19ac
287
pkg/btc-wallet/src/js/components/lib/externalInvoice.js
Normal file
287
pkg/btc-wallet/src/js/components/lib/externalInvoice.js
Normal 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;
|
@ -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}>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user