Skip to content

Commit f0dad22

Browse files
ShahanaFarooquicdecker
authored andcommitted
example: Add simple Javascript Examples for gltestserver
1 parent d145f5f commit f0dad22

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ libs/cln-version-manager/poetry.lock
2626
.gltestserver
2727
uv.lock
2828
metadata.json
29+
30+
# JS Examples
31+
examples/javascript/node_modules
32+
examples/javascript/response.bin
33+
examples/javascript/package-lock.json

examples/javascript/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# How to run javascript examples with gltestserver
2+
3+
## Step 1 (Terminal 1): Start the Server
4+
```bash
5+
make gltestserver
6+
```
7+
8+
## Step 2 (Terminal 2): Register the Node
9+
```bash
10+
GL_CA_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/ca.crt \
11+
GL_NOBODY_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody.crt \
12+
GL_NOBODY_KEY=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody-key.pem \
13+
GL_SCHEDULER_GRPC_URI=https://localhost:38067 \
14+
cargo run --bin glcli scheduler register --network=regtest --data-dir=$HOME/greenlight/.gltestserver/gl-testserver
15+
```
16+
17+
## Step 3 (Terminal 2): Schedule the Node
18+
```bash
19+
GL_CA_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/ca.crt \
20+
GL_NOBODY_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody.crt \
21+
GL_NOBODY_KEY=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody-key.pem \
22+
GL_SCHEDULER_GRPC_URI=https://localhost:38067 \
23+
cargo run --bin glcli scheduler schedule --verbose --network=regtest --data-dir=$HOME/greenlight/.gltestserver/gl-testserver
24+
```
25+
26+
## Step 4 (Terminal 2): Start the Signer
27+
```bash
28+
GL_CA_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/ca.crt \
29+
GL_NOBODY_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody.crt \
30+
GL_NOBODY_KEY=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody-key.pem \
31+
GL_SCHEDULER_GRPC_URI=https://localhost:38067 \
32+
cargo run --bin glcli signer run --verbose --network=regtest --data-dir=$HOME/greenlight/.gltestserver/gl-testserver
33+
```
34+
35+
## Step 5 (Terminal 3): Run the Example
36+
### 5.1: Navigate and Install Dependencies for the Example
37+
```bash
38+
cd ./examples/javascript
39+
npm install
40+
```
41+
42+
### 5.2: Get Node ID
43+
```bash
44+
lightning-hsmtool getnodeid $HOME/greenlight/.gltestserver/gl-testserver/hsm_secret
45+
```
46+
Sample Output: 034c46b632a9ff3975fb7cd4e764a36ec476b522be2555e83a3183ab1ee3e36e93
47+
48+
### 5.3: Encode Node ID to Base64
49+
```python
50+
import binascii
51+
import base64
52+
print(base64.b64encode(binascii.unhexlify("<node id from step 5.2>")).decode('utf-8'))
53+
```
54+
Sample Output: A0xGtjKp/zl1+3zU52SjbsR2tSK+JVXoOjGDqx7j426T
55+
56+
### 5.4: Modify Default Values
57+
- Open the file `./examples/javascript/grpc-web-proxy-client.js`.
58+
59+
- Locate the line defining `AUTH_PUBKEY` and replace its value with the Base64-encoded public key output from Step 5.3:
60+
61+
```javascript
62+
const AUTH_PUBKEY = 'replace+this+with+your+base64+encoded+pubkey';
63+
```
64+
65+
- Replace the default PORT value `1111` with the port number from `grpc_web_proxy_uri` obtained in Step 1:
66+
```javascript
67+
const PORT = process.argv[2] || '1111';
68+
```
69+
Alternatively, the port number can be passed as a command-line argument when running the nodejs script in the next step.
70+
71+
- Save the changes to the file.
72+
73+
### 5.5: Run the Example
74+
```bash
75+
node grpc-web-proxy-client.js
76+
```
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
const path = require('path');
2+
const axios = require('axios');
3+
const protobuf = require('protobufjs');
4+
5+
const PORT = process.argv[2] || '1111';
6+
const AUTH_PUBKEY = 'replace+this+with+your+base64+encoded+pubkey';
7+
const AUTH_SIGNATURE = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
8+
const PROTO_PATHS = [
9+
path.join(process.cwd(), '../../libs/gl-client/.resources/proto/node.proto'),
10+
path.join(process.cwd(), '../../libs/gl-client/.resources/proto/primitives.proto')
11+
];
12+
13+
function getGrpcErrorMessage(grpcStatusCode) {
14+
const grpcStatusMessages = {
15+
0: 'OK: The operation completed successfully.',
16+
1: 'CANCELLED: The operation was cancelled (typically by the caller).',
17+
2: 'UNKNOWN: Unknown error. Usually means an internal error occurred.',
18+
3: 'INVALID_ARGUMENT: The client specified an invalid argument.',
19+
4: 'DEADLINE_EXCEEDED: The operation took too long and exceeded the time limit.',
20+
5: 'NOT_FOUND: A specified resource was not found.',
21+
6: 'ALREADY_EXISTS: The resource already exists.',
22+
7: 'PERMISSION_DENIED: The caller does not have permission to execute the operation.',
23+
8: 'RESOURCE_EXHAUSTED: A resource (such as quota) was exhausted.',
24+
9: 'FAILED_PRECONDITION: The operation was rejected due to a failed precondition.',
25+
10: 'ABORTED: The operation was aborted, typically due to a concurrency issue.',
26+
11: 'OUT_OF_RANGE: The operation attempted to access an out-of-range value.',
27+
12: 'UNIMPLEMENTED: The operation is not implemented or supported by the server.',
28+
13: 'INTERNAL: Internal server error.',
29+
14: 'UNAVAILABLE: The service is unavailable (e.g., network issues, server down).',
30+
15: 'DATA_LOSS: Unrecoverable data loss or corruption.',
31+
16: 'UNAUTHENTICATED: The request is missing or has invalid authentication credentials.'
32+
}
33+
return grpcStatusMessages[grpcStatusCode] || "UNKNOWN_STATUS_CODE: The status code returned by gRPC server is not in the list.";
34+
}
35+
36+
async function encodePayload(clnNode, method, payload) {
37+
const methodRequest = clnNode.lookupType(`cln.${method}Request`);
38+
const errMsg = methodRequest.verify(payload);
39+
if (errMsg) throw new Error(errMsg);
40+
const header = Buffer.alloc(4);
41+
header.writeUInt8(0, 0);
42+
const requestPayload = methodRequest.create(payload);
43+
const encodedPayload = methodRequest.encodeDelimited(requestPayload).finish();
44+
return Buffer.concat([header, encodedPayload]);
45+
}
46+
47+
async function sendRequest(methodUrl, encodedPayload) {
48+
const buffer = Buffer.alloc(8);
49+
buffer.writeUInt32BE(Math.floor(Date.now() / 1000), 4);
50+
const axiosConfig = {
51+
responseType: 'arraybuffer',
52+
headers: {
53+
'content-type': 'application/grpc',
54+
'accept': 'application/grpc',
55+
'glauthpubkey': AUTH_PUBKEY,
56+
'glauthsig': AUTH_SIGNATURE,
57+
'glts': buffer.toString('base64'),
58+
},
59+
};
60+
return await axios.post(`http://localhost:${PORT}/cln.Node/${methodUrl}`, encodedPayload, axiosConfig);
61+
}
62+
63+
function transformValue(key, value) {
64+
if ((value.type && value.type === "Buffer") || value instanceof Buffer || value instanceof Uint8Array) {
65+
return Buffer.from(value).toString('hex');
66+
}
67+
if (value.msat && !Number.isNaN(parseInt(value.msat))) {
68+
// FIXME: Amount.varify check will work with 0 NOT '0'. Amount default is '0'.
69+
return parseInt(value.msat);
70+
}
71+
return value;
72+
}
73+
74+
function decodeResponse(clnNode, method, response) {
75+
const methodResponse = clnNode.lookupType(`cln.${method}Response`)
76+
const offset = 5;
77+
const responseData = new Uint8Array(response.data).slice(offset);
78+
const grpcStatus = +response.headers['grpc-status'];
79+
if (grpcStatus !== 0) {
80+
let errorDecoded = new TextDecoder("utf-8").decode(responseData);
81+
if (errorDecoded !== 'None') {
82+
errorDecoded = JSON.parse(errorDecoded.replace(/([a-zA-Z0-9_]+):/g, '"$1":'));
83+
} else {
84+
errorDecoded = {code: grpcStatus, message: getGrpcErrorMessage(grpcStatus)};
85+
}
86+
return { grpc_code: grpcStatus, grpc_error: getGrpcErrorMessage(grpcStatus), error: errorDecoded};
87+
} else {
88+
// FIXME: Use decodeDelimited
89+
const decodedRes = methodResponse.decode(responseData);
90+
const decodedResObject = methodResponse.toObject(decodedRes, {
91+
longs: String,
92+
enums: String,
93+
bytes: Buffer,
94+
defaults: true,
95+
arrays: true,
96+
objects: true,
97+
});
98+
return JSON.parse(JSON.stringify(decodedResObject, transformValue));
99+
}
100+
}
101+
102+
async function fetchNodeData() {
103+
try {
104+
const clnNode = new protobuf.Root().loadSync(PROTO_PATHS, { keepCase: true });
105+
const FeeratesStyle = clnNode.lookupEnum('cln.FeeratesStyle');
106+
const NewaddrAddresstype = clnNode.lookupEnum('cln.NewaddrAddresstype');
107+
const methods = ['Getinfo', 'Feerates', 'NewAddr', 'Invoice', 'ListInvoices'];
108+
const method_payloads = [{}, {style: FeeratesStyle.values.PERKW}, {addresstype: NewaddrAddresstype.values.ALL}, {amount_msat: {amount: {msat: 500000}}, description: 'My coffee', label: 'coffeeinvat' + Date.now()}, {}];
109+
for (let i = 0; i < methods.length; i++) {
110+
console.log('--------------------------------------------\n', (i + 1), '-', methods[i], '\n--------------------------------------------');
111+
console.log('Payload Raw:\n', method_payloads[i]);
112+
const CapitalizedMethodName = methods[i].charAt(0).toUpperCase() + methods[i].slice(1).toLowerCase();
113+
const encodedPayload = await encodePayload(clnNode, CapitalizedMethodName, method_payloads[i]);
114+
console.log('\nPayload Encoded:\n', encodedPayload);
115+
try {
116+
const response = await sendRequest(methods[i], encodedPayload);
117+
console.log('\nResponse Headers:\n', response.headers);
118+
console.log('\nResponse Data:\n', response.data);
119+
const responseJSON = decodeResponse(clnNode, CapitalizedMethodName, response);
120+
console.log('\nResponse Decoded:');
121+
console.dir(responseJSON, { depth: null, color: true });
122+
} catch (error) {
123+
console.error('\nResponse Error:\n', error.response.status, ' - ', error.response.statusText);
124+
}
125+
}
126+
} catch (error) {
127+
console.error('Error:', error.message);
128+
if (error.response) {
129+
console.error('Error status:', error.response.status);
130+
console.error('Error data:', error.response.data);
131+
}
132+
}
133+
}
134+
135+
fetchNodeData();

examples/javascript/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "grpc-web-proxy-client",
3+
"version": "1.0.0",
4+
"description": "Example for grpc web proxy client",
5+
"main": "grpc-web-proxy-client.js",
6+
"directories": {
7+
"doc": "doc",
8+
"test": "tests"
9+
},
10+
"scripts": {
11+
"test": "echo \"Error: no test specified\" && exit 1"
12+
},
13+
"author": "",
14+
"license": "ISC",
15+
"dependencies": {
16+
"axios": "^1.7.9",
17+
"protobufjs": "^7.4.0"
18+
}
19+
}

0 commit comments

Comments
 (0)