diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.tm_properties b/.tm_properties new file mode 100644 index 00000000..7eecf8ce --- /dev/null +++ b/.tm_properties @@ -0,0 +1 @@ +exclude = '{$exclude,node_modules}' \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 00000000..ddff151b --- /dev/null +++ b/app.js @@ -0,0 +1,48 @@ +const bodyParser = require('body-parser'); +const compress = require('compression')(); +const express = require('express'); +const logger = require('morgan'); +const walnut = require('walnut'); + +const balanceRouter = require('./routers/balance'); +const channelsRouter = require('./routers/channels'); +const lndGrpcInterface = require('./libs/lnd_grpc_interface'); +const historyRouter = require('./routers/history'); +const invoicesRouter = require('./routers/invoices'); +const networkInfoRouter = require('./routers/network_info'); +const payReqRouter = require('./routers/payment_request'); +const peersRouter = require('./routers/peers'); +const purchasedRouter = require('./routers/purchased'); +const walletInfoRouter = require('./routers/wallet_info'); + +const lndGrpcHost = 'localhost:10009'; +const logFormat = ':method :url :status - :response-time ms - :user-agent'; +const port = process.env.PORT || 10553; + +const app = express(); +const lndGrpcApi = lndGrpcInterface('./config/grpc.proto', lndGrpcHost); + +app +.listen(port, () => { console.log(`Listening on port: ${port}`); }) +.on('error', (e) => { console.log('Listen error', e); }); + +app.disable('x-powered-by'); + +app.use(compress); +app.use(bodyParser.json()); +app.use(logger(logFormat)); + +app.use('/v0/balance', balanceRouter({lnd_grpc_api: lndGrpcApi})); +app.use('/v0/channels', channelsRouter({lnd_grpc_api: lndGrpcApi})); +app.use('/v0/history', historyRouter({lnd_grpc_api: lndGrpcApi})); +app.use('/v0/invoices', invoicesRouter({lnd_grpc_api: lndGrpcApi})); +app.use('/v0/network_info', networkInfoRouter({lnd_grpc_api: lndGrpcApi})); +app.use('/v0/peers', peersRouter({lnd_grpc_api: lndGrpcApi})); +app.use('/v0/purchased', purchasedRouter({lnd_grpc_api: lndGrpcApi})); +app.use('/v0/payment_request', payReqRouter({lnd_grpc_api: lndGrpcApi})); +app.use('/v0/wallet_info', walletInfoRouter({lnd_grpc_api: lndGrpcApi})); + +if (process.env.NODE_ENV !== 'production') { + walnut.check(require('./package')); +} + diff --git a/config/grpc.proto b/config/grpc.proto new file mode 100644 index 00000000..ffb885b0 --- /dev/null +++ b/config/grpc.proto @@ -0,0 +1,573 @@ +syntax = "proto3"; + +//import "google/api/annotations.proto"; + +package lnrpc; + +service Lightning { + rpc WalletBalance(WalletBalanceRequest) returns (WalletBalanceResponse) { + option (google.api.http) = { + get: "/v1/balance/blockchain" + }; + } + rpc ChannelBalance(ChannelBalanceRequest) returns (ChannelBalanceResponse) { + option (google.api.http) = { + get: "/v1/balance/channels" + }; + } + + rpc GetTransactions(GetTransactionsRequest) returns (TransactionDetails) { + option (google.api.http) = { + get: "/v1/transactions" + }; + } + rpc SendCoins(SendCoinsRequest) returns (SendCoinsResponse) { + option (google.api.http) = { + post: "/v1/transactions" + body: "*" + }; + } + rpc SubscribeTransactions(GetTransactionsRequest) returns (stream Transaction); + + rpc SendMany(SendManyRequest) returns (SendManyResponse); + + rpc NewAddress(NewAddressRequest) returns (NewAddressResponse); + rpc NewWitnessAddress(NewWitnessAddressRequest) returns (NewAddressResponse) { + option (google.api.http) = { + get: "/v1/newaddress" + }; + } + + rpc ConnectPeer(ConnectPeerRequest) returns (ConnectPeerResponse) { + option (google.api.http) = { + post: "/v1/peers" + body: "*" + }; + } + rpc ListPeers(ListPeersRequest) returns (ListPeersResponse) { + option (google.api.http) = { + get: "/v1/peers" + }; + } + rpc GetInfo(GetInfoRequest) returns (GetInfoResponse) { + option (google.api.http) = { + get: "/v1/getinfo" + }; + } + + // TODO(roasbeef): merge with below with bool? + rpc PendingChannels(PendingChannelRequest) returns (PendingChannelResponse) { + option (google.api.http) = { + get: "/v1/channels/pending" + }; + } + rpc ListChannels(ListChannelsRequest) returns (ListChannelsResponse) { + option (google.api.http) = { + get: "/v1/channels" + }; + } + rpc OpenChannelSync(OpenChannelRequest) returns (ChannelPoint) { + option (google.api.http) = { + post: "/v1/channels" + body: "*" + }; + } + + rpc OpenChannel(OpenChannelRequest) returns (stream OpenStatusUpdate); + + rpc CloseChannel(CloseChannelRequest) returns (stream CloseStatusUpdate) { + option (google.api.http) = { + delete: "/v1/channels/{channel_point.funding_txid}/{channel_point.output_index}/{force}" + }; + } + + rpc SendPayment(stream SendRequest) returns (stream SendResponse); + + rpc SendPaymentSync(SendRequest) returns (SendResponse) { + option (google.api.http) = { + post: "/v1/channels/transactions" + body: "*" + }; + } + + rpc AddInvoice(Invoice) returns (AddInvoiceResponse) { + option (google.api.http) = { + post: "/v1/invoices" + body: "*" + }; + } + rpc ListInvoices(ListInvoiceRequest) returns (ListInvoiceResponse) { + option (google.api.http) = { + get: "/v1/invoices/{pending_only}" + }; + } + rpc LookupInvoice(PaymentHash) returns (Invoice) { + option (google.api.http) = { + get: "/v1/invoices/{r_hash_str}" + }; + } + rpc SubscribeInvoices(InvoiceSubscription) returns (stream Invoice) { + option (google.api.http) = { + get: "/v1/invoices/subscribe" + }; + } + rpc DecodePayReq(PayReqString) returns (PayReq) { + option (google.api.http) = { + get: "/v1/payreq/{pay_req}" + }; + } + + rpc ListPayments(ListPaymentsRequest) returns (ListPaymentsResponse){ + option (google.api.http) = { + get: "/v1/payments" + }; + }; + + rpc DeleteAllPayments(DeleteAllPaymentsRequest) returns (DeleteAllPaymentsResponse) { + option (google.api.http) = { + delete: "/v1/payments" + }; + }; + + rpc DescribeGraph(ChannelGraphRequest) returns (ChannelGraph) { + option (google.api.http) = { + get: "/v1/graph" + }; + } + + rpc GetChanInfo(ChanInfoRequest) returns (ChannelEdge) { + option (google.api.http) = { + get: "/v1/graph/edge/{chan_id}" + }; + } + + rpc GetNodeInfo(NodeInfoRequest) returns (NodeInfo) { + option (google.api.http) = { + get: "/v1/graph/node/{pub_key}" + }; + } + + rpc QueryRoute(RouteRequest) returns (Route) { + option (google.api.http) = { + get: "/v1/graph/route/{pub_key}/{amt}" + }; + } + + rpc GetNetworkInfo(NetworkInfoRequest) returns (NetworkInfo) { + option (google.api.http) = { + get: "/v1/graph/info" + }; + } + + rpc SetAlias(SetAliasRequest) returns (SetAliasResponse); + + rpc DebugLevel(DebugLevelRequest) returns (DebugLevelResponse); +} + +message Transaction { + string tx_hash = 1 [ json_name = "tx_hash" ]; + double amount = 2 [ json_name = "amount" ]; + int32 num_confirmations = 3 [ json_name = "num_confirmations" ]; + string block_hash = 4 [ json_name = "block_hash" ]; + int32 block_height = 5 [ json_name = "block_height" ]; + int64 time_stamp = 6 [ json_name = "time_stamp" ]; + int64 total_fees = 7 [ json_name = "total_fees" ]; +} +message GetTransactionsRequest { +} +message TransactionDetails { + repeated Transaction transactions = 1 [ json_name = "transactions" ]; +} + +message SendRequest { + bytes dest = 1; + string dest_string = 2; + + int64 amt = 3; + + bytes payment_hash = 4; + string payment_hash_string = 5; + + string payment_request = 6; +} +message SendResponse { + bytes payment_preimage = 1 [ json_name = "payment_preimage" ]; + Route payment_route = 2 [ json_name = "payment_route" ]; +} + +message ChannelPoint { + bytes funding_txid = 1 [ json_name = "funding_txid" ]; + string funding_txid_str = 2 [ json_name = "funding_txid_str" ]; + uint32 output_index = 3 [ json_name = "output_index" ]; +} + +message LightningAddress { + string pubkey = 1 [ json_name = "pubkey" ]; + string host = 2 [ json_name = "host" ]; +} + +message SendManyRequest { + map AddrToAmount = 1; +} +message SendManyResponse { + string txid = 1 [ json_name = "txid" ]; +} + +message SendCoinsRequest { + string addr = 1; + int64 amount = 2; +} +message SendCoinsResponse { + string txid = 1 [ json_name = "txid" ]; +} + +message NewAddressRequest { + enum AddressType { + WITNESS_PUBKEY_HASH = 0; + NESTED_PUBKEY_HASH = 1; + PUBKEY_HASH = 2; + } + AddressType type = 1; +} +message NewWitnessAddressRequest {} +message NewAddressResponse { + string address = 1 [ json_name = "address" ]; +} + +message ConnectPeerRequest { + LightningAddress addr = 1; + bool perm = 2; +} +message ConnectPeerResponse { + int32 peer_id = 1 [ json_name = "peer_id" ]; +} + +message HTLC { + bool incoming = 1 [ json_name = "incoming" ]; + int64 amount = 2 [ json_name = "amount" ]; + bytes hash_lock = 3 [ json_name = "hash_lock" ]; + uint32 expiration_height = 4 [ json_name = "expiration_height" ]; + uint32 revocation_delay = 5 [ json_name = "revocation_delay" ]; +} + +message ActiveChannel { + string remote_pubkey = 1 [ json_name = "remote_pubkey" ]; + string channel_point = 2 [ json_name = "channel_point" ]; + uint64 chan_id = 3 [ json_name = "chan_id" ]; + + int64 capacity = 4 [ json_name = "capacity" ]; + int64 local_balance = 5 [ json_name = "local_balance" ]; + int64 remote_balance = 6 [ json_name = "remote_balance" ]; + + int64 unsettled_balance = 7 [ json_name = "unsettled_balance" ]; + int64 total_satoshis_sent = 8 [ json_name = "total_satoshis_sent" ]; + int64 total_satoshis_received = 9 [ json_name = "total_satoshis_received" ]; + uint64 num_updates = 10 [ json_name = "num_updates" ]; + + repeated HTLC pending_htlcs = 11 [ json_name = "pending_htlcs" ]; +} + +message ListChannelsRequest {} +message ListChannelsResponse { + repeated ActiveChannel channels = 11 [ json_name = "channels" ]; +} + +message Peer { + string pub_key = 1 [ json_name = "pub_key" ]; + int32 peer_id = 2 [ json_name = "peer_id" ]; + string address = 3 [ json_name = "address" ]; + + uint64 bytes_sent = 4 [ json_name = "bytes_sent" ]; + uint64 bytes_recv = 5 [ json_name = "bytes_recv" ]; + + int64 sat_sent = 6 [ json_name = "sat_sent" ]; + int64 sat_recv = 7 [ json_name = "sat_recv" ]; + + bool inbound = 8 [ json_name = "inbound" ]; + + int64 ping_time = 9 [ json_name = "ping_time" ]; +} + +message ListPeersRequest {} +message ListPeersResponse { + repeated Peer peers = 1 [ json_name = "peers" ]; +} + +message GetInfoRequest{} +message GetInfoResponse { + string identity_pubkey = 1 [ json_name = "identity_pubkey" ]; + string alias = 2 [ json_name = "alias" ]; + + uint32 num_pending_channels = 3 [ json_name = "num_pending_channels" ]; + uint32 num_active_channels = 4 [ json_name = "num_active_channels" ]; + + uint32 num_peers = 5 [ json_name = "num_peers" ]; + + uint32 block_height = 6 [ json_name = "block_height" ]; + string block_hash = 8 [ json_name = "block_hash" ]; + + bool synced_to_chain = 9 [ json_name = "synced_to_chain" ]; + bool testnet = 10 [ json_name = "testnet" ]; +} + +message ConfirmationUpdate { + bytes block_sha = 1; + int32 block_height = 2; + + uint32 num_confs_left = 3; +} + +message ChannelOpenUpdate { + ChannelPoint channel_point = 1 [ json_name = "channel_point"] ; +} + +message ChannelCloseUpdate { + bytes closing_txid = 1 [ json_name = "closing_txid" ]; + + bool success = 2 [ json_name = "success" ]; +} + +message CloseChannelRequest { + ChannelPoint channel_point = 1; + int64 time_limit = 2; + bool force = 3; +} +message CloseStatusUpdate { + oneof update { + PendingUpdate close_pending = 1 [ json_name = "close_pending" ]; + ConfirmationUpdate confirmation = 2 [ json_name = "confirmation" ]; + ChannelCloseUpdate chan_close = 3 [ json_name = "chan_close" ]; + } +} + +message PendingUpdate { + bytes txid = 1 [ json_name = "txid" ]; + uint32 output_index = 2 [ json_name = "output_index" ]; +} + +message OpenChannelRequest { + int32 target_peer_id = 1 [ json_name = "target_peer_id" ]; + bytes node_pubkey = 2 [ json_name = "node_pubkey" ]; + string node_pubkey_string = 3 [ json_name = "node_pubkey_string" ]; + + int64 local_funding_amount = 4 [ json_name = "local_funding_amount" ]; + int64 push_sat = 5 [ json_name = "push_sat" ]; + + uint32 num_confs = 6 [ json_name = "num_confs" ]; +} +message OpenStatusUpdate { + oneof update { + PendingUpdate chan_pending = 1 [ json_name = "chan_pending" ]; + ConfirmationUpdate confirmation = 2 [ json_name = "confirmation" ]; + ChannelOpenUpdate chan_open = 3 [ json_name = "chan_open" ]; + } +} + +enum ChannelStatus { + ALL = 0; + OPENING = 1; + CLOSING = 2; +} +message PendingChannelRequest { + ChannelStatus status = 1; +} +message PendingChannelResponse { + message PendingChannel { + string identity_key = 1 [ json_name = "identity_key" ]; + string channel_point = 2 [ json_name = "channel_point" ]; + + int64 capacity = 3 [ json_name = "capacity" ]; + int64 local_balance = 4 [ json_name = "local_balance" ]; + int64 remote_balance = 5 [ json_name = "remote_balance" ]; + + string closing_txid = 6 [ json_name = "closing_txid" ]; + + ChannelStatus status = 7 [ json_name = "status" ]; + } + + repeated PendingChannel pending_channels = 1 [ json_name = "pending_channels" ]; +} + +message WalletBalanceRequest { + bool witness_only = 1; +} +message WalletBalanceResponse { + double balance = 1 [ json_name = "balance" ]; +} + +message ChannelBalanceRequest { +} +message ChannelBalanceResponse { + int64 balance = 1 [ json_name = "balance" ]; +} + +message RouteRequest { + string pub_key = 1; + int64 amt = 2; +} + +message Hop { + uint64 chan_id = 1 [ json_name = "chan_id" ]; + int64 chan_capacity = 2 [ json_name = "chan_capacity" ]; + int64 amt_to_forward = 3 [ json_name = "amt_to_forward" ]; + int64 fee = 4 [ json_name = "fee" ]; +} + +message Route { + uint32 total_time_lock = 1 [ json_name = "total_time_lock" ]; + int64 total_fees = 2 [ json_name = "total_fees" ]; + int64 total_amt = 3 [ json_name = "total_amt" ]; + + repeated Hop hops = 4 [ json_name = "hops" ]; +} + +message NodeInfoRequest{ + string pub_key = 1; +} + +message NodeInfo { + LightningNode node = 1 [ json_name = "node" ]; + + uint32 num_channels = 2 [ json_name = "num_channels" ]; + int64 total_capacity = 3 [ json_name = "total_capacity" ]; +} + +message LightningNode { + uint32 last_update = 1 [ json_name = "last_update" ]; + string pub_key = 2 [ json_name = "pub_key" ]; + string address = 3 [ json_name = "address" ]; + string alias = 4 [ json_name = "alias" ]; +} + +message RoutingPolicy { + uint32 time_lock_delta = 1 [ json_name = "time_lock_delta" ]; + int64 min_htlc = 2 [ json_name = "min_htlc" ]; + int64 fee_base_msat = 3 [ json_name = "fee_base_msat" ]; + int64 fee_rate_milli_msat = 4 [ json_name = "fee_rate_milli_msat" ]; +} + +message ChannelEdge { + uint64 channel_id = 1 [ json_name = "channel_id" ]; + string chan_point = 2 [ json_name = "chan_point" ]; + + uint32 last_update = 3 [ json_name = "last_update" ]; + + string node1_pub = 4 [ json_name = "node1_pub" ]; + string node2_pub = 5 [ json_name = "node2_pub" ]; + + int64 capacity = 6 [ json_name = "capacity" ]; + + RoutingPolicy node1_policy = 7 [ json_name = "node1_policy" ]; + RoutingPolicy node2_policy = 8 [ json_name = "node2_policy" ]; +} + +message ChannelGraphRequest{} + +message ChannelGraph { + repeated LightningNode nodes = 1 [ json_name = "nodes" ]; + repeated ChannelEdge edges = 2 [ json_name = "edges" ]; +} + +message ChanInfoRequest { + uint64 chan_id = 1; +} + +message NetworkInfoRequest{} +message NetworkInfo { + uint32 graph_diameter = 1 [ json_name = "graph_diameter" ]; + double avg_out_degree = 2 [ json_name = "avg_out_degree" ]; + uint32 max_out_degree = 3 [ json_name = "max_out_degree" ]; + + uint32 num_nodes = 4 [ json_name = "num_nodes" ]; + uint32 num_channels = 5 [ json_name = "num_channels" ]; + + int64 total_network_capacity = 6 [ json_name = "total_network_capacity" ]; + + double avg_channel_size = 7 [ json_name = "avg_channel_size" ]; + int64 min_channel_size = 8 [ json_name = "min_channel_size" ]; + int64 max_channel_size = 9 [ json_name = "max_channel_size" ]; + + // TODO(roasbeef): fee rate info, expiry + // * also additional RPC for tracking fee info once in +} + +message SetAliasRequest { + string new_alias = 1; +} +message SetAliasResponse{} + +message Invoice { + string memo = 1 [ json_name = "memo" ]; + bytes receipt = 2 [ json_name = "receipt" ]; + + bytes r_preimage = 3 [ json_name = "r_preimage" ]; + bytes r_hash = 4 [ json_name = "r_hash" ]; + + int64 value = 5 [ json_name = "value" ]; + + bool settled = 6 [ json_name = "settled" ]; + + int64 creation_date = 7 [ json_name = "creation_date" ]; + int64 settle_date = 8 [ json_name = "settle_date" ]; + + string payment_request = 9 [ json_name = "payment_request" ]; +} +message AddInvoiceResponse { + bytes r_hash = 1 [ json_name = "r_hash" ]; + + string payment_request = 2 [ json_name = "payment_request" ]; +} +message PaymentHash { + string r_hash_str = 1 [ json_name = "r_hash_str" ]; + bytes r_hash = 2 [ json_name = "r_hash" ]; +} +message ListInvoiceRequest { + bool pending_only = 1; +} +message ListInvoiceResponse { + repeated Invoice invoices = 1 [ json_name = "invoices" ]; +} + +message InvoiceSubscription {} + + +message Payment { + string payment_hash = 1 [ json_name = "payment_hash" ]; + int64 value = 2 [ json_name = "value" ]; + + int64 creation_date = 3 [ json_name = "creation_date" ]; + + repeated string path = 4 [ json_name = "string" ]; + + int64 fee = 5 [ json_name = "fee" ]; +} + +message ListPaymentsRequest { +} + +message ListPaymentsResponse { + repeated Payment payments = 1 [ json_name= "payments" ]; +} + +message DeleteAllPaymentsRequest { +} + +message DeleteAllPaymentsResponse { +} + +message DebugLevelRequest { + bool show = 1; + string level_spec = 2; +} +message DebugLevelResponse { + string sub_systems = 1 [ json_name = "sub_systems" ]; +} + +message PayReqString { + string pay_req = 1; +} +message PayReq { + string destination = 1 [ json_name = "destination" ]; + string payment_hash = 2 [ json_name = "payment_hash" ]; + int64 num_satoshis = 3 [ json_name = "num_satoshis" ]; +} diff --git a/libs/create_invoice.js b/libs/create_invoice.js new file mode 100644 index 00000000..5c34aedd --- /dev/null +++ b/libs/create_invoice.js @@ -0,0 +1,39 @@ +/** Create an invoice + + { + amount: + lnd_grpc_api: + memo: + } + + @returns via cbk + { + payment_request: + id: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api || !args.amount) { + return cbk([500, 'Missing lnd grpc api, or amount', args]); + } + + return args.lnd_grpc_api.addInvoice({ + memo: args.memo, + value: args.amount, + }, + (err, response) => { + if (!!err) { return cbk([500, 'Add invoice error', err]); } + + if (!response.payment_request) { return cbk([500, 'No payment request']); } + + if (!Buffer.isBuffer(response.r_hash)) { + return cbk([500, 'Rhash is not a buffer']); + } + + return cbk(null, { + id: response.r_hash.toString('hex'), + payment_request: response.payment_request, + }); + }); +}; + diff --git a/libs/get_balance.js b/libs/get_balance.js new file mode 100644 index 00000000..02ba3878 --- /dev/null +++ b/libs/get_balance.js @@ -0,0 +1,43 @@ +const asyncAuto = require('async/auto'); + +const getChainBalance = require('./get_chain_balance'); +const getChannelBalance = require('./get_channel_balance'); + +/** Get history + + { + lnd_grpc_api: + } + + @returns via cbk + { + chain_balance: + channel_balance: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + return asyncAuto({ + getChainBalance: (cbk) => { + return getChainBalance({lnd_grpc_api: args.lnd_grpc_api}, cbk); + }, + + getChannelBalance: (cbk) => { + return getChannelBalance({lnd_grpc_api: args.lnd_grpc_api}, cbk); + }, + + balance: ['getChainBalance', 'getChannelBalance', (res, cbk) => { + return cbk(null, { + chain_balance: res.getChainBalance, + channel_balance: res.getChannelBalance, + }); + }], + }, + (err, res) => { + if (!!err) { return cbk(err); } + + return cbk(null, res.balance); + }); +}; + diff --git a/libs/get_chain_balance.js b/libs/get_chain_balance.js new file mode 100644 index 00000000..9c067570 --- /dev/null +++ b/libs/get_chain_balance.js @@ -0,0 +1,23 @@ +const _ = require('lodash'); +const smallTokenUnitsPerBigUnit = 100000000; + +/** Get balance + + { + lnd_grpc_api: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + return args.lnd_grpc_api.walletBalance({}, (err, res) => { + if (!!err) { return cbk([500, 'Get chain balance error', err]); } + + if (!res || !_(res.balance).isNumber()) { + return cbk([500, 'Expected balance', res]); + } + + return cbk(null, res.balance * smallTokenUnitsPerBigUnit); + }); +}; + diff --git a/libs/get_channel_balance.js b/libs/get_channel_balance.js new file mode 100644 index 00000000..f83d4e72 --- /dev/null +++ b/libs/get_channel_balance.js @@ -0,0 +1,22 @@ +const _ = require('lodash'); + +/** Get balance + + { + lnd_grpc_api: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + return args.lnd_grpc_api.channelBalance({}, (err, res) => { + if (!!err) { return cbk([500, 'Get channel balance error', err]); } + + if (!res || res.balance === undefined) { + return cbk([500, 'Expected channel balance', res]); + } + + return cbk(null, parseInt(res.balance)); + }); +}; + diff --git a/libs/get_history.js b/libs/get_history.js new file mode 100644 index 00000000..7fbb32c9 --- /dev/null +++ b/libs/get_history.js @@ -0,0 +1,51 @@ +const _ = require('lodash'); +const asyncAuto = require('async/auto'); + +const getInvoices = require('./get_invoices'); +const getPayments = require('./get_payments'); + +/** Get history + + { + lnd_grpc_api: + } + + @returns via cbk + [{ + amount: + confirmed: + created_at: + [destination]: + [fee]: + [hops]: + [id]: + [memo]: + outgoing: + [payment]: + }] +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + return asyncAuto({ + getInvoices: (cbk) => { + return getInvoices({lnd_grpc_api: args.lnd_grpc_api}, cbk); + }, + + getPayments: (cbk) => { + return getPayments({lnd_grpc_api: args.lnd_grpc_api}, cbk); + }, + + history: ['getInvoices', 'getPayments', (res, cbk) => { + const allTransactions = res.getInvoices.concat(res.getPayments); + + return cbk(null, _(allTransactions).sortBy(['created_at']).reverse()); + }], + }, + (err, res) => { + if (!!err) { return cbk(err); } + + return cbk(null, res.history); + }); +}; + diff --git a/libs/get_invoices.js b/libs/get_invoices.js new file mode 100644 index 00000000..ddc87a5d --- /dev/null +++ b/libs/get_invoices.js @@ -0,0 +1,47 @@ +const _ = require('lodash'); + +const msPerSecond = 1000; + +/** Get invoices + + { + lnd_grpc_api: + } + + @returns via cbk + [{ + amount: + confirmed: + created_at: + memo: + outgoing: + payment: + }] +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + return args.lnd_grpc_api.listInvoices({}, (err, res) => { + if (!!err) { return cbk([500, 'Get invoices error', err]); } + + if (!res || !res.invoices) { return cbk([500, 'Expected invoices', res]); } + + // FIXME: - find any missing expected values, do this map async + + const invoices = res.invoices.map((invoice) => { + const creationDate = parseInt(invoice.creation_date) * msPerSecond; + + return { + amount: parseInt(invoice.value), + confirmed: invoice.settled, + created_at: new Date(creationDate).toISOString(), + memo: invoice.memo, + outgoing: false, + payment: invoice.payment_request, + }; + }); + + return cbk(null, _(invoices).sortBy('created_at')); + }); +}; + diff --git a/libs/get_network_info.js b/libs/get_network_info.js new file mode 100644 index 00000000..f6370e15 --- /dev/null +++ b/libs/get_network_info.js @@ -0,0 +1,59 @@ +const _ = require('lodash'); + +/** Get network info + + { + lnd_grpc_api: + } + + @returns via cbk + { + average_channel_size: + channel_count: + maximum_channel_size: + minimum_channel_size: + node_count: + total_capacity: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + return args.lnd_grpc_api.getNetworkInfo({}, (err, networkInfo) => { + if (!!err) { return cbk([500, 'Get network info error', err]); } + + if (!_(networkInfo.num_nodes).isNumber()) { + return cbk([500, 'Expected num_nodes', networkInfo]); + } + + if (!_(networkInfo.num_channels).isNumber()) { + return cbk([500, 'Expected num_channels', networkInfo]); + } + + if (!_(networkInfo.total_network_capacity).isString()) { + return cbk([500, 'Expected total_network_capacity', networkInfo]); + } + + if (!_(networkInfo.avg_channel_size).isNumber()) { + return cbk([500, 'Expected avg_channel_size', networkInfo]); + } + + if (!_(networkInfo.min_channel_size).isString()) { + return cbk([500, 'Expected min_channel_size', networkInfo]); + } + + if (!_(networkInfo.max_channel_size).isString()) { + return cbk([500, 'Expected min_channel_size', networkInfo]); + } + + return cbk(null, { + average_channel_size: networkInfo.avg_channel_size, + channel_count: networkInfo.num_channels, + maximum_channel_size: networkInfo.max_channel_size, + minimum_channel_size: networkInfo.min_channel_size, + node_count: networkInfo.num_nodes, + total_capacity: networkInfo.total_network_capacity, + }); + }); +}; + diff --git a/libs/get_payment_request.js b/libs/get_payment_request.js new file mode 100644 index 00000000..682b797c --- /dev/null +++ b/libs/get_payment_request.js @@ -0,0 +1,44 @@ +const _ = require('lodash'); + +/** Get balance + + { + lnd_grpc_api: + payment_request: + } + + @returns via cbk + { + destination: + id: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + if (!args.payment_request) { + return cbk([500, 'Missing payment request', args]); + } + + return args.lnd_grpc_api.decodePayReq({ + pay_req: args.payment_request, + }, + (err, res) => { + if (!!err) { return cbk([500, 'Get payment request error', err]); } + + if (!res.destination) { return cbk([500, 'Expected destination', res]); } + + if (!res.payment_hash) { return cbk([500, 'Expected payment hash', res]); } + + if (res.num_satoshis === undefined) { + return cbk([500, 'Expected num satoshis', res]); + } + + return cbk(null, { + amount: parseInt(res.num_satoshis), + destination: res.destination, + id: res.payment_hash, + }); + }); +}; + diff --git a/libs/get_payments.js b/libs/get_payments.js new file mode 100644 index 00000000..d0c274a2 --- /dev/null +++ b/libs/get_payments.js @@ -0,0 +1,49 @@ +const msPerSecond = 1000; + +/** Get payments + + { + lnd_grpc_api: + } + + @returns via cbk + [{ + amount: + confirmed: + created_at: + destination: + fee: + hops: + id: // rhash + outgoing: + }] +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + return args.lnd_grpc_api.listPayments({}, (err, res) => { + if (!!err) { return cbk([500, 'Get payments error', err]); } + + if (!res || !res.payments) { return cbk([500, 'Expected payments', res]); } + + // FIXME: - find any missing expected values, do this map async + + const payments = res.payments.map((payment) => { + const creationDate = parseInt(payment.creation_date) * msPerSecond; + + return { + amount: parseInt(payment.value), + confirmed: true, + created_at: new Date(creationDate).toISOString(), + destination: payment.path[payment.path.length - 1], + fee: parseInt(payment.fee), + hops: payment.path.length - 1, + id: payment.payment_hash, + outgoing: true, + }; + }); + + return cbk(null, payments); + }); +}; + diff --git a/libs/get_peers.js b/libs/get_peers.js new file mode 100644 index 00000000..3ed2d120 --- /dev/null +++ b/libs/get_peers.js @@ -0,0 +1,32 @@ +/** Get balance + + { + lnd_grpc_api: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + return args.lnd_grpc_api.listPeers({}, (err, res) => { + if (!!err) { return cbk([500, 'Get peers error', err]); } + + if (!res || !Array.isArray(res.peers)) { + return cbk([500, 'Expected peers array', res]); + } + + // FIXME: - check for valid peer data + + const peers = res.peers.map((peer) => { + return { + amount_received: peer.sat_recv, + amount_sent: peer.sat_sent, + id: peer.peer_id, + network_address: peer.address, + public_key: peer.pub_key, + }; + }); + + return cbk(null, peers); + }); +}; + diff --git a/libs/get_wallet_info.js b/libs/get_wallet_info.js new file mode 100644 index 00000000..12db99ef --- /dev/null +++ b/libs/get_wallet_info.js @@ -0,0 +1,51 @@ +const _ = require('lodash'); + +/** Get balance +' + { + lnd_grpc_api: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api) { return cbk([500, 'Missing lnd grpc api', args]); } + + return args.lnd_grpc_api.getInfo({}, (err, res) => { + if (!!err) { return cbk([500, 'Get wallet info error', err]); } + + if (!res) { return cbk([500, 'Expected wallet info', res]); } + + if (!res.identity_pubkey) { + return cbk([500, 'Expected identity pubkey', res]); + } + + if (!_(res.num_pending_channels).isNumber()) { + return cbk([500, 'Expected num pending channels', res]); + } + + if (!_(res.num_active_channels).isNumber()) { + return cbk([500, 'Expected num active channels', res]); + } + + if (!_(res.num_peers).isNumber()) { + return cbk([500, 'Expected num peers', res]); + } + + if (!_(res.block_height).isNumber()) { + return cbk([500, 'Expected block height', res]); + } + + if (!_(res.testnet).isBoolean()) { + return cbk([500, 'Expected testnet flag', res]); + } + + return cbk(null, { + active_channels_count: res.num_active_channels, + block_height: res.block_height, + is_testnet: res.testnet, + peers_count: res.num_peers, + pending_channels_count: res.num_pending_channels, + public_key: res.identity_pubkey, + }); + }); +}; + diff --git a/libs/lnd_grpc_interface.js b/libs/lnd_grpc_interface.js new file mode 100644 index 00000000..db4f3efa --- /dev/null +++ b/libs/lnd_grpc_interface.js @@ -0,0 +1,10 @@ +const grpc = require('grpc'); + +/** GRPC interface to LND +*/ +module.exports = (path, host) => { + const rpc = grpc.load(path); + + return new rpc.lnrpc.Lightning(host, grpc.credentials.createInsecure()); +}; + diff --git a/libs/lookup_invoice.js b/libs/lookup_invoice.js new file mode 100644 index 00000000..526b9e78 --- /dev/null +++ b/libs/lookup_invoice.js @@ -0,0 +1,39 @@ +/** Lookup an invoice + + { + lnd_grpc_api: + rhash: + } + + @returns via cbk + { + memo: ' + settled: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api || !args.rhash) { + return cbk([500, 'Missing lnd grpc api or rhash', args]); + } + + return args.lnd_grpc_api.lookupInvoice({ + r_hash_str: args.rhash, + }, + (err, response) => { + if (!!err) { return cbk([500, 'Lookup invoice error', err]); } + + if (response.memo === undefined) { + return cbk([500, 'Missing memo', response]); + } + + if (response.settled === undefined) { + return cbk([500, 'Missing settled', response]); + } + + return cbk(null, { + memo: response.memo, + settled: response.settled, + }); + }); +}; + diff --git a/libs/return_json.js b/libs/return_json.js new file mode 100644 index 00000000..0cf7cb62 --- /dev/null +++ b/libs/return_json.js @@ -0,0 +1,15 @@ +/** Return JSON or error + + @returns + (err, json) => {} +*/ +module.exports = (args) => { + return (err, json) => { + if (!!err) { return args.res.status(err[0]).send({error: err[1]}); } + + if (!json) { return args.res.send(); } + + return args.res.json(json); + }; +}; + diff --git a/libs/send_payment.js b/libs/send_payment.js new file mode 100644 index 00000000..4a97bdb4 --- /dev/null +++ b/libs/send_payment.js @@ -0,0 +1,22 @@ +/** Send a payment + + { + lnd_grpc_api: + payment_request: + } +*/ +module.exports = (args, cbk) => { + if (!args.lnd_grpc_api || !args.payment_request) { + return cbk([500, 'Missing lnd grpc api or payment request', args]); + } + + return args.lnd_grpc_api.sendPaymentSync({ + payment_request: args.payment_request, + }, + (err, response) => { + if (!!err) { return cbk([500, 'Send payment error', err]); } + + return cbk(); + }); +}; + diff --git a/package.json b/package.json new file mode 100644 index 00000000..0deb365d --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "dependencies": { + "async": "2.1.5", + "body-parser": "1.17.1", + "compression": "1.6.2", + "express": "4.15.2", + "grpc": "1.2.0", + "lodash": "4.17.4", + "morgan": "1.8.1", + "walnut": "0.0.2" + }, + "engines": { + "node": "6.9.4", + "npm": "3.10.10" + }, + "main": "app.js", + "name": "bet", + "private": true, + "scripts": { + "start": "node app" + }, + "version": "1.0.0" +} \ No newline at end of file diff --git a/routers/balance.js b/routers/balance.js new file mode 100644 index 00000000..4cbdcd09 --- /dev/null +++ b/routers/balance.js @@ -0,0 +1,31 @@ +const ExpressRouter = require('express').Router; + +const getBalance = require('./../libs/get_balance'); +const returnJson = require('./../libs/return_json'); + +/** Get a history router + + { + lnd_grpc_api: + } + + @returns + +*/ +module.exports = (args) => { + if (!args.lnd_grpc_api) { + return (req, res) => { return res.status(500).send(); }; + } + + const router = ExpressRouter({caseSensitive: true, strict: true}); + + router.get('/', (req, res, next) => { + return getBalance({ + lnd_grpc_api: args.lnd_grpc_api, + }, + returnJson({res: res})); + }); + + return router; +}; + diff --git a/routers/channels.js b/routers/channels.js new file mode 100644 index 00000000..7cdf96c3 --- /dev/null +++ b/routers/channels.js @@ -0,0 +1,32 @@ +const ExpressRouter = require('express').Router; + +const returnJson = require('./../libs/return_json'); +const sendPayment = require('./../libs/send_payment'); + +/** Get a send payment router + + { + lnd_grpc_api: + } + + @returns + +*/ +module.exports = (args) => { + if (!args.lnd_grpc_api) { + return (req, res) => { return res.status(500).send(); }; + } + + const router = ExpressRouter({caseSensitive: true, strict: true}); + + router.post('/', (req, res, next) => { + return sendPayment({ + lnd_grpc_api: args.lnd_grpc_api, + payment_request: req.body.payment_request, + }, + returnJson({res: res})); + }); + + return router; +}; + diff --git a/routers/history.js b/routers/history.js new file mode 100644 index 00000000..352d304f --- /dev/null +++ b/routers/history.js @@ -0,0 +1,26 @@ +const ExpressRouter = require('express').Router; + +const getHistory = require('./../libs/get_history'); +const returnJson = require('./../libs/return_json'); + +/** Get a history router + + lnd_grpc_api: +*/ +module.exports = (args) => { + if (!args.lnd_grpc_api) { + return (req, res) => { return res.status(500).send(); }; + } + + const router = ExpressRouter({caseSensitive: true, strict: true}); + + router.get('/', (req, res, next) => { + return getHistory({ + lnd_grpc_api: args.lnd_grpc_api, + }, + returnJson({res: res})); + }); + + return router; +}; + diff --git a/routers/invoices.js b/routers/invoices.js new file mode 100644 index 00000000..854bd2a1 --- /dev/null +++ b/routers/invoices.js @@ -0,0 +1,41 @@ +const ExpressRouter = require('express').Router; + +const createInvoice = require('./../libs/create_invoice'); +const getInvoices = require('./../libs/get_invoices'); +const returnJson = require('./../libs/return_json'); + +/** Get a history router + + { + lnd_grpc_api: + } + + @returns + +*/ +module.exports = (args) => { + if (!args.lnd_grpc_api) { + return (req, res) => { return res.status(500).send(); }; + } + + const router = ExpressRouter({caseSensitive: true, strict: true}); + + router.get('/', (req, res, next) => { + return getInvoices({ + lnd_grpc_api: args.lnd_grpc_api, + }, + returnJson({res: res})); + }); + + router.post('/', (req, res, next) => { + return createInvoice({ + amount: req.body.amount, + lnd_grpc_api: args.lnd_grpc_api, + memo: req.body.memo, + }, + returnJson({res: res})); + }); + + return router; +}; + diff --git a/routers/network_info.js b/routers/network_info.js new file mode 100644 index 00000000..832e63a0 --- /dev/null +++ b/routers/network_info.js @@ -0,0 +1,33 @@ +const ExpressRouter = require('express').Router; + +const getNetworkInfo = require('./../libs/get_network_info'); + +/** Get a purchase router + + { + lnd_grpc_api: + } +*/ +module.exports = (args) => { + if (!args.lnd_grpc_api) { + return (req, res) => { + return res.status(500).json({error: 'Invalid arguments'}); + }; + } + + const router = ExpressRouter({caseSensitive: true, strict: true}); + + router.get("/", (req, res) => { + return getNetworkInfo({ + lnd_grpc_api: args.lnd_grpc_api, + }, + (err, networkInfo) => { + if (!!err) { return res.status(err[0]).json({error: err[1] || ''}); } + + return res.json(networkInfo); + }); + }); + + return router; +}; + diff --git a/routers/payment_request.js b/routers/payment_request.js new file mode 100644 index 00000000..652d733b --- /dev/null +++ b/routers/payment_request.js @@ -0,0 +1,31 @@ +const ExpressRouter = require('express').Router; + +const getPaymentRequest = require('./../libs/get_payment_request'); +const returnJson = require('./../libs/return_json'); + +/** Get a payment request details router + + { + lnd_grpc_api: + } +*/ +module.exports = (args) => { + if (!args.lnd_grpc_api) { + return (req, res) => { + return res.status(500).json({error: 'Invalid arguments'}); + }; + } + + const router = ExpressRouter({caseSensitive: true, strict: true}); + + router.get('/:payment_request', (req, res) => { + return getPaymentRequest({ + lnd_grpc_api: args.lnd_grpc_api, + payment_request: req.params.payment_request, + }, + returnJson({res: res})); + }); + + return router; +}; + diff --git a/routers/peers.js b/routers/peers.js new file mode 100644 index 00000000..06d1fd88 --- /dev/null +++ b/routers/peers.js @@ -0,0 +1,30 @@ +const ExpressRouter = require('express').Router; + +const getPeers = require('./../libs/get_peers'); +const returnJson = require('./../libs/return_json'); + +/** Get a peers router + + { + lnd_grpc_api: + } +*/ +module.exports = (args) => { + if (!args.lnd_grpc_api) { + return (req, res) => { + return res.status(500).json({error: 'Invalid arguments'}); + }; + } + + const router = ExpressRouter({caseSensitive: true, strict: true}); + + router.get("/", (req, res) => { + return getPeers({ + lnd_grpc_api: args.lnd_grpc_api, + }, + returnJson({res: res})); + }); + + return router; +}; + diff --git a/routers/purchased.js b/routers/purchased.js new file mode 100644 index 00000000..42fe56bd --- /dev/null +++ b/routers/purchased.js @@ -0,0 +1,32 @@ +const ExpressRouter = require('express').Router; + +const lookupInvoice = require('./../libs/lookup_invoice'); +const returnJson = require('./../libs/return_json'); + +/** Get a purchase router + + { + lnd_grpc_api: + } + + @returns + +*/ +module.exports = (args) => { + if (!args.lnd_grpc_api) { + return (req, res) => { return res.status(500).send(); }; + } + + const router = ExpressRouter({caseSensitive: true, strict: true}); + + router.get('/:rhash', (req, res, next) => { + return lookupInvoice({ + lnd_grpc_api: args.lnd_grpc_api, + rhash: req.params.rhash, + }, + returnJson({res: res})); + }); + + return router; +}; + diff --git a/routers/wallet_info.js b/routers/wallet_info.js new file mode 100644 index 00000000..07df0568 --- /dev/null +++ b/routers/wallet_info.js @@ -0,0 +1,31 @@ +const ExpressRouter = require('express').Router; + +const getWalletInfo = require('./../libs/get_wallet_info'); +const returnJson = require('./../libs/return_json'); + +/** Get a wallet info router + + { + lnd_grpc_api: + } + + @returns + +*/ +module.exports = (args) => { + if (!args.lnd_grpc_api) { + return (req, res) => { return res.status(500).send(); }; + } + + const router = ExpressRouter({caseSensitive: true, strict: true}); + + router.get('/', (req, res) => { + return getWalletInfo({ + lnd_grpc_api: args.lnd_grpc_api, + }, + returnJson({res: res})); + }); + + return router; +}; +