Feat/v0.2.0 (#19)

* chore: bump version

* style: password field changes

* Feat/lerna integration (#17)

* chore: move into package folder

* chore: initial lerna setup

* chore: yarn lock cleanup

* chore: package organization

* chore: yarn lock update

* chore: update packages

* chore: wip lerna integration

* chore: downgrade date-fns

* chore: lerna cleanup

* chore: add env file

* docs: update README

* chore: organize configs

* Feat/hodlhodl integration (#18)

* feat: hodl integration start

* chore: add pagination and more

* chore: change filter flow

* chore: change offer card ui

* chore: more offer card changes

* chore: small refactors and stylings

* v0.2.0

* docs: update README
This commit is contained in:
Anthony Potdevin 2020-04-08 20:06:59 +02:00 committed by GitHub
parent 215b3355fe
commit 22196e8468
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 6372 additions and 7841 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

View file

@ -1,7 +1,7 @@
# **ThunderHub - Lightning Node Manager**
![Home Screenshot](assets/Home.png)
[![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE) [![Known Vulnerabilities](https://snyk.io/test/github/apotdevin/thunderhub/badge.svg?targetFile=client/package.json)](https://snyk.io/test/github/apotdevin/thunderhub) [![Known Vulnerabilities](https://snyk.io/test/github/apotdevin/thunderhub/badge.svg?targetFile=server/package.json)](https://snyk.io/test/github/apotdevin/thunderhub)
[![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE) [![Known Vulnerabilities](https://snyk.io/test/github/apotdevin/thunderhub/badge.svg?targetFile=client/package.json)](https://snyk.io/test/github/apotdevin/thunderhub) [![Known Vulnerabilities](https://snyk.io/test/github/apotdevin/thunderhub/badge.svg?targetFile=server/package.json)](https://snyk.io/test/github/apotdevin/thunderhub) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/)
## Table Of Contents
@ -16,6 +16,8 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
### Tech Stack
The repository consists of two packages (client and server) and is maintained with LernaJS and Yarn Workspaces.
#### Client
[![Known Vulnerabilities](https://snyk.io/test/github/apotdevin/thunderhub/badge.svg?targetFile=client/package.json)](https://snyk.io/test/github/apotdevin/thunderhub)
@ -48,6 +50,7 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
- View all transactions.
- View all forwarded payments.
- View all chain transactions.
- View all unspent UTXOS.
### Management
@ -58,6 +61,7 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
- Balance your channels through circular payments. ([Check out the Tutorial](https://medium.com/coinmonks/lightning-network-channel-balancing-with-thunderhub-972b41bf9243))
- Update your all your channels fees or individual ones.
- Backup, verify and recover all your channels.
- Sign and verify messages.
### Visual
@ -69,13 +73,18 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
- Many ways to connect to your node: **HEX/Base64 strings**, **LNDConnect Url**, **BTCPayServer Info** or **QR codes**.
- Have view-only and/or admin accounts.
- Manage up to 10 different nodes.
- Manage however many accounts your browser storage can hold.
- Quickly sync your accounts between devices. No need to copy/paste macaroons and certificates.
### Deployment
- Docker images for easier deployment (WIP)
### Future Features
- Loop In and Out to provide liquidity or remove it from your channels.
- Docker Image for easy deployments.
- Integration with HodlHodl
- Storefront interface
## Installation
@ -88,26 +97,19 @@ git clone https://github.com/apotdevin/thunderhub.git
### **Requirements**
- Node installed
- Yarn or NPM installed
- Yarn installed
After cloning the repository, go into both the `/client` and the `/server` folders, and run `yarn` or `npm install` in both of them to get all the necessary modules installed.
After cloning the repository run `yarn` to get all the necessary modules installed. Yarn workspaces will handle installing modules for both the client and the server.
### **ThunderHub - Server**
To be able to use the HodlHodl integration create a `.env` file in the `/server` folder with `HODL_KEY='[YOUR API KEY]'` and replace `[YOUR API KEY]` with the one that HodlHodl provides you.
#### To get the server running use the following commands
##### This must be done in the `/server` folder
```javascript
//With yarn:
yarn build
yarn start
```
```javascript
//With npm:
npm run build
npm run start
yarn server:prod
yarn server:run
```
If the server starts succesfully, you should see `info [server.js]: Server ready at http://localhost:3001/` in the terminal
@ -119,51 +121,29 @@ If the server starts succesfully, you should see `info [server.js]: Server ready
##### This must be done in the `/client` folder
```javascript
//With yarn:
yarn start
```
```javascript
//With npm:
npm run start
```
If the frontend starts succesfully, you should see `Compiled successfully! You can now view app in the browser.` in the terminal and a browser window should have opened in your browser.
## Development
If you want to develop on ThunderHub and want hot reloading when you do changes use the following commands
If you want to develop on ThunderHub and want hot reloading when you do changes, use the following commands:
### ThunderHub - Server
```javascript
//With yarn:
yarn build:dev
// In another terminal
yarn dev
```
```javascript
//With npm:
npm run build:dev
// In another terminal
npm run dev
yarn server:dev
```
### ThunderHub - Client
Running the commands `yarn start` or `npm run start` works for development.
Running the commands `yarn start` in the `client` folder works for development.
#### Storybook
You can also get storybook running for quicker component development.
```javascript
//With yarn:
yarn storybook
```
```javascript
//With npm:
npm run storybook
```

View file

@ -7,7 +7,7 @@ module.exports = {
'@storybook/addon-links',
'@storybook/addon-viewport/register',
],
webpackFinal: async config => {
webpackFinal: async (config) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [

View file

@ -17,7 +17,7 @@ const StyledBackground = styled.div`
align-items: center;
`;
const ThemeDecorator = storyFn => {
const ThemeDecorator = (storyFn) => {
const background = boolean('No Background', false);
const cardBackground = boolean('Card Background', true);
return (

View file

@ -1,6 +1,6 @@
{
"name": "thunderhub-client",
"version": "0.1.17",
"name": "@thunderhub/client",
"version": "0.2.0",
"description": "",
"scripts": {
"start": "react-scripts start",
@ -9,7 +9,8 @@
"eject": "react-scripts eject",
"storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public",
"deploy": "yarn build && aws s3 --profile EBFullAccess sync build/ s3://thunderhub-client"
"deploy": "yarn build && aws s3 --profile EBFullAccess sync build/ s3://thunderhub-client",
"precommit": "pretty-quick --staged"
},
"repository": {
"type": "git",
@ -21,14 +22,14 @@
"dependencies": {
"@apollo/react-hooks": "^3.1.3",
"@types/crypto-js": "^3.1.44",
"@types/jest": "25.1.4",
"@types/jest": "25.1.5",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.merge": "^4.6.6",
"@types/lodash.sortby": "^4.7.6",
"@types/node": "13.9.8",
"@types/node": "13.11.0",
"@types/numeral": "^0.0.26",
"@types/qrcode.react": "^1.0.0",
"@types/react": "16.9.31",
"@types/react": "16.9.32",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "16.9.6",
"@types/react-modal": "^3.10.5",
@ -42,10 +43,7 @@
"@types/victory": "^33.1.4",
"@types/zxcvbn": "^4.4.0",
"apollo-boost": "^0.4.4",
"base64url": "^3.0.1",
"crypto-js": "^4.0.0",
"date-fns": "^2.11.0",
"graphql": "^14.6.0",
"intersection-observer": "^0.7.0",
"lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2",
@ -53,6 +51,7 @@
"node-sass": "^4.13.0",
"numeral": "^2.0.6",
"qrcode.react": "^1.0.0",
"qs": "^6.9.3",
"react": "^16.13.0",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "^16.13.0",
@ -83,9 +82,6 @@
"@storybook/preset-create-react-app": "^2.1.1",
"@storybook/react": "^5.3.18",
"awesome-typescript-loader": "^5.2.1",
"husky": "^4.2.3",
"prettier": "2.0.2",
"pretty-quick": "^2.0.1",
"react-docgen-typescript-loader": "^3.7.2"
},
"eslintConfig": {
@ -102,10 +98,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
}
}

View file

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-credit-card"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect><line x1="1" y1="10" x2="23" y2="10"></line></svg>

After

Width:  |  Height:  |  Size: 329 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-half-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon><polygon fill="currentColor" points="12 2 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>

After

Width:  |  Height:  |  Size: 438 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>

After

Width:  |  Height:  |  Size: 339 B

View file

@ -2,7 +2,7 @@ import React from 'react';
import { useSpring, animated } from 'react-spring';
import { getValue } from '../../helpers/Helpers';
import { useSettings } from '../../context/SettingsContext';
import { usePriceState } from 'context/PriceContext';
import { usePriceState } from '../../context/PriceContext';
type PriceProps = {
price: number;
@ -43,7 +43,7 @@ export const AnimatedNumber = ({ amount }: AnimatedProps) => {
return (
<animated.div>
{value.interpolate(amount => getValue({ amount, ...priceProps }))}
{value.interpolate((amount) => getValue({ amount, ...priceProps }))}
</animated.div>
);
};

View file

@ -26,7 +26,7 @@ export const PasswordInput = ({
<SubTitle>Please Input a Password</SubTitle>
<Line>
<Sub4Title>Password:</Sub4Title>
<Input onChange={e => setPass(e.target.value)} />
<Input onChange={(e) => setPass(e.target.value)} />
</Line>
<Line>
<Sub4Title>Strength:</Sub4Title>

View file

@ -12,9 +12,10 @@ import styled from 'styled-components';
import { useAccount } from '../../../context/AccountContext';
import { saveSessionAuth } from '../../../utils/auth';
import { useSettings } from '../../../context/SettingsContext';
import { textColorMap } from '../../../styles/Themes';
import { textColorMap, mediaDimensions } from '../../../styles/Themes';
import { ColorButton } from '../colorButton/ColorButton';
import { Input } from '../../input/Input';
import { useSize } from 'hooks/UseSize';
const RadioText = styled.div`
margin-left: 10px;
@ -42,7 +43,9 @@ export const LoginModal = ({
callback,
variables,
}: LoginProps) => {
const { width } = useSize();
const { theme } = useSettings();
const [pass, setPass] = useState<string>('');
const [storeSession, setStoreSession] = useState<boolean>(false);
const { host, cert, refreshAccount } = useAccount();
@ -83,7 +86,13 @@ export const LoginModal = ({
<SubTitle>Unlock your Account</SubTitle>
<ResponsiveLine>
<Sub4Title>Password:</Sub4Title>
<Input onChange={(e) => setPass(e.target.value)} />
<Input
withMargin={
width <= mediaDimensions.mobile ? '0' : '0 0 0 16px'
}
type={'password'}
onChange={(e) => setPass(e.target.value)}
/>
</ResponsiveLine>
<ResponsiveLine>
<NoWrapTitle>Don't ask me again this session:</NoWrapTitle>

View file

@ -1,7 +1,8 @@
import React from 'react';
import { SmallLink, DarkSubTitle, OverflowText } from './Styled';
import { SmallLink, DarkSubTitle, OverflowText, SingleLine } from './Styled';
import { StatusDot, DetailLine } from '../../views/channels/Channels.style';
import { formatDistanceStrict, format } from 'date-fns';
import { XSvg } from './Icons';
export const getTransactionLink = (transaction: string) => {
const link = `https://www.blockchain.com/btc/tx/${transaction}`;
@ -51,12 +52,23 @@ export const renderLine = (
title: string,
content: any,
key?: string | number,
deleteCallback?: () => void,
) => {
if (!content) return null;
return (
<DetailLine key={key}>
<DarkSubTitle>{title}</DarkSubTitle>
<OverflowText>{content}</OverflowText>
<SingleLine>
<OverflowText>{content}</OverflowText>
{deleteCallback && (
<div
style={{ margin: '0 0 -4px 4px' }}
onClick={deleteCallback}
>
<XSvg />
</div>
)}
</SingleLine>
</DetailLine>
);
};

View file

@ -47,6 +47,9 @@ import { ReactComponent as Mail } from '../../assets/icons/mail.svg';
import { ReactComponent as Github } from '../../assets/icons/github.svg';
import { ReactComponent as Repeat } from '../../assets/icons/repeat.svg';
import { ReactComponent as CheckIcon } from '../../assets/icons/check.svg';
import { ReactComponent as StarIcon } from '../../assets/icons/star.svg';
import { ReactComponent as HalfStarIcon } from '../../assets/icons/half-star.svg';
import { ReactComponent as CreditCardIcon } from '../../assets/icons/credit-card.svg';
export interface IconProps {
color?: string;
@ -116,3 +119,6 @@ export const MailIcon = styleIcon(Mail);
export const GithubIcon = styleIcon(Github);
export const RepeatIcon = styleIcon(Repeat);
export const Check = styleIcon(CheckIcon);
export const Star = styleIcon(StarIcon);
export const HalfStar = styleIcon(HalfStarIcon);
export const CreditCard = styleIcon(CreditCardIcon);

View file

@ -56,10 +56,11 @@ export const Separation = styled.div`
interface SubCardProps {
color?: string;
padding?: string;
withMargin?: string;
}
export const SubCard = styled.div`
margin-bottom: 10px;
margin: ${({ withMargin }) => (withMargin ? withMargin : '0 0 10px 0')};
padding: ${({ padding }) => (padding ? padding : '16px')};
background: ${subCardColor};
border: 1px solid ${cardBorderColor};

View file

@ -76,7 +76,7 @@ export const Input = ({
value={value}
color={color}
withMargin={withMargin}
onChange={e => onChange(e)}
onChange={(e) => onChange(e)}
fullWidth={fullWidth}
inputWidth={width}
maxWidth={maxWidth}

View file

@ -8,12 +8,18 @@ interface StyledProps {
fontColor?: string | ThemeSet;
underline?: string | ThemeSet;
inheritColor?: boolean;
fullWidth?: boolean;
}
const styledCss = css`
color: ${({ fontColor, inheritColor }: StyledProps) =>
inheritColor ? 'inherit' : fontColor ?? textColor};
text-decoration: none;
${({ fullWidth }: StyledProps) =>
fullWidth &&
css`
width: 100%;
`};
:hover {
background: linear-gradient(
@ -27,9 +33,11 @@ const styledCss = css`
}
`;
const StyledLink = styled(({ inheritColor, fontColor, underline, ...rest }) => (
<RouterLink {...rest} />
))(() => styledCss);
const StyledLink = styled(
({ inheritColor, fontColor, underline, fullWidth, ...rest }) => (
<RouterLink {...rest} />
),
)(() => styledCss);
const StyledALink = styled.a`
${styledCss}
@ -42,6 +50,7 @@ interface LinkProps {
color?: string | ThemeSet;
underline?: string | ThemeSet;
inheritColor?: boolean;
fullWidth?: boolean;
}
export const Link = ({
@ -51,8 +60,9 @@ export const Link = ({
color,
underline,
inheritColor,
fullWidth,
}: LinkProps) => {
const props = { fontColor: color, underline, inheritColor };
const props = { fontColor: color, underline, inheritColor, fullWidth };
if (!to && !href) return null;

View file

@ -0,0 +1,20 @@
import React from 'react';
import { number } from '@storybook/addon-knobs';
import { Rating } from './Rating';
export default {
title: 'Ratings',
};
export const Default = () => {
const options = {
range: true,
min: 0,
max: 10,
step: 1,
};
const rating = number('Rating', 5, options);
return <Rating rating={rating / 10} />;
};

View file

@ -0,0 +1,62 @@
import React from 'react';
import { Star, HalfStar } from '../../components/generic/Icons';
import { themeColors } from '../../styles/Themes';
import styled from 'styled-components';
const StyledStar = styled(Star)`
margin-bottom: -1px;
`;
const StyledHalfStar = styled(HalfStar)`
margin-bottom: -1px;
`;
const StyledRatings = styled.div`
display: flex;
`;
interface RatingProps {
rating: number | null;
size?: string;
color?: string;
}
export const Rating = ({
rating,
size = '14px',
color = themeColors.blue3,
}: RatingProps) => {
if (!rating) {
return null;
}
const correctRating = Math.min(Math.max(Math.round(rating * 10), 0), 10);
const amount = (correctRating - (correctRating % 2)) / 2;
const hasHalf = correctRating % 2 > 0 ? true : false;
let stars = [];
const starConfig = {
size,
color,
};
for (let i = 0; i < 5; i++) {
if (i < amount) {
stars.push(
<StyledStar
key={i}
{...starConfig}
fillcolor={themeColors.blue3}
/>,
);
} else if (hasHalf && i === amount) {
stars.push(<StyledHalfStar key={i} {...starConfig} />);
} else {
stars.push(<StyledStar key={i} {...starConfig} />);
}
}
return <StyledRatings>{stars.map((star) => star)}</StyledRatings>;
};

View file

@ -0,0 +1,71 @@
import gql from 'graphql-tag';
export const GET_HODL_COUNTRIES = gql`
query GetCountries {
getCountries {
code
name
native_name
currency_code
currency_name
}
}
`;
export const GET_HODL_CURRENCIES = gql`
query GetCurrencies {
getCurrencies {
code
name
type
}
}
`;
export const GET_HODL_OFFERS = gql`
query GetOffers($filter: String) {
getOffers(filter: $filter) {
id
asset_code
country
country_code
working_now
side
title
description
currency_code
price
min_amount
max_amount
first_trade_limit
fee {
author_fee_rate
}
balance
payment_window_minutes
confirmations
payment_method_instructions {
id
version
payment_method_id
payment_method_type
payment_method_name
}
trader {
login
online_status
rating
trades_count
url
verified
verified_by
strong_hodler
country
country_code
average_payment_time_minutes
average_release_time_minutes
days_since_last_trade
}
}
}
`;

View file

@ -23,6 +23,7 @@ import { BalanceView } from 'views/balance/Balance';
import { PeersList } from 'views/peers/PeersList';
import { ToolsView } from 'views/tools';
import { ChainView } from 'views/chain/ChainView';
import { TraderView } from 'views/trader/TraderView';
const Container = styled.div`
display: grid;
@ -79,6 +80,7 @@ const Content = () => {
/>
<Route path="/settings" render={() => getGrid(SettingsView)} />
<Route path="/fees" render={() => getGrid(FeesView)} />
<Route path="/trading" render={() => getGrid(TraderView)} />
<Route path="/terms" render={() => <TermsView />} />
<Route path="/privacy" render={() => <PrivacyView />} />
<Route path="/faq" render={() => <FaqView />} />

View file

@ -85,7 +85,7 @@ export const Header = () => {
const renderLoggedIn = () => {
if (width <= mediaDimensions.mobile) {
return (
<IconWrapper onClick={() => setOpen(prev => !prev)}>
<IconWrapper onClick={() => setOpen((prev) => !prev)}>
{transitions.map(({ item, key, props }) =>
item ? (
<AnimatedClose

View file

@ -22,6 +22,7 @@ import {
LinkIcon,
RepeatIcon,
Users,
CreditCard,
} from '../../components/generic/Icons';
import { useSettings } from '../../context/SettingsContext';
import { useConnectionState } from 'context/ConnectionContext';
@ -59,6 +60,7 @@ const ButtonSection = styled.div`
const NavSeparation = styled.div`
margin-left: 8px;
font-size: 14px;
`;
interface NavProps {
@ -124,6 +126,7 @@ const CHAIN_TRANS = '/chainTransactions';
const TOOLS = '/tools';
const SETTINGS = '/settings';
const FEES = '/fees';
const TRADER = '/trading';
interface NavigationProps {
isBurger?: boolean;
@ -169,6 +172,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
{renderNavButton('Forwards', FORWARDS, GitPullRequest, sidebar)}
{renderNavButton('Chain', CHAIN_TRANS, LinkIcon, sidebar)}
{renderNavButton('Tools', TOOLS, Shield, sidebar)}
{renderNavButton('P2P Trading', TRADER, CreditCard, sidebar)}
{renderNavButton('Settings', SETTINGS, Settings, sidebar)}
</ButtonSection>
);
@ -184,6 +188,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
{renderBurgerNav('Forwards', FORWARDS, GitPullRequest)}
{renderBurgerNav('Chain', CHAIN_TRANS, LinkIcon)}
{renderBurgerNav('Tools', TOOLS, Shield)}
{renderBurgerNav('Trading', TRADER, CreditCard)}
{renderBurgerNav('Settings', SETTINGS, Settings)}
</BurgerRow>
);

View file

@ -11,133 +11,134 @@
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
),
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
window.location.href,
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA',
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null &&
contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.',
);
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}

View file

@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
import { ApolloError } from 'apollo-boost';
export const getErrorContent = (error: ApolloError): ReactNode => {
const errors = error.graphQLErrors.map(x => x.message);
const errors = error.graphQLErrors.map((x) => x.message);
const renderMessage = errors.map((error, i) => {
try {

View file

@ -42,7 +42,7 @@ export const BalanceRoute = ({
}: BalancedRouteProps) => {
const [getRoute, { loading, data, called }] = useLazyQuery(GET_ROUTES, {
fetchPolicy: 'no-cache',
onError: error => {
onError: (error) => {
callback();
toast.error(getErrorContent(error));
},
@ -52,7 +52,7 @@ export const BalanceRoute = ({
incoming && outgoing && amount && data && data.getRoutes && blocked;
const [payRoute, { loading: loadingP }] = useMutation(PAY_VIA_ROUTE, {
onError: error => {
onError: (error) => {
callback();
toast.error(getErrorContent(error));
},

View file

@ -38,8 +38,9 @@ export const SessionLogin = () => {
<SingleLine>
<Sub4Title>Password:</Sub4Title>
<Input
type={'password'}
withMargin={'0 0 0 16px'}
onChange={e => setPass(e.target.value)}
onChange={(e) => setPass(e.target.value)}
/>
</SingleLine>
{pass !== '' && (

View file

@ -48,8 +48,8 @@ export const FeeCard = ({
} = channelInfo;
const [updateFees] = useMutation(UPDATE_FEES, {
onError: error => toast.error(getErrorContent(error)),
onCompleted: data => {
onError: (error) => toast.error(getErrorContent(error)),
onCompleted: (data) => {
setIndexOpen(0);
data.updateFees
? toast.success('Channel fees updated')
@ -82,7 +82,9 @@ export const FeeCard = ({
placeholder={'Sats'}
color={textColorMap[theme]}
type={textColorMap[theme]}
onChange={e => setBaseFee(parseInt(e.target.value))}
onChange={(e) =>
setBaseFee(parseInt(e.target.value))
}
/>
</ResponsiveLine>
<ResponsiveLine>
@ -93,7 +95,9 @@ export const FeeCard = ({
placeholder={'Sats/Million'}
color={textColorMap[theme]}
type={'number'}
onChange={e => setFeeRate(parseInt(e.target.value))}
onChange={(e) =>
setFeeRate(parseInt(e.target.value))
}
/>
</ResponsiveLine>
<SecureButton

View file

@ -56,7 +56,7 @@ export const CreateInvoiceCard = ({ color }: { color: string }) => {
const [request, setRequest] = useState('');
const [createInvoice, { data, loading }] = useMutation(CREATE_INVOICE, {
onError: error => toast.error(getErrorContent(error)),
onError: (error) => toast.error(getErrorContent(error)),
});
useEffect(() => {
@ -104,7 +104,7 @@ export const CreateInvoiceCard = ({ color }: { color: string }) => {
}
color={color}
type={'number'}
onChange={e => setAmount(parseInt(e.target.value))}
onChange={(e) => setAmount(parseInt(e.target.value))}
/>
<SecureButton
callback={createInvoice}

View file

@ -36,7 +36,7 @@ export const ReceiveOnChainCard = () => {
const [received, setReceived] = useState(false);
const [createAddress, { data, loading }] = useMutation(CREATE_ADDRESS, {
onError: error => toast.error(getErrorContent(error)),
onError: (error) => toast.error(getErrorContent(error)),
});
useEffect(() => {

View file

@ -63,7 +63,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
const { fast, halfHour, hour } = useBitcoinState();
const [payAddress, { loading }] = useMutation(PAY_ADDRESS, {
onError: error => toast.error(getErrorContent(error)),
onError: (error) => toast.error(getErrorContent(error)),
onCompleted: () => {
toast.success('Payment Sent!');
setOpen();
@ -113,7 +113,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
withMargin={
width <= mediaDimensions.mobile ? '' : '0 0 0 24px'
}
onChange={e => setAddress(e.target.value)}
onChange={(e) => setAddress(e.target.value)}
/>
</ResponsiveLine>
<Separation />
@ -140,7 +140,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
placeholder={'Sats'}
withMargin={'0 0 0 8px'}
type={'number'}
onChange={e => setTokens(parseInt(e.target.value))}
onChange={(e) => setTokens(parseInt(e.target.value))}
/>
</SingleLine>
)}
@ -193,7 +193,9 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
}
type={'number'}
withMargin={'0 0 0 8px'}
onChange={e => setAmount(parseInt(e.target.value))}
onChange={(e) =>
setAmount(parseInt(e.target.value))
}
/>
</>
)}

View file

@ -46,7 +46,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
const { fast, halfHour, hour } = useBitcoinState();
const [openChannel] = useMutation(OPEN_CHANNEL, {
onError: error => toast.error(getErrorContent(error)),
onError: (error) => toast.error(getErrorContent(error)),
onCompleted: () => {
toast.success('Channel Opened');
setOpenCard('none');
@ -82,7 +82,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
withMargin={
width <= mediaDimensions.mobile ? '' : '0 0 0 8px'
}
onChange={e => setPublicKey(e.target.value)}
onChange={(e) => setPublicKey(e.target.value)}
/>
</ResponsiveLine>
<ResponsiveLine>
@ -101,7 +101,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
width <= mediaDimensions.mobile ? '' : '0 0 0 8px'
}
type={'number'}
onChange={e => setSize(parseInt(e.target.value))}
onChange={(e) => setSize(parseInt(e.target.value))}
/>
</ResponsiveLine>
<Separation />
@ -152,7 +152,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
placeholder={'Sats/Byte'}
color={color}
type={'number'}
onChange={e => setFee(parseInt(e.target.value))}
onChange={(e) => setFee(parseInt(e.target.value))}
/>
// </MultiButton>
)}

View file

@ -96,7 +96,7 @@ Props) => {
grid: { stroke: chartGridColor[theme] },
axis: { stroke: 'transparent' },
}}
tickFormat={a =>
tickFormat={(a) =>
isType === 'tokens' ? format({ amount: a }) : a
}
/>

View file

@ -21,10 +21,10 @@ export const getWaterfall = (
for (let i = initialPeriod; i <= lastPeriod; i++) {
const currentInvoice = invoices.find(
invoice => invoice.period === i,
(invoice) => invoice.period === i,
) ?? { period: undefined, amount: 0, tokens: 0 };
const currentPayment = payments.find(
payment => payment.period === i,
(payment) => payment.period === i,
) ?? { period: undefined, amount: 0, tokens: 0 };
const amountChange = currentInvoice.amount - currentPayment.amount;

View file

@ -1,6 +1,6 @@
import React from "react";
import { Card } from "../../components/generic/Styled";
import React from 'react';
import { Card } from '../../components/generic/Styled';
export const NotFound = () => {
return <Card>Not Found</Card>;
return <Card>Not Found</Card>;
};

View file

@ -0,0 +1,48 @@
import React from 'react';
import styled from 'styled-components';
import { themeColors, subCardColor } from 'styles/Themes';
interface MethodProps {
id: string;
payment_method_type: string;
payment_method_name: string;
}
interface MethodBoxesProps {
methods: MethodProps[] | undefined;
}
const StyledMethodBoxes = styled.div`
width: 100%;
position: relative;
right: -16px;
top: -26px;
display: flex;
justify-content: flex-end;
margin: 0 0 -24px 0;
flex-wrap: wrap;
overflow: hidden;
height: 24px;
`;
const StyledMethod = styled.div`
font-size: 12px;
margin: 0 0 0 8px;
border: 1px solid ${themeColors.blue2};
border-radius: 4px;
padding: 2px 4px;
background: ${subCardColor};
white-space: nowrap;
`;
export const MethodBoxes = ({ methods }: MethodBoxesProps) => {
if (!methods) return null;
return (
<StyledMethodBoxes>
{methods.map((method) => (
<StyledMethod>{method.payment_method_name}</StyledMethod>
))}
</StyledMethodBoxes>
);
};

View file

@ -0,0 +1,163 @@
import React, { useState, useEffect } from 'react';
import { SubTitle } from 'components/generic/Styled';
import { SortOptions, NewOptions } from '../OfferConfigs';
import { FilterType } from '../OfferFilters';
import { useQuery } from '@apollo/react-hooks';
import {
GET_HODL_COUNTRIES,
GET_HODL_CURRENCIES,
} from 'graphql/hodlhodl/query';
import { themeColors } from 'styles/Themes';
import ScaleLoader from 'react-spinners/ScaleLoader';
import { FilteredList } from './FilteredList';
import { OptionsLoading } from '../OfferCard.styled';
import { toast } from 'react-toastify';
interface FilterProps {
type: string;
dispatch: any;
final?: {};
newOptions?: FilterType[];
setModalType: (type: string) => void;
}
interface CountryType {
code: string;
name: string;
native_name: string;
currency_code: string;
currency_name: string;
}
interface CurrencyType {
code: string;
name: string;
type: string;
}
export const FilterModal = ({
type,
dispatch,
final,
newOptions,
setModalType,
}: FilterProps) => {
const searchable: boolean = final?.['searchable'] || false;
const skipable: boolean = type !== 'Country' && type !== 'Currency';
const [selected, setSelected] = useState<{} | undefined>();
const [options, setOptions] = useState(newOptions ?? []);
const [title, setTitle] = useState(final?.['title'] || '');
const query = type === 'Country' ? GET_HODL_COUNTRIES : GET_HODL_CURRENCIES;
const { loading, data, error } = useQuery(query, {
skip: skipable,
onError: () => toast.error('Error Loading Options. Please try again.'),
});
useEffect(() => {
switch (type) {
case 'sort':
setTitle('Sort Offers by:');
setOptions(SortOptions);
break;
case 'new':
setTitle('Add New Filter:');
setOptions(NewOptions);
break;
default:
break;
}
}, [type]);
useEffect(() => {
if (!loading && data && data.getCountries) {
const countryOptions = data.getCountries.map(
(country: CountryType) => {
const { code, name, native_name } = country;
return { name: code, title: `${name} (${native_name})` };
},
);
setOptions(countryOptions);
}
if (!loading && data && data.getCurrencies) {
const filtered = data.getCurrencies.filter(
(currency: CurrencyType) => currency.type === 'fiat',
);
const currencyOptions = filtered.map((currency: CurrencyType) => {
const { code, name } = currency;
return { name: code, title: name };
});
setOptions(currencyOptions);
}
}, [data, loading]);
const handleClick = (name: string, option?: {}) => () => {
if (final) {
dispatch({
type: 'addFilter',
newItem: { [final['name']]: name },
});
setModalType('none');
}
switch (type) {
case 'sort':
if (name === 'none') {
dispatch({
type: 'removeSort',
});
} else {
dispatch({
type: 'addSort',
newItem: { by: name },
});
}
setModalType('none');
break;
case 'new':
setSelected(option);
break;
default:
break;
}
};
if (error) {
return null;
}
if (selected) {
return (
<>
<FilterModal
type={selected['title']}
dispatch={dispatch}
final={selected}
newOptions={selected['options']}
setModalType={setModalType}
/>
</>
);
}
return (
<>
<SubTitle>{title}</SubTitle>
<FilteredList
searchable={searchable}
options={options}
handleClick={handleClick}
/>
<OptionsLoading>
{loading && (
<ScaleLoader height={20} color={themeColors.blue3} />
)}
</OptionsLoading>
</>
);
};

View file

@ -0,0 +1,76 @@
import React, { useState, useEffect } from 'react';
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
import { OfferModalBox } from '../OfferCard.styled';
import { Input } from 'components/input/Input';
import { Sub4Title } from 'components/generic/Styled';
interface FilteredProps {
searchable: boolean;
options: any;
handleClick: any;
}
export const FilteredList = ({
searchable,
options,
handleClick,
}: FilteredProps) => {
const [searchTerm, setSearchTerm] = useState<string>('');
const [filteredOptions, setOptions] = useState<
{ name: string; title: string }[]
>(options);
useEffect(() => {
const filtered = options.filter(
(option: { name: string; title: string }) => {
const inName = option.name
.toLowerCase()
.includes(searchTerm.toLowerCase());
const inTitle = option.title
.toLowerCase()
.includes(searchTerm.toLowerCase());
return inName || inTitle;
},
);
setOptions(filtered);
}, [searchTerm, options]);
const handleChange = (event: any) => {
setSearchTerm(event.target.value);
};
return (
<>
{searchable && (
<Input
placeholder={'Search'}
fullWidth={true}
onChange={handleChange}
withMargin={'0 0 8px 0'}
/>
)}
<OfferModalBox>
{filteredOptions.length > 0 ? (
filteredOptions.map(
(
option: { name: string; title: string },
index: number,
) => (
<ColorButton
key={`${index}-${option.name}`}
fullWidth={true}
withMargin={'0 0 2px 0'}
onClick={handleClick(option.name, option)}
>
{option.title}
</ColorButton>
),
)
) : (
<Sub4Title>No results</Sub4Title>
)}
</OfferModalBox>
</>
);
};

View file

@ -0,0 +1,54 @@
import styled from 'styled-components';
import { unSelectedNavButton, mediaWidths } from 'styles/Themes';
import { ChevronRight } from 'components/generic/Icons';
export const TradesAmount = styled.div`
font-size: 14px;
color: ${unSelectedNavButton};
margin: 0 4px;
`;
export const StyleArrow = styled(ChevronRight)`
margin-bottom: -3px;
`;
export const OfferModalBox = styled.div`
overflow-y: auto;
max-height: 640px;
min-height: 240px;
@media (${mediaWidths.mobile}) {
max-height: 240px;
min-height: 120px;
}
`;
export const StyledTitle = styled.div`
font-size: 14px;
margin: 16px 0;
@media (${mediaWidths.mobile}) {
text-align: center;
margin-top: 8px;
}
`;
export const StyledLogin = styled.div`
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
`;
export const StyledDescription = styled(StyledLogin)`
max-width: unset;
overflow-y: auto;
font-size: 14px;
max-height: 160px;
`;
export const OptionsLoading = styled.div`
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
`;

View file

@ -0,0 +1,195 @@
import React from 'react';
import {
SubCard,
Sub4Title,
SubTitle,
SingleLine,
ResponsiveLine,
Separation,
} from 'components/generic/Styled';
import { Rating } from 'components/rating/Rating';
import {
TradesAmount,
StyleArrow,
StyledTitle,
StyledLogin,
StyledDescription,
} from './OfferCard.styled';
import { MainInfo } from 'views/channels/Channels.style';
import { themeColors } from 'styles/Themes';
import { renderLine } from 'components/generic/Helpers';
import numeral from 'numeral';
import { MethodBoxes } from './MethodBoxes';
import { Link } from 'components/link/Link';
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
const format = (value: number | string, format: string = '0,0.00') =>
numeral(value).format(format);
interface OfferCardProps {
offer: any;
index: number;
setIndexOpen: (index: number) => void;
indexOpen: number;
}
export const OfferCard = ({
offer,
index,
setIndexOpen,
indexOpen,
}: OfferCardProps) => {
const {
id,
asset_code,
country,
country_code,
working_now,
side,
title,
description,
currency_code,
price,
min_amount,
max_amount,
first_trade_limit,
fee,
balance,
payment_window_minutes,
confirmations,
payment_method_instructions,
trader,
} = offer;
const { author_fee_rate } = fee;
const {
login,
online_status,
rating,
trades_count = 0,
url,
verified,
verified_by,
strong_hodler,
country: traderCountry,
country_code: traderCode,
average_payment_time_minutes,
average_release_time_minutes,
days_since_last_trade,
} = trader;
const handleClick = () => {
if (indexOpen === index) {
setIndexOpen(0);
} else {
setIndexOpen(index);
}
};
const renderPayments = (): string => {
if (payment_method_instructions) {
const methods = payment_method_instructions.map(
(method: {
payment_method_name: string;
payment_method_type: string;
}) =>
`${method.payment_method_name} (${method.payment_method_type})`,
);
return methods.join(', ');
}
return '';
};
const renderDetails = () => (
<>
<Separation />
<StyledDescription>{description}</StyledDescription>
<Separation />
{renderLine('Price', format(price))}
{renderLine('Min Amount:', format(min_amount))}
{renderLine('Max Amount:', format(max_amount))}
{renderLine(`First Trade Limit:`, format(first_trade_limit))}
{renderLine(`Payment Options:`, renderPayments())}
{renderLine('Country:', `${country} (${country_code})`)}
{renderLine('Available Now:', working_now ? 'Yes' : 'No')}
{renderLine(`Balance:`, format(balance))}
{renderLine(`Payment Window (min):`, payment_window_minutes)}
{renderLine(`Confirmations:`, confirmations)}
{renderLine(`Fee Rate:`, `${format(author_fee_rate * 100)}%`)}
<Separation />
<Sub4Title>Trader</Sub4Title>
{renderLine('User:', login)}
{renderLine('Online:', online_status)}
{renderLine('Rating:', rating)}
{renderLine('Amount of Trades:', trades_count)}
{renderLine('Verified:', verified)}
{renderLine('Verified By:', verified_by)}
{renderLine('Strong Hodler:', strong_hodler)}
{renderLine('Country:', `${traderCountry} (${traderCode})`)}
{renderLine(
'Average Payment Time (min):',
average_payment_time_minutes,
)}
{renderLine(
'Average Release Time (min):',
average_release_time_minutes,
)}
{renderLine('Days since last trade:', days_since_last_trade)}
<SingleLine>
<Link
href={`https://hodlhodl.com/offers/${id}`}
underline={'transparent'}
fullWidth={true}
>
<ColorButton
withBorder={true}
withMargin={'16px 8px 0 0'}
fullWidth={true}
>
View Offer
</ColorButton>
</Link>
<Link href={url} underline={'transparent'} fullWidth={true}>
<ColorButton
withBorder={true}
withMargin={'16px 0 0 8px'}
fullWidth={true}
>
View trader
</ColorButton>
</Link>
</SingleLine>
</>
);
return (
<SubCard withMargin={'16px 0 24px'} key={`${index}-${id}`}>
<MainInfo onClick={() => handleClick()}>
<MethodBoxes methods={payment_method_instructions} />
<ResponsiveLine>
<SubTitle>
{side !== 'buy' ? asset_code : currency_code}
<StyleArrow color={themeColors.blue3} />
{side !== 'buy' ? currency_code : asset_code}
</SubTitle>
<SingleLine>
<StyledLogin>{login}</StyledLogin>
{trades_count > 0 && (
<TradesAmount>{`(${trades_count}) `}</TradesAmount>
)}
<Rating rating={rating} />
</SingleLine>
</ResponsiveLine>
<StyledTitle>{title}</StyledTitle>
{renderLine('Price:', format(price))}
{renderLine(
`Min/Max amount:`,
`${format(min_amount, '0a')}/${format(max_amount, '0a')}`,
)}
</MainInfo>
{index === indexOpen && renderDetails()}
</SubCard>
);
};

View file

@ -0,0 +1,141 @@
export const SortOptions = [
{
name: 'none',
title: 'None',
},
{
name: 'price',
title: 'Price',
},
{
name: 'payment_window_minutes',
title: 'Payment Window',
},
{
name: 'user_average_payment_time_minutes',
title: 'Average Payment Time',
},
{
name: 'user_average_release_time_minutes',
title: 'Average Release Time',
},
{
name: 'rating',
title: 'Rating',
},
];
export const NewOptions = [
{
name: 'asset_code',
title: 'Asset Code',
options: [
{
name: 'BTC',
title: 'Bitcoin',
},
{
name: 'BTCLN',
title: 'Lightning Bitcoin',
},
],
},
{
name: 'side',
title: 'Side',
options: [
{
name: 'buy',
title: 'I want to sell Bitcoin',
},
{
name: 'sell',
title: 'I want to buy Bitcoin',
},
],
},
{
name: 'include_global',
title: 'Include Global Offers',
options: [
{
name: 'true',
title: 'Yes',
},
{
name: 'false',
title: 'No',
},
],
},
{
name: 'only_working_now',
title: 'Only Working Now Offers',
options: [
{
name: 'true',
title: 'Yes',
},
{
name: 'false',
title: 'No',
},
],
},
{
name: 'country',
title: 'Country',
searchable: true,
},
{
name: 'currency_code',
title: 'Currency',
searchable: true,
},
{
name: 'payment_method_type',
title: 'Payment Type',
options: [
{
name: 'Bank wire',
title: 'Bank wire',
},
{
name: 'Cash',
title: 'Cash',
},
{
name: 'Cryptocurrency',
title: 'Cryptocurrency',
},
{
name: 'Online payment system',
title: 'Online payment system',
},
],
},
// {
// name: 'payment_method_id',
// title: 'Payment Id',
// },
// {
// name: 'payment_method_name',
// title: 'Payment Name',
// },
// {
// name: 'volume',
// title: 'Volume',
// },
// {
// name: 'payment_window_minutes_max',
// title: 'Max Payment Window',
// },
// {
// name: 'user_average_payment_time_minutes_max',
// title: 'Max User Payment Time',
// },
// {
// name: 'user_average_release_time_minutes_max',
// title: 'Max User Release Time',
// },
];

View file

@ -0,0 +1,253 @@
import React, { useState, useReducer } from 'react';
import {
SingleLine,
Separation,
ResponsiveLine,
NoWrapTitle,
} from 'components/generic/Styled';
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
import {
MultiButton,
SingleButton,
} from 'components/buttons/multiButton/MultiButton';
import Modal from 'components/modal/ReactModal';
import { FilterModal } from './Modal/FilterModal';
import { useHistory } from 'react-router-dom';
import { SortOptions } from './OfferConfigs';
import { QueryProps } from './TraderView';
import { XSvg } from 'components/generic/Icons';
import { renderLine } from 'components/generic/Helpers';
import { chartColors } from 'styles/Themes';
type ActionType = {
type:
| 'addFilter'
| 'addSort'
| 'removeSort'
| 'removeFilter'
| 'changeLimit';
state?: QueryProps;
newItem?: {};
removeKey?: string;
changeLimit?: number;
};
const reducer = (state: QueryProps, action: ActionType): QueryProps => {
const { sort, filters } = state;
switch (action.type) {
case 'addSort':
let direction = {};
if (sort && !sort.direction) {
direction = { direction: 'desc' };
}
const newSort = { ...sort, ...direction, ...action.newItem };
return { ...state, sort: newSort };
case 'removeSort':
return { ...state, sort: { by: '', direction: '' } };
case 'addFilter':
const newFilters = { ...filters, ...action.newItem };
return { ...state, filters: newFilters };
case 'removeFilter':
if (action.removeKey) {
const remaining = { ...filters };
delete remaining[action.removeKey];
return { ...state, filters: remaining };
}
return state;
case 'changeLimit':
if (action.changeLimit) {
return {
...state,
pagination: { limit: action.changeLimit, offset: 0 },
};
}
return state;
default:
throw new Error();
}
};
interface FilterProps {
offerFilters: QueryProps;
}
export interface FilterType {
name: string;
title: string;
optionOne?: string;
optionTwo?: string;
}
export const OfferFilters = ({ offerFilters }: FilterProps) => {
const { push } = useHistory();
const [filterState, dispatch] = useReducer(reducer, offerFilters);
const [modalType, setModalType] = useState<string>('none');
const [willApply, setWillApply] = useState<boolean>(false);
const renderButton = (
onClick: () => void,
text: string,
selected: boolean,
) => (
<SingleButton selected={selected} onClick={onClick}>
{text}
</SingleButton>
);
const handleSave = () =>
push({ search: `?filter=${btoa(JSON.stringify(filterState))}` });
const handleRemoveAll = () => push({ search: '' });
const handleRemove = (removeKey: string) => {
dispatch({ type: 'removeFilter', removeKey });
};
const renderAppliedFilters = () => {
const currentFilters = filterState.filters;
let activeFilters = [];
for (const key in currentFilters) {
if (currentFilters.hasOwnProperty(key)) {
const element = currentFilters[key];
activeFilters.push(
renderLine(key, element, `${key}-${element}`, () =>
handleRemove(key),
),
);
}
}
return <>{activeFilters.map((filter) => filter)}</>;
};
const renderLimitOptions = () => {
const currentLimit = filterState.pagination.limit;
const renderButton = (value: number, margin?: string) => (
<ColorButton
onClick={() =>
dispatch({ type: 'changeLimit', changeLimit: value })
}
selected={currentLimit === value}
withMargin={margin}
>
{value}
</ColorButton>
);
return (
<SingleLine>
{renderButton(25)}
{renderButton(50, '0 4px 0')}
{renderButton(100)}
</SingleLine>
);
};
const renderFilters = () => {
return (
<>
<Separation />
<ResponsiveLine>
<NoWrapTitle>Results:</NoWrapTitle>
{renderLimitOptions()}
</ResponsiveLine>
<Separation />
<ResponsiveLine>
<NoWrapTitle>Filters</NoWrapTitle>
<ColorButton
arrow={true}
onClick={() => setModalType('new')}
>
New Filter
</ColorButton>
</ResponsiveLine>
{Object.keys(filterState.filters).length > 0 &&
renderAppliedFilters()}
<Separation />
<ResponsiveLine>
<NoWrapTitle>Sort By:</NoWrapTitle>
<ColorButton
arrow={true}
onClick={() => setModalType('sort')}
>
{SortOptions.find(
(option) => option.name === filterState.sort.by,
)?.title ?? 'None'}
</ColorButton>
</ResponsiveLine>
{!!filterState.sort.by && (
<ResponsiveLine>
<NoWrapTitle>Sort Direction:</NoWrapTitle>
<MultiButton>
{renderButton(
() => {
dispatch({
type: 'addSort',
newItem: { direction: 'asc' },
});
},
'Asc',
filterState.sort.direction === 'asc',
)}
{renderButton(
() => {
dispatch({
type: 'addSort',
newItem: { direction: 'desc' },
});
},
'Desc',
filterState.sort.direction !== 'asc',
)}
</MultiButton>
</ResponsiveLine>
)}
<Separation />
<SingleLine>
<ColorButton
fullWidth={true}
withMargin={'16px 4px 0 0'}
onClick={handleSave}
>
Filter
</ColorButton>
<ColorButton
color={chartColors.orange2}
fullWidth={true}
withMargin={'16px 0 0 4px'}
onClick={handleRemoveAll}
>
Remove All
</ColorButton>
</SingleLine>
</>
);
};
return (
<>
<SingleLine>
Filters
<ColorButton
arrow={!willApply}
onClick={() => setWillApply((prev) => !prev)}
>
{willApply ? <XSvg /> : 'Apply'}
</ColorButton>
</SingleLine>
{willApply && renderFilters()}
<Modal
isOpen={modalType !== 'none'}
closeCallback={() => setModalType('none')}
>
<FilterModal
type={modalType}
dispatch={dispatch}
setModalType={setModalType}
/>
</Modal>
</>
);
};

View file

@ -0,0 +1,148 @@
import React, { useState } from 'react';
import {
CardWithTitle,
SubTitle,
Card,
DarkSubTitle,
ResponsiveLine,
} from 'components/generic/Styled';
import { useQuery } from '@apollo/react-hooks';
import { GET_HODL_OFFERS } from 'graphql/hodlhodl/query';
import { LoadingCard } from 'components/loading/LoadingCard';
import { OfferCard } from './OfferCard';
import { OfferFilters } from './OfferFilters';
import { useLocation } from 'react-router-dom';
import qs from 'qs';
import { toast } from 'react-toastify';
import { Link } from 'components/link/Link';
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
export interface QueryProps {
pagination: {
limit: number;
offset: number;
};
filters: {};
sort: {
by: string;
direction: string;
};
}
const defaultQuery: QueryProps = {
pagination: {
limit: 25,
offset: 0,
},
filters: {},
sort: {
by: '',
direction: '',
},
};
export const TraderView = () => {
const { search } = useLocation();
const query = qs.parse(search, { ignoreQueryPrefix: true });
let decoded: QueryProps = defaultQuery;
if (!!query.filter) {
try {
decoded = JSON.parse(atob(query.filter));
} catch (error) {
toast.error('Incorrect url.');
}
}
const queryObject = {
...defaultQuery,
...decoded,
};
const [indexOpen, setIndexOpen] = useState(0);
const [page, setPage] = useState(1);
const [fetching, setFetching] = useState(false);
const { data, loading, fetchMore, error } = useQuery(GET_HODL_OFFERS, {
variables: { filter: JSON.stringify(queryObject) },
onError: () => toast.error('Error getting offers. Please try again.'),
});
if (error) {
return null;
}
if (loading || !data || !data.getOffers) {
return <LoadingCard title={'P2P Trading'} />;
}
const amountOfOffers = data.getOffers.length;
const {
pagination: { limit },
} = queryObject;
return (
<CardWithTitle>
<ResponsiveLine>
<SubTitle>P2P Trading</SubTitle>
<DarkSubTitle>
Powered by{' '}
<Link href={'https://hodlhodl.com/'}>HodlHodl</Link>
</DarkSubTitle>
</ResponsiveLine>
<Card bottom={'16px'}>
<OfferFilters offerFilters={queryObject} />
</Card>
<Card bottom={'8px'}>
{amountOfOffers <= 0 && (
<DarkSubTitle>No Offers Found</DarkSubTitle>
)}
{data.getOffers.map((offer: any, index: number) => (
<OfferCard
offer={offer}
index={index + 1}
setIndexOpen={setIndexOpen}
indexOpen={indexOpen}
key={`${index}-${offer.id}`}
/>
))}
</Card>
{amountOfOffers > 0 && amountOfOffers === limit * page && (
<ColorButton
loading={fetching}
disabled={fetching}
onClick={() => {
setFetching(true);
fetchMore({
variables: {
filter: JSON.stringify({
...queryObject,
pagination: { limit, offset: limit * page },
}),
},
updateQuery: (
prev,
{ fetchMoreResult: result },
) => {
if (!result) return prev;
setFetching(false);
setPage((prev) => (prev += 1));
return {
getOffers: [
...prev.getOffers,
...result.getOffers,
],
};
},
});
}}
>
Show More
</ColorButton>
)}
</CardWithTitle>
);
};

View file

@ -8,8 +8,6 @@ import { toast } from 'react-toastify';
import { getErrorContent } from '../../utils/error';
import { PaymentsCard } from './PaymentsCards';
import { LoadingCard } from '../../components/loading/LoadingCard';
import { useSettings } from '../../context/SettingsContext';
import { textColorMap } from '../../styles/Themes';
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
import { FlowBox } from 'views/home/reports/flow';
@ -18,7 +16,6 @@ export const TransactionList = () => {
const [token, setToken] = useState('');
const [fetching, setFetching] = useState(false);
const { theme } = useSettings();
const { host, viewOnly, cert, sessionAdmin } = useAccount();
const auth = {
host,
@ -48,7 +45,7 @@ export const TransactionList = () => {
<FlowBox />
<CardWithTitle>
<SubTitle>Transactions</SubTitle>
<Card bottom={'5px'}>
<Card bottom={'8px'}>
{resumeList.map((entry: any, index: number) => {
if (entry.type === 'invoice') {
return (
@ -74,9 +71,7 @@ export const TransactionList = () => {
})}
</Card>
<ColorButton
selected={true}
loading={fetching}
color={textColorMap[theme]}
disabled={fetching}
onClick={() => {
setFetching(true);

6
lerna.json Normal file
View file

@ -0,0 +1,6 @@
{
"packages": ["client", "server"],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.2.0"
}

31
package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "root",
"private": true,
"scripts": {
"server:dev": "lerna run build:dev --stream --scope @thunderhub/server",
"server:prod": "lerna run build --stream --scope @thunderhub/server",
"server:run": "lerna run start --stream --scope @thunderhub/server"
},
"workspaces": {
"packages": [
"client",
"server"
]
},
"dependencies": {
"base64url": "^3.0.1",
"date-fns": "^2.0.0-beta.5",
"graphql": "^14.6.0"
},
"devDependencies": {
"husky": "^4.2.3",
"lerna": "^3.20.2",
"prettier": "2.0.2",
"pretty-quick": "^2.0.1"
},
"husky": {
"hooks": {
"pre-commit": "npx lerna run --concurrency 1 --stream precommit"
}
}
}

View file

@ -1,4 +1,4 @@
{
"ignore": ["src"],
"watch": ["dist/"]
"ignore": ["src"],
"watch": ["dist/"]
}

View file

@ -1,14 +1,15 @@
{
"name": "thunderhub-server",
"version": "0.1.17",
"name": "@thunderhub/server",
"version": "0.2.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.production.js",
"build:dev": "webpack --config webpack.development.js",
"start": "node dist/server",
"dev": "nodemon dist/server",
"deploy": "yarn build && eb deploy"
"start": "node --http-parser=legacy dist/server",
"dev": "nodemon --http-parser=legacy dist/server",
"deploy": "yarn build && eb deploy",
"precommit": "pretty-quick --staged"
},
"repository": {
"type": "git",
@ -23,10 +24,7 @@
"@types/node-fetch": "^2.5.5",
"@types/underscore": "^1.9.4",
"apollo-server": "^2.11.0",
"base64url": "^3.0.1",
"date-fns": "^2.11.0",
"dotenv": "^8.2.0",
"graphql": "^14.6.0",
"graphql-depth-limit": "^1.1.0",
"graphql-iso-date": "^3.6.1",
"graphql-rate-limit": "^2.0.1",
@ -37,19 +35,12 @@
"devDependencies": {
"@types/webpack-env": "^1.15.1",
"clean-webpack-plugin": "^3.0.0",
"husky": "^4.2.3",
"prettier": "^2.0.2",
"pretty-quick": "^2.0.0",
"ts-loader": "^6.2.2",
"typescript": "^3.8.3",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11",
"webpack-merge": "^4.2.2",
"webpack-node-externals": "^1.7.2"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
"webpack-node-externals": "^1.7.2",
"webpack-shell-plugin": "^0.5.0"
}
}

View file

@ -1,5 +1,5 @@
import base64url from 'base64url';
import { authenticatedLndGrpc } from 'ln-service';
import { envConfig } from '../utils/envConfig';
export const getIp = (req: any) => {
if (!req || !req.headers) {
@ -9,7 +9,7 @@ export const getIp = (req: any) => {
const before = forwarded
? forwarded.split(/, /)[0]
: req.connection.remoteAddress;
const ip = process.env.NODE_ENV === 'development' ? '1.2.3.4' : before;
const ip = envConfig.env === 'development' ? '1.2.3.4' : before;
return ip;
};

View file

@ -0,0 +1,18 @@
export const getHodlParams = (params: any): string => {
let paramString = '?';
for (const key in params) {
if (params.hasOwnProperty(key)) {
const element = params[key];
for (const subKey in element) {
if (element.hasOwnProperty(subKey)) {
const subElement = element[subKey];
paramString = `${paramString}&${key}[${subKey}]=${subElement}`;
}
}
}
}
return paramString;
};

View file

@ -1,38 +1,43 @@
import { createLogger, format, transports } from "winston";
import path from "path";
import { createLogger, format, transports } from 'winston';
import path from 'path';
import { envConfig } from '../utils/envConfig';
const combinedFormat =
process.env.NODE_ENV === "development"
? format.combine(
format.label({
label: path.basename(
process && process.mainModule ? process.mainModule.filename : ""
envConfig.env === 'development'
? format.combine(
format.label({
label: path.basename(
process && process.mainModule
? process.mainModule.filename
: '',
),
}),
format.splat(),
format.colorize(),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(
(info: any) =>
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`,
),
)
}),
format.splat(),
format.colorize(),
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.printf(
(info: any) =>
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
)
)
: format.combine(
format.label({
label: path.basename(
process && process.mainModule ? process.mainModule.filename : ""
)
}),
format.splat(),
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.printf(
(info: any) =>
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
)
);
: format.combine(
format.label({
label: path.basename(
process && process.mainModule
? process.mainModule.filename
: '',
),
}),
format.splat(),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(
(info: any) =>
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`,
),
);
export const logger = createLogger({
level: process.env.LOG_LEVEL || "silly",
format: combinedFormat,
transports: [new transports.Console()]
level: envConfig.logLevel,
format: combinedFormat,
transports: [new transports.Console()],
});

View file

@ -3,6 +3,7 @@ import { thunderHubSchema } from './schemas';
import { logger } from './helpers/logger';
import { getIp } from './helpers/helpers';
import depthLimit from 'graphql-depth-limit';
import { envConfig } from './utils/envConfig';
const server = new ApolloServer({
schema: thunderHubSchema,
@ -15,7 +16,7 @@ const server = new ApolloServer({
],
});
server.listen({ port: process.env.PORT || 3001 }).then(({ url }: any) => {
server.listen({ port: envConfig.port }).then(({ url }: any) => {
logger.info(`Server ready at ${url}`);
});

View file

@ -1,14 +1,14 @@
import { GraphQLSchema, GraphQLObjectType } from "graphql";
import { query } from "./query";
import { mutation } from "./mutations";
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
import { query } from './query';
import { mutation } from './mutations';
export const thunderHubSchema = new GraphQLSchema({
query: new GraphQLObjectType({
name: "Query",
fields: query
}),
mutation: new GraphQLObjectType({
name: "Mutation",
fields: mutation
})
query: new GraphQLObjectType({
name: 'Query',
fields: query,
}),
mutation: new GraphQLObjectType({
name: 'Mutation',
fields: mutation,
}),
});

View file

@ -3,8 +3,7 @@ import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLBoolean } from 'graphql';
import fetch from 'node-fetch';
import { BitcoinFeeType } from '../../types/QueryType';
const url = 'https://bitcoinfees.earn.com/api/v1/fees/recommended';
import { appUrls } from '../../../utils/appUrls';
export const getBitcoinFees = {
type: BitcoinFeeType,
@ -15,7 +14,7 @@ export const getBitcoinFees = {
await requestLimiter(context.ip, 'bitcoinFee');
try {
const response = await fetch(url);
const response = await fetch(appUrls.fees);
const json = await response.json();
if (json) {

View file

@ -2,8 +2,7 @@ import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLString, GraphQLBoolean } from 'graphql';
import fetch from 'node-fetch';
const url = 'https://blockchain.info/ticker';
import { appUrls } from '../../../utils/appUrls';
export const getBitcoinPrice = {
type: GraphQLString,
@ -17,7 +16,7 @@ export const getBitcoinPrice = {
await requestLimiter(context.ip, 'bitcoinPrice');
try {
const response = await fetch(url);
const response = await fetch(appUrls.ticker);
const json = await response.json();
return JSON.stringify(json);

View file

@ -0,0 +1,37 @@
import fetch from 'node-fetch';
import { GraphQLList } from 'graphql';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { logger } from '../../../helpers/logger';
import { appUrls } from '../../../utils/appUrls';
import { HodlCountryType } from '../../types/HodlType';
import { envConfig } from '../../../utils/envConfig';
export const getCountries = {
type: new GraphQLList(HodlCountryType),
args: {},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'getCountries');
const headers = {
Authorization: `Bearer ${envConfig.hodlKey}`,
};
try {
const response = await fetch(`${appUrls.hodlhodl}/v1/countries`, {
headers,
});
const json = await response.json();
if (json) {
const { countries } = json;
return countries;
} else {
throw new Error('Problem getting HodlHodl countries.');
}
} catch (error) {
params.logger &&
logger.error('Error getting HodlHodl countries: %o', error);
throw new Error('Problem getting HodlHodl countries.');
}
},
};

View file

@ -0,0 +1,37 @@
import fetch from 'node-fetch';
import { GraphQLList } from 'graphql';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { logger } from '../../../helpers/logger';
import { appUrls } from '../../../utils/appUrls';
import { HodlCurrencyType } from '../../types/HodlType';
import { envConfig } from '../../../utils/envConfig';
export const getCurrencies = {
type: new GraphQLList(HodlCurrencyType),
args: {},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'getCurrencies');
const headers = {
Authorization: `Bearer ${envConfig.hodlKey}`,
};
try {
const response = await fetch(`${appUrls.hodlhodl}/v1/currencies`, {
headers,
});
const json = await response.json();
if (json) {
const { currencies } = json;
return currencies;
} else {
throw new Error('Problem getting HodlHodl currencies.');
}
} catch (error) {
params.logger &&
logger.error('Error getting HodlHodl currencies: %o', error);
throw new Error('Problem getting HodlHodl currencies.');
}
},
};

View file

@ -0,0 +1,57 @@
import fetch from 'node-fetch';
import { GraphQLList, GraphQLString } from 'graphql';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { logger } from '../../../helpers/logger';
import { appUrls } from '../../../utils/appUrls';
import { HodlOfferType } from '../../types/HodlType';
import { getHodlParams } from '../../../helpers/hodlHelpers';
const defaultQuery = {
filters: {},
sort: {
by: '',
direction: '',
},
};
export const getOffers = {
type: new GraphQLList(HodlOfferType),
args: {
filter: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'getOffers');
let queryParams = defaultQuery;
if (params.filter) {
try {
queryParams = JSON.parse(params.filter);
} catch (error) {}
}
try {
const fullParams = {
...queryParams,
};
const paramString = getHodlParams(fullParams);
const response = await fetch(
`${appUrls.hodlhodl}/v1/offers${paramString}`,
);
const json = await response.json();
if (json) {
const { offers } = json;
return offers;
} else {
throw new Error('Problem getting HodlHodl offers.');
}
} catch (error) {
params.logger &&
logger.error('Error getting HodlHodl offers: %o', error);
throw new Error('Problem getting HodlHodl offers.');
}
},
};

View file

@ -0,0 +1,9 @@
import { getOffers } from './getOffers';
import { getCountries } from './getCountries';
import { getCurrencies } from './getCurrencies';
export const hodlQueries = {
getOffers,
getCountries,
getCurrencies,
};

View file

@ -9,6 +9,7 @@ import { routeQueries } from './route';
import { peerQueries } from './peer';
import { messageQueries } from './message';
import { chainQueries } from './chain';
import { hodlQueries } from './hodlhodl';
export const query = {
...channelQueries,
@ -22,4 +23,5 @@ export const query = {
...peerQueries,
...messageQueries,
...chainQueries,
...hodlQueries,
};

View file

@ -72,7 +72,7 @@ export const getForwardChannelsReport = {
const getRouteAlias = (array: any[], publicKey: string) =>
Promise.all(
array.map(async channel => {
array.map(async (channel) => {
const nodeAliasIn = await getNodeAlias(
channel.in,
publicKey,
@ -94,7 +94,7 @@ export const getForwardChannelsReport = {
const getAlias = (array: any[], publicKey: string) =>
Promise.all(
array.map(async channel => {
array.map(async (channel) => {
const nodeAlias = await getNodeAlias(
channel.name,
publicKey,
@ -120,7 +120,7 @@ export const getForwardChannelsReport = {
});
if (params.type === 'route') {
const mapped = forwardsList.forwards.map(forward => {
const mapped = forwardsList.forwards.map((forward) => {
return {
route: `${forward.incoming_channel} - ${forward.outgoing_channel}`,
...forward,

View file

@ -1,42 +1,42 @@
export interface ForwardProps {
created_at: string;
fee: number;
fee_mtokens: string;
incoming_channel: string;
mtokens: string;
outgoing_channel: string;
tokens: number;
created_at: string;
fee: number;
fee_mtokens: string;
incoming_channel: string;
mtokens: string;
outgoing_channel: string;
tokens: number;
}
export interface ForwardCompleteProps {
forwards: ForwardProps[];
next: string;
forwards: ForwardProps[];
next: string;
}
export interface ListProps {
[key: string]: ForwardProps[];
[key: string]: ForwardProps[];
}
export interface ReduceObjectProps {
fee: number;
tokens: number;
fee: number;
tokens: number;
}
export interface FinalProps {
fee: number;
tokens: number;
amount: number;
fee: number;
tokens: number;
amount: number;
}
export interface FinalList {
[key: string]: FinalProps;
[key: string]: FinalProps;
}
export interface CountProps {
[key: string]: number;
[key: string]: number;
}
export interface ChannelCounts {
name: string;
count: number;
name: string;
count: number;
}

View file

@ -46,7 +46,7 @@ export const getForwardReport = {
});
if (params.time === 'month' || params.time === 'week') {
const orderedDay = groupBy(forwardsList.forwards, item => {
const orderedDay = groupBy(forwardsList.forwards, (item) => {
return (
days -
differenceInCalendarDays(
@ -60,7 +60,7 @@ export const getForwardReport = {
return JSON.stringify(reducedOrderedDay);
} else {
const orderedHour = groupBy(forwardsList.forwards, item => {
const orderedHour = groupBy(forwardsList.forwards, (item) => {
return (
24 -
differenceInHours(endDate, new Date(item.created_at))

View file

@ -56,11 +56,11 @@ export const countArray = (list: ForwardProps[], type: boolean) => {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.map((forward) => forward.fee)
.reduce((p, c) => p + c);
const tokens = element
.map(forward => forward.tokens)
.map((forward) => forward.tokens)
.reduce((p, c) => p + c);
channelInfo.push({
@ -84,11 +84,11 @@ export const countRoutes = (list: ForwardProps[]) => {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.map((forward) => forward.fee)
.reduce((p, c) => p + c);
const tokens = element
.map(forward => forward.tokens)
.map((forward) => forward.tokens)
.reduce((p, c) => p + c);
channelInfo.push({

View file

@ -1,7 +1,7 @@
import { getForwardReport } from "./ForwardReport";
import { getForwardChannelsReport } from "./ForwardChannels";
import { getForwardReport } from './ForwardReport';
import { getForwardChannelsReport } from './ForwardChannels';
export const reportQueries = {
getForwardReport,
getForwardChannelsReport
getForwardReport,
getForwardChannelsReport,
};

View file

@ -0,0 +1,105 @@
import {
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLBoolean,
GraphQLList,
} from 'graphql';
export const HodlOfferFeeType = new GraphQLObjectType({
name: 'hodlOfferFeeType',
fields: () => {
return {
author_fee_rate: { type: GraphQLString },
};
},
});
export const HodlOfferPaymentType = new GraphQLObjectType({
name: 'hodlOfferPaymentType',
fields: () => {
return {
id: { type: GraphQLString },
version: { type: GraphQLString },
payment_method_id: { type: GraphQLString },
payment_method_type: { type: GraphQLString },
payment_method_name: { type: GraphQLString },
};
},
});
export const HodlOfferTraderType = new GraphQLObjectType({
name: 'hodlOfferTraderType',
fields: () => {
return {
login: { type: GraphQLString },
online_status: { type: GraphQLString },
rating: { type: GraphQLString },
trades_count: { type: GraphQLInt },
url: { type: GraphQLString },
verified: { type: GraphQLBoolean },
verified_by: { type: GraphQLString },
strong_hodler: { type: GraphQLBoolean },
country: { type: GraphQLString },
country_code: { type: GraphQLString },
average_payment_time_minutes: { type: GraphQLInt },
average_release_time_minutes: { type: GraphQLInt },
days_since_last_trade: { type: GraphQLInt },
};
},
});
export const HodlOfferType = new GraphQLObjectType({
name: 'hodlOfferType',
fields: () => {
return {
id: { type: GraphQLString },
version: { type: GraphQLString },
asset_code: { type: GraphQLString },
searchable: { type: GraphQLBoolean },
country: { type: GraphQLString },
country_code: { type: GraphQLString },
working_now: { type: GraphQLBoolean },
side: { type: GraphQLString },
title: { type: GraphQLString },
description: { type: GraphQLString },
currency_code: { type: GraphQLString },
price: { type: GraphQLString },
min_amount: { type: GraphQLString },
max_amount: { type: GraphQLString },
first_trade_limit: { type: GraphQLString },
fee: { type: HodlOfferFeeType },
balance: { type: GraphQLString },
payment_window_minutes: { type: GraphQLInt },
confirmations: { type: GraphQLInt },
payment_method_instructions: {
type: new GraphQLList(HodlOfferPaymentType),
},
trader: { type: HodlOfferTraderType },
};
},
});
export const HodlCountryType = new GraphQLObjectType({
name: 'hodlCountryType',
fields: () => {
return {
code: { type: GraphQLString },
name: { type: GraphQLString },
native_name: { type: GraphQLString },
currency_code: { type: GraphQLString },
currency_name: { type: GraphQLString },
};
},
});
export const HodlCurrencyType = new GraphQLObjectType({
name: 'hodlCurrencyType',
fields: () => {
return {
code: { type: GraphQLString },
name: { type: GraphQLString },
type: { type: GraphQLString },
};
},
});

View file

@ -0,0 +1,5 @@
export const appUrls = {
fees: 'https://bitcoinfees.earn.com/api/v1/fees/recommended',
ticker: 'https://blockchain.info/ticker',
hodlhodl: 'https://hodlhodl.com/api',
};

View file

@ -0,0 +1,8 @@
require('dotenv').config();
export const envConfig = {
port: process.env.PORT || 3001,
env: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'silly',
hodlKey: process.env.HODL_KEY,
};

View file

@ -47,4 +47,7 @@ export const RateConfig: RateConfigProps = {
signMessage: { max: 10, window: '1s' },
verifyMessage: { max: 10, window: '1s' },
getUtxos: { max: 10, window: '1s' },
getOffers: { max: 10, window: '1s' },
getCountries: { max: 10, window: '1s' },
getCurrencies: { max: 10, window: '1s' },
};

View file

@ -1,66 +1,66 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": [
"dom",
"es6"
] /* Specify library files to be included in the compilation. */,
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "dist" /* Redirect output structure to the directory. */,
"rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"removeComments": true /* Do not emit comments to output. */,
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": [
"dom",
"es6"
] /* Specify library files to be included in the compilation. */,
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "dist" /* Redirect output structure to the directory. */,
"rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"removeComments": true /* Do not emit comments to output. */,
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
}

View file

@ -1 +1 @@
declare module "ln-service";
declare module 'ln-service';

View file

@ -1,21 +1,21 @@
const path = require("path");
const path = require('path');
module.exports = {
module: {
rules: [
{
exclude: [path.resolve(__dirname, "node_modules")],
test: /\.ts$/,
use: "ts-loader"
}
]
},
output: {
filename: "server.js",
path: path.resolve(__dirname, "dist")
},
resolve: {
extensions: [".ts", ".js"]
},
target: "node"
module: {
rules: [
{
exclude: [path.resolve(__dirname, 'node_modules')],
test: /\.ts$/,
use: 'ts-loader',
},
],
},
output: {
filename: 'server.js',
path: path.resolve(__dirname, 'dist'),
},
resolve: {
extensions: ['.ts', '.js'],
},
target: 'node',
};

View file

@ -1,20 +1,31 @@
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const merge = require("webpack-merge");
const nodeExternals = require("webpack-node-externals");
const path = require("path");
const webpack = require("webpack");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const path = require('path');
const webpack = require('webpack');
const WebpackShellPlugin = require('webpack-shell-plugin');
const common = require("./webpack.common.js");
const common = require('./webpack.common.js');
module.exports = merge.smart(common, {
devtool: "inline-source-map",
entry: ["webpack/hot/poll?1000", path.join(__dirname, "src/main.ts")],
externals: [
nodeExternals({
whitelist: ["webpack/hot/poll?1000"]
})
],
mode: "development",
plugins: [new CleanWebpackPlugin(), new webpack.HotModuleReplacementPlugin()],
watch: true
devtool: 'inline-source-map',
entry: ['webpack/hot/poll?1000', path.join(__dirname, 'src/main.ts')],
externals: [
nodeExternals({
modulesDir: path.resolve(__dirname, '../node_modules'),
whitelist: ['webpack/hot/poll?1000'],
}),
nodeExternals({
whitelist: ['webpack/hot/poll?1000'],
}),
],
mode: 'development',
plugins: [
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin(),
new WebpackShellPlugin({
onBuildEnd: ['yarn dev'],
}),
],
watch: true,
});

View file

@ -8,7 +8,12 @@ const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'source-map',
entry: [path.join(__dirname, 'src/main.ts')],
externals: [nodeExternals({})],
externals: [
nodeExternals({
modulesDir: path.resolve(__dirname, '../node_modules'),
}),
nodeExternals({}),
],
mode: 'production',
plugins: [new CleanWebpackPlugin()],
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff