|
| 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(); |
0 commit comments