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 { useSettings } from '../../hooks/useSettings.js';
|
||||||
import { api } from '../../api';
|
import { api } from '../../api';
|
||||||
import { deSig } from '../../lib/util.js';
|
import { deSig } from '../../lib/util.js';
|
||||||
|
import ExternalInvoice from './externalInvoice.js';
|
||||||
|
|
||||||
const focusFields = {
|
const focusFields = {
|
||||||
empty: '',
|
empty: '',
|
||||||
@ -34,6 +35,12 @@ export const feeLevels = {
|
|||||||
high: 'high',
|
high: 'high',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const signMethods = {
|
||||||
|
bridge: 'bridge',
|
||||||
|
masterTicket: 'masterTicket',
|
||||||
|
external: 'external',
|
||||||
|
};
|
||||||
|
|
||||||
const Send = ({ stopSending, value, conversion }) => {
|
const Send = ({ stopSending, value, conversion }) => {
|
||||||
const { error, setError, network, psbt, denomination, shipWallets } =
|
const { error, setError, network, psbt, denomination, shipWallets } =
|
||||||
useSettings();
|
useSettings();
|
||||||
@ -55,7 +62,7 @@ const Send = ({ stopSending, value, conversion }) => {
|
|||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [note, setNote] = useState('');
|
const [note, setNote] = useState('');
|
||||||
const [choosingSignMethod, setChoosingSignMethod] = useState(false);
|
const [choosingSignMethod, setChoosingSignMethod] = useState(false);
|
||||||
const [signMethod, setSignMethod] = useState('bridge');
|
const [signMethod, setSignMethod] = useState(signMethods.bridge);
|
||||||
|
|
||||||
const feeDismiss = () => {
|
const feeDismiss = () => {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
@ -190,22 +197,40 @@ const Send = ({ stopSending, value, conversion }) => {
|
|||||||
const signReady = ready && parseInt(satsAmount) > 0 && !signing;
|
const signReady = ready && parseInt(satsAmount) > 0 && !signing;
|
||||||
|
|
||||||
let invoice = null;
|
let invoice = null;
|
||||||
if (signMethod === 'masterTicket') {
|
|
||||||
invoice = (
|
switch (signMethod) {
|
||||||
<Invoice
|
case signMethods.masterTicket: {
|
||||||
stopSending={stopSending}
|
invoice = (
|
||||||
payee={payee}
|
<Invoice
|
||||||
satsAmount={satsAmount}
|
stopSending={stopSending}
|
||||||
/>
|
payee={payee}
|
||||||
);
|
satsAmount={satsAmount}
|
||||||
} else if (signMethod === 'bridge') {
|
/>
|
||||||
invoice = (
|
);
|
||||||
<BridgeInvoice
|
break;
|
||||||
stopSending={stopSending}
|
}
|
||||||
payee={payee}
|
case signMethods.bridge: {
|
||||||
satsAmount={satsAmount}
|
invoice = (
|
||||||
/>
|
<BridgeInvoice
|
||||||
);
|
stopSending={stopSending}
|
||||||
|
payee={payee}
|
||||||
|
satsAmount={satsAmount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case signMethods.external: {
|
||||||
|
invoice = (
|
||||||
|
<ExternalInvoice
|
||||||
|
stopSending={stopSending}
|
||||||
|
payee={payee}
|
||||||
|
satsAmount={satsAmount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -435,7 +460,7 @@ const Send = ({ stopSending, value, conversion }) => {
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
{signMethod === 'masterTicket' && (
|
{signMethod === signMethod.masterTicket && (
|
||||||
<Row mt={4} alignItems="center">
|
<Row mt={4} alignItems="center">
|
||||||
<Icon icon="Info" color="yellow" height={4} width={4} />
|
<Icon icon="Info" color="yellow" height={4} width={4} />
|
||||||
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
|
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Button } from '@tlon/indigo-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 = ({
|
const Signer = ({
|
||||||
signReady,
|
signReady,
|
||||||
@ -10,28 +17,20 @@ const Signer = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return choosingSignMethod ? (
|
return choosingSignMethod ? (
|
||||||
<Box borderRadius="24px" backgroundColor="rgba(33, 157, 255, 0.2)">
|
<Box borderRadius="24px" backgroundColor="rgba(33, 157, 255, 0.2)">
|
||||||
<Button
|
{Object.keys(signMethods).map((method) => (
|
||||||
border="none"
|
<Button
|
||||||
backgroundColor="transparent"
|
key={method}
|
||||||
fontWeight="bold"
|
border="none"
|
||||||
cursor="pointer"
|
backgroundColor="transparent"
|
||||||
color={signMethod === 'masterTicket' ? 'blue' : 'lightBlue'}
|
fontWeight="bold"
|
||||||
height="48px"
|
cursor="pointer"
|
||||||
onClick={() => setSignMethod('masterTicket')}
|
color={signMethod === signMethods[method] ? 'blue' : 'lightBlue'}
|
||||||
>
|
height="48px"
|
||||||
Sign with Master Ticket
|
onClick={() => setSignMethod(signMethods[method])}
|
||||||
</Button>
|
>
|
||||||
<Button
|
{signMethodLabels[method]}
|
||||||
border="none"
|
</Button>
|
||||||
backgroundColor="transparent"
|
))}
|
||||||
fontWeight="bold"
|
|
||||||
cursor="pointer"
|
|
||||||
color={signMethod === 'bridge' ? 'blue' : 'lightBlue'}
|
|
||||||
height="48px"
|
|
||||||
onClick={() => setSignMethod('bridge')}
|
|
||||||
>
|
|
||||||
Sign with Bridge
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@ -47,7 +46,7 @@ const Signer = ({
|
|||||||
border="none"
|
border="none"
|
||||||
style={{ cursor: signReady ? 'pointer' : 'default' }}
|
style={{ cursor: signReady ? 'pointer' : 'default' }}
|
||||||
>
|
>
|
||||||
{signMethod === 'bridge' ? 'Sign with Bridge' : 'Sign with Master Ticket'}
|
{signMethodLabels[signMethod]}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user