├── .circleci └── config.yml ├── .gitignore ├── .nycrc.json ├── .prettierrc ├── LICENSE ├── README.md ├── ava.config.js ├── generate.js ├── index.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ └── send-money-data.test.ts ├── account.ts ├── index.ts ├── lightning.ts ├── plugins │ ├── client.ts │ └── server.ts ├── proto │ └── rpc.proto └── types │ └── plugin.ts ├── tsconfig.json └── tslint.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/node:10 7 | working_directory: ~/repo 8 | steps: 9 | - checkout 10 | # Download and cache dependencies 11 | - restore_cache: 12 | keys: 13 | - v10-dependencies-{{ checksum "package.json" }} 14 | # fallback to using the latest cache if no exact match is found 15 | - v10-dependencies- 16 | - run: 17 | name: Install dependencies 18 | command: npm install 19 | - save_cache: 20 | paths: 21 | - node_modules 22 | key: v10-dependencies-{{ checksum "package.json" }} 23 | - run: 24 | name: Build and compile 25 | command: npm run build 26 | - run: 27 | name: Run tests 28 | command: npm test 29 | - run: 30 | name: Lint files 31 | command: npm run lint 32 | - run: 33 | name: Upload code coverage 34 | command: npx codecov 35 | - persist_to_workspace: 36 | root: ~/repo 37 | paths: . 38 | 39 | publish: 40 | docker: 41 | - image: circleci/node:10 42 | working_directory: ~/repo 43 | steps: 44 | - attach_workspace: 45 | at: ~/repo 46 | - run: 47 | name: Authenticate with registry 48 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc 49 | - run: 50 | name: Publish package 51 | command: npm publish 52 | 53 | workflows: 54 | version: 2 55 | build_and_publish: 56 | jobs: 57 | - build: 58 | filters: 59 | tags: 60 | only: /.*/ 61 | - publish: 62 | requires: 63 | - build 64 | filters: 65 | tags: 66 | only: /^v.*/ 67 | branches: 68 | ignore: /.*/ 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Compiled javascript 61 | build/ 62 | generated/ 63 | 64 | # Editor configs 65 | .vscode/ 66 | 67 | # Mac-specific 68 | .DS_Store 69 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "exclude": ["**/__tests__/**/*.ts", "**/*.d.ts"], 4 | "extension": [".ts"], 5 | "require": ["ts-node/register"], 6 | "reporter": ["text", "html", "lcov"] 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interledger Lightning Plugin 2 | 3 | [![NPM Package](https://img.shields.io/npm/v/ilp-plugin-lightning.svg?style=flat-square&logo=npm)](https://npmjs.org/package/ilp-plugin-lightning) 4 | [![CircleCI](https://img.shields.io/circleci/project/github/interledgerjs/ilp-plugin-lightning/master.svg?style=flat-square&logo=circleci)](https://circleci.com/gh/interledgerjs/ilp-plugin-lightning/master) 5 | [![Codecov](https://img.shields.io/codecov/c/github/interledgerjs/ilp-plugin-lightning/master.svg?style=flat-square&logo=codecov)](https://codecov.io/gh/interledgerjs/ilp-plugin-lightning) 6 | [![Prettier](https://img.shields.io/badge/code_style-prettier-brightgreen.svg?style=flat-square)](https://prettier.io/) 7 | [![Apache 2.0 License](https://img.shields.io/github/license/interledgerjs/ilp-plugin-lightning.svg?style=flat-square)](https://github.com/interledgerjs/ilp-plugin-lightning/blob/master/LICENSE) 8 | 9 | :rotating_light: **Expect breaking changes while this plugin is in beta.** 10 | 11 | ## Overview 12 | 13 | `ilp-plugin-lightning` enables settlements between Interledger peers using the [Lightning Network](https://lightning.network/) on Bitcoin. Using the [ILP/Stream](https://github.com/interledger/rfcs/blob/master/0029-stream/0029-stream.md) protocol, payments are chunked down into small increments, which can facilitate faster and more relaible payments compared with native Lightning! 14 | 15 | The integration requires an existing Lightning node with connectivity to the greater Lightning network. Note that speed within the Lightning network degrades as two peers have more degrees of separation, and opening a direct channel provides a much faster experience. 16 | 17 | Additional information on the Lightning Network is available [here](https://dev.lightning.community/). 18 | 19 | ## Install 20 | 21 | ```bash 22 | npm install ilp-plugin-lightning 23 | ``` 24 | 25 | Requires Node.js 10+. 26 | 27 | ## API 28 | 29 | Here are the available options to pass to the plugin. Additional configuration options are also inherited from [ilp-plugin-btp](https://github.com/interledgerjs/ilp-plugin-btp) if the plugin is a client, and [ilp-plugin-mini-accounts](https://github.com/interledgerjs/ilp-plugin-mini-accounts) if the plugin is a server. 30 | 31 | Clients do not settle automatically. Sending Lightning payments can be triggered by invoking `sendMoney` on the plugin, and the money handler is called upon receipt of incoming payments (set using `registerMoneyHandler`). 32 | 33 | The balance configuration has been simplified for servers. Clients must prefund before sending any packets through a server, and if a client fulfills packets sent to them through a server, the server will automatically settle such that they owe 0 to the client. This configuration was chosen as a default due to it's security and protection against deadlocks. 34 | 35 | #### `role` 36 | 37 | - Type: 38 | - `"client"` to connect to a single counterparty 39 | - `"server"` enables multiple counterparties to connect 40 | - Default: `"client"` 41 | 42 | #### `lnd` 43 | 44 | - **Required** 45 | - Type: `LndOpts` 46 | - Credentials to create a connection to the LND node, or an already constructed LND service 47 | 48 | To have the plugin create the connection internally, provide an object with the following properties: 49 | 50 | ##### `macaroon` 51 | 52 | - **Required** 53 | - Type: `string` or `Buffer` 54 | - LND macaroon to used authenticate daemon requests as a Base64-encoded string or Buffer (e.g. using `fs.readFile`) 55 | 56 | ##### `tlsCert` 57 | 58 | - **Required** 59 | - Type: `string` or `Buffer` 60 | - TLS certificate to authenticate the connection to the Lightning daemon as a Base64-encoded string or Buffer (e.g. using `fs.readFile`) 61 | 62 | ##### `hostname` 63 | 64 | - **Required** 65 | - Type: `string` 66 | - Hostname of the Lightning node 67 | 68 | ##### `grpcPort` 69 | 70 | - Type: `number` 71 | - Default: `10009` 72 | - Port of LND gRPC server 73 | 74 | For example: 75 | 76 | ```js 77 | { 78 | hostname: 'localhost', 79 | macaroon: 80 | 'AgEDbG5kArsBAwoQ3/I9f6kgSE6aUPd85lWpOBIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV32ml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFgoHbWVzc2FnZRIEcmVhZBIFd3JpdGUaFwoIb2ZmY2hhaW4SBHJlYWQSBXdyaXRlGhYKB29uY2hhaW4SBHJlYWQSBXdyaXRlGhQKBXBlZXJzEgRyZWFkEgV3cml0ZQAABiAiUTBv3Eh6iDbdjmXCfNxp4HBEcOYNzXhrm+ncLHf5jA==', 81 | tlsCert: 82 | 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNpRENDQWkrZ0F3SUJBZ0lRZG81djBRQlhIbmppNGhSYWVlTWpOREFLQmdncWhrak9QUVFEQWpCSE1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1TUXdJZ1lEVlFRREV4dEtkWE4wZFhOegpMVTFoWTBKdmIyc3RVSEp2TFRNdWJHOWpZV3d3SGhjTk1UZ3dPREl6TURVMU9ERXdXaGNOTVRreE1ERTRNRFUxCk9ERXdXakJITVI4d0hRWURWUVFLRXhac2JtUWdZWFYwYjJkbGJtVnlZWFJsWkNCalpYSjBNU1F3SWdZRFZRUUQKRXh0S2RYTjBkWE56TFUxaFkwSnZiMnN0VUhKdkxUTXViRzlqWVd3d1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtpTwpQUU1CQndOQ0FBU0ZoUm0rdy9UMTBQb0t0ZzRsbTloQk5KakpENDczZmt6SHdQVUZ3eTkxdlRyUVNmNzU0M2oyCkpyZ0ZvOG1iVFYwVnRwZ3FrZksxSU1WS01MckYyMXhpbzRIOE1JSDVNQTRHQTFVZER3RUIvd1FFQXdJQ3BEQVAKQmdOVkhSTUJBZjhFQlRBREFRSC9NSUhWQmdOVkhSRUVnYzB3Z2NxQ0cwcDFjM1IxYzNNdFRXRmpRbTl2YXkxUQpjbTh0TXk1c2IyTmhiSUlKYkc5allXeG9iM04wZ2dSMWJtbDRnZ3AxYm1sNGNHRmphMlYwaHdSL0FBQUJoeEFBCkFBQUFBQUFBQUFBQUFBQUFBQUFCaHhEK2dBQUFBQUFBQUFBQUFBQUFBQUFCaHhEK2dBQUFBQUFBQUF3bGM5WmMKazdiRGh3VEFxQUVFaHhEK2dBQUFBQUFBQUJpTnAvLytHeFhHaHhEK2dBQUFBQUFBQUtXSjV0bGlET1JqaHdRSwpEd0FDaHhEK2dBQUFBQUFBQUc2V3ovLyszYXRGaHhEOTJ0RFF5djRUQVFBQUFBQUFBQkFBTUFvR0NDcUdTTTQ5CkJBTUNBMGNBTUVRQ0lBOU85eHRhem1keENLajBNZmJGSFZCcTVJN0pNbk9GUHB3UlBKWFFmcllhQWlCZDVOeUoKUUN3bFN4NUVDblBPSDVzUnB2MjZUOGFVY1hibXlueDlDb0R1ZkE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==' 83 | } 84 | ``` 85 | 86 | #### `maxPacketAmount` 87 | 88 | - Type: [`BigNumber`](http://mikemcl.github.io/bignumber.js/), `number`, or `string` 89 | - Default: `Infinity` 90 | - Maximum amount in _satoshis_ above which an incoming ILP packet should be rejected 91 | 92 | ## Bilateral Communication 93 | 94 | This plugin uses the [Bilateral Transfer Protocol](https://github.com/interledger/rfcs/blob/master/0023-bilateral-transfer-protocol/0023-bilateral-transfer-protocol.md) over WebSockets to send messages between peers. Two subprotocols are supported: 95 | 96 | #### `peeringRequest` 97 | 98 | - Format: `[Identity public key]`, UTF-8 encoded 99 | - Used for sharing pubkey of our Lightning node with the peer 100 | - Only shares pubkey, does not attempt to peer over the Lightning network 101 | 102 | #### `paymentRequest` 103 | 104 | - Format: [BOLT11](https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md) encoded, then UTF-8 encoded 105 | - Used to send a invoice to the Interledger peer, so they have the ability to send payments to our instance 106 | - By default, peers send 20 invoices ahead of time, and share an additional invoice as each invoice expires or is paid 107 | 108 | ## Known Issues 109 | 110 | - LND does not currently support pruning invoices (neither automatically nor manually). As this plugin may generate several invoices per second when a peer is actively streaming money, this can significantly increase the footprint of the LND database. 111 | - LND may soon support [spontaneous payments](https://github.com/lightningnetwork/lnd/pull/2455), which would eliminate the overhead of frequently sharing invoices. 112 | - The plugin does not perform any accounting for Lightning Network fees. 113 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | files: ['src/__tests__/**/*.ts'], 3 | failFast: true, 4 | verbose: true, 5 | serial: true, 6 | timeout: '3m', 7 | compileEnhancements: false, 8 | extensions: ['ts'], 9 | require: ['ts-node/register'] 10 | } 11 | -------------------------------------------------------------------------------- /generate.js: -------------------------------------------------------------------------------- 1 | const { readFile, writeFile } = require('fs') 2 | const { promisify } = require('util') 3 | const { resolve } = require('path') 4 | 5 | async function run() { 6 | const path = resolve('./generated/rpc.d.ts') 7 | promisify(readFile)(path, 'utf8') 8 | .then( 9 | data => 10 | // Import stream types from grpc-js 11 | `import { ClientReadableStream, ClientDuplexStream } from "@grpc/grpc-js/build/src/call"\n` + 12 | data 13 | // Fix types provide RPC implementation with streaming 14 | .split('rpcImpl: $protobuf.RPCImpl') 15 | .join('rpcImpl: $protobuf.RPCImpl | $protobuf.RPCHandler') 16 | // Change stream types to work with grpc-js 17 | .split('$protobuf.RPCServerStream') 18 | .join('ClientReadableStream') 19 | .split('$protobuf.RPCBidiStream') 20 | .join('ClientDuplexStream') 21 | ) 22 | .then(data => promisify(writeFile)(path, data)) 23 | } 24 | 25 | run().catch(err => console.error(err)) 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // This is a hack to allow default and named CommonJS exports 4 | const bundle = require('./build') 5 | module.exports = bundle.default 6 | for (let p in bundle) { 7 | if (!module.exports.hasOwnProperty(p)) { 8 | module.exports[p] = bundle[p] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ilp-plugin-lightning", 3 | "version": "1.0.0-beta.25", 4 | "description": "Settle interledger payments using the Lightning Network", 5 | "main": "index.js", 6 | "types": "build/index.d.ts", 7 | "files": [ 8 | "build/**/*", 9 | "generated/**/*", 10 | "!build/__tests__" 11 | ], 12 | "scripts": { 13 | "build": "tsc", 14 | "prepare": "run-s generate:*", 15 | "generate:dir": "make-dir generated", 16 | "generate:js": "pbjs -t static-module -w commonjs -o generated/rpc.js src/proto/rpc.proto", 17 | "generate:ts": "pbts -o generated/rpc.d.ts generated/rpc.js", 18 | "generate:fix": "node generate.js", 19 | "test": "nyc ava", 20 | "test-inspect": "node --inspect-brk node_modules/ava/profile.js", 21 | "lint": "tslint --fix --project .", 22 | "fix": "run-s format lint", 23 | "format": "prettier --write 'src/**/*.ts'" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/interledgerjs/ilp-plugin-lightning.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/interledgerjs/ilp-plugin-lightning/issues" 31 | }, 32 | "keywords": [ 33 | "interledger", 34 | "ilp", 35 | "streaming", 36 | "payments", 37 | "lightning", 38 | "lightning-network", 39 | "micropayments" 40 | ], 41 | "contributors": [ 42 | "Kevin Davis ", 43 | "Austin King ", 44 | "Kincaid O'Neil (https://kincaidoneil.com/)" 45 | ], 46 | "license": "Apache-2.0", 47 | "dependencies": { 48 | "@grpc/grpc-js": "^0.3.6", 49 | "bignumber.js": "^7.2.1", 50 | "btp-packet": "^2.2.0", 51 | "debug": "^4.1.1", 52 | "eventemitter2": "^5.0.1", 53 | "ilp-logger": "^1.0.2", 54 | "ilp-packet": "^3.0.7", 55 | "ilp-plugin-btp": "^1.3.10", 56 | "ilp-plugin-mini-accounts": "^4.0.2", 57 | "protobufjs": "github:trackforce/protobuf.js#86f968acd6b9b1489059f08e48c9469ba8d4fba1", 58 | "rxjs": "^6.4.0" 59 | }, 60 | "devDependencies": { 61 | "@types/debug": "^4.1.3", 62 | "@types/get-port": "^4.2.0", 63 | "@types/node": "^10.12.18", 64 | "ava": "^1.4.1", 65 | "get-port": "^4.2.0", 66 | "make-dir-cli": "^2.0.0", 67 | "npm-run-all": "^4.1.5", 68 | "nyc": "^13.3.0", 69 | "prettier": "^1.16.4", 70 | "ts-node": "^8.0.3", 71 | "tslint": "^5.14.0", 72 | "tslint-config-prettier": "^1.18.0", 73 | "tslint-config-standard": "^8.0.1", 74 | "tslint-eslint-rules": "^5.4.0", 75 | "typescript": "^3.4.1" 76 | }, 77 | "engines": { 78 | "node": ">=10.0.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/__tests__/send-money-data.test.ts: -------------------------------------------------------------------------------- 1 | import anyTest, { TestInterface } from 'ava' 2 | import BigNumber from 'bignumber.js' 3 | import getPort from 'get-port' 4 | import LightningPlugin from '..' 5 | import { convert, Unit } from '../account' 6 | import { createHash, randomBytes } from 'crypto' 7 | import { promisify } from 'util' 8 | import { 9 | IlpPrepare, 10 | IlpFulfill, 11 | serializeIlpPrepare, 12 | serializeIlpFulfill 13 | } from 'ilp-packet' 14 | import { base64url } from 'btp-packet' 15 | 16 | const test = anyTest as TestInterface<{ 17 | clientPlugin: LightningPlugin 18 | serverPlugin: LightningPlugin 19 | }> 20 | 21 | test.beforeEach(async t => { 22 | const port = await getPort() 23 | 24 | const token = 'secret' 25 | const clientPlugin = new LightningPlugin({ 26 | role: 'client', 27 | server: `btp+ws://:${token}@localhost:${port}`, 28 | lnd: { 29 | tlsCert: process.env.LND_TLSCERT_C_BASE64!, 30 | macaroon: process.env.LND_MACAROON_C_BASE64!, 31 | hostname: process.env.LND_PEERHOST_C!, 32 | grpcPort: parseInt(process.env.LND_GRPCPORT_C!, 10) 33 | } 34 | }) 35 | 36 | const serverPlugin = new LightningPlugin({ 37 | role: 'server', 38 | port, 39 | debugHostIldcpInfo: { 40 | assetCode: 'BTC', 41 | assetScale: 8, 42 | clientAddress: 'private.btc' 43 | }, 44 | lnd: { 45 | tlsCert: process.env.LND_TLSCERT_B_BASE64!, 46 | macaroon: process.env.LND_MACAROON_B_BASE64!, 47 | hostname: process.env.LND_PEERHOST_B!, 48 | grpcPort: parseInt(process.env.LND_GRPCPORT_B!, 10) 49 | } 50 | }) 51 | 52 | await serverPlugin.connect() 53 | await clientPlugin.connect() 54 | 55 | t.context = { 56 | clientPlugin, 57 | serverPlugin 58 | } 59 | }) 60 | 61 | test.afterEach(async t => { 62 | const { clientPlugin, serverPlugin } = t.context 63 | 64 | await serverPlugin.disconnect() 65 | await clientPlugin.disconnect() 66 | 67 | clientPlugin.deregisterDataHandler() 68 | serverPlugin.deregisterDataHandler() 69 | 70 | clientPlugin.deregisterMoneyHandler() 71 | serverPlugin.deregisterMoneyHandler() 72 | }) 73 | 74 | test('sends money and data between clients and servers', async t => { 75 | t.plan(6) 76 | 77 | const PREFUND_AMOUNT = convert(0.0002, Unit.BTC, Unit.Satoshi) 78 | const SEND_AMOUNT = convert(0.00014, Unit.BTC, Unit.Satoshi) 79 | 80 | const { clientPlugin, serverPlugin } = t.context 81 | 82 | clientPlugin.registerMoneyHandler(async () => { 83 | t.fail(`server sent money to client when it wasn't supposed to`) 84 | }) 85 | 86 | // Prefund the server 87 | await new Promise(async resolve => { 88 | serverPlugin.registerMoneyHandler(async amount => { 89 | t.true( 90 | new BigNumber(amount).isEqualTo(PREFUND_AMOUNT), 91 | 'server receives exactly the amount the client prefunded' 92 | ) 93 | resolve() 94 | }) 95 | 96 | await t.notThrowsAsync(clientPlugin.sendMoney(PREFUND_AMOUNT.toString())) 97 | }) 98 | 99 | serverPlugin.deregisterMoneyHandler() 100 | serverPlugin.registerMoneyHandler(async () => { 101 | t.fail(`client sent money to the server when it wasn't supposed to`) 102 | }) 103 | 104 | await new Promise(async resolve => { 105 | const destination = `private.btc.${base64url( 106 | createHash('sha256') 107 | .update('secret') 108 | .digest() 109 | )}` 110 | const fulfillment = await promisify(randomBytes)(32) 111 | const condition = createHash('sha256') 112 | .update(fulfillment) 113 | .digest() 114 | 115 | const prepare: IlpPrepare = { 116 | destination, 117 | amount: SEND_AMOUNT.toString(), 118 | executionCondition: condition, 119 | expiresAt: new Date(Date.now() + 5000), 120 | data: Buffer.alloc(0) 121 | } 122 | 123 | const fulfill: IlpFulfill = { 124 | fulfillment, 125 | data: Buffer.alloc(0) 126 | } 127 | 128 | serverPlugin.registerDataHandler(data => { 129 | t.true( 130 | data.equals(serializeIlpPrepare(prepare)), 131 | 'server receives PREPARE from client' 132 | ) 133 | 134 | return serverPlugin.sendData(data) 135 | }) 136 | 137 | clientPlugin.registerDataHandler(async data => { 138 | t.true( 139 | data.equals(serializeIlpPrepare(prepare)), 140 | 'server forwards PREPARE packet' 141 | ) 142 | 143 | clientPlugin.deregisterMoneyHandler() 144 | clientPlugin.registerMoneyHandler(async amount => { 145 | t.true( 146 | new BigNumber(amount).isEqualTo(SEND_AMOUNT), 147 | 'server will send settlement to client for value of fulfilled packet' 148 | ) 149 | resolve() 150 | }) 151 | 152 | return serializeIlpFulfill(fulfill) 153 | }) 154 | 155 | const reply = await clientPlugin.sendData(serializeIlpPrepare(prepare)) 156 | t.true( 157 | reply.equals(serializeIlpFulfill(fulfill)), 158 | 'server returns FULFILL packet to client' 159 | ) 160 | }) 161 | }) 162 | -------------------------------------------------------------------------------- /src/account.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js' 2 | import { 3 | MIME_APPLICATION_OCTET_STREAM, 4 | MIME_TEXT_PLAIN_UTF8, 5 | TYPE_MESSAGE 6 | } from 'btp-packet' 7 | import { randomBytes } from 'crypto' 8 | import { 9 | deserializeIlpPrepare, 10 | deserializeIlpReply, 11 | Errors, 12 | errorToReject, 13 | IlpPrepare, 14 | IlpReply, 15 | isFulfill, 16 | isReject 17 | } from 'ilp-packet' 18 | import { BtpPacket, BtpPacketData, BtpSubProtocol } from 'ilp-plugin-btp' 19 | import { BehaviorSubject } from 'rxjs' 20 | import { promisify } from 'util' 21 | import LightningPlugin from '.' 22 | import { lnrpc } from '../generated/rpc' 23 | import {createPaymentRequest, payInvoice } from './lightning' 24 | import { DataHandler, MoneyHandler } from './types/plugin' 25 | 26 | // Used to denominate which asset scale we are using 27 | export enum Unit { 28 | BTC = 8, 29 | Satoshi = 0 30 | } 31 | 32 | // Simple conversion for BTC <-> Satoshi 33 | export const convert = ( 34 | num: BigNumber.Value, 35 | from: Unit, 36 | to: Unit 37 | ): BigNumber => new BigNumber(num).shiftedBy(from - to) 38 | 39 | export const format = (num: BigNumber.Value, from: Unit) => 40 | convert(num, from, Unit.Satoshi) + ' satoshis' 41 | 42 | export type ParsedSubprotocol = string | Buffer | undefined 43 | 44 | export const getSubProtocol = ( 45 | message: BtpPacketData, 46 | name: string 47 | ): ParsedSubprotocol => { 48 | const subProtocol = message.protocolData.find( 49 | (p: BtpSubProtocol) => p.protocolName === name 50 | ) 51 | if (subProtocol) { 52 | const { contentType, data } = subProtocol 53 | return contentType === MIME_APPLICATION_OCTET_STREAM 54 | ? data 55 | : data.toString() 56 | } 57 | } 58 | 59 | export const generateBtpRequestId = async () => 60 | (await promisify(randomBytes)(4)).readUInt32BE(0) 61 | 62 | export default class LightningAccount { 63 | /** Hash/account identifier in ILP address */ 64 | readonly accountName: string 65 | 66 | /** Incoming amount owed to us by our peer for their packets we've forwarded */ 67 | readonly receivableBalance$: BehaviorSubject 68 | 69 | /** Outgoing amount owed by us to our peer for packets we've sent to them */ 70 | readonly payableBalance$: BehaviorSubject 71 | 72 | /** 73 | * Amount of failed outgoing settlements that is owed to the peer, but not reflected 74 | * in the payableBalance (e.g. due to sendMoney calls on client) 75 | */ 76 | readonly payoutAmount$: BehaviorSubject 77 | 78 | /** Lightning public key linked for the session */ 79 | peerIdentityPublicKey?: string 80 | 81 | /** Expose access to common configuration across accounts */ 82 | private readonly master: LightningPlugin 83 | 84 | /** Send the given BTP packet message to this counterparty */ 85 | private readonly sendMessage: (message: BtpPacket) => Promise 86 | 87 | /** Data handler from plugin for incoming ILP packets */ 88 | private readonly dataHandler: DataHandler 89 | 90 | /** Money handler from plugin for incoming money */ 91 | private readonly moneyHandler: MoneyHandler 92 | 93 | /** 94 | * Requests that this instance pays the the peer/counterparty, to be paid in FIFO order 95 | * - Cached for duration of session 96 | */ 97 | private incomingInvoices: { 98 | paymentRequest: string 99 | paymentHash: string 100 | expiry: BigNumber // UNIX timestamp denoting when the invoice expires in LND (seconds) 101 | }[] = [] 102 | 103 | /** 104 | * Requests that the peer/counterparty pays this instance 105 | * - Mapping of paymentRequest to a timer to send a new invoice before this one expires 106 | * - Cached for duration of session 107 | */ 108 | 109 | private outgoingInvoices = new Map() 110 | 111 | /** 112 | * Binding to the internal invoice handler for incoming payments 113 | * (on the instance so it the listener can be removed from the event emitter later) 114 | */ 115 | private invoiceHandler = (data: lnrpc.IInvoice) => 116 | this.handleIncomingPayment(data) 117 | 118 | constructor({ 119 | accountName, 120 | payableBalance$, 121 | receivableBalance$, 122 | payoutAmount$, 123 | master, 124 | sendMessage, 125 | dataHandler, 126 | moneyHandler 127 | }: { 128 | accountName: string 129 | payableBalance$: BehaviorSubject 130 | receivableBalance$: BehaviorSubject 131 | payoutAmount$: BehaviorSubject 132 | master: LightningPlugin 133 | // Wrap _call/expose method to send WS messages 134 | sendMessage: (message: BtpPacket) => Promise 135 | dataHandler: DataHandler 136 | moneyHandler: MoneyHandler 137 | }) { 138 | this.master = master 139 | this.sendMessage = sendMessage 140 | this.dataHandler = dataHandler 141 | this.moneyHandler = moneyHandler 142 | 143 | this.accountName = accountName 144 | 145 | this.payableBalance$ = payableBalance$ 146 | this.receivableBalance$ = receivableBalance$ 147 | this.payoutAmount$ = payoutAmount$ 148 | } 149 | 150 | async connect() { 151 | this.master._invoiceStream!.on('data', (data: lnrpc.IInvoice) => 152 | this.handleIncomingPayment(data) 153 | ) 154 | 155 | await this.sendPeeringInfo() 156 | 157 | // Send 10 invoices ahead of time so the peer has the ability to pay us 158 | for (const _ of [...Array(10)]) { 159 | await this.sendInvoice() 160 | } 161 | } 162 | 163 | async handleData({ data }: BtpPacket): Promise { 164 | const peeringRequest = getSubProtocol(data, 'peeringRequest') 165 | if (typeof peeringRequest === 'string') { 166 | try { 167 | const [identityPublicKey, host] = peeringRequest.split('@') 168 | 169 | // Lightning public key and invoices are linked for the duration of the session 170 | const linkedPubkey = this.peerIdentityPublicKey 171 | if (linkedPubkey && linkedPubkey !== identityPublicKey) { 172 | throw new Error( 173 | `${linkedPubkey} is already linked to account ${ 174 | this.accountName 175 | } for the remainder of the session` 176 | ) 177 | } 178 | 179 | this.peerIdentityPublicKey = identityPublicKey 180 | } catch (err) { 181 | throw new Error(`Failed to add peer: ${err.message}`) 182 | } 183 | } 184 | 185 | const paymentRequest = getSubProtocol(data, 'paymentRequest') 186 | if (typeof paymentRequest === 'string') { 187 | // Throws if the invoice wasn't signed correctly 188 | const { 189 | numSatoshis, 190 | destination, 191 | timestamp, 192 | paymentHash, 193 | expiry 194 | } = await this.master._lightning.decodePayReq({ 195 | payReq: paymentRequest 196 | }) 197 | 198 | if (!this.peerIdentityPublicKey) { 199 | throw new Error( 200 | `Cannot accept incoming invoice: no public key linked to account` 201 | ) 202 | } 203 | 204 | // Payee = entity to whom money is paid (the peer) 205 | const toPeer = destination === this.peerIdentityPublicKey 206 | if (!toPeer) { 207 | throw new Error( 208 | `Invalid incoming invoice: ${destination} does not match the peer's public key: ${ 209 | this.peerIdentityPublicKey 210 | }` 211 | ) 212 | } 213 | 214 | const anyAmount = new BigNumber(numSatoshis.toString()).isZero() 215 | if (!anyAmount) { 216 | throw new Error( 217 | `Invalid incoming invoice: amount of ${numSatoshis} does not allow paying an arbitrary amount` 218 | ) 219 | } 220 | 221 | this.incomingInvoices.push({ 222 | paymentRequest, 223 | paymentHash, 224 | expiry: new BigNumber(expiry.toString()).plus(timestamp.toString()) 225 | }) 226 | 227 | // Since there is a new invoice, attempt settlement 228 | this.sendMoney().catch(err => 229 | this.master._log.error(`Error during settlement: ${err.message}`) 230 | ) 231 | } 232 | 233 | // Handle incoming ILP PREPARE packets from peer 234 | // plugin-btp handles correlating the response packets for the dataHandler 235 | const ilp = getSubProtocol(data, 'ilp') 236 | if (Buffer.isBuffer(ilp)) { 237 | return this.handlePrepare(ilp) 238 | } 239 | 240 | return [] 241 | } 242 | 243 | /** 244 | * Handle Lightning-specific messages between peers 245 | */ 246 | 247 | private async sendPeeringInfo(): Promise { 248 | this.master._log.debug(`Sharing identity pubkey with peer`) 249 | 250 | await this.sendMessage({ 251 | type: TYPE_MESSAGE, 252 | requestId: await generateBtpRequestId(), 253 | data: { 254 | protocolData: [ 255 | { 256 | protocolName: 'peeringRequest', 257 | contentType: MIME_TEXT_PLAIN_UTF8, 258 | data: Buffer.from(this.master._lightningAddress!, 'utf8') 259 | } 260 | ] 261 | } 262 | }).catch(err => 263 | this.master._log.error( 264 | `Error while exchanging peering info: ${err.message}` 265 | ) 266 | ) 267 | } 268 | 269 | private async sendInvoice(): Promise { 270 | this.master._log.info(`Sending payment request to peer`) 271 | 272 | /** 273 | * Since each invoice is only associated with this account, and 274 | * we assume Lightning will never generate a duplicate invoice, 275 | * no single invoice should be credited to more than one account. 276 | * 277 | * Per https://api.lightning.community/#addinvoice 278 | * "Any duplicated invoices are rejected, therefore all invoices must have a unique payment preimage." 279 | */ 280 | 281 | const paymentRequest = await createPaymentRequest(this.master._lightning) 282 | 283 | /** 284 | * In 55 minutes, send a new invoice to replace this one 285 | * LND default expiry is 1 hour (3600 seconds), since we didn't specify one 286 | */ 287 | const expiry = 3300 288 | this.outgoingInvoices.set( 289 | paymentRequest, 290 | setTimeout(() => { 291 | this.outgoingInvoices.delete(paymentRequest) 292 | this.sendInvoice().catch(err => 293 | this.master._log.error( 294 | `Failed to replace soon-expiring invoice (peer may become unable to pay us): ${ 295 | err.message 296 | }` 297 | ) 298 | ) 299 | }, expiry * 1000) 300 | ) 301 | 302 | await this.sendMessage({ 303 | type: TYPE_MESSAGE, 304 | requestId: await generateBtpRequestId(), 305 | data: { 306 | protocolData: [ 307 | { 308 | protocolName: 'paymentRequest', 309 | contentType: MIME_TEXT_PLAIN_UTF8, 310 | data: Buffer.from(paymentRequest, 'utf8') 311 | } 312 | ] 313 | } 314 | }).catch(err => 315 | this.master._log.error(`Error while exchanging invoice: ${err.message}`) 316 | ) 317 | } 318 | 319 | /** 320 | * Send settlements and credit incoming settlements 321 | */ 322 | 323 | private handleIncomingPayment({ 324 | paymentRequest, 325 | amtPaidSat, 326 | settled 327 | }: lnrpc.IInvoice) { 328 | if (!paymentRequest || !amtPaidSat || !settled) { 329 | return 330 | } 331 | 332 | const isPaid = settled 333 | const isLinkedToAccount = this.outgoingInvoices.has(paymentRequest) 334 | 335 | if (isPaid && isLinkedToAccount) { 336 | clearTimeout(this.outgoingInvoices.get(paymentRequest)!) // Remove expiry timer to replace this invoice 337 | this.outgoingInvoices.delete(paymentRequest) 338 | 339 | const amount = amtPaidSat.toString() 340 | this.master._log.info( 341 | `Received incoming payment for ${format(amount, Unit.Satoshi)}` 342 | ) 343 | 344 | this.receivableBalance$.next(this.receivableBalance$.value.minus(amount)) 345 | 346 | this.moneyHandler(amount).catch(err => 347 | this.master._log.error(`Error in money handler: ${err.message}`) 348 | ) 349 | 350 | // Send another invoice to the peer so they're still able to pay us 351 | this.sendInvoice().catch(err => 352 | this.master._log.error( 353 | `Failed to send invoice (peer may become unable to pay us): ${ 354 | err.message 355 | }` 356 | ) 357 | ) 358 | } 359 | } 360 | 361 | async sendMoney(amount?: string): Promise { 362 | const amountToSend = amount || BigNumber.max(0, this.payableBalance$.value) 363 | this.payoutAmount$.next(this.payoutAmount$.value.plus(amountToSend)) 364 | 365 | const settlementBudget = this.payoutAmount$.value 366 | if (settlementBudget.isLessThanOrEqualTo(0)) { 367 | return 368 | } 369 | 370 | this.payableBalance$.next( 371 | this.payableBalance$.value.minus(settlementBudget) 372 | ) 373 | 374 | // payoutAmount$ is positive and CANNOT go below 0 375 | this.payoutAmount$.next( 376 | BigNumber.min(0, this.payoutAmount$.value.minus(settlementBudget)) 377 | ) 378 | 379 | try { 380 | // Prune invoices that expire within the next minute 381 | const minuteFromNow = new BigNumber(Date.now()).dividedBy(1000).plus(60) // Unix timestamp for 1 minute from now 382 | this.incomingInvoices = this.incomingInvoices.filter(({ expiry }) => 383 | minuteFromNow.isLessThan(expiry) 384 | ) 385 | 386 | // Get the oldest invoice as the one to pay 387 | // Remove it immediately so we don't pay it twice 388 | const invoice = this.incomingInvoices.shift() 389 | if (!invoice) { 390 | throw new Error('no valid cached invoices to pay') 391 | } 392 | 393 | this.master._log.debug( 394 | `Settlement triggered with ${this.accountName} for ${format( 395 | settlementBudget, 396 | Unit.Satoshi 397 | )}` 398 | ) 399 | 400 | const { paymentRequest, paymentHash } = invoice 401 | await payInvoice( 402 | this.master._paymentStream!, 403 | paymentRequest, 404 | paymentHash, 405 | settlementBudget 406 | ) 407 | 408 | this.master._log.info( 409 | `Successfully settled with ${this.peerIdentityPublicKey} for ${format( 410 | amountToSend, 411 | Unit.Satoshi 412 | )}` 413 | ) 414 | } catch (err) { 415 | this.payableBalance$.next( 416 | this.payableBalance$.value.plus(settlementBudget) 417 | ) 418 | 419 | // payoutAmount$ is positive and CANNOT go below 0 420 | this.payoutAmount$.next( 421 | BigNumber.max(0, this.payoutAmount$.value.plus(settlementBudget)) 422 | ) 423 | 424 | this.master._log.error('Failed to settle:', err) 425 | } 426 | } 427 | 428 | unload() { 429 | this.master._invoiceStream!.off('data', this.invoiceHandler) 430 | 431 | // Don't refresh existing invoices 432 | this.outgoingInvoices.forEach(timer => clearTimeout(timer)) 433 | this.master._accounts.delete(this.accountName) 434 | } 435 | 436 | /** 437 | * Generic plugin boilerplate (not specific to Lightning) 438 | */ 439 | 440 | private async handlePrepare(data: Buffer) { 441 | try { 442 | const { amount } = deserializeIlpPrepare(data) 443 | const amountBN = new BigNumber(amount) 444 | 445 | if (amountBN.gt(this.master._maxPacketAmount)) { 446 | throw new Errors.AmountTooLargeError('Packet size is too large.', { 447 | receivedAmount: amount, 448 | maximumAmount: this.master._maxPacketAmount.toString() 449 | }) 450 | } 451 | 452 | const newBalance = this.receivableBalance$.value.plus(amount) 453 | if (newBalance.isGreaterThan(this.master._maxBalance)) { 454 | this.master._log.debug( 455 | `Cannot forward PREPARE: cannot debit ${format( 456 | amount, 457 | Unit.Satoshi 458 | )}: proposed balance of ${format( 459 | newBalance, 460 | Unit.Satoshi 461 | )} exceeds maximum of ${format( 462 | this.master._maxBalance, 463 | Unit.Satoshi 464 | )}` 465 | ) 466 | throw new Errors.InsufficientLiquidityError('Exceeded maximum balance') 467 | } 468 | 469 | this.master._log.debug( 470 | `Forwarding PREPARE: Debited ${format( 471 | amount, 472 | Unit.Satoshi 473 | )}, new balance is ${format(newBalance, Unit.Satoshi)}` 474 | ) 475 | this.receivableBalance$.next(newBalance) 476 | 477 | const response = await this.dataHandler(data) 478 | const reply = deserializeIlpReply(response) 479 | 480 | if (isReject(reply)) { 481 | this.master._log.debug( 482 | `Credited ${format(amount, Unit.Satoshi)} in response to REJECT` 483 | ) 484 | this.receivableBalance$.next( 485 | this.receivableBalance$.value.minus(amount) 486 | ) 487 | } else if (isFulfill(reply)) { 488 | this.master._log.debug( 489 | `Received FULFILL in response to forwarded PREPARE` 490 | ) 491 | } 492 | 493 | return [ 494 | { 495 | protocolName: 'ilp', 496 | contentType: MIME_APPLICATION_OCTET_STREAM, 497 | data: response 498 | } 499 | ] 500 | } catch (err) { 501 | return [ 502 | { 503 | protocolName: 'ilp', 504 | contentType: MIME_APPLICATION_OCTET_STREAM, 505 | data: errorToReject('', err) 506 | } 507 | ] 508 | } 509 | } 510 | 511 | // Handle the response from a forwarded ILP PREPARE 512 | handlePrepareResponse(prepare: IlpPrepare, reply: IlpReply) { 513 | if (isFulfill(reply)) { 514 | // Update balance to reflect that we owe them the amount of the FULFILL 515 | const amount = new BigNumber(prepare.amount) 516 | 517 | this.master._log.debug( 518 | `Received a FULFILL in response to forwarded PREPARE: credited ${format( 519 | amount, 520 | Unit.Satoshi 521 | )}` 522 | ) 523 | this.payableBalance$.next(this.payableBalance$.value.plus(amount)) 524 | } else if (isReject(reply)) { 525 | this.master._log.debug( 526 | `Received a ${reply.code} REJECT in response to the forwarded PREPARE` 527 | ) 528 | } 529 | 530 | // Attempt to settle on fulfills *and* T04s (to resolve stalemates) 531 | const shouldSettle = 532 | isFulfill(reply) || (isReject(reply) && reply.code === 'T04') 533 | if (shouldSettle) { 534 | this.sendMoney().catch((err: Error) => 535 | this.master._log.debug(`Error during settlement: ${err.message}`) 536 | ) 537 | } 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { waitForClientReady } from '@grpc/grpc-js' 2 | import BigNumber from 'bignumber.js' 3 | import { registerProtocolNames } from 'btp-packet' 4 | import debug from 'debug' 5 | import { EventEmitter2 } from 'eventemitter2' 6 | import createLogger from 'ilp-logger' 7 | import { BtpPacket, IlpPluginBtpConstructorOptions } from 'ilp-plugin-btp' 8 | import { BehaviorSubject } from 'rxjs' 9 | import { promisify } from 'util' 10 | import LightningAccount from './account' 11 | import { 12 | createGrpcClient, 13 | createInvoiceStream, 14 | createLnrpc, 15 | createPaymentStream, 16 | GrpcConnectionOpts, 17 | InvoiceStream, 18 | LndService, 19 | PaymentStream, 20 | GrpcClient 21 | } from './lightning' 22 | import { LightningClientPlugin } from './plugins/client' 23 | import { LightningServerPlugin, MiniAccountsOpts } from './plugins/server' 24 | import { 25 | DataHandler, 26 | Logger, 27 | MemoryStore, 28 | MoneyHandler, 29 | PluginInstance, 30 | PluginServices, 31 | Store 32 | } from './types/plugin' 33 | 34 | // Re-export Lightning related-services 35 | export * from './lightning' 36 | export { LightningAccount } 37 | 38 | registerProtocolNames(['peeringRequest', 'paymentRequest']) 39 | 40 | // TODO Should the default handlers return ILP reject packets? 41 | 42 | const defaultDataHandler: DataHandler = () => { 43 | throw new Error('no request handler registered') 44 | } 45 | 46 | const defaultMoneyHandler: MoneyHandler = () => { 47 | throw new Error('no money handler registered') 48 | } 49 | 50 | export interface LightningPluginOpts 51 | extends MiniAccountsOpts, 52 | IlpPluginBtpConstructorOptions { 53 | /** 54 | * "client" to connect to a single peer or parent server that is explicity specified 55 | * "server" to enable multiple clients to openly connect to the plugin 56 | */ 57 | role: 'client' | 'server' 58 | 59 | /** Config to connection to a gRPC server, or an already-constructed LND service */ 60 | lnd: GrpcConnectionOpts | LndService 61 | 62 | /** Bidirectional streaming RPC to send outgoing payments and receive attestations */ 63 | paymentStream?: PaymentStream 64 | 65 | /** Streaming RPC of newly added or settled invoices */ 66 | invoiceStream?: InvoiceStream 67 | 68 | /** Maximum allowed amount in satoshis for incoming packets (satoshis) */ 69 | maxPacketAmount?: BigNumber.Value 70 | } 71 | 72 | export default class LightningPlugin extends EventEmitter2 73 | implements PluginInstance { 74 | static readonly version = 2 75 | readonly _log: Logger 76 | readonly _store: Store 77 | readonly _maxPacketAmount: BigNumber 78 | readonly _maxBalance: BigNumber 79 | readonly _accounts = new Map() // accountName -> account 80 | readonly _plugin: LightningServerPlugin | LightningClientPlugin 81 | _dataHandler: DataHandler = defaultDataHandler 82 | _moneyHandler: MoneyHandler = defaultMoneyHandler 83 | 84 | /** gRPC client for raw RPC calls */ 85 | readonly _grpcClient?: GrpcClient 86 | 87 | /** Wrapper around gRPC client for the Lightning RPC service, with typed methods and messages */ 88 | readonly _lightning: LndService 89 | 90 | /** Bidirectional streaming RPC to send outgoing payments and receive attestations */ 91 | _paymentStream?: PaymentStream 92 | 93 | /** Streaming RPC of newly added or settled invoices */ 94 | _invoiceStream?: InvoiceStream 95 | 96 | /** 97 | * Unique identififer and host the Lightning node of this instance: 98 | * [identityPubKey]@[hostname]:[port] 99 | */ 100 | _lightningAddress?: string 101 | 102 | constructor( 103 | { 104 | role = 'client', 105 | lnd, 106 | paymentStream, 107 | invoiceStream, 108 | maxPacketAmount = Infinity, 109 | ...opts 110 | }: LightningPluginOpts, 111 | { log, store = new MemoryStore() }: PluginServices = {} 112 | ) { 113 | super() 114 | 115 | /* 116 | * Allow consumers to both inject the LND connection 117 | * externally or pass in the credentials, and the plugin 118 | * will create it for them. 119 | */ 120 | const isConnectionOpts = (o: any): o is GrpcConnectionOpts => 121 | (typeof o.tlsCert === 'string' || Buffer.isBuffer(o.tlsCert)) && 122 | (typeof o.macaroon === 'string' || Buffer.isBuffer(o.macaroon)) && 123 | typeof o.hostname === 'string' 124 | 125 | if (isConnectionOpts(lnd)) { 126 | this._grpcClient = createGrpcClient(lnd) 127 | this._lightning = createLnrpc(this._grpcClient) 128 | } else { 129 | this._lightning = lnd 130 | } 131 | 132 | this._paymentStream = paymentStream 133 | this._invoiceStream = invoiceStream 134 | 135 | this._store = store 136 | 137 | this._log = log || createLogger(`ilp-plugin-lightning-${role}`) 138 | this._log.trace = 139 | this._log.trace || debug(`ilp-plugin-lightning-${role}:trace`) 140 | 141 | this._maxPacketAmount = new BigNumber(maxPacketAmount) 142 | .absoluteValue() 143 | .decimalPlaces(0, BigNumber.ROUND_DOWN) 144 | 145 | this._maxBalance = new BigNumber(role === 'client' ? Infinity : 0).dp( 146 | 0, 147 | BigNumber.ROUND_FLOOR 148 | ) 149 | 150 | const loadAccount = (accountName: string) => this._loadAccount(accountName) 151 | const getAccount = (accountName: string) => { 152 | const account = this._accounts.get(accountName) 153 | if (!account) { 154 | throw new Error(`Account ${accountName} is not yet loaded`) 155 | } 156 | 157 | return account 158 | } 159 | 160 | this._plugin = 161 | role === 'server' 162 | ? new LightningServerPlugin( 163 | { getAccount, loadAccount, ...opts }, 164 | { store, log } 165 | ) 166 | : new LightningClientPlugin( 167 | { getAccount, loadAccount, ...opts }, 168 | { store, log } 169 | ) 170 | 171 | this._plugin.on('connect', () => this.emitAsync('connect')) 172 | this._plugin.on('disconnect', () => this.emitAsync('disconnect')) 173 | this._plugin.on('error', e => this.emitAsync('error', e)) 174 | } 175 | 176 | async _loadAccount(accountName: string): Promise { 177 | /** Create a stream from the value in the store */ 178 | const loadValue = async (key: string) => { 179 | const storeKey = `${accountName}:${key}` 180 | const subject = new BehaviorSubject( 181 | new BigNumber((await this._store.get(storeKey)) || 0) 182 | ) 183 | 184 | // Automatically persist it to the store 185 | subject.subscribe(value => this._store.put(storeKey, value.toString())) 186 | 187 | return subject 188 | } 189 | 190 | const payableBalance$ = await loadValue('payableBalance') 191 | const receivableBalance$ = await loadValue('receivableBalance') 192 | const payoutAmount$ = await loadValue('payoutAmount') 193 | 194 | // Account data must always be loaded from store before it's in the map 195 | if (!this._accounts.has(accountName)) { 196 | const account = new LightningAccount({ 197 | sendMessage: (message: BtpPacket) => 198 | this._plugin._sendMessage(accountName, message), 199 | dataHandler: (data: Buffer) => this._dataHandler(data), 200 | moneyHandler: (amount: string) => this._moneyHandler(amount), 201 | accountName, 202 | payableBalance$, 203 | receivableBalance$, 204 | payoutAmount$, 205 | master: this 206 | }) 207 | 208 | // Since this account didn't previosuly exist, save it in the store 209 | this._accounts.set(accountName, account) 210 | } 211 | 212 | return this._accounts.get(accountName)! 213 | } 214 | 215 | async connect() { 216 | if (this._grpcClient) { 217 | await promisify(waitForClientReady)(this._grpcClient, Date.now() + 10000) 218 | } 219 | 220 | // Fetch public key & host for peering directly from LND 221 | const response = await this._lightning.getInfo({}) 222 | this._lightningAddress = response.identityPubkey 223 | 224 | /* 225 | * Create only a single HTTP/2 stream per-plugin for 226 | * invoices and payments (not per-account) 227 | * 228 | * Create the streams after the connection has been established 229 | * (otherwise if the credentials turn out to be invalid, 230 | * this can throw some odd error messages) 231 | */ 232 | if (!this._paymentStream) { 233 | this._paymentStream = createPaymentStream(this._lightning) 234 | } 235 | if (!this._invoiceStream) { 236 | this._invoiceStream = createInvoiceStream(this._lightning) 237 | } 238 | 239 | return this._plugin.connect() 240 | } 241 | 242 | async disconnect() { 243 | await this._plugin.disconnect() 244 | 245 | this._accounts.forEach(account => account.unload()) 246 | this._accounts.clear() 247 | 248 | if (this._grpcClient) { 249 | this._grpcClient.close() 250 | } 251 | } 252 | 253 | isConnected() { 254 | return this._plugin.isConnected() 255 | } 256 | 257 | sendData(data: Buffer) { 258 | return this._plugin.sendData(data) 259 | } 260 | 261 | sendMoney(amount: string) { 262 | return this._plugin.sendMoney(amount) 263 | } 264 | 265 | registerDataHandler(dataHandler: DataHandler) { 266 | if (this._dataHandler !== defaultDataHandler) { 267 | throw new Error('request handler already registered') 268 | } 269 | 270 | this._dataHandler = dataHandler 271 | return this._plugin.registerDataHandler(dataHandler) 272 | } 273 | 274 | deregisterDataHandler() { 275 | this._dataHandler = defaultDataHandler 276 | return this._plugin.deregisterDataHandler() 277 | } 278 | 279 | registerMoneyHandler(moneyHandler: MoneyHandler) { 280 | if (this._moneyHandler !== defaultMoneyHandler) { 281 | throw new Error('money handler already registered') 282 | } 283 | 284 | this._moneyHandler = moneyHandler 285 | return this._plugin.registerMoneyHandler(moneyHandler) 286 | } 287 | 288 | deregisterMoneyHandler() { 289 | this._moneyHandler = defaultMoneyHandler 290 | return this._plugin.deregisterMoneyHandler() 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/lightning.ts: -------------------------------------------------------------------------------- 1 | import { credentials } from '@grpc/grpc-js' 2 | import { 3 | ClientDuplexStream, 4 | ClientReadableStream 5 | } from '@grpc/grpc-js/build/src/call' 6 | import { 7 | CallCredentials, 8 | CallMetadataGenerator 9 | } from '@grpc/grpc-js/build/src/call-credentials' 10 | import { 11 | makeClientConstructor, 12 | ServiceClient 13 | } from '@grpc/grpc-js/build/src/make-client' 14 | import { Metadata } from '@grpc/grpc-js/build/src/metadata' 15 | import BigNumber from 'bignumber.js' 16 | import { util } from 'protobufjs' 17 | import { lnrpc } from '../generated/rpc' 18 | import { createHash } from 'crypto' 19 | 20 | /** Re-export all compiled gRPC message types */ 21 | export * from '../generated/rpc' 22 | 23 | /** Create a generic gRPC client */ 24 | 25 | export type GrpcClient = ServiceClient 26 | 27 | export interface GrpcConnectionOpts { 28 | /** TLS cert as a Base64-encoded string or Buffer (e.g. using `fs.readFile`) */ 29 | tlsCert: string | Buffer 30 | /** LND macaroon as a Base64-encoded string or Buffer (e.g. using `fs.readFile`) */ 31 | macaroon: string | Buffer 32 | /** IP address of the Lightning node */ 33 | hostname: string 34 | /** Port of LND gRPC server */ 35 | grpcPort?: number 36 | } 37 | 38 | export const createGrpcClient = ({ 39 | tlsCert, 40 | macaroon, 41 | hostname, 42 | grpcPort = 10009 43 | }: GrpcConnectionOpts): GrpcClient => { 44 | /** 45 | * Required for SSL handshake with LND 46 | * https://grpc.io/grpc/core/md_doc_environment_variables.html 47 | */ 48 | if (!process.env.GRPC_SSL_CIPHER_SUITES) { 49 | process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA' 50 | } 51 | 52 | if (typeof tlsCert === 'string') { 53 | tlsCert = Buffer.from(tlsCert, 'base64') 54 | } 55 | 56 | if (typeof macaroon === 'string') { 57 | macaroon = Buffer.from(macaroon, 'base64') 58 | } 59 | 60 | let macaroonCreds: CallCredentials 61 | try { 62 | const metadata = new Metadata() 63 | metadata.add('macaroon', macaroon.toString('hex')) 64 | const metadataGenerator: CallMetadataGenerator = (_, callback) => { 65 | callback(null, metadata) 66 | } 67 | 68 | macaroonCreds = CallCredentials.createFromMetadataGenerator( 69 | metadataGenerator 70 | ) 71 | } catch (err) { 72 | throw new Error(`Macaroon is not properly formatted: ${err.message}`) 73 | } 74 | 75 | const address = hostname + ':' + grpcPort 76 | const tlsCreds = credentials.createSsl(tlsCert) 77 | const channelCredentials = credentials.combineChannelCredentials( 78 | tlsCreds, 79 | macaroonCreds 80 | ) 81 | 82 | const Client = makeClientConstructor({}, '') 83 | return new Client(address, channelCredentials) 84 | } 85 | 86 | /** Wrap a gRPC client in a Lightning RPC service with typed methods and messages */ 87 | 88 | export type LndService = lnrpc.Lightning 89 | 90 | export const createLnrpc = (client: GrpcClient) => 91 | new lnrpc.Lightning({ 92 | unaryCall(method, requestData, callback) { 93 | client.makeUnaryRequest( 94 | getMethodPath(method.name), 95 | arg => Buffer.from(arg), 96 | arg => Buffer.from(arg), 97 | requestData, 98 | callback 99 | ) 100 | }, 101 | serverStreamCall(method, requestData, decode) { 102 | return (client.makeServerStreamRequest( 103 | getMethodPath(method.name), 104 | arg => Buffer.from(arg), 105 | decode, 106 | requestData 107 | ) as unknown) as util.EventEmitter 108 | }, 109 | clientStreamCall(method, encode, decode) { 110 | return (client.makeClientStreamRequest( 111 | getMethodPath(method.name), 112 | arg => Buffer.from(encode(arg)), 113 | decode, 114 | () => { 115 | return 116 | } 117 | ) as unknown) as util.EventEmitter 118 | }, 119 | bidiStreamCall(method, encode, decode) { 120 | return (client.makeBidiStreamRequest( 121 | getMethodPath(method.name), 122 | arg => Buffer.from(encode(arg)), 123 | decode 124 | ) as unknown) as util.EventEmitter 125 | } 126 | }) 127 | 128 | const getMethodPath = (methodName: string) => `/lnrpc.Lightning/${methodName}` 129 | 130 | /* Create streams to send outgoing and handle incoming invoices */ 131 | 132 | export type InvoiceStream = ClientReadableStream 133 | 134 | export const createInvoiceStream = (lightning: LndService): InvoiceStream => 135 | lightning.subscribeInvoices({ 136 | addIndex: 0, 137 | settleIndex: 0 138 | }) 139 | 140 | export const createPaymentRequest = async (lightning: LndService) => 141 | (await lightning.addInvoice({})).paymentRequest 142 | 143 | export type PaymentStream = ClientDuplexStream< 144 | lnrpc.SendRequest, 145 | lnrpc.SendResponse 146 | > 147 | 148 | export const createPaymentStream = (lightning: LndService): PaymentStream => 149 | lightning.sendPayment() 150 | 151 | /** 152 | * Pay a given request using a bidirectional streaming RPC. 153 | * Throw if it failed to pay the invoice 154 | */ 155 | export const payInvoice = async ( 156 | paymentStream: PaymentStream, 157 | paymentRequest: string, 158 | paymentHash: string, 159 | amount: BigNumber 160 | ) => { 161 | const didSerialize = paymentStream.write( 162 | new lnrpc.SendRequest({ 163 | amt: amount.toNumber(), 164 | paymentRequest 165 | }) 166 | ) 167 | if (!didSerialize) { 168 | throw new Error(`failed to serialize outgoing payment`) 169 | } 170 | 171 | /** 172 | * Since it's a stream, there's no request-response matching. 173 | * Disregard messages that don't correspond to this invoice 174 | */ 175 | await new Promise((resolve, reject) => { 176 | const handler = (data: lnrpc.SendResponse) => { 177 | let somePaymentHash = data.paymentHash 178 | /** 179 | * Returning the `payment_hash` in the response was merged into 180 | * lnd@master on 12/10/18, so many nodes may not support it yet: 181 | * https://github.com/lightningnetwork/lnd/pull/2033 182 | * 183 | * (if not, fallback to generating the hash from the preimage) 184 | */ 185 | if (!somePaymentHash) { 186 | somePaymentHash = sha256(data.paymentPreimage) 187 | } 188 | 189 | const isThisInvoice = 190 | somePaymentHash && 191 | paymentHash === Buffer.from(somePaymentHash).toString('hex') 192 | if (!isThisInvoice) { 193 | return 194 | } 195 | 196 | // The invoice was not paid and it's safe to undo the balance update 197 | const error = data.paymentError 198 | if (error) { 199 | paymentStream.removeListener('data', handler) 200 | return reject(`error sending payment: ${error}`) 201 | } 202 | 203 | paymentStream.removeListener('data', handler) 204 | resolve() 205 | } 206 | 207 | paymentStream.on('data', handler) 208 | }) 209 | } 210 | 211 | /** Ensure that this instance is peered with the given Lightning node, throw if not */ 212 | export const connectPeer = (lightning: LndService) => async ( 213 | /** Identity public key of the Lightning node to peer with */ 214 | peerIdentityPubkey: string, 215 | /** Network location of the Lightning node to peer with, e.g. `69.69.69.69:1337` or `localhost:10011` */ 216 | peerHost: string 217 | ) => { 218 | /** 219 | * LND throws if it failed to connect: 220 | * https://github.com/lightningnetwork/lnd/blob/f55e81a2d422d34181ea2a6579e5fcc0296386c2/rpcserver.go#L952 221 | */ 222 | await lightning 223 | .connectPeer({ 224 | addr: { 225 | pubkey: peerIdentityPubkey, 226 | host: peerHost 227 | } 228 | }) 229 | .catch((err: { details: string }) => { 230 | /** 231 | * Don't throw if we're already connected, akin to: 232 | * https://github.com/lightningnetwork/lnd/blob/8c5d6842c2ea7234512d3fb0ddc69d51c8026c74/lntest/harness.go#L428 233 | */ 234 | const alreadyConnected = err.details.includes('already connected to peer') 235 | if (!alreadyConnected) { 236 | throw err 237 | } 238 | }) 239 | } 240 | 241 | const sha256 = (preimage: string | Uint8Array | Buffer) => 242 | createHash('sha256') 243 | .update(preimage) 244 | .digest() 245 | -------------------------------------------------------------------------------- /src/plugins/client.ts: -------------------------------------------------------------------------------- 1 | import LightningAccount, { generateBtpRequestId } from '../account' 2 | import BtpPlugin, { 3 | BtpPacket, 4 | BtpSubProtocol, 5 | IlpPluginBtpConstructorOptions 6 | } from 'ilp-plugin-btp' 7 | import { TYPE_MESSAGE, MIME_APPLICATION_OCTET_STREAM } from 'btp-packet' 8 | import { PluginInstance, PluginServices } from '../types/plugin' 9 | import { 10 | deserializeIlpReply, 11 | isPrepare, 12 | deserializeIlpPrepare 13 | } from 'ilp-packet' 14 | 15 | export interface LightningClientOpts extends IlpPluginBtpConstructorOptions { 16 | getAccount: (accountName: string) => LightningAccount 17 | loadAccount: (accountName: string) => Promise 18 | } 19 | 20 | export class LightningClientPlugin extends BtpPlugin implements PluginInstance { 21 | private getAccount: () => LightningAccount 22 | private loadAccount: () => Promise 23 | 24 | constructor( 25 | { getAccount, loadAccount, ...opts }: LightningClientOpts, 26 | { log }: PluginServices 27 | ) { 28 | super(opts, { log }) 29 | 30 | this.getAccount = () => getAccount('peer') 31 | this.loadAccount = () => loadAccount('peer') 32 | 33 | // If the websocket drops, unload the account 34 | this.on('disconnect', () => this.getAccount().unload()) 35 | 36 | // Peer and re-share invoices if the websocket reconnects 37 | this.on('connect', () => this._connect()) 38 | } 39 | 40 | async _connect() { 41 | try { 42 | // If the account is loaded, assume it's connected 43 | this.getAccount() 44 | } catch (err) { 45 | const account = await this.loadAccount() 46 | return account.connect() 47 | } 48 | } 49 | 50 | _sendMessage(accountName: string, message: BtpPacket) { 51 | return this._call('', message) 52 | } 53 | 54 | _handleData(from: string, message: BtpPacket): Promise { 55 | return this.getAccount().handleData(message) 56 | } 57 | 58 | // Add hooks into sendData before and after sending a packet for 59 | // balance updates and settlement, akin to mini-accounts 60 | async sendData(buffer: Buffer): Promise { 61 | const prepare = deserializeIlpPrepare(buffer) 62 | if (!isPrepare(prepare)) { 63 | throw new Error('Packet must be a PREPARE') 64 | } 65 | 66 | const response = await this._call('', { 67 | type: TYPE_MESSAGE, 68 | requestId: await generateBtpRequestId(), 69 | data: { 70 | protocolData: [ 71 | { 72 | protocolName: 'ilp', 73 | contentType: MIME_APPLICATION_OCTET_STREAM, 74 | data: buffer 75 | } 76 | ] 77 | } 78 | }) 79 | 80 | const ilpResponse = response.protocolData.find( 81 | p => p.protocolName === 'ilp' 82 | ) 83 | if (ilpResponse) { 84 | const reply = deserializeIlpReply(ilpResponse.data) 85 | this.getAccount().handlePrepareResponse(prepare, reply) 86 | return ilpResponse.data 87 | } 88 | 89 | return Buffer.alloc(0) 90 | } 91 | 92 | sendMoney(amount: string) { 93 | return this.getAccount().sendMoney(amount) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/plugins/server.ts: -------------------------------------------------------------------------------- 1 | import { PluginInstance, PluginServices } from '../types/plugin' 2 | import MiniAccountsPlugin from 'ilp-plugin-mini-accounts' 3 | import { ServerOptions } from 'ws' 4 | import { IldcpResponse } from 'ilp-protocol-ildcp' 5 | import { BtpPacket, BtpSubProtocol } from 'ilp-plugin-btp' 6 | import { IlpPacket, IlpPrepare, Type, isPrepare } from 'ilp-packet' 7 | import LightningAccount from '../account' 8 | 9 | export interface MiniAccountsOpts { 10 | port?: number 11 | wsOpts?: ServerOptions 12 | debugHostIldcpInfo?: IldcpResponse 13 | allowedOrigins?: string[] 14 | } 15 | 16 | export interface LightningServerOpts extends MiniAccountsOpts { 17 | getAccount: (accountName: string) => LightningAccount 18 | loadAccount: (accountName: string) => Promise 19 | } 20 | 21 | export class LightningServerPlugin extends MiniAccountsPlugin 22 | implements PluginInstance { 23 | private getAccount: (address: string) => LightningAccount 24 | private loadAccount: (address: string) => Promise 25 | 26 | constructor( 27 | { getAccount, loadAccount, ...opts }: LightningServerOpts, 28 | api: PluginServices 29 | ) { 30 | super(opts, api) 31 | 32 | this.getAccount = (address: string) => 33 | getAccount(this.ilpAddressToAccount(address)) 34 | this.loadAccount = (address: string) => 35 | loadAccount(this.ilpAddressToAccount(address)) 36 | } 37 | 38 | _sendMessage(accountName: string, message: BtpPacket) { 39 | return this._call(this._prefix + accountName, message) 40 | } 41 | 42 | _handleCustomData = async ( 43 | from: string, 44 | message: BtpPacket 45 | ): Promise => { 46 | let account: LightningAccount 47 | try { 48 | account = this.getAccount(from) 49 | } catch (err) { 50 | account = await this.loadAccount(from) 51 | await account.connect() 52 | } 53 | 54 | return account.handleData(message) 55 | } 56 | 57 | _handlePrepareResponse = async ( 58 | destination: string, 59 | responsePacket: IlpPacket, 60 | preparePacket: { 61 | type: Type.TYPE_ILP_PREPARE 62 | typeString?: 'ilp_prepare' 63 | data: IlpPrepare 64 | } 65 | ) => { 66 | if (isPrepare(responsePacket.data)) { 67 | throw new Error('Received PREPARE in response to PREPARE') 68 | } 69 | 70 | return this.getAccount(destination).handlePrepareResponse( 71 | preparePacket.data, 72 | responsePacket.data 73 | ) 74 | } 75 | 76 | async sendMoney() { 77 | throw new Error( 78 | 'sendMoney is not supported: use plugin balance configuration' 79 | ) 80 | } 81 | 82 | async _close(from: string): Promise { 83 | this.getAccount(from).unload() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/proto/rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/api/annotations.proto"; 4 | 5 | package lnrpc; 6 | /** 7 | * Comments in this file will be directly parsed into the API 8 | * Documentation as descriptions of the associated method, message, or field. 9 | * These descriptions should go right above the definition of the object, and 10 | * can be in either block or /// comment format. 11 | * 12 | * One edge case exists where a // comment followed by a /// comment in the 13 | * next line will cause the description not to show up in the documentation. In 14 | * that instance, simply separate the two comments with a blank line. 15 | * 16 | * An RPC method can be matched to an lncli command by placing a line in the 17 | * beginning of the description in exactly the following format: 18 | * lncli: `methodname` 19 | * 20 | * Failure to specify the exact name of the command will cause documentation 21 | * generation to fail. 22 | * 23 | * More information on how exactly the gRPC documentation is generated from 24 | * this proto file can be found here: 25 | * https://github.com/lightninglabs/lightning-api 26 | */ 27 | 28 | // The WalletUnlocker service is used to set up a wallet password for 29 | // lnd at first startup, and unlock a previously set up wallet. 30 | service WalletUnlocker { 31 | /** 32 | GenSeed is the first method that should be used to instantiate a new lnd 33 | instance. This method allows a caller to generate a new aezeed cipher seed 34 | given an optional passphrase. If provided, the passphrase will be necessary 35 | to decrypt the cipherseed to expose the internal wallet seed. 36 | 37 | Once the cipherseed is obtained and verified by the user, the InitWallet 38 | method should be used to commit the newly generated seed, and create the 39 | wallet. 40 | */ 41 | rpc GenSeed(GenSeedRequest) returns (GenSeedResponse) { 42 | option (google.api.http) = { 43 | get: "/v1/genseed" 44 | }; 45 | } 46 | 47 | /** 48 | InitWallet is used when lnd is starting up for the first time to fully 49 | initialize the daemon and its internal wallet. At the very least a wallet 50 | password must be provided. This will be used to encrypt sensitive material 51 | on disk. 52 | 53 | In the case of a recovery scenario, the user can also specify their aezeed 54 | mnemonic and passphrase. If set, then the daemon will use this prior state 55 | to initialize its internal wallet. 56 | 57 | Alternatively, this can be used along with the GenSeed RPC to obtain a 58 | seed, then present it to the user. Once it has been verified by the user, 59 | the seed can be fed into this RPC in order to commit the new wallet. 60 | */ 61 | rpc InitWallet(InitWalletRequest) returns (InitWalletResponse) { 62 | option (google.api.http) = { 63 | post: "/v1/initwallet" 64 | body: "*" 65 | }; 66 | } 67 | 68 | /** lncli: `unlock` 69 | UnlockWallet is used at startup of lnd to provide a password to unlock 70 | the wallet database. 71 | */ 72 | rpc UnlockWallet(UnlockWalletRequest) returns (UnlockWalletResponse) { 73 | option (google.api.http) = { 74 | post: "/v1/unlockwallet" 75 | body: "*" 76 | }; 77 | } 78 | 79 | /** lncli: `changepassword` 80 | ChangePassword changes the password of the encrypted wallet. This will 81 | automatically unlock the wallet database if successful. 82 | */ 83 | rpc ChangePassword (ChangePasswordRequest) returns (ChangePasswordResponse) { 84 | option (google.api.http) = { 85 | post: "/v1/changepassword" 86 | body: "*" 87 | }; 88 | } 89 | } 90 | 91 | message GenSeedRequest { 92 | /** 93 | aezeed_passphrase is an optional user provided passphrase that will be used 94 | to encrypt the generated aezeed cipher seed. 95 | */ 96 | bytes aezeed_passphrase = 1; 97 | 98 | /** 99 | seed_entropy is an optional 16-bytes generated via CSPRNG. If not 100 | specified, then a fresh set of randomness will be used to create the seed. 101 | */ 102 | bytes seed_entropy = 2; 103 | } 104 | message GenSeedResponse { 105 | /** 106 | cipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed 107 | cipher seed obtained by the user. This field is optional, as if not 108 | provided, then the daemon will generate a new cipher seed for the user. 109 | Otherwise, then the daemon will attempt to recover the wallet state linked 110 | to this cipher seed. 111 | */ 112 | repeated string cipher_seed_mnemonic = 1; 113 | 114 | /** 115 | enciphered_seed are the raw aezeed cipher seed bytes. This is the raw 116 | cipher text before run through our mnemonic encoding scheme. 117 | */ 118 | bytes enciphered_seed = 2; 119 | } 120 | 121 | message InitWalletRequest { 122 | /** 123 | wallet_password is the passphrase that should be used to encrypt the 124 | wallet. This MUST be at least 8 chars in length. After creation, this 125 | password is required to unlock the daemon. 126 | */ 127 | bytes wallet_password = 1; 128 | 129 | /** 130 | cipher_seed_mnemonic is a 24-word mnemonic that encodes a prior aezeed 131 | cipher seed obtained by the user. This may have been generated by the 132 | GenSeed method, or be an existing seed. 133 | */ 134 | repeated string cipher_seed_mnemonic = 2; 135 | 136 | /** 137 | aezeed_passphrase is an optional user provided passphrase that will be used 138 | to encrypt the generated aezeed cipher seed. 139 | */ 140 | bytes aezeed_passphrase = 3; 141 | 142 | /** 143 | recovery_window is an optional argument specifying the address lookahead 144 | when restoring a wallet seed. The recovery window applies to each 145 | invdividual branch of the BIP44 derivation paths. Supplying a recovery 146 | window of zero indicates that no addresses should be recovered, such after 147 | the first initialization of the wallet. 148 | */ 149 | int32 recovery_window = 4; 150 | } 151 | message InitWalletResponse { 152 | } 153 | 154 | message UnlockWalletRequest { 155 | /** 156 | wallet_password should be the current valid passphrase for the daemon. This 157 | will be required to decrypt on-disk material that the daemon requires to 158 | function properly. 159 | */ 160 | bytes wallet_password = 1; 161 | 162 | /** 163 | recovery_window is an optional argument specifying the address lookahead 164 | when restoring a wallet seed. The recovery window applies to each 165 | invdividual branch of the BIP44 derivation paths. Supplying a recovery 166 | window of zero indicates that no addresses should be recovered, such after 167 | the first initialization of the wallet. 168 | */ 169 | int32 recovery_window = 2; 170 | } 171 | message UnlockWalletResponse {} 172 | 173 | message ChangePasswordRequest { 174 | /** 175 | current_password should be the current valid passphrase used to unlock the 176 | daemon. 177 | */ 178 | bytes current_password = 1; 179 | 180 | /** 181 | new_password should be the new passphrase that will be needed to unlock the 182 | daemon. 183 | */ 184 | bytes new_password = 2; 185 | } 186 | message ChangePasswordResponse {} 187 | 188 | service Lightning { 189 | /** lncli: `walletbalance` 190 | WalletBalance returns total unspent outputs(confirmed and unconfirmed), all 191 | confirmed unspent outputs and all unconfirmed unspent outputs under control 192 | of the wallet. 193 | */ 194 | rpc WalletBalance (WalletBalanceRequest) returns (WalletBalanceResponse) { 195 | option (google.api.http) = { 196 | get: "/v1/balance/blockchain" 197 | }; 198 | } 199 | 200 | /** lncli: `channelbalance` 201 | ChannelBalance returns the total funds available across all open channels 202 | in satoshis. 203 | */ 204 | rpc ChannelBalance (ChannelBalanceRequest) returns (ChannelBalanceResponse) { 205 | option (google.api.http) = { 206 | get: "/v1/balance/channels" 207 | }; 208 | } 209 | 210 | /** lncli: `listchaintxns` 211 | GetTransactions returns a list describing all the known transactions 212 | relevant to the wallet. 213 | */ 214 | rpc GetTransactions (GetTransactionsRequest) returns (TransactionDetails) { 215 | option (google.api.http) = { 216 | get: "/v1/transactions" 217 | }; 218 | } 219 | 220 | /** lncli: `sendcoins` 221 | SendCoins executes a request to send coins to a particular address. Unlike 222 | SendMany, this RPC call only allows creating a single output at a time. If 223 | neither target_conf, or sat_per_byte are set, then the internal wallet will 224 | consult its fee model to determine a fee for the default confirmation 225 | target. 226 | */ 227 | rpc SendCoins (SendCoinsRequest) returns (SendCoinsResponse) { 228 | option (google.api.http) = { 229 | post: "/v1/transactions" 230 | body: "*" 231 | }; 232 | } 233 | 234 | /** lncli: `listunspent` 235 | ListUnspent returns a list of all utxos spendable by the wallet with a 236 | number of confirmations between the specified minimum and maximum. 237 | */ 238 | rpc ListUnspent (ListUnspentRequest) returns (ListUnspentResponse) { 239 | option (google.api.http) = { 240 | get: "/v1/utxos" 241 | }; 242 | } 243 | 244 | /** 245 | SubscribeTransactions creates a uni-directional stream from the server to 246 | the client in which any newly discovered transactions relevant to the 247 | wallet are sent over. 248 | */ 249 | rpc SubscribeTransactions (GetTransactionsRequest) returns (stream Transaction); 250 | 251 | /** lncli: `sendmany` 252 | SendMany handles a request for a transaction that creates multiple specified 253 | outputs in parallel. If neither target_conf, or sat_per_byte are set, then 254 | the internal wallet will consult its fee model to determine a fee for the 255 | default confirmation target. 256 | */ 257 | rpc SendMany (SendManyRequest) returns (SendManyResponse); 258 | 259 | /** lncli: `newaddress` 260 | NewAddress creates a new address under control of the local wallet. 261 | */ 262 | rpc NewAddress (NewAddressRequest) returns (NewAddressResponse) { 263 | option (google.api.http) = { 264 | get: "/v1/newaddress" 265 | }; 266 | } 267 | 268 | /** lncli: `signmessage` 269 | SignMessage signs a message with this node's private key. The returned 270 | signature string is `zbase32` encoded and pubkey recoverable, meaning that 271 | only the message digest and signature are needed for verification. 272 | */ 273 | rpc SignMessage (SignMessageRequest) returns (SignMessageResponse) { 274 | option (google.api.http) = { 275 | post: "/v1/signmessage" 276 | body: "*" 277 | }; 278 | } 279 | 280 | /** lncli: `verifymessage` 281 | VerifyMessage verifies a signature over a msg. The signature must be 282 | zbase32 encoded and signed by an active node in the resident node's 283 | channel database. In addition to returning the validity of the signature, 284 | VerifyMessage also returns the recovered pubkey from the signature. 285 | */ 286 | rpc VerifyMessage (VerifyMessageRequest) returns (VerifyMessageResponse) { 287 | option (google.api.http) = { 288 | post: "/v1/verifymessage" 289 | body: "*" 290 | }; 291 | } 292 | 293 | /** lncli: `connect` 294 | ConnectPeer attempts to establish a connection to a remote peer. This is at 295 | the networking level, and is used for communication between nodes. This is 296 | distinct from establishing a channel with a peer. 297 | */ 298 | rpc ConnectPeer (ConnectPeerRequest) returns (ConnectPeerResponse) { 299 | option (google.api.http) = { 300 | post: "/v1/peers" 301 | body: "*" 302 | }; 303 | } 304 | 305 | /** lncli: `disconnect` 306 | DisconnectPeer attempts to disconnect one peer from another identified by a 307 | given pubKey. In the case that we currently have a pending or active channel 308 | with the target peer, then this action will be not be allowed. 309 | */ 310 | rpc DisconnectPeer (DisconnectPeerRequest) returns (DisconnectPeerResponse) { 311 | option (google.api.http) = { 312 | delete: "/v1/peers/{pub_key}" 313 | }; 314 | } 315 | 316 | /** lncli: `listpeers` 317 | ListPeers returns a verbose listing of all currently active peers. 318 | */ 319 | rpc ListPeers (ListPeersRequest) returns (ListPeersResponse) { 320 | option (google.api.http) = { 321 | get: "/v1/peers" 322 | }; 323 | } 324 | 325 | /** lncli: `getinfo` 326 | GetInfo returns general information concerning the lightning node including 327 | it's identity pubkey, alias, the chains it is connected to, and information 328 | concerning the number of open+pending channels. 329 | */ 330 | rpc GetInfo (GetInfoRequest) returns (GetInfoResponse) { 331 | option (google.api.http) = { 332 | get: "/v1/getinfo" 333 | }; 334 | } 335 | 336 | // TODO(roasbeef): merge with below with bool? 337 | /** lncli: `pendingchannels` 338 | PendingChannels returns a list of all the channels that are currently 339 | considered "pending". A channel is pending if it has finished the funding 340 | workflow and is waiting for confirmations for the funding txn, or is in the 341 | process of closure, either initiated cooperatively or non-cooperatively. 342 | */ 343 | rpc PendingChannels (PendingChannelsRequest) returns (PendingChannelsResponse) { 344 | option (google.api.http) = { 345 | get: "/v1/channels/pending" 346 | }; 347 | } 348 | 349 | /** lncli: `listchannels` 350 | ListChannels returns a description of all the open channels that this node 351 | is a participant in. 352 | */ 353 | rpc ListChannels (ListChannelsRequest) returns (ListChannelsResponse) { 354 | option (google.api.http) = { 355 | get: "/v1/channels" 356 | }; 357 | } 358 | 359 | /** lncli: `closedchannels` 360 | ClosedChannels returns a description of all the closed channels that 361 | this node was a participant in. 362 | */ 363 | rpc ClosedChannels (ClosedChannelsRequest) returns (ClosedChannelsResponse) { 364 | option (google.api.http) = { 365 | get: "/v1/channels/closed" 366 | }; 367 | } 368 | 369 | 370 | /** 371 | OpenChannelSync is a synchronous version of the OpenChannel RPC call. This 372 | call is meant to be consumed by clients to the REST proxy. As with all 373 | other sync calls, all byte slices are intended to be populated as hex 374 | encoded strings. 375 | */ 376 | rpc OpenChannelSync (OpenChannelRequest) returns (ChannelPoint) { 377 | option (google.api.http) = { 378 | post: "/v1/channels" 379 | body: "*" 380 | }; 381 | } 382 | 383 | /** lncli: `openchannel` 384 | OpenChannel attempts to open a singly funded channel specified in the 385 | request to a remote peer. Users are able to specify a target number of 386 | blocks that the funding transaction should be confirmed in, or a manual fee 387 | rate to us for the funding transaction. If neither are specified, then a 388 | lax block confirmation target is used. 389 | */ 390 | rpc OpenChannel (OpenChannelRequest) returns (stream OpenStatusUpdate); 391 | 392 | /** lncli: `closechannel` 393 | CloseChannel attempts to close an active channel identified by its channel 394 | outpoint (ChannelPoint). The actions of this method can additionally be 395 | augmented to attempt a force close after a timeout period in the case of an 396 | inactive peer. If a non-force close (cooperative closure) is requested, 397 | then the user can specify either a target number of blocks until the 398 | closure transaction is confirmed, or a manual fee rate. If neither are 399 | specified, then a default lax, block confirmation target is used. 400 | */ 401 | rpc CloseChannel (CloseChannelRequest) returns (stream CloseStatusUpdate) { 402 | option (google.api.http) = { 403 | delete: "/v1/channels/{channel_point.funding_txid_str}/{channel_point.output_index}" 404 | }; 405 | } 406 | 407 | /** lncli: `abandonchannel` 408 | AbandonChannel removes all channel state from the database except for a 409 | close summary. This method can be used to get rid of permanently unusable 410 | channels due to bugs fixed in newer versions of lnd. Only available 411 | when in debug builds of lnd. 412 | */ 413 | rpc AbandonChannel (AbandonChannelRequest) returns (AbandonChannelResponse) { 414 | option (google.api.http) = { 415 | delete: "/v1/channels/abandon/{channel_point.funding_txid_str}/{channel_point.output_index}" 416 | }; 417 | } 418 | 419 | 420 | /** lncli: `sendpayment` 421 | SendPayment dispatches a bi-directional streaming RPC for sending payments 422 | through the Lightning Network. A single RPC invocation creates a persistent 423 | bi-directional stream allowing clients to rapidly send payments through the 424 | Lightning Network with a single persistent connection. 425 | */ 426 | rpc SendPayment (stream SendRequest) returns (stream SendResponse); 427 | 428 | /** 429 | SendPaymentSync is the synchronous non-streaming version of SendPayment. 430 | This RPC is intended to be consumed by clients of the REST proxy. 431 | Additionally, this RPC expects the destination's public key and the payment 432 | hash (if any) to be encoded as hex strings. 433 | */ 434 | rpc SendPaymentSync (SendRequest) returns (SendResponse) { 435 | option (google.api.http) = { 436 | post: "/v1/channels/transactions" 437 | body: "*" 438 | }; 439 | } 440 | 441 | /** lncli: `sendtoroute` 442 | SendToRoute is a bi-directional streaming RPC for sending payment through 443 | the Lightning Network. This method differs from SendPayment in that it 444 | allows users to specify a full route manually. This can be used for things 445 | like rebalancing, and atomic swaps. 446 | */ 447 | rpc SendToRoute(stream SendToRouteRequest) returns (stream SendResponse); 448 | 449 | /** 450 | SendToRouteSync is a synchronous version of SendToRoute. It Will block 451 | until the payment either fails or succeeds. 452 | */ 453 | rpc SendToRouteSync (SendToRouteRequest) returns (SendResponse) { 454 | option (google.api.http) = { 455 | post: "/v1/channels/transactions/route" 456 | body: "*" 457 | }; 458 | } 459 | 460 | /** lncli: `addinvoice` 461 | AddInvoice attempts to add a new invoice to the invoice database. Any 462 | duplicated invoices are rejected, therefore all invoices *must* have a 463 | unique payment preimage. 464 | */ 465 | rpc AddInvoice (Invoice) returns (AddInvoiceResponse) { 466 | option (google.api.http) = { 467 | post: "/v1/invoices" 468 | body: "*" 469 | }; 470 | } 471 | 472 | /** lncli: `listinvoices` 473 | ListInvoices returns a list of all the invoices currently stored within the 474 | database. Any active debug invoices are ignored. It has full support for 475 | paginated responses, allowing users to query for specific invoices through 476 | their add_index. This can be done by using either the first_index_offset or 477 | last_index_offset fields included in the response as the index_offset of the 478 | next request. The reversed flag is set by default in order to paginate 479 | backwards. If you wish to paginate forwards, you must explicitly set the 480 | flag to false. If none of the parameters are specified, then the last 100 481 | invoices will be returned. 482 | */ 483 | rpc ListInvoices (ListInvoiceRequest) returns (ListInvoiceResponse) { 484 | option (google.api.http) = { 485 | get: "/v1/invoices" 486 | }; 487 | } 488 | 489 | /** lncli: `lookupinvoice` 490 | LookupInvoice attempts to look up an invoice according to its payment hash. 491 | The passed payment hash *must* be exactly 32 bytes, if not, an error is 492 | returned. 493 | */ 494 | rpc LookupInvoice (PaymentHash) returns (Invoice) { 495 | option (google.api.http) = { 496 | get: "/v1/invoice/{r_hash_str}" 497 | }; 498 | } 499 | 500 | /** 501 | SubscribeInvoices returns a uni-directional stream (server -> client) for 502 | notifying the client of newly added/settled invoices. The caller can 503 | optionally specify the add_index and/or the settle_index. If the add_index 504 | is specified, then we'll first start by sending add invoice events for all 505 | invoices with an add_index greater than the specified value. If the 506 | settle_index is specified, the next, we'll send out all settle events for 507 | invoices with a settle_index greater than the specified value. One or both 508 | of these fields can be set. If no fields are set, then we'll only send out 509 | the latest add/settle events. 510 | */ 511 | rpc SubscribeInvoices (InvoiceSubscription) returns (stream Invoice) { 512 | option (google.api.http) = { 513 | get: "/v1/invoices/subscribe" 514 | }; 515 | } 516 | 517 | /** lncli: `decodepayreq` 518 | DecodePayReq takes an encoded payment request string and attempts to decode 519 | it, returning a full description of the conditions encoded within the 520 | payment request. 521 | */ 522 | rpc DecodePayReq (PayReqString) returns (PayReq) { 523 | option (google.api.http) = { 524 | get: "/v1/payreq/{pay_req}" 525 | }; 526 | } 527 | 528 | /** lncli: `listpayments` 529 | ListPayments returns a list of all outgoing payments. 530 | */ 531 | rpc ListPayments (ListPaymentsRequest) returns (ListPaymentsResponse) { 532 | option (google.api.http) = { 533 | get: "/v1/payments" 534 | }; 535 | }; 536 | 537 | /** 538 | DeleteAllPayments deletes all outgoing payments from DB. 539 | */ 540 | rpc DeleteAllPayments (DeleteAllPaymentsRequest) returns (DeleteAllPaymentsResponse) { 541 | option (google.api.http) = { 542 | delete: "/v1/payments" 543 | }; 544 | }; 545 | 546 | /** lncli: `describegraph` 547 | DescribeGraph returns a description of the latest graph state from the 548 | point of view of the node. The graph information is partitioned into two 549 | components: all the nodes/vertexes, and all the edges that connect the 550 | vertexes themselves. As this is a directed graph, the edges also contain 551 | the node directional specific routing policy which includes: the time lock 552 | delta, fee information, etc. 553 | */ 554 | rpc DescribeGraph (ChannelGraphRequest) returns (ChannelGraph) { 555 | option (google.api.http) = { 556 | get: "/v1/graph" 557 | }; 558 | } 559 | 560 | /** lncli: `getchaninfo` 561 | GetChanInfo returns the latest authenticated network announcement for the 562 | given channel identified by its channel ID: an 8-byte integer which 563 | uniquely identifies the location of transaction's funding output within the 564 | blockchain. 565 | */ 566 | rpc GetChanInfo (ChanInfoRequest) returns (ChannelEdge) { 567 | option (google.api.http) = { 568 | get: "/v1/graph/edge/{chan_id}" 569 | }; 570 | } 571 | 572 | /** lncli: `getnodeinfo` 573 | GetNodeInfo returns the latest advertised, aggregated, and authenticated 574 | channel information for the specified node identified by its public key. 575 | */ 576 | rpc GetNodeInfo (NodeInfoRequest) returns (NodeInfo) { 577 | option (google.api.http) = { 578 | get: "/v1/graph/node/{pub_key}" 579 | }; 580 | } 581 | 582 | /** lncli: `queryroutes` 583 | QueryRoutes attempts to query the daemon's Channel Router for a possible 584 | route to a target destination capable of carrying a specific amount of 585 | satoshis. The retuned route contains the full details required to craft and 586 | send an HTLC, also including the necessary information that should be 587 | present within the Sphinx packet encapsulated within the HTLC. 588 | */ 589 | rpc QueryRoutes(QueryRoutesRequest) returns (QueryRoutesResponse) { 590 | option (google.api.http) = { 591 | get: "/v1/graph/routes/{pub_key}/{amt}" 592 | }; 593 | } 594 | 595 | /** lncli: `getnetworkinfo` 596 | GetNetworkInfo returns some basic stats about the known channel graph from 597 | the point of view of the node. 598 | */ 599 | rpc GetNetworkInfo (NetworkInfoRequest) returns (NetworkInfo) { 600 | option (google.api.http) = { 601 | get: "/v1/graph/info" 602 | }; 603 | } 604 | 605 | /** lncli: `stop` 606 | StopDaemon will send a shutdown request to the interrupt handler, triggering 607 | a graceful shutdown of the daemon. 608 | */ 609 | rpc StopDaemon(StopRequest) returns (StopResponse); 610 | 611 | /** 612 | SubscribeChannelGraph launches a streaming RPC that allows the caller to 613 | receive notifications upon any changes to the channel graph topology from 614 | the point of view of the responding node. Events notified include: new 615 | nodes coming online, nodes updating their authenticated attributes, new 616 | channels being advertised, updates in the routing policy for a directional 617 | channel edge, and when channels are closed on-chain. 618 | */ 619 | rpc SubscribeChannelGraph(GraphTopologySubscription) returns (stream GraphTopologyUpdate); 620 | 621 | /** lncli: `debuglevel` 622 | DebugLevel allows a caller to programmatically set the logging verbosity of 623 | lnd. The logging can be targeted according to a coarse daemon-wide logging 624 | level, or in a granular fashion to specify the logging for a target 625 | sub-system. 626 | */ 627 | rpc DebugLevel (DebugLevelRequest) returns (DebugLevelResponse); 628 | 629 | /** lncli: `feereport` 630 | FeeReport allows the caller to obtain a report detailing the current fee 631 | schedule enforced by the node globally for each channel. 632 | */ 633 | rpc FeeReport(FeeReportRequest) returns (FeeReportResponse) { 634 | option (google.api.http) = { 635 | get: "/v1/fees" 636 | }; 637 | } 638 | 639 | /** lncli: `updatechanpolicy` 640 | UpdateChannelPolicy allows the caller to update the fee schedule and 641 | channel policies for all channels globally, or a particular channel. 642 | */ 643 | rpc UpdateChannelPolicy(PolicyUpdateRequest) returns (PolicyUpdateResponse) { 644 | option (google.api.http) = { 645 | post: "/v1/chanpolicy" 646 | body: "*" 647 | }; 648 | } 649 | 650 | /** lncli: `fwdinghistory` 651 | ForwardingHistory allows the caller to query the htlcswitch for a record of 652 | all HTLC's forwarded within the target time range, and integer offset 653 | within that time range. If no time-range is specified, then the first chunk 654 | of the past 24 hrs of forwarding history are returned. 655 | 656 | A list of forwarding events are returned. The size of each forwarding event 657 | is 40 bytes, and the max message size able to be returned in gRPC is 4 MiB. 658 | As a result each message can only contain 50k entries. Each response has 659 | the index offset of the last entry. The index offset can be provided to the 660 | request to allow the caller to skip a series of records. 661 | */ 662 | rpc ForwardingHistory(ForwardingHistoryRequest) returns (ForwardingHistoryResponse) { 663 | option (google.api.http) = { 664 | post: "/v1/switch" 665 | body: "*" 666 | }; 667 | }; 668 | } 669 | 670 | message Utxo { 671 | /// The type of address 672 | AddressType type = 1 [json_name = "address_type"]; 673 | 674 | /// The address 675 | string address = 2 [json_name = "address"]; 676 | 677 | /// The value of the unspent coin in satoshis 678 | int64 amount_sat = 3 [json_name = "amount_sat"]; 679 | 680 | /// The scriptpubkey in hex 681 | string script_pubkey = 4 [json_name = "script_pubkey"]; 682 | 683 | /// The outpoint in format txid:n 684 | /// Note that this reuses the `ChannelPoint` message but 685 | /// is not actually a channel related outpoint, of course 686 | ChannelPoint outpoint = 5 [json_name = "outpoint"]; 687 | 688 | /// The number of confirmations for the Utxo 689 | int64 confirmations = 6 [json_name = "confirmations"]; 690 | } 691 | 692 | message Transaction { 693 | /// The transaction hash 694 | string tx_hash = 1 [ json_name = "tx_hash" ]; 695 | 696 | /// The transaction amount, denominated in satoshis 697 | int64 amount = 2 [ json_name = "amount" ]; 698 | 699 | /// The number of confirmations 700 | int32 num_confirmations = 3 [ json_name = "num_confirmations" ]; 701 | 702 | /// The hash of the block this transaction was included in 703 | string block_hash = 4 [ json_name = "block_hash" ]; 704 | 705 | /// The height of the block this transaction was included in 706 | int32 block_height = 5 [ json_name = "block_height" ]; 707 | 708 | /// Timestamp of this transaction 709 | int64 time_stamp = 6 [ json_name = "time_stamp" ]; 710 | 711 | /// Fees paid for this transaction 712 | int64 total_fees = 7 [ json_name = "total_fees" ]; 713 | 714 | /// Addresses that received funds for this transaction 715 | repeated string dest_addresses = 8 [ json_name = "dest_addresses" ]; 716 | } 717 | message GetTransactionsRequest { 718 | } 719 | message TransactionDetails { 720 | /// The list of transactions relevant to the wallet. 721 | repeated Transaction transactions = 1 [json_name = "transactions"]; 722 | } 723 | 724 | message FeeLimit { 725 | oneof limit { 726 | /// The fee limit expressed as a fixed amount of satoshis. 727 | int64 fixed = 1; 728 | 729 | /// The fee limit expressed as a percentage of the payment amount. 730 | int64 percent = 2; 731 | } 732 | } 733 | 734 | message SendRequest { 735 | /// The identity pubkey of the payment recipient 736 | bytes dest = 1; 737 | 738 | /// The hex-encoded identity pubkey of the payment recipient 739 | string dest_string = 2; 740 | 741 | /// Number of satoshis to send. 742 | int64 amt = 3; 743 | 744 | /// The hash to use within the payment's HTLC 745 | bytes payment_hash = 4; 746 | 747 | /// The hex-encoded hash to use within the payment's HTLC 748 | string payment_hash_string = 5; 749 | 750 | /** 751 | A bare-bones invoice for a payment within the Lightning Network. With the 752 | details of the invoice, the sender has all the data necessary to send a 753 | payment to the recipient. 754 | */ 755 | string payment_request = 6; 756 | 757 | /** 758 | The CLTV delta from the current height that should be used to set the 759 | timelock for the final hop. 760 | */ 761 | int32 final_cltv_delta = 7; 762 | 763 | /** 764 | The maximum number of satoshis that will be paid as a fee of the payment. 765 | This value can be represented either as a percentage of the amount being 766 | sent, or as a fixed amount of the maximum fee the user is willing the pay to 767 | send the payment. 768 | */ 769 | FeeLimit fee_limit = 8; 770 | } 771 | message SendResponse { 772 | string payment_error = 1 [json_name = "payment_error"]; 773 | bytes payment_preimage = 2 [json_name = "payment_preimage"]; 774 | Route payment_route = 3 [json_name = "payment_route"]; 775 | bytes payment_hash = 4 [json_name = "payment_hash"]; 776 | } 777 | 778 | message SendToRouteRequest { 779 | /// The payment hash to use for the HTLC. 780 | bytes payment_hash = 1; 781 | 782 | /// An optional hex-encoded payment hash to be used for the HTLC. 783 | string payment_hash_string = 2; 784 | 785 | /// The set of routes that should be used to attempt to complete the payment. 786 | repeated Route routes = 3; 787 | } 788 | 789 | message ChannelPoint { 790 | oneof funding_txid { 791 | /// Txid of the funding transaction 792 | bytes funding_txid_bytes = 1 [json_name = "funding_txid_bytes"]; 793 | 794 | /// Hex-encoded string representing the funding transaction 795 | string funding_txid_str = 2 [json_name = "funding_txid_str"]; 796 | } 797 | 798 | /// The index of the output of the funding transaction 799 | uint32 output_index = 3 [json_name = "output_index"]; 800 | } 801 | 802 | message LightningAddress { 803 | /// The identity pubkey of the Lightning node 804 | string pubkey = 1 [json_name = "pubkey"]; 805 | 806 | /// The network location of the lightning node, e.g. `69.69.69.69:1337` or `localhost:10011` 807 | string host = 2 [json_name = "host"]; 808 | } 809 | 810 | message SendManyRequest { 811 | /// The map from addresses to amounts 812 | map AddrToAmount = 1; 813 | 814 | /// The target number of blocks that this transaction should be confirmed by. 815 | int32 target_conf = 3; 816 | 817 | /// A manual fee rate set in sat/byte that should be used when crafting the transaction. 818 | int64 sat_per_byte = 5; 819 | } 820 | message SendManyResponse { 821 | /// The id of the transaction 822 | string txid = 1 [json_name = "txid"]; 823 | } 824 | 825 | message SendCoinsRequest { 826 | /// The address to send coins to 827 | string addr = 1; 828 | 829 | /// The amount in satoshis to send 830 | int64 amount = 2; 831 | 832 | /// The target number of blocks that this transaction should be confirmed by. 833 | int32 target_conf = 3; 834 | 835 | /// A manual fee rate set in sat/byte that should be used when crafting the transaction. 836 | int64 sat_per_byte = 5; 837 | } 838 | message SendCoinsResponse { 839 | /// The transaction ID of the transaction 840 | string txid = 1 [json_name = "txid"]; 841 | } 842 | 843 | message ListUnspentRequest { 844 | /// The minimum number of confirmations to be included. 845 | int32 min_confs = 1; 846 | 847 | /// The maximum number of confirmations to be included. 848 | int32 max_confs = 2; 849 | } 850 | message ListUnspentResponse { 851 | /// A list of utxos 852 | repeated Utxo utxos = 1 [json_name = "utxos"]; 853 | 854 | } 855 | 856 | /** 857 | `AddressType` has to be one of: 858 | 859 | - `p2wkh`: Pay to witness key hash (`WITNESS_PUBKEY_HASH` = 0) 860 | - `np2wkh`: Pay to nested witness key hash (`NESTED_PUBKEY_HASH` = 1) 861 | */ 862 | enum AddressType { 863 | WITNESS_PUBKEY_HASH = 0; 864 | NESTED_PUBKEY_HASH = 1; 865 | } 866 | 867 | message NewAddressRequest { 868 | /// The address type 869 | AddressType type = 1; 870 | } 871 | message NewAddressResponse { 872 | /// The newly generated wallet address 873 | string address = 1 [json_name = "address"]; 874 | } 875 | 876 | message SignMessageRequest { 877 | /// The message to be signed 878 | bytes msg = 1 [ json_name = "msg" ]; 879 | } 880 | message SignMessageResponse { 881 | /// The signature for the given message 882 | string signature = 1 [ json_name = "signature" ]; 883 | } 884 | 885 | message VerifyMessageRequest { 886 | /// The message over which the signature is to be verified 887 | bytes msg = 1 [ json_name = "msg" ]; 888 | 889 | /// The signature to be verified over the given message 890 | string signature = 2 [ json_name = "signature" ]; 891 | } 892 | message VerifyMessageResponse { 893 | /// Whether the signature was valid over the given message 894 | bool valid = 1 [ json_name = "valid" ]; 895 | 896 | /// The pubkey recovered from the signature 897 | string pubkey = 2 [ json_name = "pubkey" ]; 898 | } 899 | 900 | message ConnectPeerRequest { 901 | /// Lightning address of the peer, in the format `@host` 902 | LightningAddress addr = 1; 903 | 904 | /** If set, the daemon will attempt to persistently connect to the target 905 | * peer. Otherwise, the call will be synchronous. */ 906 | bool perm = 2; 907 | } 908 | message ConnectPeerResponse { 909 | } 910 | 911 | message DisconnectPeerRequest { 912 | /// The pubkey of the node to disconnect from 913 | string pub_key = 1 [json_name = "pub_key"]; 914 | } 915 | message DisconnectPeerResponse { 916 | } 917 | 918 | message HTLC { 919 | bool incoming = 1 [json_name = "incoming"]; 920 | int64 amount = 2 [json_name = "amount"]; 921 | bytes hash_lock = 3 [json_name = "hash_lock"]; 922 | uint32 expiration_height = 4 [json_name = "expiration_height"]; 923 | } 924 | 925 | message Channel { 926 | /// Whether this channel is active or not 927 | bool active = 1 [json_name = "active"]; 928 | 929 | /// The identity pubkey of the remote node 930 | string remote_pubkey = 2 [json_name = "remote_pubkey"]; 931 | 932 | /** 933 | The outpoint (txid:index) of the funding transaction. With this value, Bob 934 | will be able to generate a signature for Alice's version of the commitment 935 | transaction. 936 | */ 937 | string channel_point = 3 [json_name = "channel_point"]; 938 | 939 | /** 940 | The unique channel ID for the channel. The first 3 bytes are the block 941 | height, the next 3 the index within the block, and the last 2 bytes are the 942 | output index for the channel. 943 | */ 944 | uint64 chan_id = 4 [json_name = "chan_id"]; 945 | 946 | /// The total amount of funds held in this channel 947 | int64 capacity = 5 [json_name = "capacity"]; 948 | 949 | /// This node's current balance in this channel 950 | int64 local_balance = 6 [json_name = "local_balance"]; 951 | 952 | /// The counterparty's current balance in this channel 953 | int64 remote_balance = 7 [json_name = "remote_balance"]; 954 | 955 | /** 956 | The amount calculated to be paid in fees for the current set of commitment 957 | transactions. The fee amount is persisted with the channel in order to 958 | allow the fee amount to be removed and recalculated with each channel state 959 | update, including updates that happen after a system restart. 960 | */ 961 | int64 commit_fee = 8 [json_name = "commit_fee"]; 962 | 963 | /// The weight of the commitment transaction 964 | int64 commit_weight = 9 [json_name = "commit_weight"]; 965 | 966 | /** 967 | The required number of satoshis per kilo-weight that the requester will pay 968 | at all times, for both the funding transaction and commitment transaction. 969 | This value can later be updated once the channel is open. 970 | */ 971 | int64 fee_per_kw = 10 [json_name = "fee_per_kw"]; 972 | 973 | /// The unsettled balance in this channel 974 | int64 unsettled_balance = 11 [json_name = "unsettled_balance"]; 975 | 976 | /** 977 | The total number of satoshis we've sent within this channel. 978 | */ 979 | int64 total_satoshis_sent = 12 [json_name = "total_satoshis_sent"]; 980 | 981 | /** 982 | The total number of satoshis we've received within this channel. 983 | */ 984 | int64 total_satoshis_received = 13 [json_name = "total_satoshis_received"]; 985 | 986 | /** 987 | The total number of updates conducted within this channel. 988 | */ 989 | uint64 num_updates = 14 [json_name = "num_updates"]; 990 | 991 | /** 992 | The list of active, uncleared HTLCs currently pending within the channel. 993 | */ 994 | repeated HTLC pending_htlcs = 15 [json_name = "pending_htlcs"]; 995 | 996 | /** 997 | The CSV delay expressed in relative blocks. If the channel is force 998 | closed, we'll need to wait for this many blocks before we can regain our 999 | funds. 1000 | */ 1001 | uint32 csv_delay = 16 [json_name = "csv_delay"]; 1002 | 1003 | /// Whether this channel is advertised to the network or not 1004 | bool private = 17 [json_name = "private"]; 1005 | } 1006 | 1007 | 1008 | message ListChannelsRequest { 1009 | bool active_only = 1; 1010 | bool inactive_only = 2; 1011 | bool public_only = 3; 1012 | bool private_only = 4; 1013 | } 1014 | message ListChannelsResponse { 1015 | /// The list of active channels 1016 | repeated Channel channels = 11 [json_name = "channels"]; 1017 | } 1018 | 1019 | message ChannelCloseSummary { 1020 | /// The outpoint (txid:index) of the funding transaction. 1021 | string channel_point = 1 [json_name = "channel_point"]; 1022 | 1023 | /// The unique channel ID for the channel. 1024 | uint64 chan_id = 2 [json_name = "chan_id"]; 1025 | 1026 | /// The hash of the genesis block that this channel resides within. 1027 | string chain_hash = 3 [json_name = "chain_hash"]; 1028 | 1029 | /// The txid of the transaction which ultimately closed this channel. 1030 | string closing_tx_hash = 4 [json_name = "closing_tx_hash"]; 1031 | 1032 | /// Public key of the remote peer that we formerly had a channel with. 1033 | string remote_pubkey = 5 [json_name = "remote_pubkey"]; 1034 | 1035 | /// Total capacity of the channel. 1036 | int64 capacity = 6 [json_name = "capacity"]; 1037 | 1038 | /// Height at which the funding transaction was spent. 1039 | uint32 close_height = 7 [json_name = "close_height"]; 1040 | 1041 | /// Settled balance at the time of channel closure 1042 | int64 settled_balance = 8 [json_name = "settled_balance"]; 1043 | 1044 | /// The sum of all the time-locked outputs at the time of channel closure 1045 | int64 time_locked_balance = 9 [json_name = "time_locked_balance"]; 1046 | 1047 | enum ClosureType { 1048 | COOPERATIVE_CLOSE = 0; 1049 | LOCAL_FORCE_CLOSE = 1; 1050 | REMOTE_FORCE_CLOSE = 2; 1051 | BREACH_CLOSE = 3; 1052 | FUNDING_CANCELED = 4; 1053 | ABANDONED = 5; 1054 | } 1055 | 1056 | /// Details on how the channel was closed. 1057 | ClosureType close_type = 10 [json_name = "close_type"]; 1058 | } 1059 | 1060 | message ClosedChannelsRequest { 1061 | bool cooperative = 1; 1062 | bool local_force = 2; 1063 | bool remote_force = 3; 1064 | bool breach = 4; 1065 | bool funding_canceled = 5; 1066 | bool abandoned = 6; 1067 | } 1068 | 1069 | message ClosedChannelsResponse { 1070 | repeated ChannelCloseSummary channels = 1 [json_name = "channels"]; 1071 | } 1072 | 1073 | message Peer { 1074 | /// The identity pubkey of the peer 1075 | string pub_key = 1 [json_name = "pub_key"]; 1076 | 1077 | /// Network address of the peer; eg `127.0.0.1:10011` 1078 | string address = 3 [json_name = "address"]; 1079 | 1080 | /// Bytes of data transmitted to this peer 1081 | uint64 bytes_sent = 4 [json_name = "bytes_sent"]; 1082 | 1083 | /// Bytes of data transmitted from this peer 1084 | uint64 bytes_recv = 5 [json_name = "bytes_recv"]; 1085 | 1086 | /// Satoshis sent to this peer 1087 | int64 sat_sent = 6 [json_name = "sat_sent"]; 1088 | 1089 | /// Satoshis received from this peer 1090 | int64 sat_recv = 7 [json_name = "sat_recv"]; 1091 | 1092 | /// A channel is inbound if the counterparty initiated the channel 1093 | bool inbound = 8 [json_name = "inbound"]; 1094 | 1095 | /// Ping time to this peer 1096 | int64 ping_time = 9 [json_name = "ping_time"]; 1097 | } 1098 | 1099 | message ListPeersRequest { 1100 | } 1101 | message ListPeersResponse { 1102 | /// The list of currently connected peers 1103 | repeated Peer peers = 1 [json_name = "peers"]; 1104 | } 1105 | 1106 | message GetInfoRequest { 1107 | } 1108 | message GetInfoResponse { 1109 | 1110 | /// The identity pubkey of the current node. 1111 | string identity_pubkey = 1 [json_name = "identity_pubkey"]; 1112 | 1113 | /// If applicable, the alias of the current node, e.g. "bob" 1114 | string alias = 2 [json_name = "alias"]; 1115 | 1116 | /// Number of pending channels 1117 | uint32 num_pending_channels = 3 [json_name = "num_pending_channels"]; 1118 | 1119 | /// Number of active channels 1120 | uint32 num_active_channels = 4 [json_name = "num_active_channels"]; 1121 | 1122 | /// Number of peers 1123 | uint32 num_peers = 5 [json_name = "num_peers"]; 1124 | 1125 | /// The node's current view of the height of the best block 1126 | uint32 block_height = 6 [json_name = "block_height"]; 1127 | 1128 | /// The node's current view of the hash of the best block 1129 | string block_hash = 8 [json_name = "block_hash"]; 1130 | 1131 | /// Whether the wallet's view is synced to the main chain 1132 | bool synced_to_chain = 9 [json_name = "synced_to_chain"]; 1133 | 1134 | /// Whether the current node is connected to testnet 1135 | bool testnet = 10 [json_name = "testnet"]; 1136 | 1137 | /// A list of active chains the node is connected to 1138 | repeated string chains = 11 [json_name = "chains"]; 1139 | 1140 | /// The URIs of the current node. 1141 | repeated string uris = 12 [json_name = "uris"]; 1142 | 1143 | /// Timestamp of the block best known to the wallet 1144 | int64 best_header_timestamp = 13 [ json_name = "best_header_timestamp" ]; 1145 | 1146 | /// The version of the LND software that the node is running. 1147 | string version = 14 [ json_name = "version" ]; 1148 | 1149 | /// Number of inactive channels 1150 | uint32 num_inactive_channels = 15 [json_name = "num_inactive_channels"]; 1151 | } 1152 | 1153 | message ConfirmationUpdate { 1154 | bytes block_sha = 1; 1155 | int32 block_height = 2; 1156 | 1157 | uint32 num_confs_left = 3; 1158 | } 1159 | 1160 | message ChannelOpenUpdate { 1161 | ChannelPoint channel_point = 1 [json_name = "channel_point"]; 1162 | } 1163 | 1164 | message ChannelCloseUpdate { 1165 | bytes closing_txid = 1 [json_name = "closing_txid"]; 1166 | 1167 | bool success = 2 [json_name = "success"]; 1168 | } 1169 | 1170 | message CloseChannelRequest { 1171 | /** 1172 | The outpoint (txid:index) of the funding transaction. With this value, Bob 1173 | will be able to generate a signature for Alice's version of the commitment 1174 | transaction. 1175 | */ 1176 | ChannelPoint channel_point = 1; 1177 | 1178 | /// If true, then the channel will be closed forcibly. This means the current commitment transaction will be signed and broadcast. 1179 | bool force = 2; 1180 | 1181 | /// The target number of blocks that the closure transaction should be confirmed by. 1182 | int32 target_conf = 3; 1183 | 1184 | /// A manual fee rate set in sat/byte that should be used when crafting the closure transaction. 1185 | int64 sat_per_byte = 4; 1186 | } 1187 | 1188 | message CloseStatusUpdate { 1189 | oneof update { 1190 | PendingUpdate close_pending = 1 [json_name = "close_pending"]; 1191 | ConfirmationUpdate confirmation = 2 [json_name = "confirmation"]; 1192 | ChannelCloseUpdate chan_close = 3 [json_name = "chan_close"]; 1193 | } 1194 | } 1195 | 1196 | message PendingUpdate { 1197 | bytes txid = 1 [json_name = "txid"]; 1198 | uint32 output_index = 2 [json_name = "output_index"]; 1199 | } 1200 | 1201 | message OpenChannelRequest { 1202 | /// The pubkey of the node to open a channel with 1203 | bytes node_pubkey = 2 [json_name = "node_pubkey"]; 1204 | 1205 | /// The hex encoded pubkey of the node to open a channel with 1206 | string node_pubkey_string = 3 [json_name = "node_pubkey_string"]; 1207 | 1208 | /// The number of satoshis the wallet should commit to the channel 1209 | int64 local_funding_amount = 4 [json_name = "local_funding_amount"]; 1210 | 1211 | /// The number of satoshis to push to the remote side as part of the initial commitment state 1212 | int64 push_sat = 5 [json_name = "push_sat"]; 1213 | 1214 | /// The target number of blocks that the funding transaction should be confirmed by. 1215 | int32 target_conf = 6; 1216 | 1217 | /// A manual fee rate set in sat/byte that should be used when crafting the funding transaction. 1218 | int64 sat_per_byte = 7; 1219 | 1220 | /// Whether this channel should be private, not announced to the greater network. 1221 | bool private = 8 [json_name = "private"]; 1222 | 1223 | /// The minimum value in millisatoshi we will require for incoming HTLCs on the channel. 1224 | int64 min_htlc_msat = 9 [json_name = "min_htlc_msat"]; 1225 | 1226 | /// The delay we require on the remote's commitment transaction. If this is not set, it will be scaled automatically with the channel size. 1227 | uint32 remote_csv_delay = 10 [json_name = "remote_csv_delay"]; 1228 | 1229 | /// The minimum number of confirmations each one of your outputs used for the funding transaction must satisfy. 1230 | int32 min_confs = 11 [json_name = "min_confs"]; 1231 | 1232 | /// Whether unconfirmed outputs should be used as inputs for the funding transaction. 1233 | bool spend_unconfirmed = 12 [json_name = "spend_unconfirmed"]; 1234 | } 1235 | message OpenStatusUpdate { 1236 | oneof update { 1237 | PendingUpdate chan_pending = 1 [json_name = "chan_pending"]; 1238 | ConfirmationUpdate confirmation = 2 [json_name = "confirmation"]; 1239 | ChannelOpenUpdate chan_open = 3 [json_name = "chan_open"]; 1240 | } 1241 | } 1242 | 1243 | message PendingHTLC { 1244 | 1245 | /// The direction within the channel that the htlc was sent 1246 | bool incoming = 1 [ json_name = "incoming" ]; 1247 | 1248 | /// The total value of the htlc 1249 | int64 amount = 2 [ json_name = "amount" ]; 1250 | 1251 | /// The final output to be swept back to the user's wallet 1252 | string outpoint = 3 [ json_name = "outpoint" ]; 1253 | 1254 | /// The next block height at which we can spend the current stage 1255 | uint32 maturity_height = 4 [ json_name = "maturity_height" ]; 1256 | 1257 | /** 1258 | The number of blocks remaining until the current stage can be swept. 1259 | Negative values indicate how many blocks have passed since becoming 1260 | mature. 1261 | */ 1262 | int32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ]; 1263 | 1264 | /// Indicates whether the htlc is in its first or second stage of recovery 1265 | uint32 stage = 6 [ json_name = "stage" ]; 1266 | } 1267 | 1268 | message PendingChannelsRequest {} 1269 | message PendingChannelsResponse { 1270 | message PendingChannel { 1271 | string remote_node_pub = 1 [ json_name = "remote_node_pub" ]; 1272 | string channel_point = 2 [ json_name = "channel_point" ]; 1273 | 1274 | int64 capacity = 3 [ json_name = "capacity" ]; 1275 | 1276 | int64 local_balance = 4 [ json_name = "local_balance" ]; 1277 | int64 remote_balance = 5 [ json_name = "remote_balance" ]; 1278 | } 1279 | 1280 | message PendingOpenChannel { 1281 | /// The pending channel 1282 | PendingChannel channel = 1 [ json_name = "channel" ]; 1283 | 1284 | /// The height at which this channel will be confirmed 1285 | uint32 confirmation_height = 2 [ json_name = "confirmation_height" ]; 1286 | 1287 | /** 1288 | The amount calculated to be paid in fees for the current set of 1289 | commitment transactions. The fee amount is persisted with the channel 1290 | in order to allow the fee amount to be removed and recalculated with 1291 | each channel state update, including updates that happen after a system 1292 | restart. 1293 | */ 1294 | int64 commit_fee = 4 [json_name = "commit_fee" ]; 1295 | 1296 | /// The weight of the commitment transaction 1297 | int64 commit_weight = 5 [ json_name = "commit_weight" ]; 1298 | 1299 | /** 1300 | The required number of satoshis per kilo-weight that the requester will 1301 | pay at all times, for both the funding transaction and commitment 1302 | transaction. This value can later be updated once the channel is open. 1303 | */ 1304 | int64 fee_per_kw = 6 [ json_name = "fee_per_kw" ]; 1305 | } 1306 | 1307 | message WaitingCloseChannel { 1308 | /// The pending channel waiting for closing tx to confirm 1309 | PendingChannel channel = 1; 1310 | 1311 | /// The balance in satoshis encumbered in this channel 1312 | int64 limbo_balance = 2 [ json_name = "limbo_balance" ]; 1313 | } 1314 | 1315 | message ClosedChannel { 1316 | /// The pending channel to be closed 1317 | PendingChannel channel = 1; 1318 | 1319 | /// The transaction id of the closing transaction 1320 | string closing_txid = 2 [ json_name = "closing_txid" ]; 1321 | } 1322 | 1323 | message ForceClosedChannel { 1324 | /// The pending channel to be force closed 1325 | PendingChannel channel = 1 [ json_name = "channel" ]; 1326 | 1327 | /// The transaction id of the closing transaction 1328 | string closing_txid = 2 [ json_name = "closing_txid" ]; 1329 | 1330 | /// The balance in satoshis encumbered in this pending channel 1331 | int64 limbo_balance = 3 [ json_name = "limbo_balance" ]; 1332 | 1333 | /// The height at which funds can be sweeped into the wallet 1334 | uint32 maturity_height = 4 [ json_name = "maturity_height" ]; 1335 | 1336 | /* 1337 | Remaining # of blocks until the commitment output can be swept. 1338 | Negative values indicate how many blocks have passed since becoming 1339 | mature. 1340 | */ 1341 | int32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ]; 1342 | 1343 | /// The total value of funds successfully recovered from this channel 1344 | int64 recovered_balance = 6 [ json_name = "recovered_balance" ]; 1345 | 1346 | repeated PendingHTLC pending_htlcs = 8 [ json_name = "pending_htlcs" ]; 1347 | } 1348 | 1349 | /// The balance in satoshis encumbered in pending channels 1350 | int64 total_limbo_balance = 1 [ json_name = "total_limbo_balance" ]; 1351 | 1352 | /// Channels pending opening 1353 | repeated PendingOpenChannel pending_open_channels = 2 [ json_name = "pending_open_channels" ]; 1354 | 1355 | /// Channels pending closing 1356 | repeated ClosedChannel pending_closing_channels = 3 [ json_name = "pending_closing_channels" ]; 1357 | 1358 | /// Channels pending force closing 1359 | repeated ForceClosedChannel pending_force_closing_channels = 4 [ json_name = "pending_force_closing_channels" ]; 1360 | 1361 | /// Channels waiting for closing tx to confirm 1362 | repeated WaitingCloseChannel waiting_close_channels = 5 [ json_name = "waiting_close_channels" ]; 1363 | } 1364 | 1365 | message WalletBalanceRequest { 1366 | } 1367 | message WalletBalanceResponse { 1368 | /// The balance of the wallet 1369 | int64 total_balance = 1 [json_name = "total_balance"]; 1370 | 1371 | /// The confirmed balance of a wallet(with >= 1 confirmations) 1372 | int64 confirmed_balance = 2 [json_name = "confirmed_balance"]; 1373 | 1374 | /// The unconfirmed balance of a wallet(with 0 confirmations) 1375 | int64 unconfirmed_balance = 3 [json_name = "unconfirmed_balance"]; 1376 | } 1377 | 1378 | message ChannelBalanceRequest { 1379 | } 1380 | message ChannelBalanceResponse { 1381 | /// Sum of channels balances denominated in satoshis 1382 | int64 balance = 1 [json_name = "balance"]; 1383 | 1384 | /// Sum of channels pending balances denominated in satoshis 1385 | int64 pending_open_balance = 2 [json_name = "pending_open_balance"]; 1386 | } 1387 | 1388 | message QueryRoutesRequest { 1389 | /// The 33-byte hex-encoded public key for the payment destination 1390 | string pub_key = 1; 1391 | 1392 | /// The amount to send expressed in satoshis 1393 | int64 amt = 2; 1394 | 1395 | /// The max number of routes to return. 1396 | int32 num_routes = 3; 1397 | 1398 | /// An optional CLTV delta from the current height that should be used for the timelock of the final hop 1399 | int32 final_cltv_delta = 4; 1400 | 1401 | /** 1402 | The maximum number of satoshis that will be paid as a fee of the payment. 1403 | This value can be represented either as a percentage of the amount being 1404 | sent, or as a fixed amount of the maximum fee the user is willing the pay to 1405 | send the payment. 1406 | */ 1407 | FeeLimit fee_limit = 5; 1408 | } 1409 | message QueryRoutesResponse { 1410 | repeated Route routes = 1 [json_name = "routes"]; 1411 | } 1412 | 1413 | message Hop { 1414 | /** 1415 | The unique channel ID for the channel. The first 3 bytes are the block 1416 | height, the next 3 the index within the block, and the last 2 bytes are the 1417 | output index for the channel. 1418 | */ 1419 | uint64 chan_id = 1 [json_name = "chan_id"]; 1420 | int64 chan_capacity = 2 [json_name = "chan_capacity"]; 1421 | int64 amt_to_forward = 3 [json_name = "amt_to_forward", deprecated = true]; 1422 | int64 fee = 4 [json_name = "fee", deprecated = true]; 1423 | uint32 expiry = 5 [json_name = "expiry"]; 1424 | int64 amt_to_forward_msat = 6 [json_name = "amt_to_forward_msat"]; 1425 | int64 fee_msat = 7 [json_name = "fee_msat"]; 1426 | 1427 | /** 1428 | An optional public key of the hop. If the public key is given, the payment 1429 | can be executed without relying on a copy of the channel graph. 1430 | */ 1431 | string pub_key = 8 [json_name = "pub_key"]; 1432 | } 1433 | 1434 | /** 1435 | A path through the channel graph which runs over one or more channels in 1436 | succession. This struct carries all the information required to craft the 1437 | Sphinx onion packet, and send the payment along the first hop in the path. A 1438 | route is only selected as valid if all the channels have sufficient capacity to 1439 | carry the initial payment amount after fees are accounted for. 1440 | */ 1441 | message Route { 1442 | 1443 | /** 1444 | The cumulative (final) time lock across the entire route. This is the CLTV 1445 | value that should be extended to the first hop in the route. All other hops 1446 | will decrement the time-lock as advertised, leaving enough time for all 1447 | hops to wait for or present the payment preimage to complete the payment. 1448 | */ 1449 | uint32 total_time_lock = 1 [json_name = "total_time_lock"]; 1450 | 1451 | /** 1452 | The sum of the fees paid at each hop within the final route. In the case 1453 | of a one-hop payment, this value will be zero as we don't need to pay a fee 1454 | it ourself. 1455 | */ 1456 | int64 total_fees = 2 [json_name = "total_fees", deprecated = true]; 1457 | 1458 | /** 1459 | The total amount of funds required to complete a payment over this route. 1460 | This value includes the cumulative fees at each hop. As a result, the HTLC 1461 | extended to the first-hop in the route will need to have at least this many 1462 | satoshis, otherwise the route will fail at an intermediate node due to an 1463 | insufficient amount of fees. 1464 | */ 1465 | int64 total_amt = 3 [json_name = "total_amt", deprecated = true]; 1466 | 1467 | /** 1468 | Contains details concerning the specific forwarding details at each hop. 1469 | */ 1470 | repeated Hop hops = 4 [json_name = "hops"]; 1471 | 1472 | /** 1473 | The total fees in millisatoshis. 1474 | */ 1475 | int64 total_fees_msat = 5 [json_name = "total_fees_msat"]; 1476 | 1477 | /** 1478 | The total amount in millisatoshis. 1479 | */ 1480 | int64 total_amt_msat = 6 [json_name = "total_amt_msat"]; 1481 | } 1482 | 1483 | message NodeInfoRequest { 1484 | /// The 33-byte hex-encoded compressed public of the target node 1485 | string pub_key = 1; 1486 | } 1487 | 1488 | message NodeInfo { 1489 | 1490 | /** 1491 | An individual vertex/node within the channel graph. A node is 1492 | connected to other nodes by one or more channel edges emanating from it. As 1493 | the graph is directed, a node will also have an incoming edge attached to 1494 | it for each outgoing edge. 1495 | */ 1496 | LightningNode node = 1 [json_name = "node"]; 1497 | 1498 | uint32 num_channels = 2 [json_name = "num_channels"]; 1499 | int64 total_capacity = 3 [json_name = "total_capacity"]; 1500 | } 1501 | 1502 | /** 1503 | An individual vertex/node within the channel graph. A node is 1504 | connected to other nodes by one or more channel edges emanating from it. As the 1505 | graph is directed, a node will also have an incoming edge attached to it for 1506 | each outgoing edge. 1507 | */ 1508 | message LightningNode { 1509 | uint32 last_update = 1 [ json_name = "last_update" ]; 1510 | string pub_key = 2 [ json_name = "pub_key" ]; 1511 | string alias = 3 [ json_name = "alias" ]; 1512 | repeated NodeAddress addresses = 4 [ json_name = "addresses" ]; 1513 | string color = 5 [ json_name = "color" ]; 1514 | } 1515 | 1516 | message NodeAddress { 1517 | string network = 1 [ json_name = "network" ]; 1518 | string addr = 2 [ json_name = "addr" ]; 1519 | } 1520 | 1521 | message RoutingPolicy { 1522 | uint32 time_lock_delta = 1 [json_name = "time_lock_delta"]; 1523 | int64 min_htlc = 2 [json_name = "min_htlc"]; 1524 | int64 fee_base_msat = 3 [json_name = "fee_base_msat"]; 1525 | int64 fee_rate_milli_msat = 4 [json_name = "fee_rate_milli_msat"]; 1526 | bool disabled = 5 [json_name = "disabled"]; 1527 | } 1528 | 1529 | /** 1530 | A fully authenticated channel along with all its unique attributes. 1531 | Once an authenticated channel announcement has been processed on the network, 1532 | then an instance of ChannelEdgeInfo encapsulating the channels attributes is 1533 | stored. The other portions relevant to routing policy of a channel are stored 1534 | within a ChannelEdgePolicy for each direction of the channel. 1535 | */ 1536 | message ChannelEdge { 1537 | 1538 | /** 1539 | The unique channel ID for the channel. The first 3 bytes are the block 1540 | height, the next 3 the index within the block, and the last 2 bytes are the 1541 | output index for the channel. 1542 | */ 1543 | uint64 channel_id = 1 [json_name = "channel_id"]; 1544 | string chan_point = 2 [json_name = "chan_point"]; 1545 | 1546 | uint32 last_update = 3 [json_name = "last_update"]; 1547 | 1548 | string node1_pub = 4 [json_name = "node1_pub"]; 1549 | string node2_pub = 5 [json_name = "node2_pub"]; 1550 | 1551 | int64 capacity = 6 [json_name = "capacity"]; 1552 | 1553 | RoutingPolicy node1_policy = 7 [json_name = "node1_policy"]; 1554 | RoutingPolicy node2_policy = 8 [json_name = "node2_policy"]; 1555 | } 1556 | 1557 | message ChannelGraphRequest { 1558 | /** 1559 | Whether unannounced channels are included in the response or not. If set, 1560 | unannounced channels are included. Unannounced channels are both private 1561 | channels, and public channels that are not yet announced to the network. 1562 | */ 1563 | bool include_unannounced = 1 [json_name = "include_unannounced"]; 1564 | } 1565 | 1566 | /// Returns a new instance of the directed channel graph. 1567 | message ChannelGraph { 1568 | /// The list of `LightningNode`s in this channel graph 1569 | repeated LightningNode nodes = 1 [json_name = "nodes"]; 1570 | 1571 | /// The list of `ChannelEdge`s in this channel graph 1572 | repeated ChannelEdge edges = 2 [json_name = "edges"]; 1573 | } 1574 | 1575 | message ChanInfoRequest { 1576 | /** 1577 | The unique channel ID for the channel. The first 3 bytes are the block 1578 | height, the next 3 the index within the block, and the last 2 bytes are the 1579 | output index for the channel. 1580 | */ 1581 | uint64 chan_id = 1; 1582 | } 1583 | 1584 | message NetworkInfoRequest { 1585 | } 1586 | message NetworkInfo { 1587 | uint32 graph_diameter = 1 [json_name = "graph_diameter"]; 1588 | double avg_out_degree = 2 [json_name = "avg_out_degree"]; 1589 | uint32 max_out_degree = 3 [json_name = "max_out_degree"]; 1590 | 1591 | uint32 num_nodes = 4 [json_name = "num_nodes"]; 1592 | uint32 num_channels = 5 [json_name = "num_channels"]; 1593 | 1594 | int64 total_network_capacity = 6 [json_name = "total_network_capacity"]; 1595 | 1596 | double avg_channel_size = 7 [json_name = "avg_channel_size"]; 1597 | int64 min_channel_size = 8 [json_name = "min_channel_size"]; 1598 | int64 max_channel_size = 9 [json_name = "max_channel_size"]; 1599 | 1600 | // TODO(roasbeef): fee rate info, expiry 1601 | // * also additional RPC for tracking fee info once in 1602 | } 1603 | 1604 | message StopRequest{} 1605 | message StopResponse{} 1606 | 1607 | message GraphTopologySubscription {} 1608 | message GraphTopologyUpdate { 1609 | repeated NodeUpdate node_updates = 1; 1610 | repeated ChannelEdgeUpdate channel_updates = 2; 1611 | repeated ClosedChannelUpdate closed_chans = 3; 1612 | } 1613 | message NodeUpdate { 1614 | repeated string addresses = 1; 1615 | string identity_key = 2; 1616 | bytes global_features = 3; 1617 | string alias = 4; 1618 | } 1619 | message ChannelEdgeUpdate { 1620 | /** 1621 | The unique channel ID for the channel. The first 3 bytes are the block 1622 | height, the next 3 the index within the block, and the last 2 bytes are the 1623 | output index for the channel. 1624 | */ 1625 | uint64 chan_id = 1; 1626 | 1627 | ChannelPoint chan_point = 2; 1628 | 1629 | int64 capacity = 3; 1630 | 1631 | RoutingPolicy routing_policy = 4; 1632 | 1633 | string advertising_node = 5; 1634 | string connecting_node = 6; 1635 | } 1636 | message ClosedChannelUpdate { 1637 | /** 1638 | The unique channel ID for the channel. The first 3 bytes are the block 1639 | height, the next 3 the index within the block, and the last 2 bytes are the 1640 | output index for the channel. 1641 | */ 1642 | uint64 chan_id = 1; 1643 | int64 capacity = 2; 1644 | uint32 closed_height = 3; 1645 | ChannelPoint chan_point = 4; 1646 | } 1647 | 1648 | message HopHint { 1649 | /// The public key of the node at the start of the channel. 1650 | string node_id = 1 [json_name = "node_id"]; 1651 | 1652 | /// The unique identifier of the channel. 1653 | uint64 chan_id = 2 [json_name = "chan_id"]; 1654 | 1655 | /// The base fee of the channel denominated in millisatoshis. 1656 | uint32 fee_base_msat = 3 [json_name = "fee_base_msat"]; 1657 | 1658 | /** 1659 | The fee rate of the channel for sending one satoshi across it denominated in 1660 | millionths of a satoshi. 1661 | */ 1662 | uint32 fee_proportional_millionths = 4 [json_name = "fee_proportional_millionths"]; 1663 | 1664 | /// The time-lock delta of the channel. 1665 | uint32 cltv_expiry_delta = 5 [json_name = "cltv_expiry_delta"]; 1666 | } 1667 | 1668 | message RouteHint { 1669 | /** 1670 | A list of hop hints that when chained together can assist in reaching a 1671 | specific destination. 1672 | */ 1673 | repeated HopHint hop_hints = 1 [json_name = "hop_hints"]; 1674 | } 1675 | 1676 | message Invoice { 1677 | /** 1678 | An optional memo to attach along with the invoice. Used for record keeping 1679 | purposes for the invoice's creator, and will also be set in the description 1680 | field of the encoded payment request if the description_hash field is not 1681 | being used. 1682 | */ 1683 | string memo = 1 [json_name = "memo"]; 1684 | 1685 | /// An optional cryptographic receipt of payment 1686 | bytes receipt = 2 [json_name = "receipt"]; 1687 | 1688 | /** 1689 | The hex-encoded preimage (32 byte) which will allow settling an incoming 1690 | HTLC payable to this preimage 1691 | */ 1692 | bytes r_preimage = 3 [json_name = "r_preimage"]; 1693 | 1694 | /// The hash of the preimage 1695 | bytes r_hash = 4 [json_name = "r_hash"]; 1696 | 1697 | /// The value of this invoice in satoshis 1698 | int64 value = 5 [json_name = "value"]; 1699 | 1700 | /// Whether this invoice has been fulfilled 1701 | bool settled = 6 [json_name = "settled"]; 1702 | 1703 | /// When this invoice was created 1704 | int64 creation_date = 7 [json_name = "creation_date"]; 1705 | 1706 | /// When this invoice was settled 1707 | int64 settle_date = 8 [json_name = "settle_date"]; 1708 | 1709 | /** 1710 | A bare-bones invoice for a payment within the Lightning Network. With the 1711 | details of the invoice, the sender has all the data necessary to send a 1712 | payment to the recipient. 1713 | */ 1714 | string payment_request = 9 [json_name = "payment_request"]; 1715 | 1716 | /** 1717 | Hash (SHA-256) of a description of the payment. Used if the description of 1718 | payment (memo) is too long to naturally fit within the description field 1719 | of an encoded payment request. 1720 | */ 1721 | bytes description_hash = 10 [json_name = "description_hash"]; 1722 | 1723 | /// Payment request expiry time in seconds. Default is 3600 (1 hour). 1724 | int64 expiry = 11 [json_name = "expiry"]; 1725 | 1726 | /// Fallback on-chain address. 1727 | string fallback_addr = 12 [json_name = "fallback_addr"]; 1728 | 1729 | /// Delta to use for the time-lock of the CLTV extended to the final hop. 1730 | uint64 cltv_expiry = 13 [json_name = "cltv_expiry"]; 1731 | 1732 | /** 1733 | Route hints that can each be individually used to assist in reaching the 1734 | invoice's destination. 1735 | */ 1736 | repeated RouteHint route_hints = 14 [json_name = "route_hints"]; 1737 | 1738 | /// Whether this invoice should include routing hints for private channels. 1739 | bool private = 15 [json_name = "private"]; 1740 | 1741 | /** 1742 | The "add" index of this invoice. Each newly created invoice will increment 1743 | this index making it monotonically increasing. Callers to the 1744 | SubscribeInvoices call can use this to instantly get notified of all added 1745 | invoices with an add_index greater than this one. 1746 | */ 1747 | uint64 add_index = 16 [json_name = "add_index"]; 1748 | 1749 | /** 1750 | The "settle" index of this invoice. Each newly settled invoice will 1751 | increment this index making it monotonically increasing. Callers to the 1752 | SubscribeInvoices call can use this to instantly get notified of all 1753 | settled invoices with an settle_index greater than this one. 1754 | */ 1755 | uint64 settle_index = 17 [json_name = "settle_index"]; 1756 | 1757 | /// Deprecated, use amt_paid_sat or amt_paid_msat. 1758 | int64 amt_paid = 18 [json_name = "amt_paid", deprecated = true]; 1759 | 1760 | /** 1761 | The amount that was accepted for this invoice, in satoshis. This will ONLY 1762 | be set if this invoice has been settled. We provide this field as if the 1763 | invoice was created with a zero value, then we need to record what amount 1764 | was ultimately accepted. Additionally, it's possible that the sender paid 1765 | MORE that was specified in the original invoice. So we'll record that here 1766 | as well. 1767 | */ 1768 | int64 amt_paid_sat = 19 [json_name = "amt_paid_sat"]; 1769 | 1770 | /** 1771 | The amount that was accepted for this invoice, in millisatoshis. This will 1772 | ONLY be set if this invoice has been settled. We provide this field as if 1773 | the invoice was created with a zero value, then we need to record what 1774 | amount was ultimately accepted. Additionally, it's possible that the sender 1775 | paid MORE that was specified in the original invoice. So we'll record that 1776 | here as well. 1777 | */ 1778 | int64 amt_paid_msat = 20 [json_name = "amt_paid_msat"]; 1779 | } 1780 | message AddInvoiceResponse { 1781 | bytes r_hash = 1 [json_name = "r_hash"]; 1782 | 1783 | /** 1784 | A bare-bones invoice for a payment within the Lightning Network. With the 1785 | details of the invoice, the sender has all the data necessary to send a 1786 | payment to the recipient. 1787 | */ 1788 | string payment_request = 2 [json_name = "payment_request"]; 1789 | 1790 | /** 1791 | The "add" index of this invoice. Each newly created invoice will increment 1792 | this index making it monotonically increasing. Callers to the 1793 | SubscribeInvoices call can use this to instantly get notified of all added 1794 | invoices with an add_index greater than this one. 1795 | */ 1796 | uint64 add_index = 16 [json_name = "add_index"]; 1797 | } 1798 | message PaymentHash { 1799 | /** 1800 | The hex-encoded payment hash of the invoice to be looked up. The passed 1801 | payment hash must be exactly 32 bytes, otherwise an error is returned. 1802 | */ 1803 | string r_hash_str = 1 [json_name = "r_hash_str"]; 1804 | 1805 | /// The payment hash of the invoice to be looked up. 1806 | bytes r_hash = 2 [json_name = "r_hash"]; 1807 | } 1808 | 1809 | message ListInvoiceRequest { 1810 | /// If set, only unsettled invoices will be returned in the response. 1811 | bool pending_only = 1 [json_name = "pending_only"]; 1812 | 1813 | /** 1814 | The index of an invoice that will be used as either the start or end of a 1815 | query to determine which invoices should be returned in the response. 1816 | */ 1817 | uint64 index_offset = 4 [json_name = "index_offset"]; 1818 | 1819 | /// The max number of invoices to return in the response to this query. 1820 | uint64 num_max_invoices = 5 [json_name = "num_max_invoices"]; 1821 | 1822 | /** 1823 | If set, the invoices returned will result from seeking backwards from the 1824 | specified index offset. This can be used to paginate backwards. 1825 | */ 1826 | bool reversed = 6 [json_name = "reversed"]; 1827 | } 1828 | message ListInvoiceResponse { 1829 | /** 1830 | A list of invoices from the time slice of the time series specified in the 1831 | request. 1832 | */ 1833 | repeated Invoice invoices = 1 [json_name = "invoices"]; 1834 | 1835 | /** 1836 | The index of the last item in the set of returned invoices. This can be used 1837 | to seek further, pagination style. 1838 | */ 1839 | uint64 last_index_offset = 2 [json_name = "last_index_offset"]; 1840 | 1841 | /** 1842 | The index of the last item in the set of returned invoices. This can be used 1843 | to seek backwards, pagination style. 1844 | */ 1845 | uint64 first_index_offset = 3 [json_name = "first_index_offset"]; 1846 | } 1847 | 1848 | message InvoiceSubscription { 1849 | /** 1850 | If specified (non-zero), then we'll first start by sending out 1851 | notifications for all added indexes with an add_index greater than this 1852 | value. This allows callers to catch up on any events they missed while they 1853 | weren't connected to the streaming RPC. 1854 | */ 1855 | uint64 add_index = 1 [json_name = "add_index"]; 1856 | 1857 | /** 1858 | If specified (non-zero), then we'll first start by sending out 1859 | notifications for all settled indexes with an settle_index greater than 1860 | this value. This allows callers to catch up on any events they missed while 1861 | they weren't connected to the streaming RPC. 1862 | */ 1863 | uint64 settle_index = 2 [json_name = "settle_index"]; 1864 | } 1865 | 1866 | 1867 | message Payment { 1868 | /// The payment hash 1869 | string payment_hash = 1 [json_name = "payment_hash"]; 1870 | 1871 | /// Deprecated, use value_sat or value_msat. 1872 | int64 value = 2 [json_name = "value", deprecated = true]; 1873 | 1874 | /// The date of this payment 1875 | int64 creation_date = 3 [json_name = "creation_date"]; 1876 | 1877 | /// The path this payment took 1878 | repeated string path = 4 [ json_name = "path" ]; 1879 | 1880 | /// The fee paid for this payment in satoshis 1881 | int64 fee = 5 [json_name = "fee"]; 1882 | 1883 | /// The payment preimage 1884 | string payment_preimage = 6 [json_name = "payment_preimage"]; 1885 | 1886 | /// The value of the payment in satoshis 1887 | int64 value_sat = 7 [json_name = "value_sat"]; 1888 | 1889 | /// The value of the payment in milli-satoshis 1890 | int64 value_msat = 8 [json_name = "value_msat"]; 1891 | } 1892 | 1893 | message ListPaymentsRequest { 1894 | } 1895 | 1896 | message ListPaymentsResponse { 1897 | /// The list of payments 1898 | repeated Payment payments = 1 [json_name = "payments"]; 1899 | } 1900 | 1901 | message DeleteAllPaymentsRequest { 1902 | } 1903 | 1904 | message DeleteAllPaymentsResponse { 1905 | } 1906 | 1907 | message AbandonChannelRequest { 1908 | ChannelPoint channel_point = 1; 1909 | } 1910 | 1911 | message AbandonChannelResponse { 1912 | } 1913 | 1914 | 1915 | message DebugLevelRequest { 1916 | bool show = 1; 1917 | string level_spec = 2; 1918 | } 1919 | message DebugLevelResponse { 1920 | string sub_systems = 1 [json_name = "sub_systems"]; 1921 | } 1922 | 1923 | message PayReqString { 1924 | /// The payment request string to be decoded 1925 | string pay_req = 1; 1926 | } 1927 | message PayReq { 1928 | string destination = 1 [json_name = "destination"]; 1929 | string payment_hash = 2 [json_name = "payment_hash"]; 1930 | int64 num_satoshis = 3 [json_name = "num_satoshis"]; 1931 | int64 timestamp = 4 [json_name = "timestamp"]; 1932 | int64 expiry = 5 [json_name = "expiry"]; 1933 | string description = 6 [json_name = "description"]; 1934 | string description_hash = 7 [json_name = "description_hash"]; 1935 | string fallback_addr = 8 [json_name = "fallback_addr"]; 1936 | int64 cltv_expiry = 9 [json_name = "cltv_expiry"]; 1937 | repeated RouteHint route_hints = 10 [json_name = "route_hints"]; 1938 | } 1939 | 1940 | message FeeReportRequest {} 1941 | message ChannelFeeReport { 1942 | /// The channel that this fee report belongs to. 1943 | string chan_point = 1 [json_name = "channel_point"]; 1944 | 1945 | /// The base fee charged regardless of the number of milli-satoshis sent. 1946 | int64 base_fee_msat = 2 [json_name = "base_fee_msat"]; 1947 | 1948 | /// The amount charged per milli-satoshis transferred expressed in millionths of a satoshi. 1949 | int64 fee_per_mil = 3 [json_name = "fee_per_mil"]; 1950 | 1951 | /// The effective fee rate in milli-satoshis. Computed by dividing the fee_per_mil value by 1 million. 1952 | double fee_rate = 4 [json_name = "fee_rate"]; 1953 | } 1954 | message FeeReportResponse { 1955 | /// An array of channel fee reports which describes the current fee schedule for each channel. 1956 | repeated ChannelFeeReport channel_fees = 1 [json_name = "channel_fees"]; 1957 | 1958 | /// The total amount of fee revenue (in satoshis) the switch has collected over the past 24 hrs. 1959 | uint64 day_fee_sum = 2 [json_name = "day_fee_sum"]; 1960 | 1961 | /// The total amount of fee revenue (in satoshis) the switch has collected over the past 1 week. 1962 | uint64 week_fee_sum = 3 [json_name = "week_fee_sum"]; 1963 | 1964 | /// The total amount of fee revenue (in satoshis) the switch has collected over the past 1 month. 1965 | uint64 month_fee_sum = 4 [json_name = "month_fee_sum"]; 1966 | } 1967 | 1968 | message PolicyUpdateRequest { 1969 | oneof scope { 1970 | /// If set, then this update applies to all currently active channels. 1971 | bool global = 1 [json_name = "global"] ; 1972 | 1973 | /// If set, this update will target a specific channel. 1974 | ChannelPoint chan_point = 2 [json_name = "chan_point"]; 1975 | } 1976 | 1977 | /// The base fee charged regardless of the number of milli-satoshis sent. 1978 | int64 base_fee_msat = 3 [json_name = "base_fee_msat"]; 1979 | 1980 | /// The effective fee rate in milli-satoshis. The precision of this value goes up to 6 decimal places, so 1e-6. 1981 | double fee_rate = 4 [json_name = "fee_rate"]; 1982 | 1983 | /// The required timelock delta for HTLCs forwarded over the channel. 1984 | uint32 time_lock_delta = 5 [json_name = "time_lock_delta"]; 1985 | } 1986 | message PolicyUpdateResponse { 1987 | } 1988 | 1989 | message ForwardingHistoryRequest { 1990 | /// Start time is the starting point of the forwarding history request. All records beyond this point will be included, respecting the end time, and the index offset. 1991 | uint64 start_time = 1 [json_name = "start_time"]; 1992 | 1993 | /// End time is the end point of the forwarding history request. The response will carry at most 50k records between the start time and the end time. The index offset can be used to implement pagination. 1994 | uint64 end_time = 2 [json_name = "end_time"]; 1995 | 1996 | /// Index offset is the offset in the time series to start at. As each response can only contain 50k records, callers can use this to skip around within a packed time series. 1997 | uint32 index_offset = 3 [json_name = "index_offset"]; 1998 | 1999 | /// The max number of events to return in the response to this query. 2000 | uint32 num_max_events = 4 [json_name = "num_max_events"]; 2001 | } 2002 | message ForwardingEvent { 2003 | /// Timestamp is the time (unix epoch offset) that this circuit was completed. 2004 | uint64 timestamp = 1 [json_name = "timestamp"]; 2005 | 2006 | /// The incoming channel ID that carried the HTLC that created the circuit. 2007 | uint64 chan_id_in = 2 [json_name = "chan_id_in"]; 2008 | 2009 | /// The outgoing channel ID that carried the preimage that completed the circuit. 2010 | uint64 chan_id_out = 4 [json_name = "chan_id_out"]; 2011 | 2012 | /// The total amount of the incoming HTLC that created half the circuit. 2013 | uint64 amt_in = 5 [json_name = "amt_in"]; 2014 | 2015 | /// The total amount of the outgoign HTLC that created the second half of the circuit. 2016 | uint64 amt_out = 6 [json_name = "amt_out"]; 2017 | 2018 | /// The total fee that this payment circuit carried. 2019 | uint64 fee = 7 [json_name = "fee"]; 2020 | 2021 | // TODO(roasbeef): add settlement latency? 2022 | // * use FPE on the chan id? 2023 | // * also list failures? 2024 | } 2025 | message ForwardingHistoryResponse { 2026 | /// A list of forwarding events from the time slice of the time series specified in the request. 2027 | repeated ForwardingEvent forwarding_events = 1 [json_name = "forwarding_events"]; 2028 | 2029 | /// The index of the last time in the set of returned forwarding events. Can be used to seek further, pagination style. 2030 | uint32 last_offset_index = 2 [json_name = "last_offset_index"]; 2031 | } 2032 | -------------------------------------------------------------------------------- /src/types/plugin.ts: -------------------------------------------------------------------------------- 1 | export type DataHandler = (data: Buffer) => Promise 2 | export type MoneyHandler = (amount: string) => Promise 3 | 4 | import { EventEmitter2 } from 'eventemitter2' 5 | 6 | export interface PluginInstance extends EventEmitter2 { 7 | connect(options: {}): Promise 8 | disconnect(): Promise 9 | isConnected(): boolean 10 | sendData(data: Buffer): Promise 11 | sendMoney(amount: string): Promise 12 | registerDataHandler(dataHandler: DataHandler): void 13 | deregisterDataHandler(): void 14 | registerMoneyHandler(moneyHandler: MoneyHandler): void 15 | deregisterMoneyHandler(): void 16 | getAdminInfo?(): Promise 17 | sendAdminInfo?(info: object): Promise 18 | } 19 | 20 | export interface PluginServices { 21 | log?: Logger 22 | store?: Store 23 | } 24 | 25 | export interface Logger { 26 | info(...msg: any[]): void 27 | warn(...msg: any[]): void 28 | error(...msg: any[]): void 29 | debug(...msg: any[]): void 30 | trace(...msg: any[]): void 31 | } 32 | 33 | export interface Store { 34 | get: (key: string) => Promise 35 | put: (key: string, value: string) => Promise 36 | del: (key: string) => Promise 37 | } 38 | 39 | export class MemoryStore implements Store { 40 | private _store = new Map() 41 | 42 | async get(k: string) { 43 | return this._store.get(k) 44 | } 45 | 46 | async put(k: string, v: string) { 47 | this._store.set(k, v) 48 | } 49 | 50 | async del(k: string) { 51 | this._store.delete(k) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "rootDir": "src", 5 | "outDir": "build", 6 | "target": "es2017", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "removeComments": true, 12 | "strict": true /* Enable all strict type-checking options. */, 13 | "esModuleInterop": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-eslint-rules", 5 | "tslint-config-prettier" 6 | ] 7 | } 8 | --------------------------------------------------------------------------------