From d8b62c2179e8b3d0055f140dbe7e7dcb7bb07201 Mon Sep 17 00:00:00 2001 From: Parth Shah Date: Fri, 20 Feb 2026 16:50:05 +0000 Subject: [PATCH] feat: add FilWizard container for smart contract deployment Dedicated container handling FOC contract deployment, SP registration, wallet funding, and environment wiring. Runs with --profile foc. --- .github/workflows/build_push_filwizard.yml | 48 ++++ Dockerfile | 6 +- filwizard/Dockerfile | 59 +++++ filwizard/entrypoint.sh | 173 ++++++++++++++ filwizard/scripts/export-environment.sh | 73 ++++++ filwizard/scripts/package.json | 9 + .../scripts/register-service-provider.js | 220 ++++++++++++++++++ filwizard/scripts/warm-add.js | 197 ++++++++++++++++ 8 files changed, 782 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/build_push_filwizard.yml create mode 100644 filwizard/Dockerfile create mode 100755 filwizard/entrypoint.sh create mode 100644 filwizard/scripts/export-environment.sh create mode 100644 filwizard/scripts/package.json create mode 100755 filwizard/scripts/register-service-provider.js create mode 100755 filwizard/scripts/warm-add.js diff --git a/.github/workflows/build_push_filwizard.yml b/.github/workflows/build_push_filwizard.yml new file mode 100644 index 0000000..7f9386b --- /dev/null +++ b/.github/workflows/build_push_filwizard.yml @@ -0,0 +1,48 @@ +name: Build & Push FilWizard (Smart contract deployment tool). + +on: + workflow_dispatch: + inputs: + filwizard_tag: + type: string + required: true + default: 'latest' + description: A filwizard tag + +jobs: + antithesis: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Antithesis Google Artifact Registry + uses: docker/login-action@v3 + with: + registry: us-central1-docker.pkg.dev + username: _json_key + password: ${{ secrets.ANTITHESIS_GAR_KEY }} + + - name: Extract metadata (tags) for Docker filwizard + id: meta-filwizard + uses: docker/metadata-action@v5 + with: + images: us-central1-docker.pkg.dev/molten-verve-216720/filecoin-repository/filwizard + tags: | + type=sha + ${{ github.event.inputs.filwizard_tag }} + - name: Build and push filwizard + uses: docker/build-push-action@v5 + with: + context: ./filwizard + file: ./filwizard/Dockerfile + push: true + tags: ${{ steps.meta-filwizard.outputs.tags }}, ${{ env.GITHUB_REF_NAME }} + labels: ${{ steps.meta-filwizard.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 4477009..7fe9667 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ FROM scratch COPY docker-compose.yaml /docker-compose.yaml -COPY docker-compose2.yaml /docker-compose2.yaml COPY ./.env /.env -COPY ../data /data COPY ./drand /drand COPY ./lotus /lotus COPY ./forest /forest COPY ./curio /curio -COPY ./yugabyte /yugabyte \ No newline at end of file +COPY ./yugabyte /yugabyte +COPY ./filwizard /filwizard +COPY ./workload /workload \ No newline at end of file diff --git a/filwizard/Dockerfile b/filwizard/Dockerfile new file mode 100644 index 0000000..b51e67a --- /dev/null +++ b/filwizard/Dockerfile @@ -0,0 +1,59 @@ +# Stage 1: Clone and build the filwizard binary +FROM golang:latest AS builder + +# Clone FilWizard repo (for contract deployment only) +RUN git clone https://github.com/parthshah1/FilWizard /build +WORKDIR /build +RUN git checkout main +RUN git pull + +# Build filwizard binary +RUN go mod download +RUN CGO_ENABLED=1 go build -o filwizard . + +# Install abigen (go-ethereum ABI binding generator) +RUN go install github.com/ethereum/go-ethereum/cmd/abigen@latest + +# Stage 2: Runtime image with all tools needed for contract deployment +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y \ + curl jq git ca-certificates nodejs npm \ + && rm -rf /var/lib/apt/lists/* + +# Install pnpm +RUN npm install -g pnpm@latest + +# Install Foundry +RUN curl -L https://foundry.paradigm.xyz | bash +ENV PATH="/root/.foundry/bin:${PATH}" +RUN foundryup + +# Copy filwizard binary +COPY --from=builder /build/filwizard /usr/local/bin/filwizard + +# Copy abigen binary +COPY --from=builder /go/bin/abigen /usr/local/bin/abigen + +# Pre-clone and compile FOC contracts (air-gapped operation) +WORKDIR /opt/filwizard +COPY --from=builder /build/config/filecoin-synapse.json /opt/filwizard/config/filecoin-synapse.json + +RUN filwizard contract clone-config \ + --config /opt/filwizard/config/filecoin-synapse.json \ + --workspace /opt/filwizard/workspace + + +# Copy scripts +COPY scripts/register-service-provider.js /opt/filwizard/scripts/register-service-provider.js +COPY scripts/warm-add.js /opt/filwizard/scripts/warm-add.js +COPY scripts/package.json /opt/filwizard/scripts/package.json +COPY scripts/export-environment.sh /opt/filwizard/scripts/export-environment.sh +RUN chmod +x /opt/filwizard/scripts/register-service-provider.js /opt/filwizard/scripts/warm-add.js /opt/filwizard/scripts/export-environment.sh +# Install script dependencies locally (ethers.js) +RUN cd /opt/filwizard/scripts && npm install + +WORKDIR /opt/filwizard + +# Default entrypoint — will be overridden by compose +ENTRYPOINT ["filwizard"] diff --git a/filwizard/entrypoint.sh b/filwizard/entrypoint.sh new file mode 100755 index 0000000..4df0491 --- /dev/null +++ b/filwizard/entrypoint.sh @@ -0,0 +1,173 @@ +#!/bin/bash +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[FILWIZARD]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[FILWIZARD]${NC} $1"; } +log_error() { echo -e "${RED}[FILWIZARD]${NC} $1" >&2; } + +wait_for_file() { + local file="$1" desc="$2" + log_info "Waiting for $desc..." + while [ ! -f "$file" ] || [ ! -s "$file" ]; do sleep 2; done + log_info "$desc found" +} + +# ── Config ── +FILECOIN_RPC="http://lotus0:1234/rpc/v1" +ETH_RPC_URL="http://lotus0:1234/rpc/v1" +RPC_WS_URL="ws://lotus0:1234/rpc/v1" +RPC_LOTUS="${RPC_LOTUS:-http://lotus0:1234/rpc/v0}" +CHAIN_ID="31415926" +INIT_BLOCK_HEIGHT=5 + +WORKSPACE_PATH="/opt/filwizard/workspace" +CONFIG_PATH="/opt/filwizard/config/filecoin-synapse.json" +ENV_OUTPUT="/shared/environment.env" +CURIO_SHARED_DIR="/shared/curio" +CURIO_ENV_FILE="${CURIO_SHARED_DIR}/.env.curio" +ACCOUNTS_FILE="${WORKSPACE_PATH}/accounts.json" + +SP_SERVICE_URL="${SP_SERVICE_URL:-http://curio:80}" +SP_NAME="${SP_NAME:-My Devnet Provider}" +SP_DESCRIPTION="${SP_DESCRIPTION:-Devnet provider for Warm Storage}" + +# ── 1. Wait for chain ── +log_info "Waiting for block height ${INIT_BLOCK_HEIGHT}..." +BLOCK_HEIGHT_REACHED=0 +while [ "${INIT_BLOCK_HEIGHT}" -gt "${BLOCK_HEIGHT_REACHED}" ]; do + RESPONSE=$(curl -s --max-time 5 -X POST "$RPC_LOTUS" \ + -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","id":1,"method":"Filecoin.ChainHead","params":[]}' 2>/dev/null || echo '{}') + BLOCK_HEIGHT_REACHED=$(echo "$RESPONSE" | jq -r '.result.Height // 0' 2>/dev/null) + [ -z "$BLOCK_HEIGHT_REACHED" ] || [ "$BLOCK_HEIGHT_REACHED" = "null" ] && BLOCK_HEIGHT_REACHED=0 + [ "${INIT_BLOCK_HEIGHT}" -le "${BLOCK_HEIGHT_REACHED}" ] && break + log_info "Height: ${BLOCK_HEIGHT_REACHED}, waiting..." + sleep 5 +done +log_info "Chain ready at height ${BLOCK_HEIGHT_REACHED}" + +# ── 2. Setup Filecoin env ── +JWT_FILE="/shared/lotus0/lotus0-jwt" +wait_for_file "$JWT_FILE" "Lotus JWT" +export FILECOIN_RPC ETH_RPC_URL +export FILECOIN_TOKEN=$(cat "$JWT_FILE") + +# ── 3. Deploy FOC contracts ── +log_info "Deploying contracts via FilWizard..." +cd /opt/filwizard + +filwizard contract deploy-local \ + --config "$CONFIG_PATH" \ + --workspace "$WORKSPACE_PATH" \ + --rpc-url "$FILECOIN_RPC" \ + --create-deployer \ + --bindings \ + || log_warn "Deployment completed with warnings, continuing..." + +wait_for_file "${WORKSPACE_PATH}/deployments.json" "deployments.json" + +# ── 4. Export environment (simple bash + jq) ── +log_info "Exporting environment..." +bash /opt/filwizard/scripts/export-environment.sh \ + "$WORKSPACE_PATH" \ + "$ENV_OUTPUT" \ + "$CHAIN_ID" \ + "$FILECOIN_RPC" \ + "$RPC_WS_URL" + +log_info "environment.env written to $ENV_OUTPUT" + +# ── 5. Create Curio .env (subset with CURIO_ prefix) ── +log_info "Creating Curio environment file..." +mkdir -p "$CURIO_SHARED_DIR" + +# Source the exported env to get address variables +set -a +source "$ENV_OUTPUT" +set +a + +cat > "$CURIO_ENV_FILE" << EOF +# Curio Devnet Contract Addresses +# Generated by filwizard entrypoint + +CURIO_DEVNET_PDP_VERIFIER_ADDRESS=${PDP_VERIFIER_PROXY_ADDRESS} +CURIO_DEVNET_FWSS_ADDRESS=${FWSS_PROXY_ADDRESS} +CURIO_DEVNET_SERVICE_REGISTRY_ADDRESS=${SERVICE_PROVIDER_REGISTRY_PROXY_ADDRESS} +CURIO_DEVNET_PAYMENTS_ADDRESS=${FILECOIN_PAY_ADDRESS} +CURIO_DEVNET_USDFC_ADDRESS=${USDFC_ADDRESS} +CURIO_DEVNET_MULTICALL_ADDRESS=${MULTICALL3_ADDRESS} +EOF + +log_info "Curio env created: $CURIO_ENV_FILE" + +# ── 6. Create client wallet ── +log_info "Creating client wallet..." +filwizard wallet create \ + --type ethereum \ + --count 1 \ + --fund 5 \ + --show-private-key \ + --key-output "$ACCOUNTS_FILE" \ + --name "client" + +CLIENT_PRIVATE_KEY=$(jq -r '.accounts.client.privateKey' "$ACCOUNTS_FILE") +DEPLOYER_PRIVATE_KEY=$(jq -r '.accounts.deployer.privateKey' "$ACCOUNTS_FILE") + +# ── 7. Wait for Curio SP private key ── +SP_PRIVATE_KEY_FILE="/shared/curio/private_key" +wait_for_file "$SP_PRIVATE_KEY_FILE" "SP private key from Curio" +SP_PRIVATE_KEY=$(cat "$SP_PRIVATE_KEY_FILE" | tr -d '[:space:]') + +# ── 8. Mint tokens ── +log_info "Minting tokens..." + +filwizard payments mint-private-key --workspace "$WORKSPACE_PATH" \ + --private-key "$CLIENT_PRIVATE_KEY" --amount 1000000000000000000000 --fil 0 + +filwizard payments mint-private-key --workspace "$WORKSPACE_PATH" \ + --private-key "$CLIENT_PRIVATE_KEY" --amount 0 --fil 10 + +filwizard payments mint-private-key --workspace "$WORKSPACE_PATH" \ + --private-key "$SP_PRIVATE_KEY" --amount 10000000000000000000000 --fil 0 + +filwizard payments mint-private-key --workspace "$WORKSPACE_PATH" \ + --private-key "$SP_PRIVATE_KEY" --amount 0 --fil 10 + +log_info "Tokens minted" + +# ── 9. Register service provider ── +log_info "Registering service provider..." + +export DEPLOYER_PRIVATE_KEY SP_PRIVATE_KEY CLIENT_PRIVATE_KEY +export SP_SERVICE_URL SP_NAME SP_DESCRIPTION + +node /opt/filwizard/scripts/register-service-provider.js \ + --rpc-url "$FILECOIN_RPC" \ + --warm-storage "$FWSS_PROXY_ADDRESS" \ + --sp-registry "$SERVICE_PROVIDER_REGISTRY_PROXY_ADDRESS" + +log_info "Service provider registered" + +# ── 10. Add approved provider to warm storage ── +log_info "Adding service provider to warm storage global whitelist..." + +export DEPLOYER_PRIVATE_KEY SP_PRIVATE_KEY + +node /opt/filwizard/scripts/warm-add.js \ + --rpc-url "$FILECOIN_RPC" \ + --warm-storage "$FWSS_PROXY_ADDRESS" \ + --sp-registry "$SERVICE_PROVIDER_REGISTRY_PROXY_ADDRESS" + +log_info "Service provider added to global whitelist (ready for dapps)" + +# ── 11. Signal healthy ── +log_info "FilWizard setup complete. Signaling healthy." +touch /tmp/healthy + +# Keep container alive (compose healthcheck will gate dependents) +sleep infinity diff --git a/filwizard/scripts/export-environment.sh b/filwizard/scripts/export-environment.sh new file mode 100644 index 0000000..b35d0e7 --- /dev/null +++ b/filwizard/scripts/export-environment.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -e + +# Simple environment export using bash + jq +# Replaces the FilWizard env export command + +WORKSPACE="${1:-/opt/filwizard/workspace}" +OUTPUT="${2:-/shared/environment.env}" +CHAIN_ID="${3:-31415926}" +RPC_URL="${4:-http://lotus0:1234/rpc/v1}" +RPC_WS_URL="${5:-ws://lotus0:1234/rpc/v1}" + +# Use service contracts from workspace deployment (already deployed) +SERVICE_CONTRACTS_JSON="$WORKSPACE/filecoinwarmstorage/service_contracts/deployments.json" +WORKSPACE_DEPLOYMENTS_JSON="$WORKSPACE/deployments.json" +ACCOUNTS_JSON="$WORKSPACE/accounts.json" + +# Write to file instead of stdout +{ +echo "# Generated by filwizard environment export" +echo "# Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" +echo "" +echo "# Network" +echo "CHAIN_ID=$CHAIN_ID" +echo "RPC_URL=$RPC_URL" +echo "ETH_RPC_URL=$RPC_URL" +echo "RPC_WS_URL=$RPC_WS_URL" +echo "" + +# Service contract addresses (from workspace deployment) +echo "# Service Contract Addresses" +if [ -f "$SERVICE_CONTRACTS_JSON" ]; then + jq -r --arg chain "$CHAIN_ID" ' + .[$chain] // {} | + to_entries | + map(select(.key != "metadata")) | + sort_by(.key) | + .[] | + "\(.key)=\(.value)" + ' "$SERVICE_CONTRACTS_JSON" +else + echo "# Note: Service contracts will be loaded after deployment" +fi + +# USDFC and Multicall3 from workspace +if [ -f "$WORKSPACE_DEPLOYMENTS_JSON" ]; then + jq -r ' + .[] | + select(.name == "USDFC" or .name == "Multicall3") | + "\(.name | ascii_upcase)_ADDRESS=\(.address)" + ' "$WORKSPACE_DEPLOYMENTS_JSON" +fi +echo "" + +# Accounts +echo "# Accounts" +if [ -f "$ACCOUNTS_JSON" ]; then + # Deployer private key + DEPLOYER_KEY=$(jq -r '.accounts.deployer.privateKey // empty' "$ACCOUNTS_JSON") + if [ -n "$DEPLOYER_KEY" ]; then + echo "DEPLOYER_PRIVATE_KEY=$DEPLOYER_KEY" + fi + + # Deployer ETH address + DEPLOYER_ETH=$(jq -r '.accounts.deployer.ethAddress // empty' "$ACCOUNTS_JSON") + if [ -n "$DEPLOYER_ETH" ]; then + echo "DEPLOYER_ETH_ADDRESS=$DEPLOYER_ETH" + fi +fi +echo "" +} > "$OUTPUT" + +echo "✓ Environment exported to $OUTPUT" diff --git a/filwizard/scripts/package.json b/filwizard/scripts/package.json new file mode 100644 index 0000000..7e55e8e --- /dev/null +++ b/filwizard/scripts/package.json @@ -0,0 +1,9 @@ +{ + "name": "filwizard-scripts", + "version": "1.0.0", + "type": "module", + "description": "Service provider registration and contract deployment scripts", + "dependencies": { + "ethers": "^6.0.0" + } +} diff --git a/filwizard/scripts/register-service-provider.js b/filwizard/scripts/register-service-provider.js new file mode 100755 index 0000000..7df063b --- /dev/null +++ b/filwizard/scripts/register-service-provider.js @@ -0,0 +1,220 @@ +#!/usr/bin/env node + +import { ethers } from 'ethers'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Parse command line arguments +function parseArgs() { + const args = process.argv.slice(2); + const parsed = {}; + + for (let i = 0; i < args.length; i++) { + if (args[i].startsWith('--')) { + const key = args[i].substring(2); + const value = args[i + 1]; + parsed[key] = value; + i++; + } + } + + return parsed; +} + +const args = parseArgs(); +const RPC_URL = args['rpc-url'] || process.env.RPC_URL; +const SP_REGISTRY_ADDRESS = args['sp-registry'] || process.env.SERVICE_PROVIDER_REGISTRY_PROXY_ADDRESS; +const FWSS_ADDRESS = args['warm-storage'] || process.env.FWSS_PROXY_ADDRESS; + +// Environment variables +const DEPLOYER_PRIVATE_KEY = process.env.DEPLOYER_PRIVATE_KEY; +const SP_PRIVATE_KEY = process.env.SP_PRIVATE_KEY; +const SP_SERVICE_URL = process.env.SP_SERVICE_URL || 'http://curio:80'; +const SP_NAME = process.env.SP_NAME || 'My Devnet Provider'; +const SP_DESCRIPTION = process.env.SP_DESCRIPTION || 'Devnet provider for Warm Storage'; + +console.log('ℹ️ 🚀 Starting service provider registration'); +console.log(`ℹ️ RPC: ${RPC_URL}`); +console.log(`ℹ️ Warm Storage: ${FWSS_ADDRESS}`); +console.log(`ℹ️ SP Registry: ${SP_REGISTRY_ADDRESS}`); + +// Load ABI +const WORKSPACE_PATH = '/opt/filwizard/workspace'; +const registryAbiPath = path.join(WORKSPACE_PATH, 'filecoinwarmstorage', 'service_contracts', 'abi', 'ServiceProviderRegistry.abi.json'); + +let registryAbi; +try { + registryAbi = JSON.parse(fs.readFileSync(registryAbiPath, 'utf8')); +} catch (error) { + console.error('❌ Failed to load ServiceProviderRegistry ABI:', error.message); + process.exit(1); +} + +// Setup provider and wallets +const provider = new ethers.JsonRpcProvider(RPC_URL); +const deployerWallet = new ethers.Wallet(DEPLOYER_PRIVATE_KEY, provider); +const spWallet = new ethers.Wallet(SP_PRIVATE_KEY, provider); + +console.log(`ℹ️ Deployer: ${deployerWallet.address}`); +console.log(`ℹ️ Service Provider: ${spWallet.address}`); +console.log('ℹ️ '); + +// Create contract instance +const registry = new ethers.Contract(SP_REGISTRY_ADDRESS, registryAbi, spWallet); + +// Registration parameters +const REGISTRATION_FEE = ethers.parseEther('5'); // 5 FIL + +async function registerServiceProvider() { + console.log('📋 Step 1: Service Provider Registration in Registry'); + + // Check if already registered + try { + const providerId = await registry.addressToProviderId(spWallet.address); + if (providerId > 0) { + console.log(`✅ Service provider already registered with ID: ${providerId}`); + return providerId; + } + } catch (error) { + // Continue with registration + } + + console.log(`ℹ️ Registering new provider: ${SP_NAME}`); + console.log(`ℹ️ Note: Registration requires a 5 FIL fee`); + + // Check balance + const balance = await provider.getBalance(spWallet.address); + console.log(`ℹ️ SP Balance: ${ethers.formatEther(balance)} FIL`); + console.log(`ℹ️ Registration Fee: ${ethers.formatEther(REGISTRATION_FEE)} FIL`); + + if (balance < REGISTRATION_FEE) { + console.error(`❌ Insufficient balance. Need ${ethers.formatEther(REGISTRATION_FEE)} FIL, have ${ethers.formatEther(balance)} FIL`); + process.exit(1); + } + + // Prepare capabilities - MUST match ServiceProviderRegistry.sol REQUIRED_PDP_KEYS + // Required capability keys (case-sensitive, must match exactly): + // 1. serviceURL + // 2. minPieceSizeInBytes + // 3. maxPieceSizeInBytes + // 4. storagePricePerTibPerDay + // 5. minProvingPeriodInEpochs + // 6. location + // 7. paymentTokenAddress + + // Helper function to encode number as bytes (big-endian) + const encodeNumber = (num) => { + const bn = ethers.toBigInt(num); + const hex = bn.toString(16); + return '0x' + (hex.length % 2 === 0 ? hex : '0' + hex); + }; + + const capabilityKeys = [ + 'serviceURL', + 'minPieceSizeInBytes', + 'maxPieceSizeInBytes', + 'storagePricePerTibPerDay', + 'minProvingPeriodInEpochs', + 'location', + 'paymentTokenAddress' + ]; + + const capabilityValues = [ + ethers.hexlify(ethers.toUtf8Bytes(SP_SERVICE_URL)), // serviceURL (string) + encodeNumber(1024), // minPieceSizeInBytes (1 KiB) + encodeNumber(1024 * 1024 * 1024), // maxPieceSizeInBytes (1 GiB) + encodeNumber(ethers.parseEther('0.001')), // storagePricePerTibPerDay (0.001 FIL/TiB/day) + encodeNumber(2880), // minProvingPeriodInEpochs (2880 epochs ≈ 1 day) + ethers.hexlify(ethers.toUtf8Bytes('us-east-1')), // location (string) + ethers.zeroPadValue('0x00', 20) // paymentTokenAddress (0x00...00 = FIL) + ]; + + // ProductType.PDP = 0 + const ProductType_PDP = 0; + + console.log('ℹ️ Submitting registration transaction...'); + + try { + const tx = await registry.registerProvider( + spWallet.address, // payee - same as service provider + SP_NAME, // name + SP_DESCRIPTION, // description + ProductType_PDP, // productType + capabilityKeys, // capabilityKeys + capabilityValues, // capabilityValues + { + value: REGISTRATION_FEE // CRITICAL: Must send 5 FIL + } + ); + + console.log(`ℹ️ Transaction submitted: ${tx.hash}`); + console.log('ℹ️ Waiting for confirmation...'); + + const receipt = await tx.wait(); + console.log(`✅ Transaction confirmed in block ${receipt.blockNumber}`); + + // Extract provider ID from events + const event = receipt.logs + .map(log => { + try { + return registry.interface.parseLog(log); + } catch { + return null; + } + }) + .find(e => e && e.name === 'ProviderRegistered'); + + if (event) { + const providerId = event.args.providerId; + console.log(`✅ Service provider registered with ID: ${providerId}`); + return providerId; + } else { + console.log('✅ Registration transaction successful'); + // Fetch provider ID + const providerId = await registry.addressToProviderId(spWallet.address); + console.log(`✅ Service provider ID: ${providerId}`); + return providerId; + } + } catch (error) { + console.error('❌ Registration failed:', error.message); + + // Try to decode revert reason + if (error.data) { + try { + const decodedError = registry.interface.parseError(error.data); + console.error('❌ Revert reason:', decodedError.name, decodedError.args); + } catch { + console.error('❌ Raw error data:', error.data); + } + } + + throw error; + } +} + +// Main execution +async function main() { + try { + const providerId = await registerServiceProvider(); + + console.log(''); + console.log('✅ Service provider registration complete!'); + console.log(` Provider ID: ${providerId}`); + console.log(` Address: ${spWallet.address}`); + console.log(` Registry: ${SP_REGISTRY_ADDRESS}`); + console.log(''); + + process.exit(0); + } catch (error) { + console.error(''); + console.error('❌ Registration failed:', error.message); + console.error(''); + process.exit(1); + } +} + +main(); diff --git a/filwizard/scripts/warm-add.js b/filwizard/scripts/warm-add.js new file mode 100755 index 0000000..36e06c8 --- /dev/null +++ b/filwizard/scripts/warm-add.js @@ -0,0 +1,197 @@ +#!/usr/bin/env node + +import { ethers } from 'ethers'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Parse command line arguments +function parseArgs() { + const args = process.argv.slice(2); + const parsed = {}; + + for (let i = 0; i < args.length; i++) { + if (args[i].startsWith('--')) { + const key = args[i].substring(2); + const value = args[i + 1]; + parsed[key] = value; + i++; + } + } + + return parsed; +} + +const args = parseArgs(); +const RPC_URL = args['rpc-url'] || process.env.RPC_URL; +const FWSS_ADDRESS = args['warm-storage'] || process.env.FWSS_PROXY_ADDRESS; +const SP_REGISTRY_ADDRESS = args['sp-registry'] || process.env.SERVICE_PROVIDER_REGISTRY_PROXY_ADDRESS; + +// Environment variables +const DEPLOYER_PRIVATE_KEY = process.env.DEPLOYER_PRIVATE_KEY; +const SP_PRIVATE_KEY = process.env.SP_PRIVATE_KEY; + +console.log('ℹ️ 🔥 Adding service provider to warm storage global whitelist'); +console.log(`ℹ️ RPC: ${RPC_URL}`); +console.log(`ℹ️ Warm Storage: ${FWSS_ADDRESS}`); +console.log(`ℹ️ SP Registry: ${SP_REGISTRY_ADDRESS}`); + +// Load ABIs +const WORKSPACE_PATH = '/opt/filwizard/workspace'; +const registryAbiPath = path.join(WORKSPACE_PATH, 'filecoinwarmstorage', 'service_contracts', 'abi', 'ServiceProviderRegistry.abi.json'); +const fwssAbiPath = path.join(WORKSPACE_PATH, 'filecoinwarmstorage', 'service_contracts', 'abi', 'FilecoinWarmStorageService.abi.json'); + +let registryAbi, fwssAbi; +try { + registryAbi = JSON.parse(fs.readFileSync(registryAbiPath, 'utf8')); +} catch (error) { + console.error('❌ Failed to load ServiceProviderRegistry ABI:', error.message); + console.error('❌ Path:', registryAbiPath); + process.exit(1); +} + +try { + fwssAbi = JSON.parse(fs.readFileSync(fwssAbiPath, 'utf8')); +} catch (error) { + console.error('❌ Failed to load FWSS ABI:', error.message); + console.error('❌ Path:', fwssAbiPath); + + // Try to list available ABI files for debugging + try { + const abiDir = path.join(WORKSPACE_PATH, 'filecoinwarmstorage', 'service_contracts', 'abi'); + const files = fs.readdirSync(abiDir); + console.error('❌ Available ABI files:', files.join(', ')); + } catch (listError) { + console.error('❌ Could not list ABI directory'); + } + + process.exit(1); +} + +// Setup provider and wallets +const provider = new ethers.JsonRpcProvider(RPC_URL); +const deployerWallet = new ethers.Wallet(DEPLOYER_PRIVATE_KEY, provider); +const spWallet = new ethers.Wallet(SP_PRIVATE_KEY, provider); + +console.log(`ℹ️ Deployer (Owner): ${deployerWallet.address}`); +console.log(`ℹ️ Service Provider: ${spWallet.address}`); +console.log('ℹ️ '); + +// Create contract instances (use deployer wallet - owner only operation) +const registry = new ethers.Contract(SP_REGISTRY_ADDRESS, registryAbi, deployerWallet); +const fwss = new ethers.Contract(FWSS_ADDRESS, fwssAbi, deployerWallet); + +async function addApprovedProvider() { + console.log('📋 Step 1: Getting Provider ID from Registry'); + + // Get provider ID from registry + let providerId; + try { + providerId = await registry.addressToProviderId(spWallet.address); + + if (providerId === 0n) { + console.error('❌ Service provider not found in registry. Please register first.'); + process.exit(1); + } + + console.log(`✅ Found provider ID: ${providerId}`); + } catch (error) { + console.error('❌ Failed to get provider ID:', error.message); + throw error; + } + + console.log(''); + console.log('📋 Step 2: Checking if Provider Already in Global Whitelist'); + + // Check if already approved globally + try { + const isApproved = await fwss.isProviderApproved(providerId); + + if (isApproved) { + console.log(`✅ Provider ${providerId} is already in the global approved list`); + return providerId; + } + + console.log(`ℹ️ Provider not yet in global whitelist`); + } catch (error) { + // If check fails, continue with approval + console.log(`ℹ️ Could not check approval status, continuing...`); + } + + console.log(''); + console.log('📋 Step 3: Adding Provider to Global Whitelist (Owner Operation)'); + + try { + console.log(`ℹ️ Adding provider ${providerId} to global whitelist...`); + + const tx = await fwss.addApprovedProvider(providerId); + + console.log(`ℹ️ Transaction submitted: ${tx.hash}`); + console.log('ℹ️ Waiting for confirmation...'); + + const receipt = await tx.wait(); + console.log(`✅ Transaction confirmed in block ${receipt.blockNumber}`); + + // Extract ProviderApproved event + const event = receipt.logs + .map(log => { + try { + return fwss.interface.parseLog(log); + } catch { + return null; + } + }) + .find(e => e && e.name === 'ProviderApproved'); + + if (event) { + console.log(`✅ Provider approved event emitted:`); + console.log(` Provider ID: ${event.args.providerId || providerId}`); + } else { + console.log('✅ Provider added to global whitelist successfully'); + } + + return providerId; + + } catch (error) { + console.error('❌ Failed to add provider to global whitelist:', error.message); + + // Try to decode revert reason + if (error.data) { + try { + const decodedError = fwss.interface.parseError(error.data); + console.error('❌ Revert reason:', decodedError.name, decodedError.args); + } catch { + console.error('❌ Raw error data:', error.data); + } + } + + throw error; + } +} + +// Main execution +async function main() { + try { + const providerId = await addApprovedProvider(); + + console.log(''); + console.log('✅ Service provider added to global whitelist!'); + console.log(` Provider ID: ${providerId}`); + console.log(` SP Address: ${spWallet.address}`); + console.log(` Warm Storage: ${FWSS_ADDRESS}`); + console.log(` Status: Available for all dapps and clients`); + console.log(''); + + process.exit(0); + } catch (error) { + console.error(''); + console.error('❌ Failed to add provider to global whitelist:', error.message); + console.error(''); + process.exit(1); + } +} + +main();