decred.org/dcrdex@v1.0.3/client/asset/eth/eth.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package eth 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/hex" 11 "encoding/json" 12 "errors" 13 "fmt" 14 "math/big" 15 "os" 16 "os/exec" 17 "os/user" 18 "path/filepath" 19 "sort" 20 "strconv" 21 "strings" 22 "sync" 23 "sync/atomic" 24 "time" 25 26 "decred.org/dcrdex/client/asset" 27 "decred.org/dcrdex/dex" 28 "decred.org/dcrdex/dex/config" 29 "decred.org/dcrdex/dex/encode" 30 "decred.org/dcrdex/dex/keygen" 31 "decred.org/dcrdex/dex/networks/erc20" 32 dexeth "decred.org/dcrdex/dex/networks/eth" 33 multibal "decred.org/dcrdex/dex/networks/eth/contracts/multibalance" 34 "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 35 "github.com/decred/dcrd/hdkeychain/v3" 36 "github.com/ethereum/go-ethereum" 37 "github.com/ethereum/go-ethereum/accounts/abi/bind" 38 "github.com/ethereum/go-ethereum/accounts/keystore" 39 "github.com/ethereum/go-ethereum/common" 40 ethmath "github.com/ethereum/go-ethereum/common/math" 41 "github.com/ethereum/go-ethereum/core/types" 42 "github.com/ethereum/go-ethereum/crypto" 43 "github.com/ethereum/go-ethereum/eth/ethconfig" 44 "github.com/ethereum/go-ethereum/params" 45 "github.com/tyler-smith/go-bip39" 46 ) 47 48 func init() { 49 dexeth.MaybeReadSimnetAddrs() 50 } 51 52 func registerToken(tokenID uint32, desc string) { 53 token, found := dexeth.Tokens[tokenID] 54 if !found { 55 panic("token " + strconv.Itoa(int(tokenID)) + " not known") 56 } 57 netAddrs := make(map[dex.Network]string) 58 for net, netToken := range token.NetTokens { 59 netAddrs[net] = netToken.Address.String() 60 } 61 asset.RegisterToken(tokenID, token.Token, &asset.WalletDefinition{ 62 Type: walletTypeToken, 63 Tab: "Ethereum token", 64 Description: desc, 65 }, netAddrs) 66 } 67 68 func init() { 69 asset.Register(BipID, &Driver{}) 70 registerToken(usdcTokenID, "The USDC Ethereum ERC20 token.") 71 registerToken(usdtTokenID, "The USDT Ethereum ERC20 token.") 72 registerToken(maticTokenID, "The MATIC Ethereum ERC20 token.") 73 } 74 75 const ( 76 // BipID is the BIP-0044 asset ID for Ethereum. 77 BipID = 60 78 defaultGasFee = 82 // gwei 79 defaultGasFeeLimit = 200 // gwei 80 defaultSendGasLimit = 21_000 81 82 walletTypeGeth = "geth" 83 walletTypeRPC = "rpc" 84 walletTypeToken = "token" 85 86 providersKey = "providers" 87 88 // onChainDataFetchTimeout is the max amount of time allocated to fetching 89 // on-chain data. Testing on testnet has shown spikes up to 2.5 seconds 90 // (but on internet, with Tor, it could actually take up to 30 seconds easily). 91 // Setting it to 10 seconds for now until https://github.com/decred/dcrdex/issues/3184 92 // is resolved. 93 onChainDataFetchTimeout = 10 * time.Second 94 95 // coinIDTakerFoundMakerRedemption is a prefix to identify one of CoinID formats, 96 // see DecodeCoinID func for details. 97 coinIDTakerFoundMakerRedemption = "TakerFoundMakerRedemption:" 98 99 // maxTxFeeGwei is the default max amount of eth that can be used in one 100 // transaction. This is set by the host in the case of providers. The 101 // internal node currently has no max but also cannot be used since the 102 // merge. 103 // 104 // TODO: Find a way to ask the host about their config set max fee and 105 // gas values. 106 maxTxFeeGwei = 1_000_000_000 107 108 LiveEstimateFailedError = dex.ErrorKind("live gas estimate failed") 109 110 // txAgeOut is the amount of time after which we forego any tx 111 // synchronization efforts for unconfirmed pending txs. 112 txAgeOut = 2 * time.Hour 113 // stateUpdateTick is the minimum amount of time between checks for 114 // new block and updating of pending txs, counter-party redemptions and 115 // approval txs. 116 // HTTP RPC clients meter tip header calls to minimum 10 seconds. 117 // WebSockets will stay up-to-date, so can expect new blocks often. 118 // A shorter blockTicker would be too much for e.g. Polygon where the block 119 // time is 2 or 3 seconds. We'd be doing a ton of calls for pending tx 120 // updates. 121 stateUpdateTick = time.Second * 5 122 // maxUnindexedTxs is the number of pending txs we will allow to be 123 // unverified on-chain before we halt broadcasting of new txs. 124 maxUnindexedTxs = 10 125 peerCountTicker = 5 * time.Second // no rpc calls here 126 contractVersionNewest = ^uint32(0) 127 ) 128 129 var ( 130 usdcTokenID, _ = dex.BipSymbolID("usdc.eth") 131 usdtTokenID, _ = dex.BipSymbolID("usdt.eth") 132 maticTokenID, _ = dex.BipSymbolID("matic.eth") 133 walletOpts = []*asset.ConfigOption{ 134 { 135 Key: "gasfeelimit", 136 DisplayName: "Gas Fee Limit", 137 Description: "This is the highest network fee rate you are willing to " + 138 "pay on swap transactions. If gasfeelimit is lower than a market's " + 139 "maxfeerate, you will not be able to trade on that market with this " + 140 "wallet. Units: gwei / gas", 141 DefaultValue: defaultGasFeeLimit, 142 }, 143 } 144 RPCOpts = []*asset.ConfigOption{ 145 { 146 Key: providersKey, 147 DisplayName: "RPC Provider", 148 Description: "Specify one or more RPC providers. For infrastructure " + 149 "providers, prefer using wss address. Only url-based authentication " + 150 "is supported. For a local node, use the filepath to an IPC file.", 151 Repeatable: providerDelimiter, 152 RepeatN: 2, 153 DefaultValue: "", 154 }, 155 } 156 // WalletInfo defines some general information about a Ethereum wallet. 157 WalletInfo = asset.WalletInfo{ 158 Name: "Ethereum", 159 // SupportedVersions: For Ethereum, the server backend maintains a 160 // single protocol version, so tokens and ETH have the same set of 161 // supported versions. Even though the SupportedVersions are made 162 // accessible for tokens via (*TokenWallet).Info, the versions are not 163 // exposed though any Driver methods or assets/driver functions. Use the 164 // parent wallet's WalletInfo via (*Driver).Info if you need a token's 165 // supported versions before a wallet is available. 166 SupportedVersions: []uint32{0}, 167 UnitInfo: dexeth.UnitInfo, 168 AvailableWallets: []*asset.WalletDefinition{ 169 // { 170 // Type: walletTypeGeth, 171 // Tab: "Native", 172 // Description: "Use the built-in DEX wallet (geth light node)", 173 // ConfigOpts: WalletOpts, 174 // Seeded: true, 175 // }, 176 { 177 Type: walletTypeRPC, 178 Tab: "RPC", 179 Description: "Infrastructure providers (e.g. Infura) or local nodes", 180 ConfigOpts: append(RPCOpts, walletOpts...), 181 Seeded: true, 182 GuideLink: "https://github.com/decred/dcrdex/blob/master/docs/wiki/Ethereum.md", 183 }, 184 // MaxSwapsInTx and MaxRedeemsInTx are set in (Wallet).Info, since 185 // the value cannot be known until we connect and get network info. 186 }, 187 IsAccountBased: true, 188 } 189 190 // unlimitedAllowance is the maximum supported allowance for an erc20 191 // contract, and is effectively unlimited. 192 unlimitedAllowance = ethmath.MaxBig256 193 // unlimitedAllowanceReplenishThreshold is the threshold below which we will 194 // require a new approval. In practice, this will never be hit, but any 195 // allowance below this will signal that WE didn't set it, and we'll require 196 // an upgrade to unlimited (since we don't support limited allowance yet). 197 unlimitedAllowanceReplenishThreshold = new(big.Int).Div(unlimitedAllowance, big.NewInt(2)) 198 199 seedDerivationPath = []uint32{ 200 hdkeychain.HardenedKeyStart + 44, // purpose 44' for HD wallets 201 hdkeychain.HardenedKeyStart + 60, // eth coin type 60' 202 hdkeychain.HardenedKeyStart, // account 0' 203 0, // branch 0 204 0, // index 0 205 } 206 ) 207 208 // perTxGasLimit is the most gas we can use on a transaction. It is the lower of 209 // either the per tx or per block gas limit. 210 func perTxGasLimit(gasFeeLimit uint64) uint64 { 211 // maxProportionOfBlockGasLimitToUse sets the maximum proportion of a 212 // block's gas limit that a swap and redeem transaction will use. Since it 213 // is set to 4, the max that will be used is 25% (1/4) of the block's gas 214 // limit. 215 const maxProportionOfBlockGasLimitToUse = 4 216 217 // blockGasLimit is the amount of gas we can use in one transaction 218 // according to the block gas limit. 219 220 // Ethereum GasCeil: 30_000_000, Polygon: 8_000_000 221 blockGasLimit := ethconfig.Defaults.Miner.GasCeil / maxProportionOfBlockGasLimitToUse 222 223 // txGasLimit is the amount of gas we can use in one transaction 224 // according to the default transaction gas fee limit. 225 txGasLimit := maxTxFeeGwei / gasFeeLimit 226 227 if blockGasLimit > txGasLimit { 228 return txGasLimit 229 } 230 return blockGasLimit 231 } 232 233 // safeConfs returns the confirmations for a given tip and block number, 234 // returning 0 if the block number is zero or if the tip is lower than the 235 // block number. 236 func safeConfs(tip, blockNum uint64) uint64 { 237 if blockNum == 0 { 238 return 0 239 } 240 if tip < blockNum { 241 return 0 242 } 243 return tip - blockNum + 1 244 } 245 246 // safeConfsBig is safeConfs but with a *big.Int blockNum. A nil blockNum will 247 // result in a zero. 248 func safeConfsBig(tip uint64, blockNum *big.Int) uint64 { 249 if blockNum == nil { 250 return 0 251 } 252 return safeConfs(tip, blockNum.Uint64()) 253 } 254 255 // WalletConfig are wallet-level configuration settings. 256 type WalletConfig struct { 257 GasFeeLimit uint64 `ini:"gasfeelimit"` 258 } 259 260 // parseWalletConfig parses the settings map into a *WalletConfig. 261 func parseWalletConfig(settings map[string]string) (cfg *WalletConfig, err error) { 262 cfg = new(WalletConfig) 263 err = config.Unmapify(settings, &cfg) 264 if err != nil { 265 return nil, fmt.Errorf("error parsing wallet config: %w", err) 266 } 267 return cfg, nil 268 } 269 270 // Driver implements asset.Driver. 271 type Driver struct{} 272 273 // Check that Driver implements Driver and Creator. 274 var _ asset.Driver = (*Driver)(nil) 275 var _ asset.Creator = (*Driver)(nil) 276 277 // Open opens the ETH exchange wallet. Start the wallet with its Run method. 278 func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { 279 return newWallet(cfg, logger, network) 280 } 281 282 // DecodeCoinID creates a human-readable representation of a coin ID for Ethereum. 283 // These are supported coin ID formats: 284 // 1. A transaction hash. 32 bytes 285 // 2. An encoded ETH funding coin id which includes the account address and 286 // amount. 20 + 8 = 28 bytes 287 // 3. An encoded token funding coin id which includes the account address, 288 // a token value, and fees. 20 + 8 + 8 = 36 bytes 289 // 4. A byte encoded string of the account address. 40 or 42 (with 0x) bytes 290 // 5. A byte encoded string which represents specific case where Taker found 291 // Maker redemption on his own (while Maker failed to notify him about it 292 // first). 26 (`TakerFoundMakerRedemption:` prefix) + 42 (Maker address 293 // with 0x) bytes 294 func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { 295 switch len(coinID) { 296 case common.HashLength: 297 var txHash common.Hash 298 copy(txHash[:], coinID) 299 return txHash.String(), nil 300 case fundingCoinIDSize: 301 c, err := decodeFundingCoin(coinID) 302 if err != nil { 303 return "", err 304 } 305 return c.String(), nil 306 case tokenFundingCoinIDSize: 307 c, err := decodeTokenFundingCoin(coinID) 308 if err != nil { 309 return "", err 310 } 311 return c.String(), nil 312 case common.AddressLength * 2, common.AddressLength*2 + 2: 313 hexAddr := string(coinID) 314 if !common.IsHexAddress(hexAddr) { 315 return "", fmt.Errorf("invalid hex address %q", coinID) 316 } 317 return common.HexToAddress(hexAddr).String(), nil 318 case len(coinIDTakerFoundMakerRedemption) + common.AddressLength*2 + 2: 319 coinIDStr := string(coinID) 320 if !strings.HasPrefix(coinIDStr, coinIDTakerFoundMakerRedemption) { 321 return "", fmt.Errorf("coinID %q has no %s prefix", coinID, coinIDTakerFoundMakerRedemption) 322 } 323 return coinIDStr, nil 324 } 325 326 return "", fmt.Errorf("unknown coin ID format: %x", coinID) 327 } 328 329 // Info returns basic information about the wallet and asset. 330 func (d *Driver) Info() *asset.WalletInfo { 331 wi := WalletInfo 332 return &wi 333 } 334 335 // Exists checks the existence of the wallet. 336 func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, net dex.Network) (bool, error) { 337 switch walletType { 338 case walletTypeGeth, walletTypeRPC: 339 default: 340 return false, fmt.Errorf("wallet type %q unrecognized", walletType) 341 } 342 343 keyStoreDir := filepath.Join(getWalletDir(dataDir, net), "keystore") 344 ks := keystore.NewKeyStore(keyStoreDir, keystore.LightScryptN, keystore.LightScryptP) 345 // NOTE: If the keystore did not exist, a warning from an internal KeyStore 346 // goroutine may be printed to this effect. Not an issue. 347 return len(ks.Wallets()) > 0, nil 348 } 349 350 func (d *Driver) Create(cfg *asset.CreateWalletParams) error { 351 comp, err := NetworkCompatibilityData(cfg.Net) 352 if err != nil { 353 return fmt.Errorf("error finding compatibility data: %v", err) 354 } 355 return CreateEVMWallet(dexeth.ChainIDs[cfg.Net], cfg, &comp, false) 356 } 357 358 // Balance is the current balance, including information about the pending 359 // balance. 360 type Balance struct { 361 Current, PendingIn, PendingOut *big.Int 362 } 363 364 // ethFetcher represents a blockchain information fetcher. In practice, it is 365 // satisfied by *nodeClient. For testing, it can be satisfied by a stub. 366 type ethFetcher interface { 367 address() common.Address 368 addressBalance(ctx context.Context, addr common.Address) (*big.Int, error) 369 bestHeader(ctx context.Context) (*types.Header, error) 370 chainConfig() *params.ChainConfig 371 connect(ctx context.Context) error 372 peerCount() uint32 373 contractBackend() bind.ContractBackend 374 headerByHash(ctx context.Context, txHash common.Hash) (*types.Header, error) 375 lock() error 376 locked() bool 377 shutdown() 378 sendSignedTransaction(ctx context.Context, tx *types.Transaction, filts ...acceptabilityFilter) error 379 sendTransaction(ctx context.Context, txOpts *bind.TransactOpts, to common.Address, data []byte, filts ...acceptabilityFilter) (*types.Transaction, error) 380 signData(data []byte) (sig, pubKey []byte, err error) 381 syncProgress(context.Context) (progress *ethereum.SyncProgress, tipTime uint64, err error) 382 transactionConfirmations(context.Context, common.Hash) (uint32, error) 383 getTransaction(context.Context, common.Hash) (*types.Transaction, int64, error) 384 txOpts(ctx context.Context, val, maxGas uint64, maxFeeRate, tipCap, nonce *big.Int) (*bind.TransactOpts, error) 385 currentFees(ctx context.Context) (baseFees, tipCap *big.Int, err error) 386 unlock(pw string) error 387 getConfirmedNonce(context.Context) (uint64, error) 388 transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) 389 transactionAndReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, *types.Transaction, error) 390 nonce(ctx context.Context) (confirmed, next *big.Int, err error) 391 } 392 393 // txPoolFetcher can be implemented by node types that support fetching of 394 // txpool transactions. 395 type txPoolFetcher interface { 396 pendingTransactions() ([]*types.Transaction, error) 397 } 398 399 type pendingApproval struct { 400 txHash common.Hash 401 onConfirm func() 402 } 403 404 type cachedBalance struct { 405 stamp time.Time 406 height uint64 407 bal *big.Int 408 } 409 410 // Check that assetWallet satisfies the asset.Wallet interface. 411 var _ asset.Wallet = (*ETHWallet)(nil) 412 var _ asset.Wallet = (*TokenWallet)(nil) 413 var _ asset.AccountLocker = (*ETHWallet)(nil) 414 var _ asset.AccountLocker = (*TokenWallet)(nil) 415 var _ asset.TokenMaster = (*ETHWallet)(nil) 416 var _ asset.WalletRestorer = (*ETHWallet)(nil) 417 var _ asset.LiveReconfigurer = (*ETHWallet)(nil) 418 var _ asset.LiveReconfigurer = (*TokenWallet)(nil) 419 var _ asset.TxFeeEstimator = (*ETHWallet)(nil) 420 var _ asset.TxFeeEstimator = (*TokenWallet)(nil) 421 var _ asset.DynamicSwapper = (*ETHWallet)(nil) 422 var _ asset.DynamicSwapper = (*TokenWallet)(nil) 423 var _ asset.Authenticator = (*ETHWallet)(nil) 424 var _ asset.TokenApprover = (*TokenWallet)(nil) 425 var _ asset.WalletHistorian = (*ETHWallet)(nil) 426 var _ asset.WalletHistorian = (*TokenWallet)(nil) 427 428 type baseWallet struct { 429 // The asset subsystem starts with Connect(ctx). This ctx will be initialized 430 // in parent ETHWallet once and re-used in child TokenWallet instances. 431 ctx context.Context 432 net dex.Network 433 node ethFetcher 434 addr common.Address 435 log dex.Logger 436 dir string 437 walletType string 438 439 finalizeConfs uint64 440 441 multiBalanceAddress common.Address 442 multiBalanceContract *multibal.MultiBalanceV0 443 444 baseChainID uint32 445 chainCfg *params.ChainConfig 446 chainID int64 447 compat *CompatibilityData 448 tokens map[uint32]*dexeth.Token 449 450 startingBlocks atomic.Uint64 451 452 tipMtx sync.RWMutex 453 currentTip *types.Header 454 455 settingsMtx sync.RWMutex 456 settings map[string]string 457 458 gasFeeLimitV uint64 // atomic 459 460 walletsMtx sync.RWMutex 461 wallets map[uint32]*assetWallet 462 463 nonceMtx sync.RWMutex 464 pendingTxs []*extendedWalletTx 465 confirmedNonceAt *big.Int 466 pendingNonceAt *big.Int 467 recoveryRequestSent bool 468 469 balances struct { 470 sync.Mutex 471 m map[uint32]*cachedBalance 472 } 473 474 currentFees struct { 475 sync.Mutex 476 blockNum uint64 477 baseRate *big.Int 478 tipRate *big.Int 479 } 480 481 txDB txDB 482 } 483 484 // assetWallet is a wallet backend for Ethereum and Eth tokens. The backend is 485 // how Bison Wallet communicates with the Ethereum blockchain and wallet. 486 // assetWallet satisfies the dex.Wallet interface. 487 type assetWallet struct { 488 *baseWallet 489 assetID uint32 490 emit *asset.WalletEmitter 491 log dex.Logger 492 ui dex.UnitInfo 493 connected atomic.Bool 494 wi asset.WalletInfo 495 496 versionedContracts map[uint32]common.Address 497 versionedGases map[uint32]*dexeth.Gases 498 499 maxSwapGas uint64 500 maxRedeemGas uint64 501 502 lockedFunds struct { 503 mtx sync.RWMutex 504 initiateReserves uint64 505 redemptionReserves uint64 506 refundReserves uint64 507 } 508 509 findRedemptionMtx sync.RWMutex 510 findRedemptionReqs map[[32]byte]*findRedemptionRequest 511 512 approvalsMtx sync.RWMutex 513 pendingApprovals map[uint32]*pendingApproval 514 approvalCache map[uint32]bool 515 516 lastPeerCount uint32 517 peersChange func(uint32, error) 518 519 contractors map[uint32]contractor // version -> contractor 520 521 evmify func(uint64) *big.Int 522 atomize func(*big.Int) uint64 523 524 // pendingTxCheckBal is protected by the nonceMtx. We use this field 525 // as a secondary check to see if we need to request confirmations for 526 // pending txs, since tips are cached for up to 10 seconds. We check the 527 // status of pending txs if the tip has changed OR if the balance has 528 // changed. 529 pendingTxCheckBal *big.Int 530 } 531 532 // ETHWallet implements some Ethereum-specific methods. 533 type ETHWallet struct { 534 // 64-bit atomic variables first. See 535 // https://golang.org/pkg/sync/atomic/#pkg-note-BUG 536 tipAtConnect int64 537 defaultProviders []string 538 539 *assetWallet 540 } 541 542 // TokenWallet implements some token-specific methods. 543 type TokenWallet struct { 544 *assetWallet 545 546 cfg *tokenWalletConfig 547 parent *assetWallet 548 token *dexeth.Token 549 netToken *dexeth.NetToken 550 } 551 552 func (w *assetWallet) maxSwapsAndRedeems() (maxSwaps, maxRedeems uint64) { 553 txGasLimit := perTxGasLimit(atomic.LoadUint64(&w.gasFeeLimitV)) 554 return txGasLimit / w.maxSwapGas, txGasLimit / w.maxRedeemGas 555 } 556 557 // Info returns basic information about the wallet and asset. 558 func (w *assetWallet) Info() *asset.WalletInfo { 559 wi := w.wi 560 maxSwaps, maxRedeems := w.maxSwapsAndRedeems() 561 wi.MaxSwapsInTx = maxSwaps 562 wi.MaxRedeemsInTx = maxRedeems 563 return &wi 564 } 565 566 // genWalletSeed uses the wallet seed passed from core as the entropy for 567 // generating a BIP-39 mnemonic. Then it returns the wallet seed generated 568 // from the mnemonic which can be used to derive a private key. 569 func genWalletSeed(entropy []byte) ([]byte, error) { 570 if len(entropy) < 32 || len(entropy) > 64 { 571 return nil, fmt.Errorf("wallet entropy must be 32 to 64 bytes long") 572 } 573 mnemonic, err := bip39.NewMnemonic(entropy) 574 if err != nil { 575 return nil, fmt.Errorf("error deriving mnemonic: %w", err) 576 } 577 return bip39.NewSeed(mnemonic, ""), nil 578 } 579 580 func privKeyFromSeed(seed []byte) (pk []byte, zero func(), err error) { 581 walletSeed, err := genWalletSeed(seed) 582 if err != nil { 583 return nil, nil, err 584 } 585 defer encode.ClearBytes(walletSeed) 586 587 extKey, err := keygen.GenDeepChild(walletSeed, seedDerivationPath) 588 if err != nil { 589 return nil, nil, err 590 } 591 // defer extKey.Zero() 592 pk, err = extKey.SerializedPrivKey() 593 if err != nil { 594 extKey.Zero() 595 return nil, nil, err 596 } 597 return pk, extKey.Zero, nil 598 } 599 600 func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams, compat *CompatibilityData, skipConnect bool) error { 601 switch createWalletParams.Type { 602 case walletTypeGeth: 603 return asset.ErrWalletTypeDisabled 604 case walletTypeRPC: 605 default: 606 return fmt.Errorf("wallet type %q unrecognized", createWalletParams.Type) 607 } 608 609 walletDir := getWalletDir(createWalletParams.DataDir, createWalletParams.Net) 610 611 privateKey, zero, err := privKeyFromSeed(createWalletParams.Seed) 612 if err != nil { 613 return err 614 } 615 defer zero() 616 617 switch createWalletParams.Type { 618 // case walletTypeGeth: 619 // node, err := prepareNode(&nodeConfig{ 620 // net: createWalletParams.Net, 621 // appDir: walletDir, 622 // }) 623 // if err != nil { 624 // return err 625 // } 626 // defer node.Close() 627 // return importKeyToNode(node, privateKey, createWalletParams.Pass) 628 case walletTypeRPC: 629 // Make the wallet dir if it does not exist, otherwise we may fail to 630 // write the compliant-providers.json file. Create the keystore 631 // subdirectory as well to avoid a "failed to watch keystore folder" 632 // error from the keystore's internal account cache supervisor. 633 keystoreDir := filepath.Join(walletDir, "keystore") 634 if err := os.MkdirAll(keystoreDir, 0700); err != nil { 635 return err 636 } 637 638 // TODO: This procedure may actually work for walletTypeGeth too. 639 ks := keystore.NewKeyStore(keystoreDir, keystore.LightScryptN, keystore.LightScryptP) 640 641 priv, err := crypto.ToECDSA(privateKey) 642 if err != nil { 643 return err 644 } 645 646 // If the user supplied endpoints, check them now. 647 providerDef := createWalletParams.Settings[providersKey] 648 if !skipConnect && len(providerDef) > 0 { 649 endpoints := strings.Split(providerDef, providerDelimiter) 650 if err := createAndCheckProviders(context.Background(), walletDir, endpoints, 651 big.NewInt(chainID), compat, createWalletParams.Net, createWalletParams.Logger, false); err != nil { 652 return fmt.Errorf("create and check providers: %v", err) 653 } 654 } 655 return importKeyToKeyStore(ks, priv, createWalletParams.Pass) 656 } 657 658 return fmt.Errorf("unknown wallet type %q", createWalletParams.Type) 659 } 660 661 // newWallet is the constructor for an Ethereum asset.Wallet. 662 func newWallet(assetCFG *asset.WalletConfig, logger dex.Logger, net dex.Network) (w *ETHWallet, err error) { 663 chainCfg, err := ChainConfig(net) 664 if err != nil { 665 return nil, fmt.Errorf("failed to locate Ethereum genesis configuration for network %s", net) 666 } 667 comp, err := NetworkCompatibilityData(net) 668 if err != nil { 669 return nil, fmt.Errorf("failed to locate Ethereum compatibility data: %s", net) 670 } 671 contracts := make(map[uint32]common.Address, 1) 672 for ver, netAddrs := range dexeth.ContractAddresses { 673 for netw, addr := range netAddrs { 674 if netw == net { 675 contracts[ver] = addr 676 break 677 } 678 } 679 } 680 681 var defaultProviders []string 682 switch net { 683 case dex.Simnet: 684 u, _ := user.Current() 685 defaultProviders = []string{filepath.Join(u.HomeDir, "dextest", "eth", "alpha", "node", "geth.ipc")} 686 case dex.Testnet: 687 defaultProviders = []string{ 688 "https://rpc.ankr.com/eth_sepolia", 689 "https://ethereum-sepolia.blockpi.network/v1/rpc/public", 690 "https://eth-sepolia.public.blastapi.io", 691 "https://endpoints.omniatech.io/v1/eth/sepolia/public", 692 "https://rpc-sepolia.rockx.com", 693 "https://rpc.sepolia.org", 694 "https://eth-sepolia-public.unifra.io", 695 } 696 case dex.Mainnet: 697 defaultProviders = []string{ 698 "https://rpc.ankr.com/eth", 699 "https://ethereum.blockpi.network/v1/rpc/public", 700 "https://eth-mainnet.nodereal.io/v1/1659dfb40aa24bbb8153a677b98064d7", 701 "https://rpc.builder0x69.io", 702 "https://rpc.flashbots.net", 703 "wss://eth.llamarpc.com", 704 } 705 } 706 707 return NewEVMWallet(&EVMWalletConfig{ 708 BaseChainID: BipID, 709 ChainCfg: chainCfg, 710 AssetCfg: assetCFG, 711 CompatData: &comp, 712 VersionedGases: dexeth.VersionedGases, 713 Tokens: dexeth.Tokens, 714 FinalizeConfs: 3, 715 Logger: logger, 716 BaseChainContracts: contracts, 717 MultiBalAddress: dexeth.MultiBalanceAddresses[net], 718 WalletInfo: WalletInfo, 719 Net: net, 720 DefaultProviders: defaultProviders, 721 }) 722 } 723 724 // EVMWalletConfig is the configuration for an evm-compatible wallet. 725 type EVMWalletConfig struct { 726 BaseChainID uint32 727 ChainCfg *params.ChainConfig 728 AssetCfg *asset.WalletConfig 729 CompatData *CompatibilityData 730 VersionedGases map[uint32]*dexeth.Gases 731 Tokens map[uint32]*dexeth.Token 732 FinalizeConfs uint64 733 Logger dex.Logger 734 BaseChainContracts map[uint32]common.Address 735 DefaultProviders []string 736 MultiBalAddress common.Address // If empty, separate calls for N tokens + 1 737 WalletInfo asset.WalletInfo 738 Net dex.Network 739 } 740 741 func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { 742 assetID := cfg.BaseChainID 743 chainID := cfg.ChainCfg.ChainID.Int64() 744 745 // var cl ethFetcher 746 switch cfg.AssetCfg.Type { 747 case walletTypeGeth: 748 return nil, asset.ErrWalletTypeDisabled 749 case walletTypeRPC: 750 if providerDef := cfg.AssetCfg.Settings[providersKey]; len(providerDef) == 0 && len(cfg.DefaultProviders) == 0 { 751 return nil, errors.New("no providers specified") 752 } 753 default: 754 return nil, fmt.Errorf("unknown wallet type %q", cfg.AssetCfg.Type) 755 } 756 757 wCfg, err := parseWalletConfig(cfg.AssetCfg.Settings) 758 if err != nil { 759 return nil, err 760 } 761 762 gasFeeLimit := wCfg.GasFeeLimit 763 if gasFeeLimit == 0 { 764 gasFeeLimit = defaultGasFeeLimit 765 } 766 eth := &baseWallet{ 767 net: cfg.Net, 768 baseChainID: cfg.BaseChainID, 769 chainCfg: cfg.ChainCfg, 770 chainID: chainID, 771 compat: cfg.CompatData, 772 tokens: cfg.Tokens, 773 log: cfg.Logger, 774 dir: cfg.AssetCfg.DataDir, 775 walletType: cfg.AssetCfg.Type, 776 finalizeConfs: cfg.FinalizeConfs, 777 settings: cfg.AssetCfg.Settings, 778 gasFeeLimitV: gasFeeLimit, 779 wallets: make(map[uint32]*assetWallet), 780 multiBalanceAddress: cfg.MultiBalAddress, 781 } 782 783 var maxSwapGas, maxRedeemGas uint64 784 for _, gases := range cfg.VersionedGases { 785 if gases.Swap > maxSwapGas { 786 maxSwapGas = gases.Swap 787 } 788 if gases.Redeem > maxRedeemGas { 789 maxRedeemGas = gases.Redeem 790 } 791 } 792 793 txGasLimit := perTxGasLimit(gasFeeLimit) 794 795 if maxSwapGas == 0 || txGasLimit < maxSwapGas { 796 return nil, errors.New("max swaps cannot be zero or undefined") 797 } 798 if maxRedeemGas == 0 || txGasLimit < maxRedeemGas { 799 return nil, errors.New("max redeems cannot be zero or undefined") 800 } 801 802 aw := &assetWallet{ 803 baseWallet: eth, 804 log: cfg.Logger, 805 assetID: assetID, 806 versionedContracts: cfg.BaseChainContracts, 807 versionedGases: cfg.VersionedGases, 808 maxSwapGas: maxSwapGas, 809 maxRedeemGas: maxRedeemGas, 810 emit: cfg.AssetCfg.Emit, 811 findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest), 812 pendingApprovals: make(map[uint32]*pendingApproval), 813 approvalCache: make(map[uint32]bool), 814 peersChange: cfg.AssetCfg.PeersChange, 815 contractors: make(map[uint32]contractor), 816 evmify: dexeth.GweiToWei, 817 atomize: dexeth.WeiToGwei, 818 ui: dexeth.UnitInfo, 819 pendingTxCheckBal: new(big.Int), 820 wi: cfg.WalletInfo, 821 } 822 823 maxSwaps, maxRedeems := aw.maxSwapsAndRedeems() 824 825 cfg.Logger.Debugf("ETH wallet will support a maximum of %d swaps and %d redeems per transaction.", 826 maxSwaps, maxRedeems) 827 828 aw.wallets = map[uint32]*assetWallet{ 829 assetID: aw, 830 } 831 832 return ÐWallet{ 833 assetWallet: aw, 834 defaultProviders: cfg.DefaultProviders, 835 }, nil 836 } 837 838 func getWalletDir(dataDir string, network dex.Network) string { 839 return filepath.Join(dataDir, network.String()) 840 } 841 842 // Connect connects to the node RPC server. Satisfies dex.Connector. 843 func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) { 844 var cl ethFetcher 845 switch w.walletType { 846 case walletTypeGeth: 847 // cl, err = newNodeClient(getWalletDir(w.dir, w.net), w.net, w.log.SubLogger("NODE")) 848 // if err != nil { 849 // return nil, err 850 // } 851 return nil, asset.ErrWalletTypeDisabled 852 case walletTypeRPC: 853 w.settingsMtx.RLock() 854 defer w.settingsMtx.RUnlock() 855 endpoints := w.defaultProviders 856 if providerDef, found := w.settings[providersKey]; found && len(providerDef) > 0 { 857 endpoints = strings.Split(providerDef, " ") 858 } 859 rpcCl, err := newMultiRPCClient(w.dir, endpoints, w.log.SubLogger("RPC"), w.chainCfg, w.finalizeConfs, w.net) 860 if err != nil { 861 return nil, err 862 } 863 rpcCl.finalizeConfs = w.finalizeConfs 864 cl = rpcCl 865 default: 866 return nil, fmt.Errorf("unknown wallet type %q", w.walletType) 867 } 868 869 w.node = cl 870 w.addr = cl.address() 871 w.ctx = ctx // TokenWallet will re-use this ctx. 872 873 err = w.node.connect(ctx) 874 if err != nil { 875 return nil, err 876 } 877 878 for ver, constructor := range contractorConstructors { 879 contractAddr, exists := w.versionedContracts[ver] 880 if !exists || contractAddr == (common.Address{}) { 881 return nil, fmt.Errorf("no contract address for version %d, net %s", ver, w.net) 882 } 883 c, err := constructor(contractAddr, w.addr, w.node.contractBackend()) 884 if err != nil { 885 return nil, fmt.Errorf("error constructor version %d contractor: %v", ver, err) 886 } 887 w.contractors[ver] = c 888 } 889 890 if w.multiBalanceAddress != (common.Address{}) { 891 w.multiBalanceContract, err = multibal.NewMultiBalanceV0(w.multiBalanceAddress, cl.contractBackend()) 892 if err != nil { 893 w.log.Errorf("Error loading MultiBalance contract: %v", err) 894 } 895 } 896 897 w.txDB, err = newBadgerTxDB(filepath.Join(w.dir, "txhistorydb"), w.log.SubLogger("TXDB")) 898 if err != nil { 899 return nil, err 900 } 901 902 txCM := dex.NewConnectionMaster(w.txDB) 903 if err := txCM.ConnectOnce(ctx); err != nil { 904 return nil, err 905 } 906 907 pendingTxs, err := w.txDB.getPendingTxs() 908 if err != nil { 909 return nil, err 910 } 911 sort.Slice(pendingTxs, func(i, j int) bool { 912 return pendingTxs[i].Nonce.Cmp(pendingTxs[j].Nonce) < 0 913 }) 914 915 // Initialize the best block. 916 bestHdr, err := w.node.bestHeader(ctx) 917 if err != nil { 918 return nil, fmt.Errorf("error getting best block hash: %w", err) 919 } 920 confirmedNonce, nextNonce, err := w.node.nonce(ctx) 921 if err != nil { 922 return nil, fmt.Errorf("error establishing nonce: %w", err) 923 } 924 925 w.tipMtx.Lock() 926 w.currentTip = bestHdr 927 w.tipMtx.Unlock() 928 w.startingBlocks.Store(bestHdr.Number.Uint64()) 929 930 w.nonceMtx.Lock() 931 w.pendingTxs = pendingTxs 932 w.confirmedNonceAt = confirmedNonce 933 w.pendingNonceAt = nextNonce 934 w.nonceMtx.Unlock() 935 936 if w.log.Level() <= dex.LevelDebug { 937 var highestPendingNonce, lowestPendingNonce uint64 938 for _, pendingTx := range pendingTxs { 939 n := pendingTx.Nonce.Uint64() 940 if n > highestPendingNonce { 941 highestPendingNonce = n 942 } 943 if lowestPendingNonce == 0 || n < lowestPendingNonce { 944 lowestPendingNonce = n 945 } 946 } 947 w.log.Debugf("Synced with header %s and confirmed nonce %s, pending nonce %s, %d pending txs from nonce %d to nonce %d", 948 bestHdr.Number, confirmedNonce, nextNonce, len(pendingTxs), highestPendingNonce, lowestPendingNonce) 949 } 950 951 height := w.currentTip.Number 952 // NOTE: We should be using the tipAtConnect to set Progress in SyncStatus. 953 atomic.StoreInt64(&w.tipAtConnect, height.Int64()) 954 w.log.Infof("Connected to eth (%s), at height %d", w.walletType, height) 955 956 var wg sync.WaitGroup 957 958 wg.Add(1) 959 go func() { 960 defer wg.Done() 961 w.monitorBlocks(ctx) 962 w.node.shutdown() 963 }() 964 965 wg.Add(1) 966 go func() { 967 defer wg.Done() 968 w.monitorPeers(ctx) 969 }() 970 971 w.connected.Store(true) 972 go func() { 973 <-ctx.Done() 974 txCM.Wait() 975 w.connected.Store(false) 976 }() 977 978 return &wg, nil 979 } 980 981 // Connect waits for context cancellation and closes the WaitGroup. Satisfies 982 // dex.Connector. 983 func (w *TokenWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) { 984 if w.parent.ctx == nil || w.parent.ctx.Err() != nil { 985 return nil, fmt.Errorf("parent wallet not connected") 986 } 987 988 err := w.loadContractors() 989 if err != nil { 990 return nil, err 991 } 992 993 w.connected.Store(true) 994 995 var wg sync.WaitGroup 996 wg.Add(1) 997 go func() { 998 defer wg.Done() 999 select { 1000 case <-ctx.Done(): 1001 case <-w.parent.ctx.Done(): 1002 w.connected.Store(false) 1003 } 1004 }() 1005 1006 return &wg, nil 1007 } 1008 1009 // tipHeight gets the current best header's tip height. 1010 func (w *baseWallet) tipHeight() uint64 { 1011 w.tipMtx.RLock() 1012 defer w.tipMtx.RUnlock() 1013 return w.currentTip.Number.Uint64() 1014 } 1015 1016 // Reconfigure attempts to reconfigure the wallet. 1017 func (w *ETHWallet) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, currentAddress string) (restart bool, err error) { 1018 walletCfg, err := parseWalletConfig(cfg.Settings) 1019 if err != nil { 1020 return false, err 1021 } 1022 1023 gasFeeLimit := walletCfg.GasFeeLimit 1024 if walletCfg.GasFeeLimit == 0 { 1025 gasFeeLimit = defaultGasFeeLimit 1026 } 1027 1028 // For now, we only are supporting multiRPCClient nodes. If we re-implement 1029 // P2P nodes, we'll have to add protection to the node field to allow for 1030 // reconfiguration of type. 1031 // We also need to consider how to handle the pendingTxs if we switch node 1032 // types, since right now we only use that map for multiRPCClient. 1033 1034 if rpc, is := w.node.(*multiRPCClient); is { 1035 walletDir := getWalletDir(w.dir, w.net) 1036 providerDef := cfg.Settings[providersKey] 1037 var defaultProviders bool 1038 var endpoints []string 1039 if len(providerDef) > 0 { 1040 endpoints = strings.Split(providerDef, " ") 1041 } else { 1042 endpoints = w.defaultProviders 1043 defaultProviders = true 1044 } 1045 1046 if err := rpc.reconfigure(ctx, endpoints, w.compat, walletDir, defaultProviders); err != nil { 1047 return false, err 1048 } 1049 } 1050 1051 w.settingsMtx.Lock() 1052 w.settings = cfg.Settings 1053 w.settingsMtx.Unlock() 1054 1055 atomic.StoreUint64(&w.baseWallet.gasFeeLimitV, gasFeeLimit) 1056 1057 return false, nil 1058 } 1059 1060 // Reconfigure attempts to reconfigure the wallet. The token wallet has 1061 // no configurations. 1062 func (w *TokenWallet) Reconfigure(context.Context, *asset.WalletConfig, string) (bool, error) { 1063 return false, nil 1064 } 1065 1066 func (eth *baseWallet) connectedWallets() []*assetWallet { 1067 eth.walletsMtx.RLock() 1068 defer eth.walletsMtx.RUnlock() 1069 1070 m := make([]*assetWallet, 0, len(eth.wallets)) 1071 1072 for _, w := range eth.wallets { 1073 if w.connected.Load() { 1074 m = append(m, w) 1075 } 1076 } 1077 return m 1078 } 1079 1080 func (eth *baseWallet) wallet(assetID uint32) *assetWallet { 1081 eth.walletsMtx.RLock() 1082 defer eth.walletsMtx.RUnlock() 1083 return eth.wallets[assetID] 1084 } 1085 1086 func (eth *baseWallet) gasFeeLimit() uint64 { 1087 return atomic.LoadUint64(ð.gasFeeLimitV) 1088 } 1089 1090 // transactionGenerator is an action that uses a nonce and returns a tx, it's 1091 // type specifier, and its value. 1092 type transactionGenerator func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) 1093 1094 // withNonce is called with a function intended to generate a new transaction 1095 // using the next available nonce. If the function returns a non-nil tx, the 1096 // nonce will be treated as used, and an extendedWalletTransaction will be 1097 // generated, stored, and queued for monitoring. 1098 func (w *assetWallet) withNonce(ctx context.Context, f transactionGenerator) (err error) { 1099 w.nonceMtx.Lock() 1100 defer w.nonceMtx.Unlock() 1101 if err = nonceIsSane(w.pendingTxs, w.pendingNonceAt); err != nil { 1102 return err 1103 } 1104 nonce := func() *big.Int { 1105 n := new(big.Int).Set(w.confirmedNonceAt) 1106 for _, pendingTx := range w.pendingTxs { 1107 if pendingTx.Nonce.Cmp(n) < 0 { 1108 continue 1109 } 1110 if pendingTx.Nonce.Cmp(n) == 0 { 1111 n.Add(n, big.NewInt(1)) 1112 } else { 1113 break 1114 } 1115 } 1116 return n 1117 } 1118 1119 n := nonce() 1120 w.log.Trace("Nonce chosen for tx generator =", n) 1121 1122 // Make a first attempt with our best-known nonce. 1123 tx, txType, amt, recipient, err := f(n) 1124 if err != nil && strings.Contains(err.Error(), "nonce too low") { 1125 w.log.Warnf("Too-low nonce detected. Attempting recovery") 1126 confirmedNonceAt, pendingNonceAt, err := w.node.nonce(ctx) 1127 if err != nil { 1128 return fmt.Errorf("error during too-low nonce recovery: %v", err) 1129 } 1130 w.confirmedNonceAt = confirmedNonceAt 1131 w.pendingNonceAt = pendingNonceAt 1132 if newNonce := nonce(); newNonce != n { 1133 n = newNonce 1134 // Try again. 1135 tx, txType, amt, recipient, err = f(n) 1136 if err != nil { 1137 return err 1138 } 1139 w.log.Info("Nonce recovered and transaction broadcast") 1140 } else { 1141 return fmt.Errorf("best RPC nonce %d not better than our best nonce %d", newNonce, n) 1142 } 1143 } 1144 1145 if tx != nil { 1146 et := w.extendedTx(tx, txType, amt, recipient) 1147 w.pendingTxs = append(w.pendingTxs, et) 1148 if n.Cmp(w.pendingNonceAt) >= 0 { 1149 w.pendingNonceAt.Add(n, big.NewInt(1)) 1150 } 1151 w.emitTransactionNote(et.WalletTransaction, true) 1152 w.log.Tracef("Transaction %s generated for nonce %s", et.ID, n) 1153 } 1154 return err 1155 } 1156 1157 // nonceIsSane performs sanity checks on pending txs. 1158 func nonceIsSane(pendingTxs []*extendedWalletTx, pendingNonceAt *big.Int) error { 1159 if len(pendingTxs) == 0 && pendingNonceAt == nil { 1160 return errors.New("no pending txs and no best nonce") 1161 } 1162 var lastNonce uint64 1163 var numNotIndexed, confirmedTip int 1164 for i, pendingTx := range pendingTxs { 1165 if !pendingTx.savedToDB { 1166 return errors.New("tx database problem detected") 1167 } 1168 nonce := pendingTx.Nonce.Uint64() 1169 if nonce < lastNonce { 1170 return fmt.Errorf("pending txs not sorted") 1171 } 1172 if pendingTx.Confirmed || pendingTx.BlockNumber != 0 { 1173 if confirmedTip != i { 1174 return fmt.Errorf("confirmed tx sequence error. pending tx %s is confirmed but older txs were not", pendingTx.ID) 1175 1176 } 1177 confirmedTip = i + 1 1178 continue 1179 } 1180 lastNonce = nonce 1181 age := pendingTx.age() 1182 // Only allow a handful of txs that we haven't been seen on-chain yet. 1183 if age > stateUpdateTick*10 { 1184 numNotIndexed++ 1185 } 1186 if age >= txAgeOut { 1187 // If any tx is unindexed and aged out, wait for user to fix it. 1188 return fmt.Errorf("tx %s is aged out. waiting for user to take action", pendingTx.ID) 1189 } 1190 1191 } 1192 if numNotIndexed >= maxUnindexedTxs { 1193 return fmt.Errorf("%d unindexed txs has reached the limit of %d", numNotIndexed, maxUnindexedTxs) 1194 } 1195 return nil 1196 } 1197 1198 // tokenWalletConfig is the configuration options for token wallets. 1199 type tokenWalletConfig struct { 1200 // LimitAllowance disabled for now. 1201 // See https://github.com/decred/dcrdex/pull/1394#discussion_r780479402. 1202 // LimitAllowance bool `ini:"limitAllowance"` 1203 } 1204 1205 // parseTokenWalletConfig parses the settings map into a *tokenWalletConfig. 1206 func parseTokenWalletConfig(settings map[string]string) (cfg *tokenWalletConfig, err error) { 1207 cfg = new(tokenWalletConfig) 1208 err = config.Unmapify(settings, cfg) 1209 if err != nil { 1210 return nil, fmt.Errorf("error parsing wallet config: %w", err) 1211 } 1212 return cfg, nil 1213 } 1214 1215 // CreateTokenWallet "creates" a wallet for a token. There is really nothing 1216 // to do, except check that the token exists. 1217 func (w *baseWallet) CreateTokenWallet(tokenID uint32, _ map[string]string) error { 1218 // Just check that the token exists for now. 1219 if w.tokens[tokenID] == nil { 1220 return fmt.Errorf("token not found for asset ID %d", tokenID) 1221 } 1222 return nil 1223 } 1224 1225 // OpenTokenWallet creates a new TokenWallet. 1226 func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, error) { 1227 token, found := w.tokens[tokenCfg.AssetID] 1228 if !found { 1229 return nil, fmt.Errorf("token %d not found", tokenCfg.AssetID) 1230 } 1231 1232 cfg, err := parseTokenWalletConfig(tokenCfg.Settings) 1233 if err != nil { 1234 return nil, err 1235 } 1236 1237 netToken := token.NetTokens[w.net] 1238 if netToken == nil || len(netToken.SwapContracts) == 0 { 1239 return nil, fmt.Errorf("could not find token with ID %d on network %s", w.assetID, w.net) 1240 } 1241 1242 var maxSwapGas, maxRedeemGas uint64 1243 for _, contract := range netToken.SwapContracts { 1244 if contract.Gas.Swap > maxSwapGas { 1245 maxSwapGas = contract.Gas.Swap 1246 } 1247 if contract.Gas.Redeem > maxRedeemGas { 1248 maxRedeemGas = contract.Gas.Redeem 1249 } 1250 } 1251 1252 txGasLimit := perTxGasLimit(atomic.LoadUint64(&w.gasFeeLimitV)) 1253 1254 if maxSwapGas == 0 || txGasLimit < maxSwapGas { 1255 return nil, errors.New("max swaps cannot be zero or undefined") 1256 } 1257 if maxRedeemGas == 0 || txGasLimit < maxRedeemGas { 1258 return nil, errors.New("max redeems cannot be zero or undefined") 1259 } 1260 1261 contracts := make(map[uint32]common.Address) 1262 gases := make(map[uint32]*dexeth.Gases) 1263 for ver, c := range netToken.SwapContracts { 1264 contracts[ver] = c.Address 1265 gases[ver] = &c.Gas 1266 } 1267 1268 aw := &assetWallet{ 1269 baseWallet: w.baseWallet, 1270 log: w.baseWallet.log.SubLogger(strings.ToUpper(dex.BipIDSymbol(tokenCfg.AssetID))), 1271 assetID: tokenCfg.AssetID, 1272 versionedContracts: contracts, 1273 versionedGases: gases, 1274 maxSwapGas: maxSwapGas, 1275 maxRedeemGas: maxRedeemGas, 1276 emit: tokenCfg.Emit, 1277 peersChange: tokenCfg.PeersChange, 1278 findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest), 1279 pendingApprovals: make(map[uint32]*pendingApproval), 1280 approvalCache: make(map[uint32]bool), 1281 contractors: make(map[uint32]contractor), 1282 evmify: token.AtomicToEVM, 1283 atomize: token.EVMToAtomic, 1284 ui: token.UnitInfo, 1285 wi: asset.WalletInfo{ 1286 Name: token.Name, 1287 SupportedVersions: w.wi.SupportedVersions, 1288 UnitInfo: token.UnitInfo, 1289 }, 1290 pendingTxCheckBal: new(big.Int), 1291 } 1292 1293 w.baseWallet.walletsMtx.Lock() 1294 w.baseWallet.wallets[tokenCfg.AssetID] = aw 1295 w.baseWallet.walletsMtx.Unlock() 1296 1297 return &TokenWallet{ 1298 assetWallet: aw, 1299 cfg: cfg, 1300 parent: w.assetWallet, 1301 token: token, 1302 netToken: netToken, 1303 }, nil 1304 } 1305 1306 // OwnsDepositAddress indicates if an address belongs to the wallet. The address 1307 // need not be a EIP55-compliant formatted address. It may or may not have a 0x 1308 // prefix, and case is not important. 1309 // 1310 // In Ethereum, an address is an account. 1311 func (eth *baseWallet) OwnsDepositAddress(address string) (bool, error) { 1312 if !common.IsHexAddress(address) { 1313 return false, errors.New("invalid address") 1314 } 1315 addr := common.HexToAddress(address) 1316 return addr == eth.addr, nil 1317 } 1318 1319 func (w *assetWallet) amtString(amt uint64) string { 1320 return fmt.Sprintf("%s %s", w.ui.ConventionalString(amt), w.ui.Conventional.Unit) 1321 } 1322 1323 // fundReserveType represents the various uses for which funds need to be locked: 1324 // initiations, redemptions, and refunds. 1325 type fundReserveType uint32 1326 1327 const ( 1328 initiationReserve fundReserveType = iota 1329 redemptionReserve 1330 refundReserve 1331 ) 1332 1333 func (f fundReserveType) String() string { 1334 switch f { 1335 case initiationReserve: 1336 return "initiation" 1337 case redemptionReserve: 1338 return "redemption" 1339 case refundReserve: 1340 return "refund" 1341 default: 1342 return "" 1343 } 1344 } 1345 1346 // fundReserveOfType returns a pointer to the funds reserved for a particular 1347 // use case. 1348 func (w *assetWallet) fundReserveOfType(t fundReserveType) *uint64 { 1349 switch t { 1350 case initiationReserve: 1351 return &w.lockedFunds.initiateReserves 1352 case redemptionReserve: 1353 return &w.lockedFunds.redemptionReserves 1354 case refundReserve: 1355 return &w.lockedFunds.refundReserves 1356 default: 1357 panic(fmt.Sprintf("invalid fund reserve type: %v", t)) 1358 } 1359 } 1360 1361 // lockFunds locks funds for a use case. 1362 func (w *assetWallet) lockFunds(amt uint64, t fundReserveType) error { 1363 balance, err := w.balance() 1364 if err != nil { 1365 return err 1366 } 1367 1368 if balance.Available < amt { 1369 return fmt.Errorf("attempting to lock more %s for %s than is currently available. %d > %d %s", 1370 dex.BipIDSymbol(w.assetID), t, amt, balance.Available, w.ui.AtomicUnit) 1371 } 1372 1373 w.lockedFunds.mtx.Lock() 1374 defer w.lockedFunds.mtx.Unlock() 1375 1376 *w.fundReserveOfType(t) += amt 1377 return nil 1378 } 1379 1380 // unlockFunds unlocks funds for a use case. 1381 func (w *assetWallet) unlockFunds(amt uint64, t fundReserveType) { 1382 w.lockedFunds.mtx.Lock() 1383 defer w.lockedFunds.mtx.Unlock() 1384 1385 reserve := w.fundReserveOfType(t) 1386 1387 if *reserve < amt { 1388 *reserve = 0 1389 w.log.Errorf("attempting to unlock more for %s than is currently locked - %d > %d. "+ 1390 "clearing all locked funds", t, amt, *reserve) 1391 return 1392 } 1393 1394 *reserve -= amt 1395 } 1396 1397 // amountLocked returns the total amount currently locked. 1398 func (w *assetWallet) amountLocked() uint64 { 1399 w.lockedFunds.mtx.RLock() 1400 defer w.lockedFunds.mtx.RUnlock() 1401 return w.lockedFunds.initiateReserves + w.lockedFunds.redemptionReserves + w.lockedFunds.refundReserves 1402 } 1403 1404 // Balance returns the available and locked funds (token or eth). 1405 func (w *assetWallet) Balance() (*asset.Balance, error) { 1406 return w.balance() 1407 } 1408 1409 // balance returns the total available funds in the account. 1410 func (w *assetWallet) balance() (*asset.Balance, error) { 1411 bal, err := w.balanceWithTxPool() 1412 if err != nil { 1413 return nil, fmt.Errorf("pending balance error: %w", err) 1414 } 1415 1416 locked := w.amountLocked() 1417 var available uint64 1418 if w.atomize(bal.Current) > locked+w.atomize(bal.PendingOut) { 1419 available = w.atomize(bal.Current) - locked - w.atomize(bal.PendingOut) 1420 } 1421 1422 return &asset.Balance{ 1423 Available: available, 1424 Locked: locked, 1425 Immature: w.atomize(bal.PendingIn), 1426 }, nil 1427 } 1428 1429 // MaxOrder generates information about the maximum order size and associated 1430 // fees that the wallet can support for the given DEX configuration. The fees are an 1431 // estimate based on current network conditions, and will be <= the fees 1432 // associated with nfo.MaxFeeRate. For quote assets, the caller will have to 1433 // calculate lotSize based on a rate conversion from the base asset's lot size. 1434 func (w *ETHWallet) MaxOrder(ord *asset.MaxOrderForm) (*asset.SwapEstimate, error) { 1435 return w.maxOrder(ord.LotSize, ord.MaxFeeRate, ord.AssetVersion, 1436 ord.RedeemVersion, ord.RedeemAssetID, nil) 1437 } 1438 1439 // MaxOrder generates information about the maximum order size and associated 1440 // fees that the wallet can support for the given DEX configuration. 1441 func (w *TokenWallet) MaxOrder(ord *asset.MaxOrderForm) (*asset.SwapEstimate, error) { 1442 return w.maxOrder(ord.LotSize, ord.MaxFeeRate, ord.AssetVersion, 1443 ord.RedeemVersion, ord.RedeemAssetID, w.parent) 1444 } 1445 1446 func (w *assetWallet) maxOrder(lotSize uint64, maxFeeRate uint64, ver uint32, 1447 redeemVer, redeemAssetID uint32, feeWallet *assetWallet) (*asset.SwapEstimate, error) { 1448 balance, err := w.Balance() 1449 if err != nil { 1450 return nil, err 1451 } 1452 // Get the refund gas. 1453 if g := w.gases(ver); g == nil { 1454 return nil, fmt.Errorf("no gas table") 1455 } 1456 1457 g, err := w.initGasEstimate(1, ver, redeemVer, redeemAssetID) 1458 liveEstimateFailed := errors.Is(err, LiveEstimateFailedError) 1459 if err != nil && !liveEstimateFailed { 1460 return nil, fmt.Errorf("gasEstimate error: %w", err) 1461 } 1462 1463 refundCost := g.Refund * maxFeeRate 1464 oneFee := g.oneGas * maxFeeRate 1465 feeReservesPerLot := oneFee + refundCost 1466 var lots uint64 1467 if feeWallet == nil { 1468 lots = balance.Available / (lotSize + feeReservesPerLot) 1469 } else { // token 1470 lots = balance.Available / lotSize 1471 parentBal, err := feeWallet.Balance() 1472 if err != nil { 1473 return nil, fmt.Errorf("error getting base chain balance: %w", err) 1474 } 1475 feeLots := parentBal.Available / feeReservesPerLot 1476 if feeLots < lots { 1477 w.log.Infof("MaxOrder reducing lots because of low fee reserves: %d -> %d", lots, feeLots) 1478 lots = feeLots 1479 } 1480 } 1481 1482 if lots < 1 { 1483 return &asset.SwapEstimate{ 1484 FeeReservesPerLot: feeReservesPerLot, 1485 }, nil 1486 } 1487 return w.estimateSwap(lots, lotSize, maxFeeRate, ver, feeReservesPerLot) 1488 } 1489 1490 // PreSwap gets order estimates based on the available funds and the wallet 1491 // configuration. 1492 func (w *ETHWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) { 1493 return w.preSwap(req, nil) 1494 } 1495 1496 // PreSwap gets order estimates based on the available funds and the wallet 1497 // configuration. 1498 func (w *TokenWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) { 1499 return w.preSwap(req, w.parent) 1500 } 1501 1502 func (w *assetWallet) preSwap(req *asset.PreSwapForm, feeWallet *assetWallet) (*asset.PreSwap, error) { 1503 maxEst, err := w.maxOrder(req.LotSize, req.MaxFeeRate, req.Version, 1504 req.RedeemVersion, req.RedeemAssetID, feeWallet) 1505 if err != nil { 1506 return nil, err 1507 } 1508 1509 if maxEst.Lots < req.Lots { 1510 return nil, fmt.Errorf("%d lots available for %d-lot order", maxEst.Lots, req.Lots) 1511 } 1512 1513 est, err := w.estimateSwap(req.Lots, req.LotSize, req.MaxFeeRate, 1514 req.Version, maxEst.FeeReservesPerLot) 1515 if err != nil { 1516 return nil, err 1517 } 1518 1519 return &asset.PreSwap{ 1520 Estimate: est, 1521 }, nil 1522 } 1523 1524 // MaxFundingFees returns 0 because ETH does not have funding fees. 1525 func (w *baseWallet) MaxFundingFees(_ uint32, _ uint64, _ map[string]string) uint64 { 1526 return 0 1527 } 1528 1529 // SingleLotSwapRefundFees returns the fees for a swap transaction for a single lot. 1530 func (w *assetWallet) SingleLotSwapRefundFees(version uint32, feeSuggestion uint64, _ bool) (swapFees uint64, refundFees uint64, err error) { 1531 if version == asset.VersionNewest { 1532 version = contractVersionNewest 1533 } 1534 g := w.gases(version) 1535 if g == nil { 1536 return 0, 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version) 1537 } 1538 return g.Swap * feeSuggestion, g.Refund * feeSuggestion, nil 1539 } 1540 1541 // estimateSwap prepares an *asset.SwapEstimate. The estimate does not include 1542 // funds that might be locked for refunds. 1543 func (w *assetWallet) estimateSwap( 1544 lots, lotSize uint64, maxFeeRate uint64, ver uint32, feeReservesPerLot uint64, 1545 ) (*asset.SwapEstimate, error) { 1546 1547 if lots == 0 { 1548 return &asset.SwapEstimate{ 1549 FeeReservesPerLot: feeReservesPerLot, 1550 }, nil 1551 } 1552 1553 feeRate, err := w.currentFeeRate(w.ctx) 1554 if err != nil { 1555 return nil, err 1556 } 1557 feeRateGwei := dexeth.WeiToGweiCeil(feeRate) 1558 // This is an estimate, so we use the (lower) live gas estimates. 1559 oneSwap, err := w.estimateInitGas(w.ctx, 1, ver) 1560 if err != nil { 1561 return nil, fmt.Errorf("(%d) error estimating swap gas: %v", w.assetID, err) 1562 } 1563 1564 // NOTE: nSwap is neither best nor worst case. A single match can be 1565 // multiple lots. See RealisticBestCase descriptions. 1566 1567 value := lots * lotSize 1568 oneGasMax := oneSwap * lots 1569 maxFees := oneGasMax * maxFeeRate 1570 1571 return &asset.SwapEstimate{ 1572 Lots: lots, 1573 Value: value, 1574 MaxFees: maxFees, 1575 RealisticWorstCase: oneGasMax * feeRateGwei, 1576 RealisticBestCase: oneSwap * feeRateGwei, // not even batch, just perfect match 1577 FeeReservesPerLot: feeReservesPerLot, 1578 }, nil 1579 } 1580 1581 // gases gets the gas table for the specified contract version. 1582 func (w *assetWallet) gases(contractVer uint32) *dexeth.Gases { 1583 return gases(contractVer, w.versionedGases) 1584 } 1585 1586 // PreRedeem generates an estimate of the range of redemption fees that could 1587 // be assessed. 1588 func (w *assetWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) { 1589 oneRedeem, nRedeem, err := w.redeemGas(int(req.Lots), req.Version) 1590 if err != nil { 1591 return nil, err 1592 } 1593 1594 return &asset.PreRedeem{ 1595 Estimate: &asset.RedeemEstimate{ 1596 RealisticBestCase: nRedeem * req.FeeSuggestion, 1597 RealisticWorstCase: oneRedeem * req.Lots * req.FeeSuggestion, 1598 }, 1599 }, nil 1600 } 1601 1602 // SingleLotRedeemFees returns the fees for a redeem transaction for a single lot. 1603 func (w *assetWallet) SingleLotRedeemFees(version uint32, feeSuggestion uint64) (fees uint64, err error) { 1604 if version == asset.VersionNewest { 1605 version = contractVersionNewest 1606 } 1607 g := w.gases(version) 1608 if g == nil { 1609 return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version) 1610 } 1611 1612 return g.Redeem * feeSuggestion, nil 1613 } 1614 1615 // coin implements the asset.Coin interface for ETH 1616 type coin struct { 1617 id common.Hash 1618 // the value can be determined from the coin id, but for some 1619 // coin ids a lookup would be required from the blockchain to 1620 // determine its value, so this field is used as a cache. 1621 value uint64 1622 } 1623 1624 // ID is the ETH coins ID. For functions related to funding an order, 1625 // the ID must contain an encoded fundingCoinID, but when returned from 1626 // Swap, it will contain the transaction hash used to initiate the swap. 1627 func (c *coin) ID() dex.Bytes { 1628 return c.id[:] 1629 } 1630 1631 func (c *coin) TxID() string { 1632 return c.String() 1633 } 1634 1635 // String is a string representation of the coin. 1636 func (c *coin) String() string { 1637 return c.id.String() 1638 } 1639 1640 // Value returns the value in gwei of the coin. 1641 func (c *coin) Value() uint64 { 1642 return c.value 1643 } 1644 1645 var _ asset.Coin = (*coin)(nil) 1646 1647 func (eth *ETHWallet) createFundingCoin(amount uint64) *fundingCoin { 1648 return createFundingCoin(eth.addr, amount) 1649 } 1650 1651 func (eth *TokenWallet) createTokenFundingCoin(amount, fees uint64) *tokenFundingCoin { 1652 return createTokenFundingCoin(eth.addr, amount, fees) 1653 } 1654 1655 // FundOrder locks value for use in an order. 1656 func (w *ETHWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint64, error) { 1657 if ord.MaxFeeRate < dexeth.MinGasTipCap { 1658 return nil, nil, 0, fmt.Errorf("%v: server's max fee rate is lower than our min gas tip cap. %d < %d", 1659 dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, dexeth.MinGasTipCap) 1660 } 1661 1662 if w.gasFeeLimit() < ord.MaxFeeRate { 1663 return nil, nil, 0, fmt.Errorf( 1664 "%v: server's max fee rate %v higher than configured fee rate limit %v", 1665 dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) 1666 } 1667 1668 g, err := w.initGasEstimate(int(ord.MaxSwapCount), ord.Version, 1669 ord.RedeemVersion, ord.RedeemAssetID) 1670 if err != nil { 1671 return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) 1672 } 1673 1674 ethToLock := ord.MaxFeeRate*g.Swap*ord.MaxSwapCount + ord.Value 1675 // Note: In a future refactor, we could lock the redemption funds here too 1676 // and signal to the user so that they don't call `RedeemN`. This has the 1677 // same net effect, but avoids a lockFunds -> unlockFunds for us and likely 1678 // some work for the caller as well. We can't just always do it that way and 1679 // remove RedeemN, since we can't guarantee that the redemption asset is in 1680 // our fee-family. though it could still be an AccountRedeemer. 1681 w.log.Debugf("Locking %s to swap %s in up to %d swaps at a fee rate of %d gwei/gas using up to %d gas per swap", 1682 w.amtString(ethToLock), w.amtString(ord.Value), ord.MaxSwapCount, ord.MaxFeeRate, g.Swap) 1683 1684 coin := w.createFundingCoin(ethToLock) 1685 1686 if err = w.lockFunds(ethToLock, initiationReserve); err != nil { 1687 return nil, nil, 0, err 1688 } 1689 1690 return asset.Coins{coin}, []dex.Bytes{nil}, 0, nil 1691 } 1692 1693 // FundOrder locks value for use in an order. 1694 func (w *TokenWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint64, error) { 1695 if ord.MaxFeeRate < dexeth.MinGasTipCap { 1696 return nil, nil, 0, fmt.Errorf("%v: server's max fee rate is lower than our min gas tip cap. %d < %d", 1697 dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, dexeth.MinGasTipCap) 1698 } 1699 1700 if w.gasFeeLimit() < ord.MaxFeeRate { 1701 return nil, nil, 0, fmt.Errorf( 1702 "%v: server's max fee rate %v higher than configured fee rate limit %v", 1703 dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) 1704 } 1705 1706 approvalStatus, err := w.approvalStatus(ord.Version) 1707 if err != nil { 1708 return nil, nil, 0, fmt.Errorf("error getting approval status: %v", err) 1709 } 1710 switch approvalStatus { 1711 case asset.NotApproved: 1712 return nil, nil, 0, asset.ErrUnapprovedToken 1713 case asset.Pending: 1714 return nil, nil, 0, asset.ErrApprovalPending 1715 case asset.Approved: 1716 default: 1717 return nil, nil, 0, fmt.Errorf("unknown approval status %d", approvalStatus) 1718 } 1719 1720 g, err := w.initGasEstimate(int(ord.MaxSwapCount), ord.Version, 1721 ord.RedeemVersion, ord.RedeemAssetID) 1722 if err != nil { 1723 return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) 1724 } 1725 1726 ethToLock := ord.MaxFeeRate * g.Swap * ord.MaxSwapCount 1727 var success bool 1728 if err = w.lockFunds(ord.Value, initiationReserve); err != nil { 1729 return nil, nil, 0, fmt.Errorf("error locking token funds: %v", err) 1730 } 1731 defer func() { 1732 if !success { 1733 w.unlockFunds(ord.Value, initiationReserve) 1734 } 1735 }() 1736 1737 w.log.Debugf("Locking %s to swap %s in up to %d swaps at a fee rate of %d gwei/gas using up to %d gas per swap", 1738 w.parent.amtString(ethToLock), w.amtString(ord.Value), ord.MaxSwapCount, ord.MaxFeeRate, g.Swap) 1739 if err := w.parent.lockFunds(ethToLock, initiationReserve); err != nil { 1740 return nil, nil, 0, err 1741 } 1742 1743 coin := w.createTokenFundingCoin(ord.Value, ethToLock) 1744 1745 success = true 1746 return asset.Coins{coin}, []dex.Bytes{nil}, 0, nil 1747 } 1748 1749 // FundMultiOrder funds multiple orders in one shot. No special handling is 1750 // required for ETH as ETH does not over-lock during funding. 1751 func (w *ETHWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]asset.Coins, [][]dex.Bytes, uint64, error) { 1752 if w.gasFeeLimit() < ord.MaxFeeRate { 1753 return nil, nil, 0, fmt.Errorf( 1754 "%v: server's max fee rate %v higher than configured fee rate limit %v", 1755 dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) 1756 } 1757 1758 g, err := w.initGasEstimate(1, ord.Version, ord.RedeemVersion, ord.RedeemAssetID) 1759 if err != nil { 1760 return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) 1761 } 1762 1763 var totalToLock uint64 1764 allCoins := make([]asset.Coins, 0, len(ord.Values)) 1765 for _, value := range ord.Values { 1766 toLock := ord.MaxFeeRate*g.Swap*value.MaxSwapCount + value.Value 1767 allCoins = append(allCoins, asset.Coins{w.createFundingCoin(toLock)}) 1768 totalToLock += toLock 1769 } 1770 1771 if maxLock > 0 && maxLock < totalToLock { 1772 return nil, nil, 0, fmt.Errorf("insufficient funds to lock %d for %d orders", totalToLock, len(ord.Values)) 1773 } 1774 1775 if err = w.lockFunds(totalToLock, initiationReserve); err != nil { 1776 return nil, nil, 0, err 1777 } 1778 1779 redeemScripts := make([][]dex.Bytes, len(ord.Values)) 1780 for i := range redeemScripts { 1781 redeemScripts[i] = []dex.Bytes{nil} 1782 } 1783 1784 return allCoins, redeemScripts, 0, nil 1785 } 1786 1787 // FundMultiOrder funds multiple orders in one shot. No special handling is 1788 // required for ETH as ETH does not over-lock during funding. 1789 func (w *TokenWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]asset.Coins, [][]dex.Bytes, uint64, error) { 1790 if w.gasFeeLimit() < ord.MaxFeeRate { 1791 return nil, nil, 0, fmt.Errorf( 1792 "%v: server's max fee rate %v higher than configured fee rate limit %v", 1793 dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) 1794 } 1795 1796 approvalStatus, err := w.approvalStatus(ord.Version) 1797 if err != nil { 1798 return nil, nil, 0, fmt.Errorf("error getting approval status: %v", err) 1799 } 1800 switch approvalStatus { 1801 case asset.NotApproved: 1802 return nil, nil, 0, asset.ErrUnapprovedToken 1803 case asset.Pending: 1804 return nil, nil, 0, asset.ErrApprovalPending 1805 case asset.Approved: 1806 default: 1807 return nil, nil, 0, fmt.Errorf("unknown approval status %d", approvalStatus) 1808 } 1809 1810 g, err := w.initGasEstimate(1, ord.Version, 1811 ord.RedeemVersion, ord.RedeemAssetID) 1812 if err != nil { 1813 return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) 1814 } 1815 1816 var totalETHToLock, totalTokenToLock uint64 1817 allCoins := make([]asset.Coins, 0, len(ord.Values)) 1818 for _, value := range ord.Values { 1819 ethToLock := ord.MaxFeeRate * g.Swap * value.MaxSwapCount 1820 tokenToLock := value.Value 1821 allCoins = append(allCoins, asset.Coins{w.createTokenFundingCoin(tokenToLock, ethToLock)}) 1822 totalETHToLock += ethToLock 1823 totalTokenToLock += tokenToLock 1824 } 1825 1826 var success bool 1827 if err = w.lockFunds(totalTokenToLock, initiationReserve); err != nil { 1828 return nil, nil, 0, fmt.Errorf("error locking token funds: %v", err) 1829 } 1830 defer func() { 1831 if !success { 1832 w.unlockFunds(totalTokenToLock, initiationReserve) 1833 } 1834 }() 1835 1836 if err := w.parent.lockFunds(totalETHToLock, initiationReserve); err != nil { 1837 return nil, nil, 0, err 1838 } 1839 1840 redeemScripts := make([][]dex.Bytes, len(ord.Values)) 1841 for i := range redeemScripts { 1842 redeemScripts[i] = []dex.Bytes{nil} 1843 } 1844 1845 success = true 1846 return allCoins, redeemScripts, 0, nil 1847 } 1848 1849 // gasEstimates are estimates of gas required for operations involving a swap 1850 // combination of (swap asset, redeem asset, # lots). 1851 type gasEstimate struct { 1852 // The embedded are best calculated values for Swap gases, and redeem costs 1853 // IF the redeem asset is a fee-family asset. Otherwise Redeem and RedeemAdd 1854 // are zero. 1855 dexeth.Gases 1856 // Additional fields may be based on live estimates of the swap. Both oneGas 1857 // and nGas will include both swap and redeem gas, but note that the redeem 1858 // gas may be zero if the redeemed asset is not ETH or an ETH token. 1859 oneGas, nGas, nSwap, nRedeem uint64 1860 } 1861 1862 // initGasEstimate gets the best available gas estimate for n initiations. A 1863 // live estimate is checked against the server's configured values and our own 1864 // known values and errors or logs generated in certain cases. 1865 func (w *assetWallet) initGasEstimate(n int, initVer, redeemVer, redeemAssetID uint32) (est *gasEstimate, err error) { 1866 est = new(gasEstimate) 1867 1868 // Get the refund gas. 1869 if g := w.gases(initVer); g == nil { 1870 return nil, fmt.Errorf("no gas table") 1871 } else { // scoping g 1872 est.Refund = g.Refund 1873 } 1874 1875 est.Swap, est.nSwap, err = w.swapGas(n, initVer) 1876 if err != nil && !errors.Is(err, LiveEstimateFailedError) { 1877 return nil, err 1878 } 1879 // Could be LiveEstimateFailedError. Still populate static estimates if we 1880 // couldn't get live. Error is still propagated. 1881 est.oneGas = est.Swap 1882 est.nGas = est.nSwap 1883 1884 if redeemW := w.wallet(redeemAssetID); redeemW != nil { 1885 var er error 1886 est.Redeem, est.nRedeem, er = redeemW.redeemGas(n, redeemVer) 1887 if err != nil { 1888 return nil, fmt.Errorf("error calculating fee-family redeem gas: %w", er) 1889 } 1890 est.oneGas += est.Redeem 1891 est.nGas += est.nRedeem 1892 } 1893 1894 return 1895 } 1896 1897 // swapGas estimates gas for a number of initiations. swapGas will error if we 1898 // cannot get a live estimate from the contractor, which will happen if the 1899 // wallet has no balance. A live gas estimate will always be attempted, and used 1900 // if our expected gas values are lower (anomalous). 1901 func (w *assetWallet) swapGas(n int, ver uint32) (oneSwap, nSwap uint64, err error) { 1902 g := w.gases(ver) 1903 if g == nil { 1904 return 0, 0, fmt.Errorf("no gases known for %d version %d", w.assetID, ver) 1905 } 1906 oneSwap = g.Swap 1907 1908 // We have no way of updating the value of SwapAdd without a version change, 1909 // but we're not gonna worry about accuracy for nSwap, since it's only used 1910 // for estimates and never for dex-validated values like order funding. 1911 nSwap = oneSwap + uint64(n-1)*g.SwapAdd 1912 1913 // The amount we can estimate and ultimately the amount we can use in a 1914 // single transaction is limited by the block gas limit or the tx gas 1915 // limit. Core will use the largest gas among all versions when 1916 // determining the maximum number of swaps that can be in one 1917 // transaction. Limit our gas estimate to the same number of swaps. 1918 nMax := n 1919 maxSwaps, _ := w.maxSwapsAndRedeems() 1920 var nRemain, nFull int 1921 if uint64(n) > maxSwaps { 1922 nMax = int(maxSwaps) 1923 nFull = n / nMax 1924 nSwap = (oneSwap + uint64(nMax-1)*g.SwapAdd) * uint64(nFull) 1925 nRemain = n % nMax 1926 if nRemain != 0 { 1927 nSwap += oneSwap + uint64(nRemain-1)*g.SwapAdd 1928 } 1929 } 1930 1931 // If a live estimate is greater than our estimate from configured values, 1932 // use the live estimate with a warning. 1933 gasEst, err := w.estimateInitGas(w.ctx, nMax, ver) 1934 if err != nil { 1935 err = errors.Join(err, LiveEstimateFailedError) 1936 return 1937 // Or we could go with what we know? But this estimate error could be a 1938 // hint that the transaction would fail, and we don't have a way to 1939 // recover from that. Play it safe and allow caller to retry assuming 1940 // the error is transient with the provider. 1941 // w.log.Errorf("(%d) error estimating swap gas (using expected gas cap instead): %v", w.assetID, err) 1942 // return oneSwap, nSwap, true, nil 1943 } 1944 if nMax != n { 1945 // If we needed to adjust the max earlier, and the estimate did 1946 // not error, multiply the estimate by the number of full 1947 // transactions and add the estimate of the remainder. 1948 gasEst *= uint64(nFull) 1949 if nRemain > 0 { 1950 remainEst, err := w.estimateInitGas(w.ctx, nRemain, ver) 1951 if err != nil { 1952 w.log.Errorf("(%d) error estimating swap gas for remainder: %v", w.assetID, err) 1953 return 0, 0, err 1954 } 1955 gasEst += remainEst 1956 } 1957 } 1958 if gasEst > nSwap { 1959 w.log.Warnf("Swap gas estimate %d is greater than the server's configured value %d. Using live estimate + 10%%.", gasEst, nSwap) 1960 nSwap = gasEst * 11 / 10 // 10% buffer 1961 if n == 1 && nSwap > oneSwap { 1962 oneSwap = nSwap 1963 } 1964 } 1965 return 1966 } 1967 1968 // redeemGas gets an accurate estimate for redemption gas. We allow a DEX server 1969 // some latitude in adjusting the redemption gas, up to 2x our local estimate. 1970 func (w *assetWallet) redeemGas(n int, ver uint32) (oneGas, nGas uint64, err error) { 1971 g := w.gases(ver) 1972 if g == nil { 1973 return 0, 0, fmt.Errorf("no gas table for redemption asset %d", w.assetID) 1974 } 1975 redeemGas := g.Redeem 1976 // Not concerned with the accuracy of nGas. It's never used outside of 1977 // best case estimates. 1978 return redeemGas, redeemGas + (uint64(n)-1)*g.RedeemAdd, nil 1979 } 1980 1981 // approvalGas gets the best available estimate for an approval tx, which is 1982 // the greater of the asset's registered value and a live estimate. It is an 1983 // error if a live estimate cannot be retrieved, which will be the case if the 1984 // user's eth balance is insufficient to cover tx fees for the approval. 1985 func (w *assetWallet) approvalGas(newGas *big.Int, ver uint32) (uint64, error) { 1986 ourGas := w.gases(ver) 1987 if ourGas == nil { 1988 return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, ver) 1989 } 1990 1991 approveGas := ourGas.Approve 1992 1993 if approveEst, err := w.estimateApproveGas(newGas); err != nil { 1994 return 0, fmt.Errorf("error estimating approve gas: %v", err) 1995 } else if approveEst > approveGas { 1996 w.log.Warnf("Approve gas estimate %d is greater than the expected value %d. Using live estimate + 10%%.", approveEst, approveGas) 1997 return approveEst * 11 / 10, nil 1998 } 1999 return approveGas, nil 2000 } 2001 2002 // ReturnCoins unlocks coins. This would be necessary in the case of a 2003 // canceled order. 2004 func (w *ETHWallet) ReturnCoins(coins asset.Coins) error { 2005 var amt uint64 2006 for _, ci := range coins { 2007 c, is := ci.(*fundingCoin) 2008 if !is { 2009 return fmt.Errorf("unknown coin type %T", c) 2010 } 2011 if c.addr != w.addr { 2012 return fmt.Errorf("coin is not funded by this wallet. coin address %s != our address %s", c.addr, w.addr) 2013 } 2014 amt += c.amt 2015 2016 } 2017 w.unlockFunds(amt, initiationReserve) 2018 return nil 2019 } 2020 2021 // ReturnCoins unlocks coins. This would be necessary in the case of a 2022 // canceled order. 2023 func (w *TokenWallet) ReturnCoins(coins asset.Coins) error { 2024 var amt, fees uint64 2025 for _, ci := range coins { 2026 c, is := ci.(*tokenFundingCoin) 2027 if !is { 2028 return fmt.Errorf("unknown coin type %T", c) 2029 } 2030 if c.addr != w.addr { 2031 return fmt.Errorf("coin is not funded by this wallet. coin address %s != our address %s", c.addr, w.addr) 2032 } 2033 amt += c.amt 2034 fees += c.fees 2035 } 2036 if fees > 0 { 2037 w.parent.unlockFunds(fees, initiationReserve) 2038 } 2039 w.unlockFunds(amt, initiationReserve) 2040 return nil 2041 } 2042 2043 // FundingCoins gets funding coins for the coin IDs. The coins are locked. This 2044 // method might be called to reinitialize an order from data stored externally. 2045 func (w *ETHWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) { 2046 coins := make([]asset.Coin, 0, len(ids)) 2047 var amt uint64 2048 for _, id := range ids { 2049 c, err := decodeFundingCoin(id) 2050 if err != nil { 2051 return nil, fmt.Errorf("error decoding funding coin ID: %w", err) 2052 } 2053 if c.addr != w.addr { 2054 return nil, fmt.Errorf("funding coin has wrong address. %s != %s", c.addr, w.addr) 2055 } 2056 amt += c.amt 2057 coins = append(coins, c) 2058 } 2059 if err := w.lockFunds(amt, initiationReserve); err != nil { 2060 return nil, err 2061 } 2062 2063 return coins, nil 2064 } 2065 2066 // FundingCoins gets funding coins for the coin IDs. The coins are locked. This 2067 // method might be called to reinitialize an order from data stored externally. 2068 func (w *TokenWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) { 2069 coins := make([]asset.Coin, 0, len(ids)) 2070 var amt, fees uint64 2071 for _, id := range ids { 2072 c, err := decodeTokenFundingCoin(id) 2073 if err != nil { 2074 return nil, fmt.Errorf("error decoding funding coin ID: %w", err) 2075 } 2076 if c.addr != w.addr { 2077 return nil, fmt.Errorf("funding coin has wrong address. %s != %s", c.addr, w.addr) 2078 } 2079 2080 amt += c.amt 2081 fees += c.fees 2082 coins = append(coins, c) 2083 } 2084 2085 var success bool 2086 if fees > 0 { 2087 if err := w.parent.lockFunds(fees, initiationReserve); err != nil { 2088 return nil, fmt.Errorf("error unlocking parent asset fees: %w", err) 2089 } 2090 defer func() { 2091 if !success { 2092 w.parent.unlockFunds(fees, initiationReserve) 2093 } 2094 }() 2095 } 2096 if err := w.lockFunds(amt, initiationReserve); err != nil { 2097 return nil, err 2098 } 2099 2100 success = true 2101 return coins, nil 2102 } 2103 2104 // swapReceipt implements the asset.Receipt interface for ETH. 2105 type swapReceipt struct { 2106 txHash common.Hash 2107 secretHash [dexeth.SecretHashSize]byte 2108 // expiration and value can be determined with a blockchain 2109 // lookup, but we cache these values to avoid this. 2110 expiration time.Time 2111 value uint64 2112 ver uint32 2113 contractAddr string // specified by ver, here for naive consumers 2114 } 2115 2116 // Expiration returns the time after which the contract can be 2117 // refunded. 2118 func (r *swapReceipt) Expiration() time.Time { 2119 return r.expiration 2120 } 2121 2122 // Coin returns the coin used to fund the swap. 2123 func (r *swapReceipt) Coin() asset.Coin { 2124 return &coin{ 2125 value: r.value, 2126 id: r.txHash, // server's idea of ETH coin ID encoding 2127 } 2128 } 2129 2130 // Contract returns the swap's identifying data, which the concatenation of the 2131 // contract version and the secret hash. 2132 func (r *swapReceipt) Contract() dex.Bytes { 2133 return dexeth.EncodeContractData(r.ver, r.secretHash) 2134 } 2135 2136 // String returns a string representation of the swapReceipt. The secret hash 2137 // and contract address should be included in this to facilitate a manual refund 2138 // since the secret hash identifies the swap in the contract (for v0). Although 2139 // the user can pick this information from the transaction's "to" address and 2140 // the calldata, this simplifies the process. 2141 func (r *swapReceipt) String() string { 2142 return fmt.Sprintf("{ tx hash: %s, contract address: %s, secret hash: %x }", 2143 r.txHash, r.contractAddr, r.secretHash) 2144 } 2145 2146 // SignedRefund returns an empty byte array. ETH does not support a pre-signed 2147 // redeem script because the nonce needed in the transaction cannot be previously 2148 // determined. 2149 func (*swapReceipt) SignedRefund() dex.Bytes { 2150 return dex.Bytes{} 2151 } 2152 2153 var _ asset.Receipt = (*swapReceipt)(nil) 2154 2155 // Swap sends the swaps in a single transaction. The fees used returned are the 2156 // max fees that will possibly be used, since in ethereum with EIP-1559 we cannot 2157 // know exactly how much fees will be used. 2158 func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) { 2159 if swaps.FeeRate == 0 { 2160 return nil, nil, 0, fmt.Errorf("cannot send swap with with zero fee rate") 2161 } 2162 2163 fail := func(s string, a ...any) ([]asset.Receipt, asset.Coin, uint64, error) { 2164 return nil, nil, 0, fmt.Errorf(s, a...) 2165 } 2166 2167 var reservedVal uint64 2168 for _, input := range swaps.Inputs { // Should only ever be 1 input, I think. 2169 c, is := input.(*fundingCoin) 2170 if !is { 2171 return fail("wrong coin type: %T", input) 2172 } 2173 reservedVal += c.amt 2174 } 2175 2176 var swapVal uint64 2177 for _, contract := range swaps.Contracts { 2178 swapVal += contract.Value 2179 } 2180 2181 // Set the gas limit as high as reserves will allow. 2182 n := len(swaps.Contracts) 2183 oneSwap, nSwap, err := w.swapGas(n, swaps.Version) 2184 if err != nil { 2185 return fail("error getting gas fees: %v", err) 2186 } 2187 gasLimit := oneSwap * uint64(n) // naive unbatched, higher but not realistic 2188 fees := gasLimit * swaps.FeeRate 2189 if swapVal+fees > reservedVal { 2190 if n == 1 { 2191 return fail("unfunded swap: %d < %d", reservedVal, swapVal+fees) 2192 } 2193 w.log.Warnf("Unexpectedly low reserves for %d swaps: %d < %d", n, reservedVal, swapVal+fees) 2194 // Since this is a batch swap, attempt to use the realistic limits. 2195 gasLimit = nSwap 2196 fees = gasLimit * swaps.FeeRate 2197 if swapVal+fees > reservedVal { 2198 // If the live gas estimate is giving us an unrealistically high 2199 // value, we're in trouble, so we might consider a third fallback 2200 // that only uses our known gases: 2201 // g := w.gases(swaps.Version) 2202 // nSwap = g.Swap + uint64(n-1)*g.SwapAdd 2203 // But we've not swapped yet and we don't want a failed transaction, 2204 // so we will do nothing. 2205 return fail("unfunded swap: %d < %d", reservedVal, swapVal+fees) 2206 } 2207 } 2208 2209 maxFeeRate := dexeth.GweiToWei(swaps.FeeRate) 2210 _, tipRate, err := w.currentNetworkFees(w.ctx) 2211 if err != nil { 2212 return fail("Swap: failed to get network tip cap: %w", err) 2213 } 2214 2215 tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, gasLimit, maxFeeRate, tipRate, swaps.Version) 2216 if err != nil { 2217 return fail("Swap: initiate error: %w", err) 2218 } 2219 2220 txHash := tx.Hash() 2221 receipts := make([]asset.Receipt, 0, n) 2222 for _, swap := range swaps.Contracts { 2223 var secretHash [dexeth.SecretHashSize]byte 2224 copy(secretHash[:], swap.SecretHash) 2225 receipts = append(receipts, &swapReceipt{ 2226 expiration: time.Unix(int64(swap.LockTime), 0), 2227 value: swap.Value, 2228 txHash: txHash, 2229 secretHash: secretHash, 2230 ver: swaps.Version, 2231 contractAddr: w.versionedContracts[swaps.Version].String(), 2232 }) 2233 } 2234 2235 var change asset.Coin 2236 if swaps.LockChange { 2237 w.unlockFunds(swapVal+fees, initiationReserve) 2238 change = w.createFundingCoin(reservedVal - swapVal - fees) 2239 } else { 2240 w.unlockFunds(reservedVal, initiationReserve) 2241 } 2242 2243 return receipts, change, fees, nil 2244 } 2245 2246 // Swap sends the swaps in a single transaction. The fees used returned are the 2247 // max fees that will possibly be used, since in ethereum with EIP-1559 we cannot 2248 // know exactly how much fees will be used. 2249 func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) { 2250 if swaps.FeeRate == 0 { 2251 return nil, nil, 0, fmt.Errorf("cannot send swap with with zero fee rate") 2252 } 2253 2254 fail := func(s string, a ...any) ([]asset.Receipt, asset.Coin, uint64, error) { 2255 return nil, nil, 0, fmt.Errorf(s, a...) 2256 } 2257 2258 var reservedVal, reservedParent uint64 2259 for _, input := range swaps.Inputs { // Should only ever be 1 input, I think. 2260 c, is := input.(*tokenFundingCoin) 2261 if !is { 2262 return fail("wrong coin type: %T", input) 2263 } 2264 reservedVal += c.amt 2265 reservedParent += c.fees 2266 } 2267 2268 var swapVal uint64 2269 for _, contract := range swaps.Contracts { 2270 swapVal += contract.Value 2271 } 2272 2273 if swapVal > reservedVal { 2274 return fail("unfunded token swap: %d < %d", reservedVal, swapVal) 2275 } 2276 2277 n := len(swaps.Contracts) 2278 oneSwap, nSwap, err := w.swapGas(n, swaps.Version) 2279 if err != nil { 2280 return fail("error getting gas fees: %v", err) 2281 } 2282 2283 gasLimit := oneSwap * uint64(n) 2284 fees := gasLimit * swaps.FeeRate 2285 if fees > reservedParent { 2286 if n == 1 { 2287 return fail("unfunded token swap fees: %d < %d", reservedParent, fees) 2288 } 2289 // Since this is a batch swap, attempt to use the realistic limits. 2290 w.log.Warnf("Unexpectedly low reserves for %d swaps: %d < %d", n, reservedVal, swapVal+fees) 2291 gasLimit = nSwap 2292 fees = gasLimit * swaps.FeeRate 2293 if fees > reservedParent { 2294 return fail("unfunded token swap fees: %d < %d", reservedParent, fees) 2295 } // See (*ETHWallet).Swap comments for a third option. 2296 } 2297 2298 maxFeeRate := dexeth.GweiToWei(swaps.FeeRate) 2299 _, tipRate, err := w.currentNetworkFees(w.ctx) 2300 if err != nil { 2301 return fail("Swap: failed to get network tip cap: %w", err) 2302 } 2303 2304 tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, gasLimit, maxFeeRate, tipRate, swaps.Version) 2305 if err != nil { 2306 return fail("Swap: initiate error: %w", err) 2307 } 2308 2309 if w.netToken.SwapContracts[swaps.Version] == nil { 2310 return fail("unable to find contract address for asset %d contract version %d", w.assetID, swaps.Version) 2311 } 2312 2313 contractAddr := w.netToken.SwapContracts[swaps.Version].Address.String() 2314 2315 txHash := tx.Hash() 2316 receipts := make([]asset.Receipt, 0, n) 2317 for _, swap := range swaps.Contracts { 2318 var secretHash [dexeth.SecretHashSize]byte 2319 copy(secretHash[:], swap.SecretHash) 2320 receipts = append(receipts, &swapReceipt{ 2321 expiration: time.Unix(int64(swap.LockTime), 0), 2322 value: swap.Value, 2323 txHash: txHash, 2324 secretHash: secretHash, 2325 ver: swaps.Version, 2326 contractAddr: contractAddr, 2327 }) 2328 } 2329 2330 var change asset.Coin 2331 if swaps.LockChange { 2332 w.unlockFunds(swapVal, initiationReserve) 2333 w.parent.unlockFunds(fees, initiationReserve) 2334 change = w.createTokenFundingCoin(reservedVal-swapVal, reservedParent-fees) 2335 } else { 2336 w.unlockFunds(reservedVal, initiationReserve) 2337 w.parent.unlockFunds(reservedParent, initiationReserve) 2338 } 2339 2340 return receipts, change, fees, nil 2341 } 2342 2343 // Redeem sends the redemption transaction, which may contain more than one 2344 // redemption. All redemptions must be for the same contract version because the 2345 // current API requires a single transaction reported (asset.Coin output), but 2346 // conceptually a batch of redeems could be processed for any number of 2347 // different contract addresses with multiple transactions. (buck: what would 2348 // the difference from calling Redeem repeatedly?) 2349 func (w *ETHWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) { 2350 return w.assetWallet.Redeem(form, nil, nil) 2351 } 2352 2353 // Redeem sends the redemption transaction, which may contain more than one 2354 // redemption. 2355 func (w *TokenWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) { 2356 return w.assetWallet.Redeem(form, w.parent, nil) 2357 } 2358 2359 // Redeem sends the redemption transaction, which may contain more than one 2360 // redemption. The nonceOverride parameter is used to specify a specific nonce 2361 // to be used for the redemption transaction. It is needed when resubmitting a 2362 // redemption with a fee too low to be mined. 2363 func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, nonceOverride *uint64) ([]dex.Bytes, asset.Coin, uint64, error) { 2364 fail := func(err error) ([]dex.Bytes, asset.Coin, uint64, error) { 2365 return nil, nil, 0, err 2366 } 2367 2368 n := uint64(len(form.Redemptions)) 2369 2370 if n == 0 { 2371 return fail(errors.New("Redeem: must be called with at least 1 redemption")) 2372 } 2373 2374 var contractVer uint32 // require a consistent version since this is a single transaction 2375 secrets := make([][32]byte, 0, n) 2376 var redeemedValue uint64 2377 for i, redemption := range form.Redemptions { 2378 // NOTE: redemption.Spends.SecretHash is a dup of the hash extracted 2379 // from redemption.Spends.Contract. Even for scriptable UTXO assets, the 2380 // redeem script in this Contract field is redundant with the SecretHash 2381 // field as ExtractSwapDetails can be applied to extract the hash. 2382 ver, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) 2383 if err != nil { 2384 return fail(fmt.Errorf("Redeem: invalid versioned swap contract data: %w", err)) 2385 } 2386 if i == 0 { 2387 contractVer = ver 2388 } else if contractVer != ver { 2389 return fail(fmt.Errorf("Redeem: inconsistent contract versions in RedeemForm.Redemptions: "+ 2390 "%d != %d", contractVer, ver)) 2391 } 2392 2393 // Use the contract's free public view function to validate the secret 2394 // against the secret hash, and ensure the swap is otherwise redeemable 2395 // before broadcasting our secrets, which is especially important if we 2396 // are maker (the swap initiator). 2397 var secret [32]byte 2398 copy(secret[:], redemption.Secret) 2399 secrets = append(secrets, secret) 2400 redeemable, err := w.isRedeemable(secretHash, secret, ver) 2401 if err != nil { 2402 return fail(fmt.Errorf("Redeem: failed to check if swap is redeemable: %w", err)) 2403 } 2404 if !redeemable { 2405 return fail(fmt.Errorf("Redeem: secretHash %x not redeemable with secret %x", 2406 secretHash, secret)) 2407 } 2408 2409 swapData, err := w.swap(w.ctx, secretHash, ver) 2410 if err != nil { 2411 return nil, nil, 0, fmt.Errorf("error finding swap state: %w", err) 2412 } 2413 if swapData.State != dexeth.SSInitiated { 2414 return nil, nil, 0, asset.ErrSwapNotInitiated 2415 } 2416 redeemedValue += w.atomize(swapData.Value) 2417 } 2418 2419 g := w.gases(contractVer) 2420 if g == nil { 2421 return fail(fmt.Errorf("no gas table")) 2422 } 2423 2424 if feeWallet == nil { 2425 feeWallet = w 2426 } 2427 bal, err := feeWallet.Balance() 2428 if err != nil { 2429 return nil, nil, 0, fmt.Errorf("error getting balance in excessive gas fee recovery: %v", err) 2430 } 2431 2432 gasLimit, gasFeeCap := g.Redeem*n, form.FeeSuggestion 2433 originalFundsReserved := gasLimit * gasFeeCap 2434 2435 /* We could get a gas estimate via RPC, but this will reveal the secret key 2436 before submitting the redeem transaction. This is not OK for maker. 2437 Disable for now. 2438 2439 if gasEst, err := w.estimateRedeemGas(w.ctx, secrets, contractVer); err != nil { 2440 return fail(fmt.Errorf("error getting redemption estimate: %w", err)) 2441 } else if gasEst > gasLimit { 2442 // This is sticky. We only reserved so much for redemption, so accepting 2443 // a gas limit higher than anticipated could potentially mess us up. On 2444 // the other hand, we don't want to simply reject the redemption. 2445 // Let's see if it looks like we can cover the fee. If so go ahead, up 2446 // to a limit. 2447 candidateLimit := gasEst * 11 / 10 // Add 10% for good measure. 2448 // Cannot be more than double. 2449 if candidateLimit > gasLimit*2 { 2450 return fail(fmt.Errorf("cannot recover from excessive gas estimate %d > 2 * %d", candidateLimit, gasLimit)) 2451 } 2452 additionalFundsNeeded := (candidateLimit - gasLimit) * form.FeeSuggestion 2453 if bal.Available < additionalFundsNeeded { 2454 return fail(fmt.Errorf("no balance available for gas overshoot recovery. %d < %d", bal.Available, additionalFundsNeeded)) 2455 } 2456 w.log.Warnf("live gas estimate %d exceeded expected max value %d. using higher limit %d for redemption", gasEst, gasLimit, candidateLimit) 2457 gasLimit = candidateLimit 2458 } 2459 */ 2460 2461 // If the base fee is higher than the FeeSuggestion we attempt to increase 2462 // the gasFeeCap to 2*baseFee. If we don't have enough funds, we use the 2463 // funds we have available. 2464 baseFee, tipRate, err := w.currentNetworkFees(w.ctx) 2465 if err != nil { 2466 return fail(fmt.Errorf("Error getting net fee state: %w", err)) 2467 } 2468 baseFeeGwei := dexeth.WeiToGweiCeil(baseFee) 2469 if baseFeeGwei > form.FeeSuggestion { 2470 additionalFundsNeeded := (2 * baseFeeGwei * gasLimit) - originalFundsReserved 2471 if bal.Available > additionalFundsNeeded { 2472 gasFeeCap = 2 * baseFeeGwei 2473 } else { 2474 gasFeeCap = (bal.Available + originalFundsReserved) / gasLimit 2475 } 2476 w.log.Warnf("base fee %d > server max fee rate %d. using %d as gas fee cap for redemption", baseFeeGwei, form.FeeSuggestion, gasFeeCap) 2477 } 2478 2479 tx, err := w.redeem(w.ctx, form.Redemptions, gasFeeCap, tipRate, gasLimit, contractVer) 2480 if err != nil { 2481 return fail(fmt.Errorf("Redeem: redeem error: %w", err)) 2482 } 2483 2484 txHash := tx.Hash() 2485 2486 txs := make([]dex.Bytes, len(form.Redemptions)) 2487 for i := range txs { 2488 txs[i] = txHash[:] 2489 } 2490 2491 outputCoin := &coin{ 2492 id: txHash, 2493 value: redeemedValue, 2494 } 2495 2496 // This is still a fee estimate. If we add a redemption confirmation method 2497 // as has been discussed, then maybe the fees can be updated there. 2498 fees := g.RedeemN(len(form.Redemptions)) * form.FeeSuggestion 2499 2500 return txs, outputCoin, fees, nil 2501 } 2502 2503 // recoverPubkey recovers the uncompressed public key from the signature and the 2504 // message hash that was signed. The signature should be a compact signature 2505 // generated by a geth wallet, with the format [R || S || V], with the recover 2506 // bit V at the end. See go-ethereum/crypto.Sign. 2507 func recoverPubkey(msgHash, sig []byte) ([]byte, error) { 2508 // Using Decred's ecdsa.RecoverCompact requires moving the recovery byte to 2509 // the beginning of the serialized compact signature and adding back in the 2510 // compactSigMagicOffset scalar. 2511 sigBTC := make([]byte, 65) 2512 sigBTC[0] = sig[64] + 27 // compactSigMagicOffset 2513 copy(sigBTC[1:], sig) 2514 pubKey, _, err := ecdsa.RecoverCompact(sigBTC, msgHash) 2515 if err != nil { 2516 return nil, err 2517 } 2518 return pubKey.SerializeUncompressed(), nil 2519 } 2520 2521 // tokenBalance checks the token balance of the account handled by the wallet. 2522 func (w *assetWallet) tokenBalance() (bal *big.Int, err error) { 2523 // We don't care about the version. 2524 return bal, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { 2525 bal, err = c.balance(w.ctx) 2526 return err 2527 }) 2528 } 2529 2530 // tokenAllowance checks the amount of tokens that the swap contract is approved 2531 // to spend on behalf of the account handled by the wallet. 2532 func (w *assetWallet) tokenAllowance(version uint32) (allowance *big.Int, err error) { 2533 return allowance, w.withTokenContractor(w.assetID, version, func(c tokenContractor) error { 2534 allowance, err = c.allowance(w.ctx) 2535 return err 2536 }) 2537 } 2538 2539 // approveToken approves the token swap contract to spend tokens on behalf of 2540 // account handled by the wallet. 2541 func (w *assetWallet) approveToken(ctx context.Context, amount *big.Int, gasLimit uint64, maxFeeRate, tipRate *big.Int, contractVer uint32) (tx *types.Transaction, err error) { 2542 return tx, w.withNonce(ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) { 2543 txOpts, err := w.node.txOpts(w.ctx, 0, gasLimit, maxFeeRate, tipRate, nonce) 2544 if err != nil { 2545 return nil, 0, 0, nil, fmt.Errorf("addSignerToOpts error: %w", err) 2546 } 2547 2548 return tx, asset.ApproveToken, w.atomize(amount), nil, w.withTokenContractor(w.assetID, contractVer, func(c tokenContractor) error { 2549 tx, err = c.approve(txOpts, amount) 2550 if err != nil { 2551 return err 2552 } 2553 w.log.Infof("Approval sent for %s at token address %s, nonce = %s, txID = %s", 2554 dex.BipIDSymbol(w.assetID), c.tokenAddress(), txOpts.Nonce, tx.Hash().Hex()) 2555 return nil 2556 }) 2557 }) 2558 } 2559 2560 func (w *assetWallet) approvalStatus(version uint32) (asset.ApprovalStatus, error) { 2561 if w.assetID == w.baseChainID { 2562 return asset.Approved, nil 2563 } 2564 2565 // If the result has been cached, return what is in the cache. 2566 // The cache is cleared if an approval/unapproval tx is done. 2567 w.approvalsMtx.RLock() 2568 if approved, cached := w.approvalCache[version]; cached { 2569 w.approvalsMtx.RUnlock() 2570 if approved { 2571 return asset.Approved, nil 2572 } else { 2573 return asset.NotApproved, nil 2574 } 2575 } 2576 2577 if _, pending := w.pendingApprovals[version]; pending { 2578 w.approvalsMtx.RUnlock() 2579 return asset.Pending, nil 2580 } 2581 w.approvalsMtx.RUnlock() 2582 2583 w.approvalsMtx.Lock() 2584 defer w.approvalsMtx.Unlock() 2585 2586 currentAllowance, err := w.tokenAllowance(version) 2587 if err != nil { 2588 return asset.NotApproved, fmt.Errorf("error retrieving current allowance: %w", err) 2589 } 2590 if currentAllowance.Cmp(unlimitedAllowanceReplenishThreshold) >= 0 { 2591 w.approvalCache[version] = true 2592 return asset.Approved, nil 2593 } 2594 w.approvalCache[version] = false 2595 return asset.NotApproved, nil 2596 } 2597 2598 // ApproveToken sends an approval transaction for a specific version of 2599 // the token's swap contract. An error is returned if an approval has 2600 // already been done or is pending. The onConfirm callback is called 2601 // when the approval transaction is confirmed. 2602 func (w *TokenWallet) ApproveToken(assetVer uint32, onConfirm func()) (string, error) { 2603 approvalStatus, err := w.approvalStatus(assetVer) 2604 if err != nil { 2605 return "", fmt.Errorf("error checking approval status: %w", err) 2606 } 2607 if approvalStatus == asset.Approved { 2608 return "", fmt.Errorf("token is already approved") 2609 } 2610 if approvalStatus == asset.Pending { 2611 return "", asset.ErrApprovalPending 2612 } 2613 2614 maxFeeRate, tipRate, err := w.recommendedMaxFeeRate(w.ctx) 2615 if err != nil { 2616 return "", fmt.Errorf("error calculating approval fee rate: %w", err) 2617 } 2618 feeRateGwei := dexeth.WeiToGweiCeil(maxFeeRate) 2619 approvalGas, err := w.approvalGas(unlimitedAllowance, assetVer) 2620 if err != nil { 2621 return "", fmt.Errorf("error calculating approval gas: %w", err) 2622 } 2623 2624 ethBal, err := w.parent.balance() 2625 if err != nil { 2626 return "", fmt.Errorf("error getting eth balance: %w", err) 2627 } 2628 if ethBal.Available < approvalGas*feeRateGwei { 2629 return "", fmt.Errorf("insufficient fee balance for approval. required: %d, available: %d", 2630 approvalGas*feeRateGwei, ethBal.Available) 2631 } 2632 2633 tx, err := w.approveToken(w.ctx, unlimitedAllowance, approvalGas, maxFeeRate, tipRate, assetVer) 2634 if err != nil { 2635 return "", fmt.Errorf("error approving token: %w", err) 2636 } 2637 2638 w.approvalsMtx.Lock() 2639 defer w.approvalsMtx.Unlock() 2640 2641 delete(w.approvalCache, assetVer) 2642 w.pendingApprovals[assetVer] = &pendingApproval{ 2643 txHash: tx.Hash(), 2644 onConfirm: onConfirm, 2645 } 2646 2647 return tx.Hash().Hex(), nil 2648 } 2649 2650 // UnapproveToken removes the approval for a specific version of the token's 2651 // swap contract. 2652 func (w *TokenWallet) UnapproveToken(assetVer uint32, onConfirm func()) (string, error) { 2653 approvalStatus, err := w.approvalStatus(assetVer) 2654 if err != nil { 2655 return "", fmt.Errorf("error checking approval status: %w", err) 2656 } 2657 if approvalStatus == asset.NotApproved { 2658 return "", fmt.Errorf("token is not approved") 2659 } 2660 if approvalStatus == asset.Pending { 2661 return "", asset.ErrApprovalPending 2662 } 2663 2664 maxFeeRate, tipRate, err := w.recommendedMaxFeeRate(w.ctx) 2665 if err != nil { 2666 return "", fmt.Errorf("error calculating approval fee rate: %w", err) 2667 } 2668 feeRateGwei := dexeth.WeiToGweiCeil(maxFeeRate) 2669 approvalGas, err := w.approvalGas(big.NewInt(0), assetVer) 2670 if err != nil { 2671 return "", fmt.Errorf("error calculating approval gas: %w", err) 2672 } 2673 2674 ethBal, err := w.parent.balance() 2675 if err != nil { 2676 return "", fmt.Errorf("error getting eth balance: %w", err) 2677 } 2678 if ethBal.Available < approvalGas*feeRateGwei { 2679 return "", fmt.Errorf("insufficient eth balance for unapproval. required: %d, available: %d", 2680 approvalGas*feeRateGwei, ethBal.Available) 2681 } 2682 2683 tx, err := w.approveToken(w.ctx, big.NewInt(0), approvalGas, maxFeeRate, tipRate, assetVer) 2684 if err != nil { 2685 return "", fmt.Errorf("error unapproving token: %w", err) 2686 } 2687 2688 w.approvalsMtx.Lock() 2689 defer w.approvalsMtx.Unlock() 2690 2691 delete(w.approvalCache, assetVer) 2692 w.pendingApprovals[assetVer] = &pendingApproval{ 2693 txHash: tx.Hash(), 2694 onConfirm: onConfirm, 2695 } 2696 2697 return tx.Hash().Hex(), nil 2698 } 2699 2700 // ApprovalFee returns the estimated fee for an approval transaction. 2701 func (w *TokenWallet) ApprovalFee(assetVer uint32, approve bool) (uint64, error) { 2702 var allowance *big.Int 2703 if approve { 2704 allowance = unlimitedAllowance 2705 } else { 2706 allowance = big.NewInt(0) 2707 } 2708 approvalGas, err := w.approvalGas(allowance, assetVer) 2709 if err != nil { 2710 return 0, fmt.Errorf("error calculating approval gas: %w", err) 2711 } 2712 2713 feeRateGwei, err := w.recommendedMaxFeeRateGwei(w.ctx) 2714 if err != nil { 2715 return 0, fmt.Errorf("error calculating approval fee rate: %w", err) 2716 } 2717 2718 return approvalGas * feeRateGwei, nil 2719 } 2720 2721 // ApprovalStatus returns the approval status for each version of the 2722 // token's swap contract. 2723 func (w *TokenWallet) ApprovalStatus() map[uint32]asset.ApprovalStatus { 2724 versions := w.Info().SupportedVersions 2725 2726 statuses := map[uint32]asset.ApprovalStatus{} 2727 for _, version := range versions { 2728 status, err := w.approvalStatus(version) 2729 if err != nil { 2730 w.log.Errorf("error checking approval status for version %d: %w", version, err) 2731 continue 2732 } 2733 statuses[version] = status 2734 } 2735 2736 return statuses 2737 } 2738 2739 // ReserveNRedemptions locks funds for redemption. It is an error if there 2740 // is insufficient spendable balance. Part of the AccountLocker interface. 2741 func (w *ETHWallet) ReserveNRedemptions(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) { 2742 g := w.gases(ver) 2743 if g == nil { 2744 return 0, fmt.Errorf("no gas table") 2745 } 2746 redeemCost := g.Redeem * maxFeeRate 2747 reserve := redeemCost * n 2748 2749 if err := w.lockFunds(reserve, redemptionReserve); err != nil { 2750 return 0, err 2751 } 2752 2753 return reserve, nil 2754 } 2755 2756 // ReserveNRedemptions locks funds for redemption. It is an error if there 2757 // is insufficient spendable balance. 2758 // Part of the AccountLocker interface. 2759 func (w *TokenWallet) ReserveNRedemptions(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) { 2760 g := w.gases(ver) 2761 if g == nil { 2762 return 0, fmt.Errorf("no gas table") 2763 } 2764 reserve := g.Redeem * maxFeeRate * n 2765 2766 if err := w.parent.lockFunds(reserve, redemptionReserve); err != nil { 2767 return 0, err 2768 } 2769 2770 return reserve, nil 2771 } 2772 2773 // UnlockRedemptionReserves unlocks the specified amount from redemption 2774 // reserves. Part of the AccountLocker interface. 2775 func (w *ETHWallet) UnlockRedemptionReserves(reserves uint64) { 2776 unlockRedemptionReserves(w.assetWallet, reserves) 2777 } 2778 2779 // UnlockRedemptionReserves unlocks the specified amount from redemption 2780 // reserves. Part of the AccountLocker interface. 2781 func (w *TokenWallet) UnlockRedemptionReserves(reserves uint64) { 2782 unlockRedemptionReserves(w.parent, reserves) 2783 } 2784 2785 func unlockRedemptionReserves(w *assetWallet, reserves uint64) { 2786 w.unlockFunds(reserves, redemptionReserve) 2787 } 2788 2789 // ReReserveRedemption checks out an amount for redemptions. Use 2790 // ReReserveRedemption after initializing a new asset.Wallet. 2791 // Part of the AccountLocker interface. 2792 func (w *ETHWallet) ReReserveRedemption(req uint64) error { 2793 return w.lockFunds(req, redemptionReserve) 2794 } 2795 2796 // ReReserveRedemption checks out an amount for redemptions. Use 2797 // ReReserveRedemption after initializing a new asset.Wallet. 2798 // Part of the AccountLocker interface. 2799 func (w *TokenWallet) ReReserveRedemption(req uint64) error { 2800 return w.parent.lockFunds(req, redemptionReserve) 2801 } 2802 2803 // ReserveNRefunds locks funds for doing refunds. It is an error if there 2804 // is insufficient spendable balance. Part of the AccountLocker interface. 2805 func (w *ETHWallet) ReserveNRefunds(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) { 2806 g := w.gases(ver) 2807 if g == nil { 2808 return 0, errors.New("no gas table") 2809 } 2810 return reserveNRefunds(w.assetWallet, n, maxFeeRate, g) 2811 } 2812 2813 // ReserveNRefunds locks funds for doing refunds. It is an error if there 2814 // is insufficient spendable balance. Part of the AccountLocker interface. 2815 func (w *TokenWallet) ReserveNRefunds(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) { 2816 g := w.gases(ver) 2817 if g == nil { 2818 return 0, errors.New("no gas table") 2819 } 2820 return reserveNRefunds(w.parent, n, maxFeeRate, g) 2821 } 2822 2823 func reserveNRefunds(w *assetWallet, n, maxFeeRate uint64, g *dexeth.Gases) (uint64, error) { 2824 refundCost := g.Refund * maxFeeRate 2825 reserve := refundCost * n 2826 2827 if err := w.lockFunds(reserve, refundReserve); err != nil { 2828 return 0, err 2829 } 2830 return reserve, nil 2831 } 2832 2833 // UnlockRefundReserves unlocks the specified amount from refund 2834 // reserves. Part of the AccountLocker interface. 2835 func (w *ETHWallet) UnlockRefundReserves(reserves uint64) { 2836 unlockRefundReserves(w.assetWallet, reserves) 2837 } 2838 2839 // UnlockRefundReserves unlocks the specified amount from refund 2840 // reserves. Part of the AccountLocker interface. 2841 func (w *TokenWallet) UnlockRefundReserves(reserves uint64) { 2842 unlockRefundReserves(w.parent, reserves) 2843 } 2844 2845 func unlockRefundReserves(w *assetWallet, reserves uint64) { 2846 w.unlockFunds(reserves, refundReserve) 2847 } 2848 2849 // ReReserveRefund checks out an amount for doing refunds. Use ReReserveRefund 2850 // after initializing a new assetWallet. Part of the AccountLocker 2851 // interface. 2852 func (w *ETHWallet) ReReserveRefund(req uint64) error { 2853 return w.lockFunds(req, refundReserve) 2854 } 2855 2856 // ReReserveRefund checks out an amount for doing refunds. Use ReReserveRefund 2857 // after initializing a new assetWallet. Part of the AccountLocker 2858 // interface. 2859 func (w *TokenWallet) ReReserveRefund(req uint64) error { 2860 return w.parent.lockFunds(req, refundReserve) 2861 } 2862 2863 // SignMessage signs the message with the private key associated with the 2864 // specified funding Coin. Only a coin that came from the address this wallet 2865 // is initialized with can be used to sign. 2866 func (eth *baseWallet) SignMessage(_ asset.Coin, msg dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) { 2867 sig, pubKey, err := eth.node.signData(msg) 2868 if err != nil { 2869 return nil, nil, fmt.Errorf("SignMessage: error signing data: %w", err) 2870 } 2871 2872 return []dex.Bytes{pubKey}, []dex.Bytes{sig}, nil 2873 } 2874 2875 // AuditContract retrieves information about a swap contract on the 2876 // blockchain. This would be used to verify the counter-party's contract 2877 // during a swap. coinID is expected to be the transaction id, and must 2878 // be the same as the hash of serializedTx. contract is expected to be 2879 // (contractVersion|secretHash) where the secretHash uniquely keys the swap. 2880 func (w *assetWallet) AuditContract(coinID, contract, serializedTx dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) { 2881 tx := new(types.Transaction) 2882 err := tx.UnmarshalBinary(serializedTx) 2883 if err != nil { 2884 return nil, fmt.Errorf("AuditContract: failed to unmarshal transaction: %w", err) 2885 } 2886 2887 txHash := tx.Hash() 2888 if !bytes.Equal(coinID, txHash[:]) { 2889 return nil, fmt.Errorf("AuditContract: coin id != txHash - coin id: %x, txHash: %s", coinID, tx.Hash()) 2890 } 2891 2892 version, secretHash, err := dexeth.DecodeContractData(contract) 2893 if err != nil { 2894 return nil, fmt.Errorf("AuditContract: failed to decode contract data: %w", err) 2895 } 2896 2897 initiations, err := dexeth.ParseInitiateData(tx.Data(), version) 2898 if err != nil { 2899 return nil, fmt.Errorf("AuditContract: failed to parse initiate data: %w", err) 2900 } 2901 2902 initiation, ok := initiations[secretHash] 2903 if !ok { 2904 return nil, errors.New("AuditContract: tx does not initiate secret hash") 2905 } 2906 2907 coin := &coin{ 2908 id: txHash, 2909 value: w.atomize(initiation.Value), 2910 } 2911 2912 return &asset.AuditInfo{ 2913 Recipient: initiation.Participant.Hex(), 2914 Expiration: initiation.LockTime, 2915 Coin: coin, 2916 Contract: contract, 2917 SecretHash: secretHash[:], 2918 }, nil 2919 } 2920 2921 // LockTimeExpired returns true if the specified locktime has expired, making it 2922 // possible to redeem the locked coins. 2923 func (w *assetWallet) LockTimeExpired(ctx context.Context, lockTime time.Time) (bool, error) { 2924 header, err := w.node.bestHeader(ctx) 2925 if err != nil { 2926 return false, fmt.Errorf("unable to retrieve block header: %w", err) 2927 } 2928 blockTime := time.Unix(int64(header.Time), 0) 2929 return lockTime.Before(blockTime), nil 2930 } 2931 2932 // ContractLockTimeExpired returns true if the specified contract's locktime has 2933 // expired, making it possible to issue a Refund. 2934 func (w *assetWallet) ContractLockTimeExpired(ctx context.Context, contract dex.Bytes) (bool, time.Time, error) { 2935 contractVer, secretHash, err := dexeth.DecodeContractData(contract) 2936 if err != nil { 2937 return false, time.Time{}, err 2938 } 2939 2940 swap, err := w.swap(ctx, secretHash, contractVer) 2941 if err != nil { 2942 return false, time.Time{}, err 2943 } 2944 2945 // Time is not yet set for uninitiated swaps. 2946 if swap.State == dexeth.SSNone { 2947 return false, time.Time{}, asset.ErrSwapNotInitiated 2948 } 2949 2950 expired, err := w.LockTimeExpired(ctx, swap.LockTime) 2951 if err != nil { 2952 return false, time.Time{}, err 2953 } 2954 return expired, swap.LockTime, nil 2955 } 2956 2957 // findRedemptionResult is used internally for queued findRedemptionRequests. 2958 type findRedemptionResult struct { 2959 err error 2960 secret []byte 2961 makerAddr string 2962 } 2963 2964 // findRedemptionRequest is a request that is waiting on a redemption result. 2965 type findRedemptionRequest struct { 2966 contractVer uint32 2967 res chan *findRedemptionResult 2968 } 2969 2970 // sendFindRedemptionResult sends the result or logs a message if it cannot be 2971 // sent. 2972 func (eth *baseWallet) sendFindRedemptionResult(req *findRedemptionRequest, secretHash [32]byte, 2973 secret []byte, makerAddr string, err error) { 2974 select { 2975 case req.res <- &findRedemptionResult{secret: secret, makerAddr: makerAddr, err: err}: 2976 default: 2977 eth.log.Info("findRedemptionResult channel blocking for request %s", secretHash) 2978 } 2979 } 2980 2981 // findRedemptionRequests creates a copy of the findRedemptionReqs map. 2982 func (w *assetWallet) findRedemptionRequests() map[[32]byte]*findRedemptionRequest { 2983 w.findRedemptionMtx.RLock() 2984 defer w.findRedemptionMtx.RUnlock() 2985 reqs := make(map[[32]byte]*findRedemptionRequest, len(w.findRedemptionReqs)) 2986 for secretHash, req := range w.findRedemptionReqs { 2987 reqs[secretHash] = req 2988 } 2989 return reqs 2990 } 2991 2992 // FindRedemption checks the contract for a redemption. If the swap is initiated 2993 // but un-redeemed and un-refunded, FindRedemption will block until a redemption 2994 // is seen. 2995 func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) { 2996 // coinIDTmpl is a template for constructing Coin ID when Taker 2997 // (aka participant) finds redemption himself. %s represents Maker Ethereum 2998 // account address so that user, as Taker, could manually look it up in case 2999 // he needs it. Ideally we'd want to have transaction ID there instead of 3000 // account address, but that's currently impossible to get in Ethereum smart 3001 // contract, so we are basically doing the next best thing here. 3002 const coinIDTmpl = coinIDTakerFoundMakerRedemption + "%s" 3003 3004 contractVer, secretHash, err := dexeth.DecodeContractData(contract) 3005 if err != nil { 3006 return nil, nil, err 3007 } 3008 3009 // See if it's ready right away. 3010 secret, makerAddr, err := w.findSecret(secretHash, contractVer) 3011 if err != nil { 3012 return nil, nil, err 3013 } 3014 3015 if len(secret) > 0 { 3016 return dex.Bytes(fmt.Sprintf(coinIDTmpl, makerAddr)), secret, nil 3017 } 3018 3019 // Not ready. Queue the request. 3020 req := &findRedemptionRequest{ 3021 contractVer: contractVer, 3022 res: make(chan *findRedemptionResult, 1), 3023 } 3024 3025 w.findRedemptionMtx.Lock() 3026 3027 if w.findRedemptionReqs[secretHash] != nil { 3028 w.findRedemptionMtx.Unlock() 3029 return nil, nil, fmt.Errorf("duplicate find redemption request for %x", secretHash) 3030 } 3031 3032 w.findRedemptionReqs[secretHash] = req 3033 3034 w.findRedemptionMtx.Unlock() 3035 3036 var res *findRedemptionResult 3037 select { 3038 case res = <-req.res: 3039 case <-ctx.Done(): 3040 } 3041 3042 w.findRedemptionMtx.Lock() 3043 delete(w.findRedemptionReqs, secretHash) 3044 w.findRedemptionMtx.Unlock() 3045 3046 if res == nil { 3047 return nil, nil, fmt.Errorf("context cancelled for find redemption request %x", secretHash) 3048 } 3049 3050 if res.err != nil { 3051 return nil, nil, res.err 3052 } 3053 3054 return dex.Bytes(fmt.Sprintf(coinIDTmpl, res.makerAddr)), res.secret[:], nil 3055 } 3056 3057 // findSecret returns redemption secret from smart contract that Maker put there 3058 // redeeming Taker swap along with Maker Ethereum account address. Returns empty 3059 // values if Maker hasn't redeemed yet. 3060 func (w *assetWallet) findSecret(secretHash [32]byte, contractVer uint32) ([]byte, string, error) { 3061 ctx, cancel := context.WithTimeout(w.ctx, 10*time.Second) 3062 defer cancel() 3063 swap, err := w.swap(ctx, secretHash, contractVer) 3064 if err != nil { 3065 return nil, "", err 3066 } 3067 3068 switch swap.State { 3069 case dexeth.SSInitiated: 3070 return nil, "", nil // no Maker redeem yet, but keep checking 3071 case dexeth.SSRedeemed: 3072 return swap.Secret[:], swap.Initiator.String(), nil 3073 case dexeth.SSNone: 3074 return nil, "", fmt.Errorf("swap %x does not exist", secretHash) 3075 case dexeth.SSRefunded: 3076 return nil, "", fmt.Errorf("swap %x is already refunded", secretHash) 3077 } 3078 return nil, "", fmt.Errorf("unrecognized swap state %v", swap.State) 3079 } 3080 3081 // Refund refunds a contract. This can only be used after the time lock has 3082 // expired. 3083 func (w *assetWallet) Refund(_, contract dex.Bytes, feeRate uint64) (dex.Bytes, error) { 3084 version, secretHash, err := dexeth.DecodeContractData(contract) 3085 if err != nil { 3086 return nil, fmt.Errorf("Refund: failed to decode contract: %w", err) 3087 } 3088 3089 swap, err := w.swap(w.ctx, secretHash, version) 3090 if err != nil { 3091 return nil, err 3092 } 3093 // It's possible the swap was refunded by someone else. In that case we 3094 // cannot know the refunding tx hash. 3095 switch swap.State { 3096 case dexeth.SSInitiated: // good, check refundability 3097 case dexeth.SSNone: 3098 return nil, asset.ErrSwapNotInitiated 3099 case dexeth.SSRefunded: 3100 w.log.Infof("Swap with secret hash %x already refunded.", secretHash) 3101 zeroHash := common.Hash{} 3102 return zeroHash[:], nil 3103 case dexeth.SSRedeemed: 3104 w.log.Infof("Swap with secret hash %x already redeemed with secret key %x.", 3105 secretHash, swap.Secret) 3106 return nil, asset.CoinNotFoundError // so caller knows to FindRedemption 3107 } 3108 3109 refundable, err := w.isRefundable(secretHash, version) 3110 if err != nil { 3111 return nil, fmt.Errorf("Refund: failed to check isRefundable: %w", err) 3112 } 3113 if !refundable { 3114 return nil, fmt.Errorf("Refund: swap with secret hash %x is not refundable", secretHash) 3115 } 3116 3117 maxFeeRate := dexeth.GweiToWei(feeRate) 3118 _, tipRate, err := w.currentNetworkFees(w.ctx) 3119 if err != nil { 3120 return nil, fmt.Errorf("Refund: failed to get network tip cap: %w", err) 3121 } 3122 3123 tx, err := w.refund(secretHash, w.atomize(swap.Value), maxFeeRate, tipRate, version) 3124 if err != nil { 3125 return nil, fmt.Errorf("Refund: failed to call refund: %w", err) 3126 } 3127 3128 txHash := tx.Hash() 3129 return txHash[:], nil 3130 } 3131 3132 // DepositAddress returns an address for the exchange wallet. This implementation 3133 // is idempotent, always returning the same address for a given assetWallet. 3134 func (eth *baseWallet) DepositAddress() (string, error) { 3135 return eth.addr.String(), nil 3136 } 3137 3138 // RedemptionAddress gets an address for use in redeeming the counterparty's 3139 // swap. This would be included in their swap initialization. 3140 func (eth *baseWallet) RedemptionAddress() (string, error) { 3141 return eth.addr.String(), nil 3142 } 3143 3144 // Unlock unlocks the exchange wallet. 3145 func (eth *ETHWallet) Unlock(pw []byte) error { 3146 return eth.node.unlock(string(pw)) 3147 } 3148 3149 // Lock locks the exchange wallet. 3150 func (eth *ETHWallet) Lock() error { 3151 return eth.node.lock() 3152 } 3153 3154 // Locked will be true if the wallet is currently locked. 3155 func (eth *ETHWallet) Locked() bool { 3156 return eth.node.locked() 3157 } 3158 3159 // SendTransaction broadcasts a valid fully-signed transaction. 3160 func (eth *baseWallet) SendTransaction(rawTx []byte) ([]byte, error) { 3161 tx := new(types.Transaction) 3162 err := tx.UnmarshalBinary(rawTx) 3163 if err != nil { 3164 return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) 3165 } 3166 if err := eth.node.sendSignedTransaction(eth.ctx, tx); err != nil { 3167 return nil, err 3168 } 3169 return tx.Hash().Bytes(), nil 3170 } 3171 3172 // ValidateAddress checks whether the provided address is a valid hex-encoded 3173 // Ethereum address. 3174 func (w *ETHWallet) ValidateAddress(address string) bool { 3175 return common.IsHexAddress(address) 3176 } 3177 3178 // ValidateAddress checks whether the provided address is a valid hex-encoded 3179 // Ethereum address. 3180 func (w *TokenWallet) ValidateAddress(address string) bool { 3181 return common.IsHexAddress(address) 3182 } 3183 3184 // isValidSend is a helper function for both token and ETH wallet. It returns an 3185 // error if subtract is true, addr is invalid or value is zero. 3186 func isValidSend(addr string, value uint64, subtract bool) error { 3187 if value == 0 { 3188 return fmt.Errorf("cannot send zero amount") 3189 } 3190 if subtract { 3191 return fmt.Errorf("wallet does not support subtracting network fee from send amount") 3192 } 3193 if !common.IsHexAddress(addr) { 3194 return fmt.Errorf("invalid hex address %q", addr) 3195 } 3196 return nil 3197 } 3198 3199 // canSend ensures that the wallet has enough to cover send value and returns 3200 // the fee rate and max fee required for the send tx. If isPreEstimate is false, 3201 // wallet balance must be enough to cover total spend. 3202 func (w *ETHWallet) canSend(value uint64, verifyBalance, isPreEstimate bool) (maxFee uint64, maxFeeRate, tipRate *big.Int, err error) { 3203 maxFeeRate, tipRate, err = w.recommendedMaxFeeRate(w.ctx) 3204 if err != nil { 3205 return 0, nil, nil, fmt.Errorf("error getting max fee rate: %w", err) 3206 } 3207 maxFeeRateGwei := dexeth.WeiToGweiCeil(maxFeeRate) 3208 3209 maxFee = defaultSendGasLimit * maxFeeRateGwei 3210 3211 if isPreEstimate { 3212 maxFee = maxFee * 12 / 10 // 20% buffer 3213 } 3214 3215 if verifyBalance { 3216 bal, err := w.Balance() 3217 if err != nil { 3218 return 0, nil, nil, err 3219 } 3220 avail := bal.Available 3221 if avail < value { 3222 return 0, nil, nil, fmt.Errorf("not enough funds to send: have %d gwei need %d gwei", avail, value) 3223 } 3224 3225 if avail < value+maxFee { 3226 return 0, nil, nil, fmt.Errorf("available funds %d gwei cannot cover value being sent: need %d gwei + %d gwei max fee", avail, value, maxFee) 3227 } 3228 } 3229 return 3230 } 3231 3232 // canSend ensures that the wallet has enough to cover send value and returns 3233 // the fee rate and max fee required for the send tx. 3234 func (w *TokenWallet) canSend(value uint64, verifyBalance, isPreEstimate bool) (maxFee uint64, maxFeeRate, tipRate *big.Int, err error) { 3235 maxFeeRate, tipRate, err = w.recommendedMaxFeeRate(w.ctx) 3236 if err != nil { 3237 return 0, nil, nil, fmt.Errorf("error getting max fee rate: %w", err) 3238 } 3239 maxFeeRateGwei := dexeth.WeiToGweiCeil(maxFeeRate) 3240 3241 g := w.gases(contractVersionNewest) 3242 if g == nil { 3243 return 0, nil, nil, fmt.Errorf("gas table not found") 3244 } 3245 3246 maxFee = maxFeeRateGwei * g.Transfer 3247 3248 if isPreEstimate { 3249 maxFee = maxFee * 12 / 10 // 20% buffer 3250 } 3251 3252 if verifyBalance { 3253 bal, err := w.Balance() 3254 if err != nil { 3255 return 0, nil, nil, err 3256 } 3257 avail := bal.Available 3258 if avail < value { 3259 return 0, nil, nil, fmt.Errorf("not enough tokens: have %[1]d %[3]s need %[2]d %[3]s", avail, value, w.ui.AtomicUnit) 3260 } 3261 3262 ethBal, err := w.parent.Balance() 3263 if err != nil { 3264 return 0, nil, nil, fmt.Errorf("error getting base chain balance: %w", err) 3265 } 3266 3267 if ethBal.Available < maxFee { 3268 return 0, nil, nil, fmt.Errorf("insufficient balance to cover token transfer fees. %d < %d", 3269 ethBal.Available, maxFee) 3270 } 3271 } 3272 return 3273 } 3274 3275 // EstimateSendTxFee returns a tx fee estimate for a send tx. The provided fee 3276 // rate is ignored since all sends will use an internally derived fee rate. If 3277 // an address is provided, it will ensure wallet has enough to cover total 3278 // spend. 3279 func (w *ETHWallet) EstimateSendTxFee(addr string, value, _ uint64, _, maxWithdraw bool) (uint64, bool, error) { 3280 if err := isValidSend(addr, value, maxWithdraw); err != nil && addr != "" { // fee estimate for a send tx. 3281 return 0, false, err 3282 } 3283 maxFee, _, _, err := w.canSend(value, addr != "", true) 3284 if err != nil { 3285 return 0, false, err 3286 } 3287 return maxFee, w.ValidateAddress(addr), nil 3288 } 3289 3290 // StandardSendFees returns the fees for a simple send tx. 3291 func (w *ETHWallet) StandardSendFee(feeRate uint64) uint64 { 3292 return defaultSendGasLimit * feeRate 3293 } 3294 3295 // EstimateSendTxFee returns a tx fee estimate for a send tx. The provided fee 3296 // rate is ignored since all sends will use an internally derived fee rate. If 3297 // an address is provided, it will ensure wallet has enough to cover total 3298 // spend. 3299 func (w *TokenWallet) EstimateSendTxFee(addr string, value, _ uint64, _, maxWithdraw bool) (fee uint64, isValidAddress bool, err error) { 3300 if err := isValidSend(addr, value, maxWithdraw); err != nil && addr != "" { // fee estimate for a send tx. 3301 return 0, false, err 3302 } 3303 maxFee, _, _, err := w.canSend(value, addr != "", true) 3304 if err != nil { 3305 return 0, false, err 3306 } 3307 return maxFee, w.ValidateAddress(addr), nil 3308 } 3309 3310 // StandardSendFees returns the fees for a simple send tx. 3311 func (w *TokenWallet) StandardSendFee(feeRate uint64) uint64 { 3312 g := w.gases(contractVersionNewest) 3313 if g == nil { 3314 w.log.Errorf("error getting gases for token %s", w.token.Name) 3315 return 0 3316 } 3317 return g.Transfer * feeRate 3318 } 3319 3320 // RestorationInfo returns information about how to restore the wallet in 3321 // various external wallets. 3322 func (w *ETHWallet) RestorationInfo(seed []byte) ([]*asset.WalletRestoration, error) { 3323 privateKey, zero, err := privKeyFromSeed(seed) 3324 if err != nil { 3325 return nil, err 3326 } 3327 defer zero() 3328 3329 return []*asset.WalletRestoration{ 3330 { 3331 Target: "MetaMask", 3332 Seed: hex.EncodeToString(privateKey), 3333 SeedName: "Private Key", 3334 Instructions: "Accounts can be imported by private key only if MetaMask has already be initialized. " + 3335 "If this is your first time installing MetaMask, create a new wallet and secret recovery phrase. " + 3336 "Then, to import your DEX account into MetaMask, follow the steps below:\n" + 3337 `1. Open the settings menu 3338 2. Select "Import Account" 3339 3. Make sure "Private Key" is selected, and enter the private key above into the box`, 3340 }, 3341 }, nil 3342 } 3343 3344 // SwapConfirmations gets the number of confirmations and the spend status 3345 // for the specified swap. 3346 func (w *assetWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, contract dex.Bytes, _ time.Time) (confs uint32, spent bool, err error) { 3347 contractVer, secretHash, err := dexeth.DecodeContractData(contract) 3348 if err != nil { 3349 return 0, false, err 3350 } 3351 3352 ctx, cancel := context.WithTimeout(ctx, onChainDataFetchTimeout) 3353 defer cancel() 3354 3355 swapData, err := w.swap(ctx, secretHash, contractVer) 3356 if err != nil { 3357 return 0, false, fmt.Errorf("error finding swap state: %w", err) 3358 } 3359 3360 if swapData.State == dexeth.SSNone { 3361 // Check if we know about the tx ourselves. If it's not in pendingTxs 3362 // or the database, assume it's lost. 3363 return 0, false, asset.ErrSwapNotInitiated 3364 } 3365 3366 spent = swapData.State >= dexeth.SSRedeemed 3367 tip := w.tipHeight() 3368 // TODO: If tip < swapData.BlockHeight (which has been observed), what does 3369 // that mean? Are we using the wrong provider in a multi-provider setup? How 3370 // do we resolve provider relevance? 3371 if tip >= swapData.BlockHeight { 3372 confs = uint32(tip - swapData.BlockHeight + 1) 3373 } 3374 return 3375 } 3376 3377 // Send sends the exact value to the specified address. The provided fee rate is 3378 // ignored since all sends will use an internally derived fee rate. 3379 func (w *ETHWallet) Send(addr string, value, _ uint64) (asset.Coin, error) { 3380 if err := isValidSend(addr, value, false); err != nil { 3381 return nil, err 3382 } 3383 3384 _ /* maxFee */, maxFeeRate, tipRate, err := w.canSend(value, true, false) 3385 if err != nil { 3386 return nil, err 3387 } 3388 // TODO: Subtract option. 3389 // if avail < value+maxFee { 3390 // value -= maxFee 3391 // } 3392 3393 tx, err := w.sendToAddr(common.HexToAddress(addr), value, maxFeeRate, tipRate) 3394 if err != nil { 3395 return nil, err 3396 } 3397 3398 txHash := tx.Hash() 3399 3400 return &coin{id: txHash, value: value}, nil 3401 } 3402 3403 // Send sends the exact value to the specified address. Fees are taken from the 3404 // parent wallet. The provided fee rate is ignored since all sends will use an 3405 // internally derived fee rate. 3406 func (w *TokenWallet) Send(addr string, value, _ uint64) (asset.Coin, error) { 3407 if err := isValidSend(addr, value, false); err != nil { 3408 return nil, err 3409 } 3410 3411 _ /* maxFee */, maxFeeRate, tipRate, err := w.canSend(value, true, false) 3412 if err != nil { 3413 return nil, err 3414 } 3415 3416 tx, err := w.sendToAddr(common.HexToAddress(addr), value, maxFeeRate, tipRate) 3417 if err != nil { 3418 return nil, err 3419 } 3420 3421 return &coin{id: tx.Hash(), value: value}, nil 3422 } 3423 3424 // ValidateSecret checks that the secret satisfies the contract. 3425 func (*baseWallet) ValidateSecret(secret, secretHash []byte) bool { 3426 h := sha256.Sum256(secret) 3427 return bytes.Equal(h[:], secretHash) 3428 } 3429 3430 // SyncStatus is information about the blockchain sync status. 3431 // 3432 // TODO: Since the merge, the sync status from a geth full node, namely the 3433 // prog.CurrentBlock prog.HighestBlock, always seem to be the same number. 3434 // Initial sync will always be zero. Later when restarting the node they move 3435 // together but never indicate the highest known block on the chain. Further 3436 // more, requesting the best block header starts to fail after a few tries 3437 // during initial sync. Investigate how to get correct sync progress. 3438 func (eth *baseWallet) SyncStatus() (*asset.SyncStatus, error) { 3439 prog, tipTime, err := eth.node.syncProgress(eth.ctx) 3440 if err != nil { 3441 return nil, err 3442 } 3443 checkHeaderTime := func() bool { 3444 // Time in the header is in seconds. 3445 timeDiff := time.Now().Unix() - int64(tipTime) 3446 if timeDiff > dexeth.MaxBlockInterval && eth.net != dex.Simnet { 3447 eth.log.Infof("Time since block (%d sec) exceeds %d sec. "+ 3448 "Assuming not in sync. Ensure your computer's system clock "+ 3449 "is correct.", timeDiff, dexeth.MaxBlockInterval) 3450 return false 3451 } 3452 return true 3453 } 3454 return &asset.SyncStatus{ 3455 Synced: checkHeaderTime() && eth.node.peerCount() > 0, 3456 StartingBlocks: eth.startingBlocks.Load(), 3457 TargetHeight: prog.HighestBlock, 3458 Blocks: prog.CurrentBlock, 3459 }, nil 3460 } 3461 3462 // DynamicSwapFeesPaid returns fees for initiation transactions. Part of the 3463 // asset.DynamicSwapper interface. 3464 func (eth *assetWallet) DynamicSwapFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (fee uint64, secretHashes [][]byte, err error) { 3465 return eth.swapOrRedemptionFeesPaid(ctx, coinID, contractData, true) 3466 } 3467 3468 // DynamicRedemptionFeesPaid returns fees for redemption transactions. Part of 3469 // the asset.DynamicSwapper interface. 3470 func (eth *assetWallet) DynamicRedemptionFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (fee uint64, secretHashes [][]byte, err error) { 3471 return eth.swapOrRedemptionFeesPaid(ctx, coinID, contractData, false) 3472 } 3473 3474 // extractSecretHashes extracts the secret hashes from the reedeem or swap tx 3475 // data. The returned hashes are sorted lexicographically. 3476 func extractSecretHashes(isInit bool, txData []byte, contractVer uint32) (secretHashes [][]byte, _ error) { 3477 defer func() { 3478 sort.Slice(secretHashes, func(i, j int) bool { return bytes.Compare(secretHashes[i], secretHashes[j]) < 0 }) 3479 }() 3480 if isInit { 3481 inits, err := dexeth.ParseInitiateData(txData, contractVer) 3482 if err != nil { 3483 return nil, fmt.Errorf("invalid initiate data: %v", err) 3484 } 3485 secretHashes = make([][]byte, 0, len(inits)) 3486 for k := range inits { 3487 copyK := k 3488 secretHashes = append(secretHashes, copyK[:]) 3489 } 3490 return secretHashes, nil 3491 } 3492 // redeem 3493 redeems, err := dexeth.ParseRedeemData(txData, contractVer) 3494 if err != nil { 3495 return nil, fmt.Errorf("invalid redeem data: %v", err) 3496 } 3497 secretHashes = make([][]byte, 0, len(redeems)) 3498 for k := range redeems { 3499 copyK := k 3500 secretHashes = append(secretHashes, copyK[:]) 3501 } 3502 return secretHashes, nil 3503 } 3504 3505 // swapOrRedemptionFeesPaid returns exactly how much gwei was used to send an 3506 // initiation or redemption transaction. It also returns the secret hashes 3507 // included with this init or redeem. Secret hashes are sorted so returns are 3508 // always the same, but the order may not be the same as they exist in the 3509 // transaction on chain. The transaction must be already mined for this 3510 // function to work. Returns asset.CoinNotFoundError for unmined txn. Returns 3511 // asset.ErrNotEnoughConfirms for txn with too few confirmations. Will also 3512 // error if the secret hash in the contractData is not found in the transaction 3513 // secret hashes. 3514 func (w *baseWallet) swapOrRedemptionFeesPaid( 3515 ctx context.Context, 3516 coinID dex.Bytes, 3517 contractData dex.Bytes, 3518 isInit bool, 3519 ) (fee uint64, secretHashes [][]byte, err error) { 3520 3521 var txHash common.Hash 3522 copy(txHash[:], coinID) 3523 3524 contractVer, secretHash, err := dexeth.DecodeContractData(contractData) 3525 if err != nil { 3526 return 0, nil, err 3527 } 3528 3529 tip := w.tipHeight() 3530 3531 var blockNum uint64 3532 var tx *types.Transaction 3533 if w.withLocalTxRead(txHash, func(wt *extendedWalletTx) { 3534 blockNum = wt.BlockNumber 3535 fee = wt.Fees 3536 tx, err = wt.tx() 3537 if err != nil { 3538 w.log.Errorf("Error decoding wallet transaction %s: %v", txHash, err) 3539 } 3540 }) && err == nil { 3541 if confs := safeConfs(tip, blockNum); confs < w.finalizeConfs { 3542 return 0, nil, asset.ErrNotEnoughConfirms 3543 } 3544 secretHashes, err = extractSecretHashes(isInit, tx.Data(), contractVer) 3545 return 3546 } 3547 3548 // We don't have information locally. This really shouldn't happen anymore, 3549 // but let's look on-chain anyway. 3550 3551 receipt, tx, err := w.node.transactionAndReceipt(ctx, txHash) 3552 if err != nil { 3553 return 0, nil, err 3554 } 3555 3556 if confs := safeConfsBig(tip, receipt.BlockNumber); confs < w.finalizeConfs { 3557 return 0, nil, asset.ErrNotEnoughConfirms 3558 } 3559 3560 bigFees := new(big.Int).Mul(receipt.EffectiveGasPrice, big.NewInt(int64(receipt.GasUsed))) 3561 fee = dexeth.WeiToGweiCeil(bigFees) 3562 secretHashes, err = extractSecretHashes(isInit, tx.Data(), contractVer) 3563 if err != nil { 3564 return 0, nil, err 3565 } 3566 var found bool 3567 for i := range secretHashes { 3568 if bytes.Equal(secretHash[:], secretHashes[i]) { 3569 found = true 3570 break 3571 } 3572 } 3573 if !found { 3574 return 0, nil, fmt.Errorf("secret hash %x not found in transaction", secretHash) 3575 } 3576 return dexeth.WeiToGweiCeil(bigFees), secretHashes, nil 3577 } 3578 3579 // RegFeeConfirmations gets the number of confirmations for the specified 3580 // transaction. 3581 func (w *baseWallet) RegFeeConfirmations(ctx context.Context, coinID dex.Bytes) (confs uint32, err error) { 3582 var txHash common.Hash 3583 copy(txHash[:], coinID) 3584 if found, txData := w.localTxStatus(txHash); found { 3585 if tip := w.tipHeight(); txData.blockNum != 0 && txData.blockNum < tip { 3586 return uint32(tip - txData.blockNum + 1), nil 3587 } 3588 return 0, nil 3589 } 3590 3591 return w.node.transactionConfirmations(ctx, txHash) 3592 } 3593 3594 // currentNetworkFees give the current base fee rate (from the best header), 3595 // and recommended tip cap. 3596 func (w *baseWallet) currentNetworkFees(ctx context.Context) (baseRate, tipRate *big.Int, err error) { 3597 tip := w.tipHeight() 3598 c := &w.currentFees 3599 c.Lock() 3600 defer c.Unlock() 3601 if tip > 0 && c.blockNum == tip { 3602 return c.baseRate, c.tipRate, nil 3603 } 3604 c.baseRate, c.tipRate, err = w.node.currentFees(ctx) 3605 if err != nil { 3606 return nil, nil, fmt.Errorf("Error getting net fee state: %v", err) 3607 } 3608 c.blockNum = tip 3609 return c.baseRate, c.tipRate, nil 3610 } 3611 3612 // currentFeeRate gives the current rate of transactions being mined. Only 3613 // use this to provide informative realistic estimates of actual fee *use*. For 3614 // transaction planning, use recommendedMaxFeeRateGwei. 3615 func (w *baseWallet) currentFeeRate(ctx context.Context) (_ *big.Int, err error) { 3616 b, t, err := w.currentNetworkFees(ctx) 3617 if err != nil { 3618 return nil, err 3619 } 3620 return new(big.Int).Add(b, t), nil 3621 } 3622 3623 // recommendedMaxFeeRate finds a recommended max fee rate using the somewhat 3624 // standard baseRate * 2 + tip formula. 3625 func (eth *baseWallet) recommendedMaxFeeRate(ctx context.Context) (maxFeeRate, tipRate *big.Int, err error) { 3626 base, tip, err := eth.currentNetworkFees(ctx) 3627 if err != nil { 3628 return nil, nil, fmt.Errorf("Error getting net fee state: %v", err) 3629 } 3630 3631 return new(big.Int).Add( 3632 tip, 3633 new(big.Int).Mul(base, big.NewInt(2)), 3634 ), tip, nil 3635 } 3636 3637 // recommendedMaxFeeRateGwei gets the recommended max fee rate and converts it 3638 // to gwei. 3639 func (w *baseWallet) recommendedMaxFeeRateGwei(ctx context.Context) (uint64, error) { 3640 feeRate, _, err := w.recommendedMaxFeeRate(ctx) 3641 if err != nil { 3642 return 0, err 3643 } 3644 return dexeth.WeiToGweiSafe(feeRate) 3645 } 3646 3647 // FeeRate satisfies asset.FeeRater. 3648 func (eth *baseWallet) FeeRate() uint64 { 3649 r, err := eth.recommendedMaxFeeRateGwei(eth.ctx) 3650 if err != nil { 3651 eth.log.Errorf("Error getting max fee recommendation: %v", err) 3652 return 0 3653 } 3654 return r 3655 } 3656 3657 func (eth *ETHWallet) checkPeers() { 3658 numPeers := eth.node.peerCount() 3659 3660 for _, w := range eth.connectedWallets() { 3661 prevPeer := atomic.SwapUint32(&w.lastPeerCount, numPeers) 3662 if prevPeer != numPeers { 3663 w.peersChange(numPeers, nil) 3664 } 3665 } 3666 } 3667 3668 func (eth *ETHWallet) monitorPeers(ctx context.Context) { 3669 ticker := time.NewTicker(peerCountTicker) 3670 defer ticker.Stop() 3671 for { 3672 eth.checkPeers() 3673 3674 select { 3675 case <-ticker.C: 3676 case <-ctx.Done(): 3677 return 3678 } 3679 } 3680 } 3681 3682 // monitorBlocks pings for new blocks and runs the tipChange callback function 3683 // when the block changes. New blocks are also scanned for potential contract 3684 // redeems. 3685 func (eth *ETHWallet) monitorBlocks(ctx context.Context) { 3686 ticker := time.NewTicker(stateUpdateTick) 3687 defer ticker.Stop() 3688 for { 3689 select { 3690 case <-ticker.C: 3691 eth.checkForNewBlocks(ctx) 3692 case <-ctx.Done(): 3693 return 3694 } 3695 if ctx.Err() != nil { // shutdown during last check, disallow chance of another tick 3696 return 3697 } 3698 } 3699 } 3700 3701 // checkForNewBlocks checks for new blocks. When a tip change is detected, the 3702 // tipChange callback function is invoked and a goroutine is started to check 3703 // if any contracts in the findRedemptionQueue are redeemed in the new blocks. 3704 func (eth *ETHWallet) checkForNewBlocks(ctx context.Context) { 3705 ctx, cancel := context.WithTimeout(ctx, onChainDataFetchTimeout) 3706 defer cancel() 3707 bestHdr, err := eth.node.bestHeader(ctx) 3708 if err != nil { 3709 eth.log.Errorf("failed to get best hash: %v", err) 3710 return 3711 } 3712 bestHash := bestHdr.Hash() 3713 // This method is called frequently. Don't hold write lock 3714 // unless tip has changed. 3715 eth.tipMtx.RLock() 3716 currentTipHash := eth.currentTip.Hash() 3717 eth.tipMtx.RUnlock() 3718 if currentTipHash == bestHash { 3719 return 3720 } 3721 3722 eth.tipMtx.Lock() 3723 prevTip := eth.currentTip 3724 eth.currentTip = bestHdr 3725 eth.tipMtx.Unlock() 3726 3727 eth.log.Tracef("tip change: %s (%s) => %s (%s)", prevTip.Number, 3728 currentTipHash, bestHdr.Number, bestHash) 3729 3730 eth.checkPendingTxs() 3731 for _, w := range eth.connectedWallets() { 3732 w.checkFindRedemptions() 3733 w.checkPendingApprovals() 3734 w.emit.TipChange(bestHdr.Number.Uint64()) 3735 } 3736 } 3737 3738 // ConfirmRedemption checks the status of a redemption. If a transaction has 3739 // been fee-replaced, the caller is notified of this by having a different 3740 // coinID in the returned asset.ConfirmRedemptionStatus as was used to call the 3741 // function. Fee argument is ignored since it is calculated from the best 3742 // header. 3743 func (w *ETHWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, _ uint64) (*asset.ConfirmRedemptionStatus, error) { 3744 return w.confirmRedemption(coinID, redemption) 3745 } 3746 3747 // ConfirmRedemption checks the status of a redemption. If a transaction has 3748 // been fee-replaced, the caller is notified of this by having a different 3749 // coinID in the returned asset.ConfirmRedemptionStatus as was used to call the 3750 // function. Fee argument is ignored since it is calculated from the best 3751 // header. 3752 func (w *TokenWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, _ uint64) (*asset.ConfirmRedemptionStatus, error) { 3753 return w.confirmRedemption(coinID, redemption) 3754 } 3755 3756 func confStatus(confs, req uint64, txHash common.Hash) *asset.ConfirmRedemptionStatus { 3757 return &asset.ConfirmRedemptionStatus{ 3758 Confs: confs, 3759 Req: req, 3760 CoinID: txHash[:], 3761 } 3762 } 3763 3764 // confirmRedemption checks the confirmation status of a redemption transaction. 3765 func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Redemption) (*asset.ConfirmRedemptionStatus, error) { 3766 if len(coinID) != common.HashLength { 3767 return nil, fmt.Errorf("expected coin ID to be a transaction hash, but it has a length of %d", 3768 len(coinID)) 3769 } 3770 var txHash common.Hash 3771 copy(txHash[:], coinID) 3772 3773 contractVer, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) 3774 if err != nil { 3775 return nil, fmt.Errorf("failed to decode contract data: %w", err) 3776 } 3777 3778 tip := w.tipHeight() 3779 3780 // If we have local information, use that. 3781 if found, s := w.localTxStatus(txHash); found { 3782 if s.assumedLost || len(s.nonceReplacement) > 0 { 3783 if !s.feeReplacement { 3784 // Tell core to update it's coin ID. 3785 txHash = common.HexToHash(s.nonceReplacement) 3786 } else { 3787 return nil, asset.ErrTxLost 3788 } 3789 } 3790 3791 var confirmStatus *asset.ConfirmRedemptionStatus 3792 if s.blockNum != 0 && s.blockNum <= tip { 3793 confirmStatus = confStatus(tip-s.blockNum+1, w.finalizeConfs, txHash) 3794 } else { 3795 // Apparently not mined yet. 3796 confirmStatus = confStatus(0, w.finalizeConfs, txHash) 3797 } 3798 if s.receipt != nil && s.receipt.Status != types.ReceiptStatusSuccessful && confirmStatus.Confs >= w.finalizeConfs { 3799 return nil, asset.ErrTxRejected 3800 } 3801 return confirmStatus, nil 3802 } 3803 3804 // We know nothing of the tx locally. This shouldn't really happen, but 3805 // we'll look for it on-chain anyway. 3806 r, err := w.node.transactionReceipt(w.ctx, txHash) 3807 if err != nil { 3808 if errors.Is(err, asset.CoinNotFoundError) { 3809 // We don't know it ourselves and we can't see it on-chain. This 3810 // used to be a CoinNotFoundError, but since we have local tx 3811 // storage, we'll assume it's lost to space and time now. 3812 return nil, asset.ErrTxLost 3813 } 3814 return nil, err 3815 } 3816 3817 // We could potentially grab the tx, check the from address, and store it 3818 // to our db right here, but I suspect that this case would be exceedingly 3819 // rare anyway. 3820 3821 confs := safeConfsBig(tip, r.BlockNumber) 3822 if confs >= w.finalizeConfs { 3823 if r.Status == types.ReceiptStatusSuccessful { 3824 return confStatus(w.finalizeConfs, w.finalizeConfs, txHash), nil 3825 } 3826 // We weren't able to redeem. Perhaps fees were too low, but we'll 3827 // check the status in the contract for a couple of other conditions. 3828 swap, err := w.swap(w.ctx, secretHash, contractVer) 3829 if err != nil { 3830 return nil, fmt.Errorf("error pulling swap data from contract: %v", err) 3831 } 3832 switch swap.State { 3833 case dexeth.SSRedeemed: 3834 w.log.Infof("Redemption in tx %s was apparently redeemed by another tx. OK.", txHash) 3835 return confStatus(w.finalizeConfs, w.finalizeConfs, txHash), nil 3836 case dexeth.SSRefunded: 3837 return nil, asset.ErrSwapRefunded 3838 } 3839 3840 err = fmt.Errorf("tx %s failed to redeem %s funds", txHash, dex.BipIDSymbol(w.assetID)) 3841 return nil, errors.Join(err, asset.ErrTxRejected) 3842 } 3843 return confStatus(confs, w.finalizeConfs, txHash), nil 3844 } 3845 3846 // withLocalTxRead runs a function that reads a pending or DB tx under 3847 // read-lock. Certain DB transactions in undeterminable states will not be 3848 // used. 3849 func (w *baseWallet) withLocalTxRead(txHash common.Hash, f func(*extendedWalletTx)) bool { 3850 withPendingTxRead := func(txHash common.Hash, f func(*extendedWalletTx)) bool { 3851 w.nonceMtx.RLock() 3852 defer w.nonceMtx.RUnlock() 3853 for _, pendingTx := range w.pendingTxs { 3854 if pendingTx.txHash == txHash { 3855 f(pendingTx) 3856 return true 3857 } 3858 } 3859 return false 3860 } 3861 if withPendingTxRead(txHash, f) { 3862 return true 3863 } 3864 // Could be finalized and in the database. 3865 if confirmedTx, err := w.txDB.getTx(txHash); err != nil { 3866 w.log.Errorf("Error getting DB transaction: %v", err) 3867 } else if confirmedTx != nil { 3868 if !confirmedTx.Confirmed && confirmedTx.Receipt == nil && !confirmedTx.AssumedLost && confirmedTx.NonceReplacement == "" { 3869 // If it's in the db but not in pendingTxs, and we know nothing 3870 // about the tx, don't use it. 3871 return false 3872 } 3873 f(confirmedTx) 3874 return true 3875 } 3876 return false 3877 } 3878 3879 // walletTxStatus is data copied from an extendedWalletTx. 3880 type walletTxStatus struct { 3881 confirmed bool 3882 blockNum uint64 3883 nonceReplacement string 3884 feeReplacement bool 3885 receipt *types.Receipt 3886 assumedLost bool 3887 } 3888 3889 // localTxStatus looks for an extendedWalletTx and copies critical values to 3890 // a walletTxStatus for use without mutex protection. 3891 func (w *baseWallet) localTxStatus(txHash common.Hash) (_ bool, s *walletTxStatus) { 3892 return w.withLocalTxRead(txHash, func(wt *extendedWalletTx) { 3893 s = &walletTxStatus{ 3894 confirmed: wt.Confirmed, 3895 blockNum: wt.BlockNumber, 3896 nonceReplacement: wt.NonceReplacement, 3897 feeReplacement: wt.FeeReplacement, 3898 receipt: wt.Receipt, 3899 assumedLost: wt.AssumedLost, 3900 } 3901 }), s 3902 } 3903 3904 // checkFindRedemptions checks queued findRedemptionRequests. 3905 func (w *assetWallet) checkFindRedemptions() { 3906 for secretHash, req := range w.findRedemptionRequests() { 3907 if w.ctx.Err() != nil { 3908 return 3909 } 3910 secret, makerAddr, err := w.findSecret(secretHash, req.contractVer) 3911 if err != nil { 3912 w.sendFindRedemptionResult(req, secretHash, nil, "", err) 3913 } else if len(secret) > 0 { 3914 w.sendFindRedemptionResult(req, secretHash, secret, makerAddr, nil) 3915 } 3916 } 3917 } 3918 3919 func (w *assetWallet) checkPendingApprovals() { 3920 w.approvalsMtx.Lock() 3921 defer w.approvalsMtx.Unlock() 3922 3923 for version, pendingApproval := range w.pendingApprovals { 3924 if w.ctx.Err() != nil { 3925 return 3926 } 3927 var confirmed bool 3928 if found, txData := w.localTxStatus(pendingApproval.txHash); found { 3929 confirmed = txData.blockNum > 0 && txData.blockNum <= w.tipHeight() 3930 } else { 3931 confs, err := w.node.transactionConfirmations(w.ctx, pendingApproval.txHash) 3932 if err != nil && !errors.Is(err, asset.CoinNotFoundError) { 3933 w.log.Errorf("error getting confirmations for tx %s: %v", pendingApproval.txHash, err) 3934 continue 3935 } 3936 confirmed = confs > 0 3937 } 3938 if confirmed { 3939 go pendingApproval.onConfirm() 3940 delete(w.pendingApprovals, version) 3941 } 3942 } 3943 } 3944 3945 // sumPendingTxs sums the expected incoming and outgoing values in pending 3946 // transactions stored in pendingTxs. Not used if the node is a 3947 // txPoolFetcher. 3948 func (w *assetWallet) sumPendingTxs() (out, in uint64) { 3949 isToken := w.assetID != w.baseChainID 3950 3951 sumPendingTx := func(pendingTx *extendedWalletTx) { 3952 // Already confirmed, but still in the unconfirmed txs map waiting for 3953 // txConfsNeededToConfirm confirmations. 3954 if pendingTx.BlockNumber != 0 { 3955 return 3956 } 3957 3958 txAssetID := w.baseChainID 3959 if pendingTx.TokenID != nil { 3960 txAssetID = *pendingTx.TokenID 3961 } 3962 3963 if txAssetID == w.assetID { 3964 if asset.IncomingTxType(pendingTx.Type) { 3965 in += pendingTx.Amount 3966 } else { 3967 out += pendingTx.Amount 3968 } 3969 } 3970 if !isToken { 3971 out += pendingTx.Fees 3972 } 3973 } 3974 3975 w.nonceMtx.RLock() 3976 defer w.nonceMtx.RUnlock() 3977 3978 for _, pendingTx := range w.pendingTxs { 3979 sumPendingTx(pendingTx) 3980 } 3981 3982 return 3983 } 3984 3985 func (w *assetWallet) getConfirmedBalance() (*big.Int, error) { 3986 now := time.Now() 3987 tip := w.tipHeight() 3988 3989 w.balances.Lock() 3990 defer w.balances.Unlock() 3991 3992 if w.balances.m == nil { 3993 w.balances.m = make(map[uint32]*cachedBalance) 3994 } 3995 // Check to see if we already have one up-to-date 3996 cached := w.balances.m[w.assetID] 3997 if cached != nil && cached.height == tip && time.Since(cached.stamp) < time.Minute { 3998 return cached.bal, nil 3999 } 4000 4001 if w.multiBalanceContract == nil { 4002 var bal *big.Int 4003 var err error 4004 if w.assetID == w.baseChainID { 4005 bal, err = w.node.addressBalance(w.ctx, w.addr) 4006 } else { 4007 bal, err = w.tokenBalance() 4008 } 4009 if err != nil { 4010 return nil, err 4011 } 4012 w.balances.m[w.assetID] = &cachedBalance{ 4013 stamp: now, 4014 height: tip, 4015 bal: bal, 4016 } 4017 return bal, nil 4018 } 4019 4020 // Either not cached, or outdated. Fetch anew. 4021 var tokenAddrs []common.Address 4022 idIndexes := map[int]uint32{ 4023 0: w.baseChainID, 4024 } 4025 i := 1 4026 for assetID, tkn := range w.tokens { 4027 netToken := tkn.NetTokens[w.net] 4028 if netToken == nil || netToken.Address == (common.Address{}) { 4029 continue 4030 } 4031 tokenAddrs = append(tokenAddrs, netToken.Address) 4032 idIndexes[i] = assetID 4033 i++ 4034 } 4035 callOpts := &bind.CallOpts{ 4036 From: w.addr, 4037 Context: w.ctx, 4038 } 4039 4040 bals, err := w.multiBalanceContract.Balances(callOpts, w.addr, tokenAddrs) 4041 if err != nil { 4042 return nil, fmt.Errorf("error fetching multi-balance: %w", err) 4043 } 4044 if len(bals) != len(idIndexes) { 4045 return nil, fmt.Errorf("wrong number of balances in multi-balance result. wanted %d, got %d", len(idIndexes), len(bals)) 4046 } 4047 var reqBal *big.Int 4048 for i, bal := range bals { 4049 assetID := idIndexes[i] 4050 if assetID == w.assetID { 4051 reqBal = bal 4052 } 4053 w.balances.m[assetID] = &cachedBalance{ 4054 stamp: now, 4055 height: tip, 4056 bal: bal, 4057 } 4058 } 4059 if reqBal == nil { 4060 return nil, fmt.Errorf("requested asset not in multi-balance result: %v", err) 4061 } 4062 return reqBal, nil 4063 } 4064 4065 func (w *assetWallet) balanceWithTxPool() (*Balance, error) { 4066 isToken := w.assetID != w.baseChainID 4067 confirmed, err := w.getConfirmedBalance() 4068 if err != nil { 4069 return nil, fmt.Errorf("balance error: %v", err) 4070 } 4071 4072 txPoolNode, is := w.node.(txPoolFetcher) 4073 if !is { 4074 for { 4075 out, in := w.sumPendingTxs() 4076 checkBal, err := w.getConfirmedBalance() 4077 if err != nil { 4078 return nil, fmt.Errorf("balance consistency check error: %v", err) 4079 } 4080 outEVM := w.evmify(out) 4081 // If our balance has gone down in the interim, we'll use the lower 4082 // balance, but ensure that we're not setting up an underflow for 4083 // available balance. 4084 if checkBal.Cmp(confirmed) != 0 { 4085 w.log.Debugf("balance changed while checking pending txs. Trying again.") 4086 confirmed = checkBal 4087 continue 4088 } 4089 if outEVM.Cmp(confirmed) > 0 { 4090 return nil, fmt.Errorf("balance underflow detected. pending out (%s) > balance (%s)", outEVM, confirmed) 4091 } 4092 4093 return &Balance{ 4094 Current: confirmed, 4095 PendingOut: outEVM, 4096 PendingIn: w.evmify(in), 4097 }, nil 4098 } 4099 } 4100 4101 pendingTxs, err := txPoolNode.pendingTransactions() 4102 if err != nil { 4103 return nil, fmt.Errorf("error getting pending txs: %w", err) 4104 } 4105 4106 outgoing := new(big.Int) 4107 incoming := new(big.Int) 4108 zero := new(big.Int) 4109 4110 addFees := func(tx *types.Transaction) { 4111 gas := new(big.Int).SetUint64(tx.Gas()) 4112 // For legacy transactions, GasFeeCap returns gas price 4113 if gasFeeCap := tx.GasFeeCap(); gasFeeCap != nil { 4114 outgoing.Add(outgoing, new(big.Int).Mul(gas, gasFeeCap)) 4115 } else { 4116 w.log.Errorf("unable to calculate fees for tx %s", tx.Hash()) 4117 } 4118 } 4119 4120 ethSigner := types.LatestSigner(w.node.chainConfig()) // "latest" good for pending 4121 4122 for _, tx := range pendingTxs { 4123 from, _ := ethSigner.Sender(tx) 4124 if from != w.addr { 4125 continue 4126 } 4127 4128 if !isToken { 4129 // Add tx fees 4130 addFees(tx) 4131 } 4132 4133 var contractOut uint64 4134 for ver, c := range w.contractors { 4135 in, out, err := c.value(w.ctx, tx) 4136 if err != nil { 4137 w.log.Errorf("version %d contractor incomingValue error: %v", ver, err) 4138 continue 4139 } 4140 contractOut += out 4141 if in > 0 { 4142 incoming.Add(incoming, w.evmify(in)) 4143 } 4144 } 4145 if contractOut > 0 { 4146 outgoing.Add(outgoing, w.evmify(contractOut)) 4147 } else if !isToken { 4148 // Count withdraws and sends for ETH. 4149 v := tx.Value() 4150 if v.Cmp(zero) > 0 { 4151 outgoing.Add(outgoing, v) 4152 } 4153 } 4154 } 4155 4156 return &Balance{ 4157 Current: confirmed, 4158 PendingOut: outgoing, 4159 PendingIn: incoming, 4160 }, nil 4161 } 4162 4163 // Uncomment here and in sendToAddr to test actionTypeLostNonce. 4164 // var nonceBorked atomic.Bool 4165 // func (w *ETHWallet) borkNonce(tx *types.Transaction) { 4166 // fmt.Printf("\n##### losing tx for lost nonce testing\n\n") 4167 // txHash := tx.Hash() 4168 // v := big.NewInt(dexeth.GweiFactor) 4169 // spoofTx := types.NewTransaction(tx.Nonce(), w.addr, v, defaultSendGasLimit, v, nil) 4170 // spoofHash := tx.Hash() 4171 // pendingSpoofTx := w.extendedTx(spoofTx, asset.SelfSend, dexeth.GweiFactor) 4172 // w.nonceMtx.Lock() 4173 // for i, pendingTx := range w.pendingTxs { 4174 // if pendingTx.txHash == txHash { 4175 // w.pendingTxs[i] = pendingSpoofTx 4176 // w.tryStoreDBTx(pendingSpoofTx) 4177 // fmt.Printf("\n##### Replaced pending tx %s with spoof tx %s, nonce %d \n\n", txHash, spoofHash, tx.Nonce()) 4178 // break 4179 // } 4180 // } 4181 // w.nonceMtx.Unlock() 4182 // } 4183 4184 // Uncomment here and in sendToAddr to test actionTypeMissingNonces. 4185 // var nonceFuturized atomic.Bool 4186 4187 // Uncomment here and in sendToAddr to test actionTypeTooCheap 4188 // var nonceSkimped atomic.Bool 4189 4190 // sendToAddr sends funds to the address. 4191 func (w *ETHWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate, tipRate *big.Int) (tx *types.Transaction, err error) { 4192 4193 // Uncomment here and above to test actionTypeLostNonce. 4194 // Also change txAgeOut to like 1 minute. 4195 // if nonceBorked.CompareAndSwap(false, true) { 4196 // defer w.borkNonce(tx) 4197 // } 4198 4199 return tx, w.withNonce(w.ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) { 4200 4201 // Uncomment here and above to test actionTypeMissingNonces. 4202 // if nonceFuturized.CompareAndSwap(false, true) { 4203 // fmt.Printf("\n##### advancing nonce for missing nonce testing\n\n") 4204 // nonce.Add(nonce, big.NewInt(3)) 4205 // } 4206 4207 // Uncomment here and above to test actionTypeTooCheap. 4208 // if nonceSkimped.CompareAndSwap(false, true) { 4209 // fmt.Printf("\n##### lower max fee rate to test cheap tx handling\n\n") 4210 // maxFeeRate.SetUint64(1) 4211 // tipRate.SetUint64(0) 4212 // } 4213 4214 txOpts, err := w.node.txOpts(w.ctx, amt, defaultSendGasLimit, maxFeeRate, tipRate, nonce) 4215 if err != nil { 4216 return nil, 0, 0, nil, err 4217 } 4218 tx, err = w.node.sendTransaction(w.ctx, txOpts, addr, nil) 4219 if err != nil { 4220 return nil, 0, 0, nil, err 4221 } 4222 txType := asset.Send 4223 if addr == w.addr { 4224 txType = asset.SelfSend 4225 } 4226 recipient := addr.Hex() 4227 return tx, txType, amt, &recipient, nil 4228 }) 4229 } 4230 4231 // sendToAddr sends funds to the address. 4232 func (w *TokenWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate, tipRate *big.Int) (tx *types.Transaction, err error) { 4233 g := w.gases(contractVersionNewest) 4234 if g == nil { 4235 return nil, fmt.Errorf("no gas table") 4236 } 4237 return tx, w.withNonce(w.ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) { 4238 txOpts, err := w.node.txOpts(w.ctx, 0, g.Transfer, maxFeeRate, tipRate, nonce) 4239 if err != nil { 4240 return nil, 0, 0, nil, err 4241 } 4242 txType := asset.Send 4243 if addr == w.addr { 4244 txType = asset.SelfSend 4245 } 4246 recipient := addr.Hex() 4247 return tx, txType, amt, &recipient, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { 4248 tx, err = c.transfer(txOpts, addr, w.evmify(amt)) 4249 if err != nil { 4250 return err 4251 } 4252 return nil 4253 }) 4254 }) 4255 4256 } 4257 4258 // swap gets a swap keyed by secretHash in the contract. 4259 func (w *assetWallet) swap(ctx context.Context, secretHash [32]byte, contractVer uint32) (swap *dexeth.SwapState, err error) { 4260 return swap, w.withContractor(contractVer, func(c contractor) error { 4261 swap, err = c.swap(ctx, secretHash) 4262 return err 4263 }) 4264 } 4265 4266 // initiate initiates multiple swaps in the same transaction. 4267 func (w *assetWallet) initiate( 4268 ctx context.Context, assetID uint32, contracts []*asset.Contract, gasLimit uint64, maxFeeRate, tipRate *big.Int, contractVer uint32, 4269 ) (tx *types.Transaction, err error) { 4270 4271 var val, amt uint64 4272 for _, c := range contracts { 4273 amt += c.Value 4274 if assetID == w.baseChainID { 4275 val += c.Value 4276 } 4277 } 4278 return tx, w.withNonce(ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) { 4279 txOpts, err := w.node.txOpts(ctx, val, gasLimit, maxFeeRate, tipRate, nonce) 4280 if err != nil { 4281 return nil, 0, 0, nil, err 4282 } 4283 4284 return tx, asset.Swap, amt, nil, w.withContractor(contractVer, func(c contractor) error { 4285 tx, err = c.initiate(txOpts, contracts) 4286 return err 4287 }) 4288 }) 4289 } 4290 4291 // estimateInitGas checks the amount of gas that is used for the 4292 // initialization. 4293 func (w *assetWallet) estimateInitGas(ctx context.Context, numSwaps int, contractVer uint32) (gas uint64, err error) { 4294 return gas, w.withContractor(contractVer, func(c contractor) error { 4295 gas, err = c.estimateInitGas(ctx, numSwaps) 4296 return err 4297 }) 4298 } 4299 4300 // estimateRedeemGas checks the amount of gas that is used for the redemption. 4301 // Only used with testing and development tools like the 4302 // nodeclient_harness_test.go suite (GetGasEstimates, testRedeemGas, etc.). 4303 // Never use this with a public RPC provider, especially as maker, since it 4304 // reveals the secret keys. 4305 func (w *assetWallet) estimateRedeemGas(ctx context.Context, secrets [][32]byte, contractVer uint32) (gas uint64, err error) { 4306 return gas, w.withContractor(contractVer, func(c contractor) error { 4307 gas, err = c.estimateRedeemGas(ctx, secrets) 4308 return err 4309 }) 4310 } 4311 4312 // estimateRefundGas checks the amount of gas that is used for a refund. 4313 func (w *assetWallet) estimateRefundGas(ctx context.Context, secretHash [32]byte, contractVer uint32) (gas uint64, err error) { 4314 return gas, w.withContractor(contractVer, func(c contractor) error { 4315 gas, err = c.estimateRefundGas(ctx, secretHash) 4316 return err 4317 }) 4318 } 4319 4320 // loadContractors prepares the token contractors and add them to the map. 4321 func (w *assetWallet) loadContractors() error { 4322 token, found := w.tokens[w.assetID] 4323 if !found { 4324 return fmt.Errorf("token %d not found", w.assetID) 4325 } 4326 netToken, found := token.NetTokens[w.net] 4327 if !found { 4328 return fmt.Errorf("token %d not found", w.assetID) 4329 } 4330 4331 for ver := range netToken.SwapContracts { 4332 constructor, found := tokenContractorConstructors[ver] 4333 if !found { 4334 w.log.Errorf("contractor constructor not found for token %s, version %d", token.Name, ver) 4335 continue 4336 } 4337 c, err := constructor(w.net, token, w.addr, w.node.contractBackend()) 4338 if err != nil { 4339 return fmt.Errorf("error constructing token %s contractor version %d: %w", token.Name, ver, err) 4340 } 4341 4342 if netToken.Address != c.tokenAddress() { 4343 return fmt.Errorf("wrong %s token address. expected %s, got %s", token.Name, netToken.Address, c.tokenAddress()) 4344 } 4345 4346 w.contractors[ver] = c 4347 } 4348 return nil 4349 } 4350 4351 // withContractor runs the provided function with the versioned contractor. 4352 func (w *assetWallet) withContractor(contractVer uint32, f func(contractor) error) error { 4353 if contractVer == contractVersionNewest { 4354 var bestVer uint32 4355 var bestContractor contractor 4356 for ver, c := range w.contractors { 4357 if ver >= bestVer { 4358 bestContractor = c 4359 bestVer = ver 4360 } 4361 } 4362 return f(bestContractor) 4363 } 4364 contractor, found := w.contractors[contractVer] 4365 if !found { 4366 return fmt.Errorf("no version %d contractor for asset %d", contractVer, w.assetID) 4367 } 4368 return f(contractor) 4369 } 4370 4371 // withTokenContractor runs the provided function with the tokenContractor. 4372 func (w *assetWallet) withTokenContractor(assetID, ver uint32, f func(tokenContractor) error) error { 4373 return w.withContractor(ver, func(c contractor) error { 4374 tc, is := c.(tokenContractor) 4375 if !is { 4376 return fmt.Errorf("contractor for %d %T is not a tokenContractor", assetID, c) 4377 } 4378 return f(tc) 4379 }) 4380 } 4381 4382 // estimateApproveGas estimates the gas required for a transaction approving a 4383 // spender for an ERC20 contract. 4384 func (w *assetWallet) estimateApproveGas(newGas *big.Int) (gas uint64, err error) { 4385 return gas, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { 4386 gas, err = c.estimateApproveGas(w.ctx, newGas) 4387 return err 4388 }) 4389 } 4390 4391 // estimateTransferGas estimates the gas needed for a token transfer call to an 4392 // ERC20 contract. 4393 func (w *assetWallet) estimateTransferGas(val uint64) (gas uint64, err error) { 4394 return gas, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { 4395 gas, err = c.estimateTransferGas(w.ctx, w.evmify(val)) 4396 return err 4397 }) 4398 } 4399 4400 // Can uncomment here and in redeem to test rejected redemption reauthorization. 4401 // var firstRedemptionBorked atomic.Bool 4402 4403 // Uncomment here and below to test core's handling of lost redemption txs. 4404 // var firstRedemptionLost atomic.Bool 4405 4406 // redeem redeems a swap contract. Any on-chain failure, such as this secret not 4407 // matching the hash, will not cause this to error. 4408 func (w *assetWallet) redeem( 4409 ctx context.Context, 4410 redemptions []*asset.Redemption, 4411 maxFeeRate uint64, 4412 tipRate *big.Int, 4413 gasLimit uint64, 4414 contractVer uint32, 4415 ) (tx *types.Transaction, err error) { 4416 4417 // // Uncomment here and above to test core's handling of ErrTxLost from 4418 // if firstRedemptionLost.CompareAndSwap(false, true) { 4419 // fmt.Printf("\n##### Spoofing tx for lost tx testing\n\n") 4420 // return types.NewTransaction(10, w.addr, big.NewInt(dexeth.GweiFactor), gasLimit, dexeth.GweiToWei(maxFeeRate), nil), nil 4421 // } 4422 4423 return tx, w.withNonce(ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) { 4424 var amt uint64 4425 for _, r := range redemptions { 4426 amt += r.Spends.Coin.Value() 4427 } 4428 // Uncomment here and above to test rejected redemption handling. 4429 // if firstRedemptionBorked.CompareAndSwap(false, true) { 4430 // fmt.Printf("\n##### Borking gas limit for rejected tx testing\n\n") 4431 // gasLimit /= 4 4432 // } 4433 4434 txOpts, err := w.node.txOpts(ctx, 0, gasLimit, dexeth.GweiToWei(maxFeeRate), tipRate, nonce) 4435 if err != nil { 4436 return nil, 0, 0, nil, err 4437 } 4438 return tx, asset.Redeem, amt, nil, w.withContractor(contractVer, func(c contractor) error { 4439 tx, err = c.redeem(txOpts, redemptions) 4440 return err 4441 }) 4442 }) 4443 } 4444 4445 // refund refunds a swap contract using the account controlled by the wallet. 4446 // Any on-chain failure, such as the locktime not being past, will not cause 4447 // this to error. 4448 func (w *assetWallet) refund(secretHash [32]byte, amt uint64, maxFeeRate, tipRate *big.Int, contractVer uint32) (tx *types.Transaction, err error) { 4449 gas := w.gases(contractVer) 4450 if gas == nil { 4451 return nil, fmt.Errorf("no gas table for asset %d, version %d", w.assetID, contractVer) 4452 } 4453 return tx, w.withNonce(w.ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) { 4454 txOpts, err := w.node.txOpts(w.ctx, 0, gas.Refund, maxFeeRate, tipRate, nonce) 4455 if err != nil { 4456 return nil, 0, 0, nil, err 4457 } 4458 return tx, asset.Refund, amt, nil, w.withContractor(contractVer, func(c contractor) error { 4459 tx, err = c.refund(txOpts, secretHash) 4460 return err 4461 }) 4462 }) 4463 } 4464 4465 // isRedeemable checks if the swap identified by secretHash is redeemable using 4466 // secret. This must NOT be a contractor call. 4467 func (w *assetWallet) isRedeemable(secretHash [32]byte, secret [32]byte, contractVer uint32) (redeemable bool, err error) { 4468 swap, err := w.swap(w.ctx, secretHash, contractVer) 4469 if err != nil { 4470 return false, err 4471 } 4472 4473 if swap.State != dexeth.SSInitiated { 4474 return false, nil 4475 } 4476 4477 return w.ValidateSecret(secret[:], secretHash[:]), nil 4478 } 4479 4480 func (w *assetWallet) isRefundable(secretHash [32]byte, contractVer uint32) (refundable bool, err error) { 4481 return refundable, w.withContractor(contractVer, func(c contractor) error { 4482 refundable, err = c.isRefundable(secretHash) 4483 return err 4484 }) 4485 } 4486 4487 func checkTxStatus(receipt *types.Receipt, gasLimit uint64) error { 4488 if receipt.Status != types.ReceiptStatusSuccessful { 4489 return fmt.Errorf("transaction status failed") 4490 4491 } 4492 4493 if receipt.GasUsed > gasLimit { 4494 return fmt.Errorf("gas used, %d, appears to have exceeded limit of %d", receipt.GasUsed, gasLimit) 4495 } 4496 4497 return nil 4498 } 4499 4500 // emitTransactionNote sends a TransactionNote to the base asset wallet and 4501 // also the wallet, if applicable. 4502 func (w *baseWallet) emitTransactionNote(tx *asset.WalletTransaction, new bool) { 4503 w.walletsMtx.RLock() 4504 baseWallet, found := w.wallets[w.baseChainID] 4505 var tokenWallet *assetWallet 4506 if tx.TokenID != nil { 4507 tokenWallet = w.wallets[*tx.TokenID] 4508 } 4509 w.walletsMtx.RUnlock() 4510 4511 if found { 4512 baseWallet.emit.TransactionNote(tx, new) 4513 } else { 4514 w.log.Error("emitTransactionNote: base asset wallet not found") 4515 } 4516 if tokenWallet != nil { 4517 tokenWallet.emit.TransactionNote(tx, new) 4518 } 4519 } 4520 4521 func findMissingNonces(confirmedAt, pendingAt *big.Int, pendingTxs []*extendedWalletTx) (ns []uint64) { 4522 pendingTxMap := make(map[uint64]struct{}) 4523 // It's not clear whether all providers will update PendingNonceAt if 4524 // there are gaps. geth doesn't do it on simnet, apparently. We'll use 4525 // our own pendingTxs max nonce as a backup. 4526 nonceHigh := big.NewInt(-1) 4527 for _, pendingTx := range pendingTxs { 4528 if pendingTx.indexed && pendingTx.Nonce.Cmp(nonceHigh) > 0 { 4529 nonceHigh.Set(pendingTx.Nonce) 4530 } 4531 pendingTxMap[pendingTx.Nonce.Uint64()] = struct{}{} 4532 } 4533 nonceHigh.Add(nonceHigh, big.NewInt(1)) 4534 if pendingAt.Cmp(nonceHigh) > 0 { 4535 nonceHigh.Set(pendingAt) 4536 } 4537 for n := confirmedAt.Uint64(); n < nonceHigh.Uint64(); n++ { 4538 if _, found := pendingTxMap[n]; !found { 4539 ns = append(ns, n) 4540 } 4541 } 4542 return 4543 } 4544 4545 func (w *baseWallet) missingNoncesActionID() string { 4546 return fmt.Sprintf("missingNonces_%d", w.baseChainID) 4547 } 4548 4549 // updatePendingTx checks the confirmation status of a transaction. The 4550 // BlockNumber, Fees, and Timestamp fields of the extendedWalletTx are updated 4551 // if the transaction is confirmed, and if the transaction has reached the 4552 // required number of confirmations, it is removed from w.pendingTxs. 4553 // 4554 // w.nonceMtx must be held. 4555 func (w *baseWallet) updatePendingTx(tip uint64, pendingTx *extendedWalletTx) { 4556 if pendingTx.Confirmed && pendingTx.savedToDB { 4557 return 4558 } 4559 waitingOnConfs := pendingTx.BlockNumber > 0 && safeConfs(tip, pendingTx.BlockNumber) < w.finalizeConfs 4560 if waitingOnConfs { 4561 // We're just waiting on confs. Don't check again until we expect to be 4562 // finalized. 4563 return 4564 } 4565 // Only check when the tip has changed. 4566 if pendingTx.lastCheck == tip { 4567 return 4568 } 4569 pendingTx.lastCheck = tip 4570 4571 var updated bool 4572 defer func() { 4573 if updated || !pendingTx.savedToDB { 4574 w.tryStoreDBTx(pendingTx) 4575 w.emitTransactionNote(pendingTx.WalletTransaction, false) 4576 } 4577 }() 4578 4579 receipt, tx, err := w.node.transactionAndReceipt(w.ctx, pendingTx.txHash) 4580 if w.log.Level() == dex.LevelTrace { 4581 w.log.Tracef("Attempted to fetch tx and receipt for %s. receipt = %+v, tx = %+v, err = %+v", pendingTx.txHash, receipt, tx, err) 4582 } 4583 if err != nil { 4584 if errors.Is(err, asset.CoinNotFoundError) { 4585 pendingTx.indexed = tx != nil 4586 // transactionAndReceipt will return a CoinNotFound for either no 4587 // reciept or no tx. If they report the tx, we'll consider the tx 4588 // to be "indexed", and won't send lost tx action-required requests. 4589 if pendingTx.BlockNumber > 0 { 4590 w.log.Warnf("Transaction %s was previously mined but now cannot be confirmed. Might be normal.", pendingTx.txHash) 4591 pendingTx.BlockNumber = 0 4592 pendingTx.Timestamp = 0 4593 updated = true 4594 } 4595 } else { 4596 w.log.Errorf("Error getting confirmations for pending tx %s: %v", pendingTx.txHash, err) 4597 } 4598 return 4599 } 4600 4601 pendingTx.Receipt = receipt 4602 pendingTx.indexed = true 4603 pendingTx.Rejected = receipt.Status != types.ReceiptStatusSuccessful 4604 updated = true 4605 4606 if receipt.BlockNumber == nil || receipt.BlockNumber.Cmp(new(big.Int)) == 0 { 4607 if pendingTx.BlockNumber > 0 { 4608 w.log.Warnf("Transaction %s was previously mined but is now unconfirmed", pendingTx.txHash) 4609 pendingTx.Timestamp = 0 4610 pendingTx.BlockNumber = 0 4611 } 4612 return 4613 } 4614 effectiveGasPrice := receipt.EffectiveGasPrice 4615 // NOTE: Would be nice if the receipt contained the block time so we could 4616 // set the timestamp without having to fetch the header. We could use 4617 // SubmissionTime, I guess. Less accurate, but probably not by much. 4618 // NOTE: I don't really know why effectiveGasPrice would be nil, but the 4619 // code I'm replacing got it from the header, so I'm gonna add this check 4620 // just in case. 4621 if pendingTx.Timestamp == 0 || effectiveGasPrice == nil { 4622 hdr, err := w.node.headerByHash(w.ctx, receipt.BlockHash) 4623 if err != nil { 4624 w.log.Errorf("Error getting header for hash %v: %v", receipt.BlockHash, err) 4625 return 4626 } 4627 if hdr == nil { 4628 w.log.Errorf("Header for hash %v is nil", receipt.BlockHash) 4629 return 4630 } 4631 pendingTx.Timestamp = hdr.Time 4632 if effectiveGasPrice == nil { 4633 effectiveGasPrice = new(big.Int).Add(hdr.BaseFee, tx.EffectiveGasTipValue(hdr.BaseFee)) 4634 } 4635 } 4636 4637 bigFees := new(big.Int).Mul(effectiveGasPrice, big.NewInt(int64(receipt.GasUsed))) 4638 pendingTx.Fees = dexeth.WeiToGweiCeil(bigFees) 4639 pendingTx.BlockNumber = receipt.BlockNumber.Uint64() 4640 pendingTx.Confirmed = safeConfs(tip, pendingTx.BlockNumber) >= w.finalizeConfs 4641 } 4642 4643 // checkPendingTxs checks the confirmation status of all pending transactions. 4644 func (w *baseWallet) checkPendingTxs() { 4645 tip := w.tipHeight() 4646 4647 w.nonceMtx.Lock() 4648 defer w.nonceMtx.Unlock() 4649 4650 // If we have pending txs, trace log the before and after. 4651 if w.log.Level() == dex.LevelTrace { 4652 if nPending := len(w.pendingTxs); nPending > 0 { 4653 defer func() { 4654 w.log.Tracef("Checked %d pending txs. Finalized %d", nPending, nPending-len(w.pendingTxs)) 4655 }() 4656 } 4657 4658 } 4659 4660 // keepFromIndex will be the index of the first un-finalized tx. 4661 // lastConfirmed, will be the index of the last confirmed tx. All txs with 4662 // nonces lower that lastConfirmed should also be confirmed, or else 4663 // something isn't right and we may need to request user input. 4664 var keepFromIndex int 4665 var lastConfirmed int = -1 4666 for i, pendingTx := range w.pendingTxs { 4667 if w.ctx.Err() != nil { 4668 return 4669 } 4670 w.updatePendingTx(tip, pendingTx) 4671 if pendingTx.Confirmed { 4672 lastConfirmed = i 4673 if pendingTx.Nonce.Cmp(w.confirmedNonceAt) == 0 { 4674 w.confirmedNonceAt.Add(w.confirmedNonceAt, big.NewInt(1)) 4675 } 4676 if pendingTx.savedToDB { 4677 if i == keepFromIndex { 4678 // This is what we expect. No tx should be confirmed before a 4679 // tx with a lower nonce. We'll delete at least up to this 4680 // one. 4681 keepFromIndex = i + 1 4682 } 4683 } 4684 // This transaction is finalized. If we had previously sought action 4685 // on it, cancel that request. 4686 if pendingTx.actionRequested { 4687 pendingTx.actionRequested = false 4688 w.resolveAction(pendingTx.ID, pendingTx.TokenID) 4689 } 4690 } 4691 } 4692 4693 // If we have missing nonces, send an alert. 4694 if !w.recoveryRequestSent && len(findMissingNonces(w.confirmedNonceAt, w.pendingNonceAt, w.pendingTxs)) != 0 { 4695 w.recoveryRequestSent = true 4696 w.requestAction(actionTypeMissingNonces, w.missingNoncesActionID(), nil, nil) 4697 } 4698 4699 // Loop again, classifying problems and sending action requests. 4700 for i, pendingTx := range w.pendingTxs { 4701 if pendingTx.Confirmed || pendingTx.BlockNumber > 0 { 4702 continue 4703 } 4704 if time.Since(pendingTx.actionIgnored) < txAgeOut { 4705 // They asked us to keep waiting. 4706 continue 4707 } 4708 age := pendingTx.age() 4709 // i < lastConfirmed means unconfirmed nonce < a confirmed nonce. 4710 if (i < lastConfirmed) || 4711 w.confirmedNonceAt.Cmp(pendingTx.Nonce) > 0 || 4712 (age >= txAgeOut && pendingTx.Receipt == nil && !pendingTx.indexed) { 4713 4714 // The tx in our records wasn't accepted. Where's the right one? 4715 req := newLostNonceNote(*pendingTx.WalletTransaction, pendingTx.Nonce.Uint64()) 4716 pendingTx.actionRequested = true 4717 w.requestAction(actionTypeLostNonce, pendingTx.ID, req, pendingTx.TokenID) 4718 continue 4719 } 4720 // Recheck fees periodically. 4721 const feeCheckInterval = time.Minute * 5 4722 if time.Since(pendingTx.lastFeeCheck) < feeCheckInterval { 4723 continue 4724 } 4725 pendingTx.lastFeeCheck = time.Now() 4726 tx, err := pendingTx.tx() 4727 if err != nil { 4728 w.log.Errorf("Error decoding raw tx %s for fee check: %v", pendingTx.ID, err) 4729 continue 4730 } 4731 txCap := tx.GasFeeCap() 4732 baseRate, tipRate, err := w.currentNetworkFees(w.ctx) 4733 if err != nil { 4734 w.log.Errorf("Error getting base fee: %w", err) 4735 continue 4736 } 4737 if txCap.Cmp(baseRate) < 0 { 4738 maxFees := new(big.Int).Add(tipRate, new(big.Int).Mul(baseRate, big.NewInt(2))) 4739 maxFees.Mul(maxFees, new(big.Int).SetUint64(tx.Gas())) 4740 req := newLowFeeNote(*pendingTx.WalletTransaction, dexeth.WeiToGweiCeil(maxFees)) 4741 pendingTx.actionRequested = true 4742 w.requestAction(actionTypeTooCheap, pendingTx.ID, req, pendingTx.TokenID) 4743 continue 4744 } 4745 // Fees look good and there's no reason to believe this tx will 4746 // not be mined. Do we do anything? 4747 // actionRequired(actionTypeStuckTx, pendingTx) 4748 } 4749 4750 // Delete finalized txs from local tracking. 4751 w.pendingTxs = w.pendingTxs[keepFromIndex:] 4752 4753 // Re-broadcast any txs that are not indexed and haven't been re-broadcast 4754 // in a while, logging any errors as warnings. 4755 const rebroadcastPeriod = time.Minute * 5 4756 for _, pendingTx := range w.pendingTxs { 4757 if pendingTx.Confirmed || pendingTx.BlockNumber > 0 || 4758 pendingTx.actionRequested || // Waiting on action 4759 pendingTx.indexed || // Provider knows about it 4760 time.Since(pendingTx.lastBroadcast) < rebroadcastPeriod { 4761 4762 continue 4763 } 4764 pendingTx.lastBroadcast = time.Now() 4765 tx, err := pendingTx.tx() 4766 if err != nil { 4767 w.log.Errorf("Error decoding raw tx %s for rebroadcast: %v", pendingTx.ID, err) 4768 continue 4769 } 4770 if err := w.node.sendSignedTransaction(w.ctx, tx, allowAlreadyKnownFilter); err != nil { 4771 w.log.Warnf("Error rebroadcasting tx %s: %v", pendingTx.ID, err) 4772 } else { 4773 w.log.Infof("Rebroadcasted un-indexed transaction %s", pendingTx.ID) 4774 } 4775 } 4776 } 4777 4778 // Required Actions: Extraordinary conditions that might require user input. 4779 4780 var _ asset.ActionTaker = (*assetWallet)(nil) 4781 4782 const ( 4783 actionTypeMissingNonces = "missingNonces" 4784 actionTypeLostNonce = "lostNonce" 4785 actionTypeTooCheap = "tooCheap" 4786 ) 4787 4788 // TransactionActionNote is used to request user action on transactions in 4789 // abnormal states. 4790 type TransactionActionNote struct { 4791 Tx *asset.WalletTransaction `json:"tx"` 4792 Nonce uint64 `json:"nonce,omitempty"` 4793 NewFees uint64 `json:"newFees,omitempty"` 4794 } 4795 4796 // newLostNonceNote is information regarding a tx that appears to be lost. 4797 func newLostNonceNote(tx asset.WalletTransaction, nonce uint64) *TransactionActionNote { 4798 return &TransactionActionNote{ 4799 Tx: &tx, 4800 Nonce: nonce, 4801 } 4802 } 4803 4804 // newLowFeeNote is data about a tx that is stuck in mempool with too-low fees. 4805 func newLowFeeNote(tx asset.WalletTransaction, newFees uint64) *TransactionActionNote { 4806 return &TransactionActionNote{ 4807 Tx: &tx, 4808 NewFees: newFees, 4809 } 4810 } 4811 4812 // parse the pending tx and index from the slice. 4813 func pendingTxWithID(txID string, pendingTxs []*extendedWalletTx) (int, *extendedWalletTx) { 4814 for i, pendingTx := range pendingTxs { 4815 if pendingTx.ID == txID { 4816 return i, pendingTx 4817 } 4818 } 4819 return 0, nil 4820 } 4821 4822 // amendPendingTx is called with a function that intends to modify a pendingTx 4823 // under mutex lock. 4824 func (w *assetWallet) amendPendingTx(txID string, f func(common.Hash, *types.Transaction, *extendedWalletTx, int) error) error { 4825 txHash := common.HexToHash(txID) 4826 if txHash == (common.Hash{}) { 4827 return fmt.Errorf("invalid tx ID %q", txID) 4828 } 4829 w.nonceMtx.Lock() 4830 defer w.nonceMtx.Unlock() 4831 idx, pendingTx := pendingTxWithID(txID, w.pendingTxs) 4832 if pendingTx == nil { 4833 // Nothing to do anymore. 4834 return nil 4835 } 4836 tx, err := pendingTx.tx() 4837 if err != nil { 4838 return fmt.Errorf("error decoding transaction: %w", err) 4839 } 4840 if err := f(txHash, tx, pendingTx, idx); err != nil { 4841 return err 4842 } 4843 w.emit.ActionResolved(txID) 4844 pendingTx.actionRequested = false 4845 return nil 4846 } 4847 4848 // userActionBumpFees is a request by a user to resolve a actionTypeTooCheap 4849 // condition. 4850 func (w *assetWallet) userActionBumpFees(actionB []byte) error { 4851 var action struct { 4852 TxID string `json:"txID"` 4853 Bump *bool `json:"bump"` 4854 } 4855 if err := json.Unmarshal(actionB, &action); err != nil { 4856 return fmt.Errorf("error unmarshaling bump action: %v", err) 4857 } 4858 if action.Bump == nil { 4859 return errors.New("no bump value specified") 4860 } 4861 return w.amendPendingTx(action.TxID, func(txHash common.Hash, tx *types.Transaction, pendingTx *extendedWalletTx, idx int) error { 4862 if !*action.Bump { 4863 pendingTx.actionIgnored = time.Now() 4864 return nil 4865 } 4866 4867 nonce := new(big.Int).SetUint64(tx.Nonce()) 4868 maxFeeRate, tipCap, err := w.recommendedMaxFeeRate(w.ctx) 4869 if err != nil { 4870 return fmt.Errorf("error getting new fee rate: %w", err) 4871 } 4872 txOpts, err := w.node.txOpts(w.ctx, 0 /* set below */, tx.Gas(), maxFeeRate, tipCap, nonce) 4873 if err != nil { 4874 return fmt.Errorf("error preparing tx opts: %w", err) 4875 } 4876 txOpts.Value = tx.Value() 4877 addr := tx.To() 4878 if addr == nil { 4879 return errors.New("pending tx has no recipient?") 4880 } 4881 4882 newTx, err := w.node.sendTransaction(w.ctx, txOpts, *addr, tx.Data()) 4883 if err != nil { 4884 return fmt.Errorf("error sending bumped-fee transaction: %w", err) 4885 } 4886 4887 newPendingTx := w.extendedTx(newTx, pendingTx.Type, pendingTx.Amount, pendingTx.Recipient) 4888 4889 pendingTx.NonceReplacement = newPendingTx.ID 4890 pendingTx.FeeReplacement = true 4891 4892 w.tryStoreDBTx(pendingTx) 4893 w.tryStoreDBTx(newPendingTx) 4894 4895 w.pendingTxs[idx] = newPendingTx 4896 return nil 4897 }) 4898 } 4899 4900 // tryStoreDBTx attempts to store the DB tx and logs errors internally. This 4901 // method sets the savedToDB flag, so if this tx is in pendingTxs, the nonceMtx 4902 // must be held for reading. 4903 func (w *baseWallet) tryStoreDBTx(wt *extendedWalletTx) { 4904 err := w.txDB.storeTx(wt) 4905 if err != nil { 4906 w.log.Errorf("Error storing tx %s to DB: %v", wt.txHash, err) 4907 } 4908 wt.savedToDB = err == nil 4909 } 4910 4911 // userActionNonceReplacement is a request by a user to resolve a 4912 // actionTypeLostNonce condition. 4913 func (w *assetWallet) userActionNonceReplacement(actionB []byte) error { 4914 var action struct { 4915 TxID string `json:"txID"` 4916 Abandon *bool `json:"abandon"` 4917 ReplacementID string `json:"replacementID"` 4918 } 4919 if err := json.Unmarshal(actionB, &action); err != nil { 4920 return fmt.Errorf("error unmarshaling user action: %v", err) 4921 } 4922 if action.Abandon == nil { 4923 return fmt.Errorf("no abandon value provided for user action for tx %s", action.TxID) 4924 } 4925 abandon := *action.Abandon 4926 if !abandon && action.ReplacementID == "" { // keep waiting 4927 return w.amendPendingTx(action.TxID, func(_ common.Hash, _ *types.Transaction, pendingTx *extendedWalletTx, idx int) error { 4928 pendingTx.actionIgnored = time.Now() 4929 return nil 4930 }) 4931 } 4932 if abandon { // abandon 4933 return w.amendPendingTx(action.TxID, func(txHash common.Hash, _ *types.Transaction, wt *extendedWalletTx, idx int) error { 4934 w.log.Infof("Abandoning transaction %s via user action", txHash) 4935 wt.AssumedLost = true 4936 w.tryStoreDBTx(wt) 4937 copy(w.pendingTxs[idx:], w.pendingTxs[idx+1:]) 4938 w.pendingTxs = w.pendingTxs[:len(w.pendingTxs)-1] 4939 return nil 4940 }) 4941 } 4942 4943 replacementHash := common.HexToHash(action.ReplacementID) 4944 replacementTx, _, err := w.node.getTransaction(w.ctx, replacementHash) 4945 if err != nil { 4946 return fmt.Errorf("error fetching nonce replacement tx: %v", err) 4947 } 4948 4949 from, err := types.LatestSigner(w.node.chainConfig()).Sender(replacementTx) 4950 if err != nil { 4951 return fmt.Errorf("error parsing originator address from specified replacement tx %s: %w", from, err) 4952 } 4953 if from != w.addr { 4954 return fmt.Errorf("specified replacement tx originator %s is not you", from) 4955 } 4956 return w.amendPendingTx(action.TxID, func(txHash common.Hash, oldTx *types.Transaction, pendingTx *extendedWalletTx, idx int) error { 4957 if replacementTx.Nonce() != pendingTx.Nonce.Uint64() { 4958 return fmt.Errorf("nonce replacement doesn't have the right nonce. %d != %s", replacementTx.Nonce(), pendingTx.Nonce) 4959 } 4960 recipient := w.addr.Hex() 4961 newPendingTx := w.extendedTx(replacementTx, asset.Unknown, 0, &recipient) 4962 pendingTx.NonceReplacement = newPendingTx.ID 4963 var oldTo, newTo common.Address 4964 if oldAddr := oldTx.To(); oldAddr != nil { 4965 oldTo = *oldAddr 4966 } 4967 if newAddr := replacementTx.To(); newAddr != nil { 4968 newTo = *newAddr 4969 } 4970 if bytes.Equal(oldTx.Data(), replacementTx.Data()) && oldTo == newTo { 4971 pendingTx.FeeReplacement = true 4972 } 4973 w.tryStoreDBTx(pendingTx) 4974 w.tryStoreDBTx(newPendingTx) 4975 w.pendingTxs[idx] = newPendingTx 4976 return nil 4977 }) 4978 } 4979 4980 // userActionRecoverNonces, if recover is true, examines our confirmed and 4981 // pending nonces and our pendingTx set and sends zero-value txs to ourselves 4982 // to fill any gaps or replace any rogue transactions. This should never happen 4983 // if we're only running one instance of this wallet. 4984 func (w *assetWallet) userActionRecoverNonces(actionB []byte) error { 4985 var action struct { 4986 Recover *bool `json:"recover"` 4987 } 4988 if err := json.Unmarshal(actionB, &action); err != nil { 4989 return fmt.Errorf("error unmarshaling user action: %v", err) 4990 } 4991 if action.Recover == nil { 4992 return errors.New("no recover value specified") 4993 } 4994 if !*action.Recover { 4995 // Don't reset recoveryRequestSent. They won't see this message again until 4996 // they reboot. 4997 return nil 4998 } 4999 maxFeeRate, tipRate, err := w.recommendedMaxFeeRate(w.ctx) 5000 if err != nil { 5001 return fmt.Errorf("error getting max fee rate for nonce resolution: %v", err) 5002 } 5003 w.nonceMtx.Lock() 5004 defer w.nonceMtx.Unlock() 5005 missingNonces := findMissingNonces(w.confirmedNonceAt, w.pendingNonceAt, w.pendingTxs) 5006 if len(missingNonces) == 0 { 5007 return nil 5008 } 5009 for i, n := range missingNonces { 5010 nonce := new(big.Int).SetUint64(n) 5011 txOpts, err := w.node.txOpts(w.ctx, 0, defaultSendGasLimit, maxFeeRate, tipRate, nonce) 5012 if err != nil { 5013 return fmt.Errorf("error getting tx opts for nonce resolution: %v", err) 5014 } 5015 var skip bool 5016 tx, err := w.node.sendTransaction(w.ctx, txOpts, w.addr, nil, func(err error) (discard, propagate, fail bool) { 5017 if errorFilter(err, "replacement transaction underpriced") { 5018 skip = true 5019 return true, false, false 5020 } 5021 return false, false, true 5022 }) 5023 if err != nil { 5024 return fmt.Errorf("error sending tx %d for nonce resolution: %v", nonce, err) 5025 } 5026 if skip { 5027 w.log.Warnf("skipping storing underpriced replacement tx for nonce %d", nonce) 5028 } else { 5029 recipient := w.addr.Hex() 5030 pendingTx := w.extendAndStoreTx(tx, asset.SelfSend, 0, nil, &recipient) 5031 w.emitTransactionNote(pendingTx.WalletTransaction, true) 5032 w.pendingTxs = append(w.pendingTxs, pendingTx) 5033 sort.Slice(w.pendingTxs, func(i, j int) bool { 5034 return w.pendingTxs[i].Nonce.Cmp(w.pendingTxs[j].Nonce) < 0 5035 }) 5036 } 5037 if i < len(missingNonces)-1 { 5038 select { 5039 case <-time.After(time.Second * 1): 5040 case <-w.ctx.Done(): 5041 return nil 5042 } 5043 } 5044 } 5045 w.emit.ActionResolved(w.missingNoncesActionID()) 5046 return nil 5047 } 5048 5049 // requestAction sends a ActionRequired notification up the chain of command. 5050 // nonceMtx must be locked. 5051 func (w *baseWallet) requestAction(actionID, uniqueID string, req *TransactionActionNote, tokenID *uint32) { 5052 assetID := w.baseChainID 5053 if tokenID != nil { 5054 assetID = *tokenID 5055 } 5056 aw := w.wallet(assetID) 5057 if aw == nil { // sanity 5058 return 5059 } 5060 aw.emit.ActionRequired(uniqueID, actionID, req) 5061 } 5062 5063 // resolveAction sends a ActionResolved notification up the chain of command. 5064 // nonceMtx must be locked. 5065 func (w *baseWallet) resolveAction(uniqueID string, tokenID *uint32) { 5066 assetID := w.baseChainID 5067 if tokenID != nil { 5068 assetID = *tokenID 5069 } 5070 aw := w.wallet(assetID) 5071 if aw == nil { // sanity 5072 return 5073 } 5074 aw.emit.ActionResolved(uniqueID) 5075 } 5076 5077 // TakeAction satisfies asset.ActionTaker. This handles responses from the 5078 // user for an ActionRequired request, usually for a stuck tx or otherwise 5079 // abnormal condition. 5080 func (w *assetWallet) TakeAction(actionID string, actionB []byte) error { 5081 switch actionID { 5082 case actionTypeTooCheap: 5083 return w.userActionBumpFees(actionB) 5084 case actionTypeMissingNonces: 5085 return w.userActionRecoverNonces(actionB) 5086 case actionTypeLostNonce: 5087 return w.userActionNonceReplacement(actionB) 5088 default: 5089 return fmt.Errorf("unknown action %q", actionID) 5090 } 5091 } 5092 5093 const txHistoryNonceKey = "Nonce" 5094 5095 // transactionFeeLimit calculates the maximum tx fees that are allowed by a tx. 5096 func transactionFeeLimit(tx *types.Transaction) *big.Int { 5097 fees := new(big.Int) 5098 feeCap, tipCap := tx.GasFeeCap(), tx.GasTipCap() 5099 if feeCap != nil && tipCap != nil { 5100 fees.Add(fees, feeCap) 5101 fees.Add(fees, tipCap) 5102 fees.Mul(fees, new(big.Int).SetUint64(tx.Gas())) 5103 } 5104 return fees 5105 } 5106 5107 // extendedTx generates an *extendedWalletTx for a newly-broadcasted tx and 5108 // stores it in the DB. 5109 func (w *assetWallet) extendedTx(tx *types.Transaction, txType asset.TransactionType, amt uint64, recipient *string) *extendedWalletTx { 5110 var tokenAssetID *uint32 5111 if w.assetID != w.baseChainID { 5112 tokenAssetID = &w.assetID 5113 } 5114 return w.baseWallet.extendAndStoreTx(tx, txType, amt, tokenAssetID, recipient) 5115 } 5116 5117 func (w *baseWallet) extendAndStoreTx(tx *types.Transaction, txType asset.TransactionType, amt uint64, tokenAssetID *uint32, recipient *string) *extendedWalletTx { 5118 nonce := tx.Nonce() 5119 rawTx, err := tx.MarshalBinary() 5120 if err != nil { 5121 w.log.Errorf("Error marshaling tx %q: %v", tx.Hash(), err) 5122 } 5123 5124 if recipient == nil { 5125 if to := tx.To(); to != nil { 5126 s := to.String() 5127 recipient = &s 5128 } 5129 } 5130 5131 now := time.Now() 5132 5133 wt := &extendedWalletTx{ 5134 WalletTransaction: &asset.WalletTransaction{ 5135 Type: txType, 5136 ID: tx.Hash().String(), 5137 Amount: amt, 5138 Fees: dexeth.WeiToGweiCeil(transactionFeeLimit(tx)), // updated later 5139 TokenID: tokenAssetID, 5140 Recipient: recipient, 5141 AdditionalData: map[string]string{ 5142 txHistoryNonceKey: strconv.FormatUint(nonce, 10), 5143 }, 5144 }, 5145 SubmissionTime: uint64(now.Unix()), 5146 RawTx: rawTx, 5147 Nonce: big.NewInt(int64(nonce)), 5148 txHash: tx.Hash(), 5149 savedToDB: true, 5150 lastBroadcast: now, 5151 lastFeeCheck: now, 5152 } 5153 5154 w.tryStoreDBTx(wt) 5155 5156 return wt 5157 } 5158 5159 // TxHistory returns all the transactions the wallet has made. This 5160 // includes the ETH wallet and all token wallets. If refID is nil, then 5161 // transactions starting from the most recent are returned (past is ignored). 5162 // If past is true, the transactions prior to the refID are returned, otherwise 5163 // the transactions after the refID are returned. n is the number of 5164 // transactions to return. If n is <= 0, all the transactions will be returned. 5165 func (w *ETHWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { 5166 return w.txHistory(n, refID, past, nil) 5167 } 5168 5169 // TxHistory returns all the transactions the token wallet has made. If refID 5170 // is nil, then transactions starting from the most recent are returned (past 5171 // is ignored). If past is true, the transactions prior to the refID are 5172 // returned, otherwise the transactions after the refID are returned. n is the 5173 // number of transactions to return. If n is <= 0, all the transactions will be 5174 // returned. 5175 func (w *TokenWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { 5176 return w.txHistory(n, refID, past, &w.assetID) 5177 } 5178 5179 func (w *baseWallet) txHistory(n int, refID *string, past bool, assetID *uint32) ([]*asset.WalletTransaction, error) { 5180 var hashID *common.Hash 5181 if refID != nil { 5182 h := common.HexToHash(*refID) 5183 if h == (common.Hash{}) { 5184 return nil, fmt.Errorf("invalid reference ID %q provided", *refID) 5185 } 5186 hashID = &h 5187 } 5188 return w.txDB.getTxs(n, hashID, past, assetID) 5189 } 5190 5191 func (w *ETHWallet) getReceivingTransaction(ctx context.Context, txHash common.Hash) (*asset.WalletTransaction, error) { 5192 tx, blockHeight, err := w.node.getTransaction(ctx, txHash) 5193 if err != nil { 5194 return nil, err 5195 } 5196 5197 if *tx.To() != w.addr { 5198 return nil, asset.CoinNotFoundError 5199 } 5200 5201 addr := w.addr.String() 5202 return &asset.WalletTransaction{ 5203 Type: asset.Receive, 5204 ID: tx.Hash().String(), 5205 Amount: w.atomize(tx.Value()), 5206 Recipient: &addr, 5207 AdditionalData: map[string]string{ 5208 txHistoryNonceKey: strconv.FormatUint(tx.Nonce(), 10), 5209 }, 5210 // For receiving transactions, if the transaction is mined, it is 5211 // confirmed confirmed, because the value received will be part of 5212 // the available balance. 5213 Confirmed: blockHeight > 0, 5214 }, nil 5215 } 5216 5217 // WalletTransaction returns a transaction that either the wallet has made or 5218 // one in which the wallet has received funds. 5219 func (w *ETHWallet) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) { 5220 txHash := common.HexToHash(txID) 5221 var localTx asset.WalletTransaction 5222 if w.withLocalTxRead(txHash, func(wt *extendedWalletTx) { 5223 localTx = *wt.WalletTransaction 5224 }) { 5225 // Do not modify tx in pending tx map. Only modify copy. 5226 // Confirmed should be true, because the value is part of the available 5227 // balance. 5228 if localTx.BlockNumber > 0 { 5229 localTx.Confirmed = true 5230 } 5231 return &localTx, nil 5232 } 5233 return w.getReceivingTransaction(ctx, txHash) 5234 } 5235 5236 // extractValueFromTransferLog checks the Transfer event logs in the 5237 // transaction, finds the log that sends tokens to the wallet's address, 5238 // and returns the value of the transfer. 5239 func (w *TokenWallet) extractValueFromTransferLog(receipt *types.Receipt) (v uint64, err error) { 5240 return v, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { 5241 v, err = c.parseTransfer(receipt) 5242 return err 5243 }) 5244 } 5245 5246 func (w *TokenWallet) getReceivingTransaction(ctx context.Context, txHash common.Hash) (*asset.WalletTransaction, error) { 5247 receipt, _, err := w.node.transactionAndReceipt(ctx, txHash) 5248 if err != nil { 5249 return nil, err 5250 } 5251 5252 blockHeight := receipt.BlockNumber.Int64() 5253 value, err := w.extractValueFromTransferLog(receipt) 5254 if err != nil { 5255 w.log.Errorf("Error extracting value from transfer log: %v", err) 5256 return &asset.WalletTransaction{ 5257 Type: asset.Unknown, 5258 ID: txHash.String(), 5259 BlockNumber: uint64(blockHeight), 5260 Confirmed: blockHeight > 0, 5261 }, nil 5262 } 5263 5264 addr := w.addr.String() 5265 return &asset.WalletTransaction{ 5266 Type: asset.Receive, 5267 ID: txHash.String(), 5268 BlockNumber: uint64(blockHeight), 5269 TokenID: &w.assetID, 5270 Amount: value, 5271 Recipient: &addr, 5272 // For receiving transactions, if the transaction is mined, it is 5273 // confirmed confirmed, because the value received will be part of 5274 // the available balance. 5275 Confirmed: blockHeight > 0, 5276 }, nil 5277 } 5278 5279 func (w *TokenWallet) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) { 5280 txHash := common.HexToHash(txID) 5281 var localTx asset.WalletTransaction 5282 if w.withLocalTxRead(txHash, func(wt *extendedWalletTx) { 5283 localTx = *wt.WalletTransaction 5284 }) { 5285 // Do not modify tx in pending tx map. Only modify copy. 5286 // Confirmed should be true, because the value is part of the available 5287 // balance. 5288 if localTx.BlockNumber > 0 { 5289 localTx.Confirmed = true 5290 } 5291 return &localTx, nil 5292 } 5293 return w.getReceivingTransaction(ctx, txHash) 5294 } 5295 5296 // providersFile reads a file located at ~/dextest/credentials.json. 5297 // The file contains seed and provider information for wallets used for 5298 // getgas, deploy, and nodeclient testing. If simnet providers are not 5299 // specified, getFileCredentials will add the simnet alpha node. 5300 type providersFile struct { 5301 Seed dex.Bytes `json:"seed"` 5302 Providers map[string] /* symbol */ map[string] /* network */ []string `json:"providers"` 5303 } 5304 5305 // getFileCredentials reads the file at path and extracts the seed and the 5306 // provider for the network. 5307 func getFileCredentials(chain, path string, net dex.Network) (seed []byte, providers []string, err error) { 5308 b, err := os.ReadFile(path) 5309 if err != nil { 5310 return nil, nil, fmt.Errorf("error reading credentials file: %v", err) 5311 } 5312 var p providersFile 5313 if err := json.Unmarshal(b, &p); err != nil { 5314 return nil, nil, fmt.Errorf("error parsing credentials file: %v", err) 5315 } 5316 if len(p.Seed) == 0 { 5317 return nil, nil, fmt.Errorf("must provide both seeds in credentials file") 5318 } 5319 seed = p.Seed 5320 for _, uri := range p.Providers[chain][net.String()] { 5321 if !strings.HasPrefix(uri, "#") && !strings.HasPrefix(uri, ";") { 5322 providers = append(providers, uri) 5323 } 5324 } 5325 if len(providers) == 0 { 5326 return nil, nil, fmt.Errorf("no providers in the file at %s for chain %s, network %s", path, chain, net) 5327 } 5328 if net == dex.Simnet && len(providers) == 0 { 5329 u, _ := user.Current() 5330 switch chain { 5331 case "polygon": 5332 providers = []string{filepath.Join(u.HomeDir, "dextest", chain, "alpha", "bor", "bor.ipc")} 5333 default: 5334 providers = []string{filepath.Join(u.HomeDir, "dextest", chain, "alpha", "node", "geth.ipc")} 5335 } 5336 } 5337 return 5338 } 5339 5340 // quickNode constructs a multiRPCClient and a contractor for the specified 5341 // asset. The client is connected and unlocked. 5342 func quickNode(ctx context.Context, walletDir string, contractVer uint32, 5343 seed []byte, providers []string, wParams *GetGasWalletParams, net dex.Network, log dex.Logger) (*multiRPCClient, contractor, error) { 5344 5345 pw := []byte("abc") 5346 chainID := wParams.ChainCfg.ChainID.Int64() 5347 5348 if err := CreateEVMWallet(chainID, &asset.CreateWalletParams{ 5349 Type: walletTypeRPC, 5350 Seed: seed, 5351 Pass: pw, 5352 Settings: map[string]string{providersKey: strings.Join(providers, " ")}, 5353 DataDir: walletDir, 5354 Net: net, 5355 Logger: log, 5356 }, wParams.Compat, false); err != nil { 5357 return nil, nil, fmt.Errorf("error creating initiator wallet: %v", err) 5358 } 5359 5360 cl, err := newMultiRPCClient(walletDir, providers, log, wParams.ChainCfg, 3, net) 5361 if err != nil { 5362 return nil, nil, fmt.Errorf("error opening initiator rpc client: %v", err) 5363 } 5364 5365 if err = cl.connect(ctx); err != nil { 5366 return nil, nil, fmt.Errorf("error connecting: %v", err) 5367 } 5368 5369 success := false 5370 defer func() { 5371 if !success { 5372 cl.shutdown() 5373 } 5374 }() 5375 5376 if err = cl.unlock(string(pw)); err != nil { 5377 return nil, nil, fmt.Errorf("error unlocking initiator node: %v", err) 5378 } 5379 5380 var c contractor 5381 if wParams.Token == nil { 5382 ctor := contractorConstructors[contractVer] 5383 if ctor == nil { 5384 return nil, nil, fmt.Errorf("no contractor constructor for eth contract version %d", contractVer) 5385 } 5386 c, err = ctor(wParams.ContractAddr, cl.address(), cl.contractBackend()) 5387 if err != nil { 5388 return nil, nil, fmt.Errorf("contractor constructor error: %v", err) 5389 } 5390 } else { 5391 ctor := tokenContractorConstructors[contractVer] 5392 if ctor == nil { 5393 return nil, nil, fmt.Errorf("no token contractor constructor for eth contract version %d", contractVer) 5394 } 5395 c, err = ctor(net, wParams.Token, cl.address(), cl.contractBackend()) 5396 if err != nil { 5397 return nil, nil, fmt.Errorf("token contractor constructor error: %v", err) 5398 } 5399 } 5400 success = true 5401 return cl, c, nil 5402 } 5403 5404 // waitForConfirmation waits for the specified transaction to have > 0 5405 // confirmations. 5406 func waitForConfirmation(ctx context.Context, desc string, cl ethFetcher, txHash common.Hash, log dex.Logger) error { 5407 bestHdr, err := cl.bestHeader(ctx) 5408 if err != nil { 5409 return fmt.Errorf("error getting best header: %w", err) 5410 } 5411 ticker := time.NewTicker(stateUpdateTick) 5412 lastReport := time.Now() 5413 defer ticker.Stop() 5414 for { 5415 select { 5416 case <-ticker.C: 5417 hdr, _ := cl.bestHeader(ctx) 5418 if hdr != nil && hdr.Number.Cmp(bestHdr.Number) > 0 { 5419 bestHdr = hdr 5420 confs, err := cl.transactionConfirmations(ctx, txHash) 5421 if err != nil { 5422 if !errors.Is(err, asset.CoinNotFoundError) { 5423 return fmt.Errorf("error getting transaction confirmations: %v", err) 5424 } 5425 continue 5426 } 5427 if confs > 0 { 5428 return nil 5429 } 5430 if time.Since(lastReport) > time.Second*30 { 5431 log.Infof("Awaiting confirmations for %s tx %s", desc, txHash) 5432 lastReport = time.Now() 5433 } 5434 } 5435 5436 case <-ctx.Done(): 5437 return ctx.Err() 5438 } 5439 } 5440 } 5441 5442 // runSimnetMiner starts a gouroutine to generate a simnet block every 5 seconds 5443 // until the ctx is canceled. By default, the eth harness will mine a block 5444 // every 15s. We want to speed it up a bit for e.g. GetGas testing. 5445 func runSimnetMiner(ctx context.Context, symbol string, log dex.Logger) { 5446 log.Infof("Starting the simnet miner") 5447 go func() { 5448 tick := time.NewTicker(time.Second * 5) 5449 u, err := user.Current() 5450 if err != nil { 5451 log.Criticalf("cannot run simnet miner because we couldn't get the current user") 5452 return 5453 } 5454 for { 5455 select { 5456 case <-tick.C: 5457 log.Debugf("Mining a simnet block") 5458 mine := exec.CommandContext(ctx, "./mine-alpha", "1") 5459 mine.Dir = filepath.Join(u.HomeDir, "dextest", symbol, "harness-ctl") 5460 b, err := mine.CombinedOutput() 5461 if err != nil { 5462 log.Errorf("Mining error: %v", err) 5463 log.Errorf("Mining error output: %s", string(b)) 5464 } 5465 case <-ctx.Done(): 5466 return 5467 } 5468 } 5469 }() 5470 } 5471 5472 type getGas byte 5473 5474 // GetGas provides access to the gas estimation utilities. 5475 var GetGas getGas 5476 5477 // GetGasWalletParams are the configuration parameters required to estimate 5478 // swap contract gas usage. 5479 type GetGasWalletParams struct { 5480 ChainCfg *params.ChainConfig 5481 Gas *dexeth.Gases 5482 Token *dexeth.Token 5483 UnitInfo *dex.UnitInfo 5484 BaseUnitInfo *dex.UnitInfo 5485 Compat *CompatibilityData 5486 ContractAddr common.Address // Base chain contract addr. 5487 } 5488 5489 // ReadCredentials reads the credentials for the network from the credentials 5490 // file. 5491 func (getGas) ReadCredentials(chain, credentialsPath string, net dex.Network) (addr string, providers []string, err error) { 5492 seed, providers, err := getFileCredentials(chain, credentialsPath, net) 5493 if err != nil { 5494 return "", nil, err 5495 } 5496 privB, zero, err := privKeyFromSeed(seed) 5497 if err != nil { 5498 return "", nil, err 5499 } 5500 defer zero() 5501 privateKey, err := crypto.ToECDSA(privB) 5502 if err != nil { 5503 return "", nil, err 5504 } 5505 5506 addr = crypto.PubkeyToAddress(privateKey.PublicKey).String() 5507 return 5508 } 5509 5510 func getGetGasClientWithEstimatesAndBalances(ctx context.Context, net dex.Network, contractVer uint32, maxSwaps int, 5511 walletDir string, providers []string, seed []byte, wParams *GetGasWalletParams, log dex.Logger) (cl *multiRPCClient, c contractor, 5512 ethReq, swapReq, feeRate uint64, ethBal, tokenBal *big.Int, err error) { 5513 5514 cl, c, err = quickNode(ctx, walletDir, contractVer, seed, providers, wParams, net, log) 5515 if err != nil { 5516 return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error creating initiator wallet: %v", err) 5517 } 5518 5519 var success bool 5520 defer func() { 5521 if !success { 5522 cl.shutdown() 5523 } 5524 }() 5525 5526 base, tip, err := cl.currentFees(ctx) 5527 if err != nil { 5528 return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("Error estimating fee rate: %v", err) 5529 } 5530 5531 ethBal, err = cl.addressBalance(ctx, cl.address()) 5532 if err != nil { 5533 return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error getting eth balance: %v", err) 5534 } 5535 5536 feeRate = dexeth.WeiToGweiCeil(new(big.Int).Add(tip, new(big.Int).Mul(base, big.NewInt(2)))) 5537 5538 // Check that we have a balance for swaps and fees. 5539 g := wParams.Gas 5540 const swapVal = 1 5541 n := uint64(maxSwaps) 5542 swapReq = n * (n + 1) / 2 * swapVal // Sum of positive integers up to n 5543 m := n - 1 // for n swaps there will be (0 + 1 + 2 + ... + n - 1) AddSwaps 5544 initGas := (g.Swap * n) + g.SwapAdd*(m*(m+1)/2) 5545 redeemGas := (g.Redeem * n) + g.RedeemAdd*(m*(m+1)/2) 5546 5547 fees := (initGas + redeemGas + defaultSendGasLimit) * // fees for participant wallet 5548 2 * // fudge factor 5549 6 / 5 * // base rate increase accommodation 5550 feeRate 5551 5552 isToken := wParams.Token != nil 5553 ethReq = fees + swapReq 5554 if isToken { 5555 tc := c.(tokenContractor) 5556 tokenBal, err = tc.balance(ctx) 5557 if err != nil { 5558 return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error fetching token balance: %v", err) 5559 } 5560 5561 fees += (g.Transfer*2 + g.Approve*2*2 /* two approvals */ + defaultSendGasLimit /* approval client fee funding tx */) * 5562 2 * 6 / 5 * feeRate // Adding 20% in case base rate goes up 5563 ethReq = fees 5564 } 5565 success = true 5566 return 5567 } 5568 5569 func (getGas) chainForAssetID(assetID uint32) string { 5570 ti := asset.TokenInfo(assetID) 5571 if ti == nil { 5572 return dex.BipIDSymbol(assetID) 5573 } 5574 return dex.BipIDSymbol(ti.ParentID) 5575 } 5576 5577 // EstimateFunding estimates how much funding is needed for estimating gas, and 5578 // prints helpful messages for the user. 5579 func (getGas) EstimateFunding(ctx context.Context, net dex.Network, assetID, contractVer uint32, 5580 maxSwaps int, credentialsPath string, wParams *GetGasWalletParams, log dex.Logger) error { 5581 5582 symbol := dex.BipIDSymbol(assetID) 5583 log.Infof("Estimating required funding for up to %d swaps of asset %s, contract version %d on %s", maxSwaps, symbol, contractVer, net) 5584 5585 seed, providers, err := getFileCredentials(GetGas.chainForAssetID(assetID), credentialsPath, net) 5586 if err != nil { 5587 return err 5588 } 5589 5590 walletDir, err := os.MkdirTemp("", "") 5591 if err != nil { 5592 return err 5593 } 5594 defer os.RemoveAll(walletDir) 5595 5596 cl, _, ethReq, swapReq, _, ethBalBig, tokenBalBig, err := getGetGasClientWithEstimatesAndBalances(ctx, net, contractVer, maxSwaps, walletDir, providers, seed, wParams, log) 5597 if err != nil { 5598 return fmt.Errorf("%s: getGetGasClientWithEstimatesAndBalances error: %w", symbol, err) 5599 } 5600 defer cl.shutdown() 5601 ethBal := dexeth.WeiToGwei(ethBalBig) 5602 5603 ui := wParams.UnitInfo 5604 assetFmt := ui.ConventionalString 5605 bui := wParams.BaseUnitInfo 5606 ethFmt := bui.ConventionalString 5607 5608 log.Info("Address:", cl.address()) 5609 log.Infof("%s balance: %s", bui.Conventional.Unit, ethFmt(ethBal)) 5610 5611 isToken := wParams.Token != nil 5612 tokenBalOK := true 5613 if isToken { 5614 log.Infof("%s required for fees: %s", bui.Conventional.Unit, ethFmt(ethReq)) 5615 5616 ui, err := asset.UnitInfo(assetID) 5617 if err != nil { 5618 return fmt.Errorf("error getting unit info for %d: %v", assetID, err) 5619 } 5620 5621 tokenBal := wParams.Token.EVMToAtomic(tokenBalBig) 5622 log.Infof("%s balance: %s", ui.Conventional.Unit, assetFmt(tokenBal)) 5623 log.Infof("%s required for trading: %s", ui.Conventional.Unit, assetFmt(swapReq)) 5624 if tokenBal < swapReq { 5625 tokenBalOK = false 5626 log.Infof("❌ Insufficient %[2]s balance. Deposit %[1]s %[2]s before getting a gas estimate", 5627 assetFmt(swapReq-tokenBal), ui.Conventional.Unit) 5628 } 5629 5630 } else { 5631 log.Infof("%s required: %s (swaps) + %s (fees) = %s", 5632 ui.Conventional.Unit, ethFmt(swapReq), ethFmt(ethReq-swapReq), ethFmt(ethReq)) 5633 } 5634 5635 if ethBal < ethReq { 5636 // Add 10% for fee drift. 5637 ethRecommended := ethReq * 11 / 10 5638 log.Infof("❌ Insufficient %s balance. Deposit about %s %s before getting a gas estimate", 5639 bui.Conventional.Unit, ethFmt(ethRecommended-ethBal), bui.Conventional.Unit) 5640 } else if tokenBalOK { 5641 log.Infof("👍 You have sufficient funding to run a gas estimate") 5642 } 5643 5644 return nil 5645 } 5646 5647 // Return returns the estimation wallet's base-chain or token balance to a 5648 // specified address, if it is more than fees required to send. 5649 func (getGas) Return( 5650 ctx context.Context, 5651 assetID uint32, 5652 credentialsPath, 5653 returnAddr string, 5654 wParams *GetGasWalletParams, 5655 net dex.Network, 5656 log dex.Logger, 5657 ) error { 5658 5659 const contractVer = 0 // Doesn't matter 5660 5661 if !common.IsHexAddress(returnAddr) { 5662 return fmt.Errorf("supplied return address %q is not an Ethereum address", returnAddr) 5663 } 5664 5665 seed, providers, err := getFileCredentials(GetGas.chainForAssetID(assetID), credentialsPath, net) 5666 if err != nil { 5667 return err 5668 } 5669 5670 walletDir, err := os.MkdirTemp("", "") 5671 if err != nil { 5672 return err 5673 } 5674 defer os.RemoveAll(walletDir) 5675 5676 cl, _, err := quickNode(ctx, walletDir, contractVer, seed, providers, wParams, net, log) 5677 if err != nil { 5678 return fmt.Errorf("error creating initiator wallet: %v", err) 5679 } 5680 defer cl.shutdown() 5681 5682 baseRate, tipRate, err := cl.currentFees(ctx) 5683 if err != nil { 5684 return fmt.Errorf("Error estimating fee rate: %v", err) 5685 } 5686 maxFeeRate := new(big.Int).Add(tipRate, new(big.Int).Mul(baseRate, big.NewInt(2))) 5687 5688 return GetGas.returnFunds(ctx, cl, maxFeeRate, tipRate, common.HexToAddress(returnAddr), wParams.Token, wParams.UnitInfo, log, net) 5689 } 5690 5691 func (getGas) returnFunds( 5692 ctx context.Context, 5693 cl *multiRPCClient, 5694 maxFeeRate *big.Int, 5695 tipRate *big.Int, 5696 returnAddr common.Address, 5697 token *dexeth.Token, // nil for base chain 5698 ui *dex.UnitInfo, 5699 log dex.Logger, 5700 net dex.Network, 5701 ) error { 5702 5703 bigEthBal, err := cl.addressBalance(ctx, cl.address()) 5704 if err != nil { 5705 return fmt.Errorf("error getting eth balance: %v", err) 5706 } 5707 ethBal := dexeth.WeiToGwei(bigEthBal) 5708 5709 if token != nil { 5710 nt, found := token.NetTokens[net] 5711 if !found { 5712 return fmt.Errorf("no %s token for %s", token.Name, net) 5713 } 5714 var g dexeth.Gases 5715 for _, sc := range nt.SwapContracts { 5716 g = sc.Gas 5717 break 5718 } 5719 fees := g.Transfer * dexeth.WeiToGweiCeil(maxFeeRate) 5720 if fees > ethBal { 5721 return fmt.Errorf("not enough base chain balance (%s) to cover fees (%s)", 5722 dexeth.UnitInfo.ConventionalString(ethBal), dexeth.UnitInfo.ConventionalString(fees)) 5723 } 5724 5725 tokenContract, err := erc20.NewIERC20(nt.Address, cl.contractBackend()) 5726 if err != nil { 5727 return fmt.Errorf("NewIERC20 error: %v", err) 5728 } 5729 5730 callOpts := &bind.CallOpts{ 5731 From: cl.address(), 5732 Context: ctx, 5733 } 5734 5735 bigTokenBal, err := tokenContract.BalanceOf(callOpts, cl.address()) 5736 if err != nil { 5737 return fmt.Errorf("error getting token balance: %w", err) 5738 } 5739 5740 txOpts, err := cl.txOpts(ctx, 0, g.Transfer, maxFeeRate, tipRate, nil) 5741 if err != nil { 5742 return fmt.Errorf("error generating tx opts: %w", err) 5743 } 5744 5745 tx, err := tokenContract.Transfer(txOpts, returnAddr, bigTokenBal) 5746 if err != nil { 5747 return fmt.Errorf("error transferring tokens : %w", err) 5748 } 5749 log.Infof("Sent %s in transaction %s", ui.ConventionalString(token.EVMToAtomic(bigTokenBal)), tx.Hash()) 5750 return nil 5751 } 5752 5753 bigFees := new(big.Int).Mul(new(big.Int).SetUint64(defaultSendGasLimit), maxFeeRate) 5754 5755 fees := dexeth.WeiToGweiCeil(bigFees) 5756 5757 ethFmt := ui.ConventionalString 5758 if fees >= ethBal { 5759 return fmt.Errorf("balance is lower than projected fees: %s < %s", ethFmt(ethBal), ethFmt(fees)) 5760 } 5761 5762 remainder := ethBal - fees 5763 txOpts, err := cl.txOpts(ctx, remainder, defaultSendGasLimit, maxFeeRate, tipRate, nil) 5764 if err != nil { 5765 return fmt.Errorf("error generating tx opts: %w", err) 5766 } 5767 tx, err := cl.sendTransaction(ctx, txOpts, returnAddr, nil) 5768 if err != nil { 5769 return fmt.Errorf("error sending funds: %w", err) 5770 } 5771 log.Infof("Sent %s in transaction %s", ui.ConventionalString(remainder), tx.Hash()) 5772 return nil 5773 } 5774 5775 // Estimate gets estimates useful for populating dexeth.Gases fields. Initiation 5776 // and redeeem transactions with 1, 2, ... , and n swaps per transaction will be 5777 // sent, for a total of n * (n + 1) / 2 total swaps in 2 * n transactions. If 5778 // this is a token, and additional 1 approval transaction and 1 transfer 5779 // transaction will be sent. The transfer transaction will send 1 atom to a 5780 // random address (with zero token balance), to maximize gas costs. This atom is 5781 // not recoverable. If you run this function with insufficient or zero ETH 5782 // and/or token balance on the seed, the function will error with a message 5783 // indicating the amount of funding needed to run. 5784 func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVer uint32, maxSwaps int, 5785 credentialsPath string, wParams *GetGasWalletParams, log dex.Logger) error { 5786 5787 symbol := dex.BipIDSymbol(assetID) 5788 log.Infof("Getting gas estimates for up to %d swaps of asset %s, contract version %d on %s", maxSwaps, symbol, contractVer, symbol) 5789 5790 isToken := wParams.Token != nil 5791 5792 seed, providers, err := getFileCredentials(GetGas.chainForAssetID(assetID), credentialsPath, net) 5793 if err != nil { 5794 return err 5795 } 5796 5797 walletDir, err := os.MkdirTemp("", "") 5798 if err != nil { 5799 return err 5800 } 5801 defer os.RemoveAll(walletDir) 5802 5803 cl, c, ethReq, swapReq, feeRate, ethBal, tokenBal, err := getGetGasClientWithEstimatesAndBalances(ctx, net, contractVer, maxSwaps, walletDir, providers, seed, wParams, log) 5804 if err != nil { 5805 return fmt.Errorf("%s: getGetGasClientWithEstimatesAndBalances error: %w", symbol, err) 5806 } 5807 defer cl.shutdown() 5808 5809 log.Infof("Initiator address: %s", cl.address()) 5810 5811 ui := wParams.UnitInfo 5812 assetFmt := ui.ConventionalString 5813 bui := wParams.BaseUnitInfo 5814 bUnit := bui.Conventional.Unit 5815 ethFmt := bui.ConventionalString 5816 5817 atomicBal := dexeth.WeiToGwei(ethBal) 5818 log.Infof("%s balance: %s", bUnit, ethFmt(atomicBal)) 5819 if atomicBal < ethReq { 5820 return fmt.Errorf("%s balance insufficient to get gas estimates. current: %[2]s, required ~ %[3]s %[1]s. send %[1]s to %[4]s", 5821 bUnit, ethFmt(atomicBal), ethFmt(ethReq*5/4), cl.address()) 5822 } 5823 5824 // Run the miner now, in case we need it for the approval client preload. 5825 if net == dex.Simnet { 5826 symbolParts := strings.Split(symbol, ".") // e.g. usdc.polygon, usdc.eth 5827 runSimnetMiner(ctx, symbolParts[len(symbolParts)-1], log) 5828 } 5829 5830 var approvalClient *multiRPCClient 5831 var approvalContractor tokenContractor 5832 if isToken { 5833 5834 atomicBal := wParams.Token.EVMToAtomic(tokenBal) 5835 5836 convUnit := ui.Conventional.Unit 5837 log.Infof("%s balance: %s %s", strings.ToUpper(symbol), assetFmt(atomicBal), convUnit) 5838 log.Infof("%d %s required for swaps", swapReq, ui.AtomicUnit) 5839 log.Infof("%d gwei %s required for fees", ethReq, bui.Conventional.Unit) 5840 if atomicBal < swapReq { 5841 return fmt.Errorf("%[3]s balance insufficient to get gas estimates. current: %[1]s, required ~ %[2]s %[3]s. send %[3]s to %[4]s", 5842 assetFmt(atomicBal), assetFmt(swapReq), convUnit, cl.address()) 5843 } 5844 5845 var mrc contractor 5846 approvalClient, mrc, err = quickNode(ctx, filepath.Join(walletDir, "ac_dir"), contractVer, encode.RandomBytes(32), providers, wParams, net, log) 5847 if err != nil { 5848 return fmt.Errorf("error creating approval contract node: %v", err) 5849 } 5850 approvalContractor = mrc.(tokenContractor) 5851 defer approvalClient.shutdown() 5852 5853 // TODO: We're overloading by probably 140% here, but this is what 5854 // we've reserved in our fee checks. Is it worth recovering unused 5855 // balance? 5856 feePreload := wParams.Gas.Approve * 2 * 6 / 5 * feeRate 5857 txOpts, err := cl.txOpts(ctx, feePreload, defaultSendGasLimit, nil, nil, nil) 5858 if err != nil { 5859 return fmt.Errorf("error creating tx opts for sending fees for approval client: %v", err) 5860 } 5861 5862 tx, err := cl.sendTransaction(ctx, txOpts, approvalClient.address(), nil) 5863 if err != nil { 5864 return fmt.Errorf("error sending fee reserves to approval client: %v", err) 5865 } 5866 log.Infof("Funded approval client gas with %s in transaction %s", wParams.UnitInfo.FormatAtoms(feePreload), tx.Hash()) 5867 if err = waitForConfirmation(ctx, "approval client fee funding", approvalClient, tx.Hash(), log); err != nil { 5868 return fmt.Errorf("error waiting for approval fee funding tx: %w", err) 5869 } 5870 5871 } else { 5872 log.Infof("%d gwei %s required for fees and swaps", ethReq, bui.Conventional.Unit) 5873 } 5874 5875 log.Debugf("Getting gas estimates") 5876 return getGasEstimates(ctx, cl, approvalClient, c, approvalContractor, maxSwaps, wParams.Gas, log) 5877 } 5878 5879 // getGasEstimate is used to get a gas table for an asset's contract(s). The 5880 // provided gases, g, should be generous estimates of what the gas might be. 5881 // Errors are thrown if the provided estimates are too small by more than a 5882 // factor of 2. The account should already have a trading balance of at least 5883 // maxSwaps gwei (token or eth), and sufficient eth balance to cover the 5884 // requisite tx fees. 5885 // 5886 // acl (approval client) and ac (approval contractor) should be an ethFetcher 5887 // and tokenContractor for a fresh account. They are used to get the approval 5888 // gas estimate. These are only needed when the asset is a token. For eth, they 5889 // can be nil. 5890 func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac tokenContractor, 5891 maxSwaps int, g *dexeth.Gases, log dex.Logger) (err error) { 5892 5893 tc, isToken := c.(tokenContractor) 5894 5895 stats := struct { 5896 swaps []uint64 5897 redeems []uint64 5898 refunds []uint64 5899 approves []uint64 5900 transfers []uint64 5901 }{} 5902 5903 avg := func(vs []uint64) uint64 { 5904 var sum uint64 5905 for _, v := range vs { 5906 sum += v 5907 } 5908 return sum / uint64(len(vs)) 5909 } 5910 5911 avgDiff := func(vs []uint64) uint64 { 5912 diffs := make([]uint64, 0, len(vs)-1) 5913 for i := 0; i < len(vs)-1; i++ { 5914 diffs = append(diffs, vs[i+1]-vs[i]) 5915 } 5916 return avg(diffs) 5917 } 5918 5919 recommendedGas := func(v uint64) uint64 { 5920 return v * 13 / 10 5921 } 5922 5923 baseRate, tipRate, err := cl.currentFees(ctx) 5924 if err != nil { 5925 return fmt.Errorf("error getting network fees: %v", err) 5926 } 5927 5928 defer func() { 5929 if len(stats.swaps) == 0 { 5930 return 5931 } 5932 firstSwap := stats.swaps[0] 5933 fmt.Printf(" First swap used %d gas Recommended Gases.Swap = %d\n", firstSwap, recommendedGas(firstSwap)) 5934 if len(stats.swaps) > 1 { 5935 swapAdd := avgDiff(stats.swaps) 5936 fmt.Printf(" %d additional swaps averaged %d gas each. Recommended Gases.SwapAdd = %d\n", 5937 len(stats.swaps)-1, swapAdd, recommendedGas(swapAdd)) 5938 fmt.Printf(" %+v \n", stats.swaps) 5939 } 5940 if len(stats.redeems) == 0 { 5941 return 5942 } 5943 firstRedeem := stats.redeems[0] 5944 fmt.Printf(" First redeem used %d gas. Recommended Gases.Redeem = %d\n", firstRedeem, recommendedGas(firstRedeem)) 5945 if len(stats.redeems) > 1 { 5946 redeemAdd := avgDiff(stats.redeems) 5947 fmt.Printf(" %d additional redeems averaged %d gas each. recommended Gases.RedeemAdd = %d\n", 5948 len(stats.redeems)-1, redeemAdd, recommendedGas(redeemAdd)) 5949 fmt.Printf(" %+v \n", stats.redeems) 5950 } 5951 redeemGas := avg(stats.refunds) 5952 fmt.Printf(" Average of %d refunds: %d. Recommended Gases.Refund = %d\n", 5953 len(stats.refunds), redeemGas, recommendedGas(redeemGas)) 5954 fmt.Printf(" %+v \n", stats.refunds) 5955 if !isToken { 5956 return 5957 } 5958 approveGas := avg(stats.approves) 5959 fmt.Printf(" Average of %d approvals: %d. Recommended Gases.Approve = %d\n", 5960 len(stats.approves), approveGas, recommendedGas(approveGas)) 5961 fmt.Printf(" %+v \n", stats.approves) 5962 transferGas := avg(stats.transfers) 5963 fmt.Printf(" Average of %d transfers: %d. Recommended Gases.Transfer = %d\n", 5964 len(stats.transfers), transferGas, recommendedGas(transferGas)) 5965 fmt.Printf(" %+v \n", stats.transfers) 5966 }() 5967 5968 // Estimate approve for tokens. 5969 if isToken { 5970 sendApprove := func(cl ethFetcher, c tokenContractor) error { 5971 txOpts, err := cl.txOpts(ctx, 0, g.Approve*2, baseRate, tipRate, nil) 5972 if err != nil { 5973 return fmt.Errorf("error constructing signed tx opts for approve: %w", err) 5974 } 5975 tx, err := c.approve(txOpts, unlimitedAllowance) 5976 if err != nil { 5977 return fmt.Errorf("error estimating approve gas: %w", err) 5978 } 5979 if err = waitForConfirmation(ctx, "approval", cl, tx.Hash(), log); err != nil { 5980 return fmt.Errorf("error waiting for approve transaction: %w", err) 5981 } 5982 receipt, _, err := cl.transactionAndReceipt(ctx, tx.Hash()) 5983 if err != nil { 5984 return fmt.Errorf("error getting receipt for approve tx: %w", err) 5985 } 5986 if err = checkTxStatus(receipt, g.Approve*2); err != nil { 5987 return fmt.Errorf("approve tx failed status check: [%w]. %d gas used out of %d available", err, receipt.GasUsed, g.Approve*2) 5988 } 5989 5990 log.Infof("%d gas used for approval tx", receipt.GasUsed) 5991 stats.approves = append(stats.approves, receipt.GasUsed) 5992 return nil 5993 } 5994 5995 log.Debugf("Sending approval transaction for random test client") 5996 if err = sendApprove(acl, ac); err != nil { 5997 return fmt.Errorf("error sending approve transaction for the new client: %w", err) 5998 } 5999 6000 log.Debugf("Sending approval transaction for initiator") 6001 if err = sendApprove(cl, tc); err != nil { 6002 return fmt.Errorf("error sending approve transaction for the initiator: %w", err) 6003 } 6004 6005 txOpts, err := cl.txOpts(ctx, 0, g.Transfer*2, baseRate, tipRate, nil) 6006 if err != nil { 6007 return fmt.Errorf("error constructing signed tx opts for transfer: %w", err) 6008 } 6009 log.Debugf("Sending transfer transaction") 6010 // Transfer should be to a random address to maximize costs. This is a 6011 // sacrificial atom. 6012 var randomAddr common.Address 6013 copy(randomAddr[:], encode.RandomBytes(20)) 6014 transferTx, err := tc.transfer(txOpts, randomAddr, big.NewInt(1)) 6015 if err != nil { 6016 return fmt.Errorf("transfer error: %w", err) 6017 } 6018 if err = waitForConfirmation(ctx, "transfer", cl, transferTx.Hash(), log); err != nil { 6019 return fmt.Errorf("error waiting for transfer tx: %w", err) 6020 } 6021 receipt, _, err := cl.transactionAndReceipt(ctx, transferTx.Hash()) 6022 if err != nil { 6023 return fmt.Errorf("error getting tx receipt for transfer tx: %w", err) 6024 } 6025 if err = checkTxStatus(receipt, g.Transfer*2); err != nil { 6026 return fmt.Errorf("transfer tx failed status check: [%w]. %d gas used out of %d available", err, receipt.GasUsed, g.Transfer*2) 6027 } 6028 log.Infof("%d gas used for transfer tx", receipt.GasUsed) 6029 stats.transfers = append(stats.transfers, receipt.GasUsed) 6030 } 6031 6032 for n := 1; n <= maxSwaps; n++ { 6033 contracts := make([]*asset.Contract, 0, n) 6034 secrets := make([][32]byte, 0, n) 6035 for i := 0; i < n; i++ { 6036 secretB := encode.RandomBytes(32) 6037 var secret [32]byte 6038 copy(secret[:], secretB) 6039 secretHash := sha256.Sum256(secretB) 6040 contracts = append(contracts, &asset.Contract{ 6041 Address: cl.address().String(), // trading with self 6042 Value: 1, 6043 SecretHash: secretHash[:], 6044 LockTime: uint64(time.Now().Add(-time.Hour).Unix()), 6045 }) 6046 secrets = append(secrets, secret) 6047 } 6048 6049 var optsVal uint64 6050 if !isToken { 6051 optsVal = uint64(n) 6052 } 6053 6054 // Send the inits 6055 txOpts, err := cl.txOpts(ctx, optsVal, g.SwapN(n)*2, baseRate, tipRate, nil) 6056 if err != nil { 6057 return fmt.Errorf("error constructing signed tx opts for %d swaps: %v", n, err) 6058 } 6059 log.Debugf("Sending %d swaps", n) 6060 tx, err := c.initiate(txOpts, contracts) 6061 if err != nil { 6062 return fmt.Errorf("initiate error for %d swaps: %v", n, err) 6063 } 6064 if err = waitForConfirmation(ctx, "init", cl, tx.Hash(), log); err != nil { 6065 return fmt.Errorf("error waiting for init tx to be mined: %w", err) 6066 } 6067 receipt, _, err := cl.transactionAndReceipt(ctx, tx.Hash()) 6068 if err != nil { 6069 return fmt.Errorf("error getting init tx receipt: %w", err) 6070 } 6071 if err = checkTxStatus(receipt, txOpts.GasLimit); err != nil { 6072 return fmt.Errorf("init tx failed status check: %w", err) 6073 } 6074 log.Infof("%d gas used for %d initiation txs", receipt.GasUsed, n) 6075 stats.swaps = append(stats.swaps, receipt.GasUsed) 6076 6077 // Estimate a refund 6078 var firstSecretHash [32]byte 6079 copy(firstSecretHash[:], contracts[0].SecretHash) 6080 refundGas, err := c.estimateRefundGas(ctx, firstSecretHash) 6081 if err != nil { 6082 return fmt.Errorf("error estimate refund gas: %w", err) 6083 } 6084 log.Infof("%d gas estimated for a refund", refundGas) 6085 stats.refunds = append(stats.refunds, refundGas) 6086 6087 redemptions := make([]*asset.Redemption, 0, n) 6088 for i, contract := range contracts { 6089 redemptions = append(redemptions, &asset.Redemption{ 6090 Spends: &asset.AuditInfo{ 6091 SecretHash: contract.SecretHash, 6092 }, 6093 Secret: secrets[i][:], 6094 }) 6095 } 6096 6097 txOpts, err = cl.txOpts(ctx, 0, g.RedeemN(n)*2, baseRate, tipRate, nil) 6098 if err != nil { 6099 return fmt.Errorf("error constructing signed tx opts for %d redeems: %v", n, err) 6100 } 6101 log.Debugf("Sending %d redemption txs", n) 6102 tx, err = c.redeem(txOpts, redemptions) 6103 if err != nil { 6104 return fmt.Errorf("redeem error for %d swaps: %v", n, err) 6105 } 6106 if err = waitForConfirmation(ctx, "redeem", cl, tx.Hash(), log); err != nil { 6107 return fmt.Errorf("error waiting for redeem tx to be mined: %w", err) 6108 } 6109 receipt, _, err = cl.transactionAndReceipt(ctx, tx.Hash()) 6110 if err != nil { 6111 return fmt.Errorf("error getting redeem tx receipt: %w", err) 6112 } 6113 if err = checkTxStatus(receipt, txOpts.GasLimit); err != nil { 6114 return fmt.Errorf("redeem tx failed status check: %w", err) 6115 } 6116 log.Infof("%d gas used for %d redemptions", receipt.GasUsed, n) 6117 stats.redeems = append(stats.redeems, receipt.GasUsed) 6118 } 6119 6120 return nil 6121 } 6122 6123 // newTxOpts is a constructor for a TransactOpts. 6124 func newTxOpts(ctx context.Context, from common.Address, val, maxGas uint64, maxFeeRate, gasTipCap *big.Int) *bind.TransactOpts { 6125 // We'll enforce dexeth.MinGasTipCap since the server does, but this isn't 6126 // necessarily a constant for all networks or under all conditions. 6127 minGasWei := dexeth.GweiToWei(dexeth.MinGasTipCap) 6128 if gasTipCap.Cmp(minGasWei) < 0 { 6129 gasTipCap.Set(minGasWei) 6130 } 6131 // This is enforced by concensus. We shouldn't be able to get here with a 6132 // swap tx. 6133 if gasTipCap.Cmp(maxFeeRate) > 0 { 6134 gasTipCap.Set(maxFeeRate) 6135 } 6136 return &bind.TransactOpts{ 6137 Context: ctx, 6138 From: from, 6139 Value: dexeth.GweiToWei(val), 6140 GasFeeCap: maxFeeRate, 6141 GasTipCap: gasTipCap, 6142 GasLimit: maxGas, 6143 } 6144 } 6145 6146 func gases(contractVer uint32, versionedGases map[uint32]*dexeth.Gases) *dexeth.Gases { 6147 if contractVer != contractVersionNewest { 6148 return versionedGases[contractVer] 6149 } 6150 var bestVer uint32 6151 var bestGases *dexeth.Gases 6152 for ver, gases := range versionedGases { 6153 if ver >= bestVer { 6154 bestGases = gases 6155 bestVer = ver 6156 } 6157 } 6158 return bestGases 6159 }