-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Grpc web proxy client example #553
base: main
Are you sure you want to change the base?
Changes from all commits
54e6ccf
dc4d5d3
044e83d
435c519
788327b
df04fc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,22 @@ | ||
FROM ubuntu:22.04 AS python-builder | ||
|
||
ARG BITCOIN_VERSION=24.0 | ||
ARG GID=0 | ||
ARG UID=0 | ||
ARG DOCKER_USER=dev | ||
ARG REPO_PATH=/repo | ||
|
||
ENV RUST_VERSION=1.74 | ||
ENV PATH=$CARGO_HOME/bin:$PATH | ||
ENV PROTOC_VERSION=3.19.3 | ||
ENV CFSSL_VERSION=1.6.5 | ||
ENV GL_TESTING_IGNORE_HASH=true | ||
ARG BITCOIN_VERSION=24.0 | ||
ARG GID=0 | ||
ARG UID=0 | ||
ARG DOCKER_USER=dev | ||
ENV PATH=$PATH:/home/$DOCKER_USER/.local/bin/:/opt/bitcoin/bin:/home/$DOCKER_USER/.cargo/bin | ||
#ENV VIRTUAL_ENV=/tmp/venv | ||
ENV CARGO_TARGET_DIR=/tmp/cargo | ||
ENV REPO=$REPO_PATH | ||
|
||
# Force this target dir, so the scripts can find all the binaries. | ||
#ENV CARGO_TARGET_DIR=${REPO}/target | ||
ENV CARGO_TARGET_DIR=/tmp/target/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This no longer deposita the artifacts in the shared island, and thus the host cannot access them after the container built them. |
||
|
||
RUN apt update && apt install -qqy \ | ||
curl \ | ||
|
@@ -50,9 +55,15 @@ RUN cd /tmp/ && \ | |
mv /tmp/bitcoin-$BITCOIN_VERSION/ /opt/bitcoin && \ | ||
rm -rf bitcoin.tar.gz /tmp/bitcoin-$BITCOIN_VERSION | ||
|
||
ADD ../ /repo/libs | ||
RUN chown $DOCKER_USER:users -R /repo | ||
RUN mkdir -p ${REPO_PATH} && \ | ||
chown $DOCKER_USER:users ${REPO_PATH} | ||
|
||
ADD ../../libs/ ${REPO_PATH}/libs | ||
ADD ../../pyproject.toml ${REPO_PATH}/ | ||
|
||
ADD ../../ ${REPO_PATH}/ | ||
RUN chown $DOCKER_USER:users -R ${REPO_PATH} | ||
RUN chown $DOCKER_USER:users -R /home/$DOCKER_USER | ||
USER $DOCKER_USER | ||
|
||
RUN curl \ | ||
|
@@ -62,10 +73,13 @@ RUN curl \ | |
-s -- -y --default-toolchain ${RUST_VERSION} | ||
RUN rustup default stable | ||
|
||
WORKDIR ${REPO_PATH}/libs/gl-testserver/ | ||
|
||
RUN cargo build --bin gl-plugin | ||
RUN cargo build --bin gl-signerproxy | ||
|
||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh | ||
|
||
WORKDIR /repo/libs/gl-testserver/ | ||
RUN echo $PATH | ||
RUN uv sync --locked -v | ||
RUN uv lock && uv sync --locked -v | ||
RUN uv run clnvm get-all | ||
CMD uv run gltestserver run --metadata /tmp/gltestserver | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I must've missed this one, as it also doesn't point to the shared island 👍 |
||
CMD uv run gltestserver run --metadata ${REPO}/ --directory ${REPO}/.gltestserver |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
const path = require('path'); | ||
const axios = require('axios'); | ||
const protobuf = require('protobufjs'); | ||
|
||
const PORT = process.argv[2] || '1111'; | ||
const AUTH_PUBKEY = 'AqqultE98ITS3BQRsNzuctWdYqeX87TRbY4tRe8NmllJ'; | ||
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 first coffee invoice', label: 'firstcoffeeinv'}, {}]; | ||
for (let i = 0; i < methods.length; i++) { | ||
console.log('--------------------------------------------\n', (i + 1), '-', methods[i], '\n--------------------------------------------'); | ||
console.log('Raw Payload:\n', method_payloads[i]); | ||
const encodedPayload = await encodePayload(clnNode, methods[i], method_payloads[i]); | ||
console.log('\nEncoded Payload:\n', encodedPayload); | ||
try { | ||
const response = await sendRequest(methods[i], encodedPayload); | ||
console.log('\nRaw Response:\nHeaders: ', response.headers, '\nData: ', response.data); | ||
const responseJSON = decodeResponse(clnNode, methods[i], response); | ||
console.log('\nDecoded Response:'); | ||
console.dir(responseJSON, { depth: null, color: true }); | ||
} catch (error) { | ||
console.error('\nERROR:\n', error.response.status, ' - ', error.response.statusText); | ||
} | ||
} | ||
} catch (error) { | ||
console.error('Error:', error.message); | ||
if (error.response) { | ||
console.error('Response status:', error.response.status); | ||
console.error('Response data:', error.response.data); | ||
} | ||
} | ||
} | ||
|
||
fetchNodeData(); |
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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
[project] | ||
name = "gl-client" | ||
version = "0.3.0" | ||
|
||
dependencies = [ | ||
"protobuf>=3", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notice that with this you cannot run the test server outside of the test servers code itself, as it mounts its own code into the container as a volume. The goal here was to create a small shared island inside and outside, but otherwise leave the host directly untouched. This allows for users to use the test server for example from inside their projects repository, allowing them to test against GL.