Convert components, remove old store

This commit is contained in:
finned-palmer 2021-06-30 16:17:31 -05:00
parent 69c123c22e
commit f269be8ba9
28 changed files with 2099 additions and 2570 deletions

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; 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 { api } from './js/api.js';
import { subscription } from "./js/subscription.js"; import { SettingsProvider } from './js/hooks/useSettings';
import './css/indigo-static.css'; import './css/indigo-static.css';
import './css/fonts.css'; import './css/fonts.css';
@ -13,11 +13,13 @@ import './css/custom.css';
const channel = new window.channel(); const channel = new window.channel();
api.setChannel(window.ship, channel); api.setChannel(window.ship, channel);
if (module.hot) { if (module.hot) {
module.hot.accept() module.hot.accept();
} }
ReactDOM.render(( ReactDOM.render(
<Root channel={channel}/> <SettingsProvider channel={channel}>
), document.querySelectorAll("#root")[0]); <Root />
</SettingsProvider>,
document.querySelectorAll('#root')[0]
);

View File

@ -1,170 +1,140 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { Row, Text, Button, Col } from '@tlon/indigo-react'; import { Row, Text, Button, Col } from '@tlon/indigo-react';
import Send from './send.js'; import Send from './send.js';
import CurrencyPicker from './currencyPicker.js'; import CurrencyPicker from './currencyPicker.js';
import { satsToCurrency } from '../../lib/util.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 { const Balance = () => {
constructor(props) { const {
super(props); 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 = { const copyAddress = (arg) => {
sending: false,
copiedButton: false,
copiedString: false,
};
this.copyAddress = this.copyAddress.bind(this);
}
copyAddress(arg) {
let address = this.props.state.address;
navigator.clipboard.writeText(address); navigator.clipboard.writeText(address);
this.props.api.btcWalletCommand({ 'gen-new-address': null }); api.btcWalletCommand({ 'gen-new-address': null });
if (arg === 'button') { if (arg === 'button') {
this.setState({ copiedButton: true }); setCopiedButton(true);
setTimeout(() => { setTimeout(() => {
this.setState({ copiedButton: false }); setCopiedButton(false);
}, 2000); }, 2000);
} else if (arg === 'string') { } else if (arg === 'string') {
this.setState({ copiedString: true }); setCopiedString(true);
setTimeout(() => { setTimeout(() => {
this.setState({ copiedString: false }); setCopiedString(false);
}, 2000); }, 2000);
} }
} };
render() { const unconfirmedString = unconfirmedSats ? ` (${unconfirmedSats}) ` : '';
const sats = this.props.state.confirmedBalance || 0;
const unconfirmedSats = this.props.state.unconfirmedBalance;
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 conversion = currencyRates[denomination]?.last;
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 = this.props.state.currencyRates[denomination].last; return (
<>
return ( {sending ? (
<> <Send
{this.state.sending ? ( value={value}
<Send conversion={conversion}
state={this.props.state} stopSending={() => {
api={this.props.api} setSending(false);
psbt={this.props.state.psbt} setPsbt('');
fee={this.props.state.fee} setFee(0);
currencyRates={this.props.state.currencyRates} setError('');
shipWallets={this.props.state.shipWallets} }}
value={value} />
denomination={denomination} ) : (
sats={sats} <Col
conversion={conversion} height="400px"
network={this.props.network} width="100%"
error={this.props.state.error} backgroundColor="white"
stopSending={() => { borderRadius="48px"
this.setState({ sending: false }); justifyContent="space-between"
store.handleEvent({ mb={5}
data: { psbt: '', fee: 0, error: '', 'broadcast-fail': null }, p={5}
}); >
}} <Row justifyContent="space-between">
/> <Text color="orange" fontSize={1}>
) : ( Balance
<Col </Text>
height="400px" <Text
width="100%" color="lightGray"
backgroundColor="white" fontSize="14px"
borderRadius="48px" mono
justifyContent="space-between" style={{ cursor: 'pointer' }}
mb={5} onClick={() => copyAddress('string')}
p={5} >
> {copiedString ? 'copied' : addressText}
<Row justifyContent="space-between"> </Text>
<Text color="orange" fontSize={1}> <CurrencyPicker />
Balance </Row>
</Text> <Col justifyContent="center" alignItems="center">
<Text <Text
color="lightGray" fontSize="40px"
fontSize="14px" color="orange"
mono style={{ whiteSpace: 'nowrap' }}
style={{ cursor: 'pointer' }} >
onClick={() => { {value}
this.copyAddress('string'); </Text>
}} <Text
> fontSize={1}
{this.state.copiedString ? 'copied' : addressText} color="orange"
</Text> >{`${sats}${unconfirmedString} sats`}</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>
</Col> </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;

View File

@ -1,72 +1,49 @@
import React, { Component } from 'react'; import React from 'react';
import { import { Box, LoadingSpinner, Col } from '@tlon/indigo-react';
Box, import { Switch, Route } from 'react-router-dom';
Icon, import Balance from './balance.js';
Row,
Text,
LoadingSpinner,
Col,
} from '@tlon/indigo-react';
import {
Switch,
Route,
} from 'react-router-dom';
import Balance from './balance.js';
import Transactions from './transactions.js'; import Transactions from './transactions.js';
import Warning from './warning.js'; import Warning from './warning.js';
import Header from './header.js'; import Header from './header.js';
import Settings from './settings.js'; import Settings from './settings.js';
import { useSettings } from '../../hooks/useSettings.js';
export default class Body extends Component { const Body = () => {
constructor(props) { const { loaded, showWarning: warning } = useSettings();
super(props); 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() { export default Body;
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>
);
}
}
}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { import {
Box, Box,
Icon, Icon,
@ -9,231 +9,203 @@ import {
Col, Col,
LoadingSpinner, LoadingSpinner,
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
import { Sigil } from './sigil.js'; import { Sigil } from './sigil.js';
import * as bitcoin from 'bitcoinjs-lib'; import * as bitcoin from 'bitcoinjs-lib';
import * as kg from 'urbit-key-generation';
import { isValidPatp } from 'urbit-ob'; import { isValidPatp } from 'urbit-ob';
import Sent from './sent.js';
import Sent from './sent.js' import Error from './error.js';
import Error from './error.js'
import { satsToCurrency } from '../../lib/util.js'; import { satsToCurrency } from '../../lib/util.js';
import { useSettings } from '../../hooks/useSettings.js';
import { api } from '../../api';
export default class BridgeInvoice extends Component { const BridgeInvoice = ({ payee, stopSending, satsAmount }) => {
constructor(props) { const { error, currencyRates, fee, broadcastSuccess, denomination, psbt } =
super(props); useSettings();
const [txHex, setTxHex] = useState('');
const [ready, setReady] = useState(false);
const [localError, setLocalError] = useState('');
const [broadcasting, setBroadcasting] = useState(false);
const invoiceRef = useRef();
this.state = { useEffect(() => {
txHex: '', if (broadcasting && localError !== '') {
ready: false, setBroadcasting(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
} }
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 !== '') { if (error !== '') {
inputColor = 'red'; setLocalError(error);
inputBg = 'veryLightRed';
inputBorder = 'red';
} }
}, [error, broadcasting, setBroadcasting]);
const isShip = isValidPatp(payee); useEffect(() => {
window.open('https://bridge.urbit.org/?kind=btc&utx=' + psbt);
});
const icon = (isShip) const broadCastTx = (hex) => {
? <Sigil ship={payee} size={24} color="black" classes={''} icon padding={5}/> let command = {
: <Box backgroundColor="lighterGray" 'broadcast-tx': hex,
width="24px" };
height="24px" return api.btcWalletCommand(command);
textAlign="center" };
alignItems="center"
borderRadius="2px"
p={1}
><Icon icon="Bitcoin" color="gray"/></Box>;
return ( const sendBitcoin = (hex) => {
<> try {
{ this.props.state.broadcastSuccess ? bitcoin.Transaction.fromHex(hex);
<Sent broadCastTx(hex);
payee={payee} setBroadcasting(true);
stopSending={stopSending} } catch (e) {
denomination={denomination} setLocalError('invalid-signed');
currencyRates={currencyRates} setBroadcasting(false);
satsAmount={satsAmount} }
/> : };
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 <Col
ref={this.setInvoiceRef}
width='100%'
backgroundColor='white'
borderRadius='48px'
mb={5}
p={5} p={5}
mt={4}
backgroundColor="veryLightGreen"
borderRadius="24px"
alignItems="center"
> >
<Col <Row>
p={5} <Text color="green" fontSize="40px">
mt={4} {satsToCurrency(satsAmount, denomination, currencyRates)}
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
</Text> </Text>
</Box> </Row>
<Box mt={1} mb={2}> <Row>
<Text gray fontSize='14px'> <Text
Copy the signed transaction from Bridge 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> </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> </Row>
</Col> </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;

View File

@ -1,50 +1,37 @@
import React, { Component } from 'react'; import React from 'react';
import { import { Icon, Row, Text } from '@tlon/indigo-react';
Box, import { api } from '../../api';
Icon, import { useSettings } from '../../hooks/useSettings';
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import _ from 'lodash';
import { satsToCurrency } from '../../lib/util.js' const CurrencyPicker = () => {
import { store } from '../../store'; const { denomination, currencyRates } = useSettings();
const switchCurrency = () => {
export default class CurrencyPicker extends Component {
constructor(props) {
super(props);
this.switchCurrency = this.switchCurrency.bind(this);
}
switchCurrency(){
let newCurrency; let newCurrency;
if (this.props.denomination === 'BTC') { if (denomination === 'BTC') {
if (this.props.currencies['USD']) { if (currencyRates['USD']) {
newCurrency = "USD"; newCurrency = 'USD';
} }
} else if (this.props.denomination === 'USD') { } else if (denomination === 'USD') {
newCurrency = "BTC"; newCurrency = 'BTC';
} }
let setCurrency = { let setCurrency = {
"put-entry": { 'put-entry': {
value: newCurrency, value: newCurrency,
"entry-key": "currency", 'entry-key': 'currency',
"bucket-key": "btc-wallet", 'bucket-key': 'btc-wallet',
} },
} };
this.props.api.settingsEvent(setCurrency); 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() { export default CurrencyPicker;
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>
);
}
}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React from 'react';
import { Text } from '@tlon/indigo-react'; import { Text } from '@tlon/indigo-react';
const errorToString = (error) => { const errorToString = (error) => {
@ -26,16 +26,12 @@ const errorToString = (error) => {
if (error === 'invalid-signed') { if (error === 'invalid-signed') {
return 'Invalid signed bitcoin transaction'; return 'Invalid signed bitcoin transaction';
} }
} };
export default function Error(props) { const Error = ({ error, ...rest }) => (
const error = errorToString(props.error); <Text color="red" {...rest}>
{errorToString(error)}
</Text>
);
return( export default Error;
<Text
color='red'
{...props}>
{error}
</Text>
);
}

View File

@ -1,99 +1,99 @@
import React, { Component } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { import {
Box, Box,
Icon,
Row,
Text, Text,
Button,
Col, Col,
StatelessRadioButtonField as RadioButton, StatelessRadioButtonField as RadioButton,
Label, Label,
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
const feeLevels = {
low: 'low',
mid: 'mid',
high: 'high',
};
export default class FeePicker extends Component { const FeePicker = ({ feeChoices, feeSelect, feeDismiss }) => {
constructor(props) { const [feeSelected, setFeeSelected] = useState(feeLevels.mid);
super(props); const [modalElement, setModalElement] = useState();
const modalRef = useRef();
this.state = { // const clickDismiss = (e) => {
selected: 'mid' // console.log(modalElement, e);
} // // if (modalRef && !modalRef.contains(e.target)) {
// // feeDismiss();
// // }
// };
this.select = this.select.bind(this); const select = (which) => {
this.clickDismiss = this.clickDismiss.bind(this); setFeeSelected(which);
this.setModalRef = this.setModalRef.bind(this); feeSelect(which);
} feeDismiss();
};
componentDidMount() { // useEffect(() => {
document.addEventListener("click", this.clickDismiss); // document.addEventListener('click', (e) => clickDismiss(e));
} // setModalElement(modalRef.current);
// console.log(modalRef.current);
// return () => document.addEventListener('click', clickDismiss);
// }, []);
componentWillUnount() { return (
document.removeEventListener("click", this.clickDismiss); <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) { <RadioButton
this.modalRef = n; 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) { <RadioButton
if (this.modalRef && !(this.modalRef.contains(e.target))){ name="feeRadio"
this.props.feeDismiss(); 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) { export default FeePicker;
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>
);
}
}

View File

@ -1,78 +1,81 @@
import React, { Component } from 'react'; import React from 'react';
import { import { Box, Icon, Row, Text } from '@tlon/indigo-react';
Box,
Icon,
Row,
Text,
} from '@tlon/indigo-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useSettings } from '../../hooks/useSettings';
export default class Header extends Component { const Header = ({ settings }) => {
constructor(props) { const { provider } = useSettings();
super(props); 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>
);
}
} }
return (
render() { <Row
let icon = this.props.settings ? "X" : "Adjust"; height={8}
let iconColor = this.props.settings ? "black" : "orange"; width="100%"
let iconLink = this.props.settings ? "/~btc" : "/~btc/settings"; justifyContent="space-between"
alignItems="center"
let connection = null; pt={5}
let badge = null; pb={5}
if (!(this.props.state.provider && this.props.state.provider.connected)) { >
connection = <Row alignItems="center" justifyContent="center">
<Text fontSize={1} color="red" fontWeight="bold" mr={3}> <Box
Provider Offline 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> </Text>
</Row>
if (!this.props.settings) { <Row alignItems="center">
badge = <Box borderRadius="50%" width="8px" height="8px" backgroundColor="red" position="absolute" top="0px" right="0px"></Box> {connection}
<Link to={iconLink}>
} <Box
} backgroundColor="white"
borderRadius={4}
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} width={5}
height={5} height={5}
alignItems="center" p={2}
justifyContent="center" position="relative"
> >
<Icon icon="Bitcoin" width={4} p={1} height={4} color="white"/> {badge}
<Icon icon={icon} color={iconColor} />
</Box> </Box>
<Text fontSize={2} fontWeight="bold" color="orange"> </Link>
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>
</Row> </Row>
); </Row>
} );
} };
export default Header;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { useRef, useState, useEffect } from 'react';
import { import {
Box, Box,
Icon, Icon,
@ -9,18 +9,15 @@ import {
Col, Col,
LoadingSpinner, LoadingSpinner,
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
import { Sigil } from './sigil.js';
import { Sigil } from './sigil.js'
import * as bitcoin from 'bitcoinjs-lib'; import * as bitcoin from 'bitcoinjs-lib';
import * as kg from 'urbit-key-generation'; 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 { patp2dec, isValidPatq, isValidPatp } from 'urbit-ob';
import { satsToCurrency } from '../../lib/util.js'; import { satsToCurrency } from '../../lib/util.js';
import Error from './error.js'; import Error from './error.js';
import { useSettings } from '../../hooks/useSettings.js';
import { api } from '../../api';
const BITCOIN_MAINNET_INFO = { const BITCOIN_MAINNET_INFO = {
messagePrefix: '\x18Bitcoin Signed Message:\n', messagePrefix: '\x18Bitcoin Signed Message:\n',
@ -46,246 +43,234 @@ const BITCOIN_TESTNET_INFO = {
wif: 0xef, wif: 0xef,
}; };
export default class Invoice extends Component { const Invoice = ({ stopSending, payee, satsAmount }) => {
constructor(props) { const {
super(props); 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 = { useEffect(() => {
masterTicket: '', if (broadcasting && localError !== '') {
ready: false, setBroadcasting(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();
} }
} }, [error, broadcasting, setBroadcasting]);
componentDidUpdate(prevProps, prevState) { const clickDismiss = (e) => {
if (this.state.broadcasting) { if (invoiceRef && !invoiceRef.contains(e.target)) {
if (this.state.error !== '') { stopSending();
this.setState({broadcasting: false});
}
} }
} };
broadCastTx(psbtHex) { useEffect(() => {
document.addEventListener('click', clickDismiss);
return () => document.removeEventListener('click', clickDismiss);
}, []);
const broadCastTx = (psbtHex) => {
let command = { let command = {
'broadcast-tx': psbtHex 'broadcast-tx': psbtHex,
} };
return this.props.api.btcWalletCommand(command) return api.btcWalletCommand(command);
} };
sendBitcoin(ticket, psbt) { const sendBitcoin = (ticket, psbt) => {
const newPsbt = bitcoin.Psbt.fromBase64(psbt); const newPsbt = bitcoin.Psbt.fromBase64(psbt);
this.setState({broadcasting: true}); setBroadcasting(true);
kg.generateWallet({ ticket, ship: parseInt(patp2dec('~' + window.ship)) }) kg.generateWallet({
.then(urbitWallet => { ticket,
const { xpub } = this.props.network === 'testnet' ship: parseInt(patp2dec('~' + window.ship)),
? urbitWallet.bitcoinTestnet.keys }).then((urbitWallet) => {
: urbitWallet.bitcoinMainnet.keys; // const { xpub } =
// network === 'testnet'
// ? urbitWallet.bitcoinTestnet.keys
// : urbitWallet.bitcoinMainnet.keys;
const { xprv: zprv } = urbitWallet.bitcoinMainnet.keys; const { xprv: zprv } = urbitWallet.bitcoinMainnet.keys;
const { xprv: vprv } = urbitWallet.bitcoinTestnet.keys; const { xprv: vprv } = urbitWallet.bitcoinTestnet.keys;
const isTestnet = (this.props.network === 'testnet'); const isTestnet = network === 'testnet';
const derivationPrefix = isTestnet ? "m/84'/1'/0'/" : "m/84'/0'/0'/"; const derivationPrefix = isTestnet ? "m/84'/1'/0'/" : "m/84'/0'/0'/";
const btcWallet = (isTestnet) const btcWallet = isTestnet
? bitcoin.bip32.fromBase58(vprv, BITCOIN_TESTNET_INFO) ? bitcoin.bip32.fromBase58(vprv, BITCOIN_TESTNET_INFO)
: bitcoin.bip32.fromBase58(zprv, BITCOIN_MAINNET_INFO); : bitcoin.bip32.fromBase58(zprv, BITCOIN_MAINNET_INFO);
try { try {
const hex = newPsbt.data.inputs const hex = newPsbt.data.inputs
.reduce((psbt, input, idx) => { .reduce((psbt, input, idx) => {
// removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0 // removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
const path = input.bip32Derivation[0].path const path = input.bip32Derivation[0].path
.split(derivationPrefix) .split(derivationPrefix)
.join(''); .join('');
const prv = btcWallet.derivePath(path).privateKey; const prv = btcWallet.derivePath(path).privateKey;
return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv)); return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
}, newPsbt) }, newPsbt)
.finalizeAllInputs() .finalizeAllInputs()
.extractTransaction() .extractTransaction()
.toHex(); .toHex();
this.broadCastTx(hex); broadCastTx(hex);
} } catch (e) {
catch(e) { setLocalError('invalid-master-ticket');
this.setState({error: 'invalid-master-ticket', broadcasting: false}); setBroadcasting(false);
} }
}); });
};
} const checkTicket = (e) => {
checkTicket(e){
// TODO: port over bridge ticket validation logic // TODO: port over bridge ticket validation logic
let masterTicket = e.target.value; setMasterTicket(e.target.value);
let ready = isValidPatq(masterTicket); setReady(isValidPatq(e.target.value));
let error = (ready) ? '' : 'invalid-master-ticket'; setLocalError(isValidPatq(e.target.value) ? '' : 'invalid-master-ticket');
this.setState({masterTicket, ready, error}); };
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (error !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
} }
render() { const isShip = isValidPatp(payee);
const broadcastSuccess = this.props.state.broadcastSuccess;
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates, fee } = this.props;
const { sent, error } = this.state;
let inputColor = 'black'; const icon = isShip ? (
let inputBg = 'white'; <Sigil ship={payee} size={24} color="black" classes={''} icon padding={5} />
let inputBorder = 'lightGray'; ) : (
<Box
backgroundColor="lighterGray"
width="24px"
height="24px"
textAlign="center"
alignItems="center"
borderRadius="2px"
p={1}
>
<Icon icon="Bitcoin" color="gray" />
</Box>
);
if (error !== '') { return (
inputColor = 'red'; <>
inputBg = 'veryLightRed'; {broadcastSuccess ? (
inputBorder = 'red'; <Sent payee={payee} stopSending={stopSending} satsAmount={satsAmount} />
} ) : (
<Col
const isShip = isValidPatp(payee); ref={invoiceRef}
width="100%"
const icon = (isShip) backgroundColor="white"
? <Sigil ship={payee} size={24} color="black" classes={''} icon padding={5}/> borderRadius="48px"
: <Box backgroundColor="lighterGray" mb={5}
width="24px" p={5}
height="24px" onClick={() => stopSending()}
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}
/> :
<Col <Col
ref={this.setInvoiceRef}
width='100%'
backgroundColor='white'
borderRadius='48px'
mb={5}
p={5} p={5}
mt={4}
backgroundColor="veryLightGreen"
borderRadius="24px"
alignItems="center"
> >
<Col <Row>
p={5} <Text color="green" fontSize="40px">
mt={4} {satsToCurrency(satsAmount, denomination, currencyRates)}
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
</Text> </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> </Row>
{(error !== '') && <Row>
<Row> <Text
<Error fontWeight="bold"
fontSize='14px' fontSize="16px"
color='red' color="midGreen"
error={error} >{`${satsAmount} sats`}</Text>
mt={2}/> </Row>
</Row> <Row mt={2}>
} <Text fontSize="14px" color="midGreen">{`Fee: ${satsToCurrency(
<Row fee,
flexDirection='row-reverse' denomination,
mt={4} currencyRates
alignItems="center" )} (${fee} sats)`}</Text>
> </Row>
<Button <Row mt={4}>
primary <Text fontSize="16px" fontWeight="bold" color="gray">
children='Send BTC' You are paying
mr={3} </Text>
fontSize={1} </Row>
border="none" <Row mt={2} alignItems="center">
borderRadius='24px' {icon}
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"} <Text
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"} ml={2}
height='48px' mono
onClick={() => this.sendBitcoin(this.state.masterTicket, psbt)} color="gray"
disabled={!this.state.ready || error || this.state.broadcasting} fontSize="14px"
style={{cursor: (this.state.ready && !error && !this.state.broadcasting) ? "pointer" : "default"}} style={{ display: 'block', 'overflow-wrap': 'anywhere' }}
/> >
{ (this.state.broadcasting) ? <LoadingSpinner mr={3}/> : null} {payee}
</Text>
</Row> </Row>
</Col> </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;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Box, Box,
Text, Text,
@ -8,159 +8,157 @@ import {
Row, Row,
LoadingSpinner, LoadingSpinner,
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
import { isValidPatp } from 'urbit-ob'; import { isValidPatp } from 'urbit-ob';
import { api } from '../../api';
import { useSettings } from '../../hooks/useSettings';
export default class ProviderModal extends Component { const providerStatuses = {
constructor(props) { checking: 'checking',
super(props); failed: 'failed',
ready: 'ready',
initial: '',
};
this.state = { const ProviderModal = () => {
potentialProvider: null, const { providerPerms } = useSettings();
checkingProvider: false, const [providerStatus, setProviderStatus] = useState(
providerFailed: false, providerStatuses.initial
ready: false, );
provider: null, const [potentialProvider, setPotentialProvider] = useState(null);
connecting: false, const [provider, setProvider] = useState(null);
}; const [connecting, setConnecting] = useState(false);
this.checkProvider = this.checkProvider.bind(this); const checkProvider = (e) => {
this.submitProvider = this.submitProvider.bind(this);
}
checkProvider(e) {
// TODO: loading states // TODO: loading states
let provider = e.target.value; setProviderStatus(providerStatuses.initial);
let ready = false; let givenProvider = e.target.value;
let checkingProvider = false; if (isValidPatp(givenProvider)) {
let potentialProvider = this.state.potentialProvider;
if (isValidPatp(provider)) {
let command = { let command = {
'check-provider': provider, 'check-provider': givenProvider,
}; };
potentialProvider = provider; setPotentialProvider(givenProvider);
checkingProvider = true; setProviderStatus(providerStatuses.checking);
this.props.api.btcWalletCommand(command); api.btcWalletCommand(command);
setTimeout(() => { setTimeout(() => {
this.setState({ providerFailed: true, checkingProvider: false }); setProviderStatus(providerStatuses.failed);
}, 5000); }, 5000);
} }
this.setState({ provider, ready, checkingProvider, potentialProvider }); setProvider(givenProvider);
} };
componentDidUpdate() { const submitProvider = () => {
if (!this.state.ready) { if (providerStatus === providerStatuses.ready) {
if (this.props.providerPerms[this.state.provider]) { let command = {
this.setState({ 'set-provider': provider,
ready: true, };
checkingProvider: false, api.btcWalletCommand(command);
providerFailed: false, setConnecting(true);
}); }
};
useEffect(() => {
if (providerStatus !== providerStatuses.ready) {
if (providerPerms.provider === provider && providerPerms.permitted) {
setProviderStatus(providerStatuses.ready);
} }
} }
} }, [providerStatus, providerPerms, provider, setProviderStatus]);
submitProvider() { let workingNode = null;
if (this.state.ready) { let workingColor = null;
let command = { let workingBg = null;
'set-provider': this.state.provider, if (providerStatus === providerStatuses.ready) {
}; workingColor = 'green';
this.props.api.btcWalletCommand(command); workingBg = 'veryLightGreen';
this.setState({ connecting: true }); workingNode = (
} <Box mt={3}>
} <Text fontSize="14px" color="green">
{provider} is a working provider node
render() { </Text>
let workingNode = null; </Box>
let workingColor = null; );
let workingBg = null; } else if (providerStatus === providerStatuses.failed) {
if (this.state.ready) { workingColor = 'red';
workingColor = 'green'; workingBg = 'veryLightRed';
workingBg = 'veryLightGreen'; workingNode = (
workingNode = ( <Box mt={3}>
<Box mt={3}> <Text fontSize="14px" color="red">
<Text fontSize="14px" color="green"> {potentialProvider} is not a working provider node
{this.state.provider} is a working provider node </Text>
</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&apos;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>
</Box> </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&apos;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;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Box, Box,
Icon, Icon,
@ -8,450 +8,443 @@ import {
Button, Button,
Col, Col,
LoadingSpinner, LoadingSpinner,
StatelessRadioButtonField as RadioButton,
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
import Invoice from './invoice.js';
import Invoice from './invoice.js' import BridgeInvoice from './bridgeInvoice.js';
import BridgeInvoice from './bridgeInvoice.js' import FeePicker from './feePicker.js';
import FeePicker from './feePicker.js' import Error from './error.js';
import Error from './error.js' import Signer from './signer.js';
import Signer from './signer.js'
import { validate } from 'bitcoin-address-validation'; import { validate } from 'bitcoin-address-validation';
import * as ob from 'urbit-ob'; import * as ob from 'urbit-ob';
import { useSettings } from '../../hooks/useSettings.js';
import { api } from '../../api';
export default class Send extends Component { const focusFields = {
constructor(props) { empty: '',
super(props); payee: 'payee',
currency: 'currency',
sats: 'sats',
note: 'note',
};
this.state = { const Send = ({ stopSending, value, conversion }) => {
signing: false, const { error, setError, network, psbt, denomination, shipWallets } =
denomAmount: '0.00', useSettings();
satsAmount: '0', const [signing, setSigning] = useState(false);
payee: '', const [denomAmount, setDenomAmount] = useState('0.00');
checkingPatp: false, const [satsAmount, setSatsAmount] = useState('0');
payeeType: '', const [payee, setPayee] = useState('');
ready: false, const [checkingPatp, setCheckingPatp] = useState(false);
validPayee: false, const [payeeType, setPayeeType] = useState('');
focusPayee: true, const [ready, setReady] = useState(false);
focusCurrency: false, const [validPayee, setValidPayee] = useState(false);
focusSats: false, const [focusedField, setFocusedField] = useState(focusFields.empty);
focusNote: false, const [feeChoices, setFeeChoices] = useState({
submitting: false, low: [10, 1],
feeChoices: { mid: [10, 1],
low: [10, 1], high: [10, 1],
mid: [10, 1], });
high: [10, 1], const [feeValue, setFeeValue] = useState('mid');
}, const [showModal, setShowModal] = useState(false);
feeValue: "mid", const [note, setNote] = useState('');
showModal: false, const [choosingSignMethod, setChoosingSignMethod] = useState(false);
note: '', const [signMethod, setSignMethod] = useState('bridge');
choosingSignMethod: false,
signMethod: 'bridge',
};
this.initPayment = this.initPayment.bind(this); const feeDismiss = () => {
this.checkPayee = this.checkPayee.bind(this); setShowModal(false);
this.feeSelect = this.feeSelect.bind(this); };
this.feeDismiss = this.feeDismiss.bind(this);
this.toggleSignMethod = this.toggleSignMethod.bind(this);
this.setSignMethod = this.setSignMethod.bind(this);
}
feeDismiss() { const feeSelect = (which) => {
this.setState({showModal: false}); setFeeValue(which);
} };
feeSelect(which) { const handleSetSignMethod = (signMethod) => {
this.setState({feeValue: which}); setSignMethod(signMethod);
} setChoosingSignMethod(false);
};
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 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) { if (isPatp) {
let command = {'check-payee': payee} console.log('isPatp', isPatp);
this.props.api.btcWalletCommand(command) let command = { 'check-payee': payeeReceived };
api.btcWalletCommand(command);
setTimeout(() => { setTimeout(() => {
this.setState({checkingPatp: false}); setCheckingPatp(false);
}, 5000); }, 5000);
this.setState({ setCheckingPatp(true);
checkingPatp: true, setPayeeType('ship');
payeeType: 'ship', setPayee(payeeReceived);
payee,
});
} else if (isAddress) { } else if (isAddress) {
this.setState({ setPayee(payeeReceived);
payee, setReady(true);
ready: true, setCheckingPatp(false);
checkingPatp: false, setPayeeType('address');
payeeType: 'address', setValidPayee(true);
validPayee: true,
});
} else { } else {
this.setState({ setPayee(payeeReceived);
payee, setReady(false);
ready: false, setCheckingPatp(false);
checkingPatp: false, setPayeeType('');
payeeType: '', setValidPayee(false);
validPayee: false,
});
} }
} };
componentDidUpdate(prevProps, prevState) { const toggleSignMethod = () => {
if ((prevProps.error !== this.props.error) && setChoosingSignMethod(!choosingSignMethod);
(this.props.error !== '') && (this.props.error !== 'broadcast-fail')) { };
this.setState({signing: false});
}
if (!this.state.ready && this.state.checkingPatp) { const initPayment = () => {
if (this.props.shipWallets[this.state.payee.slice(1)]) { if (payeeType === 'ship') {
this.setState({ready: true, checkingPatp: false, validPayee: true});
}
}
}
toggleSignMethod(toggle) {
this.setState({choosingSignMethod: !toggle});
}
initPayment() {
if (this.state.payeeType === 'ship') {
let command = { let command = {
'init-payment': { 'init-payment': {
'payee': this.state.payee, payee,
'value': parseInt(this.state.satsAmount), value: parseInt(satsAmount),
'feyb': this.state.feeChoices[this.state.feeValue][1], feyb: feeChoices[feeValue][1],
'note': (this.state.note || null), note: note || null,
} },
} };
this.props.api.btcWalletCommand(command).then(res => this.setState({signing: true}));
} else if (this.state.payeeType === 'address') { api.btcWalletCommand(command).then(() => setSigning(true));
} else if (payeeType === 'address') {
let command = { let command = {
'init-payment-external': { 'init-payment-external': {
'address': this.state.payee, address: payee,
'value': parseInt(this.state.satsAmount), value: parseInt(satsAmount),
'feyb': 1, feyb: 1,
'note': (this.state.note || null), note: note || null,
} },
} };
this.props.api.btcWalletCommand(command).then(res => this.setState({signing: true})); 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() { const signReady = ready && parseInt(satsAmount) > 0 && !signing;
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";
}
let invoice = null;
const { api, value, conversion, stopSending, denomination, psbt, currencyRates, error, network, fee } = this.props; if (signMethod === 'masterTicket') {
const { denomAmount, satsAmount, signing, payee, choosingSignMethod, signMethod } = this.state; invoice = (
<Invoice
const signReady = (this.state.ready && (parseInt(this.state.satsAmount) > 0)) && !signing; stopSending={stopSending}
payee={payee}
let invoice = null; satsAmount={satsAmount}
if (signMethod === 'masterTicket') { />
invoice = );
<Invoice } else if (signMethod === 'bridge') {
network={network} invoice = (
api={api} <BridgeInvoice
psbt={psbt} stopSending={stopSending}
fee={fee} payee={payee}
currencyRates={currencyRates} satsAmount={satsAmount}
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>
}
</>
); );
} }
}
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;

View File

@ -1,59 +1,36 @@
import React, { Component } from 'react'; import React from 'react';
import { import { Icon, Row, Col, Center, Text } from '@tlon/indigo-react';
Box,
Icon,
StatelessTextInput as Input,
Row,
Center,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import { satsToCurrency } from '../../lib/util.js'; import { satsToCurrency } from '../../lib/util.js';
import { useSettings } from '../../hooks/useSettings';
export default function Sent(props) { const Sent = ({ payee, stopSending, satsAmount }) => {
const { payee, denomination, satsAmount, stopSending, currencyRates } = props; const { denomination, currencyRates } = useSettings();
return ( return (
<Col <Col
height='400px' height="400px"
width='100%' width="100%"
backgroundColor='orange' backgroundColor="orange"
borderRadius='48px' borderRadius="48px"
mb={5} mb={5}
p={5} p={5}
> >
<Row <Row flexDirection="row-reverse">
flexDirection='row-reverse' <Icon color="white" icon="X" cursor="pointer" onClick={stopSending} />
>
<Icon
color='white'
icon='X'
cursor='pointer'
onClick={stopSending}
/>
</Row> </Row>
<Center> <Center>
<Text <Text
style={{'display': 'block', 'overflow-wrap': 'anywhere'}} style={{ display: 'block', 'overflow-wrap': 'anywhere' }}
color='white'>{`You sent BTC to ${payee}`}</Text> color="white"
>{`You sent BTC to ${payee}`}</Text>
</Center> </Center>
<Center <Center flexDirection="column" flex="1 1 auto">
flexDirection='column' <Text color="white" fontSize="40px">
flex='1 1 auto'
>
<Text
color='white'
fontSize='40px'
>
{satsToCurrency(satsAmount, denomination, currencyRates)} {satsToCurrency(satsAmount, denomination, currencyRates)}
</Text> </Text>
<Text <Text color="white">{`${satsAmount} sats`}</Text>
color='white'
>
{`${satsAmount} sats`}
</Text>
</Center> </Center>
</Col> </Col>
); );
} };
export default Sent;

View File

@ -1,121 +1,114 @@
import React, { Component } from 'react'; import React from 'react';
import { import { Row, Text, Button, Col } from '@tlon/indigo-react';
Box, import { useSettings } from '../../hooks/useSettings';
Icon, import { api } from '../../api';
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
export default class Settings extends Component { const Settings = () => {
constructor(props) { const { wallet, provider } = useSettings();
super(props);
this.changeProvider = this.changeProvider.bind(this);
this.replaceWallet = this.replaceWallet.bind(this);
}
changeProvider(){ const changeProvider = () => {
this.props.api.btcWalletCommand({'set-provider': null}); api.btcWalletCommand({ 'set-provider': null });
} };
replaceWallet(){ const replaceWallet = () => {
this.props.api.btcWalletCommand({ api.btcWalletCommand({
'delete-wallet': this.props.state.wallet, 'delete-wallet': wallet,
}); });
} };
render() { let connColor = 'red';
let connColor = "red"; let connBackground = 'veryLightRed';
let connBackground = "veryLightRed"; let conn = 'Offline';
let conn = 'Offline' let host = '';
let host = ''; if (provider) {
if (this.props.state.provider){ if (provider.connected) conn = 'Connected';
if (this.props.state.provider.connected) conn = 'Connected'; if (provider.host) host = provider.host;
if (this.props.state.provider.host) host = this.props.state.provider.host; if (provider.connected && provider.host) {
if (this.props.state.provider.connected && this.props.state.provider.host) { connColor = 'orange';
connColor = "orange"; connBackground = 'lightOrange';
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;

View File

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

View File

@ -1,52 +1,44 @@
import React, { Component } from 'react'; import React from 'react';
import { Box } from '@tlon/indigo-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' const StartupModal = () => {
import ProviderModal from './providerModal.js' const { wallet, provider } = useSettings();
let modal = null;
if (wallet && provider) {
export default class StartupModal extends Component { return null;
constructor(props) { } else if (!provider) {
super(props); modal = <ProviderModal />;
} else if (!wallet) {
modal = <WalletModal />;
} }
return (
<Box
render() { backgroundColor="scales.black20"
let modal = null; left="0px"
top="0px"
if (this.props.state.wallet && this.props.state.provider) { width="100%"
return null; height="100%"
} else if (!this.props.state.provider){ position="fixed"
modal = display="flex"
<ProviderModal zIndex={10}
api={this.props.api} justifyContent="center"
providerPerms={this.props.state.providerPerms} alignItems="center"
/> >
} else if (!this.props.state.wallet){
modal = <WalletModal api={this.props.api} network={this.props.network}/>
}
return (
<Box <Box
backgroundColor="scales.black20"
left="0px"
top="0px"
width="100%"
height="100%"
position="fixed"
display="flex" display="flex"
zIndex={10} flexDirection="column"
justifyContent="center" width="400px"
alignItems="center" backgroundColor="white"
borderRadius={3}
> >
<Box display="flex" {modal}
flexDirection="column"
width='400px'
backgroundColor="white"
borderRadius={3}
>
{modal}
</Box>
</Box> </Box>
); </Box>
} );
} };
export default StartupModal;

View File

@ -1,105 +1,96 @@
import React, { Component } from 'react'; import React from 'react';
import { import { Box, Row, Text, Col } from '@tlon/indigo-react';
Box,
Icon,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import _ from 'lodash'; 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' const Transaction = ({ tx }) => {
import TxAction from './tx-action.js' const { denomination, currencyRates } = useSettings();
import TxCounterparty from './tx-counterparty.js' const pending = !tx.recvd;
import { satsToCurrency } from '../../lib/util.js'
export default class Transaction extends Component { let weSent = _.find(tx.inputs, (input) => {
constructor(props) { return input.ship === window.ship;
super(props); });
} let weRecv = tx.outputs.every((output) => {
return output.ship === window.ship;
});
render() { let action = weRecv ? 'recv' : weSent ? 'sent' : 'recv';
const pending = (!this.props.tx.recvd);
let weSent = _.find(this.props.tx.inputs, (input) => { let counterShip = null;
return (input.ship === window.ship); 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) => { counterShip = _.get(counter, 'ship', null);
return (output.ship === window.ship) counterAddress = _.get(counter, 'val.address', null);
}); value = _.get(counter, 'val.value', null);
sign = '-';
let action = } else if (action === 'recv') {
(weRecv) ? "recv" : value = _.reduce(
(weSent) ? "sent" : "recv"; tx.outputs,
(sum, output) => {
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) => {
if (output.ship === window.ship) { if (output.ship === window.ship) {
return sum + output.val.value; return sum + output.val.value;
} else { } else {
return sum; return sum;
} }
}, 0); },
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>
); );
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;

View File

@ -1,62 +1,43 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import { Box, Text, Col } from '@tlon/indigo-react';
Box,
Icon,
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import Transaction from './transaction.js'; import Transaction from './transaction.js';
import { useSettings } from '../../hooks/useSettings.js';
const Transactions = () => {
export default class Transactions extends Component { const { history } = useSettings();
constructor(props) { if (!history || history.length <= 0) {
super(props); 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>
);
} }
};
export default Transactions;
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>
);
}
}
}

View File

@ -1,74 +1,72 @@
import React, { Component } from 'react'; import React from 'react';
import { Box, Icon, Row, Text, LoadingSpinner } from '@tlon/indigo-react'; import { Box, Icon, Row, Text, LoadingSpinner } from '@tlon/indigo-react';
import { useSettings } from '../../hooks/useSettings';
export default class TxAction extends Component { const TxAction = ({ action, pending, txid }) => {
constructor(props) { const { network } = useSettings();
super(props); const leftIcon =
} action === 'sent'
? 'ArrowSouth'
: action === 'recv'
? 'ArrowNorth'
: action === 'fail'
? 'X'
: 'NullIcon';
render() { const actionColor =
const leftIcon = action === 'sent'
this.props.action === 'sent' ? 'sentBlue'
? 'ArrowSouth' : action === 'recv'
: this.props.action === 'recv' ? 'recvGreen'
? 'ArrowNorth' : action === 'fail'
: this.props.action === 'fail' ? 'gray'
? 'X' : 'red';
: 'NullIcon';
const actionColor = const actionText =
this.props.action === 'sent' action === 'sent' && !pending
? 'sentBlue' ? 'Sent BTC'
: this.props.action === 'recv' : action === 'sent' && pending
? 'recvGreen' ? 'Sending BTC'
: this.props.action === 'fail' : action === 'recv' && !pending
? 'gray' ? 'Received BTC'
: 'red'; : action === 'recv' && pending
? 'Receiving BTC'
: action === 'fail'
? 'Failed'
: 'error';
const actionText = const pendingSpinner = !pending ? null : (
this.props.action === 'sent' && !this.props.pending <LoadingSpinner background="midOrange" foreground="orange" />
? '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 pending = !this.props.pending ? null : ( const url =
<LoadingSpinner background="midOrange" foreground="orange" /> network === 'testnet'
); ? `http://blockstream.info/testnet/tx/${txid}`
: `http://blockstream.info/tx/${txid}`;
const url = return (
this.props.network === 'testnet' <Row alignItems="center">
? `http://blockstream.info/testnet/tx/${this.props.txid}` <Box
: `http://blockstream.info/tx/${this.props.txid}`; 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 ( export default TxAction;
<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>
);
}
}

View File

@ -1,53 +1,36 @@
import React, { Component } from 'react'; import React from 'react';
import { import { Box, Icon, Row, Text } from '@tlon/indigo-react';
Box, import { Sigil } from './sigil.js';
Icon,
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import { Sigil } from './sigil.js' const TxCounterparty = ({ ship, address }) => {
import TxAction from './tx-action.js' 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 { return (
constructor(props) { <Row alignItems="center">
super(props); {icon}
} <Text ml={2} mono fontSize="14px" color="gray">
{text}
</Text>
</Row>
);
};
export default TxCounterparty;
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>
);
}
}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { import {
Box, Box,
Text, Text,
@ -6,247 +6,251 @@ import {
StatelessTextInput, StatelessTextInput,
Icon, Icon,
Row, Row,
Input,
LoadingSpinner, LoadingSpinner,
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
import { patp2dec, isValidPatq } from 'urbit-ob'; 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 WalletModal = () => {
const bitcoin = require('bitcoinjs-lib'); const { network } = useSettings();
const bs58check = require('bs58check') const [mode, setMode] = useState('xpub');
import { Buffer } from 'buffer'; 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 { const checkTicket = ({
constructor(props) { event: {
super(props); target: { value },
},
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){
// TODO: port over bridge ticket validation logic // TODO: port over bridge ticket validation logic
if (this.state.confirmingMasterTicket) { if (confirmingMasterTicket) {
let confirmedMasterTicket = e.target.value; setConfirmedMasterTicket(value);
let readyToSubmit = isValidPatq(confirmedMasterTicket); setReadyToSubmit(isValidPatq(value));
this.setState({confirmedMasterTicket, readyToSubmit});
} else { } else {
let masterTicket = e.target.value; setMasterTicket(value);
let readyToSubmit = isValidPatq(masterTicket); setReadyToSubmit(isValidPatq(value));
this.setState({masterTicket, readyToSubmit});
} }
} };
checkXPub(e){ const checkXPub = ({
let xpub = e.target.value; event: {
let readyToSubmit = (xpub.length > 0); target: { value: xpubGiven },
this.setState({xpub, readyToSubmit}); },
} }) => {
setXpub(xpubGiven);
setReadyToSubmit(xpubGiven.length > 0);
};
submitMasterTicket(ticket){ const submitXPub = (givenXpub) => {
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 command = { const command = {
"add-wallet": { 'add-wallet': {
"xpub": xpub, xpub: givenXpub,
"fprint": [4, 0], fprint: [4, 0],
"scan-to": null, 'scan-to': null,
"max-gap": 8, 'max-gap': 8,
"confs": 1 confs: 1,
} },
} };
api.btcWalletCommand(command); api.btcWalletCommand(command);
this.setState({processingSubmission: true}); setProcessingSubmission(true);
} };
render() { const submitMasterTicket = (ticket) => {
const buttonDisabled = (!this.state.readyToSubmit || this.state.processingSubmission ); setProcessingSubmission(true);
const inputDisabled = this.state.processingSubmission; kg.generateWallet({
const processingSpinner = (!this.state.processingSubmission) ? null : ticket,
<LoadingSpinner/> ship: parseInt(patp2dec('~' + window.ship)),
}).then((urbitWallet) => {
const { xpub: xpubFromWallet } =
network === 'testnet'
? urbitWallet.bitcoinTestnet.keys
: urbitWallet.bitcoinMainnet.keys;
if (this.state.mode === 'masterTicket'){ submitXPub(xpubFromWallet);
return ( });
<Box };
width="100%"
height="100%" const buttonDisabled = !readyToSubmit || processingSubmission;
padding={3} const inputDisabled = processingSubmission;
> // const processingSpinner = !processingSubmission ? null : <LoadingSpinner />;
<Row>
<Icon icon="Bitcoin" mr={2}/> if (mode === 'masterTicket') {
<Text fontSize="14px" fontWeight="bold"> return (
Step 2 of 2: Import your extended public key <Box width="100%" height="100%" padding={3}>
</Text> <Row>
</Row> <Icon icon="Bitcoin" mr={2} />
<Row <Text fontSize="14px" fontWeight="bold">
mt={3} Step 2 of 2: Import your extended public key
alignItems='center' </Text>
> </Row>
<Icon icon='Info' color='yellow' height={4} width={4}/> <Row mt={3} alignItems="center">
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}> <Icon icon="Info" color="yellow" height={4} width={4} />
We recommend that you import your wallet using Bridge to protect your master ticket. <Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
</Text> We recommend that you import your wallet using Bridge to protect
</Row> your master ticket.
<Box </Text>
display='flex' </Row>
alignItems='center' <Box display="flex" alignItems="center" mt={3} mb={2}>
mt={3} {confirmingMasterTicket && (
mb={2}> <Icon
{this.state.confirmingMasterTicket && icon="ArrowWest"
<Icon cursor="pointer"
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"}}
onClick={() => { onClick={() => {
if (!this.state.confirmingMasterTicket) { setConfirmingMasterTicket(false);
this.setState({confirmingMasterTicket: true}); setMasterTicket('');
} else { setConfirmedMasterTicket('');
if (this.state.masterTicket === this.state.confirmedMasterTicket) { setError(false);
this.setState({error: false});
this.submitMasterTicket(this.state.masterTicket);
} else {
this.setState({error: true});
}
}
}} }}
/> />
</Row> )}
<Text fontSize="14px" fontWeight="500">
{confirmingMasterTicket ? 'Confirm Master Ticket' : 'Master Ticket'}
</Text>
</Box> </Box>
); <Row alignItems="center">
} 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>
<StatelessTextInput <StatelessTextInput
value={this.state.xpub} mr={2}
width="256px"
value={
confirmingMasterTicket ? confirmedMasterTicket : masterTicket
}
disabled={inputDisabled} disabled={inputDisabled}
fontSize="14px" fontSize="14px"
type="password" type="password"
name="xpub" name="masterTicket"
obscure={(value) => value.replace(/[^~-]+/g, '••••••')}
placeholder="••••••-••••••-••••••-••••••"
autoCapitalize="none" autoCapitalize="none"
autoCorrect="off" autoCorrect="off"
onChange={this.checkXPub} onChange={(e) => checkTicket(e)}
/> />
<Box mt={3} mb={3}> {!inputDisabled ? null : <LoadingSpinner />}
<Text fontSize="14px" fontWeight="regular" </Row>
color={(inputDisabled) ? "lighterGray" : "gray"} {error && (
style={{cursor: (inputDisabled) ? "default" : "pointer"}} <Row mt={2}>
onClick={() => { <Text fontSize="14px" color="red">
if (inputDisabled) return; Master tickets do not match
this.setState({mode: 'masterTicket', xpub: '', masterTicket: '', readyToSubmit: false})
}}
>
Import using master ticket ->
</Text> </Text>
</Box> </Row>
)}
<Row mt={3}>
<Button <Button
primary primary
mt={3} color="black"
disabled={buttonDisabled} backgroundColor="veryLightGray"
children="Next Step" borderColor="veryLightGray"
fontSize="14px" fontSize="14px"
style={{cursor: this.state.ready ? "pointer" : "default"}} mr={2}
onClick={() => { this.submitXPub(this.state.xpub) }} 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>
); <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 -&gt;
</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;

View File

@ -1,79 +1,72 @@
import React, { Component } from 'react'; import React from 'react';
import { import { Box, Text, Button, Col, Anchor } from '@tlon/indigo-react';
Box, import { api } from '../../api';
Icon, import { useSettings } from '../../hooks/useSettings';
Row,
Text,
Button,
Col,
Anchor,
} from '@tlon/indigo-react';
import { store } from '../../store' const Warning = () => {
const { setWarning } = useSettings();
const understand = () => {
setWarning(false);
export default class Warning extends Component {
constructor(props) {
super(props);
this.understand = this.understand.bind(this);
}
understand(){
store.handleEvent({ data: { bucket: { warning: false}}});
let removeWarning = { let removeWarning = {
"put-entry": { 'put-entry': {
value: false, value: false,
"entry-key": "warning", 'entry-key': 'warning',
"bucket-key": "btc-wallet", 'bucket-key': 'btc-wallet',
} },
} };
this.props.api.settingsEvent(removeWarning); api.settingsEvent(removeWarning);
} };
render() { return (
return ( <Box
<Box backgroundColor="red"
backgroundColor="red" color="white"
color="white" borderRadius="32px"
borderRadius="32px" justifyContent="space-between"
justifyContent="space-between" width="100%"
width='100%' p={5}
p={5} mb={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&apos;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> I understand
<Text color="white" fontWeight='bold' fontSize={1}> </Button>
Warning! </Box>
</Text> );
<br/> };
<Text color="white" fontWeight='bold' fontSize={1}>
Be safe while using this wallet, and be sure to store responsible amounts export default Warning;
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>
);
}
}

View File

@ -1,67 +1,39 @@
import React, { Component } from 'react'; import React from 'react';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { api } from '../api.js';
import { store } from '../store.js';
import { ThemeProvider } from 'styled-components'; import { ThemeProvider } from 'styled-components';
import light from './themes/light'; import light from './themes/light';
// import dark from './themes/dark';
import { Box, Reset } from '@tlon/indigo-react'; import { Box, Reset } from '@tlon/indigo-react';
import StartupModal from './lib/startupModal.js'; import StartupModal from './lib/startupModal.js';
import Body from './lib/body.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 { return (
constructor(props) { <BrowserRouter>
super(props); <ThemeProvider theme={light}>
this.ship = window.ship; <Reset />
this.state = store.state; {loaded ? <StartupModal /> : null}
store.setStateHandler(this.setState.bind(this)); <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() { export default Root;
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>
);
}
}

View File

@ -1,21 +1,17 @@
import _ from 'lodash';
import classnames from 'classnames';
export function uuid() { export function uuid() {
let str = "0v" let str = '0v';
str += Math.ceil(Math.random()*8)+"." str += Math.ceil(Math.random() * 8) + '.';
for (var i = 0; i < 5; i++) { for (var i = 0; i < 5; i++) {
let _str = Math.ceil(Math.random()*10000000).toString(32); let _str = Math.ceil(Math.random() * 10000000).toString(32);
_str = ("00000"+_str).substr(-5,5); _str = ('00000' + _str).substr(-5, 5);
str += _str+"."; str += _str + '.';
} }
return str.slice(0,-1); return str.slice(0, -1);
} }
export function isPatTa(str) { export function isPatTa(str) {
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str) const r = /^[a-z,0-9,\-,.,_,~]+$/.exec(str);
return !!r; return !!r;
} }
@ -26,13 +22,15 @@ export function isPatTa(str) {
(javascript Date object) (javascript Date object)
*/ */
export function daToDate(st) { export function daToDate(st) {
var dub = function(n) { var dub = function (n) {
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString(); return parseInt(n) < 10 ? '0' + parseInt(n) : n.toString();
}; };
var da = st.split('..'); var da = st.split('..');
var bigEnd = da[0].split('.'); var bigEnd = da[0].split('.');
var lilEnd = da[1].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); return new Date(ds);
} }
@ -44,18 +42,18 @@ export function daToDate(st) {
*/ */
export function dateToDa(d, mil) { export function dateToDa(d, mil) {
  var fil = function(n) { var fil = function (n) {
    return n >= 10 ? n : "0" + n; return n >= 10 ? n : '0' + n;
  }; };
  return ( return (
    `~${d.getUTCFullYear()}.` + `~${d.getUTCFullYear()}.` +
    `${(d.getUTCMonth() + 1)}.` + `${d.getUTCMonth() + 1}.` +
    `${fil(d.getUTCDate())}..` + `${fil(d.getUTCDate())}..` +
    `${fil(d.getUTCHours())}.` + `${fil(d.getUTCHours())}.` +
    `${fil(d.getUTCMinutes())}.` + `${fil(d.getUTCMinutes())}.` +
    `${fil(d.getUTCSeconds())}` + `${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}` `${mil ? '..0000' : ''}`
  ); );
} }
export function deSig(ship) { export function deSig(ship) {
@ -64,49 +62,59 @@ export function deSig(ship) {
// trim patps to match dojo, chat-cli // trim patps to match dojo, chat-cli
export function cite(ship) { export function cite(ship) {
let patp = ship, shortened = ""; let patp = ship,
if (patp.startsWith("~")) { shortened = '';
if (patp.startsWith('~')) {
patp = patp.substr(1); patp = patp.substr(1);
} }
// comet // comet
if (patp.length === 56) { if (patp.length === 56) {
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56); shortened = '~' + patp.slice(0, 6) + '_' + patp.slice(50, 56);
return shortened; return shortened;
} }
// moon // moon
if (patp.length === 27) { 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 shortened;
} }
return `~${patp}`; return `~${patp}`;
} }
export function satsToCurrency(sats, denomination, rates){ export function satsToCurrency(sats, denomination, rates) {
if (!rates) { if (!rates) {
throw "nonexistent currency table" throw 'nonexistent currency table';
} }
if (!rates[denomination]){ if (!rates[denomination]) {
denomination = "BTC"; denomination = 'BTC';
} }
let rate = rates[denomination]; 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; let text;
if (denomination === 'BTC'){ if (denomination === 'BTC' && rate) {
text = val + ' ' + rate.symbol text = val + ' ' + rate.symbol;
} else { } else if (rate) {
text = rate.symbol + val.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,') 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) { if (!rates) {
throw "nonexistent currency table" throw 'nonexistent currency table';
} }
if (!rates[denomination]){ if (!rates[denomination]) {
throw 'currency not in table' throw 'currency not in table';
} }
let rate = rates[denomination]; let rate = rates[denomination];
let sats = (parseFloat(val) / rate.last) * 100000000; let sats = (parseFloat(val) / rate.last) * 100000000;
return sats; 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;
});
}

View File

@ -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
}
}
}
}

View File

@ -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)
})
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;