feat: more queries and mutations

This commit is contained in:
AP 2019-11-07 07:53:11 +01:00
parent 39fbfe3bbf
commit 9973c3c54f
22 changed files with 540 additions and 1 deletions

10
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "thunderhub",
"version": "1.0.0",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -224,6 +224,14 @@
"graphql": "^14.5.3"
}
},
"@types/graphql-iso-date": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@types/graphql-iso-date/-/graphql-iso-date-3.3.3.tgz",
"integrity": "sha512-lchvlAox/yqk2Rcrgqh+uvwc1UC9i1hap+0tqQqyYYcAica6Uw2D4mUkCNcw+WeZ8dvSS5QdtIlJuDYUf4nLXQ==",
"requires": {
"graphql": "^14.5.3"
}
},
"@types/graphql-upload": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/@types/graphql-upload/-/graphql-upload-8.0.3.tgz",

View file

@ -22,6 +22,7 @@
"homepage": "https://github.com/apotdevin/thunderhub#readme",
"dependencies": {
"@types/graphql-depth-limit": "^1.1.2",
"@types/graphql-iso-date": "^3.3.3",
"apollo-server": "^2.9.7",
"dotenv": "^8.2.0",
"graphql": "^14.5.8",

View file

@ -6,6 +6,7 @@ import depthLimit from "graphql-depth-limit";
import lnService from "ln-service";
const { lnd } = lnService.authenticatedLndGrpc({
cert: process.env.LND_CERT,
macaroon: process.env.LND_MAC,
socket: process.env.LND_IP
});

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,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

@ -8,6 +8,9 @@ export const ChannelType = new GraphQLObjectType({
capacity: {
type: GraphQLInt
},
id: {
type: GraphQLString
},
isActive: {
type: GraphQLBoolean
},

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

@ -1,9 +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,53 @@
import { closeChannel as lnCloseChannel } from "ln-service";
import { logger } from "../../../helpers/logger";
import { requestLimiter } from "../../../helpers/rateLimiter";
import { GraphQLBoolean, GraphQLString } from "graphql";
import { CloseChannelType } from "../../../schemaTypes/mutation.ts/channels/closeChannel";
interface CloseChannelProps {
transaction_id: string;
transaction_vout: string;
}
export const closeChannel = {
type: CloseChannelType,
args: {
forceClose: {
type: GraphQLBoolean
},
id: {
type: GraphQLString
},
transactionId: {
type: GraphQLString
},
transactionOutput: {
type: GraphQLString
}
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, params, "closeChannel", 1, "1s");
const { lnd } = context;
if (!params.id && !params.transactionId && !params.transactionOutput)
throw new Error(
"Id, transaction id or transaction output index are required"
);
try {
const info: CloseChannelProps = await lnCloseChannel({
lnd: lnd,
id: params.id,
is_force_close: params.forceClose,
transaction_id: params.transactionId
});
return {
transactionId: info.transaction_id,
transactionOutputIndex: info.transaction_vout
};
} catch (error) {
logger.error("Error closing channel: %o", error);
throw new Error("Failed to close channel.");
}
}
};

View file

@ -0,0 +1,7 @@
import { closeChannel } from "./closeChannel";
import { openChannel } from "./openChannel";
export const channels = {
closeChannel,
openChannel
};

View file

@ -0,0 +1,48 @@
import { openChannel as lnOpenChannel } from "ln-service";
import { logger } from "../../../helpers/logger";
import { requestLimiter } from "../../../helpers/rateLimiter";
import { GraphQLBoolean, GraphQLString, GraphQLInt } from "graphql";
import { OpenChannelType } from "../../../schemaTypes/mutation.ts/channels/openChannel";
interface OpenChannelProps {
transaction_id: string;
transaction_vout: string;
}
export const openChannel = {
type: OpenChannelType,
args: {
isPrivate: {
type: GraphQLBoolean
},
amount: {
type: GraphQLInt
},
partnerPublicKey: {
type: GraphQLString
}
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, params, "openChannel", 1, "1s");
const { lnd } = context;
if (!params.amount || !params.partnerPublicKey)
throw new Error("Amount and partner public key are required");
try {
const info: OpenChannelProps = await lnOpenChannel({
lnd: lnd,
is_private: params.isPrivate,
local_tokens: params.amount,
partner_public_key: params.partnerPublicKey
});
return {
transactionId: info.transaction_id,
transactionOutputIndex: info.transaction_vout
};
} catch (error) {
logger.error("Error opening channel: %o", error);
throw new Error("Failed to open channel.");
}
}
};

View file

@ -0,0 +1,4 @@
import { channels } from "./channels";
import { invoices } from "./invoices";
export const mutation = { ...channels, ...invoices };

View file

@ -0,0 +1,49 @@
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";
interface InvoiceProps {
chain_address: string;
created_at: string;
description: string;
id: string;
request: string;
secret: string;
tokens: number;
}
// TODO: Allow more params
export const createInvoice = {
type: InvoiceType,
args: {
amount: {
type: new GraphQLNonNull(GraphQLInt)
}
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, params, "createInvoice", 1, "1s");
const { lnd } = context;
try {
const invoice: InvoiceProps = await createInvoiceRequest({
lnd: 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) {
logger.error("Error creating invoice: %o", error);
throw new Error("Failed to create invoice.");
}
}
};

View file

@ -0,0 +1,9 @@
import { parsePayment } from "./parsePayment";
import { pay } from "./pay";
import { createInvoice } from "./createInvoice";
export const invoices = {
parsePayment,
pay,
createInvoice
};

View file

@ -0,0 +1,78 @@
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";
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: {
request: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, params, "parsePayment", 1, "1s");
const { lnd } = context;
try {
const request: RequestProps = await parsePaymentRequest({
lnd: 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) {
logger.error("Error decoding request: %o", error);
throw new Error("Failed to decode request.");
}
}
};

View file

@ -0,0 +1,71 @@
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";
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: {
request: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, params, "payRequest", 1, "1s");
const { lnd } = context;
try {
const payment: RequestProps = await payRequest({
lnd: 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) {
logger.error("Error paying request: %o", error);
throw new Error("Failed to pay request.");
}
}
};

View file

@ -48,6 +48,7 @@ export const channels = {
const channels = channelList.channels.map((channel, index) => {
return {
capacity: channel.capacity,
id: channel.id,
isActive: channel.is_active,
isClosing: channel.is_closing,
isOpening: channel.is_opening,

View file

@ -2,6 +2,7 @@ import { chainBalance } from "./chainBalance";
import { channelBalance } from "./channelBalance";
import { channels } from "./channels";
import { closedChannels } from "./closedChannels";
import { networkInfo } from "./networkInfo";
import { nodeInfo } from "./nodeInfo";
import { pendingChannels } from "./pendingChannels";
@ -10,6 +11,7 @@ export const info = {
channelBalance,
channels,
closedChannels,
networkInfo,
nodeInfo,
pendingChannels
};

View file

@ -0,0 +1,43 @@
import { getNetworkInfo } from "ln-service";
import { logger } from "../../../helpers/logger";
import { requestLimiter } from "../../../helpers/rateLimiter";
import { NetworkInfoType } from "../../../schemaTypes/query/info/networkInfo";
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 networkInfo = {
type: NetworkInfoType,
resolve: async (root: any, params: any, context: any) => {
await requestLimiter(context.ip, params, "networkInfo", 1, "1s");
const { lnd } = context;
try {
const info: NetworkInfoProps = await getNetworkInfo({
lnd: 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) {
logger.error("Error getting network info: %o", error);
throw new Error("Failed to get network info.");
}
}
};