diff --git a/dex/testing/dcrdex/harness.sh b/dex/testing/dcrdex/harness.sh index 4e24c04cf6..c92d65ea03 100755 --- a/dex/testing/dcrdex/harness.sh +++ b/dex/testing/dcrdex/harness.sh @@ -217,6 +217,15 @@ EOF fi if [ $ETH_ON -eq 0 ]; then + +ETH_CONFIG_PATH=${TEST_ROOT}/eth.conf +ETH_IPC_FILE=${TEST_ROOT}/eth/alpha/node/geth.ipc +cat << EOF >> $ETH_CONFIG_PATH +ws://localhost:38557 +# comments are respected +# http://localhost:38556 +${ETH_IPC_FILE} +EOF cat << EOF >> "./markets.json" }, "ETH_simnet": { @@ -224,7 +233,7 @@ if [ $ETH_ON -eq 0 ]; then "network": "simnet", "maxFeeRate": 200, "swapConf": 2, - "configPath": "ws://localhost:38557" + "configPath": "$ETH_CONFIG_PATH" }, "DEXTT_simnet": { "bip44symbol": "dextt.eth", diff --git a/server/asset/eth/config.go b/server/asset/eth/config.go index 0acd099f8f..3eb2daac98 100644 --- a/server/asset/eth/config.go +++ b/server/asset/eth/config.go @@ -6,15 +6,10 @@ package eth import ( - "path/filepath" - "github.com/decred/dcrd/dcrutil/v4" ) -var ( - ethHomeDir = dcrutil.AppDataDir("ethereum", false) - defaultIPC = filepath.Join(ethHomeDir, "geth/geth.ipc") -) +var ethHomeDir = dcrutil.AppDataDir("ethereum", false) // For tokens, the file at the config path can contain overrides for // token gas values. Gas used for token swaps is dependent on the token contract diff --git a/server/asset/eth/eth.go b/server/asset/eth/eth.go index e541304ef6..5cd4bfdbb6 100644 --- a/server/asset/eth/eth.go +++ b/server/asset/eth/eth.go @@ -6,12 +6,14 @@ package eth import ( + "bufio" "bytes" "context" "crypto/sha256" "errors" "fmt" "math/big" + "os" "strings" "sync" "time" @@ -161,7 +163,7 @@ type ethFetcher interface { bestHeader(ctx context.Context) (*types.Header, error) blockNumber(ctx context.Context) (uint64, error) headerByHeight(ctx context.Context, height uint64) (*types.Header, error) - connect(ctx context.Context, log dex.Logger) error + connect(ctx context.Context) error shutdown() suggestGasTipCap(ctx context.Context) (*big.Int, error) syncProgress(ctx context.Context) (*ethereum.SyncProgress, error) @@ -265,7 +267,7 @@ func unconnectedETH(logger dex.Logger, net dex.Network) (*ETHBackend, error) { // NewBackend is the exported constructor by which the DEX will import the // Backend. -func NewBackend(endpoint string, logger dex.Logger, net dex.Network) (*ETHBackend, error) { +func NewBackend(configPath string, log dex.Logger, net dex.Network) (*ETHBackend, error) { switch net { case dex.Simnet: case dex.Testnet: @@ -276,15 +278,34 @@ func NewBackend(endpoint string, logger dex.Logger, net dex.Network) (*ETHBacken return nil, fmt.Errorf("unknown network ID: %d", net) } - if endpoint == "" { - endpoint = defaultIPC + file, err := os.Open(configPath) + if err != nil { + return nil, err + } + defer file.Close() + + var endpoints []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.Trim(scanner.Text(), " ") + if line == "" || strings.HasPrefix(line, "#") { + continue + } + endpoints = append(endpoints, line) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading eth config file at %q. %v", configPath, err) + } + if len(endpoints) == 0 { + return nil, fmt.Errorf("no endpoint found in the eth config file at %q", configPath) } + log.Debugf("Parsed %d endpoints from the ETH config file", len(endpoints)) - eth, err := unconnectedETH(logger, net) + eth, err := unconnectedETH(log, net) if err != nil { return nil, err } - eth.node = newRPCClient(eth.net, endpoint) + eth.node = newRPCClient(eth.net, endpoints, log.SubLogger("RPC")) return eth, nil } @@ -296,7 +317,7 @@ func (eth *baseBackend) shutdown() { func (eth *ETHBackend) Connect(ctx context.Context) (*sync.WaitGroup, error) { eth.baseBackend.ctx = ctx - if err := eth.node.connect(ctx, eth.log); err != nil { + if err := eth.node.connect(ctx); err != nil { return nil, err } @@ -338,7 +359,8 @@ func (eth *TokenBackend) Connect(ctx context.Context) (*sync.WaitGroup, error) { } // TokenBackend creates an *AssetBackend for a token. Part of the -// asset.TokenBacker interface. +// asset.TokenBacker interface. Do not call TokenBackend concurrently for the +// same asset. func (eth *ETHBackend) TokenBackend(assetID uint32, configPath string) (asset.Backend, error) { if _, found := eth.baseBackend.tokens[assetID]; found { return nil, fmt.Errorf("asset %d backend already loaded", assetID) diff --git a/server/asset/eth/eth_test.go b/server/asset/eth/eth_test.go index 0b199c2f8d..33ae45a662 100644 --- a/server/asset/eth/eth_test.go +++ b/server/asset/eth/eth_test.go @@ -111,7 +111,7 @@ type testNode struct { acctBalErr error } -func (n *testNode) connect(ctx context.Context, log dex.Logger) error { +func (n *testNode) connect(ctx context.Context) error { return n.connectErr } diff --git a/server/asset/eth/rpcclient.go b/server/asset/eth/rpcclient.go index 6da08ccde9..88bef4390d 100644 --- a/server/asset/eth/rpcclient.go +++ b/server/asset/eth/rpcclient.go @@ -7,9 +7,12 @@ package eth import ( "context" + "errors" "fmt" "math/big" "strings" + "sync" + "time" "decred.org/dcrdex/dex" dexeth "decred.org/dcrdex/dex/networks/eth" @@ -26,68 +29,76 @@ import ( var ( _ ethFetcher = (*rpcclient)(nil) - bigZero = new(big.Int) + bigZero = new(big.Int) + headerExpirationTime = time.Minute ) type ContextCaller interface { CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error } -type rpcclient struct { - net dex.Network +type ethConn struct { + *ethclient.Client endpoint string - // ec wraps a *rpc.Client with some useful calls. - ec *ethclient.Client - // caller is a client for raw calls not implemented by *ethclient.Client. - caller ContextCaller // swapContract is the current ETH swapContract. swapContract swapContract - // tokens are tokeners for loaded tokens. tokens is not protected by a // mutex, as it is expected that the caller will connect and place calls to // loadToken sequentially in the same thread during initialization. tokens map[uint32]*tokener - + // caller is a client for raw calls not implemented by *ethclient.Client. + caller ContextCaller txPoolSupported bool } -func newRPCClient(net dex.Network, endpoint string) *rpcclient { +type rpcclient struct { + net dex.Network + log dex.Logger + endpoints []string + clients []*ethConn + + idxMtx sync.RWMutex + endpointIdx int +} + +func newRPCClient(net dex.Network, endpoints []string, log dex.Logger) *rpcclient { return &rpcclient{ - net: net, - tokens: make(map[uint32]*tokener), - endpoint: endpoint, + net: net, + endpoints: endpoints, + log: log, } } -// connect connects to an ipc socket. It then wraps ethclient's client and -// bundles commands in a form we can easily use. -func (c *rpcclient) connect(ctx context.Context, log dex.Logger) error { - var client *rpc.Client - var err error - if strings.HasSuffix(c.endpoint, ".ipc") { - client, err = rpc.DialIPC(ctx, c.endpoint) - if err != nil { - return fmt.Errorf("unable to dial ipc: %v", err) +func (c *rpcclient) withClient(f func(ec *ethConn) error, haltOnNotFound ...bool) (err error) { + for range c.endpoints { + c.idxMtx.RLock() + idx := c.endpointIdx + ec := c.clients[idx] + c.idxMtx.RUnlock() + + err = f(ec) + if err == nil { + return nil } - } else { - log.Debugf("dialing endpoint: %s", c.endpoint) - client, err = rpc.DialContext(ctx, c.endpoint) - if err != nil { - return fmt.Errorf("unable to dial rpc: %v", err) + if len(haltOnNotFound) > 0 && haltOnNotFound[0] && (errors.Is(err, ethereum.NotFound) || strings.Contains(err.Error(), "not found")) { + return err } + c.log.Errorf("Unpropagated error from %q: %v", c.endpoints[idx], err) + // Try the next client. + c.idxMtx.Lock() + // Only advance it if another thread hasn't. + if c.endpointIdx == idx && len(c.endpoints) > 0 { + c.endpointIdx = (c.endpointIdx + 1) % len(c.endpoints) + c.log.Infof("Switching RPC endpoint to %q", c.endpoints[c.endpointIdx]) + } + c.idxMtx.Unlock() } + return fmt.Errorf("all providers failed. last error: %w", err) +} - reqModules := []string{"eth", "txpool"} - if err := dexeth.CheckAPIModules(client, c.endpoint, log, reqModules); err != nil { - log.Warnf("Error checking required modules: %v", err) - log.Warn("Will not account for pending transactions in balance calculations") - c.txPoolSupported = false - } else { - c.txPoolSupported = true - } - - c.ec = ethclient.NewClient(client) - +// connect connects to an ipc socket. It then wraps ethclient's client and +// bundles commands in a form we can easily use. +func (c *rpcclient) connect(ctx context.Context) (err error) { netAddrs, found := dexeth.ContractAddresses[ethContractVersion] if !found { return fmt.Errorf("no contract address for eth version %d", ethContractVersion) @@ -97,74 +108,152 @@ func (c *rpcclient) connect(ctx context.Context, log dex.Logger) error { return fmt.Errorf("no contract address for eth version %d on %s", ethContractVersion, c.net) } - es, err := swapv0.NewETHSwap(contractAddr, c.ec) - if err != nil { - return fmt.Errorf("unable to find swap contract: %v", err) + var success bool + + c.clients = make([]*ethConn, len(c.endpoints)) + for i, endpoint := range c.endpoints { + client, err := rpc.DialContext(ctx, endpoint) + if err != nil { + return fmt.Errorf("unable to dial rpc to %q: %v", endpoint, err) + } + + defer func() { + if !success { + client.Close() + } + }() + + ec := ðConn{ + Client: ethclient.NewClient(client), + endpoint: endpoint, + tokens: make(map[uint32]*tokener), + } + + reqModules := []string{"eth", "txpool"} + if err := dexeth.CheckAPIModules(client, endpoint, c.log, reqModules); err != nil { + c.log.Warnf("Error checking required modules at %q: %v", endpoint, err) + c.log.Warnf("Will not account for pending transactions in balance calculations at %q", endpoint) + ec.txPoolSupported = false + } else { + ec.txPoolSupported = true + } + + hdr, err := ec.HeaderByNumber(ctx, nil) + if err != nil { + return fmt.Errorf("error getting best header from %q: %v", endpoint, err) + } + if c.headerIsOutdated(hdr) { + return fmt.Errorf("initial header fetched from %q appears to be outdated (time %s). If you continue to see this message, you might need to check your system clock", + endpoint, time.Unix(int64(hdr.Time), 0)) + } + + es, err := swapv0.NewETHSwap(contractAddr, ec.Client) + if err != nil { + return fmt.Errorf("unable to initialize eth contract for %q: %v", endpoint, err) + } + ec.swapContract = &swapSourceV0{es} + ec.caller = client + + c.clients[i] = ec } - c.swapContract = &swapSourceV0{es} - c.caller = client + success = true return nil } +func (c *rpcclient) headerIsOutdated(hdr *types.Header) bool { + return c.net != dex.Simnet && hdr.Time < uint64(time.Now().Add(-headerExpirationTime).Unix()) +} + // shutdown shuts down the client. func (c *rpcclient) shutdown() { - if c.ec != nil { - c.ec.Close() + for _, ec := range c.clients { + ec.Close() } } func (c *rpcclient) loadToken(ctx context.Context, assetID uint32) error { - tkn, err := newTokener(ctx, assetID, c.net, c.ec) - if err != nil { - return fmt.Errorf("error constructing ERC20Swap: %w", err) - } + for _, cl := range c.clients { + tkn, err := newTokener(ctx, assetID, c.net, cl.Client) + if err != nil { + return fmt.Errorf("error constructing ERC20Swap: %w", err) + } - c.tokens[assetID] = tkn + cl.tokens[assetID] = tkn + } return nil } func (c *rpcclient) withTokener(assetID uint32, f func(*tokener) error) error { - tkn, found := c.tokens[assetID] - if !found { - return fmt.Errorf("no swap source for asset %d", assetID) - } - return f(tkn) + return c.withClient(func(ec *ethConn) error { + tkn, found := ec.tokens[assetID] + if !found { + return fmt.Errorf("no swap source for asset %d", assetID) + } + return f(tkn) + }) + } // bestHeader gets the best header at the time of calling. -func (c *rpcclient) bestHeader(ctx context.Context) (*types.Header, error) { - bn, err := c.ec.BlockNumber(ctx) - if err != nil { - return nil, err - } - return c.ec.HeaderByNumber(ctx, big.NewInt(int64(bn))) +func (c *rpcclient) bestHeader(ctx context.Context) (hdr *types.Header, err error) { + return hdr, c.withClient(func(ec *ethConn) error { + hdr, err = ec.HeaderByNumber(ctx, nil) + if err == nil && c.headerIsOutdated(hdr) { + c.log.Errorf("Best header from %q appears to be outdated (time %s). If you continue to see this message, you might need to check your system clock", + ec.endpoint, time.Unix(int64(hdr.Time), 0)) + if len(c.endpoints) > 0 { + c.idxMtx.Lock() + c.endpointIdx = (c.endpointIdx + 1) % len(c.endpoints) + endpoint := c.endpoints[c.endpointIdx] + c.idxMtx.Unlock() + c.log.Infof("Switching RPC endpoint to %q", endpoint) + } + } + return err + }) + } // headerByHeight gets the best header at height. -func (c *rpcclient) headerByHeight(ctx context.Context, height uint64) (*types.Header, error) { - return c.ec.HeaderByNumber(ctx, big.NewInt(int64(height))) +func (c *rpcclient) headerByHeight(ctx context.Context, height uint64) (hdr *types.Header, err error) { + return hdr, c.withClient(func(ec *ethConn) error { + hdr, err = ec.HeaderByNumber(ctx, big.NewInt(int64(height))) + return err + }) } // suggestGasTipCap retrieves the currently suggested priority fee to allow a // timely execution of a transaction. -func (c *rpcclient) suggestGasTipCap(ctx context.Context) (*big.Int, error) { - return c.ec.SuggestGasTipCap(ctx) +func (c *rpcclient) suggestGasTipCap(ctx context.Context) (tipCap *big.Int, err error) { + return tipCap, c.withClient(func(ec *ethConn) error { + tipCap, err = ec.SuggestGasTipCap(ctx) + return err + }) } // syncProgress return the current sync progress. Returns no error and nil when not syncing. -func (c *rpcclient) syncProgress(ctx context.Context) (*ethereum.SyncProgress, error) { - return c.ec.SyncProgress(ctx) +func (c *rpcclient) syncProgress(ctx context.Context) (prog *ethereum.SyncProgress, err error) { + return prog, c.withClient(func(ec *ethConn) error { + prog, err = ec.SyncProgress(ctx) + return err + }) } // blockNumber gets the chain length at the time of calling. -func (c *rpcclient) blockNumber(ctx context.Context) (uint64, error) { - return c.ec.BlockNumber(ctx) +func (c *rpcclient) blockNumber(ctx context.Context) (bn uint64, err error) { + return bn, c.withClient(func(ec *ethConn) error { + bn, err = ec.BlockNumber(ctx) + return err + }) } // swap gets a swap keyed by secretHash in the contract. func (c *rpcclient) swap(ctx context.Context, assetID uint32, secretHash [32]byte) (state *dexeth.SwapState, err error) { if assetID == BipID { - return c.swapContract.Swap(ctx, secretHash) + return state, c.withClient(func(ec *ethConn) error { + state, err = ec.swapContract.Swap(ctx, secretHash) + return err + }) } return state, c.withTokener(assetID, func(tkn *tokener) error { state, err = tkn.Swap(ctx, secretHash) @@ -175,27 +264,28 @@ func (c *rpcclient) swap(ctx context.Context, assetID uint32, secretHash [32]byt // transaction gets the transaction that hashes to hash from the chain or // mempool. Errors if tx does not exist. func (c *rpcclient) transaction(ctx context.Context, hash common.Hash) (tx *types.Transaction, isMempool bool, err error) { - return c.ec.TransactionByHash(ctx, hash) + return tx, isMempool, c.withClient(func(ec *ethConn) error { + tx, isMempool, err = ec.TransactionByHash(ctx, hash) + return err + }, true) // stop on first provider with "not found", because this should be an error if tx does not exist } // dumbBalance gets the account balance, ignoring the effects of unmined // transactions. -func (c *rpcclient) dumbBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) { +func (c *rpcclient) dumbBalance(ctx context.Context, ec *ethConn, assetID uint32, addr common.Address) (bal *big.Int, err error) { if assetID == BipID { - return c.ec.BalanceAt(ctx, addr, nil) + return ec.BalanceAt(ctx, addr, nil) } - - bal := new(big.Int) - return bal, c.withTokener(assetID, func(tkn *tokener) error { - var err error - bal, err = tkn.balanceOf(ctx, addr) - return err - }) + tkn := ec.tokens[assetID] + if tkn == nil { + return nil, fmt.Errorf("no tokener for asset ID %d", assetID) + } + return tkn.balanceOf(ctx, addr) } // smartBalance gets the account balance, including the effects of known // unmined transactions. -func (c *rpcclient) smartBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) { +func (c *rpcclient) smartBalance(ctx context.Context, ec *ethConn, assetID uint32, addr common.Address) (bal *big.Int, err error) { tip, err := c.blockNumber(ctx) if err != nil { return nil, fmt.Errorf("blockNumber error: %v", err) @@ -210,13 +300,12 @@ func (c *rpcclient) smartBalance(ctx context.Context, assetID uint32, addr commo // reason, so we'll have to use CallContext and copy the mimic the // internal RPCTransaction type. var txs map[string]map[string]*RPCTransaction - err = c.caller.CallContext(ctx, &txs, "txpool_contentFrom", addr) - if err != nil { + if err := ec.caller.CallContext(ctx, &txs, "txpool_contentFrom", addr); err != nil { return nil, fmt.Errorf("contentFrom error: %w", err) } if assetID == BipID { - ethBalance, err := c.ec.BalanceAt(ctx, addr, big.NewInt(int64(tip))) + ethBalance, err := ec.BalanceAt(ctx, addr, big.NewInt(int64(tip))) if err != nil { return nil, err } @@ -239,39 +328,46 @@ func (c *rpcclient) smartBalance(ctx context.Context, assetID uint32, addr commo // For tokens, we'll do something similar, but with checks for pending txs // that transfer tokens or pay to the swap contract. - bal := new(big.Int) - return bal, c.withTokener(assetID, func(tkn *tokener) error { - bal, err = tkn.balanceOf(ctx, addr) - if err != nil { - return err - } - for _, group := range txs { - for _, rpcTx := range group { - to := *rpcTx.To - if to == tkn.tokenAddr { - if sent := tkn.transferred(rpcTx.Input); sent != nil { - bal.Sub(bal, sent) - } + // Can't use withTokener because we need to use the same ethConn due to + // txPoolSupported being used to decide between {smart/dumb}Balance. + tkn := ec.tokens[assetID] + if tkn == nil { + return nil, fmt.Errorf("no tokener for asset ID %d", assetID) + } + bal, err = tkn.balanceOf(ctx, addr) + if err != nil { + return nil, err + } + for _, group := range txs { + for _, rpcTx := range group { + to := *rpcTx.To + if to == tkn.tokenAddr { + if sent := tkn.transferred(rpcTx.Input); sent != nil { + bal.Sub(bal, sent) } - if to == tkn.contractAddr { - if swapped := tkn.swapped(rpcTx.Input); swapped != nil { - bal.Sub(bal, swapped) - } + } + if to == tkn.contractAddr { + if swapped := tkn.swapped(rpcTx.Input); swapped != nil { + bal.Sub(bal, swapped) } } } - return nil - }) + } + return bal, nil } // accountBalance gets the account balance. If txPool functions are supported by the // client, it will include the effects of unmined transactions, otherwise it will not. -func (c *rpcclient) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) { - if c.txPoolSupported { - return c.smartBalance(ctx, assetID, addr) - } else { - return c.dumbBalance(ctx, assetID, addr) - } +func (c *rpcclient) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (bal *big.Int, err error) { + return bal, c.withClient(func(ec *ethConn) error { + if ec.txPoolSupported { + bal, err = c.smartBalance(ctx, ec, assetID, addr) + } else { + bal, err = c.dumbBalance(ctx, ec, assetID, addr) + } + return err + }) + } type RPCTransaction struct { diff --git a/server/asset/eth/rpcclient_harness_test.go b/server/asset/eth/rpcclient_harness_test.go index 3f9df2da4d..9870d9625b 100644 --- a/server/asset/eth/rpcclient_harness_test.go +++ b/server/asset/eth/rpcclient_harness_test.go @@ -24,9 +24,10 @@ import ( ) var ( - homeDir = os.Getenv("HOME") - // endpoint = filepath.Join(homeDir, "dextest/eth/delta/node/geth.ipc") - endpoint = "ws://localhost:38557" + wsEndpoint = "ws://localhost:38557" + homeDir = os.Getenv("HOME") + alphaIPCFile = filepath.Join(homeDir, "dextest", "eth", "alpha", "node", "geth.ipc") + contractAddrFile = filepath.Join(homeDir, "dextest", "eth", "eth_swap_contract_address.txt") tokenSwapAddrFile = filepath.Join(homeDir, "dextest", "eth", "erc20_swap_contract_address.txt") tokenErc20AddrFile = filepath.Join(homeDir, "dextest", "eth", "test_token_contract_address.txt") @@ -41,7 +42,8 @@ func TestMain(m *testing.M) { run := func() (int, error) { var cancel context.CancelFunc ctx, cancel = context.WithCancel(context.Background()) - ethClient = newRPCClient(dex.Simnet, endpoint) + log := dex.StdOutLogger("T", dex.LevelTrace) + ethClient = newRPCClient(dex.Simnet, []string{wsEndpoint, alphaIPCFile}, log) defer func() { cancel() ethClient.shutdown() @@ -52,9 +54,8 @@ func TestMain(m *testing.M) { netToken := dexeth.Tokens[testTokenID].NetTokens[dex.Simnet] netToken.Address = getContractAddrFromFile(tokenErc20AddrFile) netToken.SwapContracts[0].Address = getContractAddrFromFile(tokenSwapAddrFile) - logger := dex.StdOutLogger("ETHTEST", dex.LevelTrace) - if err := ethClient.connect(ctx, logger); err != nil { + if err := ethClient.connect(ctx); err != nil { return 1, fmt.Errorf("Connect error: %w", err) } @@ -168,6 +169,39 @@ func testAccountBalance(t *testing.T, assetID uint32) { } } +func TestRPCRotation(t *testing.T) { + ethClient.idxMtx.RLock() + idx := ethClient.endpointIdx + ethClient.idxMtx.RUnlock() + if idx != 0 { + t.Fatal("expected initial index to be zero") + } + + // Requesting a non-existent transaction should propagate the error. Also + // check logs to ensure the endpoint index was not advanced. + _, _, err := ethClient.transaction(ctx, common.Hash{}) + if !errors.Is(err, ethereum.NotFound) { + t.Fatalf("'not found' error not propagated. got err = %v", err) + } + ethClient.log.Info("Not found error successfully propagated") + + // Shut down the zeroth client and ensure the endpoint index is advanced. + cl := ethClient.clients[idx] + cl.Close() + + _, err = ethClient.bestHeader(ctx) + if err != nil { + t.Fatalf("error getting best header with index advance: %v", err) + } + + ethClient.idxMtx.RLock() + idx = ethClient.endpointIdx + ethClient.idxMtx.RUnlock() + if idx == 0 { + t.Fatalf("endpoint index not advanced") + } +} + func tmuxRun(cmd string) error { cmd += "; tmux wait-for -S harnessdone" err := exec.Command("tmux", "send-keys", "-t", "eth-harness:0", cmd, "C-m").Run() // ; wait-for harnessdone diff --git a/server/cmd/dcrdex/sample-markets.json b/server/cmd/dcrdex/sample-markets.json index 30f0b2cc7d..3811ed090c 100644 --- a/server/cmd/dcrdex/sample-markets.json +++ b/server/cmd/dcrdex/sample-markets.json @@ -75,7 +75,7 @@ "rateStep": 1000000000, "maxFeeRate": 200, "swapConf": 12, - "configPath": "/home/.ethereum/geth/geth.ipc" + "configPath": "/home/.ethereum/dex.conf" } } }