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