diff --git a/.github/workflows/backend-relayer-e2e.yml b/.github/workflows/backend-relayer-e2e.yml new file mode 100644 index 0000000..5b71731 --- /dev/null +++ b/.github/workflows/backend-relayer-e2e.yml @@ -0,0 +1,155 @@ +name: Backend Relayer E2E + +on: + push: + branches: [main] + paths: + - "apps/backend-relayer/**" + - "contracts/**" + - "proof_circuits/**" + - "scripts/**" + - "packages/**" + - "package.json" + - "pnpm-workspace.yaml" + - "pnpm-lock.yaml" + - ".github/workflows/backend-relayer-e2e.yml" + pull_request: + paths: + - "apps/backend-relayer/**" + - "contracts/**" + - "proof_circuits/**" + - "scripts/**" + - "packages/**" + - "package.json" + - "pnpm-workspace.yaml" + - "pnpm-lock.yaml" + - ".github/workflows/backend-relayer-e2e.yml" + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + SNAPSHOT_PATH: ${{ github.workspace }}/scripts/relayer-e2e/deployed.json + COMPOSE_FILE: apps/backend-relayer/docker-compose.e2e.yaml + COMPOSE_PROJECT_NAME: relayer-e2e + HOST_DATABASE_URL: postgresql://relayer:relayer@localhost:5433/relayer + RELAYER_URL: http://localhost:2005 + STELLAR_CHAIN_ID: "1000001" + EVM_CHAIN_ID: "31337" + +jobs: + backend-relayer-e2e: + name: Backend Relayer E2E + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + + - name: Install wasm32v1-none target + run: rustup target add wasm32v1-none + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Install Stellar CLI + env: + STELLAR_CLI_VERSION: "23.3.0" + STELLAR_CLI_SHA256: "b3a5455d7113a53a8bd0f1ba0148a14b7aa7a46ee2f49c3b9277424775b309ad" + run: | + set -euo pipefail + url="https://github.com/stellar/stellar-cli/releases/download/v${STELLAR_CLI_VERSION}/stellar-cli-${STELLAR_CLI_VERSION}-x86_64-unknown-linux-gnu.tar.gz" + curl -fsSL "$url" -o /tmp/stellar-cli.tar.gz + echo "${STELLAR_CLI_SHA256} /tmp/stellar-cli.tar.gz" | sha256sum -c - + mkdir -p "$HOME/.local/bin" + tar -xzf /tmp/stellar-cli.tar.gz -C "$HOME/.local/bin" stellar + chmod +x "$HOME/.local/bin/stellar" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + - name: Install Node dependencies + run: pnpm install --frozen-lockfile + + - name: Cache cargo directories + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + contracts/stellar/target + key: ${{ runner.os }}-cargo-relayer-e2e-${{ hashFiles('contracts/stellar/Cargo.lock') }} + + - name: Generate Prisma client + run: pnpm --filter backend-relayer exec prisma generate + + # ── 1. chains ──────────────────────────────────────────────────── + - name: Start chains (Stellar + Anvil) and build contracts + run: bash scripts/start_chains.sh + + - name: Export chain env + run: | + # .chains.env uses `export KEY='value'`; $GITHUB_ENV expects KEY=value. + sed -E "s/^export[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)='(.*)'$/\1=\2/" .chains.env >> "$GITHUB_ENV" + + # ── 2. deploy ──────────────────────────────────────────────────── + - name: Deploy contracts + run: pnpm --filter relayer-e2e exec tsx cli.ts deploy --out "$SNAPSHOT_PATH" + + # ── 3. postgres + backend-relayer containers ───────────────────── + - name: Start postgres + backend-relayer + env: + STELLAR_NETWORK_PASSPHRASE: "Standalone Network ; February 2017" + run: docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" up -d --build --wait + + # ── 4. seed ────────────────────────────────────────────────────── + - name: Seed database + run: DATABASE_URL="$HOST_DATABASE_URL" pnpm --filter relayer-e2e exec tsx cli.ts seed --in "$SNAPSHOT_PATH" + + # ── 5. flows ───────────────────────────────────────────────────── + - name: Run lifecycle flows + run: pnpm --filter relayer-e2e exec tsx cli.ts flows + + # ── teardown ───────────────────────────────────────────────────── + - name: Dump relayer logs + if: always() + run: | + echo "::group::backend-relayer logs" + docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" logs --no-color --tail=500 backend-relayer | tee .relayer.log || true + echo "::endgroup::" + echo "::group::postgres logs" + docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" logs --no-color --tail=200 postgres | tee .postgres.log || true + echo "::endgroup::" + echo "::group::container state" + docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" ps -a || true + echo "::endgroup::" + + - name: Stop containers + if: always() + run: docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" down --remove-orphans --volumes || true + + - name: Stop chains + if: always() + run: bash scripts/stop_chains.sh || true + + - name: Upload logs + snapshot + if: failure() + uses: actions/upload-artifact@v4 + with: + name: relayer-logs + path: | + .relayer.log + .postgres.log + .chains.anvil.log + scripts/relayer-e2e/deployed.json + if-no-files-found: ignore diff --git a/.github/workflows/backend-relayer-tests.yml b/.github/workflows/backend-relayer-tests.yml new file mode 100644 index 0000000..420d520 --- /dev/null +++ b/.github/workflows/backend-relayer-tests.yml @@ -0,0 +1,74 @@ +name: Backend Relayer Tests + +on: + push: + branches: [main] + paths: + - "apps/backend-relayer/**" + - "packages/**" + - "package.json" + - "pnpm-workspace.yaml" + - "pnpm-lock.yaml" + - ".github/workflows/backend-relayer-tests.yml" + pull_request: + paths: + - "apps/backend-relayer/**" + - "packages/**" + - "package.json" + - "pnpm-workspace.yaml" + - "pnpm-lock.yaml" + - ".github/workflows/backend-relayer-tests.yml" + workflow_dispatch: + +jobs: + unit: + name: Unit tests + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + - name: Install Node dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Prisma client + run: pnpm --filter backend-relayer exec prisma generate + + - name: Run unit tests + run: pnpm --filter backend-relayer test + + e2e: + name: Jest e2e tests + runs-on: ubuntu-latest + timeout-minutes: 25 + + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + - name: Install Node dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Prisma client + run: pnpm --filter backend-relayer exec prisma generate + + - name: Run Jest e2e tests + run: pnpm --filter backend-relayer test:e2e diff --git a/.github/workflows/cross-chain-e2e.yml b/.github/workflows/cross-chain-e2e.yml index 967fa5b..23f18de 100644 --- a/.github/workflows/cross-chain-e2e.yml +++ b/.github/workflows/cross-chain-e2e.yml @@ -67,5 +67,12 @@ jobs: contracts/stellar/target key: ${{ runner.os }}-cargo-e2e-${{ hashFiles('contracts/stellar/Cargo.lock') }} + - name: Start chains (Stellar + Anvil) and build contracts + run: bash scripts/start_chains.sh + - name: Run cross-chain E2E test - run: bash scripts/run_cross_chain_e2e.sh + run: pnpm --filter cross-chain-e2e exec tsx run.ts + + - name: Stop chains + if: always() + run: bash scripts/stop_chains.sh diff --git a/apps/backend-relayer/.env.example b/apps/backend-relayer/.env.example index df6278a..cb26e61 100644 --- a/apps/backend-relayer/.env.example +++ b/apps/backend-relayer/.env.example @@ -1,16 +1,16 @@ -ADMIN_SECRET="" -DATABASE_URL="" -EVM_RPC_API_KEY="" -JWT_EXPIRY="" -JWT_REFRESH_EXPIRTY="" -JWT_SECRET="" -NODE_ENV="" -PORT="" -SECRET_KEY="" +EVM_ADMIN_PRIVATE_KEY= +DATABASE_URL= +EVM_RPC_API_KEY= +JWT_EXPIRY= +JWT_REFRESH_EXPIRY= +JWT_SECRET= +NODE_ENV= +PORT= +SECRET_KEY= SIGN_DOMAIN=proof-bridge.vercel.app SIGN_URI=https://proof-bridge.vercel.app # Stellar SEP-10 server signing key. Generate with: Keypair.random().secret() -STELLAR_AUTH_SECRET="" -STELLAR_RPC_URL="" -STELLAR_NETWORK_PASSPHRASE="" -STELLAR_ADMIN_SECRET="" \ No newline at end of file +STELLAR_AUTH_SECRET= +STELLAR_RPC_URL= +STELLAR_NETWORK_PASSPHRASE= +STELLAR_ADMIN_SECRET= diff --git a/apps/backend-relayer/Dockerfile b/apps/backend-relayer/Dockerfile new file mode 100644 index 0000000..efc8eaa --- /dev/null +++ b/apps/backend-relayer/Dockerfile @@ -0,0 +1,68 @@ +# syntax=docker/dockerfile:1.7 + +# ----------------------------------------------------------------------------- +# Stage 1 — builder +# ----------------------------------------------------------------------------- +FROM node:20-slim AS builder + +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN corepack enable && corepack prepare pnpm@10.10.0 --activate + +WORKDIR /repo + +# Copy workspace manifests first for better cache hits. +COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ +COPY apps/backend-relayer/package.json apps/backend-relayer/ +COPY packages/proofbridge_mmr/package.json packages/proofbridge_mmr/ + +# Prefetch all workspace deps once. +RUN pnpm fetch --ignore-scripts + +# Now pull in the sources we need to build. +COPY apps/backend-relayer apps/backend-relayer +COPY packages/proofbridge_mmr packages/proofbridge_mmr + +# Install & build the relayer (and only the relayer — avoid compiling every +# workspace package). +RUN pnpm install --frozen-lockfile --offline \ + && pnpm --filter backend-relayer exec prisma generate \ + && pnpm --filter backend-relayer build + +# `pnpm deploy` only copies files listed in the package's `files` field, so +# copy dist/ and prisma/ explicitly, then re-run prisma generate inside /out +# so .prisma/client lands in the final node_modules. +RUN pnpm --filter backend-relayer deploy --prod --legacy /out \ + && cp -r apps/backend-relayer/dist /out/dist \ + && cp -r apps/backend-relayer/prisma /out/prisma \ + && cd /out && npx prisma generate --schema=prisma/schema.prisma + +# ----------------------------------------------------------------------------- +# Stage 2 — runtime +# ----------------------------------------------------------------------------- +FROM node:20-slim AS runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates tini \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY --from=builder /out/package.json ./package.json +COPY --from=builder /out/node_modules ./node_modules +COPY --from=builder /out/dist ./dist +COPY --from=builder /out/prisma ./prisma + +RUN chown -R node:node /app +USER node + +ENV NODE_ENV=production +ENV PORT=2005 +EXPOSE 2005 + +# `tini` keeps signal forwarding sane. Migrations run on every boot — idempotent +# for an already-migrated DB. +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["sh", "-c", "npx prisma migrate deploy && node dist/src/main.js"] diff --git a/apps/backend-relayer/README.md b/apps/backend-relayer/README.md index 1da1f30..8e85f53 100644 --- a/apps/backend-relayer/README.md +++ b/apps/backend-relayer/README.md @@ -282,7 +282,8 @@ pnpm run start:prod **Core Configuration**: ```bash -ADMIN_SECRET="" +EVM_ADMIN_PRIVATE_KEY="" +STELLAR_ADMIN_SECRET="" DATABASE_URL="" EVM_RPC_API_KEY="" JWT_EXPIRY="" diff --git a/apps/backend-relayer/docker-compose.e2e.yaml b/apps/backend-relayer/docker-compose.e2e.yaml new file mode 100644 index 0000000..e37a620 --- /dev/null +++ b/apps/backend-relayer/docker-compose.e2e.yaml @@ -0,0 +1,47 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: relayer + POSTGRES_PASSWORD: relayer + POSTGRES_DB: relayer + ports: + - "5433:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U relayer -d relayer"] + interval: 2s + timeout: 3s + retries: 30 + + backend-relayer: + build: + context: ../.. + dockerfile: apps/backend-relayer/Dockerfile + depends_on: + postgres: + condition: service_healthy + environment: + NODE_ENV: production + PORT: 2005 + DATABASE_URL: postgresql://relayer:relayer@postgres:5432/relayer + JWT_ACCESS_SECRET: test-access-secret + JWT_REFRESH_SECRET: test-refresh-secret + SIGN_DOMAIN: proofbridge.xyz + SIGN_URI: https://proofbridge.xyz + STELLAR_AUTH_SECRET: ${STELLAR_AUTH_SECRET:-SA3C2KPR5TCHYJ5TNQXAY2776Z3H4CB723GDCAMEX5I2NLWP25QUYB3X} + SECRET_KEY: ${SECRET_KEY:-0xfdba5a242ddce02cd1d585297aa4afe5aa2831391198746c680a3e16a41676dc} + # Chain RPCs live on the host — rewrite localhost → host.docker.internal. + ETHEREUM_RPC_URL: http://host.docker.internal:9545 + STELLAR_RPC_URL: http://host.docker.internal:8000/soroban/rpc + STELLAR_NETWORK_PASSPHRASE: ${STELLAR_NETWORK_PASSPHRASE:-Standalone Network ; February 2017} + STELLAR_ADMIN_SECRET: ${STELLAR_ADMIN_SECRET} + EVM_ADMIN_PRIVATE_KEY: ${EVM_ADMIN_PRIVATE_KEY} + extra_hosts: + - "host.docker.internal:host-gateway" + ports: + - "2005:2005" + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:2005/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 3s + timeout: 5s + retries: 40 diff --git a/apps/backend-relayer/package.json b/apps/backend-relayer/package.json index 9082ad2..76e1c9b 100644 --- a/apps/backend-relayer/package.json +++ b/apps/backend-relayer/package.json @@ -11,14 +11,13 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node dist/src/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/e2e/jest-e2e.json", - "test:integrations": "jest --config ./test/integrations/jest-e2e.json", "prisma:migrate": "prisma migrate deploy" }, "dependencies": { @@ -35,6 +34,7 @@ "@node-rs/argon2": "^2.0.2", "@noir-lang/noir_js": "1.0.0-beta.9", "@prisma/client": "6.16.1", + "prisma": "6.16.1", "@stellar/stellar-sdk": "^15.0.1", "@types/morgan": "^1.9.10", "@zkpassport/poseidon2": "^0.6.2", @@ -46,6 +46,9 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "date-fns": "^4.1.0", + "dotenv": "^17.2.2", + "ethers": "^6.15.0", + "express": "^5.1.0", "level": "^10.0.0", "morgan": "^1.10.1", "nest-winston": "^1.10.2", @@ -53,6 +56,7 @@ "rave-level": "^1.0.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "siwe": "^3.0.0", "unique-names-generator": "^4.7.1", "uuid": "^13.0.0", "viem": "^2.37.7", @@ -69,17 +73,13 @@ "@types/jest": "^30.0.0", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", - "dotenv": "^17.2.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2", - "ethers": "^6.15.0", "execa": "^9.6.0", "globals": "^16.0.0", "jest": "^30.0.0", "prettier": "^3.4.2", - "prisma": "6.16.1", - "siwe": "^3.0.0", "source-map-support": "^0.5.21", "supertest": "^7.1.4", "ts-jest": "^29.2.5", diff --git a/apps/backend-relayer/src/libs/configs.ts b/apps/backend-relayer/src/libs/configs.ts index a6ed405..c8093f5 100644 --- a/apps/backend-relayer/src/libs/configs.ts +++ b/apps/backend-relayer/src/libs/configs.ts @@ -18,7 +18,7 @@ export const env = { port: process.env.PORT || 9090, appDomain: process.env.SIGN_DOMAIN || 'proofbridge.xyz', appUri: process.env.SIGN_URI || 'https://proofbridge.xyz', - admin: process.env.ADMIN_SECRET || '', + admin: process.env.EVM_ADMIN_PRIVATE_KEY || '', secretKey: process.env.SECRET_KEY || '32_byte_secret_key_for_aes!', evmRpcApiKey: process.env.EVM_RPC_API_KEY || '', rpcUrlHedera: process.env.RPC_URL_HEDERA || '', diff --git a/apps/backend-relayer/src/modules/ads/ad.service.ts b/apps/backend-relayer/src/modules/ads/ad.service.ts index dca9b0f..b80cf21 100644 --- a/apps/backend-relayer/src/modules/ads/ad.service.ts +++ b/apps/backend-relayer/src/modules/ads/ad.service.ts @@ -16,13 +16,37 @@ import { ConfirmAdActionDto, CloseAdDto, } from './dto/ad.dto'; -import { getAddress } from 'viem'; -import { AdStatus, Prisma } from '@prisma/client'; +import { AdStatus, ChainKind, Prisma } from '@prisma/client'; import { Request } from 'express'; import { ChainAdapterService } from '../../chain-adapters/chain-adapter.service'; -import { toBytes32 } from '../../providers/viem/ethers/typedData'; +import { + normalizeChainAddress, + toBytes32, +} from '../../providers/viem/ethers/typedData'; import { randomUUID } from 'crypto'; +// Caller-owns-ad check bound to the ad's chain kind. +function assertCallerOwnsAd( + walletAddress: string, + ad: { + creatorAddress: string; + route: { adToken: { chain: { kind: ChainKind } } }; + }, +): void { + let normalized: string; + try { + normalized = normalizeChainAddress( + walletAddress, + ad.route.adToken.chain.kind, + ); + } catch { + throw new ForbiddenException('Unauthorized'); + } + if (normalized !== ad.creatorAddress) { + throw new ForbiddenException('Unauthorized'); + } +} + type AdQueryInput = { routeId?: string; creatorAddress?: string; @@ -343,7 +367,7 @@ export class AdsService { select: { id: true, symbol: true, - chain: { select: { chainId: true } }, + chain: { select: { chainId: true, kind: true } }, }, }, }, @@ -387,6 +411,18 @@ export class AdsService { ); } + // creatorDstAddress lives on the order chain (where the ad creator + // receives proceeds on unlock), so validate against that chain's kind. + let normalizedCreatorDst: string; + try { + normalizedCreatorDst = normalizeChainAddress( + dto.creatorDstAddress, + route.orderToken.chain.kind, + ); + } catch { + throw new BadRequestException('Invalid creatorDstAddress'); + } + const adId = randomUUID(); const reqContractDetails = await this.chainAdapters @@ -399,15 +435,27 @@ export class AdsService { orderChainId: route.orderToken.chain.chainId, adToken: route.adToken.address as `0x${string}`, initialAmount: fundAmount.toFixed(0), - adRecipient: toBytes32(dto.creatorDstAddress), + adRecipient: toBytes32(normalizedCreatorDst), }); + let normalizedCreator: string; + try { + normalizedCreator = normalizeChainAddress( + user.walletAddress, + route.adToken.chain.kind, + ); + } catch { + throw new BadRequestException( + 'Authenticated wallet does not match ad chain', + ); + } + const requestDetails = await this.prisma.$transaction(async (prisma) => { const ad = await prisma.ad.create({ data: { id: adId, - creatorAddress: getAddress(user.walletAddress), - creatorDstAddress: getAddress(dto.creatorDstAddress), + creatorAddress: normalizedCreator, + creatorDstAddress: normalizedCreatorDst, routeId: route.id, adTokenId: route.adToken.id, orderTokenId: route.orderToken.id, @@ -482,7 +530,7 @@ export class AdsService { if (!user) throw new ForbiddenException('Unauthorized'); const ad = await this.prisma.ad.findUnique({ - where: { id, creatorAddress: getAddress(user.walletAddress) }, + where: { id }, select: { id: true, creatorAddress: true, @@ -509,6 +557,8 @@ export class AdsService { if (!ad) throw new NotFoundException('Ad not found'); + assertCallerOwnsAd(user.walletAddress, ad); + if (ad.adUpdateLog) { throw new BadRequestException( 'Ad has pending update; please wait a few minutes and try again', @@ -601,7 +651,7 @@ export class AdsService { if (!user) throw new ForbiddenException('Unauthorized'); const ad = await this.prisma.ad.findUnique({ - where: { id, creatorAddress: getAddress(user.walletAddress) }, + where: { id }, select: { id: true, creatorAddress: true, @@ -628,6 +678,8 @@ export class AdsService { if (!ad) throw new NotFoundException('Ad not found'); + assertCallerOwnsAd(user.walletAddress, ad); + if (ad.adUpdateLog) { throw new BadRequestException( 'Ad has pending update; please wait a few minutes and try again', @@ -648,8 +700,6 @@ export class AdsService { const locked = lockSum._sum.amount ?? new Prisma.Decimal(0); const available = ad.poolAmount.sub(locked); - console.log(available, withdrawAmt); - if (withdrawAmt.gt(available)) throw new BadRequestException('Insufficient available balance'); @@ -657,6 +707,16 @@ export class AdsService { ? 'EXHAUSTED' : ad.status; + let normalizedTo: string; + try { + normalizedTo = normalizeChainAddress( + dto.to, + ad.route.adToken.chain.kind, + ); + } catch { + throw new BadRequestException('Invalid withdraw destination'); + } + const reqContractDetails = await this.chainAdapters .forChain(ad.route.adToken.chain.kind) .getWithdrawFromAdRequestContractDetails({ @@ -665,7 +725,7 @@ export class AdsService { adChainId: ad.route.adToken.chain.chainId, adId: ad.id, amount: withdrawAmt.toFixed(0), - to: dto.to as `0x${string}`, + to: normalizedTo as `0x${string}`, }); const finalValue = ad.poolAmount.sub(withdrawAmt); @@ -812,7 +872,7 @@ export class AdsService { if (!user) throw new ForbiddenException('Unauthorized'); const ad = await this.prisma.ad.findFirst({ - where: { id, creatorAddress: getAddress(user.walletAddress) }, + where: { id }, select: { id: true, creatorAddress: true, @@ -838,6 +898,8 @@ export class AdsService { }); if (!ad) throw new NotFoundException('Ad not found'); + assertCallerOwnsAd(user.walletAddress, ad); + if (ad.adUpdateLog) { throw new BadRequestException( 'Ad has pending update; please wait a few minutes and try again', @@ -861,6 +923,16 @@ export class AdsService { throw new BadRequestException('Ad is already closed'); } + let normalizedTo: string; + try { + normalizedTo = normalizeChainAddress( + dto.to, + ad.route.adToken.chain.kind, + ); + } catch { + throw new BadRequestException('Invalid close destination'); + } + const reqContractDetails = await this.chainAdapters .forChain(ad.route.adToken.chain.kind) .getCloseAdRequestContractDetails({ @@ -868,7 +940,7 @@ export class AdsService { .adManagerAddress as `0x${string}`, adChainId: ad.route.adToken.chain.chainId, adId: ad.id, - to: dto.to as `0x${string}`, + to: normalizedTo as `0x${string}`, }); await this.prisma.$transaction(async (prisma) => { @@ -926,7 +998,8 @@ export class AdsService { async confirmChainAction( req: Request, adId: string, - dto: ConfirmAdActionDto, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _dto: ConfirmAdActionDto, ) { try { const reqUser = req.user; @@ -941,43 +1014,35 @@ export class AdsService { const adLogUpdate = await this.prisma.adUpdateLog.findUnique({ where: { adId }, - include: { ad: true, log: true }, - }); - - if (!adLogUpdate) throw new NotFoundException('Ad update log not found'); - - if ( - getAddress(adLogUpdate.ad.creatorAddress) !== - getAddress(user.walletAddress) - ) { - throw new ForbiddenException('Unauthorized'); - } - - // get ad details - const ad = await this.prisma.ad.findUnique({ - where: { id: adId }, - select: { - poolAmount: true, - status: true, - route: { - select: { - adToken: { + include: { + ad: { + include: { + route: { select: { - chain: { + adToken: { select: { - adManagerAddress: true, - chainId: true, - kind: true, + chain: { + select: { + adManagerAddress: true, + chainId: true, + kind: true, + }, + }, }, }, }, }, }, }, + log: true, }, }); - if (!ad) throw new NotFoundException('Ad for Ad Id not found'); + if (!adLogUpdate) throw new NotFoundException('Ad update log not found'); + + const ad = adLogUpdate.ad; + + assertCallerOwnsAd(user.walletAddress, ad); // // verify adLog const isValidated = await this.chainAdapters @@ -1015,8 +1080,6 @@ export class AdsService { this.prisma.adUpdateLog.delete({ where: { id: adLogUpdate.id } }), ]); - console.log(dto); - return { adId: adId, success: true, diff --git a/apps/backend-relayer/src/modules/ads/dto/ad.dto.ts b/apps/backend-relayer/src/modules/ads/dto/ad.dto.ts index 9bbb089..ed04d09 100644 --- a/apps/backend-relayer/src/modules/ads/dto/ad.dto.ts +++ b/apps/backend-relayer/src/modules/ads/dto/ad.dto.ts @@ -1,4 +1,11 @@ -import { IsIn, IsInt, IsOptional, IsString, IsUUID, Matches } from 'class-validator'; +import { + IsIn, + IsInt, + IsOptional, + IsString, + IsUUID, + Matches, +} from 'class-validator'; import { Transform } from 'class-transformer'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { JsonObject, JsonArray } from '@prisma/client/runtime/library'; @@ -205,7 +212,7 @@ export class CloseAdDto { example: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) @IsString() - to: string; + to!: string; } export class ConfirmAdActionDto { @@ -326,7 +333,7 @@ export class ListAdResponseDto { data!: AdResponseDto[]; @ApiProperty({ type: String, nullable: true }) - nextCursor: string | null; + nextCursor!: string | null; } export class CreateAdResponseDto { @@ -349,6 +356,12 @@ export class CreateAdResponseDto { }) signature!: `0x${string}`; + @ApiPropertyOptional({ + description: + 'Signer public key (Stellar chains only — 0x-prefixed 32-byte hex of the relayer signer)', + }) + signerPublicKey?: string; + @ApiProperty({ description: 'Request auth token', example: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', diff --git a/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.spec.ts b/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.spec.ts index bd1d225..3449c1b 100644 --- a/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.spec.ts +++ b/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.spec.ts @@ -51,11 +51,11 @@ describe('StellarAuthService (SEP-10)', () => { describe('verifyLogin', () => { const sign = (xdr: string, kp: Keypair) => { - const tx = TransactionBuilder.fromXDR( - xdr, - (process.env.STELLAR_NETWORK_PASSPHRASE as Networks) ?? - Networks.TESTNET, - ); + // Match the service's fallback: configs.ts uses `||` so an empty env + // var falls back to TESTNET. `??` would keep `""` and mismatch. + const passphrase = + (process.env.STELLAR_NETWORK_PASSPHRASE as Networks) || Networks.TESTNET; + const tx = TransactionBuilder.fromXDR(xdr, passphrase); tx.sign(kp); return tx.toEnvelope().toXDR('base64'); }; diff --git a/apps/backend-relayer/src/modules/trades/trade.service.ts b/apps/backend-relayer/src/modules/trades/trade.service.ts index 969d64f..04d82e2 100644 --- a/apps/backend-relayer/src/modules/trades/trade.service.ts +++ b/apps/backend-relayer/src/modules/trades/trade.service.ts @@ -14,7 +14,6 @@ import { QueryTradesDto, UnlockTradeDto, } from './dto/trade.dto'; -import { getAddress, isAddress } from 'viem'; import { Request } from 'express'; import { ChainAdapterService } from '../../chain-adapters/chain-adapter.service'; import { MMRService } from '../mmr/mmr.service'; @@ -22,7 +21,11 @@ import { ProofService } from '../../providers/noir/proof.service'; import { randomUUID } from 'crypto'; import { Prisma, TradeStatus } from '@prisma/client'; import { EncryptionService } from '@libs/encryption.service'; -import { toBytes32, uuidToBigInt } from '../../providers/viem/ethers/typedData'; +import { + normalizeChainAddress, + toBytes32, + uuidToBigInt, +} from '../../providers/viem/ethers/typedData'; @Injectable() export class TradesService { @@ -138,9 +141,14 @@ export class TradesService { if (q.routeId) where.routeId = q.routeId; if (q.adId) where.adId = q.adId; - if (q.adCreatorAddress) - where.adCreatorAddress = getAddress(q.adCreatorAddress); - if (q.bridgerAddress) where.bridgerAddress = getAddress(q.bridgerAddress); + try { + if (q.adCreatorAddress) + where.adCreatorAddress = normalizeChainAddress(q.adCreatorAddress); + if (q.bridgerAddress) + where.bridgerAddress = normalizeChainAddress(q.bridgerAddress); + } catch { + throw new BadRequestException('Invalid address filter'); + } if (q.adTokenId || q.orderTokenId) { where.route = { @@ -274,10 +282,6 @@ export class TradesService { if (!user) throw new UnauthorizedException('Unauthorized'); - if (!isAddress(dto.bridgerDstAddress)) { - throw new BadRequestException('Invalid address'); - } - const ad = await this.prisma.ad .findUnique({ where: { id: dto.adId }, @@ -305,6 +309,7 @@ export class TradesService { select: { adManagerAddress: true, chainId: true, + kind: true, }, }, }, @@ -346,6 +351,18 @@ export class TradesService { ); } + // bridgerDstAddress lives on the ad chain (where the bridger receives + // the locked tokens), so validate against that chain's kind. + let normalizedBridgerDst: string; + try { + normalizedBridgerDst = normalizeChainAddress( + dto.bridgerDstAddress, + ad.route.adToken.chain.kind, + ); + } catch { + throw new BadRequestException('Invalid bridgerDstAddress'); + } + const amount = new Prisma.Decimal(dto.amount); if (ad.minAmount && amount.lt(ad.minAmount)) { throw new BadRequestException('Amount below minAmount'); @@ -382,7 +399,7 @@ export class TradesService { orderPortal: toBytes32( ad.route.orderToken.chain.orderPortalAddress, ), - orderRecipient: toBytes32(dto.bridgerDstAddress), + orderRecipient: toBytes32(normalizedBridgerDst), adChainId: ad.route.adToken.chain.chainId.toString(), adManager: toBytes32(ad.route.adToken.chain.adManagerAddress), adId: ad.id, @@ -400,10 +417,10 @@ export class TradesService { adId: ad.id, routeId: ad.route.id, amount: amount.toFixed(0), - adCreatorAddress: getAddress(ad.creatorAddress), - adCreatorDstAddress: getAddress(ad.creatorDstAddress), - bridgerAddress: getAddress(user.walletAddress), - bridgerDstAddress: getAddress(dto.bridgerDstAddress), + adCreatorAddress: normalizeChainAddress(ad.creatorAddress), + adCreatorDstAddress: normalizeChainAddress(ad.creatorDstAddress), + bridgerAddress: normalizeChainAddress(user.walletAddress), + bridgerDstAddress: normalizedBridgerDst, orderHash: reqContractDetails.orderHash, }, select: { id: true, status: true }, @@ -525,27 +542,30 @@ export class TradesService { if (!trade) throw new NotFoundException('Trade not found'); if ( - getAddress(trade.bridgerAddress) !== getAddress(user.walletAddress) && - getAddress(trade.adCreatorAddress) !== getAddress(user.walletAddress) + normalizeChainAddress(trade.bridgerAddress) !== + normalizeChainAddress(user.walletAddress) && + normalizeChainAddress(trade.adCreatorAddress) !== + normalizeChainAddress(user.walletAddress) ) { throw new ForbiddenException('Unauthorized'); } + // All address-like fields are declared bytes32 in the cross-chain + // Order typed-data (EVM addresses left-padded; Stellar accounts already + // 32 bytes), so return the padded wire form here. return { - orderChainToken: getAddress(trade.route.orderToken.address), - adChainToken: getAddress(trade.route.adToken.address), + orderChainToken: toBytes32(trade.route.orderToken.address), + adChainToken: toBytes32(trade.route.adToken.address), amount: trade.amount.toFixed(0), - bridger: getAddress(trade.bridgerAddress), + bridger: toBytes32(trade.bridgerAddress), orderChainId: trade.route.orderToken.chain.chainId.toString(), - orderPortal: getAddress( - trade.route.orderToken.chain.orderPortalAddress, - ), - orderRecipient: getAddress(trade.bridgerDstAddress), + orderPortal: toBytes32(trade.route.orderToken.chain.orderPortalAddress), + orderRecipient: toBytes32(trade.bridgerDstAddress), adChainId: trade.route.adToken.chain.chainId.toString(), - adManager: getAddress(trade.route.adToken.chain.adManagerAddress), + adManager: toBytes32(trade.route.adToken.chain.adManagerAddress), adId: trade.adId, - adCreator: getAddress(trade.adCreatorAddress), - adRecipient: getAddress(trade.adCreatorDstAddress), + adCreator: toBytes32(trade.adCreatorAddress), + adRecipient: toBytes32(trade.adCreatorDstAddress), salt: uuidToBigInt(trade.id).toString(), }; } catch (e) { @@ -581,7 +601,7 @@ export class TradesService { const trade = await this.prisma.trade.findFirst({ where: { id: tradeId, - adCreatorAddress: getAddress(user.walletAddress), + adCreatorAddress: normalizeChainAddress(user.walletAddress), }, select: { id: true, @@ -776,11 +796,11 @@ export class TradesService { if (!trade) throw new NotFoundException('Trade not found'); - const caller = getAddress(user.walletAddress); + const caller = normalizeChainAddress(user.walletAddress); const isParty = - caller === getAddress(trade.bridgerAddress) || - caller === getAddress(trade.adCreatorAddress); + caller === normalizeChainAddress(trade.bridgerAddress) || + caller === normalizeChainAddress(trade.adCreatorAddress); if (!isParty) throw new UnauthorizedException('Not a participant'); @@ -791,16 +811,25 @@ export class TradesService { ); } - const isAdCreator = caller === getAddress(trade.adCreatorAddress); + const isAdCreator = + caller === normalizeChainAddress(trade.adCreatorAddress); const unlockChain = isAdCreator ? trade.route.orderToken.chain : trade.route.adToken.chain; + // The on-chain contract recovers the unlocker's *destination* address on + // the unlock chain from the signature. Caller's walletAddress is their + // origin-chain wallet, which is the wrong format when unlocking cross-chain. + const unlockSigner = normalizeChainAddress( + isAdCreator ? trade.adCreatorDstAddress : trade.bridgerDstAddress, + unlockChain.kind, + ); + const isAuthorized = this.chainAdapters .forChain(unlockChain.kind) .verifyOrderSignature( - caller, + unlockSigner as `0x${string}`, trade.orderHash as `0x${string}`, dto.signature as `0x${string}`, ); @@ -963,10 +992,10 @@ export class TradesService { throw new NotFoundException('Trade update log not found'); if ( - getAddress(tradeLogUpdate.trade.bridgerAddress) !== - getAddress(user.walletAddress) && - getAddress(tradeLogUpdate.trade.adCreatorAddress) !== - getAddress(user.walletAddress) + normalizeChainAddress(tradeLogUpdate.trade.bridgerAddress) !== + normalizeChainAddress(user.walletAddress) && + normalizeChainAddress(tradeLogUpdate.trade.adCreatorAddress) !== + normalizeChainAddress(user.walletAddress) ) { throw new ForbiddenException('Unauthorized'); } @@ -1119,7 +1148,8 @@ export class TradesService { const authorizationLog = await this.prisma.authorizationLog.findFirst({ where: { tradeId: tradeId, - userAddress: getAddress(user.walletAddress), + userAddress: normalizeChainAddress(user.walletAddress), + ...(dto.signature ? { signature: dto.signature } : {}), }, orderBy: { createdAt: 'desc' }, include: { trade: true }, @@ -1129,10 +1159,10 @@ export class TradesService { throw new NotFoundException('Authorization log not found'); if ( - getAddress(authorizationLog.trade.bridgerAddress) !== - getAddress(user.walletAddress) && - getAddress(authorizationLog.trade.adCreatorAddress) !== - getAddress(user.walletAddress) + normalizeChainAddress(authorizationLog.trade.bridgerAddress) !== + normalizeChainAddress(user.walletAddress) && + normalizeChainAddress(authorizationLog.trade.adCreatorAddress) !== + normalizeChainAddress(user.walletAddress) ) { throw new ForbiddenException('Unauthorized'); } @@ -1202,10 +1232,11 @@ export class TradesService { } } - const caller = getAddress(user.walletAddress); + const caller = normalizeChainAddress(user.walletAddress); const isAdCreator = - caller === getAddress(authorizationLog.trade.adCreatorAddress); + caller === + normalizeChainAddress(authorizationLog.trade.adCreatorAddress); const updatedTrade = await this.prisma.trade.update({ where: { id: authorizationLog.tradeId }, diff --git a/apps/backend-relayer/src/providers/stellar/stellar.service.ts b/apps/backend-relayer/src/providers/stellar/stellar.service.ts index 18d830b..64f2f47 100644 --- a/apps/backend-relayer/src/providers/stellar/stellar.service.ts +++ b/apps/backend-relayer/src/providers/stellar/stellar.service.ts @@ -117,6 +117,7 @@ export class StellarService { } { const signer = this.getSigner(); const sig = signEd25519(message, signer.seed); + return { signature: `0x${sig.toString('hex')}`, signerPublicKey: `0x${signer.publicKey.toString('hex')}`, @@ -166,6 +167,7 @@ export class StellarService { contractAddress: hex32ToBuffer(adContractAddress), }); const { signature, signerPublicKey } = this.sign(message); + return Promise.resolve({ chainId: adChainId.toString(), contractAddress: adContractAddress, diff --git a/apps/backend-relayer/src/providers/stellar/utils/eip712.ts b/apps/backend-relayer/src/providers/stellar/utils/eip712.ts index 9afd147..0795c9e 100644 --- a/apps/backend-relayer/src/providers/stellar/utils/eip712.ts +++ b/apps/backend-relayer/src/providers/stellar/utils/eip712.ts @@ -6,6 +6,7 @@ import { keccak256 } from 'viem'; import { T_OrderParams } from '../../../chain-adapters/types'; +import { uuidToBigInt } from '../../viem/ethers/typedData'; function keccak(data: Buffer): Buffer { const hex = keccak256(`0x${data.toString('hex')}`); @@ -55,7 +56,10 @@ function structHashOrder(p: T_OrderParams): Buffer { keccak(Buffer.from(p.adId)), hexToBytes32(p.adCreator), hexToBytes32(p.adRecipient), - u256BE(BigInt(p.salt)), + // salt is carried as a UUID string across the API; both EVM typedData + // and the Stellar contracts encode it as a uint256 derived from the + // raw 128-bit UUID. Use uuidToBigInt to match. + u256BE(uuidToBigInt(p.salt)), ]), ); } diff --git a/apps/backend-relayer/src/providers/viem/ethers/localnet.ts b/apps/backend-relayer/src/providers/viem/ethers/localnet.ts index 11c56a3..196b7af 100644 --- a/apps/backend-relayer/src/providers/viem/ethers/localnet.ts +++ b/apps/backend-relayer/src/providers/viem/ethers/localnet.ts @@ -1,5 +1,7 @@ import { Chain, defineChain } from 'viem'; +// Defaults target a host-local anvil/hedera. In containerized e2e the host is +// reachable via `host.docker.internal`, so honor env overrides when present. export const ethLocalnet: Chain = defineChain({ id: 31337, name: 'ETH LOCALNET', @@ -10,7 +12,7 @@ export const ethLocalnet: Chain = defineChain({ }, rpcUrls: { default: { - http: ['http://localhost:9545'], + http: [process.env.ETHEREUM_RPC_URL || 'http://localhost:9545'], }, }, }); @@ -25,7 +27,7 @@ export const hederaLocalnet: Chain = defineChain({ }, rpcUrls: { default: { - http: ['http://localhost:7546'], + http: [process.env.HEDERA_RPC_URL || 'http://localhost:7546'], }, }, }); diff --git a/apps/backend-relayer/src/providers/viem/ethers/typedData.ts b/apps/backend-relayer/src/providers/viem/ethers/typedData.ts index b5f971e..66b742c 100644 --- a/apps/backend-relayer/src/providers/viem/ethers/typedData.ts +++ b/apps/backend-relayer/src/providers/viem/ethers/typedData.ts @@ -1,10 +1,14 @@ import { TypedDataEncoder, Wallet, recoverAddress } from 'ethers'; +import { ChainKind } from '@prisma/client'; +import { StrKey } from '@stellar/stellar-sdk'; +import { getAddress, isAddress } from 'viem'; import { Bytes32Hex, T_AdManagerOrderParams, T_OrderParams, T_OrderPortalParams, } from '../../../chain-adapters/types'; +import { accountIdToHex32 } from '../../stellar/utils/address'; // Left-pad a 20-byte EVM address to 32 bytes (the cross-chain wire format). // Accepts an already-32-byte hex string and returns it unchanged. Throws on @@ -16,6 +20,39 @@ export function toBytes32(value: string): Bytes32Hex { throw new Error(`toBytes32: expected 20- or 32-byte hex, got ${value}`); } +// Chain-aware canonicalization for addresses. Returns the storage-canonical +// form: +// EVM — EIP-55 20-byte hex +// STELLAR — lowercased 0x-prefixed 32-byte hex of the account public key +// +// When `chainKind` is provided, the native form is accepted (EVM 20-byte hex +// or Stellar G-strkey) alongside the 32-byte hex wire form. +// When `chainKind` is omitted, the chain is inferred from the canonical +// stored form: 40 hex chars → EVM, 64 hex chars → Stellar. Use this variant +// for values already read from the DB where the chain is implicit. +// Throws if the value is not a valid address for the given / inferred chain. +export function normalizeChainAddress( + value: string, + chainKind?: ChainKind, +): string { + if (chainKind === ChainKind.EVM) { + if (isAddress(value)) return getAddress(value); + throw new Error(`normalizeChainAddress: invalid EVM address ${value}`); + } + if (chainKind === ChainKind.STELLAR) { + if (StrKey.isValidEd25519PublicKey(value)) return accountIdToHex32(value); + const hex = value.replace(/^0x/i, ''); + if (/^[a-fA-F0-9]{64}$/.test(hex)) return `0x${hex.toLowerCase()}`; + throw new Error(`normalizeChainAddress: invalid Stellar address ${value}`); + } + const hex = value.replace(/^0x/i, ''); + if (hex.length === 40) return getAddress(`0x${hex}`); + if (hex.length === 64) return `0x${hex.toLowerCase()}`; + throw new Error( + `normalizeChainAddress: cannot infer chain from ${value}; pass chainKind`, + ); +} + // ---------------------------- // OrderPortal typed data // ---------------------------- diff --git a/apps/backend-relayer/test/integrations/api.ts b/apps/backend-relayer/test/integrations/api.ts deleted file mode 100644 index c7ae2a2..0000000 --- a/apps/backend-relayer/test/integrations/api.ts +++ /dev/null @@ -1,152 +0,0 @@ -import request from 'supertest'; -import { INestApplication } from '@nestjs/common'; -import { parseEther } from 'viem'; - -export const getRoutes = ( - app: INestApplication, - fromId: string, - toId: string, -) => - request(app.getHttpServer()) - .get('/v1/routes') - .query({ adChainId: fromId, orderChainId: toId }); - -export const apiCreateAd = ( - app: INestApplication, - access: string, - routeId: string, - dst: `0x${string}`, - fundAmount: string, -) => - request(app.getHttpServer()) - .post('/v1/ads/create') - .set('Authorization', `Bearer ${access}`) - .send({ routeId, creatorDstAddress: dst, fundAmount }); - -export const apiConfirm = ( - app: INestApplication, - adId: string, - access: string, - txHash: `0x${string}`, -) => - request(app.getHttpServer()) - .post(`/v1/ads/${adId}/confirm`) - .set('Authorization', `Bearer ${access}`) - .send({ txHash }); - -export const apiFundAd = ( - app: INestApplication, - adId: string, - access: string, - amtEth: string, -) => - request(app.getHttpServer()) - .post(`/v1/ads/${adId}/fund`) - .set('Authorization', `Bearer ${access}`) - .send({ poolAmountTopUp: parseEther(amtEth).toString() }); - -export const apiWithdraw = ( - app: INestApplication, - adId: string, - access: string, - amtEth: string, - to: `0x${string}`, -) => - request(app.getHttpServer()) - .post(`/v1/ads/${adId}/withdraw`) - .set('Authorization', `Bearer ${access}`) - .send({ poolAmountWithdraw: parseEther(amtEth).toString(), to }); - -export const apiUpdateAd = ( - app: INestApplication, - adId: string, - access: string, - body: any, -) => - request(app.getHttpServer()) - .patch(`/v1/ads/${adId}/update`) - .set('Authorization', `Bearer ${access}`) - .send(body); - -export const apiGetAd = (app: INestApplication, adId: string) => - request(app.getHttpServer()).get(`/v1/ads/${adId}`); - -export const apiCloseAd = ( - app: INestApplication, - adId: string, - access: string, - body: any, -) => - request(app.getHttpServer()) - .post(`/v1/ads/${adId}/close`) - .set('Authorization', `Bearer ${access}`) - .send(body); - -export const apiCreateOrder = ( - app: INestApplication, - access: string, - body: any, -) => - request(app.getHttpServer()) - .post('/v1/trades/create') - .set('Authorization', `Bearer ${access}`) - .send(body); - -export const apiGetTrade = (app: INestApplication, tradeId: string) => - request(app.getHttpServer()).get(`/v1/trades/${tradeId}`); - -export const apiTradeConfirm = ( - app: INestApplication, - tradeId: string, - access: string, - txHash: `0x${string}`, -) => - request(app.getHttpServer()) - .post(`/v1/trades/${tradeId}/confirm`) - .set('Authorization', `Bearer ${access}`) - .send({ txHash }); - -export const apiLockOrder = ( - app: INestApplication, - access: string, - tradeId: string, -) => - request(app.getHttpServer()) - .post(`/v1/trades/${tradeId}/lock`) - .set('Authorization', `Bearer ${access}`) - .send(); - -export const apiTradeParams = ( - app: INestApplication, - access: string, - tradeId: string, -) => - request(app.getHttpServer()) - .get(`/v1/trades/${tradeId}/params`) - .set('Authorization', `Bearer ${access}`); - -export const apiUnlockOrder = ( - app: INestApplication, - access: string, - tradeId: string, - signature: string, -) => - request(app.getHttpServer()) - .post(`/v1/trades/${tradeId}/unlock`) - .set('Authorization', `Bearer ${access}`) - .send({ - signature, - }); - -export const apiTradeUnlockConfirm = ( - app: INestApplication, - access: string, - tradeId: string, - txHash: string, -) => - request(app.getHttpServer()) - .post(`/v1/trades/${tradeId}/unlock/confirm`) - .set('Authorization', `Bearer ${access}`) - .send({ - txHash, - }); diff --git a/apps/backend-relayer/test/integrations/eth-stellar.e2e-integration.ts b/apps/backend-relayer/test/integrations/eth-stellar.e2e-integration.ts deleted file mode 100644 index 1f90239..0000000 --- a/apps/backend-relayer/test/integrations/eth-stellar.e2e-integration.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { INestApplication } from '@nestjs/common'; -import { Keypair, StrKey } from '@stellar/stellar-sdk'; -import { createTestingApp } from '../setups/create-app'; -import { - fundEthAddress, - loginStellarUser, - loginUser, - makeEthClient, -} from '../setups/utils'; -import * as ethContracts from '../setups/evm-deployed-contracts.json'; -import { - getRoutes, - apiCreateAd, - apiConfirm, - apiFundAd, - apiWithdraw, - apiGetAd, - apiCloseAd, - apiCreateOrder, - apiGetTrade, - apiTradeConfirm, - apiLockOrder, - apiTradeParams, - apiUnlockOrder, - apiTradeUnlockConfirm, -} from './api'; -import { - createOrder, - unlockOrderChain, - mintToken, - approveToken, -} from '../setups/evm-actions'; -import { - createAdSoroban, - fundAdSoroban, - withdrawFromAdSoroban, - closeAdSoroban, - lockForOrderSoroban, - unlockSoroban, - StellarOrderParams, -} from '../setups/stellar-actions'; -import type { StellarChainData } from '../setups/stellar-setup'; -import { getAddress, parseEther } from 'viem'; -import { expectObject } from '../setups/utils'; -import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; -import { - AdResponseDto, - CreateAdResponseDto, -} from '../../src/modules/ads/dto/ad.dto'; -import { - CreateOrderRequestContractDetailsDto, - LockForOrderResponseDto, - UnlockOrderResponseDto, -} from '../../src/modules/trades/dto/trade.dto'; -import { - domain, - orderTypes, - signTypedOrder, - verifyTypedData, -} from '../../src/providers/viem/ethers/typedData'; -import { - T_OrderParams, - T_OrderPortalParams, -} from '../../src/chain-adapters/types'; -import { TypedDataEncoder } from 'ethers'; - -// Gate the entire suite on the external orchestrator having provisioned -// a Stellar localnet + keypairs. When running `jest` standalone without the -// bash runner, the global will be unset and we skip cleanly. -const stellarContracts = (global as any).__STELLAR_CONTRACTS__ as - | StellarChainData - | undefined; -const describeIfStellar = stellarContracts ? describe : describe.skip; - -describeIfStellar('Integrations E2E — (Stellar → ETH)', () => { - let app: INestApplication; - - // EVM-side bridger (creates the order on the EVM order chain). - const bridgerKey = generatePrivateKey(); - const bridger = privateKeyToAccount(bridgerKey); - - // Ad creator is a Stellar account. The orchestrator passes the secret via - // STELLAR_AD_CREATOR_SECRET; fall back to the deploy admin as a last resort. - const adCreatorSecret = - process.env.STELLAR_AD_CREATOR_SECRET || - (stellarContracts?.adminSecret as string); - const adCreator = adCreatorSecret - ? Keypair.fromSecret(adCreatorSecret) - : Keypair.random(); - // EVM destination for the ad creator's proceeds on unlock. - const adCreatorEvmKey = generatePrivateKey(); - const adCreatorEvm = privateKeyToAccount(adCreatorEvmKey); - - const ethChain = { - ...ethContracts, - adManagerAddress: ethContracts.adManagerAddress as `0x${string}`, - orderPortalAddress: ethContracts.orderPortalAddress as `0x${string}`, - tokenAddress: ethContracts.tokenAddress as `0x${string}`, - }; - - const ethClient = makeEthClient(); - - let route: AdResponseDto; - - beforeAll(async () => { - app = await createTestingApp(); - await fundEthAddress(ethClient, bridger.address); - await fundEthAddress(ethClient, adCreatorEvm.address); - - // Route: Stellar ad token → EVM order token. - const routes = await getRoutes( - app, - stellarContracts!.chainId, - ethChain.chainId.toString(), - ).expect(200); - expect(routes.body.data.length).toBeGreaterThan(0); - route = routes.body.data[0] as AdResponseDto; - }, 120_000); - - afterAll(async () => { - await app.close(); - }); - - it('Ad lifecycle', async () => { - const access = await loginStellarUser(app, adCreator); - - // Create ad — Stellar uses 7-decimal XLM; use a modest amount in stroops. - const INITIAL = '500000000'; // 50 XLM - const create = await apiCreateAd( - app, - access, - route.id, - adCreatorEvm.address, - INITIAL, - ).expect(201); - - const req = create.body as CreateAdResponseDto; - const adId = req.adId; - - const txCreate = await createAdSoroban( - adCreator, - req.signature, - (req as any).signer, - req.authToken, - req.timeToExpire, - adCreator.publicKey(), - req.adId, - req.adToken, - req.initialAmount, - req.orderChainId, - req.adRecipient, - req.contractAddress, - ); - await apiConfirm(app, adId, access, txCreate as `0x${string}`).expect(200); - - const adAfterCreate = await apiGetAd(app, adId).expect(200); - expectObject(adAfterCreate.body, { - id: adId, - status: 'ACTIVE', - poolAmount: INITIAL, - }); - - // Fund. - const topup = await apiFundAd(app, adId, access, '5').expect(200); - const txFund = await fundAdSoroban( - adCreator, - topup.body.signature, - topup.body.signer, - topup.body.authToken, - topup.body.timeToExpire, - topup.body.adId, - topup.body.amount, - topup.body.contractAddress, - ); - await apiConfirm(app, adId, access, txFund as `0x${string}`).expect(200); - - // Withdraw — destination is the ad creator's Stellar account. - const withdraw = await apiWithdraw( - app, - adId, - access, - '1', - adCreator.publicKey() as `0x${string}`, - ).expect(200); - const txW = await withdrawFromAdSoroban( - adCreator, - withdraw.body.signature, - withdraw.body.signer, - withdraw.body.authToken, - withdraw.body.timeToExpire, - withdraw.body.adId, - withdraw.body.amount, - StrKey.isValidEd25519PublicKey(withdraw.body.to) - ? withdraw.body.to - : adCreator.publicKey(), - withdraw.body.contractAddress, - ); - await apiConfirm(app, adId, access, txW as `0x${string}`).expect(200); - - // Close. - const close = await apiCloseAd(app, adId, access, { - to: adCreator.publicKey(), - }).expect(200); - const txClose = await closeAdSoroban( - adCreator, - close.body.signature, - close.body.signer, - close.body.authToken, - close.body.timeToExpire, - close.body.adId, - StrKey.isValidEd25519PublicKey(close.body.to) - ? close.body.to - : adCreator.publicKey(), - close.body.contractAddress, - ); - await apiConfirm(app, adId, access, txClose as `0x${string}`).expect(200); - - const finalAd = await apiGetAd(app, adId); - expectObject(finalAd.body, { status: 'CLOSED', poolAmount: '0' }); - }, 600_000); - - it('Trade lifecycle', async () => { - const adAccess = await loginStellarUser(app, adCreator); - - // Seed the ad. - const INITIAL = '500000000'; // 50 XLM - const create = await apiCreateAd( - app, - adAccess, - route.id, - adCreatorEvm.address, - INITIAL, - ).expect(201); - const req = create.body as CreateAdResponseDto; - const adId = req.adId; - - const txCreate = await createAdSoroban( - adCreator, - req.signature, - (req as any).signer, - req.authToken, - req.timeToExpire, - adCreator.publicKey(), - req.adId, - req.adToken, - req.initialAmount, - req.orderChainId, - req.adRecipient, - req.contractAddress, - ); - await apiConfirm(app, adId, adAccess, txCreate as `0x${string}`).expect(200); - - // Bridger creates the order on the EVM side. - const bridgerAccess = await loginUser(app, bridgerKey); - - // Bridger's destination on the ad chain is a Stellar account (the ad creator here). - const order = await apiCreateOrder(app, bridgerAccess, { - adId, - routeId: route.id, - amount: '100000000', // 10 XLM worth - bridgerDstAddress: adCreator.publicKey(), - }).expect(201); - - const orderReq = order.body - .reqContractDetails as CreateOrderRequestContractDetailsDto; - const tradeId = order.body.tradeId as string; - - expect(getAddress(ethChain.orderPortalAddress)).toEqual( - getAddress(orderReq.contractAddress), - ); - - await mintToken( - ethClient, - bridger, - ethChain.tokenAddress, - bridger.address, - parseEther('1000'), - ); - await approveToken( - ethClient, - bridger, - ethChain.tokenAddress, - ethChain.orderPortalAddress, - parseEther('100'), - ); - - const orderCreateTx = await createOrder( - ethClient, - bridger, - orderReq.signature, - orderReq.authToken as `0x${string}`, - orderReq.timeToExpire, - orderReq.orderParams as T_OrderPortalParams, - ethChain.orderPortalAddress, - ); - await apiTradeConfirm(app, tradeId, bridgerAccess, orderCreateTx).expect( - 200, - ); - - // Lock on the Stellar ad chain — signed by the ad creator (maker). - const lockOrder = await apiLockOrder(app, adAccess, tradeId).expect(200); - const lockReq = lockOrder.body as LockForOrderResponseDto; - - const lockTxn = await lockForOrderSoroban( - adCreator, - lockReq.signature as `0x${string}`, - (lockReq as any).signer, - lockReq.authToken as `0x${string}`, - lockReq.timeToExpire, - lockReq.orderParams as unknown as StellarOrderParams, - lockReq.contractAddress, - ); - await apiTradeConfirm(app, tradeId, adAccess, lockTxn as `0x${string}`).expect( - 200, - ); - - const afterLock = await apiGetTrade(app, tradeId); - expectObject(afterLock.body, { status: 'LOCKED' }); - - // Ad-creator unlocks on the EVM order chain. - const adCreatorParams = await apiTradeParams(app, adAccess, tradeId).expect( - 200, - ); - const adCreatorOrderParams = adCreatorParams.body as T_OrderParams; - // Ad-creator signs with their EVM destination key so the order chain - // recognises the signature. - const adCreatorSig = await signTypedOrder( - adCreatorEvmKey, - adCreatorOrderParams, - ); - const adCreatorHash = TypedDataEncoder.hash( - domain, - orderTypes, - adCreatorOrderParams, - ); - expect( - verifyTypedData( - adCreatorHash as `0x${string}`, - adCreatorSig as `0x${string}`, - adCreatorEvm.address, - ), - ).toBe(true); - - const unlockOnOrder = await apiUnlockOrder( - app, - adAccess, - tradeId, - adCreatorSig, - ).expect(200); - const unlockOrderReq = unlockOnOrder.body as UnlockOrderResponseDto; - - const unlockOrderTx = await unlockOrderChain( - ethClient, - adCreatorEvm, - unlockOrderReq.signature, - unlockOrderReq.authToken as `0x${string}`, - unlockOrderReq.timeToExpire, - unlockOrderReq.orderParams as T_OrderPortalParams, - unlockOrderReq.nullifierHash as `0x${string}`, - unlockOrderReq.targetRoot as `0x${string}`, - unlockOrderReq.proof as `0x${string}`, - unlockOrderReq.contractAddress, - ); - await apiTradeUnlockConfirm(app, adAccess, tradeId, unlockOrderTx).expect( - 200, - ); - - // Bridger unlocks on the Stellar ad chain. - const bridgerParams = await apiTradeParams( - app, - bridgerAccess, - tradeId, - ).expect(200); - const bridgerOrderParams = bridgerParams.body as T_OrderParams; - const bridgerSig = await signTypedOrder(bridgerKey, bridgerOrderParams); - - const unlockOnAd = await apiUnlockOrder( - app, - bridgerAccess, - tradeId, - bridgerSig, - ).expect(200); - const unlockAdReq = unlockOnAd.body as UnlockOrderResponseDto; - - const unlockAdTx = await unlockSoroban( - adCreator, - unlockAdReq.signature as `0x${string}`, - (unlockAdReq as any).signer, - unlockAdReq.authToken as `0x${string}`, - unlockAdReq.timeToExpire, - unlockAdReq.orderParams as unknown as StellarOrderParams, - unlockAdReq.nullifierHash as `0x${string}`, - unlockAdReq.targetRoot as `0x${string}`, - Buffer.from((unlockAdReq.proof as string).replace(/^0x/, ''), 'hex'), - unlockAdReq.contractAddress, - ); - await apiTradeUnlockConfirm( - app, - bridgerAccess, - tradeId, - unlockAdTx as `0x${string}`, - ).expect(200); - }, 600_000); -}); diff --git a/apps/backend-relayer/test/integrations/jest-e2e.json b/apps/backend-relayer/test/integrations/jest-e2e.json deleted file mode 100644 index cfdaaf8..0000000 --- a/apps/backend-relayer/test/integrations/jest-e2e.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": "../", - "testEnvironment": "node", - "testRegex": ".e2e-integration.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "transformIgnorePatterns": ["node_modules/(?!(?:\\.pnpm/)?@noble)"], - "setupFilesAfterEnv": [], - "globalSetup": "/setups/jest-integrations.setup.ts", - "globalTeardown": "/setups/jest.teardown.ts", - "moduleNameMapper": { - "^@prisma/(?!client)(.*)$": "/../prisma/$1", - "^@libs/(.*)$": "/../src/libs/$1" - } -} diff --git a/apps/backend-relayer/test/setups/create-app.ts b/apps/backend-relayer/test/setups/create-app.ts index 2506adc..3f6c4c1 100644 --- a/apps/backend-relayer/test/setups/create-app.ts +++ b/apps/backend-relayer/test/setups/create-app.ts @@ -4,15 +4,26 @@ import { AppModule } from '../../src/app.module'; import { ChainAdapterService } from '../../src/chain-adapters/chain-adapter.service'; import { MockChainAdapter } from './mock-chain-adapter'; -export async function createTestingApp(): Promise { - const mockAdapter = new MockChainAdapter(); +export interface CreateTestingAppOptions { + // When true, bypass the MockChainAdapter override so the real chain-adapter + // service is used. Required for `test:integrations` which drives real + // on-chain EVM + Stellar contracts. + useRealChainAdapters?: boolean; +} + +export async function createTestingApp( + opts: CreateTestingAppOptions = {}, +): Promise { + const builder = Test.createTestingModule({ imports: [AppModule] }); + + if (!opts.useRealChainAdapters) { + const mockAdapter = new MockChainAdapter(); + builder + .overrideProvider(ChainAdapterService) + .useValue({ forChain: () => mockAdapter }); + } - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }) - .overrideProvider(ChainAdapterService) - .useValue({ forChain: () => mockAdapter }) - .compile(); + const moduleFixture: TestingModule = await builder.compile(); const app = moduleFixture.createNestApplication(); app.useGlobalPipes( diff --git a/apps/backend-relayer/test/setups/evm-deployed-contracts.json b/apps/backend-relayer/test/setups/evm-deployed-contracts.json deleted file mode 100644 index a62e161..0000000 --- a/apps/backend-relayer/test/setups/evm-deployed-contracts.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "adManagerAddress": "0x366D90CB2A8606A82164C717cF1889c3ed5aE1f4", - "orderPortalAddress": "0xF1C313faAD40ccAeDb4Fd3e7C838993569E2572C", - "chainId": "11155111", - "name": "ETH SEPOLIA", - "tokenName": "ProofBridge", - "tokenSymbol": "PBT", - "tokenAddress": "0x1B62aDdB315CC98ab4625ffA170c1BC5C75F9da7", - "merkleManagerAddress": "0x397E7356aF447B2754D8Ea0838d285FB78F2482d", - "verifierAddress": "0xDc930A3b5CC073092750aE7f4FF45409B2428592" -} diff --git a/apps/backend-relayer/test/setups/evm-setup.ts b/apps/backend-relayer/test/setups/evm-setup.ts deleted file mode 100644 index d75d927..0000000 --- a/apps/backend-relayer/test/setups/evm-setup.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { writeFileSync } from 'fs'; -import path from 'path'; -import dotenv from 'dotenv'; - -import MerkleManagerArtifact from '../../../../contracts/evm/out/MerkleManager.sol/MerkleManager.json'; -import VerifierArtifact from '../../../../contracts/evm/out/Verifier.sol/HonkVerifier.json'; -import AdManagerArtifact from '../../../../contracts/evm/out/AdManager.sol/AdManager.json'; -import OrderPortalArtifact from '../../../../contracts/evm/out/OrderPortal.sol/OrderPortal.json'; -import Erc20MockArtifact from '../../../../contracts/evm/out/ERC20Mock.sol/ERC20Mock.json'; - -import { createPublicClient, createWalletClient, http } from 'viem'; -import { ethLocalnet } from '../../src/providers/viem/ethers/localnet'; -import { AddressLike, ChainData, fundEthAddress } from './utils'; -import { privateKeyToAccount } from 'viem/accounts'; - -dotenv.config({ path: path.resolve(__dirname, '../../.env.test') }); - -export async function deployEvmContracts(): Promise { - console.log(`Deploying ETH contracts...`); - - const managerKey = process.env.MANAGER_KEY; - if (!managerKey) { - throw new Error('MANAGER_KEY not set in environment'); - } - const chain = ethLocalnet; - - const publicClient = createPublicClient({ - chain, - transport: http(), - }); - - const wallet = createWalletClient({ - chain, - transport: http(), - account: privateKeyToAccount(managerKey as AddressLike), - }); - - const managerAddress = wallet.account.address; - - await fundEthAddress(publicClient, managerAddress, '1'); - console.log('Using manager address:', wallet.account.address); - - // Deploy mock ERC20 token - const hash = await wallet.deployContract({ - abi: Erc20MockArtifact.abi, - bytecode: Erc20MockArtifact.bytecode.object as `0x${string}`, - args: [], - }); - const receipt = await publicClient.waitForTransactionReceipt({ hash }); - const erc20Address = receipt.contractAddress!; - console.log('ERC20Mock deployed to:', erc20Address); - - // Deploy Verifier contract - const txHash = await wallet.deployContract({ - abi: VerifierArtifact.abi, - bytecode: VerifierArtifact.bytecode.object as `0x${string}`, - args: [], - }); - const txReceipt = await publicClient.waitForTransactionReceipt({ - hash: txHash, - }); - const verifierAddress = txReceipt.contractAddress!; - console.log('Verifier deployed to:', verifierAddress); - - // Deploy MerkleManager contract - const mmHash = await wallet.deployContract({ - abi: MerkleManagerArtifact.abi, - bytecode: MerkleManagerArtifact.bytecode.object as `0x${string}`, - args: [managerAddress], - }); - const mmReceipt = await publicClient.waitForTransactionReceipt({ - hash: mmHash, - }); - const merkleManagerAddress = mmReceipt.contractAddress!; - console.log('MerkleManager deployed to:', merkleManagerAddress); - - // Deploy AdManager contract - const adHash = await wallet.deployContract({ - abi: AdManagerArtifact.abi, - bytecode: AdManagerArtifact.bytecode.object as `0x${string}`, - args: [managerAddress, verifierAddress, merkleManagerAddress], - }); - const adReceipt = await publicClient.waitForTransactionReceipt({ - hash: adHash, - }); - const adManagerAddress = adReceipt.contractAddress!; - console.log('AdManager deployed to:', adManagerAddress); - - // Deploy OrderPortal contract - const orderHash = await wallet.deployContract({ - abi: OrderPortalArtifact.abi, - bytecode: OrderPortalArtifact.bytecode.object as `0x${string}`, - args: [managerAddress, verifierAddress, merkleManagerAddress], - }); - const orderReceipt = await publicClient.waitForTransactionReceipt({ - hash: orderHash, - }); - const orderPortalAddress = orderReceipt.contractAddress!; - console.log('OrderPortal deployed to:', orderPortalAddress); - - const contracts: ChainData = { - adManagerAddress, - orderPortalAddress, - chainId: chain.id.toString(), - name: 'ETH LOCALNET', - tokenName: 'ERC20Mock', - tokenSymbol: 'E20M', - tokenAddress: erc20Address, - merkleManagerAddress, - verifierAddress, - }; - - const filePath = path.join(__dirname, 'evm-deployed-contracts.json'); - writeFileSync(filePath, JSON.stringify(contracts, null, 2)); - console.log('Contract addresses saved to:', filePath); - - return contracts; -} diff --git a/apps/backend-relayer/test/setups/jest-e2e.setup.ts b/apps/backend-relayer/test/setups/jest-e2e.setup.ts index fc80733..f6a86d5 100644 --- a/apps/backend-relayer/test/setups/jest-e2e.setup.ts +++ b/apps/backend-relayer/test/setups/jest-e2e.setup.ts @@ -39,10 +39,18 @@ export default async () => { const databaseUrl = container.getConnectionUri(); process.env.DATABASE_URL = databaseUrl; process.env.NODE_ENV = 'test'; + process.env.JWT_EXPIRY = '7d'; + process.env.JWT_REFRESH_EXPIRY = '30d'; process.env.JWT_ACCESS_SECRET = 'test-access-secret'; process.env.JWT_REFRESH_SECRET = 'test-refresh-secret'; process.env.SIGN_DOMAIN = process.env.SIGN_DOMAIN || 'proofbridge.xyz'; process.env.SIGN_URI = process.env.SIGN_URI || 'https://proofbridge.xyz'; + process.env.STELLAR_AUTH_SECRET = + process.env.STELLAR_AUTH_SECRET || + 'SA3C2KPR5TCHYJ5TNQXAY2776Z3H4CB723GDCAMEX5I2NLWP25QUYB3X'; + process.env.SECRET_KEY = + process.env.SECRET_KEY || + '0xfdba5a242ddce02cd1d585297aa4afe5aa2831391198746c680a3e16a41676dc'; await migrate(databaseUrl); diff --git a/apps/backend-relayer/test/setups/jest-integrations.setup.ts b/apps/backend-relayer/test/setups/jest-integrations.setup.ts deleted file mode 100644 index 404db5e..0000000 --- a/apps/backend-relayer/test/setups/jest-integrations.setup.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - StartedPostgreSqlContainer, - PostgreSqlContainer, -} from '@testcontainers/postgresql'; -import * as dotenv from 'dotenv'; -import { execa } from 'execa'; -import path from 'path'; -import { deployEvmContracts } from './evm-setup'; -import { - deployStellarContracts, - linkStellarAdManagerToOrderChain, - StellarChainData, -} from './stellar-setup'; -import { seedDB } from './seed'; - -// Load .env (optional) -dotenv.config({ path: path.resolve(__dirname, '../../.env.test') }); - -let container: StartedPostgreSqlContainer; - -async function migrate(databaseUrl: string) { - // prisma migrate deploy - await execa('npx', ['prisma', 'migrate', 'deploy'], { - stdio: 'inherit', - env: { ...process.env, DATABASE_URL: databaseUrl }, - }); -} - -export default async () => { - container = await new PostgreSqlContainer('postgres:16-alpine') - .withDatabase('testdb') - .withUsername('test') - .withPassword('test') - .start(); - - const databaseUrl = container.getConnectionUri(); - process.env.DATABASE_URL = databaseUrl; - process.env.NODE_ENV = 'test'; - process.env.JWT_ACCESS_SECRET = 'test-access-secret'; - process.env.JWT_REFRESH_SECRET = 'test-refresh-secret'; - process.env.SIGN_DOMAIN = process.env.SIGN_DOMAIN || 'proofbridge.xyz'; - process.env.SIGN_URI = process.env.SIGN_URI || 'https://proofbridge.xyz'; - - await migrate(databaseUrl); - - const ethContracts = await deployEvmContracts(); - - // Stellar side is optional — only engages when the external bash - // orchestrator (scripts/run_cross_chain_e2e.sh) has exported the RPC + - // admin secret. Tests that depend on Stellar should skip when absent. - let stellarContracts: StellarChainData | undefined; - if (process.env.STELLAR_RPC_URL && process.env.STELLAR_ADMIN_SECRET) { - stellarContracts = await deployStellarContracts(); - await linkStellarAdManagerToOrderChain(stellarContracts, ethContracts); - } - - await seedDB(ethContracts, stellarContracts); - - (global as any).__ETH_CONTRACTS__ = ethContracts; - if (stellarContracts) { - (global as any).__STELLAR_CONTRACTS__ = stellarContracts; - } - (global as any).__PG_CONTAINER__ = container; -}; diff --git a/apps/backend-relayer/test/setups/mock-chain-adapter.ts b/apps/backend-relayer/test/setups/mock-chain-adapter.ts index 493f6a9..26f5458 100644 --- a/apps/backend-relayer/test/setups/mock-chain-adapter.ts +++ b/apps/backend-relayer/test/setups/mock-chain-adapter.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { randomBytes } from 'crypto'; import { ChainAdapter } from '../../src/chain-adapters/adapters/chain-adapter.abstract'; import { @@ -24,8 +25,7 @@ import { const ZERO_32 = '0x0000000000000000000000000000000000000000000000000000000000000000' as const; -const FAKE_SIG = - ('0x' + 'ab'.repeat(65)) as `0x${string}`; +const FAKE_SIG = ('0x' + 'ab'.repeat(65)) as `0x${string}`; const ONE_HOUR_S = 3600; function uniqueHash(): `0x${string}` { diff --git a/apps/backend-relayer/test/setups/seed.ts b/apps/backend-relayer/test/setups/seed.ts index 6781966..3059b5a 100644 --- a/apps/backend-relayer/test/setups/seed.ts +++ b/apps/backend-relayer/test/setups/seed.ts @@ -1,68 +1,5 @@ -import { ChainKind, PrismaClient } from '@prisma/client'; -import { ChainData, seedAdmin, seedChain, seedRoute, seedToken } from './utils'; -import { StellarChainData } from './stellar-setup'; - -export const seedDB = async ( - ethContracts: ChainData, - stellarContracts?: StellarChainData, -) => { - const prisma = new PrismaClient(); - - try { - await prisma.$connect(); - - await seedAdmin(prisma, 'admin@x.com', 'ChangeMe123!'); - - const ethChain = await seedChain(prisma, { - name: ethContracts.name, - chainId: BigInt(ethContracts.chainId), - ad: ethContracts.adManagerAddress, - op: ethContracts.orderPortalAddress, - kind: ChainKind.EVM, - }); - - const ethToken = await seedToken( - prisma, - ethChain.id, - ethContracts.tokenName, - ethContracts.tokenSymbol, - ethContracts.tokenAddress, - ); - - if (stellarContracts) { - const stellarChain = await seedChain(prisma, { - name: stellarContracts.name, - chainId: BigInt(stellarContracts.chainId), - ad: stellarContracts.adManagerAddress, - // Stellar side has no OrderPortal in this direction — reuse the - // AdManager address as a non-null placeholder for the schema. - op: stellarContracts.adManagerAddress, - kind: ChainKind.STELLAR, - }); - - const stellarToken = await seedToken( - prisma, - stellarChain.id, - stellarContracts.tokenName, - stellarContracts.tokenSymbol, - stellarContracts.tokenAddress, - 'NATIVE', - 7, - ); - - // Stellar ad token → EVM order token. - await seedRoute(prisma, stellarToken.id, ethToken.id); - } - - await prisma.$disconnect(); - - console.log('Seeding completed.'); - } catch (error) { - console.error('Error seeding db:', error); - } finally { - await prisma.$disconnect(); - } -}; +import { PrismaClient } from '@prisma/client'; +import { seedAdmin } from './utils'; export const seedDBe2e = async () => { const prisma = new PrismaClient(); diff --git a/apps/backend-relayer/test/setups/stellar-setup.ts b/apps/backend-relayer/test/setups/stellar-setup.ts deleted file mode 100644 index 911aa0b..0000000 --- a/apps/backend-relayer/test/setups/stellar-setup.ts +++ /dev/null @@ -1,300 +0,0 @@ -// Stellar deploy helper — symmetric with `deployEvmContracts()`. -// Uploads each WASM, instantiates the contracts, initialises them, and -// deploys the native XLM SAC so the test has a token route. -// -// Assumes a Stellar network is already running at STELLAR_RPC_URL with the -// admin account (STELLAR_ADMIN_SECRET) friendbot-funded. The external -// `run_cross_chain_e2e.sh` script sets both up. - -import fs from 'node:fs'; -import path from 'node:path'; -import { - Asset, - Keypair, - Networks, - Operation, - StrKey, - TransactionBuilder, - hash, - nativeToScVal, - rpc, - xdr, - Address, -} from '@stellar/stellar-sdk'; -import { - accountIdToHex32, - contractIdToHex32, - hex32ToBuffer, - hex32ToContractId, -} from '../../src/providers/stellar/utils/address'; -import { ChainData } from './utils'; - -const BASE_FEE = '1000'; - -export interface StellarChainData { - adManagerAddress: `0x${string}`; - merkleManagerAddress: `0x${string}`; - verifierAddress: `0x${string}`; - // The native XLM SAC doubles as the ad token for this test. - tokenAddress: `0x${string}`; - chainId: string; - name: string; - tokenName: string; - tokenSymbol: string; - adminPublicKeyHex: `0x${string}`; - adminSecret: string; // S… strkey, handed back so the test can sign with it -} - -export const STELLAR_CHAIN_ID = '1000001'; -const STELLAR_CHAIN_NAME = 'STELLAR LOCALNET'; - -function getServer(): rpc.Server { - const url = process.env.STELLAR_RPC_URL; - if (!url) throw new Error('STELLAR_RPC_URL not set'); - return new rpc.Server(url, { allowHttp: url.startsWith('http://') }); -} - -function networkPassphrase(): string { - return process.env.STELLAR_NETWORK_PASSPHRASE || Networks.TESTNET; -} - -function loadAdminKeypair(): Keypair { - const raw = (process.env.STELLAR_ADMIN_SECRET ?? '').trim(); - if (!raw) throw new Error('STELLAR_ADMIN_SECRET not set'); - if (StrKey.isValidEd25519SecretSeed(raw)) return Keypair.fromSecret(raw); - if (/^0x[a-fA-F0-9]{64}$/.test(raw)) { - return Keypair.fromRawEd25519Seed(Buffer.from(raw.slice(2), 'hex')); - } - throw new Error( - 'Invalid STELLAR_ADMIN_SECRET (expected S… strkey or 0x + 64 hex)', - ); -} - -async function submit( - server: rpc.Server, - signer: Keypair, - buildOp: () => xdr.Operation, -): Promise { - const source = await server.getAccount(signer.publicKey()); - const tx = new TransactionBuilder(source, { - fee: BASE_FEE, - networkPassphrase: networkPassphrase(), - }) - .addOperation(buildOp()) - .setTimeout(60) - .build(); - const prepared = await server.prepareTransaction(tx); - prepared.sign(signer); - const sent = await server.sendTransaction(prepared); - if (sent.status === 'ERROR') { - throw new Error( - `Stellar send failed: ${JSON.stringify(sent.errorResult)}`, - ); - } - for (let i = 0; i < 20; i++) { - const got = await server.getTransaction(sent.hash); - if (got.status === rpc.Api.GetTransactionStatus.SUCCESS) return got; - if (got.status === rpc.Api.GetTransactionStatus.FAILED) { - throw new Error(`Stellar tx FAILED hash=${sent.hash}`); - } - await new Promise((r) => setTimeout(r, 1000)); - } - throw new Error(`Stellar tx timed out hash=${sent.hash}`); -} - -async function uploadWasm( - server: rpc.Server, - signer: Keypair, - wasm: Buffer, -): Promise { - await submit(server, signer, () => Operation.uploadContractWasm({ wasm })); - return hash(wasm); -} - -async function createContract( - server: rpc.Server, - signer: Keypair, - wasmHash: Buffer, - constructorArgs: xdr.ScVal[] = [], -): Promise { - const salt = new Uint8Array(32); - globalThis.crypto.getRandomValues(salt); - const res = await submit(server, signer, () => - Operation.createCustomContract({ - address: Address.fromString(signer.publicKey()), - wasmHash, - salt: Buffer.from(salt), - constructorArgs, - }), - ); - const retval = res.returnValue; - if (!retval) throw new Error('createCustomContract: no return value'); - const addr = Address.fromScAddress(retval.address()).toString(); - if (!addr.startsWith('C')) throw new Error(`unexpected contract addr: ${addr}`); - return addr; -} - -async function invoke( - server: rpc.Server, - signer: Keypair, - contractIdStrkey: string, - method: string, - args: xdr.ScVal[], -): Promise { - await submit(server, signer, () => - Operation.invokeContractFunction({ - contract: contractIdStrkey, - function: method, - args, - }), - ); -} - -async function deployNativeSac( - server: rpc.Server, - signer: Keypair, -): Promise { - // If the native SAC is already deployed on this network, createStellarAssetContract - // returns an error — fall back to the deterministic contractId. - const asset = Asset.native(); - try { - const res = await submit(server, signer, () => - Operation.createStellarAssetContract({ asset }), - ); - const retval = res.returnValue; - if (!retval) throw new Error('createStellarAssetContract: no return value'); - return Address.fromScAddress(retval.address()).toString(); - } catch { - return asset.contractId(networkPassphrase()); - } -} - -function wasmPath(name: string): string { - return path.join( - __dirname, - '../../src/providers/stellar/wasm', - `${name}.wasm`, - ); -} - -function vkBytes(): Buffer { - const vkPath = path.resolve( - __dirname, - '../../../../proof_circuits/deposits/target/vk', - ); - if (!fs.existsSync(vkPath)) { - throw new Error( - `Verifier VK not found at ${vkPath}. Run scripts/build_circuits.sh first.`, - ); - } - return fs.readFileSync(vkPath); -} - -export async function deployStellarContracts(): Promise { - const admin = loadAdminKeypair(); - const server = getServer(); - console.log( - `Deploying STELLAR contracts (admin=${admin.publicKey()}, rpc=${process.env.STELLAR_RPC_URL})...`, - ); - - // Upload WASMs. - const verifierWasm = fs.readFileSync(wasmPath('verifier')); - const merkleWasm = fs.readFileSync(wasmPath('merkle_manager')); - const adWasm = fs.readFileSync(wasmPath('ad_manager')); - const verifierHash = await uploadWasm(server, admin, verifierWasm); - const merkleHash = await uploadWasm(server, admin, merkleWasm); - const adHash = await uploadWasm(server, admin, adWasm); - - // Native XLM SAC — used as the Stellar-side ad token. - const xlmSacStrkey = await deployNativeSac(server, admin); - console.log(` Native XLM SAC: ${xlmSacStrkey}`); - - // Verifier — constructor takes the VK bytes. - const verifierStrkey = await createContract(server, admin, verifierHash, [ - nativeToScVal(vkBytes(), { type: 'bytes' }), - ]); - console.log(` Verifier: ${verifierStrkey}`); - - // MerkleManager — initialize(admin). - const merkleStrkey = await createContract(server, admin, merkleHash); - await invoke(server, admin, merkleStrkey, 'initialize', [ - new Address(admin.publicKey()).toScVal(), - ]); - console.log(` MerkleManager: ${merkleStrkey}`); - - // AdManager — initialize(admin, verifier, merkle, w_native, chain_id). - const adStrkey = await createContract(server, admin, adHash); - await invoke(server, admin, adStrkey, 'initialize', [ - new Address(admin.publicKey()).toScVal(), - new Address(verifierStrkey).toScVal(), - new Address(merkleStrkey).toScVal(), - new Address(xlmSacStrkey).toScVal(), - nativeToScVal(BigInt(STELLAR_CHAIN_ID), { type: 'u128' }), - ]); - console.log(` AdManager: ${adStrkey}`); - - // Grant AdManager merkle_manager role so it can write roots on create/lock. - await invoke(server, admin, merkleStrkey, 'set_manager', [ - new Address(adStrkey).toScVal(), - xdr.ScVal.scvBool(true), - ]); - - const contracts: StellarChainData = { - adManagerAddress: contractIdToHex32(adStrkey), - merkleManagerAddress: contractIdToHex32(merkleStrkey), - verifierAddress: contractIdToHex32(verifierStrkey), - tokenAddress: contractIdToHex32(xlmSacStrkey), - chainId: STELLAR_CHAIN_ID, - name: STELLAR_CHAIN_NAME, - tokenName: 'Native XLM', - tokenSymbol: 'XLM', - adminPublicKeyHex: accountIdToHex32(admin.publicKey()), - adminSecret: admin.secret(), - }; - - const filePath = path.join(__dirname, 'stellar-deployed-contracts.json'); - fs.writeFileSync(filePath, JSON.stringify(contracts, null, 2)); - console.log('Stellar contract addresses saved to:', filePath); - - return contracts; -} - -// Hex-form bytes32 helpers so callers on the EVM side can pass raw addresses. -function bytes32FromEvmAddress(evmAddress: string): `0x${string}` { - const clean = evmAddress.replace(/^0x/i, '').toLowerCase().padStart(40, '0'); - return `0x${'00'.repeat(12)}${clean}` as `0x${string}`; -} - -function bytesN(hex: string) { - return nativeToScVal(hex32ToBuffer(hex), { type: 'bytes' }); -} - -// Register the order chain + token route on the Stellar AdManager so orders -// from the EVM chain are accepted. Analogous to setupAdManager() on EVM. -export async function linkStellarAdManagerToOrderChain( - stellar: StellarChainData, - orderChain: ChainData, -): Promise { - const admin = loadAdminKeypair(); - const server = getServer(); - const adStrkey = hex32ToContractId(stellar.adManagerAddress); - - // Stellar AdManager expects bytes32 for addresses from the order chain — - // left-pad 20-byte EVM addresses to 32 bytes. - const orderPortalBytes32 = bytes32FromEvmAddress(orderChain.orderPortalAddress); - const orderTokenBytes32 = bytes32FromEvmAddress(orderChain.tokenAddress); - - await invoke(server, admin, adStrkey, 'set_chain', [ - nativeToScVal(BigInt(orderChain.chainId), { type: 'u128' }), - bytesN(orderPortalBytes32), - xdr.ScVal.scvBool(true), - ]); - console.log(' Stellar AdManager.set_chain → order chain registered'); - - await invoke(server, admin, adStrkey, 'set_token_route', [ - bytesN(stellar.tokenAddress), - bytesN(orderTokenBytes32), - nativeToScVal(BigInt(orderChain.chainId), { type: 'u128' }), - ]); - console.log(' Stellar AdManager.set_token_route → route set'); -} diff --git a/apps/backend-relayer/test/setups/utils.ts b/apps/backend-relayer/test/setups/utils.ts index 9a10afd..0733e43 100644 --- a/apps/backend-relayer/test/setups/utils.ts +++ b/apps/backend-relayer/test/setups/utils.ts @@ -5,38 +5,11 @@ import { SiweMessage } from 'siwe'; import { PrismaClient, ChainKind } from '@prisma/client'; import { hash } from '@node-rs/argon2'; import { privateKeyToAddress, signMessage } from 'viem/accounts'; -import { - createPublicClient, - createWalletClient, - http, - PublicClient, -} from 'viem'; -import { parseEther } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { - Keypair, - Networks, - TransactionBuilder, -} from '@stellar/stellar-sdk'; -import { ethLocalnet } from '../../src/providers/viem/ethers/localnet'; - -export type AddressLike = `0x${string}`; - -export interface ChainData { - adManagerAddress: AddressLike; - orderPortalAddress: AddressLike; - merkleManagerAddress: AddressLike; - verifierAddress: AddressLike; - chainId: string; - name: string; - tokenName: string; - tokenSymbol: string; - tokenAddress: AddressLike; -} +import { Keypair, Networks, TransactionBuilder } from '@stellar/stellar-sdk'; export interface ChallengeResponse { nonce: string; - address: AddressLike; + address: `0x${string}`; expiresAt: string; domain: string; uri: string; @@ -250,65 +223,6 @@ export function randomAddress() { return wallet.address; } -export const makeEthClient = () => - createPublicClient({ chain: ethLocalnet, transport: http() }); - -async function tryTopUpViaRpc(addr: AddressLike, hexWei: string) { - const ethRpc = process.env.ETHEREUM_RPC_URL ?? 'http://localhost:9545'; - - const provider = new ethers.JsonRpcProvider(ethRpc); - try { - await provider.send('anvil_setBalance', [addr, hexWei]); - return true; - } catch { - // ignore - } - - try { - await provider.send('hardhat_setBalance', [addr, hexWei]); - return true; - } catch { - // ignore - } - - return false; -} - -export async function fundEthAddress( - client: PublicClient, - to: AddressLike, - minBalanceEther = '1.0', -): Promise { - const needed = parseEther(minBalanceEther); - const current = await client.getBalance({ address: to }); - if (current >= needed) return; - - const funderKey = process.env.FUNDER_KEY as `0x${string}` | undefined; - - if (funderKey) { - const wallet = createWalletClient({ - chain: client.chain ?? ethLocalnet, - transport: http(), - account: privateKeyToAccount(funderKey), - }); - - const hash = await wallet.sendTransaction({ - to, - value: parseEther('10'), // send 10 ETH - }); - await client.waitForTransactionReceipt({ hash }); - return; - } - - // No FUNDER_KEY; attempt node-specific balance set - const ok = await tryTopUpViaRpc(to, '0x8AC7230489E80000'); // 10 ETH - if (!ok) { - throw new Error( - 'Unable to fund address. Set FUNDER_KEY in env, or run against Anvil/Hardhat and allow *_setBalance.', - ); - } -} - export const expectObject = ( obj: any, fields: Partial>, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4d138b..8a40e00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,15 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + dotenv: + specifier: ^17.2.2 + version: 17.4.1 + ethers: + specifier: ^6.15.0 + version: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + express: + specifier: ^5.1.0 + version: 5.2.1 level: specifier: ^10.0.0 version: 10.0.0 @@ -112,6 +121,9 @@ importers: nest-winston: specifier: ^1.10.2 version: 1.10.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.19.0) + prisma: + specifier: 6.16.1 + version: 6.16.1(typescript@5.9.3) proofbridge-mmr: specifier: 1.0.8 version: 1.0.8(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -124,6 +136,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.2 + siwe: + specifier: ^3.0.0 + version: 3.0.0(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) unique-names-generator: specifier: ^4.7.1 version: 4.7.1 @@ -167,9 +182,6 @@ importers: '@types/supertest': specifier: ^6.0.2 version: 6.0.3 - dotenv: - specifier: ^17.2.2 - version: 17.4.1 eslint: specifier: ^9.18.0 version: 9.39.4(jiti@2.6.1) @@ -179,9 +191,6 @@ importers: eslint-plugin-prettier: specifier: ^5.2.2 version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.2) - ethers: - specifier: ^6.15.0 - version: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) execa: specifier: ^9.6.0 version: 9.6.1 @@ -194,12 +203,6 @@ importers: prettier: specifier: ^3.4.2 version: 3.8.2 - prisma: - specifier: 6.16.1 - version: 6.16.1(typescript@5.9.3) - siwe: - specifier: ^3.0.0 - version: 3.0.0(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -235,10 +238,10 @@ importers: version: 2.1.2(gsap@3.14.2)(react@19.1.0) '@rainbow-me/rainbowkit': specifier: ^2.2.8 - version: 2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + version: 2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)) '@rainbow-me/rainbowkit-siwe-next-auth': specifier: ^0.5.0 - version: 0.5.0(@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(next-auth@4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 0.5.0(@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)))(next-auth@4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react@19.1.0) '@tanstack/react-query': specifier: ^5.89.0 version: 5.98.0(react@19.1.0) @@ -304,10 +307,10 @@ importers: version: 4.0.0(tailwindcss@4.2.2) viem: specifier: ^2.37.7 - version: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) wagmi: specifier: ^2.17.1 - version: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + version: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6) devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -485,6 +488,40 @@ importers: specifier: ^5.3.3 version: 5.9.3 + scripts/relayer-e2e: + dependencies: + '@node-rs/argon2': + specifier: ^2.0.2 + version: 2.0.2 + '@prisma/client': + specifier: 6.16.1 + version: 6.16.1(prisma@6.16.1(typescript@5.9.3))(typescript@5.9.3) + '@stellar/stellar-sdk': + specifier: ^15.0.1 + version: 15.0.1 + cross-chain-e2e: + specifier: workspace:* + version: link:../cross-chain-e2e + ethers: + specifier: ^6.15.0 + version: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + siwe: + specifier: ^3.0.0 + version: 3.0.0(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + viem: + specifier: ^2.37.7 + version: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + devDependencies: + '@types/node': + specifier: ^22.10.7 + version: 22.19.17 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + packages: '@adraffy/ens-normalize@1.10.1': @@ -8313,16 +8350,16 @@ snapshots: '@balena/dockerignore@1.0.2': {} - '@base-org/account@2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@base-org/account@2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@coinbase/cdp-sdk': 1.47.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) + ox: 0.6.9(typescript@5.9.3)(zod@4.3.6) preact: 10.24.2 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) zustand: 5.0.3(@types/react@19.2.14)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)) transitivePeerDependencies: - '@types/react' @@ -8385,15 +8422,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@coinbase/wallet-sdk@4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@coinbase/wallet-sdk@4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) + ox: 0.6.9(typescript@5.9.3)(zod@4.3.6) preact: 10.24.2 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) zustand: 5.0.3(@types/react@19.2.14)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)) transitivePeerDependencies: - '@types/react' @@ -8602,11 +8639,11 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 - '@gemini-wallet/core@0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@gemini-wallet/core@0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) transitivePeerDependencies: - supports-color @@ -9729,13 +9766,13 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@rainbow-me/rainbowkit-siwe-next-auth@0.5.0(@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(next-auth@4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react@19.1.0)': + '@rainbow-me/rainbowkit-siwe-next-auth@0.5.0(@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)))(next-auth@4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react@19.1.0)': dependencies: - '@rainbow-me/rainbowkit': 2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + '@rainbow-me/rainbowkit': 2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)) next-auth: 4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0) react: 19.1.0 - '@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))': + '@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6))': dependencies: '@tanstack/react-query': 5.98.0(react@19.1.0) '@vanilla-extract/css': 1.17.3 @@ -9747,8 +9784,8 @@ snapshots: react-dom: 19.2.5(react@19.1.0) react-remove-scroll: 2.6.2(@types/react@19.2.14)(react@19.1.0) ua-parser-js: 1.0.41 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + wagmi: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6) transitivePeerDependencies: - '@types/react' - babel-plugin-macros @@ -9833,24 +9870,24 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-controllers@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) valtio: 1.13.2(@types/react@19.2.14)(react@19.1.0) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9879,12 +9916,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-pay@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6) lit: 3.3.0 valtio: 1.13.2(@types/react@19.2.14)(react@19.1.0) transitivePeerDependencies: @@ -9919,12 +9956,12 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -9956,10 +9993,10 @@ snapshots: - valtio - zod - '@reown/appkit-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 @@ -9991,16 +10028,16 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76)': + '@reown/appkit-utils@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) valtio: 1.13.2(@types/react@19.2.14)(react@19.1.0) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10040,21 +10077,21 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-pay': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-pay': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0 - '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.2.14)(react@19.1.0) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10085,9 +10122,9 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) events: 3.3.0 transitivePeerDependencies: - bufferutil @@ -10095,10 +10132,10 @@ snapshots: - utf-8-validate - zod - '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) transitivePeerDependencies: - bufferutil - typescript @@ -11129,19 +11166,19 @@ snapshots: dependencies: '@vanilla-extract/css': 1.17.3 - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@6.2.0(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6))(zod@4.3.6)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@base-org/account': 2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@4.3.6) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@4.3.6) + '@gemini-wallet/core': 0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + porto: 0.2.35(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -11182,11 +11219,11 @@ snapshots: - wagmi - zod - '@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) zustand: 5.0.0(@types/react@19.2.14)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)) optionalDependencies: '@tanstack/query-core': 5.98.0 @@ -11197,7 +11234,7 @@ snapshots: - react - use-sync-external-store - '@walletconnect/core@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/core@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -11211,7 +11248,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0 - '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -11241,7 +11278,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/core@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -11255,7 +11292,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1 - '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -11289,18 +11326,18 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1 - '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@walletconnect/types': 2.21.1 - '@walletconnect/universal-provider': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -11423,16 +11460,16 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: - '@walletconnect/core': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/core': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0 - '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -11459,16 +11496,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: - '@walletconnect/core': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/core': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1 - '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -11557,7 +11594,7 @@ snapshots: - ioredis - uploadthing - '@walletconnect/universal-provider@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -11566,9 +11603,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1 '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@walletconnect/types': 2.21.0 - '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -11597,7 +11634,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -11606,9 +11643,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1 '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) '@walletconnect/types': 2.21.1 - '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -11637,7 +11674,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/utils@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -11655,7 +11692,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -11681,7 +11718,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/utils@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -11699,7 +11736,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -11821,10 +11858,10 @@ snapshots: typescript: 5.9.3 zod: 3.25.76 - abitype@1.0.8(typescript@5.9.3)(zod@3.25.76): + abitype@1.0.8(typescript@5.9.3)(zod@4.3.6): optionalDependencies: typescript: 5.9.3 - zod: 3.25.76 + zod: 4.3.6 abitype@1.2.3(typescript@5.9.3)(zod@3.22.4): optionalDependencies: @@ -13050,7 +13087,7 @@ snapshots: '@next/eslint-plugin-next': 16.2.3 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) @@ -13077,7 +13114,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -13092,13 +13129,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13113,7 +13150,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15144,28 +15181,28 @@ snapshots: transitivePeerDependencies: - zod - ox@0.6.7(typescript@5.9.3)(zod@3.25.76): + ox@0.6.7(typescript@5.9.3)(zod@4.3.6): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) + abitype: 1.0.8(typescript@5.9.3)(zod@4.3.6) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - zod - ox@0.6.9(typescript@5.9.3)(zod@3.25.76): + ox@0.6.9(typescript@5.9.3)(zod@4.3.6): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -15299,21 +15336,21 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.35(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) hono: 4.12.12 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) ox: 0.9.17(typescript@5.9.3)(zod@4.3.6) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) zod: 4.3.6 zustand: 5.0.12(@types/react@19.2.14)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)) optionalDependencies: '@tanstack/react-query': 5.98.0(react@19.1.0) react: 19.1.0 typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6) transitivePeerDependencies: - '@types/react' - immer @@ -16923,15 +16960,15 @@ snapshots: vary@1.1.2: {} - viem@2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): + viem@2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6): dependencies: '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 '@scure/bip32': 1.6.2 '@scure/bip39': 1.5.4 - abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) + abitype: 1.0.8(typescript@5.9.3)(zod@4.3.6) isows: 1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - ox: 0.6.7(typescript@5.9.3)(zod@3.25.76) + ox: 0.6.7(typescript@5.9.3)(zod@4.3.6) ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 @@ -16991,14 +17028,14 @@ snapshots: - utf-8-validate - zod - wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): + wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6): dependencies: '@tanstack/react-query': 5.98.0(react@19.1.0) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6))(zod@4.3.6) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) react: 19.1.0 use-sync-external-store: 1.4.0(react@19.1.0) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c54acd8..21271b0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,4 @@ packages: - "packages/*" - "contracts/stellar/tests/fixtures" - "scripts/cross-chain-e2e" + - "scripts/relayer-e2e" diff --git a/scripts/cross-chain-e2e/lib/deploy.ts b/scripts/cross-chain-e2e/lib/deploy.ts new file mode 100644 index 0000000..5621cd2 --- /dev/null +++ b/scripts/cross-chain-e2e/lib/deploy.ts @@ -0,0 +1,291 @@ +/** + * Shared chain deployment + cross-chain linking. + * + * Extracted from run.ts phases 1-3 so both the monolithic cross-chain-e2e + * runner and the relayer-e2e script can share the same deploy path. + * + * The function signatures intentionally stay close to the inline originals so + * run.ts could keep behaving identically before and after the refactor. + */ + +import * as path from "path"; +import * as fs from "fs"; +import { ethers } from "ethers"; +import { + deployContract, + deploySAC, + invokeContract, + strkeyToHex, + evmAddressToBytes32, +} from "./stellar.js"; +import { + deployEvmContracts, + NonceTracker, + type EvmContracts, +} from "./evm.js"; + +// ── env / paths ─────────────────────────────────────────────────────── + +function requireEnv(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`env ${name} not set`); + return v; +} + +export const DEFAULT_STELLAR_CHAIN_ID = 1000001n; +export const DEFAULT_EVM_CHAIN_ID = 31337n; // Anvil default + +// ── types ───────────────────────────────────────────────────────────── + +export interface StellarDeployResult { + verifier: string; // contract id (strkey) + merkleManager: string; + adToken: string; // native XLM SAC (strkey) + adTokenHex: string; // 0x + 64 hex + adManager: string; // strkey + adManagerHex: string; + adminStrkey: string; + chainId: bigint; +} + +export interface EvmDeployResult { + chainId: bigint; + signer: ethers.Wallet; + nonces: NonceTracker; + contracts: EvmContracts; + addresses: EvmContracts["addresses"]; +} + +export interface DeployAllOpts { + /** Absolute repo root. Defaults to process.env.ROOT_DIR. */ + rootDir?: string; + /** Absolute path to compiled .wasm outputs. Defaults to {rootDir}/contracts/stellar/target/wasm32v1-none/release. */ + wasmDir?: string; + /** Absolute path to the verifier vk bytes. Defaults to {rootDir}/proof_circuits/deposits/target/vk. */ + vkPath?: string; + /** EVM JSON-RPC. Defaults to process.env.EVM_RPC_URL. */ + evmRpcUrl?: string; + /** EVM admin private key (deployer + manager). Defaults to process.env.EVM_ADMIN_PRIVATE_KEY. */ + evmAdminPrivateKey?: string; + /** Stellar chain id. */ + stellarChainId?: bigint; + /** EVM chain id. */ + evmChainId?: bigint; +} + +export interface DeployAllResult { + stellar: StellarDeployResult; + evm: EvmDeployResult; +} + +// ── stellar ─────────────────────────────────────────────────────────── + +export interface DeployStellarOpts { + wasmDir: string; + vkPath: string; + adminStrkey: string; + chainId: bigint; +} + +export function deployStellarChain(opts: DeployStellarOpts): StellarDeployResult { + const { wasmDir, vkPath, adminStrkey, chainId } = opts; + + console.log("Deploying Stellar Verifier..."); + const verifier = deployContract(path.join(wasmDir, "verifier.wasm"), [ + `--vk_bytes-file-path`, + vkPath, + ]); + console.log(` Verifier: ${verifier}`); + + console.log("Deploying Stellar MerkleManager..."); + const merkleManager = deployContract(path.join(wasmDir, "merkle_manager.wasm")); + console.log(` MerkleManager: ${merkleManager}`); + invokeContract(merkleManager, "initialize", [`--admin`, adminStrkey]); + + console.log("Deploying native XLM SAC..."); + const adToken = deploySAC("native"); + const adTokenHex = strkeyToHex(adToken); + console.log(` NativeXLM SAC: ${adToken}`); + + console.log("Deploying Stellar AdManager..."); + const adManager = deployContract(path.join(wasmDir, "ad_manager.wasm")); + console.log(` AdManager: ${adManager}`); + invokeContract(adManager, "initialize", [ + `--admin`, + adminStrkey, + `--verifier`, + verifier, + `--merkle_manager`, + merkleManager, + `--w_native_token`, + adToken, + `--chain_id`, + chainId.toString(), + ]); + + invokeContract(merkleManager, "set_manager", [ + `--manager`, + adManager, + `--status`, + "true", + ]); + + return { + verifier, + merkleManager, + adToken, + adTokenHex, + adManager, + adManagerHex: strkeyToHex(adManager), + adminStrkey, + chainId, + }; +} + +// ── evm ─────────────────────────────────────────────────────────────── + +export interface DeployEvmOpts { + rpcUrl: string; + adminPrivateKey: string; + chainId: bigint; +} + +export async function deployEvmChain(opts: DeployEvmOpts): Promise { + const contracts = await deployEvmContracts(opts.rpcUrl, opts.adminPrivateKey); + return { + chainId: opts.chainId, + signer: contracts.signer, + nonces: contracts.nonces, + contracts, + addresses: contracts.addresses, + }; +} + +// ── cross-link ──────────────────────────────────────────────────────── + +export async function linkChains( + stellar: StellarDeployResult, + evm: EvmDeployResult, +): Promise { + const { contracts, nonces, chainId: evmChainId } = evm; + const stellarChainIdStr = stellar.chainId.toString(); + const evmChainIdStr = evmChainId.toString(); + + const evmOrderPortalBytes32 = evmAddressToBytes32(contracts.addresses.orderPortal); + const evmTokenBytes32 = evmAddressToBytes32(contracts.addresses.testToken); + + console.log("Linking Stellar AdManager → EVM OrderPortal..."); + invokeContract(stellar.adManager, "set_chain", [ + `--order_chain_id`, + evmChainIdStr, + `--order_portal`, + evmOrderPortalBytes32.replace(/^0x/, ""), + `--supported`, + "true", + ]); + + console.log("Setting Stellar token route..."); + invokeContract(stellar.adManager, "set_token_route", [ + `--ad_token`, + stellar.adTokenHex.replace(/^0x/, ""), + `--order_token`, + evmTokenBytes32.replace(/^0x/, ""), + `--order_chain_id`, + evmChainIdStr, + ]); + + console.log("Linking EVM OrderPortal → Stellar AdManager..."); + { + const tx = await contracts.orderPortal.getFunction("setChain")( + stellar.chainId, + stellar.adManagerHex, + true, + { nonce: nonces.next() }, + ); + await tx.wait(); + } + + console.log("Setting EVM token route..."); + { + const tx = await contracts.orderPortal.getFunction("setTokenRoute")( + contracts.addresses.testToken, + stellar.chainId, + stellar.adTokenHex, + { nonce: nonces.next() }, + ); + await tx.wait(); + } + + console.log("Cross-chain linking complete."); +} + +// ── top-level ───────────────────────────────────────────────────────── + +export async function deployAll(opts: DeployAllOpts = {}): Promise { + const rootDir = opts.rootDir ?? requireEnv("ROOT_DIR"); + const wasmDir = + opts.wasmDir ?? path.join(rootDir, "contracts/stellar/target/wasm32v1-none/release"); + const vkPath = opts.vkPath ?? path.join(rootDir, "proof_circuits/deposits/target/vk"); + const evmRpcUrl = opts.evmRpcUrl ?? requireEnv("EVM_RPC_URL"); + const evmAdminPrivateKey = + opts.evmAdminPrivateKey ?? requireEnv("EVM_ADMIN_PRIVATE_KEY"); + const stellarChainId = opts.stellarChainId ?? DEFAULT_STELLAR_CHAIN_ID; + const evmChainId = opts.evmChainId ?? DEFAULT_EVM_CHAIN_ID; + + const { getAddress } = await import("./stellar.js"); + const adminStrkey = getAddress(); + + const stellar = deployStellarChain({ + wasmDir, + vkPath, + adminStrkey, + chainId: stellarChainId, + }); + + const evm = await deployEvmChain({ + rpcUrl: evmRpcUrl, + adminPrivateKey: evmAdminPrivateKey, + chainId: evmChainId, + }); + + await linkChains(stellar, evm); + + return { stellar, evm }; +} + +/** Write a JSON snapshot of deployed addresses. Used by relayer-e2e to seed the DB. */ +export function writeDeployedSnapshot( + outPath: string, + { stellar, evm }: DeployAllResult, +): void { + // Undeployed roles are emitted as null so consumers fail fast on a real use + // (instead of silently contract-calling the zero address). In this flow the + // EVM side only plays the order role and the Stellar side only plays the ad + // role, so the counterpart address on each chain stays null. + const snapshot = { + eth: { + name: "AnvilLocal", + chainId: evm.chainId.toString(), + adManagerAddress: null as string | null, + orderPortalAddress: evm.addresses.orderPortal, + merkleManagerAddress: evm.addresses.merkleManager, + verifierAddress: evm.addresses.verifier, + tokenName: "TestToken", + tokenSymbol: "TT", + tokenAddress: evm.addresses.testToken, + }, + stellar: { + name: "StellarLocal", + chainId: stellar.chainId.toString(), + adManagerAddress: stellar.adManagerHex, + orderPortalAddress: null as string | null, + merkleManagerAddress: strkeyToHex(stellar.merkleManager), + verifierAddress: strkeyToHex(stellar.verifier), + tokenName: "XLM", + tokenSymbol: "XLM", + tokenAddress: stellar.adTokenHex, + }, + }; + fs.writeFileSync(outPath, JSON.stringify(snapshot, null, 2)); + console.log(`[deploy] wrote snapshot → ${outPath}`); +} diff --git a/scripts/cross-chain-e2e/run.ts b/scripts/cross-chain-e2e/run.ts index 9428c60..ee7a1d0 100644 --- a/scripts/cross-chain-e2e/run.ts +++ b/scripts/cross-chain-e2e/run.ts @@ -4,7 +4,7 @@ * Deploys ProofBridge on a local Stellar network (ad chain) and Anvil (EVM * order chain), then exercises the full cross-chain bridge flow. * - * Four actors drive the flow (all provisioned by run_cross_chain_e2e.sh): + * Four actors drive the flow (all provisioned by scripts/start_chains.sh): * 1. stellarAdmin — configures AdManager on Stellar; ed25519 key signs * manager pre-auth for create_ad / lock_for_order / unlock. * 2. evmAdmin — configures OrderPortal on EVM; ECDSA key signs manager @@ -20,8 +20,6 @@ import * as path from "path"; import { ethers } from "ethers"; import { - deployContract, - deploySAC, invokeContract, getAddress, getSecret, @@ -29,11 +27,12 @@ import { strkeyToHex, evmAddressToBytes32, } from "./lib/stellar.js"; +import { NonceTracker, getContract } from "./lib/evm.js"; import { - deployEvmContracts, - NonceTracker, - getContract, -} from "./lib/evm.js"; + deployStellarChain, + deployEvmChain, + linkChains, +} from "./lib/deploy.js"; import { AuthTokenCounter, signEd25519, @@ -165,60 +164,26 @@ async function main() { // ════════════════════════════════════════════════════════════════ phase(1, "Deploy Stellar Contracts (ad chain)"); - console.log("Deploying Verifier..."); - const stellarVerifier = deployContract(path.join(WASM_DIR, "verifier.wasm"), [ - `--vk_bytes-file-path`, - VK_PATH, - ]); - console.log(` Verifier: ${stellarVerifier}`); - - console.log("Deploying MerkleManager..."); - const stellarMerkle = deployContract( - path.join(WASM_DIR, "merkle_manager.wasm"), - ); - console.log(` MerkleManager: ${stellarMerkle}`); - - invokeContract(stellarMerkle, "initialize", [`--admin`, stellarAdmin]); - - // Native XLM SAC — admin has friendbot-funded XLM the AdManager can pull - // on create_ad; the same SAC credits the order creator on Stellar unlock. - console.log("Deploying native XLM SAC..."); - const stellarAdToken = deploySAC("native"); - console.log(` NativeXLM SAC: ${stellarAdToken}`); - const stellarAdTokenHex = strkeyToHex(stellarAdToken); - - console.log("Deploying AdManager..."); - const stellarAdManager = deployContract( - path.join(WASM_DIR, "ad_manager.wasm"), - ); - console.log(` AdManager: ${stellarAdManager}`); - - invokeContract(stellarAdManager, "initialize", [ - `--admin`, - stellarAdmin, - `--verifier`, - stellarVerifier, - `--merkle_manager`, - stellarMerkle, - `--w_native_token`, - stellarAdToken, - `--chain_id`, - STELLAR_CHAIN_ID.toString(), - ]); - - invokeContract(stellarMerkle, "set_manager", [ - `--manager`, - stellarAdManager, - `--status`, - "true", - ]); - + const stellarDeploy = deployStellarChain({ + wasmDir: WASM_DIR, + vkPath: VK_PATH, + adminStrkey: stellarAdmin, + chainId: STELLAR_CHAIN_ID, + }); + const stellarVerifier = stellarDeploy.verifier; + const stellarMerkle = stellarDeploy.merkleManager; + const stellarAdToken = stellarDeploy.adToken; + const stellarAdTokenHex = stellarDeploy.adTokenHex; + const stellarAdManager = stellarDeploy.adManager; console.log("Stellar contracts deployed and initialized."); // Snapshot XLM balances before any contract movement so we can assert // deltas at the end: ad creator should lose AMOUNT (locked in the ad), // order creator should gain AMOUNT (received on Stellar unlock). - const adCreatorXlmBefore = stellarTokenBalance(stellarAdToken, adCreatorStellar); + const adCreatorXlmBefore = stellarTokenBalance( + stellarAdToken, + adCreatorStellar, + ); const orderCreatorXlmBefore = stellarTokenBalance( stellarAdToken, orderCreatorStellar, @@ -231,7 +196,12 @@ async function main() { // ════════════════════════════════════════════════════════════════ phase(2, "Deploy EVM Contracts (order chain)"); - const evm = await deployEvmContracts(EVM_RPC_URL, EVM_ADMIN_PRIVATE_KEY); + const evmDeploy = await deployEvmChain({ + rpcUrl: EVM_RPC_URL, + adminPrivateKey: EVM_ADMIN_PRIVATE_KEY, + chainId: EVM_CHAIN_ID, + }); + const evm = evmDeploy.contracts; const evmSigner = evm.signer; const nonces = evm.nonces; const evmAdmin = await evmSigner.getAddress(); @@ -270,55 +240,13 @@ async function main() { // ════════════════════════════════════════════════════════════════ phase(3, "Cross-Chain Linking"); - const stellarAdManagerHex = strkeyToHex(stellarAdManager); + await linkChains(stellarDeploy, evmDeploy); + + const stellarAdManagerHex = stellarDeploy.adManagerHex; const stellarAdManagerBuf = hexToBuffer(stellarAdManagerHex); const evmOrderPortalBytes32 = evmAddressToBytes32(evm.addresses.orderPortal); const evmTokenBytes32 = evmAddressToBytes32(evm.addresses.testToken); - console.log("Linking Stellar AdManager → EVM OrderPortal..."); - invokeContract(stellarAdManager, "set_chain", [ - `--order_chain_id`, - EVM_CHAIN_ID.toString(), - `--order_portal`, - evmOrderPortalBytes32.replace(/^0x/, ""), - `--supported`, - "true", - ]); - - console.log("Setting Stellar token route..."); - invokeContract(stellarAdManager, "set_token_route", [ - `--ad_token`, - stellarAdTokenHex.replace(/^0x/, ""), - `--order_token`, - evmTokenBytes32.replace(/^0x/, ""), - `--order_chain_id`, - EVM_CHAIN_ID.toString(), - ]); - - console.log("Linking EVM OrderPortal → Stellar AdManager..."); - { - const tx = await evm.orderPortal.getFunction("setChain")( - STELLAR_CHAIN_ID, - stellarAdManagerHex, - true, - { nonce: nonces.next() }, - ); - await tx.wait(); - } - - console.log("Setting EVM token route..."); - { - const tx = await evm.orderPortal.getFunction("setTokenRoute")( - evm.addresses.testToken, - STELLAR_CHAIN_ID, - stellarAdTokenHex, - { nonce: nonces.next() }, - ); - await tx.wait(); - } - - console.log("Cross-chain linking complete."); - // ════════════════════════════════════════════════════════════════ // Phase 4: Create Ad on Stellar // ════════════════════════════════════════════════════════════════ @@ -530,8 +458,12 @@ async function main() { console.log(` Target root: ${proofResult.targetRoot}`); console.log(` Bridger nullifier: ${proofResult.bridgerNullifier}`); console.log(` Ad-creator nullifier: ${proofResult.adCreatorNullifier}`); - console.log(` Bridger proof: ${proofResult.bridgerProof.length} bytes`); - console.log(` Ad-creator proof: ${proofResult.adCreatorProof.length} bytes`); + console.log( + ` Bridger proof: ${proofResult.bridgerProof.length} bytes`, + ); + console.log( + ` Ad-creator proof: ${proofResult.adCreatorProof.length} bytes`, + ); // ════════════════════════════════════════════════════════════════ // Phase 8: Order Creator Unlocks on Stellar (ad chain) @@ -643,15 +575,18 @@ async function main() { // EVM token accounting: // order creator: minted 10*AMOUNT, spent AMOUNT on createOrder → 9*AMOUNT // ad creator (EVM recipient): 0 → AMOUNT after unlock - const orderCreatorBalance = await evm.testToken.getFunction("balanceOf")( - orderCreatorEvm, - ); + const orderCreatorBalance = + await evm.testToken.getFunction("balanceOf")(orderCreatorEvm); const adRecipientBalance = await evm.testToken.getFunction("balanceOf")( AD_CREATOR_EVM_RECIPIENT, ); - console.log(` Order creator EVM token balance: ${orderCreatorBalance} (expected ${AMOUNT * 9n})`); - console.log(` Ad creator EVM token balance: ${adRecipientBalance} (expected ${AMOUNT})`); + console.log( + ` Order creator EVM token balance: ${orderCreatorBalance} (expected ${AMOUNT * 9n})`, + ); + console.log( + ` Ad creator EVM token balance: ${adRecipientBalance} (expected ${AMOUNT})`, + ); assert( orderCreatorBalance === AMOUNT * 9n, @@ -665,7 +600,10 @@ async function main() { // Stellar XLM accounting — allow small slop for network fees (both // accounts were the source of at least one tx). - const adCreatorXlmAfter = stellarTokenBalance(stellarAdToken, adCreatorStellar); + const adCreatorXlmAfter = stellarTokenBalance( + stellarAdToken, + adCreatorStellar, + ); const orderCreatorXlmAfter = stellarTokenBalance( stellarAdToken, orderCreatorStellar, diff --git a/scripts/relayer-e2e/cli.ts b/scripts/relayer-e2e/cli.ts new file mode 100644 index 0000000..1887df5 --- /dev/null +++ b/scripts/relayer-e2e/cli.ts @@ -0,0 +1,88 @@ +// CLI entrypoint for the relayer e2e lifecycle. +// +// Usage: +// tsx cli.ts deploy [--out ] +// tsx cli.ts seed [--in ] +// tsx cli.ts flows +// tsx cli.ts all [--out ] +// +// `deploy` brings up the on-chain state and writes a JSON snapshot. +// `seed` feeds that snapshot into Postgres via Prisma. +// `flows` drives the relayer over HTTP through the ad + trade lifecycles. +// `all` runs every step in-process; intended for local dev. In CI, the shell +// orchestrator in `e2e.sh` calls the subcommands individually so Docker can +// be started in between. + +import * as fs from "fs"; +import * as path from "path"; +import { deploy } from "./lib/deploy.js"; +import { seedDb, type DeployedContracts } from "./lib/seed.js"; +import { runAdLifecycle } from "./flows/ad-lifecycle.js"; +import { runTradeLifecycle } from "./flows/trade-lifecycle.js"; + +function parseFlag(argv: string[], name: string): string | undefined { + const i = argv.indexOf(name); + if (i === -1) return undefined; + return argv[i + 1]; +} + +function defaultSnapshotPath(): string { + return path.resolve(process.cwd(), "deployed.json"); +} + +function readSnapshot(p: string): DeployedContracts { + const raw = fs.readFileSync(p, "utf8"); + return JSON.parse(raw) as DeployedContracts; +} + +async function cmdDeploy(argv: string[]): Promise { + const out = parseFlag(argv, "--out") ?? defaultSnapshotPath(); + await deploy(out); +} + +async function cmdSeed(argv: string[]): Promise { + const inPath = parseFlag(argv, "--in") ?? defaultSnapshotPath(); + const snapshot = readSnapshot(inPath); + await seedDb(snapshot); +} + +async function cmdFlows(): Promise { + await runAdLifecycle(); + await runTradeLifecycle(); +} + +async function cmdAll(argv: string[]): Promise { + const out = parseFlag(argv, "--out") ?? defaultSnapshotPath(); + await deploy(out); + const snapshot = readSnapshot(out); + await seedDb(snapshot); + await cmdFlows(); +} + +async function main(): Promise { + const [, , cmd, ...rest] = process.argv; + switch (cmd) { + case "deploy": + await cmdDeploy(rest); + return; + case "seed": + await cmdSeed(rest); + return; + case "flows": + await cmdFlows(); + return; + case "all": + await cmdAll(rest); + return; + default: + console.error( + `Unknown command '${cmd ?? ""}'. Usage: tsx cli.ts {deploy|seed|flows|all} [--out/--in ]`, + ); + process.exit(2); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/relayer-e2e/e2e.sh b/scripts/relayer-e2e/e2e.sh new file mode 100755 index 0000000..9b378b8 --- /dev/null +++ b/scripts/relayer-e2e/e2e.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +# End-to-end driver for the backend-relayer lifecycle: +# 1. start_chains.sh — anvil + stellar localnet on the host +# 2. cli.ts deploy — deploy contracts + write deployed.json +# 3. docker compose up — postgres + backend-relayer (relayer runs +# prisma migrate deploy on boot) +# 4. cli.ts seed — seed Postgres from deployed.json +# 5. cli.ts flows — exercise ad + trade lifecycles over HTTP +# 6. teardown — compose down + stop_chains.sh +# +# Controlled via env: +# SKIP_START_CHAINS=1 — assume chains are already running and +# `.chains.env` is sourced. +# SKIP_TEARDOWN=1 — leave docker + chains running on exit. +# SNAPSHOT_PATH — override deployed.json location. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +RELAYER_DIR="$ROOT_DIR/apps/backend-relayer" +E2E_DIR="$ROOT_DIR/scripts/relayer-e2e" +SNAPSHOT_PATH="${SNAPSHOT_PATH:-$E2E_DIR/deployed.json}" + +COMPOSE_FILE="$RELAYER_DIR/docker-compose.e2e.yaml" +COMPOSE=(docker compose -f "$COMPOSE_FILE" -p relayer-e2e) + +# DB URL used from the host (postgres exposes 5433:5432 in the compose file). +HOST_DATABASE_URL="${HOST_DATABASE_URL:-postgresql://relayer:relayer@localhost:5433/relayer}" + +cleanup() { + local ec=$? + if [[ "${SKIP_TEARDOWN:-0}" == "1" ]]; then + echo "[e2e.sh] SKIP_TEARDOWN=1 — leaving services up." + exit "$ec" + fi + echo "[e2e.sh] tearing down…" + "${COMPOSE[@]}" logs backend-relayer > "$ROOT_DIR/.relayer.log" 2>&1 || true + "${COMPOSE[@]}" down --remove-orphans --volumes || true + if [[ "${SKIP_START_CHAINS:-0}" != "1" ]]; then + bash "$ROOT_DIR/scripts/stop_chains.sh" || true + fi + exit "$ec" +} +trap cleanup EXIT INT TERM + +# ── 1. chains ───────────────────────────────────────────────────────── +if [[ "${SKIP_START_CHAINS:-0}" != "1" ]]; then + echo "[e2e.sh] starting chains…" + bash "$ROOT_DIR/scripts/start_chains.sh" +fi + +if [[ -f "$ROOT_DIR/.chains.env" ]]; then + # shellcheck disable=SC1091 + source "$ROOT_DIR/.chains.env" +fi + +# ── 2. deploy ───────────────────────────────────────────────────────── +echo "[e2e.sh] deploying contracts…" +cd "$E2E_DIR" +pnpm --filter relayer-e2e exec tsx cli.ts deploy --out "$SNAPSHOT_PATH" +cd "$ROOT_DIR" + +# ── 3. compose up postgres + relayer ────────────────────────────────── +echo "[e2e.sh] starting postgres + backend-relayer containers…" +export STELLAR_ADMIN_SECRET="${STELLAR_ADMIN_SECRET:-}" +export STELLAR_NETWORK_PASSPHRASE="${STELLAR_NETWORK_PASSPHRASE:-Standalone Network ; February 2017}" +"${COMPOSE[@]}" up -d --build --wait + +# ── 4. seed ─────────────────────────────────────────────────────────── +echo "[e2e.sh] seeding database…" +# Generate prisma client against the relayer schema so seed.ts can import +# @prisma/client. Idempotent. +pnpm --filter backend-relayer exec prisma generate >/dev/null + +DATABASE_URL="$HOST_DATABASE_URL" \ + pnpm --filter relayer-e2e exec tsx cli.ts seed --in "$SNAPSHOT_PATH" + +# ── 5. flows ────────────────────────────────────────────────────────── +echo "[e2e.sh] running flows…" +RELAYER_URL="${RELAYER_URL:-http://localhost:2005}" \ +STELLAR_CHAIN_ID="${STELLAR_CHAIN_ID:-1000001}" \ +EVM_CHAIN_ID="${EVM_CHAIN_ID:-31337}" \ + pnpm --filter relayer-e2e exec tsx cli.ts flows + +echo "[e2e.sh] all phases passed ✓" diff --git a/scripts/relayer-e2e/flows/ad-lifecycle.ts b/scripts/relayer-e2e/flows/ad-lifecycle.ts new file mode 100644 index 0000000..ec18b72 --- /dev/null +++ b/scripts/relayer-e2e/flows/ad-lifecycle.ts @@ -0,0 +1,190 @@ +// Ad lifecycle flow — port of the `Ad lifecycle` jest test in +// apps/backend-relayer/test/integrations/eth-stellar.e2e-integration.ts. +// +// Ad creator is a Stellar identity; its EVM destination is the anvil +// prefunded #2 address. The flow: +// create → confirm → getAd → fund → confirm → withdraw → confirm → close → confirm + +import { Keypair, StrKey } from "@stellar/stellar-sdk"; +import { privateKeyToAccount } from "viem/accounts"; +import { + apiCreateAd, + apiConfirm, + apiFundAd, + apiWithdraw, + apiGetAd, + apiCloseAd, + getRoutes, + expectStatus, +} from "../lib/api.js"; +import { loginStellar } from "../lib/auth.js"; +import { toBaseUnits } from "../lib/amount.js"; +import { assert, assertObject, note, phase, step } from "../lib/assert.js"; +import { + createAdSoroban, + fundAdSoroban, + withdrawFromAdSoroban, + closeAdSoroban, +} from "../lib/stellar-actions.js"; + +export async function runAdLifecycle(): Promise { + phase("A", "Ad lifecycle"); + + const adCreatorSecret = process.env.STELLAR_AD_CREATOR_SECRET!; + const adCreator = Keypair.fromSecret(adCreatorSecret); + + const adCreatorEvmKey = process.env.EVM_AD_CREATOR_PRIVATE_KEY as `0x${string}`; + const adCreatorEvm = privateKeyToAccount(adCreatorEvmKey); + + const stellarChainId = process.env.STELLAR_CHAIN_ID!; + const evmChainId = process.env.EVM_CHAIN_ID!; + + note(`ad creator stellar: ${adCreator.publicKey()}`); + note(`ad creator evm dst: ${adCreatorEvm.address}`); + note(`stellar chain ${stellarChainId} → evm chain ${evmChainId}`); + + const route = await step("fetch routes", async () => { + const routes = expectStatus( + await getRoutes(stellarChainId, evmChainId), + 200, + "getRoutes", + ); + assert(routes.body.data.length > 0, "no routes seeded"); + const r = routes.body.data[0]; + note(`route ${r.id}`); + return r; + }); + + const access = await step("login (stellar SEP-10)", () => loginStellar(adCreator)); + + const INITIAL = toBaseUnits("50", "STELLAR"); + const adId = await step(`create ad (${INITIAL} base units)`, async () => { + const create = expectStatus( + await apiCreateAd(access, route.id, adCreatorEvm.address, INITIAL), + 201, + "apiCreateAd", + ); + const req = create.body as any; + note(`adId ${req.adId}`); + + const txCreate = await createAdSoroban( + adCreator, + req.signature, + req.signerPublicKey, + req.authToken, + req.timeToExpire, + adCreator.publicKey(), + req.adId, + req.adToken, + req.initialAmount, + req.orderChainId, + req.adRecipient, + req.contractAddress, + ); + note(`soroban tx ${txCreate}`); + expectStatus( + await apiConfirm(req.adId, access, txCreate as `0x${string}`), + 200, + "apiConfirm(create)", + ); + return req.adId as string; + }); + + await step("verify ad ACTIVE", async () => { + const adAfterCreate = expectStatus(await apiGetAd(adId), 200, "apiGetAd(create)"); + assertObject(adAfterCreate.body, { + id: adId, + status: "ACTIVE", + poolAmount: INITIAL, + }); + }); + + await step("fund ad (+5 XLM)", async () => { + const topup = expectStatus( + await apiFundAd(adId, access, toBaseUnits("5", "STELLAR")), + 200, + "apiFundAd", + ); + const txFund = await fundAdSoroban( + adCreator, + topup.body.signature, + topup.body.signerPublicKey, + topup.body.authToken, + topup.body.timeToExpire, + topup.body.adId, + topup.body.amount, + topup.body.contractAddress, + ); + note(`soroban tx ${txFund}`); + expectStatus( + await apiConfirm(adId, access, txFund as `0x${string}`), + 200, + "apiConfirm(fund)", + ); + }); + + await step("withdraw (-1 XLM)", async () => { + const withdraw = expectStatus( + await apiWithdraw( + adId, + access, + toBaseUnits("1", "STELLAR"), + adCreator.publicKey(), + ), + 200, + "apiWithdraw", + ); + const txW = await withdrawFromAdSoroban( + adCreator, + withdraw.body.signature, + withdraw.body.signerPublicKey, + withdraw.body.authToken, + withdraw.body.timeToExpire, + withdraw.body.adId, + withdraw.body.amount, + StrKey.isValidEd25519PublicKey(withdraw.body.to) + ? withdraw.body.to + : adCreator.publicKey(), + withdraw.body.contractAddress, + ); + note(`soroban tx ${txW}`); + expectStatus( + await apiConfirm(adId, access, txW as `0x${string}`), + 200, + "apiConfirm(withdraw)", + ); + }); + + await step("close ad", async () => { + const close = expectStatus( + await apiCloseAd(adId, access, { to: adCreator.publicKey() }), + 200, + "apiCloseAd", + ); + const txClose = await closeAdSoroban( + adCreator, + close.body.signature, + close.body.signerPublicKey, + close.body.authToken, + close.body.timeToExpire, + close.body.adId, + StrKey.isValidEd25519PublicKey(close.body.to) + ? close.body.to + : adCreator.publicKey(), + close.body.contractAddress, + ); + note(`soroban tx ${txClose}`); + expectStatus( + await apiConfirm(adId, access, txClose as `0x${string}`), + 200, + "apiConfirm(close)", + ); + }); + + await step("verify ad CLOSED", async () => { + const finalAd = expectStatus(await apiGetAd(adId), 200, "apiGetAd(final)"); + assertObject(finalAd.body, { status: "CLOSED", poolAmount: "0" }); + }); + + console.log("[ad-lifecycle] passed"); +} diff --git a/scripts/relayer-e2e/flows/trade-lifecycle.ts b/scripts/relayer-e2e/flows/trade-lifecycle.ts new file mode 100644 index 0000000..285103f --- /dev/null +++ b/scripts/relayer-e2e/flows/trade-lifecycle.ts @@ -0,0 +1,306 @@ +// Trade lifecycle flow — port of the `Trade lifecycle` jest test in +// apps/backend-relayer/test/integrations/eth-stellar.e2e-integration.ts. +// +// Ad on Stellar, order on EVM. Ad creator unlocks on EVM (ECDSA). Bridger +// unlocks on Stellar (ed25519 over the order hash). + +import { Keypair } from "@stellar/stellar-sdk"; +import { getAddress, parseEther } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { TypedDataEncoder } from "ethers"; +import { + apiCreateAd, + apiConfirm, + apiCreateOrder, + apiTradeConfirm, + apiLockOrder, + apiTradeParams, + apiUnlockOrder, + apiTradeUnlockConfirm, + apiGetTrade, + getRoutes, + expectStatus, +} from "../lib/api.js"; +import { loginStellar, loginEvm } from "../lib/auth.js"; +import { toBaseUnits } from "../lib/amount.js"; +import { assert, assertObject, note, phase, step } from "../lib/assert.js"; +import { + createAdSoroban, + lockForOrderSoroban, + unlockSoroban, +} from "../lib/stellar-actions.js"; +import { + createOrder, + unlockOrderChain, + mintToken, + approveToken, +} from "../lib/evm-actions.js"; +import { makeEthClient, fundEthAddress } from "../lib/eth.js"; +import { + domain, + orderTypes, + signTypedOrder, +} from "../../../apps/backend-relayer/src/providers/viem/ethers/typedData.js"; + +export async function runTradeLifecycle(): Promise { + phase("B", "Trade lifecycle"); + + const bridgerKey = process.env.EVM_ORDER_CREATOR_PRIVATE_KEY as `0x${string}`; + const bridger = privateKeyToAccount(bridgerKey); + + const bridgerStellarSecret = process.env.STELLAR_ORDER_CREATOR_SECRET!; + const bridgerStellar = Keypair.fromSecret(bridgerStellarSecret); + + const adCreatorSecret = process.env.STELLAR_AD_CREATOR_SECRET!; + const adCreator = Keypair.fromSecret(adCreatorSecret); + + const adCreatorEvmKey = process.env.EVM_AD_CREATOR_PRIVATE_KEY as `0x${string}`; + const adCreatorEvm = privateKeyToAccount(adCreatorEvmKey); + + const stellarChainId = process.env.STELLAR_CHAIN_ID!; + const evmChainId = process.env.EVM_CHAIN_ID!; + + note(`ad creator stellar: ${adCreator.publicKey()}`); + note(`ad creator evm dst: ${adCreatorEvm.address}`); + note(`bridger evm: ${bridger.address}`); + note(`bridger stellar dst: ${bridgerStellar.publicKey()}`); + note(`stellar chain ${stellarChainId} → evm chain ${evmChainId}`); + + const ethClient = makeEthClient(); + await step("fund evm participants", async () => { + await fundEthAddress(ethClient, bridger.address); + await fundEthAddress(ethClient, adCreatorEvm.address); + }); + + const route = await step("fetch routes", async () => { + const routes = expectStatus( + await getRoutes(stellarChainId, evmChainId), + 200, + "getRoutes", + ); + assert(routes.body.data.length > 0, "no routes seeded"); + const r = routes.body.data[0]; + note(`route ${r.id}`); + return r; + }); + + const adAccess = await step("login ad creator (stellar)", () => + loginStellar(adCreator), + ); + + const INITIAL = toBaseUnits("50", "STELLAR"); + const adId = await step(`seed ad (${INITIAL} base units)`, async () => { + const create = expectStatus( + await apiCreateAd(adAccess, route.id, adCreatorEvm.address, INITIAL), + 201, + "apiCreateAd", + ); + const req = create.body as any; + note(`adId ${req.adId}`); + const txCreate = await createAdSoroban( + adCreator, + req.signature, + req.signerPublicKey, + req.authToken, + req.timeToExpire, + adCreator.publicKey(), + req.adId, + req.adToken, + req.initialAmount, + req.orderChainId, + req.adRecipient, + req.contractAddress, + ); + note(`soroban tx ${txCreate}`); + expectStatus( + await apiConfirm(req.adId, adAccess, txCreate as `0x${string}`), + 200, + "apiConfirm(create)", + ); + return req.adId as string; + }); + + const bridgerAccess = await step("login bridger (evm SIWE)", () => + loginEvm(bridgerKey), + ); + + const { tradeId, orderReq, orderPortalAddress, tokenAddr20 } = await step( + "create order on EVM", + async () => { + const order = expectStatus( + await apiCreateOrder(bridgerAccess, { + adId, + routeId: route.id, + amount: toBaseUnits("10", "STELLAR"), + bridgerDstAddress: bridgerStellar.publicKey(), + }), + 201, + "apiCreateOrder", + ); + const orderReq = order.body.reqContractDetails; + const tradeId = order.body.tradeId as string; + const orderPortalAddress = orderReq.contractAddress as `0x${string}`; + const tokenAddr20 = getAddress( + `0x${orderReq.orderParams.orderChainToken.slice(-40)}`, + ); + note(`tradeId ${tradeId}`); + note(`orderPortal ${orderPortalAddress}`); + note(`orderChainToken ${tokenAddr20}`); + return { tradeId, orderReq, orderPortalAddress, tokenAddr20 }; + }, + ); + + await step("mint + approve EVM test token for bridger", async () => { + await mintToken( + ethClient, + bridger, + tokenAddr20, + bridger.address, + parseEther("1000"), + ); + await approveToken( + ethClient, + bridger, + tokenAddr20, + orderPortalAddress, + parseEther("100"), + ); + }); + + await step("submit + confirm EVM createOrder", async () => { + const orderCreateTx = await createOrder( + ethClient, + bridger, + orderReq.signature, + orderReq.authToken as `0x${string}`, + orderReq.timeToExpire, + orderReq.orderParams, + orderPortalAddress, + ); + note(`evm tx ${orderCreateTx}`); + expectStatus( + await apiTradeConfirm(tradeId, bridgerAccess, orderCreateTx), + 200, + "apiTradeConfirm(order)", + ); + }); + + await step("lock on Stellar ad chain", async () => { + const lockOrder = expectStatus( + await apiLockOrder(adAccess, tradeId), + 200, + "apiLockOrder", + ); + const lockReq = lockOrder.body; + const lockTxn = await lockForOrderSoroban( + adCreator, + lockReq.signature as `0x${string}`, + lockReq.signerPublicKey, + lockReq.authToken as `0x${string}`, + lockReq.timeToExpire, + lockReq.orderParams, + lockReq.contractAddress, + ); + note(`soroban tx ${lockTxn}`); + expectStatus( + await apiTradeConfirm(tradeId, adAccess, lockTxn as `0x${string}`), + 200, + "apiTradeConfirm(lock)", + ); + }); + + await step("verify trade LOCKED", async () => { + const afterLock = expectStatus( + await apiGetTrade(tradeId), + 200, + "apiGetTrade(after lock)", + ); + assertObject(afterLock.body, { status: "LOCKED" }); + }); + + await step("ad-creator unlocks on EVM (ECDSA)", async () => { + const adCreatorParams = expectStatus( + await apiTradeParams(adAccess, tradeId), + 200, + "apiTradeParams(adCreator)", + ); + const adCreatorOrderParams = adCreatorParams.body; + const adCreatorSig = await signTypedOrder( + adCreatorEvmKey, + adCreatorOrderParams, + ); + + const unlockOnOrder = expectStatus( + await apiUnlockOrder(adAccess, tradeId, adCreatorSig), + 200, + "apiUnlockOrder(adCreator)", + ); + const unlockOrderReq = unlockOnOrder.body; + const unlockOrderTx = await unlockOrderChain( + ethClient, + adCreatorEvm, + unlockOrderReq.signature, + unlockOrderReq.authToken as `0x${string}`, + unlockOrderReq.timeToExpire, + unlockOrderReq.orderParams, + unlockOrderReq.nullifierHash as `0x${string}`, + unlockOrderReq.targetRoot as `0x${string}`, + unlockOrderReq.proof as `0x${string}`, + unlockOrderReq.contractAddress, + ); + note(`evm tx ${unlockOrderTx}`); + expectStatus( + await apiTradeUnlockConfirm(adAccess, tradeId, unlockOrderTx), + 200, + "apiTradeUnlockConfirm(adCreator)", + ); + }); + + await step("bridger unlocks on Stellar (ed25519)", async () => { + const bridgerParams = expectStatus( + await apiTradeParams(bridgerAccess, tradeId), + 200, + "apiTradeParams(bridger)", + ); + const bridgerOrderParams = bridgerParams.body; + const bridgerOrderHash = TypedDataEncoder.hash(domain, orderTypes, { + ...bridgerOrderParams, + salt: BigInt(bridgerOrderParams.salt), + }); + const bridgerSigBytes = bridgerStellar.sign( + Buffer.from(bridgerOrderHash.replace(/^0x/, ""), "hex"), + ); + const bridgerSig = `0x${bridgerSigBytes.toString("hex")}`; + + const unlockOnAd = expectStatus( + await apiUnlockOrder(bridgerAccess, tradeId, bridgerSig), + 200, + "apiUnlockOrder(bridger)", + ); + const unlockAdReq = unlockOnAd.body; + const unlockAdTx = await unlockSoroban( + bridgerStellar, + unlockAdReq.signature, + unlockAdReq.signerPublicKey, + unlockAdReq.authToken, + unlockAdReq.timeToExpire, + unlockAdReq.orderParams, + unlockAdReq.nullifierHash, + unlockAdReq.targetRoot, + Buffer.from(unlockAdReq.proof.replace(/^0x/, ""), "hex"), + unlockAdReq.contractAddress, + ); + note(`soroban tx ${unlockAdTx}`); + expectStatus( + await apiTradeUnlockConfirm( + bridgerAccess, + tradeId, + unlockAdTx as `0x${string}`, + ), + 200, + "apiTradeUnlockConfirm(bridger)", + ); + }); + + console.log("[trade-lifecycle] passed"); +} diff --git a/scripts/relayer-e2e/lib/amount.ts b/scripts/relayer-e2e/lib/amount.ts new file mode 100644 index 0000000..4faf4c7 --- /dev/null +++ b/scripts/relayer-e2e/lib/amount.ts @@ -0,0 +1,18 @@ +// Chain-aware base-unit formatter — mirrors apps/backend-relayer/test/setups/amount.ts. + +import { parseUnits } from "viem"; + +export type ChainKind = "EVM" | "STELLAR"; + +export const DEFAULT_DECIMALS: Record = { + EVM: 18, + STELLAR: 7, +}; + +export function toBaseUnits( + amount: string, + chainKind: ChainKind, + decimals: number = DEFAULT_DECIMALS[chainKind] +): string { + return parseUnits(amount, decimals).toString(); +} diff --git a/scripts/relayer-e2e/lib/api.ts b/scripts/relayer-e2e/lib/api.ts new file mode 100644 index 0000000..6e7b5ba --- /dev/null +++ b/scripts/relayer-e2e/lib/api.ts @@ -0,0 +1,141 @@ +// HTTP client against the running backend-relayer. Mirrors the surface of +// apps/backend-relayer/test/integrations/api.ts but uses fetch instead of +// supertest so we can drive a containerized relayer over the network. + +const RELAYER_URL = process.env.RELAYER_URL ?? "http://localhost:2005"; + +export interface ApiResponse { + status: number; + ok: boolean; + body: T; +} + +async function request( + method: "GET" | "POST" | "PATCH" | "DELETE", + path: string, + opts: { body?: unknown; token?: string } = {} +): Promise> { + const headers: Record = { + "content-type": "application/json", + }; + if (opts.token) headers.authorization = `Bearer ${opts.token}`; + + const res = await fetch(`${RELAYER_URL}${path}`, { + method, + headers, + body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, + }); + + let body: any = null; + const text = await res.text(); + if (text) { + try { + body = JSON.parse(text); + } catch { + body = text; + } + } + + return { status: res.status, ok: res.ok, body }; +} + +function expectStatus(res: ApiResponse, expected: number, label: string): ApiResponse { + if (res.status !== expected) { + throw new Error( + `[${label}] expected status ${expected}, got ${res.status}: ${JSON.stringify(res.body)}` + ); + } + return res; +} + +// ── auth ────────────────────────────────────────────────────────────── + +export const apiAuthChallenge = (body: { address: string; chainKind: "EVM" | "STELLAR" }) => + request("POST", "/v1/auth/challenge", { body }); + +export const apiAuthLogin = (body: Record) => + request("POST", "/v1/auth/login", { body }); + +// ── routes ──────────────────────────────────────────────────────────── + +export const getRoutes = (fromChainId: string, toChainId: string) => + request("GET", `/v1/routes?fromChainId=${fromChainId}&toChainId=${toChainId}`); + +// ── ads ─────────────────────────────────────────────────────────────── + +export const apiCreateAd = ( + token: string, + routeId: string, + creatorDstAddress: string, + fundAmount: string +) => + request("POST", "/v1/ads/create", { + token, + body: { routeId, creatorDstAddress, fundAmount }, + }); + +export const apiConfirm = (adId: string, token: string, txHash: `0x${string}`) => + request("POST", `/v1/ads/${adId}/confirm`, { token, body: { txHash } }); + +export const apiFundAd = (adId: string, token: string, amount: string) => + request("POST", `/v1/ads/${adId}/fund`, { + token, + body: { poolAmountTopUp: amount }, + }); + +export const apiWithdraw = ( + adId: string, + token: string, + amount: string, + to: string +) => + request("POST", `/v1/ads/${adId}/withdraw`, { + token, + body: { poolAmountWithdraw: amount, to }, + }); + +export const apiGetAd = (adId: string) => request("GET", `/v1/ads/${adId}`); + +export const apiCloseAd = (adId: string, token: string, body: { to: string }) => + request("POST", `/v1/ads/${adId}/close`, { token, body }); + +// ── trades ──────────────────────────────────────────────────────────── + +export const apiCreateOrder = ( + token: string, + body: { + adId: string; + routeId: string; + amount: string; + bridgerDstAddress: string; + } +) => request("POST", "/v1/trades/create", { token, body }); + +export const apiGetTrade = (tradeId: string) => request("GET", `/v1/trades/${tradeId}`); + +export const apiTradeConfirm = (tradeId: string, token: string, txHash: `0x${string}`) => + request("POST", `/v1/trades/${tradeId}/confirm`, { token, body: { txHash } }); + +export const apiLockOrder = (token: string, tradeId: string) => + request("POST", `/v1/trades/${tradeId}/lock`, { token }); + +export const apiTradeParams = (token: string, tradeId: string) => + request("GET", `/v1/trades/${tradeId}/params`, { token }); + +export const apiUnlockOrder = (token: string, tradeId: string, signature: string) => + request("POST", `/v1/trades/${tradeId}/unlock`, { + token, + body: { signature }, + }); + +export const apiTradeUnlockConfirm = ( + token: string, + tradeId: string, + txHash: `0x${string}` +) => + request("POST", `/v1/trades/${tradeId}/unlock/confirm`, { + token, + body: { txHash }, + }); + +export { expectStatus, RELAYER_URL }; diff --git a/scripts/relayer-e2e/lib/assert.ts b/scripts/relayer-e2e/lib/assert.ts new file mode 100644 index 0000000..666aff3 --- /dev/null +++ b/scripts/relayer-e2e/lib/assert.ts @@ -0,0 +1,51 @@ +export function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(`ASSERT: ${message}`); +} + +export function assertEq(actual: T, expected: T, label = "value"): void { + if (actual !== expected) { + throw new Error( + `ASSERT: ${label} mismatch — expected ${JSON.stringify( + expected, + )}, got ${JSON.stringify(actual)}`, + ); + } +} + +export function assertObject( + obj: Record, + expected: Record, +): void { + for (const [k, v] of Object.entries(expected)) { + const a = obj[k]; + if (a !== v && JSON.stringify(a) !== JSON.stringify(v)) { + throw new Error( + `ASSERT: field '${k}' mismatch — expected ${JSON.stringify( + v, + )}, got ${JSON.stringify(a)}`, + ); + } + } +} + +export function phase(n: number | string, title: string): void { + const bar = "=".repeat(60); + console.log(`\n${bar}\nPhase ${n}: ${title}\n${bar}`); +} + +export async function step(label: string, fn: () => Promise): Promise { + const t0 = Date.now(); + console.log(` → ${label}`); + try { + const out = await fn(); + console.log(` ✓ ${label} (${Date.now() - t0}ms)`); + return out; + } catch (e) { + console.log(` ✗ ${label} (${Date.now() - t0}ms)`); + throw e; + } +} + +export function note(msg: string): void { + console.log(` · ${msg}`); +} diff --git a/scripts/relayer-e2e/lib/auth.ts b/scripts/relayer-e2e/lib/auth.ts new file mode 100644 index 0000000..a428eb5 --- /dev/null +++ b/scripts/relayer-e2e/lib/auth.ts @@ -0,0 +1,71 @@ +// HTTP login flows — EVM (SIWE) and Stellar (signed XDR). +// Port of apps/backend-relayer/test/setups/utils.ts:loginUser/loginStellarUser +// but driven by fetch against the running relayer. + +import { SiweMessage } from "siwe"; +import { privateKeyToAddress, signMessage } from "viem/accounts"; +import { + Keypair, + Networks, + TransactionBuilder, +} from "@stellar/stellar-sdk"; +import { apiAuthChallenge, apiAuthLogin, expectStatus } from "./api.js"; + +export async function loginEvm(privateKey: `0x${string}`): Promise { + const address = privateKeyToAddress(privateKey); + + const challenge = expectStatus( + await apiAuthChallenge({ address, chainKind: "EVM" }), + 200, + "auth.challenge(EVM)" + ); + + const now = new Date().toISOString(); + const exp = new Date(Date.now() + 5 * 60_000).toISOString(); + + const msg = new SiweMessage({ + domain: challenge.body.domain, + address, + statement: "Sign in to ProofBridge", + uri: challenge.body.uri, + version: "1", + chainId: 1, + nonce: challenge.body.nonce, + issuedAt: now, + expirationTime: exp, + }); + const message = msg.prepareMessage(); + const signature = await signMessage({ message, privateKey }); + + const login = expectStatus( + await apiAuthLogin({ message, signature, chainKind: "EVM" }), + 201, + "auth.login(EVM)" + ); + return login.body.tokens.access as string; +} + +export async function loginStellar(keypair: Keypair): Promise { + const challenge = expectStatus( + await apiAuthChallenge({ address: keypair.publicKey(), chainKind: "STELLAR" }), + 200, + "auth.challenge(STELLAR)" + ); + + const xdrString = challenge.body.transaction as string; + const passphrase = + (challenge.body.networkPassphrase as string) || + process.env.STELLAR_NETWORK_PASSPHRASE || + Networks.TESTNET; + + const tx = TransactionBuilder.fromXDR(xdrString, passphrase as Networks); + tx.sign(keypair); + const signedXdr = tx.toEnvelope().toXDR("base64"); + + const login = expectStatus( + await apiAuthLogin({ transaction: signedXdr, chainKind: "STELLAR" }), + 201, + "auth.login(STELLAR)" + ); + return login.body.tokens.access as string; +} diff --git a/scripts/relayer-e2e/lib/deploy.ts b/scripts/relayer-e2e/lib/deploy.ts new file mode 100644 index 0000000..c3e00f2 --- /dev/null +++ b/scripts/relayer-e2e/lib/deploy.ts @@ -0,0 +1,15 @@ +import * as path from "path"; +import { + deployAll, + writeDeployedSnapshot, + type DeployAllResult, +} from "cross-chain-e2e/lib/deploy.js"; + +export async function deploy(outPath?: string): Promise { + const result = await deployAll(); + const snapshotPath = outPath ?? path.resolve(process.cwd(), "deployed.json"); + writeDeployedSnapshot(snapshotPath, result); + return result; +} + +export { deployAll, writeDeployedSnapshot }; diff --git a/scripts/relayer-e2e/lib/eth.ts b/scripts/relayer-e2e/lib/eth.ts new file mode 100644 index 0000000..09e1c1b --- /dev/null +++ b/scripts/relayer-e2e/lib/eth.ts @@ -0,0 +1,85 @@ +import { ethers } from "ethers"; +import { + createPublicClient, + createWalletClient, + http, + parseEther, + type PublicClient, +} from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { ethLocalnet } from "../../../apps/backend-relayer/src/providers/viem/ethers/localnet.js"; + +export type AddressLike = `0x${string}`; + +export interface EthChainData { + adManagerAddress: AddressLike; + orderPortalAddress: AddressLike; + merkleManagerAddress: AddressLike; + verifierAddress: AddressLike; + chainId: string; + name: string; + tokenName: string; + tokenSymbol: string; + tokenAddress: AddressLike; +} + +export const makeEthClient = () => + createPublicClient({ chain: ethLocalnet, transport: http() }); + +async function tryTopUpViaRpc(addr: AddressLike, hexWei: string) { + const ethRpc = process.env.ETHEREUM_RPC_URL ?? "http://localhost:9545"; + const provider = new ethers.JsonRpcProvider(ethRpc); + + try { + await provider.send("anvil_setBalance", [addr, hexWei]); + return true; + } catch { + // ignore + } + + try { + await provider.send("hardhat_setBalance", [addr, hexWei]); + return true; + } catch { + // ignore + } + + return false; +} + +export async function fundEthAddress( + client: PublicClient, + to: AddressLike, + minBalanceEther = "1.0", +): Promise { + const needed = parseEther(minBalanceEther); + const current = await client.getBalance({ address: to }); + if (current >= needed) return; + const missing = needed - current; + + const funderKey = process.env.FUNDER_KEY as `0x${string}` | undefined; + + if (funderKey) { + const wallet = createWalletClient({ + chain: client.chain ?? ethLocalnet, + transport: http(), + account: privateKeyToAccount(funderKey), + }); + + const hash = await wallet.sendTransaction({ to, value: missing }); + await client.waitForTransactionReceipt({ hash }); + return; + } + + const ok = await tryTopUpViaRpc(to, `0x${needed.toString(16)}`); + if (!ok) { + throw new Error( + "Unable to fund address. Set FUNDER_KEY in env, or run against Anvil/Hardhat and allow *_setBalance.", + ); + } + + const funded = await client.getBalance({ address: to }); + if (funded < needed) { + throw new Error(`Unable to fund ${to} to ${minBalanceEther} ETH.`); + } +} diff --git a/apps/backend-relayer/test/setups/evm-actions.ts b/scripts/relayer-e2e/lib/evm-actions.ts similarity index 91% rename from apps/backend-relayer/test/setups/evm-actions.ts rename to scripts/relayer-e2e/lib/evm-actions.ts index cd98cb1..eda4825 100644 --- a/apps/backend-relayer/test/setups/evm-actions.ts +++ b/scripts/relayer-e2e/lib/evm-actions.ts @@ -1,15 +1,35 @@ -import { AD_MANAGER_ABI } from '../../src/providers/viem/abis/adManager.abi'; -import { ORDER_PORTAL_ABI } from '../../src/providers/viem/abis/orderPortal.abi'; -import { MERKLE_MANAGER_ABI } from '../../src/providers/viem/abis/merkleManager.abi'; - -import Erc20MockArtifact from '../../../../contracts/evm/out/ERC20Mock.sol/ERC20Mock.json'; +import * as fs from 'fs'; +import * as path from 'path'; import { ethers } from 'ethers'; -import { ChainData, AddressLike } from './utils'; +import { EthChainData, AddressLike } from './eth.js'; + +// Same pattern as scripts/cross-chain-e2e/lib/evm.ts — read Foundry output at +// runtime from $ROOT_DIR/contracts/evm/out. +const ROOT_DIR = process.env.ROOT_DIR!; +const EVM_OUT = path.join(ROOT_DIR ?? '', 'contracts/evm/out'); + +function loadArtifact(contractFileName: string, contractName: string) { + const artifactPath = path.join( + EVM_OUT, + `${contractFileName}.sol`, + `${contractName}.json`, + ); + if (!fs.existsSync(artifactPath)) { + throw new Error(`Artifact not found: ${artifactPath}`); + } + const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8')); + return { abi: artifact.abi, bytecode: artifact.bytecode.object }; +} + +const AD_MANAGER_ABI = loadArtifact('AdManager', 'AdManager').abi; +const ORDER_PORTAL_ABI = loadArtifact('OrderPortal', 'OrderPortal').abi; +const MERKLE_MANAGER_ABI = loadArtifact('MerkleManager', 'MerkleManager').abi; +const ERC20_MOCK_ABI = loadArtifact('ERC20Mock', 'ERC20Mock').abi; import { T_AdManagerOrderParams, T_OrderPortalParams, -} from '../../src/chain-adapters/types'; +} from '../../../apps/backend-relayer/src/chain-adapters/types.js'; import { createWalletClient, getAddress, @@ -23,7 +43,7 @@ const DEFAULT_ADMIN_ROLE = ethers.ZeroHash as `0x${string}`; export async function grantManagerRole( publicClient: PublicClient, account: PrivateKeyAccount, - chain: ChainData, + chain: EthChainData, ) { const mgrAddr = account.address; @@ -77,8 +97,8 @@ export async function grantManagerRole( export async function setupAdManager( publicClient: PublicClient, account: PrivateKeyAccount, - adChain: ChainData, - orderChain: ChainData, + adChain: EthChainData, + orderChain: EthChainData, ) { const mgrAddr = account.address; @@ -141,8 +161,8 @@ export async function setupAdManager( export async function setupOrderPortal( publicClient: PublicClient, account: PrivateKeyAccount, - adChain: ChainData, - orderChain: ChainData, + adChain: EthChainData, + orderChain: EthChainData, ) { const mgrAddr = account.address; @@ -205,8 +225,8 @@ export async function setupOrderPortal( export async function adminSetup( publicClient: PublicClient, account: PrivateKeyAccount, - chain1: ChainData, - chain2: ChainData, + chain1: EthChainData, + chain2: EthChainData, ) { // On the same provider chain (chain 1) // Setup roles and contracts @@ -590,7 +610,7 @@ export async function mintToken( const hash = await wallet.writeContract({ chain: publicClient.chain, address: tokenAddress, - abi: Erc20MockArtifact.abi, + abi: ERC20_MOCK_ABI, functionName: 'mint', args: [to, amount], }); @@ -622,7 +642,7 @@ export async function approveToken( const hash = await wallet.writeContract({ chain: publicClient.chain, address: tokenAddress, - abi: Erc20MockArtifact.abi, + abi: ERC20_MOCK_ABI, functionName: 'approve', args: [spender, amount], }); diff --git a/scripts/relayer-e2e/lib/seed.ts b/scripts/relayer-e2e/lib/seed.ts new file mode 100644 index 0000000..ea43549 --- /dev/null +++ b/scripts/relayer-e2e/lib/seed.ts @@ -0,0 +1,171 @@ +import { PrismaClient } from "@prisma/client"; +import { hash as argon2hash } from "@node-rs/argon2"; +import { ethers } from "ethers"; + +// `null` means the role wasn't deployed for this chain in the current flow; +// the Prisma columns are non-null so we substitute a recognizable sentinel at +// the DB boundary (see `sentinelFor`) and log a warning. +export interface DeployedContracts { + eth: { + name: string; + chainId: string; + adManagerAddress: string | null; + orderPortalAddress: string | null; + merkleManagerAddress: string; + verifierAddress: string; + tokenName: string; + tokenSymbol: string; + tokenAddress: string; + }; + stellar?: { + name: string; + chainId: string; + adManagerAddress: string | null; // 0x + 64 hex + orderPortalAddress: string | null; + merkleManagerAddress: string; + verifierAddress: string; + tokenName: string; + tokenSymbol: string; + tokenAddress: string; + }; +} + +function sentinelFor(role: string, chain: string, width: 40 | 64): string { + console.warn( + `[seed] ${chain} has no ${role} deployed — writing zero-address sentinel.`, + ); + return "0x" + "0".repeat(width); +} + +export async function seedDb(deployed: DeployedContracts): Promise { + const prisma = new PrismaClient(); + try { + await prisma.$connect(); + + // Admin. + const passwordHash = await argon2hash("ChangeMe123!"); + await prisma.admin.upsert({ + where: { email: "admin@x.com" }, + create: { email: "admin@x.com", passwordHash }, + update: { passwordHash }, + }); + + // EVM chain + token. + const ethAdManager = + deployed.eth.adManagerAddress ?? sentinelFor("adManager", "eth", 40); + const ethOrderPortal = + deployed.eth.orderPortalAddress ?? sentinelFor("orderPortal", "eth", 40); + const ethChain = await prisma.chain.upsert({ + where: { chainId: BigInt(deployed.eth.chainId) }, + create: { + name: deployed.eth.name, + chainId: BigInt(deployed.eth.chainId), + kind: "EVM", + adManagerAddress: ethAdManager, + orderPortalAddress: ethOrderPortal, + mmr: { create: { chainId: deployed.eth.chainId } }, + }, + update: { + name: deployed.eth.name, + adManagerAddress: ethAdManager, + orderPortalAddress: ethOrderPortal, + }, + select: { id: true }, + }); + + const ethToken = await prisma.token.upsert({ + where: { + chainUid_address: { + chainUid: ethChain.id, + address: deployed.eth.tokenAddress, + }, + }, + create: { + chainUid: ethChain.id, + symbol: deployed.eth.tokenSymbol, + name: deployed.eth.tokenName, + address: deployed.eth.tokenAddress, + decimals: 18, + kind: "ERC20", + }, + update: { + symbol: deployed.eth.tokenSymbol, + name: deployed.eth.tokenName, + decimals: 18, + kind: "ERC20", + }, + select: { id: true }, + }); + + // Stellar chain + token (optional). + if (deployed.stellar) { + const s = deployed.stellar; + const stellarAdManager = + s.adManagerAddress ?? sentinelFor("adManager", "stellar", 64); + const stellarOrderPortal = + s.orderPortalAddress ?? sentinelFor("orderPortal", "stellar", 64); + const stellarChain = await prisma.chain.upsert({ + where: { chainId: BigInt(s.chainId) }, + create: { + name: s.name, + chainId: BigInt(s.chainId), + kind: "STELLAR", + adManagerAddress: stellarAdManager, + orderPortalAddress: stellarOrderPortal, + mmr: { create: { chainId: s.chainId } }, + }, + update: { + name: s.name, + adManagerAddress: stellarAdManager, + orderPortalAddress: stellarOrderPortal, + }, + select: { id: true }, + }); + + const stellarToken = await prisma.token.upsert({ + where: { + chainUid_address: { + chainUid: stellarChain.id, + address: s.tokenAddress, + }, + }, + create: { + chainUid: stellarChain.id, + symbol: s.tokenSymbol, + name: s.tokenName, + address: s.tokenAddress, + decimals: 7, + kind: "NATIVE", + }, + update: { + symbol: s.tokenSymbol, + name: s.tokenName, + decimals: 7, + kind: "NATIVE", + }, + select: { id: true }, + }); + await prisma.route.upsert({ + where: { + orderTokenId_adTokenId: { + orderTokenId: ethToken.id, + adTokenId: stellarToken.id, + }, + }, + create: { + adTokenId: stellarToken.id, + orderTokenId: ethToken.id, + }, + update: {}, + }); + } + + console.log("[seed] done"); + } finally { + await prisma.$disconnect(); + } +} + +export function randomAddress(): string { + return ethers.Wallet.createRandom().address; +} diff --git a/apps/backend-relayer/test/setups/stellar-actions.ts b/scripts/relayer-e2e/lib/stellar-actions.ts similarity index 97% rename from apps/backend-relayer/test/setups/stellar-actions.ts rename to scripts/relayer-e2e/lib/stellar-actions.ts index 6c1f460..1cfcc7e 100644 --- a/apps/backend-relayer/test/setups/stellar-actions.ts +++ b/scripts/relayer-e2e/lib/stellar-actions.ts @@ -14,7 +14,10 @@ import { rpc, xdr, } from '@stellar/stellar-sdk'; -import { hex32ToBuffer, hex32ToContractId } from '../../src/providers/stellar/utils/address'; +import { + hex32ToBuffer, + hex32ToContractId, +} from '../../../apps/backend-relayer/src/providers/stellar/utils/address.js'; const BASE_FEE = '1000'; @@ -215,8 +218,8 @@ function orderParamsScVal(p: StellarOrderParams): xdr.ScVal { ['src_order_portal', bytesN(p.srcOrderPortal)], ]; return xdr.ScVal.scvMap( - entries.map(([k, v]) => - new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol(k), val: v }), + entries.map( + ([k, v]) => new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol(k), val: v }), ), ); } diff --git a/scripts/relayer-e2e/package.json b/scripts/relayer-e2e/package.json new file mode 100644 index 0000000..c64f962 --- /dev/null +++ b/scripts/relayer-e2e/package.json @@ -0,0 +1,23 @@ +{ + "name": "relayer-e2e", + "private": true, + "type": "module", + "scripts": { + "cli": "tsx cli.ts", + "e2e": "bash e2e.sh" + }, + "dependencies": { + "@node-rs/argon2": "^2.0.2", + "@prisma/client": "6.16.1", + "@stellar/stellar-sdk": "^15.0.1", + "cross-chain-e2e": "workspace:*", + "ethers": "^6.15.0", + "siwe": "^3.0.0", + "viem": "^2.37.7" + }, + "devDependencies": { + "@types/node": "^22.10.7", + "tsx": "^4.21.0", + "typescript": "^5.7.3" + } +} diff --git a/scripts/relayer-e2e/tsconfig.json b/scripts/relayer-e2e/tsconfig.json new file mode 100644 index 0000000..b1fb4b9 --- /dev/null +++ b/scripts/relayer-e2e/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": false, + "noEmit": true + }, + "include": [ + "cli.ts", + "lib/**/*.ts", + "flows/**/*.ts" + ] +} \ No newline at end of file diff --git a/scripts/run_cross_chain_e2e.sh b/scripts/start_chains.sh similarity index 57% rename from scripts/run_cross_chain_e2e.sh rename to scripts/start_chains.sh index dbd3b65..ec91d07 100755 --- a/scripts/run_cross_chain_e2e.sh +++ b/scripts/start_chains.sh @@ -1,12 +1,23 @@ #!/usr/bin/env bash set -euo pipefail -# Cross-chain E2E test orchestrator. +# Cross-chain dev-environment bring-up. # Starts a Stellar localnet (Docker) and Anvil (EVM), builds all prerequisites, -# then runs the TypeScript test that exercises the full bridge flow. +# funds keypairs, and exposes the resulting addresses/keys/secrets as +# environment variables. This script does NOT run any test — the caller is +# expected to consume the exported env and run whichever command they want +# (e.g. `npx tsx scripts/cross-chain-e2e/run.ts` or +# `pnpm --filter backend-relayer test:integrations`). +# +# Outputs: +# - Writes all exports to `/.chains.env` (source it in another shell). +# - Appends to $GITHUB_ENV when running under GitHub Actions so subsequent +# job steps inherit the values automatically. +# - Writes Anvil's PID to `/.chains.anvil.pid` so a teardown script +# can stop it later (Stellar container is stopped via `stellar container +# stop $STELLAR_CONTAINER_NAME`). ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SCRIPT_DIR="$ROOT_DIR/scripts/cross-chain-e2e" # ── configuration ──────────────────────────────────────────────────── @@ -15,9 +26,9 @@ NETWORK_NAME="${STELLAR_NETWORK_NAME:-local}" STELLAR_RPC_URL="${STELLAR_RPC_URL:-http://localhost:8000/soroban/rpc}" NETWORK_PASSPHRASE="${STELLAR_NETWORK_PASSPHRASE:-Standalone Network ; February 2017}" -ANVIL_PORT="${ANVIL_PORT:-8545}" -# Use 127.0.0.1 explicitly — on GitHub runners, Node resolves "localhost" -# to ::1 first, which fails to reach Anvil (bound on IPv4 0.0.0.0). +# apps/backend-relayer/src/providers/viem/ethers/localnet.ts pins 9545; +# scripts/cross-chain-e2e/run.ts reads EVM_RPC_URL so it is port-agnostic. +ANVIL_PORT="${ANVIL_PORT:-9545}" EVM_RPC_URL="http://127.0.0.1:$ANVIL_PORT" # ── user roles (4 actors drive the flow) ───────────────────────────── @@ -33,29 +44,15 @@ STELLAR_ADMIN_ACCOUNT="${STELLAR_ADMIN_ACCOUNT:-admin}" STELLAR_AD_CREATOR_ACCOUNT="${STELLAR_AD_CREATOR_ACCOUNT:-alice}" STELLAR_ORDER_CREATOR_ACCOUNT="${STELLAR_ORDER_CREATOR_ACCOUNT:-bridger}" -# Anvil prefunded keys — #0 admin, #1 order creator. +# Anvil prefunded keys — #0 admin, #1 order creator, #2 ad-creator EVM side. EVM_ADMIN_PRIVATE_KEY="${EVM_ADMIN_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}" EVM_ORDER_CREATOR_PRIVATE_KEY="${EVM_ORDER_CREATOR_PRIVATE_KEY:-0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d}" -# Ad creator's EVM recipient — receive-only, no key needed. Defaults to -# Anvil prefunded account #2's address. +EVM_AD_CREATOR_PRIVATE_KEY="${EVM_AD_CREATOR_PRIVATE_KEY:-0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a}" AD_CREATOR_EVM_RECIPIENT="${AD_CREATOR_EVM_RECIPIENT:-0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC}" # stellar.ts reads STELLAR_SOURCE_ACCOUNT as the default CLI source. export STELLAR_SOURCE_ACCOUNT="$STELLAR_ADMIN_ACCOUNT" -ANVIL_PID="" - -# ── cleanup ────────────────────────────────────────────────────────── - -cleanup() { - echo "" - echo "Cleaning up..." - [[ -n "$ANVIL_PID" ]] && kill "$ANVIL_PID" 2>/dev/null || true - stellar container stop "$CONTAINER_NAME" >/dev/null 2>&1 || true - echo "Done." -} -trap cleanup EXIT - # ── start Stellar localnet ─────────────────────────────────────────── echo "=== Starting Stellar quickstart container ===" @@ -109,19 +106,29 @@ for acct in "$STELLAR_ADMIN_ACCOUNT" "$STELLAR_AD_CREATOR_ACCOUNT" "$STELLAR_ORD done echo "Stellar accounts funded." +# Dump Stellar secret seeds so Node-side tests can sign without re-running +# the CLI. `stellar keys show` prints just the S… strkey. +STELLAR_ADMIN_SECRET="$(stellar keys show "$STELLAR_ADMIN_ACCOUNT")" +STELLAR_AD_CREATOR_SECRET="$(stellar keys show "$STELLAR_AD_CREATOR_ACCOUNT")" +STELLAR_ORDER_CREATOR_SECRET="$(stellar keys show "$STELLAR_ORDER_CREATOR_ACCOUNT")" + # ── start Anvil ────────────────────────────────────────────────────── echo "" echo "=== Starting Anvil (EVM devnet) ===" -# Kill any existing Anvil on the target port lsof -ti :"$ANVIL_PORT" | xargs -r kill -9 2>/dev/null || true sleep 1 -anvil --host 0.0.0.0 --port "$ANVIL_PORT" --block-time 2 --silent & +# nohup + disown keeps Anvil alive after this script exits, so downstream +# CI steps (or a separate dev shell) can talk to it. +nohup anvil --host 0.0.0.0 --port "$ANVIL_PORT" --block-time 2 --silent \ + > "$ROOT_DIR/.chains.anvil.log" 2>&1 & ANVIL_PID=$! +disown "$ANVIL_PID" || true +echo "$ANVIL_PID" > "$ROOT_DIR/.chains.anvil.pid" sleep 2 if ! kill -0 "$ANVIL_PID" 2>/dev/null; then - echo "Anvil failed to start" >&2 + echo "Anvil failed to start — see $ROOT_DIR/.chains.anvil.log" >&2 exit 1 fi echo "Anvil running on port $ANVIL_PORT (PID $ANVIL_PID)." @@ -148,25 +155,50 @@ cd "$ROOT_DIR/contracts/evm" forge build --silent cd "$ROOT_DIR" -# ── run the TypeScript test ────────────────────────────────────────── +# ── expose addresses, keys, and secrets ────────────────────────────── + +ENV_FILE="$ROOT_DIR/.chains.env" +: > "$ENV_FILE" + +emit() { + local key="$1" + local val="$2" + # Quote the value so passphrases with spaces/semicolons survive a round-trip + # through `source`. Single-quote-safe: escape any ' by closing, escaping, reopening. + local escaped="${val//\'/\'\\\'\'}" + echo "export ${key}='${escaped}'" >> "$ENV_FILE" + if [[ -n "${GITHUB_ENV:-}" ]]; then + # $GITHUB_ENV uses plain KEY=VALUE (no export, no quoting). + # Multiline-safe form isn't needed here since none of these contain newlines. + echo "${key}=${val}" >> "$GITHUB_ENV" + fi +} -echo "" -echo "=== Running cross-chain E2E test ===" - -export STELLAR_RPC_URL -export STELLAR_NETWORK="$NETWORK_NAME" -export STELLAR_NETWORK_PASSPHRASE="$NETWORK_PASSPHRASE" -export STELLAR_ADMIN_ACCOUNT -export STELLAR_AD_CREATOR_ACCOUNT -export STELLAR_ORDER_CREATOR_ACCOUNT -export EVM_RPC_URL -export EVM_ADMIN_PRIVATE_KEY -export EVM_ORDER_CREATOR_PRIVATE_KEY -export AD_CREATOR_EVM_RECIPIENT -export ROOT_DIR - -cd "$SCRIPT_DIR" -npx tsx run.ts +emit STELLAR_RPC_URL "$STELLAR_RPC_URL" +emit STELLAR_NETWORK "$NETWORK_NAME" +emit STELLAR_NETWORK_PASSPHRASE "$NETWORK_PASSPHRASE" +emit STELLAR_CONTAINER_NAME "$CONTAINER_NAME" +emit STELLAR_SOURCE_ACCOUNT "$STELLAR_ADMIN_ACCOUNT" +emit STELLAR_ADMIN_ACCOUNT "$STELLAR_ADMIN_ACCOUNT" +emit STELLAR_AD_CREATOR_ACCOUNT "$STELLAR_AD_CREATOR_ACCOUNT" +emit STELLAR_ORDER_CREATOR_ACCOUNT "$STELLAR_ORDER_CREATOR_ACCOUNT" +emit STELLAR_ADMIN_SECRET "$STELLAR_ADMIN_SECRET" +emit STELLAR_AD_CREATOR_SECRET "$STELLAR_AD_CREATOR_SECRET" +emit STELLAR_ORDER_CREATOR_SECRET "$STELLAR_ORDER_CREATOR_SECRET" +emit EVM_RPC_URL "$EVM_RPC_URL" +emit EVM_ADMIN_PRIVATE_KEY "$EVM_ADMIN_PRIVATE_KEY" +emit EVM_ORDER_CREATOR_PRIVATE_KEY "$EVM_ORDER_CREATOR_PRIVATE_KEY" +emit EVM_AD_CREATOR_PRIVATE_KEY "$EVM_AD_CREATOR_PRIVATE_KEY" +emit AD_CREATOR_EVM_RECIPIENT "$AD_CREATOR_EVM_RECIPIENT" +emit ROOT_DIR "$ROOT_DIR" echo "" -echo "=== Cross-chain E2E test passed! ===" +echo "=== Chains ready ===" +echo "Env written to: $ENV_FILE" +echo "Anvil log: $ROOT_DIR/.chains.anvil.log" +echo "Anvil PID file: $ROOT_DIR/.chains.anvil.pid" +echo "" +echo "Next steps:" +echo " - local dev: source $ENV_FILE && " +echo " - CI: env inherited via \$GITHUB_ENV in subsequent steps" +echo " - teardown: bash scripts/stop_chains.sh" diff --git a/scripts/stop_chains.sh b/scripts/stop_chains.sh new file mode 100755 index 0000000..f4f311b --- /dev/null +++ b/scripts/stop_chains.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -uo pipefail + +# Tears down the chains brought up by start_chains.sh. +# Stops Anvil (via PID file) and the Stellar quickstart container. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +ENV_FILE="$ROOT_DIR/.chains.env" +if [[ -f "$ENV_FILE" ]]; then + # shellcheck disable=SC1090 + source "$ENV_FILE" +fi + +PID_FILE="$ROOT_DIR/.chains.anvil.pid" +if [[ -f "$PID_FILE" ]]; then + ANVIL_PID="$(cat "$PID_FILE")" + if [[ -n "$ANVIL_PID" ]] && kill -0 "$ANVIL_PID" 2>/dev/null; then + CMD="$(ps -o comm= -p "$ANVIL_PID" 2>/dev/null || true)" + if [[ "$CMD" == anvil* ]]; then + echo "Stopping Anvil (PID $ANVIL_PID)..." + kill "$ANVIL_PID" 2>/dev/null || true + else + echo "Skipping stale PID $ANVIL_PID (now: '${CMD:-unknown}')." + fi + fi + rm -f "$PID_FILE" +fi + +CONTAINER_NAME="${STELLAR_CONTAINER_NAME:-stellar-e2e}" +echo "Stopping Stellar container '$CONTAINER_NAME'..." +stellar container stop "$CONTAINER_NAME" >/dev/null 2>&1 || true + +rm -f "$ROOT_DIR/.chains.env" "$ROOT_DIR/.chains.anvil.log" + +echo "Done."