diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..166bd7f3 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,79 @@ +name: E2E Tests + +on: + pull_request: + paths: + - 'cmd/**' + - 'pkg/**' + - 'tests/e2e/**' + - '.github/workflows/e2e.yml' + - 'docker-compose.yaml' + - 'Makefile' + - 'contracts/**' + - 'go.mod' + - 'go.sum' + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + repository-projects: read + +jobs: + e2e: + name: E2E Tests + runs-on: ubuntu-22.04 + + steps: + - name: Configure git SSH to HTTPS rewrite + # canton-erc20 and ethereum-wayfinder submodules use SSH URLs (git@github.com:). + # actions/checkout cannot authenticate SSH URLs via its token: input, so we + # rewrite them to HTTPS globally before the recursive submodule checkout. + run: git config --global url."https://${{ secrets.GH_PAT }}@github.com/".insteadOf "git@github.com:" + + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.24" + cache: true + + - name: Checkout canton-docker repository + uses: actions/checkout@v4 + with: + repository: chainsafe/canton-docker + token: ${{ secrets.GH_PAT }} + path: canton-docker + + - name: Build Canton Docker image + run: | + cd canton-docker + ./build_contianer.sh + + - name: Download dependencies + run: go mod download + + - name: Install Daml SDK + run: | + curl -sSL https://get.daml.com/ | sh -s -- 3.4.8 + echo "$HOME/.daml/bin" >> "$GITHUB_PATH" + + - name: Build DAML contracts + run: make build-dars + + - name: Generate CANTON_MASTER_KEY + run: echo "CANTON_MASTER_KEY=$(openssl rand -base64 32)" >> "$GITHUB_ENV" + + - name: Run E2E tests + run: make test-e2e + + - name: Stop devstack + if: always() + run: make devstack-down diff --git a/.gitignore b/.gitignore index 461d2993..e30df6f5 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,4 @@ docker-compose.remote.yaml # Binary output from demo interop-demo +/devstack diff --git a/Makefile b/Makefile index 7bac9f90..39858123 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test clean run setup db-up db-down docker-up docker-down deploy-contracts install-mockery check-mockery generate-mocks test-e2e test-e2e-api test-e2e-bridge test-e2e-indexer lint lint-e2e test-coverage test-coverage-check +.PHONY: build test clean run setup db-up db-down docker-up docker-down deploy-contracts install-mockery check-mockery generate-mocks build-dars devstack-up devstack-down test-e2e test-e2e-api test-e2e-bridge test-e2e-indexer lint lint-e2e test-coverage test-coverage-check GREEN := \033[0;32m RED := \033[0;31m @@ -134,17 +134,25 @@ setup: deps db-up cp config.example.yaml config.yaml @echo "Setup complete! Edit config.yaml and run 'make run'" -# E2E tests -test-e2e: test-e2e-api test-e2e-bridge test-e2e-indexer +CANTON_MASTER_KEY := $(or $(CANTON_MASTER_KEY),$(shell openssl rand -base64 32)) +export CANTON_MASTER_KEY -test-e2e-api: - CANTON_MASTER_KEY=$${CANTON_MASTER_KEY:-$$(openssl rand -base64 32)} \ - go test -v -tags e2e -timeout 10m ./tests/e2e/tests/api/... +build-dars: + ./scripts/setup/build-dars.sh -test-e2e-bridge: - CANTON_MASTER_KEY=$${CANTON_MASTER_KEY:-$$(openssl rand -base64 32)} \ - go test -v -tags e2e -timeout 15m ./tests/e2e/tests/bridge/... +devstack-up: + go run ./tests/e2e/cmd/devstack up -test-e2e-indexer: - CANTON_MASTER_KEY=$${CANTON_MASTER_KEY:-$$(openssl rand -base64 32)} \ - go test -v -tags e2e -timeout 20m ./tests/e2e/tests/indexer/... +devstack-down: + go run ./tests/e2e/cmd/devstack down + +test-e2e-api: devstack-up + go test -v -tags e2e -timeout 10m -parallel 4 ./tests/e2e/tests/api/... + +test-e2e-bridge: devstack-up + go test -v -tags e2e -timeout 15m -parallel 4 ./tests/e2e/tests/bridge/... + +test-e2e-indexer: devstack-up + go test -v -tags e2e -timeout 20m -parallel 4 ./tests/e2e/tests/indexer/... + +test-e2e: devstack-up test-e2e-api test-e2e-bridge test-e2e-indexer diff --git a/docker-compose.yaml b/docker-compose.yaml index ee56e43f..1ad62e48 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -43,6 +43,7 @@ services: deployer: image: ghcr.io/foundry-rs/foundry container_name: deployer + user: root depends_on: anvil: condition: service_healthy @@ -54,7 +55,7 @@ services: DEPLOYER_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" RELAYER_ADDRESS: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" ETH_RPC_URL: "http://anvil:8545" - entrypoint: /bin/sh -c "forge clean; while ! cast block-number --rpc-url http://anvil:8545 > /dev/null 2>&1; do sleep 1; done; forge script script/Deployer.s.sol --rpc-url http://anvil:8545 --broadcast" + entrypoint: /bin/sh -c "forge clean || true; while ! cast block-number --rpc-url http://anvil:8545 > /dev/null 2>&1; do sleep 1; done; forge script script/Deployer.s.sol --rpc-url http://anvil:8545 --broadcast" networks: - bridge-network diff --git a/pkg/transfer/service.go b/pkg/transfer/service.go index 193cfbdb..eb90df6e 100644 --- a/pkg/transfer/service.go +++ b/pkg/transfer/service.go @@ -8,6 +8,9 @@ import ( "strings" "time" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + apperrors "github.com/chainsafe/canton-middleware/pkg/app/errors" "github.com/chainsafe/canton-middleware/pkg/cantonsdk/token" "github.com/chainsafe/canton-middleware/pkg/user" @@ -142,6 +145,12 @@ func (s *TransferService) Execute(ctx context.Context, senderEVMAddr string, req SignedBy: req.SignedBy, }) if err != nil { + if st, ok := status.FromError(err); ok { + switch st.Code() { + case codes.InvalidArgument, codes.PermissionDenied: + return nil, apperrors.ForbiddenError(err, "signature verification failed") + } + } return nil, fmt.Errorf("execute transfer: %w", err) } diff --git a/pkg/transfer/service_test.go b/pkg/transfer/service_test.go index b4b2893d..5c15a373 100644 --- a/pkg/transfer/service_test.go +++ b/pkg/transfer/service_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + grpcstatus "google.golang.org/grpc/status" apperrors "github.com/chainsafe/canton-middleware/pkg/app/errors" "github.com/chainsafe/canton-middleware/pkg/cantonsdk/token" @@ -213,3 +215,28 @@ func TestTransferService_Execute_TransferExpired(t *testing.T) { }) assertServiceErrorCategory(t, err, apperrors.CategoryGone) } + +func TestTransferService_Execute_InvalidSignature_ReturnsForbidden(t *testing.T) { + ctx := context.Background() + sender := senderUser() + + store := mocks.NewUserStore(t) + store.EXPECT().GetUserByEVMAddress(ctx, sender.EVMAddress).Return(sender, nil).Once() + + pt := &token.PreparedTransfer{TransferID: "txn-sig-fail"} + cache := mocks.NewTransferCache(t) + cache.EXPECT().GetAndDelete("txn-sig-fail").Return(pt, nil).Once() + + cantonErr := grpcstatus.Error(codes.InvalidArgument, "signature verification failed") + tok := mocks.NewToken(t) + tok.EXPECT().ExecuteTransfer(ctx, mock.Anything).Return(cantonErr).Once() + + svc := newTestService(tok, store, cache) + + _, err := svc.Execute(ctx, sender.EVMAddress, &ExecuteRequest{ + TransferID: "txn-sig-fail", + Signature: "0xdeadbeef", + SignedBy: sender.CantonPublicKeyFingerprint, + }) + assertServiceErrorCategory(t, err, apperrors.CategoryForbidden) +} diff --git a/tests/e2e/cmd/devstack/main.go b/tests/e2e/cmd/devstack/main.go new file mode 100644 index 00000000..3c5de37a --- /dev/null +++ b/tests/e2e/cmd/devstack/main.go @@ -0,0 +1,90 @@ +// devstack manages the Docker Compose devstack lifecycle for E2E tests. +// +// Usage: +// +// go run ./tests/e2e/cmd/devstack +// +// up Starts the E2E devstack and waits for all services to be healthy. +// +// Sets SKIP_CANTON_SIG_VERIFY=true so Canton native registration tests +// work without a real Loop wallet signature. +// +// down Tears down the E2E devstack and removes all volumes. +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" +) + +const ( + composeFile = "tests/e2e/docker-compose.e2e.yaml" + projectName = "canton-e2e" +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "usage: devstack \n") + os.Exit(1) + } + ctx := context.Background() + var err error + switch os.Args[1] { + case "up": + err = up(ctx) + case "down": + err = down(ctx) + default: + fmt.Fprintf(os.Stderr, "unknown subcommand %q: want up or down\n", os.Args[1]) + os.Exit(1) + } + if err != nil { + fmt.Fprintf(os.Stderr, "devstack %s: %v\n", os.Args[1], err) + os.Exit(1) + } +} + +func up(ctx context.Context) error { + if err := compose(ctx, upEnv(), + "up", "--build", "--wait", "--remove-orphans", + ); err != nil { + fmt.Fprintln(os.Stderr, "=== deployer logs ===") + _ = compose(ctx, os.Environ(), "logs", "--no-color", "deployer") + return err + } + return nil +} + +func down(ctx context.Context) error { + return compose(ctx, os.Environ(), + "down", "-v", "--remove-orphans", + ) +} + +// upEnv returns the host environment with SKIP_CANTON_SIG_VERIFY forced to +// "true" so that Canton native registration tests work in the devnet. +func upEnv() []string { + const key = "SKIP_CANTON_SIG_VERIFY" + env := os.Environ() + for i, e := range env { + if strings.HasPrefix(e, key+"=") { + env[i] = key + "=true" + return env + } + } + return append(env, key+"=true") +} + +func compose(ctx context.Context, env []string, args ...string) error { + // #nosec G204 -- args are fixed constants from this file, not user input. + cmd := exec.CommandContext(ctx, "docker", append([]string{ + "compose", "-f", composeFile, "-p", projectName, + }, args...)...) + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + cmd.Env = env + return cmd.Run() +} diff --git a/tests/e2e/devstack/dsl/dsl.go b/tests/e2e/devstack/dsl/dsl.go index 2b29a734..e3c6e85f 100644 --- a/tests/e2e/devstack/dsl/dsl.go +++ b/tests/e2e/devstack/dsl/dsl.go @@ -12,10 +12,12 @@ import ( "math/big" "net/http" "strings" + "sync" "testing" "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/chainsafe/canton-middleware/pkg/keys" "github.com/chainsafe/canton-middleware/pkg/user" @@ -305,3 +307,63 @@ func (d *DSL) Withdraw(ctx context.Context, t *testing.T, partyID, fingerprint, return withdrawalReqCID } + +// anvilFundingMu serializes all NewFundedAccount calls so that concurrent +// parallel tests never race on AnvilAccount0's nonce. A package-level mutex is +// used because each test creates its own DSL instance; the mutex must be shared +// across instances. Each funding call internally waits for the transaction to +// mine, so the nonce is always monotonically incremented before the next caller +// acquires the lock. +var anvilFundingMu sync.Mutex + +// NewFundedAccount generates a fresh secp256k1 key and funds it from +// AnvilAccount0. eth is the amount of ETH to transfer (whole units, e.g. 1 for +// 1 ETH). tokens is the amount of the ERC-20 at tokenAddr to transfer (whole +// units, 18-decimal assumed). Pass 0 for either to skip that transfer. +// +// The method is safe to call from parallel tests. Internally it holds a +// package-level mutex while touching AnvilAccount0's nonce, so callers never +// race each other. The returned account is fully funded before the method +// returns. +func (d *DSL) NewFundedAccount(ctx context.Context, t *testing.T, eth int, tokenAddr common.Address, tokens int) stack.Account { + t.Helper() + if d.anvil == nil { + t.Fatal("NewFundedAccount not available: Anvil shim not initialized") + return stack.Account{} + } + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("NewFundedAccount: generate key: %v", err) + } + acc := stack.Account{ + Address: crypto.PubkeyToAddress(key.PublicKey), + PrivateKey: hex.EncodeToString(crypto.FromECDSA(key)), + } + + const ( + base = 10 + decimals = 18 + ) + exp18 := new(big.Int).Exp(big.NewInt(base), big.NewInt(decimals), nil) + + anvilFundingMu.Lock() + defer anvilFundingMu.Unlock() + + funder := stack.AnvilAccount0 + if eth > 0 { + ethWei := new(big.Int).Mul(big.NewInt(int64(eth)), exp18) + if err := d.anvil.FundWithETH(ctx, &funder, acc.Address, ethWei); err != nil { + t.Fatalf("NewFundedAccount: fund ETH: %v", err) + } + } + if tokens > 0 { + if (tokenAddr == common.Address{}) { + t.Fatalf("NewFundedAccount: tokens > 0 but tokenAddr is zero address") + } + tokenWei := new(big.Int).Mul(big.NewInt(int64(tokens)), exp18) + if err := d.anvil.TransferERC20(ctx, &funder, acc.Address, tokenAddr, tokenWei); err != nil { + t.Fatalf("NewFundedAccount: fund ERC20: %v", err) + } + } + return acc +} diff --git a/tests/e2e/devstack/presets/presets.go b/tests/e2e/devstack/presets/presets.go index 50fda476..bfa2dee8 100644 --- a/tests/e2e/devstack/presets/presets.go +++ b/tests/e2e/devstack/presets/presets.go @@ -9,9 +9,7 @@ package presets import ( "context" "fmt" - "os/signal" "sync" - "syscall" "testing" "time" @@ -20,46 +18,29 @@ import ( "github.com/chainsafe/canton-middleware/tests/e2e/devstack/system" ) -const stopTimeout = 60 * time.Second - var ( mu sync.Mutex manifest *stack.ServiceManifest ) -// DoMain starts the Docker Compose stack, resolves the service manifest, runs -// all tests via m.Run(), and tears down the stack when done. It must be called -// from TestMain. The exit code from m.Run() is returned and the caller should -// pass it to os.Exit. +// DoMain resolves the service manifest from the running devstack, then runs +// all tests via m.Run(). It must be called from TestMain. The exit code from +// m.Run() is returned and the caller should pass it to os.Exit. +// +// The devstack must be running before calling DoMain. Start it with: // -// SIGINT and SIGTERM are trapped so that Ctrl+C during a test run still -// triggers a clean docker compose down. +// make devstack-up // // func TestMain(m *testing.M) { os.Exit(presets.DoMain(m)) } func DoMain(m *testing.M, opts ...Option) int { o := applyOptions(opts) - // Signal-aware context: cancels on SIGINT/SIGTERM so in-flight docker - // operations (Start) abort promptly. Stop always uses a fresh context so - // it is not affected by signal cancellation. - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() - - orch := docker.NewOrchestrator(o.composeFile, o.projectName) - if err := orch.Start(ctx); err != nil { - fmt.Printf("devstack start: %v\n", err) - return 1 - } - defer func() { - stopCtx, cancel := context.WithTimeout(context.Background(), stopTimeout) - defer cancel() - _ = orch.Stop(stopCtx) - }() - disc := docker.NewServiceDiscovery(o.projectName, o.composeFile) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() mfst, err := disc.Manifest(ctx) if err != nil { - fmt.Printf("service discovery: %v\n", err) + fmt.Printf("service discovery failed (is the devstack running? try: make devstack-up): %v\n", err) return 1 } diff --git a/tests/e2e/tests/api/balance_test.go b/tests/e2e/tests/api/balance_test.go index 0c1f94e9..48f17228 100644 --- a/tests/e2e/tests/api/balance_test.go +++ b/tests/e2e/tests/api/balance_test.go @@ -10,12 +10,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/chainsafe/canton-middleware/tests/e2e/devstack/presets" - "github.com/chainsafe/canton-middleware/tests/e2e/devstack/stack" ) // TestERC20Balance_UnregisteredAddress_ReturnsZero checks that the ERC-20 // balance of a fresh address is zero, exercising the /eth JSON-RPC facade. func TestERC20Balance_UnregisteredAddress_ReturnsZero(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -36,6 +37,8 @@ func TestERC20Balance_UnregisteredAddress_ReturnsZero(t *testing.T) { // api-server's /eth JSON-RPC facade (balanceOf on the DEMO virtual EVM // address) reflects the new balance. func TestGetBalance_AfterMintDEMO(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -48,20 +51,23 @@ func TestGetBalance_AfterMintDEMO(t *testing.T) { } // TestERC20Balance_AfterDeposit_ReflectsChange verifies that after depositing -// PROMPT tokens via the bridge, the bridge contract's PROMPT balance increases. -// -// The deposit is submitted from stack.AnvilAccount0 because it is the only -// pre-funded Anvil account that holds both ETH (gas) and PROMPT tokens. -// sys.Accounts.User1 is derived per-test and is not funded on Anvil. +// PROMPT tokens via the bridge, the bridge contract's PROMPT balance increases +// by at least the deposited amount. func TestERC20Balance_AfterDeposit_ReflectsChange(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() tokenAddr := common.HexToAddress(sys.Manifest.PromptTokenAddr) bridgeAddr := common.HexToAddress(sys.Manifest.BridgeAddr) - // Register AnvilAccount0 so the api-server has a Canton party for it. - sys.DSL.RegisterUser(ctx, t, stack.AnvilAccount0) + // Fund a fresh isolated account with 1 ETH and 10 PROMPT. + // NewFundedAccount serializes AnvilAccount0 nonce ops internally. + account := sys.DSL.NewFundedAccount(ctx, t, 1, tokenAddr, 10) + + // Register account so the bridge knows a Canton party for the recipient. + sys.DSL.RegisterUser(ctx, t, account) // Check the bridge balance before deposit. balBefore, err := sys.Anvil.ERC20Balance(ctx, tokenAddr, bridgeAddr) @@ -69,19 +75,19 @@ func TestERC20Balance_AfterDeposit_ReflectsChange(t *testing.T) { t.Fatalf("erc20 balance before: %v", err) } - // AnvilAccount0 is pre-funded with PROMPT tokens and ETH for gas. depositAmount := new(big.Int).Mul(big.NewInt(10), new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)) - sys.DSL.Deposit(ctx, t, stack.AnvilAccount0, depositAmount) + sys.DSL.Deposit(ctx, t, account, depositAmount) - // Bridge contract should now hold depositAmount more PROMPT tokens. + // Bridge contract should hold at least depositAmount more PROMPT tokens. + // Use >= rather than == because concurrent parallel tests may also deposit. balAfter, err := sys.Anvil.ERC20Balance(ctx, tokenAddr, bridgeAddr) if err != nil { t.Fatalf("erc20 balance after: %v", err) } diff := new(big.Int).Sub(balAfter, balBefore) - if diff.Cmp(depositAmount) != 0 { - t.Fatalf("expected bridge balance to increase by %s, got diff %s (before=%s after=%s)", + if diff.Cmp(depositAmount) < 0 { + t.Fatalf("expected bridge balance to increase by at least %s, got diff %s (before=%s after=%s)", depositAmount, diff, balBefore, balAfter) } } diff --git a/tests/e2e/tests/api/register_test.go b/tests/e2e/tests/api/register_test.go index 1bb126b5..8962c36a 100644 --- a/tests/e2e/tests/api/register_test.go +++ b/tests/e2e/tests/api/register_test.go @@ -21,6 +21,8 @@ const registerMessage = "register" // TestRegister_Web3_Success verifies that a whitelisted EVM address can // register in custodial (web3) mode via POST /register. func TestRegister_Web3_Success(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -38,6 +40,8 @@ func TestRegister_Web3_Success(t *testing.T) { // a second time returns HTTP 409 Conflict. The api-server rejects duplicate // registrations rather than silently returning the existing record. func TestRegister_Duplicate_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -64,6 +68,8 @@ func TestRegister_Duplicate_Fails(t *testing.T) { // TestRegister_NotWhitelisted_Fails verifies that a non-whitelisted EVM // address is rejected with HTTP 403 Forbidden. func TestRegister_NotWhitelisted_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -93,6 +99,8 @@ func TestRegister_NotWhitelisted_Fails(t *testing.T) { // different private key is rejected. The server recovers the wrong EVM address // from the mismatched signature and rejects it as not whitelisted (HTTP 403). func TestRegister_InvalidSignature_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -121,6 +129,8 @@ func TestRegister_InvalidSignature_Fails(t *testing.T) { // TestRegister_MissingFields_Fails verifies that an empty POST /register body // is rejected with HTTP 401 (missing signature and message). func TestRegister_MissingFields_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -134,6 +144,8 @@ func TestRegister_MissingFields_Fails(t *testing.T) { // TestRegister_ExternalUser_TwoStep_Success verifies the non-custodial // two-step registration flow: prepare-topology → sign → register with external key. func TestRegister_ExternalUser_TwoStep_Success(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -161,6 +173,8 @@ func TestRegister_ExternalUser_TwoStep_Success(t *testing.T) { // Canton signature verification is skipped in the E2E devstack // (SKIP_CANTON_SIG_VERIFY=true) so no Loop wallet private key is needed. func TestRegister_CantonNative_Success(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -193,6 +207,8 @@ func TestRegister_CantonNative_Success(t *testing.T) { // TestRegister_CantonNative_Duplicate_Fails verifies that registering the // same Canton party ID a second time returns HTTP 409 Conflict. func TestRegister_CantonNative_Duplicate_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -220,6 +236,8 @@ func TestRegister_CantonNative_Duplicate_Fails(t *testing.T) { // TestRegister_CantonNative_InvalidPartyID_Fails verifies that a malformed // canton_party_id (missing the "hint::fingerprint" separator) returns HTTP 400. func TestRegister_CantonNative_InvalidPartyID_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -235,6 +253,8 @@ func TestRegister_CantonNative_InvalidPartyID_Fails(t *testing.T) { // TestRegister_PrepareTopology_MissingPublicKey_Fails verifies that step 1 of // external registration returns HTTP 400 when canton_public_key is absent. func TestRegister_PrepareTopology_MissingPublicKey_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -263,6 +283,8 @@ func TestRegister_PrepareTopology_MissingPublicKey_Fails(t *testing.T) { // TestRegister_PrepareTopology_NotWhitelisted_Fails verifies that step 1 of // external registration returns HTTP 403 when the EVM address is not whitelisted. func TestRegister_PrepareTopology_NotWhitelisted_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -293,6 +315,8 @@ func TestRegister_PrepareTopology_NotWhitelisted_Fails(t *testing.T) { // step 2 of external registration is rejected with HTTP 400 when the topology // signature is absent. func TestRegister_ExternalUser_MissingTopologySignature_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() diff --git a/tests/e2e/tests/api/registry_test.go b/tests/e2e/tests/api/registry_test.go index 8db470da..695541c0 100644 --- a/tests/e2e/tests/api/registry_test.go +++ b/tests/e2e/tests/api/registry_test.go @@ -15,6 +15,8 @@ import ( // contract_id. This exercises the Splice Registry API endpoint used by Canton // Loop wallet to discover the active TransferFactory contract. func TestTransferFactory_ReturnsContractID(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -31,6 +33,8 @@ func TestTransferFactory_ReturnsContractID(t *testing.T) { // /registry/transfer-instruction/v1/transfer-factory returns HTTP 405. The // handler only accepts POST per the Splice Registry API spec. func TestTransferFactory_MethodNotAllowed(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() diff --git a/tests/e2e/tests/api/rpc_test.go b/tests/e2e/tests/api/rpc_test.go index 44b8667b..6fc671ea 100644 --- a/tests/e2e/tests/api/rpc_test.go +++ b/tests/e2e/tests/api/rpc_test.go @@ -22,6 +22,8 @@ import ( // TestRPC_ChainID verifies that eth_chainId returns a non-zero chain ID. func TestRPC_ChainID(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -36,6 +38,8 @@ func TestRPC_ChainID(t *testing.T) { // TestRPC_BlockNumber verifies that eth_blockNumber returns without error. func TestRPC_BlockNumber(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -47,6 +51,8 @@ func TestRPC_BlockNumber(t *testing.T) { // TestRPC_GasPrice verifies that eth_gasPrice returns a non-nil value. func TestRPC_GasPrice(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -62,6 +68,8 @@ func TestRPC_GasPrice(t *testing.T) { // TestRPC_MaxPriorityFeePerGas verifies that eth_maxPriorityFeePerGas returns // a non-nil value. func TestRPC_MaxPriorityFeePerGas(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -77,6 +85,8 @@ func TestRPC_MaxPriorityFeePerGas(t *testing.T) { // TestRPC_EstimateGas verifies that eth_estimateGas returns a non-zero estimate // for a simple ETH transfer to a well-known address. func TestRPC_EstimateGas(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -96,6 +106,8 @@ func TestRPC_EstimateGas(t *testing.T) { // TestRPC_GetBalance verifies that eth_getBalance returns for a known address // without error. func TestRPC_GetBalance(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -111,6 +123,8 @@ func TestRPC_GetBalance(t *testing.T) { // TestRPC_GetTransactionCount verifies that eth_getTransactionCount returns // the nonce for a known address without error. func TestRPC_GetTransactionCount(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -123,6 +137,8 @@ func TestRPC_GetTransactionCount(t *testing.T) { // TestRPC_GetCode verifies that eth_getCode returns the bytecode for the // deployed PROMPT token contract (must be non-empty). func TestRPC_GetCode(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -139,6 +155,8 @@ func TestRPC_GetCode(t *testing.T) { // TestRPC_Syncing verifies that eth_syncing returns without error. The local // Anvil devnet is always synced so the result should be false. func TestRPC_Syncing(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -156,6 +174,8 @@ func TestRPC_Syncing(t *testing.T) { // go-ethereum to send "latest" as the block tag, which the api-server's // eth_getLogs handler rejects (it expects a hex uint64, not a block tag string). func TestRPC_GetLogs(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -227,6 +247,8 @@ func TestRPC_GetBlockByHash(t *testing.T) { // TestRPC_Call_TotalSupply verifies that eth_call with totalSupply() returns // 32 bytes (uint256) for the PROMPT token contract. func TestRPC_Call_TotalSupply(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -247,6 +269,8 @@ func TestRPC_Call_TotalSupply(t *testing.T) { // TestRPC_Call_Decimals verifies that eth_call with decimals() returns 32 // bytes encoding uint8 for the PROMPT token. func TestRPC_Call_Decimals(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -267,6 +291,8 @@ func TestRPC_Call_Decimals(t *testing.T) { // TestRPC_Call_Symbol verifies that eth_call with symbol() returns a non-empty // ABI-encoded string for the PROMPT token. func TestRPC_Call_Symbol(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -287,6 +313,8 @@ func TestRPC_Call_Symbol(t *testing.T) { // TestRPC_Call_Name verifies that eth_call with name() returns a non-empty // ABI-encoded string for the PROMPT token. func TestRPC_Call_Name(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -307,6 +335,8 @@ func TestRPC_Call_Name(t *testing.T) { // TestRPC_Call_BalanceOf_PROMPT verifies that eth_call with balanceOf() for a // fresh address returns a 32-byte zero value for the PROMPT token. func TestRPC_Call_BalanceOf_PROMPT(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -333,6 +363,8 @@ func TestRPC_Call_BalanceOf_PROMPT(t *testing.T) { // TestRPC_Call_BalanceOf_DEMO verifies that eth_call with balanceOf() on the // DEMO virtual EVM address returns a 32-byte zero value for a fresh address. func TestRPC_Call_BalanceOf_DEMO(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -362,6 +394,8 @@ func TestRPC_Call_BalanceOf_DEMO(t *testing.T) { // TestRPC_NetVersion verifies that net_version returns a non-empty chain ID // string. func TestRPC_NetVersion(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -376,6 +410,8 @@ func TestRPC_NetVersion(t *testing.T) { // TestRPC_NetListening verifies that net_listening returns true. func TestRPC_NetListening(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -390,6 +426,8 @@ func TestRPC_NetListening(t *testing.T) { // TestRPC_NetPeerCount verifies that net_peerCount returns without error. func TestRPC_NetPeerCount(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -406,6 +444,8 @@ func TestRPC_NetPeerCount(t *testing.T) { // TestRPC_Web3ClientVersion verifies that web3_clientVersion returns a // non-empty version string. func TestRPC_Web3ClientVersion(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -421,6 +461,8 @@ func TestRPC_Web3ClientVersion(t *testing.T) { // TestRPC_Web3Sha3 verifies that web3_sha3("0x") returns the keccak256 of // empty bytes — a well-known value used as a correctness check. func TestRPC_Web3Sha3(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() diff --git a/tests/e2e/tests/api/transfer_test.go b/tests/e2e/tests/api/transfer_test.go index 79ae9662..5b78547c 100644 --- a/tests/e2e/tests/api/transfer_test.go +++ b/tests/e2e/tests/api/transfer_test.go @@ -49,6 +49,8 @@ func signTransferHash(t *testing.T, kp interface { // register two external users, mint DEMO to User1, prepare+sign+execute a // transfer to User2, and assert User2's balance via the api-server RPC. func TestTransfer_DEMO_BetweenExternalUsers(t *testing.T) { + t.Parallel() + sys := presets.NewFullStack(t) ctx := context.Background() @@ -89,6 +91,8 @@ func TestTransfer_DEMO_BetweenExternalUsers(t *testing.T) { // TestTransfer_CustodialUser_PrepareRejects verifies that calling PrepareTransfer // for a custodial (web3) user returns HTTP 400 since the API requires external key mode. func TestTransfer_CustodialUser_PrepareRejects(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -108,6 +112,8 @@ func TestTransfer_CustodialUser_PrepareRejects(t *testing.T) { // TestTransfer_InvalidAmount_Zero verifies that PrepareTransfer with amount "0" // returns HTTP 400. func TestTransfer_InvalidAmount_Zero(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -127,6 +133,8 @@ func TestTransfer_InvalidAmount_Zero(t *testing.T) { // TestTransfer_InvalidAmount_Negative verifies that PrepareTransfer with a // negative amount returns HTTP 400. func TestTransfer_InvalidAmount_Negative(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -146,6 +154,8 @@ func TestTransfer_InvalidAmount_Negative(t *testing.T) { // TestTransfer_UnknownRecipient_Fails verifies that PrepareTransfer where the // recipient EVM address is not registered returns HTTP 400. func TestTransfer_UnknownRecipient_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -168,6 +178,8 @@ func TestTransfer_UnknownRecipient_Fails(t *testing.T) { // kp2 — the server finds User1's record via the correct fingerprint but fails // signature verification because kp2 ≠ kp1. func TestTransfer_InvalidSignature_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -213,6 +225,8 @@ func TestTransfer_InvalidSignature_Fails(t *testing.T) { // TestTransfer_InsufficientBalance_Fails verifies that PrepareTransfer for an // amount exceeding the user's canton balance returns HTTP 400. func TestTransfer_InsufficientBalance_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -239,6 +253,8 @@ func TestTransfer_InsufficientBalance_Fails(t *testing.T) { // /api/v2/transfer/prepare without X-Signature and X-Message headers returns // HTTP 401. This exercises the authentication gate before any business logic. func TestTransfer_MissingAuthHeaders_Returns401(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -275,6 +291,8 @@ func TestTransfer_MissingAuthHeaders_Returns401(t *testing.T) { // signed with a timestamp more than 5 minutes old is rejected with HTTP 401. // This exercises the replay-protection window enforced by ValidateTimedMessage. func TestTransfer_ExpiredTimestamp_Returns401(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -319,6 +337,8 @@ func TestTransfer_ExpiredTimestamp_Returns401(t *testing.T) { // with an empty request body (missing transfer_id, signature, signed_by) returns // HTTP 400. Auth headers are valid — the validation happens after authentication. func TestTransfer_Execute_MissingFields_Returns400(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() @@ -337,6 +357,8 @@ func TestTransfer_Execute_MissingFields_Returns400(t *testing.T) { // for the PROMPT token with no Canton balance returns HTTP 400, confirming that // PROMPT is a recognized token symbol and the balance gate fires correctly. func TestTransfer_PROMPT_InsufficientBalance_Fails(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background() diff --git a/tests/e2e/tests/bridge/deposit_test.go b/tests/e2e/tests/bridge/deposit_test.go index d35c87f4..aa3c5c36 100644 --- a/tests/e2e/tests/bridge/deposit_test.go +++ b/tests/e2e/tests/bridge/deposit_test.go @@ -7,32 +7,36 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum/common" + "github.com/chainsafe/canton-middleware/tests/e2e/devstack/presets" - "github.com/chainsafe/canton-middleware/tests/e2e/devstack/stack" ) // one18 is 1 × 10^18 — one full token unit expressed in wei (18-decimal tokens). var one18 = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) // TestDeposit_PROMPT_EthereumToCanton exercises the full EVM → Canton bridge -// deposit flow: AnvilAccount0 deposits PROMPT tokens, the relayer picks up the -// event, creates a PendingDeposit on Canton, and mints the corresponding PROMPT -// holding. The test asserts that the relayer records a completed transfer and -// that the Canton PROMPT balance reflects the deposit. +// deposit flow: a freshly funded account deposits PROMPT tokens, the relayer +// picks up the event, creates a PendingDeposit on Canton, and mints the +// corresponding PROMPT holding. The test asserts that the relayer records a +// completed transfer and that the Canton PROMPT balance reflects the deposit. func TestDeposit_PROMPT_EthereumToCanton(t *testing.T) { + t.Parallel() + sys := presets.NewFullStack(t) ctx := context.Background() - // AnvilAccount0 is pre-funded with PROMPT tokens and ETH for gas. - account := stack.AnvilAccount0 + admin := sys.Manifest.PromptInstrumentAdmin + id := sys.Manifest.PromptInstrumentID + tokenAddr := common.HexToAddress(sys.Manifest.PromptTokenAddr) + + // Fund a fresh isolated account. NewFundedAccount serializes AnvilAccount0 + // nonce operations internally, so t.Parallel() at the top is safe. + account := sys.DSL.NewFundedAccount(ctx, t, 1, tokenAddr, 1) // Register account so the api-server has a Canton party. regResp := sys.DSL.RegisterUser(ctx, t, account) - // Record the index-side admin and instrument ID for balance polling. - admin := sys.Manifest.PromptInstrumentAdmin - id := sys.Manifest.PromptInstrumentID - // Deposit 1 PROMPT (1e18 wei) into the bridge contract. depositAmount := new(big.Int).Set(one18) txHash := sys.DSL.Deposit(ctx, t, account, depositAmount) @@ -52,19 +56,20 @@ func TestDeposit_PROMPT_EthereumToCanton(t *testing.T) { // TestDeposit_SmallAmount_Succeeds verifies that a small PROMPT deposit (0.1 // tokens = 1e17 wei) is handled correctly end-to-end. This confirms there is no // minimum-amount gate in the relayer or DAML bridge. -// -// Uses AnvilAccount0 (the only pre-funded PROMPT account). WaitForCantonBalance -// and WaitForAPIBalance use >= semantics so accumulated balance from prior tests -// does not cause a false failure. func TestDeposit_SmallAmount_Succeeds(t *testing.T) { + t.Parallel() + sys := presets.NewFullStack(t) ctx := context.Background() - account := stack.AnvilAccount0 - regResp := sys.DSL.RegisterUser(ctx, t, account) - admin := sys.Manifest.PromptInstrumentAdmin id := sys.Manifest.PromptInstrumentID + tokenAddr := common.HexToAddress(sys.Manifest.PromptTokenAddr) + + // Fund with 1 PROMPT; deposit only 0.1 PROMPT from it. + account := sys.DSL.NewFundedAccount(ctx, t, 1, tokenAddr, 1) + + regResp := sys.DSL.RegisterUser(ctx, t, account) // 0.1 PROMPT = 1e17 wei depositAmount := new(big.Int).Div(one18, big.NewInt(10)) @@ -78,25 +83,22 @@ func TestDeposit_SmallAmount_Succeeds(t *testing.T) { // TestDeposit_TwoDeposits_Accumulate verifies that two sequential deposits from // the same address accumulate in the user's Canton balance. The relayer must // process both events independently and the indexer must reflect the sum. -// -// NOTE: This test shares AnvilAccount0 with TestDeposit_PROMPT_EthereumToCanton. -// Canton holdings for a party persist across tests, so AnvilAccount0's balance -// will already be >= 1 PROMPT when this test runs. The WaitForCantonBalance / -// WaitForAPIBalance assertions use >=, so they tolerate that pre-existing balance. -// What this test actually verifies is that each of the two deposits it submits -// is individually processed by the relayer and reflected in the balance. func TestDeposit_TwoDeposits_Accumulate(t *testing.T) { + t.Parallel() + sys := presets.NewFullStack(t) ctx := context.Background() - account := stack.AnvilAccount0 - regResp := sys.DSL.RegisterUser(ctx, t, account) - admin := sys.Manifest.PromptInstrumentAdmin id := sys.Manifest.PromptInstrumentID + tokenAddr := common.HexToAddress(sys.Manifest.PromptTokenAddr) + + // Fund with 2 PROMPT to cover two 1-PROMPT deposits. + account := sys.DSL.NewFundedAccount(ctx, t, 1, tokenAddr, 2) + + regResp := sys.DSL.RegisterUser(ctx, t, account) - // First deposit: 1 PROMPT. Balance check uses >= so prior accumulated - // holdings from other tests using AnvilAccount0 do not cause a false failure. + // First deposit: 1 PROMPT. tx1 := sys.DSL.Deposit(ctx, t, account, new(big.Int).Set(one18)) sys.DSL.WaitForRelayerTransfer(ctx, t, tx1.Hex()) sys.DSL.WaitForCantonBalance(ctx, t, regResp.Party, admin, id, "1") diff --git a/tests/e2e/tests/bridge/withdrawal_test.go b/tests/e2e/tests/bridge/withdrawal_test.go index feb74e1a..80cf9303 100644 --- a/tests/e2e/tests/bridge/withdrawal_test.go +++ b/tests/e2e/tests/bridge/withdrawal_test.go @@ -13,27 +13,29 @@ import ( "github.com/chainsafe/canton-middleware/pkg/transfer" "github.com/chainsafe/canton-middleware/tests/e2e/devstack/presets" - "github.com/chainsafe/canton-middleware/tests/e2e/devstack/stack" ) // TestWithdrawal_PROMPT_CantonToEthereum exercises the full Canton → EVM // withdrawal flow: -// 1. Register AnvilAccount0. -// 2. Deposit PROMPT tokens so the user has a Canton holding. +// 1. Fund a fresh isolated account with 1 ETH and 2 PROMPT. +// 2. Register the account and deposit 2 PROMPT via the bridge. // 3. Wait for the relayer to mint the Canton holding. // 4. Initiate a withdrawal via the WayfinderBridgeConfig DAML choice. // 5. Wait for the relayer to release tokens on Ethereum (EVM balance check). func TestWithdrawal_PROMPT_CantonToEthereum(t *testing.T) { + t.Parallel() + sys := presets.NewFullStack(t) ctx := context.Background() - account := stack.AnvilAccount0 - regResp := sys.DSL.RegisterUser(ctx, t, account) - admin := sys.Manifest.PromptInstrumentAdmin id := sys.Manifest.PromptInstrumentID tokenAddr := common.HexToAddress(sys.Manifest.PromptTokenAddr) + account := sys.DSL.NewFundedAccount(ctx, t, 1, tokenAddr, 2) + + regResp := sys.DSL.RegisterUser(ctx, t, account) + // Deposit 2 PROMPT to the bridge so there is a Canton holding to withdraw from. depositAmount := new(big.Int).Mul(big.NewInt(2), one18) txHash := sys.DSL.Deposit(ctx, t, account, depositAmount) @@ -63,16 +65,19 @@ func TestWithdrawal_PROMPT_CantonToEthereum(t *testing.T) { // Canton holding leaves the remainder on Canton. After the withdrawal, the // remaining Canton balance is >= the un-withdrawn portion. func TestWithdrawal_PartialAmount(t *testing.T) { + t.Parallel() + sys := presets.NewFullStack(t) ctx := context.Background() - account := stack.AnvilAccount0 - regResp := sys.DSL.RegisterUser(ctx, t, account) - admin := sys.Manifest.PromptInstrumentAdmin id := sys.Manifest.PromptInstrumentID tokenAddr := common.HexToAddress(sys.Manifest.PromptTokenAddr) + account := sys.DSL.NewFundedAccount(ctx, t, 1, tokenAddr, 3) + + regResp := sys.DSL.RegisterUser(ctx, t, account) + // Deposit 3 PROMPT to Canton. depositAmount := new(big.Int).Mul(big.NewInt(3), one18) txHash := sys.DSL.Deposit(ctx, t, account, depositAmount) @@ -101,36 +106,31 @@ func TestWithdrawal_PartialAmount(t *testing.T) { // that were not directly created by the deposit flow. // // Flow: -// 1. Fund sys.Accounts.User1 with ETH (gas) and PROMPT from AnvilAccount0. -// 2. Register User1 as external (PrepareTransfer requires external key mode). -// 3. Register User2 as external (receives the Canton transfer). -// 4. User1 deposits 2 PROMPT via the bridge. -// 5. Transfer 1 PROMPT from User1 to User2 via the api-server transfer API. -// 6. User2 initiates a withdrawal of 1 PROMPT to their EVM address. -// 7. Relayer releases 1 PROMPT to User2's EVM address. +// 1. Create a fresh funded sender account (1 ETH + 2 PROMPT). +// 2. Register sender as external (PrepareTransfer requires external key mode). +// 3. Register receiver as external (receives the Canton transfer). +// 4. Sender deposits 2 PROMPT via the bridge. +// 5. Transfer 1 PROMPT from sender to receiver via the api-server transfer API. +// 6. Receiver initiates a withdrawal of 1 PROMPT to their EVM address. +// 7. Relayer releases 1 PROMPT to receiver's EVM address. // -// User1 and User2 are derived from t.Name() so they are unique per test run -// and do not conflict with the custodial AnvilAccount0 registrations in other -// tests (PrepareTransfer requires key_mode=external, so a custodially-registered -// account cannot be reused here). +// Sender is a freshly generated account funded via NewFundedAccount. +// Receiver is derived from t.Name() — unique per test run, no EVM funding +// needed since it only receives a Canton transfer and withdraws through the relayer. func TestWithdrawal_AfterCantonTransfer(t *testing.T) { + t.Parallel() + sys := presets.NewFullStack(t) ctx := context.Background() - sender := sys.Accounts.User1 - receiver := sys.Accounts.User2 - tokenAddr := common.HexToAddress(sys.Manifest.PromptTokenAddr) depositAmount := new(big.Int).Mul(big.NewInt(2), one18) - // Fund sender with ETH for gas and PROMPT for the bridge deposit. - // AnvilAccount0 is the deployer and holds all initial PROMPT on EVM. - if err := sys.Anvil.FundWithETH(ctx, &stack.AnvilAccount0, sender.Address, one18); err != nil { - t.Fatalf("fund sender with ETH: %v", err) - } - if err := sys.Anvil.TransferERC20(ctx, &stack.AnvilAccount0, sender.Address, tokenAddr, depositAmount); err != nil { - t.Fatalf("fund sender with PROMPT: %v", err) - } + // Fresh funded sender. NewFundedAccount serializes AnvilAccount0 nonce ops. + sender := sys.DSL.NewFundedAccount(ctx, t, 1, tokenAddr, 2) + + // Receiver is derived per test — unique, no EVM funding needed. + receiver := sys.Accounts.User2 // Register sender as external (PrepareTransfer requires external key mode). regResp0, kp0 := sys.DSL.RegisterExternalUser(ctx, t, sender) diff --git a/tests/e2e/tests/indexer/mint_burn_test.go b/tests/e2e/tests/indexer/mint_burn_test.go index 19c1cf36..3785a3dd 100644 --- a/tests/e2e/tests/indexer/mint_burn_test.go +++ b/tests/e2e/tests/indexer/mint_burn_test.go @@ -12,7 +12,6 @@ import ( "github.com/chainsafe/canton-middleware/pkg/indexer" "github.com/chainsafe/canton-middleware/tests/e2e/devstack/presets" - "github.com/chainsafe/canton-middleware/tests/e2e/devstack/stack" ) var one18 = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) @@ -27,6 +26,8 @@ var one18 = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) // IssuerMint DAML choice — it indexes any TokenTransferEvent it observes. // Using a freshly allocated party guarantees no prior events to filter. func TestIndexer_MintEvent(t *testing.T) { + t.Parallel() + sys := presets.NewIndexerStack(t) ctx := context.Background() @@ -78,20 +79,20 @@ func TestIndexer_MintEvent(t *testing.T) { // created by the relayer when it processes a WithdrawalRequest — there is no // Canton-native path to create a BURN without the bridge. func TestIndexer_BurnEvent_AfterWithdrawal(t *testing.T) { + t.Parallel() + sys := presets.NewFullStack(t) ctx := context.Background() - // Use AnvilAccount0: it holds all PROMPT tokens (minted to the deployer at - // contract deployment). Account1 starts with zero PROMPT so its deposit - // would revert. MaxPartyEventOffset records a sinceOffset before the - // withdrawal so stale burn events from other tests are skipped. - account := stack.AnvilAccount0 - regResp := sys.DSL.RegisterUser(ctx, t, account) - admin := sys.Manifest.PromptInstrumentAdmin id := sys.Manifest.PromptInstrumentID tokenAddr := common.HexToAddress(sys.Manifest.PromptTokenAddr) + // Fresh isolated account: 1 ETH for gas, 2 PROMPT to deposit. + account := sys.DSL.NewFundedAccount(ctx, t, 1, tokenAddr, 2) + + regResp := sys.DSL.RegisterUser(ctx, t, account) + // Deposit 2 PROMPT so there is a holding large enough to withdraw 1 from. depositAmount := new(big.Int).Mul(big.NewInt(2), one18) txHash := sys.DSL.Deposit(ctx, t, account, depositAmount) @@ -112,9 +113,9 @@ func TestIndexer_BurnEvent_AfterWithdrawal(t *testing.T) { } // Record the highest BURN ledger offset already indexed for this party - // before initiating the withdrawal. AnvilAccount0 is a long-lived account - // reused across test runs; matching by fingerprint is not sufficient because - // the fingerprint is stable per registration and would match stale burns. + // before initiating the withdrawal. The account is freshly created so there + // are no prior burns — burnSinceOffset will be 0. The offset predicate still + // ensures we match only the burn produced by this withdrawal. burnSinceOffset := sys.DSL.MaxPartyEventOffset(ctx, t, regResp.Party, indexer.EventBurn) // Initiate a 1 PROMPT withdrawal to the same EVM address. @@ -129,7 +130,7 @@ func TestIndexer_BurnEvent_AfterWithdrawal(t *testing.T) { // Wait for the indexer to record a BURN event produced by this withdrawal. // Match by LedgerOffset > burnSinceOffset so that stale burns from prior - // test runs against AnvilAccount1 are not mistakenly accepted. + // test runs are not mistakenly accepted. ev := sys.DSL.WaitForPartyEventMatching(ctx, t, regResp.Party, indexer.EventBurn, func(e *indexer.ParsedEvent) bool { return e.LedgerOffset > burnSinceOffset diff --git a/tests/e2e/tests/indexer/transfer_test.go b/tests/e2e/tests/indexer/transfer_test.go index 2a057051..e9aa53bd 100644 --- a/tests/e2e/tests/indexer/transfer_test.go +++ b/tests/e2e/tests/indexer/transfer_test.go @@ -45,6 +45,8 @@ func signCantonTx(t *testing.T, kp interface { // Uses NewAPIStack (api-server + canton + postgres + indexer) rather than // NewFullStack because the relayer and Anvil are not needed for DEMO transfers. func TestIndexer_TransferEvent_AfterAPITransfer(t *testing.T) { + t.Parallel() + sys := presets.NewAPIStack(t) ctx := context.Background()