Skip to content

Commit

Permalink
example: Add simple Javascript Getinfo Example
Browse files Browse the repository at this point in the history
  • Loading branch information
ShahanaFarooqui committed Jan 16, 2025
1 parent 2da012f commit f3d478b
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ libs/cln-version-manager/poetry.lock
.gltestserver
uv.lock
metadata.json

# JS Examples
examples/javascript/node_modules
examples/javascript/response.bin
examples/javascript/package-lock.json
76 changes: 76 additions & 0 deletions examples/javascript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# How to run javascript examples with gltestserver

## Step 1 (Terminal 1): Start the Server
```bash
make gltestserver
```

## Step 2 (Terminal 2): Register the Node
```bash
GL_CA_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/ca.crt \
GL_NOBODY_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody.crt \
GL_NOBODY_KEY=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody-key.pem \
GL_SCHEDULER_GRPC_URI=https://localhost:38067 \
cargo run --bin glcli scheduler register --network=regtest --data-dir=$HOME/greenlight/.gltestserver/gl-testserver
```

## Step 3 (Terminal 2): Schedule the Node
```bash
GL_CA_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/ca.crt \
GL_NOBODY_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody.crt \
GL_NOBODY_KEY=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody-key.pem \
GL_SCHEDULER_GRPC_URI=https://localhost:38067 \
cargo run --bin glcli scheduler schedule --verbose --network=regtest --data-dir=$HOME/greenlight/.gltestserver/gl-testserver
```

## Step 4 (Terminal 2): Start the Signer
```bash
GL_CA_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/ca.crt \
GL_NOBODY_CRT=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody.crt \
GL_NOBODY_KEY=$HOME/greenlight/.gltestserver/gl-testserver/certs/users/nobody-key.pem \
GL_SCHEDULER_GRPC_URI=https://localhost:38067 \
cargo run --bin glcli signer run --verbose --network=regtest --data-dir=$HOME/greenlight/.gltestserver/gl-testserver
```

## Step 5 (Terminal 3): Run the Example
### 5.1: Navigate and Install Dependencies for the Example
```bash
cd ./examples/javascript
npm install
```

### 5.2: Get Node ID
```bash
lightning-hsmtool getnodeid $HOME/greenlight/.gltestserver/gl-testserver/hsm_secret
```
Sample Output: 034c46b632a9ff3975fb7cd4e764a36ec476b522be2555e83a3183ab1ee3e36e93

### 5.3: Encode Node ID to Base64
```python
import binascii
import base64
print(base64.b64encode(binascii.unhexlify("<node id from step 5.2>")).decode('utf-8'))
```
Sample Output: A0xGtjKp/zl1+3zU52SjbsR2tSK+JVXoOjGDqx7j426T

### 5.4: Modify Default Values
- Open the file `./examples/javascript/grpc-web-proxy-client.js`.

- Locate the line defining `AUTH_PUBKEY` and replace its value with the Base64-encoded public key output from Step 5.3:

```javascript
const AUTH_PUBKEY = 'replace+this+with+your+base64+encoded+pubkey';
```

- Replace the default PORT value `1111` with the port number from `grpc_web_proxy_uri` obtained in Step 1:
```javascript
const PORT = process.argv[2] || '1111';
```
Alternatively, the port number can be passed as a command-line argument when running the nodejs script in the next step.

- Save the changes to the file.

### 5.5: Run the Example
```bash
node grpc-web-proxy-client.js
```
135 changes: 135 additions & 0 deletions examples/javascript/grpc-web-proxy-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const path = require('path');
const axios = require('axios');
const protobuf = require('protobufjs');

const PORT = process.argv[2] || '1111';
const AUTH_PUBKEY = 'replace+this+with+your+base64+encoded+pubkey';
const AUTH_SIGNATURE = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
const PROTO_PATHS = [
path.join(process.cwd(), '../../libs/gl-client/.resources/proto/node.proto'),
path.join(process.cwd(), '../../libs/gl-client/.resources/proto/primitives.proto')
];

function getGrpcErrorMessage(grpcStatusCode) {
const grpcStatusMessages = {
0: 'OK: The operation completed successfully.',
1: 'CANCELLED: The operation was cancelled (typically by the caller).',
2: 'UNKNOWN: Unknown error. Usually means an internal error occurred.',
3: 'INVALID_ARGUMENT: The client specified an invalid argument.',
4: 'DEADLINE_EXCEEDED: The operation took too long and exceeded the time limit.',
5: 'NOT_FOUND: A specified resource was not found.',
6: 'ALREADY_EXISTS: The resource already exists.',
7: 'PERMISSION_DENIED: The caller does not have permission to execute the operation.',
8: 'RESOURCE_EXHAUSTED: A resource (such as quota) was exhausted.',
9: 'FAILED_PRECONDITION: The operation was rejected due to a failed precondition.',
10: 'ABORTED: The operation was aborted, typically due to a concurrency issue.',
11: 'OUT_OF_RANGE: The operation attempted to access an out-of-range value.',
12: 'UNIMPLEMENTED: The operation is not implemented or supported by the server.',
13: 'INTERNAL: Internal server error.',
14: 'UNAVAILABLE: The service is unavailable (e.g., network issues, server down).',
15: 'DATA_LOSS: Unrecoverable data loss or corruption.',
16: 'UNAUTHENTICATED: The request is missing or has invalid authentication credentials.'
}
return grpcStatusMessages[grpcStatusCode] || "UNKNOWN_STATUS_CODE: The status code returned by gRPC server is not in the list.";
}

async function encodePayload(clnNode, method, payload) {
const methodRequest = clnNode.lookupType(`cln.${method}Request`);
const errMsg = methodRequest.verify(payload);
if (errMsg) throw new Error(errMsg);
const header = Buffer.alloc(4);
header.writeUInt8(0, 0);
const requestPayload = methodRequest.create(payload);
const encodedPayload = methodRequest.encodeDelimited(requestPayload).finish();
return Buffer.concat([header, encodedPayload]);
}

async function sendRequest(methodUrl, encodedPayload) {
const buffer = Buffer.alloc(8);
buffer.writeUInt32BE(Math.floor(Date.now() / 1000), 4);
const axiosConfig = {
responseType: 'arraybuffer',
headers: {
'content-type': 'application/grpc',
'accept': 'application/grpc',
'glauthpubkey': AUTH_PUBKEY,
'glauthsig': AUTH_SIGNATURE,
'glts': buffer.toString('base64'),
},
};
return await axios.post(`http://localhost:${PORT}/cln.Node/${methodUrl}`, encodedPayload, axiosConfig);
}

function transformValue(key, value) {
if ((value.type && value.type === "Buffer") || value instanceof Buffer || value instanceof Uint8Array) {
return Buffer.from(value).toString('hex');
}
if (value.msat && !Number.isNaN(parseInt(value.msat))) {
// FIXME: Amount.varify check will work with 0 NOT '0'. Amount default is '0'.
return parseInt(value.msat);
}
return value;
}

function decodeResponse(clnNode, method, response) {
const methodResponse = clnNode.lookupType(`cln.${method}Response`)
const offset = 5;
const responseData = new Uint8Array(response.data).slice(offset);
const grpcStatus = +response.headers['grpc-status'];
if (grpcStatus !== 200) {
let errorDecoded = new TextDecoder("utf-8").decode(responseData);
if (errorDecoded !== 'None') {
errorDecoded = JSON.parse(errorDecoded.replace(/([a-zA-Z0-9_]+):/g, '"$1":'));
} else {
errorDecoded = {code: grpcStatus, message: getGrpcErrorMessage(grpcStatus)};
}
return { grpc_code: grpcStatus, grpc_error: getGrpcErrorMessage(grpcStatus), error: errorDecoded};
} else {
// FIXME: Use decodeDelimited
const decodedRes = methodResponse.decode(responseData);
const decodedResObject = methodResponse.toObject(decodedRes, {
longs: String,
enums: String,
bytes: Buffer,
defaults: true,
arrays: true,
objects: true,
});
return JSON.parse(JSON.stringify(decodedResObject, transformValue));
}
}

async function fetchNodeData() {
try {
const clnNode = new protobuf.Root().loadSync(PROTO_PATHS, { keepCase: true });
const FeeratesStyle = clnNode.lookupEnum('cln.FeeratesStyle');
const NewaddrAddresstype = clnNode.lookupEnum('cln.NewaddrAddresstype');
const methods = ['Getinfo', 'Feerates', 'NewAddr', 'Invoice', 'ListInvoices'];
const method_payloads = [{}, {style: FeeratesStyle.values.PERKW}, {addresstype: NewaddrAddresstype.values.ALL}, {amount_msat: {amount: {msat: 500000}}, description: 'My coffee', label: 'coffeeinvat' + Date.now()}, {}];
for (let i = 0; i < methods.length; i++) {
console.log('--------------------------------------------\n', (i + 1), '-', methods[i], '\n--------------------------------------------');
console.log('Payload Raw:\n', method_payloads[i]);
const CapitalizedMethodName = methods[i].charAt(0).toUpperCase() + methods[i].slice(1).toLowerCase();
const encodedPayload = await encodePayload(clnNode, CapitalizedMethodName, method_payloads[i]);
console.log('\nPayload Encoded:\n', encodedPayload);
try {
const response = await sendRequest(methods[i], encodedPayload);
console.log('\nResponse Headers:\n', response.headers);
console.log('\nResponse Data:\n', response.data);
const responseJSON = decodeResponse(clnNode, CapitalizedMethodName, response);
console.log('\nResponse Decoded:');
console.dir(responseJSON, { depth: null, color: true });
} catch (error) {
console.error('\nResponse Error:\n', error.response.status, ' - ', error.response.statusText);
}
}
} catch (error) {
console.error('Error:', error.message);
if (error.response) {
console.error('Error status:', error.response.status);
console.error('Error data:', error.response.data);
}
}
}

fetchNodeData();
19 changes: 19 additions & 0 deletions examples/javascript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "grpc-web-proxy-client",
"version": "1.0.0",
"description": "Example for grpc web proxy client",
"main": "grpc-web-proxy-client.js",
"directories": {
"doc": "doc",
"test": "tests"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.7.9",
"protobufjs": "^7.4.0"
}
}

0 comments on commit f3d478b

Please sign in to comment.