chore: migration

This commit is contained in:
apotdevin 2021-12-07 22:16:40 -05:00
parent 59916b847e
commit a6ad30b599
No known key found for this signature in database
GPG Key ID: 4403F1DFBE779457
730 changed files with 29323 additions and 38218 deletions

View File

@ -1,85 +0,0 @@
version: 2
jobs:
# Define in CircleCi Project Variables: $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS
# Publish jobs require those variables
amd64:
machine:
enabled: true
steps:
- checkout
- run:
command: |
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker build --pull -t $DOCKERHUB_REPO:$CIRCLE_TAG-amd64 -f Dockerfile .
sudo docker push $DOCKERHUB_REPO:$CIRCLE_TAG-amd64
arm32v7:
machine:
enabled: true
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
#
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker build --pull -t $DOCKERHUB_REPO:$CIRCLE_TAG-arm32v7 -f arm32v7.Dockerfile .
sudo docker push $DOCKERHUB_REPO:$CIRCLE_TAG-arm32v7
arm64v8:
machine:
enabled: true
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
#
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker build --pull -t $DOCKERHUB_REPO:$CIRCLE_TAG-arm64v8 -f arm64v8.Dockerfile .
sudo docker push $DOCKERHUB_REPO:$CIRCLE_TAG-arm64v8
multiarch:
machine:
enabled: true
image: circleci/classic:201808-01
steps:
- run:
command: |
# Turn on Experimental features
sudo mkdir $HOME/.docker
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
#
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
sudo docker manifest create --amend $DOCKERHUB_REPO:$CIRCLE_TAG $DOCKERHUB_REPO:$CIRCLE_TAG-amd64 $DOCKERHUB_REPO:$CIRCLE_TAG-arm32v7 $DOCKERHUB_REPO:$CIRCLE_TAG-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$CIRCLE_TAG $DOCKERHUB_REPO:$CIRCLE_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$CIRCLE_TAG $DOCKERHUB_REPO:$CIRCLE_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$CIRCLE_TAG $DOCKERHUB_REPO:$CIRCLE_TAG-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$CIRCLE_TAG -p
workflows:
version: 2
publish:
jobs:
- amd64:
filters:
branches:
only:
- ci/circleci-testing
- arm32v7:
filters:
branches:
only:
- ci/circleci-testing
- arm64v8:
filters:
branches:
only:
- ci/circleci-testing
- multiarch:
requires:
- amd64
- arm32v7
- arm64v8
filters:
branches:
only:
- ci/circleci-testing

View File

@ -1,3 +0,0 @@
{
"extends": ["@commitlint/config-conventional"]
}

View File

@ -1,60 +1,26 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: { jsx: true },
},
env: {
browser: true,
amd: true,
node: true,
},
plugins: ['react', 'jest', 'import', 'prettier'],
settings: {
react: {
createClass: 'createReactClass',
pragma: 'React',
version: 'detect',
flowVersion: '0.53',
},
propWrapperFunctions: [
'forbidExtraProps',
{ property: 'freeze', object: 'Object' },
{ property: 'myFavoriteWrapper' },
],
linkComponents: ['Hyperlink', { name: 'Link', linkAttribute: 'to' }],
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:jest/recommended',
'plugin:prettier/recommended',
'prettier',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 0,
'import/no-unresolved': 'off',
'import/order': 2,
'no-unused-vars': 0,
camelcase: 'off',
'@typescript-eslint/camelcase': 'off',
'react/prop-types': 'off',
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
'@typescript-eslint/no-unused-vars': 'error',
},
};

58
.gitignore vendored
View File

@ -1,30 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
# compiled output
/dist
/node_modules
/.pnp
.pnp.js
/.next
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
# debug
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.development
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1

View File

@ -1,5 +0,0 @@
/.next
/.node_modules
**/*.generated.tsx
src/graphql/types.ts

View File

@ -1,3 +0,0 @@
{
"editor.formatOnSave": true
}

View File

@ -1,16 +1,18 @@
overwrite: true
schema: 'http://localhost:3000/api/v1'
documents: 'src/graphql/**/*.ts'
schema: 'http://localhost:3000/graphql'
documents: 'src/client/src/graphql/**/*.ts'
hooks:
afterAllFileWrite:
- prettier --write
- eslint --fix
generates:
src/graphql/fragmentTypes.json:
src/client/src/graphql/fragmentTypes.json:
plugins:
- fragment-matcher
src/graphql/types.ts:
src/client/src/graphql/types.ts:
plugins:
- typescript
- add:
content: '/* eslint-disable */'
src/graphql/:
src/client/src/graphql/:
preset: near-operation-file
presetConfig:
baseTypesPath: types.ts
@ -24,5 +26,3 @@ generates:
plugins:
- 'typescript-operations'
- 'typescript-react-apollo'
- add:
content: '/* eslint-disable */'

View File

@ -1,75 +0,0 @@
/* eslint @typescript-eslint/no-var-requires: 0 */
import { IncomingMessage, ServerResponse } from 'http';
import { useMemo } from 'react';
import {
ApolloClient,
InMemoryCache,
NormalizedCacheObject,
} from '@apollo/client';
import getConfig from 'next/config';
import possibleTypes from 'src/graphql/fragmentTypes.json';
const { publicRuntimeConfig } = getConfig();
const { apiUrl: uri } = publicRuntimeConfig;
let apolloClient: ApolloClient<NormalizedCacheObject> | undefined;
export type ResolverContext = {
req?: IncomingMessage;
res?: ServerResponse;
};
function createIsomorphLink(context: ResolverContext = {}) {
if (typeof window === 'undefined') {
const { SchemaLink } = require('@apollo/client/link/schema');
const { schema } = require('server/schema');
const { getContext } = require('server/schema/context');
return new SchemaLink({
schema,
context: getContext(context),
});
} else {
const { HttpLink } = require('@apollo/client/link/http');
return new HttpLink({
uri,
credentials: 'same-origin',
});
}
}
function createApolloClient(context?: ResolverContext) {
return new ApolloClient({
credentials: 'same-origin',
ssrMode: typeof window === 'undefined',
link: createIsomorphLink(context),
cache: new InMemoryCache({
...possibleTypes,
}),
defaultOptions: {
query: {
fetchPolicy: 'cache-first',
},
},
});
}
export function initializeApollo(
initialState: NormalizedCacheObject | null = null,
context?: ResolverContext
) {
const _apolloClient = apolloClient ?? createApolloClient(context);
if (initialState) {
const existingCache = _apolloClient.extract();
_apolloClient.cache.restore({ ...existingCache, ...initialState });
}
if (typeof window === 'undefined') return _apolloClient;
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function useApollo(initialState: NormalizedCacheObject | null) {
const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store;
}

View File

@ -1,22 +0,0 @@
version: '3'
services:
thunderhub:
image: apotdevin/thunderhub:test-amd64
restart: unless-stopped
stop_signal: SIGKILL
environment:
# BASE_PATH: '/thub'
# COOKIE_PATH: '/data/.cookie'
# SSO_SERVER_URL: 'lnd_bitcoin:10009'
# SSO_MACAROON_PATH: '/etc/lnd'
# SSO_CERT_PATH: '/etc/lnd/tls.cert'
# NO_CLIENT_ACCOUNTS: 'true'
ACCOUNT_CONFIG_PATH: '/data/thubConfig.yaml'
LOG_LEVEL: debug
volumes:
- D:/data:/data
ports:
- '3000:3000'
expose:
- '3000'
command: ['npm', 'run', 'start']

View File

@ -1,20 +0,0 @@
module.exports = {
collectCoverageFrom: [
'**/*.{js,jsx,ts,tsx}',
'!**/*.d.ts',
'!**/node_modules/**',
],
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest',
},
transformIgnorePatterns: [
'/node_modules/',
'^.+\\.module\\.(css|sass|scss)$',
],
moduleNameMapper: {
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
},
modulePaths: ['<rootDir>/'],
};

4
nest-cli.json Normal file
View File

@ -0,0 +1,4 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src/server"
}

33188
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,78 +1,76 @@
{
"name": "thunderhub",
"version": "0.12.31",
"description": "",
"main": "index.js",
"description": "Lightning Node Manager",
"license": "MIT",
"scripts": {
"bs": "yarn build && yarn start",
"dev": "next",
"build": "next build",
"start": "next start",
"start:two": "next start -p 3001",
"start:cookie": "sh ./scripts/initCookie.sh",
"start:secure": "node server/utils/secure-server.js",
"lint": "eslint . --ext ts --ext tsx --ext js",
"prebuild": "rimraf dist",
"build": "npm run build:nest && npm run build:next",
"build:nest": "nest build",
"build:next": "cd src/client && next build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"lint-staged": "lint-staged",
"format": "prettier --write \"**/*.{js,ts,tsx}\"",
"release": "standard-version",
"release:test": "standard-version --dry-run",
"release:minor": "standard-version --release-as minor",
"analyze": "npx cross-env ANALYZE=true next build",
"generate": "graphql-codegen --config codegen.yml",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"build:image": "docker build --no-cache -t apotdevin/thunderhub:test-amd64 .",
"build:32": "docker build --no-cache -f arm32v7.Dockerfile -t apotdevin/thunderhub:test-arm32v7 .",
"build:64": "docker build -f arm64v8.Dockerfile -t apotdevin/thunderhub:test-arm64v8 .",
"build:manifest": "docker manifest create apotdevin/thunderhub:test apotdevin/thunderhub:test-amd64 apotdevin/thunderhub:test-arm32v7 apotdevin/thunderhub:test-arm64v8",
"build:all": "sh ./scripts/buildAllImages.sh",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"upgrade-latest": "npx npm-check -u",
"tsc": "tsc",
"update": "sh ./scripts/updateToLatest.sh",
"prepare": "husky install"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@apollo/client": "^3.4.16",
"@emotion/babel-plugin": "^11.3.0",
"@visx/axis": "^2.2.2",
"@fullerstack/nax-ipware": "^0.0.4",
"@graphql-tools/schema": "^7.1.5",
"@nestjs/common": "^8.1.2",
"@nestjs/config": "^1.0.3",
"@nestjs/core": "^8.1.2",
"@nestjs/graphql": "^9.1.1",
"@nestjs/passport": "^8.0.1",
"@nestjs/platform-express": "^8.1.2",
"@nestjs/throttler": "^2.0.0",
"@visx/axis": "^2.3.0",
"@visx/chord": "^2.1.2",
"@visx/curve": "^2.1.0",
"@visx/event": "^2.1.2",
"@visx/group": "^2.1.0",
"@visx/responsive": "^2.1.2",
"@visx/scale": "^2.2.2",
"@visx/shape": "^2.2.2",
"@visx/tooltip": "^2.2.2",
"apollo-server-micro": "^2.25.2",
"apollo-server-express": "^3.4.0",
"balanceofsatoshis": "^11.8.1",
"bcryptjs": "^2.4.3",
"bech32": "^2.0.0",
"big.js": "^6.1.1",
"bech32": "^1.1.4",
"big.js": "^5.2.2",
"bip32": "^2.0.6",
"bip39": "^3.0.4",
"bitcoinjs-lib": "^5.2.0",
"boltz-core": "^0.4.1",
"cookie": "^0.4.1",
"cookie": "^0.4.0",
"crypto-js": "^4.1.1",
"d3-array": "^3.1.1",
"d3-array": "^2.12.1",
"d3-time-format": "^4.0.0",
"date-fns": "^2.25.0",
"graphql": "^15.7.1",
"graphql-iso-date": "^3.6.1",
"graphql-middleware": "^6.1.11",
"graphql-rate-limit": "^3.3.0",
"graphql": "^15.7.2",
"jest-fetch-mock": "^3.0.3",
"js-cookie": "^3.0.1",
"js-yaml": "^4.1.0",
"js-yaml": "^3.14.1",
"jsonwebtoken": "^8.5.1",
"ln-service": "^52.14.2",
"lodash": "^4.17.21",
"nest-winston": "^1.6.1",
"next": "^12.0.1",
"node-fetch": "^2.6.1",
"numeral": "^2.0.6",
"passport": "^0.5.0",
"passport-jwt": "^4.0.0",
"qrcode.react": "^1.0.1",
"react": "^17.0.2",
"react-circular-progressbar": "^2.0.4",
@ -81,13 +79,15 @@
"react-feather": "^2.0.9",
"react-grid-layout": "^1.3.0",
"react-qr-reader": "^2.2.1",
"react-select": "^5.1.0",
"react-select": "^5.2.0",
"react-slider": "^1.3.1",
"react-spinners": "^0.11.0",
"react-spring": "^9.3.0",
"react-table": "^7.7.0",
"react-toastify": "^8.0.3",
"react-tooltip": "^4.2.21",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.4.0",
"secp256k1": "^4.0.2",
"socks-proxy-agent": "^6.1.0",
"styled-components": "^5.3.3",
@ -97,30 +97,29 @@
"winston": "^3.3.3"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@commitlint/cli": "^13.2.1",
"@commitlint/config-conventional": "^13.2.0",
"@graphql-codegen/add": "^3.1.0",
"@graphql-codegen/cli": "^2.2.2",
"@graphql-codegen/cli": "^2.3.0",
"@graphql-codegen/fragment-matcher": "^3.2.0",
"@graphql-codegen/introspection": "^2.1.0",
"@graphql-codegen/near-operation-file-preset": "^2.2.0",
"@graphql-codegen/typescript": "^2.3.0",
"@graphql-codegen/typescript-operations": "^2.2.0",
"@graphql-codegen/typescript-react-apollo": "^3.2.0",
"@graphql-codegen/typescript-resolvers": "^2.4.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.2",
"@graphql-codegen/near-operation-file-preset": "^2.2.2",
"@graphql-codegen/typescript": "^2.4.1",
"@graphql-codegen/typescript-operations": "^2.2.1",
"@graphql-codegen/typescript-react-apollo": "^3.2.2",
"@graphql-codegen/typescript-resolvers": "^2.4.2",
"@nestjs/cli": "^8.1.4",
"@nestjs/schematics": "^8.0.4",
"@nestjs/testing": "^8.1.2",
"@types/bcryptjs": "^2.4.2",
"@types/big.js": "^6.1.2",
"@types/cookie": "^0.4.1",
"@types/crypto-js": "^4.0.2",
"@types/d3-array": "^3.0.2",
"@types/d3-time-format": "^4.0.0",
"@types/graphql-iso-date": "^3.4.0",
"@types/js-cookie": "^2.2.7",
"@types/express": "^4.17.13",
"@types/jest": "^27.0.2",
"@types/js-cookie": "^3.0.0",
"@types/js-yaml": "^4.0.4",
"@types/jsonwebtoken": "^8.5.5",
"@types/lodash": "^4.14.176",
"@types/node": "^16.11.6",
"@types/node-fetch": "^2.5.12",
"@types/numeral": "^2.0.2",
@ -135,33 +134,47 @@
"@types/styled-components": "^5.1.15",
"@types/styled-react-modal": "^1.2.1",
"@types/styled-theming": "^2.2.5",
"@types/supertest": "^2.0.11",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^5.2.0",
"@typescript-eslint/eslint-plugin": "^4.29.2",
"@typescript-eslint/parser": "^4.29.2",
"apollo-server": "^2.25.2",
"apollo-server-testing": "^2.25.2",
"babel-jest": "^27.3.1",
"babel-loader": "^8.2.3",
"babel-plugin-inline-react-svg": "^2.0.1",
"babel-plugin-styled-components": "^1.13.3",
"babel-preset-react-app": "^10.0.0",
"cross-env": "^7.0.3",
"eslint": "^7.32.0",
"eslint-config-next": "^12.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jest": "^25.2.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^7.0.4",
"eslint-plugin-prettier": "^3.4.1",
"husky": "^7.0.0",
"jest": "^27.3.1",
"jest-fetch-mock": "^3.0.3",
"lint-staged": "^11.2.6",
"lint-staged": "^12.1.2",
"prettier": "^2.4.1",
"standard-version": "^9.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.6",
"ts-jest": "^27.0.7",
"ts-loader": "^9.2.6",
"ts-node": "^10.4.0",
"tsconfig-paths": "^3.11.0",
"typescript": "^4.4.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"lint-staged": {
"*.+(ts|tsx)": [
"prettier --write",

View File

@ -1,30 +0,0 @@
import crypto from 'crypto';
import { ApolloServer } from 'apollo-server-micro';
import getConfig from 'next/config';
import { readCookie } from 'server/helpers/fileHelpers';
import { schema } from 'server/schema';
import { getContext } from 'server/schema/context';
const { publicRuntimeConfig, serverRuntimeConfig } = getConfig();
const { apiBaseUrl, nodeEnv } = publicRuntimeConfig;
const { cookiePath } = serverRuntimeConfig;
export const secret =
nodeEnv === 'development'
? '123456789'
: crypto.randomBytes(64).toString('hex');
readCookie(cookiePath);
const apolloServer = new ApolloServer({
schema,
context: ctx => getContext(ctx),
});
export const config = {
api: {
bodyParser: false,
},
};
export default apolloServer.createHandler({ path: apiBaseUrl });

772
schema.gql Normal file
View File

@ -0,0 +1,772 @@
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type AmbossSubscription {
end_date: String!
subscribed: Boolean!
upgradable: Boolean!
}
type AmbossUser {
subscription: AmbossSubscription
}
type AuthResponse {
message: String!
status: String!
}
type Balances {
lightning: LightningBalance!
onchain: OnChainBalance!
}
type BaseInvoice {
id: String!
request: String!
}
type BaseNode {
_id: String
name: String
public_key: String!
socket: String!
}
type BasePoints {
alias: String!
amount: Float!
}
type BitcoinFee {
fast: Float!
halfHour: Float!
hour: Float!
minimum: Float!
}
type BoltzInfoType {
feePercent: Float!
max: Float!
min: Float!
}
type BoltzSwap {
boltz: BoltzSwapStatus
id: String
}
type BoltzSwapStatus {
status: String!
transaction: BoltzSwapTransaction
}
type BoltzSwapTransaction {
eta: Float
hex: String
id: String
}
type BosDecrease {
decreased_inbound_on: String!
liquidity_inbound: String!
liquidity_inbound_opening: String!
liquidity_inbound_pending: String!
liquidity_outbound: String!
liquidity_outbound_opening: String!
liquidity_outbound_pending: String!
}
type BosIncrease {
increased_inbound_on: String!
liquidity_inbound: String!
liquidity_inbound_opening: String!
liquidity_inbound_pending: String!
liquidity_outbound: String!
liquidity_outbound_opening: String!
liquidity_outbound_pending: String!
}
type BosRebalanceResult {
decrease: BosDecrease
increase: BosIncrease
result: BosResult
}
type BosResult {
rebalance_fees_spent: String!
rebalanced: String!
}
type BosScore {
alias: String!
position: Float!
public_key: String!
score: Float!
updated: String!
}
type BosScoreInfo {
count: Float!
first: BosScore
last: BosScore
}
type ChainAddressSend {
confirmationCount: Float!
id: String!
isConfirmed: Boolean!
isOutgoing: Boolean!
tokens: Float
}
type ChainTransaction {
block_id: String
confirmation_count: Float
confirmation_height: Float
created_at: String!
description: String
fee: Float
id: String!
is_confirmed: Boolean!
is_outgoing: Boolean!
output_addresses: [String!]!
tokens: Float!
transaction: String
}
type Channel {
bosScore: BosScore
capacity: Float!
channel_age: Float!
commit_transaction_fee: Float!
commit_transaction_weight: Float!
id: String!
is_active: Boolean!
is_closing: Boolean!
is_opening: Boolean!
is_partner_initiated: Boolean!
is_private: Boolean!
is_static_remote_key: Boolean
local_balance: Float!
local_reserve: Float!
partner_fee_info: SingleChannel!
partner_node_info: Node!
partner_public_key: String!
pending_payments: [PendingPayment!]!
pending_resume: PendingResume!
received: Float!
remote_balance: Float!
remote_reserve: Float!
sent: Float!
time_offline: Float
time_online: Float
transaction_id: String!
transaction_vout: Float!
unsettled_balance: Float!
}
type ChannelFeeHealth {
id: String!
mySide: FeeHealth!
partner: Node!
partnerSide: FeeHealth!
}
type ChannelHealth {
averageVolumeNormalized: String!
id: String!
partner: Node!
score: Float!
volumeNormalized: String!
}
type ChannelReport {
commit: Float!
incomingPendingHtlc: Float!
local: Float!
maxIn: Float!
maxOut: Float!
outgoingPendingHtlc: Float!
remote: Float!
totalPendingHtlc: Float!
}
type ChannelRequest {
callback: String
k1: String
tag: String
uri: String
}
type ChannelTimeHealth {
id: String!
monitoredDowntime: Float!
monitoredTime: Float!
monitoredUptime: Float!
partner: Node!
score: Float!
significant: Boolean!
}
type ChannelsFeeHealth {
channels: ChannelFeeHealth!
score: Float!
}
type ChannelsHealth {
channels: ChannelHealth!
score: Float!
}
type ChannelsTimeHealth {
channels: ChannelTimeHealth!
score: Float!
}
type ClosedChannel {
capacity: Float!
channel_age: Float!
close_confirm_height: Float
close_transaction_id: String
final_local_balance: Float!
final_time_locked_balance: Float!
id: String
is_breach_close: Boolean!
is_cooperative_close: Boolean!
is_funding_cancel: Boolean!
is_local_force_close: Boolean!
is_remote_force_close: Boolean!
partner_node_info: Node!
partner_public_key: String!
transaction_id: String!
transaction_vout: Float!
}
type CreateBoltzReverseSwapType {
decodedInvoice: DecodeInvoice
id: String!
invoice: String!
lockupAddress: String!
minerFeeInvoice: String
onchainAmount: Float!
preimage: String
preimageHash: String
privateKey: String
publicKey: String
receivingAddress: String!
redeemScript: String!
timeoutBlockHeight: Float!
}
type CreateInvoice {
chain_address: String
created_at: String!
description: String!
id: String!
mtokens: String
request: String!
secret: String!
tokens: Float
}
type CreateMacaroon {
base: String!
hex: String!
}
type DecodeInvoice {
chain_address: String
cltv_delta: Float
description: String!
description_hash: String
destination: String!
destination_node: Node
expires_at: String!
id: String!
mtokens: String!
payment: String
routes: [[Route!]!]!
safe_tokens: Float!
tokens: Float!
}
type FeeHealth {
base: String!
baseOver: Boolean!
baseScore: Float!
rate: Float!
rateOver: Boolean!
rateScore: Float!
score: Float!
}
type Forward {
created_at: String!
fee: Float!
fee_mtokens: String!
incoming_channel: String!
mtokens: String!
outgoing_channel: String!
tokens: Float!
}
type GetMessages {
messages: [Message!]!
token: String
}
type GetResumeType {
offset: Float!
resume: [Transaction!]!
}
type Hops {
channel: String!
channel_capacity: Float!
fee_mtokens: String!
forward_mtokens: String!
timeout: Float!
}
type InvoicePayment {
in_channel: String!
messages: MessageType
}
type InvoiceType {
chain_address: String
confirmed_at: String
created_at: String
date: String!
description: String!
description_hash: String
expires_at: String!
id: String!
is_canceled: Boolean
is_confirmed: Boolean!
is_held: Boolean
is_private: Boolean!
is_push: Boolean
payments: [InvoicePayment!]!
received: Float
received_mtokens: String!
request: String
secret: String!
tokens: String!
type: String!
}
type LightningAddress {
lightning_address: String!
pubkey: String!
}
type LightningBalance {
active: String!
commit: String!
confirmed: String!
pending: String!
}
type LightningNodeSocialInfo {
socials: NodeSocial
}
type LnMarketsUserInfo {
account_type: String
balance: String
last_ip: String
linkingpublickey: String
uid: String
username: String
}
union LnUrlRequest = ChannelRequest | PayRequest | WithdrawRequest
type Message {
alias: String
contentType: String
date: String!
id: String!
message: String
sender: String
tokens: Float
verified: Boolean!
}
type MessageType {
message: String
}
type Mutation {
addPeer(isTemporary: Boolean, publicKey: String, socket: String, url: String): Boolean!
bosRebalance(avoid: [String!], in_through: String, max_fee: Float, max_fee_rate: Float, max_rebalance: Float, node: String, out_inbound: Float, out_through: String, timeout_minutes: Float): BosRebalanceResult!
claimBoltzTransaction(destination: String!, fee: Float!, preimage: String!, privateKey: String!, redeem: String!, transaction: String!): String!
closeChannel(forceClose: Boolean, id: String!, targetConfirmations: Float, tokensPerVByte: Float): OpenOrCloseChannel!
createAddress: String!
createBaseInvoice(amount: Float!): BaseInvoice!
createBoltzReverseSwap(address: String, amount: Float!): CreateBoltzReverseSwapType!
createInvoice(amount: Float!, description: String, includePrivate: Boolean, secondsUntil: Float): CreateInvoice!
createMacaroon(permissions: NetworkInfoInput!): CreateMacaroon!
createThunderPoints(alias: String!, id: String!, public_key: String!, uris: [String!]!): Boolean!
fetchLnUrl(url: String!): LnUrlRequest!
getAuthToken(cookie: String): Boolean!
getSessionToken(id: String!, password: String!): String!
keysend(destination: String, tokens: Float!): PayInvoice!
lnMarketsDeposit(amount: Float!): Boolean!
lnMarketsLogin: AuthResponse!
lnMarketsLogout: Boolean!
lnMarketsWithdraw(amount: Float!): Boolean!
lnUrlAuth(url: String!): AuthResponse!
lnUrlChannel(callback: String!, k1: String!, uri: String!): String!
lnUrlPay(amount: Float!, callback: String!, comment: String): PaySuccess!
lnUrlWithdraw(amount: Float!, callback: String!, description: String, k1: String!): String!
loginAmboss: Boolean!
logout: Boolean!
openChannel(amount: Float!, isPrivate: Boolean, partnerPublicKey: String!, pushTokens: Float = 0, tokensPerVByte: Float): OpenOrCloseChannel!
pay(max_fee: Float!, max_paths: Float!, out: [String!], request: String!): Boolean!
removePeer(publicKey: String): Boolean!
sendMessage(maxFee: Float, message: String!, messageType: String, publicKey: String!, tokens: Float): Float!
sendToAddress(address: String!, fee: Float, sendAll: Boolean, target: Float, tokens: Float): ChainAddressSend!
updateFees(base_fee_tokens: Float, cltv_delta: Float, fee_rate: Float, max_htlc_mtokens: String, min_htlc_mtokens: String, transaction_id: String, transaction_vout: Float): Boolean!
updateMultipleFees(channels: [UpdateRoutingFeesParams!]!): Boolean!
}
type NetworkInfo {
averageChannelSize: Float!
channelCount: Float!
maxChannelSize: Float!
medianChannelSize: Float!
minChannelSize: Float!
nodeCount: Float!
notRecentlyUpdatedPolicyCount: Float!
totalCapacity: Float!
}
input NetworkInfoInput {
is_ok_to_adjust_peers: Boolean!
is_ok_to_create_chain_addresses: Boolean!
is_ok_to_create_invoices: Boolean!
is_ok_to_create_macaroons: Boolean!
is_ok_to_derive_keys: Boolean!
is_ok_to_get_chain_transactions: Boolean!
is_ok_to_get_invoices: Boolean!
is_ok_to_get_payments: Boolean!
is_ok_to_get_peers: Boolean!
is_ok_to_get_wallet_info: Boolean!
is_ok_to_pay: Boolean!
is_ok_to_send_to_chain_addresses: Boolean!
is_ok_to_sign_bytes: Boolean!
is_ok_to_sign_messages: Boolean!
is_ok_to_stop_daemon: Boolean!
is_ok_to_verify_bytes_signatures: Boolean!
is_ok_to_verify_messages: Boolean!
}
type Node {
node: NodeType!
}
type NodeBosHistory {
info: BosScoreInfo!
scores: [BosScore!]!
}
type NodeInfo {
active_channels_count: Float!
alias: String!
chains: [String!]!
closed_channels_count: Float!
color: String!
current_block_hash: String!
current_block_height: Float!
is_synced_to_chain: Boolean!
is_synced_to_graph: Boolean!
latest_block_at: String!
peers_count: Float!
pending_channels_count: Float!
public_key: String!
uris: [String!]!
version: String!
}
type NodePolicy {
base_fee_mtokens: String
cltv_delta: Float
fee_rate: Float
is_disabled: Boolean
max_htlc_mtokens: String
min_htlc_mtokens: String
node: Node
updated_at: String
}
type NodeSocial {
info: NodeSocialInfo
}
type NodeSocialInfo {
email: String
private: Boolean
telegram: String
twitter: String
twitter_verified: Boolean
website: String
}
type NodeType {
alias: String!
capacity: String
channel_count: Float
color: String
public_key: String
updated_at: String
}
type OnChainBalance {
closing: String!
confirmed: String!
pending: String!
}
type OpenOrCloseChannel {
transactionId: String!
transactionOutputIndex: String!
}
type PayInvoice {
fee: Float!
fee_mtokens: String!
hops: [Hops!]!
id: String!
is_confirmed: Boolean!
is_outgoing: Boolean!
mtokens: String!
safe_fee: Float!
safe_tokens: Float!
secret: String!
tokens: Float!
}
type PayRequest {
callback: String
commentAllowed: Float
maxSendable: String
metadata: String
minSendable: String
tag: String
}
type PaySuccess {
ciphertext: String
description: String
iv: String
message: String
tag: String
url: String
}
type PaymentType {
created_at: String
date: String!
destination: String!
destination_node: Node!
fee: Float!
fee_mtokens: String!
hops: [Node!]!
id: String!
index: Float
is_confirmed: Boolean!
is_outgoing: Boolean!
mtokens: String!
request: String
safe_fee: Float!
safe_tokens: Float
secret: String!
tokens: String!
type: String!
}
type Peer {
bytes_received: Float!
bytes_sent: Float!
is_inbound: Boolean!
is_sync_peer: Boolean
partner_node_info: Node!
ping_time: Float!
public_key: String!
socket: String!
tokens_received: Float!
tokens_sent: Float!
}
type PendingChannel {
close_transaction_id: String
is_active: Boolean!
is_closing: Boolean!
is_opening: Boolean!
is_timelocked: Boolean!
local_balance: Float!
local_reserve: Float!
partner_node_info: Node!
partner_public_key: String!
received: Float!
remote_balance: Float!
remote_reserve: Float!
sent: Float!
timelock_blocks: Float
timelock_expiration: Float
transaction_fee: Float
transaction_id: String!
transaction_vout: Float!
}
type PendingPayment {
id: String!
is_outgoing: Boolean!
timeout: Float!
tokens: Float!
}
type PendingResume {
incoming_amount: Float!
incoming_tokens: Float!
outgoing_amount: Float!
outgoing_tokens: Float!
total_amount: Float!
total_tokens: Float!
}
type Policy {
base_fee_mtokens: String
cltv_delta: Float
fee_rate: Float
is_disabled: Boolean
max_htlc_mtokens: String
min_htlc_mtokens: String
public_key: String!
updated_at: String
}
type Query {
decodeRequest(request: String!): DecodeInvoice!
getAccount: ServerAccount!
getAccountingReport(category: String, currency: String, fiat: String, month: String, year: String): String!
getAmbossLoginToken: String!
getAmbossUser: AmbossUser
getBackups: String!
getBaseCanConnect: Boolean!
getBaseNodes: [BaseNode!]!
getBasePoints: [BasePoints!]!
getBitcoinFees: BitcoinFee!
getBitcoinPrice: String!
getBoltzInfo: BoltzInfoType!
getBoltzSwapStatus(ids: [String!]!): [BoltzSwap!]!
getBosScores: [BosScore!]!
getChainTransactions: [ChainTransaction!]!
getChannel(id: String!, pubkey: String): SingleChannel!
getChannelReport: ChannelReport!
getChannels(active: Boolean): [Channel!]!
getClosedChannels: [ClosedChannel!]!
getFeeHealth: ChannelsFeeHealth!
getForwards(days: Float!): [Forward!]!
getHello: String!
getInvoiceStatusChange(id: String!): String!
getLatestVersion: String!
getLightningAddressInfo(address: String!): PayRequest!
getLightningAddresses: [LightningAddress!]!
getLnMarketsStatus: String!
getLnMarketsUrl: String!
getLnMarketsUserInfo: LnMarketsUserInfo!
getMessages(initialize: Boolean): GetMessages!
getNetworkInfo: NetworkInfo!
getNode(publicKey: String!, withoutChannels: Boolean): Node!
getNodeBalances: Balances!
getNodeBosHistory(pubkey: String!): NodeBosHistory!
getNodeInfo: NodeInfo!
getNodeSocialInfo(pubkey: String!): LightningNodeSocialInfo!
getPeers: [Peer!]!
getPendingChannels: [PendingChannel!]!
getResume(limit: Float, offset: Float): GetResumeType!
getServerAccounts: [ServerAccount!]!
getTimeHealth: ChannelsTimeHealth!
getUtxos: [Utxo!]!
getVolumeHealth: ChannelsHealth!
getWalletInfo: Wallet!
recoverFunds(backup: String!): Boolean!
signMessage(message: String!): String!
verifyBackups(backup: String!): Boolean!
verifyMessage(message: String!, signature: String!): String!
}
type Route {
base_fee_mtokens: String
channel: String
cltv_delta: Float
fee_rate: Float
public_key: String!
}
type ServerAccount {
id: String!
loggedIn: Boolean!
name: String!
type: String!
}
type SingleChannel {
capacity: Float!
id: String!
node_policies: NodePolicy
partner_node_policies: NodePolicy
policies: [Policy!]!
transaction_id: String!
transaction_vout: Float!
updated_at: String
}
union Transaction = InvoiceType | PaymentType
input UpdateRoutingFeesParams {
base_fee_mtokens: String
base_fee_tokens: Float
cltv_delta: Float
fee_rate: Float
max_htlc_mtokens: String
min_htlc_mtokens: String
transaction_id: String
transaction_vout: Float
}
type Utxo {
address: String!
address_format: String!
confirmation_count: Float!
output_script: String!
tokens: Float!
transaction_id: String!
transaction_vout: Float!
}
type Wallet {
build_tags: [String!]!
commit_hash: String!
is_autopilotrpc_enabled: Boolean!
is_chainrpc_enabled: Boolean!
is_invoicesrpc_enabled: Boolean!
is_signrpc_enabled: Boolean!
is_walletrpc_enabled: Boolean!
is_watchtowerrpc_enabled: Boolean!
is_wtclientrpc_enabled: Boolean!
}
type WithdrawRequest {
callback: String
defaultDescription: String
k1: String
maxWithdrawable: String
minWithdrawable: String
tag: String
}

View File

@ -1,144 +0,0 @@
#!/bin/sh
REPO=apotdevin/thunderhub
BASE=base
echo
echo
echo "------------------------------------------"
echo "Building images for" $REPO
echo "------------------------------------------"
echo
echo
git checkout master || exit
git pull || exit
VERSION=$(git describe --tags --abbrev=0 2>&1)
git checkout $VERSION || exit
NOT_LATEST=false
echo "Do you want to build images for version" $VERSION "?"
select yn in "Yes" "No" "Specify"; do
case $yn in
Yes ) break;;
Specify) NOT_LATEST=true; break;;
No ) exit;;
esac
done
if [ "$NOT_LATEST" = true ]; then
read -p "Enter the version you want to build: " VERSION
git checkout $VERSION || exit
fi
START=`date +%s`
echo
echo
echo "------------------------------------------"
echo "Building amd64 image for version" $VERSION
echo "------------------------------------------"
echo
echo
docker build --pull -t $REPO:$VERSION-amd64 -f Dockerfile .
docker push $REPO:$VERSION-amd64
docker build --build-arg BASE_PATH='/thub' --pull -t $REPO:$BASE-$VERSION-amd64 -f Dockerfile .
docker push $REPO:$BASE-$VERSION-amd64
ENDAMD=`date +%s`
echo
echo
echo "------------------------------------------"
echo "Building arm32v7 image for version" $VERSION
echo "------------------------------------------"
echo
echo
docker build --pull -t $REPO:$VERSION-arm32v7 -f arm32v7.Dockerfile .
docker push $REPO:$VERSION-arm32v7
docker build --build-arg BASE_PATH='/thub' --pull -t $REPO:$BASE-$VERSION-arm32v7 -f arm32v7.Dockerfile .
docker push $REPO:$BASE-$VERSION-arm32v7
ENDARM32=`date +%s`
echo
echo
echo "------------------------------------------"
echo "Building arm64v8 image for version" $VERSION
echo "------------------------------------------"
echo
echo
docker build --pull -t $REPO:$VERSION-arm64v8 -f arm64v8.Dockerfile .
docker push $REPO:$VERSION-arm64v8
docker build --build-arg BASE_PATH='/thub' --pull -t $REPO:$BASE-$VERSION-arm64v8 -f arm64v8.Dockerfile .
docker push $REPO:$BASE-$VERSION-arm64v8
ENDARM64=`date +%s`
echo
echo
echo "------------------------------------------"
echo "Creating manifest for version" $VERSION
echo "------------------------------------------"
echo
echo
docker manifest create --amend $REPO:$VERSION $REPO:$VERSION-amd64 $REPO:$VERSION-arm32v7 $REPO:$VERSION-arm64v8
docker manifest annotate $REPO:$VERSION $REPO:$VERSION-amd64 --os linux --arch amd64
docker manifest annotate $REPO:$VERSION $REPO:$VERSION-arm32v7 --os linux --arch arm --variant v7
docker manifest annotate $REPO:$VERSION $REPO:$VERSION-arm64v8 --os linux --arch arm64 --variant v8
docker manifest push $REPO:$VERSION -p
echo
echo
echo "------------------------------------------"
echo "Creating manifest for version" $BASE $VERSION
echo "------------------------------------------"
echo
echo
docker manifest create --amend $REPO:$BASE-$VERSION $REPO:$BASE-$VERSION-amd64 $REPO:$BASE-$VERSION-arm32v7 $REPO:$BASE-$VERSION-arm64v8
docker manifest annotate $REPO:$BASE-$VERSION $REPO:$BASE-$VERSION-amd64 --os linux --arch amd64
docker manifest annotate $REPO:$BASE-$VERSION $REPO:$BASE-$VERSION-arm32v7 --os linux --arch arm --variant v7
docker manifest annotate $REPO:$BASE-$VERSION $REPO:$BASE-$VERSION-arm64v8 --os linux --arch arm64 --variant v8
docker manifest push $REPO:$BASE-$VERSION -p
echo
echo
echo "------------------------------------------"
echo "Build Stats"
echo "------------------------------------------"
echo
echo
RUNTIME=$((ENDAMD-START))
RUNTIME1=$((ENDARM32-ENDAMD))
RUNTIME2=$((ENDARM64-ENDARM32))
git checkout master
git pull
echo
echo
echo "------------------------------------------"
echo "DONE"
echo "------------------------------------------"
echo
echo
echo "Finished building and pushing images for" $REPO:$VERSION "and for" $REPO:$BASE-$VERSION
echo
echo "amd64 took" $RUNTIME "seconds"
echo "arm32v7 took" $RUNTIME1 "seconds"
echo "arm64v8 took" $RUNTIME2 "seconds"
echo
echo

View File

@ -1,13 +0,0 @@
#!/bin/sh
FILE=$1
echo "Creating cookie file for SSO authentication at" $FILE
mkdir -p "${FILE%/*}"
echo "86AOqw7OfLeBKn0VlOuH5V0E51Qxy9BoXQ8qMDql901mc5GuXvdVRogWrZkuH2nRel5FA9H2Ie4rTLDO" > "$FILE"
echo "Cookie file created."
echo "Starting ThunderHub server..."
npm run start

View File

@ -1,33 +0,0 @@
#!/bin/sh
# fetch latest master
echo "Checking for changes upstream ..."
git fetch
UPSTREAM=${1:-'@{u}'}
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse "$UPSTREAM")
if [ $LOCAL = $REMOTE ]; then
TAG=$(git tag | sort -V | tail -1)
echo "You are up-to-date on version" $TAG
else
echo "Reseting repository..."
git reset --hard
echo "Pulling latest changes..."
git pull -p
# install deps
echo "Installing dependencies..."
npm install --quiet
# build nextjs
echo "Building application..."
npm run build
# remove useless deps
echo "Removing unneccesary modules..."
npm prune --production
TAG=$(git tag | sort -V | tail -1)
echo "Updated to version" $TAG
fi

View File

@ -1,86 +0,0 @@
import { logger } from 'server/helpers/logger';
import { appUrls } from 'server/utils/appUrls';
import { fetchWithProxy } from 'server/utils/fetch';
export const BoltzApi = {
getPairs: async () => {
try {
const response = await fetchWithProxy(`${appUrls.boltz}/getpairs`);
return (await response.json()) as any;
} catch (error: any) {
logger.error('Error getting pairs from Boltz: %o', error);
throw new Error('ErrorGettingBoltzPairs');
}
},
getFeeEstimations: async () => {
try {
const response = await fetchWithProxy(
`${appUrls.boltz}/getfeeestimation`
);
return (await response.json()) as any;
} catch (error: any) {
logger.error('Error getting fee estimations from Boltz: %o', error);
throw new Error(error);
}
},
getSwapStatus: async (id: string) => {
try {
const body = { id };
const response = await fetchWithProxy(`${appUrls.boltz}/swapstatus`, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
});
return (await response.json()) as any;
} catch (error: any) {
logger.error('Error getting fee estimations from Boltz: %o', error);
throw new Error(error);
}
},
createReverseSwap: async (
invoiceAmount: number,
preimageHash: string,
claimPublicKey: string
) => {
try {
const body = {
type: 'reversesubmarine',
pairId: 'BTC/BTC',
orderSide: 'buy',
referralId: 'thunderhub',
invoiceAmount,
preimageHash,
claimPublicKey,
};
const response = await fetchWithProxy(`${appUrls.boltz}/createswap`, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
});
return (await response.json()) as any;
} catch (error: any) {
logger.error('Error getting fee estimations from Boltz: %o', error);
throw new Error(error);
}
},
broadcastTransaction: async (transactionHex: string) => {
try {
const body = {
currency: 'BTC',
transactionHex,
};
const response = await fetchWithProxy(
`${appUrls.boltz}/broadcasttransaction`,
{
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
}
);
return (await response.json()) as any;
} catch (error: any) {
logger.error('Error broadcasting transaction from Boltz: %o', error);
throw new Error(error);
}
},
};

View File

@ -1,66 +0,0 @@
import { logger } from 'server/helpers/logger';
import { appUrls } from 'server/utils/appUrls';
import { fetchWithProxy } from 'server/utils/fetch';
export const LnMarketsApi = {
getUser: async (token: string) => {
try {
const response = await fetchWithProxy(`${appUrls.lnMarkets}/user`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
});
return (await response.json()) as any;
} catch (error: any) {
logger.error(
`Error getting user info from ${appUrls.lnMarkets}/user. Error: %o`,
error
);
throw new Error('ProblemAuthenticatingWithLnMarkets');
}
},
getDepositInvoice: async (token: string, amount: number) => {
try {
const response = await fetchWithProxy(
`${appUrls.lnMarkets}/user/deposit`,
{
method: 'post',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'content-type': 'application/json',
},
body: JSON.stringify({ amount, unit: 'sat' }),
}
);
return (await response.json()) as any;
} catch (error: any) {
logger.error(
`Error getting invoice to deposit from LnMarkets. Error: %o`,
error
);
throw new Error('ProblemGettingDepositInvoice');
}
},
withdraw: async (token: string, amount: number, invoice: string) => {
try {
const response = await fetchWithProxy(
`${appUrls.lnMarkets}/user/withdraw`,
{
method: 'post',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'content-type': 'application/json',
},
body: JSON.stringify({ amount, unit: 'sat', invoice }),
}
);
return (await response.json()) as any;
} catch (error: any) {
logger.error(`Error withdrawing from LnMarkets. Error: %o`, error);
throw new Error('ProblemWithdrawingFromLnMarkets');
}
},
};

View File

@ -1,78 +0,0 @@
import { resolveEnvVarsInAccount } from '../env';
import { AccountType, UnresolvedAccountType } from '../fileHelpers';
const vars = {
YML_ENV_1: 'firstEnv',
YML_ENV_2: 'macaroonString',
YML_ENV_3: 'false',
YML_ENV_4: 'true',
};
jest.mock('next/config', () => () => ({
serverRuntimeConfig: vars,
}));
describe('resolveEnvVarsInAccount', () => {
it('returns resolved account', () => {
const account: UnresolvedAccountType = {
name: '{YML_ENV_1}',
serverUrl: 'server.url:10009',
macaroon: '{YML_ENV_2}',
};
const resolved = resolveEnvVarsInAccount(account);
const result: AccountType = {
name: 'firstEnv',
serverUrl: 'server.url:10009',
macaroon: 'macaroonString',
};
expect(resolved).toStrictEqual(result);
});
it('resolves false boolean values', () => {
const account: UnresolvedAccountType = {
name: '{YML_ENV_1}',
serverUrl: 'server.url:10009',
encrypted: '{YML_ENV_3}',
};
const resolved = resolveEnvVarsInAccount(account);
const result: AccountType = {
name: 'firstEnv',
serverUrl: 'server.url:10009',
encrypted: false,
};
expect(resolved).toStrictEqual(result);
});
it('resolves true boolean values', () => {
const account: UnresolvedAccountType = {
name: '{YML_ENV_1}',
serverUrl: 'server.url:10009',
encrypted: '{YML_ENV_4}',
};
const resolved = resolveEnvVarsInAccount(account);
const result: AccountType = {
name: 'firstEnv',
serverUrl: 'server.url:10009',
encrypted: true,
};
expect(resolved).toStrictEqual(result);
});
it('does not resolve non existing env vars', () => {
const account: UnresolvedAccountType = {
macaroon: '{YML_ENV_NONE}',
};
const resolved = resolveEnvVarsInAccount(account);
expect(resolved).toStrictEqual({
macaroon: '{YML_ENV_NONE}',
});
});
});

View File

@ -1,330 +0,0 @@
import { existsSync, readFileSync } from 'fs';
import path from 'path';
import {
getParsedAccount,
hashPasswords,
getAccountsFromYaml,
AccountType,
} from '../fileHelpers';
const mockedExistsSync: jest.Mock = existsSync as any;
const mockedReadFileSync: jest.Mock = readFileSync as any;
jest.mock('fs');
describe('getParsedAccount', () => {
const masterPassword = 'master password';
it('returns null without an account name', () => {
const raw = {
lndDir: 'LND DIR',
name: '',
certificate: 'RAW CERT',
serverUrl: 'server.url',
macaroon: 'RAW MACAROON',
};
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account).toBeNull();
});
it('returns null on invalid network', () => {
const raw = {
lndDir: 'LND DIR',
name: 'name',
certificate: 'RAW CERT',
serverUrl: 'server.url',
macaroon: 'RAW MACAROON',
network: 'foo' as any,
};
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account).toBeNull();
});
it('returns null without an account server url', () => {
const raw = {
name: 'NAME',
certificate: 'RAW CERT',
serverUrl: '',
macaroon: 'RAW MACAROON',
};
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account).toBeNull();
});
it('defaults to given bitcoin network', () => {
const raw = {
name: 'NAME',
serverUrl: 'server.url',
lndDir: 'lnd dir',
};
mockedExistsSync.mockReturnValue(true);
mockedReadFileSync.mockImplementation(file => {
if ((file as string).includes('tls.cert')) {
return 'cert';
}
if ((file as string).includes(path.join('regtest', 'admin.macaroon'))) {
return 'macaroon';
}
return 'something else ';
});
const account = getParsedAccount(raw, 0, masterPassword, 'regtest');
expect(account?.macaroon).toContain('macaroon');
});
it('picks up other networks', () => {
const raw = {
name: 'NAME',
serverUrl: 'server.url',
lndDir: 'lnd dir',
network: 'testnet',
} as const;
mockedExistsSync.mockReturnValue(true);
mockedReadFileSync.mockImplementation(file => {
if ((file as string).includes('tls.cert')) {
return 'cert';
}
if ((file as string).includes(path.join('testnet', 'admin.macaroon'))) {
return 'macaroon';
}
return 'something else ';
});
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account?.macaroon).toContain('macaroon');
});
describe('macaroon handling', () => {
it('defaults to raw macaroon', () => {
const raw = {
lndDir: 'LND DIR',
name: 'NAME',
certificate: 'RAW CERT',
serverUrl: 'server.url',
macaroon: 'RAW MACAROON',
macaroonPath: 'MACAROON PATH',
};
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account?.macaroon).toBe('RAW MACAROON');
});
it('falls back to macaroon path after that', () => {
const raw = {
lndDir: 'LND DIR',
name: 'NAME',
certificate: 'RAW CERT',
certificatePath: 'CERT PATH',
serverUrl: 'server.url',
macaroon: '',
macaroonPath: 'MACAROON PATH',
};
mockedExistsSync.mockReturnValueOnce(true);
mockedReadFileSync.mockImplementationOnce(file => {
if ((file as string).includes(raw.macaroonPath)) {
return 'yay';
} else {
return 'nay';
}
});
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account?.macaroon).toBe('yay');
});
it('falls back to lnd dir finally', () => {
const raw = {
lndDir: 'LND DIR',
name: 'NAME',
certificate: 'RAW CERT',
certificatePath: 'CERT PATH',
serverUrl: 'server.url',
macaroon: '',
macaroonPath: '',
};
mockedExistsSync.mockReturnValueOnce(true);
mockedReadFileSync.mockImplementationOnce(file => {
if ((file as string).includes(raw.lndDir)) {
return 'yay';
} else {
return 'nay';
}
});
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account?.macaroon).toBe('yay');
});
});
describe('certificate handling', () => {
it('defaults to raw certificate', () => {
const raw = {
lndDir: 'LND DIR',
name: 'NAME',
certificate: 'RAW CERT',
certificatePath: 'CERT PATH',
serverUrl: 'server.url',
macaroon: 'RAW MACAROON',
};
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account?.cert).toBe('RAW CERT');
});
it('falls back to certificate path after that', () => {
const raw = {
lndDir: 'LND DIR',
name: 'NAME',
certificatePath: 'CERT PATH',
serverUrl: 'server.url',
macaroon: 'RAW MACAROON',
};
mockedExistsSync.mockReturnValueOnce(true);
mockedReadFileSync.mockImplementationOnce(file => {
if ((file as string).includes(raw.certificatePath)) {
return 'yay';
} else {
return 'nay';
}
});
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account?.cert).toBe('yay');
});
it('falls back to lnd dir finally', () => {
const raw = {
lndDir: 'LND DIR',
name: 'NAME',
serverUrl: 'server.url',
macaroon: 'RAW MACAROON',
};
mockedExistsSync.mockReturnValueOnce(true);
mockedReadFileSync.mockImplementationOnce(file => {
if ((file as string).includes(raw.lndDir)) {
return 'yay';
} else {
return 'nay';
}
});
const account = getParsedAccount(raw, 0, masterPassword, 'mainnet');
expect(account?.cert).toBe('yay');
});
});
});
describe('encrypted accounts', () => {
it('returns correct props when encrypted', () => {
const raw: AccountType = {
name: 'NAME',
certificate: 'RAW CERT',
serverUrl: 'server.url',
macaroon: 'RAW MACAROON',
encrypted: true,
};
const account = getParsedAccount(raw, 0, 'master password', 'mainnet');
expect(account?.encrypted).toBeTruthy();
expect(account?.macaroon).toBe(raw.macaroon);
expect((account as any)?.encryptedMacaroon).toBe(raw.macaroon);
});
it('returns correct props when not encrypted', () => {
const raw = {
lndDir: 'LND DIR',
name: 'NAME',
certificate: 'RAW CERT',
serverUrl: 'server.url',
macaroon: 'RAW MACAROON',
};
const account = getParsedAccount(raw, 0, 'master password', 'mainnet');
expect(account?.encrypted).toBeFalsy();
expect(account).not.toHaveProperty('encryptedMacaroon');
});
});
describe('hashPasswords', () => {
it('does not throw on missing master password', () => {
const config = {
hashed: false,
masterPassword: null,
defaultNetwork: null,
accounts: [],
};
expect(hashPasswords(false, config, 'file-path')).toEqual({
accounts: [],
defaultNetwork: null,
hashed: false,
masterPassword: null,
});
});
});
describe('getAccountsFromYaml', () => {
it('needs either account password or master password', () => {
const conf = {
hashed: false,
masterPassword: 'masterPassword',
defaultNetwork: null,
accounts: [
{
name: 'first account',
serverUrl: 'server.url',
password: 'accountPassword',
certificate: 'cert',
macaroon: 'macaroon',
},
],
};
// no password and no account password
expect(
getAccountsFromYaml(
{
...conf,
masterPassword: null,
accounts: [
{
...conf.accounts[0],
password: null,
},
],
},
'file-path'
)
).toHaveLength(0);
// just master password
expect(
getAccountsFromYaml(
{
...conf,
accounts: [
{
...conf.accounts[0],
password: null,
},
],
},
'file-path'
)
).toHaveLength(1);
// just account password
expect(
getAccountsFromYaml(
{
...conf,
masterPassword: null,
accounts: [
{
...conf.accounts[0],
password: 'accountPassword',
},
],
},
'file-path'
)
).toHaveLength(1);
});
});

View File

@ -1,76 +0,0 @@
import { authenticatedLndGrpc } from 'ln-service';
import { SSOType } from 'server/types/apiTypes';
import { LndObject } from 'server/types/ln-service.types';
import { v5 as uuidv5 } from 'uuid';
import { getSHA256Hash } from './crypto';
import { ParsedAccount } from './fileHelpers';
import { logger } from './logger';
import { SavedLnd } from './savedLnd';
type LndAuthType = {
cert: string | null;
macaroon: string;
socket: string;
};
const THUNDERHUB_NAMESPACE = '00000000-0000-0000-0000-000000000000';
export const saved = new SavedLnd();
export const getUUID = (text: string): string =>
uuidv5(text, THUNDERHUB_NAMESPACE);
export const getAuthLnd = (
id: string,
sso: SSOType | null,
accounts: ParsedAccount[]
): LndObject | null => {
const hash = getSHA256Hash(JSON.stringify({ id, sso, accounts }));
if (saved.isSame(hash)) {
logger.silly('Using recycled LND Object');
return saved.lnd;
}
if (!id) {
logger.silly('Account not authenticated');
return null;
}
let authDetails: LndAuthType | null = null;
if (id === 'test') {
authDetails = {
socket: process.env.TEST_HOST || '',
macaroon: process.env.TEST_MACAROON || '',
cert: process.env.TEST_CERT || '',
};
}
if (id === 'sso' && !sso) {
logger.debug('SSO Account is not verified');
throw new Error('AccountNotAuthenticated');
}
if (id === 'sso' && sso) {
authDetails = sso;
}
if (!authDetails) {
const verifiedAccount = accounts.find(a => a.id === id) || null;
if (!verifiedAccount) {
logger.debug('Account not found in config file');
throw new Error('AccountNotAuthenticated');
}
authDetails = verifiedAccount;
}
logger.debug('Creating a new LND object');
const { lnd } = authenticatedLndGrpc(authDetails);
saved.save(hash, lnd);
return lnd;
};

View File

@ -1,455 +0,0 @@
import fs from 'fs';
import crypto from 'crypto';
import path from 'path';
import os from 'os';
import { logger } from 'server/helpers/logger';
import yaml from 'js-yaml';
import { getUUID } from './auth';
import { hashPassword } from './crypto';
import { resolveEnvVarsInAccount } from './env';
type EncodingType = 'hex' | 'utf-8';
type BitcoinNetwork = 'mainnet' | 'regtest' | 'testnet';
export type AccountType = {
name?: string;
serverUrl?: string;
lndDir?: string;
network?: BitcoinNetwork;
macaroonPath?: string;
certificatePath?: string;
password?: string | null;
macaroon?: string;
certificate?: string;
encrypted?: boolean;
};
export type UnresolvedAccountType = {
name?: string;
serverUrl?: string;
lndDir?: string;
network?: BitcoinNetwork;
macaroonPath?: string;
certificatePath?: string;
password?: string | null;
macaroon?: string;
certificate?: string;
encrypted?: boolean | string;
};
export type ParsedAccount = {
name: string;
id: string;
socket: string;
macaroon: string;
cert: string;
password: string;
} & EncryptedAccount;
type EncryptedAccount =
| {
encrypted: true;
encryptedMacaroon: string;
}
| {
encrypted: false;
};
type AccountConfigType = {
hashed: boolean | null;
masterPassword: string | null;
defaultNetwork: string | null;
accounts: AccountType[];
};
const isValidNetwork = (network: string | null): network is BitcoinNetwork =>
network === 'mainnet' || network === 'regtest' || network === 'testnet';
export const PRE_PASS_STRING = 'thunderhub-';
export const readFile = (
filePath: string,
encoding: EncodingType = 'hex'
): string | null => {
if (filePath === '') {
return null;
}
const fileExists = fs.existsSync(filePath);
if (!fileExists) {
logger.error(`No file found at path: ${filePath}`);
return null;
} else {
try {
const file = fs.readFileSync(filePath, encoding);
return file;
} catch (error: any) {
logger.error('Something went wrong while reading the file: \n' + error);
return null;
}
}
};
export const parseYaml = (filePath: string): AccountConfigType | null => {
if (filePath === '') {
return null;
}
const yamlConfig = readFile(filePath, 'utf-8');
if (!yamlConfig) {
return null;
}
try {
const yamlObject = yaml.load(yamlConfig);
// TODO: validate this, before returning?
return yamlObject as AccountConfigType;
} catch (error: any) {
logger.error(
'Something went wrong while parsing the YAML config file: \n' + error
);
return null;
}
};
const saveHashedYaml = (config: AccountConfigType, filePath: string): void => {
if (filePath === '' || !config) return;
logger.info('Saving new yaml file with hashed passwords');
try {
const yamlString = yaml.dump(config);
fs.writeFileSync(filePath, yamlString);
logger.info('Succesfully saved');
} catch (error: any) {
logger.error(
'Error saving yaml file with hashed passwords. Passwords are still in cleartext on your server.'
);
}
};
export const hashPasswords = (
isHashed: boolean,
config: AccountConfigType,
filePath: string
): AccountConfigType => {
// Return early when passwords are already hashed
if (isHashed) return config;
let hasChanged = false;
const cloned = { ...config };
let hashedMasterPassword = config?.masterPassword;
if (
hashedMasterPassword &&
hashedMasterPassword.indexOf(PRE_PASS_STRING) < 0
) {
hasChanged = true;
hashedMasterPassword = hashPassword(hashedMasterPassword);
}
cloned.masterPassword = hashedMasterPassword;
const hashedAccounts: AccountType[] = [];
for (let i = 0; i < config.accounts.length; i++) {
const account: AccountType = config.accounts[i];
if (account.password) {
let hashedPassword = account.password;
if (hashedPassword.indexOf(PRE_PASS_STRING) < 0) {
hasChanged = true;
hashedPassword = hashPassword(account.password);
}
hashedAccounts.push({ ...account, password: hashedPassword });
} else {
hashedAccounts.push(account);
}
}
cloned.accounts = hashedAccounts;
hasChanged && saveHashedYaml(cloned, filePath);
return cloned;
};
const getCertificate = ({
certificate,
certificatePath,
lndDir,
}: AccountType): string | null => {
if (certificate) {
return certificate;
}
if (certificatePath) {
return readFile(certificatePath);
}
if (lndDir) {
return readFile(path.join(lndDir, 'tls.cert'));
}
return null;
};
const getMacaroon = (
{ macaroon, macaroonPath, network, lndDir, encrypted }: AccountType,
defaultNetwork: BitcoinNetwork
): string | null => {
if (macaroon) {
return macaroon;
}
if (macaroonPath) {
return readFile(macaroonPath, encrypted ? 'utf-8' : 'hex');
}
if (!lndDir) {
return null;
}
return readFile(
path.join(
lndDir,
'data',
'chain',
'bitcoin',
network || defaultNetwork,
'admin.macaroon'
)
);
};
export const getAccounts = (filePath: string): ParsedAccount[] => {
if (filePath === '') {
logger.verbose('No account config file path provided');
return [];
}
const accountConfig = parseYaml(filePath);
if (!accountConfig) {
logger.info(`No account config file found at path ${filePath}`);
return [];
}
return getAccountsFromYaml(accountConfig, filePath);
};
export const getParsedAccount = (
account: UnresolvedAccountType,
index: number,
masterPassword: string | null,
defaultNetwork: BitcoinNetwork
): ParsedAccount | null => {
const resolvedAccount = resolveEnvVarsInAccount(account);
const {
name,
serverUrl,
network,
lndDir,
macaroonPath,
macaroon: macaroonValue,
password,
encrypted,
} = resolvedAccount;
const missingFields: string[] = [];
if (!name) missingFields.push('name');
if (!serverUrl) missingFields.push('server url');
if (!lndDir && !macaroonPath && !macaroonValue) {
missingFields.push('macaroon');
}
if (missingFields.length > 0) {
const text = missingFields.join(', ');
logger.error(`Account in index ${index} is missing the fields ${text}`);
return null;
}
if (network && !isValidNetwork(network)) {
logger.error(`Account ${name} has invalid network: ${network}`);
return null;
}
if (!password && !masterPassword) {
logger.error(
`You must set a password for account ${name} or set a master password`
);
return null;
}
const cert = getCertificate(resolvedAccount);
if (!cert) {
logger.warn(
`No certificate for account ${name}. Make sure you don't need it to connect.`
);
}
const macaroon = getMacaroon(resolvedAccount, defaultNetwork);
if (!macaroon) {
logger.error(
`Account ${name} has neither lnd directory, macaroon nor macaroon path specified.`
);
return null;
}
const id = getUUID(`${name}${serverUrl}${macaroon}${cert}`);
let encryptedProps: EncryptedAccount = {
encrypted: false,
};
if (encrypted) {
encryptedProps = {
encrypted: true,
encryptedMacaroon: macaroon,
};
}
return {
name: name || '',
id,
socket: serverUrl || '',
macaroon,
cert: cert || '',
password: password || masterPassword || '',
...encryptedProps,
};
};
export const getAccountsFromYaml = (
config: AccountConfigType,
filePath: string
): ParsedAccount[] => {
const { hashed, accounts: preAccounts } = config;
if (!preAccounts || preAccounts.length <= 0) {
logger.warn(`Account config found at path ${filePath} but had no accounts`);
return [];
}
const { defaultNetwork, masterPassword, accounts } = hashPasswords(
hashed || false,
config,
filePath
);
const network: BitcoinNetwork = isValidNetwork(defaultNetwork)
? defaultNetwork
: 'mainnet';
const parsedAccounts = accounts
.map((account, index) =>
getParsedAccount(account, index, masterPassword, network)
)
.filter(Boolean);
logger.info(
`Server accounts that will be available: ${parsedAccounts
.map(account => account?.name)
.join(', ')}`
);
return parsedAccounts as ParsedAccount[];
};
export const readMacaroons = (macaroonPath: string): string | null => {
if (macaroonPath === '') {
logger.verbose('No macaroon path provided');
return null;
}
const adminExists = fs.existsSync(`${macaroonPath}/admin.macaroon`);
if (!adminExists) {
logger.error(
`No admin.macaroon file found at path: ${macaroonPath}/admin.macaroon`
);
return null;
} else {
try {
const ssoAdmin = fs.readFileSync(`${macaroonPath}/admin.macaroon`, 'hex');
return ssoAdmin;
} catch (error: any) {
logger.error(
'Something went wrong while reading the admin.macaroon: \n' + error
);
return null;
}
}
};
export const createDirectory = (dirname: string) => {
const initDir = path.isAbsolute(dirname) ? path.sep : '';
dirname.split(path.sep).reduce((parentDir, childDir) => {
const curDir = path.resolve(parentDir, childDir);
try {
if (!fs.existsSync(curDir)) {
fs.mkdirSync(curDir);
}
} catch (error: any) {
if (error.code !== 'EEXIST') {
if (error.code === 'ENOENT') {
throw new Error(
`ENOENT: No such file or directory, mkdir '${dirname}'. Ensure that path separator is '${
os.platform() === 'win32' ? '\\\\' : '/'
}'`
);
} else {
throw error;
}
}
}
return curDir;
}, initDir);
};
export const readCookie = (cookieFile: string): string | null => {
if (cookieFile === '') {
logger.verbose('No cookie path provided');
return null;
}
const exists = fs.existsSync(cookieFile);
if (exists) {
try {
logger.verbose(`Found cookie at path ${cookieFile}`);
const cookie = fs.readFileSync(cookieFile, 'utf-8');
return cookie;
} catch (error: any) {
logger.error('Something went wrong while reading cookie: \n' + error);
throw new Error(error);
}
} else {
try {
const dirname = path.dirname(cookieFile);
createDirectory(dirname);
fs.writeFileSync(cookieFile, crypto.randomBytes(64).toString('hex'));
logger.info(`Cookie created at directory: ${dirname}`);
const cookie = fs.readFileSync(cookieFile, 'utf-8');
return cookie;
} catch (error: any) {
logger.error('Something went wrong while reading the cookie: \n' + error);
throw new Error(error);
}
}
};
export const refreshCookie = (cookieFile: string) => {
try {
logger.verbose('Refreshing cookie for next authentication');
fs.writeFileSync(cookieFile, crypto.randomBytes(64).toString('hex'));
} catch (error: any) {
logger.error('Something went wrong while refreshing cookie: \n' + error);
throw new Error(error);
}
};

View File

@ -1,65 +0,0 @@
import { getNode, getChannel } from 'ln-service';
import { logger } from 'server/helpers/logger';
import { toWithError } from 'server/helpers/async';
import {
LndObject,
GetChannelType,
GetNodeType,
ChannelType,
} from 'server/types/ln-service.types';
const errorNode = {
alias: 'Partner node not found',
color: '#000000',
};
export const getNodeFromChannel = async (
id: string,
publicKey: string,
lnd: LndObject | null,
closedChannel?: ChannelType
) => {
let partnerPublicKey: string;
if (closedChannel?.partner_public_key) {
partnerPublicKey = closedChannel.partner_public_key;
} else {
const [channelInfo, channelError] = await toWithError(
getChannel({
lnd,
id,
})
);
if (channelError || !channelInfo) {
logger.verbose(`Error getting channel with id ${id}: %o`, channelError);
return errorNode;
}
partnerPublicKey =
(channelInfo as GetChannelType).policies[0].public_key !== publicKey
? (channelInfo as GetChannelType).policies[0].public_key
: (channelInfo as GetChannelType).policies[1].public_key;
}
const [nodeInfo, nodeError] = await toWithError(
getNode({
lnd,
is_omitting_channels: true,
public_key: partnerPublicKey,
})
);
if (nodeError || !nodeInfo) {
logger.verbose(
`Error getting node with public key ${partnerPublicKey}: %o`,
nodeError
);
return errorNode;
}
return {
alias: (nodeInfo as GetNodeType).alias,
color: (nodeInfo as GetNodeType).color,
};
};

View File

@ -1,148 +0,0 @@
import { getWalletInfo, diffieHellmanComputeSecret } from 'ln-service';
import {
LndObject,
DiffieHellmanComputeSecretType,
GetWalletInfoType,
} from 'server/types/ln-service.types';
import hmacSHA256 from 'crypto-js/hmac-sha256';
import { enc } from 'crypto-js';
import * as bip39 from 'bip39';
import * as bip32 from 'bip32';
import * as secp256k1 from 'secp256k1';
import { appUrls } from 'server/utils/appUrls';
import { decodeLnUrl } from 'src/utils/url';
import { fetchWithProxy } from 'server/utils/fetch';
import { to } from './async';
import { logger } from './logger';
const fromHexString = (hexString: string) =>
new Uint8Array(
hexString.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
);
const toHexString = (bytes: Uint8Array) =>
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
export const lnAuthUrlGenerator = async (
url: string,
lnd: LndObject
): Promise<string> => {
const domainUrl = new URL(url);
const host = domainUrl.host;
const k1 = domainUrl.searchParams.get('k1');
if (!host || !k1) {
logger.error('Missing host or k1 in url: %o', url);
throw new Error('WrongUrlFormat');
}
const wallet = await to<GetWalletInfoType>(getWalletInfo({ lnd }));
// Generate entropy
const secret = await to<DiffieHellmanComputeSecretType>(
diffieHellmanComputeSecret({
lnd,
key_family: 138,
key_index: 0,
partner_public_key: wallet?.public_key,
})
);
// Generate hash from host and entropy
const hashed = hmacSHA256(host, secret.secret).toString(enc.Hex);
const indexes =
hashed.match(/.{1,4}/g)?.map(index => parseInt(index, 16)) || [];
// Generate private seed from entropy
const secretKey = bip39.entropyToMnemonic(hashed);
const base58 = bip39.mnemonicToSeedSync(secretKey);
// Derive private seed from previous private seed and path
const node: bip32.BIP32Interface = bip32.fromSeed(base58);
const derived = node.derivePath(
`m/138/${indexes[0]}/${indexes[1]}/${indexes[2]}/${indexes[3]}`
);
// Get private and public key from derived private seed
const privateKey = derived.privateKey?.toString('hex');
const linkingKey = derived.publicKey.toString('hex');
if (!privateKey || !linkingKey) {
logger.error('Error deriving private or public key: %o', url);
throw new Error('ErrorDerivingPrivateKey');
}
// Sign k1 with derived private seed
const sigObj = secp256k1.ecdsaSign(
fromHexString(k1),
fromHexString(privateKey)
);
// Get signature
const signature = secp256k1.signatureExport(sigObj.signature);
const encodedSignature = toHexString(signature);
// Build and return final url with signature and public key
return `${url}&sig=${encodedSignature}&key=${linkingKey}`;
};
export const getLnMarketsAuth = async (
lnd: LndObject | null,
cookie?: string | null
): Promise<{
newCookie: boolean;
cookieString?: string;
json?: { status: string; reason: string; token: string };
}> => {
if (cookie) {
return { newCookie: false, cookieString: cookie };
}
if (!lnd) {
logger.error('Error getting authenticated LND instance in lnUrlAuth');
throw new Error('ProblemAuthenticatingWithLnUrlService');
}
let lnUrl = '';
// Get a new lnUrl from LnMarkets
try {
const response = await fetchWithProxy(`${appUrls.lnMarkets}/lnurl/auth`, {
method: 'post',
});
const json = (await response.json()) as any;
logger.debug('Get lnUrl from LnMarkets response: %o', json);
lnUrl = json?.lnurl;
if (!lnUrl) throw new Error();
} catch (error: any) {
logger.error(
`Error getting lnAuth url from ${appUrls.lnMarkets}. Error: %o`,
error
);
throw new Error('ProblemAuthenticatingWithLnMarkets');
}
// Decode the LnUrl and authenticate with it
const decoded = decodeLnUrl(lnUrl);
const finalUrl = await lnAuthUrlGenerator(decoded, lnd);
// Try to authenticate with lnMarkets
try {
const response = await fetchWithProxy(`${finalUrl}&jwt=true&expiry=3600`);
const json = (await response.json()) as any;
logger.debug('LnUrlAuth response: %o', json);
if (!json?.token) {
throw new Error('No token in response');
}
return { newCookie: true, cookieString: json.token, json };
} catch (error: any) {
logger.error('Error authenticating with LnUrl service: %o', error);
throw new Error('ProblemAuthenticatingWithLnUrlService');
}
};

View File

@ -1,25 +0,0 @@
import { createLogger, format, transports } from 'winston';
import getConfig from 'next/config';
const { serverRuntimeConfig = {} } = getConfig() || {};
const { logLevel } = serverRuntimeConfig;
const combinedFormat = format.combine(
format.label({ label: 'THUB' }),
format.splat(),
format.colorize(),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf((info: any) => {
if (typeof info.message === 'object') {
info.message = JSON.stringify(info.message, null, 4);
}
return `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`;
})
);
export const logger = createLogger({
level: logLevel,
format: combinedFormat,
transports: [new transports.Console()],
silent: process.env.NODE_ENV === 'test' ? true : false,
});

View File

@ -1,40 +0,0 @@
import { getGraphQLRateLimiter } from 'graphql-rate-limit';
import { logger } from './logger';
interface RateConfigProps {
[key: string]: {
max: number;
window: string;
};
}
export const RateConfig: RateConfigProps = {
getMessages: { max: 10, window: '5s' },
nodeInfo: { max: 10, window: '5s' },
chainBalance: { max: 10, window: '5s' },
pendingChainBalance: { max: 10, window: '5s' },
channelBalance: { max: 10, window: '5s' },
getChannel: { max: 1000, window: '5s' },
};
const rateLimiter = getGraphQLRateLimiter({
identifyContext: (ctx: string) => ctx,
formatError: () => 'Rate Limit Reached',
});
export const requestLimiter = async (rate: string, field: string) => {
const { max, window } = RateConfig[field] || { max: 5, window: '5s' };
const errorMessage = await rateLimiter(
{
parent: rate,
args: {},
context: rate,
info: { fieldName: field } as any,
},
{ max, window }
);
if (errorMessage) {
logger.warn(`Rate limit reached for '${field}' from ip ${rate}`);
throw new Error(errorMessage);
}
};

View File

@ -1,28 +0,0 @@
import { LndObject } from 'server/types/ln-service.types';
export class SavedLnd {
hash: string | null;
lnd: LndObject | null;
constructor() {
this.hash = null;
this.lnd = null;
}
save(hash: string, lnd: LndObject) {
this.hash = hash;
this.lnd = lnd;
}
reset() {
this.hash = null;
this.lnd = null;
}
isSame(hash: string): boolean {
if (hash === this.hash && this.lnd) {
return true;
}
return false;
}
}

View File

@ -1,67 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Account Resolvers getServerAccounts full accounts 1`] = `
Object {
"data": Object {
"getServerAccounts": Array [
Object {
"id": "accountID",
"loggedIn": false,
"name": "account",
"type": "server",
},
],
},
"errors": undefined,
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;
exports[`Account Resolvers getServerAccounts without SSO 1`] = `
Object {
"data": Object {
"getServerAccounts": Array [
Object {
"id": "accountID",
"loggedIn": false,
"name": "account",
"type": "server",
},
],
},
"errors": undefined,
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;
exports[`Account Resolvers getServerAccounts without accounts 1`] = `
Object {
"data": Object {
"getServerAccounts": Array [
Object {
"id": "accountID",
"loggedIn": false,
"name": "account",
"type": "server",
},
],
},
"errors": undefined,
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;

View File

@ -1,40 +0,0 @@
import testServer from 'server/tests/testServer';
import { GET_SERVER_ACCOUNTS } from 'src/graphql/queries/getServerAccounts';
import { ContextMockNoSSO } from 'server/tests/testMocks';
jest.mock('ln-service');
describe('Account Resolvers', () => {
describe('getServerAccounts', () => {
test('full accounts', async () => {
const { query } = testServer();
const res = await query({
query: GET_SERVER_ACCOUNTS,
});
expect(res.errors).toBe(undefined);
expect(res).toMatchSnapshot();
});
test('without SSO', async () => {
const { query } = testServer(ContextMockNoSSO);
const res = await query({
query: GET_SERVER_ACCOUNTS,
});
expect(res.errors).toBe(undefined);
expect(res).toMatchSnapshot();
});
test('without accounts', async () => {
const { query } = testServer(ContextMockNoSSO);
const res = await query({
query: GET_SERVER_ACCOUNTS,
});
expect(res.errors).toBe(undefined);
expect(res).toMatchSnapshot();
});
});
});

View File

@ -1,71 +0,0 @@
import { ContextType } from 'server/types/apiTypes';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { saved } from 'server/helpers/auth';
export const accountResolvers = {
Query: {
getAccount: async (_: undefined, __: undefined, context: ContextType) => {
const { ip, accounts, id } = context;
await requestLimiter(ip, 'getAccount');
if (!id) {
logger.error(`Not authenticated`);
throw new Error('NotAuthenticated');
}
if (id === 'sso') {
return {
name: 'SSO Account',
id: 'sso',
loggedIn: true,
type: 'sso',
};
}
const currentAccount = accounts.find(a => a.id === id);
if (!currentAccount) {
logger.error(`No account found for id ${id}`);
throw new Error('NoAccountFound');
}
return { ...currentAccount, type: 'server', loggedIn: true };
},
getServerAccounts: async (
_: undefined,
__: undefined,
context: ContextType
) => {
const { ip, accounts, id, sso } = context;
await requestLimiter(ip, 'getServerAccounts');
saved.reset();
let ssoAccount = null;
if (id === 'sso' && sso) {
const { cert, socket } = sso;
logger.debug(
`Macaroon${
cert ? ', certificate' : ''
} and host (${socket}) found for SSO.`
);
ssoAccount = {
name: 'SSO Account',
id: 'sso',
loggedIn: true,
type: 'sso',
};
}
const withStatus =
accounts?.map(a => ({
...a,
loggedIn: a.id === id,
type: 'server',
})) || [];
return ssoAccount ? [ssoAccount, ...withStatus] : withStatus;
},
},
};

View File

@ -1,10 +0,0 @@
import { gql } from 'apollo-server-micro';
export const accountTypes = gql`
type serverAccountType {
name: String!
id: String!
type: String!
loggedIn: Boolean!
}
`;

View File

@ -1,382 +0,0 @@
import { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes';
import { appUrls } from 'server/utils/appUrls';
import { graphqlFetchWithProxy } from 'server/utils/fetch';
import { signMessage } from 'ln-service';
import { toWithError } from 'server/helpers/async';
import cookieLib from 'cookie';
import { appConstants } from 'server/utils/appConstants';
import { logger } from 'server/helpers/logger';
import { gql } from 'graphql-tag';
import { print } from 'graphql';
const ONE_MONTH_SECONDS = 60 * 60 * 24 * 30;
const getUserQuery = gql`
query GetUser {
getUser {
subscription {
end_date
subscribed
upgradable
}
}
}
`;
const getLoginTokenQuery = gql`
query GetLoginToken($seconds: Float) {
getLoginToken(seconds: $seconds)
}
`;
const getSignInfoQuery = gql`
query GetSignInfo {
getSignInfo {
expiry
identifier
message
}
}
`;
const loginMutation = gql`
mutation Login(
$identifier: String!
$signature: String!
$seconds: Float
$details: String
$token: Boolean
) {
login(
identifier: $identifier
signature: $signature
seconds: $seconds
details: $details
token: $token
)
}
`;
const getNodeBosHistoryQuery = gql`
query GetNodeBosHistory($pubkey: String!) {
getNodeBosHistory(pubkey: $pubkey) {
info {
count
first {
position
score
updated
}
last {
position
score
updated
}
}
scores {
position
score
updated
}
}
}
`;
const getLastNodeScoreQuery = gql`
query GetNodeBosHistory($pubkey: String!) {
getNodeBosHistory(pubkey: $pubkey) {
info {
last {
alias
public_key
position
score
updated
}
}
}
}
`;
const getBosScoresQuery = gql`
query GetBosScores {
getBosScores {
position
score
updated
alias
public_key
}
}
`;
const getLightningAddresses = gql`
query GetLightningAddresses {
getLightningAddresses {
pubkey
lightning_address
}
}
`;
const getNodeSocialInfo = gql`
query GetNodeSocialInfo($pubkey: String!) {
getNode(pubkey: $pubkey) {
socials {
info {
private
telegram
twitter
twitter_verified
website
email
}
}
}
}
`;
export const ambossResolvers = {
Query: {
getAmbossUser: async (
_: undefined,
__: undefined,
{ ip, ambossAuth }: ContextType
) => {
await requestLimiter(ip, 'getAmbossUser');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
print(getUserQuery),
undefined,
{
authorization: ambossAuth ? `Bearer ${ambossAuth}` : '',
}
);
if (!data?.getUser || error) {
return null;
}
return data.getUser;
},
getAmbossLoginToken: async (
_: undefined,
__: undefined,
{ ip, ambossAuth }: ContextType
) => {
await requestLimiter(ip, 'getAmbossLoginToken');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
print(getLoginTokenQuery),
{ seconds: ONE_MONTH_SECONDS },
{
authorization: ambossAuth ? `Bearer ${ambossAuth}` : '',
}
);
if (!data?.getLoginToken || error) {
throw new Error('Error getting login token from Amboss');
}
return data.getLoginToken;
},
getNodeBosHistory: async (
_: undefined,
{ pubkey }: { pubkey: string },
{ ip, ambossAuth }: ContextType
) => {
await requestLimiter(ip, 'getNodeBosHistory');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
print(getNodeBosHistoryQuery),
{ pubkey },
{
authorization: ambossAuth ? `Bearer ${ambossAuth}` : '',
}
);
if (!data?.getNodeBosHistory || error) {
if (error) {
logger.error(error);
}
throw new Error('Error getting BOS scores for this node');
}
return data.getNodeBosHistory;
},
getBosScores: async (_: undefined, __: undefined, { ip }: ContextType) => {
await requestLimiter(ip, 'getBosScores');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
print(getBosScoresQuery)
);
if (!data?.getBosScores || error) {
if (error) {
logger.error(error);
}
throw new Error('Error getting BOS scores');
}
return data.getBosScores;
},
getLightningAddresses: async (
_: undefined,
__: undefined,
{ ip }: ContextType
) => {
await requestLimiter(ip, 'getLightningAddresses');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
print(getLightningAddresses)
);
if (!data?.getLightningAddresses || error) {
if (error) {
logger.error(error);
}
throw new Error('Error getting Lightning Addresses from Amboss');
}
return data.getLightningAddresses;
},
getNodeSocialInfo: async (
_: undefined,
{ pubkey }: { pubkey: string },
{ ip, ambossAuth }: ContextType
) => {
await requestLimiter(ip, 'getNodeSocialInfo');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
print(getNodeSocialInfo),
{ pubkey },
{
authorization: ambossAuth ? `Bearer ${ambossAuth}` : '',
}
);
if (!data?.getNode || error) {
if (error) {
logger.error(error);
}
throw new Error('Error getting node information from Amboss');
}
return data.getNode;
},
},
Mutation: {
loginAmboss: async (
_: undefined,
__: undefined,
{ ip, lnd, res }: ContextType
) => {
await requestLimiter(ip, 'loginAmboss');
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
print(getSignInfoQuery)
);
if (!data?.getSignInfo || error) {
if (error) {
logger.error(error);
}
throw new Error('Error getting login information from Amboss');
}
const [message, signError] = await toWithError<{ signature: string }>(
signMessage({
lnd,
message: data.getSignInfo.message,
})
);
if (!message?.signature || signError) {
if (signError) {
logger.error(signError);
}
throw new Error('Error signing message to login');
}
logger.debug('Signed Amboss login message');
const { identifier } = data.getSignInfo;
const params = {
details: 'ThunderHub',
identifier,
signature: message.signature,
token: true,
seconds: ONE_MONTH_SECONDS,
};
const { data: loginData, error: loginError } =
await graphqlFetchWithProxy(
appUrls.amboss,
print(loginMutation),
params
);
if (!loginData.login || loginError) {
if (loginError) {
logger.silly(`Error logging into Amboss: ${loginError}`);
}
throw new Error('Error logging into Amboss');
}
logger.debug('Got Amboss login token');
res.setHeader(
'Set-Cookie',
cookieLib.serialize(appConstants.ambossCookieName, loginData.login, {
maxAge: ONE_MONTH_SECONDS,
httpOnly: true,
sameSite: true,
path: '/',
})
);
return true;
},
},
channelType: {
bosScore: async (
{
partner_public_key,
}: {
partner_public_key: string;
},
_: undefined,
{ ambossAuth }: ContextType
) => {
if (!ambossAuth) {
return null;
}
const { data, error } = await graphqlFetchWithProxy(
appUrls.amboss,
print(getLastNodeScoreQuery),
{
pubkey: partner_public_key,
},
{
authorization: ambossAuth ? `Bearer ${ambossAuth}` : '',
}
);
if (error) {
logger.silly(`Error getting bos score for node: ${error}`);
return null;
}
return data?.getNodeBosHistory?.info?.last || null;
},
},
};

View File

@ -1,54 +0,0 @@
import { gql } from 'apollo-server-micro';
export const ambossTypes = gql`
type AmbossSubscriptionType {
end_date: String!
subscribed: Boolean!
upgradable: Boolean!
}
type AmbossUserType {
subscription: AmbossSubscriptionType
}
type BosScore {
position: Float!
score: Float!
updated: String!
alias: String!
public_key: String!
}
type BosScoreInfo {
count: Float!
first: BosScore
last: BosScore
}
type NodeBosHistory {
info: BosScoreInfo!
scores: [BosScore!]!
}
type LightningAddress {
pubkey: String!
lightning_address: String!
}
type NodeSocialInfo {
private: Boolean
telegram: String
twitter: String
twitter_verified: Boolean
website: String
email: String
}
type NodeSocial {
info: NodeSocialInfo
}
type LightningNodeSocialInfo {
socials: NodeSocial
}
`;

View File

@ -1,197 +0,0 @@
import getConfig from 'next/config';
import jwt from 'jsonwebtoken';
import { readCookie, refreshCookie } from 'server/helpers/fileHelpers';
import { ContextType } from 'server/types/apiTypes';
import { logger } from 'server/helpers/logger';
import cookieLib from 'cookie';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { appConstants } from 'server/utils/appConstants';
import { GetWalletInfoType } from 'server/types/ln-service.types';
import { authenticatedLndGrpc, getWalletInfo } from 'ln-service';
import { toWithError } from 'server/helpers/async';
import { decodeMacaroon, isCorrectPassword } from 'server/helpers/crypto';
const { serverRuntimeConfig } = getConfig() || {};
const { cookiePath, nodeEnv, dangerousNoSSOAuth } = serverRuntimeConfig || {};
export const authResolvers = {
Mutation: {
getAuthToken: async (
_: undefined,
{ cookie }: { cookie: string },
{ ip, secret, sso, res }: ContextType
): Promise<boolean> => {
await requestLimiter(ip, 'getAuthToken');
if (!sso) {
logger.warn('No SSO account available');
return false;
}
if (!sso.socket || !sso.macaroon) {
logger.warn('Host and macaroon are required for SSO');
return false;
}
if (dangerousNoSSOAuth) {
logger.warn(
'SSO authentication is disabled. Make sure this is what you want.'
);
} else {
// No cookie or cookiePath needed when SSO authentication is turned off
if (!cookie) {
return false;
}
if (cookiePath === '') {
logger.warn(
'SSO auth not available since no cookie path was provided'
);
return false;
}
}
if (nodeEnv === 'development') {
logger.warn('SSO authentication is disabled in development.');
}
const cookieFile = readCookie(cookiePath);
if (
(cookieFile && cookieFile.trim() === cookie.trim()) ||
nodeEnv === 'development' ||
dangerousNoSSOAuth
) {
cookiePath && refreshCookie(cookiePath);
const { lnd } = authenticatedLndGrpc(sso);
const [, error] = await toWithError<GetWalletInfoType>(
getWalletInfo({
lnd,
})
);
if (error) {
logger.error('Unable to connect to this node: %o', error);
throw new Error('UnableToConnectToThisNode');
}
const token = jwt.sign({ id: 'sso' }, secret);
res.setHeader(
'Set-Cookie',
cookieLib.serialize(appConstants.cookieName, token, {
httpOnly: true,
sameSite: true,
path: '/',
})
);
return true;
}
logger.debug(`Cookie ${cookie} different to file ${cookieFile}`);
return false;
},
getSessionToken: async (
_: undefined,
{ id, password }: { id: string; password: string },
{ ip, secret, res, accounts }: ContextType
): Promise<string> => {
await requestLimiter(ip, 'getSessionToken');
const account = accounts.find(a => a.id === id) || null;
if (!account) {
logger.debug(`Account ${id} not found`);
return '';
}
if (account.encrypted) {
if (nodeEnv === 'development') {
logger.error(
'Encrypted accounts only work in a production environment'
);
throw new Error('UnableToLogin');
}
const macaroon = decodeMacaroon(account.encryptedMacaroon, password);
// Store decrypted macaroon in memory.
// In development NextJS rebuilds the files so this only works in production env.
account.macaroon = macaroon;
logger.debug(`Decrypted the macaroon for account ${id}`);
} else {
if (!isCorrectPassword(password, account.password)) {
logger.error(
`Authentication failed from ip: ${ip} - Invalid Password!`
);
throw new Error('WrongPasswordForLogin');
}
logger.debug(`Correct password for account ${id}`);
}
// Try to connect to node. The authenticatedLndGrpc method will also check if the macaroon is base64 or hex.
const { lnd } = authenticatedLndGrpc(account);
const [info, error] = await toWithError<GetWalletInfoType>(
getWalletInfo({
lnd,
})
);
if (error) {
logger.error('Unable to connect to this node: %o', error);
throw new Error('UnableToConnectToThisNode');
}
const token = jwt.sign({ id }, secret);
res.setHeader(
'Set-Cookie',
cookieLib.serialize(appConstants.cookieName, token, {
httpOnly: true,
sameSite: true,
path: '/',
})
);
return info?.version || '';
},
logout: async (
_: undefined,
__: any,
context: ContextType
): Promise<boolean> => {
const { ip, res } = context;
await requestLimiter(ip, 'logout');
res.setHeader('Set-Cookie', [
cookieLib.serialize(appConstants.cookieName, '', {
maxAge: -1,
httpOnly: true,
sameSite: true,
path: '/',
}),
cookieLib.serialize(appConstants.ambossCookieName, '', {
maxAge: -1,
httpOnly: true,
sameSite: true,
path: '/',
}),
cookieLib.serialize(appConstants.lnMarketsAuth, '', {
maxAge: -1,
httpOnly: true,
sameSite: true,
path: '/',
}),
cookieLib.serialize(appConstants.tokenCookieName, '', {
maxAge: -1,
httpOnly: true,
sameSite: true,
path: '/',
}),
]);
return true;
},
},
};

View File

@ -1,70 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bitcoin Resolvers getBitcoinFees failure 1`] = `
Object {
"data": Object {
"getBitcoinFees": null,
},
"errors": Array [
[GraphQLError: Problem getting Bitcoin fees.],
],
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;
exports[`Bitcoin Resolvers getBitcoinFees success 1`] = `
Object {
"data": Object {
"getBitcoinFees": Object {
"fast": 3,
"halfHour": 2,
"hour": 1,
"minimum": 5,
},
},
"errors": undefined,
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;
exports[`Bitcoin Resolvers getBitcoinPrice failure 1`] = `
Object {
"data": Object {
"getBitcoinPrice": null,
},
"errors": Array [
[GraphQLError: Problem getting Bitcoin price.],
],
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;
exports[`Bitcoin Resolvers getBitcoinPrice success 1`] = `
Object {
"data": Object {
"getBitcoinPrice": "{\\"price\\":\\"price\\"}",
},
"errors": undefined,
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;

View File

@ -1,80 +0,0 @@
import testServer from 'server/tests/testServer';
import fetchMock from 'jest-fetch-mock';
import { GraphQLError } from 'graphql';
import { GET_BITCOIN_PRICE } from 'src/graphql/queries/getBitcoinPrice';
import { GET_BITCOIN_FEES } from 'src/graphql/queries/getBitcoinFees';
describe('Bitcoin Resolvers', () => {
beforeEach(() => {
fetchMock.resetMocks();
});
describe('getBitcoinPrice', () => {
test('success', async () => {
fetchMock.mockResponseOnce(JSON.stringify({ price: 'price' }));
const { query } = testServer();
const res = await query({
query: GET_BITCOIN_PRICE,
});
expect(res.errors).toBe(undefined);
expect(fetchMock).toBeCalledWith(
'https://blockchain.info/ticker',
undefined
);
expect(res).toMatchSnapshot();
});
test('failure', async () => {
fetchMock.mockRejectOnce(new Error('Error'));
const { query } = testServer();
const res = await query({
query: GET_BITCOIN_PRICE,
});
expect(res.errors).toStrictEqual([
new GraphQLError('Problem getting Bitcoin price.'),
]);
expect(res).toMatchSnapshot();
});
});
describe('getBitcoinFees', () => {
test('success', async () => {
fetchMock.mockResponseOnce(
JSON.stringify({
fastestFee: 3,
halfHourFee: 2,
hourFee: 1,
minimumFee: 5,
})
);
const { query } = testServer();
const res = await query({
query: GET_BITCOIN_FEES,
});
expect(res.errors).toBe(undefined);
expect(fetchMock).toBeCalledWith(
'undefined/api/v1/fees/recommended',
undefined
);
expect(res).toMatchSnapshot();
});
test('failure', async () => {
fetchMock.mockRejectOnce(new Error('Error'));
const { query } = testServer();
const res = await query({
query: GET_BITCOIN_FEES,
});
expect(res.errors).toStrictEqual([
new GraphQLError('Problem getting Bitcoin fees.'),
]);
expect(res).toMatchSnapshot();
});
});
});

View File

@ -1,53 +0,0 @@
import { ContextType } from 'server/types/apiTypes';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { appUrls } from 'server/utils/appUrls';
import { fetchWithProxy } from 'server/utils/fetch';
export const bitcoinResolvers = {
Query: {
getBitcoinPrice: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'bitcoinPrice');
try {
const response = await fetchWithProxy(appUrls.ticker);
const json = await response.json();
return JSON.stringify(json);
} catch (error: any) {
logger.error('Error getting bitcoin price: %o', error);
throw new Error('Problem getting Bitcoin price.');
}
},
getBitcoinFees: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'bitcoinFee');
try {
const response = await fetchWithProxy(appUrls.fees);
const json = (await response.json()) as any;
if (json) {
const { fastestFee, halfHourFee, hourFee, minimumFee } = json;
return {
fast: fastestFee,
halfHour: halfHourFee,
hour: hourFee,
minimum: minimumFee,
};
}
throw new Error('Problem getting Bitcoin fees.');
} catch (error: any) {
logger.error('Error getting bitcoin fees: %o', error);
throw new Error('Problem getting Bitcoin fees.');
}
},
},
};

View File

@ -1,10 +0,0 @@
import { gql } from 'apollo-server-micro';
export const bitcoinTypes = gql`
type bitcoinFeeType {
fast: Int
halfHour: Int
hour: Int
minimum: Int
}
`;

View File

@ -1,294 +0,0 @@
import { BoltzApi } from 'server/api/Boltz';
import { to, toWithError } from 'server/helpers/async';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes';
import { createChainAddress, decodePaymentRequest } from 'ln-service';
import { constructClaimTransaction, detectSwap } from 'boltz-core';
import {
CreateChainAddressType,
DecodedType,
} from 'server/types/ln-service.types';
import { getPreimageAndHash } from 'server/helpers/crypto';
import { address, ECPair, Network, networks, Transaction } from 'bitcoinjs-lib';
import { GraphQLError } from 'graphql';
const getHexBuffer = (input: string) => {
return Buffer.from(input, 'hex');
};
const getHexString = (input?: Buffer): string => {
if (!input) return '';
return input.toString('hex');
};
const validateAddress = (
btcAddress: string,
network: Network = networks.bitcoin
): boolean => {
try {
address.toOutputScript(btcAddress, network);
return true;
} catch (e) {
return false;
}
};
export const generateKeys = (network: Network = networks.bitcoin) => {
const keys = ECPair.makeRandom({ network });
return {
publicKey: getHexString(keys.publicKey),
privateKey: getHexString(keys.privateKey),
};
};
type BoltzInfoType = {
max: number;
min: number;
feePercent: number;
};
type BoltzSwapStatusParams = {
ids: string[];
};
type CreateSwapParams = {
amount: number; // Value in satoshis
address?: string;
};
type ClaimTransactionParams = {
redeem: string;
transaction: string;
preimage: string;
privateKey: string;
destination: string;
fee: number;
};
type CreateBoltzReverseSwapType = {
id: string;
invoice: string;
redeemScript: string;
onchainAmount: number;
timeoutBlockHeight: number;
lockupAddress: string;
minerFeeInvoice: string;
};
export const boltzResolvers = {
Query: {
getBoltzInfo: async (
_: undefined,
__: any,
context: ContextType
): Promise<BoltzInfoType> => {
await requestLimiter(context.ip, 'getBoltzInfo');
const info = await BoltzApi.getPairs();
if (info?.error) {
logger.error(
'Error getting swap information from Boltz. Error: %o',
info.error
);
throw new Error(info.error);
}
const btcPair = info?.pairs?.['BTC/BTC'];
if (!btcPair) {
logger.error('No BTC > LN BTC information received from Boltz');
throw new Error('MissingBtcRatesFromBoltz');
}
const max = btcPair.limits?.maximal || 0;
const min = btcPair.limits?.minimal || 0;
const feePercent = btcPair.fees?.percentage || 0;
return { max, min, feePercent };
},
getBoltzSwapStatus: async (_: undefined, { ids }: BoltzSwapStatusParams) =>
ids,
},
Mutation: {
claimBoltzTransaction: async (
_: undefined,
{
redeem,
transaction,
preimage,
privateKey,
destination,
fee,
}: ClaimTransactionParams,
{ ip }: ContextType
) => {
await requestLimiter(ip, 'claimBoltzTransaction');
if (!validateAddress(destination)) {
logger.error(`Invalid bitcoin address: ${destination}`);
throw new GraphQLError('InvalidBitcoinAddress');
}
const redeemScript = getHexBuffer(redeem);
const lockupTransaction = Transaction.fromHex(transaction);
const info = detectSwap(redeemScript, lockupTransaction);
if (info?.vout === undefined || info?.type === undefined) {
logger.error('Cannot get vout or type from Boltz');
logger.debug('Swap info: %o', {
redeemScript,
lockupTransaction,
info,
});
throw new Error('ErrorCreatingClaimTransaction');
}
const utxos = [
{
...info,
redeemScript,
txHash: lockupTransaction.getHash(),
preimage: getHexBuffer(preimage),
keys: ECPair.fromPrivateKey(getHexBuffer(privateKey)),
},
];
const destinationScript = address.toOutputScript(
destination,
networks.bitcoin
);
const finalTransaction = constructClaimTransaction(
utxos,
destinationScript,
fee
);
logger.debug('Final transaction: %o', finalTransaction);
const response = await BoltzApi.broadcastTransaction(
finalTransaction.toHex()
);
logger.debug('Response from Boltz: %o', { response });
if (!response?.transactionId) {
logger.error('Did not receive a transaction id from Boltz');
throw new Error('NoTransactionIdFromBoltz');
}
return response.transactionId;
},
createBoltzReverseSwap: async (
_: undefined,
{ amount, address }: CreateSwapParams,
context: ContextType
) => {
await requestLimiter(context.ip, 'createReverseSwap');
if (address && !validateAddress(address)) {
logger.error(`Invalid bitcoin address: ${address}`);
throw new GraphQLError('InvalidBitcoinAddress');
}
const { lnd } = context;
const { preimage, hash } = getPreimageAndHash();
const { privateKey, publicKey } = generateKeys();
let btcAddress = address;
if (!btcAddress) {
const info = await to<CreateChainAddressType>(
createChainAddress({
lnd,
is_unused: true,
format: 'p2wpkh',
})
);
if (!info?.address) {
logger.error('Error creating onchain address for swap');
throw new Error('ErrorCreatingOnChainAddress');
}
btcAddress = info.address;
}
logger.debug('Creating swap with these params: %o', {
amount,
hash,
publicKey,
});
const info = await BoltzApi.createReverseSwap(amount, hash, publicKey);
if (info?.error) {
logger.error('Error creating reverse swap with Boltz: %o', info.error);
throw new Error(info.error);
}
const finalInfo = {
...info,
receivingAddress: btcAddress,
preimage: preimage.toString('hex'),
preimageHash: hash,
privateKey,
publicKey,
};
logger.debug('Swap info: %o', { finalInfo });
return finalInfo;
},
},
BoltzSwap: {
id: (parent: string) => parent,
boltz: async (parent: string) => {
const [info, error] = await toWithError(BoltzApi.getSwapStatus(parent));
if (error || info?.error) {
logger.error(
`Error getting status for swap with id: ${parent}. Error: %o`,
error || info.error
);
return null;
}
if (!info?.status) {
logger.debug(
`No status in Boltz response for swap with id: ${parent}. Response: %o`,
info
);
return null;
}
return info;
},
},
CreateBoltzReverseSwapType: {
decodedInvoice: async (
parent: CreateBoltzReverseSwapType,
_: undefined,
context: ContextType
) => {
const { lnd } = context;
const decoded = await to<DecodedType>(
decodePaymentRequest({
lnd,
request: parent.invoice,
})
);
return {
...decoded,
destination_node: { lnd, publicKey: decoded.destination },
};
},
},
};

View File

@ -1,41 +0,0 @@
import { gql } from '@apollo/client';
export const boltzTypes = gql`
type BoltzInfoType {
max: Int!
min: Int!
feePercent: Float!
}
type BoltzSwapTransaction {
id: String
hex: String
eta: Int
}
type BoltzSwapStatus {
status: String!
transaction: BoltzSwapTransaction
}
type BoltzSwap {
id: String
boltz: BoltzSwapStatus
}
type CreateBoltzReverseSwapType {
id: String!
invoice: String!
redeemScript: String!
onchainAmount: Int!
timeoutBlockHeight: Int!
lockupAddress: String!
minerFeeInvoice: String
decodedInvoice: decodeType
receivingAddress: String!
preimage: String
preimageHash: String
privateKey: String
publicKey: String
}
`;

View File

@ -1,147 +0,0 @@
import fs from 'fs';
import { ContextType } from 'server/types/apiTypes';
import { to, toWithError } from 'server/helpers/async';
import { logger } from 'server/helpers/logger';
import { rebalance } from 'balanceofsatoshis/swaps';
import { pay } from 'balanceofsatoshis/network';
import { getAccountingReport } from 'balanceofsatoshis/balances';
import { fetchRequest } from 'balanceofsatoshis/commands';
import { RebalanceResponseType } from 'server/types/balanceofsatoshis.types';
import { getErrorMsg } from 'server/helpers/helpers';
type PayType = {
max_fee: Number;
max_paths: Number;
request: String;
message?: String;
out?: String[];
};
type RebalanceType = {
avoid?: String[];
in_through?: String;
max_fee?: Number;
max_fee_rate?: Number;
max_rebalance?: Number;
timeout_minutes?: Number;
node?: String;
out_channels?: String[];
out_through?: String;
out_inbound?: Number;
};
type AccountingType = {
category?: String;
currency?: String;
fiat?: String;
month?: String;
year?: String;
};
export const bosResolvers = {
Query: {
getAccountingReport: async (
_: undefined,
params: AccountingType,
context: ContextType
) => {
const { lnd } = context;
const response = await to(
getAccountingReport({
lnd,
logger,
request: fetchRequest,
is_csv: true,
...params,
})
);
return response;
},
},
Mutation: {
bosPay: async (_: undefined, params: PayType, context: ContextType) => {
const { lnd } = context;
const { max_fee, max_paths, message, out, request } = params;
const props = {
max_fee,
max_paths,
...(message && { message }),
out: out || [],
avoid: [],
};
logger.debug('Paying invoice with params: %o', props);
const [response, error] = await toWithError(
pay({
lnd,
logger,
...props,
request,
})
);
if (error) {
logger.error('Error paying invoice: %o', error);
throw new Error(getErrorMsg(error));
}
logger.debug('Paid invoice: %o', response);
return true;
},
bosRebalance: async (
_: undefined,
{
avoid,
in_through,
max_fee,
max_fee_rate,
max_rebalance,
timeout_minutes,
node,
out_through,
out_inbound,
}: RebalanceType,
{ lnd }: ContextType
) => {
const filteredParams = {
out_channels: [],
avoid,
...(in_through && { in_through }),
...(max_fee && max_fee > 0 && { max_fee }),
...(max_fee_rate && max_fee_rate > 0 && { max_fee_rate }),
...(timeout_minutes && timeout_minutes > 0 && { timeout_minutes }),
...(max_rebalance && max_rebalance > 0
? { max_rebalance: `${max_rebalance}` }
: {}),
...(node && { node }),
...(out_through && { out_through }),
...(out_inbound && out_inbound > 0
? { out_inbound: `${out_inbound}` }
: {}),
};
logger.info('Rebalance Params: %o', filteredParams);
const response = await to<RebalanceResponseType>(
rebalance({
lnd,
logger,
fs: { getFile: fs.readFile },
...filteredParams,
})
);
const result = {
increase: response.rebalance[0],
decrease: response.rebalance[1],
result: response.rebalance[2],
};
return result;
},
},
};

View File

@ -1,33 +0,0 @@
import { gql } from 'apollo-server-micro';
export const bosTypes = gql`
type bosIncreaseType {
increased_inbound_on: String
liquidity_inbound: String
liquidity_inbound_opening: String
liquidity_inbound_pending: String
liquidity_outbound: String
liquidity_outbound_opening: String
liquidity_outbound_pending: String
}
type bosDecreaseType {
decreased_inbound_on: String
liquidity_inbound: String
liquidity_inbound_opening: String
liquidity_inbound_pending: String
liquidity_outbound: String
liquidity_outbound_opening: String
liquidity_outbound_pending: String
}
type bosResultType {
rebalanced: String
rebalance_fees_spent: String
}
type bosRebalanceResultType {
increase: bosIncreaseType
decrease: bosDecreaseType
result: bosResultType
}
`;

View File

@ -1,100 +0,0 @@
import {
getChainTransactions,
getUtxos,
sendToChainAddress,
createChainAddress,
} from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { sortBy } from 'lodash';
import { to } from 'server/helpers/async';
import {
GetChainTransactionsType,
GetUtxosType,
SendToChainAddressType,
} from 'server/types/ln-service.types';
export const chainResolvers = {
Query: {
getChainTransactions: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'chainTransactions');
const { lnd } = context;
const transactionList = await to<GetChainTransactionsType>(
getChainTransactions({
lnd,
})
);
const transactions = sortBy(
transactionList.transactions,
'created_at'
).reverse();
return transactions;
},
getUtxos: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getUtxos');
const { lnd } = context;
const info = await to<GetUtxosType>(getUtxos({ lnd }));
return info?.utxos;
},
},
Mutation: {
createAddress: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getAddress');
const { lnd } = context;
const format = params.nested ? 'np2wpkh' : 'p2wpkh';
const address = await to<{ address: string }>(
createChainAddress({
lnd,
is_unused: true,
format,
})
);
return address.address;
},
sendToAddress: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'sendToAddress');
const { lnd } = context;
const props = params.fee
? { fee_tokens_per_vbyte: params.fee }
: params.target
? { target_confirmations: params.target }
: {};
const sendAll = params.sendAll ? { is_send_all: true } : {};
const send = await to<SendToChainAddressType>(
sendToChainAddress({
lnd,
address: params.address,
...(params.tokens && { tokens: params.tokens }),
...props,
...sendAll,
})
);
return {
confirmationCount: send.confirmation_count,
id: send.id,
isConfirmed: send.is_confirmed,
isOutgoing: send.is_outgoing,
...(send.tokens && { tokens: send.tokens }),
};
},
},
};

View File

@ -1,31 +0,0 @@
import { gql } from 'apollo-server-micro';
export const chainTypes = gql`
type getUtxosType {
address: String!
address_format: String!
confirmation_count: Int!
output_script: String!
tokens: Int!
transaction_id: String!
transaction_vout: Int!
}
type sendToType {
confirmationCount: String!
id: String!
isConfirmed: Boolean!
isOutgoing: Boolean!
tokens: Int
}
type getTransactionsType {
block_id: String
confirmation_count: Int
confirmation_height: Int
created_at: String!
fee: Int
id: String!
output_addresses: [String]!
tokens: Int!
}
`;

View File

@ -1,119 +0,0 @@
import { logger } from 'server/helpers/logger';
import { toWithError } from 'server/helpers/async';
import { getChannel as getLnChannel } from 'ln-service';
import { ChannelType, GetChannelType } from 'server/types/ln-service.types';
import { openChannel } from './resolvers/mutation/openChannel';
import { closeChannel } from './resolvers/mutation/closeChannel';
import { updateFees } from './resolvers/mutation/updateFees';
import { updateMultipleFees } from './resolvers/mutation/updateMultipleFees';
import { getChannels } from './resolvers/query/getChannels';
import { getClosedChannels } from './resolvers/query/getClosedChannels';
import { getPendingChannels } from './resolvers/query/getPendingChannels';
import { getChannel } from './resolvers/query/getChannel';
type ParentType = {
id: string;
partner_fee_info: {
lnd: {};
localKey: String;
};
};
export const channelResolvers = {
Query: {
getChannel,
getChannels,
getClosedChannels,
getPendingChannels,
},
Mutation: {
openChannel,
closeChannel,
updateFees,
updateMultipleFees,
},
channelType: {
partner_fee_info: async ({
id,
partner_fee_info: { lnd, localKey },
}: ParentType) => {
if (!lnd) {
logger.debug('ExpectedLNDToGetChannel');
return null;
}
if (!id) {
logger.debug('ExpectedIdToGetChannel');
return null;
}
const [channel, error] = await toWithError<GetChannelType>(
getLnChannel({ lnd, id })
);
if (error) {
logger.debug(`Error getting channel with id ${id}: %o`, error);
return null;
}
let node_policies = null;
let partner_node_policies = null;
if (channel) {
channel.policies.forEach(policy => {
if (localKey && localKey === policy.public_key) {
node_policies = {
...policy,
node: { lnd, publicKey: policy.public_key },
};
} else {
partner_node_policies = {
...policy,
node: { lnd, publicKey: policy.public_key },
};
}
});
}
return {
...(channel as GetChannelType),
node_policies,
partner_node_policies,
};
},
pending_resume({ pending_payments }: ChannelType) {
const total = pending_payments.reduce(
(prev, current) => {
const { is_outgoing, tokens } = current;
return {
incoming_tokens: is_outgoing
? prev.incoming_tokens
: prev.incoming_tokens + tokens,
outgoing_tokens: is_outgoing
? prev.outgoing_tokens + tokens
: prev.outgoing_tokens,
incoming_amount: is_outgoing
? prev.incoming_amount
: prev.incoming_amount + 1,
outgoing_amount: is_outgoing
? prev.incoming_amount + 1
: prev.incoming_amount,
};
},
{
incoming_tokens: 0,
outgoing_tokens: 0,
incoming_amount: 0,
outgoing_amount: 0,
}
);
return {
...total,
total_tokens: total.incoming_tokens + total.outgoing_tokens,
total_amount: total.incoming_amount + total.outgoing_amount,
};
},
},
};

View File

@ -1,39 +0,0 @@
import { closeChannel as lnCloseChannel } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import { CloseChannelType } from 'server/types/ln-service.types';
import { logger } from 'server/helpers/logger';
export const closeChannel = async (
_: undefined,
params: any,
context: ContextType
) => {
await requestLimiter(context.ip, 'closeChannel');
const { lnd } = context;
const closeParams = {
id: params.id,
target_confirmations: params.targetConfirmations,
tokens_per_vbyte: params.tokensPerVByte,
is_force_close: params.forceClose,
};
logger.info('Closing channel with params: %o', closeParams);
const info = await to<CloseChannelType>(
lnCloseChannel({
lnd,
...closeParams,
})
);
logger.info('Channel closed: %o', params.id);
return {
transactionId: info.transaction_id,
transactionOutputIndex: info.transaction_vout,
};
};

View File

@ -1,63 +0,0 @@
import { openChannel as lnOpenChannel, addPeer } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import { OpenChannelType } from 'server/types/ln-service.types';
type OpenChannelParams = {
isPrivate: boolean;
amount: number;
partnerPublicKey: string;
tokensPerVByte: number;
pushTokens: number;
};
export const openChannel = async (
_: undefined,
params: OpenChannelParams,
context: ContextType
) => {
await requestLimiter(context.ip, 'openChannel');
const { lnd } = context;
const {
isPrivate,
amount,
partnerPublicKey,
tokensPerVByte,
pushTokens = 0,
} = params;
let public_key = partnerPublicKey;
if (partnerPublicKey.indexOf('@') >= 0) {
const parts = partnerPublicKey.split('@');
public_key = parts[0];
await to(addPeer({ lnd, socket: parts[1], public_key }));
}
const openParams = {
is_private: isPrivate,
local_tokens: amount,
partner_public_key: public_key,
chain_fee_tokens_per_vbyte: tokensPerVByte,
give_tokens: Math.min(pushTokens, amount),
};
logger.info('Opening channel with params: %o', openParams);
const info = await to<OpenChannelType>(
lnOpenChannel({
lnd,
...openParams,
})
);
logger.info('Channel opened');
return {
transactionId: info.transaction_id,
transactionOutputIndex: info.transaction_vout,
};
};

View File

@ -1,66 +0,0 @@
import { updateRoutingFees } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import { logger } from 'server/helpers/logger';
type ParamsType = {
transaction_id: string;
transaction_vout: number;
base_fee_tokens: number;
fee_rate: number;
cltv_delta: number;
max_htlc_mtokens: string;
min_htlc_mtokens: string;
};
export const updateFees = async (
_: undefined,
{
transaction_id,
transaction_vout,
base_fee_tokens,
fee_rate,
cltv_delta,
max_htlc_mtokens,
min_htlc_mtokens,
}: ParamsType,
context: ContextType
) => {
await requestLimiter(context.ip, 'updateFees');
const { lnd } = context;
const hasBaseFee = base_fee_tokens >= 0;
const hasFee = fee_rate >= 0;
if (
!hasBaseFee &&
!hasFee &&
!cltv_delta &&
!max_htlc_mtokens &&
!min_htlc_mtokens
) {
throw new Error('NoDetailsToUpdateChannel');
}
const baseFee =
base_fee_tokens === 0
? { base_fee_tokens: 0 }
: { base_fee_mtokens: Math.trunc((base_fee_tokens || 0) * 1000) };
const props = {
transaction_id,
transaction_vout,
...(hasBaseFee && baseFee),
...(hasFee && { fee_rate }),
...(cltv_delta && { cltv_delta }),
...(max_htlc_mtokens && { max_htlc_mtokens }),
...(min_htlc_mtokens && { min_htlc_mtokens }),
};
logger.debug('Updating channel details with props: %o', props);
await to(updateRoutingFees({ lnd, ...props }));
return true;
};

View File

@ -1,69 +0,0 @@
import { updateRoutingFees } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { logger } from 'server/helpers/logger';
import { toWithError } from 'server/helpers/async';
export const updateMultipleFees = async (
_: undefined,
params: any,
context: ContextType
) => {
await requestLimiter(context.ip, 'updateMultipleFees');
const { lnd } = context;
let errors = 0;
for (let i = 0; i < params.channels.length; i++) {
const channel = params.channels[i];
const {
transaction_id,
transaction_vout,
base_fee_tokens,
fee_rate,
cltv_delta,
max_htlc_mtokens,
min_htlc_mtokens,
} = channel;
const hasBaseFee = base_fee_tokens >= 0;
const hasFee = fee_rate >= 0;
if (
!hasBaseFee &&
!hasFee &&
!cltv_delta &&
!max_htlc_mtokens &&
!min_htlc_mtokens
) {
throw new Error('NoDetailsToUpdateChannel');
}
const baseFee =
base_fee_tokens === 0
? { base_fee_tokens: 0 }
: { base_fee_mtokens: Math.trunc((base_fee_tokens || 0) * 1000) };
const props = {
lnd,
transaction_id,
transaction_vout,
...(hasBaseFee && baseFee),
...(hasFee && { fee_rate }),
...(cltv_delta && { cltv_delta }),
...(max_htlc_mtokens && { max_htlc_mtokens }),
...(min_htlc_mtokens && { min_htlc_mtokens }),
};
const [, error] = await toWithError(updateRoutingFees(props));
if (error) {
logger.error('Error updating channel: %o', error);
errors = errors + 1;
}
}
return errors ? false : true;
};

View File

@ -1,42 +0,0 @@
import { getChannel as getLnChannel } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { to } from 'server/helpers/async';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { GetChannelType } from 'server/types/ln-service.types';
export const getChannel = async (
_: undefined,
{ id, pubkey }: { id: string; pubkey?: string },
{ ip, lnd }: ContextType
) => {
await requestLimiter(ip, 'getChannel');
const channel = await to<GetChannelType>(getLnChannel({ lnd, id }));
if (!pubkey) {
return channel;
}
let node_policies = null;
let partner_node_policies = null;
channel.policies.forEach(policy => {
if (pubkey && pubkey === policy.public_key) {
node_policies = {
...policy,
node: { lnd, publicKey: policy.public_key },
};
} else {
partner_node_policies = {
...policy,
node: { lnd, publicKey: policy.public_key },
};
}
});
return {
...(channel as GetChannelType),
node_policies,
partner_node_policies,
};
};

View File

@ -1,33 +0,0 @@
import { getChannels as getLnChannels, getWalletInfo } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { to } from 'server/helpers/async';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { getChannelAge } from 'server/schema/health/helpers';
import { GetChannelsType } from 'server/types/ln-service.types';
export const getChannels = async (
_: undefined,
params: any,
{ ip, lnd }: ContextType
) => {
await requestLimiter(ip, 'channels');
const { public_key, current_block_height } = await to(getWalletInfo({ lnd }));
const { channels } = await to<GetChannelsType>(
getLnChannels({
lnd,
is_active: params.active,
})
);
return channels.map(channel => ({
...channel,
time_offline: Math.round((channel.time_offline || 0) / 1000),
time_online: Math.round((channel.time_online || 0) / 1000),
partner_node_info: { lnd, publicKey: channel.partner_public_key },
partner_fee_info: { lnd, localKey: public_key },
channel_age: getChannelAge(channel.id, current_block_height),
}));
};

View File

@ -1,51 +0,0 @@
import {
getClosedChannels as getLnClosedChannels,
getWalletInfo,
} from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { to } from 'server/helpers/async';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { getChannelAge } from 'server/schema/health/helpers';
interface ChannelListProps {
channels: ChannelProps[];
}
interface ChannelProps {
capacity: number;
close_confirm_height: number;
close_transaction_id: string;
final_local_balance: number;
final_time_locked_balance: number;
id: string;
is_breach_close: boolean;
is_cooperative_close: boolean;
is_funding_cancel: boolean;
is_local_force_close: boolean;
is_remote_force_close: boolean;
partner_public_key: string;
transaction_id: string;
transaction_vout: number;
}
export const getClosedChannels = async (
_: undefined,
__: any,
context: ContextType
) => {
await requestLimiter(context.ip, 'closedChannels');
const { lnd } = context;
const { current_block_height } = await to(getWalletInfo({ lnd }));
const { channels }: ChannelListProps = await to(getLnClosedChannels({ lnd }));
return channels.map(channel => ({
...channel,
partner_node_info: {
lnd,
publicKey: channel.partner_public_key,
},
channel_age: getChannelAge(channel.id, current_block_height),
}));
};

View File

@ -1,27 +0,0 @@
import { getPendingChannels as getLnPendingChannels } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { to } from 'server/helpers/async';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { GetPendingChannelsType } from 'server/types/ln-service.types';
export const getPendingChannels = async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'pendingChannels');
const { lnd } = context;
const { pending_channels } = await to<GetPendingChannelsType>(
getLnPendingChannels({ lnd })
);
return pending_channels.map(channel => ({
...channel,
partner_node_info: {
lnd,
publicKey: channel.partner_public_key,
},
}));
};

View File

@ -1,146 +0,0 @@
import { gql } from 'apollo-server-micro';
export const channelTypes = gql`
input channelDetailInput {
alias: String
id: String
transaction_id: String
transaction_vout: Int
base_fee_tokens: Float
fee_rate: Int
cltv_delta: Int
max_htlc_mtokens: String
min_htlc_mtokens: String
}
type policyType {
base_fee_mtokens: String
cltv_delta: Int
fee_rate: Int
is_disabled: Boolean
max_htlc_mtokens: String
min_htlc_mtokens: String
public_key: String!
updated_at: String
}
type nodePolicyType {
base_fee_mtokens: String
cltv_delta: Int
fee_rate: Int
is_disabled: Boolean
max_htlc_mtokens: String
min_htlc_mtokens: String
public_key: String!
updated_at: String
node: Node
}
type singleChannelType {
capacity: Int!
id: String!
policies: [policyType!]!
transaction_id: String!
transaction_vout: Int!
updated_at: String
node_policies: nodePolicyType
partner_node_policies: nodePolicyType
}
type pendingPaymentType {
id: String!
is_outgoing: Boolean!
timeout: Int!
tokens: Int!
}
type pendingResumeType {
incoming_tokens: Int!
outgoing_tokens: Int!
incoming_amount: Int!
outgoing_amount: Int!
total_tokens: Int!
total_amount: Int!
}
type channelType {
capacity: Int!
commit_transaction_fee: Int!
commit_transaction_weight: Int!
id: String!
is_active: Boolean!
is_closing: Boolean!
is_opening: Boolean!
is_partner_initiated: Boolean!
is_private: Boolean!
is_static_remote_key: Boolean
local_balance: Int!
local_reserve: Int!
partner_public_key: String!
received: Int!
remote_balance: Int!
remote_reserve: Int!
sent: Int!
time_offline: Int
time_online: Int
transaction_id: String!
transaction_vout: Int!
unsettled_balance: Int!
partner_node_info: Node!
partner_fee_info: singleChannelType
channel_age: Int!
pending_payments: [pendingPaymentType]!
pending_resume: pendingResumeType!
bosScore: BosScore
}
type closeChannelType {
transactionId: String
transactionOutputIndex: String
}
type closedChannelType {
capacity: Int!
close_confirm_height: Int
close_transaction_id: String
final_local_balance: Int!
final_time_locked_balance: Int!
id: String
is_breach_close: Boolean!
is_cooperative_close: Boolean!
is_funding_cancel: Boolean!
is_local_force_close: Boolean!
is_remote_force_close: Boolean!
partner_public_key: String!
transaction_id: String!
transaction_vout: Int!
partner_node_info: Node!
channel_age: Int!
}
type openChannelType {
transactionId: String
transactionOutputIndex: String
}
type pendingChannelType {
close_transaction_id: String
is_active: Boolean!
is_closing: Boolean!
is_opening: Boolean!
is_timelocked: Boolean!
local_balance: Int!
local_reserve: Int!
partner_public_key: String!
received: Int!
remote_balance: Int!
remote_reserve: Int!
sent: Int!
transaction_fee: Int
transaction_id: String!
transaction_vout: Int!
partner_node_info: Node!
timelock_blocks: Int
timelock_expiration: Int
}
`;

View File

@ -1,180 +0,0 @@
import { randomBytes, createHash } from 'crypto';
import {
payViaPaymentDetails,
getWalletInfo,
probeForRoute,
signMessage,
getInvoices,
verifyMessage,
} from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { to, toWithError } from 'server/helpers/async';
import { requestLimiter } from 'server/helpers/rateLimiter';
import {
createCustomRecords,
decodeMessage,
} from 'server/helpers/customRecords';
import { logger } from 'server/helpers/logger';
import {
GetInvoicesType,
GetWalletInfoType,
} from 'server/types/ln-service.types';
export const chatResolvers = {
Query: {
getMessages: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getMessages');
const { lnd } = context;
const invoiceList = await to<GetInvoicesType>(
getInvoices({
lnd,
limit: params.initialize ? 100 : 5,
})
);
const getFiltered = () =>
Promise.all(
invoiceList.invoices.map(async invoice => {
if (!invoice.is_confirmed) {
return;
}
const messages = invoice.payments[0].messages;
let customRecords: { [key: string]: string } = {};
messages.map(message => {
const { type, value } = message;
const obj = decodeMessage({ type, value });
customRecords = { ...customRecords, ...obj };
});
if (Object.keys(customRecords).length <= 0) {
return;
}
let isVerified = false;
if (customRecords.signature) {
const messageToVerify = JSON.stringify({
sender: customRecords.sender,
message: customRecords.message,
});
const [verified, error] = await toWithError(
verifyMessage({
lnd,
message: messageToVerify,
signature: customRecords.signature,
})
);
if (error) {
logger.debug(`Error verifying message: ${messageToVerify}`);
}
if (
!error &&
(verified as { signed_by: string })?.signed_by ===
customRecords.sender
) {
isVerified = true;
}
}
return {
date: invoice.confirmed_at,
id: invoice.id,
tokens: invoice.tokens,
verified: isVerified,
...customRecords,
};
})
);
const filtered = await getFiltered();
const final = filtered.filter(Boolean) || [];
return { token: invoiceList.next, messages: final };
},
},
Mutation: {
sendMessage: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'sendMessage');
const { lnd } = context;
if (params.maxFee) {
const tokens = Math.max(params.tokens || 100, 100);
const { route } = await to(
probeForRoute({
destination: params.publicKey,
lnd,
tokens,
})
);
if (!route) {
throw new Error('NoRouteFound');
}
if (route.safe_fee > params.maxFee) {
throw new Error('Higher fee limit must be set');
}
}
let satsToSend = params.tokens || 1;
let messageToSend = params.message;
if (params.messageType === 'paymentrequest') {
satsToSend = 1;
messageToSend = `${params.tokens},${params.message}`;
}
const nodeInfo = await to<GetWalletInfoType>(
getWalletInfo({
lnd,
})
);
const userAlias = nodeInfo.alias;
const userKey = nodeInfo.public_key;
const preimage = randomBytes(32);
const secret = preimage.toString('hex');
const id = createHash('sha256').update(preimage).digest().toString('hex');
const messageToSign = JSON.stringify({
sender: userKey,
message: messageToSend,
});
const { signature } = await to(
signMessage({ lnd, message: messageToSign })
);
const customRecords = createCustomRecords({
message: messageToSend,
sender: userKey,
alias: userAlias,
contentType: params.messageType || 'text',
requestType: params.messageType || 'text',
signature,
secret,
});
const { safe_fee } = await to(
payViaPaymentDetails({
id,
lnd,
tokens: satsToSend,
destination: params.publicKey,
messages: customRecords,
})
);
// +1 is needed so that a fee of 0 doesnt evaluate to false
return safe_fee + 1;
},
},
};

View File

@ -1,19 +0,0 @@
import { gql } from 'apollo-server-micro';
export const chatTypes = gql`
type getMessagesType {
token: String
messages: [messagesType]!
}
type messagesType {
date: String!
id: String!
verified: Boolean!
contentType: String
sender: String
alias: String
message: String
tokens: Int
}
`;

View File

@ -1,78 +0,0 @@
import { getIp } from 'server/helpers/helpers';
import jwt from 'jsonwebtoken';
import { logger } from 'server/helpers/logger';
import {
readMacaroons,
readFile,
getAccounts,
} from 'server/helpers/fileHelpers';
import getConfig from 'next/config';
import { ContextType, SSOType } from 'server/types/apiTypes';
import cookie from 'cookie';
import { LndObject } from 'server/types/ln-service.types';
import { getAuthLnd } from 'server/helpers/auth';
import { appConstants } from 'server/utils/appConstants';
import { secret } from 'pages/api/v1';
import { ResolverContext } from 'config/client';
const { serverRuntimeConfig } = getConfig();
const { macaroonPath, lnCertPath, lnServerUrl, accountConfigPath } =
serverRuntimeConfig;
const ssoMacaroon = readMacaroons(macaroonPath);
const ssoCert = readFile(lnCertPath);
const accountConfig = getAccounts(accountConfigPath);
let sso: SSOType | null = null;
if (ssoMacaroon && lnServerUrl) {
sso = {
macaroon: ssoMacaroon,
socket: lnServerUrl,
cert: ssoCert,
};
}
export const getContext = (context: ResolverContext) => {
const { req, res } = context;
if (!req || !res) return {};
const ip = getIp(req);
const cookies = cookie.parse(req.headers.cookie ?? '') || {};
const auth = cookies[appConstants.cookieName];
const lnMarketsAuth = cookies[appConstants.lnMarketsAuth];
const tokenAuth = cookies[appConstants.tokenCookieName];
const ambossAuth = cookies[appConstants.ambossCookieName];
let lnd: LndObject | null = null;
let id: string | null = null;
if (auth) {
try {
const data = jwt.verify(auth, secret) as { id: string };
if (data && data.id) {
lnd = getAuthLnd(data.id, sso, accountConfig);
id = data.id;
}
} catch (error: any) {
logger.silly('Authentication cookie failed');
}
}
const resolverContext: ContextType = {
ip,
lnd,
secret,
id,
sso,
accounts: accountConfig,
res,
lnMarketsAuth,
tokenAuth,
ambossAuth,
};
return resolverContext;
};

View File

@ -1,55 +0,0 @@
import { subDays } from 'date-fns';
import { to } from 'server/helpers/async';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes';
import { GetForwardsType } from 'server/types/ln-service.types';
import { getForwards as getLnForwards } from 'ln-service';
import { sortBy } from 'lodash';
export const forwardsResolver = {
Query: {
getForwards: async (
_: undefined,
{ days }: { days: number },
context: ContextType
) => {
await requestLimiter(context.ip, 'getForwardsPastDays');
const { lnd } = context;
const today = new Date();
const startDate = subDays(today, days);
const forwardsList = await to<GetForwardsType>(
getLnForwards({
lnd,
after: startDate,
before: today,
})
);
let forwards = forwardsList.forwards;
let next = forwardsList.next;
let finishedFetching = false;
if (!next || !forwards || forwards.length <= 0) {
finishedFetching = true;
}
while (!finishedFetching) {
if (next) {
const moreForwards = await to<GetForwardsType>(
getLnForwards({ lnd, token: next })
);
forwards = [...forwards, ...moreForwards.forwards];
next = moreForwards.next;
} else {
finishedFetching = true;
}
}
return sortBy(forwards, 'created_at').reverse();
},
},
};

View File

@ -1,31 +0,0 @@
import { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes';
import { toWithError } from 'server/helpers/async';
import { appUrls } from 'server/utils/appUrls';
import { logger } from 'server/helpers/logger';
import { fetchWithProxy } from 'server/utils/fetch';
export const githubResolvers = {
Query: {
getLatestVersion: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'getLnPay');
const [response, error] = await toWithError<any>(
fetchWithProxy(appUrls.github)
);
if (error || !response) {
logger.debug('Unable to get latest github version');
throw new Error('NoGithubVersion');
}
const json = await response.json();
return json.tag_name;
},
},
};

View File

@ -1,11 +0,0 @@
import getVolumeHealth from './resolvers/getVolumeHealth';
import getTimeHealth from './resolvers/getTimeHealth';
import getFeeHealth from './resolvers/getFeeHealth';
export const healthResolvers = {
Query: {
getVolumeHealth,
getTimeHealth,
getFeeHealth,
},
};

View File

@ -1,133 +0,0 @@
import { getChannels, getChannel, getWalletInfo } from 'ln-service';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to, toWithError } from 'server/helpers/async';
import { logger } from 'server/helpers/logger';
import { ContextType } from 'server/types/apiTypes';
import { GetChannelsType, GetChannelType } from 'server/types/ln-service.types';
import { getFeeScore, getAverage, getMyFeeScore } from '../helpers';
type ChannelFeesType = {
id: string;
publicKey: string;
partnerBaseFee: number;
partnerFeeRate: number;
myBaseFee: number;
myFeeRate: number;
};
export default async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getFeeHealth');
const { lnd } = context;
const { public_key } = await to(getWalletInfo({ lnd }));
const { channels } = await to<GetChannelsType>(getChannels({ lnd }));
const getChannelList = () =>
Promise.all(
channels
.map(async channel => {
const { id, partner_public_key: publicKey } = channel;
const [channelInfo, channelError] = await toWithError(
getChannel({
lnd,
id,
})
);
if (channelError || !channelInfo) {
logger.debug(
`Error getting channel with id ${id}: %o`,
channelError
);
return null;
}
const policies = (channelInfo as GetChannelType).policies;
let partnerBaseFee = 0;
let partnerFeeRate = 0;
let myBaseFee = 0;
let myFeeRate = 0;
if (!channelError && policies) {
for (let i = 0; i < policies.length; i++) {
const policy = policies[i];
if (policy.public_key === public_key) {
myBaseFee = Number(policy.base_fee_mtokens) || 0;
myFeeRate = policy.fee_rate || 0;
} else {
partnerBaseFee = Number(policy.base_fee_mtokens) || 0;
partnerFeeRate = policy.fee_rate || 0;
}
}
}
return {
id,
publicKey,
partnerBaseFee,
partnerFeeRate,
myBaseFee,
myFeeRate,
};
})
.filter(Boolean)
);
const list = await getChannelList();
const health = (list as ChannelFeesType[]).map((channel: ChannelFeesType) => {
const partnerRateScore = getFeeScore(2000, channel.partnerFeeRate);
const partnerBaseScore = getFeeScore(100000, channel.partnerBaseFee);
const myRateScore = getMyFeeScore(2000, channel.myFeeRate, 200);
const myBaseScore = getMyFeeScore(100000, channel.myBaseFee, 1000);
const partnerScore = Math.round(
getAverage([partnerBaseScore, partnerRateScore])
);
const myScore = Math.round(
getAverage([myRateScore.score, myBaseScore.score])
);
const mySide = {
score: myScore,
rate: channel.myFeeRate,
base: Math.round(channel.myBaseFee / 1000),
rateScore: myRateScore.score,
baseScore: myBaseScore.score,
rateOver: myRateScore.over,
baseOver: myBaseScore.over,
};
const partnerSide = {
score: partnerScore,
rate: channel.partnerFeeRate,
base: Math.round(channel.partnerBaseFee / 1000),
rateScore: partnerRateScore,
baseScore: partnerBaseScore,
rateOver: true,
baseOver: true,
};
return {
id: channel.id,
partnerSide,
mySide,
partner: { publicKey: channel.publicKey, lnd },
};
});
const score = Math.round(
getAverage([
...health.map(c => c.partnerSide.score),
...health.map(c => c.mySide.score),
])
);
return {
score,
channels: health,
};
};

View File

@ -1,50 +0,0 @@
import { getChannels } from 'ln-service';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import { ContextType } from 'server/types/apiTypes';
import { GetChannelsType } from 'server/types/ln-service.types';
import { getAverage } from '../helpers';
const halfMonthInMilliSeconds = 1296000000;
export default async (_: undefined, __: any, context: ContextType) => {
await requestLimiter(context.ip, 'getTimeHealth');
const { lnd } = context;
const { channels } = await to<GetChannelsType>(getChannels({ lnd }));
const health = channels.map(channel => {
const {
time_offline = 1,
time_online = 1,
id,
partner_public_key,
} = channel;
const significant = time_offline + time_online > halfMonthInMilliSeconds;
const defaultProps = {
id,
significant,
monitoredTime: Math.round((time_online + time_offline) / 1000),
monitoredUptime: Math.round(time_online / 1000),
monitoredDowntime: Math.round(time_offline / 1000),
partner: { publicKey: partner_public_key, lnd },
};
const percentOnline = time_online / (time_online + time_offline);
return {
score: Math.round(percentOnline * 100),
...defaultProps,
};
});
const average = Math.round(getAverage(health.map(c => c.score)));
return {
score: average,
channels: health,
};
};

View File

@ -1,77 +0,0 @@
import { getForwards, getChannels, getWalletInfo } from 'ln-service';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import { subMonths } from 'date-fns';
import { ContextType } from 'server/types/apiTypes';
import {
GetChannelsType,
GetForwardsType,
} from 'server/types/ln-service.types';
import { getChannelVolume, getChannelIdInfo, getAverage } from '../helpers';
const monthInBlocks = 4380;
export default async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getVolumeHealth');
const { lnd } = context;
const before = new Date().toISOString();
const after = subMonths(new Date(), 1).toISOString();
const { current_block_height } = await to(getWalletInfo({ lnd }));
const { channels } = await to<GetChannelsType>(getChannels({ lnd }));
const { forwards } = await to<GetForwardsType>(
getForwards({ lnd, after, before })
);
const channelVolume: { channel: string; tokens: number }[] =
getChannelVolume(forwards);
const channelDetails = channels
.map(channel => {
const { tokens } = channelVolume.find(c => c.channel === channel.id) || {
tokens: 0,
};
const info = getChannelIdInfo(channel.id);
if (!info) return;
const age = Math.min(
current_block_height - info.blockHeight,
monthInBlocks
);
return {
id: channel.id,
volume: tokens,
volumeNormalized: Math.round(tokens / age) || 0,
publicKey: channel.partner_public_key,
};
})
.filter(Boolean);
const average = getAverage(channelDetails.map(c => c?.volumeNormalized || 0));
const health = channelDetails
.map(channel => {
if (!channel) return null;
const diff = (channel.volumeNormalized - average) / average || -1;
const score = Math.round((diff + 1) * 100);
return {
id: channel.id,
score,
volumeNormalized: channel.volumeNormalized,
averageVolumeNormalized: average,
partner: { publicKey: channel.publicKey, lnd },
};
})
.filter(Boolean);
const globalAverage = Math.round(
getAverage(health.map(c => Math.min(c?.score || 0, 100)))
);
return { score: globalAverage, channels: health };
};

View File

@ -1,53 +0,0 @@
import { gql } from 'apollo-server-micro';
export const healthTypes = gql`
type channelHealth {
id: String
score: Int
volumeNormalized: String
averageVolumeNormalized: String
partner: Node
}
type channelsHealth {
score: Int
channels: [channelHealth]
}
type channelTimeHealth {
id: String
score: Int
significant: Boolean
monitoredTime: Int
monitoredUptime: Int
monitoredDowntime: Int
partner: Node
}
type channelsTimeHealth {
score: Int
channels: [channelTimeHealth]
}
type feeHealth {
score: Int
rate: Int
base: String
rateScore: Int
baseScore: Int
rateOver: Boolean
baseOver: Boolean
}
type channelFeeHealth {
id: String
partnerSide: feeHealth
mySide: feeHealth
partner: Node
}
type channelsFeeHealth {
score: Int
channels: [channelFeeHealth]
}
`;

View File

@ -1,108 +0,0 @@
import { merge } from 'lodash';
import { makeExecutableSchema } from 'graphql-tools';
import { nodeTypes } from './node/types';
import { nodeResolvers } from './node/resolvers';
import { authResolvers } from './auth/resolvers';
import { generalTypes, queryTypes, mutationTypes } from './types';
import { accountResolvers } from './account/resolvers';
import { accountTypes } from './account/types';
import { bitcoinResolvers } from './bitcoin/resolvers';
import { bitcoinTypes } from './bitcoin/types';
import { peerTypes } from './peer/types';
import { peerResolvers } from './peer/resolvers';
import { routeResolvers } from './route/resolvers';
import { chainTypes } from './chain/types';
import { chainResolvers } from './chain/resolvers';
import { toolsResolvers } from './tools/resolvers';
import { chatTypes } from './chat/types';
import { chatResolvers } from './chat/resolvers';
import { widgetResolvers } from './widgets/resolvers';
import { widgetTypes } from './widgets/types';
import { invoiceResolvers } from './invoice/resolvers';
import { channelResolvers } from './channel/resolvers';
import { walletResolvers } from './wallet/resolvers';
import { transactionResolvers } from './transactions/resolvers';
import { channelTypes } from './channel/types';
import { walletTypes } from './wallet/types';
import { invoiceTypes } from './invoice/types';
import { networkTypes } from './network/types';
import { transactionTypes } from './transactions/types';
import { healthResolvers } from './health/resolvers';
import { healthTypes } from './health/types';
import { githubResolvers } from './github/resolvers';
import { routeTypes } from './route/types';
import { generalResolvers } from './resolvers';
import { macaroonResolvers } from './macaroon/resolvers';
import { networkResolvers } from './network/resolvers';
import { bosResolvers } from './bos/resolvers';
import { bosTypes } from './bos/types';
import { tbaseResolvers } from './tbase/resolvers';
import { tbaseTypes } from './tbase/types';
import { lnUrlResolvers } from './lnurl/resolvers';
import { lnUrlTypes } from './lnurl/types';
import { lnMarketsResolvers } from './lnmarkets/resolvers';
import { lnMarketsTypes } from './lnmarkets/types';
import { boltzResolvers } from './boltz/resolvers';
import { boltzTypes } from './boltz/types';
import { forwardsResolver } from './forwards/resolvers';
import { macaroonTypes } from './macaroon/types';
import { ambossTypes } from './amboss/types';
import { ambossResolvers } from './amboss/resolvers';
const typeDefs = [
generalTypes,
queryTypes,
mutationTypes,
nodeTypes,
accountTypes,
bitcoinTypes,
peerTypes,
chainTypes,
chatTypes,
widgetTypes,
channelTypes,
walletTypes,
invoiceTypes,
networkTypes,
transactionTypes,
healthTypes,
routeTypes,
bosTypes,
tbaseTypes,
lnUrlTypes,
lnMarketsTypes,
boltzTypes,
macaroonTypes,
ambossTypes,
];
const resolvers = merge(
generalResolvers,
nodeResolvers,
authResolvers,
accountResolvers,
bitcoinResolvers,
peerResolvers,
routeResolvers,
chainResolvers,
toolsResolvers,
chatResolvers,
widgetResolvers,
invoiceResolvers,
channelResolvers,
walletResolvers,
transactionResolvers,
healthResolvers,
githubResolvers,
macaroonResolvers,
networkResolvers,
bosResolvers,
tbaseResolvers,
lnUrlResolvers,
lnMarketsResolvers,
boltzResolvers,
forwardsResolver,
ambossResolvers
);
export const schema = makeExecutableSchema({ typeDefs, resolvers });

View File

@ -1,232 +0,0 @@
import { randomBytes, createHash } from 'crypto';
import {
pay,
payViaRoutes,
createInvoice,
decodePaymentRequest,
payViaPaymentDetails,
createInvoice as createInvoiceRequest,
subscribeToInvoice,
} from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { getErrorMsg } from 'server/helpers/helpers';
import { to, toWithError } from 'server/helpers/async';
import { CreateInvoiceType, DecodedType } from 'server/types/ln-service.types';
const KEYSEND_TYPE = '5482373484';
type PayType = {
max_fee: Number;
max_paths: Number;
request: String;
out?: String[];
};
export const invoiceResolvers = {
Query: {
getInvoiceStatusChange: async (
_: undefined,
params: { id: string },
context: ContextType
) => {
await requestLimiter(context.ip, 'getInvoiceStatusChange');
const { id } = params;
const { lnd } = context;
const sub = subscribeToInvoice({ id, lnd });
return Promise.race([
new Promise(resolve => {
sub.on('invoice_updated', (data: any) => {
if (data.is_confirmed) {
resolve(true);
}
});
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), 90000)
),
])
.then((res: any) => {
if (res) {
return 'paid';
}
return 'not_paid';
})
.catch(e => {
if (e) return 'timeout';
});
},
decodeRequest: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'decode');
const { lnd } = context;
const decoded = await to<DecodedType>(
decodePaymentRequest({
lnd,
request: params.request,
})
);
return {
...decoded,
destination_node: { lnd, publicKey: decoded.destination },
probe_route: {
lnd,
destination: decoded.destination,
tokens: decoded.tokens,
},
};
},
},
Mutation: {
createInvoice: async (
_: undefined,
{
amount,
description,
secondsUntil,
includePrivate,
}: {
amount: number;
description?: string;
secondsUntil?: number;
includePrivate?: boolean;
},
context: ContextType
) => {
await requestLimiter(context.ip, 'createInvoice');
const { lnd } = context;
const getDate = (secondsUntil: number) => {
const date = new Date();
date.setSeconds(date.getSeconds() + secondsUntil);
return date.toISOString();
};
const invoiceParams = {
tokens: amount,
...(description && { description }),
...(!!secondsUntil && { expires_at: getDate(secondsUntil) }),
...(includePrivate && { is_including_private_channels: true }),
};
logger.info('Creating invoice with params: %o', invoiceParams);
return await to<CreateInvoiceType>(
createInvoiceRequest({
lnd,
...invoiceParams,
})
);
},
keysend: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'keysend');
const { destination, tokens } = params;
const { lnd } = context;
const preimage = randomBytes(32);
const secret = preimage.toString('hex');
const id = createHash('sha256').update(preimage).digest().toString('hex');
return await to(
payViaPaymentDetails({
id,
lnd,
tokens,
destination,
messages: [
{
type: KEYSEND_TYPE,
value: secret,
},
],
})
);
},
circularRebalance: async (
_: undefined,
params: any,
context: ContextType
) => {
await requestLimiter(context.ip, 'circularRebalance');
const { lnd } = context;
let route;
try {
route = JSON.parse(params.route);
} catch (error: any) {
logger.error('Corrupt route json: %o', error);
throw new Error('Corrupt Route JSON');
}
const { id } = await createInvoice({
lnd,
tokens: params.tokens,
description: 'Rebalance',
}).catch((error: any) => {
logger.error('Error getting invoice: %o', error);
throw new Error(getErrorMsg(error));
});
await payViaRoutes({ lnd, routes: [route], id }).catch((error: any) => {
logger.error('Error making payment: %o', error);
throw new Error(getErrorMsg(error));
});
return true;
},
pay: async (
_: undefined,
{ max_fee, max_paths, out, request }: PayType,
context: ContextType
) => {
const { lnd } = context;
const props = {
request,
max_fee,
max_paths,
outgoing_channels: out || [],
};
logger.debug('Paying invoice with params: %o', props);
const [response, error] = await toWithError(pay({ lnd, ...props }));
if (error) {
logger.error('Error paying invoice: %o', error);
throw new Error(getErrorMsg(error));
}
logger.debug('Paid invoice: %o', response);
return true;
},
payViaRoute: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'payViaRoute');
const { route: routeJSON, id } = params;
const { lnd } = context;
let route;
try {
route = JSON.parse(routeJSON);
} catch (error: any) {
logger.error('Corrupt route json: %o', error);
throw new Error('Corrupt Route JSON');
}
await to(payViaRoutes({ lnd, routes: [route], id }));
return true;
},
},
};

View File

@ -1,60 +0,0 @@
import { gql } from 'apollo-server-micro';
export const invoiceTypes = gql`
type decodeType {
chain_address: String
cltv_delta: Int
description: String!
description_hash: String
destination: String!
expires_at: String!
id: String!
mtokens: String!
payment: String
routes: [[RouteType]]!
safe_tokens: Int!
tokens: Int!
destination_node: Node!
probe_route: ProbeRoute
}
type RouteType {
base_fee_mtokens: String
channel: String
cltv_delta: Int
fee_rate: Int
public_key: String!
}
type payType {
fee: Int
fee_mtokens: String
hops: [hopsType]
id: String
is_confirmed: Boolean
is_outgoing: Boolean
mtokens: String
secret: String
safe_fee: Int
safe_tokens: Int
tokens: Int
}
type hopsType {
channel: String
channel_capacity: Int
fee_mtokens: String
forward_mtokens: String
timeout: Int
}
type newInvoiceType {
chain_address: String
created_at: DateTime!
description: String!
id: String!
request: String!
secret: String!
tokens: Int
}
`;

View File

@ -1,206 +0,0 @@
import { getLnMarketsAuth } from 'server/helpers/lnAuth';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes';
import { appConstants } from 'server/utils/appConstants';
import { appUrls } from 'server/utils/appUrls';
import cookie from 'cookie';
import { LnMarketsApi } from 'server/api/LnMarkets';
import { pay, decodePaymentRequest, createInvoice } from 'ln-service';
import { to } from 'server/helpers/async';
import {
CreateInvoiceType,
DecodedType,
PayInvoiceType,
} from 'server/types/ln-service.types';
export const lnMarketsResolvers = {
Query: {
getLnMarketsUrl: async (
_: undefined,
__: undefined,
context: ContextType
): Promise<string> => {
await requestLimiter(context.ip, 'getLnMarketsUrl');
const { lnMarketsAuth, lnd } = context;
const { cookieString } = await getLnMarketsAuth(lnd, lnMarketsAuth);
if (!cookieString) {
logger.error('Error getting auth cookie from lnmarkets');
throw new Error('ProblemAuthenticatingWithLnMarkets');
}
return `${appUrls.lnMarketsExchange}/login/token?token=${cookieString}`;
},
getLnMarketsStatus: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'getLnMarketsStatus');
const { lnMarketsAuth } = context;
if (!lnMarketsAuth) {
return 'out';
}
const json = await LnMarketsApi.getUser(lnMarketsAuth);
logger.debug('Get userInfo from LnMarkets: %o', json);
if (json?.code === 'jwtExpired') {
return 'out';
}
return 'in';
},
getLnMarketsUserInfo: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'getLnMarketsUserInfo');
const { lnMarketsAuth } = context;
if (!lnMarketsAuth) {
logger.debug('Not authenticated on LnMarkets');
throw new Error('NotAuthenticated');
}
const json = await LnMarketsApi.getUser(lnMarketsAuth);
logger.debug('Get userInfo from LnMarkets: %o', json);
if (json?.code === 'jwtExpired') {
logger.debug('Token for LnMarkets is expired');
throw new Error('NotAuthenticated');
}
return json;
},
},
Mutation: {
lnMarketsDeposit: async (
_: undefined,
{ amount }: { amount: number },
context: ContextType
) => {
await requestLimiter(context.ip, 'lnMarketsDeposit');
const { lnMarketsAuth, lnd } = context;
const { cookieString } = await getLnMarketsAuth(lnd, lnMarketsAuth);
if (!cookieString) {
logger.error('Error getting auth cookie from lnmarkets');
throw new Error('ProblemAuthenticatingWithLnMarkets');
}
const info = await LnMarketsApi.getDepositInvoice(cookieString, amount);
logger.debug('Response from lnmarkets: %o', info);
if (!info?.paymentRequest) {
logger.error('Error getting deposit invoice from lnmarkets');
throw new Error('ProblemGettingDepositInvoiceFromLnMarkets');
}
const decoded = await to<DecodedType>(
decodePaymentRequest({
lnd,
request: info.paymentRequest,
})
);
logger.debug('Decoded invoice from lnMarkets: %o', decoded);
if (amount !== decoded.tokens) {
logger.error(
`Tokens in LnMarkets invoice ${decoded.tokens} is different to requested ${amount}`
);
throw new Error('WrongAmountInLnMarketsInvoice');
}
await to<PayInvoiceType>(pay({ lnd, request: info.paymentRequest }));
return true;
},
lnMarketsWithdraw: async (
_: undefined,
{ amount }: { amount: number },
context: ContextType
) => {
await requestLimiter(context.ip, 'lnMarketsWithdraw');
const { lnMarketsAuth, lnd } = context;
const { cookieString } = await getLnMarketsAuth(lnd, lnMarketsAuth);
if (!cookieString) {
logger.error('Error getting auth cookie from lnmarkets');
throw new Error('ProblemAuthenticatingWithLnMarkets');
}
const invoice = await to<CreateInvoiceType>(
createInvoice({
lnd,
description: 'LnMarkets Withdraw',
tokens: amount,
})
);
const response = await LnMarketsApi.withdraw(
cookieString,
amount,
invoice.request
);
logger.debug('Withdraw request from LnMarkets: %o', response);
return true;
},
lnMarketsLogin: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'lnMarketsLogin');
const { lnd, res } = context;
const { cookieString, json } = await getLnMarketsAuth(lnd);
if (!json || !cookieString) {
throw new Error('ProblemAuthenticatingWithLnMarkets');
}
if (json.status === 'ERROR') {
return { ...json, message: json.reason || 'LnServiceError' };
}
res.setHeader(
'Set-Cookie',
cookie.serialize(appConstants.lnMarketsAuth, cookieString, {
httpOnly: true,
sameSite: true,
path: '/',
})
);
return { ...json, message: 'LnMarketsAuthSuccess' };
},
lnMarketsLogout: async (_: undefined, __: any, context: ContextType) => {
const { ip, res } = context;
await requestLimiter(ip, 'lnMarketsLogout');
res.setHeader(
'Set-Cookie',
cookie.serialize(appConstants.lnMarketsAuth, '', {
maxAge: -1,
httpOnly: true,
sameSite: true,
path: '/',
})
);
return true;
},
},
};

View File

@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
export const lnMarketsTypes = gql`
type LnMarketsUserInfo {
uid: String
balance: String
account_type: String
username: String
linkingpublickey: String
last_ip: String
}
`;

View File

@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LNURL Resolvers getBitcoinPrice failure 1`] = `
Object {
"data": null,
"errors": Array [
[GraphQLError: ProblemWithdrawingFromLnUrlService],
],
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;
exports[`LNURL Resolvers getBitcoinPrice success 1`] = `
Object {
"data": Object {
"lnUrlWithdraw": "requestId",
},
"errors": undefined,
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;

View File

@ -1,94 +0,0 @@
import testServer from 'server/tests/testServer';
import fetchMock from 'jest-fetch-mock';
import { GraphQLError } from 'graphql';
import { WITHDRAW_LN_URL } from 'src/graphql/mutations/lnUrl';
jest.mock('ln-service');
describe('LNURL Resolvers', () => {
beforeEach(() => {
fetchMock.resetMocks();
});
describe('getBitcoinPrice', () => {
test('success', async () => {
fetchMock.mockResponseOnce(JSON.stringify({ status: 'SUCCESS' }));
const { mutate } = testServer();
const res = await mutate({
mutation: WITHDRAW_LN_URL,
variables: {
callback: 'https://domain.com',
amount: 1000,
k1: 'random',
description: 'ln-withdraw',
},
});
expect(res.errors).toBe(undefined);
expect(fetchMock).toBeCalledWith(
'https://domain.com?k1=random&pr=boltEncodedRequest',
undefined
);
expect(res).toMatchSnapshot();
});
test('success with callback that has query string', async () => {
fetchMock.mockResponseOnce(JSON.stringify({ status: 'SUCCESS' }));
const { mutate } = testServer();
const res = await mutate({
mutation: WITHDRAW_LN_URL,
variables: {
callback: 'https://domain.com?user=123456',
amount: 1000,
k1: 'random',
description: 'ln-withdraw',
},
});
expect(res.errors).toBe(undefined);
expect(fetchMock).toBeCalledWith(
'https://domain.com?user=123456&k1=random&pr=boltEncodedRequest',
undefined
);
});
test('success but not able to withdraw', async () => {
fetchMock.mockResponseOnce(JSON.stringify({ status: 'ERROR' }));
const { mutate } = testServer();
const res = await mutate({
mutation: WITHDRAW_LN_URL,
variables: {
callback: 'https://domain.com',
amount: 1000,
k1: 'random',
description: 'ln-withdraw',
},
});
expect(res.errors).toStrictEqual([
new GraphQLError('ProblemWithdrawingFromLnUrlService'),
]);
});
test('failure', async () => {
fetchMock.mockRejectOnce(new Error('Error'));
const { mutate } = testServer();
const res = await mutate({
mutation: WITHDRAW_LN_URL,
variables: {
callback: 'domain.com',
amount: 1000,
k1: 'random',
description: 'ln-withdraw',
},
});
expect(res.errors).toStrictEqual([
new GraphQLError('ProblemWithdrawingFromLnUrlService'),
]);
expect(res).toMatchSnapshot();
});
});
});

View File

@ -1,342 +0,0 @@
import { randomBytes } from 'crypto';
import { to } from 'server/helpers/async';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes';
import {
createInvoice,
decodePaymentRequest,
pay,
addPeer,
getWalletInfo,
} from 'ln-service';
import {
CreateInvoiceType,
DecodedType,
GetWalletInfoType,
PayInvoiceType,
} from 'server/types/ln-service.types';
import { lnAuthUrlGenerator } from 'server/helpers/lnAuth';
import { fetchWithProxy } from 'server/utils/fetch';
type LnUrlPayResponseType = {
pr?: string;
successAction?: { tag: string };
status?: string;
reason?: string;
};
type LnUrlParams = {
type: string;
url: string;
};
type FetchLnUrlParams = {
url: string;
};
type LnUrlChannelType = { callback: string; k1: string; uri: string };
type LnUrlPayType = { callback: string; amount: number; comment: string };
type LnUrlWithdrawType = {
callback: string;
k1: string;
amount: number;
description: string;
};
type PayRequestType = {
callback: string;
maxSendable: string;
minSendable: string;
metadata: string;
commentAllowed: number;
tag: string;
};
type WithdrawRequestType = {
callback: string;
k1: string;
maxWithdrawable: string;
defaultDescription: string;
minWithdrawable: string;
tag: string;
};
type RequestType = PayRequestType | WithdrawRequestType;
type RequestWithType = { isTypeOf: string } & RequestType;
export const lnUrlResolvers = {
Query: {
getLightningAddressInfo: async (
_: undefined,
{ address }: { address: string },
{ ip }: ContextType
) => {
await requestLimiter(ip, 'getLightningAddressInfo');
const split = address.split('@');
if (split.length !== 2) {
throw new Error('Invalid lightning address');
}
try {
const response = await fetchWithProxy(
`https://${split[1]}/.well-known/lnurlp/${split[0]}`
);
const result = await response.json();
let valid = true;
if (!result.callback) valid = false;
if (!result.maxSendable) valid = false;
if (!result.minSendable) valid = false;
if (!valid) {
throw new Error('Invalid lightning address');
}
return result;
} catch (error) {
throw new Error('Invalid lightning address');
}
},
},
Mutation: {
lnUrlAuth: async (
_: undefined,
{ url }: LnUrlParams,
context: ContextType
): Promise<{ status: string; message: string }> => {
await requestLimiter(context.ip, 'lnUrl');
const { lnd } = context;
if (!lnd) {
logger.error('Error getting authenticated LND instance in lnUrlAuth');
throw new Error('ProblemAuthenticatingWithLnUrlService');
}
const finalUrl = await lnAuthUrlGenerator(url, lnd);
try {
const response = await fetchWithProxy(finalUrl);
const json = (await response.json()) as any;
logger.debug('LnUrlAuth response: %o', json);
if (json.status === 'ERROR') {
return { ...json, message: json.reason || 'LnServiceError' };
}
return { ...json, message: json.event || 'LnServiceSuccess' };
} catch (error: any) {
logger.error('Error authenticating with LnUrl service: %o', error);
throw new Error('ProblemAuthenticatingWithLnUrlService');
}
},
fetchLnUrl: async (
_: undefined,
{ url }: FetchLnUrlParams,
context: ContextType
) => {
await requestLimiter(context.ip, 'fetchLnUrl');
try {
const response = await fetchWithProxy(url);
const json = (await response.json()) as any;
if (json.status === 'ERROR') {
throw new Error(json.reason || 'LnServiceError');
}
return json;
} catch (error: any) {
logger.error('Error fetching from LnUrl service: %o', error);
throw new Error('ProblemFetchingFromLnUrlService');
}
},
lnUrlPay: async (
_: undefined,
{ callback, amount, comment }: LnUrlPayType,
context: ContextType
) => {
await requestLimiter(context.ip, 'lnUrlPay');
const { lnd } = context;
logger.debug('LnUrlPay initiated with params %o', {
callback,
amount,
comment,
});
const random8byteNonce = randomBytes(8).toString('hex');
// If the callback url already has an initial query '?' identifier we don't need to add it again.
const initialIdentifier = callback.indexOf('?') != -1 ? '&' : '?';
const finalUrl = `${callback}${initialIdentifier}amount=${
amount * 1000
}&nonce=${random8byteNonce}&comment=${comment}`;
let lnServiceResponse: LnUrlPayResponseType = {
status: 'ERROR',
reason: 'FailedToFetchLnService',
};
try {
const response = await fetchWithProxy(finalUrl);
lnServiceResponse = (await response.json()) as any;
if (lnServiceResponse.status === 'ERROR') {
throw new Error(lnServiceResponse.reason || 'LnServiceError');
}
} catch (error: any) {
logger.error('Error paying to LnUrl service: %o', error);
throw new Error('ProblemPayingLnUrlService');
}
logger.debug('LnUrlPay response: %o', lnServiceResponse);
if (!lnServiceResponse.pr) {
logger.error('No invoice in response from LnUrlService');
throw new Error('ProblemPayingLnUrlService');
}
if (lnServiceResponse.successAction) {
const { tag } = lnServiceResponse.successAction;
if (tag !== 'url' && tag !== 'message' && tag !== 'aes') {
logger.error('LnUrlService provided an invalid tag: %o', tag);
throw new Error('InvalidTagFromLnUrlService');
}
}
const decoded = await to<DecodedType>(
decodePaymentRequest({
lnd,
request: lnServiceResponse.pr,
})
);
if (decoded.tokens > amount) {
logger.error(
`Invoice amount ${decoded.tokens} is higher than amount defined ${amount}`
);
throw new Error('LnServiceInvoiceAmountToHigh');
}
const info = await to<PayInvoiceType>(
pay({ lnd, request: lnServiceResponse.pr })
);
if (!info.is_confirmed) {
logger.error(`Failed to pay invoice: ${lnServiceResponse.pr}`);
throw new Error('FailedToPayInvoiceToLnUrlService');
}
return (
lnServiceResponse.successAction || {
tag: 'message',
message: 'Succesfully Paid',
}
);
},
lnUrlWithdraw: async (
_: undefined,
{ callback, k1, amount, description }: LnUrlWithdrawType,
context: ContextType
) => {
await requestLimiter(context.ip, 'lnUrlWithdraw');
const { lnd } = context;
logger.debug('LnUrlWithdraw initiated with params: %o', {
callback,
amount,
k1,
description,
});
// Create invoice to be paid by LnUrlService
const info = await to<CreateInvoiceType>(
createInvoice({ lnd, tokens: amount, description })
);
// If the callback url already has an initial query '?' identifier we don't need to add it again.
const initialIdentifier = callback.indexOf('?') != -1 ? '&' : '?';
const finalUrl = `${callback}${initialIdentifier}k1=${k1}&pr=${info.request}`;
try {
const response = await fetchWithProxy(finalUrl);
const json = (await response.json()) as any;
logger.debug('LnUrlWithdraw response: %o', json);
if (json.status === 'ERROR') {
throw new Error(json.reason || 'LnServiceError');
}
// Return invoice id to check status
return info.id;
} catch (error: any) {
logger.error('Error withdrawing from LnUrl service: %o', error);
throw new Error('ProblemWithdrawingFromLnUrlService');
}
},
lnUrlChannel: async (
_: undefined,
{ callback, k1, uri }: LnUrlChannelType,
context: ContextType
) => {
await requestLimiter(context.ip, 'lnUrlChannel');
const { lnd } = context;
logger.debug('LnUrlChannel initiated with params: %o', {
callback,
uri,
k1,
});
const split = uri.split('@');
await to(addPeer({ lnd, socket: split[1], public_key: split[0] }));
const info = await to<GetWalletInfoType>(getWalletInfo({ lnd }));
// If the callback url already has an initial query '?' identifier we don't need to add it again.
const initialIdentifier = callback.indexOf('?') != -1 ? '&' : '?';
const finalUrl = `${callback}${initialIdentifier}k1=${k1}&remoteid=${info.public_key}&private=0`;
try {
const response = await fetchWithProxy(finalUrl);
const json = (await response.json()) as any;
logger.debug('LnUrlChannel response: %o', json);
if (json.status === 'ERROR') {
throw new Error(json.reason || 'LnServiceError');
}
return 'Successfully requested a channel open';
} catch (error: any) {
logger.error('Error requesting channel from LnUrl service: %o', error);
throw new Error(
`Error requesting channel from LnUrl service: ${error}`
);
}
},
},
LnUrlRequest: {
__resolveType(parent: RequestWithType) {
if (parent.tag === 'payRequest') {
return 'PayRequest';
}
if (parent.tag === 'withdrawRequest') {
return 'WithdrawRequest';
}
if (parent.tag === 'channelRequest') {
return 'ChannelRequest';
}
return 'Unknown';
},
},
};

View File

@ -1,44 +0,0 @@
import { gql } from '@apollo/client';
export const lnUrlTypes = gql`
type WithdrawRequest {
callback: String
k1: String
maxWithdrawable: String
defaultDescription: String
minWithdrawable: String
tag: String
}
type PayRequest {
callback: String
maxSendable: String
minSendable: String
metadata: String
commentAllowed: Int
tag: String
}
type ChannelRequest {
tag: String
k1: String
callback: String
uri: String
}
union LnUrlRequest = WithdrawRequest | PayRequest | ChannelRequest
type AuthResponse {
status: String!
message: String!
}
type PaySuccess {
tag: String
description: String
url: String
message: String
ciphertext: String
iv: String
}
`;

View File

@ -1,57 +0,0 @@
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import { grantAccess } from 'ln-service';
import { logger } from 'server/helpers/logger';
export type PermissionsType = {
is_ok_to_adjust_peers: boolean;
is_ok_to_create_chain_addresses: boolean;
is_ok_to_create_invoices: boolean;
is_ok_to_create_macaroons: boolean;
is_ok_to_derive_keys: boolean;
is_ok_to_get_chain_transactions: boolean;
is_ok_to_get_invoices: boolean;
is_ok_to_get_wallet_info: boolean;
is_ok_to_get_payments: boolean;
is_ok_to_get_peers: boolean;
is_ok_to_pay: boolean;
is_ok_to_send_to_chain_addresses: boolean;
is_ok_to_sign_bytes: boolean;
is_ok_to_sign_messages: boolean;
is_ok_to_stop_daemon: boolean;
is_ok_to_verify_bytes_signatures: boolean;
is_ok_to_verify_messages: boolean;
};
type ParamsType = {
permissions: PermissionsType;
};
export const macaroonResolvers = {
Mutation: {
createMacaroon: async (
_: undefined,
params: ParamsType,
context: ContextType
) => {
await requestLimiter(context.ip, 'createMacaroon');
const { permissions } = params;
const { lnd } = context;
const { macaroon, permissions: permissionList } = await to(
grantAccess({ lnd, ...permissions })
);
logger.debug(
'Macaroon created with the following permissions: %o',
permissionList.join(', ')
);
const hex = Buffer.from(macaroon, 'base64').toString('hex');
return { base: macaroon, hex };
},
},
};

View File

@ -1,8 +0,0 @@
import { gql } from 'apollo-server-micro';
export const macaroonTypes = gql`
type CreateMacaroon {
base: String!
hex: String!
}
`;

View File

@ -1,42 +0,0 @@
import { getNetworkInfo } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
interface NetworkInfoProps {
average_channel_size: number;
channel_count: number;
max_channel_size: number;
median_channel_size: number;
min_channel_size: number;
node_count: number;
not_recently_updated_policy_count: number;
total_capacity: number;
}
export const networkResolvers = {
Query: {
getNetworkInfo: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'networkInfo');
const { lnd } = context;
const info: NetworkInfoProps = await to(getNetworkInfo({ lnd }));
return {
averageChannelSize: info.average_channel_size,
channelCount: info.channel_count,
maxChannelSize: info.max_channel_size,
medianChannelSize: info.median_channel_size,
minChannelSize: info.min_channel_size,
nodeCount: info.node_count,
notRecentlyUpdatedPolicyCount: info.not_recently_updated_policy_count,
totalCapacity: info.total_capacity,
};
},
},
};

View File

@ -1,14 +0,0 @@
import { gql } from 'apollo-server-micro';
export const networkTypes = gql`
type networkInfoType {
averageChannelSize: String
channelCount: Int
maxChannelSize: Int
medianChannelSize: Int
minChannelSize: Int
nodeCount: Int
notRecentlyUpdatedPolicyCount: Int
totalCapacity: String
}
`;

View File

@ -1,201 +0,0 @@
import {
getNode,
getWalletInfo,
getClosedChannels,
getPendingChannels,
getChannelBalance,
getChannels,
getChainBalance,
getPendingChainBalance,
} from 'ln-service';
import { to, toWithError } from 'server/helpers/async';
import { requestLimiter } from 'server/helpers/rateLimiter';
import {
ClosedChannelsType,
LndObject,
GetWalletInfoType,
GetNodeType,
GetPendingChannelsType,
GetChannelsType,
GetChainBalanceType,
GetPendingChainBalanceType,
} from 'server/types/ln-service.types';
import { ContextType } from '../../types/apiTypes';
import { logger } from '../../helpers/logger';
const errorNode = { alias: 'Node not found' };
type ChannelBalanceProps = {
channel_balance: number;
pending_balance: number;
};
type ChainBalanceProps = {
chain_balance: number;
};
type PendingChainBalanceProps = {
pending_chain_balance: number;
};
type NodeParent = {
lnd: LndObject;
publicKey: string;
withChannels?: boolean;
};
export const nodeResolvers = {
Query: {
getNodeBalances: async (_: undefined, __: any, context: ContextType) => {
await requestLimiter(context.ip, 'getNodeBalances');
return {};
},
getNode: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getNode');
const { withoutChannels = true, publicKey } = params;
const { lnd } = context;
return { lnd, publicKey, withChannels: !withoutChannels };
},
getNodeInfo: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'nodeInfo');
const { lnd } = context;
const info = await to<GetWalletInfoType>(
getWalletInfo({
lnd,
})
);
const closedChannels: ClosedChannelsType = await to(
getClosedChannels({
lnd,
})
);
const { pending_channels } = await to<GetPendingChannelsType>(
getPendingChannels({ lnd })
);
const pending_channels_count = pending_channels.length;
return {
...info,
pending_channels_count,
closed_channels_count: closedChannels?.channels?.length || 0,
};
},
},
BalancesType: {
onchain: async () => {
return 0;
},
lightning: async (_: undefined, __: undefined, { lnd }: ContextType) => {
const { channels } = await to<GetChannelsType>(getChannels({ lnd }));
const confirmed = channels
.map(c => c.local_balance)
.reduce((total, size) => total + size, 0);
const active = channels
.filter(c => c.is_active)
.map(c => c.local_balance)
.reduce((total, size) => total + size, 0);
const commit = channels
.filter(c => !c.is_partner_initiated)
.map(c => c.commit_transaction_fee)
.reduce((total, fee) => total + fee, 0);
return {
confirmed,
active,
commit,
};
},
},
OnChainBalanceType: {
confirmed: async (
_: undefined,
__: undefined,
{ ip, lnd }: ContextType
) => {
await requestLimiter(ip, 'chainBalance');
const value: ChainBalanceProps = await to<GetChainBalanceType>(
getChainBalance({
lnd,
})
);
return value.chain_balance || 0;
},
pending: async (_: undefined, __: undefined, { lnd }: ContextType) => {
const pendingValue: PendingChainBalanceProps =
await to<GetPendingChainBalanceType>(
getPendingChainBalance({
lnd,
})
);
return pendingValue.pending_chain_balance || 0;
},
closing: async (_: undefined, __: undefined, { lnd }: ContextType) => {
const { pending_channels } = await to<GetPendingChannelsType>(
getPendingChannels({ lnd })
);
const closing =
pending_channels
.filter(p => p.is_timelocked)
.reduce((p, c) => p + c.local_balance, 0) || 0;
return closing || 0;
},
},
LightningBalanceType: {
pending: async (_: undefined, __: undefined, { lnd }: ContextType) => {
const channelBalance: ChannelBalanceProps = await to(
getChannelBalance({
lnd,
})
);
return channelBalance.pending_balance;
},
},
Node: {
node: async (parent: NodeParent) => {
const { lnd, withChannels, publicKey } = parent;
if (!lnd) {
logger.debug('ExpectedLNDToGetNode');
return errorNode;
}
if (!publicKey) {
logger.debug('ExpectedPublicKeyToGetNode');
return errorNode;
}
const [info, error] = await toWithError(
getNode({
lnd,
is_omitting_channels: !withChannels,
public_key: publicKey,
})
);
if (error || !info) {
logger.debug(`Error getting node with key: ${publicKey}`);
return errorNode;
}
return { ...(info as GetNodeType), public_key: publicKey };
},
},
};

View File

@ -1,52 +0,0 @@
import { gql } from 'apollo-server-micro';
export const nodeTypes = gql`
type nodeType {
alias: String!
capacity: String
channel_count: Int
color: String
updated_at: String
public_key: String
}
type Node {
node: nodeType!
}
type nodeInfoType {
chains: [String!]!
color: String!
active_channels_count: Int!
closed_channels_count: Int!
alias: String!
current_block_hash: String!
current_block_height: Int!
is_synced_to_chain: Boolean!
is_synced_to_graph: Boolean!
latest_block_at: String!
peers_count: Int!
pending_channels_count: Int!
public_key: String!
uris: [String!]!
version: String!
}
type BalancesType {
onchain: OnChainBalanceType!
lightning: LightningBalanceType!
}
type OnChainBalanceType {
confirmed: String!
pending: String!
closing: String!
}
type LightningBalanceType {
confirmed: String!
active: String!
commit: String!
pending: String!
}
`;

View File

@ -1,97 +0,0 @@
import { getPeers, removePeer, addPeer } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { getErrorMsg } from 'server/helpers/helpers';
import { to } from 'server/helpers/async';
interface PeerProps {
bytes_received: number;
bytes_sent: number;
is_inbound: boolean;
is_sync_peer: boolean;
ping_time: number;
public_key: string;
socket: string;
tokens_received: number;
tokens_sent: number;
}
export const peerResolvers = {
Query: {
getPeers: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getPeers');
const { lnd } = context;
const { peers }: { peers: PeerProps[] } = await to(
getPeers({
lnd,
})
);
return peers.map(peer => ({
...peer,
partner_node_info: { lnd, publicKey: peer.public_key },
}));
},
},
Mutation: {
addPeer: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'addPeer');
const { url, publicKey, socket, isTemporary } = params;
if (!url && !publicKey && !socket) {
logger.error('Expected public key and socket to connect');
throw new Error('ExpectedPublicKeyAndSocketToConnect');
}
let peerSocket = socket || '';
let peerPublicKey = publicKey || '';
if (url) {
const parts = url.split('@');
if (parts.length !== 2) {
logger.error(`Wrong url format to connect (${url})`);
throw new Error('WrongUrlFormatToConnect');
}
peerPublicKey = parts[0];
peerSocket = parts[1];
}
const { lnd } = context;
try {
const success: boolean = await addPeer({
lnd,
public_key: peerPublicKey,
socket: peerSocket,
is_temporary: isTemporary,
});
return success;
} catch (error: any) {
logger.error('Error adding peer: %o', error);
throw new Error(getErrorMsg(error));
}
},
removePeer: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'removePeer');
const { lnd } = context;
try {
const success: boolean = await removePeer({
lnd,
public_key: params.publicKey,
});
return success;
} catch (error: any) {
logger.error('Error removing peer: %o', error);
throw new Error(getErrorMsg(error));
}
},
},
};

View File

@ -1,16 +0,0 @@
import { gql } from 'apollo-server-micro';
export const peerTypes = gql`
type peerType {
bytes_received: Int!
bytes_sent: Int!
is_inbound: Boolean!
is_sync_peer: Boolean
ping_time: Int!
public_key: String!
socket: String!
tokens_received: Int!
tokens_sent: Int!
partner_node_info: Node!
}
`;

View File

@ -1,7 +0,0 @@
import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date';
export const generalResolvers = {
Date: GraphQLDate,
Time: GraphQLTime,
DateTime: GraphQLDateTime,
};

View File

@ -1,86 +0,0 @@
import {
getRouteToDestination,
getWalletInfo,
probeForRoute,
} from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { toWithError, to } from 'server/helpers/async';
import { LndObject, ProbeForRouteType } from 'server/types/ln-service.types';
type RouteParent = {
lnd: LndObject;
destination: string;
tokens: number;
};
export const routeResolvers = {
Query: {
getRoutes: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'getRoutes');
const { lnd } = context;
const { public_key } = await getWalletInfo({ lnd });
const { route } = await to(
getRouteToDestination({
lnd,
outgoing_channel: params.outgoing,
incoming_peer: params.incoming,
destination: public_key,
tokens: params.tokens,
...(params.maxFee && { max_fee: params.maxFee }),
})
);
if (!route) {
throw new Error('NoRouteFound');
}
return route;
},
},
ProbeRoute: {
route: async (parent: RouteParent) => {
const { lnd, destination, tokens } = parent;
if (!lnd) {
logger.debug('ExpectedLNDToProbeForRoute');
return null;
}
if (!destination) {
logger.debug('ExpectedDestinationToProbeForRoute');
return null;
}
const [info, error] = await toWithError(
probeForRoute({ lnd, destination, tokens })
);
if (!info || error) {
logger.debug(
`Error probing route to destination ${destination} for ${tokens} tokens`
);
return null;
}
if (!(info as ProbeForRouteType).route) {
logger.debug(
`No route found to destination ${destination} for ${tokens} tokens`
);
return null;
}
const hopsWithNodes =
(info as ProbeForRouteType).route?.hops.map(h => ({
...h,
node: { lnd, publicKey: h.public_key },
})) || [];
return { ...(info as ProbeForRouteType).route, hops: hopsWithNodes };
},
},
};

View File

@ -1,60 +0,0 @@
import { gql } from 'apollo-server-micro';
export const routeTypes = gql`
type RouteMessageType {
type: String!
value: String!
}
type RouteHopType {
channel: String!
channel_capacity: Int!
fee: Int!
fee_mtokens: String!
forward: Int!
forward_mtokens: String!
public_key: String!
timeout: Int!
}
type GetRouteType {
confidence: Int
fee: Int!
fee_mtokens: String!
hops: [RouteHopType!]!
messages: [RouteMessageType]
mtokens: String!
safe_fee: Int!
safe_tokens: Int!
timeout: Int!
tokens: Int!
}
type probedRouteHop {
channel: String!
channel_capacity: Int!
fee: Int!
fee_mtokens: String!
forward: Int!
forward_mtokens: String!
public_key: String!
timeout: Int!
node: Node!
}
type probedRoute {
confidence: Int!
fee: Int!
fee_mtokens: String!
hops: [probedRouteHop!]!
mtokens: String!
safe_fee: Int!
safe_tokens: Int!
timeout: Int!
tokens: Int!
}
type ProbeRoute {
route: probedRoute
}
`;

View File

@ -1,224 +0,0 @@
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { appUrls } from 'server/utils/appUrls';
import { logger } from 'server/helpers/logger';
import { appConstants } from 'server/utils/appConstants';
import cookieLib from 'cookie';
import { graphqlFetchWithProxy } from 'server/utils/fetch';
const getBaseCanConnectQuery = `
{
hello
}
`;
const getBaseNodesQuery = `
{
getNodes {
_id
name
public_key
socket
}
}
`;
const getBasePointsQuery = `
{
getPoints {
alias
amount
}
}
`;
const createBaseInvoiceQuery = `
mutation CreateInvoice($amount: Int!) {
createInvoice(amount: $amount) {
request
id
}
}
`;
const createBaseTokenInvoiceQuery = `
mutation CreateTokenInvoice($days: Int) {
createTokenInvoice(days: $days) {
request
id
}
}
`;
const createThunderPointsQuery = `
mutation CreatePoints(
$id: String!
$alias: String!
$uris: [String!]!
$public_key: String!
) {
createPoints(id: $id, alias: $alias, uris: $uris, public_key: $public_key)
}
`;
const createBaseTokenQuery = `
mutation CreateBaseToken($id: String!) {
createBaseToken(id: $id)
}
`;
export const tbaseResolvers = {
Query: {
getBaseCanConnect: async (
_: undefined,
__: undefined,
context: ContextType
): Promise<boolean> => {
await requestLimiter(context.ip, 'getBaseCanConnect');
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
getBaseCanConnectQuery
);
if (error || !data?.hello) return false;
return true;
},
getBaseNodes: async (_: undefined, __: any, context: ContextType) => {
await requestLimiter(context.ip, 'getBaseNodes');
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
getBaseNodesQuery
);
if (error || !data?.getNodes) return [];
return data.getNodes.filter(
(n: { public_key: string; socket: string }) => n.public_key && n.socket
);
},
getBasePoints: async (_: undefined, __: any, context: ContextType) => {
await requestLimiter(context.ip, 'getBasePoints');
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
getBasePointsQuery
);
if (error || !data?.getPoints) return [];
return data.getPoints;
},
},
Mutation: {
createBaseInvoice: async (
_: undefined,
params: { amount: number },
context: ContextType
) => {
await requestLimiter(context.ip, 'createBaseInvoice');
if (!params?.amount) return '';
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
createBaseInvoiceQuery,
params
);
if (error) return null;
if (data?.createInvoice) return data.createInvoice;
return null;
},
createBaseToken: async (
_: undefined,
{ id }: { id: string },
{ ip, res }: ContextType
) => {
await requestLimiter(ip, 'createBaseInvoice');
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
createBaseTokenQuery,
{ id }
);
if (error || !data?.createBaseToken) {
logger.debug('Error getting thunderbase token');
throw new Error('ErrorGettingToken');
}
res.setHeader(
'Set-Cookie',
cookieLib.serialize(
appConstants.tokenCookieName,
data.createBaseToken,
{
maxAge: 60 * 60 * 24 * 30, //One month
httpOnly: true,
sameSite: true,
path: '/',
}
)
);
return true;
},
deleteBaseToken: async (
_: undefined,
__: undefined,
{ ip, res }: ContextType
) => {
await requestLimiter(ip, 'deleteBaseToken');
res.setHeader(
'Set-Cookie',
cookieLib.serialize(appConstants.tokenCookieName, '', {
maxAge: -1,
httpOnly: true,
sameSite: true,
path: '/',
})
);
return true;
},
createBaseTokenInvoice: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'createBaseTokenInvoice');
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
createBaseTokenInvoiceQuery
);
if (error || !data?.createTokenInvoice) {
logger.error('Error getting invoice for token');
throw new Error('ErrorGettingInvoice');
}
return data.createTokenInvoice;
},
createThunderPoints: async (
_: undefined,
params: { id: string; alias: string; uris: string[]; public_key: string },
context: ContextType
): Promise<boolean> => {
await requestLimiter(context.ip, 'createThunderPoints');
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
createThunderPointsQuery,
params
);
if (error || !data?.createPoints) return false;
return true;
},
},
};

View File

@ -1,26 +0,0 @@
import { gql } from 'apollo-server-micro';
export const tbaseTypes = gql`
type baseNodesType {
_id: String
name: String
public_key: String!
socket: String!
}
type basePointsType {
alias: String!
amount: Int!
}
type baseInvoiceType {
id: String!
request: String!
}
type BaseInfo {
lastBosUpdate: String!
apiTokenSatPrice: Int!
apiTokenOriginalSatPrice: Int!
}
`;

View File

@ -1,151 +0,0 @@
import {
verifyBackups as verifyLnBackups,
recoverFundsFromChannels,
getBackups,
pay,
verifyMessage,
signMessage,
} from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { getErrorMsg } from 'server/helpers/helpers';
import { toWithError } from 'server/helpers/async';
import { ChannelType } from 'server/types/ln-service.types';
export const toolsResolvers = {
Query: {
verifyBackups: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'verifyBackups');
const { lnd } = context;
let backupObj = { backup: '', channels: [] as ChannelType[] };
try {
backupObj = JSON.parse(params.backup);
} catch (error: any) {
logger.error('Corrupt backup file: %o', error);
throw new Error('Corrupt backup file');
}
const { backup, channels } = backupObj;
try {
const { is_valid } = await verifyLnBackups({
lnd,
backup,
channels,
});
return is_valid;
} catch (error: any) {
logger.error('Error verifying backups: %o', error);
throw new Error(getErrorMsg(error));
}
},
recoverFunds: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'recoverFunds');
const { lnd } = context;
let backupObj = { backup: '' };
try {
backupObj = JSON.parse(params.backup);
} catch (error: any) {
logger.error('Corrupt backup file: %o', error);
throw new Error('Corrupt backup file');
}
const { backup } = backupObj;
try {
await recoverFundsFromChannels({
lnd,
backup,
});
return true;
} catch (error: any) {
logger.error('Error recovering funds from channels: %o', error);
throw new Error(getErrorMsg(error));
}
},
getBackups: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getBackups');
const { lnd } = context;
try {
const backups = await getBackups({
lnd,
});
return JSON.stringify(backups);
} catch (error: any) {
logger.error('Error getting backups: %o', error);
throw new Error(getErrorMsg(error));
}
},
adminCheck: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'adminCheck');
const { lnd } = context;
const [, error] = await toWithError(
pay({
lnd,
request: 'admin check',
})
);
if (error && error.length >= 2) {
if (error[2]?.err?.details?.indexOf('permission denied') >= 0) {
logger.warn('Admin permission check failed.');
throw new Error('PermissionDenied');
}
if (
error[2]?.err?.details?.indexOf('invalid character in string:') >= 0
) {
logger.info('Admin permission checked');
return true;
}
}
logger.info('%o', error);
const errorMessage = getErrorMsg(error);
throw new Error(errorMessage);
},
verifyMessage: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'verifyMessage');
const { lnd } = context;
try {
const message: { signed_by: string } = await verifyMessage({
lnd,
message: params.message,
signature: params.signature,
});
return message.signed_by;
} catch (error: any) {
logger.error('Error verifying message: %o', error);
throw new Error(getErrorMsg(error));
}
},
signMessage: async (_: undefined, params: any, context: ContextType) => {
await requestLimiter(context.ip, 'signMessage');
const { lnd } = context;
try {
const message: { signature: string } = await signMessage({
lnd,
message: params.message,
});
return message.signature;
} catch (error: any) {
logger.error('Error signing message: %o', error);
throw new Error(getErrorMsg(error));
}
},
},
};

View File

@ -1,265 +0,0 @@
import { compareDesc } from 'date-fns';
import { getChannel, getNode, getPayments, getInvoices } from 'ln-service';
import { to, toWithError } from 'server/helpers/async';
import { logger } from 'server/helpers/logger';
import {
ChannelType,
GetChannelType,
GetInvoicesType,
GetNodeType,
GetPaymentsType,
LndObject,
} from 'server/types/ln-service.types';
// Limit the amount of transactions that are fetched
const FETCH_LIMIT = 50000;
const BATCH_SIZE = 250;
export const getNodeFromChannel = async (
lnd: {},
channelId: string,
public_key: string,
closedChannels: ChannelType[]
) => {
const closedChannel = closedChannels.find(c => c.id === channelId);
if (closedChannel) {
const [nodeInfo, nodeError] = await toWithError<GetNodeType>(
getNode({
lnd,
is_omitting_channels: true,
public_key: closedChannel.partner_public_key,
})
);
if (nodeError || !nodeInfo) {
logger.error(
`Unable to get node with public key: ${closedChannel.partner_public_key}. Error %o`,
nodeError
);
return null;
}
return {
...nodeInfo,
public_key: closedChannel.partner_public_key,
channel_id: channelId,
};
}
const [info, error] = await toWithError<GetChannelType>(
getChannel({
lnd,
id: channelId,
})
);
if (error || !info) {
logger.error(
`Unable to get channel with id: ${channelId}. Error %o`,
error
);
return null;
}
const partner_node_policy = info.policies.find(
policy => policy.public_key !== public_key
);
if (!partner_node_policy?.public_key) {
logger.error(`Unable to get partner public key for channel: ${channelId}`);
return null;
}
const [nodeInfo, nodeError] = await toWithError<GetNodeType>(
getNode({
lnd,
is_omitting_channels: true,
public_key: partner_node_policy?.public_key,
})
);
if (nodeError || !nodeInfo) {
logger.error(
`Unable to get node with public key: ${partner_node_policy?.public_key}. Error %o`,
nodeError
);
return null;
}
return {
...nodeInfo,
public_key: partner_node_policy?.public_key,
channel_id: channelId,
};
};
export const getPaymentsBetweenDates = async ({
lnd,
from,
until,
}: {
lnd: LndObject | null;
from?: string;
until?: string;
}) => {
const paymentList = await to<GetPaymentsType>(
getPayments({
lnd,
limit: BATCH_SIZE,
})
);
if (!paymentList?.payments?.length) {
return [];
}
if (!from || !until) {
return paymentList.payments;
}
const firstPayment = paymentList.payments[0];
const isOutOf =
compareDesc(new Date(firstPayment.created_at), new Date(until)) === 1;
const filterArray = (payment: GetPaymentsType['payments'][0]) => {
const date = payment.created_at;
const last = compareDesc(new Date(until), new Date(date)) === 1;
const first = compareDesc(new Date(date), new Date(from)) === 1;
return last && first;
};
if (isOutOf || paymentList.payments.length < BATCH_SIZE) {
return paymentList.payments.filter(filterArray);
}
let completePayments = paymentList.payments;
let nextToken = paymentList.next;
let finished = false;
while (!finished) {
if (completePayments.length >= FETCH_LIMIT) {
finished = true;
break;
}
const newPayments = await to<GetPaymentsType>(
getPayments({
lnd,
token: nextToken,
})
);
if (!newPayments?.payments?.length) {
finished = true;
break;
}
completePayments = [...completePayments, ...newPayments.payments];
const firstPayment = newPayments.payments[0];
if (compareDesc(new Date(firstPayment.created_at), new Date(until)) === 1) {
finished = true;
break;
}
if (!newPayments.next) {
finished = true;
break;
}
nextToken = newPayments.next;
}
return completePayments.filter(filterArray);
};
export const getInvoicesBetweenDates = async ({
lnd,
from,
until,
}: {
lnd: LndObject | null;
from?: string;
until?: string;
}) => {
const invoiceList = await to<GetInvoicesType>(
getInvoices({
lnd,
limit: BATCH_SIZE,
})
);
if (!invoiceList?.invoices?.length) {
return [];
}
if (!from || !until) {
return invoiceList.invoices;
}
const firstInvoice = invoiceList.invoices[0];
const firstDate = firstInvoice.confirmed_at || firstInvoice.created_at;
const isOutOf = compareDesc(new Date(firstDate), new Date(until)) === 1;
const filterArray = (invoice: GetInvoicesType['invoices'][0]) => {
const date = invoice.confirmed_at || invoice.created_at;
const last = compareDesc(new Date(until), new Date(date)) === 1;
const first = compareDesc(new Date(date), new Date(from)) === 1;
return last && first;
};
if (isOutOf || invoiceList.invoices.length < BATCH_SIZE) {
return invoiceList.invoices.filter(filterArray);
}
let completeInvoices = invoiceList.invoices;
let nextToken = invoiceList.next;
let finished = false;
while (!finished) {
if (completeInvoices.length >= FETCH_LIMIT) {
finished = true;
break;
}
const newInvoices = await to<GetInvoicesType>(
getInvoices({
lnd,
token: nextToken,
})
);
if (!newInvoices?.invoices?.length) {
finished = true;
break;
}
completeInvoices = [...completeInvoices, ...newInvoices.invoices];
const firstNewInvoice = newInvoices.invoices[0];
const firstNewDate =
firstNewInvoice.confirmed_at || firstNewInvoice.created_at;
if (compareDesc(new Date(firstNewDate), new Date(until)) === 1) {
finished = true;
break;
}
if (!newInvoices.next) {
finished = true;
break;
}
nextToken = newInvoices.next;
}
return completeInvoices.filter(filterArray);
};

View File

@ -1,78 +0,0 @@
import { subDays } from 'date-fns';
import { sortBy } from 'lodash';
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { InvoiceType, PaymentType } from 'server/types/ln-service.types';
import { decodeMessages } from 'server/helpers/customRecords';
import { getInvoicesBetweenDates, getPaymentsBetweenDates } from './helpers';
type TransactionType = InvoiceType | PaymentType;
type TransactionWithType = { isTypeOf: string } & TransactionType;
export const transactionResolvers = {
Query: {
getResume: async (
_: undefined,
{ offset, limit }: { offset?: number; limit?: number },
context: ContextType
) => {
await requestLimiter(context.ip, 'payments');
const { lnd } = context;
const start = offset || 0;
const end = (offset || 0) + (limit || 7);
const today = new Date();
const startDate = subDays(today, start).toISOString();
const endDate = subDays(today, end).toISOString();
const payments = await getPaymentsBetweenDates({
lnd,
from: startDate,
until: endDate,
});
const mappedPayments = payments.map(payment => ({
...payment,
type: 'payment',
date: payment.created_at,
destination_node: { lnd, publicKey: payment.destination },
hops: [...payment.hops.map(hop => ({ lnd, publicKey: hop }))],
isTypeOf: 'PaymentType',
}));
const invoices = await getInvoicesBetweenDates({
lnd,
from: startDate,
until: endDate,
});
const mappedInvoices = invoices.map(invoice => ({
type: 'invoice',
date: invoice.confirmed_at || invoice.created_at,
...invoice,
isTypeOf: 'InvoiceType',
payments: invoice.payments.map(p => ({
...p,
messages: decodeMessages(p.messages),
})),
}));
const resume = sortBy(
[...mappedInvoices, ...mappedPayments],
'date'
).reverse();
return {
offset: end,
resume,
};
},
},
Transaction: {
__resolveType(parent: TransactionWithType) {
return parent.isTypeOf;
},
},
};

View File

@ -1,83 +0,0 @@
import { gql } from 'apollo-server-micro';
export const transactionTypes = gql`
type Forward {
created_at: String!
fee: Int!
fee_mtokens: String!
incoming_channel: String!
mtokens: String!
outgoing_channel: String!
tokens: Int!
}
type ForwardNodeType {
alias: String
capacity: String
channel_count: Int
color: String
updated_at: String
channel_id: String
public_key: String
}
type MessageType {
message: String
}
type PaymentType {
created_at: String!
destination: String!
destination_node: Node
fee: Int!
fee_mtokens: String!
hops: [Node!]!
id: String!
index: Int
is_confirmed: Boolean!
is_outgoing: Boolean!
mtokens: String!
request: String
safe_fee: Int!
safe_tokens: Int
secret: String!
tokens: String!
type: String!
date: String!
}
type InvoicePayment {
in_channel: String!
messages: MessageType
}
type InvoiceType {
chain_address: String
confirmed_at: String
created_at: String!
description: String!
description_hash: String
expires_at: String!
id: String!
is_canceled: Boolean
is_confirmed: Boolean!
is_held: Boolean
is_private: Boolean!
is_push: Boolean
received: Int!
received_mtokens: String!
request: String
secret: String!
tokens: String!
type: String!
date: String!
payments: [InvoicePayment]!
}
union Transaction = InvoiceType | PaymentType
type getResumeType {
offset: Int
resume: [Transaction]!
}
`;

View File

@ -1,222 +0,0 @@
import { gql } from 'apollo-server-micro';
export const generalTypes = gql`
input permissionsType {
is_ok_to_adjust_peers: Boolean
is_ok_to_create_chain_addresses: Boolean
is_ok_to_create_invoices: Boolean
is_ok_to_create_macaroons: Boolean
is_ok_to_derive_keys: Boolean
is_ok_to_get_chain_transactions: Boolean
is_ok_to_get_invoices: Boolean
is_ok_to_get_wallet_info: Boolean
is_ok_to_get_payments: Boolean
is_ok_to_get_peers: Boolean
is_ok_to_pay: Boolean
is_ok_to_send_to_chain_addresses: Boolean
is_ok_to_sign_bytes: Boolean
is_ok_to_sign_messages: Boolean
is_ok_to_stop_daemon: Boolean
is_ok_to_verify_bytes_signatures: Boolean
is_ok_to_verify_messages: Boolean
}
scalar Date
scalar Time
scalar DateTime
`;
export const queryTypes = gql`
type Query {
getNodeSocialInfo(pubkey: String!): LightningNodeSocialInfo!
getLightningAddressInfo(address: String!): PayRequest!
getLightningAddresses: [LightningAddress!]!
getAmbossLoginToken: String!
getAmbossUser: AmbossUserType
getNodeBalances: BalancesType!
getNodeBosHistory(pubkey: String!): NodeBosHistory!
getBosScores: [BosScore!]!
getBaseInfo: BaseInfo!
getBoltzSwapStatus(ids: [String]!): [BoltzSwap]!
getBoltzInfo: BoltzInfoType!
getLnMarketsStatus: String!
getLnMarketsUrl: String!
getLnMarketsUserInfo: LnMarketsUserInfo
getInvoiceStatusChange(id: String!): String
getBaseCanConnect: Boolean!
getBaseNodes: [baseNodesType]!
getBasePoints: [basePointsType]!
getAccountingReport(
category: String
currency: String
fiat: String
month: String
year: String
): String!
getVolumeHealth: channelsHealth
getTimeHealth: channelsTimeHealth
getFeeHealth: channelsFeeHealth
getChannel(id: String!, pubkey: String): singleChannelType!
getChannels(active: Boolean): [channelType]!
getClosedChannels(type: String): [closedChannelType]
getPendingChannels: [pendingChannelType]
getChannelReport: channelReportType
getNetworkInfo: networkInfoType
getNodeInfo: nodeInfoType
adminCheck: Boolean
getNode(publicKey: String!, withoutChannels: Boolean): Node!
decodeRequest(request: String!): decodeType
getWalletInfo: walletInfoType
getResume(offset: Int, limit: Int): getResumeType!
getForwards(days: Int!): [Forward]!
getBitcoinPrice(logger: Boolean, currency: String): String
getBitcoinFees(logger: Boolean): bitcoinFeeType
getForwardChannelsReport(time: String, order: String, type: String): String
getBackups: String
verifyBackups(backup: String!): Boolean
recoverFunds(backup: String!): Boolean
getRoutes(
outgoing: String!
incoming: String!
tokens: Int!
maxFee: Int
): GetRouteType
getPeers: [peerType]
signMessage(message: String!): String
verifyMessage(message: String!, signature: String!): String
getChainTransactions: [getTransactionsType]
getUtxos: [getUtxosType]
getMessages(
token: String
initialize: Boolean
lastMessage: String
): getMessagesType
getServerAccounts: [serverAccountType]
getAccount: serverAccountType
getLatestVersion: String
}
`;
export const mutationTypes = gql`
type Mutation {
loginAmboss: Boolean
getAuthToken(cookie: String): Boolean!
getSessionToken(id: String, password: String): String!
claimBoltzTransaction(
redeem: String!
transaction: String!
preimage: String!
privateKey: String!
destination: String!
fee: Int!
): String!
createBoltzReverseSwap(
amount: Int!
address: String
): CreateBoltzReverseSwapType!
lnMarketsDeposit(amount: Int!): Boolean!
lnMarketsWithdraw(amount: Int!): Boolean!
lnMarketsLogin: AuthResponse!
lnMarketsLogout: Boolean!
lnUrlAuth(url: String!): AuthResponse!
lnUrlPay(callback: String!, amount: Int!, comment: String): PaySuccess!
lnUrlChannel(callback: String!, k1: String!, uri: String!): String!
lnUrlWithdraw(
callback: String!
amount: Int!
k1: String!
description: String
): String!
fetchLnUrl(url: String!): LnUrlRequest
createBaseTokenInvoice: baseInvoiceType
createBaseToken(id: String!): Boolean!
deleteBaseToken: Boolean!
createBaseInvoice(amount: Int!): baseInvoiceType
createThunderPoints(
id: String!
alias: String!
uris: [String!]!
public_key: String!
): Boolean!
closeChannel(
id: String!
forceClose: Boolean
targetConfirmations: Int
tokensPerVByte: Int
): closeChannelType
openChannel(
amount: Int!
partnerPublicKey: String!
tokensPerVByte: Int
isPrivate: Boolean
pushTokens: Int
): openChannelType
updateFees(
transaction_id: String
transaction_vout: Int
base_fee_tokens: Float
fee_rate: Int
cltv_delta: Int
max_htlc_mtokens: String
min_htlc_mtokens: String
): Boolean
updateMultipleFees(channels: [channelDetailInput!]!): Boolean
keysend(destination: String!, tokens: Int!): payType
createInvoice(
amount: Int!
description: String
secondsUntil: Int
includePrivate: Boolean
): newInvoiceType
circularRebalance(route: String!): Boolean
pay(
max_fee: Int!
max_paths: Int!
out: [String]
request: String!
): Boolean
bosPay(
max_fee: Int!
max_paths: Int!
message: String
out: [String]
request: String!
): Boolean
bosRebalance(
avoid: [String]
in_through: String
max_fee: Int
max_fee_rate: Int
max_rebalance: Int
timeout_minutes: Int
node: String
out_through: String
out_inbound: Int
): bosRebalanceResultType
payViaRoute(route: String!, id: String!): Boolean
createAddress(nested: Boolean): String
sendToAddress(
address: String!
tokens: Int
fee: Int
target: Int
sendAll: Boolean
): sendToType
addPeer(
url: String
publicKey: String
socket: String
isTemporary: Boolean
): Boolean
removePeer(publicKey: String!): Boolean
sendMessage(
publicKey: String!
message: String!
messageType: String
tokens: Int
maxFee: Int
): Int
logout: Boolean!
createMacaroon(permissions: permissionsType!): CreateMacaroon!
}
`;

View File

@ -1,24 +0,0 @@
import { getWalletVersion } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { to } from 'server/helpers/async';
import { requestLimiter } from 'server/helpers/rateLimiter';
export const walletResolvers = {
Query: {
getWalletInfo: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'getWalletInfo');
const { lnd } = context;
return await to(
getWalletVersion({
lnd,
})
);
},
},
};

View File

@ -1,15 +0,0 @@
import { gql } from 'apollo-server-micro';
export const walletTypes = gql`
type walletInfoType {
build_tags: [String!]!
commit_hash: String!
is_autopilotrpc_enabled: Boolean!
is_chainrpc_enabled: Boolean!
is_invoicesrpc_enabled: Boolean!
is_signrpc_enabled: Boolean!
is_walletrpc_enabled: Boolean!
is_watchtowerrpc_enabled: Boolean!
is_wtclientrpc_enabled: Boolean!
}
`;

View File

@ -1,7 +0,0 @@
import { getChannelReport } from './resolvers/getChannelReport';
export const widgetResolvers = {
Query: {
getChannelReport,
},
};

View File

@ -1,71 +0,0 @@
import { getChannels } from 'ln-service';
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import { GetChannelsType } from 'server/types/ln-service.types';
export const getChannelReport = async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'channelReport');
const { lnd } = context;
const info = await to<GetChannelsType>(getChannels({ lnd }));
if (!info || info?.channels?.length <= 0) {
return;
}
const { channels } = info;
const pending = channels.reduce(
(prev, current) => {
const { pending_payments } = current;
const total = pending_payments.length;
const outgoing = pending_payments.filter(p => p.is_outgoing).length;
const incoming = total - outgoing;
return {
totalPendingHtlc: prev.totalPendingHtlc + total,
outgoingPendingHtlc: prev.outgoingPendingHtlc + outgoing,
incomingPendingHtlc: prev.incomingPendingHtlc + incoming,
};
},
{
totalPendingHtlc: 0,
outgoingPendingHtlc: 0,
incomingPendingHtlc: 0,
}
);
const commit = channels
.filter(c => !c.is_partner_initiated)
.map(c => c.commit_transaction_fee)
.reduce((total, fee) => total + fee, 0);
const localBalances = channels
.filter(c => c.is_active)
.map(c => c.local_balance);
const remoteBalances = channels
.filter(c => c.is_active)
.map(c => c.remote_balance);
const local = localBalances.reduce((total, size) => total + size, 0) - commit;
const remote = remoteBalances.reduce((total, size) => total + size, 0);
const maxOut = Math.max(...localBalances);
const maxIn = Math.max(...remoteBalances);
return {
local,
remote,
maxIn,
maxOut,
commit,
...pending,
};
};

View File

@ -1,39 +0,0 @@
export interface FinalProps {
fee: number;
tokens: number;
amount: number;
}
export interface FinalList {
[key: string]: FinalProps;
}
export interface CountProps {
[key: string]: number;
}
export interface ChannelCounts {
name: string;
count: number;
}
export interface PaymentProps {
created_at: string;
is_confirmed: boolean;
tokens: number;
}
export interface PaymentsProps {
payments: PaymentProps[];
}
export interface InvoiceProps {
created_at: string;
is_confirmed: boolean;
received: number;
}
export interface InvoicesProps {
invoices: InvoiceProps[];
next: string;
}

View File

@ -1,14 +0,0 @@
import { gql } from 'apollo-server-micro';
export const widgetTypes = gql`
type channelReportType {
local: Int
remote: Int
maxIn: Int
maxOut: Int
commit: Int
totalPendingHtlc: Int
outgoingPendingHtlc: Int
incomingPendingHtlc: Int
}
`;

Some files were not shown because too many files have changed in this diff Show More