decred.org/dcrdex@v1.0.5/server/asset/eth/rpcclient_harness_test.go (about) 1 //go:build harness 2 3 // This test requires that the testnet harness be running and the unix socket 4 // be located at $HOME/dextest/eth/delta/node/geth.ipc 5 6 package eth 7 8 import ( 9 "errors" 10 "fmt" 11 "math/big" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "time" 16 17 "context" 18 "testing" 19 20 "decred.org/dcrdex/dex" 21 "decred.org/dcrdex/dex/encode" 22 dexeth "decred.org/dcrdex/dex/networks/eth" 23 "github.com/ethereum/go-ethereum" 24 "github.com/ethereum/go-ethereum/common" 25 ) 26 27 var ( 28 wsEndpoint = "ws://localhost:38559" // beta ws port, with txpool api namespace enabled 29 homeDir = os.Getenv("HOME") 30 alphaIPCFile = filepath.Join(homeDir, "dextest", "eth", "alpha", "node", "geth.ipc") 31 32 contractAddrFile = filepath.Join(homeDir, "dextest", "eth", "eth_swap_contract_address.txt") 33 tokenSwapAddrFile = filepath.Join(homeDir, "dextest", "eth", "erc20_swap_contract_address.txt") 34 tokenErc20AddrFile = filepath.Join(homeDir, "dextest", "eth", "test_usdc_contract_address.txt") 35 deltaAddress = "d12ab7cf72ccf1f3882ec99ddc53cd415635c3be" 36 gammaAddress = "41293c2032bac60aa747374e966f79f575d42379" 37 ethClient *rpcclient 38 ctx context.Context 39 ) 40 41 func TestMain(m *testing.M) { 42 monitorConnectionsInterval = 3 * time.Second 43 44 // Run in function so that defers happen before os.Exit is called. 45 run := func() (int, error) { 46 var cancel context.CancelFunc 47 ctx, cancel = context.WithCancel(context.Background()) 48 defer cancel() 49 log := dex.StdOutLogger("T", dex.LevelTrace) 50 51 netAddrs, found := dexeth.ContractAddresses[ethContractVersion] 52 if !found { 53 return 1, fmt.Errorf("no contract address for eth version %d", ethContractVersion) 54 } 55 ethContractAddr, found := netAddrs[dex.Simnet] 56 if !found { 57 return 1, fmt.Errorf("no contract address for eth version %d on %s", ethContractVersion, dex.Simnet) 58 } 59 60 ethClient = newRPCClient(BipID, 42, dex.Simnet, []endpoint{{url: wsEndpoint}, {url: alphaIPCFile}}, ethContractAddr, log) 61 62 dexeth.ContractAddresses[0][dex.Simnet] = getContractAddrFromFile(contractAddrFile) 63 64 netToken := dexeth.Tokens[usdcID].NetTokens[dex.Simnet] 65 netToken.Address = getContractAddrFromFile(tokenErc20AddrFile) 66 netToken.SwapContracts[0].Address = getContractAddrFromFile(tokenSwapAddrFile) 67 68 if err := ethClient.connect(ctx); err != nil { 69 return 1, fmt.Errorf("Connect error: %w", err) 70 } 71 72 if err := ethClient.loadToken(ctx, usdcID, registeredTokens[usdcID]); err != nil { 73 return 1, fmt.Errorf("loadToken error: %w", err) 74 } 75 76 return m.Run(), nil 77 } 78 exitCode, err := run() 79 if err != nil { 80 fmt.Println(err) 81 } 82 os.Exit(exitCode) 83 } 84 85 func TestBestHeader(t *testing.T) { 86 _, err := ethClient.bestHeader(ctx) 87 if err != nil { 88 t.Fatal(err) 89 } 90 } 91 92 func TestHeaderByHeight(t *testing.T) { 93 _, err := ethClient.headerByHeight(ctx, 0) 94 if err != nil { 95 t.Fatal(err) 96 } 97 } 98 99 func TestBlockNumber(t *testing.T) { 100 _, err := ethClient.blockNumber(ctx) 101 if err != nil { 102 t.Fatal(err) 103 } 104 } 105 106 func TestSuggestGasTipCap(t *testing.T) { 107 _, err := ethClient.suggestGasTipCap(ctx) 108 if err != nil { 109 t.Fatal(err) 110 } 111 } 112 113 func TestSwap(t *testing.T) { 114 var secretHash [32]byte 115 copy(secretHash[:], encode.RandomBytes(32)) 116 _, err := ethClient.swap(ctx, BipID, secretHash) 117 if err != nil { 118 t.Fatal(err) 119 } 120 } 121 122 func TestTransaction(t *testing.T) { 123 var hash [32]byte 124 copy(hash[:], encode.RandomBytes(32)) 125 _, _, err := ethClient.transaction(ctx, hash) 126 // TODO: Test positive route. 127 if !errors.Is(err, ethereum.NotFound) { 128 t.Fatal(err) 129 } 130 } 131 132 func TestAccountBalance(t *testing.T) { 133 t.Run("eth", func(t *testing.T) { testAccountBalance(t, BipID) }) 134 t.Run("token", func(t *testing.T) { testAccountBalance(t, usdcID) }) 135 } 136 137 func testAccountBalance(t *testing.T, assetID uint32) { 138 addr := common.HexToAddress(deltaAddress) 139 const vGwei = 1e7 140 141 balBefore, err := ethClient.accountBalance(ctx, assetID, addr) 142 if err != nil { 143 t.Fatalf("accountBalance error: %v", err) 144 } 145 146 if assetID == BipID { 147 err = tmuxSend(deltaAddress, gammaAddress, vGwei) 148 } else { 149 err = tmuxSendToken(gammaAddress, vGwei) 150 } 151 if err != nil { 152 t.Fatalf("send error: %v", err) 153 } 154 155 // NOTE: this test does not mine the above sends, and as such the node or 156 // provider for this test must have the txpool api namespace enabled. 157 balAfter, err := ethClient.accountBalance(ctx, assetID, addr) 158 if err != nil { 159 t.Fatalf("accountBalance error: %v", err) 160 } 161 162 if assetID == BipID { 163 diff := new(big.Int).Sub(balBefore, balAfter) 164 if diff.Cmp(dexeth.GweiToWei(vGwei)) <= 0 { 165 t.Fatalf("account balance changed by %d. expected > %d", dexeth.WeiToGwei(diff), uint64(vGwei)) 166 } 167 } 168 169 if assetID == usdcID { 170 diff := new(big.Int).Sub(balBefore, balAfter) 171 if diff.Cmp(dexeth.GweiToWei(vGwei)) != 0 { 172 t.Fatalf("account balance changed by %d. expected > %d", dexeth.WeiToGwei(diff), uint64(vGwei)) 173 } 174 } 175 } 176 177 func TestMonitorHealth(t *testing.T) { 178 // Requesting a non-existent transaction should propagate the error. Also 179 // check logs to ensure the endpoint index was not advanced. 180 _, _, err := ethClient.transaction(ctx, common.Hash{}) 181 if !errors.Is(err, ethereum.NotFound) { 182 t.Fatalf("'not found' error not propagated. got err = %v", err) 183 } 184 ethClient.log.Info("Not found error successfully propagated") 185 186 originalClients := ethClient.clientsCopy() 187 originalClients[0].Close() 188 189 fmt.Println("Waiting for client health check...") 190 time.Sleep(5 * time.Second) 191 192 updatedClients := ethClient.clientsCopy() 193 194 fmt.Println("Original clients:", originalClients) 195 fmt.Println("Updated clients:", updatedClients) 196 197 if originalClients[0].endpoint != updatedClients[len(updatedClients)-1].endpoint { 198 t.Fatalf("failing client was not moved to the end. got %s, expected %s", updatedClients[len(updatedClients)-1].endpoint, originalClients[0].endpoint) 199 } 200 } 201 202 func TestHeaderSubscription(t *testing.T) { 203 ctx, cancel := context.WithTimeout(ctx, headerExpirationTime) 204 defer cancel() 205 ept := endpoint{url: wsEndpoint} 206 cl := newRPCClient(BipID, 42, dex.Simnet, []endpoint{ept}, ethClient.ethContractAddr, ethClient.log) 207 ec, err := cl.connectToEndpoint(ctx, ept) 208 if err != nil { 209 t.Fatalf("connectToEndpoint error: %v", err) 210 } 211 hdr, err := ec.tip(ctx) 212 if err != nil { 213 t.Fatalf("Error getting initial tip: %v", err) 214 } 215 for { 216 select { 217 case <-ctx.Done(): 218 return 219 case <-time.After(time.Second): 220 } 221 ec.tipCache.Lock() 222 bestHdr := ec.tipCache.hdr 223 ec.tipCache.Unlock() 224 if bestHdr.Number.Cmp(hdr.Number) > 0 { 225 hdr = bestHdr 226 fmt.Println("New header seen at height", bestHdr.Number) 227 } 228 } 229 } 230 231 func tmuxRun(cmd string) error { 232 cmd += "; tmux wait-for -S harnessdone" 233 err := exec.Command("tmux", "send-keys", "-t", "eth-harness:0", cmd, "C-m").Run() // ; wait-for harnessdone 234 if err != nil { 235 return nil 236 } 237 return exec.Command("tmux", "wait-for", "harnessdone").Run() 238 } 239 240 func tmuxSend(from, to string, v uint64) error { 241 return tmuxRun(fmt.Sprintf("./delta attach --preload send.js --exec \"send(\\\"%s\\\",\\\"%s\\\",%s)\"", from, to, dexeth.GweiToWei(v))) 242 } 243 244 func tmuxSendToken(to string, v uint64) error { 245 return tmuxRun(fmt.Sprintf("./delta attach --preload loadTestToken.js --exec \"testToken.transfer(\\\"0x%s\\\",%s)\"", to, dexeth.GweiToWei(v))) 246 } 247 248 func getContractAddrFromFile(fileName string) common.Address { 249 addrBytes, err := os.ReadFile(fileName) 250 if err != nil { 251 panic(fmt.Sprintf("error reading contract address: %v", err)) 252 } 253 addrLen := len(addrBytes) 254 if addrLen == 0 { 255 panic(fmt.Sprintf("no contract address found at %v", fileName)) 256 } 257 addrStr := string(addrBytes[:addrLen-1]) 258 address := common.HexToAddress(addrStr) 259 return address 260 }