mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-01 11:33:41 +03:00
Convert components, remove old store
This commit is contained in:
parent
9751555038
commit
ebf16f0f74
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Root } from './js/components/root.js';
|
||||
import Root from './js/components/root.js';
|
||||
import { api } from './js/api.js';
|
||||
import { subscription } from "./js/subscription.js";
|
||||
import { SettingsProvider } from './js/hooks/useSettings';
|
||||
|
||||
import './css/indigo-static.css';
|
||||
import './css/fonts.css';
|
||||
@ -13,11 +13,13 @@ import './css/custom.css';
|
||||
const channel = new window.channel();
|
||||
api.setChannel(window.ship, channel);
|
||||
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept()
|
||||
module.hot.accept();
|
||||
}
|
||||
|
||||
ReactDOM.render((
|
||||
<Root channel={channel}/>
|
||||
), document.querySelectorAll("#root")[0]);
|
||||
ReactDOM.render(
|
||||
<SettingsProvider channel={channel}>
|
||||
<Root />
|
||||
</SettingsProvider>,
|
||||
document.querySelectorAll('#root')[0]
|
||||
);
|
||||
|
@ -1,170 +1,140 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Row, Text, Button, Col } from '@tlon/indigo-react';
|
||||
import Send from './send.js';
|
||||
import CurrencyPicker from './currencyPicker.js';
|
||||
import { satsToCurrency } from '../../lib/util.js';
|
||||
import { store } from '../../store.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
import { api } from '../../api';
|
||||
|
||||
export default class Balance extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const Balance = () => {
|
||||
const {
|
||||
address,
|
||||
confirmedBalance: sats,
|
||||
unconfirmedBalance: unconfirmedSats,
|
||||
denomination,
|
||||
currencyRates,
|
||||
setPsbt,
|
||||
setFee,
|
||||
setError,
|
||||
} = useSettings();
|
||||
const [sending, setSending] = useState(false);
|
||||
const [copiedButton, setCopiedButton] = useState(false);
|
||||
const [copiedString, setCopiedString] = useState(false);
|
||||
|
||||
this.state = {
|
||||
sending: false,
|
||||
copiedButton: false,
|
||||
copiedString: false,
|
||||
};
|
||||
|
||||
this.copyAddress = this.copyAddress.bind(this);
|
||||
}
|
||||
|
||||
copyAddress(arg) {
|
||||
let address = this.props.state.address;
|
||||
const copyAddress = (arg) => {
|
||||
navigator.clipboard.writeText(address);
|
||||
this.props.api.btcWalletCommand({ 'gen-new-address': null });
|
||||
api.btcWalletCommand({ 'gen-new-address': null });
|
||||
|
||||
if (arg === 'button') {
|
||||
this.setState({ copiedButton: true });
|
||||
setCopiedButton(true);
|
||||
setTimeout(() => {
|
||||
this.setState({ copiedButton: false });
|
||||
setCopiedButton(false);
|
||||
}, 2000);
|
||||
} else if (arg === 'string') {
|
||||
this.setState({ copiedString: true });
|
||||
setCopiedString(true);
|
||||
setTimeout(() => {
|
||||
this.setState({ copiedString: false });
|
||||
setCopiedString(false);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const sats = this.props.state.confirmedBalance || 0;
|
||||
const unconfirmedSats = this.props.state.unconfirmedBalance;
|
||||
const unconfirmedString = unconfirmedSats ? ` (${unconfirmedSats}) ` : '';
|
||||
|
||||
const unconfirmedString = unconfirmedSats ? ` (${unconfirmedSats}) ` : '';
|
||||
const value = satsToCurrency(sats, denomination, currencyRates);
|
||||
const sendDisabled = sats === 0;
|
||||
const addressText =
|
||||
address === null ? '' : address.slice(0, 6) + '...' + address.slice(-6);
|
||||
|
||||
const denomination = this.props.state.denomination;
|
||||
const value = satsToCurrency(
|
||||
sats,
|
||||
denomination,
|
||||
this.props.state.currencyRates
|
||||
);
|
||||
const sendDisabled = sats === 0;
|
||||
const addressText =
|
||||
this.props.state.address === null
|
||||
? ''
|
||||
: this.props.state.address.slice(0, 6) +
|
||||
'...' +
|
||||
this.props.state.address.slice(-6);
|
||||
const conversion = currencyRates[denomination]?.last;
|
||||
|
||||
const conversion = this.props.state.currencyRates[denomination].last;
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.state.sending ? (
|
||||
<Send
|
||||
state={this.props.state}
|
||||
api={this.props.api}
|
||||
psbt={this.props.state.psbt}
|
||||
fee={this.props.state.fee}
|
||||
currencyRates={this.props.state.currencyRates}
|
||||
shipWallets={this.props.state.shipWallets}
|
||||
value={value}
|
||||
denomination={denomination}
|
||||
sats={sats}
|
||||
conversion={conversion}
|
||||
network={this.props.network}
|
||||
error={this.props.state.error}
|
||||
stopSending={() => {
|
||||
this.setState({ sending: false });
|
||||
store.handleEvent({
|
||||
data: { psbt: '', fee: 0, error: '', 'broadcast-fail': null },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Col
|
||||
height="400px"
|
||||
width="100%"
|
||||
backgroundColor="white"
|
||||
borderRadius="48px"
|
||||
justifyContent="space-between"
|
||||
mb={5}
|
||||
p={5}
|
||||
>
|
||||
<Row justifyContent="space-between">
|
||||
<Text color="orange" fontSize={1}>
|
||||
Balance
|
||||
</Text>
|
||||
<Text
|
||||
color="lightGray"
|
||||
fontSize="14px"
|
||||
mono
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
this.copyAddress('string');
|
||||
}}
|
||||
>
|
||||
{this.state.copiedString ? 'copied' : addressText}
|
||||
</Text>
|
||||
<CurrencyPicker
|
||||
api={this.props.api}
|
||||
denomination={denomination}
|
||||
currencies={this.props.state.currencyRates}
|
||||
/>
|
||||
</Row>
|
||||
<Col justifyContent="center" alignItems="center">
|
||||
<Text
|
||||
fontSize="40px"
|
||||
color="orange"
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={1}
|
||||
color="orange"
|
||||
>{`${sats}${unconfirmedString} sats`}</Text>
|
||||
</Col>
|
||||
<Row flexDirection="row-reverse">
|
||||
<Button
|
||||
disabled={sendDisabled}
|
||||
fontSize={1}
|
||||
fontWeight="bold"
|
||||
color={sendDisabled ? 'lighterGray' : 'white'}
|
||||
backgroundColor={sendDisabled ? 'veryLightGray' : 'orange'}
|
||||
style={{ cursor: sendDisabled ? 'default' : 'pointer' }}
|
||||
borderColor="none"
|
||||
borderRadius="24px"
|
||||
height="48px"
|
||||
onClick={() => this.setState({ sending: true })}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<Button
|
||||
mr={3}
|
||||
disabled={this.state.copiedButton}
|
||||
fontSize={1}
|
||||
fontWeight="bold"
|
||||
color={this.state.copiedButton ? 'green' : 'orange'}
|
||||
backgroundColor={
|
||||
this.state.copiedButton ? 'veryLightGreen' : 'midOrange'
|
||||
}
|
||||
style={{
|
||||
cursor: this.state.copiedButton ? 'default' : 'pointer',
|
||||
}}
|
||||
borderColor="none"
|
||||
borderRadius="24px"
|
||||
height="48px"
|
||||
onClick={() => {
|
||||
this.copyAddress('button');
|
||||
}}
|
||||
>
|
||||
{this.state.copiedButton ? 'Address Copied!' : 'Copy Address'}
|
||||
</Button>
|
||||
</Row>
|
||||
return (
|
||||
<>
|
||||
{sending ? (
|
||||
<Send
|
||||
value={value}
|
||||
conversion={conversion}
|
||||
stopSending={() => {
|
||||
setSending(false);
|
||||
setPsbt('');
|
||||
setFee(0);
|
||||
setError('');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Col
|
||||
height="400px"
|
||||
width="100%"
|
||||
backgroundColor="white"
|
||||
borderRadius="48px"
|
||||
justifyContent="space-between"
|
||||
mb={5}
|
||||
p={5}
|
||||
>
|
||||
<Row justifyContent="space-between">
|
||||
<Text color="orange" fontSize={1}>
|
||||
Balance
|
||||
</Text>
|
||||
<Text
|
||||
color="lightGray"
|
||||
fontSize="14px"
|
||||
mono
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => copyAddress('string')}
|
||||
>
|
||||
{copiedString ? 'copied' : addressText}
|
||||
</Text>
|
||||
<CurrencyPicker />
|
||||
</Row>
|
||||
<Col justifyContent="center" alignItems="center">
|
||||
<Text
|
||||
fontSize="40px"
|
||||
color="orange"
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={1}
|
||||
color="orange"
|
||||
>{`${sats}${unconfirmedString} sats`}</Text>
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
<Row flexDirection="row-reverse">
|
||||
<Button
|
||||
disabled={sendDisabled}
|
||||
fontSize={1}
|
||||
fontWeight="bold"
|
||||
color={sendDisabled ? 'lighterGray' : 'white'}
|
||||
backgroundColor={sendDisabled ? 'veryLightGray' : 'orange'}
|
||||
style={{ cursor: sendDisabled ? 'default' : 'pointer' }}
|
||||
borderColor="none"
|
||||
borderRadius="24px"
|
||||
height="48px"
|
||||
onClick={() => setSending(true)}
|
||||
>
|
||||
Send
|
||||
</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={() => copyAddress('button')}
|
||||
>
|
||||
{copiedButton ? 'Address Copied!' : 'Copy Address'}
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Balance;
|
||||
|
@ -1,72 +1,49 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
LoadingSpinner,
|
||||
Col,
|
||||
} from '@tlon/indigo-react';
|
||||
import {
|
||||
Switch,
|
||||
Route,
|
||||
} from 'react-router-dom';
|
||||
import Balance from './balance.js';
|
||||
import React from 'react';
|
||||
import { Box, LoadingSpinner, Col } from '@tlon/indigo-react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import Balance from './balance.js';
|
||||
import Transactions from './transactions.js';
|
||||
import Warning from './warning.js';
|
||||
import Header from './header.js';
|
||||
import Settings from './settings.js';
|
||||
import Warning from './warning.js';
|
||||
import Header from './header.js';
|
||||
import Settings from './settings.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
|
||||
export default class Body extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
const Body = () => {
|
||||
const { loaded, showWarning: warning } = useSettings();
|
||||
const cardWidth = window.innerWidth <= 475 ? '350px' : '400px';
|
||||
return !loaded ? (
|
||||
<Box
|
||||
display="flex"
|
||||
width="100%"
|
||||
height="100%"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<LoadingSpinner
|
||||
width={7}
|
||||
height={7}
|
||||
background="midOrange"
|
||||
foreground="orange"
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Switch>
|
||||
<Route path="/~btc/settings">
|
||||
<Col display="flex" flexDirection="column" width={cardWidth}>
|
||||
<Header settings={true} />
|
||||
<Settings />
|
||||
</Col>
|
||||
</Route>
|
||||
<Route path="/~btc">
|
||||
<Col display="flex" flexDirection="column" width={cardWidth}>
|
||||
<Header settings={false} />
|
||||
{!warning ? null : <Warning />}
|
||||
<Balance />
|
||||
<Transactions />
|
||||
</Col>
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
const cardWidth = window.innerWidth <= 475 ? '350px' : '400px'
|
||||
|
||||
if (!this.props.loaded) {
|
||||
return (
|
||||
<Box display="flex" width="100%" height="100%" alignItems="center" justifyContent="center">
|
||||
<LoadingSpinner
|
||||
width={7}
|
||||
height={7}
|
||||
background="midOrange"
|
||||
foreground="orange"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/~btc/settings">
|
||||
<Col
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
width={cardWidth}
|
||||
>
|
||||
<Header settings={true} state={this.props.state}/>
|
||||
<Settings state={this.props.state}
|
||||
api={this.props.api}
|
||||
network={this.props.network}
|
||||
/>
|
||||
</Col>
|
||||
</Route>
|
||||
<Route path="/~btc">
|
||||
<Col
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
width={cardWidth}
|
||||
>
|
||||
<Header settings={false} state={this.props.state}/>
|
||||
{ (!this.props.warning) ? null : <Warning api={this.props.api}/>}
|
||||
<Balance api={this.props.api} state={this.props.state} network={this.props.network}/>
|
||||
<Transactions state={this.props.state} network={this.props.network}/>
|
||||
</Col>
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default Body;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
@ -9,231 +9,203 @@ import {
|
||||
Col,
|
||||
LoadingSpinner,
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
import { Sigil } from './sigil.js';
|
||||
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import * as kg from 'urbit-key-generation';
|
||||
import { isValidPatp } from 'urbit-ob';
|
||||
|
||||
import Sent from './sent.js'
|
||||
import Error from './error.js'
|
||||
|
||||
import Sent from './sent.js';
|
||||
import Error from './error.js';
|
||||
import { satsToCurrency } from '../../lib/util.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
import { api } from '../../api';
|
||||
|
||||
export default class BridgeInvoice extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const BridgeInvoice = ({ payee, stopSending, satsAmount }) => {
|
||||
const { error, currencyRates, fee, broadcastSuccess, denomination, psbt } =
|
||||
useSettings();
|
||||
const [txHex, setTxHex] = useState('');
|
||||
const [ready, setReady] = useState(false);
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [broadcasting, setBroadcasting] = useState(false);
|
||||
const invoiceRef = useRef();
|
||||
|
||||
this.state = {
|
||||
txHex: '',
|
||||
ready: false,
|
||||
error: this.props.state.error,
|
||||
broadcasting: false,
|
||||
};
|
||||
|
||||
this.checkTxHex = this.checkTxHex.bind(this);
|
||||
this.broadCastTx = this.broadCastTx.bind(this);
|
||||
this.sendBitcoin = this.sendBitcoin.bind(this);
|
||||
this.clickDismiss = this.clickDismiss.bind(this);
|
||||
this.setInvoiceRef = this.setInvoiceRef.bind(this);
|
||||
}
|
||||
|
||||
broadCastTx(hex) {
|
||||
let command = {
|
||||
'broadcast-tx': hex
|
||||
useEffect(() => {
|
||||
if (broadcasting && localError !== '') {
|
||||
setBroadcasting(false);
|
||||
}
|
||||
return this.props.api.btcWalletCommand(command)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.open('https://bridge.urbit.org/?kind=btc&utx=' + this.props.psbt);
|
||||
document.addEventListener("click", this.clickDismiss);
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
document.removeEventListener("click", this.clickDismiss);
|
||||
}
|
||||
|
||||
setInvoiceRef(n){
|
||||
this.invoiceRef = n;
|
||||
}
|
||||
|
||||
clickDismiss(e){
|
||||
if (this.invoiceRef && !(this.invoiceRef.contains(e.target))){
|
||||
this.props.stopSending();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
this.setState({broadcasting: true});
|
||||
}
|
||||
|
||||
catch(e) {
|
||||
this.setState({error: 'invalid-signed', broadcasting: false});
|
||||
}
|
||||
}
|
||||
|
||||
checkTxHex(e){
|
||||
let txHex = e.target.value;
|
||||
let ready = (txHex.length > 0);
|
||||
let error = '';
|
||||
this.setState({txHex, ready, error});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates, fee } = this.props;
|
||||
const { error, txHex } = this.state;
|
||||
|
||||
let inputColor = 'black';
|
||||
let inputBg = 'white';
|
||||
let inputBorder = 'lightGray';
|
||||
|
||||
if (error !== '') {
|
||||
inputColor = 'red';
|
||||
inputBg = 'veryLightRed';
|
||||
inputBorder = 'red';
|
||||
setLocalError(error);
|
||||
}
|
||||
}, [error, broadcasting, setBroadcasting]);
|
||||
|
||||
const isShip = isValidPatp(payee);
|
||||
useEffect(() => {
|
||||
window.open('https://bridge.urbit.org/?kind=btc&utx=' + psbt);
|
||||
});
|
||||
|
||||
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>;
|
||||
const broadCastTx = (hex) => {
|
||||
let command = {
|
||||
'broadcast-tx': hex,
|
||||
};
|
||||
return api.btcWalletCommand(command);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ this.props.state.broadcastSuccess ?
|
||||
<Sent
|
||||
payee={payee}
|
||||
stopSending={stopSending}
|
||||
denomination={denomination}
|
||||
currencyRates={currencyRates}
|
||||
satsAmount={satsAmount}
|
||||
/> :
|
||||
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('');
|
||||
};
|
||||
|
||||
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
|
||||
ref={this.setInvoiceRef}
|
||||
width='100%'
|
||||
backgroundColor='white'
|
||||
borderRadius='48px'
|
||||
mb={5}
|
||||
p={5}
|
||||
mt={4}
|
||||
backgroundColor="veryLightGreen"
|
||||
borderRadius="24px"
|
||||
alignItems="center"
|
||||
>
|
||||
<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', 'overflow-wrap': 'anywhere'}}
|
||||
>{payee}</Text>
|
||||
</Row>
|
||||
</Col>
|
||||
<Box mt={3}>
|
||||
<Text fontSize='14px' fontWeight='500'>
|
||||
Bridge signed transaction
|
||||
<Row>
|
||||
<Text color="green" fontSize="40px">
|
||||
{satsToCurrency(satsAmount, denomination, currencyRates)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box mt={1} mb={2}>
|
||||
<Text gray fontSize='14px'>
|
||||
Copy the signed transaction from Bridge
|
||||
</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', 'overflow-wrap': 'anywhere' }}
|
||||
>
|
||||
{payee}
|
||||
</Text>
|
||||
</Box>
|
||||
<Input
|
||||
value={this.state.txHex}
|
||||
fontSize='14px'
|
||||
placeholder='010000000001019e478cc370323ac539097...'
|
||||
autoCapitalize='none'
|
||||
autoCorrect='off'
|
||||
color={inputColor}
|
||||
backgroundColor={inputBg}
|
||||
borderColor={inputBorder}
|
||||
style={{'line-height': '4'}}
|
||||
onChange={this.checkTxHex}
|
||||
/>
|
||||
{ (error !== '') &&
|
||||
<Row>
|
||||
<Error
|
||||
error={error}
|
||||
fontSize='14px'
|
||||
mt={2}/>
|
||||
</Row>
|
||||
}
|
||||
<Row
|
||||
flexDirection='row-reverse'
|
||||
mt={4}
|
||||
alignItems="center"
|
||||
>
|
||||
<Button
|
||||
primary
|
||||
children='Send BTC'
|
||||
mr={3}
|
||||
fontSize={1}
|
||||
borderRadius='24px'
|
||||
border='none'
|
||||
height='48px'
|
||||
onClick={() => this.sendBitcoin(txHex)}
|
||||
disabled={!this.state.ready || error || this.state.broadcasting}
|
||||
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"}
|
||||
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"}
|
||||
style={{cursor: (this.state.ready && !error) ? "pointer" : "default"}}
|
||||
/>
|
||||
{this.state.broadcasting ? <LoadingSpinner mr={3}/> : null}
|
||||
</Row>
|
||||
</Col>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
<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={txHex}
|
||||
fontSize="14px"
|
||||
placeholder="010000000001019e478cc370323ac539097..."
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
color={inputColor}
|
||||
backgroundColor={inputBg}
|
||||
borderColor={inputBorder}
|
||||
style={{ 'line-height': '4' }}
|
||||
onChange={(e) => checkTxHex(e)}
|
||||
/>
|
||||
{localError !== '' && (
|
||||
<Row>
|
||||
<Error error={localError} fontSize="14px" mt={2} />
|
||||
</Row>
|
||||
)}
|
||||
<Row flexDirection="row-reverse" mt={4} alignItems="center">
|
||||
<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 BridgeInvoice;
|
||||
|
@ -1,50 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
Button,
|
||||
Col,
|
||||
LoadingSpinner,
|
||||
} from '@tlon/indigo-react';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import { api } from '../../api';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
|
||||
import { satsToCurrency } from '../../lib/util.js'
|
||||
import { store } from '../../store';
|
||||
|
||||
export default class CurrencyPicker extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.switchCurrency = this.switchCurrency.bind(this);
|
||||
}
|
||||
|
||||
switchCurrency(){
|
||||
const CurrencyPicker = () => {
|
||||
const { denomination, currencyRates } = useSettings();
|
||||
const switchCurrency = () => {
|
||||
let newCurrency;
|
||||
if (this.props.denomination === 'BTC') {
|
||||
if (this.props.currencies['USD']) {
|
||||
newCurrency = "USD";
|
||||
if (denomination === 'BTC') {
|
||||
if (currencyRates['USD']) {
|
||||
newCurrency = 'USD';
|
||||
}
|
||||
} else if (this.props.denomination === 'USD') {
|
||||
newCurrency = "BTC";
|
||||
} else if (denomination === 'USD') {
|
||||
newCurrency = 'BTC';
|
||||
}
|
||||
let setCurrency = {
|
||||
"put-entry": {
|
||||
'put-entry': {
|
||||
value: newCurrency,
|
||||
"entry-key": "currency",
|
||||
"bucket-key": "btc-wallet",
|
||||
}
|
||||
}
|
||||
this.props.api.settingsEvent(setCurrency);
|
||||
}
|
||||
'entry-key': 'currency',
|
||||
'bucket-key': 'btc-wallet',
|
||||
},
|
||||
};
|
||||
api.settingsEvent(setCurrency);
|
||||
};
|
||||
|
||||
return (
|
||||
<Row style={{ cursor: 'pointer' }} onClick={() => switchCurrency()}>
|
||||
<Icon icon="ChevronDouble" color="orange" pt="2px" pr={1} />
|
||||
<Text color="orange" fontSize={1}>
|
||||
{denomination}
|
||||
</Text>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Row style={{cursor: "pointer"}} onClick={this.switchCurrency}>
|
||||
<Icon icon="ChevronDouble" color="orange" pt="2px" pr={1} />
|
||||
<Text color="orange" fontSize={1}>{this.props.denomination}</Text>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default CurrencyPicker;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Text } from '@tlon/indigo-react';
|
||||
|
||||
const errorToString = (error) => {
|
||||
@ -26,16 +26,12 @@ const errorToString = (error) => {
|
||||
if (error === 'invalid-signed') {
|
||||
return 'Invalid signed bitcoin transaction';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function Error(props) {
|
||||
const error = errorToString(props.error);
|
||||
const Error = ({ error, ...rest }) => (
|
||||
<Text color="red" {...rest}>
|
||||
{errorToString(error)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return(
|
||||
<Text
|
||||
color='red'
|
||||
{...props}>
|
||||
{error}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
export default Error;
|
||||
|
@ -1,99 +1,99 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
Button,
|
||||
Col,
|
||||
StatelessRadioButtonField as RadioButton,
|
||||
Label,
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
const feeLevels = {
|
||||
low: 'low',
|
||||
mid: 'mid',
|
||||
high: 'high',
|
||||
};
|
||||
|
||||
export default class FeePicker extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const FeePicker = ({ feeChoices, feeSelect, feeDismiss }) => {
|
||||
const [feeSelected, setFeeSelected] = useState(feeLevels.mid);
|
||||
const [modalElement, setModalElement] = useState();
|
||||
const modalRef = useRef();
|
||||
|
||||
this.state = {
|
||||
selected: 'mid'
|
||||
}
|
||||
// const clickDismiss = (e) => {
|
||||
// console.log(modalElement, e);
|
||||
// // if (modalRef && !modalRef.contains(e.target)) {
|
||||
// // feeDismiss();
|
||||
// // }
|
||||
// };
|
||||
|
||||
this.select = this.select.bind(this);
|
||||
this.clickDismiss = this.clickDismiss.bind(this);
|
||||
this.setModalRef = this.setModalRef.bind(this);
|
||||
}
|
||||
const select = (which) => {
|
||||
setFeeSelected(which);
|
||||
feeSelect(which);
|
||||
feeDismiss();
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener("click", this.clickDismiss);
|
||||
}
|
||||
// useEffect(() => {
|
||||
// document.addEventListener('click', (e) => clickDismiss(e));
|
||||
// setModalElement(modalRef.current);
|
||||
// console.log(modalRef.current);
|
||||
// return () => document.addEventListener('click', clickDismiss);
|
||||
// }, []);
|
||||
|
||||
componentWillUnount() {
|
||||
document.removeEventListener("click", this.clickDismiss);
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
// ref={modalRef}
|
||||
// onClick={() => feeDismiss()}
|
||||
position="absolute"
|
||||
p={4}
|
||||
border="1px solid green"
|
||||
zIndex={10}
|
||||
backgroundColor="white"
|
||||
borderRadius={3}
|
||||
>
|
||||
<Text fontSize={1} color="black" fontWeight="bold" mb={4}>
|
||||
Transaction Speed
|
||||
</Text>
|
||||
<Col mt={4}>
|
||||
<RadioButton
|
||||
name="feeRadio"
|
||||
selected={feeSelected === feeLevels.low}
|
||||
p="2"
|
||||
onChange={() => {
|
||||
select('low');
|
||||
}}
|
||||
>
|
||||
<Label fontSize="14px">
|
||||
Slow: {feeChoices.low[1]} sats/vbyte ~{feeChoices.low[0]}m
|
||||
</Label>
|
||||
</RadioButton>
|
||||
|
||||
setModalRef(n) {
|
||||
this.modalRef = n;
|
||||
}
|
||||
<RadioButton
|
||||
name="feeRadio"
|
||||
selected={feeSelected === feeLevels.mid}
|
||||
p="2"
|
||||
onChange={() => {
|
||||
select('mid');
|
||||
}}
|
||||
>
|
||||
<Label fontSize="14px">
|
||||
Normal: {feeChoices.mid[1]} sats/vbyte ~{feeChoices.mid[0]}m
|
||||
</Label>
|
||||
</RadioButton>
|
||||
|
||||
clickDismiss(e) {
|
||||
if (this.modalRef && !(this.modalRef.contains(e.target))){
|
||||
this.props.feeDismiss();
|
||||
}
|
||||
}
|
||||
<RadioButton
|
||||
name="feeRadio"
|
||||
selected={feeSelected === feeLevels.high}
|
||||
p="2"
|
||||
onChange={() => {
|
||||
select('high');
|
||||
}}
|
||||
>
|
||||
<Label fontSize="14px">
|
||||
Fast: {feeChoices.high[1]} sats/vbyte ~{feeChoices.high[0]}m
|
||||
</Label>
|
||||
</RadioButton>
|
||||
</Col>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
select(which) {
|
||||
this.setState({selected: which});
|
||||
this.props.feeSelect(which);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Box
|
||||
ref={this.setModalRef}
|
||||
position="absolute" p={4}
|
||||
border="1px solid green" zIndex={10}
|
||||
backgroundColor="white" borderRadius={3}
|
||||
>
|
||||
<Text fontSize={1} color="black" fontWeight="bold" mb={4}>
|
||||
Transaction Speed
|
||||
</Text>
|
||||
<Col mt={4}>
|
||||
<RadioButton
|
||||
name="feeRadio"
|
||||
selected={this.state.selected === 'low'}
|
||||
p="2"
|
||||
onChange={() => {
|
||||
this.select('low');
|
||||
}}
|
||||
>
|
||||
<Label fontSize="14px">Slow: {this.props.feeChoices.low[1]} sats/vbyte ~{this.props.feeChoices.low[0]}m</Label>
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
name="feeRadio"
|
||||
selected={this.state.selected === 'mid'}
|
||||
p="2"
|
||||
onChange={() => {
|
||||
this.select('mid');
|
||||
}}
|
||||
>
|
||||
<Label fontSize="14px">Normal: {this.props.feeChoices.mid[1]} sats/vbyte ~{this.props.feeChoices.mid[0]}m</Label>
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
name="feeRadio"
|
||||
selected={this.state.selected === 'high'}
|
||||
p="2"
|
||||
onChange={() => {
|
||||
this.select('high');
|
||||
}}
|
||||
>
|
||||
<Label fontSize="14px">Fast: {this.props.feeChoices.high[1]} sats/vbyte ~{this.props.feeChoices.high[0]}m</Label>
|
||||
</RadioButton>
|
||||
</Col>
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default FeePicker;
|
||||
|
@ -1,78 +1,81 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
} from '@tlon/indigo-react';
|
||||
import React from 'react';
|
||||
import { Box, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
|
||||
export default class Header extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const Header = ({ settings }) => {
|
||||
const { provider } = useSettings();
|
||||
let icon = settings ? 'X' : 'Adjust';
|
||||
let iconColor = settings ? 'black' : 'orange';
|
||||
let iconLink = settings ? '/~btc' : '/~btc/settings';
|
||||
|
||||
let connection = null;
|
||||
let badge = null;
|
||||
if (!(provider && provider.connected)) {
|
||||
connection = (
|
||||
<Text fontSize={1} color="red" fontWeight="bold" mr={3}>
|
||||
Provider Offline
|
||||
</Text>
|
||||
);
|
||||
|
||||
if (!settings) {
|
||||
badge = (
|
||||
<Box
|
||||
borderRadius="50%"
|
||||
width="8px"
|
||||
height="8px"
|
||||
backgroundColor="red"
|
||||
position="absolute"
|
||||
top="0px"
|
||||
right="0px"
|
||||
></Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let icon = this.props.settings ? "X" : "Adjust";
|
||||
let iconColor = this.props.settings ? "black" : "orange";
|
||||
let iconLink = this.props.settings ? "/~btc" : "/~btc/settings";
|
||||
|
||||
let connection = null;
|
||||
let badge = null;
|
||||
if (!(this.props.state.provider && this.props.state.provider.connected)) {
|
||||
connection =
|
||||
<Text fontSize={1} color="red" fontWeight="bold" mr={3}>
|
||||
Provider Offline
|
||||
return (
|
||||
<Row
|
||||
height={8}
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
pt={5}
|
||||
pb={5}
|
||||
>
|
||||
<Row alignItems="center" justifyContent="center">
|
||||
<Box
|
||||
backgroundColor="orange"
|
||||
borderRadius={4}
|
||||
mr="12px"
|
||||
width={5}
|
||||
height={5}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Icon icon="Bitcoin" width={4} p={1} height={4} color="white" />
|
||||
</Box>
|
||||
<Text fontSize={2} fontWeight="bold" color="orange">
|
||||
Bitcoin
|
||||
</Text>
|
||||
|
||||
if (!this.props.settings) {
|
||||
badge = <Box borderRadius="50%" width="8px" height="8px" backgroundColor="red" position="absolute" top="0px" right="0px"></Box>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Row
|
||||
height={8}
|
||||
width='100%'
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
pt={5}
|
||||
pb={5}
|
||||
>
|
||||
<Row alignItems="center" justifyContent="center">
|
||||
<Box backgroundColor="orange"
|
||||
borderRadius={4} mr="12px"
|
||||
</Row>
|
||||
<Row alignItems="center">
|
||||
{connection}
|
||||
<Link to={iconLink}>
|
||||
<Box
|
||||
backgroundColor="white"
|
||||
borderRadius={4}
|
||||
width={5}
|
||||
height={5}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
p={2}
|
||||
position="relative"
|
||||
>
|
||||
<Icon icon="Bitcoin" width={4} p={1} height={4} color="white"/>
|
||||
{badge}
|
||||
<Icon icon={icon} color={iconColor} />
|
||||
</Box>
|
||||
<Text fontSize={2} fontWeight="bold" color="orange">
|
||||
Bitcoin
|
||||
</Text>
|
||||
</Row>
|
||||
<Row alignItems="center">
|
||||
{connection}
|
||||
<Link to={iconLink}>
|
||||
<Box backgroundColor="white"
|
||||
borderRadius={4}
|
||||
width={5}
|
||||
height={5}
|
||||
p={2}
|
||||
position="relative"
|
||||
>
|
||||
{badge}
|
||||
<Icon icon={icon} color={iconColor} />
|
||||
</Box>
|
||||
</Link>
|
||||
</Row>
|
||||
</Link>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
@ -9,18 +9,15 @@ import {
|
||||
Col,
|
||||
LoadingSpinner,
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
import { Sigil } from './sigil.js'
|
||||
|
||||
import { Sigil } from './sigil.js';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import * as kg from 'urbit-key-generation';
|
||||
import * as bip39 from 'bip39';
|
||||
|
||||
import Sent from './sent.js'
|
||||
import Sent from './sent.js';
|
||||
import { patp2dec, isValidPatq, isValidPatp } from 'urbit-ob';
|
||||
|
||||
import { satsToCurrency } from '../../lib/util.js';
|
||||
import Error from './error.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
import { api } from '../../api';
|
||||
|
||||
const BITCOIN_MAINNET_INFO = {
|
||||
messagePrefix: '\x18Bitcoin Signed Message:\n',
|
||||
@ -46,246 +43,234 @@ const BITCOIN_TESTNET_INFO = {
|
||||
wif: 0xef,
|
||||
};
|
||||
|
||||
export default class Invoice extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const Invoice = ({ stopSending, payee, satsAmount }) => {
|
||||
const {
|
||||
error,
|
||||
currencyRates,
|
||||
psbt,
|
||||
fee,
|
||||
broadcastSuccess,
|
||||
network,
|
||||
denomination,
|
||||
} = useSettings();
|
||||
const [masterTicket, setMasterTicket] = useState('');
|
||||
const [ready, setReady] = useState(false);
|
||||
const [localError, setLocalError] = useState(error);
|
||||
const [broadcasting, setBroadcasting] = useState(false);
|
||||
const invoiceRef = useRef();
|
||||
|
||||
this.state = {
|
||||
masterTicket: '',
|
||||
ready: false,
|
||||
error: this.props.state.error,
|
||||
sent: false,
|
||||
broadcasting: false,
|
||||
};
|
||||
|
||||
this.checkTicket = this.checkTicket.bind(this);
|
||||
this.broadCastTx = this.broadCastTx.bind(this);
|
||||
this.sendBitcoin = this.sendBitcoin.bind(this);
|
||||
this.clickDismiss = this.clickDismiss.bind(this);
|
||||
this.setInvoiceRef = this.setInvoiceRef.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
document.addEventListener("click", this.clickDismiss);
|
||||
}
|
||||
|
||||
componentWillUnMount(){
|
||||
document.removeEventListener("click", this.clickDismiss);
|
||||
}
|
||||
|
||||
setInvoiceRef(n){
|
||||
this.invoiceRef = n;
|
||||
}
|
||||
|
||||
clickDismiss(e){
|
||||
if (this.invoiceRef && !(this.invoiceRef.contains(e.target))) {
|
||||
this.props.stopSending();
|
||||
useEffect(() => {
|
||||
if (broadcasting && localError !== '') {
|
||||
setBroadcasting(false);
|
||||
}
|
||||
}
|
||||
}, [error, broadcasting, setBroadcasting]);
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.broadcasting) {
|
||||
if (this.state.error !== '') {
|
||||
this.setState({broadcasting: false});
|
||||
}
|
||||
const clickDismiss = (e) => {
|
||||
if (invoiceRef && !invoiceRef.contains(e.target)) {
|
||||
stopSending();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
broadCastTx(psbtHex) {
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', clickDismiss);
|
||||
return () => document.removeEventListener('click', clickDismiss);
|
||||
}, []);
|
||||
|
||||
const broadCastTx = (psbtHex) => {
|
||||
let command = {
|
||||
'broadcast-tx': psbtHex
|
||||
}
|
||||
return this.props.api.btcWalletCommand(command)
|
||||
}
|
||||
'broadcast-tx': psbtHex,
|
||||
};
|
||||
return api.btcWalletCommand(command);
|
||||
};
|
||||
|
||||
sendBitcoin(ticket, psbt) {
|
||||
const sendBitcoin = (ticket, psbt) => {
|
||||
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;
|
||||
setBroadcasting(true);
|
||||
kg.generateWallet({
|
||||
ticket,
|
||||
ship: parseInt(patp2dec('~' + window.ship)),
|
||||
}).then((urbitWallet) => {
|
||||
// const { xpub } =
|
||||
// network === 'testnet'
|
||||
// ? urbitWallet.bitcoinTestnet.keys
|
||||
// : urbitWallet.bitcoinMainnet.keys;
|
||||
|
||||
const { xprv: zprv } = urbitWallet.bitcoinMainnet.keys;
|
||||
const { xprv: vprv } = urbitWallet.bitcoinTestnet.keys;
|
||||
const { xprv: zprv } = urbitWallet.bitcoinMainnet.keys;
|
||||
const { xprv: vprv } = urbitWallet.bitcoinTestnet.keys;
|
||||
|
||||
const isTestnet = (this.props.network === 'testnet');
|
||||
const derivationPrefix = isTestnet ? "m/84'/1'/0'/" : "m/84'/0'/0'/";
|
||||
const isTestnet = 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);
|
||||
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();
|
||||
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});
|
||||
}
|
||||
});
|
||||
broadCastTx(hex);
|
||||
} catch (e) {
|
||||
setLocalError('invalid-master-ticket');
|
||||
setBroadcasting(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
checkTicket(e){
|
||||
const checkTicket = (e) => {
|
||||
// TODO: port over bridge ticket validation logic
|
||||
let masterTicket = e.target.value;
|
||||
let ready = isValidPatq(masterTicket);
|
||||
let error = (ready) ? '' : 'invalid-master-ticket';
|
||||
this.setState({masterTicket, ready, error});
|
||||
setMasterTicket(e.target.value);
|
||||
setReady(isValidPatq(e.target.value));
|
||||
setLocalError(isValidPatq(e.target.value) ? '' : 'invalid-master-ticket');
|
||||
};
|
||||
|
||||
let inputColor = 'black';
|
||||
let inputBg = 'white';
|
||||
let inputBorder = 'lightGray';
|
||||
|
||||
if (error !== '') {
|
||||
inputColor = 'red';
|
||||
inputBg = 'veryLightRed';
|
||||
inputBorder = 'red';
|
||||
}
|
||||
|
||||
render() {
|
||||
const broadcastSuccess = this.props.state.broadcastSuccess;
|
||||
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates, fee } = this.props;
|
||||
const { sent, error } = this.state;
|
||||
const isShip = isValidPatp(payee);
|
||||
|
||||
let inputColor = 'black';
|
||||
let inputBg = 'white';
|
||||
let inputBorder = 'lightGray';
|
||||
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>
|
||||
);
|
||||
|
||||
if (error !== '') {
|
||||
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}
|
||||
denomination={denomination}
|
||||
currencyRates={currencyRates}
|
||||
satsAmount={satsAmount}
|
||||
/> :
|
||||
return (
|
||||
<>
|
||||
{broadcastSuccess ? (
|
||||
<Sent payee={payee} stopSending={stopSending} satsAmount={satsAmount} />
|
||||
) : (
|
||||
<Col
|
||||
ref={invoiceRef}
|
||||
width="100%"
|
||||
backgroundColor="white"
|
||||
borderRadius="48px"
|
||||
mb={5}
|
||||
p={5}
|
||||
onClick={() => stopSending()}
|
||||
>
|
||||
<Col
|
||||
ref={this.setInvoiceRef}
|
||||
width='100%'
|
||||
backgroundColor='white'
|
||||
borderRadius='48px'
|
||||
mb={5}
|
||||
p={5}
|
||||
mt={4}
|
||||
backgroundColor="veryLightGreen"
|
||||
borderRadius="24px"
|
||||
alignItems="center"
|
||||
>
|
||||
<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', 'overflow-wrap': 'anywhere'}}
|
||||
>{payee}</Text>
|
||||
</Row>
|
||||
</Col>
|
||||
<Row mt={3} mb={2} alignItems="center">
|
||||
<Text gray fontSize={1} fontWeight='600' mr={4}>
|
||||
Ticket
|
||||
<Row>
|
||||
<Text color="green" fontSize="40px">
|
||||
{satsToCurrency(satsAmount, denomination, currencyRates)}
|
||||
</Text>
|
||||
<Input
|
||||
value={this.state.masterTicket}
|
||||
fontSize="14px"
|
||||
type="password"
|
||||
name="masterTicket"
|
||||
obscure={value => value.replace(/[^~-]+/g, '••••••')}
|
||||
placeholder="••••••-••••••-••••••-••••••"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
color={inputColor}
|
||||
backgroundColor={inputBg}
|
||||
borderColor={inputBorder}
|
||||
onChange={this.checkTicket}
|
||||
/>
|
||||
</Row>
|
||||
{(error !== '') &&
|
||||
<Row>
|
||||
<Error
|
||||
fontSize='14px'
|
||||
color='red'
|
||||
error={error}
|
||||
mt={2}/>
|
||||
</Row>
|
||||
}
|
||||
<Row
|
||||
flexDirection='row-reverse'
|
||||
mt={4}
|
||||
alignItems="center"
|
||||
>
|
||||
<Button
|
||||
primary
|
||||
children='Send BTC'
|
||||
mr={3}
|
||||
fontSize={1}
|
||||
border="none"
|
||||
borderRadius='24px'
|
||||
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"}
|
||||
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"}
|
||||
height='48px'
|
||||
onClick={() => this.sendBitcoin(this.state.masterTicket, psbt)}
|
||||
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>
|
||||
<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', 'overflow-wrap': 'anywhere' }}
|
||||
>
|
||||
{payee}
|
||||
</Text>
|
||||
</Row>
|
||||
</Col>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
<Row mt={3} mb={2} alignItems="center">
|
||||
<Text gray fontSize={1} fontWeight="600" mr={4}>
|
||||
Ticket
|
||||
</Text>
|
||||
<Input
|
||||
value={masterTicket}
|
||||
fontSize="14px"
|
||||
type="password"
|
||||
name="masterTicket"
|
||||
obscure={(value) => value.replace(/[^~-]+/g, '••••••')}
|
||||
placeholder="••••••-••••••-••••••-••••••"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
color={inputColor}
|
||||
backgroundColor={inputBg}
|
||||
borderColor={inputBorder}
|
||||
onChange={() => checkTicket()}
|
||||
/>
|
||||
</Row>
|
||||
{error !== '' && (
|
||||
<Row>
|
||||
<Error fontSize="14px" color="red" error={error} mt={2} />
|
||||
</Row>
|
||||
)}
|
||||
<Row flexDirection="row-reverse" mt={4} alignItems="center">
|
||||
<Button
|
||||
primary
|
||||
mr={3}
|
||||
fontSize={1}
|
||||
border="none"
|
||||
borderRadius="24px"
|
||||
color={ready && !error && !broadcasting ? 'white' : 'lighterGray'}
|
||||
backgroundColor={
|
||||
ready && !error && !broadcasting ? 'green' : 'veryLightGray'
|
||||
}
|
||||
height="48px"
|
||||
onClick={() => sendBitcoin(masterTicket, psbt)}
|
||||
disabled={!ready || error || broadcasting}
|
||||
style={{
|
||||
cursor:
|
||||
ready && !error && !broadcasting ? 'pointer' : 'default',
|
||||
}}
|
||||
>
|
||||
Send BTC
|
||||
</Button>
|
||||
{broadcasting ? <LoadingSpinner mr={3} /> : null}
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Invoice;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
@ -8,159 +8,157 @@ import {
|
||||
Row,
|
||||
LoadingSpinner,
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
import { isValidPatp } from 'urbit-ob';
|
||||
import { api } from '../../api';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
|
||||
export default class ProviderModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const providerStatuses = {
|
||||
checking: 'checking',
|
||||
failed: 'failed',
|
||||
ready: 'ready',
|
||||
initial: '',
|
||||
};
|
||||
|
||||
this.state = {
|
||||
potentialProvider: null,
|
||||
checkingProvider: false,
|
||||
providerFailed: false,
|
||||
ready: false,
|
||||
provider: null,
|
||||
connecting: false,
|
||||
};
|
||||
const ProviderModal = () => {
|
||||
const { providerPerms } = useSettings();
|
||||
const [providerStatus, setProviderStatus] = useState(
|
||||
providerStatuses.initial
|
||||
);
|
||||
const [potentialProvider, setPotentialProvider] = useState(null);
|
||||
const [provider, setProvider] = useState(null);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
|
||||
this.checkProvider = this.checkProvider.bind(this);
|
||||
this.submitProvider = this.submitProvider.bind(this);
|
||||
}
|
||||
|
||||
checkProvider(e) {
|
||||
const checkProvider = (e) => {
|
||||
// TODO: loading states
|
||||
let provider = e.target.value;
|
||||
let ready = false;
|
||||
let checkingProvider = false;
|
||||
let potentialProvider = this.state.potentialProvider;
|
||||
|
||||
if (isValidPatp(provider)) {
|
||||
setProviderStatus(providerStatuses.initial);
|
||||
let givenProvider = e.target.value;
|
||||
if (isValidPatp(givenProvider)) {
|
||||
let command = {
|
||||
'check-provider': provider,
|
||||
'check-provider': givenProvider,
|
||||
};
|
||||
potentialProvider = provider;
|
||||
checkingProvider = true;
|
||||
this.props.api.btcWalletCommand(command);
|
||||
setPotentialProvider(givenProvider);
|
||||
setProviderStatus(providerStatuses.checking);
|
||||
api.btcWalletCommand(command);
|
||||
setTimeout(() => {
|
||||
this.setState({ providerFailed: true, checkingProvider: false });
|
||||
setProviderStatus(providerStatuses.failed);
|
||||
}, 5000);
|
||||
}
|
||||
this.setState({ provider, ready, checkingProvider, potentialProvider });
|
||||
}
|
||||
setProvider(givenProvider);
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.state.ready) {
|
||||
if (this.props.providerPerms[this.state.provider]) {
|
||||
this.setState({
|
||||
ready: true,
|
||||
checkingProvider: false,
|
||||
providerFailed: false,
|
||||
});
|
||||
const submitProvider = () => {
|
||||
if (providerStatus === providerStatuses.ready) {
|
||||
let command = {
|
||||
'set-provider': provider,
|
||||
};
|
||||
api.btcWalletCommand(command);
|
||||
setConnecting(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (providerStatus !== providerStatuses.ready) {
|
||||
if (providerPerms.provider === provider && providerPerms.permitted) {
|
||||
setProviderStatus(providerStatuses.ready);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [providerStatus, providerPerms, provider, setProviderStatus]);
|
||||
|
||||
submitProvider() {
|
||||
if (this.state.ready) {
|
||||
let command = {
|
||||
'set-provider': this.state.provider,
|
||||
};
|
||||
this.props.api.btcWalletCommand(command);
|
||||
this.setState({ connecting: true });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let workingNode = null;
|
||||
let workingColor = null;
|
||||
let workingBg = null;
|
||||
if (this.state.ready) {
|
||||
workingColor = 'green';
|
||||
workingBg = 'veryLightGreen';
|
||||
workingNode = (
|
||||
<Box mt={3}>
|
||||
<Text fontSize="14px" color="green">
|
||||
{this.state.provider} is a working provider node
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
} else if (this.state.providerFailed) {
|
||||
workingColor = 'red';
|
||||
workingBg = 'veryLightRed';
|
||||
workingNode = (
|
||||
<Box mt={3}>
|
||||
<Text fontSize="14px" color="red">
|
||||
{this.state.potentialProvider} is not a working provider node
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box width="100%" height="100%" padding={3}>
|
||||
<Row>
|
||||
<Icon icon="Bitcoin" mr={2} />
|
||||
<Text fontSize="14px" fontWeight="bold">
|
||||
Step 1 of 2: Set up Bitcoin Provider Node
|
||||
</Text>
|
||||
</Row>
|
||||
<Box mt={3}>
|
||||
<Text fontSize="14px" fontWeight="regular" color="gray">
|
||||
In order to perform Bitcoin transaction in Landscape, you'll
|
||||
need to set a provider node. A provider node is an urbit which
|
||||
maintains a synced Bitcoin ledger.
|
||||
<a
|
||||
fontSize="14px"
|
||||
target="_blank"
|
||||
href="https://urbit.org/bitcoin-wallet"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{' '}
|
||||
Learn More
|
||||
</a>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box mt={3} mb={2}>
|
||||
<Text fontSize="14px" fontWeight="500">
|
||||
Provider Node
|
||||
</Text>
|
||||
</Box>
|
||||
<Row alignItems="center">
|
||||
<StatelessTextInput
|
||||
mr={2}
|
||||
width="256px"
|
||||
fontSize="14px"
|
||||
type="text"
|
||||
name="masterTicket"
|
||||
placeholder="e.g. ~zod"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
mono
|
||||
backgroundColor={workingBg}
|
||||
color={workingColor}
|
||||
borderColor={workingColor}
|
||||
onChange={this.checkProvider}
|
||||
/>
|
||||
{this.state.checkingProvider ? <LoadingSpinner /> : null}
|
||||
</Row>
|
||||
{workingNode}
|
||||
<Row alignItems="center" mt={3}>
|
||||
<Button
|
||||
mr={2}
|
||||
primary
|
||||
disabled={!this.state.ready}
|
||||
fontSize="14px"
|
||||
style={{ cursor: this.state.ready ? 'pointer' : 'default' }}
|
||||
onClick={() => {
|
||||
this.submitProvider(this.state.provider);
|
||||
}}
|
||||
>
|
||||
Set Peer Node
|
||||
</Button>
|
||||
{this.state.connecting ? <LoadingSpinner /> : null}
|
||||
</Row>
|
||||
let workingNode = null;
|
||||
let workingColor = null;
|
||||
let workingBg = null;
|
||||
if (providerStatus === providerStatuses.ready) {
|
||||
workingColor = 'green';
|
||||
workingBg = 'veryLightGreen';
|
||||
workingNode = (
|
||||
<Box mt={3}>
|
||||
<Text fontSize="14px" color="green">
|
||||
{provider} is a working provider node
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
} else if (providerStatus === providerStatuses.failed) {
|
||||
workingColor = 'red';
|
||||
workingBg = 'veryLightRed';
|
||||
workingNode = (
|
||||
<Box mt={3}>
|
||||
<Text fontSize="14px" color="red">
|
||||
{potentialProvider} is not a working provider node
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box width="100%" height="100%" padding={3}>
|
||||
<Row>
|
||||
<Icon icon="Bitcoin" mr={2} />
|
||||
<Text fontSize="14px" fontWeight="bold">
|
||||
Step 1 of 2: Set up Bitcoin Provider Node
|
||||
</Text>
|
||||
</Row>
|
||||
<Box mt={3}>
|
||||
<Text fontSize="14px" fontWeight="regular" color="gray">
|
||||
In order to perform Bitcoin transaction in Landscape, you'll need
|
||||
to set a provider node. A provider node is an urbit which maintains a
|
||||
synced Bitcoin ledger.
|
||||
<a
|
||||
fontSize="14px"
|
||||
target="_blank"
|
||||
href="https://urbit.org/bitcoin-wallet"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{' '}
|
||||
Learn More
|
||||
</a>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box mt={3} mb={2}>
|
||||
<Text fontSize="14px" fontWeight="500">
|
||||
Provider Node
|
||||
</Text>
|
||||
</Box>
|
||||
<Row alignItems="center">
|
||||
<StatelessTextInput
|
||||
mr={2}
|
||||
width="256px"
|
||||
fontSize="14px"
|
||||
type="text"
|
||||
name="masterTicket"
|
||||
placeholder="e.g. ~zod"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
mono
|
||||
backgroundColor={workingBg}
|
||||
color={workingColor}
|
||||
borderColor={workingColor}
|
||||
onChange={(e) => checkProvider(e)}
|
||||
/>
|
||||
{providerStatus === providerStatuses.checking ? (
|
||||
<LoadingSpinner />
|
||||
) : null}
|
||||
</Row>
|
||||
{workingNode}
|
||||
<Row alignItems="center" mt={3}>
|
||||
<Button
|
||||
mr={2}
|
||||
primary
|
||||
disabled={providerStatus !== providerStatuses.ready}
|
||||
fontSize="14px"
|
||||
style={{
|
||||
cursor:
|
||||
providerStatus === providerStatuses.ready ? 'pointer' : 'default',
|
||||
}}
|
||||
onClick={() => {
|
||||
submitProvider(provider);
|
||||
}}
|
||||
>
|
||||
Set Peer Node
|
||||
</Button>
|
||||
{connecting ? <LoadingSpinner /> : null}
|
||||
</Row>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProviderModal;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
@ -8,450 +8,443 @@ import {
|
||||
Button,
|
||||
Col,
|
||||
LoadingSpinner,
|
||||
StatelessRadioButtonField as RadioButton,
|
||||
} 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 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';
|
||||
|
||||
import * as ob from 'urbit-ob';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
import { api } from '../../api';
|
||||
|
||||
export default class Send extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const focusFields = {
|
||||
empty: '',
|
||||
payee: 'payee',
|
||||
currency: 'currency',
|
||||
sats: 'sats',
|
||||
note: 'note',
|
||||
};
|
||||
|
||||
this.state = {
|
||||
signing: false,
|
||||
denomAmount: '0.00',
|
||||
satsAmount: '0',
|
||||
payee: '',
|
||||
checkingPatp: false,
|
||||
payeeType: '',
|
||||
ready: false,
|
||||
validPayee: false,
|
||||
focusPayee: true,
|
||||
focusCurrency: false,
|
||||
focusSats: false,
|
||||
focusNote: false,
|
||||
submitting: false,
|
||||
feeChoices: {
|
||||
low: [10, 1],
|
||||
mid: [10, 1],
|
||||
high: [10, 1],
|
||||
},
|
||||
feeValue: "mid",
|
||||
showModal: false,
|
||||
note: '',
|
||||
choosingSignMethod: false,
|
||||
signMethod: 'bridge',
|
||||
};
|
||||
const Send = ({ stopSending, value, conversion }) => {
|
||||
const { error, setError, network, psbt, denomination, shipWallets } =
|
||||
useSettings();
|
||||
const [signing, setSigning] = useState(false);
|
||||
const [denomAmount, setDenomAmount] = useState('0.00');
|
||||
const [satsAmount, setSatsAmount] = useState('0');
|
||||
const [payee, setPayee] = useState('');
|
||||
const [checkingPatp, setCheckingPatp] = useState(false);
|
||||
const [payeeType, setPayeeType] = useState('');
|
||||
const [ready, setReady] = useState(false);
|
||||
const [validPayee, setValidPayee] = useState(false);
|
||||
const [focusedField, setFocusedField] = useState(focusFields.empty);
|
||||
const [feeChoices, setFeeChoices] = useState({
|
||||
low: [10, 1],
|
||||
mid: [10, 1],
|
||||
high: [10, 1],
|
||||
});
|
||||
const [feeValue, setFeeValue] = useState('mid');
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [note, setNote] = useState('');
|
||||
const [choosingSignMethod, setChoosingSignMethod] = useState(false);
|
||||
const [signMethod, setSignMethod] = useState('bridge');
|
||||
|
||||
this.initPayment = this.initPayment.bind(this);
|
||||
this.checkPayee = this.checkPayee.bind(this);
|
||||
this.feeSelect = this.feeSelect.bind(this);
|
||||
this.feeDismiss = this.feeDismiss.bind(this);
|
||||
this.toggleSignMethod = this.toggleSignMethod.bind(this);
|
||||
this.setSignMethod = this.setSignMethod.bind(this);
|
||||
}
|
||||
const feeDismiss = () => {
|
||||
setShowModal(false);
|
||||
};
|
||||
|
||||
feeDismiss() {
|
||||
this.setState({showModal: false});
|
||||
}
|
||||
const feeSelect = (which) => {
|
||||
setFeeValue(which);
|
||||
};
|
||||
|
||||
feeSelect(which) {
|
||||
this.setState({feeValue: which});
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if (this.props.network === 'bitcoin'){
|
||||
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;
|
||||
this.setState({
|
||||
feeChoices: {
|
||||
high: [30, n.estimates[30]["sat_per_vbyte"]],
|
||||
mid: [180, n.estimates[180]["sat_per_vbyte"]],
|
||||
low: [360, n.estimates[360]["sat_per_vbyte"]],
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setSignMethod(signMethod) {
|
||||
this.setState({signMethod, choosingSignMethod: false});
|
||||
}
|
||||
|
||||
checkPayee(e){
|
||||
store.handleEvent({data: {error: ''}});
|
||||
|
||||
let payee = e.target.value;
|
||||
let isPatp = ob.isValidPatp(payee);
|
||||
let isAddress = validate(payee);
|
||||
const handleSetSignMethod = (signMethod) => {
|
||||
setSignMethod(signMethod);
|
||||
setChoosingSignMethod(false);
|
||||
};
|
||||
|
||||
const checkPayee = (e) => {
|
||||
console.log('checkPayee', { e });
|
||||
setError('');
|
||||
|
||||
let payeeReceived = e.target.value;
|
||||
let isPatp = ob.isValidPatp(payeeReceived);
|
||||
let isAddress = validate(payeeReceived);
|
||||
console.log({ payeeReceived, isPatp, isAddress });
|
||||
if (isPatp) {
|
||||
let command = {'check-payee': payee}
|
||||
this.props.api.btcWalletCommand(command)
|
||||
console.log('isPatp', isPatp);
|
||||
let command = { 'check-payee': payeeReceived };
|
||||
api.btcWalletCommand(command);
|
||||
setTimeout(() => {
|
||||
this.setState({checkingPatp: false});
|
||||
setCheckingPatp(false);
|
||||
}, 5000);
|
||||
this.setState({
|
||||
checkingPatp: true,
|
||||
payeeType: 'ship',
|
||||
payee,
|
||||
});
|
||||
setCheckingPatp(true);
|
||||
setPayeeType('ship');
|
||||
setPayee(payeeReceived);
|
||||
} else if (isAddress) {
|
||||
this.setState({
|
||||
payee,
|
||||
ready: true,
|
||||
checkingPatp: false,
|
||||
payeeType: 'address',
|
||||
validPayee: true,
|
||||
});
|
||||
setPayee(payeeReceived);
|
||||
setReady(true);
|
||||
setCheckingPatp(false);
|
||||
setPayeeType('address');
|
||||
setValidPayee(true);
|
||||
} else {
|
||||
this.setState({
|
||||
payee,
|
||||
ready: false,
|
||||
checkingPatp: false,
|
||||
payeeType: '',
|
||||
validPayee: false,
|
||||
});
|
||||
setPayee(payeeReceived);
|
||||
setReady(false);
|
||||
setCheckingPatp(false);
|
||||
setPayeeType('');
|
||||
setValidPayee(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if ((prevProps.error !== this.props.error) &&
|
||||
(this.props.error !== '') && (this.props.error !== 'broadcast-fail')) {
|
||||
this.setState({signing: false});
|
||||
}
|
||||
const toggleSignMethod = () => {
|
||||
setChoosingSignMethod(!choosingSignMethod);
|
||||
};
|
||||
|
||||
if (!this.state.ready && this.state.checkingPatp) {
|
||||
if (this.props.shipWallets[this.state.payee.slice(1)]) {
|
||||
this.setState({ready: true, checkingPatp: false, validPayee: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleSignMethod(toggle) {
|
||||
this.setState({choosingSignMethod: !toggle});
|
||||
}
|
||||
|
||||
initPayment() {
|
||||
if (this.state.payeeType === 'ship') {
|
||||
const initPayment = () => {
|
||||
if (payeeType === 'ship') {
|
||||
let command = {
|
||||
'init-payment': {
|
||||
'payee': this.state.payee,
|
||||
'value': parseInt(this.state.satsAmount),
|
||||
'feyb': this.state.feeChoices[this.state.feeValue][1],
|
||||
'note': (this.state.note || null),
|
||||
}
|
||||
}
|
||||
this.props.api.btcWalletCommand(command).then(res => this.setState({signing: true}));
|
||||
} else if (this.state.payeeType === 'address') {
|
||||
payee,
|
||||
value: parseInt(satsAmount),
|
||||
feyb: feeChoices[feeValue][1],
|
||||
note: note || null,
|
||||
},
|
||||
};
|
||||
|
||||
api.btcWalletCommand(command).then(() => setSigning(true));
|
||||
} else if (payeeType === 'address') {
|
||||
let command = {
|
||||
'init-payment-external': {
|
||||
'address': this.state.payee,
|
||||
'value': parseInt(this.state.satsAmount),
|
||||
'feyb': 1,
|
||||
'note': (this.state.note || null),
|
||||
}
|
||||
}
|
||||
this.props.api.btcWalletCommand(command).then(res => this.setState({signing: true}));
|
||||
address: payee,
|
||||
value: parseInt(satsAmount),
|
||||
feyb: 1,
|
||||
note: note || null,
|
||||
},
|
||||
};
|
||||
api.btcWalletCommand(command).then(() => setSigning(true));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (network === 'bitcoin') {
|
||||
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;
|
||||
setFeeChoices({
|
||||
high: [30, n.estimates[30]['sat_per_vbyte']],
|
||||
mid: [180, n.estimates[180]['sat_per_vbyte']],
|
||||
low: [360, n.estimates[360]['sat_per_vbyte']],
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready && checkingPatp) {
|
||||
console.log({ ready, checkingPatp, shipWallets, payee });
|
||||
if (shipWallets.payee === payee.slice(1) && shipWallets.hasWallet) {
|
||||
console.log('good');
|
||||
setReady(true);
|
||||
setCheckingPatp(false);
|
||||
setValidPayee(true);
|
||||
}
|
||||
}
|
||||
}, [ready, checkingPatp, shipWallets]);
|
||||
|
||||
let payeeColor = 'black';
|
||||
let payeeBg = 'white';
|
||||
let payeeBorder = 'lightGray';
|
||||
if (error) {
|
||||
payeeColor = 'red';
|
||||
payeeBorder = 'red';
|
||||
payeeBg = 'veryLightRed';
|
||||
} else if (focusedField === focusFields.payee && validPayee) {
|
||||
payeeColor = 'green';
|
||||
payeeBorder = 'green';
|
||||
payeeBg = 'veryLightGreen';
|
||||
} else if (!focusedField === focusFields.payee && validPayee) {
|
||||
payeeColor = 'blue';
|
||||
payeeBorder = 'white';
|
||||
payeeBg = 'white';
|
||||
} else if (focusedField !== focusFields.payee && !validPayee) {
|
||||
payeeColor = 'red';
|
||||
payeeBorder = 'red';
|
||||
payeeBg = 'veryLightRed';
|
||||
} else if (
|
||||
focusedField === 'payee' &&
|
||||
!validPayee &&
|
||||
!checkingPatp &&
|
||||
payeeType === 'ship'
|
||||
) {
|
||||
payeeColor = 'red';
|
||||
payeeBorder = 'red';
|
||||
payeeBg = 'veryLightRed';
|
||||
}
|
||||
|
||||
render() {
|
||||
let payeeColor = "black";
|
||||
let payeeBg = "white";
|
||||
let payeeBorder = "lightGray";
|
||||
if (this.props.error) {
|
||||
payeeColor="red";
|
||||
payeeBorder = "red";
|
||||
payeeBg="veryLightRed";
|
||||
} else if (this.state.focusPayee && this.state.validPayee) {
|
||||
payeeColor = "green";
|
||||
payeeBorder = "green";
|
||||
payeeBg = "veryLightGreen";
|
||||
} else if (!this.state.focusPayee && this.state.validPayee){
|
||||
payeeColor="blue";
|
||||
payeeBorder = "white";
|
||||
payeeBg = "white";
|
||||
} else if (!this.state.focusPayee && !this.state.validPayee) {
|
||||
payeeColor="red";
|
||||
payeeBorder = "red";
|
||||
payeeBg="veryLightRed";
|
||||
} else if (this.state.focusPayee &&
|
||||
!this.state.validPayee &&
|
||||
!this.state.checkingPatp &&
|
||||
this.state.payeeType === 'ship'){
|
||||
payeeColor="red";
|
||||
payeeBorder = "red";
|
||||
payeeBg="veryLightRed";
|
||||
}
|
||||
const signReady = ready && parseInt(satsAmount) > 0 && !signing;
|
||||
|
||||
|
||||
const { api, value, conversion, stopSending, denomination, psbt, currencyRates, error, network, fee } = this.props;
|
||||
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 === 'masterTicket') {
|
||||
invoice =
|
||||
<Invoice
|
||||
network={network}
|
||||
api={api}
|
||||
psbt={psbt}
|
||||
fee={fee}
|
||||
currencyRates={currencyRates}
|
||||
stopSending={stopSending}
|
||||
payee={payee}
|
||||
denomination={denomination}
|
||||
satsAmount={satsAmount}
|
||||
state={this.props.state}
|
||||
/>
|
||||
} else if (signMethod === 'bridge') {
|
||||
invoice =
|
||||
<BridgeInvoice
|
||||
state={this.props.state}
|
||||
api={api}
|
||||
psbt={psbt}
|
||||
fee={fee}
|
||||
currencyRates={currencyRates}
|
||||
stopSending={stopSending}
|
||||
payee={payee}
|
||||
denomination={denomination}
|
||||
satsAmount={satsAmount}
|
||||
/>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ (signing && psbt) ? invoice :
|
||||
<Col
|
||||
width='100%'
|
||||
backgroundColor='white'
|
||||
borderRadius='48px'
|
||||
mb={5}
|
||||
p={5}
|
||||
>
|
||||
<Col width="100%">
|
||||
<Row
|
||||
justifyContent='space-between'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text highlight color='blue' fontSize={1}>Send BTC</Text>
|
||||
<Text highlight color='blue' fontSize={1}>{value}</Text>
|
||||
<Icon
|
||||
icon='X'
|
||||
cursor='pointer'
|
||||
onClick={() => stopSending()}
|
||||
/>
|
||||
</Row>
|
||||
<Row
|
||||
alignItems='center'
|
||||
mt={6}
|
||||
justifyContent='space-between'>
|
||||
<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
|
||||
}
|
||||
</Row>
|
||||
<Input
|
||||
autoFocus
|
||||
onFocus={() => {this.setState({focusPayee: true})}}
|
||||
onBlur={() => {this.setState({focusPayee: false})}}
|
||||
color={payeeColor}
|
||||
backgroundColor={payeeBg}
|
||||
borderColor={payeeBorder}
|
||||
ml={2}
|
||||
flexGrow="1"
|
||||
fontSize='14px'
|
||||
placeholder='~sampel-palnet or BTC address'
|
||||
value={payee}
|
||||
fontFamily="mono"
|
||||
disabled={signing}
|
||||
onChange={this.checkPayee}
|
||||
/>
|
||||
</Row>
|
||||
{error &&
|
||||
<Row
|
||||
alignItems='center'
|
||||
justifyContent='space-between'>
|
||||
{/* yes this is a hack */}
|
||||
<Box width='calc(40% - 30px)'/>
|
||||
<Error
|
||||
error={error}
|
||||
fontSize='14px'
|
||||
ml={2}
|
||||
mt={2}
|
||||
width='100%' />
|
||||
</Row>
|
||||
}
|
||||
<Row
|
||||
alignItems='center'
|
||||
mt={4}
|
||||
justifyContent='space-between'>
|
||||
<Text
|
||||
gray
|
||||
fontSize={1}
|
||||
fontWeight='600'
|
||||
width="40%"
|
||||
>Amount</Text>
|
||||
<Input
|
||||
onFocus={() => {this.setState({focusCurrency: true})}}
|
||||
onBlur={() => {this.setState({focusCurrency: false})}}
|
||||
fontSize='14px'
|
||||
width='100%'
|
||||
type='number'
|
||||
borderColor={this.state.focusCurrency ? "lightGray" : "none"}
|
||||
disabled={signing}
|
||||
value={denomAmount}
|
||||
onChange={e => {
|
||||
this.setState({
|
||||
denomAmount: e.target.value,
|
||||
satsAmount: Math.round(parseFloat(e.target.value) / conversion * 100000000)
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Text color="lighterGray" fontSize={1} ml={3}>{denomination}</Text>
|
||||
</Row>
|
||||
<Row
|
||||
alignItems='center'
|
||||
mt={2}
|
||||
justifyContent='space-between'>
|
||||
{/* yes this is a hack */}
|
||||
<Box width='40%'/>
|
||||
<Input
|
||||
onFocus={() => {this.setState({focusSats: true})}}
|
||||
onBlur={() => {this.setState({focusSats: false})}}
|
||||
fontSize='14px'
|
||||
width='100%'
|
||||
type='number'
|
||||
borderColor={this.state.focusSats ? "lightGray" : "none"}
|
||||
disabled={signing}
|
||||
value={satsAmount}
|
||||
onChange={e => {
|
||||
this.setState({
|
||||
denomAmount: parseFloat(e.target.value) * (conversion / 100000000),
|
||||
satsAmount: e.target.value
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Text color="lightGray" fontSize={1} ml={3}>sats</Text>
|
||||
</Row>
|
||||
<Row mt={4} width="100%" justifyContent="space-between">
|
||||
<Text
|
||||
gray
|
||||
fontSize={1}
|
||||
fontWeight='600'
|
||||
width="40%"
|
||||
>Fee</Text>
|
||||
<Row alignItems="center">
|
||||
<Text mr={2} color="lightGray" fontSize="14px">
|
||||
{this.state.feeChoices[this.state.feeValue][1]} sats/vbyte
|
||||
</Text>
|
||||
<Icon icon="ChevronSouth"
|
||||
fontSize="14px"
|
||||
color="lightGray"
|
||||
onClick={() => {if (!this.state.showModal) this.setState({showModal: true}); }}
|
||||
cursor="pointer"/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Col alignItems="center">
|
||||
{!this.state.showModal ? null :
|
||||
<FeePicker
|
||||
feeChoices={this.state.feeChoices}
|
||||
feeSelect={this.feeSelect}
|
||||
feeDismiss={this.feeDismiss}
|
||||
/>
|
||||
}
|
||||
</Col>
|
||||
<Row mt={4} width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems='center'
|
||||
>
|
||||
<Text
|
||||
gray
|
||||
fontSize={1}
|
||||
fontWeight='600'
|
||||
width="40%"
|
||||
>Note</Text>
|
||||
<Input
|
||||
onFocus={() => {this.setState({focusNote: true})}}
|
||||
onBlur={() => {this.setState({focusNote: false})}}
|
||||
fontSize='14px'
|
||||
width='100%'
|
||||
placeholder="What's this for?"
|
||||
type='text'
|
||||
borderColor={this.state.focusNote ? "lightGray" : "none"}
|
||||
disabled={signing}
|
||||
value={this.state.note}
|
||||
onChange={e => {
|
||||
this.setState({
|
||||
note: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
<Row
|
||||
flexDirection='row-reverse'
|
||||
alignItems="center"
|
||||
mt={4}
|
||||
>
|
||||
<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}
|
||||
height='48px'
|
||||
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>
|
||||
{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}>
|
||||
We recommend that you sign transactions using Bridge to protect your master ticket.
|
||||
</Text>
|
||||
</Row>
|
||||
}
|
||||
</Col>
|
||||
}
|
||||
</>
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{signing && psbt ? (
|
||||
invoice
|
||||
) : (
|
||||
<Col
|
||||
width="100%"
|
||||
backgroundColor="white"
|
||||
borderRadius="48px"
|
||||
mb={5}
|
||||
p={5}
|
||||
>
|
||||
<Col width="100%">
|
||||
<Row justifyContent="space-between" alignItems="center">
|
||||
<Text highlight color="blue" fontSize={1}>
|
||||
Send BTC
|
||||
</Text>
|
||||
<Text highlight color="blue" fontSize={1}>
|
||||
{value}
|
||||
</Text>
|
||||
<Icon icon="X" cursor="pointer" onClick={() => stopSending()} />
|
||||
</Row>
|
||||
<Row alignItems="center" mt={6} justifyContent="space-between">
|
||||
<Row
|
||||
justifyContent="space-between"
|
||||
width="calc(40% - 30px)"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text gray fontSize={1} fontWeight="600">
|
||||
To
|
||||
</Text>
|
||||
{checkingPatp ? (
|
||||
<LoadingSpinner background="midOrange" foreground="orange" />
|
||||
) : null}
|
||||
</Row>
|
||||
<Input
|
||||
// autoFocus
|
||||
onFocus={() => {
|
||||
setFocusedField(focusFields.payee);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusedField(focusFields.empty);
|
||||
}}
|
||||
color={payeeColor}
|
||||
backgroundColor={payeeBg}
|
||||
borderColor={payeeBorder}
|
||||
ml={2}
|
||||
flexGrow="1"
|
||||
fontSize="14px"
|
||||
placeholder="~sampel-palnet or BTC address"
|
||||
value={payee}
|
||||
fontFamily="mono"
|
||||
disabled={signing}
|
||||
onChange={(e) => checkPayee(e)}
|
||||
/>
|
||||
</Row>
|
||||
{error && (
|
||||
<Row alignItems="center" justifyContent="space-between">
|
||||
{/* yes this is a hack */}
|
||||
<Box width="calc(40% - 30px)" />
|
||||
<Error
|
||||
error={error}
|
||||
fontSize="14px"
|
||||
ml={2}
|
||||
mt={2}
|
||||
width="100%"
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
<Row alignItems="center" mt={4} justifyContent="space-between">
|
||||
<Text gray fontSize={1} fontWeight="600" width="40%">
|
||||
Amount
|
||||
</Text>
|
||||
<Input
|
||||
onFocus={() => {
|
||||
setFocusedField(focusFields.currency);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusedField(focusFields.empty);
|
||||
}}
|
||||
fontSize="14px"
|
||||
width="100%"
|
||||
type="number"
|
||||
borderColor={
|
||||
focusedField === focusFields.currency ? 'lightGray' : 'none'
|
||||
}
|
||||
disabled={signing}
|
||||
value={denomAmount}
|
||||
onChange={(e) => {
|
||||
setDenomAmount(e.target.value);
|
||||
setSatsAmount(
|
||||
Math.round(
|
||||
(parseFloat(e.target.value) / conversion) * 100000000
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Text color="lighterGray" fontSize={1} ml={3}>
|
||||
{denomination}
|
||||
</Text>
|
||||
</Row>
|
||||
<Row alignItems="center" mt={2} justifyContent="space-between">
|
||||
{/* yes this is a hack */}
|
||||
<Box width="40%" />
|
||||
<Input
|
||||
onFocus={() => {
|
||||
setFocusedField(focusFields.sats);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusedField(focusFields.empty);
|
||||
}}
|
||||
fontSize="14px"
|
||||
width="100%"
|
||||
type="number"
|
||||
borderColor={
|
||||
focusedField === focusFields.sats ? 'lightGray' : 'none'
|
||||
}
|
||||
disabled={signing}
|
||||
value={satsAmount}
|
||||
onChange={(e) => {
|
||||
setDenomAmount(
|
||||
parseFloat(e.target.value) * (conversion / 100000000)
|
||||
);
|
||||
setSatsAmount(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Text color="lightGray" fontSize={1} ml={3}>
|
||||
sats
|
||||
</Text>
|
||||
</Row>
|
||||
<Row mt={4} width="100%" justifyContent="space-between">
|
||||
<Text gray fontSize={1} fontWeight="600" width="40%">
|
||||
Fee
|
||||
</Text>
|
||||
<Row alignItems="center">
|
||||
<Text mr={2} color="lightGray" fontSize="14px">
|
||||
{feeChoices[feeValue][1]} sats/vbyte
|
||||
</Text>
|
||||
<Icon
|
||||
icon="ChevronSouth"
|
||||
fontSize="14px"
|
||||
color="lightGray"
|
||||
onClick={() => {
|
||||
if (!showModal) setShowModal(true);
|
||||
}}
|
||||
cursor="pointer"
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Col alignItems="center">
|
||||
{!showModal ? null : (
|
||||
<FeePicker
|
||||
feeChoices={feeChoices}
|
||||
feeSelect={feeSelect}
|
||||
feeDismiss={feeDismiss}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Row
|
||||
mt={4}
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text gray fontSize={1} fontWeight="600" width="40%">
|
||||
Note
|
||||
</Text>
|
||||
<Input
|
||||
onFocus={() => {
|
||||
setFocusedField(focusFields.note);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusedField(focusFields.empty);
|
||||
}}
|
||||
fontSize="14px"
|
||||
width="100%"
|
||||
placeholder="What's this for?"
|
||||
type="text"
|
||||
borderColor={
|
||||
focusedField === focusFields.note ? 'lightGray' : 'none'
|
||||
}
|
||||
disabled={signing}
|
||||
value={note}
|
||||
onChange={(e) => {
|
||||
setNote(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
<Row flexDirection="row-reverse" alignItems="center" mt={4}>
|
||||
<Signer
|
||||
signReady={signReady}
|
||||
choosingSignMethod={choosingSignMethod}
|
||||
signMethod={signMethod}
|
||||
setSignMethod={handleSetSignMethod}
|
||||
initPayment={initPayment}
|
||||
/>
|
||||
{!(signing && !error) ? null : (
|
||||
<LoadingSpinner
|
||||
mr={2}
|
||||
background="midOrange"
|
||||
foreground="orange"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
width="48px"
|
||||
fontSize={1}
|
||||
fontWeight="bold"
|
||||
borderRadius="24px"
|
||||
mr={2}
|
||||
height="48px"
|
||||
onClick={() => toggleSignMethod(choosingSignMethod)}
|
||||
color={signReady ? 'white' : 'lighterGray'}
|
||||
backgroundColor={
|
||||
signReady ? 'rgba(33, 157, 255, 0.2)' : 'veryLightGray'
|
||||
}
|
||||
disabled={!signReady}
|
||||
border="none"
|
||||
style={{ cursor: signReady ? 'pointer' : 'default' }}
|
||||
>
|
||||
<Icon
|
||||
icon={choosingSignMethod ? 'X' : 'Ellipsis'}
|
||||
color={signReady ? 'blue' : 'lighterGray'}
|
||||
/>
|
||||
</Button>
|
||||
</Row>
|
||||
{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}>
|
||||
We recommend that you sign transactions using Bridge to protect
|
||||
your master ticket.
|
||||
</Text>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Send;
|
||||
|
@ -1,59 +1,36 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
StatelessTextInput as Input,
|
||||
Row,
|
||||
Center,
|
||||
Text,
|
||||
Button,
|
||||
Col,
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
import React from 'react';
|
||||
import { Icon, Row, Col, Center, Text } from '@tlon/indigo-react';
|
||||
import { satsToCurrency } from '../../lib/util.js';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
|
||||
export default function Sent(props) {
|
||||
const { payee, denomination, satsAmount, stopSending, currencyRates } = props;
|
||||
const Sent = ({ payee, stopSending, satsAmount }) => {
|
||||
const { denomination, currencyRates } = useSettings();
|
||||
return (
|
||||
<Col
|
||||
height='400px'
|
||||
width='100%'
|
||||
backgroundColor='orange'
|
||||
borderRadius='48px'
|
||||
height="400px"
|
||||
width="100%"
|
||||
backgroundColor="orange"
|
||||
borderRadius="48px"
|
||||
mb={5}
|
||||
p={5}
|
||||
>
|
||||
<Row
|
||||
flexDirection='row-reverse'
|
||||
>
|
||||
<Icon
|
||||
color='white'
|
||||
icon='X'
|
||||
cursor='pointer'
|
||||
onClick={stopSending}
|
||||
/>
|
||||
<Row flexDirection="row-reverse">
|
||||
<Icon color="white" icon="X" cursor="pointer" onClick={stopSending} />
|
||||
</Row>
|
||||
<Center>
|
||||
<Text
|
||||
style={{'display': 'block', 'overflow-wrap': 'anywhere'}}
|
||||
color='white'>{`You sent BTC to ${payee}`}</Text>
|
||||
style={{ display: 'block', 'overflow-wrap': 'anywhere' }}
|
||||
color="white"
|
||||
>{`You sent BTC to ${payee}`}</Text>
|
||||
</Center>
|
||||
<Center
|
||||
flexDirection='column'
|
||||
flex='1 1 auto'
|
||||
>
|
||||
<Text
|
||||
color='white'
|
||||
fontSize='40px'
|
||||
>
|
||||
<Center flexDirection="column" flex="1 1 auto">
|
||||
<Text color="white" fontSize="40px">
|
||||
{satsToCurrency(satsAmount, denomination, currencyRates)}
|
||||
</Text>
|
||||
<Text
|
||||
color='white'
|
||||
>
|
||||
{`${satsAmount} sats`}
|
||||
</Text>
|
||||
<Text color="white">{`${satsAmount} sats`}</Text>
|
||||
</Center>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Sent;
|
||||
|
@ -1,121 +1,114 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
Button,
|
||||
Col,
|
||||
} from '@tlon/indigo-react';
|
||||
import React from 'react';
|
||||
import { Row, Text, Button, Col } from '@tlon/indigo-react';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { api } from '../../api';
|
||||
|
||||
export default class Settings extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.changeProvider = this.changeProvider.bind(this);
|
||||
this.replaceWallet = this.replaceWallet.bind(this);
|
||||
}
|
||||
const Settings = () => {
|
||||
const { wallet, provider } = useSettings();
|
||||
|
||||
changeProvider(){
|
||||
this.props.api.btcWalletCommand({'set-provider': null});
|
||||
}
|
||||
const changeProvider = () => {
|
||||
api.btcWalletCommand({ 'set-provider': null });
|
||||
};
|
||||
|
||||
replaceWallet(){
|
||||
this.props.api.btcWalletCommand({
|
||||
'delete-wallet': this.props.state.wallet,
|
||||
const replaceWallet = () => {
|
||||
api.btcWalletCommand({
|
||||
'delete-wallet': wallet,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let connColor = "red";
|
||||
let connBackground = "veryLightRed";
|
||||
let conn = 'Offline'
|
||||
let host = '';
|
||||
if (this.props.state.provider){
|
||||
if (this.props.state.provider.connected) conn = 'Connected';
|
||||
if (this.props.state.provider.host) host = this.props.state.provider.host;
|
||||
if (this.props.state.provider.connected && this.props.state.provider.host) {
|
||||
connColor = "orange";
|
||||
connBackground = "lightOrange";
|
||||
}
|
||||
let connColor = 'red';
|
||||
let connBackground = 'veryLightRed';
|
||||
let conn = 'Offline';
|
||||
let host = '';
|
||||
if (provider) {
|
||||
if (provider.connected) conn = 'Connected';
|
||||
if (provider.host) host = provider.host;
|
||||
if (provider.connected && provider.host) {
|
||||
connColor = 'orange';
|
||||
connBackground = 'lightOrange';
|
||||
}
|
||||
|
||||
return (
|
||||
<Col
|
||||
display="flex"
|
||||
width="100%"
|
||||
p={5}
|
||||
mb={5}
|
||||
borderRadius="48px"
|
||||
backgroundColor="white"
|
||||
>
|
||||
<Row mb="12px">
|
||||
<Text fontSize={1} fontWeight="bold" color="black">
|
||||
XPub Derivation
|
||||
</Text>
|
||||
</Row>
|
||||
<Row borderRadius="12px"
|
||||
backgroundColor="veryLightGray"
|
||||
py={5}
|
||||
px="36px"
|
||||
mb="12px"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text mono
|
||||
fontSize={1}
|
||||
style={{wordBreak: "break-all"}}
|
||||
color="gray"
|
||||
>
|
||||
{this.props.state.wallet}
|
||||
</Text>
|
||||
</Row>
|
||||
<Row width="100%" mb={5}>
|
||||
<Button children="Replace Wallet"
|
||||
width="100%"
|
||||
fontSize={1}
|
||||
fontWeight="bold"
|
||||
backgroundColor="gray"
|
||||
color="white"
|
||||
borderColor="none"
|
||||
borderRadius="12px"
|
||||
p={4}
|
||||
onClick={this.replaceWallet}
|
||||
/>
|
||||
</Row>
|
||||
<Row mb="12px">
|
||||
<Text fontSize={1} fontWeight="bold" color="black">
|
||||
BTC Node Provider
|
||||
</Text>
|
||||
</Row>
|
||||
<Col mb="12px"
|
||||
py={5}
|
||||
px="36px"
|
||||
borderRadius="12px"
|
||||
backgroundColor={connBackground}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text fontSize={1} color={connColor} mono>
|
||||
~{host}
|
||||
</Text>
|
||||
<Text fontSize={0} color={connColor}>
|
||||
{conn}
|
||||
</Text>
|
||||
</Col>
|
||||
<Row width="100%">
|
||||
<Button children="Change Provider"
|
||||
width="100%"
|
||||
fontSize={1}
|
||||
fontWeight="bold"
|
||||
backgroundColor="orange"
|
||||
color="white"
|
||||
borderColor="none"
|
||||
borderRadius="12px"
|
||||
p={4}
|
||||
onClick={this.changeProvider}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Col
|
||||
display="flex"
|
||||
width="100%"
|
||||
p={5}
|
||||
mb={5}
|
||||
borderRadius="48px"
|
||||
backgroundColor="white"
|
||||
>
|
||||
<Row mb="12px">
|
||||
<Text fontSize={1} fontWeight="bold" color="black">
|
||||
XPub Derivation
|
||||
</Text>
|
||||
</Row>
|
||||
<Row
|
||||
borderRadius="12px"
|
||||
backgroundColor="veryLightGray"
|
||||
py={5}
|
||||
px="36px"
|
||||
mb="12px"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text mono fontSize={1} style={{ wordBreak: 'break-all' }} color="gray">
|
||||
{wallet}
|
||||
</Text>
|
||||
</Row>
|
||||
<Row width="100%" mb={5}>
|
||||
<Button
|
||||
width="100%"
|
||||
fontSize={1}
|
||||
fontWeight="bold"
|
||||
backgroundColor="gray"
|
||||
color="white"
|
||||
borderColor="none"
|
||||
borderRadius="12px"
|
||||
p={4}
|
||||
onClick={() => replaceWallet()}
|
||||
>
|
||||
Replace Wallet
|
||||
</Button>
|
||||
</Row>
|
||||
<Row mb="12px">
|
||||
<Text fontSize={1} fontWeight="bold" color="black">
|
||||
BTC Node Provider
|
||||
</Text>
|
||||
</Row>
|
||||
<Col
|
||||
mb="12px"
|
||||
py={5}
|
||||
px="36px"
|
||||
borderRadius="12px"
|
||||
backgroundColor={connBackground}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text fontSize={1} color={connColor} mono>
|
||||
~{host}
|
||||
</Text>
|
||||
<Text fontSize={0} color={connColor}>
|
||||
{conn}
|
||||
</Text>
|
||||
</Col>
|
||||
<Row width="100%">
|
||||
<Button
|
||||
width="100%"
|
||||
fontSize={1}
|
||||
fontWeight="bold"
|
||||
backgroundColor="orange"
|
||||
color="white"
|
||||
borderColor="none"
|
||||
borderRadius="12px"
|
||||
p={4}
|
||||
onClick={() => changeProvider()}
|
||||
>
|
||||
Change Provider
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
@ -1,52 +1,55 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Box, Button } from '@tlon/indigo-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)'
|
||||
>
|
||||
const Signer = ({
|
||||
signReady,
|
||||
initPayment,
|
||||
choosingSignMethod,
|
||||
signMethod,
|
||||
setSignMethod,
|
||||
}) => {
|
||||
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'
|
||||
border="none"
|
||||
backgroundColor="transparent"
|
||||
fontWeight="bold"
|
||||
cursor="pointer"
|
||||
color={signMethod === 'masterTicket' ? 'blue' : 'lightBlue'}
|
||||
height="48px"
|
||||
onClick={() => setSignMethod('masterTicket')}
|
||||
children='Sign with Master Ticket' />
|
||||
>
|
||||
Sign with Master Ticket
|
||||
</Button>
|
||||
<Button
|
||||
border='none'
|
||||
backgroundColor='transparent'
|
||||
fontWeight='bold'
|
||||
cursor='pointer'
|
||||
color={(signMethod === 'bridge') ? 'blue' : 'lightBlue'}
|
||||
height='48px'
|
||||
border="none"
|
||||
backgroundColor="transparent"
|
||||
fontWeight="bold"
|
||||
cursor="pointer"
|
||||
color={signMethod === 'bridge' ? 'blue' : 'lightBlue'}
|
||||
height="48px"
|
||||
onClick={() => setSignMethod('bridge')}
|
||||
children='Sign with Bridge' />
|
||||
>
|
||||
Sign with Bridge
|
||||
</Button>
|
||||
</Box>
|
||||
:
|
||||
) : (
|
||||
<Button
|
||||
primary
|
||||
children={signMethod === 'bridge' ? 'Sign with Bridge' : 'Sign with Master Ticket'}
|
||||
fontSize={1}
|
||||
fontWeight='bold'
|
||||
borderRadius='24px'
|
||||
height='48px'
|
||||
fontWeight="bold"
|
||||
borderRadius="24px"
|
||||
height="48px"
|
||||
onClick={initPayment}
|
||||
color={signReady ? 'white' : 'lighterGray'}
|
||||
backgroundColor={signReady ? 'blue' : 'veryLightGray'}
|
||||
disabled={!signReady}
|
||||
border='none'
|
||||
style={{cursor: signReady ? 'pointer' : 'default'}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
border="none"
|
||||
style={{ cursor: signReady ? 'pointer' : 'default' }}
|
||||
>
|
||||
{signMethod === 'bridge' ? 'Sign with Bridge' : 'Sign with Master Ticket'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Signer;
|
||||
|
@ -1,52 +1,44 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
import WalletModal from './walletModal.js';
|
||||
import ProviderModal from './providerModal.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
|
||||
import WalletModal from './walletModal.js'
|
||||
import ProviderModal from './providerModal.js'
|
||||
const StartupModal = () => {
|
||||
const { wallet, provider } = useSettings();
|
||||
let modal = null;
|
||||
|
||||
|
||||
export default class StartupModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
if (wallet && provider) {
|
||||
return null;
|
||||
} else if (!provider) {
|
||||
modal = <ProviderModal />;
|
||||
} else if (!wallet) {
|
||||
modal = <WalletModal />;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let modal = null;
|
||||
|
||||
if (this.props.state.wallet && this.props.state.provider) {
|
||||
return null;
|
||||
} else if (!this.props.state.provider){
|
||||
modal =
|
||||
<ProviderModal
|
||||
api={this.props.api}
|
||||
providerPerms={this.props.state.providerPerms}
|
||||
/>
|
||||
} else if (!this.props.state.wallet){
|
||||
modal = <WalletModal api={this.props.api} network={this.props.network}/>
|
||||
}
|
||||
return (
|
||||
return (
|
||||
<Box
|
||||
backgroundColor="scales.black20"
|
||||
left="0px"
|
||||
top="0px"
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="fixed"
|
||||
display="flex"
|
||||
zIndex={10}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box
|
||||
backgroundColor="scales.black20"
|
||||
left="0px"
|
||||
top="0px"
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="fixed"
|
||||
display="flex"
|
||||
zIndex={10}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
flexDirection="column"
|
||||
width="400px"
|
||||
backgroundColor="white"
|
||||
borderRadius={3}
|
||||
>
|
||||
<Box display="flex"
|
||||
flexDirection="column"
|
||||
width='400px'
|
||||
backgroundColor="white"
|
||||
borderRadius={3}
|
||||
>
|
||||
{modal}
|
||||
</Box>
|
||||
{modal}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartupModal;
|
||||
|
@ -1,105 +1,96 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
Button,
|
||||
Col,
|
||||
LoadingSpinner,
|
||||
} from '@tlon/indigo-react';
|
||||
import React from 'react';
|
||||
import { Box, Row, Text, Col } from '@tlon/indigo-react';
|
||||
import _ from 'lodash';
|
||||
import TxAction from './tx-action.js';
|
||||
import TxCounterparty from './tx-counterparty.js';
|
||||
import { satsToCurrency } from '../../lib/util.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
|
||||
import { Sigil } from './sigil.js'
|
||||
import TxAction from './tx-action.js'
|
||||
import TxCounterparty from './tx-counterparty.js'
|
||||
import { satsToCurrency } from '../../lib/util.js'
|
||||
const Transaction = ({ tx }) => {
|
||||
const { denomination, currencyRates } = useSettings();
|
||||
const pending = !tx.recvd;
|
||||
|
||||
export default class Transaction extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
let weSent = _.find(tx.inputs, (input) => {
|
||||
return input.ship === window.ship;
|
||||
});
|
||||
let weRecv = tx.outputs.every((output) => {
|
||||
return output.ship === window.ship;
|
||||
});
|
||||
|
||||
render() {
|
||||
const pending = (!this.props.tx.recvd);
|
||||
let action = weRecv ? 'recv' : weSent ? 'sent' : 'recv';
|
||||
|
||||
let weSent = _.find(this.props.tx.inputs, (input) => {
|
||||
return (input.ship === window.ship);
|
||||
let counterShip = null;
|
||||
let counterAddress = null;
|
||||
let value;
|
||||
let sign;
|
||||
|
||||
if (action === 'sent') {
|
||||
let counter = _.find(tx.outputs, (output) => {
|
||||
return output.ship !== window.ship;
|
||||
});
|
||||
let weRecv = this.props.tx.outputs.every((output) => {
|
||||
return (output.ship === window.ship)
|
||||
});
|
||||
|
||||
let action =
|
||||
(weRecv) ? "recv" :
|
||||
(weSent) ? "sent" : "recv";
|
||||
|
||||
let counterShip = null;
|
||||
let counterAddress = null;
|
||||
let value;
|
||||
let sign;
|
||||
|
||||
if (action === "sent") {
|
||||
let counter = _.find(this.props.tx.outputs, (output) => {
|
||||
return (output.ship !== window.ship);
|
||||
});
|
||||
counterShip = _.get(counter, 'ship', null);
|
||||
counterAddress = _.get(counter, 'val.address', null);
|
||||
value = _.get(counter, 'val.value', null);
|
||||
sign = '-'
|
||||
}
|
||||
else if (action === "recv") {
|
||||
value = _.reduce(this.props.tx.outputs, (sum, output) => {
|
||||
counterShip = _.get(counter, 'ship', null);
|
||||
counterAddress = _.get(counter, 'val.address', null);
|
||||
value = _.get(counter, 'val.value', null);
|
||||
sign = '-';
|
||||
} else if (action === 'recv') {
|
||||
value = _.reduce(
|
||||
tx.outputs,
|
||||
(sum, output) => {
|
||||
if (output.ship === window.ship) {
|
||||
return sum + output.val.value;
|
||||
} else {
|
||||
return sum;
|
||||
}
|
||||
}, 0);
|
||||
|
||||
|
||||
if (weSent && weRecv) {
|
||||
counterAddress = _.get(_.find(this.props.tx.inputs, (input) => {
|
||||
return (input.ship === window.ship);
|
||||
}), 'val.address', null);
|
||||
} else {
|
||||
let counter = _.find(this.props.tx.inputs, (input) => {
|
||||
return (input.ship !== window.ship);
|
||||
});
|
||||
counterShip = _.get(counter, 'ship', null);
|
||||
counterAddress = _.get(counter, 'val.address', null);
|
||||
}
|
||||
sign = '';
|
||||
}
|
||||
|
||||
let currencyValue = sign + satsToCurrency(value, this.props.denom, this.props.rates);
|
||||
|
||||
const failure = Boolean(this.props.tx.failure);
|
||||
if (failure) action = "fail";
|
||||
|
||||
const txid = this.props.tx.txid.dat.slice(2).replaceAll('.','');
|
||||
|
||||
|
||||
return (
|
||||
<Col
|
||||
width='100%'
|
||||
backgroundColor="white"
|
||||
justifyContent="space-between"
|
||||
mb="16px"
|
||||
>
|
||||
<Row justifyContent="space-between" alignItems="center">
|
||||
<TxAction action={action} pending={pending} txid={txid} network={this.props.network}/>
|
||||
<Text fontSize="14px" alignItems="center" color="gray">
|
||||
{sign}{value} sats
|
||||
</Text>
|
||||
</Row>
|
||||
<Box ml="11px" borderLeft="2px solid black" height="4px">
|
||||
</Box>
|
||||
<Row justifyContent="space-between" alignItems="center">
|
||||
<TxCounterparty address={counterAddress} ship={counterShip}/>
|
||||
<Text fontSize="14px">{currencyValue}</Text>
|
||||
</Row>
|
||||
</Col>
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
if (weSent && weRecv) {
|
||||
counterAddress = _.get(
|
||||
_.find(tx.inputs, (input) => {
|
||||
return input.ship === window.ship;
|
||||
}),
|
||||
'val.address',
|
||||
null
|
||||
);
|
||||
} else {
|
||||
let counter = _.find(tx.inputs, (input) => {
|
||||
return input.ship !== window.ship;
|
||||
});
|
||||
counterShip = _.get(counter, 'ship', null);
|
||||
counterAddress = _.get(counter, 'val.address', null);
|
||||
}
|
||||
sign = '';
|
||||
}
|
||||
}
|
||||
|
||||
let currencyValue = sign + satsToCurrency(value, denomination, currencyRates);
|
||||
|
||||
const failure = Boolean(tx.failure);
|
||||
if (failure) action = 'fail';
|
||||
|
||||
const txid = tx.txid.dat.slice(2).replaceAll('.', '');
|
||||
|
||||
return (
|
||||
<Col
|
||||
width="100%"
|
||||
backgroundColor="white"
|
||||
justifyContent="space-between"
|
||||
mb="16px"
|
||||
>
|
||||
<Row justifyContent="space-between" alignItems="center">
|
||||
<TxAction action={action} pending={pending} txid={txid} />
|
||||
<Text fontSize="14px" alignItems="center" color="gray">
|
||||
{sign}
|
||||
{value} sats
|
||||
</Text>
|
||||
</Row>
|
||||
<Box ml="11px" borderLeft="2px solid black" height="4px"></Box>
|
||||
<Row justifyContent="space-between" alignItems="center">
|
||||
<TxCounterparty address={counterAddress} ship={counterShip} />
|
||||
<Text fontSize="14px">{currencyValue}</Text>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transaction;
|
||||
|
@ -1,62 +1,43 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
Button,
|
||||
Col,
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
import { Box, Text, Col } from '@tlon/indigo-react';
|
||||
import Transaction from './transaction.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
|
||||
|
||||
export default class Transactions extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const Transactions = () => {
|
||||
const { history } = useSettings();
|
||||
if (!history || history.length <= 0) {
|
||||
return (
|
||||
<Box
|
||||
alignItems="center"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
height="340px"
|
||||
width="100%"
|
||||
p={5}
|
||||
mb={5}
|
||||
borderRadius="48px"
|
||||
backgroundColor="white"
|
||||
>
|
||||
<Text color="gray" fontSize={2} fontWeight="bold">
|
||||
No Transactions Yet
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Col
|
||||
width="100%"
|
||||
backgroundColor="white"
|
||||
borderRadius="48px"
|
||||
mb={5}
|
||||
p={5}
|
||||
>
|
||||
{history.map((tx, i) => {
|
||||
return <Transaction tx={tx} key={i} />;
|
||||
})}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
if (!this.props.state.history || this.props.state.history.length <= 0) {
|
||||
return (
|
||||
<Box alignItems="center"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
height="340px"
|
||||
width="100%"
|
||||
p={5}
|
||||
mb={5}
|
||||
borderRadius="48px"
|
||||
backgroundColor="white"
|
||||
>
|
||||
<Text color="gray" fontSize={2} fontWeight="bold">No Transactions Yet</Text>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Col
|
||||
width='100%'
|
||||
backgroundColor="white"
|
||||
borderRadius="48px"
|
||||
mb={5}
|
||||
p={5}
|
||||
>
|
||||
{
|
||||
this.props.state.history.map((tx, i) => {
|
||||
return(
|
||||
<Transaction
|
||||
tx={tx}
|
||||
key={i}
|
||||
denom={this.props.state.denomination}
|
||||
rates={this.props.state.currencyRates}
|
||||
network={this.props.network}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default Transactions;
|
||||
|
@ -1,74 +1,72 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Box, Icon, Row, Text, LoadingSpinner } from '@tlon/indigo-react';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
|
||||
export default class TxAction extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
const TxAction = ({ action, pending, txid }) => {
|
||||
const { network } = useSettings();
|
||||
const leftIcon =
|
||||
action === 'sent'
|
||||
? 'ArrowSouth'
|
||||
: action === 'recv'
|
||||
? 'ArrowNorth'
|
||||
: action === 'fail'
|
||||
? 'X'
|
||||
: 'NullIcon';
|
||||
|
||||
render() {
|
||||
const leftIcon =
|
||||
this.props.action === 'sent'
|
||||
? 'ArrowSouth'
|
||||
: this.props.action === 'recv'
|
||||
? 'ArrowNorth'
|
||||
: this.props.action === 'fail'
|
||||
? 'X'
|
||||
: 'NullIcon';
|
||||
const actionColor =
|
||||
action === 'sent'
|
||||
? 'sentBlue'
|
||||
: action === 'recv'
|
||||
? 'recvGreen'
|
||||
: action === 'fail'
|
||||
? 'gray'
|
||||
: 'red';
|
||||
|
||||
const actionColor =
|
||||
this.props.action === 'sent'
|
||||
? 'sentBlue'
|
||||
: this.props.action === 'recv'
|
||||
? 'recvGreen'
|
||||
: this.props.action === 'fail'
|
||||
? 'gray'
|
||||
: 'red';
|
||||
const actionText =
|
||||
action === 'sent' && !pending
|
||||
? 'Sent BTC'
|
||||
: action === 'sent' && pending
|
||||
? 'Sending BTC'
|
||||
: action === 'recv' && !pending
|
||||
? 'Received BTC'
|
||||
: action === 'recv' && pending
|
||||
? 'Receiving BTC'
|
||||
: action === 'fail'
|
||||
? 'Failed'
|
||||
: 'error';
|
||||
|
||||
const actionText =
|
||||
this.props.action === 'sent' && !this.props.pending
|
||||
? 'Sent BTC'
|
||||
: this.props.action === 'sent' && this.props.pending
|
||||
? 'Sending BTC'
|
||||
: this.props.action === 'recv' && !this.props.pending
|
||||
? 'Received BTC'
|
||||
: this.props.action === 'recv' && this.props.pending
|
||||
? 'Receiving BTC'
|
||||
: this.props.action === 'fail'
|
||||
? 'Failed'
|
||||
: 'error';
|
||||
const pendingSpinner = !pending ? null : (
|
||||
<LoadingSpinner background="midOrange" foreground="orange" />
|
||||
);
|
||||
|
||||
const pending = !this.props.pending ? null : (
|
||||
<LoadingSpinner background="midOrange" foreground="orange" />
|
||||
);
|
||||
const url =
|
||||
network === 'testnet'
|
||||
? `http://blockstream.info/testnet/tx/${txid}`
|
||||
: `http://blockstream.info/tx/${txid}`;
|
||||
|
||||
const url =
|
||||
this.props.network === 'testnet'
|
||||
? `http://blockstream.info/testnet/tx/${this.props.txid}`
|
||||
: `http://blockstream.info/tx/${this.props.txid}`;
|
||||
return (
|
||||
<Row alignItems="center">
|
||||
<Box
|
||||
backgroundColor={actionColor}
|
||||
width="24px"
|
||||
height="24px"
|
||||
textAlign="center"
|
||||
alignItems="center"
|
||||
borderRadius="2px"
|
||||
mr={2}
|
||||
p={1}
|
||||
>
|
||||
<Icon icon={leftIcon} color="white" />
|
||||
</Box>
|
||||
<Text color={actionColor} fontSize="14px">
|
||||
{actionText}
|
||||
</Text>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<Icon color={actionColor} icon="ArrowNorthEast" ml={1} mr={2} />
|
||||
</a>
|
||||
{pendingSpinner}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Row alignItems="center">
|
||||
<Box
|
||||
backgroundColor={actionColor}
|
||||
width="24px"
|
||||
height="24px"
|
||||
textAlign="center"
|
||||
alignItems="center"
|
||||
borderRadius="2px"
|
||||
mr={2}
|
||||
p={1}
|
||||
>
|
||||
<Icon icon={leftIcon} color="white" />
|
||||
</Box>
|
||||
<Text color={actionColor} fontSize="14px">
|
||||
{actionText}
|
||||
</Text>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<Icon color={actionColor} icon="ArrowNorthEast" ml={1} mr={2} />
|
||||
</a>
|
||||
{pending}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default TxAction;
|
||||
|
@ -1,53 +1,36 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
Button,
|
||||
Col,
|
||||
} from '@tlon/indigo-react';
|
||||
import React from 'react';
|
||||
import { Box, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import { Sigil } from './sigil.js';
|
||||
|
||||
import { Sigil } from './sigil.js'
|
||||
import TxAction from './tx-action.js'
|
||||
const TxCounterparty = ({ ship, address }) => {
|
||||
const icon = ship ? (
|
||||
<Sigil ship={ship} 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>
|
||||
);
|
||||
const addressText = !address
|
||||
? ''
|
||||
: address.slice(0, 6) + '...' + address.slice(-6);
|
||||
const text = ship ? `~${ship}` : addressText;
|
||||
|
||||
export default class TxCounterparty extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
return (
|
||||
<Row alignItems="center">
|
||||
{icon}
|
||||
<Text ml={2} mono fontSize="14px" color="gray">
|
||||
{text}
|
||||
</Text>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
const icon = (this.props.ship)
|
||||
? <Sigil
|
||||
ship={this.props.ship}
|
||||
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>
|
||||
const addressText = (!this.props.address) ? '' :
|
||||
this.props.address.slice(0, 6) + '...' +
|
||||
this.props.address.slice(-6);
|
||||
const text = (this.props.ship) ?
|
||||
`~${this.props.ship}` : addressText;
|
||||
|
||||
return (
|
||||
<Row alignItems="center">
|
||||
{icon}
|
||||
<Text ml={2} mono fontSize="14px" color="gray">{text}</Text>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default TxCounterparty;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
@ -6,247 +6,251 @@ import {
|
||||
StatelessTextInput,
|
||||
Icon,
|
||||
Row,
|
||||
Input,
|
||||
LoadingSpinner,
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
import { patp2dec, isValidPatq } from 'urbit-ob';
|
||||
import * as kg from 'urbit-key-generation';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { api } from '../../api';
|
||||
|
||||
const kg = require('urbit-key-generation');
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const bs58check = require('bs58check')
|
||||
import { Buffer } from 'buffer';
|
||||
const WalletModal = () => {
|
||||
const { network } = useSettings();
|
||||
const [mode, setMode] = useState('xpub');
|
||||
const [masterTicket, setMasterTicket] = useState('');
|
||||
const [confirmedMasterTicket, setConfirmedMasterTicket] = useState('');
|
||||
const [xpub, setXpub] = useState('');
|
||||
const [readyToSubmit, setReadyToSubmit] = useState(false);
|
||||
const [processingSubmission, setProcessingSubmission] = useState(false);
|
||||
const [confirmingMasterTicket, setConfirmingMasterTicket] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
export default class WalletModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
mode: 'xpub',
|
||||
masterTicket: '',
|
||||
confirmedMasterTicket: '',
|
||||
xpub: '',
|
||||
readyToSubmit: false,
|
||||
processingSubmission: false,
|
||||
confirmingMasterTicket: false,
|
||||
error: false,
|
||||
}
|
||||
this.checkTicket = this.checkTicket.bind(this);
|
||||
this.checkXPub = this.checkXPub.bind(this);
|
||||
this.submitMasterTicket = this.submitMasterTicket.bind(this);
|
||||
this.submitXPub = this.submitXPub.bind(this);
|
||||
|
||||
}
|
||||
|
||||
checkTicket(e){
|
||||
const checkTicket = ({
|
||||
event: {
|
||||
target: { value },
|
||||
},
|
||||
}) => {
|
||||
// TODO: port over bridge ticket validation logic
|
||||
if (this.state.confirmingMasterTicket) {
|
||||
let confirmedMasterTicket = e.target.value;
|
||||
let readyToSubmit = isValidPatq(confirmedMasterTicket);
|
||||
this.setState({confirmedMasterTicket, readyToSubmit});
|
||||
if (confirmingMasterTicket) {
|
||||
setConfirmedMasterTicket(value);
|
||||
setReadyToSubmit(isValidPatq(value));
|
||||
} else {
|
||||
let masterTicket = e.target.value;
|
||||
let readyToSubmit = isValidPatq(masterTicket);
|
||||
this.setState({masterTicket, readyToSubmit});
|
||||
setMasterTicket(value);
|
||||
setReadyToSubmit(isValidPatq(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkXPub(e){
|
||||
let xpub = e.target.value;
|
||||
let readyToSubmit = (xpub.length > 0);
|
||||
this.setState({xpub, readyToSubmit});
|
||||
}
|
||||
const checkXPub = ({
|
||||
event: {
|
||||
target: { value: xpubGiven },
|
||||
},
|
||||
}) => {
|
||||
setXpub(xpubGiven);
|
||||
setReadyToSubmit(xpubGiven.length > 0);
|
||||
};
|
||||
|
||||
submitMasterTicket(ticket){
|
||||
this.setState({processingSubmission: true});
|
||||
kg.generateWallet({ ticket, ship: parseInt(patp2dec('~' + window.ship)) })
|
||||
.then(urbitWallet => {
|
||||
const { xpub } = this.props.network === 'testnet'
|
||||
? urbitWallet.bitcoinTestnet.keys :
|
||||
urbitWallet.bitcoinMainnet.keys
|
||||
|
||||
this.submitXPub(xpub);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
submitXPub(xpub){
|
||||
const submitXPub = (givenXpub) => {
|
||||
const command = {
|
||||
"add-wallet": {
|
||||
"xpub": xpub,
|
||||
"fprint": [4, 0],
|
||||
"scan-to": null,
|
||||
"max-gap": 8,
|
||||
"confs": 1
|
||||
}
|
||||
}
|
||||
'add-wallet': {
|
||||
xpub: givenXpub,
|
||||
fprint: [4, 0],
|
||||
'scan-to': null,
|
||||
'max-gap': 8,
|
||||
confs: 1,
|
||||
},
|
||||
};
|
||||
api.btcWalletCommand(command);
|
||||
this.setState({processingSubmission: true});
|
||||
}
|
||||
setProcessingSubmission(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const buttonDisabled = (!this.state.readyToSubmit || this.state.processingSubmission );
|
||||
const inputDisabled = this.state.processingSubmission;
|
||||
const processingSpinner = (!this.state.processingSubmission) ? null :
|
||||
<LoadingSpinner/>
|
||||
const submitMasterTicket = (ticket) => {
|
||||
setProcessingSubmission(true);
|
||||
kg.generateWallet({
|
||||
ticket,
|
||||
ship: parseInt(patp2dec('~' + window.ship)),
|
||||
}).then((urbitWallet) => {
|
||||
const { xpub: xpubFromWallet } =
|
||||
network === 'testnet'
|
||||
? urbitWallet.bitcoinTestnet.keys
|
||||
: urbitWallet.bitcoinMainnet.keys;
|
||||
|
||||
if (this.state.mode === 'masterTicket'){
|
||||
return (
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
padding={3}
|
||||
>
|
||||
<Row>
|
||||
<Icon icon="Bitcoin" mr={2}/>
|
||||
<Text fontSize="14px" fontWeight="bold">
|
||||
Step 2 of 2: Import your extended public key
|
||||
</Text>
|
||||
</Row>
|
||||
<Row
|
||||
mt={3}
|
||||
alignItems='center'
|
||||
>
|
||||
<Icon icon='Info' color='yellow' height={4} width={4}/>
|
||||
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
|
||||
We recommend that you import your wallet using Bridge to protect your master ticket.
|
||||
</Text>
|
||||
</Row>
|
||||
<Box
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
mt={3}
|
||||
mb={2}>
|
||||
{this.state.confirmingMasterTicket &&
|
||||
<Icon
|
||||
icon='ArrowWest'
|
||||
cursor='pointer'
|
||||
onClick={() => this.setState({
|
||||
confirmingMasterTicket: false,
|
||||
masterTicket: '',
|
||||
confirmedMasterTicket: '',
|
||||
error: false
|
||||
})}/>}
|
||||
<Text fontSize="14px" fontWeight="500">
|
||||
{this.state.confirmingMasterTicket ? 'Confirm Master Ticket' : 'Master Ticket'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Row alignItems="center">
|
||||
<StatelessTextInput
|
||||
mr={2}
|
||||
width="256px"
|
||||
value={this.state.confirmingMasterTicket ? this.state.confirmedMasterTicket : this.state.masterTicket}
|
||||
disabled={inputDisabled}
|
||||
fontSize="14px"
|
||||
type="password"
|
||||
name="masterTicket"
|
||||
obscure={value => value.replace(/[^~-]+/g, '••••••')}
|
||||
placeholder="••••••-••••••-••••••-••••••"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
onChange={this.checkTicket}
|
||||
/>
|
||||
{(!inputDisabled) ? null : <LoadingSpinner/>}
|
||||
</Row>
|
||||
{this.state.error &&
|
||||
<Row mt={2}>
|
||||
<Text
|
||||
fontSize='14px'
|
||||
color='red'>
|
||||
Master tickets do not match
|
||||
</Text>
|
||||
</Row>
|
||||
}
|
||||
<Row mt={3}>
|
||||
<Button
|
||||
primary
|
||||
color="black"
|
||||
backgroundColor="veryLightGray"
|
||||
borderColor="veryLightGray"
|
||||
children="Cancel"
|
||||
fontSize="14px"
|
||||
mr={2}
|
||||
style={{cursor: "pointer"}}
|
||||
onClick={() => {this.setState({mode: 'xpub', masterTicket: '', xpub: '', readyToSubmit: false})}}
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
disabled={buttonDisabled}
|
||||
children="Next Step"
|
||||
fontSize="14px"
|
||||
style={{cursor: buttonDisabled ? "default" : "pointer"}}
|
||||
submitXPub(xpubFromWallet);
|
||||
});
|
||||
};
|
||||
|
||||
const buttonDisabled = !readyToSubmit || processingSubmission;
|
||||
const inputDisabled = processingSubmission;
|
||||
// const processingSpinner = !processingSubmission ? null : <LoadingSpinner />;
|
||||
|
||||
if (mode === 'masterTicket') {
|
||||
return (
|
||||
<Box width="100%" height="100%" padding={3}>
|
||||
<Row>
|
||||
<Icon icon="Bitcoin" mr={2} />
|
||||
<Text fontSize="14px" fontWeight="bold">
|
||||
Step 2 of 2: Import your extended public key
|
||||
</Text>
|
||||
</Row>
|
||||
<Row mt={3} alignItems="center">
|
||||
<Icon icon="Info" color="yellow" height={4} width={4} />
|
||||
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
|
||||
We recommend that you import your wallet using Bridge to protect
|
||||
your master ticket.
|
||||
</Text>
|
||||
</Row>
|
||||
<Box display="flex" alignItems="center" mt={3} mb={2}>
|
||||
{confirmingMasterTicket && (
|
||||
<Icon
|
||||
icon="ArrowWest"
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
if (!this.state.confirmingMasterTicket) {
|
||||
this.setState({confirmingMasterTicket: true});
|
||||
} else {
|
||||
if (this.state.masterTicket === this.state.confirmedMasterTicket) {
|
||||
this.setState({error: false});
|
||||
this.submitMasterTicket(this.state.masterTicket);
|
||||
} else {
|
||||
this.setState({error: true});
|
||||
}
|
||||
}
|
||||
setConfirmingMasterTicket(false);
|
||||
setMasterTicket('');
|
||||
setConfirmedMasterTicket('');
|
||||
setError(false);
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
<Text fontSize="14px" fontWeight="500">
|
||||
{confirmingMasterTicket ? 'Confirm Master Ticket' : 'Master Ticket'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
} else if (this.state.mode === 'xpub') {
|
||||
return (
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
padding={3}
|
||||
>
|
||||
<Row>
|
||||
<Icon icon="Bitcoin" mr={2}/>
|
||||
<Text fontSize="14px" fontWeight="bold">
|
||||
Step 2 of 2: Import your extended public key
|
||||
</Text>
|
||||
</Row>
|
||||
<Box mt={3}>
|
||||
<Text fontSize="14px" fontWeight="regular" color="gray">
|
||||
Visit <a href='https://bridge.urbit.org/?kind=xpub' target='_blank' style={{color: 'black'}} >bridge.urbit.org</a> to obtain your key
|
||||
</Text>
|
||||
</Box>
|
||||
<Box mt={3} mb={2}>
|
||||
<Text fontSize="14px" fontWeight="500">
|
||||
Extended Public Key (XPub)
|
||||
</Text>
|
||||
</Box>
|
||||
<Row alignItems="center">
|
||||
<StatelessTextInput
|
||||
value={this.state.xpub}
|
||||
mr={2}
|
||||
width="256px"
|
||||
value={
|
||||
confirmingMasterTicket ? confirmedMasterTicket : masterTicket
|
||||
}
|
||||
disabled={inputDisabled}
|
||||
fontSize="14px"
|
||||
type="password"
|
||||
name="xpub"
|
||||
name="masterTicket"
|
||||
obscure={(value) => value.replace(/[^~-]+/g, '••••••')}
|
||||
placeholder="••••••-••••••-••••••-••••••"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
onChange={this.checkXPub}
|
||||
onChange={(e) => checkTicket(e)}
|
||||
/>
|
||||
<Box mt={3} mb={3}>
|
||||
<Text fontSize="14px" fontWeight="regular"
|
||||
color={(inputDisabled) ? "lighterGray" : "gray"}
|
||||
style={{cursor: (inputDisabled) ? "default" : "pointer"}}
|
||||
onClick={() => {
|
||||
if (inputDisabled) return;
|
||||
this.setState({mode: 'masterTicket', xpub: '', masterTicket: '', readyToSubmit: false})
|
||||
}}
|
||||
>
|
||||
Import using master ticket ->
|
||||
{!inputDisabled ? null : <LoadingSpinner />}
|
||||
</Row>
|
||||
{error && (
|
||||
<Row mt={2}>
|
||||
<Text fontSize="14px" color="red">
|
||||
Master tickets do not match
|
||||
</Text>
|
||||
</Box>
|
||||
</Row>
|
||||
)}
|
||||
<Row mt={3}>
|
||||
<Button
|
||||
primary
|
||||
mt={3}
|
||||
disabled={buttonDisabled}
|
||||
children="Next Step"
|
||||
color="black"
|
||||
backgroundColor="veryLightGray"
|
||||
borderColor="veryLightGray"
|
||||
fontSize="14px"
|
||||
style={{cursor: this.state.ready ? "pointer" : "default"}}
|
||||
onClick={() => { this.submitXPub(this.state.xpub) }}
|
||||
/>
|
||||
mr={2}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setMode('xpub');
|
||||
setMasterTicket('');
|
||||
setXpub('');
|
||||
setReadyToSubmit(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
primary
|
||||
disabled={buttonDisabled}
|
||||
fontSize="14px"
|
||||
style={{ cursor: buttonDisabled ? 'default' : 'pointer' }}
|
||||
onClick={() => {
|
||||
if (!confirmingMasterTicket) {
|
||||
setConfirmingMasterTicket(true);
|
||||
} else {
|
||||
if (masterTicket === confirmedMasterTicket) {
|
||||
setError(false);
|
||||
submitMasterTicket(masterTicket);
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
Next Step
|
||||
</Button>
|
||||
</Row>
|
||||
</Box>
|
||||
);
|
||||
} else if (mode === 'xpub') {
|
||||
return (
|
||||
<Box width="100%" height="100%" padding={3}>
|
||||
<Row>
|
||||
<Icon icon="Bitcoin" mr={2} />
|
||||
<Text fontSize="14px" fontWeight="bold">
|
||||
Step 2 of 2: Import your extended public key
|
||||
</Text>
|
||||
</Row>
|
||||
<Box mt={3}>
|
||||
<Text fontSize="14px" fontWeight="regular" color="gray">
|
||||
Visit{' '}
|
||||
<a
|
||||
rel="noreferrer"
|
||||
href="https://bridge.urbit.org/?kind=xpub"
|
||||
target="_blank"
|
||||
style={{ color: 'black' }}
|
||||
>
|
||||
bridge.urbit.org
|
||||
</a>{' '}
|
||||
to obtain your key
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
<Box mt={3} mb={2}>
|
||||
<Text fontSize="14px" fontWeight="500">
|
||||
Extended Public Key (XPub)
|
||||
</Text>
|
||||
</Box>
|
||||
<StatelessTextInput
|
||||
value={xpub}
|
||||
disabled={inputDisabled}
|
||||
fontSize="14px"
|
||||
type="password"
|
||||
name="xpub"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
onChange={(e) => checkXPub(e)}
|
||||
/>
|
||||
<Box mt={3} mb={3}>
|
||||
<Text
|
||||
fontSize="14px"
|
||||
fontWeight="regular"
|
||||
color={inputDisabled ? 'lighterGray' : 'gray'}
|
||||
style={{ cursor: inputDisabled ? 'default' : 'pointer' }}
|
||||
onClick={() => {
|
||||
if (inputDisabled) return;
|
||||
setMode('masterTicket');
|
||||
setXpub('');
|
||||
setMasterTicket('');
|
||||
setReadyToSubmit(false);
|
||||
}}
|
||||
>
|
||||
Import using master ticket ->
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
primary
|
||||
mt={3}
|
||||
disabled={buttonDisabled}
|
||||
fontSize="14px"
|
||||
style={{ cursor: readyToSubmit ? 'pointer' : 'default' }}
|
||||
onClick={() => {
|
||||
submitXPub(xpub);
|
||||
}}
|
||||
>
|
||||
Next Step
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default WalletModal;
|
||||
|
@ -1,79 +1,72 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
Row,
|
||||
Text,
|
||||
Button,
|
||||
Col,
|
||||
Anchor,
|
||||
} from '@tlon/indigo-react';
|
||||
import React from 'react';
|
||||
import { Box, Text, Button, Col, Anchor } from '@tlon/indigo-react';
|
||||
import { api } from '../../api';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
|
||||
import { store } from '../../store'
|
||||
|
||||
|
||||
|
||||
export default class Warning extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.understand = this.understand.bind(this);
|
||||
}
|
||||
|
||||
understand(){
|
||||
store.handleEvent({ data: { bucket: { warning: false}}});
|
||||
const Warning = () => {
|
||||
const { setWarning } = useSettings();
|
||||
const understand = () => {
|
||||
setWarning(false);
|
||||
let removeWarning = {
|
||||
"put-entry": {
|
||||
'put-entry': {
|
||||
value: false,
|
||||
"entry-key": "warning",
|
||||
"bucket-key": "btc-wallet",
|
||||
}
|
||||
}
|
||||
this.props.api.settingsEvent(removeWarning);
|
||||
}
|
||||
'entry-key': 'warning',
|
||||
'bucket-key': 'btc-wallet',
|
||||
},
|
||||
};
|
||||
api.settingsEvent(removeWarning);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Box
|
||||
backgroundColor="red"
|
||||
color="white"
|
||||
borderRadius="32px"
|
||||
justifyContent="space-between"
|
||||
width='100%'
|
||||
p={5}
|
||||
mb={5}
|
||||
return (
|
||||
<Box
|
||||
backgroundColor="red"
|
||||
color="white"
|
||||
borderRadius="32px"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
p={5}
|
||||
mb={5}
|
||||
>
|
||||
<Col>
|
||||
<Text color="white" fontWeight="bold" fontSize={1}>
|
||||
Warning!
|
||||
</Text>
|
||||
<br />
|
||||
<Text color="white" fontWeight="bold" fontSize={1}>
|
||||
Be safe while using this wallet, and be sure to store responsible
|
||||
amounts of BTC.
|
||||
</Text>
|
||||
<Text color="white" fontWeight="bold" fontSize={1}>
|
||||
Always ensure that the checksum of the wallet matches that of the
|
||||
wallet's repo.
|
||||
</Text>
|
||||
<br />
|
||||
<Anchor href="https://urbit.org/bitcoin-wallet" target="_blank">
|
||||
<Text
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
fontSize={1}
|
||||
style={{ textDecoration: 'underline' }}
|
||||
>
|
||||
Learn more on urbit.org
|
||||
</Text>
|
||||
</Anchor>
|
||||
</Col>
|
||||
<Button
|
||||
backgroundColor="white"
|
||||
fontSize={1}
|
||||
mt={5}
|
||||
color="red"
|
||||
fontWeight="bold"
|
||||
borderRadius="24px"
|
||||
p="24px"
|
||||
borderColor="none"
|
||||
onClick={() => understand()}
|
||||
>
|
||||
<Col>
|
||||
<Text color="white" fontWeight='bold' fontSize={1}>
|
||||
Warning!
|
||||
</Text>
|
||||
<br/>
|
||||
<Text color="white" fontWeight='bold' fontSize={1}>
|
||||
Be safe while using this wallet, and be sure to store responsible amounts
|
||||
of BTC.
|
||||
</Text>
|
||||
<Text color="white" fontWeight='bold' fontSize={1}>
|
||||
Always ensure that the checksum of the wallet matches that of the wallet's repo.
|
||||
</Text>
|
||||
<br/>
|
||||
<Anchor href="https://urbit.org/bitcoin-wallet" target="_blank">
|
||||
<Text color="white" fontWeight="bold" fontSize={1} style={{textDecoration:'underline'}}>
|
||||
Learn more on urbit.org
|
||||
</Text>
|
||||
</Anchor>
|
||||
</Col>
|
||||
<Button children="I Understand"
|
||||
backgroundColor="white"
|
||||
fontSize={1}
|
||||
mt={5}
|
||||
color="red"
|
||||
fontWeight="bold"
|
||||
borderRadius="24px"
|
||||
p="24px"
|
||||
borderColor="none"
|
||||
onClick={this.understand}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
I understand
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Warning;
|
||||
|
@ -1,67 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { api } from '../api.js';
|
||||
import { store } from '../store.js';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import light from './themes/light';
|
||||
// import dark from './themes/dark';
|
||||
import { Box, Reset } from '@tlon/indigo-react';
|
||||
import StartupModal from './lib/startupModal.js';
|
||||
import Body from './lib/body.js';
|
||||
import { subscription } from '../subscription.js';
|
||||
import { useSettings } from '../hooks/useSettings.js';
|
||||
|
||||
const network = 'bitcoin';
|
||||
const Root = () => {
|
||||
const { loaded, wallet, provider } = useSettings();
|
||||
const blur = !loaded ? false : !(wallet && provider);
|
||||
|
||||
export class Root extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.ship = window.ship;
|
||||
this.state = store.state;
|
||||
store.setStateHandler(this.setState.bind(this));
|
||||
}
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ThemeProvider theme={light}>
|
||||
<Reset />
|
||||
{loaded ? <StartupModal /> : null}
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
position="absolute"
|
||||
alignItems="center"
|
||||
backgroundColor="lightOrange"
|
||||
width="100%"
|
||||
minHeight={loaded ? '100%' : 'none'}
|
||||
height={loaded ? 'none' : '100%'}
|
||||
style={{ filter: blur ? 'blur(8px)' : 'none' }}
|
||||
px={[0, 4]}
|
||||
pb={[0, 4]}
|
||||
>
|
||||
<Body />
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.channel.setOnChannelError(() => {
|
||||
subscription.start();
|
||||
});
|
||||
subscription.start();
|
||||
}
|
||||
|
||||
render() {
|
||||
const loaded = this.state.loaded;
|
||||
const warning = this.state.showWarning;
|
||||
const blur = !loaded ? false : !(this.state.wallet && this.state.provider);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ThemeProvider theme={light}>
|
||||
<Reset />
|
||||
{loaded ? (
|
||||
<StartupModal api={api} state={this.state} network={network} />
|
||||
) : null}
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
position="absolute"
|
||||
alignItems="center"
|
||||
backgroundColor="lightOrange"
|
||||
width="100%"
|
||||
minHeight={loaded ? '100%' : 'none'}
|
||||
height={loaded ? 'none' : '100%'}
|
||||
style={{ filter: blur ? 'blur(8px)' : 'none' }}
|
||||
px={[0, 4]}
|
||||
pb={[0, 4]}
|
||||
>
|
||||
<Body
|
||||
loaded={loaded}
|
||||
state={this.state}
|
||||
api={api}
|
||||
network={network}
|
||||
warning={warning}
|
||||
/>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Root;
|
||||
|
@ -1,21 +1,17 @@
|
||||
import _ from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
||||
export function uuid() {
|
||||
let str = "0v"
|
||||
str += Math.ceil(Math.random()*8)+"."
|
||||
let str = '0v';
|
||||
str += Math.ceil(Math.random() * 8) + '.';
|
||||
for (var i = 0; i < 5; i++) {
|
||||
let _str = Math.ceil(Math.random()*10000000).toString(32);
|
||||
_str = ("00000"+_str).substr(-5,5);
|
||||
str += _str+".";
|
||||
let _str = Math.ceil(Math.random() * 10000000).toString(32);
|
||||
_str = ('00000' + _str).substr(-5, 5);
|
||||
str += _str + '.';
|
||||
}
|
||||
|
||||
return str.slice(0,-1);
|
||||
return str.slice(0, -1);
|
||||
}
|
||||
|
||||
export function isPatTa(str) {
|
||||
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str)
|
||||
const r = /^[a-z,0-9,\-,.,_,~]+$/.exec(str);
|
||||
return !!r;
|
||||
}
|
||||
|
||||
@ -26,13 +22,15 @@ export function isPatTa(str) {
|
||||
(javascript Date object)
|
||||
*/
|
||||
export function daToDate(st) {
|
||||
var dub = function(n) {
|
||||
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
|
||||
var dub = function (n) {
|
||||
return parseInt(n) < 10 ? '0' + parseInt(n) : n.toString();
|
||||
};
|
||||
var da = st.split('..');
|
||||
var bigEnd = da[0].split('.');
|
||||
var lilEnd = da[1].split('.');
|
||||
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
||||
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(
|
||||
lilEnd[0]
|
||||
)}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
||||
return new Date(ds);
|
||||
}
|
||||
|
||||
@ -44,18 +42,18 @@ export function daToDate(st) {
|
||||
*/
|
||||
|
||||
export function dateToDa(d, mil) {
|
||||
var fil = function(n) {
|
||||
return n >= 10 ? n : "0" + n;
|
||||
};
|
||||
return (
|
||||
`~${d.getUTCFullYear()}.` +
|
||||
`${(d.getUTCMonth() + 1)}.` +
|
||||
`${fil(d.getUTCDate())}..` +
|
||||
`${fil(d.getUTCHours())}.` +
|
||||
`${fil(d.getUTCMinutes())}.` +
|
||||
`${fil(d.getUTCSeconds())}` +
|
||||
`${mil ? "..0000" : ""}`
|
||||
);
|
||||
var fil = function (n) {
|
||||
return n >= 10 ? n : '0' + n;
|
||||
};
|
||||
return (
|
||||
`~${d.getUTCFullYear()}.` +
|
||||
`${d.getUTCMonth() + 1}.` +
|
||||
`${fil(d.getUTCDate())}..` +
|
||||
`${fil(d.getUTCHours())}.` +
|
||||
`${fil(d.getUTCMinutes())}.` +
|
||||
`${fil(d.getUTCSeconds())}` +
|
||||
`${mil ? '..0000' : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
export function deSig(ship) {
|
||||
@ -64,49 +62,59 @@ export function deSig(ship) {
|
||||
|
||||
// trim patps to match dojo, chat-cli
|
||||
export function cite(ship) {
|
||||
let patp = ship, shortened = "";
|
||||
if (patp.startsWith("~")) {
|
||||
let patp = ship,
|
||||
shortened = '';
|
||||
if (patp.startsWith('~')) {
|
||||
patp = patp.substr(1);
|
||||
}
|
||||
// comet
|
||||
if (patp.length === 56) {
|
||||
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
|
||||
shortened = '~' + patp.slice(0, 6) + '_' + patp.slice(50, 56);
|
||||
return shortened;
|
||||
}
|
||||
// moon
|
||||
if (patp.length === 27) {
|
||||
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
|
||||
shortened = '~' + patp.slice(14, 20) + '^' + patp.slice(21, 27);
|
||||
return shortened;
|
||||
}
|
||||
return `~${patp}`;
|
||||
}
|
||||
|
||||
export function satsToCurrency(sats, denomination, rates){
|
||||
export function satsToCurrency(sats, denomination, rates) {
|
||||
if (!rates) {
|
||||
throw "nonexistent currency table"
|
||||
throw 'nonexistent currency table';
|
||||
}
|
||||
if (!rates[denomination]){
|
||||
denomination = "BTC";
|
||||
if (!rates[denomination]) {
|
||||
denomination = 'BTC';
|
||||
}
|
||||
let rate = rates[denomination];
|
||||
let val = parseFloat(((sats * rate.last) * 0.00000001).toFixed(8));
|
||||
let val = rate ? parseFloat((sats * rate.last * 0.00000001).toFixed(8)) : 0;
|
||||
let text;
|
||||
if (denomination === 'BTC'){
|
||||
text = val + ' ' + rate.symbol
|
||||
} else {
|
||||
text = rate.symbol + val.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')
|
||||
if (denomination === 'BTC' && rate) {
|
||||
text = val + ' ' + rate.symbol;
|
||||
} else if (rate) {
|
||||
text =
|
||||
rate.symbol + val.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
}
|
||||
return text
|
||||
return text;
|
||||
}
|
||||
|
||||
export function currencyToSats(val, denomination, rates){
|
||||
export function currencyToSats(val, denomination, rates) {
|
||||
if (!rates) {
|
||||
throw "nonexistent currency table"
|
||||
throw 'nonexistent currency table';
|
||||
}
|
||||
if (!rates[denomination]){
|
||||
throw 'currency not in table'
|
||||
if (!rates[denomination]) {
|
||||
throw 'currency not in table';
|
||||
}
|
||||
let rate = rates[denomination];
|
||||
let sats = (parseFloat(val) / rate.last) * 100000000;
|
||||
return sats;
|
||||
}
|
||||
|
||||
export function reduceHistory(history) {
|
||||
return Object.values(history).sort((hest1, hest2) => {
|
||||
if (hest1.recvd === null) return -1;
|
||||
if (hest2.recvd === null) return +1;
|
||||
return hest2.recvd - hest1.recvd;
|
||||
});
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class CurrencyReducer {
|
||||
reduce(json, state) {
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
if (json.currencyRates) {
|
||||
for (var c in json.currencyRates) {
|
||||
state.currencyRates[c] = json.currencyRates[c];
|
||||
}
|
||||
}
|
||||
|
||||
if (json.denomination) {
|
||||
if (state.currencyRates[json.denomination]) {
|
||||
state.denomination = json.denomination
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class InitialReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'initial', false);
|
||||
if (data) {
|
||||
state.provider = data.provider;
|
||||
state.wallet = data.wallet;
|
||||
state.confirmedBalance = _.get(data.balance, 'confirmed', null);
|
||||
state.unconfirmedBalance = _.get(data.balance, 'unconfirmed', null);
|
||||
state.btcState = data['btc-state'];
|
||||
state.history = this.reduceHistory(data.history);
|
||||
state.address = data.address;
|
||||
|
||||
state.loadedBtc = true;
|
||||
if (state.loadedSettings) {
|
||||
state.loaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reduceHistory(history) {
|
||||
return Object.values(history).sort((hest1, hest2) => {
|
||||
if (hest1.recvd === null) return -1;
|
||||
if (hest2.recvd === null) return +1;
|
||||
return (hest2.recvd - hest1.recvd)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class SettingsReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'bucket', false);
|
||||
if (data) {
|
||||
let warning = _.get(json, 'bucket.warning', -1);
|
||||
if (warning !== -1) {
|
||||
state.showWarning = warning
|
||||
}
|
||||
let currency = _.get(json, 'bucket.currency', -1);
|
||||
if (currency !== -1) {
|
||||
state.denomination = currency;
|
||||
}
|
||||
|
||||
state.loadedSettings = true;
|
||||
if (state.loadedBtc) {
|
||||
state.loaded = true;
|
||||
}
|
||||
}
|
||||
let entry = _.get(json, 'settings-event.put-entry.entry-key', false);
|
||||
if (entry === 'currency') {
|
||||
let value = _.get(json, 'settings-event.put-entry.value', false);
|
||||
state.denomination = value;
|
||||
} else if (entry === 'warning') {
|
||||
let value = _.get(json, 'settings-event.put-entry.value', false);
|
||||
state.showWarning = value;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export class UpdateReducer {
|
||||
reduce(json, state) {
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
if (json.providerStatus) {
|
||||
this.reduceProviderStatus(json.providerStatus, state);
|
||||
}
|
||||
if (json.checkPayee) {
|
||||
this.reduceCheckPayee(json.checkPayee, state);
|
||||
}
|
||||
if ("change-provider" in json) {
|
||||
this.reduceChangeProvider(json["change-provider"], state);
|
||||
}
|
||||
if (json["change-wallet"]) {
|
||||
this.changeWallet(json["change-wallet"], state);
|
||||
}
|
||||
if (json.hasOwnProperty('psbt')) {
|
||||
this.reducePsbt(json.psbt, state);
|
||||
}
|
||||
if (json["btc-state"]) {
|
||||
this.reduceBtcState(json["btc-state"], state);
|
||||
}
|
||||
if (json["new-tx"]) {
|
||||
this.reduceNewTx(json["new-tx"], state);
|
||||
}
|
||||
if (json["cancel-tx"]) {
|
||||
this.reduceCancelTx(json["cancel-tx"], state);
|
||||
}
|
||||
if (json.address) {
|
||||
this.reduceAddress(json.address, state);
|
||||
}
|
||||
if (json.balance) {
|
||||
this.reduceBalance(json.balance, state);
|
||||
}
|
||||
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) {
|
||||
state.providerPerms[json.provider] = json.permitted;
|
||||
}
|
||||
|
||||
reduceCheckPayee(json, state) {
|
||||
state.shipWallets[json.payee] = json.hasWallet;
|
||||
}
|
||||
|
||||
reduceChangeProvider(json, state) {
|
||||
state.provider = json;
|
||||
}
|
||||
|
||||
reduceChangeWallet(json, state) {
|
||||
state.wallet = json;
|
||||
}
|
||||
|
||||
reducePsbt(json, state) {
|
||||
state.psbt = json.pb;
|
||||
state.fee = json.fee;
|
||||
}
|
||||
|
||||
reduceBtcState(json, state) {
|
||||
state.btcState = json;
|
||||
}
|
||||
|
||||
reduceNewTx(json, state) {
|
||||
let old = _.findIndex(state.history, (h) => {
|
||||
return ( h.txid.dat === json.txid.dat &&
|
||||
h.txid.wid === json.txid.wid );
|
||||
});
|
||||
if (old !== -1) {
|
||||
delete state.history.splice(old, 1);
|
||||
}
|
||||
if (json.recvd === null) {
|
||||
state.history.unshift(json);
|
||||
} else {
|
||||
// we expect history to have null recvd values first, and the rest in
|
||||
// descending order
|
||||
let insertionIndex = _.findIndex(state.history, (h) => {
|
||||
return ((h.recvd < json.recvd) && (h.recvd !== null));
|
||||
});
|
||||
state.history.splice(insertionIndex, 0, json);
|
||||
}
|
||||
}
|
||||
|
||||
reduceCancelTx(json, state) {
|
||||
let entryIndex = _.findIndex(state.history, (h) => {
|
||||
return ((json.wid === h.txid.wid) && (json.dat === h.txid.dat));
|
||||
});
|
||||
if (entryIndex > -1) {
|
||||
state.history[entryIndex].failure = true;
|
||||
}
|
||||
}
|
||||
|
||||
reduceAddress(json, state) {
|
||||
state.address = json;
|
||||
}
|
||||
|
||||
reduceBalance(json, state) {
|
||||
state.unconfirmedBalance = json.unconfirmed;
|
||||
state.confirmedBalance = json.confirmed;
|
||||
}
|
||||
|
||||
reduceError(json, state) {
|
||||
state.error = json;
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import { InitialReducer } from './reducers/initial';
|
||||
import { UpdateReducer } from './reducers/update';
|
||||
import { CurrencyReducer } from './reducers/currency';
|
||||
import { SettingsReducer } from './reducers/settings';
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.state = {
|
||||
loadedBtc: false,
|
||||
loadedSettings: false,
|
||||
loaded: false,
|
||||
providerPerms: {},
|
||||
shipWallets: {},
|
||||
provider: null,
|
||||
wallet: null,
|
||||
confirmedBalance: null,
|
||||
unconfirmedBalance: null,
|
||||
btcState: null,
|
||||
history: [],
|
||||
psbt: '',
|
||||
address: null,
|
||||
currencyRates: {
|
||||
BTC: { last: 1, symbol: 'BTC' }
|
||||
},
|
||||
denomination: 'BTC',
|
||||
showWarning: true,
|
||||
error: '',
|
||||
broadcastSuccess: false,
|
||||
};
|
||||
|
||||
this.initialReducer = new InitialReducer();
|
||||
this.updateReducer = new UpdateReducer();
|
||||
this.currencyReducer = new CurrencyReducer();
|
||||
this.settingsReducer = new SettingsReducer();
|
||||
this.setState = () => { };
|
||||
}
|
||||
|
||||
setStateHandler(setState) {
|
||||
this.setState = setState;
|
||||
}
|
||||
|
||||
handleEvent(data) {
|
||||
let json = data.data;
|
||||
this.initialReducer.reduce(json, this.state);
|
||||
this.updateReducer.reduce(json, this.state);
|
||||
this.currencyReducer.reduce(json, this.state);
|
||||
this.settingsReducer.reduce(json, this.state);
|
||||
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
export let store = new Store();
|
||||
window.store = store;
|
Loading…
Reference in New Issue
Block a user