mirror of
https://github.com/apotdevin/thunderhub.git
synced 2024-11-19 01:40:03 +01:00
chore: migration
This commit is contained in:
parent
59916b847e
commit
a6ad30b599
@ -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
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
58
.eslintrc.js
58
.eslintrc.js
@ -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
58
.gitignore
vendored
@ -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
|
@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit $1
|
@ -1,5 +0,0 @@
|
||||
/.next
|
||||
/.node_modules
|
||||
|
||||
**/*.generated.tsx
|
||||
src/graphql/types.ts
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
18
codegen.yml
18
codegen.yml
@ -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 */'
|
||||
|
@ -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;
|
||||
}
|
@ -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']
|
@ -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
4
nest-cli.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src/server"
|
||||
}
|
33188
package-lock.json
generated
33188
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
153
package.json
153
package.json
@ -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",
|
||||
|
@ -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
772
schema.gql
Normal 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
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
@ -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');
|
||||
}
|
||||
},
|
||||
};
|
@ -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}',
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
};
|
@ -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);
|
||||
}
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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,
|
||||
});
|
@ -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);
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
import { gql } from 'apollo-server-micro';
|
||||
|
||||
export const accountTypes = gql`
|
||||
type serverAccountType {
|
||||
name: String!
|
||||
id: String!
|
||||
type: String!
|
||||
loggedIn: Boolean!
|
||||
}
|
||||
`;
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -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 {},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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.');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
import { gql } from 'apollo-server-micro';
|
||||
|
||||
export const bitcoinTypes = gql`
|
||||
type bitcoinFeeType {
|
||||
fast: Int
|
||||
halfHour: Int
|
||||
hour: Int
|
||||
minimum: Int
|
||||
}
|
||||
`;
|
@ -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 },
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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 }),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
@ -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!
|
||||
}
|
||||
`;
|
@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
@ -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;
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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),
|
||||
}));
|
||||
};
|
@ -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),
|
||||
}));
|
||||
};
|
@ -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,
|
||||
},
|
||||
}));
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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;
|
||||
};
|
@ -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();
|
||||
},
|
||||
},
|
||||
};
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -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,
|
||||
},
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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 };
|
||||
};
|
@ -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]
|
||||
}
|
||||
`;
|
@ -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 });
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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 {},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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';
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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 };
|
||||
},
|
||||
},
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
import { gql } from 'apollo-server-micro';
|
||||
|
||||
export const macaroonTypes = gql`
|
||||
type CreateMacaroon {
|
||||
base: String!
|
||||
hex: String!
|
||||
}
|
||||
`;
|
@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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 };
|
||||
},
|
||||
},
|
||||
};
|
@ -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!
|
||||
}
|
||||
`;
|
@ -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));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
@ -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!
|
||||
}
|
||||
`;
|
@ -1,7 +0,0 @@
|
||||
import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date';
|
||||
|
||||
export const generalResolvers = {
|
||||
Date: GraphQLDate,
|
||||
Time: GraphQLTime,
|
||||
DateTime: GraphQLDateTime,
|
||||
};
|
@ -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 };
|
||||
},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
`;
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -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!
|
||||
}
|
||||
`;
|
@ -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));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
@ -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);
|
||||
};
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
@ -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]!
|
||||
}
|
||||
`;
|
@ -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!
|
||||
}
|
||||
`;
|
@ -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,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
@ -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!
|
||||
}
|
||||
`;
|
@ -1,7 +0,0 @@
|
||||
import { getChannelReport } from './resolvers/getChannelReport';
|
||||
|
||||
export const widgetResolvers = {
|
||||
Query: {
|
||||
getChannelReport,
|
||||
},
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user