Convert components, remove old store

This commit is contained in:
finned-palmer 2021-06-30 16:17:31 -05:00 committed by ixv
parent 9751555038
commit ebf16f0f74
28 changed files with 2099 additions and 2570 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,59 +1,53 @@
import React, { Component } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
StatelessRadioButtonField as RadioButton,
Label,
} from '@tlon/indigo-react';
const feeLevels = {
low: 'low',
mid: 'mid',
high: 'high',
};
export default class FeePicker extends Component {
constructor(props) {
super(props);
const FeePicker = ({ feeChoices, feeSelect, feeDismiss }) => {
const [feeSelected, setFeeSelected] = useState(feeLevels.mid);
const [modalElement, setModalElement] = useState();
const modalRef = useRef();
this.state = {
selected: 'mid'
}
// const clickDismiss = (e) => {
// console.log(modalElement, e);
// // if (modalRef && !modalRef.contains(e.target)) {
// // feeDismiss();
// // }
// };
this.select = this.select.bind(this);
this.clickDismiss = this.clickDismiss.bind(this);
this.setModalRef = this.setModalRef.bind(this);
}
const select = (which) => {
setFeeSelected(which);
feeSelect(which);
feeDismiss();
};
componentDidMount() {
document.addEventListener("click", this.clickDismiss);
}
// useEffect(() => {
// document.addEventListener('click', (e) => clickDismiss(e));
// setModalElement(modalRef.current);
// console.log(modalRef.current);
// return () => document.addEventListener('click', clickDismiss);
// }, []);
componentWillUnount() {
document.removeEventListener("click", this.clickDismiss);
}
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 (
<Box
ref={this.setModalRef}
position="absolute" p={4}
border="1px solid green" zIndex={10}
backgroundColor="white" borderRadius={3}
// ref={modalRef}
// onClick={() => feeDismiss()}
position="absolute"
p={4}
border="1px solid green"
zIndex={10}
backgroundColor="white"
borderRadius={3}
>
<Text fontSize={1} color="black" fontWeight="bold" mb={4}>
Transaction Speed
@ -61,39 +55,45 @@ export default class FeePicker extends Component {
<Col mt={4}>
<RadioButton
name="feeRadio"
selected={this.state.selected === 'low'}
selected={feeSelected === feeLevels.low}
p="2"
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
name="feeRadio"
selected={this.state.selected === 'mid'}
selected={feeSelected === feeLevels.mid}
p="2"
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
name="feeRadio"
selected={this.state.selected === 'high'}
selected={feeSelected === feeLevels.high}
p="2"
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>
</Col>
</Box>
);
}
}
};
export default FeePicker;

View File

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

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { useRef, useState, useEffect } from 'react';
import {
Box,
Icon,
@ -9,18 +9,15 @@ import {
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import { Sigil } from './sigil.js'
import { Sigil } from './sigil.js';
import * as bitcoin from 'bitcoinjs-lib';
import * as kg from 'urbit-key-generation';
import * as bip39 from 'bip39';
import Sent from './sent.js'
import Sent from './sent.js';
import { patp2dec, isValidPatq, isValidPatp } from 'urbit-ob';
import { satsToCurrency } from '../../lib/util.js';
import Error from './error.js';
import { useSettings } from '../../hooks/useSettings.js';
import { api } from '../../api';
const BITCOIN_MAINNET_INFO = {
messagePrefix: '\x18Bitcoin Signed Message:\n',
@ -46,74 +43,65 @@ const BITCOIN_TESTNET_INFO = {
wif: 0xef,
};
export default class Invoice extends Component {
constructor(props) {
super(props);
const Invoice = ({ stopSending, payee, satsAmount }) => {
const {
error,
currencyRates,
psbt,
fee,
broadcastSuccess,
network,
denomination,
} = useSettings();
const [masterTicket, setMasterTicket] = useState('');
const [ready, setReady] = useState(false);
const [localError, setLocalError] = useState(error);
const [broadcasting, setBroadcasting] = useState(false);
const invoiceRef = useRef();
this.state = {
masterTicket: '',
ready: false,
error: this.props.state.error,
sent: false,
broadcasting: false,
useEffect(() => {
if (broadcasting && localError !== '') {
setBroadcasting(false);
}
}, [error, broadcasting, setBroadcasting]);
const clickDismiss = (e) => {
if (invoiceRef && !invoiceRef.contains(e.target)) {
stopSending();
}
};
this.checkTicket = this.checkTicket.bind(this);
this.broadCastTx = this.broadCastTx.bind(this);
this.sendBitcoin = this.sendBitcoin.bind(this);
this.clickDismiss = this.clickDismiss.bind(this);
this.setInvoiceRef = this.setInvoiceRef.bind(this);
}
useEffect(() => {
document.addEventListener('click', clickDismiss);
return () => document.removeEventListener('click', clickDismiss);
}, []);
componentDidMount(){
document.addEventListener("click", this.clickDismiss);
}
componentWillUnMount(){
document.removeEventListener("click", this.clickDismiss);
}
setInvoiceRef(n){
this.invoiceRef = n;
}
clickDismiss(e){
if (this.invoiceRef && !(this.invoiceRef.contains(e.target))) {
this.props.stopSending();
}
}
componentDidUpdate(prevProps, prevState) {
if (this.state.broadcasting) {
if (this.state.error !== '') {
this.setState({broadcasting: false});
}
}
}
broadCastTx(psbtHex) {
const broadCastTx = (psbtHex) => {
let command = {
'broadcast-tx': psbtHex
}
return this.props.api.btcWalletCommand(command)
}
'broadcast-tx': psbtHex,
};
return api.btcWalletCommand(command);
};
sendBitcoin(ticket, psbt) {
const sendBitcoin = (ticket, psbt) => {
const newPsbt = bitcoin.Psbt.fromBase64(psbt);
this.setState({broadcasting: true});
kg.generateWallet({ ticket, ship: parseInt(patp2dec('~' + window.ship)) })
.then(urbitWallet => {
const { xpub } = this.props.network === 'testnet'
? urbitWallet.bitcoinTestnet.keys
: urbitWallet.bitcoinMainnet.keys;
setBroadcasting(true);
kg.generateWallet({
ticket,
ship: parseInt(patp2dec('~' + window.ship)),
}).then((urbitWallet) => {
// const { xpub } =
// network === 'testnet'
// ? urbitWallet.bitcoinTestnet.keys
// : urbitWallet.bitcoinMainnet.keys;
const { xprv: zprv } = urbitWallet.bitcoinMainnet.keys;
const { xprv: vprv } = urbitWallet.bitcoinTestnet.keys;
const isTestnet = (this.props.network === 'testnet');
const isTestnet = network === 'testnet';
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(zprv, BITCOIN_MAINNET_INFO);
@ -131,28 +119,20 @@ export default class Invoice extends Component {
.extractTransaction()
.toHex();
this.broadCastTx(hex);
}
catch(e) {
this.setState({error: 'invalid-master-ticket', broadcasting: false});
broadCastTx(hex);
} catch (e) {
setLocalError('invalid-master-ticket');
setBroadcasting(false);
}
});
};
}
checkTicket(e){
const checkTicket = (e) => {
// TODO: port over bridge ticket validation logic
let masterTicket = e.target.value;
let ready = isValidPatq(masterTicket);
let error = (ready) ? '' : 'invalid-master-ticket';
this.setState({masterTicket, ready, error});
}
render() {
const broadcastSuccess = this.props.state.broadcastSuccess;
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates, fee } = this.props;
const { sent, error } = this.state;
setMasterTicket(e.target.value);
setReady(isValidPatq(e.target.value));
setLocalError(isValidPatq(e.target.value) ? '' : 'invalid-master-ticket');
};
let inputColor = 'black';
let inputBg = 'white';
@ -166,126 +146,131 @@ export default class Invoice extends Component {
const isShip = isValidPatp(payee);
const icon = (isShip)
? <Sigil ship={payee} size={24} color="black" classes={''} icon padding={5}/>
: <Box backgroundColor="lighterGray"
const icon = isShip ? (
<Sigil ship={payee} size={24} color="black" classes={''} icon padding={5} />
) : (
<Box
backgroundColor="lighterGray"
width="24px"
height="24px"
textAlign="center"
alignItems="center"
borderRadius="2px"
p={1}
><Icon icon="Bitcoin" color="gray"/></Box>;
>
<Icon icon="Bitcoin" color="gray" />
</Box>
);
return (
<>
{ broadcastSuccess ?
<Sent
payee={payee}
stopSending={stopSending}
denomination={denomination}
currencyRates={currencyRates}
satsAmount={satsAmount}
/> :
{broadcastSuccess ? (
<Sent payee={payee} stopSending={stopSending} satsAmount={satsAmount} />
) : (
<Col
ref={this.setInvoiceRef}
width='100%'
backgroundColor='white'
borderRadius='48px'
ref={invoiceRef}
width="100%"
backgroundColor="white"
borderRadius="48px"
mb={5}
p={5}
onClick={() => stopSending()}
>
<Col
p={5}
mt={4}
backgroundColor='veryLightGreen'
borderRadius='24px'
backgroundColor="veryLightGreen"
borderRadius="24px"
alignItems="center"
>
<Row>
<Text
color='green'
fontSize='40px'
>{satsToCurrency(satsAmount, denomination, currencyRates)}</Text>
<Text color="green" fontSize="40px">
{satsToCurrency(satsAmount, denomination, currencyRates)}
</Text>
</Row>
<Row>
<Text
fontWeight="bold"
fontSize='16px'
color='midGreen'
fontSize="16px"
color="midGreen"
>{`${satsAmount} sats`}</Text>
</Row>
<Row mt={2}>
<Text
fontSize='14px'
color='midGreen'
>{`Fee: ${satsToCurrency(fee, denomination, currencyRates)} (${fee} sats)`}</Text>
<Text fontSize="14px" color="midGreen">{`Fee: ${satsToCurrency(
fee,
denomination,
currencyRates
)} (${fee} sats)`}</Text>
</Row>
<Row mt={4}>
<Text fontSize='16px' fontWeight="bold" color="gray">You are paying</Text>
<Text fontSize="16px" fontWeight="bold" color="gray">
You are paying
</Text>
</Row>
<Row mt={2} alignItems="center">
{icon}
<Text ml={2}
<Text
ml={2}
mono
color="gray"
fontSize='14px'
style={{'display': 'block', 'overflow-wrap': 'anywhere'}}
>{payee}</Text>
fontSize="14px"
style={{ display: 'block', 'overflow-wrap': 'anywhere' }}
>
{payee}
</Text>
</Row>
</Col>
<Row mt={3} mb={2} alignItems="center">
<Text gray fontSize={1} fontWeight='600' mr={4}>
<Text gray fontSize={1} fontWeight="600" mr={4}>
Ticket
</Text>
<Input
value={this.state.masterTicket}
value={masterTicket}
fontSize="14px"
type="password"
name="masterTicket"
obscure={value => value.replace(/[^~-]+/g, '••••••')}
obscure={(value) => value.replace(/[^~-]+/g, '••••••')}
placeholder="••••••-••••••-••••••-••••••"
autoCapitalize="none"
autoCorrect="off"
color={inputColor}
backgroundColor={inputBg}
borderColor={inputBorder}
onChange={this.checkTicket}
onChange={() => checkTicket()}
/>
</Row>
{(error !== '') &&
{error !== '' && (
<Row>
<Error
fontSize='14px'
color='red'
error={error}
mt={2}/>
<Error fontSize="14px" color="red" error={error} mt={2} />
</Row>
}
<Row
flexDirection='row-reverse'
mt={4}
alignItems="center"
>
)}
<Row flexDirection="row-reverse" mt={4} alignItems="center">
<Button
primary
children='Send BTC'
mr={3}
fontSize={1}
border="none"
borderRadius='24px'
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"}
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"}
height='48px'
onClick={() => this.sendBitcoin(this.state.masterTicket, psbt)}
disabled={!this.state.ready || error || this.state.broadcasting}
style={{cursor: (this.state.ready && !error && !this.state.broadcasting) ? "pointer" : "default"}}
/>
{ (this.state.broadcasting) ? <LoadingSpinner mr={3}/> : null}
borderRadius="24px"
color={ready && !error && !broadcasting ? 'white' : 'lighterGray'}
backgroundColor={
ready && !error && !broadcasting ? 'green' : 'veryLightGray'
}
height="48px"
onClick={() => sendBitcoin(masterTicket, psbt)}
disabled={!ready || error || broadcasting}
style={{
cursor:
ready && !error && !broadcasting ? 'pointer' : 'default',
}}
>
Send BTC
</Button>
{broadcasting ? <LoadingSpinner mr={3} /> : null}
</Row>
</Col>
}
)}
</>
);
}
}
};
export default Invoice;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { useEffect, useState } from 'react';
import {
Box,
Text,
@ -8,90 +8,82 @@ import {
Row,
LoadingSpinner,
} from '@tlon/indigo-react';
import { isValidPatp } from 'urbit-ob';
import { api } from '../../api';
import { useSettings } from '../../hooks/useSettings';
export default class ProviderModal extends Component {
constructor(props) {
super(props);
this.state = {
potentialProvider: null,
checkingProvider: false,
providerFailed: false,
ready: false,
provider: null,
connecting: false,
const providerStatuses = {
checking: 'checking',
failed: 'failed',
ready: 'ready',
initial: '',
};
this.checkProvider = this.checkProvider.bind(this);
this.submitProvider = this.submitProvider.bind(this);
}
const ProviderModal = () => {
const { providerPerms } = useSettings();
const [providerStatus, setProviderStatus] = useState(
providerStatuses.initial
);
const [potentialProvider, setPotentialProvider] = useState(null);
const [provider, setProvider] = useState(null);
const [connecting, setConnecting] = useState(false);
checkProvider(e) {
const checkProvider = (e) => {
// TODO: loading states
let provider = e.target.value;
let ready = false;
let checkingProvider = false;
let potentialProvider = this.state.potentialProvider;
if (isValidPatp(provider)) {
setProviderStatus(providerStatuses.initial);
let givenProvider = e.target.value;
if (isValidPatp(givenProvider)) {
let command = {
'check-provider': provider,
'check-provider': givenProvider,
};
potentialProvider = provider;
checkingProvider = true;
this.props.api.btcWalletCommand(command);
setPotentialProvider(givenProvider);
setProviderStatus(providerStatuses.checking);
api.btcWalletCommand(command);
setTimeout(() => {
this.setState({ providerFailed: true, checkingProvider: false });
setProviderStatus(providerStatuses.failed);
}, 5000);
}
this.setState({ provider, ready, checkingProvider, potentialProvider });
}
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,
setProvider(givenProvider);
};
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 workingColor = null;
let workingBg = null;
if (this.state.ready) {
if (providerStatus === providerStatuses.ready) {
workingColor = 'green';
workingBg = 'veryLightGreen';
workingNode = (
<Box mt={3}>
<Text fontSize="14px" color="green">
{this.state.provider} is a working provider node
{provider} is a working provider node
</Text>
</Box>
);
} else if (this.state.providerFailed) {
} else if (providerStatus === providerStatuses.failed) {
workingColor = 'red';
workingBg = 'veryLightRed';
workingNode = (
<Box mt={3}>
<Text fontSize="14px" color="red">
{this.state.potentialProvider} is not a working provider node
{potentialProvider} is not a working provider node
</Text>
</Box>
);
@ -107,9 +99,9 @@ export default class ProviderModal extends Component {
</Row>
<Box mt={3}>
<Text fontSize="14px" fontWeight="regular" color="gray">
In order to perform Bitcoin transaction in Landscape, you&apos;ll
need to set a provider node. A provider node is an urbit which
maintains a synced Bitcoin ledger.
In order to perform Bitcoin transaction in Landscape, you&apos;ll need
to set a provider node. A provider node is an urbit which maintains a
synced Bitcoin ledger.
<a
fontSize="14px"
target="_blank"
@ -140,27 +132,33 @@ export default class ProviderModal extends Component {
backgroundColor={workingBg}
color={workingColor}
borderColor={workingColor}
onChange={this.checkProvider}
onChange={(e) => checkProvider(e)}
/>
{this.state.checkingProvider ? <LoadingSpinner /> : null}
{providerStatus === providerStatuses.checking ? (
<LoadingSpinner />
) : null}
</Row>
{workingNode}
<Row alignItems="center" mt={3}>
<Button
mr={2}
primary
disabled={!this.state.ready}
disabled={providerStatus !== providerStatuses.ready}
fontSize="14px"
style={{ cursor: this.state.ready ? 'pointer' : 'default' }}
style={{
cursor:
providerStatus === providerStatuses.ready ? 'pointer' : 'default',
}}
onClick={() => {
this.submitProvider(this.state.provider);
submitProvider(provider);
}}
>
Set Peer Node
</Button>
{this.state.connecting ? <LoadingSpinner /> : null}
{connecting ? <LoadingSpinner /> : null}
</Row>
</Box>
);
}
}
};
export default ProviderModal;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { useEffect, useState } from 'react';
import {
Box,
Icon,
@ -8,450 +8,443 @@ import {
Button,
Col,
LoadingSpinner,
StatelessRadioButtonField as RadioButton,
} from '@tlon/indigo-react';
import Invoice from './invoice.js'
import BridgeInvoice from './bridgeInvoice.js'
import FeePicker from './feePicker.js'
import Error from './error.js'
import Signer from './signer.js'
import Invoice from './invoice.js';
import BridgeInvoice from './bridgeInvoice.js';
import FeePicker from './feePicker.js';
import Error from './error.js';
import Signer from './signer.js';
import { validate } from 'bitcoin-address-validation';
import * as ob from 'urbit-ob';
import { useSettings } from '../../hooks/useSettings.js';
import { api } from '../../api';
export default class Send extends Component {
constructor(props) {
super(props);
const focusFields = {
empty: '',
payee: 'payee',
currency: 'currency',
sats: 'sats',
note: 'note',
};
this.state = {
signing: false,
denomAmount: '0.00',
satsAmount: '0',
payee: '',
checkingPatp: false,
payeeType: '',
ready: false,
validPayee: false,
focusPayee: true,
focusCurrency: false,
focusSats: false,
focusNote: false,
submitting: false,
feeChoices: {
const Send = ({ stopSending, value, conversion }) => {
const { error, setError, network, psbt, denomination, shipWallets } =
useSettings();
const [signing, setSigning] = useState(false);
const [denomAmount, setDenomAmount] = useState('0.00');
const [satsAmount, setSatsAmount] = useState('0');
const [payee, setPayee] = useState('');
const [checkingPatp, setCheckingPatp] = useState(false);
const [payeeType, setPayeeType] = useState('');
const [ready, setReady] = useState(false);
const [validPayee, setValidPayee] = useState(false);
const [focusedField, setFocusedField] = useState(focusFields.empty);
const [feeChoices, setFeeChoices] = useState({
low: [10, 1],
mid: [10, 1],
high: [10, 1],
},
feeValue: "mid",
showModal: false,
note: '',
choosingSignMethod: false,
signMethod: 'bridge',
});
const [feeValue, setFeeValue] = useState('mid');
const [showModal, setShowModal] = useState(false);
const [note, setNote] = useState('');
const [choosingSignMethod, setChoosingSignMethod] = useState(false);
const [signMethod, setSignMethod] = useState('bridge');
const feeDismiss = () => {
setShowModal(false);
};
this.initPayment = this.initPayment.bind(this);
this.checkPayee = this.checkPayee.bind(this);
this.feeSelect = this.feeSelect.bind(this);
this.feeDismiss = this.feeDismiss.bind(this);
this.toggleSignMethod = this.toggleSignMethod.bind(this);
this.setSignMethod = this.setSignMethod.bind(this);
}
const feeSelect = (which) => {
setFeeValue(which);
};
feeDismiss() {
this.setState({showModal: 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 handleSetSignMethod = (signMethod) => {
setSignMethod(signMethod);
setChoosingSignMethod(false);
};
const checkPayee = (e) => {
console.log('checkPayee', { e });
setError('');
let payeeReceived = e.target.value;
let isPatp = ob.isValidPatp(payeeReceived);
let isAddress = validate(payeeReceived);
console.log({ payeeReceived, isPatp, isAddress });
if (isPatp) {
let command = {'check-payee': payee}
this.props.api.btcWalletCommand(command)
console.log('isPatp', isPatp);
let command = { 'check-payee': payeeReceived };
api.btcWalletCommand(command);
setTimeout(() => {
this.setState({checkingPatp: false});
setCheckingPatp(false);
}, 5000);
this.setState({
checkingPatp: true,
payeeType: 'ship',
payee,
});
setCheckingPatp(true);
setPayeeType('ship');
setPayee(payeeReceived);
} else if (isAddress) {
this.setState({
payee,
ready: true,
checkingPatp: false,
payeeType: 'address',
validPayee: true,
});
setPayee(payeeReceived);
setReady(true);
setCheckingPatp(false);
setPayeeType('address');
setValidPayee(true);
} else {
this.setState({
payee,
ready: false,
checkingPatp: false,
payeeType: '',
validPayee: false,
});
}
setPayee(payeeReceived);
setReady(false);
setCheckingPatp(false);
setPayeeType('');
setValidPayee(false);
}
};
componentDidUpdate(prevProps, prevState) {
if ((prevProps.error !== this.props.error) &&
(this.props.error !== '') && (this.props.error !== 'broadcast-fail')) {
this.setState({signing: false});
}
const toggleSignMethod = () => {
setChoosingSignMethod(!choosingSignMethod);
};
if (!this.state.ready && this.state.checkingPatp) {
if (this.props.shipWallets[this.state.payee.slice(1)]) {
this.setState({ready: true, checkingPatp: false, validPayee: true});
}
}
}
toggleSignMethod(toggle) {
this.setState({choosingSignMethod: !toggle});
}
initPayment() {
if (this.state.payeeType === 'ship') {
const initPayment = () => {
if (payeeType === 'ship') {
let command = {
'init-payment': {
'payee': this.state.payee,
'value': parseInt(this.state.satsAmount),
'feyb': this.state.feeChoices[this.state.feeValue][1],
'note': (this.state.note || null),
}
}
this.props.api.btcWalletCommand(command).then(res => this.setState({signing: true}));
} else if (this.state.payeeType === 'address') {
payee,
value: parseInt(satsAmount),
feyb: feeChoices[feeValue][1],
note: note || null,
},
};
api.btcWalletCommand(command).then(() => setSigning(true));
} else if (payeeType === 'address') {
let command = {
'init-payment-external': {
'address': this.state.payee,
'value': parseInt(this.state.satsAmount),
'feyb': 1,
'note': (this.state.note || null),
address: payee,
value: parseInt(satsAmount),
feyb: 1,
note: note || null,
},
};
api.btcWalletCommand(command).then(() => setSigning(true));
}
};
useEffect(() => {
if (network === 'bitcoin') {
let url = 'https://bitcoiner.live/api/fees/estimates/latest';
fetch(url)
.then((res) => res.json())
.then((n) => {
// let estimates = Object.keys(n.estimates);
// let mid = Math.floor(estimates.length / 2);
// let high = estimates.length - 1;
setFeeChoices({
high: [30, n.estimates[30]['sat_per_vbyte']],
mid: [180, n.estimates[180]['sat_per_vbyte']],
low: [360, n.estimates[360]['sat_per_vbyte']],
});
});
}
}, []);
useEffect(() => {
if (!ready && checkingPatp) {
console.log({ ready, checkingPatp, shipWallets, payee });
if (shipWallets.payee === payee.slice(1) && shipWallets.hasWallet) {
console.log('good');
setReady(true);
setCheckingPatp(false);
setValidPayee(true);
}
}
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() {
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;
const signReady = ready && parseInt(satsAmount) > 0 && !signing;
let invoice = null;
if (signMethod === 'masterTicket') {
invoice =
invoice = (
<Invoice
network={network}
api={api}
psbt={psbt}
fee={fee}
currencyRates={currencyRates}
stopSending={stopSending}
payee={payee}
denomination={denomination}
satsAmount={satsAmount}
state={this.props.state}
/>
);
} else if (signMethod === 'bridge') {
invoice =
invoice = (
<BridgeInvoice
state={this.props.state}
api={api}
psbt={psbt}
fee={fee}
currencyRates={currencyRates}
stopSending={stopSending}
payee={payee}
denomination={denomination}
satsAmount={satsAmount}
/>
);
}
return (
<>
{ (signing && psbt) ? invoice :
{signing && psbt ? (
invoice
) : (
<Col
width='100%'
backgroundColor='white'
borderRadius='48px'
width="100%"
backgroundColor="white"
borderRadius="48px"
mb={5}
p={5}
>
<Col width="100%">
<Row
justifyContent='space-between'
alignItems='center'
>
<Text highlight color='blue' fontSize={1}>Send BTC</Text>
<Text highlight color='blue' fontSize={1}>{value}</Text>
<Icon
icon='X'
cursor='pointer'
onClick={() => stopSending()}
/>
<Row justifyContent="space-between" alignItems="center">
<Text highlight color="blue" fontSize={1}>
Send BTC
</Text>
<Text highlight color="blue" fontSize={1}>
{value}
</Text>
<Icon icon="X" cursor="pointer" onClick={() => stopSending()} />
</Row>
<Row alignItems="center" mt={6} justifyContent="space-between">
<Row
alignItems='center'
mt={6}
justifyContent='space-between'>
<Row justifyContent="space-between" width='calc(40% - 30px)' alignItems="center">
<Text gray fontSize={1} fontWeight='600'>To</Text>
{this.state.checkingPatp ?
<LoadingSpinner background="midOrange" foreground="orange"/> : null
}
justifyContent="space-between"
width="calc(40% - 30px)"
alignItems="center"
>
<Text gray fontSize={1} fontWeight="600">
To
</Text>
{checkingPatp ? (
<LoadingSpinner background="midOrange" foreground="orange" />
) : null}
</Row>
<Input
autoFocus
onFocus={() => {this.setState({focusPayee: true})}}
onBlur={() => {this.setState({focusPayee: false})}}
// autoFocus
onFocus={() => {
setFocusedField(focusFields.payee);
}}
onBlur={() => {
setFocusedField(focusFields.empty);
}}
color={payeeColor}
backgroundColor={payeeBg}
borderColor={payeeBorder}
ml={2}
flexGrow="1"
fontSize='14px'
placeholder='~sampel-palnet or BTC address'
fontSize="14px"
placeholder="~sampel-palnet or BTC address"
value={payee}
fontFamily="mono"
disabled={signing}
onChange={this.checkPayee}
onChange={(e) => checkPayee(e)}
/>
</Row>
{error &&
<Row
alignItems='center'
justifyContent='space-between'>
{error && (
<Row alignItems="center" justifyContent="space-between">
{/* yes this is a hack */}
<Box width='calc(40% - 30px)'/>
<Box width="calc(40% - 30px)" />
<Error
error={error}
fontSize='14px'
fontSize="14px"
ml={2}
mt={2}
width='100%' />
width="100%"
/>
</Row>
}
<Row
alignItems='center'
mt={4}
justifyContent='space-between'>
<Text
gray
fontSize={1}
fontWeight='600'
width="40%"
>Amount</Text>
)}
<Row alignItems="center" mt={4} justifyContent="space-between">
<Text gray fontSize={1} fontWeight="600" width="40%">
Amount
</Text>
<Input
onFocus={() => {this.setState({focusCurrency: true})}}
onBlur={() => {this.setState({focusCurrency: false})}}
fontSize='14px'
width='100%'
type='number'
borderColor={this.state.focusCurrency ? "lightGray" : "none"}
onFocus={() => {
setFocusedField(focusFields.currency);
}}
onBlur={() => {
setFocusedField(focusFields.empty);
}}
fontSize="14px"
width="100%"
type="number"
borderColor={
focusedField === focusFields.currency ? 'lightGray' : 'none'
}
disabled={signing}
value={denomAmount}
onChange={e => {
this.setState({
denomAmount: e.target.value,
satsAmount: Math.round(parseFloat(e.target.value) / conversion * 100000000)
});
onChange={(e) => {
setDenomAmount(e.target.value);
setSatsAmount(
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
alignItems='center'
mt={2}
justifyContent='space-between'>
<Row alignItems="center" mt={2} justifyContent="space-between">
{/* yes this is a hack */}
<Box width='40%'/>
<Box width="40%" />
<Input
onFocus={() => {this.setState({focusSats: true})}}
onBlur={() => {this.setState({focusSats: false})}}
fontSize='14px'
width='100%'
type='number'
borderColor={this.state.focusSats ? "lightGray" : "none"}
onFocus={() => {
setFocusedField(focusFields.sats);
}}
onBlur={() => {
setFocusedField(focusFields.empty);
}}
fontSize="14px"
width="100%"
type="number"
borderColor={
focusedField === focusFields.sats ? 'lightGray' : 'none'
}
disabled={signing}
value={satsAmount}
onChange={e => {
this.setState({
denomAmount: parseFloat(e.target.value) * (conversion / 100000000),
satsAmount: e.target.value
});
onChange={(e) => {
setDenomAmount(
parseFloat(e.target.value) * (conversion / 100000000)
);
setSatsAmount(e.target.value);
}}
/>
<Text color="lightGray" fontSize={1} ml={3}>sats</Text>
<Text color="lightGray" fontSize={1} ml={3}>
sats
</Text>
</Row>
<Row mt={4} width="100%" justifyContent="space-between">
<Text
gray
fontSize={1}
fontWeight='600'
width="40%"
>Fee</Text>
<Text gray fontSize={1} fontWeight="600" width="40%">
Fee
</Text>
<Row alignItems="center">
<Text mr={2} color="lightGray" fontSize="14px">
{this.state.feeChoices[this.state.feeValue][1]} sats/vbyte
{feeChoices[feeValue][1]} sats/vbyte
</Text>
<Icon icon="ChevronSouth"
<Icon
icon="ChevronSouth"
fontSize="14px"
color="lightGray"
onClick={() => {if (!this.state.showModal) this.setState({showModal: true}); }}
cursor="pointer"/>
onClick={() => {
if (!showModal) setShowModal(true);
}}
cursor="pointer"
/>
</Row>
</Row>
<Col alignItems="center">
{!this.state.showModal ? null :
{!showModal ? null : (
<FeePicker
feeChoices={this.state.feeChoices}
feeSelect={this.feeSelect}
feeDismiss={this.feeDismiss}
feeChoices={feeChoices}
feeSelect={feeSelect}
feeDismiss={feeDismiss}
/>
}
)}
</Col>
<Row mt={4} width="100%"
<Row
mt={4}
width="100%"
justifyContent="space-between"
alignItems='center'
alignItems="center"
>
<Text
gray
fontSize={1}
fontWeight='600'
width="40%"
>Note</Text>
<Text gray fontSize={1} fontWeight="600" width="40%">
Note
</Text>
<Input
onFocus={() => {this.setState({focusNote: true})}}
onBlur={() => {this.setState({focusNote: false})}}
fontSize='14px'
width='100%'
onFocus={() => {
setFocusedField(focusFields.note);
}}
onBlur={() => {
setFocusedField(focusFields.empty);
}}
fontSize="14px"
width="100%"
placeholder="What's this for?"
type='text'
borderColor={this.state.focusNote ? "lightGray" : "none"}
type="text"
borderColor={
focusedField === focusFields.note ? 'lightGray' : 'none'
}
disabled={signing}
value={this.state.note}
onChange={e => {
this.setState({
note: e.target.value,
});
value={note}
onChange={(e) => {
setNote(e.target.value);
}}
/>
</Row>
</Col>
<Row
flexDirection='row-reverse'
alignItems="center"
mt={4}
>
<Row flexDirection="row-reverse" alignItems="center" mt={4}>
<Signer
signReady={signReady}
choosingSignMethod={choosingSignMethod}
signMethod={signMethod}
setSignMethod={this.setSignMethod}
initPayment={this.initPayment} />
{ (!(signing && !error)) ? null :
<LoadingSpinner mr={2} background="midOrange" foreground="orange"/>
}
setSignMethod={handleSetSignMethod}
initPayment={initPayment}
/>
{!(signing && !error) ? null : (
<LoadingSpinner
mr={2}
background="midOrange"
foreground="orange"
/>
)}
<Button
width='48px'
children={
width="48px"
fontSize={1}
fontWeight="bold"
borderRadius="24px"
mr={2}
height="48px"
onClick={() => toggleSignMethod(choosingSignMethod)}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={
signReady ? 'rgba(33, 157, 255, 0.2)' : 'veryLightGray'
}
disabled={!signReady}
border="none"
style={{ cursor: signReady ? 'pointer' : 'default' }}
>
<Icon
icon={choosingSignMethod ? 'X' : 'Ellipsis'}
color={signReady ? 'blue' : 'lighterGray'}
/>
}
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'}} />
</Button>
</Row>
{signMethod === 'masterTicket' &&
<Row
mt={4}
alignItems='center'
>
<Icon icon='Info' color='yellow' height={4} width={4}/>
{signMethod === 'masterTicket' && (
<Row mt={4} alignItems="center">
<Icon icon="Info" color="yellow" height={4} width={4} />
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
We recommend that you sign transactions using Bridge to protect your master ticket.
We recommend that you sign transactions using Bridge to protect
your master ticket.
</Text>
</Row>
}
)}
</Col>
}
)}
</>
);
}
}
};
export default Send;

View File

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

View File

@ -1,41 +1,31 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import React from 'react';
import { Row, Text, Button, Col } from '@tlon/indigo-react';
import { useSettings } from '../../hooks/useSettings';
import { api } from '../../api';
export default class Settings extends Component {
constructor(props) {
super(props);
this.changeProvider = this.changeProvider.bind(this);
this.replaceWallet = this.replaceWallet.bind(this);
}
const Settings = () => {
const { wallet, provider } = useSettings();
changeProvider(){
this.props.api.btcWalletCommand({'set-provider': null});
}
const changeProvider = () => {
api.btcWalletCommand({ 'set-provider': null });
};
replaceWallet(){
this.props.api.btcWalletCommand({
'delete-wallet': this.props.state.wallet,
const replaceWallet = () => {
api.btcWalletCommand({
'delete-wallet': wallet,
});
}
};
render() {
let connColor = "red";
let connBackground = "veryLightRed";
let conn = 'Offline'
let connColor = 'red';
let connBackground = 'veryLightRed';
let conn = 'Offline';
let host = '';
if (this.props.state.provider){
if (this.props.state.provider.connected) conn = 'Connected';
if (this.props.state.provider.host) host = this.props.state.provider.host;
if (this.props.state.provider.connected && this.props.state.provider.host) {
connColor = "orange";
connBackground = "lightOrange";
if (provider) {
if (provider.connected) conn = 'Connected';
if (provider.host) host = provider.host;
if (provider.connected && provider.host) {
connColor = 'orange';
connBackground = 'lightOrange';
}
}
@ -53,7 +43,8 @@ export default class Settings extends Component {
XPub Derivation
</Text>
</Row>
<Row borderRadius="12px"
<Row
borderRadius="12px"
backgroundColor="veryLightGray"
py={5}
px="36px"
@ -61,16 +52,12 @@ export default class Settings extends Component {
alignItems="center"
justifyContent="space-between"
>
<Text mono
fontSize={1}
style={{wordBreak: "break-all"}}
color="gray"
>
{this.props.state.wallet}
<Text mono fontSize={1} style={{ wordBreak: 'break-all' }} color="gray">
{wallet}
</Text>
</Row>
<Row width="100%" mb={5}>
<Button children="Replace Wallet"
<Button
width="100%"
fontSize={1}
fontWeight="bold"
@ -79,15 +66,18 @@ export default class Settings extends Component {
borderColor="none"
borderRadius="12px"
p={4}
onClick={this.replaceWallet}
/>
onClick={() => replaceWallet()}
>
Replace Wallet
</Button>
</Row>
<Row mb="12px">
<Text fontSize={1} fontWeight="bold" color="black">
BTC Node Provider
</Text>
</Row>
<Col mb="12px"
<Col
mb="12px"
py={5}
px="36px"
borderRadius="12px"
@ -103,7 +93,7 @@ export default class Settings extends Component {
</Text>
</Col>
<Row width="100%">
<Button children="Change Provider"
<Button
width="100%"
fontSize={1}
fontWeight="bold"
@ -112,10 +102,13 @@ export default class Settings extends Component {
borderColor="none"
borderRadius="12px"
p={4}
onClick={this.changeProvider}
/>
onClick={() => changeProvider()}
>
Change Provider
</Button>
</Row>
</Col>
);
}
}
};
export default Settings;

View File

@ -1,52 +1,55 @@
import React, { Component } from 'react';
import React from 'react';
import { Box, Button } from '@tlon/indigo-react';
import {
Box,
Button,
} from '@tlon/indigo-react';
export default function Signer(props) {
const { signReady, initPayment, choosingSignMethod, signMethod, setSignMethod } = props;
return (
choosingSignMethod ?
<Box
borderRadius='24px'
backgroundColor='rgba(33, 157, 255, 0.2)'
>
const Signer = ({
signReady,
initPayment,
choosingSignMethod,
signMethod,
setSignMethod,
}) => {
return choosingSignMethod ? (
<Box borderRadius="24px" backgroundColor="rgba(33, 157, 255, 0.2)">
<Button
border='none'
backgroundColor='transparent'
fontWeight='bold'
cursor='pointer'
color={(signMethod === 'masterTicket') ? 'blue' : 'lightBlue'}
height='48px'
border="none"
backgroundColor="transparent"
fontWeight="bold"
cursor="pointer"
color={signMethod === 'masterTicket' ? 'blue' : 'lightBlue'}
height="48px"
onClick={() => setSignMethod('masterTicket')}
children='Sign with Master Ticket' />
>
Sign with Master Ticket
</Button>
<Button
border='none'
backgroundColor='transparent'
fontWeight='bold'
cursor='pointer'
color={(signMethod === 'bridge') ? 'blue' : 'lightBlue'}
height='48px'
border="none"
backgroundColor="transparent"
fontWeight="bold"
cursor="pointer"
color={signMethod === 'bridge' ? 'blue' : 'lightBlue'}
height="48px"
onClick={() => setSignMethod('bridge')}
children='Sign with Bridge' />
>
Sign with Bridge
</Button>
</Box>
:
) : (
<Button
primary
children={signMethod === 'bridge' ? 'Sign with Bridge' : 'Sign with Master Ticket'}
fontSize={1}
fontWeight='bold'
borderRadius='24px'
height='48px'
fontWeight="bold"
borderRadius="24px"
height="48px"
onClick={initPayment}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={signReady ? 'blue' : 'veryLightGray'}
disabled={!signReady}
border='none'
border="none"
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 WalletModal from './walletModal.js';
import ProviderModal from './providerModal.js';
import { useSettings } from '../../hooks/useSettings.js';
import WalletModal from './walletModal.js'
import ProviderModal from './providerModal.js'
export default class StartupModal extends Component {
constructor(props) {
super(props);
}
render() {
const StartupModal = () => {
const { wallet, provider } = useSettings();
let modal = null;
if (this.props.state.wallet && this.props.state.provider) {
if (wallet && provider) {
return null;
} else if (!this.props.state.provider){
modal =
<ProviderModal
api={this.props.api}
providerPerms={this.props.state.providerPerms}
/>
} else if (!this.props.state.wallet){
modal = <WalletModal api={this.props.api} network={this.props.network}/>
} else if (!provider) {
modal = <ProviderModal />;
} else if (!wallet) {
modal = <WalletModal />;
}
return (
<Box
@ -38,9 +28,10 @@ export default class StartupModal extends Component {
justifyContent="center"
alignItems="center"
>
<Box display="flex"
<Box
display="flex"
flexDirection="column"
width='400px'
width="400px"
backgroundColor="white"
borderRadius={3}
>
@ -48,5 +39,6 @@ export default class StartupModal extends Component {
</Box>
</Box>
);
}
}
};
export default StartupModal;

View File

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

View File

@ -1,26 +1,14 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import { Box, Text, Col } from '@tlon/indigo-react';
import Transaction from './transaction.js';
import { useSettings } from '../../hooks/useSettings.js';
export default class Transactions extends Component {
constructor(props) {
super(props);
}
render() {
if (!this.props.state.history || this.props.state.history.length <= 0) {
const Transactions = () => {
const { history } = useSettings();
if (!history || history.length <= 0) {
return (
<Box alignItems="center"
<Box
alignItems="center"
display="flex"
justifyContent="center"
height="340px"
@ -30,33 +18,26 @@ export default class Transactions extends Component {
borderRadius="48px"
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>
);
} else {
return (
<Col
width='100%'
width="100%"
backgroundColor="white"
borderRadius="48px"
mb={5}
p={5}
>
{
this.props.state.history.map((tx, i) => {
return(
<Transaction
tx={tx}
key={i}
denom={this.props.state.denomination}
rates={this.props.state.currencyRates}
network={this.props.network}
/>
);
})
}
{history.map((tx, i) => {
return <Transaction tx={tx} key={i} />;
})}
</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 { useSettings } from '../../hooks/useSettings';
export default class TxAction extends Component {
constructor(props) {
super(props);
}
render() {
const TxAction = ({ action, pending, txid }) => {
const { network } = useSettings();
const leftIcon =
this.props.action === 'sent'
action === 'sent'
? 'ArrowSouth'
: this.props.action === 'recv'
: action === 'recv'
? 'ArrowNorth'
: this.props.action === 'fail'
: action === 'fail'
? 'X'
: 'NullIcon';
const actionColor =
this.props.action === 'sent'
action === 'sent'
? 'sentBlue'
: this.props.action === 'recv'
: action === 'recv'
? 'recvGreen'
: this.props.action === 'fail'
: action === 'fail'
? 'gray'
: 'red';
const actionText =
this.props.action === 'sent' && !this.props.pending
action === 'sent' && !pending
? 'Sent BTC'
: this.props.action === 'sent' && this.props.pending
: action === 'sent' && pending
? 'Sending BTC'
: this.props.action === 'recv' && !this.props.pending
: action === 'recv' && !pending
? 'Received BTC'
: this.props.action === 'recv' && this.props.pending
: action === 'recv' && pending
? 'Receiving BTC'
: this.props.action === 'fail'
: action === 'fail'
? 'Failed'
: 'error';
const pending = !this.props.pending ? null : (
const pendingSpinner = !pending ? null : (
<LoadingSpinner background="midOrange" foreground="orange" />
);
const url =
this.props.network === 'testnet'
? `http://blockstream.info/testnet/tx/${this.props.txid}`
: `http://blockstream.info/tx/${this.props.txid}`;
network === 'testnet'
? `http://blockstream.info/testnet/tx/${txid}`
: `http://blockstream.info/tx/${txid}`;
return (
<Row alignItems="center">
@ -67,8 +64,9 @@ export default class TxAction extends Component {
<a href={url} target="_blank" rel="noreferrer">
<Icon color={actionColor} icon="ArrowNorthEast" ml={1} mr={2} />
</a>
{pending}
{pendingSpinner}
</Row>
);
}
}
};
export default TxAction;

View File

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

View File

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

View File

@ -1,68 +1,58 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
Anchor,
} from '@tlon/indigo-react';
import React from 'react';
import { Box, Text, Button, Col, Anchor } from '@tlon/indigo-react';
import { api } from '../../api';
import { useSettings } from '../../hooks/useSettings';
import { store } from '../../store'
export default class Warning extends Component {
constructor(props) {
super(props);
this.understand = this.understand.bind(this);
}
understand(){
store.handleEvent({ data: { bucket: { warning: false}}});
const Warning = () => {
const { setWarning } = useSettings();
const understand = () => {
setWarning(false);
let removeWarning = {
"put-entry": {
'put-entry': {
value: false,
"entry-key": "warning",
"bucket-key": "btc-wallet",
}
}
this.props.api.settingsEvent(removeWarning);
}
'entry-key': 'warning',
'bucket-key': 'btc-wallet',
},
};
api.settingsEvent(removeWarning);
};
render() {
return (
<Box
backgroundColor="red"
color="white"
borderRadius="32px"
justifyContent="space-between"
width='100%'
width="100%"
p={5}
mb={5}
>
<Col>
<Text color="white" fontWeight='bold' fontSize={1}>
<Text color="white" fontWeight="bold" fontSize={1}>
Warning!
</Text>
<br />
<Text color="white" fontWeight='bold' fontSize={1}>
Be safe while using this wallet, and be sure to store responsible amounts
of BTC.
<Text color="white" fontWeight="bold" fontSize={1}>
Be safe while using this wallet, and be sure to store responsible
amounts of BTC.
</Text>
<Text color="white" fontWeight='bold' fontSize={1}>
Always ensure that the checksum of the wallet matches that of the wallet's repo.
<Text color="white" fontWeight="bold" fontSize={1}>
Always ensure that the checksum of the wallet matches that of the
wallet&apos;s repo.
</Text>
<br />
<Anchor href="https://urbit.org/bitcoin-wallet" target="_blank">
<Text color="white" fontWeight="bold" fontSize={1} style={{textDecoration:'underline'}}>
<Text
color="white"
fontWeight="bold"
fontSize={1}
style={{ textDecoration: 'underline' }}
>
Learn more on urbit.org
</Text>
</Anchor>
</Col>
<Button children="I Understand"
<Button
backgroundColor="white"
fontSize={1}
mt={5}
@ -71,9 +61,12 @@ export default class Warning extends Component {
borderRadius="24px"
p="24px"
borderColor="none"
onClick={this.understand}
/>
onClick={() => understand()}
>
I understand
</Button>
</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 { api } from '../api.js';
import { store } from '../store.js';
import { ThemeProvider } from 'styled-components';
import light from './themes/light';
// import dark from './themes/dark';
import { Box, Reset } from '@tlon/indigo-react';
import StartupModal from './lib/startupModal.js';
import Body from './lib/body.js';
import { subscription } from '../subscription.js';
import { useSettings } from '../hooks/useSettings.js';
const network = 'bitcoin';
export class Root extends Component {
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);
const Root = () => {
const { loaded, wallet, provider } = useSettings();
const blur = !loaded ? false : !(wallet && provider);
return (
<BrowserRouter>
<ThemeProvider theme={light}>
<Reset />
{loaded ? (
<StartupModal api={api} state={this.state} network={network} />
) : null}
{loaded ? <StartupModal /> : null}
<Box
display="flex"
flexDirection="column"
@ -52,16 +29,11 @@ export class Root extends Component {
px={[0, 4]}
pb={[0, 4]}
>
<Body
loaded={loaded}
state={this.state}
api={api}
network={network}
warning={warning}
/>
<Body />
</Box>
</ThemeProvider>
</BrowserRouter>
);
}
}
};
export default Root;

View File

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

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;