Add token-mint-service as tool

This commit is contained in:
stubbsta 2025-07-01 11:09:06 +02:00
parent c6b201d264
commit 363d373cc0
No known key found for this signature in database
5 changed files with 273 additions and 3 deletions

View File

@ -2,9 +2,7 @@
set -e
# 1. Install foundry, pnpm, and required tools
apt update && apt install -y jq
# 1. Install foundry and pnpm
curl -L https://foundry.paradigm.xyz | bash && . /root/.bashrc && foundryup && export PATH=$PATH:$HOME/.foundry/bin
echo "installing pnpm..."

View File

@ -0,0 +1,20 @@
FROM python:3.11-alpine
WORKDIR /app
# Install system packages needed by the script
RUN apk add --no-cache bind-tools jq
# Install requirements
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy scripts
COPY init_node_tokens.py .
COPY get_account_key.sh .
# Make scripts executable
RUN chmod +x /app/init_node_tokens.py /app/get_account_key.sh
# Use the account key helper as entrypoint
ENTRYPOINT ["/bin/sh", "/app/get_account_key.sh"]

View File

@ -0,0 +1,57 @@
#!/bin/sh
# Helper script to get the index of the container and use it to retrieve a unique account private key per nwaku node to be used to generate the keystore
set -e
# Tools already installed in Dockerfile
ANVIL_CONFIG_PATH=${ANVIL_CONFIG_PATH:-/shared/anvil-config.txt}
# Wait for anvil config to be available
echo "Waiting for anvil config at $ANVIL_CONFIG_PATH..."
while [ ! -f "$ANVIL_CONFIG_PATH" ]; do
sleep 2
done
# Get container IP and determine index (same method as run_nwaku.sh)
IP=$(ip a | grep "inet " | grep -Fv 127.0.0.1 | sed 's/.*inet \([^/]*\).*/\1/')
echo "Container IP: $IP"
# Extract container name from reverse DNS lookup and get index
CNTR=$(dig -x $IP +short | cut -d'.' -f1)
INDEX=$(echo $CNTR | sed 's/.*[-_]\([0-9]*\)/\1/')
if [ $? -ne 0 ] || [ -z "$INDEX" ]; then
echo "Error: Failed to determine the replica index from IP." >&2
exit 1
fi
echo "Determined container index: $INDEX"
# Read anvil config
json_content=$(cat "$ANVIL_CONFIG_PATH")
if [ -z "$json_content" ]; then
echo "Error: Failed to read the JSON file or the file is empty." >&2
exit 1
fi
# Get private key and address for this index
ARRAY_INDEX=$((INDEX - 1))
ACCOUNT_PRIVATE_KEY=$(echo "$json_content" | jq -r ".private_keys[$ARRAY_INDEX]")
ACCOUNT_ADDRESS=$(echo "$json_content" | jq -r ".available_accounts[$ARRAY_INDEX]")
if [ "$ACCOUNT_PRIVATE_KEY" = "null" ] || [ "$ACCOUNT_ADDRESS" = "null" ]; then
echo "Failed to get account private key or address for index $INDEX (array index $ARRAY_INDEX)" >&2
exit 1
fi
# Export for the Python script
export NODE_PRIVATE_KEY="$ACCOUNT_PRIVATE_KEY"
export NODE_ADDRESS="$ACCOUNT_ADDRESS"
export NODE_INDEX="$INDEX"
echo "Node $INDEX using Ethereum account: $ACCOUNT_ADDRESS"
# Run the Python initialization script
exec python3 /app/init_node_tokens.py

View File

@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""
Per-node token initialization service.
This script runs as an init container for each nwaku node to:
1. Mint ERC20 tokens to the node's address
2. Approve the RLN contract to spend those tokens
Each node gets its own private key and handles its own token setup.
"""
import os
import sys
import time
import logging
from web3 import Web3
from web3.exceptions import TransactionNotFound
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - [Node Init] %(message)s'
)
logger = logging.getLogger(__name__)
class NodeTokenInitializer:
def __init__(self):
"""Initialize the node token service."""
# Required environment variables
self.rpc_url = os.getenv('RPC_URL', 'http://foundry:8545')
self.token_address = os.getenv('TOKEN_ADDRESS', '0x5FbDB2315678afecb367f032d93F642f64180aa3')
self.contract_address = os.getenv('CONTRACT_ADDRESS', '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707')
self.private_key = os.getenv('NODE_PRIVATE_KEY') # Ethereum account private key (not nwaku node-key)
self.node_address = os.getenv('NODE_ADDRESS') # Ethereum account address
self.node_index = os.getenv('NODE_INDEX', '0')
self.mint_amount = int(os.getenv('MINT_AMOUNT', '5000000000000000000')) # 5 tokens
if not self.private_key:
raise ValueError("NODE_PRIVATE_KEY (Ethereum account private key) environment variable is required")
if not self.node_address:
raise ValueError("NODE_ADDRESS (Ethereum account address) environment variable is required")
# Initialize Web3
self.w3 = Web3(Web3.HTTPProvider(self.rpc_url))
if not self.w3.is_connected():
raise Exception(f"Failed to connect to Ethereum node at {self.rpc_url}")
# Convert addresses to proper checksum format
self.node_address = self.w3.to_checksum_address(self.node_address)
self.token_address = self.w3.to_checksum_address(self.token_address)
self.contract_address = self.w3.to_checksum_address(self.contract_address)
logger.info(f"Node {self.node_index} initializing tokens")
logger.info(f"Address: {self.node_address}")
logger.info(f"Token: {self.token_address}")
logger.info(f"Contract: {self.contract_address}")
def wait_for_transaction(self, tx_hash: str, timeout: int = 120) -> bool:
"""Wait for transaction to be mined."""
start_time = time.time()
while time.time() - start_time < timeout:
try:
receipt = self.w3.eth.get_transaction_receipt(tx_hash)
if receipt.status == 1:
logger.info(f"Transaction {tx_hash} confirmed")
return True
else:
logger.error(f"Transaction {tx_hash} failed with status {receipt.status}")
return False
except TransactionNotFound:
time.sleep(2)
continue
logger.error(f"Transaction {tx_hash} timed out after {timeout} seconds")
return False
def mint_tokens(self) -> bool:
"""Mint tokens to this node's address using the node's own private key."""
try:
logger.info(f"Minting {self.mint_amount} tokens to {self.node_address}")
# Use the node's own private key since mint() is public
nonce = self.w3.eth.get_transaction_count(self.node_address)
# Build mint transaction
function_signature = self.w3.keccak(text="mint(address,uint256)")[:4]
encoded_address = self.node_address[2:].lower().zfill(64)
encoded_amount = hex(self.mint_amount)[2:].zfill(64)
data = function_signature.hex() + encoded_address + encoded_amount
transaction = {
'to': self.token_address,
'value': 0,
'gas': 200000,
'gasPrice': self.w3.eth.gas_price,
'nonce': nonce,
'data': data,
}
# Sign and send with node's own key
signed_txn = self.w3.eth.account.sign_transaction(transaction, self.private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
logger.info(f"Mint transaction sent: {tx_hash.hex()}")
if self.wait_for_transaction(tx_hash.hex()):
logger.info(f"✓ Mint successful for node {self.node_index}")
return True
else:
logger.error(f"✗ Mint failed for node {self.node_index}")
return False
except Exception as e:
logger.error(f"✗ Mint failed for node {self.node_index}: {str(e)}")
return False
def approve_tokens(self) -> bool:
"""Approve RLN contract to spend tokens."""
try:
logger.info(f"Approving {self.mint_amount} tokens for contract {self.contract_address}")
nonce = self.w3.eth.get_transaction_count(self.node_address)
# Build approve transaction
function_signature = self.w3.keccak(text="approve(address,uint256)")[:4]
encoded_contract = self.contract_address[2:].lower().zfill(64)
encoded_amount = hex(self.mint_amount)[2:].zfill(64)
data = function_signature.hex() + encoded_contract + encoded_amount
transaction = {
'to': self.token_address,
'value': 0,
'gas': 200000,
'gasPrice': self.w3.eth.gas_price,
'nonce': nonce,
'data': data,
}
# Sign and send with node's own key
signed_txn = self.w3.eth.account.sign_transaction(transaction, self.private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
logger.info(f"Approve transaction sent: {tx_hash.hex()}")
if self.wait_for_transaction(tx_hash.hex()):
logger.info(f"✓ Approval successful for node {self.node_index}")
return True
else:
logger.error(f"✗ Approval failed for node {self.node_index}")
return False
except Exception as e:
logger.error(f"✗ Approval failed for node {self.node_index}: {str(e)}")
return False
def run(self) -> bool:
"""Run the token initialization process."""
try:
logger.info(f"Starting token initialization for node {self.node_index}")
# Step 1: Mint tokens
if not self.mint_tokens():
return False
# Step 2: Approve contract
if not self.approve_tokens():
return False
logger.info(f"✓ Node {self.node_index} token initialization completed successfully")
return True
except Exception as e:
logger.error(f"✗ Node {self.node_index} initialization failed: {str(e)}")
return False
def main():
"""Main entry point."""
try:
initializer = NodeTokenInitializer()
success = initializer.run()
if success:
logger.info("Node ready to start")
sys.exit(0)
else:
logger.error("Node initialization failed")
sys.exit(1)
except Exception as e:
logger.error(f"Failed to initialize: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1 @@
web3==6.15.1