Skip to content

Commit 6156707

Browse files
murtyjonesjamaljsr
authored andcommitted
feat: add example of an LND TypeScript app
1 parent 5f60770 commit 6156707

11 files changed

+1351
-1
lines changed

examples/typescript/lnd/.env.example

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# These values can come from any `lnd` node in the polar app:
2+
ALICE_LND_MACAROON_PATH=/Users/satoshi/.polar/networks/1/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon
3+
ALICE_LND_HOST=127.0.0.1:10001
4+
ALICE_TLS_CERT_PATH=/Users/satoshi/.polar/networks/1/volumes/lnd/alice/tls.cert

examples/typescript/lnd/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
.env

examples/typescript/lnd/README.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# TypeScript LND app
2+
3+
This is an example app that shows how Polar can be used in your development workflow while working on Lightning Network applications.
4+
5+
This example is a simple app, designed for LND nodes, that allows the user to:
6+
7+
1. Open a channel
8+
1. List peers
9+
1. List pending channels
10+
1. List open channels
11+
12+
# Getting Started
13+
14+
1. Start the Polar app using `yarn dev` or opening the app itself
15+
1. In Polar, run an environment with at least one `lnd` node and has at least two nodes (the default environment, which has one of each node type, is good)
16+
1. Run `cp .env.example .env` in this directory
17+
1. Under the `Connect` tab for the `lnd` node in your Polar environment, copy the following values to `.env`:
18+
- Copy the `Admin Macaroon` path to `ALICE_LND_MACAROON_PATH`
19+
- Copy the `GRPC Host` path to `ALICE_LND_HOST`
20+
- Copy the `TLS Cert` path to `ALICE_TLS_CERT_PATH`
21+
1. Install this app's dependencies by running `yarn`
22+
1. Run the app by running `yarn start`

examples/typescript/lnd/package.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "example-ts-lnd-app",
3+
"version": "1.0.0",
4+
"description": "An example TypeScript app developed using Polar",
5+
"main": "src/index.js",
6+
"license": "MIT",
7+
"scripts": {
8+
"start": "ts-node --transpile-only src/index.ts"
9+
},
10+
"dependencies": {
11+
"@radar/lnrpc": "^0.11.1-beta.0",
12+
"dotenv": "^8.2.0",
13+
"inquirer": "^7.3.3",
14+
"prompts": "^2.4.0"
15+
},
16+
"devDependencies": {
17+
"@types/inquirer": "^7.3.1",
18+
"@types/prompts": "^2.0.9",
19+
"ts-node": "^9.1.1",
20+
"typescript": "^4.1.2"
21+
}
22+
}

examples/typescript/lnd/src/config.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { LnRpcClientConfig } from '@radar/lnrpc';
2+
import dotenv from 'dotenv';
3+
4+
dotenv.config();
5+
6+
export const ALICE_LND: LnRpcClientConfig = {
7+
server: String(process.env.ALICE_LND_HOST),
8+
tls: String(process.env.ALICE_TLS_CERT_PATH),
9+
macaroonPath: String(process.env.ALICE_LND_MACAROON_PATH),
10+
};

examples/typescript/lnd/src/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import createLnrpc from '@radar/lnrpc';
2+
import { ALICE_LND } from './config';
3+
import NodeHandler from './nodeHandler';
4+
5+
(async () => {
6+
const lnrpc = await createLnrpc(ALICE_LND);
7+
const nodeHandler = new NodeHandler(lnrpc);
8+
nodeHandler.whatWouldYouLikeToDo();
9+
})();
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { LnRpc, Peer, WalletUnlockerRpc } from '@radar/lnrpc';
2+
import inquirer from 'inquirer';
3+
import { cropStringMiddle } from './utils';
4+
5+
enum BaseAnswers {
6+
OpenChannel = 'Open a channel',
7+
ListPeers = 'List peers',
8+
ListPendingChannels = 'List pending channels',
9+
ListOpenChannels = 'List open channels',
10+
}
11+
12+
export default class NodeHandler {
13+
constructor(private lnrpc: LnRpc & WalletUnlockerRpc) {}
14+
15+
whatWouldYouLikeToDo = async () => {
16+
try {
17+
const info = await this.lnrpc.getInfo();
18+
const { action } = await inquirer.prompt<{ action: BaseAnswers }>({
19+
type: 'list',
20+
name: 'action',
21+
message: `Hi ${info.alias}, what would you like to do?`,
22+
choices: Object.values(BaseAnswers),
23+
});
24+
let answerOrNextQuestion = await this.getAnswer(action);
25+
while (typeof answerOrNextQuestion !== 'string') {
26+
answerOrNextQuestion = await answerOrNextQuestion();
27+
}
28+
console.log(answerOrNextQuestion);
29+
await this.whatWouldYouLikeToDo();
30+
} catch (err) {
31+
console.error('Sorry, something went wrong trying to connect with your LND node');
32+
}
33+
};
34+
35+
getAnswer = async (
36+
question: BaseAnswers,
37+
): Promise<string | (() => Promise<string>)> => {
38+
switch (question) {
39+
case 'Open a channel': {
40+
return this.specifyChannelOpeningParameters;
41+
}
42+
case 'List peers': {
43+
const { peersWithAliases } = await this.getPeers();
44+
return `Your peers:\n\n`.concat(
45+
peersWithAliases.map(p => p.alias || cropStringMiddle(p.pubKey, 4)).join('\n'),
46+
);
47+
}
48+
case 'List pending channels': {
49+
const info = await this.lnrpc.pendingChannels();
50+
return `Pending channels:\n${JSON.stringify(info)}`;
51+
}
52+
case 'List open channels': {
53+
const info = await this.lnrpc.listChannels();
54+
return `Open channels:\n${JSON.stringify(info)}`;
55+
}
56+
default:
57+
throw new Error('Question not recognized');
58+
}
59+
};
60+
61+
specifyChannelOpeningParameters = async () => {
62+
const { peersWithAliases } = await this.getPeers();
63+
const { pubKey } = await inquirer.prompt<{ pubKey: string }>({
64+
type: 'list',
65+
name: 'pubKey',
66+
message: 'Which peer would you like to open a channel with?',
67+
choices: peersWithAliases.map(p => p.alias || cropStringMiddle(p.pubKey, 4)),
68+
});
69+
const { amount } = await inquirer.prompt<{ amount: number }>({
70+
type: 'number',
71+
name: 'amount',
72+
message: 'How much (in Satoshis) would you like to open with (e.g. 100000)?',
73+
validate: answer => {
74+
if (!answer || answer < 100000) {
75+
return 'You must choose an amount greater than 100,000 satoshis.';
76+
}
77+
return true;
78+
},
79+
});
80+
await this.lnrpc.openChannelSync({
81+
nodePubkeyString: pubKey,
82+
localFundingAmount: amount.toString(),
83+
});
84+
return 'Success!';
85+
};
86+
87+
getPeers = async () => {
88+
const { peers } = await this.lnrpc.listPeers();
89+
const { nodes } = await this.lnrpc.describeGraph();
90+
const peersWithAliases = peers.map((peer, i) => {
91+
const peerWithAlias: Peer & { alias?: string } = { ...peer };
92+
const maybeNode = nodes.filter(e => e.pubKey === peerWithAlias.pubKey)[0];
93+
if (maybeNode?.alias) {
94+
peerWithAlias.alias = maybeNode.alias;
95+
}
96+
return peerWithAlias;
97+
});
98+
return { peersWithAliases };
99+
};
100+
}

examples/typescript/lnd/src/utils.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Replaces the middle of a string with ellipses.
3+
*
4+
* @example
5+
* // returns '03f1...7472'
6+
* cropStringMiddle('03f101adfdb17bcca537b46c8bed691f88f9949865f448479a9245646f2b5c7472', 4);
7+
* @param str
8+
* @param around
9+
*/
10+
export const cropStringMiddle = (str: string, around: number) => {
11+
if (around * 2 >= str.length) {
12+
return str;
13+
}
14+
return str.substr(0, around) + '...' + str.substr(str.length - around, str.length);
15+
};

examples/typescript/lnd/tsconfig.json

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"compilerOptions": {
3+
/* Visit https://aka.ms/tsconfig.json to read more about this file */
4+
5+
/* Basic Options */
6+
// "incremental": true, /* Enable incremental compilation */
7+
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
8+
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9+
// "lib": [], /* Specify library files to be included in the compilation. */
10+
// "allowJs": true, /* Allow javascript files to be compiled. */
11+
// "checkJs": true, /* Report errors in .js files. */
12+
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13+
// "declaration": true, /* Generates corresponding '.d.ts' file. */
14+
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15+
// "sourceMap": true, /* Generates corresponding '.map' file. */
16+
// "outFile": "./", /* Concatenate and emit output to single file. */
17+
"outDir": "./dist", /* Redirect output structure to the directory. */
18+
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19+
// "composite": true, /* Enable project compilation */
20+
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21+
// "removeComments": true, /* Do not emit comments to output. */
22+
// "noEmit": true, /* Do not emit outputs. */
23+
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
24+
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25+
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26+
27+
/* Strict Type-Checking Options */
28+
"strict": true, /* Enable all strict type-checking options. */
29+
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30+
// "strictNullChecks": true, /* Enable strict null checks. */
31+
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
32+
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33+
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34+
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35+
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36+
37+
/* Additional Checks */
38+
// "noUnusedLocals": true, /* Report errors on unused locals. */
39+
// "noUnusedParameters": true, /* Report errors on unused parameters. */
40+
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41+
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42+
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
43+
44+
/* Module Resolution Options */
45+
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
46+
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
47+
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
48+
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
49+
// "typeRoots": [], /* List of folders to include type definitions from. */
50+
// "types": [], /* Type declaration files to be included in compilation. */
51+
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
52+
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
53+
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
54+
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
55+
56+
/* Source Map Options */
57+
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
58+
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
59+
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
60+
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
61+
62+
/* Experimental Options */
63+
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
64+
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
65+
66+
/* Advanced Options */
67+
"skipLibCheck": true, /* Skip type checking of declaration files. */
68+
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
69+
}
70+
}

0 commit comments

Comments
 (0)