From fcf2e458a6186b8939bfd457a049b132867a385d Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 20 Mar 2026 14:22:52 +0900 Subject: [PATCH 1/7] Add workflow to check network cli --- .github/workflows/python.yml | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 85060e5..2ad569c 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -91,6 +91,10 @@ jobs: matrix: python-version: ['3.9', '3.10'] + env: + XRPL_VERSION: 3.1.1 + XAHAU_VERSION: 2025.7.9-release+1951 + steps: - uses: actions/checkout@v3 @@ -129,6 +133,7 @@ jobs: - name: Test CLI subcommand help run: | poetry run xrpld-netgen create:network --help + poetry run xrpld-netgen up --help poetry run xrpld-netgen up:standalone --help poetry run xrpld-netgen up:local --help @@ -136,16 +141,37 @@ jobs: run: | poetry run xrpld-netgen || true - - name: Test CLI command for xrpl + - name: Test CLI standalone command for xrpl run: | - poetry run xrpld-netgen up:standalone --protocol xrpl + poetry run xrpld-netgen up:standalone --protocol xrpl --version ${{ env.XRPL_VERSION }} sleep 5 docker logs xrpl docker ps | grep xrpl + # poetry run xrpld-netgen down:standalone --protocol xrpl --version ${{ env.XRPL_VERSION }} - - name: Test CLI command for xahau + - name: Test CLI standalone command for xahau run: | - poetry run xrpld-netgen up:standalone --protocol xahau + poetry run xrpld-netgen up:standalone --protocol xahau --version ${{ env.XAHAU_VERSION }} sleep 5 docker logs xahau docker ps | grep xahau + # poetry run xrpld-netgen down:standalone --protocol xahau --version ${{ env.XAHAU_VERSION }} + + # TODO: not supported? + # - name: Test CLI create:network, up, stop, down command for xrpl + # run: | + # poetry run xrpld-netgen create:network --protocol xrpl --build_version ${{ env.XRPL_VERSION }} + # poetry run xrpld-netgen up --name xrpld-${{ env.XRPL_VERSION }}-cluster + # sleep 5 + # docker ps | grep cluster + # poetry run xrpld-netgen down --name xrpld-${{ env.XRPL_VERSION }}-cluster + # poetry run xrpld-netgen remove --name xrpld-${{ env.XRPL_VERSION }}-cluster + + - name: Test CLI create:network, up, stop, down command for xahau + run: | + poetry run xrpld-netgen create:network --protocol xahau --build_version ${{ env.XAHAU_VERSION }} + poetry run xrpld-netgen up --name ${{ env.XAHAU_VERSION }}-cluster + sleep 5 + docker ps | grep cluster + poetry run xrpld-netgen down --name ${{ env.XAHAU_VERSION }}-cluster + poetry run xrpld-netgen remove --name ${{ env.XAHAU_VERSION }}-cluster From 466e189aadba40ed04b200d264c4ce101aeecf60 Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 20 Mar 2026 14:42:56 +0900 Subject: [PATCH 2/7] fix code --- xrpld_netgen/utils/deploy_kit.py | 16 +++++++++------- xrpld_netgen/utils/misc.py | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/xrpld_netgen/utils/deploy_kit.py b/xrpld_netgen/utils/deploy_kit.py index 6e75977..2b04f49 100644 --- a/xrpld_netgen/utils/deploy_kit.py +++ b/xrpld_netgen/utils/deploy_kit.py @@ -99,7 +99,7 @@ def create_dockerfile( dockerfile += "COPY genesis.json /genesis.json\n" if binary: - dockerfile += f"COPY {protocol}d.{version} /app/{protocol}d\n" + dockerfile += f"COPY xrpld.{version} /app/xrpld\n" if network: dockerfile += f""" @@ -222,7 +222,7 @@ def build_stop_sh( standalone: bool = False, local: bool = False, ) -> str: - stop_sh_content = "#! /bin/bash\n" + stop_sh_content = '#! /bin/bash\ncd "$(dirname "$0")"\n' if num_validators > 0 and num_peers > 0: stop_sh_content += f"docker compose -f {basedir}/{protocol}-{name}/docker-compose.yml down --remove-orphans\n" # noqa: E501 @@ -243,7 +243,7 @@ def build_stop_sh( if local: stop_sh_content = ( - "#! /bin/bash\ndocker compose -f docker-compose.yml down --remove-orphans\n" + '#! /bin/bash\ncd "$(dirname "$0")"\ndocker compose -f docker-compose.yml down --remove-orphans\n' ) stop_sh_content += "rm -r db\n" stop_sh_content += "rm -r debug.log\n" @@ -257,7 +257,8 @@ def build_start_sh( name: str, ): return ( - "#! /bin/bash\n" + '#! /bin/bash\n' + 'cd "$(dirname "$0")"\n' "docker compose" f" -f {basedir}/{protocol}-{name}" "/docker-compose.yml" @@ -273,7 +274,8 @@ def build_local_start_sh( config_filename: str = "xrpld.cfg" if protocol == "xrpl" else "xahaud.cfg" flag = "-a" if net_type == "standalone" else "" return ( - "#! /bin/bash\n" + '#! /bin/bash\n' + 'cd "$(dirname "$0")"\n' "docker compose -f docker-compose.yml" " up --build --force-recreate -d\n" f"./{exe_filename} {flag} --conf config/{config_filename}" @@ -286,7 +288,7 @@ def build_network_start_sh( num_validators: int, num_peers: int, ): - start_sh_content = "#! /bin/bash \n" + start_sh_content = '#! /bin/bash\ncd "$(dirname "$0")"\n' for i in range(1, num_validators + 1): start_sh_content += f"cp xrpld.{name} vnode{i}/xrpld.{name}\n" @@ -304,7 +306,7 @@ def build_network_stop_sh( num_validators: int, num_peers: int, ) -> str: - stop_sh_content = "#! /bin/bash\n" + stop_sh_content = '#! /bin/bash\ncd "$(dirname "$0")"\n' stop_sh_content += "REMOVE_FLAG=false \n" stop_sh_content += """ for arg in "$@"; do diff --git a/xrpld_netgen/utils/misc.py b/xrpld_netgen/utils/misc.py index 512c714..bd6d603 100644 --- a/xrpld_netgen/utils/misc.py +++ b/xrpld_netgen/utils/misc.py @@ -53,7 +53,7 @@ def run_start(cmd: List[str], protocol: str, version: str, type: str): ) if result.returncode == 0: print( - f"{bcolors.CYAN}{protocol.capitalize()} {bcolors.GREEN}{version} " + f"{bcolors.CYAN}{protocol.capitalize() if protocol is not None else ''} {bcolors.GREEN}{version} " f"{type} running at: {bcolors.PURPLE}6006 {bcolors.END}" ) print(f"{bcolors.CYAN}Explorer running / starting container{bcolors.END}") From 70f8b97ff49bf3170f648f7f2aa3984862187580 Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 20 Mar 2026 14:57:06 +0900 Subject: [PATCH 3/7] {protocol}d.{name} --- tests/integration/test_network.py | 1 + xrpld_netgen/cli.py | 2 +- xrpld_netgen/network.py | 32 ++++++++++++++++--------------- xrpld_netgen/utils/deploy_kit.py | 18 +++++++++-------- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/tests/integration/test_network.py b/tests/integration/test_network.py index e1a01a7..a77b24e 100644 --- a/tests/integration/test_network.py +++ b/tests/integration/test_network.py @@ -80,6 +80,7 @@ def _test_create_ansible(cls): def _test_update_node(cls): update_node_binary( "2025.2.6-release+1299-cluster", # network name + "xahau", # protocol "1", # node id "peer", # node type: validator or peer "https://build.xahau.tech", # build server diff --git a/xrpld_netgen/cli.py b/xrpld_netgen/cli.py index 6378d12..5042406 100644 --- a/xrpld_netgen/cli.py +++ b/xrpld_netgen/cli.py @@ -585,7 +585,7 @@ def main(): print(f" - Node Type: {NODE_VERSION}") print(f" - Build Server: {BUILD_SERVER}") print(f" - Build Version: {BUILD_VERSION}") - update_node_binary(NAME, NODE_ID, NODE_VERSION, BUILD_SERVER, BUILD_VERSION) + update_node_binary(NAME, PROTOCOL, NODE_ID, NODE_VERSION, BUILD_SERVER, BUILD_VERSION) if args.command == "enable:amendment": NAME = args.name diff --git a/xrpld_netgen/network.py b/xrpld_netgen/network.py index 92389e5..385aed2 100644 --- a/xrpld_netgen/network.py +++ b/xrpld_netgen/network.py @@ -382,7 +382,7 @@ def create_network( ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" - download_binary(url, f"{basedir}/{name}-cluster/xrpld.{build_version}") + download_binary(url, f"{basedir}/{name}-cluster/{protocol}d.{build_version}") image: str = "ubuntu:jammy" if protocol == "xrpl": @@ -394,7 +394,7 @@ def create_network( name = name.replace("/", "-") os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) repo = "rippled" - copy_file("./xrpld", f"{basedir}/{name}-cluster/xrpld.{name}") + copy_file("./xrpld", f"{basedir}/{name}-cluster/{protocol}d.{name}") content_bytes = download_file_at_commit_or_tag( owner, repo, @@ -523,10 +523,11 @@ def create_network( write_file( f"{basedir}/{name}-cluster/start.sh", - build_network_start_sh(name, num_validators, num_peers), # noqa: E501 + build_network_start_sh(name, protocol, num_validators, num_peers), # noqa: E501 ) stop_sh_content: str = build_network_stop_sh( name, + protocol, num_validators, num_peers, ) @@ -546,11 +547,12 @@ def create_network( os.chmod(f"{basedir}/{name}-cluster/start.sh", 0o755) os.chmod(f"{basedir}/{name}-cluster/stop.sh", 0o755) - os.chmod(f"{basedir}/{name}-cluster/xrpld.{name}", 0o755) + os.chmod(f"{basedir}/{name}-cluster/{protocol}d.{name}", 0o755) def update_node_binary( name: str, + protocol: str, node_id: str, node_type: str, build_server: str, @@ -559,14 +561,14 @@ def update_node_binary( node_dir: str = f"{'v' if node_type == 'validator' else 'p'}node{node_id}" run_command(f"{basedir}/{name}", f"docker-compose stop {node_dir}") url: str = f"{build_server}/{new_version}" - download_binary(url, f"{basedir}/{name}/xrpld.{new_version}") + download_binary(url, f"{basedir}/{name}/{protocol}d.{new_version}") shutil.copyfile( - f"{basedir}/{name}/xrpld.{new_version}", - f"{basedir}/{name}/{node_dir}/xrpld.{new_version}", + f"{basedir}/{name}/{protocol}d.{new_version}", + f"{basedir}/{name}/{node_dir}/{protocol}d.{new_version}", ) # remove the db run_command(f"{basedir}/{name}", f"rm -r {node_dir}/lib") - os.chmod(f"{basedir}/{name}/{node_dir}/xrpld.{new_version}", 0o755) + os.chmod(f"{basedir}/{name}/{node_dir}/{protocol}d.{new_version}", 0o755) update_dockerfile(new_version, f"{basedir}/{name}/{node_dir}/Dockerfile") run_command( f"{basedir}/{name}", @@ -626,7 +628,7 @@ def create_ansible( ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" - download_binary(url, f"{basedir}/{name}-cluster/xrpld.{build_version}") + download_binary(url, f"{basedir}/{name}-cluster/{protocol}d.{build_version}") image: str = "ubuntu:jammy" if protocol == "xrpl": @@ -638,7 +640,7 @@ def create_ansible( name = name.split("/tree/")[1] if "/tree/" in name else name name = name.replace("/", "-") os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) - copy_file("./xrpld", f"{basedir}/{name}-cluster/xrpld.{name}") + copy_file("./xrpld", f"{basedir}/{name}-cluster/{protocol}d.{name}") content_bytes = download_file_at_commit_or_tag( owner, repo, @@ -766,9 +768,9 @@ def create_ansible( write_file( f"{basedir}/{name}-cluster/start.sh", - build_network_start_sh(name, num_validators, num_peers), # noqa: E501 + build_network_start_sh(name, protocol, num_validators, num_peers), # noqa: E501 ) - stop_sh_content: str = build_network_stop_sh(name, num_validators, num_peers) + stop_sh_content: str = build_network_stop_sh(name, protocol, num_validators, num_peers) write_file(f"{basedir}/{name}-cluster/stop.sh", stop_sh_content) os.makedirs(f"{basedir}/{name}-cluster/vl", exist_ok=True) @@ -785,7 +787,7 @@ def create_ansible( os.chmod(f"{basedir}/{name}-cluster/start.sh", 0o755) os.chmod(f"{basedir}/{name}-cluster/stop.sh", 0o755) - os.chmod(f"{basedir}/{name}-cluster/xrpld.{name}", 0o755) + os.chmod(f"{basedir}/{name}-cluster/{protocol}d.{name}", 0o755) os.makedirs(f"{basedir}/{name}-cluster/ansible", exist_ok=True) os.makedirs(f"{basedir}/{name}-cluster/ansible/host_vars", exist_ok=True) @@ -838,7 +840,7 @@ def create_ansible( run_command( f"{basedir}/{name}-cluster", - f"cp xrpld.{name} {basedir}/{name}-cluster/vnode1", + f"cp {protocol}d.{name} {basedir}/{name}-cluster/vnode1", ) run_command( f"{basedir}/{name}-cluster/vnode1", @@ -851,7 +853,7 @@ def create_ansible( ) run_command( f"{basedir}/{name}-cluster/vnode1", - f"rm -r xrpld.{name}", + f"rm -r {protocol}d.{name}", ) if k[:5] == "pnode": index: int = int(k[5:]) diff --git a/xrpld_netgen/utils/deploy_kit.py b/xrpld_netgen/utils/deploy_kit.py index 2b04f49..2c76518 100644 --- a/xrpld_netgen/utils/deploy_kit.py +++ b/xrpld_netgen/utils/deploy_kit.py @@ -99,7 +99,7 @@ def create_dockerfile( dockerfile += "COPY genesis.json /genesis.json\n" if binary: - dockerfile += f"COPY xrpld.{version} /app/xrpld\n" + dockerfile += f"COPY {protocol}d.{version} /app/{protocol}d\n" if network: dockerfile += f""" @@ -193,20 +193,20 @@ def download_binary(url: str, save_path: str) -> None: raise ValueError(f"{bcolors.RED}An error occurred: {e}") -def update_dockerfile(build_version: str, save_path: str) -> None: +def update_dockerfile(protocol: str, build_version: str, save_path: str) -> None: # Read the Dockerfile with open(save_path, "r") as file: lines = file.readlines() # Define the pattern to search for any xrpld COPY line - pattern = re.compile(r"^COPY xrpld.* /app/xrpld$") + pattern = re.compile(fr"^COPY {protocol}d.* /app/{protocol}d$") # Replace the line with the new version with open(save_path, "w") as file: for line in lines: if pattern.match(line): # Replace the line with the new xrpld version - file.write(f"COPY xrpld.{build_version} /app/xrpld\n") + file.write(f"COPY {protocol}d.{build_version} /app/{protocol}d\n") else: file.write(line) @@ -285,15 +285,16 @@ def build_local_start_sh( def build_network_start_sh( name: str, + protocol: str, num_validators: int, num_peers: int, ): start_sh_content = '#! /bin/bash\ncd "$(dirname "$0")"\n' for i in range(1, num_validators + 1): - start_sh_content += f"cp xrpld.{name} vnode{i}/xrpld.{name}\n" + start_sh_content += f"cp {protocol}d.{name} vnode{i}/{protocol}d.{name}\n" for i in range(1, num_peers + 1): - start_sh_content += f"cp xrpld.{name} pnode{i}/xrpld.{name}\n" + start_sh_content += f"cp {protocol}d.{name} pnode{i}/{protocol}d.{name}\n" start_sh_content += ( "docker compose -f docker-compose.yml" " up --build --force-recreate -d" @@ -303,6 +304,7 @@ def build_network_start_sh( def build_network_stop_sh( name: str, + protocol: str, num_validators: int, num_peers: int, ) -> str: @@ -324,12 +326,12 @@ def build_network_stop_sh( for i in range(1, num_validators + 1): stop_sh_content += f"rm -r vnode{i}/lib\n" stop_sh_content += f"rm -r vnode{i}/log\n" - stop_sh_content += f"rm -r vnode{i}/xrpld.{name}\n" + stop_sh_content += f"rm -r vnode{i}/{protocol}d.{name}\n" for i in range(1, num_peers + 1): stop_sh_content += f"rm -r pnode{i}/lib\n" stop_sh_content += f"rm -r pnode{i}/log\n" - stop_sh_content += f"rm -r pnode{i}/xrpld.{name}\n" + stop_sh_content += f"rm -r pnode{i}/{protocol}d.{name}\n" stop_sh_content += "else \n" if num_validators > 0 and num_peers > 0: From e40398b72dda0167e25bbcc0196505c8edd7b452 Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 20 Mar 2026 15:38:46 +0900 Subject: [PATCH 4/7] fix --- xrpld_netgen/deploykit/network.entrypoint | 23 +++++++++++++++-------- xrpld_netgen/utils/deploy_kit.py | 4 ++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/xrpld_netgen/deploykit/network.entrypoint b/xrpld_netgen/deploykit/network.entrypoint index e5af0cc..cc7bfbd 100755 --- a/xrpld_netgen/deploykit/network.entrypoint +++ b/xrpld_netgen/deploykit/network.entrypoint @@ -1,27 +1,34 @@ #!/bin/bash -# ./entrypoint.sh ledgerfile quorum -# ./entrypoint.sh /genesis.json 5 +# ./entrypoint.sh protocol ledgerfile quorum +# ./entrypoint.sh protocol /genesis.json 5 # Check if $1 is passed if [[ -n "$1" ]]; then - ledgerfile="--ledgerfile $1" + protocol="$1" else - ledgerfile="" + protocol="xrpl" fi # Check if $2 is passed if [[ -n "$2" ]]; then - quorum="--quorum=$2" + ledgerfile="--ledgerfile $2" else - quorum="" + ledgerfile="" fi # Check if $3 is passed if [[ -n "$3" ]]; then - standalone="$3" + quorum="--quorum=$3" +else + quorum="" +fi + +# Check if $4 is passed +if [[ -n "$4" ]]; then + standalone="$4" else standalone="" fi # Start xrpld with or without ledgerfile and quorum -exec /app/xrpld $ledgerfile $quorum $standalone --conf /opt/ripple/config/xrpld.cfg \ No newline at end of file +exec /app/"$protocol"d $ledgerfile $quorum $standalone --conf /opt/ripple/config/"$protocol"d.cfg diff --git a/xrpld_netgen/utils/deploy_kit.py b/xrpld_netgen/utils/deploy_kit.py index 2c76518..dc2ccc9 100644 --- a/xrpld_netgen/utils/deploy_kit.py +++ b/xrpld_netgen/utils/deploy_kit.py @@ -128,9 +128,9 @@ def create_dockerfile( ) if include_genesis: - dockerfile += f'ENTRYPOINT [ "/entrypoint.sh", "/genesis.json", "{quorum}", "{standalone}" ]' # noqa: E501 + dockerfile += f'ENTRYPOINT [ "/entrypoint.sh", "{protocol}", "/genesis.json", "{quorum}", "{standalone}" ]' # noqa: E501 else: - dockerfile += 'ENTRYPOINT [ "/entrypoint.sh" ]' + dockerfile += f'ENTRYPOINT [ "/entrypoint.sh", "{protocol}" ]' return dockerfile From 9f2047f508b9b5174769b77a8e9fb0720f34579b Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 20 Mar 2026 15:42:44 +0900 Subject: [PATCH 5/7] fix --- xrpld_netgen/deploykit/xahau.entrypoint | 26 +++++++++++++++---------- xrpld_netgen/deploykit/xrpl.entrypoint | 26 +++++++++++++++---------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/xrpld_netgen/deploykit/xahau.entrypoint b/xrpld_netgen/deploykit/xahau.entrypoint index 605441d..d65980c 100755 --- a/xrpld_netgen/deploykit/xahau.entrypoint +++ b/xrpld_netgen/deploykit/xahau.entrypoint @@ -1,6 +1,6 @@ #!/bin/bash -# ./entrypoint.sh ledgerfile quorum -# ./entrypoint.sh /genesis.json 5 +# ./entrypoint.sh xahau ledgerfile quorum +# ./entrypoint.sh xahau /genesis.json 5 xahaudconfig=`/bin/cat /config/xahaud.cfg 2>/dev/null | wc -l` validatorstxt=`/bin/cat /config/validators.txt 2>/dev/null | wc -l` @@ -17,23 +17,29 @@ if [[ "$xahaudconfig" -gt "0" && "$validatorstxt" -gt "0" ]]; then fi -# Check if $1 is passed -if [[ -n "$1" ]]; then - ledgerfile="--ledgerfile $1" -else - ledgerfile="" +# Check if $1 is xahau +if [[ "$1" != "xahau" ]]; then + echo "Invalid protocol: $1" + exit 1 fi # Check if $2 is passed if [[ -n "$2" ]]; then - quorum="--quorum=$2" + ledgerfile="--ledgerfile $2" else - quorum="" + ledgerfile="" fi # Check if $3 is passed if [[ -n "$3" ]]; then - standalone="$3" + quorum="--quorum=$3" +else + quorum="" +fi + +# Check if $4 is passed +if [[ -n "$4" ]]; then + standalone="$4" else standalone="" fi diff --git a/xrpld_netgen/deploykit/xrpl.entrypoint b/xrpld_netgen/deploykit/xrpl.entrypoint index 84c9749..57d296b 100755 --- a/xrpld_netgen/deploykit/xrpl.entrypoint +++ b/xrpld_netgen/deploykit/xrpl.entrypoint @@ -1,6 +1,6 @@ #!/bin/bash -# ./entrypoint.sh ledgerfile quorum -# ./entrypoint.sh /genesis.json 5 +# ./entrypoint.sh xrpl ledgerfile quorum +# ./entrypoint.sh xrpl /genesis.json 5 xrpldconfig=`/bin/cat /config/xrpld.cfg 2>/dev/null | wc -l` validatorstxt=`/bin/cat /config/validators.txt 2>/dev/null | wc -l` @@ -17,23 +17,29 @@ if [[ "$xrpldconfig" -gt "0" && "$validatorstxt" -gt "0" ]]; then fi -# Check if $1 is passed -if [[ -n "$1" ]]; then - ledgerfile="--ledgerfile $1" -else - ledgerfile="" +# Check if $1 is xrpl +if [[ "$1" != "xrpl" ]]; then + echo "Invalid protocol: $1" + exit 1 fi # Check if $2 is passed if [[ -n "$2" ]]; then - quorum="--quorum=$2" + ledgerfile="--ledgerfile $2" else - quorum="" + ledgerfile="" fi # Check if $3 is passed if [[ -n "$3" ]]; then - standalone="$3" + quorum="--quorum=$3" +else + quorum="" +fi + +# Check if $4 is passed +if [[ -n "$4" ]]; then + standalone="$4" else standalone="" fi From 1977bb1ec185611d343c90db7304d1956199e922 Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 20 Mar 2026 17:13:17 +0900 Subject: [PATCH 6/7] add faucet container --- xrpld_netgen/faucet.py | 396 ++++++++++++++++++++++++++++++++++++++++ xrpld_netgen/network.py | 73 +++++++- 2 files changed, 466 insertions(+), 3 deletions(-) create mode 100644 xrpld_netgen/faucet.py diff --git a/xrpld_netgen/faucet.py b/xrpld_netgen/faucet.py new file mode 100644 index 0000000..f14a260 --- /dev/null +++ b/xrpld_netgen/faucet.py @@ -0,0 +1,396 @@ +#!/usr/bin/env python +# coding: utf-8 + +import json + + +def generate_faucet_package_json() -> str: + """Generate package.json for the faucet service.""" + package = { + "name": "xrpld-faucet", + "version": "1.0.0", + "description": "Faucet service for XRPLD local networks", + "main": "dist/server.js", + "scripts": { + "start": "tsx src/server.ts", + "test": "jest", + "build": "tsc", + }, + "dependencies": { + "xrpl": "^4.0.0", + "hono": "^4.0.0", + "@hono/node-server": "^1.0.0", + }, + "devDependencies": { + "typescript": "^5.0.0", + "tsx": "^4.21.0", + "jest": "^29.0.0", + "ts-jest": "^29.0.0", + "@types/jest": "^29.0.0", + "@types/node": "^20.0.0", + }, + } + return json.dumps(package, indent=2) + "\n" + + +def generate_faucet_tsconfig() -> str: + """Generate tsconfig.json for the faucet service.""" + tsconfig = { + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": True, + "esModuleInterop": True, + "skipLibCheck": True, + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + } + return json.dumps(tsconfig, indent=2) + "\n" + + +def generate_faucet_server() -> str: + """Generate the main faucet server TypeScript source.""" + return '''\ +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; +import { Client, Wallet, dropsToXrp, xrpToDrops, ECDSA } from "xrpl"; + +const RATE_LIMIT_MS = parseInt(process.env.RATE_LIMIT_MS || "10000"); +const DEFAULT_AMOUNT = process.env.DEFAULT_XRP_AMOUNT || "1000"; +const XRPLD_WS_URL = process.env.XRPLD_WS_URL || "ws://localhost:6006"; +const PORT = parseInt(process.env.PORT || "8080"); + +const genesisWallet = Wallet.fromSecret('snoPBrXtMeMyMHUVTgbuqAfg1SUTb', { algorithm: ECDSA.secp256k1 }); + +let faucetWallet: Wallet; + +const rateLimitMap = new Map(); + +const app = new Hono(); + +async function connectWithRetry(client: Client): Promise { + const maxRetries = 60; + for (let i = 0; i < maxRetries; i++) { + try { + await client.connect(); + console.log("Connected to xrpld"); + return; + } catch (err) { + console.log( + `Connection attempt ${i + 1}/${maxRetries} failed, retrying in 5s...` + ); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } + throw new Error("Failed to connect to xrpld after maximum retries"); +} + +async function initFaucetWallet(client: Client): Promise { + const xrpBalance = await client.getXrpBalance(genesisWallet.address); + + const balanceDrops = BigInt(xrpToDrops(xrpBalance)); + const fundingDrops = (balanceDrops * BigInt(90)) / BigInt(100); + + console.log(`Genesis wallet address: ${genesisWallet.address}`); + faucetWallet = Wallet.generate(); + console.log(`Faucet wallet address: ${faucetWallet.address}`); + + const payment = { + TransactionType: "Payment" as const, + Account: genesisWallet.address, + Destination: faucetWallet.address, + Amount: fundingDrops.toString(), + }; + + const result = await client.submitAndWait(payment, { + wallet: genesisWallet, + }); + console.log( + `Funded faucet wallet: ${dropsToXrp(fundingDrops.toString())} XRP` + ); + console.log(`Transaction result: ${JSON.stringify(result.result, null, 2)}`); +} + +app.get("/health", (c) => { + return c.json({ + status: "ok", + faucetAddress: faucetWallet?.address, + }); +}); + +app.post("/accounts", async (c) => { + try { + const ip = c.req.header("x-forwarded-for") || "unknown"; + const now = Date.now(); + const lastRequest = rateLimitMap.get(ip); + + if (lastRequest && now - lastRequest < RATE_LIMIT_MS) { + return c.json( + { error: "Rate limit exceeded. Please wait before requesting again." }, + 429 + ); + } + rateLimitMap.set(ip, now); + + const body = await c.req.json().catch(() => ({})); + const { destination, xrpAmount } = body as { + destination?: string; + xrpAmount?: string; + }; + + const amount = xrpAmount || DEFAULT_AMOUNT; + let targetAddress: string; + let secret: string | undefined; + + if (destination) { + if ( + !destination.startsWith("r") || + destination.length < 25 || + destination.length > 35 + ) { + return c.json({ error: "Invalid destination address" }, 400); + } + targetAddress = destination; + } else { + const newWallet = Wallet.generate(); + targetAddress = newWallet.address; + secret = newWallet.seed; + } + + const client = new Client(XRPLD_WS_URL); + client.apiVersion = 1; + await client.connect(); + + try { + const payment = { + TransactionType: "Payment" as const, + Account: faucetWallet.address, + Destination: targetAddress, + Amount: xrpToDrops(amount), + }; + + await client.submit(payment, { wallet: faucetWallet }); + + const xrpBalance = await client.getXrpBalance(targetAddress, { ledger_index: 'current' }); + + const response: Record = { + account: { + address: targetAddress, + classicAddress: targetAddress, + ...(secret ? { secret } : {}), + }, + amount: parseInt(amount), + balance: xrpBalance, + }; + + return c.json(response); + } finally { + await client.disconnect(); + } + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "Unknown error"; + console.error(`Error funding account: ${message}`); + return c.json({ error: `Failed to fund account: ${message}` }, 500); + } +}); + +async function wait(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function main(): Promise { + while (true) { + const client = new Client(XRPLD_WS_URL); + client.apiVersion = 1; + try { + await connectWithRetry(client); + await initFaucetWallet(client); + break; + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "Unknown error"; + console.error(`Fatal error: ${message}`); + await wait(5000); + } finally { + await client.disconnect(); + } + } +} + +main(); +serve({ fetch: app.fetch, port: PORT }); + +export { app }; +''' + + +def generate_faucet_dockerfile() -> str: + """Generate Dockerfile for the faucet service.""" + return """\ +FROM node:20-slim + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 8080 + +CMD ["npx", "tsx", "src/server.ts"] +""" + + +def generate_faucet_test() -> str: + """Generate Jest test code for the faucet server.""" + return '''\ +import { app } from "./server"; + +jest.mock("xrpl", () => { + const mockClient = { + connect: jest.fn().mockResolvedValue(undefined), + disconnect: jest.fn().mockResolvedValue(undefined), + getBalances: jest.fn().mockResolvedValue([ + { currency: "XRP", value: "1000" }, + ]), + submitAndWait: jest.fn().mockResolvedValue({ + result: { meta: "tesSUCCESS" }, + }), + }; + + return { + Client: jest.fn(() => mockClient), + Wallet: { + fromSeed: jest.fn(() => ({ + address: "rGenesisAddress", + seed: "sGenesisSecret", + })), + generate: jest.fn(() => ({ + address: "rNewTestAddress", + seed: "sNewTestSecret", + classicAddress: "rNewTestAddress", + })), + }, + dropsToXrp: jest.fn((drops: string) => + (parseInt(drops) / 1000000).toString() + ), + xrpToDrops: jest.fn((xrp: string) => + (parseFloat(xrp) * 1000000).toString() + ), + }; +}); + +describe("Faucet Server", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("GET /health", () => { + it("should return status ok", async () => { + const res = await app.request("/health"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.status).toBe("ok"); + }); + }); + + describe("POST /accounts", () => { + it("should create a new account", async () => { + const res = await app.request("/accounts", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-forwarded-for": "10.0.0.1", + }, + body: JSON.stringify({}), + }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.account).toBeDefined(); + expect(body.account.address).toBeDefined(); + expect(body.account.secret).toBeDefined(); + expect(body.amount).toBeDefined(); + }); + + it("should fund destination without returning secret", async () => { + const res = await app.request("/accounts", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-forwarded-for": "10.0.0.2", + }, + body: JSON.stringify({ destination: "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe" }), + }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.account).toBeDefined(); + expect(body.account.address).toBe("rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"); + expect(body.account.secret).toBeUndefined(); + }); + + it("should accept custom xrpAmount", async () => { + const res = await app.request("/accounts", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-forwarded-for": "10.0.0.3", + }, + body: JSON.stringify({ xrpAmount: "500" }), + }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.amount).toBe(500); + }); + + it("should return 429 on rapid requests", async () => { + const headers = { + "Content-Type": "application/json", + "x-forwarded-for": "10.0.0.4", + }; + + const res1 = await app.request("/accounts", { + method: "POST", + headers, + body: JSON.stringify({}), + }); + expect(res1.status).toBe(200); + + const res2 = await app.request("/accounts", { + method: "POST", + headers, + body: JSON.stringify({}), + }); + expect(res2.status).toBe(429); + }); + + it("should return 400 for invalid destination address", async () => { + const res = await app.request("/accounts", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-forwarded-for": "10.0.0.5", + }, + body: JSON.stringify({ destination: "invalid_address" }), + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toBeDefined(); + }); + }); +}); +''' + + +def generate_faucet_jest_config() -> str: + """Generate Jest configuration for the faucet service.""" + return """\ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + roots: ["/src"], +}; +""" diff --git a/xrpld_netgen/network.py b/xrpld_netgen/network.py index 385aed2..cf57c20 100644 --- a/xrpld_netgen/network.py +++ b/xrpld_netgen/network.py @@ -47,6 +47,15 @@ get_feature_lines_from_path, ) +from xrpld_netgen.faucet import ( + generate_faucet_package_json, + generate_faucet_tsconfig, + generate_faucet_server, + generate_faucet_dockerfile, + generate_faucet_test, + generate_faucet_jest_config, +) + from xrpld_publisher.publisher import PublisherClient from xrpld_publisher.validator import ValidatorClient @@ -378,7 +387,9 @@ def create_network( repo = "xahaud" commit_hash = get_commit_hash_from_server_version(build_server, build_version) content_bytes = download_file_at_commit_or_tag( - owner, repo, commit_hash, "src/ripple/protocol/impl/Feature.cpp", "include/xrpl/protocol/detail/features.macro" + owner, repo, commit_hash, + "src/ripple/protocol/impl/Feature.cpp", + "include/xrpl/protocol/detail/features.macro", ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" @@ -513,6 +524,57 @@ def create_network( "networks": [f"{name}-network"], } + # Faucet service + ws_port = 6006 + (1 * 100) + services["faucet"] = { + "build": { + "context": "faucet", + "dockerfile": "Dockerfile", + }, + "container_name": "faucet", + "environment": [ + f"XRPLD_WS_URL=ws://vnode1:{ws_port}", + "DEFAULT_XRP_AMOUNT=1000", + f"PROTOCOL={protocol}", + ], + "ports": ["8080:8080"], + "depends_on": { + "vnode1": {"condition": "service_started"}, + }, + "networks": [f"{name}-network"], + } + + # Create faucet directory and files + faucet_dir = f"{basedir}/{name}-cluster/faucet" + os.makedirs(faucet_dir, exist_ok=True) + os.makedirs(f"{faucet_dir}/src", exist_ok=True) + write_file( + f"{faucet_dir}/package.json", + generate_faucet_package_json(), + ) + write_file( + f"{faucet_dir}/tsconfig.json", + generate_faucet_tsconfig(), + ) + write_file( + f"{faucet_dir}/src/server.ts", + generate_faucet_server(), + ) + write_file( + f"{faucet_dir}/Dockerfile", + generate_faucet_dockerfile(), + ) + write_file( + f"{faucet_dir}/src/server.test.ts", + generate_faucet_test(), + ) + write_file( + f"{faucet_dir}/jest.config.js", + generate_faucet_jest_config(), + ) + + print(f"✅ {bcolors.CYAN}Created faucet service") + compose = { "version": "3.9", "services": services, @@ -624,7 +686,9 @@ def create_ansible( repo = "xahaud" commit_hash = get_commit_hash_from_server_version(build_server, build_version) content_bytes = download_file_at_commit_or_tag( - owner, repo, commit_hash, "src/ripple/protocol/impl/Feature.cpp", "include/xrpl/protocol/detail/features.macro" + owner, repo, commit_hash, + "src/ripple/protocol/impl/Feature.cpp", + "include/xrpl/protocol/detail/features.macro", ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" @@ -1146,7 +1210,10 @@ def create_local_network( elif os.path.exists(macro_path): content = get_feature_lines_from_path(macro_path) else: - print(f"{bcolors.RED}Error: Cannot find features file at {local_path} or {macro_path}") + print( + f"{bcolors.RED}Error: Cannot find features" + f" file at {local_path} or {macro_path}" + ) print(f"Please run this command from your build directory.{bcolors.END}") return From e0991aca427c673950939ff637062f9c741ef705 Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 20 Mar 2026 21:01:28 +0900 Subject: [PATCH 7/7] copy binary on create not start.sh --- xrpld_netgen/main.py | 2 +- xrpld_netgen/network.py | 33 ++++++++++++++++++++------------ xrpld_netgen/utils/deploy_kit.py | 21 +++----------------- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/xrpld_netgen/main.py b/xrpld_netgen/main.py index 27b83c9..feaf03e 100644 --- a/xrpld_netgen/main.py +++ b/xrpld_netgen/main.py @@ -390,7 +390,7 @@ def create_standalone_binary( ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" - download_binary(url, f"{basedir}/{protocol}-{name}/{protocol}d.{name}") + download_binary(url, f"{basedir}/{protocol}-{name}/{protocol}d") image: str = "ubuntu:jammy" create_xahau_standalone_folder( True, diff --git a/xrpld_netgen/network.py b/xrpld_netgen/network.py index 385aed2..f3e384d 100644 --- a/xrpld_netgen/network.py +++ b/xrpld_netgen/network.py @@ -224,6 +224,10 @@ def create_node_folders( f"{package_dir}/deploykit/network.entrypoint", f"{basedir}/{name}-cluster/{node_dir}/entrypoint", ) + shutil.copyfile( + f"{basedir}/{name}-cluster/{protocol}d", + f"{basedir}/{name}-cluster/{node_dir}/{protocol}d", + ) print(f"✅ {bcolors.CYAN}Built validator: {i} docker container...") @@ -329,6 +333,10 @@ def create_node_folders( f"{package_dir}/deploykit/network.entrypoint", f"{basedir}/{name}-cluster/{node_dir}/entrypoint", ) + shutil.copyfile( + f"{basedir}/{name}-cluster/{protocol}d", + f"{basedir}/{name}-cluster/{node_dir}/{protocol}d", + ) print(f"✅ {bcolors.CYAN}Built peer: {i} docker container...") @@ -382,7 +390,7 @@ def create_network( ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" - download_binary(url, f"{basedir}/{name}-cluster/{protocol}d.{build_version}") + download_binary(url, f"{basedir}/{name}-cluster/{protocol}d") image: str = "ubuntu:jammy" if protocol == "xrpl": @@ -394,7 +402,7 @@ def create_network( name = name.replace("/", "-") os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) repo = "rippled" - copy_file("./xrpld", f"{basedir}/{name}-cluster/{protocol}d.{name}") + copy_file("./xrpld", f"{basedir}/{name}-cluster/{protocol}d") content_bytes = download_file_at_commit_or_tag( owner, repo, @@ -547,7 +555,7 @@ def create_network( os.chmod(f"{basedir}/{name}-cluster/start.sh", 0o755) os.chmod(f"{basedir}/{name}-cluster/stop.sh", 0o755) - os.chmod(f"{basedir}/{name}-cluster/{protocol}d.{name}", 0o755) + os.chmod(f"{basedir}/{name}-cluster/{protocol}d", 0o755) def update_node_binary( @@ -561,14 +569,14 @@ def update_node_binary( node_dir: str = f"{'v' if node_type == 'validator' else 'p'}node{node_id}" run_command(f"{basedir}/{name}", f"docker-compose stop {node_dir}") url: str = f"{build_server}/{new_version}" - download_binary(url, f"{basedir}/{name}/{protocol}d.{new_version}") + download_binary(url, f"{basedir}/{name}/{protocol}d") shutil.copyfile( - f"{basedir}/{name}/{protocol}d.{new_version}", - f"{basedir}/{name}/{node_dir}/{protocol}d.{new_version}", + f"{basedir}/{name}/{protocol}d", + f"{basedir}/{name}/{node_dir}/{protocol}d", ) # remove the db run_command(f"{basedir}/{name}", f"rm -r {node_dir}/lib") - os.chmod(f"{basedir}/{name}/{node_dir}/{protocol}d.{new_version}", 0o755) + os.chmod(f"{basedir}/{name}/{node_dir}/{protocol}d", 0o755) update_dockerfile(new_version, f"{basedir}/{name}/{node_dir}/Dockerfile") run_command( f"{basedir}/{name}", @@ -628,7 +636,7 @@ def create_ansible( ) content = get_feature_lines_from_content(content_bytes) url: str = f"{build_server}/{build_version}" - download_binary(url, f"{basedir}/{name}-cluster/{protocol}d.{build_version}") + download_binary(url, f"{basedir}/{name}-cluster/{protocol}d") image: str = "ubuntu:jammy" if protocol == "xrpl": @@ -640,7 +648,7 @@ def create_ansible( name = name.split("/tree/")[1] if "/tree/" in name else name name = name.replace("/", "-") os.makedirs(f"{basedir}/{name}-cluster", exist_ok=True) - copy_file("./xrpld", f"{basedir}/{name}-cluster/{protocol}d.{name}") + copy_file("./xrpld", f"{basedir}/{name}-cluster/{protocol}d") content_bytes = download_file_at_commit_or_tag( owner, repo, @@ -787,7 +795,7 @@ def create_ansible( os.chmod(f"{basedir}/{name}-cluster/start.sh", 0o755) os.chmod(f"{basedir}/{name}-cluster/stop.sh", 0o755) - os.chmod(f"{basedir}/{name}-cluster/{protocol}d.{name}", 0o755) + os.chmod(f"{basedir}/{name}-cluster/{protocol}d", 0o755) os.makedirs(f"{basedir}/{name}-cluster/ansible", exist_ok=True) os.makedirs(f"{basedir}/{name}-cluster/ansible/host_vars", exist_ok=True) @@ -840,7 +848,7 @@ def create_ansible( run_command( f"{basedir}/{name}-cluster", - f"cp {protocol}d.{name} {basedir}/{name}-cluster/vnode1", + f"cp {protocol}d {basedir}/{name}-cluster/vnode1", ) run_command( f"{basedir}/{name}-cluster/vnode1", @@ -853,7 +861,7 @@ def create_ansible( ) run_command( f"{basedir}/{name}-cluster/vnode1", - f"rm -r {protocol}d.{name}", + f"rm -r {protocol}d", ) if k[:5] == "pnode": index: int = int(k[5:]) @@ -986,6 +994,7 @@ def create_local_node_folders( cfg_path = f"{cluster_dir}/{node_dir}/config" # GENERATE PORTS rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports( + i, "validator" ) # GENERATE CONFIG - Use local paths instead of Docker paths diff --git a/xrpld_netgen/utils/deploy_kit.py b/xrpld_netgen/utils/deploy_kit.py index dc2ccc9..89efa6c 100644 --- a/xrpld_netgen/utils/deploy_kit.py +++ b/xrpld_netgen/utils/deploy_kit.py @@ -99,7 +99,7 @@ def create_dockerfile( dockerfile += "COPY genesis.json /genesis.json\n" if binary: - dockerfile += f"COPY {protocol}d.{version} /app/{protocol}d\n" + dockerfile += f"COPY {protocol}d /app/{protocol}d\n" if network: dockerfile += f""" @@ -199,14 +199,14 @@ def update_dockerfile(protocol: str, build_version: str, save_path: str) -> None lines = file.readlines() # Define the pattern to search for any xrpld COPY line - pattern = re.compile(fr"^COPY {protocol}d.* /app/{protocol}d$") + pattern = re.compile(fr"^COPY {protocol}d /app/{protocol}d$") # Replace the line with the new version with open(save_path, "w") as file: for line in lines: if pattern.match(line): # Replace the line with the new xrpld version - file.write(f"COPY {protocol}d.{build_version} /app/{protocol}d\n") + file.write(f"COPY {protocol}d /app/{protocol}d\n") else: file.write(line) @@ -290,11 +290,6 @@ def build_network_start_sh( num_peers: int, ): start_sh_content = '#! /bin/bash\ncd "$(dirname "$0")"\n' - for i in range(1, num_validators + 1): - start_sh_content += f"cp {protocol}d.{name} vnode{i}/{protocol}d.{name}\n" - - for i in range(1, num_peers + 1): - start_sh_content += f"cp {protocol}d.{name} pnode{i}/{protocol}d.{name}\n" start_sh_content += ( "docker compose -f docker-compose.yml" " up --build --force-recreate -d" @@ -323,16 +318,6 @@ def build_network_stop_sh( if num_validators > 0 and num_peers > 0: stop_sh_content += "docker compose -f docker-compose.yml down --remove-orphans\n" # noqa: E501 - for i in range(1, num_validators + 1): - stop_sh_content += f"rm -r vnode{i}/lib\n" - stop_sh_content += f"rm -r vnode{i}/log\n" - stop_sh_content += f"rm -r vnode{i}/{protocol}d.{name}\n" - - for i in range(1, num_peers + 1): - stop_sh_content += f"rm -r pnode{i}/lib\n" - stop_sh_content += f"rm -r pnode{i}/log\n" - stop_sh_content += f"rm -r pnode{i}/{protocol}d.{name}\n" - stop_sh_content += "else \n" if num_validators > 0 and num_peers > 0: stop_sh_content += "docker compose -f docker-compose.yml down\n" # noqa: E501