Skip to content

Commit 76d5fae

Browse files
Add script to setup two gl nodes with one local CLN node
1 parent 4aac888 commit 76d5fae

3 files changed

Lines changed: 361 additions & 0 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ gltestserver-image: ${REPO_ROOT}/docker/gl-testserver/Dockerfile
207207
.
208208

209209
gltestserver: gltestserver-image
210+
rm -rf .gltestserver/gl-testserver && \
210211
docker run \
211212
--rm \
212213
--user $(shell id -u):$(shell id -g) \
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const path = require('path');
2+
const axios = require('axios');
3+
const protobuf = require('protobufjs');
4+
5+
async function encodePayload(clnNode, method, payload) {
6+
const methodRequest = clnNode.lookupType(`cln.${method}Request`);
7+
const errMsg = methodRequest.verify(payload);
8+
if (errMsg) throw new Error(errMsg);
9+
const requestPayload = methodRequest.create(payload);
10+
const encodedPayload = methodRequest.encode(requestPayload).finish();
11+
const flags = Buffer.alloc(1);
12+
flags.writeUInt8(0, 0);
13+
const header = Buffer.alloc(4);
14+
header.writeUInt32BE(encodedPayload.length, 0);
15+
return Buffer.concat([flags, header, encodedPayload]);
16+
}
17+
18+
async function sendRequest(port, authPubkey, methodUrl, encodedPayload) {
19+
const buffer = Buffer.alloc(8);
20+
buffer.writeUInt32BE(Math.floor(Date.now() / 1000), 4);
21+
const axiosConfig = {
22+
responseType: 'arraybuffer',
23+
headers: {
24+
'content-type': 'application/grpc',
25+
'accept': 'application/grpc',
26+
'glauthpubkey': Buffer.from(authPubkey, 'hex').toString('base64'),
27+
'glauthsig': 'A'.repeat(64),
28+
'glts': buffer.toString('base64'),
29+
},
30+
};
31+
return await axios.post(`http://localhost:${port}/cln.Node/${methodUrl}`, encodedPayload, axiosConfig);
32+
}
33+
34+
function transformValue(key, value) {
35+
if ((value.type && value.type === "Buffer") || value instanceof Buffer || value instanceof Uint8Array) {
36+
return Buffer.from(value).toString('hex');
37+
}
38+
if (value.msat && !Number.isNaN(parseInt(value.msat))) {
39+
return parseInt(value.msat);
40+
}
41+
return value;
42+
}
43+
44+
function decodeResponse(clnNode, method, response) {
45+
const methodResponse = clnNode.lookupType(`cln.${method}Response`);
46+
const dataBuffer = Buffer.from(response.data);
47+
const resFlag = dataBuffer.subarray(0, 1);
48+
const resDataLength = dataBuffer.subarray(1, 5);
49+
const responseData = dataBuffer.subarray(5);
50+
const grpcStatus = +response.headers['grpc-status'];
51+
if (grpcStatus !== 0) {
52+
let errorMessage = 'None';
53+
try {
54+
errorMessage = decodeURIComponent(new TextDecoder('utf-8').decode(responseData)).trim();
55+
if (errorMessage == 'None') {
56+
errorMessage = grpcStatus;
57+
}
58+
} catch (decodeError) {
59+
errorMessage = decodeError;
60+
}
61+
throw new Error(errorMessage);
62+
}
63+
const decodedRes = methodResponse.decode(responseData);
64+
const decodedResObject = methodResponse.toObject(decodedRes, {
65+
longs: String,
66+
enums: String,
67+
bytes: Buffer,
68+
defaults: true,
69+
arrays: true,
70+
objects: true,
71+
});
72+
return JSON.parse(JSON.stringify(decodedResObject, transformValue));
73+
}
74+
75+
async function callMethod(port, auth_pubkey, clnNode, method, methodReq, methodRes, methodPayload) {
76+
await new Promise(resolve => setTimeout(resolve, 1000));
77+
const encodedPayload = await encodePayload(clnNode, methodReq, methodPayload);
78+
try {
79+
const response = await sendRequest(port, auth_pubkey, method, encodedPayload);
80+
const responseJSON = decodeResponse(clnNode, methodRes, response);
81+
console.log(JSON.stringify(responseJSON, null, 2));
82+
} catch (error) {
83+
throw error;
84+
}
85+
}
86+
87+
async function main() {
88+
try {
89+
const args = process.argv.slice(2);
90+
const port = args[0];
91+
const auth_pubkey = args[1];
92+
const method = args[2];
93+
const methodReq = args[3];
94+
const methodRes = args[4];
95+
const methodPayload = JSON.parse(args[5]);
96+
const proto_path = [
97+
path.join(process.cwd(), '../../libs/gl-client/.resources/proto/node.proto'),
98+
path.join(process.cwd(), '../../libs/gl-client/.resources/proto/primitives.proto')
99+
];
100+
const clnNode = new protobuf.Root().loadSync(proto_path, { keepCase: true });
101+
await callMethod(port, auth_pubkey, clnNode, method, methodReq, methodRes, methodPayload);
102+
} catch (error) {
103+
console.error(error);
104+
}
105+
}
106+
107+
main();

setup-nodes.sh

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#!/bin/bash
2+
# shellcheck disable=SC2317
3+
4+
# Set AUTO_START to the first argument, default to "yes" if not provided
5+
AUTO_START=${1:-"start"}
6+
7+
# Read environment variables from metadata.json
8+
GL_SERVER_DATA_PATH="$HOME/workspace/greenlight/.gltestserver"
9+
GL_CA_CRT=$(jq -r '.ca_crt_path' ./metadata.json)
10+
GL_NOBODY_CRT=$(jq -r '.nobody_crt_path' ./metadata.json)
11+
GL_NOBODY_KEY=$(jq -r '.nobody_key_path' ./metadata.json)
12+
GL_SCHEDULER_GRPC_URI=$(jq -r '.scheduler_grpc_uri' ./metadata.json)
13+
GL_BITCOIN_RPC_URI=$(jq -r '.bitcoind_rpc_uri' ./metadata.json)
14+
GL_GRPC_WEB_PROXY_URI=$(jq -r '.grpc_web_proxy_uri' ./metadata.json)
15+
GL_GRPC_PORT=$(echo "$GL_GRPC_WEB_PROXY_URI" | sed -E 's/.*:([0-9]+)$/\1/')
16+
LSP_LIGHTNING_DIR="/tmp/.lightning"
17+
18+
# Extract values using parameter expansion and regular expressions
19+
RPC_USER=$(echo "$GL_BITCOIN_RPC_URI" | sed -E 's|^http://([^:]+):([^@]+)@([^:]+):([0-9]+)$|\1|')
20+
RPC_PASS=$(echo "$GL_BITCOIN_RPC_URI" | sed -E 's|^http://([^:]+):([^@]+)@([^:]+):([0-9]+)$|\2|')
21+
BITCOIN_HOST=$(echo "$GL_BITCOIN_RPC_URI" | sed -E 's|^http://([^:]+):([^@]+)@([^:]+):([0-9]+)$|\3|')
22+
BITCOIN_PORT=$(echo "$GL_BITCOIN_RPC_URI" | sed -E 's|^http://([^:]+):([^@]+)@([^:]+):([0-9]+)$|\4|')
23+
WALLET_NAME="testwallet"
24+
NODE_PUBKEY_1="0279da9a93e50b008a7ba6bd25355fb7132f5015b790a05ee9f41bc9fbdeb30d19"
25+
NODE_PUBKEY_2="036fc50c7183d47baf1b80f312f1ea53b25c117183456b788c05f91b3b4507c8d3"
26+
LSP_NODE_PUBKEY="0208d70da220f05b859512fd805365af6a6b8583fa3ff10fbd00b6b7fbb29acd32"
27+
28+
# Function to handle Ctrl+C
29+
trap 'echo "Stopping terminals"; pkill -f "gnome-terminal"; exit' SIGINT
30+
31+
# Function to print environment variables
32+
print_envs() {
33+
echo "GL_SERVER_DATA_PATH=$GL_SERVER_DATA_PATH"
34+
echo "GL_CA_CRT=$GL_CA_CRT"
35+
echo "GL_NOBODY_CRT=$GL_NOBODY_CRT"
36+
echo "GL_NOBODY_KEY=$GL_NOBODY_KEY"
37+
echo "GL_SCHEDULER_GRPC_URI=$GL_SCHEDULER_GRPC_URI"
38+
echo "GL_BITCOIN_RPC_URI=$GL_BITCOIN_RPC_URI"
39+
echo "GL_GRPC_WEB_PROXY_URI=$GL_GRPC_WEB_PROXY_URI"
40+
echo "GL_GRPC_PORT=$GL_GRPC_PORT"
41+
echo "LSP_LIGHTNING_DIR=$LSP_LIGHTNING_DIR"
42+
echo "RPC_USER=$RPC_USER"
43+
echo "RPC_PASS=$RPC_PASS"
44+
echo "BITCOIN_HOST=$BITCOIN_HOST"
45+
echo "BITCOIN_PORT=$BITCOIN_PORT"
46+
echo "WALLET_NAME=testwallet"
47+
echo "NODE_PUBKEY_1=0279da9a93e50b008a7ba6bd25355fb7132f5015b790a05ee9f41bc9fbdeb30d19"
48+
echo "NODE_PUBKEY_2=036fc50c7183d47baf1b80f312f1ea53b25c117183456b788c05f91b3b4507c8d3"
49+
echo "LSP_NODE_PUBKEY=0208d70da220f05b859512fd805365af6a6b8583fa3ff10fbd00b6b7fbb29acd32"
50+
}
51+
52+
# Function to register and schedule a node
53+
run_scheduler_for_gl_node() {
54+
local node="$1"
55+
local hsm_secret="$2"
56+
if [ -z "$node" ] || [ -z "$hsm_secret" ]; then
57+
echo "Forgot params node/hsm_secret? Exiting..."
58+
else
59+
echo "Scheduler for $node"
60+
rm -rf "${GL_SERVER_DATA_PATH}/${node:?}" && mkdir -p "${GL_SERVER_DATA_PATH}/${node}"
61+
printf '%b' "$hsm_secret" > "${GL_SERVER_DATA_PATH}/${node}/hsm_secret"
62+
gnome-terminal --title="Scheduler $node" -- bash -c \
63+
"GL_CA_CRT=$GL_CA_CRT \
64+
GL_NOBODY_CRT=$GL_NOBODY_CRT \
65+
GL_NOBODY_KEY=$GL_NOBODY_KEY \
66+
GL_SCHEDULER_GRPC_URI=$GL_SCHEDULER_GRPC_URI \
67+
cargo run --bin glcli scheduler register \
68+
--network=regtest \
69+
--data-dir=${GL_SERVER_DATA_PATH}/${node} && sleep 1 && \
70+
GL_CA_CRT=$GL_CA_CRT \
71+
GL_NOBODY_CRT=$GL_NOBODY_CRT \
72+
GL_NOBODY_KEY=$GL_NOBODY_KEY \
73+
GL_SCHEDULER_GRPC_URI=$GL_SCHEDULER_GRPC_URI \
74+
cargo run --bin glcli scheduler schedule \
75+
--verbose \
76+
--network=regtest \
77+
--data-dir=${GL_SERVER_DATA_PATH}/${node}; \
78+
echo 'Press Enter to close this terminal...'; \
79+
read"
80+
fi
81+
}
82+
83+
# Function to start the signer for a node
84+
run_signer_for_gl_node() {
85+
local node="$1"
86+
if [ -z "$node" ]; then
87+
echo "Forgot param node? Exiting..."
88+
else
89+
echo "Signer for $node"
90+
gnome-terminal --title="Signer $node" -- bash -c \
91+
"GL_CA_CRT=$GL_CA_CRT \
92+
GL_NOBODY_CRT=$GL_NOBODY_CRT \
93+
GL_NOBODY_KEY=$GL_NOBODY_KEY \
94+
GL_SCHEDULER_GRPC_URI=$GL_SCHEDULER_GRPC_URI \
95+
cargo run --bin glcli signer run --verbose --network=regtest --data-dir=${GL_SERVER_DATA_PATH}/${node}; \
96+
echo 'Press Enter to close this terminal...'; \
97+
read"
98+
fi
99+
}
100+
101+
# Function to start the local CLN node
102+
run_local_lsp_node() {
103+
local node="$1"
104+
local hsm_secret="$2"
105+
if [ -z "$node" ] || [ -z "$hsm_secret" ]; then
106+
echo "Forgot params node/hsm_secret? Exiting..."
107+
else
108+
echo "Running $node node"
109+
rm -rf $LSP_LIGHTNING_DIR && mkdir -p "$LSP_LIGHTNING_DIR/regtest"
110+
printf '%b' "$hsm_secret" > "$LSP_LIGHTNING_DIR/regtest/hsm_secret"
111+
gnome-terminal --title="Local LSP node" -- bash -c \
112+
"lightningd --network=regtest --log-level=debug --lightning-dir=$LSP_LIGHTNING_DIR --bitcoin-rpcconnect=$BITCOIN_HOST --bitcoin-rpcport=$BITCOIN_PORT --bitcoin-datadir=$GL_SERVER_DATA_PATH/gl-testserver --bitcoin-rpcuser=$RPC_USER --bitcoin-rpcpassword=$RPC_PASS --alias=CLNLocalLSPNode --bind-addr=0.0.0.0:9735 --bind-addr=ws:0.0.0.0:5019 --announce-addr=0.0.0.0:9735 --addr=localhost:7171 --grpc-port=9737 --clnrest-port=3010 --clnrest-protocol=http; \
113+
echo 'Press Enter to close this terminal...'; \
114+
read"
115+
fi
116+
}
117+
118+
connect_and_fund_channels_from_gl_nodes() {
119+
cd "./examples/javascript" || exit
120+
CONNECT_PAYLOAD='{"id": "'"$LSP_NODE_PUBKEY"'", "host": "127.0.0.1", "port": 9735}'
121+
echo "gl1 + LSP"
122+
node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_1" "ConnectPeer" "Connect" "Connect" "$CONNECT_PAYLOAD"
123+
echo "gl2 + LSP"
124+
node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_2" "ConnectPeer" "Connect" "Connect" "$CONNECT_PAYLOAD"
125+
BUFFER_ID=$(echo -n "$LSP_NODE_PUBKEY" | xxd -r -p | base64)
126+
FUND_PAYLOAD='{"id": "'"$BUFFER_ID"'", "amount": {"amount": {"msat": 11000000000}}}'
127+
echo "gl1 -> LSP"
128+
node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_1" "FundChannel" "Fundchannel" "Fundchannel" "$FUND_PAYLOAD"
129+
echo "gl2 -> LSP"
130+
node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_2" "FundChannel" "Fundchannel" "Fundchannel" "$FUND_PAYLOAD"
131+
cd "../.." || exit
132+
mine_blocks 6
133+
}
134+
135+
fund_channels_from_local_node() {
136+
echo "LSP -> gl1"
137+
lightning-cli --network=regtest --lightning-dir="$LSP_LIGHTNING_DIR" fundchannel "$NODE_PUBKEY_1" 12000000
138+
mine_blocks 6
139+
140+
echo "LSP -> gl2"
141+
lightning-cli --network=regtest --lightning-dir="$LSP_LIGHTNING_DIR" fundchannel "$NODE_PUBKEY_2" 12000000
142+
mine_blocks 6
143+
}
144+
145+
create_invoice_and_pay() {
146+
cd "./examples/javascript" || exit
147+
echo "Create invoice 1"
148+
INVOICE_1_PAYLOAD=$(jq -nc --arg msat "120000000" --arg desc "Learning Bitcoin" --arg label "bookinvat$(date +%s)" \
149+
'{"amount_msat": {"amount": {"msat": ($msat | tonumber)}}, "description": $desc, "label": $label}')
150+
INVOICE_1_RESPONSE=$(node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_1" "Invoice" "Invoice" "Invoice" "$INVOICE_1_PAYLOAD")
151+
BOLT11_1=$(echo "$INVOICE_1_RESPONSE" | jq -r '.bolt11')
152+
echo "Invoice 1: $BOLT11_1"
153+
echo "Create invoice 2"
154+
INVOICE_2_PAYLOAD=$(jq -nc --arg msat "5000000" --arg desc "Coffee" --arg label "coffeeinvat$(date +%s)" \
155+
'{"amount_msat": {"amount": {"msat": ($msat | tonumber)}}, "description": $desc, "label": $label}')
156+
INVOICE_2_RESPONSE=$(node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_2" "Invoice" "Invoice" "Invoice" "$INVOICE_2_PAYLOAD")
157+
BOLT11_2=$(echo "$INVOICE_2_RESPONSE" | jq -r '.bolt11')
158+
echo "Invoice 2: $BOLT11_2"
159+
echo "Pay invoice 1"
160+
PAY_PAYLOAD_1='{"bolt11": "'"$BOLT11_1"'"}'
161+
node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_2" "Pay" "Pay" "Pay" "$PAY_PAYLOAD_1"
162+
echo "Pay invoice 2"
163+
PAY_PAYLOAD_2='{"bolt11": "'"$BOLT11_2"'"}'
164+
node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_1" "Pay" "Pay" "Pay" "$PAY_PAYLOAD_2"
165+
cd "../.." || exit
166+
}
167+
168+
start_nodes() {
169+
run_scheduler_for_gl_node "gl1" '\x0f\x29\xa7\x60\xea\xa1\xa3\xba\xbb\x7b\xa2\x5b\x7a\x82\xd1\x0b\x38\x55\xb9\xd2\xc6\x39\xb2\x79\xa9\xa0\x8d\x9b\xd1\xe6\x67\x9d'
170+
run_signer_for_gl_node "gl1"
171+
172+
run_scheduler_for_gl_node "gl2" '\x5d\x74\x25\x7e\x03\xb0\xee\x2f\xf4\x7a\x06\x97\x3a\xd9\x14\xf1\x7c\x61\xec\xac\x20\xb1\xf9\x3b\xb2\x33\x98\xc9\x40\x86\xad\x67'
173+
run_signer_for_gl_node "gl2"
174+
175+
run_local_lsp_node "local1" '\x9f\xaa\x67\x08\x67\x05\x8c\x9d\xcc\x1f\xea\xd9\x9c\xe4\x91\xe2\x85\x95\x6d\xdb\x66\xa8\xed\x05\x85\xf3\x2a\x77\x0e\x1d\x14\xa6'
176+
}
177+
178+
wait_for_local_funds_confirmation() {
179+
local is_confirmed=false
180+
while ! $is_confirmed; do
181+
if lightning-cli --network=regtest --lightning-dir="$LSP_LIGHTNING_DIR" listfunds | jq -e '.outputs[] | select(.status == "confirmed" and .reserved == false)' > /dev/null; then
182+
is_confirmed=true
183+
else
184+
echo "Waiting for funds to confirm on LSP..."
185+
sleep 1
186+
fi
187+
done
188+
}
189+
190+
wait_for_gl_funds_confirmation() {
191+
cd "./examples/javascript" || exit
192+
while ! node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_1" "ListFunds" "Listfunds" "Listfunds" "{}" | grep -q "outputs"; do
193+
echo "Waiting for funds to confirm on gl node 1..."
194+
sleep 1
195+
done
196+
while ! node node-operations.js "$GL_GRPC_PORT" "$NODE_PUBKEY_2" "ListFunds" "Listfunds" "Listfunds" "{}" | grep -q "outputs"; do
197+
echo "Waiting for funds to confirm on gl node 2..."
198+
sleep 1
199+
done
200+
cd "../.." || exit
201+
}
202+
203+
mine_blocks() {
204+
local number_of_blocks=${1:-"6"}
205+
bitcoin-cli -rpcconnect="$BITCOIN_HOST" -rpcport="$BITCOIN_PORT" -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" -generate "$number_of_blocks" > /dev/null
206+
wait_for_gl_funds_confirmation
207+
wait_for_local_funds_confirmation
208+
}
209+
210+
fund_nodes() {
211+
rm -rf "${GL_SERVER_DATA_PATH}/gl-testserver/regtest/${WALLET_NAME}"
212+
echo "Creating bitcoin wallet"
213+
bitcoin-cli -rpcconnect="$BITCOIN_HOST" -rpcport="$BITCOIN_PORT" -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" createwallet "$WALLET_NAME" > /dev/null
214+
echo "Generating 101 blocks"
215+
bitcoin-cli -rpcconnect="$BITCOIN_HOST" -rpcport="$BITCOIN_PORT" -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" -generate 101 > /dev/null
216+
echo "Sending bitcoin to local node"
217+
bitcoin-cli -rpcconnect="$BITCOIN_HOST" -rpcport="$BITCOIN_PORT" -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" sendtoaddress "bcrt1qaew9v5m5q8cjjuh9mdruuujykzxrcufma28x82" 1 > /dev/null
218+
echo "Sending bitcoin to gl1"
219+
bitcoin-cli -rpcconnect="$BITCOIN_HOST" -rpcport="$BITCOIN_PORT" -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" sendtoaddress "bcrt1q0mlp2u676wv9rgz5e6nrrq2vc76rllxcazcldy" 1 > /dev/null
220+
echo "Sending bitcoin to gl2"
221+
bitcoin-cli -rpcconnect="$BITCOIN_HOST" -rpcport="$BITCOIN_PORT" -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS" -rpcwallet="$WALLET_NAME" sendtoaddress "bcrt1q68n5mqlkf0l877chhpa2w2zxlug343kn0p8c6r" 1 > /dev/null
222+
mine_blocks 1
223+
}
224+
225+
if [ "$AUTO_START" = "start" ]; then
226+
print_envs
227+
start_nodes
228+
fund_nodes
229+
connect_and_fund_channels_from_gl_nodes
230+
fund_channels_from_local_node
231+
create_invoice_and_pay
232+
else
233+
echo "List of functions: "
234+
echo "print_envs: Prints all environment variables used in the script."
235+
echo "start_nodes: Starts all nodes (GL nodes and the local CLN node)."
236+
echo "fund_nodes: Creates a Bitcoin wallet, generates blocks, and sends funds to nodes."
237+
echo "connect_and_fund_channels_from_gl_nodes: Connects and funds channels between Greenlight (GL) nodes and the local CLN node."
238+
echo "fund_channels_from_local_node: Funds channels from the local CLN node to GL nodes."
239+
echo "create_invoice_and_pay: Creates invoices and simulates payments between nodes."
240+
echo "run_scheduler_for_gl_node: Registers and schedules a Greenlight (GL) node."
241+
echo "run_signer_for_gl_node: Starts the signer for a Greenlight (GL) node."
242+
echo "run_local_lsp_node: Starts the local Core Lightning node."
243+
echo "wait_for_local_funds_confirmation: Waits for funds to be confirmed on the local CLN node."
244+
echo "wait_for_gl_funds_confirmation: Waits for funds to be confirmed on Greenlight (GL) nodes."
245+
echo "mine_blocks: Mines a specified number of blocks and waits for funds to be confirmed."
246+
# shellcheck disable=SC2016
247+
echo 'lightning-cli --network=regtest --lightning-dir="$LSP_LIGHTNING_DIR" listfunds'
248+
# shellcheck disable=SC2016
249+
echo 'cd "./examples/javascript" && node node-operations.js $GL_GRPC_PORT $NODE_PUBKEY_1 "ListFunds" "Listfunds" "Listfunds" "{}" && cd "../.."'
250+
fi
251+
252+
# Keep the script running to listen for Ctrl+C
253+
wait

0 commit comments

Comments
 (0)