Merge remote-tracking branch 'thunderhub-server/master'

This commit is contained in:
AP 2020-02-26 18:44:38 +01:00
commit 15caa283cf
89 changed files with 9175 additions and 0 deletions

2
.ebignore Normal file
View file

@ -0,0 +1,2 @@
/node_modules
.env

29
.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
#webpack
/dist
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml

2
.npmrc Normal file
View file

@ -0,0 +1,2 @@
# Force npm to run node-gyp also as root, preventing permission denied errors in AWS with npm@5
unsafe-perm=true

7
.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"printWidth": 80,
"trailingComma": "all",
"tabWidth": 4,
"semi": true,
"singleQuote": true
}

4
nodemon.json Normal file
View file

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

56
package.json Normal file
View file

@ -0,0 +1,56 @@
{
"name": "thunderhub",
"version": "0.1.3",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.production.js",
"build:dev": "webpack --config webpack.development.js",
"start": "node dist/server",
"dev": "nodemon dist/server",
"deploy": "yarn build && eb deploy"
},
"repository": {
"type": "git",
"url": "git+https://github.com/apotdevin/thunderhub.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/graphql-depth-limit": "^1.1.2",
"@types/graphql-iso-date": "^3.3.3",
"@types/node-fetch": "^2.5.3",
"@types/underscore": "^1.9.4",
"apollo-server": "^2.10.1",
"base64url": "^3.0.1",
"date-fns": "^2.8.1",
"dotenv": "^8.2.0",
"graphql": "^14.6.0",
"graphql-depth-limit": "^1.1.0",
"graphql-iso-date": "^3.6.1",
"graphql-rate-limit": "^2.0.1",
"ln-service": "^47.14.7",
"underscore": "^1.9.1",
"winston": "^3.2.1"
},
"devDependencies": {
"@types/webpack-env": "^1.15.1",
"clean-webpack-plugin": "^3.0.0",
"husky": "^4.2.3",
"prettier": "^1.18.2",
"pretty-quick": "^2.0.0",
"ts-loader": "^6.2.1",
"typescript": "^3.7.5",
"webpack": "^4.41.6",
"webpack-cli": "^3.3.11",
"webpack-merge": "^4.2.2",
"webpack-node-externals": "^1.7.2"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
}
}

View file

@ -0,0 +1,7 @@
import { GraphQLNonNull, GraphQLBoolean } from 'graphql';
import { AuthType } from '../schemaTypes/Auth';
export const defaultParams = {
auth: { type: new GraphQLNonNull(AuthType) },
logger: { type: GraphQLBoolean },
};

62
src/helpers/helpers.ts Normal file
View file

@ -0,0 +1,62 @@
import base64url from 'base64url';
import { authenticatedLndGrpc } from 'ln-service';
export const getIp = (req: any) => {
if (!req || !req.headers) {
return '';
}
const forwarded = req.headers['x-forwarded-for'];
const before = forwarded
? forwarded.split(/, /)[0]
: req.connection.remoteAddress;
const ip = process.env.NODE_ENV === 'development' ? '1.2.3.4' : before;
return ip;
};
export const getAuthLnd = (auth: {
cert: string;
macaroon: string;
host: string;
}) => {
const encodedCert = auth.cert || '';
const encodedMacaroon = auth.macaroon || '';
const socket = auth.host || '';
const cert = encodedCert;
const macaroon = encodedMacaroon;
const params =
encodedCert !== ''
? {
cert,
macaroon,
socket,
}
: { macaroon, socket };
const { lnd } = authenticatedLndGrpc(params);
return lnd;
};
export const getErrorDetails = (error: any[]): string => {
let details = '';
if (error.length > 2) {
if (error[2].err) {
details = error[2].err.details;
} else if (error[2].details) {
details = error[2].details;
}
}
return details;
};
export const getErrorMsg = (error: any[]): string => {
const code = error[0];
const msg = error[1];
let details = getErrorDetails(error);
return JSON.stringify({ code, msg, details });
};

38
src/helpers/logger.ts Normal file
View file

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

View file

@ -0,0 +1,22 @@
import { getGraphQLRateLimiter } from 'graphql-rate-limit';
import { RateConfig } from '../utils/rateLimitConfig';
const rateLimiter = getGraphQLRateLimiter({
identifyContext: (ctx: string) => ctx,
formatError: () => 'Rate Limit Reached',
});
export const requestLimiter = async (rate: string, field: string) => {
if (!RateConfig[field]) throw new Error('Invalid Rate Field');
const { max, window } = RateConfig[field];
const errorMessage = await rateLimiter(
{
parent: rate,
args: {},
context: rate,
info: { fieldName: field } as any,
},
{ max, window },
);
if (errorMessage) throw new Error(errorMessage);
};

25
src/main.ts Normal file
View file

@ -0,0 +1,25 @@
import { ApolloServer } from 'apollo-server';
import { thunderHubSchema } from './schemas';
import { logger } from './helpers/logger';
import { getIp } from './helpers/helpers';
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema: thunderHubSchema,
context: async ({ req }: any) => {
const ip = getIp(req);
return { ip };
},
validationRules: [
depthLimit(2, { ignore: [/_trusted$/, 'idontcare', 'whatever'] }),
],
});
server.listen({ port: process.env.PORT || 3001 }).then(({ url }: any) => {
logger.info(`Server ready at ${url}`);
});
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => server.stop());
}

18
src/schemaTypes/Auth.ts Normal file
View file

@ -0,0 +1,18 @@
import { GraphQLInputObjectType, GraphQLString } from 'graphql';
export const AuthType = new GraphQLInputObjectType({
name: 'authType',
fields: () => {
return {
host: {
type: GraphQLString,
},
macaroon: {
type: GraphQLString,
},
cert: {
type: GraphQLString,
},
};
},
});

View file

@ -0,0 +1,15 @@
import { GraphQLObjectType, GraphQLString } from "graphql";
export const CloseChannelType = new GraphQLObjectType({
name: "closeChannelType",
fields: () => {
return {
transactionId: {
type: GraphQLString
},
transactionOutputIndex: {
type: GraphQLString
}
};
}
});

View file

@ -0,0 +1,15 @@
import { GraphQLObjectType, GraphQLString } from "graphql";
export const OpenChannelType = new GraphQLObjectType({
name: "openChannelType",
fields: () => {
return {
transactionId: {
type: GraphQLString
},
transactionOutputIndex: {
type: GraphQLString
}
};
}
});

View file

@ -0,0 +1,17 @@
import { GraphQLObjectType, GraphQLString, GraphQLInt } from "graphql";
import { GraphQLDateTime } from "graphql-iso-date";
export const InvoiceType = new GraphQLObjectType({
name: "invoiceType",
fields: () => {
return {
chainAddress: { type: GraphQLString },
createdAt: { type: GraphQLDateTime },
description: { type: GraphQLString },
id: { type: GraphQLString },
request: { type: GraphQLString },
secret: { type: GraphQLString },
tokens: { type: GraphQLInt }
};
}
});

View file

@ -0,0 +1,34 @@
import {
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLList,
} from 'graphql';
const RoutesType = new GraphQLObjectType({
name: 'routeType',
fields: () => ({
baseFeeMTokens: { type: GraphQLString },
channel: { type: GraphQLString },
cltvDelta: { type: GraphQLInt },
feeRate: { type: GraphQLInt },
publicKey: { type: GraphQLString },
}),
});
export const DecodeType = new GraphQLObjectType({
name: 'decodeType',
fields: () => {
return {
chainAddress: { type: GraphQLString },
cltvDelta: { type: GraphQLInt },
description: { type: GraphQLString },
descriptionHash: { type: GraphQLString },
destination: { type: GraphQLString },
expiresAt: { type: GraphQLString },
id: { type: GraphQLString },
routes: { type: new GraphQLList(RoutesType) },
tokens: { type: GraphQLInt },
};
},
});

View file

@ -0,0 +1,40 @@
import {
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLBoolean,
GraphQLList
} from "graphql";
import { GraphQLDateTime } from "graphql-iso-date";
const RouteType = new GraphQLObjectType({
name: "RouteType",
fields: () => ({
mTokenFee: { type: GraphQLString },
channel: { type: GraphQLString },
cltvDelta: { type: GraphQLInt },
feeRate: { type: GraphQLInt },
publicKey: { type: GraphQLString }
})
});
export const ParsePaymentType = new GraphQLObjectType({
name: "parsePaymentType",
fields: () => {
return {
chainAddresses: { type: new GraphQLList(GraphQLString) },
cltvDelta: { type: GraphQLInt },
createdAt: { type: GraphQLDateTime },
description: { type: GraphQLString },
descriptionHash: { type: GraphQLString },
destination: { type: GraphQLString },
expiresAt: { type: GraphQLDateTime },
id: { type: GraphQLString },
isExpired: { type: GraphQLBoolean },
mTokens: { type: GraphQLString },
network: { type: GraphQLString },
routes: { type: new GraphQLList(RouteType) },
tokens: { type: GraphQLInt }
};
}
});

View file

@ -0,0 +1,35 @@
import {
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLList,
GraphQLBoolean
} from "graphql";
const HopsType = new GraphQLObjectType({
name: "hopsType",
fields: () => ({
channel: { type: GraphQLString },
channelCapacity: { type: GraphQLInt },
mTokenFee: { type: GraphQLString },
forwardMTokens: { type: GraphQLString },
timeout: { type: GraphQLInt }
})
});
export const PayType = new GraphQLObjectType({
name: "payType",
fields: () => {
return {
fee: { type: GraphQLInt },
feeMTokens: { type: GraphQLString },
hops: { type: new GraphQLList(HopsType) },
id: { type: GraphQLString },
isConfirmed: { type: GraphQLBoolean },
isOutgoing: { type: GraphQLBoolean },
mtokens: { type: GraphQLString },
secret: { type: GraphQLString },
tokens: { type: GraphQLInt }
};
}
});

View file

@ -0,0 +1,19 @@
import {
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLBoolean,
} from 'graphql';
export const SendToType = new GraphQLObjectType({
name: 'sendToType',
fields: () => {
return {
confirmationCount: { type: GraphQLString },
id: { type: GraphQLString },
isConfirmed: { type: GraphQLBoolean },
isOutgoing: { type: GraphQLBoolean },
tokens: { type: GraphQLInt },
};
},
});

View file

@ -0,0 +1,16 @@
import { GraphQLObjectType } from 'graphql';
import { GraphQLInt } from 'graphql';
export const ChannelBalanceType = new GraphQLObjectType({
name: 'channelBalanceType',
fields: () => {
return {
confirmedBalance: {
type: GraphQLInt,
},
pendingBalance: {
type: GraphQLInt,
},
};
},
});

View file

@ -0,0 +1,27 @@
import { GraphQLObjectType, GraphQLInt, GraphQLString } from 'graphql';
export const ChannelFeeType = new GraphQLObjectType({
name: 'channelFeeType',
fields: () => {
return {
alias: {
type: GraphQLString,
},
color: {
type: GraphQLString,
},
baseFee: {
type: GraphQLInt,
},
feeRate: {
type: GraphQLInt,
},
transactionId: {
type: GraphQLString,
},
transactionVout: {
type: GraphQLInt,
},
};
},
});

View file

@ -0,0 +1,21 @@
import { GraphQLObjectType, GraphQLInt } from 'graphql';
export const ChannelReportType = new GraphQLObjectType({
name: 'channelReportType',
fields: () => {
return {
local: {
type: GraphQLInt,
},
remote: {
type: GraphQLInt,
},
maxIn: {
type: GraphQLInt,
},
maxOut: {
type: GraphQLInt,
},
};
},
});

View file

@ -0,0 +1,46 @@
import { GraphQLObjectType, GraphQLBoolean, GraphQLString } from 'graphql';
import { GraphQLInt } from 'graphql';
export const PartnerNodeType = new GraphQLObjectType({
name: 'partnerNodeType',
fields: () => {
return {
alias: { type: GraphQLString },
capacity: { type: GraphQLString },
channel_count: { type: GraphQLInt },
color: { type: GraphQLString },
updated_at: { type: GraphQLString },
};
},
});
export const ChannelType = new GraphQLObjectType({
name: 'channelType',
fields: () => {
return {
capacity: { type: GraphQLInt },
commit_transaction_fee: { type: GraphQLInt },
commit_transaction_weight: { type: GraphQLInt },
id: { type: GraphQLString },
is_active: { type: GraphQLBoolean },
is_closing: { type: GraphQLBoolean },
is_opening: { type: GraphQLBoolean },
is_partner_initiated: { type: GraphQLBoolean },
is_private: { type: GraphQLBoolean },
is_static_remote_key: { type: GraphQLBoolean },
local_balance: { type: GraphQLInt },
local_reserve: { type: GraphQLInt },
partner_public_key: { type: GraphQLString },
received: { type: GraphQLInt },
remote_balance: { type: GraphQLInt },
remote_reserve: { type: GraphQLInt },
sent: { type: GraphQLInt },
time_offline: { type: GraphQLInt },
time_online: { type: GraphQLInt },
transaction_id: { type: GraphQLString },
transaction_vout: { type: GraphQLInt },
unsettled_balance: { type: GraphQLInt },
partner_node_info: { type: PartnerNodeType },
};
},
});

View file

@ -0,0 +1,26 @@
import { GraphQLObjectType, GraphQLBoolean, GraphQLString } from 'graphql';
import { GraphQLInt } from 'graphql';
import { PartnerNodeType } from './channels';
export const ClosedChannelType = new GraphQLObjectType({
name: 'closedChannelType',
fields: () => {
return {
capacity: { type: GraphQLInt },
close_confirm_height: { type: GraphQLInt },
close_transaction_id: { type: GraphQLString },
final_local_balance: { type: GraphQLInt },
final_time_locked_balance: { type: GraphQLInt },
id: { type: GraphQLString },
is_breach_close: { type: GraphQLBoolean },
is_cooperative_close: { type: GraphQLBoolean },
is_funding_cancel: { type: GraphQLBoolean },
is_local_force_close: { type: GraphQLBoolean },
is_remote_force_close: { type: GraphQLBoolean },
partner_public_key: { type: GraphQLString },
transaction_id: { type: GraphQLString },
transaction_vout: { type: GraphQLInt },
partner_node_info: { type: PartnerNodeType },
};
},
});

View file

@ -0,0 +1,27 @@
import { GraphQLObjectType, GraphQLBoolean, GraphQLString } from 'graphql';
import { GraphQLInt } from 'graphql';
import { PartnerNodeType } from './channels';
// TODO: INCOMPLETE TYPE
export const PendingChannelType = new GraphQLObjectType({
name: 'pendingChannelType',
fields: () => {
return {
close_transaction_id: { type: GraphQLString },
is_active: { type: GraphQLBoolean },
is_closing: { type: GraphQLBoolean },
is_opening: { type: GraphQLBoolean },
local_balance: { type: GraphQLInt },
local_reserve: { type: GraphQLInt },
partner_public_key: { type: GraphQLString },
received: { type: GraphQLInt },
remote_balance: { type: GraphQLInt },
remote_reserve: { type: GraphQLInt },
sent: { type: GraphQLInt },
transaction_fee: { type: GraphQLInt },
transaction_id: { type: GraphQLString },
transaction_vout: { type: GraphQLInt },
partner_node_info: { type: PartnerNodeType },
};
},
});

View file

@ -0,0 +1,18 @@
import { GraphQLObjectType, GraphQLInt } from 'graphql';
export const BitcoinFeeType = new GraphQLObjectType({
name: 'bitcoinFeeType',
fields: () => {
return {
fast: {
type: GraphQLInt,
},
halfHour: {
type: GraphQLInt,
},
hour: {
type: GraphQLInt,
},
};
},
});

View file

@ -0,0 +1,13 @@
import { GraphQLObjectType, GraphQLString, GraphQLInt } from 'graphql';
export const InOutType = new GraphQLObjectType({
name: 'InOutType',
fields: () => {
return {
invoices: { type: GraphQLString },
payments: { type: GraphQLString },
confirmedInvoices: { type: GraphQLInt },
unConfirmedInvoices: { type: GraphQLInt },
};
},
});

View file

@ -0,0 +1,34 @@
import { GraphQLObjectType, GraphQLString } from "graphql";
import { GraphQLInt } from "graphql";
export const NetworkInfoType = new GraphQLObjectType({
name: "networkInfoType",
fields: () => {
return {
averageChannelSize: {
type: GraphQLString
},
channelCount: {
type: GraphQLInt
},
maxChannelSize: {
type: GraphQLString
},
medianChannelSize: {
type: GraphQLString
},
minChannelSize: {
type: GraphQLInt
},
nodeCount: {
type: GraphQLInt
},
notRecentlyUpdatedPolicyCount: {
type: GraphQLInt
},
totalCapacity: {
type: GraphQLString
}
};
}
});

View file

@ -0,0 +1,30 @@
import {
GraphQLObjectType,
GraphQLBoolean,
GraphQLString,
GraphQLList,
} from 'graphql';
import { GraphQLInt } from 'graphql';
export const NodeInfoType = new GraphQLObjectType({
name: 'nodeInfoType',
fields: () => {
return {
chains: { type: new GraphQLList(GraphQLString) },
color: { type: GraphQLString },
active_channels_count: { type: GraphQLInt },
closed_channels_count: { type: GraphQLInt },
alias: { type: GraphQLString },
current_block_hash: { type: GraphQLString },
current_block_height: { type: GraphQLBoolean },
is_synced_to_chain: { type: GraphQLBoolean },
is_synced_to_graph: { type: GraphQLBoolean },
latest_block_at: { type: GraphQLString },
peers_count: { type: GraphQLInt },
pending_channels_count: { type: GraphQLInt },
public_key: { type: GraphQLString },
uris: { type: new GraphQLList(GraphQLString) },
version: { type: GraphQLString },
};
},
});

View file

@ -0,0 +1,16 @@
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql';
import { GraphQLInt } from 'graphql';
export const GetChainTransactionsType = new GraphQLObjectType({
name: 'getTransactionsType',
fields: () => ({
block_id: { type: GraphQLString },
confirmation_count: { type: GraphQLInt },
confirmation_height: { type: GraphQLInt },
created_at: { type: GraphQLString },
fee: { type: GraphQLInt },
id: { type: GraphQLString },
output_addresses: { type: new GraphQLList(GraphQLString) },
tokens: { type: GraphQLInt },
}),
});

View file

@ -0,0 +1,27 @@
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql';
import { GraphQLInt } from 'graphql';
export const ForwardType = new GraphQLObjectType({
name: 'forwardType',
fields: () => ({
created_at: { type: GraphQLString },
fee: { type: GraphQLInt },
fee_mtokens: { type: GraphQLString },
incoming_channel: { type: GraphQLString },
incoming_alias: { type: GraphQLString },
incoming_color: { type: GraphQLString },
mtokens: { type: GraphQLString },
outgoing_channel: { type: GraphQLString },
outgoing_alias: { type: GraphQLString },
outgoing_color: { type: GraphQLString },
tokens: { type: GraphQLInt },
}),
});
export const GetForwardType = new GraphQLObjectType({
name: 'getForwardType',
fields: () => ({
token: { type: GraphQLString },
forwards: { type: new GraphQLList(ForwardType) },
}),
});

View file

@ -0,0 +1,9 @@
import { GraphQLObjectType, GraphQLString } from 'graphql';
export const GetResumeType = new GraphQLObjectType({
name: 'getResumeType',
fields: () => ({
token: { type: GraphQLString },
resume: { type: GraphQLString },
}),
});

14
src/schemas/index.ts Normal file
View file

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

View file

@ -0,0 +1,49 @@
import { closeChannel as lnCloseChannel } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import {
GraphQLBoolean,
GraphQLString,
GraphQLInt,
GraphQLNonNull,
} from 'graphql';
import { CloseChannelType } from '../../../schemaTypes/mutation.ts/channels/closeChannel';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface CloseChannelProps {
transaction_id: string;
transaction_vout: string;
}
export const closeChannel = {
type: CloseChannelType,
args: {
...defaultParams,
id: { type: new GraphQLNonNull(GraphQLString) },
forceClose: { type: GraphQLBoolean },
targetConfirmations: { type: GraphQLInt },
tokensPerVByte: { type: GraphQLInt },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'closeChannel');
const lnd = getAuthLnd(params.auth);
try {
const info: CloseChannelProps = await lnCloseChannel({
lnd,
id: params.id,
target_confirmations: params.targetConfirmations,
tokens_per_vbyte: params.tokensPerVByte,
});
return {
transactionId: info.transaction_id,
transactionOutputIndex: info.transaction_vout,
};
} catch (error) {
params.logger && logger.error('Error closing channel: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,9 @@
import { closeChannel } from './closeChannel';
import { openChannel } from './openChannel';
import { updateFees } from './updateFees';
export const channels = {
closeChannel,
openChannel,
updateFees,
};

View file

@ -0,0 +1,50 @@
import { openChannel as lnOpenChannel } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import {
GraphQLBoolean,
GraphQLString,
GraphQLInt,
GraphQLNonNull,
} from 'graphql';
import { OpenChannelType } from '../../../schemaTypes/mutation.ts/channels/openChannel';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface OpenChannelProps {
transaction_id: string;
transaction_vout: string;
}
export const openChannel = {
type: OpenChannelType,
args: {
...defaultParams,
amount: { type: new GraphQLNonNull(GraphQLInt) },
partnerPublicKey: { type: new GraphQLNonNull(GraphQLString) },
tokensPerVByte: { type: GraphQLInt },
isPrivate: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'openChannel');
const lnd = getAuthLnd(params.auth);
try {
const info: OpenChannelProps = await lnOpenChannel({
lnd,
is_private: params.isPrivate,
local_tokens: params.amount,
partner_public_key: params.partnerPublicKey,
chain_fee_tokens_per_vbyte: params.tokensPerVByte,
});
return {
transactionId: info.transaction_id,
transactionOutputIndex: info.transaction_vout,
};
} catch (error) {
params.logger && logger.error('Error opening channel: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,51 @@
import { updateRoutingFees } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLBoolean, GraphQLString, GraphQLInt } from 'graphql';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const updateFees = {
type: GraphQLBoolean,
args: {
...defaultParams,
transactionId: { type: GraphQLString },
transactionVout: { type: GraphQLInt },
baseFee: { type: GraphQLInt },
feeRate: { type: GraphQLInt },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'updateFees');
const {
auth,
transactionId,
transactionVout,
baseFee,
feeRate,
} = params;
const lnd = getAuthLnd(auth);
if (!baseFee && !feeRate) {
throw new Error('No Base Fee or Fee Rate to update channels');
}
const props = {
lnd,
transaction_id: transactionId,
transaction_vout: transactionVout,
...(params.baseFee && { base_fee_tokens: params.baseFee }),
...(params.feeRate && { fee_rate: params.feeRate }),
};
try {
await updateRoutingFees(props);
return true;
} catch (error) {
params.logger &&
logger.error('Error updating routing fees: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,5 @@
import { channels } from './channels';
import { invoices } from './invoices';
import { onChain } from './onchain';
export const mutation = { ...channels, ...invoices, ...onChain };

View file

@ -0,0 +1,50 @@
import { createInvoice as createInvoiceRequest } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLNonNull, GraphQLInt } from 'graphql';
import { InvoiceType } from '../../../schemaTypes/mutation.ts/invoice/createInvoice';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface InvoiceProps {
chain_address: string;
created_at: string;
description: string;
id: string;
request: string;
secret: string;
tokens: number;
}
export const createInvoice = {
type: InvoiceType,
args: {
...defaultParams,
amount: { type: new GraphQLNonNull(GraphQLInt) },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'createInvoice');
const lnd = getAuthLnd(params.auth);
try {
const invoice: InvoiceProps = await createInvoiceRequest({
lnd,
tokens: params.amount,
});
return {
chainAddress: invoice.chain_address,
createdAt: invoice.created_at,
description: invoice.description,
id: invoice.id,
request: invoice.request,
secret: invoice.secret,
tokens: invoice.tokens,
};
} catch (error) {
params.logger && logger.error('Error creating invoice: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,92 @@
import { decodePaymentRequest } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLString, GraphQLNonNull } from 'graphql';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { DecodeType } from '../../../schemaTypes/mutation.ts/invoice/decode';
import { defaultParams } from '../../../helpers/defaultProps';
interface RouteProps {
base_fee_mtokens: string;
channel: string;
cltv_delta: number;
fee_rate: number;
public_key: string;
}
interface DecodeProps {
chain_address: string;
cltv_delta: number;
description: string;
description_hash: string;
destination: string;
expires_at: string;
id: string;
routes: RouteProps[][];
tokens: number;
}
export const decodeRequest = {
type: DecodeType,
args: {
...defaultParams,
request: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'decode');
const lnd = getAuthLnd(params.auth);
try {
const decode: DecodeProps = await decodePaymentRequest({
lnd,
request: params.request,
});
const routes = decode.routes.map(route => {
route.map(nodeChannel => {
const {
base_fee_mtokens,
channel,
cltv_delta,
fee_rate,
public_key,
} = nodeChannel;
return {
baseFeeTokens: base_fee_mtokens,
channel,
cltvDelta: cltv_delta,
feeRate: fee_rate,
publicKey: public_key,
};
});
});
const {
chain_address,
cltv_delta,
description,
description_hash,
destination,
expires_at,
id,
tokens,
} = decode;
return {
chainAddress: chain_address,
cltvDelta: cltv_delta,
description,
descriptionHash: description_hash,
destination,
expiresAt: expires_at,
id,
routes,
tokens,
};
} catch (error) {
params.logger && logger.error('Error paying request: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,13 @@
import { parsePayment } from './parsePayment';
import { pay } from './pay';
import { createInvoice } from './createInvoice';
import { decodeRequest } from './decode';
import { payViaRoute } from './payViaRoute';
export const invoices = {
parsePayment,
pay,
createInvoice,
decodeRequest,
payViaRoute,
};

View file

@ -0,0 +1,80 @@
import { parsePaymentRequest } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLString, GraphQLNonNull } from 'graphql';
import { ParsePaymentType } from '../../../schemaTypes/mutation.ts/invoice/parsePayment';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface RouteProps {
base_fee_mtokens: string;
channel: string;
cltv_delta: number;
fee_rate: number;
public_key: string;
}
interface RequestProps {
chain_addresses: string[];
cltv_delta: number;
created_at: string;
description: string;
description_hash: string;
destination: string;
expires_at: string;
id: string;
is_expired: string;
mtokens: string;
network: string;
routes: RouteProps[];
tokens: number;
}
export const parsePayment = {
type: ParsePaymentType,
args: {
...defaultParams,
request: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'parsePayment');
const lnd = getAuthLnd(params.auth);
try {
const request: RequestProps = await parsePaymentRequest({
lnd,
request: params.request,
});
const routes = request.routes.map(route => {
return {
mTokenFee: route.base_fee_mtokens,
channel: route.channel,
cltvDelta: route.cltv_delta,
feeRate: route.fee_rate,
publicKey: route.public_key,
};
});
return {
chainAddresses: request.chain_addresses,
cltvDelta: request.cltv_delta,
createdAt: request.created_at,
description: request.description,
descriptionHash: request.description_hash,
destination: request.destination,
expiresAt: request.expires_at,
id: request.id,
isExpired: request.is_expired,
mTokens: request.mtokens,
network: request.network,
routes: routes,
tokens: request.tokens,
};
} catch (error) {
params.logger && logger.error('Error decoding request: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,73 @@
import { pay as payRequest } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLString, GraphQLNonNull } from 'graphql';
import { PayType } from '../../../schemaTypes/mutation.ts/invoice/pay';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface HopProps {
channel: string;
channel_capacity: number;
fee_mtokens: string;
forward_mtokens: string;
timeout: number;
}
interface RequestProps {
fee: number;
fee_mtokens: string;
hops: HopProps[];
id: string;
is_confirmed: boolean;
is_outgoing: boolean;
mtokens: string;
secret: string;
tokens: number;
}
// TODO: Allow path payments as well
export const pay = {
type: PayType,
args: {
...defaultParams,
request: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'pay');
const lnd = getAuthLnd(params.auth);
try {
const payment: RequestProps = await payRequest({
lnd,
request: params.request,
});
const hops = payment.hops.map(hop => {
return {
channel: hop.channel,
channelCapacity: hop.channel_capacity,
mTokenFee: hop.fee_mtokens,
forwardMTokens: hop.forward_mtokens,
timeout: hop.timeout,
};
});
return {
fee: payment.fee,
feeMTokens: payment.fee_mtokens,
hops: hops,
id: payment.id,
isConfirmed: payment.is_confirmed,
isOutgoing: payment.is_outgoing,
mtokens: payment.mtokens,
secret: payment.secret,
tokens: payment.tokens,
};
} catch (error) {
params.logger && logger.error('Error paying request: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,43 @@
import { GraphQLNonNull, GraphQLBoolean, GraphQLString } from 'graphql';
import { payViaRoutes, createInvoice } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const payViaRoute = {
type: GraphQLBoolean,
args: {
...defaultParams,
route: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'payViaRoute');
const lnd = getAuthLnd(params.auth);
let route;
try {
route = JSON.parse(params.route);
} catch (error) {
params.logger && logger.error('Corrupt route json: %o', error);
throw new Error('Corrupt Route JSON');
}
const { id } = await createInvoice({
lnd,
tokens: params.tokens,
description: 'Balancing Channel',
}).catch((error: any) => {
params.logger && logger.error('Error getting invoice: %o', error);
throw new Error(getErrorMsg(error));
});
await payViaRoutes({ lnd, routes: [route], id }).catch((error: any) => {
params.logger && logger.error('Error making payment: %o', error);
throw new Error(getErrorMsg(error));
});
return true;
},
};

View file

@ -0,0 +1,38 @@
import { createChainAddress } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLString, GraphQLBoolean } from 'graphql';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface AddressProps {
address: string;
}
export const createAddress = {
type: GraphQLString,
args: {
...defaultParams,
nested: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'getAddress');
const lnd = getAuthLnd(params.auth);
const format = params.nested ? 'np2wpkh' : 'p2wpkh';
try {
const address: AddressProps = await createChainAddress({
lnd,
is_unused: true,
format,
});
return address.address;
} catch (error) {
params.logger && logger.error('Error creating address: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,7 @@
import { createAddress } from './getAddress';
import { sendToAddress } from './sendToAddress';
export const onChain = {
createAddress,
sendToAddress,
};

View file

@ -0,0 +1,66 @@
import { sendToChainAddress } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import {
GraphQLNonNull,
GraphQLString,
GraphQLBoolean,
GraphQLInt,
} from 'graphql';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { SendToType } from '../../../schemaTypes/mutation.ts/onchain/sentToAddress';
import { defaultParams } from '../../../helpers/defaultProps';
interface SendProps {
confirmation_count: number;
id: string;
is_confirmed: boolean;
is_outgoing: boolean;
tokens: number;
}
export const sendToAddress = {
type: SendToType,
args: {
...defaultParams,
address: { type: new GraphQLNonNull(GraphQLString) },
tokens: { type: GraphQLInt },
fee: { type: GraphQLInt },
target: { type: GraphQLInt },
sendAll: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'sendToAddress');
const lnd = getAuthLnd(params.auth);
const props = params.fee
? { fee_tokens_per_vbyte: params.fee }
: params.target
? { target_confirmations: params.target }
: {};
const sendAll = params.sendAll ? { is_send_all: true } : {};
try {
const send: SendProps = await sendToChainAddress({
lnd,
address: params.address,
tokens: params.tokens,
...props,
...sendAll,
});
return {
confirmationCount: send.confirmation_count,
id: send.id,
isConfirmed: send.is_confirmed,
isOutgoing: send.is_outgoing,
tokens: send.tokens,
};
} catch (error) {
params.logger && logger.error('Error creating address: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,26 @@
import { getBackups as getLnBackups } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLString } from 'graphql';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const getBackups = {
type: GraphQLString,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'getBackups');
const lnd = getAuthLnd(params.auth);
try {
const backups = await getLnBackups({
lnd,
});
return JSON.stringify(backups);
} catch (error) {
params.logger && logger.error('Error getting backups: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,9 @@
import { getBackups } from './getBackups';
import { verifyBackups } from './verifyBackups';
import { recoverFunds } from './recoverFunds';
export const backupQueries = {
getBackups,
verifyBackups,
recoverFunds,
};

View file

@ -0,0 +1,45 @@
import { recoverFundsFromChannels } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLNonNull, GraphQLString, GraphQLBoolean } from 'graphql';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface BackupProps {
backup: string;
}
export const recoverFunds = {
type: GraphQLBoolean,
args: {
...defaultParams,
backup: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'recoverFunds');
const lnd = getAuthLnd(params.auth);
let backupObj: BackupProps = { backup: '' };
try {
backupObj = JSON.parse(params.backup);
} catch (error) {
params.logger && 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) {
params.logger &&
logger.error('Error recovering funds from channels: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,46 @@
import { verifyBackups as verifyLnBackups } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLNonNull, GraphQLString, GraphQLBoolean } from 'graphql';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface BackupProps {
backup: string;
channels: {}[];
}
export const verifyBackups = {
type: GraphQLBoolean,
args: {
...defaultParams,
backup: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'verifyBackups');
const lnd = getAuthLnd(params.auth);
let backupObj: BackupProps = { backup: '', channels: [] };
try {
backupObj = JSON.parse(params.backup);
} catch (error) {
params.logger && 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) {
params.logger && logger.error('Error verifying backups: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,37 @@
import { getChannelBalance as getLnChannelBalance } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { ChannelBalanceType } from '../../../schemaTypes/query/channels/channelBalance';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface ChannelBalanceProps {
channel_balance: number;
pending_balance: number;
}
export const getChannelBalance = {
type: ChannelBalanceType,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'channelBalance');
const lnd = getAuthLnd(params.auth);
try {
const channelBalance: ChannelBalanceProps = await getLnChannelBalance(
{
lnd,
},
);
return {
confirmedBalance: channelBalance.channel_balance,
pendingBalance: channelBalance.pending_balance,
};
} catch (error) {
params.logger &&
logger.error('Error getting channel balance: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,89 @@
import { getFeeRates, getChannels, getNode } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLList } from 'graphql';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { ChannelFeeType } from '../../../schemaTypes/query/channels/channelFees';
import { defaultParams } from '../../../helpers/defaultProps';
interface GetChannelsProps {
channels: ChannelsProps[];
}
interface GetFeeRatesProps {
channels: ChannelFeesProps[];
}
interface ChannelsProps {
partner_public_key: number;
transaction_id: string;
}
interface ChannelFeesProps {
base_fee: number;
fee_rate: number;
transaction_id: string;
transaction_vout: number;
}
interface NodeProps {
alias: string;
color: string;
}
export const getChannelFees = {
type: new GraphQLList(ChannelFeeType),
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'channelFees');
const lnd = getAuthLnd(params.auth);
try {
const channels: GetChannelsProps = await getChannels({ lnd });
const channelFees: GetFeeRatesProps = await getFeeRates({ lnd });
const getConsolidated = () =>
Promise.all(
channels.channels.map(async channel => {
const nodeInfo: NodeProps = await getNode({
lnd,
is_omitting_channels: true,
public_key: channel.partner_public_key,
});
const fees = channelFees.channels.find(
channelFee =>
channelFee.transaction_id ===
channel.transaction_id,
);
if (!fees) return;
const {
base_fee,
fee_rate,
transaction_id,
transaction_vout,
} = fees;
return {
alias: nodeInfo.alias,
color: nodeInfo.color,
baseFee: base_fee,
feeRate: fee_rate,
transactionId: transaction_id,
transactionVout: transaction_vout,
};
}),
);
const consolidated = await getConsolidated();
return consolidated;
} catch (error) {
params.logger &&
logger.error('Error getting channel fees: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,65 @@
import { getChannels } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { ChannelReportType } from '../../../schemaTypes/query/channels/channelReport';
import { defaultParams } from '../../../helpers/defaultProps';
interface GetChannelsProps {
channels: ChannelsProps[];
}
interface ChannelsProps {
remote_balance: number;
local_balance: number;
}
export const getChannelReport = {
type: ChannelReportType,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'channelReport');
const lnd = getAuthLnd(params.auth);
try {
const channels: GetChannelsProps = await getChannels({ lnd });
if (channels.channels.length <= 0) {
return;
}
const maxOutgoing = Math.max.apply(
Math,
channels.channels.map(o => {
return o.local_balance;
}),
);
const maxIncoming = Math.max.apply(
Math,
channels.channels.map(o => {
return o.remote_balance;
}),
);
const consolidated = channels.channels.reduce((p, c) => {
return {
remote_balance: p.remote_balance + c.remote_balance,
local_balance: p.local_balance + c.local_balance,
};
});
return {
local: consolidated.local_balance,
remote: consolidated.remote_balance,
maxIn: maxIncoming,
maxOut: maxOutgoing,
};
} catch (error) {
params.logger &&
logger.error('Error getting channel report: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,81 @@
import { GraphQLList, GraphQLBoolean } from 'graphql';
import { getChannels as getLnChannels, getNode } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { ChannelType } from '../../../schemaTypes/query/channels/channels';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface ChannelListProps {
channels: ChannelProps[];
}
interface ChannelProps {
capacity: number;
commit_transaction_fee: number;
commit_transaction_weight: number;
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: number;
local_reserve: number;
partner_public_key: string;
pending_payments: [];
received: number;
remote_balance: number;
remote_reserve: number;
sent: number;
time_offline: number;
time_online: number;
transaction_id: string;
transaction_vout: number;
unsettled_balance: number;
}
export const getChannels = {
type: new GraphQLList(ChannelType),
args: {
...defaultParams,
active: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'channels');
const lnd = getAuthLnd(params.auth);
try {
const channelList: ChannelListProps = await getLnChannels({
lnd,
is_active: params.active,
});
const getChannelList = () =>
Promise.all(
channelList.channels.map(async channel => {
const nodeInfo = await getNode({
lnd,
is_omitting_channels: true,
public_key: channel.partner_public_key,
});
return {
...channel,
partner_node_info: {
...nodeInfo,
},
};
}),
);
const channels = await getChannelList();
return channels;
} catch (error) {
params.logger && logger.error('Error getting channels: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,67 @@
import { GraphQLList, GraphQLString } from 'graphql';
import { getClosedChannels as getLnClosedChannels, getNode } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { ClosedChannelType } from '../../../schemaTypes/query/channels/closedChannels';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
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 = {
type: new GraphQLList(ClosedChannelType),
args: {
...defaultParams,
type: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'closedChannels');
const lnd = getAuthLnd(params.auth);
try {
const closedChannels: ChannelListProps = await getLnClosedChannels({
lnd,
});
const channels = closedChannels.channels.map(async channel => {
const nodeInfo = await getNode({
lnd,
is_omitting_channels: true,
public_key: channel.partner_public_key,
});
return {
...channel,
partner_node_info: {
...nodeInfo,
},
};
});
return channels;
} catch (error) {
params.logger &&
logger.error('Error getting closed channels: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,15 @@
import { getChannelBalance } from './channelBalance';
import { getChannels } from './channels';
import { getClosedChannels } from './closedChannels';
import { getPendingChannels } from './pendingChannels';
import { getChannelFees } from './channelFees';
import { getChannelReport } from './channelReport';
export const channelQueries = {
getChannelBalance,
getChannels,
getClosedChannels,
getPendingChannels,
getChannelFees,
getChannelReport,
};

View file

@ -0,0 +1,71 @@
import {
getPendingChannels as getLnPendingChannels,
getNode,
} from 'ln-service';
import { logger } from '../../../helpers/logger';
import { PendingChannelType } from '../../../schemaTypes/query/channels/pendingChannels';
import { GraphQLList } from 'graphql';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface PendingChannelListProps {
pending_channels: PendingChannelProps[];
}
interface PendingChannelProps {
close_transaction_id: string;
is_active: boolean;
is_closing: boolean;
is_opening: boolean;
local_balance: number;
local_reserve: number;
partner_public_key: string;
received: number;
remote_balance: number;
remote_reserve: number;
sent: number;
transaction_fee: number;
transaction_id: string;
transaction_vout: number;
}
export const getPendingChannels = {
type: new GraphQLList(PendingChannelType),
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'pendingChannels');
const lnd = getAuthLnd(params.auth);
try {
const pendingChannels: PendingChannelListProps = await getLnPendingChannels(
{
lnd,
},
);
const channels = pendingChannels.pending_channels.map(
async channel => {
const nodeInfo = await getNode({
lnd,
is_omitting_channels: true,
public_key: channel.partner_public_key,
});
return {
...channel,
partner_node_info: {
...nodeInfo,
},
};
},
);
return channels;
} catch (error) {
params.logger &&
logger.error('Error getting pending channels: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,37 @@
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { BitcoinFeeType } from '../../../schemaTypes/query/data/bitcoinFee';
import { GraphQLBoolean } from 'graphql';
import fetch from 'node-fetch';
const url = 'https://bitcoinfees.earn.com/api/v1/fees/recommended';
export const getBitcoinFees = {
type: BitcoinFeeType,
args: {
logger: { type: GraphQLBoolean },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'bitcoinFee');
try {
const response = await fetch(url);
const json = await response.json();
if (json) {
const { fastestFee, halfHourFee, hourFee } = json;
return {
fast: fastestFee,
halfHour: halfHourFee,
hour: hourFee,
};
} else {
throw new Error('Problem getting Bitcoin fees.');
}
} catch (error) {
params.logger &&
logger.error('Error getting bitcoin fees: %o', error);
throw new Error('Problem getting Bitcoin fees.');
}
},
};

View file

@ -0,0 +1,30 @@
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLString, GraphQLBoolean } from 'graphql';
import fetch from 'node-fetch';
const url = 'https://blockchain.info/ticker';
export const getBitcoinPrice = {
type: GraphQLString,
args: {
logger: { type: GraphQLBoolean },
currency: {
type: GraphQLString,
},
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'bitcoinPrice');
try {
const response = await fetch(url);
const json = await response.json();
return JSON.stringify(json);
} catch (error) {
params.logger &&
logger.error('Error getting bitcoin price: %o', error);
throw new Error('Problem getting Bitcoin price.');
}
},
};

View file

@ -0,0 +1,7 @@
import { getBitcoinPrice } from './bitcoinPrice';
import { getBitcoinFees } from './bitcoinFee';
export const dataQueries = {
getBitcoinPrice,
getBitcoinFees,
};

View file

@ -0,0 +1,28 @@
export interface InOutProps {
tokens: number;
}
export interface InOutListProps {
[key: string]: InOutProps[];
}
export interface PaymentProps {
created_at: string;
is_confirmed: boolean;
tokens: number;
}
export interface PaymentsProps {
payments: PaymentProps[];
}
export interface InvoiceProps {
created_at: string;
is_confirmed: boolean;
received: number;
}
export interface InvoicesProps {
invoices: InvoiceProps[];
next: string;
}

View file

@ -0,0 +1,99 @@
import { GraphQLString } from 'graphql';
import {
getInvoices as getLnInvoices,
getPayments as getLnPayments,
} from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { differenceInHours, differenceInCalendarDays } from 'date-fns';
import { groupBy } from 'underscore';
import { reduceInOutArray } from '../report/Helpers';
import { InOutType } from '../../../schemaTypes/query/flow/InOut';
import { InvoicesProps, PaymentsProps } from './getInOut.interface';
import { defaultParams } from '../../../helpers/defaultProps';
export const getInOut = {
type: InOutType,
args: {
...defaultParams,
time: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'getInOut');
const lnd = getAuthLnd(params.auth);
const endDate = new Date();
let periods = 7;
let differenceFn = differenceInCalendarDays;
if (params.time === 'month') {
periods = 30;
} else if (params.time === 'day') {
periods = 24;
differenceFn = differenceInHours;
}
let invoiceList: InvoicesProps;
let paymentList: PaymentsProps;
try {
invoiceList = await getLnInvoices({
lnd,
});
paymentList = await getLnPayments({
lnd,
});
} catch (error) {
params.logger && logger.error('Error getting invoices: %o', error);
throw new Error(getErrorMsg(error));
}
const invoices = invoiceList.invoices.map(invoice => ({
createdAt: invoice.created_at,
isConfirmed: invoice.is_confirmed,
tokens: invoice.received,
}));
const payments = paymentList.payments.map(payment => ({
createdAt: payment.created_at,
isConfirmed: payment.is_confirmed,
tokens: payment.tokens,
}));
const confirmedInvoices = invoices.filter(invoice => {
const dif = differenceFn(endDate, new Date(invoice.createdAt));
return invoice.isConfirmed && dif < periods;
});
const confirmedPayments = payments.filter(payment => {
const dif = differenceFn(endDate, new Date(payment.createdAt));
return payment.isConfirmed && dif < periods;
});
const allInvoices = invoices.filter(invoice => {
const dif = differenceFn(endDate, new Date(invoice.createdAt));
return dif < periods;
});
const totalConfirmed = confirmedInvoices.length;
const totalUnConfirmed = allInvoices.length - totalConfirmed;
const orderedInvoices = groupBy(confirmedInvoices, invoice => {
return periods - differenceFn(endDate, new Date(invoice.createdAt));
});
const orderedPayments = groupBy(confirmedPayments, payment => {
return periods - differenceFn(endDate, new Date(payment.createdAt));
});
const reducedInvoices = reduceInOutArray(orderedInvoices);
const reducedPayments = reduceInOutArray(orderedPayments);
return {
invoices: JSON.stringify(reducedInvoices),
payments: JSON.stringify(reducedPayments),
confirmedInvoices: totalConfirmed,
unConfirmedInvoices: totalUnConfirmed,
};
},
};

View file

@ -0,0 +1,5 @@
import { getInOut } from './getInOut';
export const flowQueries = {
getInOut,
};

View file

@ -0,0 +1,29 @@
import { pay as payRequest } from 'ln-service';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLBoolean } from 'graphql';
import { getAuthLnd, getErrorDetails } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const adminCheck = {
type: GraphQLBoolean,
args: {
...defaultParams,
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'adminCheck');
const lnd = getAuthLnd(params.auth);
try {
await payRequest({
lnd,
request: 'admin check',
});
} catch (error) {
const details = getErrorDetails(error);
if (details.includes('invalid character in string')) return true;
throw new Error();
}
},
};

View file

@ -0,0 +1,59 @@
import {
getChainBalance as getBalance,
getPendingChainBalance as getPending,
} from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GraphQLInt } from 'graphql';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface ChainBalanceProps {
chain_balance: number;
}
interface PendingChainBalanceProps {
pending_chain_balance: number;
}
export const getChainBalance = {
type: GraphQLInt,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'chainBalance');
const lnd = getAuthLnd(params.auth);
try {
const value: ChainBalanceProps = await getBalance({
lnd,
});
return value.chain_balance;
} catch (error) {
params.logger &&
logger.error('Error getting chain balance: %o', error);
throw new Error(getErrorMsg(error));
}
},
};
export const getPendingChainBalance = {
type: GraphQLInt,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'pendingChainBalance');
const lnd = getAuthLnd(params.auth);
try {
const pendingValue: PendingChainBalanceProps = await getPending({
lnd,
});
return pendingValue.pending_chain_balance;
} catch (error) {
params.logger &&
logger.error('Error getting pending chain balance: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,12 @@
import { getChainBalance, getPendingChainBalance } from './chainBalance';
import { getNetworkInfo } from './networkInfo';
import { getNodeInfo } from './nodeInfo';
import { adminCheck } from './adminCheck';
export const generalQueries = {
getChainBalance,
getPendingChainBalance,
getNetworkInfo,
getNodeInfo,
adminCheck,
};

View file

@ -0,0 +1,48 @@
import { getNetworkInfo as getLnNetworkInfo } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { NetworkInfoType } from '../../../schemaTypes/query/info/networkInfo';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
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 getNetworkInfo = {
type: NetworkInfoType,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'networkInfo');
const lnd = getAuthLnd(params.auth);
try {
const info: NetworkInfoProps = await getLnNetworkInfo({ 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,
};
} catch (error) {
params.logger &&
logger.error('Error getting network info: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,49 @@
import { getWalletInfo, getClosedChannels } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { NodeInfoType } from '../../../schemaTypes/query/info/nodeInfo';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface NodeInfoProps {
chains: string[];
color: string;
active_channels_count: number;
alias: string;
current_block_hash: string;
current_block_height: number;
is_synced_to_chain: boolean;
is_synced_to_graph: boolean;
latest_block_at: string;
peers_count: number;
pending_channels_count: number;
public_key: string;
uris: string[];
version: string;
}
export const getNodeInfo = {
type: NodeInfoType,
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'nodeInfo');
const lnd = getAuthLnd(params.auth);
try {
const info: NodeInfoProps = await getWalletInfo({
lnd,
});
const closedChannels: { channels: [] } = await getClosedChannels({
lnd,
});
return {
...info,
closed_channels_count: closedChannels.channels.length,
};
} catch (error) {
params.logger && logger.error('Error getting node info: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,19 @@
import { channelQueries } from './channels';
import { generalQueries } from './general';
import { invoiceQueries } from './transactions';
import { dataQueries } from './data';
import { reportQueries } from './report';
import { flowQueries } from './flow';
import { backupQueries } from './backup';
import { routeQueries } from './route';
export const query = {
...channelQueries,
...generalQueries,
...invoiceQueries,
...dataQueries,
...reportQueries,
...flowQueries,
...backupQueries,
...routeQueries,
};

View file

@ -0,0 +1,167 @@
import { GraphQLString } from 'graphql';
import {
getForwards as getLnForwards,
getNode,
getChannel,
getWalletInfo,
} from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { subHours, subDays } from 'date-fns';
import { countArray, countRoutes } from './Helpers';
import { ForwardCompleteProps } from './ForwardReport.interface';
import { sortBy } from 'underscore';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
interface NodeProps {
alias: string;
color: string;
}
interface ChannelsProps {
policies: { public_key: string }[];
}
export const getForwardChannelsReport = {
type: GraphQLString,
args: {
...defaultParams,
time: { type: GraphQLString },
order: { type: GraphQLString },
type: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'forwardChannels');
const lnd = getAuthLnd(params.auth);
let startDate = new Date();
const endDate = new Date();
if (params.time === 'month') {
startDate = subDays(endDate, 30);
} else if (params.time === 'week') {
startDate = subDays(endDate, 7);
} else {
startDate = subHours(endDate, 24);
}
const getNodeAlias = async (id: string, publicKey: string) => {
const channelInfo: ChannelsProps = await getChannel({
lnd,
id,
});
const partnerPublicKey =
channelInfo.policies[0].public_key !== publicKey
? channelInfo.policies[0].public_key
: channelInfo.policies[1].public_key;
const nodeInfo: NodeProps = await getNode({
lnd,
is_omitting_channels: true,
public_key: partnerPublicKey,
});
return {
alias: nodeInfo.alias,
color: nodeInfo.color,
};
};
const getRouteAlias = (array: any[], publicKey: string) =>
Promise.all(
array.map(async channel => {
const nodeAliasIn = await getNodeAlias(
channel.in,
publicKey,
);
const nodeAliasOut = await getNodeAlias(
channel.out,
publicKey,
);
return {
aliasIn: nodeAliasIn.alias,
colorIn: nodeAliasIn.color,
aliasOut: nodeAliasOut.alias,
colorOut: nodeAliasOut.color,
...channel,
};
}),
);
const getAlias = (array: any[], publicKey: string) =>
Promise.all(
array.map(async channel => {
const nodeAlias = await getNodeAlias(
channel.name,
publicKey,
);
return {
alias: nodeAlias.alias,
color: nodeAlias.color,
...channel,
};
}),
);
try {
const forwardsList: ForwardCompleteProps = await getLnForwards({
lnd,
after: startDate,
before: endDate,
limit: 10000,
});
const walletInfo: { public_key: string } = await getWalletInfo({
lnd,
});
if (params.type === 'route') {
const mapped = forwardsList.forwards.map(forward => {
return {
route: `${forward.incoming_channel} - ${forward.outgoing_channel}`,
...forward,
};
});
const grouped = countRoutes(mapped);
const routeAlias = await getRouteAlias(
grouped,
walletInfo.public_key,
);
const sortedRoute = sortBy(routeAlias, params.order)
.reverse()
.slice(0, 10);
return JSON.stringify(sortedRoute);
} else if (params.type === 'incoming') {
const incomingCount = countArray(forwardsList.forwards, true);
const incomingAlias = await getAlias(
incomingCount,
walletInfo.public_key,
);
const sortedInCount = sortBy(incomingAlias, params.order)
.reverse()
.slice(0, 10);
return JSON.stringify(sortedInCount);
} else {
const outgoingCount = countArray(forwardsList.forwards, false);
const outgoingAlias = await getAlias(
outgoingCount,
walletInfo.public_key,
);
const sortedOutCount = sortBy(outgoingAlias, params.order)
.reverse()
.slice(0, 10);
return JSON.stringify(sortedOutCount);
}
} catch (error) {
params.logger &&
logger.error('Error getting forward channel report: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,42 @@
export interface ForwardProps {
created_at: string;
fee: number;
fee_mtokens: string;
incoming_channel: string;
mtokens: string;
outgoing_channel: string;
tokens: number;
}
export interface ForwardCompleteProps {
forwards: ForwardProps[];
next: string;
}
export interface ListProps {
[key: string]: ForwardProps[];
}
export interface ReduceObjectProps {
fee: number;
tokens: number;
}
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;
}

View file

@ -0,0 +1,80 @@
import { GraphQLString } from 'graphql';
import { getForwards as getLnForwards } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { groupBy } from 'underscore';
import {
subHours,
subDays,
differenceInHours,
differenceInCalendarDays,
} from 'date-fns';
import { reduceForwardArray } from './Helpers';
import { ForwardCompleteProps } from './ForwardReport.interface';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const getForwardReport = {
type: GraphQLString,
args: {
...defaultParams,
time: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'forwardReport');
const lnd = getAuthLnd(params.auth);
let startDate = new Date();
const endDate = new Date();
let days = 7;
if (params.time === 'month') {
startDate = subDays(endDate, 30);
days = 30;
} else if (params.time === 'week') {
startDate = subDays(endDate, 7);
} else {
startDate = subHours(endDate, 24);
}
try {
const forwardsList: ForwardCompleteProps = await getLnForwards({
lnd,
after: startDate,
before: endDate,
});
if (params.time === 'month' || params.time === 'week') {
const orderedDay = groupBy(forwardsList.forwards, item => {
return (
days -
differenceInCalendarDays(
endDate,
new Date(item.created_at),
)
);
});
const reducedOrderedDay = reduceForwardArray(orderedDay);
return JSON.stringify(reducedOrderedDay);
} else {
const orderedHour = groupBy(forwardsList.forwards, item => {
return (
24 -
differenceInHours(endDate, new Date(item.created_at))
);
});
const reducedOrderedHour = reduceForwardArray(orderedHour);
return JSON.stringify(reducedOrderedHour);
}
} catch (error) {
params.logger &&
logger.error('Error getting forward report: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,106 @@
import { reduce, groupBy } from 'underscore';
import {
ForwardProps,
ReduceObjectProps,
ListProps,
} from './ForwardReport.interface';
import { InOutListProps, InOutProps } from '../flow/getInOut.interface';
export const reduceForwardArray = (list: ListProps) => {
const reducedOrder = [];
for (const key in list) {
if (list.hasOwnProperty(key)) {
const element: ForwardProps[] = list[key];
const reducedArray: ReduceObjectProps = reduce(element, (a, b) => {
return {
fee: a.fee + b.fee,
tokens: a.tokens + b.tokens,
};
});
reducedOrder.push({
period: parseInt(key),
amount: element.length,
...reducedArray,
});
}
}
return reducedOrder;
};
export const reduceInOutArray = (list: InOutListProps) => {
const reducedOrder = [];
for (const key in list) {
if (list.hasOwnProperty(key)) {
const element: InOutProps[] = list[key];
const reducedArray: InOutProps = reduce(element, (a, b) => ({
tokens: a.tokens + b.tokens,
}));
reducedOrder.push({
period: parseInt(key),
amount: element.length,
tokens: reducedArray.tokens,
});
}
}
return reducedOrder;
};
export const countArray = (list: ForwardProps[], type: boolean) => {
const inOrOut = type ? 'incoming_channel' : 'outgoing_channel';
const grouped = groupBy(list, inOrOut);
const channelInfo = [];
for (const key in grouped) {
if (grouped.hasOwnProperty(key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p, c) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p, c) => p + c);
channelInfo.push({
name: key,
amount: element.length,
fee: fee,
tokens: tokens,
});
}
}
return channelInfo;
};
export const countRoutes = (list: ForwardProps[]) => {
const grouped = groupBy(list, 'route');
const channelInfo = [];
for (const key in grouped) {
if (grouped.hasOwnProperty(key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p, c) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p, c) => p + c);
channelInfo.push({
route: key,
in: element[0].incoming_channel,
out: element[0].outgoing_channel,
amount: element.length,
fee: fee,
tokens: tokens,
});
}
}
return channelInfo;
};

View file

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

View file

@ -0,0 +1,42 @@
import { GraphQLNonNull, GraphQLString, GraphQLInt } from 'graphql';
import { getRouteToDestination, getWalletInfo } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { defaultParams } from '../../../helpers/defaultProps';
export const getRoutes = {
type: GraphQLString,
args: {
...defaultParams,
outgoing: { type: new GraphQLNonNull(GraphQLString) },
incoming: { type: new GraphQLNonNull(GraphQLString) },
tokens: { type: new GraphQLNonNull(GraphQLInt) },
maxFee: { type: GraphQLInt },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'getRoutes');
const lnd = getAuthLnd(params.auth);
const { public_key } = await getWalletInfo({ lnd });
const { route } = await getRouteToDestination({
lnd,
outgoing_channel: params.outgoing,
incoming_peer: params.incoming,
destination: public_key,
tokens: params.tokens,
...(params.maxFee && { max_fee: params.maxFee }),
}).catch((error: any) => {
params.logger && logger.error('Error getting routes: %o', error);
throw new Error(getErrorMsg(error));
});
if (!route) {
throw new Error('No route found.');
}
return JSON.stringify(route);
},
};

View file

@ -0,0 +1,5 @@
import { getRoutes } from './getRoutes';
export const routeQueries = {
getRoutes,
};

View file

@ -0,0 +1,51 @@
import { GraphQLList } from 'graphql';
import { getChainTransactions as getLnChainTransactions } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { GetChainTransactionsType } from '../../../schemaTypes/query/transactions/chainTransactions';
import { sortBy } from 'underscore';
import { defaultParams } from '../../../helpers/defaultProps';
interface TransactionProps {
block_id: string;
confirmation_count: number;
confirmation_height: number;
created_at: string;
fee: number;
id: string;
output_addresses: string[];
tokens: number;
}
interface TransactionsProps {
transactions: TransactionProps[];
}
export const getChainTransactions = {
type: new GraphQLList(GetChainTransactionsType),
args: defaultParams,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'chainTransactions');
const lnd = getAuthLnd(params.auth);
try {
const transactionList: TransactionsProps = await getLnChainTransactions(
{
lnd,
},
);
const transactions = sortBy(
transactionList.transactions,
'created_at',
).reverse();
return transactions;
} catch (error) {
params.logger &&
logger.error('Error getting chain transactions: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,124 @@
import { GraphQLString } from 'graphql';
import {
getForwards as getLnForwards,
getChannel,
getNode,
getWalletInfo,
} from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { GetForwardType } from '../../../schemaTypes/query/transactions/forwards';
import { getErrorMsg, getAuthLnd } from '../../../helpers/helpers';
import { sortBy } from 'underscore';
import { ForwardCompleteProps } from '../report/ForwardReport.interface';
import { subHours, subDays, subMonths, subYears } from 'date-fns';
import { defaultParams } from '../../../helpers/defaultProps';
interface NodeProps {
alias: string;
color: string;
}
interface ChannelsProps {
policies: { public_key: string }[];
}
export const getForwards = {
type: GetForwardType,
args: {
...defaultParams,
time: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'forwards');
const lnd = getAuthLnd(params.auth);
let startDate = new Date();
const endDate = new Date();
if (params.time === 'oneYear') {
startDate = subYears(endDate, 1);
} else if (params.time === 'sixMonths') {
startDate = subMonths(endDate, 6);
} else if (params.time === 'threeMonths') {
startDate = subMonths(endDate, 3);
} else if (params.time === 'month') {
startDate = subMonths(endDate, 1);
} else if (params.time === 'week') {
startDate = subDays(endDate, 7);
} else {
startDate = subHours(endDate, 24);
}
const walletInfo: { public_key: string } = await getWalletInfo({
lnd,
});
const getNodeAlias = async (id: string, publicKey: string) => {
const channelInfo: ChannelsProps = await getChannel({
lnd,
id,
});
const partnerPublicKey =
channelInfo.policies[0].public_key !== publicKey
? channelInfo.policies[0].public_key
: channelInfo.policies[1].public_key;
const nodeInfo: NodeProps = await getNode({
lnd,
is_omitting_channels: true,
public_key: partnerPublicKey,
});
return {
alias: nodeInfo.alias,
color: nodeInfo.color,
};
};
const getAlias = (array: any[], publicKey: string) =>
Promise.all(
array.map(async forward => {
const inNodeAlias = await getNodeAlias(
forward.incoming_channel,
publicKey,
);
const outNodeAlias = await getNodeAlias(
forward.outgoing_channel,
publicKey,
);
return {
incoming_alias: inNodeAlias.alias,
incoming_color: inNodeAlias.color,
outgoing_alias: outNodeAlias.alias,
outgoing_color: outNodeAlias.color,
...forward,
};
}),
);
try {
const forwardsList: ForwardCompleteProps = await getLnForwards({
lnd,
after: startDate,
before: endDate,
});
const list = await getAlias(
forwardsList.forwards,
walletInfo.public_key,
);
const forwards = sortBy(list, 'created_at').reverse();
return {
token: forwardsList.next,
forwards,
};
} catch (error) {
params.logger && logger.error('Error getting forwards: %o', error);
throw new Error(getErrorMsg(error));
}
},
};

View file

@ -0,0 +1,9 @@
import { getForwards } from './Forwards';
import { getResume } from './resume';
import { getChainTransactions } from './ChainTransactions';
export const invoiceQueries = {
getResume,
getForwards,
getChainTransactions,
};

View file

@ -0,0 +1,61 @@
export interface PaymentProps {
created_at: string;
destination: string;
fee: number;
fee_mtokens: string;
hops: string[];
id: string;
is_confirmed: boolean;
is_outgoing: boolean;
mtokens: string;
request: string;
secret: string;
tokens: number;
}
export interface PaymentsProps {
payments: PaymentProps[];
}
export interface InvoicePaymentProps {
confirmed_at: string;
created_at: string;
created_height: number;
in_channel: string;
is_canceled: boolean;
is_confirmed: boolean;
is_held: boolean;
mtokens: string;
pending_index: number;
tokens: number;
}
export interface InvoiceProps {
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_outgoing: boolean;
is_private: boolean;
payments: InvoicePaymentProps[];
received: number;
received_mtokens: string;
request: string;
secret: string;
tokens: number;
}
export interface InvoicesProps {
invoices: InvoiceProps[];
next: string;
}
export interface NodeProps {
alias: string;
}

View file

@ -0,0 +1,121 @@
import { GraphQLString } from 'graphql';
import { getPayments, getInvoices, getNode } from 'ln-service';
import { logger } from '../../../helpers/logger';
import { requestLimiter } from '../../../helpers/rateLimiter';
import { getAuthLnd, getErrorMsg } from '../../../helpers/helpers';
import { PaymentsProps, InvoicesProps, NodeProps } from './Resume.interface';
import { compareDesc } from 'date-fns';
import { sortBy } from 'underscore';
import { GetResumeType } from '../../../schemaTypes/query/transactions/resume';
import { defaultParams } from '../../../helpers/defaultProps';
export const getResume = {
type: GetResumeType,
args: {
...defaultParams,
token: { type: GraphQLString },
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, 'payments');
const lnd = getAuthLnd(params.auth);
let payments;
let invoices;
try {
const paymentList: PaymentsProps = await getPayments({
lnd,
});
const getMappedPayments = () =>
Promise.all(
paymentList.payments.map(async payment => {
let nodeInfo: NodeProps;
try {
nodeInfo = await getNode({
lnd,
is_omitting_channels: true,
public_key: payment.destination,
});
} catch (error) {
nodeInfo = { alias: 'unknown' };
}
return {
type: 'payment',
alias: nodeInfo.alias,
date: payment.created_at,
...payment,
};
}),
);
payments = await getMappedPayments();
} catch (error) {
params.logger && logger.error('Error getting payments: %o', error);
throw new Error(getErrorMsg(error));
}
const invoiceProps = params.token
? { token: params.token }
: { limit: 25 };
let lastInvoiceDate = '';
let firstInvoiceDate = '';
let token = '';
let withInvoices = true;
try {
const invoiceList: InvoicesProps = await getInvoices({
lnd,
...invoiceProps,
});
invoices = invoiceList.invoices.map(invoice => {
return {
type: 'invoice',
date: invoice.confirmed_at || invoice.created_at,
...invoice,
};
});
if (invoices.length <= 0) {
withInvoices = false;
} else {
const { date } = invoices[invoices.length - 1];
firstInvoiceDate = invoices[0].date;
lastInvoiceDate = date;
token = invoiceList.next;
}
} catch (error) {
params.logger && logger.error('Error getting invoices: %o', error);
throw new Error(getErrorMsg(error));
}
const filteredPayments = withInvoices
? payments.filter(payment => {
const last =
compareDesc(
new Date(lastInvoiceDate),
new Date(payment.date),
) === 1;
const first = params.token
? compareDesc(
new Date(payment.date),
new Date(firstInvoiceDate),
) === 1
: true;
return last && first;
})
: payments;
const resumeArray = sortBy(
[...invoices, ...filteredPayments],
'date',
).reverse();
return {
token,
resume: JSON.stringify(resumeArray),
};
},
};

View file

@ -0,0 +1,44 @@
interface RateConfigProps {
[key: string]: {
max: number;
window: string;
};
}
export const RateConfig: RateConfigProps = {
channelBalance: { max: 3, window: '1s' },
channelFees: { max: 3, window: '1s' },
channels: { max: 3, window: '1s' },
channelReport: { max: 3, window: '1s' },
closedChannels: { max: 3, window: '1s' },
pendingChannels: { max: 3, window: '1s' },
bitcoinFee: { max: 3, window: '1s' },
bitcoinPrice: { max: 3, window: '1s' },
getInOut: { max: 3, window: '1s' },
chainBalance: { max: 3, window: '1s' },
pendingChainBalance: { max: 3, window: '1s' },
networkInfo: { max: 3, window: '1s' },
nodeInfo: { max: 3, window: '1s' },
forwards: { max: 3, window: '1s' },
invoices: { max: 3, window: '1s' },
payments: { max: 3, window: '1s' },
forwardChannels: { max: 3, window: '1s' },
forwardReport: { max: 3, window: '1s' },
getRoute: { max: 3, window: '1s' },
closeChannel: { max: 3, window: '1s' },
openChannel: { max: 3, window: '1s' },
createInvoice: { max: 3, window: '1s' },
decode: { max: 3, window: '1s' },
parsePayment: { max: 3, window: '1s' },
pay: { max: 3, window: '1s' },
getAddress: { max: 3, window: '1s' },
sendToAddress: { max: 3, window: '1s' },
getBackups: { max: 3, window: '1s' },
verifyBackups: { max: 3, window: '1s' },
recoverFunds: { max: 3, window: '1s' },
updateFees: { max: 3, window: '1s' },
chainTransactions: { max: 3, window: '1s' },
getRoutes: { max: 3, window: '1s' },
payViaRoute: { max: 3, window: '1s' },
adminCheck: { max: 3, window: '1s' },
};

66
tsconfig.json Normal file
View file

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

1
types/ln-service.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module "ln-service";

21
webpack.common.js Normal file
View file

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

20
webpack.development.js Normal file
View file

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

14
webpack.production.js Normal file
View file

@ -0,0 +1,14 @@
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const path = require('path');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'source-map',
entry: [path.join(__dirname, 'src/main.ts')],
externals: [nodeExternals({})],
mode: 'production',
plugins: [new CleanWebpackPlugin()],
});

5824
yarn.lock Normal file

File diff suppressed because it is too large Load diff