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,84 +1,63 @@
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 sats = this.props.state.confirmedBalance || 0;
const unconfirmedSats = this.props.state.unconfirmedBalance;
const unconfirmedString = unconfirmedSats ? ` (${unconfirmedSats}) ` : ''; const unconfirmedString = unconfirmedSats ? ` (${unconfirmedSats}) ` : '';
const denomination = this.props.state.denomination; const value = satsToCurrency(sats, denomination, currencyRates);
const value = satsToCurrency(
sats,
denomination,
this.props.state.currencyRates
);
const sendDisabled = sats === 0; const sendDisabled = sats === 0;
const addressText = const addressText =
this.props.state.address === null address === null ? '' : address.slice(0, 6) + '...' + address.slice(-6);
? ''
: this.props.state.address.slice(0, 6) +
'...' +
this.props.state.address.slice(-6);
const conversion = this.props.state.currencyRates[denomination].last; const conversion = currencyRates[denomination]?.last;
return ( return (
<> <>
{this.state.sending ? ( {sending ? (
<Send <Send
state={this.props.state}
api={this.props.api}
psbt={this.props.state.psbt}
fee={this.props.state.fee}
currencyRates={this.props.state.currencyRates}
shipWallets={this.props.state.shipWallets}
value={value} value={value}
denomination={denomination}
sats={sats}
conversion={conversion} conversion={conversion}
network={this.props.network}
error={this.props.state.error}
stopSending={() => { stopSending={() => {
this.setState({ sending: false }); setSending(false);
store.handleEvent({ setPsbt('');
data: { psbt: '', fee: 0, error: '', 'broadcast-fail': null }, setFee(0);
}); setError('');
}} }}
/> />
) : ( ) : (
@ -100,17 +79,11 @@ export default class Balance extends Component {
fontSize="14px" fontSize="14px"
mono mono
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => copyAddress('string')}
this.copyAddress('string');
}}
> >
{this.state.copiedString ? 'copied' : addressText} {copiedString ? 'copied' : addressText}
</Text> </Text>
<CurrencyPicker <CurrencyPicker />
api={this.props.api}
denomination={denomination}
currencies={this.props.state.currencyRates}
/>
</Row> </Row>
<Col justifyContent="center" alignItems="center"> <Col justifyContent="center" alignItems="center">
<Text <Text
@ -136,35 +109,32 @@ export default class Balance extends Component {
borderColor="none" borderColor="none"
borderRadius="24px" borderRadius="24px"
height="48px" height="48px"
onClick={() => this.setState({ sending: true })} onClick={() => setSending(true)}
> >
Send Send
</Button> </Button>
<Button <Button
mr={3} mr={3}
disabled={this.state.copiedButton} disabled={copiedButton}
fontSize={1} fontSize={1}
fontWeight="bold" fontWeight="bold"
color={this.state.copiedButton ? 'green' : 'orange'} color={copiedButton ? 'green' : 'orange'}
backgroundColor={ backgroundColor={copiedButton ? 'veryLightGreen' : 'midOrange'}
this.state.copiedButton ? 'veryLightGreen' : 'midOrange'
}
style={{ style={{
cursor: this.state.copiedButton ? 'default' : 'pointer', cursor: copiedButton ? 'default' : 'pointer',
}} }}
borderColor="none" borderColor="none"
borderRadius="24px" borderRadius="24px"
height="48px" height="48px"
onClick={() => { onClick={() => copyAddress('button')}
this.copyAddress('button');
}}
> >
{this.state.copiedButton ? 'Address Copied!' : 'Copy Address'} {copiedButton ? 'Address Copied!' : 'Copy Address'}
</Button> </Button>
</Row> </Row>
</Col> </Col>
)} )}
</> </>
); );
} };
}
export default Balance;

View File

@ -1,34 +1,24 @@
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,
Row,
Text,
LoadingSpinner,
Col,
} from '@tlon/indigo-react';
import {
Switch,
Route,
} from 'react-router-dom';
import Balance from './balance.js'; 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
render() { display="flex"
width="100%"
const cardWidth = window.innerWidth <= 475 ? '350px' : '400px' height="100%"
alignItems="center"
if (!this.props.loaded) { justifyContent="center"
return ( >
<Box display="flex" width="100%" height="100%" alignItems="center" justifyContent="center">
<LoadingSpinner <LoadingSpinner
width={7} width={7}
height={7} height={7}
@ -36,37 +26,24 @@ export default class Body extends Component {
foreground="orange" foreground="orange"
/> />
</Box> </Box>
); ) : (
} else {
return (
<Switch> <Switch>
<Route path="/~btc/settings"> <Route path="/~btc/settings">
<Col <Col display="flex" flexDirection="column" width={cardWidth}>
display='flex' <Header settings={true} />
flexDirection='column' <Settings />
width={cardWidth}
>
<Header settings={true} state={this.props.state}/>
<Settings state={this.props.state}
api={this.props.api}
network={this.props.network}
/>
</Col> </Col>
</Route> </Route>
<Route path="/~btc"> <Route path="/~btc">
<Col <Col display="flex" flexDirection="column" width={cardWidth}>
display='flex' <Header settings={false} />
flexDirection='column' {!warning ? null : <Warning />}
width={cardWidth} <Balance />
> <Transactions />
<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> </Col>
</Route> </Route>
</Switch> </Switch>
); );
} };
}
} export default Body;

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,102 +9,66 @@ 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, if (error !== '') {
setLocalError(error);
}
}, [error, broadcasting, setBroadcasting]);
useEffect(() => {
window.open('https://bridge.urbit.org/?kind=btc&utx=' + psbt);
});
const broadCastTx = (hex) => {
let command = {
'broadcast-tx': hex,
};
return api.btcWalletCommand(command);
}; };
this.checkTxHex = this.checkTxHex.bind(this); const sendBitcoin = (hex) => {
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 { try {
bitcoin.Transaction.fromHex(hex) bitcoin.Transaction.fromHex(hex);
this.broadCastTx(hex) broadCastTx(hex);
this.setState({broadcasting: true}); setBroadcasting(true);
} catch (e) {
setLocalError('invalid-signed');
setBroadcasting(false);
} }
};
catch(e) { const checkTxHex = (e) => {
this.setState({error: 'invalid-signed', broadcasting: false}); setTxHex(e.target.value);
} setReady(txHex.length > 0);
} setLocalError('');
};
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 inputColor = 'black';
let inputBg = 'white'; let inputBg = 'white';
let inputBorder = 'lightGray'; let inputBorder = 'lightGray';
if (error !== '') { if (localError !== '') {
inputColor = 'red'; inputColor = 'red';
inputBg = 'veryLightRed'; inputBg = 'veryLightRed';
inputBorder = 'red'; inputBorder = 'red';
@ -112,128 +76,136 @@ export default class BridgeInvoice extends Component {
const isShip = isValidPatp(payee); const isShip = isValidPatp(payee);
const icon = (isShip) const icon = isShip ? (
? <Sigil ship={payee} size={24} color="black" classes={''} icon padding={5}/> <Sigil ship={payee} size={24} color="black" classes={''} icon padding={5} />
: <Box backgroundColor="lighterGray" ) : (
<Box
backgroundColor="lighterGray"
width="24px" width="24px"
height="24px" height="24px"
textAlign="center" textAlign="center"
alignItems="center" alignItems="center"
borderRadius="2px" borderRadius="2px"
p={1} p={1}
><Icon icon="Bitcoin" color="gray"/></Box>; >
<Icon icon="Bitcoin" color="gray" />
</Box>
);
return ( return (
<> <>
{ this.props.state.broadcastSuccess ? {broadcastSuccess ? (
<Sent <Sent payee={payee} stopSending={stopSending} satsAmount={satsAmount} />
payee={payee} ) : (
stopSending={stopSending}
denomination={denomination}
currencyRates={currencyRates}
satsAmount={satsAmount}
/> :
<Col <Col
ref={this.setInvoiceRef} ref={invoiceRef}
width='100%' width="100%"
backgroundColor='white' backgroundColor="white"
borderRadius='48px' borderRadius="48px"
mb={5} mb={5}
p={5} p={5}
> >
<Col <Col
p={5} p={5}
mt={4} mt={4}
backgroundColor='veryLightGreen' backgroundColor="veryLightGreen"
borderRadius='24px' borderRadius="24px"
alignItems="center" alignItems="center"
> >
<Row> <Row>
<Text <Text color="green" fontSize="40px">
color='green' {satsToCurrency(satsAmount, denomination, currencyRates)}
fontSize='40px' </Text>
>{satsToCurrency(satsAmount, denomination, currencyRates)}</Text>
</Row> </Row>
<Row> <Row>
<Text <Text
fontWeight="bold" fontWeight="bold"
fontSize='16px' fontSize="16px"
color='midGreen' color="midGreen"
>{`${satsAmount} sats`}</Text> >{`${satsAmount} sats`}</Text>
</Row> </Row>
<Row mt={2}> <Row mt={2}>
<Text <Text fontSize="14px" color="midGreen">{`Fee: ${satsToCurrency(
fontSize='14px' fee,
color='midGreen' denomination,
>{`Fee: ${satsToCurrency(fee, denomination, currencyRates)} (${fee} sats)`}</Text> currencyRates
)} (${fee} sats)`}</Text>
</Row> </Row>
<Row mt={4}> <Row mt={4}>
<Text fontSize='16px' fontWeight="bold" color="gray">You are paying</Text> <Text fontSize="16px" fontWeight="bold" color="gray">
You are paying
</Text>
</Row> </Row>
<Row mt={2} alignItems="center"> <Row mt={2} alignItems="center">
{icon} {icon}
<Text ml={2} <Text
ml={2}
mono mono
color="gray" color="gray"
fontSize='14px' fontSize="14px"
style={{'display': 'block', 'overflow-wrap': 'anywhere'}} style={{ display: 'block', 'overflow-wrap': 'anywhere' }}
>{payee}</Text> >
{payee}
</Text>
</Row> </Row>
</Col> </Col>
<Box mt={3}> <Box mt={3}>
<Text fontSize='14px' fontWeight='500'> <Text fontSize="14px" fontWeight="500">
Bridge signed transaction Bridge signed transaction
</Text> </Text>
</Box> </Box>
<Box mt={1} mb={2}> <Box mt={1} mb={2}>
<Text gray fontSize='14px'> <Text gray fontSize="14px">
Copy the signed transaction from Bridge Copy the signed transaction from Bridge
</Text> </Text>
</Box> </Box>
<Input <Input
value={this.state.txHex} value={txHex}
fontSize='14px' fontSize="14px"
placeholder='010000000001019e478cc370323ac539097...' placeholder="010000000001019e478cc370323ac539097..."
autoCapitalize='none' autoCapitalize="none"
autoCorrect='off' autoCorrect="off"
color={inputColor} color={inputColor}
backgroundColor={inputBg} backgroundColor={inputBg}
borderColor={inputBorder} borderColor={inputBorder}
style={{ 'line-height': '4' }} style={{ 'line-height': '4' }}
onChange={this.checkTxHex} onChange={(e) => checkTxHex(e)}
/> />
{ (error !== '') && {localError !== '' && (
<Row> <Row>
<Error <Error error={localError} fontSize="14px" mt={2} />
error={error}
fontSize='14px'
mt={2}/>
</Row> </Row>
} )}
<Row <Row flexDirection="row-reverse" mt={4} alignItems="center">
flexDirection='row-reverse'
mt={4}
alignItems="center"
>
<Button <Button
primary primary
children='Send BTC'
mr={3} mr={3}
fontSize={1} fontSize={1}
borderRadius='24px' borderRadius="24px"
border='none' border="none"
height='48px' height="48px"
onClick={() => this.sendBitcoin(txHex)} onClick={() => sendBitcoin(txHex)}
disabled={!this.state.ready || error || this.state.broadcasting} disabled={!ready || localError || broadcasting}
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"} color={
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"} ready && !localError && !broadcasting ? 'white' : 'lighterGray'
style={{cursor: (this.state.ready && !error) ? "pointer" : "default"}} }
/> backgroundColor={
{this.state.broadcasting ? <LoadingSpinner mr={3}/> : null} ready && !localError && !broadcasting
? 'green'
: 'veryLightGray'
}
style={{
cursor: ready && !localError ? 'pointer' : 'default',
}}
>
Send BTC
</Button>
{broadcasting ? <LoadingSpinner mr={3} /> : null}
</Row> </Row>
</Col> </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);
} };
render() {
return ( return (
<Row style={{cursor: "pointer"}} onClick={this.switchCurrency}> <Row style={{ cursor: 'pointer' }} onClick={() => switchCurrency()}>
<Icon icon="ChevronDouble" color="orange" pt="2px" pr={1} /> <Icon icon="ChevronDouble" color="orange" pt="2px" pr={1} />
<Text color="orange" fontSize={1}>{this.props.denomination}</Text> <Text color="orange" fontSize={1}>
{denomination}
</Text>
</Row> </Row>
); );
} };
}
export default CurrencyPicker;

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)}
return(
<Text
color='red'
{...props}>
{error}
</Text> </Text>
); );
}
export default Error;

View File

@ -1,59 +1,53 @@
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() {
document.removeEventListener("click", this.clickDismiss);
}
setModalRef(n) {
this.modalRef = n;
}
clickDismiss(e) {
if (this.modalRef && !(this.modalRef.contains(e.target))){
this.props.feeDismiss();
}
}
select(which) {
this.setState({selected: which});
this.props.feeSelect(which);
}
render() {
return ( return (
<Box <Box
ref={this.setModalRef} // ref={modalRef}
position="absolute" p={4} // onClick={() => feeDismiss()}
border="1px solid green" zIndex={10} position="absolute"
backgroundColor="white" borderRadius={3} p={4}
border="1px solid green"
zIndex={10}
backgroundColor="white"
borderRadius={3}
> >
<Text fontSize={1} color="black" fontWeight="bold" mb={4}> <Text fontSize={1} color="black" fontWeight="bold" mb={4}>
Transaction Speed Transaction Speed
@ -61,39 +55,45 @@ export default class FeePicker extends Component {
<Col mt={4}> <Col mt={4}>
<RadioButton <RadioButton
name="feeRadio" name="feeRadio"
selected={this.state.selected === 'low'} selected={feeSelected === feeLevels.low}
p="2" p="2"
onChange={() => { onChange={() => {
this.select('low'); select('low');
}} }}
> >
<Label fontSize="14px">Slow: {this.props.feeChoices.low[1]} sats/vbyte ~{this.props.feeChoices.low[0]}m</Label> <Label fontSize="14px">
Slow: {feeChoices.low[1]} sats/vbyte ~{feeChoices.low[0]}m
</Label>
</RadioButton> </RadioButton>
<RadioButton <RadioButton
name="feeRadio" name="feeRadio"
selected={this.state.selected === 'mid'} selected={feeSelected === feeLevels.mid}
p="2" p="2"
onChange={() => { onChange={() => {
this.select('mid'); select('mid');
}} }}
> >
<Label fontSize="14px">Normal: {this.props.feeChoices.mid[1]} sats/vbyte ~{this.props.feeChoices.mid[0]}m</Label> <Label fontSize="14px">
Normal: {feeChoices.mid[1]} sats/vbyte ~{feeChoices.mid[0]}m
</Label>
</RadioButton> </RadioButton>
<RadioButton <RadioButton
name="feeRadio" name="feeRadio"
selected={this.state.selected === 'high'} selected={feeSelected === feeLevels.high}
p="2" p="2"
onChange={() => { onChange={() => {
this.select('high'); select('high');
}} }}
> >
<Label fontSize="14px">Fast: {this.props.feeChoices.high[1]} sats/vbyte ~{this.props.feeChoices.high[0]}m</Label> <Label fontSize="14px">
Fast: {feeChoices.high[1]} sats/vbyte ~{feeChoices.high[0]}m
</Label>
</RadioButton> </RadioButton>
</Col> </Col>
</Box> </Box>
); );
} };
}
export default FeePicker;

View File

@ -1,51 +1,52 @@
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';
render() {
let icon = this.props.settings ? "X" : "Adjust";
let iconColor = this.props.settings ? "black" : "orange";
let iconLink = this.props.settings ? "/~btc" : "/~btc/settings";
let connection = null; let connection = null;
let badge = null; let badge = null;
if (!(this.props.state.provider && this.props.state.provider.connected)) { if (!(provider && provider.connected)) {
connection = connection = (
<Text fontSize={1} color="red" fontWeight="bold" mr={3}> <Text fontSize={1} color="red" fontWeight="bold" mr={3}>
Provider Offline Provider Offline
</Text> </Text>
);
if (!this.props.settings) { if (!settings) {
badge = <Box borderRadius="50%" width="8px" height="8px" backgroundColor="red" position="absolute" top="0px" right="0px"></Box> badge = (
<Box
borderRadius="50%"
width="8px"
height="8px"
backgroundColor="red"
position="absolute"
top="0px"
right="0px"
></Box>
);
} }
} }
return ( return (
<Row <Row
height={8} height={8}
width='100%' width="100%"
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
pt={5} pt={5}
pb={5} pb={5}
> >
<Row alignItems="center" justifyContent="center"> <Row alignItems="center" justifyContent="center">
<Box backgroundColor="orange" <Box
borderRadius={4} mr="12px" backgroundColor="orange"
borderRadius={4}
mr="12px"
width={5} width={5}
height={5} height={5}
alignItems="center" alignItems="center"
@ -60,7 +61,8 @@ export default class Header extends Component {
<Row alignItems="center"> <Row alignItems="center">
{connection} {connection}
<Link to={iconLink}> <Link to={iconLink}>
<Box backgroundColor="white" <Box
backgroundColor="white"
borderRadius={4} borderRadius={4}
width={5} width={5}
height={5} height={5}
@ -74,5 +76,6 @@ export default class Header extends Component {
</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,74 +43,65 @@ 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, }, [error, broadcasting, setBroadcasting]);
broadcasting: false,
const clickDismiss = (e) => {
if (invoiceRef && !invoiceRef.contains(e.target)) {
stopSending();
}
}; };
this.checkTicket = this.checkTicket.bind(this); useEffect(() => {
this.broadCastTx = this.broadCastTx.bind(this); document.addEventListener('click', clickDismiss);
this.sendBitcoin = this.sendBitcoin.bind(this); return () => document.removeEventListener('click', clickDismiss);
this.clickDismiss = this.clickDismiss.bind(this); }, []);
this.setInvoiceRef = this.setInvoiceRef.bind(this);
}
componentDidMount(){ const broadCastTx = (psbtHex) => {
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, prevState) {
if (this.state.broadcasting) {
if (this.state.error !== '') {
this.setState({broadcasting: false});
}
}
}
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);
@ -131,28 +119,20 @@ export default class Invoice extends Component {
.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}); };
}
render() {
const broadcastSuccess = this.props.state.broadcastSuccess;
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates, fee } = this.props;
const { sent, error } = this.state;
let inputColor = 'black'; let inputColor = 'black';
let inputBg = 'white'; let inputBg = 'white';
@ -166,126 +146,131 @@ export default class Invoice extends Component {
const isShip = isValidPatp(payee); const isShip = isValidPatp(payee);
const icon = (isShip) const icon = isShip ? (
? <Sigil ship={payee} size={24} color="black" classes={''} icon padding={5}/> <Sigil ship={payee} size={24} color="black" classes={''} icon padding={5} />
: <Box backgroundColor="lighterGray" ) : (
<Box
backgroundColor="lighterGray"
width="24px" width="24px"
height="24px" height="24px"
textAlign="center" textAlign="center"
alignItems="center" alignItems="center"
borderRadius="2px" borderRadius="2px"
p={1} p={1}
><Icon icon="Bitcoin" color="gray"/></Box>; >
<Icon icon="Bitcoin" color="gray" />
</Box>
);
return ( return (
<> <>
{ broadcastSuccess ? {broadcastSuccess ? (
<Sent <Sent payee={payee} stopSending={stopSending} satsAmount={satsAmount} />
payee={payee} ) : (
stopSending={stopSending}
denomination={denomination}
currencyRates={currencyRates}
satsAmount={satsAmount}
/> :
<Col <Col
ref={this.setInvoiceRef} ref={invoiceRef}
width='100%' width="100%"
backgroundColor='white' backgroundColor="white"
borderRadius='48px' borderRadius="48px"
mb={5} mb={5}
p={5} p={5}
onClick={() => stopSending()}
> >
<Col <Col
p={5} p={5}
mt={4} mt={4}
backgroundColor='veryLightGreen' backgroundColor="veryLightGreen"
borderRadius='24px' borderRadius="24px"
alignItems="center" alignItems="center"
> >
<Row> <Row>
<Text <Text color="green" fontSize="40px">
color='green' {satsToCurrency(satsAmount, denomination, currencyRates)}
fontSize='40px' </Text>
>{satsToCurrency(satsAmount, denomination, currencyRates)}</Text>
</Row> </Row>
<Row> <Row>
<Text <Text
fontWeight="bold" fontWeight="bold"
fontSize='16px' fontSize="16px"
color='midGreen' color="midGreen"
>{`${satsAmount} sats`}</Text> >{`${satsAmount} sats`}</Text>
</Row> </Row>
<Row mt={2}> <Row mt={2}>
<Text <Text fontSize="14px" color="midGreen">{`Fee: ${satsToCurrency(
fontSize='14px' fee,
color='midGreen' denomination,
>{`Fee: ${satsToCurrency(fee, denomination, currencyRates)} (${fee} sats)`}</Text> currencyRates
)} (${fee} sats)`}</Text>
</Row> </Row>
<Row mt={4}> <Row mt={4}>
<Text fontSize='16px' fontWeight="bold" color="gray">You are paying</Text> <Text fontSize="16px" fontWeight="bold" color="gray">
You are paying
</Text>
</Row> </Row>
<Row mt={2} alignItems="center"> <Row mt={2} alignItems="center">
{icon} {icon}
<Text ml={2} <Text
ml={2}
mono mono
color="gray" color="gray"
fontSize='14px' fontSize="14px"
style={{'display': 'block', 'overflow-wrap': 'anywhere'}} style={{ display: 'block', 'overflow-wrap': 'anywhere' }}
>{payee}</Text> >
{payee}
</Text>
</Row> </Row>
</Col> </Col>
<Row mt={3} mb={2} alignItems="center"> <Row mt={3} mb={2} alignItems="center">
<Text gray fontSize={1} fontWeight='600' mr={4}> <Text gray fontSize={1} fontWeight="600" mr={4}>
Ticket Ticket
</Text> </Text>
<Input <Input
value={this.state.masterTicket} value={masterTicket}
fontSize="14px" fontSize="14px"
type="password" type="password"
name="masterTicket" name="masterTicket"
obscure={value => value.replace(/[^~-]+/g, '••••••')} obscure={(value) => value.replace(/[^~-]+/g, '••••••')}
placeholder="••••••-••••••-••••••-••••••" placeholder="••••••-••••••-••••••-••••••"
autoCapitalize="none" autoCapitalize="none"
autoCorrect="off" autoCorrect="off"
color={inputColor} color={inputColor}
backgroundColor={inputBg} backgroundColor={inputBg}
borderColor={inputBorder} borderColor={inputBorder}
onChange={this.checkTicket} onChange={() => checkTicket()}
/> />
</Row> </Row>
{(error !== '') && {error !== '' && (
<Row> <Row>
<Error <Error fontSize="14px" color="red" error={error} mt={2} />
fontSize='14px'
color='red'
error={error}
mt={2}/>
</Row> </Row>
} )}
<Row <Row flexDirection="row-reverse" mt={4} alignItems="center">
flexDirection='row-reverse'
mt={4}
alignItems="center"
>
<Button <Button
primary primary
children='Send BTC'
mr={3} mr={3}
fontSize={1} fontSize={1}
border="none" border="none"
borderRadius='24px' borderRadius="24px"
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"} color={ready && !error && !broadcasting ? 'white' : 'lighterGray'}
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"} backgroundColor={
height='48px' ready && !error && !broadcasting ? 'green' : 'veryLightGray'
onClick={() => this.sendBitcoin(this.state.masterTicket, psbt)} }
disabled={!this.state.ready || error || this.state.broadcasting} height="48px"
style={{cursor: (this.state.ready && !error && !this.state.broadcasting) ? "pointer" : "default"}} onClick={() => sendBitcoin(masterTicket, psbt)}
/> disabled={!ready || error || broadcasting}
{ (this.state.broadcasting) ? <LoadingSpinner mr={3}/> : null} style={{
cursor:
ready && !error && !broadcasting ? 'pointer' : 'default',
}}
>
Send BTC
</Button>
{broadcasting ? <LoadingSpinner mr={3} /> : null}
</Row> </Row>
</Col> </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,90 +8,82 @@ 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',
this.state = { initial: '',
potentialProvider: null,
checkingProvider: false,
providerFailed: false,
ready: false,
provider: null,
connecting: false,
}; };
this.checkProvider = this.checkProvider.bind(this); const ProviderModal = () => {
this.submitProvider = this.submitProvider.bind(this); const { providerPerms } = useSettings();
} const [providerStatus, setProviderStatus] = useState(
providerStatuses.initial
);
const [potentialProvider, setPotentialProvider] = useState(null);
const [provider, setProvider] = useState(null);
const [connecting, setConnecting] = useState(false);
checkProvider(e) { const 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() {
if (!this.state.ready) {
if (this.props.providerPerms[this.state.provider]) {
this.setState({
ready: true,
checkingProvider: false,
providerFailed: false,
});
}
}
}
submitProvider() {
if (this.state.ready) {
let command = {
'set-provider': this.state.provider,
}; };
this.props.api.btcWalletCommand(command);
this.setState({ connecting: true });
}
}
render() { const submitProvider = () => {
if (providerStatus === providerStatuses.ready) {
let command = {
'set-provider': provider,
};
api.btcWalletCommand(command);
setConnecting(true);
}
};
useEffect(() => {
if (providerStatus !== providerStatuses.ready) {
if (providerPerms.provider === provider && providerPerms.permitted) {
setProviderStatus(providerStatuses.ready);
}
}
}, [providerStatus, providerPerms, provider, setProviderStatus]);
let workingNode = null; let workingNode = null;
let workingColor = null; let workingColor = null;
let workingBg = null; let workingBg = null;
if (this.state.ready) { if (providerStatus === providerStatuses.ready) {
workingColor = 'green'; workingColor = 'green';
workingBg = 'veryLightGreen'; workingBg = 'veryLightGreen';
workingNode = ( workingNode = (
<Box mt={3}> <Box mt={3}>
<Text fontSize="14px" color="green"> <Text fontSize="14px" color="green">
{this.state.provider} is a working provider node {provider} is a working provider node
</Text> </Text>
</Box> </Box>
); );
} else if (this.state.providerFailed) { } else if (providerStatus === providerStatuses.failed) {
workingColor = 'red'; workingColor = 'red';
workingBg = 'veryLightRed'; workingBg = 'veryLightRed';
workingNode = ( workingNode = (
<Box mt={3}> <Box mt={3}>
<Text fontSize="14px" color="red"> <Text fontSize="14px" color="red">
{this.state.potentialProvider} is not a working provider node {potentialProvider} is not a working provider node
</Text> </Text>
</Box> </Box>
); );
@ -107,9 +99,9 @@ export default class ProviderModal extends Component {
</Row> </Row>
<Box mt={3}> <Box mt={3}>
<Text fontSize="14px" fontWeight="regular" color="gray"> <Text fontSize="14px" fontWeight="regular" color="gray">
In order to perform Bitcoin transaction in Landscape, you&apos;ll In order to perform Bitcoin transaction in Landscape, you&apos;ll need
need to set a provider node. A provider node is an urbit which to set a provider node. A provider node is an urbit which maintains a
maintains a synced Bitcoin ledger. synced Bitcoin ledger.
<a <a
fontSize="14px" fontSize="14px"
target="_blank" target="_blank"
@ -140,27 +132,33 @@ export default class ProviderModal extends Component {
backgroundColor={workingBg} backgroundColor={workingBg}
color={workingColor} color={workingColor}
borderColor={workingColor} borderColor={workingColor}
onChange={this.checkProvider} onChange={(e) => checkProvider(e)}
/> />
{this.state.checkingProvider ? <LoadingSpinner /> : null} {providerStatus === providerStatuses.checking ? (
<LoadingSpinner />
) : null}
</Row> </Row>
{workingNode} {workingNode}
<Row alignItems="center" mt={3}> <Row alignItems="center" mt={3}>
<Button <Button
mr={2} mr={2}
primary primary
disabled={!this.state.ready} disabled={providerStatus !== providerStatuses.ready}
fontSize="14px" fontSize="14px"
style={{ cursor: this.state.ready ? 'pointer' : 'default' }} style={{
cursor:
providerStatus === providerStatuses.ready ? 'pointer' : 'default',
}}
onClick={() => { onClick={() => {
this.submitProvider(this.state.provider); submitProvider(provider);
}} }}
> >
Set Peer Node Set Peer Node
</Button> </Button>
{this.state.connecting ? <LoadingSpinner /> : null} {connecting ? <LoadingSpinner /> : null}
</Row> </Row>
</Box> </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,
feeChoices: {
low: [10, 1], low: [10, 1],
mid: [10, 1], mid: [10, 1],
high: [10, 1], high: [10, 1],
}, });
feeValue: "mid", const [feeValue, setFeeValue] = useState('mid');
showModal: false, const [showModal, setShowModal] = useState(false);
note: '', const [note, setNote] = useState('');
choosingSignMethod: false, const [choosingSignMethod, setChoosingSignMethod] = useState(false);
signMethod: 'bridge', const [signMethod, setSignMethod] = useState('bridge');
const feeDismiss = () => {
setShowModal(false);
}; };
this.initPayment = this.initPayment.bind(this); const feeSelect = (which) => {
this.checkPayee = this.checkPayee.bind(this); setFeeValue(which);
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 handleSetSignMethod = (signMethod) => {
this.setState({showModal: false}); setSignMethod(signMethod);
} setChoosingSignMethod(false);
};
feeSelect(which) {
this.setState({feeValue: which});
}
componentDidMount(){
if (this.props.network === 'bitcoin'){
let url = "https://bitcoiner.live/api/fees/estimates/latest";
fetch(url).then(res => res.json()).then(n => {
let estimates = Object.keys(n.estimates);
let mid = Math.floor(estimates.length/2)
let high = estimates.length - 1;
this.setState({
feeChoices: {
high: [30, n.estimates[30]["sat_per_vbyte"]],
mid: [180, n.estimates[180]["sat_per_vbyte"]],
low: [360, n.estimates[360]["sat_per_vbyte"]],
}
});
})
}
}
setSignMethod(signMethod) {
this.setState({signMethod, choosingSignMethod: false});
}
checkPayee(e){
store.handleEvent({data: {error: ''}});
let payee = e.target.value;
let isPatp = ob.isValidPatp(payee);
let isAddress = validate(payee);
const 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,
},
};
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);
} }
} }
this.props.api.btcWalletCommand(command).then(res => this.setState({signing: 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";
}
const { api, value, conversion, stopSending, denomination, psbt, currencyRates, error, network, fee } = this.props;
const { denomAmount, satsAmount, signing, payee, choosingSignMethod, signMethod } = this.state;
const signReady = (this.state.ready && (parseInt(this.state.satsAmount) > 0)) && !signing;
let invoice = null; let invoice = null;
if (signMethod === 'masterTicket') { if (signMethod === 'masterTicket') {
invoice = invoice = (
<Invoice <Invoice
network={network}
api={api}
psbt={psbt}
fee={fee}
currencyRates={currencyRates}
stopSending={stopSending} stopSending={stopSending}
payee={payee} payee={payee}
denomination={denomination}
satsAmount={satsAmount} satsAmount={satsAmount}
state={this.props.state}
/> />
);
} else if (signMethod === 'bridge') { } else if (signMethod === 'bridge') {
invoice = invoice = (
<BridgeInvoice <BridgeInvoice
state={this.props.state}
api={api}
psbt={psbt}
fee={fee}
currencyRates={currencyRates}
stopSending={stopSending} stopSending={stopSending}
payee={payee} payee={payee}
denomination={denomination}
satsAmount={satsAmount} satsAmount={satsAmount}
/> />
);
} }
return ( return (
<> <>
{ (signing && psbt) ? invoice : {signing && psbt ? (
invoice
) : (
<Col <Col
width='100%' width="100%"
backgroundColor='white' backgroundColor="white"
borderRadius='48px' borderRadius="48px"
mb={5} mb={5}
p={5} p={5}
> >
<Col width="100%"> <Col width="100%">
<Row <Row justifyContent="space-between" alignItems="center">
justifyContent='space-between' <Text highlight color="blue" fontSize={1}>
alignItems='center' Send BTC
> </Text>
<Text highlight color='blue' fontSize={1}>Send BTC</Text> <Text highlight color="blue" fontSize={1}>
<Text highlight color='blue' fontSize={1}>{value}</Text> {value}
<Icon </Text>
icon='X' <Icon icon="X" cursor="pointer" onClick={() => stopSending()} />
cursor='pointer'
onClick={() => stopSending()}
/>
</Row> </Row>
<Row alignItems="center" mt={6} justifyContent="space-between">
<Row <Row
alignItems='center' justifyContent="space-between"
mt={6} width="calc(40% - 30px)"
justifyContent='space-between'> alignItems="center"
<Row justifyContent="space-between" width='calc(40% - 30px)' alignItems="center"> >
<Text gray fontSize={1} fontWeight='600'>To</Text> <Text gray fontSize={1} fontWeight="600">
{this.state.checkingPatp ? To
<LoadingSpinner background="midOrange" foreground="orange"/> : null </Text>
} {checkingPatp ? (
<LoadingSpinner background="midOrange" foreground="orange" />
) : null}
</Row> </Row>
<Input <Input
autoFocus // autoFocus
onFocus={() => {this.setState({focusPayee: true})}} onFocus={() => {
onBlur={() => {this.setState({focusPayee: false})}} setFocusedField(focusFields.payee);
}}
onBlur={() => {
setFocusedField(focusFields.empty);
}}
color={payeeColor} color={payeeColor}
backgroundColor={payeeBg} backgroundColor={payeeBg}
borderColor={payeeBorder} borderColor={payeeBorder}
ml={2} ml={2}
flexGrow="1" flexGrow="1"
fontSize='14px' fontSize="14px"
placeholder='~sampel-palnet or BTC address' placeholder="~sampel-palnet or BTC address"
value={payee} value={payee}
fontFamily="mono" fontFamily="mono"
disabled={signing} disabled={signing}
onChange={this.checkPayee} onChange={(e) => checkPayee(e)}
/> />
</Row> </Row>
{error && {error && (
<Row <Row alignItems="center" justifyContent="space-between">
alignItems='center'
justifyContent='space-between'>
{/* yes this is a hack */} {/* yes this is a hack */}
<Box width='calc(40% - 30px)'/> <Box width="calc(40% - 30px)" />
<Error <Error
error={error} error={error}
fontSize='14px' fontSize="14px"
ml={2} ml={2}
mt={2} mt={2}
width='100%' /> width="100%"
/>
</Row> </Row>
} )}
<Row <Row alignItems="center" mt={4} justifyContent="space-between">
alignItems='center' <Text gray fontSize={1} fontWeight="600" width="40%">
mt={4} Amount
justifyContent='space-between'> </Text>
<Text
gray
fontSize={1}
fontWeight='600'
width="40%"
>Amount</Text>
<Input <Input
onFocus={() => {this.setState({focusCurrency: true})}} onFocus={() => {
onBlur={() => {this.setState({focusCurrency: false})}} setFocusedField(focusFields.currency);
fontSize='14px' }}
width='100%' onBlur={() => {
type='number' setFocusedField(focusFields.empty);
borderColor={this.state.focusCurrency ? "lightGray" : "none"} }}
fontSize="14px"
width="100%"
type="number"
borderColor={
focusedField === focusFields.currency ? 'lightGray' : 'none'
}
disabled={signing} disabled={signing}
value={denomAmount} value={denomAmount}
onChange={e => { onChange={(e) => {
this.setState({ setDenomAmount(e.target.value);
denomAmount: e.target.value, setSatsAmount(
satsAmount: Math.round(parseFloat(e.target.value) / conversion * 100000000) Math.round(
}); (parseFloat(e.target.value) / conversion) * 100000000
)
);
}} }}
/> />
<Text color="lighterGray" fontSize={1} ml={3}>{denomination}</Text> <Text color="lighterGray" fontSize={1} ml={3}>
{denomination}
</Text>
</Row> </Row>
<Row <Row alignItems="center" mt={2} justifyContent="space-between">
alignItems='center'
mt={2}
justifyContent='space-between'>
{/* yes this is a hack */} {/* yes this is a hack */}
<Box width='40%'/> <Box width="40%" />
<Input <Input
onFocus={() => {this.setState({focusSats: true})}} onFocus={() => {
onBlur={() => {this.setState({focusSats: false})}} setFocusedField(focusFields.sats);
fontSize='14px' }}
width='100%' onBlur={() => {
type='number' setFocusedField(focusFields.empty);
borderColor={this.state.focusSats ? "lightGray" : "none"} }}
fontSize="14px"
width="100%"
type="number"
borderColor={
focusedField === focusFields.sats ? 'lightGray' : 'none'
}
disabled={signing} disabled={signing}
value={satsAmount} value={satsAmount}
onChange={e => { onChange={(e) => {
this.setState({ setDenomAmount(
denomAmount: parseFloat(e.target.value) * (conversion / 100000000), parseFloat(e.target.value) * (conversion / 100000000)
satsAmount: e.target.value );
}); setSatsAmount(e.target.value);
}} }}
/> />
<Text color="lightGray" fontSize={1} ml={3}>sats</Text> <Text color="lightGray" fontSize={1} ml={3}>
sats
</Text>
</Row> </Row>
<Row mt={4} width="100%" justifyContent="space-between"> <Row mt={4} width="100%" justifyContent="space-between">
<Text <Text gray fontSize={1} fontWeight="600" width="40%">
gray Fee
fontSize={1} </Text>
fontWeight='600'
width="40%"
>Fee</Text>
<Row alignItems="center"> <Row alignItems="center">
<Text mr={2} color="lightGray" fontSize="14px"> <Text mr={2} color="lightGray" fontSize="14px">
{this.state.feeChoices[this.state.feeValue][1]} sats/vbyte {feeChoices[feeValue][1]} sats/vbyte
</Text> </Text>
<Icon icon="ChevronSouth" <Icon
icon="ChevronSouth"
fontSize="14px" fontSize="14px"
color="lightGray" color="lightGray"
onClick={() => {if (!this.state.showModal) this.setState({showModal: true}); }} onClick={() => {
cursor="pointer"/> if (!showModal) setShowModal(true);
}}
cursor="pointer"
/>
</Row> </Row>
</Row> </Row>
<Col alignItems="center"> <Col alignItems="center">
{!this.state.showModal ? null : {!showModal ? null : (
<FeePicker <FeePicker
feeChoices={this.state.feeChoices} feeChoices={feeChoices}
feeSelect={this.feeSelect} feeSelect={feeSelect}
feeDismiss={this.feeDismiss} feeDismiss={feeDismiss}
/> />
} )}
</Col> </Col>
<Row mt={4} width="100%" <Row
mt={4}
width="100%"
justifyContent="space-between" justifyContent="space-between"
alignItems='center' alignItems="center"
> >
<Text <Text gray fontSize={1} fontWeight="600" width="40%">
gray Note
fontSize={1} </Text>
fontWeight='600'
width="40%"
>Note</Text>
<Input <Input
onFocus={() => {this.setState({focusNote: true})}} onFocus={() => {
onBlur={() => {this.setState({focusNote: false})}} setFocusedField(focusFields.note);
fontSize='14px' }}
width='100%' onBlur={() => {
setFocusedField(focusFields.empty);
}}
fontSize="14px"
width="100%"
placeholder="What's this for?" placeholder="What's this for?"
type='text' type="text"
borderColor={this.state.focusNote ? "lightGray" : "none"} borderColor={
focusedField === focusFields.note ? 'lightGray' : 'none'
}
disabled={signing} disabled={signing}
value={this.state.note} value={note}
onChange={e => { onChange={(e) => {
this.setState({ setNote(e.target.value);
note: e.target.value,
});
}} }}
/> />
</Row> </Row>
</Col> </Col>
<Row <Row flexDirection="row-reverse" alignItems="center" mt={4}>
flexDirection='row-reverse'
alignItems="center"
mt={4}
>
<Signer <Signer
signReady={signReady} signReady={signReady}
choosingSignMethod={choosingSignMethod} choosingSignMethod={choosingSignMethod}
signMethod={signMethod} signMethod={signMethod}
setSignMethod={this.setSignMethod} setSignMethod={handleSetSignMethod}
initPayment={this.initPayment} /> initPayment={initPayment}
{ (!(signing && !error)) ? null : />
<LoadingSpinner mr={2} background="midOrange" foreground="orange"/> {!(signing && !error) ? null : (
} <LoadingSpinner
mr={2}
background="midOrange"
foreground="orange"
/>
)}
<Button <Button
width='48px' width="48px"
children={ 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
icon={choosingSignMethod ? 'X' : 'Ellipsis'} icon={choosingSignMethod ? 'X' : 'Ellipsis'}
color={signReady ? 'blue' : 'lighterGray'} color={signReady ? 'blue' : 'lighterGray'}
/> />
} </Button>
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> </Row>
{signMethod === 'masterTicket' && {signMethod === 'masterTicket' && (
<Row <Row mt={4} alignItems="center">
mt={4} <Icon icon="Info" color="yellow" height={4} width={4} />
alignItems='center'
>
<Icon icon='Info' color='yellow' height={4} width={4}/>
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}> <Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
We recommend that you sign transactions using Bridge to protect your master ticket. We recommend that you sign transactions using Bridge to protect
your master ticket.
</Text> </Text>
</Row> </Row>
} )}
</Col> </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,41 +1,31 @@
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 (this.props.state.provider){ if (provider) {
if (this.props.state.provider.connected) conn = 'Connected'; if (provider.connected) conn = 'Connected';
if (this.props.state.provider.host) host = this.props.state.provider.host; if (provider.host) host = provider.host;
if (this.props.state.provider.connected && this.props.state.provider.host) { if (provider.connected && provider.host) {
connColor = "orange"; connColor = 'orange';
connBackground = "lightOrange"; connBackground = 'lightOrange';
} }
} }
@ -53,7 +43,8 @@ export default class Settings extends Component {
XPub Derivation XPub Derivation
</Text> </Text>
</Row> </Row>
<Row borderRadius="12px" <Row
borderRadius="12px"
backgroundColor="veryLightGray" backgroundColor="veryLightGray"
py={5} py={5}
px="36px" px="36px"
@ -61,16 +52,12 @@ export default class Settings extends Component {
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
> >
<Text mono <Text mono fontSize={1} style={{ wordBreak: 'break-all' }} color="gray">
fontSize={1} {wallet}
style={{wordBreak: "break-all"}}
color="gray"
>
{this.props.state.wallet}
</Text> </Text>
</Row> </Row>
<Row width="100%" mb={5}> <Row width="100%" mb={5}>
<Button children="Replace Wallet" <Button
width="100%" width="100%"
fontSize={1} fontSize={1}
fontWeight="bold" fontWeight="bold"
@ -79,15 +66,18 @@ export default class Settings extends Component {
borderColor="none" borderColor="none"
borderRadius="12px" borderRadius="12px"
p={4} p={4}
onClick={this.replaceWallet} onClick={() => replaceWallet()}
/> >
Replace Wallet
</Button>
</Row> </Row>
<Row mb="12px"> <Row mb="12px">
<Text fontSize={1} fontWeight="bold" color="black"> <Text fontSize={1} fontWeight="bold" color="black">
BTC Node Provider BTC Node Provider
</Text> </Text>
</Row> </Row>
<Col mb="12px" <Col
mb="12px"
py={5} py={5}
px="36px" px="36px"
borderRadius="12px" borderRadius="12px"
@ -103,7 +93,7 @@ export default class Settings extends Component {
</Text> </Text>
</Col> </Col>
<Row width="100%"> <Row width="100%">
<Button children="Change Provider" <Button
width="100%" width="100%"
fontSize={1} fontSize={1}
fontWeight="bold" fontWeight="bold"
@ -112,10 +102,13 @@ export default class Settings extends Component {
borderColor="none" borderColor="none"
borderRadius="12px" borderRadius="12px"
p={4} p={4}
onClick={this.changeProvider} onClick={() => changeProvider()}
/> >
Change Provider
</Button>
</Row> </Row>
</Col> </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,29 +1,19 @@
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();
export default class StartupModal extends Component {
constructor(props) {
super(props);
}
render() {
let modal = null; let modal = null;
if (this.props.state.wallet && this.props.state.provider) { if (wallet && provider) {
return null; return null;
} else if (!this.props.state.provider){ } else if (!provider) {
modal = modal = <ProviderModal />;
<ProviderModal } else if (!wallet) {
api={this.props.api} modal = <WalletModal />;
providerPerms={this.props.state.providerPerms}
/>
} else if (!this.props.state.wallet){
modal = <WalletModal api={this.props.api} network={this.props.network}/>
} }
return ( return (
<Box <Box
@ -38,9 +28,10 @@ export default class StartupModal extends Component {
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
> >
<Box display="flex" <Box
display="flex"
flexDirection="column" flexDirection="column"
width='400px' width="400px"
backgroundColor="white" backgroundColor="white"
borderRadius={3} borderRadius={3}
> >
@ -48,5 +39,6 @@ export default class StartupModal extends Component {
</Box> </Box>
</Box> </Box>
); );
} };
}
export default StartupModal;

View File

@ -1,70 +1,61 @@
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);
}
render() {
const pending = (!this.props.tx.recvd);
let weSent = _.find(this.props.tx.inputs, (input) => {
return (input.ship === window.ship);
}); });
let weRecv = this.props.tx.outputs.every((output) => { let weRecv = tx.outputs.every((output) => {
return (output.ship === window.ship) return output.ship === window.ship;
}); });
let action = let action = weRecv ? 'recv' : weSent ? 'sent' : 'recv';
(weRecv) ? "recv" :
(weSent) ? "sent" : "recv";
let counterShip = null; let counterShip = null;
let counterAddress = null; let counterAddress = null;
let value; let value;
let sign; let sign;
if (action === "sent") { if (action === 'sent') {
let counter = _.find(this.props.tx.outputs, (output) => { let counter = _.find(tx.outputs, (output) => {
return (output.ship !== window.ship); return output.ship !== window.ship;
}); });
counterShip = _.get(counter, 'ship', null); counterShip = _.get(counter, 'ship', null);
counterAddress = _.get(counter, 'val.address', null); counterAddress = _.get(counter, 'val.address', null);
value = _.get(counter, 'val.value', null); value = _.get(counter, 'val.value', null);
sign = '-' sign = '-';
} } else if (action === 'recv') {
else if (action === "recv") { value = _.reduce(
value = _.reduce(this.props.tx.outputs, (sum, output) => { 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) { if (weSent && weRecv) {
counterAddress = _.get(_.find(this.props.tx.inputs, (input) => { counterAddress = _.get(
return (input.ship === window.ship); _.find(tx.inputs, (input) => {
}), 'val.address', null); return input.ship === window.ship;
}),
'val.address',
null
);
} else { } else {
let counter = _.find(this.props.tx.inputs, (input) => { let counter = _.find(tx.inputs, (input) => {
return (input.ship !== window.ship); return input.ship !== window.ship;
}); });
counterShip = _.get(counter, 'ship', null); counterShip = _.get(counter, 'ship', null);
counterAddress = _.get(counter, 'val.address', null); counterAddress = _.get(counter, 'val.address', null);
@ -72,34 +63,34 @@ export default class Transaction extends Component {
sign = ''; sign = '';
} }
let currencyValue = sign + satsToCurrency(value, this.props.denom, this.props.rates); let currencyValue = sign + satsToCurrency(value, denomination, currencyRates);
const failure = Boolean(this.props.tx.failure); const failure = Boolean(tx.failure);
if (failure) action = "fail"; if (failure) action = 'fail';
const txid = this.props.tx.txid.dat.slice(2).replaceAll('.','');
const txid = tx.txid.dat.slice(2).replaceAll('.', '');
return ( return (
<Col <Col
width='100%' width="100%"
backgroundColor="white" backgroundColor="white"
justifyContent="space-between" justifyContent="space-between"
mb="16px" mb="16px"
> >
<Row justifyContent="space-between" alignItems="center"> <Row justifyContent="space-between" alignItems="center">
<TxAction action={action} pending={pending} txid={txid} network={this.props.network}/> <TxAction action={action} pending={pending} txid={txid} />
<Text fontSize="14px" alignItems="center" color="gray"> <Text fontSize="14px" alignItems="center" color="gray">
{sign}{value} sats {sign}
{value} sats
</Text> </Text>
</Row> </Row>
<Box ml="11px" borderLeft="2px solid black" height="4px"> <Box ml="11px" borderLeft="2px solid black" height="4px"></Box>
</Box>
<Row justifyContent="space-between" alignItems="center"> <Row justifyContent="space-between" alignItems="center">
<TxCounterparty address={counterAddress} ship={counterShip} /> <TxCounterparty address={counterAddress} ship={counterShip} />
<Text fontSize="14px">{currencyValue}</Text> <Text fontSize="14px">{currencyValue}</Text>
</Row> </Row>
</Col> </Col>
); );
} };
}
export default Transaction;

View File

@ -1,26 +1,14 @@
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);
}
render() {
if (!this.props.state.history || this.props.state.history.length <= 0) {
return ( return (
<Box alignItems="center" <Box
alignItems="center"
display="flex" display="flex"
justifyContent="center" justifyContent="center"
height="340px" height="340px"
@ -30,33 +18,26 @@ export default class Transactions extends Component {
borderRadius="48px" borderRadius="48px"
backgroundColor="white" backgroundColor="white"
> >
<Text color="gray" fontSize={2} fontWeight="bold">No Transactions Yet</Text> <Text color="gray" fontSize={2} fontWeight="bold">
No Transactions Yet
</Text>
</Box> </Box>
); );
} else { } else {
return ( return (
<Col <Col
width='100%' width="100%"
backgroundColor="white" backgroundColor="white"
borderRadius="48px" borderRadius="48px"
mb={5} mb={5}
p={5} p={5}
> >
{ {history.map((tx, i) => {
this.props.state.history.map((tx, i) => { return <Transaction tx={tx} key={i} />;
return( })}
<Transaction
tx={tx}
key={i}
denom={this.props.state.denomination}
rates={this.props.state.currencyRates}
network={this.props.network}
/>
);
})
}
</Col> </Col>
); );
} }
} };
}
export default Transactions;

View File

@ -1,51 +1,48 @@
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);
}
render() {
const leftIcon = const leftIcon =
this.props.action === 'sent' action === 'sent'
? 'ArrowSouth' ? 'ArrowSouth'
: this.props.action === 'recv' : action === 'recv'
? 'ArrowNorth' ? 'ArrowNorth'
: this.props.action === 'fail' : action === 'fail'
? 'X' ? 'X'
: 'NullIcon'; : 'NullIcon';
const actionColor = const actionColor =
this.props.action === 'sent' action === 'sent'
? 'sentBlue' ? 'sentBlue'
: this.props.action === 'recv' : action === 'recv'
? 'recvGreen' ? 'recvGreen'
: this.props.action === 'fail' : action === 'fail'
? 'gray' ? 'gray'
: 'red'; : 'red';
const actionText = const actionText =
this.props.action === 'sent' && !this.props.pending action === 'sent' && !pending
? 'Sent BTC' ? 'Sent BTC'
: this.props.action === 'sent' && this.props.pending : action === 'sent' && pending
? 'Sending BTC' ? 'Sending BTC'
: this.props.action === 'recv' && !this.props.pending : action === 'recv' && !pending
? 'Received BTC' ? 'Received BTC'
: this.props.action === 'recv' && this.props.pending : action === 'recv' && pending
? 'Receiving BTC' ? 'Receiving BTC'
: this.props.action === 'fail' : action === 'fail'
? 'Failed' ? 'Failed'
: 'error'; : 'error';
const pending = !this.props.pending ? null : ( const pendingSpinner = !pending ? null : (
<LoadingSpinner background="midOrange" foreground="orange" /> <LoadingSpinner background="midOrange" foreground="orange" />
); );
const url = const url =
this.props.network === 'testnet' network === 'testnet'
? `http://blockstream.info/testnet/tx/${this.props.txid}` ? `http://blockstream.info/testnet/tx/${txid}`
: `http://blockstream.info/tx/${this.props.txid}`; : `http://blockstream.info/tx/${txid}`;
return ( return (
<Row alignItems="center"> <Row alignItems="center">
@ -67,8 +64,9 @@ export default class TxAction extends Component {
<a href={url} target="_blank" rel="noreferrer"> <a href={url} target="_blank" rel="noreferrer">
<Icon color={actionColor} icon="ArrowNorthEast" ml={1} mr={2} /> <Icon color={actionColor} icon="ArrowNorthEast" ml={1} mr={2} />
</a> </a>
{pending} {pendingSpinner}
</Row> </Row>
); );
} };
}
export default TxAction;

View File

@ -1,33 +1,13 @@
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} />
export default class TxCounterparty extends Component { ) : (
constructor(props) { <Box
super(props); backgroundColor="lighterGray"
}
render() {
const icon = (this.props.ship)
? <Sigil
ship={this.props.ship}
size={24}
color="black"
classes={''}
icon
padding={5}
/>
: <Box backgroundColor="lighterGray"
width="24px" width="24px"
height="24px" height="24px"
textAlign="center" textAlign="center"
@ -37,17 +17,20 @@ export default class TxCounterparty extends Component {
> >
<Icon icon="Bitcoin" color="gray" /> <Icon icon="Bitcoin" color="gray" />
</Box> </Box>
const addressText = (!this.props.address) ? '' : );
this.props.address.slice(0, 6) + '...' + const addressText = !address
this.props.address.slice(-6); ? ''
const text = (this.props.ship) ? : address.slice(0, 6) + '...' + address.slice(-6);
`~${this.props.ship}` : addressText; const text = ship ? `~${ship}` : addressText;
return ( return (
<Row alignItems="center"> <Row alignItems="center">
{icon} {icon}
<Text ml={2} mono fontSize="14px" color="gray">{text}</Text> <Text ml={2} mono fontSize="14px" color="gray">
{text}
</Text>
</Row> </Row>
); );
} };
}
export default TxCounterparty;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { import {
Box, Box,
Text, Text,
@ -6,198 +6,184 @@ 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);
});
};
const buttonDisabled = !readyToSubmit || processingSubmission;
const inputDisabled = processingSubmission;
// const processingSpinner = !processingSubmission ? null : <LoadingSpinner />;
if (mode === 'masterTicket') {
return ( return (
<Box <Box width="100%" height="100%" padding={3}>
width="100%"
height="100%"
padding={3}
>
<Row> <Row>
<Icon icon="Bitcoin" mr={2} /> <Icon icon="Bitcoin" mr={2} />
<Text fontSize="14px" fontWeight="bold"> <Text fontSize="14px" fontWeight="bold">
Step 2 of 2: Import your extended public key Step 2 of 2: Import your extended public key
</Text> </Text>
</Row> </Row>
<Row <Row mt={3} alignItems="center">
mt={3} <Icon icon="Info" color="yellow" height={4} width={4} />
alignItems='center'
>
<Icon icon='Info' color='yellow' height={4} width={4}/>
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}> <Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
We recommend that you import your wallet using Bridge to protect your master ticket. We recommend that you import your wallet using Bridge to protect
your master ticket.
</Text> </Text>
</Row> </Row>
<Box <Box display="flex" alignItems="center" mt={3} mb={2}>
display='flex' {confirmingMasterTicket && (
alignItems='center'
mt={3}
mb={2}>
{this.state.confirmingMasterTicket &&
<Icon <Icon
icon='ArrowWest' icon="ArrowWest"
cursor='pointer' cursor="pointer"
onClick={() => this.setState({ onClick={() => {
confirmingMasterTicket: false, setConfirmingMasterTicket(false);
masterTicket: '', setMasterTicket('');
confirmedMasterTicket: '', setConfirmedMasterTicket('');
error: false setError(false);
})}/>} }}
/>
)}
<Text fontSize="14px" fontWeight="500"> <Text fontSize="14px" fontWeight="500">
{this.state.confirmingMasterTicket ? 'Confirm Master Ticket' : 'Master Ticket'} {confirmingMasterTicket ? 'Confirm Master Ticket' : 'Master Ticket'}
</Text> </Text>
</Box> </Box>
<Row alignItems="center"> <Row alignItems="center">
<StatelessTextInput <StatelessTextInput
mr={2} mr={2}
width="256px" width="256px"
value={this.state.confirmingMasterTicket ? this.state.confirmedMasterTicket : this.state.masterTicket} value={
confirmingMasterTicket ? confirmedMasterTicket : masterTicket
}
disabled={inputDisabled} disabled={inputDisabled}
fontSize="14px" fontSize="14px"
type="password" type="password"
name="masterTicket" name="masterTicket"
obscure={value => value.replace(/[^~-]+/g, '••••••')} obscure={(value) => value.replace(/[^~-]+/g, '••••••')}
placeholder="••••••-••••••-••••••-••••••" placeholder="••••••-••••••-••••••-••••••"
autoCapitalize="none" autoCapitalize="none"
autoCorrect="off" autoCorrect="off"
onChange={this.checkTicket} onChange={(e) => checkTicket(e)}
/> />
{(!inputDisabled) ? null : <LoadingSpinner/>} {!inputDisabled ? null : <LoadingSpinner />}
</Row> </Row>
{this.state.error && {error && (
<Row mt={2}> <Row mt={2}>
<Text <Text fontSize="14px" color="red">
fontSize='14px'
color='red'>
Master tickets do not match Master tickets do not match
</Text> </Text>
</Row> </Row>
} )}
<Row mt={3}> <Row mt={3}>
<Button <Button
primary primary
color="black" color="black"
backgroundColor="veryLightGray" backgroundColor="veryLightGray"
borderColor="veryLightGray" borderColor="veryLightGray"
children="Cancel"
fontSize="14px" fontSize="14px"
mr={2} mr={2}
style={{cursor: "pointer"}} style={{ cursor: 'pointer' }}
onClick={() => {this.setState({mode: 'xpub', masterTicket: '', xpub: '', readyToSubmit: false})}} onClick={() => {
/> setMode('xpub');
setMasterTicket('');
setXpub('');
setReadyToSubmit(false);
}}
>
Cancel
</Button>
<Button <Button
primary primary
disabled={buttonDisabled} disabled={buttonDisabled}
children="Next Step"
fontSize="14px" fontSize="14px"
style={{cursor: buttonDisabled ? "default" : "pointer"}} style={{ cursor: buttonDisabled ? 'default' : 'pointer' }}
onClick={() => { onClick={() => {
if (!this.state.confirmingMasterTicket) { if (!confirmingMasterTicket) {
this.setState({confirmingMasterTicket: true}); setConfirmingMasterTicket(true);
} else { } else {
if (this.state.masterTicket === this.state.confirmedMasterTicket) { if (masterTicket === confirmedMasterTicket) {
this.setState({error: false}); setError(false);
this.submitMasterTicket(this.state.masterTicket); submitMasterTicket(masterTicket);
} else { } else {
this.setState({error: true}); setError(true);
} }
} }
}} }}
/> >
Next Step
</Button>
</Row> </Row>
</Box> </Box>
); );
} else if (this.state.mode === 'xpub') { } else if (mode === 'xpub') {
return ( return (
<Box <Box width="100%" height="100%" padding={3}>
width="100%"
height="100%"
padding={3}
>
<Row> <Row>
<Icon icon="Bitcoin" mr={2} /> <Icon icon="Bitcoin" mr={2} />
<Text fontSize="14px" fontWeight="bold"> <Text fontSize="14px" fontWeight="bold">
@ -206,7 +192,16 @@ export default class WalletModal extends Component {
</Row> </Row>
<Box mt={3}> <Box mt={3}>
<Text fontSize="14px" fontWeight="regular" color="gray"> <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 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> </Text>
</Box> </Box>
<Box mt={3} mb={2}> <Box mt={3} mb={2}>
@ -215,38 +210,47 @@ export default class WalletModal extends Component {
</Text> </Text>
</Box> </Box>
<StatelessTextInput <StatelessTextInput
value={this.state.xpub} value={xpub}
disabled={inputDisabled} disabled={inputDisabled}
fontSize="14px" fontSize="14px"
type="password" type="password"
name="xpub" name="xpub"
autoCapitalize="none" autoCapitalize="none"
autoCorrect="off" autoCorrect="off"
onChange={this.checkXPub} onChange={(e) => checkXPub(e)}
/> />
<Box mt={3} mb={3}> <Box mt={3} mb={3}>
<Text fontSize="14px" fontWeight="regular" <Text
color={(inputDisabled) ? "lighterGray" : "gray"} fontSize="14px"
style={{cursor: (inputDisabled) ? "default" : "pointer"}} fontWeight="regular"
color={inputDisabled ? 'lighterGray' : 'gray'}
style={{ cursor: inputDisabled ? 'default' : 'pointer' }}
onClick={() => { onClick={() => {
if (inputDisabled) return; if (inputDisabled) return;
this.setState({mode: 'masterTicket', xpub: '', masterTicket: '', readyToSubmit: false}) setMode('masterTicket');
setXpub('');
setMasterTicket('');
setReadyToSubmit(false);
}} }}
> >
Import using master ticket -> Import using master ticket -&gt;
</Text> </Text>
</Box> </Box>
<Button <Button
primary primary
mt={3} mt={3}
disabled={buttonDisabled} disabled={buttonDisabled}
children="Next Step"
fontSize="14px" fontSize="14px"
style={{cursor: this.state.ready ? "pointer" : "default"}} style={{ cursor: readyToSubmit ? 'pointer' : 'default' }}
onClick={() => { this.submitXPub(this.state.xpub) }} onClick={() => {
/> submitXPub(xpub);
}}
>
Next Step
</Button>
</Box> </Box>
); );
} }
} };
}
export default WalletModal;

View File

@ -1,68 +1,58 @@
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> <Col>
<Text color="white" fontWeight='bold' fontSize={1}> <Text color="white" fontWeight="bold" fontSize={1}>
Warning! Warning!
</Text> </Text>
<br /> <br />
<Text color="white" fontWeight='bold' fontSize={1}> <Text color="white" fontWeight="bold" fontSize={1}>
Be safe while using this wallet, and be sure to store responsible amounts Be safe while using this wallet, and be sure to store responsible
of BTC. amounts of BTC.
</Text> </Text>
<Text color="white" fontWeight='bold' fontSize={1}> <Text color="white" fontWeight="bold" fontSize={1}>
Always ensure that the checksum of the wallet matches that of the wallet's repo. Always ensure that the checksum of the wallet matches that of the
wallet&apos;s repo.
</Text> </Text>
<br /> <br />
<Anchor href="https://urbit.org/bitcoin-wallet" target="_blank"> <Anchor href="https://urbit.org/bitcoin-wallet" target="_blank">
<Text color="white" fontWeight="bold" fontSize={1} style={{textDecoration:'underline'}}> <Text
color="white"
fontWeight="bold"
fontSize={1}
style={{ textDecoration: 'underline' }}
>
Learn more on urbit.org Learn more on urbit.org
</Text> </Text>
</Anchor> </Anchor>
</Col> </Col>
<Button children="I Understand" <Button
backgroundColor="white" backgroundColor="white"
fontSize={1} fontSize={1}
mt={5} mt={5}
@ -71,9 +61,12 @@ export default class Warning extends Component {
borderRadius="24px" borderRadius="24px"
p="24px" p="24px"
borderColor="none" borderColor="none"
onClick={this.understand} onClick={() => understand()}
/> >
I understand
</Button>
</Box> </Box>
); );
} };
}
export default Warning;

View File

@ -1,44 +1,21 @@
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();
export class Root extends Component { const blur = !loaded ? false : !(wallet && provider);
constructor(props) {
super(props);
this.ship = window.ship;
this.state = store.state;
store.setStateHandler(this.setState.bind(this));
}
componentDidMount() {
this.props.channel.setOnChannelError(() => {
subscription.start();
});
subscription.start();
}
render() {
const loaded = this.state.loaded;
const warning = this.state.showWarning;
const blur = !loaded ? false : !(this.state.wallet && this.state.provider);
return ( return (
<BrowserRouter> <BrowserRouter>
<ThemeProvider theme={light}> <ThemeProvider theme={light}>
<Reset /> <Reset />
{loaded ? ( {loaded ? <StartupModal /> : null}
<StartupModal api={api} state={this.state} network={network} />
) : null}
<Box <Box
display="flex" display="flex"
flexDirection="column" flexDirection="column"
@ -52,16 +29,11 @@ export class Root extends Component {
px={[0, 4]} px={[0, 4]}
pb={[0, 4]} pb={[0, 4]}
> >
<Body <Body />
loaded={loaded}
state={this.state}
api={api}
network={network}
warning={warning}
/>
</Box> </Box>
</ThemeProvider> </ThemeProvider>
</BrowserRouter> </BrowserRouter>
); );
} };
}
export default Root;

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;
} }
@ -27,12 +23,14 @@ export function isPatTa(str) {
*/ */
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,18 +62,19 @@ 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}`;
@ -83,30 +82,39 @@ export function cite(ship) {
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;