-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdsl.go
More file actions
218 lines (195 loc) · 6.98 KB
/
Copy pathdsl.go
File metadata and controls
218 lines (195 loc) · 6.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
//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
for time.Now().Before(deadline) {
bal, err := d.apiServer.ERC20Balance(ctx, tok.Address, ownerAddr)
if err != nil {
lastErr = err
} else 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)
}
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
}