-
Notifications
You must be signed in to change notification settings - Fork 1
feat: [E2E] Layers 4/5/6 — System, DSL, and Presets #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
ed643d0
feat: [E2E] bootstrap writes deploy manifest for test discovery (#177)
sadiq1971 fa8dea2
feat: [E2E] Layer 1 — stack interfaces, types, compose override, Make…
sadiq1971 e8e23c2
used existing types
sadiq1971 2d8fb9c
used existing types
sadiq1971 00cc4a2
added per db dsn
sadiq1971 e79889a
feat: docker discovry added
sadiq1971 b163542
feat: [E2E] Layer 2 — shim implementations
sadiq1971 a9df74c
resolved gemni comments
sadiq1971 214bfef
removed unused db
sadiq1971 4854f06
feat: [E2E] Layers 4/5/6 — System, DSL, and Presets
sadiq1971 189e253
feat: [E2E] add IndexerSystem and APISystem subset views
sadiq1971 0078058
fix: address Gemini PR #193 review comments
sadiq1971 9e49dc7
merged main
sadiq1971 14a0e2d
lint fixed
sadiq1971 31e930c
api server to act as ethclient
sadiq1971 0505f87
fix lint
sadiq1971 dc9f255
optimized
sadiq1971 0930433
Merge branch 'main' into feat/e2e-layer2-shim
sadiq1971 7c295a1
Merge branch 'feat/e2e-layer2-shim' into feat/e2e-layer4-5-6-system-d…
sadiq1971 1d52eee
optimized implementation
sadiq1971 86d1290
merged base
sadiq1971 6538be0
merged base
sadiq1971 06c72a2
lint fixed
sadiq1971 91fe6a2
reolved gemini comments
sadiq1971 91f0e9a
feat: [E2E] Layer 2 — token client, error-safe NewCanton, HTTPError, …
sadiq1971 eb34f44
Merge branch 'feat/e2e-layer2-shim' into feat/e2e-layer4-5-6-system-d…
sadiq1971 54793e7
feat: [E2E] Layers 4/5/6 — Canton integration, token ops, WaitForAPIB…
sadiq1971 fe03317
resolved sigh hash bug
sadiq1971 a151f49
removed unused function
sadiq1971 3a33b48
fix lint
sadiq1971 fb5100e
merged main
sadiq1971 0927570
addressed pr comment
sadiq1971 d926bde
addressed pr comment
sadiq1971 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| //go:build e2e | ||
|
|
||
| // Package dsl provides high-level test operations built on top of the stack | ||
| // service interfaces. Methods accept *testing.T and call t.Fatal on error so | ||
| // tests read as plain imperative steps without error-handling boilerplate. | ||
| package dsl | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/hex" | ||
| "math/big" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/ethereum/go-ethereum/common" | ||
|
|
||
| "github.com/chainsafe/canton-middleware/pkg/keys" | ||
| "github.com/chainsafe/canton-middleware/pkg/user" | ||
| "github.com/chainsafe/canton-middleware/tests/e2e/devstack/stack" | ||
| "github.com/chainsafe/canton-middleware/tests/e2e/devstack/util" | ||
| ) | ||
|
|
||
| // DSL exposes high-level operations over the service interfaces. Accessed via | ||
| // System.DSL. | ||
| type DSL struct { | ||
| apiServer stack.APIServer | ||
| canton stack.Canton | ||
| relayer stack.Relayer | ||
| indexer stack.Indexer | ||
| postgres stack.APIDatabase | ||
| anvil stack.Anvil | ||
| } | ||
|
|
||
| const ( | ||
| decimalBase = 10 | ||
| waitForAPIBalanceTimeout = 60 * time.Second | ||
| ) | ||
|
|
||
| // New wires a DSL to the given service interfaces. canton, relayer, and indexer | ||
| // may be nil when the system under test does not include those services; calling | ||
| // DSL methods that require them will produce a descriptive t.Fatal message. | ||
| func New( | ||
| api stack.APIServer, | ||
| canton stack.Canton, | ||
| relayer stack.Relayer, | ||
| indexer stack.Indexer, | ||
| postgres stack.APIDatabase, | ||
| anvil stack.Anvil, | ||
| ) *DSL { | ||
| return &DSL{ | ||
| apiServer: api, | ||
| canton: canton, | ||
| relayer: relayer, | ||
| indexer: indexer, | ||
| postgres: postgres, | ||
| anvil: anvil, | ||
| } | ||
| } | ||
|
|
||
| // RegisterUser whitelists the account's EVM address and registers it as a | ||
| // custodial web3 user via POST /register. Returns the RegisterResponse. | ||
| func (d *DSL) RegisterUser(ctx context.Context, t *testing.T, account stack.Account) *user.RegisterResponse { | ||
| t.Helper() | ||
|
|
||
| if err := d.postgres.WhitelistAddress(ctx, account.Address.Hex()); err != nil { | ||
| t.Fatalf("whitelist %s: %v", account.Address.Hex(), err) | ||
| } | ||
|
|
||
| msg := "register" | ||
| sig, err := util.SignEIP191(account.PrivateKey, msg) | ||
| if err != nil { | ||
| t.Fatalf("sign register message: %v", err) | ||
| } | ||
|
|
||
| resp, err := d.apiServer.Register(ctx, &user.RegisterRequest{ | ||
| Signature: sig, | ||
| Message: msg, | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("register %s: %v", account.Address.Hex(), err) | ||
| } | ||
| return resp | ||
| } | ||
|
|
||
| // RegisterExternalUser whitelists account's EVM address and completes the | ||
| // two-step external (non-custodial) registration flow: | ||
| // 1. Generates a fresh secp256k1 Canton keypair. | ||
| // 2. Calls POST /register/prepare-topology to get the topology hash. | ||
| // 3. Signs the topology hash with the Canton key (DER, SHA-256). | ||
| // 4. Calls POST /register with key_mode=external. | ||
| // | ||
| // Returns the RegisterResponse and the Canton keypair (needed to sign transfers). | ||
| func (d *DSL) RegisterExternalUser(ctx context.Context, t *testing.T, account stack.Account) (*user.RegisterResponse, *keys.CantonKeyPair) { | ||
| t.Helper() | ||
|
|
||
| if err := d.postgres.WhitelistAddress(ctx, account.Address.Hex()); err != nil { | ||
| t.Fatalf("whitelist %s: %v", account.Address.Hex(), err) | ||
| } | ||
|
|
||
| kp, err := keys.GenerateCantonKeyPair() | ||
| if err != nil { | ||
| t.Fatalf("generate canton keypair: %v", err) | ||
| } | ||
|
|
||
| msg := "register" | ||
| sig, err := util.SignEIP191(account.PrivateKey, msg) | ||
| if err != nil { | ||
| t.Fatalf("sign register message: %v", err) | ||
| } | ||
|
|
||
| topoResp, err := d.apiServer.PrepareTopology(ctx, &user.RegisterRequest{ | ||
| Signature: sig, | ||
| Message: msg, | ||
| CantonPublicKey: kp.PublicKeyHex(), | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("prepare-topology %s: %v", account.Address.Hex(), err) | ||
| } | ||
|
|
||
| // TopologyHash is "0x" + hex(multiHash). Sign raw bytes (SignDER SHA-256s internally). | ||
| hashBytes, err := hex.DecodeString(strings.TrimPrefix(topoResp.TopologyHash, "0x")) | ||
| if err != nil { | ||
| t.Fatalf("decode topology hash: %v", err) | ||
| } | ||
| derSig, err := kp.SignDER(hashBytes) | ||
| if err != nil { | ||
| t.Fatalf("sign topology hash: %v", err) | ||
| } | ||
| topologySig := "0x" + hex.EncodeToString(derSig) | ||
|
|
||
| resp, err := d.apiServer.RegisterExternal(ctx, &user.RegisterRequest{ | ||
| Signature: sig, | ||
| Message: msg, | ||
| KeyMode: user.KeyModeExternal, | ||
| CantonPublicKey: kp.PublicKeyHex(), | ||
| RegistrationToken: topoResp.RegistrationToken, | ||
| TopologySignature: topologySig, | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("register-external %s: %v", account.Address.Hex(), err) | ||
| } | ||
| return resp, kp | ||
| } | ||
|
|
||
| // MintDEMO mints amount of DEMO tokens to recipientParty via the Canton | ||
| // ledger (IssuerMint DAML choice). Requires a Canton shim with token client. | ||
| func (d *DSL) MintDEMO(ctx context.Context, t *testing.T, recipientParty, amount string) { | ||
| t.Helper() | ||
| if d.canton == nil { | ||
| t.Fatal("MintDEMO not available: Canton shim not initialized") | ||
| return | ||
| } | ||
| if err := d.canton.MintToken(ctx, recipientParty, "DEMO", amount); err != nil { | ||
| t.Fatalf("mint DEMO to %s: %v", recipientParty, err) | ||
| } | ||
| } | ||
|
|
||
| // Deposit approves the bridge and submits a depositToCanton transaction on | ||
| // behalf of account. Returns the deposit transaction hash. | ||
| func (d *DSL) Deposit(ctx context.Context, t *testing.T, account stack.Account, amount *big.Int) common.Hash { | ||
| t.Helper() | ||
| hash, err := d.anvil.ApproveAndDeposit(ctx, &account, amount) | ||
| if err != nil { | ||
| t.Fatalf("deposit for %s: %v", account.Address.Hex(), err) | ||
| } | ||
| return hash | ||
| } | ||
|
|
||
| // WaitForAPIBalance polls the api-server's /eth JSON-RPC facade until the | ||
| // ERC-20 balance of ownerAddr for tok is >= minTokens (human-readable token | ||
| // amount, e.g. "50"). This is the preferred balance check for api-server tests | ||
| // — no indexer needed. Pass sys.Tokens.DEMO or sys.Tokens.PROMPT as tok. | ||
| func (d *DSL) WaitForAPIBalance(ctx context.Context, t *testing.T, tok stack.Token, ownerAddr common.Address, minTokens string) { | ||
| t.Helper() | ||
| // Scale minTokens by 10^tok.Decimals. | ||
| exp := new(big.Int).Exp(big.NewInt(decimalBase), big.NewInt(int64(tok.Decimals)), nil) | ||
| minF, ok := new(big.Float).SetString(minTokens) | ||
| if !ok { | ||
| t.Fatalf("WaitForAPIBalance: invalid amount %q", minTokens) | ||
| } | ||
| minF.Mul(minF, new(big.Float).SetInt(exp)) | ||
| minWei, _ := minF.Int(nil) | ||
|
|
||
| deadline := time.Now().Add(waitForAPIBalanceTimeout) | ||
| ticker := time.NewTicker(pollInterval) | ||
| defer ticker.Stop() | ||
| var lastErr error | ||
| var lastBal *big.Int | ||
| for time.Now().Before(deadline) { | ||
| bal, err := d.apiServer.ERC20Balance(ctx, tok.Address, ownerAddr) | ||
| if err != nil { | ||
| lastErr = err | ||
| } else { | ||
| lastBal = bal | ||
| if bal.Cmp(minWei) >= 0 { | ||
| return | ||
| } | ||
| } | ||
| select { | ||
| case <-ctx.Done(): | ||
| t.Fatal("context canceled waiting for API balance") | ||
| case <-ticker.C: | ||
| } | ||
| } | ||
| if lastErr != nil { | ||
| t.Fatalf("WaitForAPIBalance: timed out waiting for %s %s balance >= %s (owner %s): last error: %v", | ||
| minTokens, tok.Symbol, minWei, ownerAddr.Hex(), lastErr) | ||
| } | ||
| if lastBal != nil { | ||
| t.Fatalf("WaitForAPIBalance: timed out waiting for %s %s balance >= %s (owner %s): last seen balance: %s", | ||
| minTokens, tok.Symbol, minWei, ownerAddr.Hex(), lastBal.String()) | ||
| } | ||
| t.Fatalf("WaitForAPIBalance: timed out waiting for %s %s balance >= %s (owner %s)", | ||
| minTokens, tok.Symbol, minWei, ownerAddr.Hex()) | ||
| } | ||
|
|
||
| // ERC20Balance returns the on-chain ERC-20 balance of account for tokenAddr. | ||
| func (d *DSL) ERC20Balance(ctx context.Context, t *testing.T, tokenAddr common.Address, account stack.Account) *big.Int { | ||
| t.Helper() | ||
| bal, err := d.anvil.ERC20Balance(ctx, tokenAddr, account.Address) | ||
| if err != nil { | ||
| t.Fatalf("erc20 balance for %s: %v", account.Address.Hex(), err) | ||
| } | ||
| return bal | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| //go:build e2e | ||
|
|
||
| package dsl | ||
|
|
||
| import ( | ||
| "context" | ||
| "math/big" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/chainsafe/canton-middleware/pkg/indexer" | ||
| "github.com/chainsafe/canton-middleware/pkg/relayer" | ||
| ) | ||
|
sadiq1971 marked this conversation as resolved.
|
||
|
|
||
| const ( | ||
| pollInterval = 500 * time.Millisecond | ||
| relayerReadyTimeout = 60 * time.Second | ||
| cantonBalanceTimeout = 60 * time.Second | ||
| relayerTransferTimeout = 120 * time.Second | ||
| indexerEventTimeout = 60 * time.Second | ||
| ) | ||
|
|
||
| // WaitForRelayerReady polls until the relayer reports ready or the 60s timeout | ||
| // is reached. | ||
| func (d *DSL) WaitForRelayerReady(ctx context.Context, t *testing.T) { | ||
| t.Helper() | ||
| if d.relayer == nil { | ||
| t.Fatal("WaitForRelayerReady not available: Relayer shim not initialized (use NewFullStack)") | ||
| return | ||
| } | ||
| deadline := time.Now().Add(relayerReadyTimeout) | ||
| ticker := time.NewTicker(pollInterval) | ||
| defer ticker.Stop() | ||
| for time.Now().Before(deadline) { | ||
| if d.relayer.IsReady(ctx) { | ||
| return | ||
| } | ||
| select { | ||
| case <-ctx.Done(): | ||
| t.Fatal("context canceled waiting for relayer ready") | ||
| case <-ticker.C: | ||
| } | ||
| } | ||
| t.Fatal("timeout waiting for relayer to be ready") | ||
| } | ||
|
|
||
| // WaitForCantonBalance polls the indexer until partyID holds at least | ||
| // minAmount for the token identified by (admin, id), or the 60s timeout | ||
| // is reached. | ||
| func (d *DSL) WaitForCantonBalance(ctx context.Context, t *testing.T, partyID, admin, id, minAmount string) { | ||
| t.Helper() | ||
| if d.indexer == nil { | ||
| t.Fatal("WaitForCantonBalance not available: Indexer shim not initialized (use NewFullStack)") | ||
| return | ||
| } | ||
| deadline := time.Now().Add(cantonBalanceTimeout) | ||
| ticker := time.NewTicker(pollInterval) | ||
| defer ticker.Stop() | ||
| var lastBalance string | ||
| for time.Now().Before(deadline) { | ||
| bal, err := d.indexer.GetBalance(ctx, partyID, admin, id) | ||
| if err == nil && bal != nil { | ||
| lastBalance = bal.Amount | ||
| if amountGTE(bal.Amount, minAmount) { | ||
| return | ||
| } | ||
| } | ||
| select { | ||
| case <-ctx.Done(): | ||
| t.Fatal("context canceled waiting for Canton balance") | ||
| case <-ticker.C: | ||
| } | ||
| } | ||
| t.Fatalf("timeout waiting for Canton balance: party=%s admin=%s id=%s min=%s last=%s", | ||
| partyID, admin, id, minAmount, lastBalance) | ||
| } | ||
|
|
||
| // WaitForRelayerTransfer polls until the relayer has a completed transfer | ||
| // matching sourceTxHash, or the 120s timeout is reached. | ||
| func (d *DSL) WaitForRelayerTransfer(ctx context.Context, t *testing.T, sourceTxHash string) { | ||
| t.Helper() | ||
| if d.relayer == nil { | ||
| t.Fatal("WaitForRelayerTransfer not available: Relayer shim not initialized (use NewFullStack)") | ||
| return | ||
| } | ||
| deadline := time.Now().Add(relayerTransferTimeout) | ||
| ticker := time.NewTicker(pollInterval) | ||
| defer ticker.Stop() | ||
| var lastStatus relayer.TransferStatus | ||
| for time.Now().Before(deadline) { | ||
| transfers, err := d.relayer.ListTransfers(ctx) | ||
| if err == nil { | ||
| for _, tr := range transfers { | ||
| if strings.EqualFold(tr.SourceTxHash, sourceTxHash) { | ||
| lastStatus = tr.Status | ||
| if tr.Status == relayer.TransferStatusCompleted { | ||
| return | ||
| } | ||
| } | ||
| } | ||
| } | ||
| select { | ||
| case <-ctx.Done(): | ||
| t.Fatal("context canceled waiting for relayer transfer") | ||
| case <-ticker.C: | ||
| } | ||
| } | ||
| t.Fatalf("timeout waiting for relayer transfer: sourceTxHash=%s lastStatus=%s", sourceTxHash, lastStatus) | ||
| } | ||
|
|
||
| // WaitForIndexerEvent polls until the indexer has an event with the given | ||
| // contractID, or the 60s timeout is reached. | ||
| func (d *DSL) WaitForIndexerEvent(ctx context.Context, t *testing.T, contractID string) *indexer.ParsedEvent { | ||
| t.Helper() | ||
| if d.indexer == nil { | ||
| t.Fatal("WaitForIndexerEvent not available: Indexer shim not initialized (use NewFullStack)") | ||
| return nil // unreachable; t.Fatal calls runtime.Goexit | ||
| } | ||
| deadline := time.Now().Add(indexerEventTimeout) | ||
| ticker := time.NewTicker(pollInterval) | ||
| defer ticker.Stop() | ||
| var lastErr error | ||
| for time.Now().Before(deadline) { | ||
| ev, err := d.indexer.GetEvent(ctx, contractID) | ||
| if err == nil && ev != nil { | ||
| return ev | ||
| } | ||
| lastErr = err | ||
| select { | ||
| case <-ctx.Done(): | ||
| t.Fatalf("context canceled waiting for indexer event: contractID=%s", contractID) | ||
| case <-ticker.C: | ||
| } | ||
| } | ||
| t.Fatalf("timeout waiting for indexer event: contractID=%s lastErr=%v", contractID, lastErr) | ||
| return nil // unreachable; t.Fatalf calls runtime.Goexit | ||
| } | ||
|
|
||
| // amountGTE returns true when amount >= min, comparing both as decimal numbers. | ||
| // String comparison is intentionally avoided: "20" > "100" lexicographically. | ||
| func amountGTE(amount, min string) bool { | ||
| a, ok1 := new(big.Float).SetString(amount) | ||
| m, ok2 := new(big.Float).SetString(min) | ||
| if !ok1 || !ok2 { | ||
| return false | ||
| } | ||
| return a.Cmp(m) >= 0 | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.