From 15b8378c766442e6d35332d39e7ed05803c4e40f Mon Sep 17 00:00:00 2001 From: Dmitry Holodov Date: Mon, 17 Apr 2023 11:59:47 -0500 Subject: [PATCH] ERC20 balance support (#380) * Added --token flag to swapcli's balances request allowing the user to query their token balance * --eth-asset flag on swapcli make changed to --token * Added TokenInfo RPC API method to retrieve an ERC20 token's metadata * Added an end-to-end ERC20 unit test to the daemon package * Fixed ERC20 token approval amount to SwapCreator contract to use the minimum value required (instead of approving the entire user's balance for transfer by the contract) * Fixed a race condition between concurrent contract calls to approve/new_swap in ERC20 token swaps * Removed hard coded assumptions that all tokens have 18 decimals * Maked the decimal places in our test ERC20 token configurable * Fixed token exchange rate calculations * Updated ongoing and past swap queries to correctly support ERC20 tokens * Initial unit tests in swapcli's main package --- cmd/swapcli/main.go | 154 ++++++++++------- cmd/swapcli/main_test.go | 30 ++++ cmd/swapcli/suite_test.go | 46 +++++ cmd/swapcli/util.go | 126 ++++++++++++++ cmd/swapcli/util_test.go | 73 ++++++++ cmd/swapd/main.go | 1 + cmd/swapd/main_test.go | 3 +- coins/coins.go | 154 +++++++++++++---- coins/coins_test.go | 18 +- coins/errors.go | 2 +- coins/exchange_rate.go | 19 ++- coins/exchange_rate_test.go | 43 +++++ coins/provides.go | 4 + coins/test_support.go | 14 +- coins/validate.go | 3 + coins/validate_test.go | 10 ++ common/rpctypes/types.go | 30 +++- common/types/ethasset.go | 28 ++- common/types/ethasset_test.go | 21 ++- daemon/swap_daemon.go | 8 +- daemon/swap_daemon_erc20_test.go | 119 +++++++++++++ daemon/swap_daemon_test.go | 138 +++------------ daemon/test_support.go | 171 +++++++++++++++++++ db/database_test.go | 6 +- ethereum/check_swap_creator_contract_test.go | 4 +- ethereum/contracts/ERC20Mock.sol | 8 + ethereum/erc20_mock.go | 8 +- ethereum/erc20_mock_test.go | 8 +- ethereum/extethclient/eth_wallet_client.go | 68 +++++--- net/initiate.go | 2 +- protocol/backend/backend.go | 2 +- protocol/ethereum_asset_amount.go | 20 +-- protocol/txsender/external_sender.go | 27 +-- protocol/txsender/sender.go | 116 +++++++------ protocol/utils.go | 5 +- protocol/xmrmaker/backend_offers.go | 20 +-- protocol/xmrmaker/checks.go | 2 +- protocol/xmrmaker/claim.go | 30 +--- protocol/xmrmaker/claim_test.go | 1 + protocol/xmrmaker/instance.go | 2 +- protocol/xmrmaker/net.go | 13 +- protocol/xmrmaker/swap_state.go | 14 +- protocol/xmrmaker/watcher.go | 1 - protocol/xmrtaker/errors.go | 16 +- protocol/xmrtaker/net.go | 42 ++--- protocol/xmrtaker/swap_state.go | 62 +++---- protocol/xmrtaker/swap_state_test.go | 67 ++++++-- relayer/submit_transaction.go | 4 +- rpc/personal.go | 40 ++++- rpc/swap.go | 8 +- rpcclient/personal.go | 24 ++- scripts/run-integration-tests.sh | 5 +- tests/erc20_integration_test.go | 92 +++++++--- tests/ganache.go | 1 + tests/integration_test.go | 8 +- 55 files changed, 1402 insertions(+), 539 deletions(-) create mode 100644 cmd/swapcli/main_test.go create mode 100644 cmd/swapcli/suite_test.go create mode 100644 cmd/swapcli/util.go create mode 100644 cmd/swapcli/util_test.go create mode 100644 daemon/swap_daemon_erc20_test.go diff --git a/cmd/swapcli/main.go b/cmd/swapcli/main.go index 6e3b7600..eda77b0b 100644 --- a/cmd/swapcli/main.go +++ b/cmd/swapcli/main.go @@ -8,9 +8,11 @@ package main import ( "fmt" "os" + "strconv" "strings" "time" + "github.com/cockroachdb/apd/v3" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/libp2p/go-libp2p/core/peer" "github.com/skip2/go-qrcode" @@ -40,11 +42,12 @@ const ( flagProvidesAmount = "provides-amount" flagUseRelayer = "use-relayer" flagSearchTime = "search-time" + flagToken = "token" flagDetached = "detached" ) -var ( - app = &cli.App{ +func cliApp() *cli.App { + return &cli.App{ Name: "swapcli", Usage: "Client for swapd", Version: cliutil.GetVersion(), @@ -76,6 +79,12 @@ var ( Action: runBalances, Flags: []cli.Flag{ swapdPortFlag, + &cli.StringSliceFlag{ + Name: flagToken, + Aliases: []string{"t"}, + EnvVars: []string{"SWAPCLI_TOKENS"}, + Usage: "Token address to include in the balance response", + }, }, }, { @@ -174,8 +183,8 @@ var ( Usage: "Exit immediately instead of subscribing to notifications about the swap's status", }, &cli.StringFlag{ - Name: "eth-asset", - Usage: "Ethereum ERC-20 token address to receive, or the zero address for regular ETH", + Name: flagToken, + Usage: "Use to pass the ethereum ERC20 token address to receive instead of ETH", }, &cli.BoolFlag{ Name: flagUseRelayer, @@ -326,7 +335,9 @@ var ( }, }, } +} +var ( swapdPortFlag = &cli.UintFlag{ Name: flagSwapdPort, Aliases: []string{"p"}, @@ -337,7 +348,7 @@ var ( ) func main() { - if err := app.Run(os.Args); err != nil { + if err := cliApp().Run(os.Args); err != nil { _, _ = fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } @@ -391,7 +402,17 @@ func runPeers(ctx *cli.Context) error { func runBalances(ctx *cli.Context) error { c := newRRPClient(ctx) - balances, err := c.Balances() + + request := &rpctypes.BalancesRequest{} + tokens := ctx.StringSlice(flagToken) + for _, tokenAddr := range tokens { + if !ethcommon.IsHexAddress(tokenAddr) { + return fmt.Errorf("invalid token address: %q", tokenAddr) + } + request.TokenAddrs = append(request.TokenAddrs, ethcommon.HexToAddress(tokenAddr)) + } + + balances, err := c.Balances(request) if err != nil { return err } @@ -399,6 +420,15 @@ func runBalances(ctx *cli.Context) error { fmt.Printf("Ethereum address: %s\n", balances.EthAddress) fmt.Printf("ETH Balance: %s\n", balances.WeiBalance.AsEtherString()) fmt.Println() + + for _, tokenBalance := range balances.TokenBalances { + fmt.Printf("Token: %s\n", tokenBalance.TokenInfo.Address) + fmt.Printf("Name: %q\n", tokenBalance.TokenInfo.Name) + fmt.Printf("Symbol: %q\n", tokenBalance.TokenInfo.Symbol) + fmt.Printf("Balance: %s\n", tokenBalance.AsStandard().Text('f')) + fmt.Println() + } + fmt.Printf("Monero address: %s\n", balances.MoneroAddress) fmt.Printf("XMR Balance: %s\n", balances.PiconeroBalance.AsMoneroString()) fmt.Printf("Unlocked XMR balance: %s\n", @@ -409,7 +439,7 @@ func runBalances(ctx *cli.Context) error { func runETHAddress(ctx *cli.Context) error { c := newRRPClient(ctx) - balances, err := c.Balances() + balances, err := c.Balances(nil) if err != nil { return err } @@ -424,7 +454,7 @@ func runETHAddress(ctx *cli.Context) error { func runXMRAddress(ctx *cli.Context) error { c := newRRPClient(ctx) - balances, err := c.Balances() + balances, err := c.Balances(nil) if err != nil { return err } @@ -468,7 +498,7 @@ func runQuery(ctx *cli.Context) error { } for i, o := range res.Offers { - err = printOffer(o, i, "") + err = printOffer(c, o, i, "") if err != nil { return err } @@ -498,7 +528,7 @@ func runQueryAll(ctx *cli.Context) error { fmt.Printf(" Peer ID: %v\n", po.PeerID) fmt.Printf(" Offers:\n") for j, o := range po.Offers { - err = printOffer(o, j, " ") + err = printOffer(c, o, j, " ") if err != nil { return err } @@ -509,6 +539,8 @@ func runQueryAll(ctx *cli.Context) error { } func runMake(ctx *cli.Context) error { + c := newRRPClient(ctx) + min, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagMinAmount) if err != nil { return err @@ -519,35 +551,55 @@ func runMake(ctx *cli.Context) error { return err } - exchangeRateDec, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagExchangeRate) - if err != nil { - return err - } - exchangeRate := coins.ToExchangeRate(exchangeRateDec) - // TODO: How to handle this if the other asset is not ETH? - otherMin, err := exchangeRate.ToETH(min) - if err != nil { - return err - } - otherMax, err := exchangeRate.ToETH(max) - if err != nil { - return err - } - - ethAssetStr := ctx.String("eth-asset") + ethAssetStr := ctx.String(flagToken) ethAsset := types.EthAssetETH if ethAssetStr != "" { ethAsset = types.EthAsset(ethcommon.HexToAddress(ethAssetStr)) } - c := newRRPClient(ctx) + exchangeRateDec, err := cliutil.ReadUnsignedDecimalFlag(ctx, flagExchangeRate) + if err != nil { + return err + } + exchangeRate := coins.ToExchangeRate(exchangeRateDec) + + var otherMin, otherMax *apd.Decimal + var symbol string + + if ethAsset.IsETH() { + symbol = "ETH" + + if otherMin, err = exchangeRate.ToETH(min); err != nil { + return err + } + + if otherMax, err = exchangeRate.ToETH(max); err != nil { + return err + } + } else { + tokenInfo, err := c.TokenInfo(ethAsset.Address()) //nolint:govet + if err != nil { + return err + } + + symbol = strconv.Quote(tokenInfo.Symbol) + + if otherMin, err = exchangeRate.ToERC20Amount(min, tokenInfo); err != nil { + return err + } + + if otherMax, err = exchangeRate.ToERC20Amount(max, tokenInfo); err != nil { + return err + } + + } printOfferSummary := func(offerResp *rpctypes.MakeOfferResponse) { fmt.Println("Published:") fmt.Printf("\tOffer ID: %s\n", offerResp.OfferID) fmt.Printf("\tPeer ID: %s\n", offerResp.PeerID) - fmt.Printf("\tTaker Min: %s %s\n", otherMin.Text('f'), ethAsset) - fmt.Printf("\tTaker Max: %s %s\n", otherMax.Text('f'), ethAsset) + fmt.Printf("\tTaker Min: %s %s\n", otherMin.Text('f'), symbol) + fmt.Printf("\tTaker Max: %s %s\n", otherMax.Text('f'), symbol) } alwaysUseRelayer := ctx.Bool(flagUseRelayer) @@ -667,14 +719,14 @@ func runGetOngoingSwap(ctx *cli.Context) error { fmt.Printf("---\n") } - receivedCoin := "ETH" - if info.Provided == coins.ProvidesETH { - receivedCoin = "XMR" + providedCoin, receivedCoin, err := providedAndReceivedSymbols(c, info.Provided, info.EthAsset) + if err != nil { + return err } fmt.Printf("ID: %s\n", info.ID) fmt.Printf("Start time: %s\n", info.StartTime.Format(common.TimeFmtSecs)) - fmt.Printf("Provided: %s %s\n", info.ProvidedAmount.Text('f'), info.Provided) + fmt.Printf("Provided: %s %s\n", info.ProvidedAmount.Text('f'), providedCoin) fmt.Printf("Receiving: %s %s\n", info.ExpectedAmount.Text('f'), receivedCoin) fmt.Printf("Exchange Rate: %s ETH/XMR\n", info.ExchangeRate) fmt.Printf("Status: %s\n", info.Status) @@ -717,9 +769,9 @@ func runGetPastSwap(ctx *cli.Context) error { fmt.Printf("---\n") } - receivedCoin := "ETH" - if info.Provided == coins.ProvidesETH { - receivedCoin = "XMR" + providedCoin, receivedCoin, err := providedAndReceivedSymbols(c, info.Provided, info.EthAsset) + if err != nil { + return err } endTime := "-" @@ -730,7 +782,7 @@ func runGetPastSwap(ctx *cli.Context) error { fmt.Printf("ID: %s\n", info.ID) fmt.Printf("Start time: %s\n", info.StartTime.Format(common.TimeFmtSecs)) fmt.Printf("End time: %s\n", endTime) - fmt.Printf("Provided: %s %s\n", info.ProvidedAmount.Text('f'), info.Provided) + fmt.Printf("Provided: %s %s\n", info.ProvidedAmount.Text('f'), providedCoin) fmt.Printf("Received: %s %s\n", info.ExpectedAmount.Text('f'), receivedCoin) fmt.Printf("Exchange Rate: %s ETH/XMR\n", info.ExchangeRate) fmt.Printf("Status: %s\n", info.Status) @@ -797,7 +849,7 @@ func runGetOffers(ctx *cli.Context) error { fmt.Println("Peer ID (self):", resp.PeerID) fmt.Println("Offers:") for i, offer := range resp.Offers { - err = printOffer(offer, i, " ") + err = printOffer(c, offer, i, " ") if err != nil { return err } @@ -867,32 +919,6 @@ func runSuggestedExchangeRate(ctx *cli.Context) error { return nil } -func printOffer(o *types.Offer, index int, indent string) error { - if index > 0 { - fmt.Printf("%s---\n", indent) - } - - xRate := o.ExchangeRate - minETH, err := xRate.ToETH(o.MinAmount) - if err != nil { - return err - } - maxETH, err := xRate.ToETH(o.MaxAmount) - if err != nil { - return err - } - - fmt.Printf("%sOffer ID: %s\n", indent, o.ID) - fmt.Printf("%sProvides: %s\n", indent, o.Provides) - fmt.Printf("%sTakes: %s\n", indent, o.EthAsset) - fmt.Printf("%sExchange Rate: %s %s/%s\n", indent, o.ExchangeRate, o.EthAsset, o.Provides) - fmt.Printf("%sMaker Min: %s %s\n", indent, o.MinAmount.Text('f'), o.Provides) - fmt.Printf("%sMaker Max: %s %s\n", indent, o.MaxAmount.Text('f'), o.Provides) - fmt.Printf("%sTaker Min: %s %s\n", indent, minETH.Text('f'), o.EthAsset) - fmt.Printf("%sTaker Max: %s %s\n", indent, maxETH.Text('f'), o.EthAsset) - return nil -} - func runGetVersions(ctx *cli.Context) error { fmt.Printf("swapcli: %s\n", cliutil.GetVersion()) diff --git a/cmd/swapcli/main_test.go b/cmd/swapcli/main_test.go new file mode 100644 index 00000000..71262624 --- /dev/null +++ b/cmd/swapcli/main_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "fmt" + + "github.com/stretchr/testify/require" +) + +func (s *swapCLITestSuite) Test_runGetVersions() { + // get the version of swapcli in isolation + args := []string{"swapcli", "--version"} + err := cliApp().RunContext(context.Background(), args) + require.NoError(s.T(), err) + + // get both the swapcli version and the daemon version information + args = []string{"swapcli", "version"} + err = cliApp().RunContext(context.Background(), args) + require.NoError(s.T(), err) +} + +func (s *swapCLITestSuite) Test_runBalances() { + args := []string{ + "swapcli", + "balances", + fmt.Sprintf("--%s=%s", flagToken, s.mockDaiAddr().Hex()), + } + err := cliApp().RunContext(context.Background(), args) + require.NoError(s.T(), err) +} diff --git a/cmd/swapcli/suite_test.go b/cmd/swapcli/suite_test.go new file mode 100644 index 00000000..5e06c2ae --- /dev/null +++ b/cmd/swapcli/suite_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "fmt" + "strconv" + "testing" + "time" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" + + "github.com/athanorlabs/atomic-swap/daemon" + "github.com/athanorlabs/atomic-swap/rpcclient" + "github.com/athanorlabs/atomic-swap/tests" +) + +// swapCLITestSuite provides a suite that unit tests can associate themselves +// if they need access to a swapd daemon and some preconfigured ERC20 tokens. +type swapCLITestSuite struct { + suite.Suite + conf *daemon.SwapdConfig + mockTokens map[string]ethcommon.Address +} + +func TestRunIntegrationTests(t *testing.T) { + s := new(swapCLITestSuite) + + s.conf = daemon.CreateTestConf(t, tests.GetMakerTestKey(t)) + t.Setenv("SWAPD_PORT", strconv.Itoa(int(s.conf.RPCPort))) + daemon.LaunchDaemons(t, 10*time.Minute, s.conf) + s.mockTokens = daemon.GetMockTokens(t, s.conf.EthereumClient) + suite.Run(t, s) +} + +func (s *swapCLITestSuite) rpcEndpoint() *rpcclient.Client { + return rpcclient.NewClient(context.Background(), fmt.Sprintf("http://127.0.0.1:%d", s.conf.RPCPort)) +} + +func (s *swapCLITestSuite) mockDaiAddr() ethcommon.Address { + return s.mockTokens[daemon.MockDAI] +} + +func (s *swapCLITestSuite) mockTetherAddr() ethcommon.Address { + return s.mockTokens[daemon.MockTether] +} diff --git a/cmd/swapcli/util.go b/cmd/swapcli/util.go new file mode 100644 index 00000000..8edaf2cb --- /dev/null +++ b/cmd/swapcli/util.go @@ -0,0 +1,126 @@ +package main + +import ( + "fmt" + + "github.com/cockroachdb/apd/v3" + ethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/athanorlabs/atomic-swap/coins" + "github.com/athanorlabs/atomic-swap/common/types" + "github.com/athanorlabs/atomic-swap/rpcclient" +) + +// _tokenCache should only be directly accessed by lookupToken +var _tokenCache = make(map[ethcommon.Address]*coins.ERC20TokenInfo) + +func lookupToken(c *rpcclient.Client, tokenAddr ethcommon.Address) (*coins.ERC20TokenInfo, error) { + token, ok := _tokenCache[tokenAddr] + if ok { + return token, nil + } + + token, err := c.TokenInfo(tokenAddr) + if err != nil { + return nil, err + } + + _tokenCache[tokenAddr] = token + + return token, nil +} + +func ethAssetSymbol(c *rpcclient.Client, ethAsset types.EthAsset) (string, error) { + if ethAsset.IsETH() { + return "ETH", nil + } + + token, err := lookupToken(c, ethAsset.Address()) + if err != nil { + return "", err + } + + return token.SanitizedSymbol(), nil +} + +// providedAndReceivedSymbols returns our provided asset symbol name followed +// by the counterparty's received asset symbol name. +func providedAndReceivedSymbols( + c *rpcclient.Client, + provides coins.ProvidesCoin, // determines whether we are the maker or taker + ethAsset types.EthAsset, // determines provided or received ETH asset symbol +) (string, string, error) { + ethAssetSymbol, err := ethAssetSymbol(c, ethAsset) + if err != nil { + return "", "", err + } + + switch provides { + case coins.ProvidesXMR: // we are the maker + return "XMR", ethAssetSymbol, nil + case coins.ProvidesETH: // We are the taker + return ethAssetSymbol, "XMR", nil + default: + return "", "", fmt.Errorf("unhandled provides value %q", provides) + } +} + +func printOffer(c *rpcclient.Client, o *types.Offer, index int, indent string) error { + if index > 0 { + fmt.Printf("%s---\n", indent) + } + + xRate := o.ExchangeRate + var ( + minTake *apd.Decimal + maxTake *apd.Decimal + err error + ) + if o.EthAsset.IsETH() { + minTake, err = xRate.ToETH(o.MinAmount) + if err != nil { + return err + } + + maxTake, err = xRate.ToETH(o.MaxAmount) + if err != nil { + return err + } + } else { + token, err := lookupToken(c, o.EthAsset.Address()) //nolint:govet + if err != nil { + return err + } + + minTake, err = xRate.ToERC20Amount(o.MinAmount, token) + if err != nil { + return err + } + + maxTake, err = xRate.ToERC20Amount(o.MaxAmount, token) + if err != nil { + return err + } + } + + // At the current time, offers always have the "Provides" field set to + // ProvidesXMR, so the Provides/Takes fields below are always from the + // perspective of the Maker. + providedCoin, receivedCoin, err := providedAndReceivedSymbols(c, o.Provides, o.EthAsset) + if err != nil { + return err + } + + fmt.Printf("%sOffer ID: %s\n", indent, o.ID) + fmt.Printf("%sProvides: %s\n", indent, providedCoin) + fmt.Printf("%sTakes: %s\n", indent, o.EthAsset) + if o.EthAsset.IsToken() { + fmt.Printf("%s %s (self reported symbol)\n", indent, receivedCoin) + } + fmt.Printf("%sExchange Rate: %s %s/%s\n", indent, o.ExchangeRate, receivedCoin, providedCoin) + fmt.Printf("%sMaker Min: %s %s\n", indent, o.MinAmount.Text('f'), providedCoin) + fmt.Printf("%sMaker Max: %s %s\n", indent, o.MaxAmount.Text('f'), providedCoin) + fmt.Printf("%sTaker Min: %s %s\n", indent, minTake.Text('f'), receivedCoin) + fmt.Printf("%sTaker Max: %s %s\n", indent, maxTake.Text('f'), receivedCoin) + return nil +} diff --git a/cmd/swapcli/util_test.go b/cmd/swapcli/util_test.go new file mode 100644 index 00000000..eccd1968 --- /dev/null +++ b/cmd/swapcli/util_test.go @@ -0,0 +1,73 @@ +package main + +import ( + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/athanorlabs/atomic-swap/coins" + "github.com/athanorlabs/atomic-swap/common/types" +) + +func (s *swapCLITestSuite) Test_lookupToken() { + c := s.rpcEndpoint() + + // First call triggers a lookup (assuming not cached yet) + token1, err := lookupToken(c, s.mockDaiAddr()) + require.NoError(s.T(), err) + require.NotNil(s.T(), token1) + + // Second call hits the cache + token2, err := lookupToken(c, s.mockDaiAddr()) + require.NoError(s.T(), err) + require.NotNil(s.T(), token1) + + // same address, since it came from the cache + require.True(s.T(), token1 == token2) + + invalidAddr := ethcommon.Address{0x1} + _, err = lookupToken(c, invalidAddr) + require.ErrorContains(s.T(), err, "no contract code at given address") +} + +func (s *swapCLITestSuite) Test_ethAssetSymbol() { + c := s.rpcEndpoint() + symbol, err := ethAssetSymbol(c, types.EthAssetETH) + require.NoError(s.T(), err) + require.Equal(s.T(), symbol, "ETH") + + symbol, err = ethAssetSymbol(c, types.EthAsset(s.mockTetherAddr())) + require.NoError(s.T(), err) + require.Equal(s.T(), symbol, `"USDT"`) // quoted at the current time +} + +func (s *swapCLITestSuite) Test_providedAndReceivedSymbols() { + c := s.rpcEndpoint() + + // 2nd parameter says we are the maker + providedSym, receivedSym, err := providedAndReceivedSymbols(c, coins.ProvidesXMR, types.EthAssetETH) + require.NoError(s.T(), err) + require.Equal(s.T(), providedSym, "XMR") + require.Equal(s.T(), receivedSym, "ETH") + + // 2nd parameter says we are the taker, but not necessarily that the ETH asset is ETH + ethAsset := types.EthAsset(s.mockTetherAddr()) + providedSym, receivedSym, err = providedAndReceivedSymbols(c, coins.ProvidesETH, ethAsset) + require.NoError(s.T(), err) + require.Equal(s.T(), providedSym, `"USDT"`) + require.Equal(s.T(), receivedSym, "XMR") +} + +func (s *swapCLITestSuite) Test_printOffer() { + c := s.rpcEndpoint() + + o := types.NewOffer( + coins.ProvidesXMR, + coins.StrToDecimal("1.5"), // maker min + coins.StrToDecimal("2.5"), // maker max + coins.StrToExchangeRate("200"), // 250 USDT per 1 XMR + types.EthAsset(s.mockTetherAddr()), + ) + + err := printOffer(c, o, 0, "") + require.NoError(s.T(), err) +} diff --git a/cmd/swapd/main.go b/cmd/swapd/main.go index c85cf334..7ddaf462 100644 --- a/cmd/swapd/main.go +++ b/cmd/swapd/main.go @@ -262,6 +262,7 @@ func setLogLevels(level string) { _ = logging.SetLogLevel("protocol", level) _ = logging.SetLogLevel("relayer", level) // external and internal _ = logging.SetLogLevel("rpc", level) + _ = logging.SetLogLevel("txsender", level) _ = logging.SetLogLevel("xmrmaker", level) _ = logging.SetLogLevel("xmrtaker", level) } diff --git a/cmd/swapd/main_test.go b/cmd/swapd/main_test.go index ac799878..e6c00262 100644 --- a/cmd/swapd/main_test.go +++ b/cmd/swapd/main_test.go @@ -22,6 +22,7 @@ import ( "github.com/athanorlabs/atomic-swap/coins" "github.com/athanorlabs/atomic-swap/common" + "github.com/athanorlabs/atomic-swap/common/rpctypes" "github.com/athanorlabs/atomic-swap/common/types" "github.com/athanorlabs/atomic-swap/daemon" contracts "github.com/athanorlabs/atomic-swap/ethereum" @@ -294,7 +295,7 @@ func TestDaemon_PersistOffers(t *testing.T) { // make an offer client := rpcclient.NewClient(ctx1, rpcEndpoint) - balance, err := client.Balances() + balance, err := client.Balances(new(rpctypes.BalancesRequest)) require.NoError(t, err) require.GreaterOrEqual(t, balance.PiconeroUnlockedBalance.Cmp(coins.MoneroToPiconero(one)), 0) diff --git a/coins/coins.go b/coins/coins.go index 517bd711..d7554014 100644 --- a/coins/coins.go +++ b/coins/coins.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/cockroachdb/apd/v3" + ethcommon "github.com/ethereum/go-ethereum/common" ) // PiconeroAmount represents some amount of piconero (the smallest denomination of monero) @@ -119,11 +120,20 @@ func FmtPiconeroAsXMR(piconeros uint64) string { return NewPiconeroAmount(piconeros).AsMoneroString() } -// WeiAmount represents some amount of ether in the smallest denomination (wei) +// EthAssetAmount represents an amount of an Ethereum asset (ie. ether or an ERC20) +type EthAssetAmount interface { + BigInt() *big.Int + AsStandard() *apd.Decimal + StandardSymbol() string + IsToken() bool + TokenAddress() ethcommon.Address +} + +// WeiAmount represents some amount of ETH in the smallest denomination (Wei) type WeiAmount apd.Decimal // NewWeiAmount converts the passed *big.Int representation of a -// wei amount to the WeiAmount type. The returned value is a copy +// Wei amount to the WeiAmount type. The returned value is a copy // with no references to the passed value. func NewWeiAmount(amount *big.Int) *WeiAmount { a := new(apd.BigInt).SetMathBigInt(amount) @@ -135,6 +145,15 @@ func (a *WeiAmount) Decimal() *apd.Decimal { return (*apd.Decimal)(a) } +// Cmp compares a and b and returns: +// +// -1 if a < b +// 0 if a == b +// +1 if a > b +func (a *WeiAmount) Cmp(b *WeiAmount) int { + return a.Decimal().Cmp(b.Decimal()) +} + // UnmarshalText hands off JSON decoding to apd.Decimal func (a *WeiAmount) UnmarshalText(b []byte) error { err := a.Decimal().UnmarshalText(b) @@ -152,12 +171,12 @@ func (a *WeiAmount) MarshalText() ([]byte, error) { return a.Decimal().MarshalText() } -// ToWeiAmount casts an *apd.Decimal to *WeiAmount +// ToWeiAmount casts an *apd.Decimal that is already in Wei to *WeiAmount func ToWeiAmount(wei *apd.Decimal) *WeiAmount { return (*WeiAmount)(wei) } -// EtherToWei converts some amount of standard ether to an WeiAmount. +// EtherToWei converts some amount of standard ETH to a WeiAmount. func EtherToWei(ethAmt *apd.Decimal) *WeiAmount { weiAmt := new(apd.Decimal).Set(ethAmt) increaseExponent(weiAmt, NumEtherDecimals) @@ -173,7 +192,7 @@ func EtherToWei(ethAmt *apd.Decimal) *WeiAmount { // BigInt returns the given WeiAmount as a *big.Int func (a *WeiAmount) BigInt() *big.Int { // Passing Quantize(...) zero as the exponent sets the coefficient to a whole-number - // wei value. Round-half-up is used by default. Assuming no rounding occurs, the + // Wei value. Round-half-up is used by default. Assuming no rounding occurs, the // operation below is the opposite of Reduce(...) which lops off even factors of // 10 from the coefficient, placing them on the exponent. wholeWeiVal := new(apd.Decimal) @@ -182,13 +201,13 @@ func (a *WeiAmount) BigInt() *big.Int { panic(err) } if cond.Inexact() { - // We round when converting from Ether to Wei, so we shouldn't see this + // We round when converting from ETH to Wei, so we shouldn't see this log.Warnf("converting WeiAmount=%s to big.Int required rounding", a.String()) } return new(big.Int).SetBytes(wholeWeiVal.Coeff.Bytes()) } -// AsEther returns the wei amount as ether +// AsEther returns the Wei amount as ETH func (a *WeiAmount) AsEther() *apd.Decimal { ether := new(apd.Decimal).Set(a.Decimal()) decreaseExponent(ether, NumEtherDecimals) @@ -196,91 +215,158 @@ func (a *WeiAmount) AsEther() *apd.Decimal { return ether } -// AsEtherString converts the wei amount to an eth amount string +// AsEtherString converts the Wei amount to an ETH amount string func (a *WeiAmount) AsEtherString() string { return a.AsEther().Text('f') } -// AsStandard is an alias for AsEther, returning the wei amount as ether +// AsStandard is an alias for AsEther, returning the Wei amount as ETH func (a *WeiAmount) AsStandard() *apd.Decimal { return a.AsEther() } -// String returns the wei amount as a base10 string +// AsStandardString is an alias for AsEtherString, returning the Wei amount as +// an ETH string +func (a *WeiAmount) AsStandardString() *apd.Decimal { + return a.AsEther() +} + +// StandardSymbol returns the string "ETH" +func (a *WeiAmount) StandardSymbol() string { + return "ETH" +} + +// IsToken returns false, as WeiAmount is not an ERC20 token +func (a *WeiAmount) IsToken() bool { + return false +} + +// TokenAddress returns the all-zero address as WeiAmount is not an ERC20 token +func (a *WeiAmount) TokenAddress() ethcommon.Address { + return ethcommon.Address{} +} + +// String returns the Wei amount as a base10 string func (a *WeiAmount) String() string { return a.Decimal().Text('f') } -// FmtWeiAsETH takes wei as input and produces a formatted string of the amount +// FmtWeiAsETH takes Wei as input and produces a formatted string of the amount // in ETH. func FmtWeiAsETH(wei *big.Int) string { return NewWeiAmount(wei).AsEther().Text('f') } +// ERC20TokenInfo stores the token contract address and basic info that most +// ERC20 tokens support +type ERC20TokenInfo struct { + Address ethcommon.Address `json:"address" validate:"required"` + NumDecimals uint8 `json:"decimals"` // digits after the Decimal point needed for smallest denomination + Name string `json:"name"` + Symbol string `json:"symbol"` +} + +// NewERC20TokenInfo constructs and returns a new ERC20TokenInfo object +func NewERC20TokenInfo(address ethcommon.Address, decimals uint8, name string, symbol string) *ERC20TokenInfo { + return &ERC20TokenInfo{ + Address: address, + NumDecimals: decimals, + Name: name, + Symbol: symbol, + } +} + +// SanitizedSymbol quotes the symbol ensuring escape sequences, newlines, etc. are escaped. +func (t *ERC20TokenInfo) SanitizedSymbol() string { + return strconv.Quote(t.Symbol) +} + // ERC20TokenAmount represents some amount of an ERC20 token in the smallest denomination type ERC20TokenAmount struct { - amount *apd.Decimal - numDecimals uint8 // num digits after the Decimal point needed for smallest denomination + Amount *apd.Decimal `json:"amount" validate:"required"` // in smallest non-divisible units of token + TokenInfo *ERC20TokenInfo `json:"tokenInfo" validate:"required"` } // NewERC20TokenAmountFromBigInt converts some amount in the smallest token denomination into an ERC20TokenAmount. -func NewERC20TokenAmountFromBigInt(amount *big.Int, decimals uint8) *ERC20TokenAmount { +func NewERC20TokenAmountFromBigInt(amount *big.Int, tokenInfo *ERC20TokenInfo) *ERC20TokenAmount { asDecimal := new(apd.Decimal) asDecimal.Coeff.SetBytes(amount.Bytes()) return &ERC20TokenAmount{ - amount: asDecimal, - numDecimals: decimals, + Amount: asDecimal, + TokenInfo: tokenInfo, } } // NewERC20TokenAmount converts some amount in the smallest token denomination into an ERC20TokenAmount. -func NewERC20TokenAmount(amount int64, decimals uint8) *ERC20TokenAmount { +func NewERC20TokenAmount(amount int64, tokenInfo *ERC20TokenInfo) *ERC20TokenAmount { return &ERC20TokenAmount{ - amount: apd.New(amount, 0), - numDecimals: decimals, + Amount: apd.New(amount, 0), + TokenInfo: tokenInfo, } } -// NewERC20TokenAmountFromDecimals converts some amount of standard token in standard format -// to its smaller denomination. -// For example, if amount is 1.99 and decimals is 9, the resulting value stored -// is 1.99 * 10^9. -func NewERC20TokenAmountFromDecimals(amount *apd.Decimal, decimals uint8) *ERC20TokenAmount { +// NewERC20TokenAmountFromDecimals converts some amount for a token in its +// standard form into the smallest denomination that the token supports. For +// example, if amount is 1.99 and decimals is 4, the resulting value stored is +// 19900 +func NewERC20TokenAmountFromDecimals(amount *apd.Decimal, tokenInfo *ERC20TokenInfo) *ERC20TokenAmount { adjusted := new(apd.Decimal).Set(amount) - increaseExponent(adjusted, decimals) + increaseExponent(adjusted, tokenInfo.NumDecimals) // If we are rejecting token amounts that have too many decimal places on input, rounding // below will never occur. if err := roundToDecimalPlace(adjusted, adjusted, 0); err != nil { panic(err) // this shouldn't be possible } return &ERC20TokenAmount{ - amount: adjusted, - numDecimals: decimals, + Amount: adjusted, + TokenInfo: tokenInfo, } } // BigInt returns the ERC20TokenAmount as a *big.Int func (a *ERC20TokenAmount) BigInt() *big.Int { wholeTokenUnits := new(apd.Decimal) - cond, err := decimalCtx.Quantize(wholeTokenUnits, a.amount, 0) + cond, err := decimalCtx.Quantize(wholeTokenUnits, a.Amount, 0) if err != nil { panic(err) } if cond.Inexact() { - log.Warn("Converting ERC20TokenAmount=%s (digits=%d) to big.Int required rounding", a.amount, a.numDecimals) + log.Warn("Converting ERC20TokenAmount=%s (digits=%d) to big.Int required rounding", + a.Amount, a.TokenInfo.NumDecimals) } return new(big.Int).SetBytes(wholeTokenUnits.Coeff.Bytes()) } // AsStandard returns the amount in standard form func (a *ERC20TokenAmount) AsStandard() *apd.Decimal { - tokenAmt := new(apd.Decimal).Set(a.amount) - decreaseExponent(tokenAmt, a.numDecimals) + tokenAmt := new(apd.Decimal).Set(a.Amount) + decreaseExponent(tokenAmt, a.TokenInfo.NumDecimals) _, _ = tokenAmt.Reduce(tokenAmt) return tokenAmt } -// String returns the ERC20TokenAmount as a base10 string -func (a *ERC20TokenAmount) String() string { - return a.amount.Text('f') +// AsStandardString returns the amount as a standard (decimal adjusted) string +func (a *ERC20TokenAmount) AsStandardString() string { + return a.AsStandard().Text('f') +} + +// StandardSymbol returns the token's symbol in a format that is safe to log and display +func (a *ERC20TokenAmount) StandardSymbol() string { + return a.TokenInfo.SanitizedSymbol() +} + +// IsToken returns true, because ERC20TokenAmount represents and ERC20 token +func (a *ERC20TokenAmount) IsToken() bool { + return true +} + +// TokenAddress returns the ERC20 token's ethereum contract address +func (a *ERC20TokenAmount) TokenAddress() ethcommon.Address { + return a.TokenInfo.Address +} + +// String returns the ERC20TokenAmount as a base10 string of the token's smallest, +// non-divisible units. +func (a *ERC20TokenAmount) String() string { + return a.Amount.Text('f') } diff --git a/coins/coins_test.go b/coins/coins_test.go index 7634c6b7..06a4b56a 100644 --- a/coins/coins_test.go +++ b/coins/coins_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/cockroachdb/apd/v3" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -109,37 +110,40 @@ func TestWeiAmount_BigInt(t *testing.T) { } func TestERC20TokenAmount(t *testing.T) { + const numDecimals = 9 + tokenInfo := NewERC20TokenInfo(ethcommon.Address{}, numDecimals, "", "") + amount := StrToDecimal("33.999999999") - wei := NewERC20TokenAmountFromDecimals(amount, 9) + wei := NewERC20TokenAmountFromDecimals(amount, tokenInfo) assert.Equal(t, amount.String(), wei.AsStandard().String()) amount = StrToDecimal("33.000000005") - wei = NewERC20TokenAmountFromDecimals(amount, 9) + wei = NewERC20TokenAmountFromDecimals(amount, tokenInfo) assert.Equal(t, "33.000000005", wei.AsStandard().String()) amount = StrToDecimal("33.0000000005") - wei = NewERC20TokenAmountFromDecimals(amount, 9) + wei = NewERC20TokenAmountFromDecimals(amount, tokenInfo) assert.Equal(t, "33.000000001", wei.AsStandard().String()) amount = StrToDecimal("999999999999999999.0000000005") - wei = NewERC20TokenAmountFromDecimals(amount, 9) + wei = NewERC20TokenAmountFromDecimals(amount, tokenInfo) assert.Equal(t, "999999999999999999.000000001", wei.AsStandard().String()) amountUint := int64(8181) - tokenAmt := NewERC20TokenAmount(amountUint, 9) + tokenAmt := NewERC20TokenAmount(amountUint, tokenInfo) assert.Equal(t, amountUint, tokenAmt.BigInt().Int64()) } func TestNewERC20TokenAmountFromBigInt(t *testing.T) { bi := big.NewInt(4321) - token := NewERC20TokenAmountFromBigInt(bi, 2) + token := NewERC20TokenAmountFromBigInt(bi, &ERC20TokenInfo{NumDecimals: 2}) assert.Equal(t, "4321", token.String()) assert.Equal(t, "43.21", token.AsStandard().String()) } func TestNewERC20TokenAmountFromDecimals(t *testing.T) { stdAmount := StrToDecimal("0.19") - token := NewERC20TokenAmountFromDecimals(stdAmount, 1) + token := NewERC20TokenAmountFromDecimals(stdAmount, &ERC20TokenInfo{NumDecimals: 1}) // There's only one decimal place, so this is getting rounded to 2 // under the current implementation. It's not entirely clear what diff --git a/coins/errors.go b/coins/errors.go index 986d3fe7..f833c87d 100644 --- a/coins/errors.go +++ b/coins/errors.go @@ -9,7 +9,7 @@ import ( var ( errNegativePiconeros = errors.New("negative piconero values are not supported") - errNegativeWei = errors.New("negative wei values are not supported") + errNegativeWei = errors.New("negative Wei values are not supported") // ErrInvalidCoin is generated when a ProvidesCoin type has an invalid string ErrInvalidCoin = errors.New("invalid ProvidesCoin") ) diff --git a/coins/exchange_rate.go b/coins/exchange_rate.go index 3aff16f5..c1f9b315 100644 --- a/coins/exchange_rate.go +++ b/coins/exchange_rate.go @@ -51,7 +51,7 @@ func (r *ExchangeRate) MarshalText() ([]byte, error) { return r.Decimal().MarshalText() } -// ToXMR converts an ether amount to a monero amount with the given exchange rate +// ToXMR converts an ETH amount to an XMR amount with the given exchange rate func (r *ExchangeRate) ToXMR(ethAmount *apd.Decimal) (*apd.Decimal, error) { xmrAmt := new(apd.Decimal) _, err := decimalCtx.Quo(xmrAmt, ethAmount, r.Decimal()) @@ -64,13 +64,14 @@ func (r *ExchangeRate) ToXMR(ethAmount *apd.Decimal) (*apd.Decimal, error) { return xmrAmt, nil } -// ToETH converts a monero amount to an eth amount with the given exchange rate +// ToETH converts an XMR amount to an ETH amount with the given exchange rate func (r *ExchangeRate) ToETH(xmrAmount *apd.Decimal) (*apd.Decimal, error) { ethAmt := new(apd.Decimal) _, err := decimalCtx.Mul(ethAmt, r.Decimal(), xmrAmount) if err != nil { return nil, err } + // Assuming the xmrAmount was capped at 12 decimal places and the exchange // rate was capped at 6 decimal places, you can't generate more than 18 // decimal places below, so no rounding occurs. @@ -80,6 +81,20 @@ func (r *ExchangeRate) ToETH(xmrAmount *apd.Decimal) (*apd.Decimal, error) { return ethAmt, nil } +// ToERC20Amount converts an XMR amount to a token amount in standard units with +// the given exchange rate +func (r *ExchangeRate) ToERC20Amount(xmrAmount *apd.Decimal, token *ERC20TokenInfo) (*apd.Decimal, error) { + erc20Amount := new(apd.Decimal) + _, err := decimalCtx.Mul(erc20Amount, r.Decimal(), xmrAmount) + if err != nil { + return nil, err + } + + // The token, if required, will get rounded to whole token units in + // NewERC20TokenAmountFromDecimals. + return NewERC20TokenAmountFromDecimals(erc20Amount, token).AsStandard(), nil +} + func (r *ExchangeRate) String() string { return r.Decimal().Text('f') } diff --git a/coins/exchange_rate_test.go b/coins/exchange_rate_test.go index f0fdb396..016e723f 100644 --- a/coins/exchange_rate_test.go +++ b/coins/exchange_rate_test.go @@ -60,6 +60,49 @@ func TestExchangeRate_ToETH(t *testing.T) { assert.Equal(t, expectedETHAmount, ethAmount.String()) } +func TestExchangeRate_ToERC20Amount(t *testing.T) { + rate := StrToExchangeRate("1.5") // 1.5 XMR * 2 = 3 Standard token units + xmrAmount := StrToDecimal("2") + const tokenDecimals = 10 + const expectedTokenStandardAmount = "3" + erc20Info := &ERC20TokenInfo{NumDecimals: tokenDecimals} + + erc20Amt, err := rate.ToERC20Amount(xmrAmount, erc20Info) + require.NoError(t, err) + assert.Equal(t, expectedTokenStandardAmount, erc20Amt.Text('f')) +} + +func TestExchangeRate_ToERC20Amount_roundDown(t *testing.T) { + // 0.333333 * 1.0000015 = 0.333333499... + // = 0.333333 (token only supports 6 decimals) + rate := StrToExchangeRate("0.333333") + xmrAmount := StrToDecimal("1.0000015") + + const tokenDecimals = 6 + const expectedTokenStandardAmount = "0.333333" + erc20Info := &ERC20TokenInfo{NumDecimals: tokenDecimals} + + erc20Amt, err := rate.ToERC20Amount(xmrAmount, erc20Info) + require.NoError(t, err) + assert.Equal(t, expectedTokenStandardAmount, erc20Amt.Text('f')) +} + +func TestExchangeRate_ToERC20Amount_roundUp(t *testing.T) { + // 0.333333 * 1.000001501 = 0.333333500.. + // = 0.333334 (token only supports 6 decimals) + + rate := StrToExchangeRate("0.333333") + xmrAmount := StrToDecimal("1.000001501") + + const tokenDecimals = 6 + const expectedTokenStandardAmount = "0.333334" + erc20Info := &ERC20TokenInfo{NumDecimals: tokenDecimals} + + erc20Amt, err := rate.ToERC20Amount(xmrAmount, erc20Info) + require.NoError(t, err) + assert.Equal(t, expectedTokenStandardAmount, erc20Amt.Text('f')) +} + func TestExchangeRate_String(t *testing.T) { rate := ToExchangeRate(apd.New(3, -4)) // 0.0003 assert.Equal(t, "0.0003", rate.String()) diff --git a/coins/provides.go b/coins/provides.go index dc541296..7ad11e90 100644 --- a/coins/provides.go +++ b/coins/provides.go @@ -27,6 +27,10 @@ func NewProvidesCoin(s string) (ProvidesCoin, error) { } } +func (c *ProvidesCoin) String() string { + return string(*c) +} + // MarshalText hands off JSON encoding to apd.Decimal func (c *ProvidesCoin) MarshalText() ([]byte, error) { switch *c { diff --git a/coins/test_support.go b/coins/test_support.go index 1f35833e..417eb3d6 100644 --- a/coins/test_support.go +++ b/coins/test_support.go @@ -36,10 +36,22 @@ func StrToExchangeRate(rate string) *ExchangeRate { return r } -// IntToWei converts some amount of wei into an WeiAmount for unit tests. +// IntToWei converts some amount of Wei into an WeiAmount for unit tests. func IntToWei(amount int64) *WeiAmount { if amount < 0 { panic(fmt.Sprintf("Wei amount %d is negative", amount)) // test only function } return NewWeiAmount(big.NewInt(amount)) } + +// Sub returns the value of a-b in a newly allocated WeiAmount variable. +// If a or b is NaN, this function will panic, but we exclude such values +// during input validation. +func (a *WeiAmount) Sub(b *WeiAmount) *WeiAmount { + result := new(WeiAmount) + _, err := decimalCtx.Sub(result.Decimal(), a.Decimal(), b.Decimal()) + if err != nil { + panic(err) + } + return result +} diff --git a/coins/validate.go b/coins/validate.go index 949ce2d6..2bb7ca02 100644 --- a/coins/validate.go +++ b/coins/validate.go @@ -21,6 +21,9 @@ func ValidatePositive(jsonFieldName string, maxDecimals uint8, value *apd.Decima if value.Negative { return fmt.Errorf("%q cannot be negative", jsonFieldName) } + if value.Form != apd.Finite { + return fmt.Errorf("%q must be finite", jsonFieldName) + } // In most cases, this line won't do anything. If the coefficient is divisible // by one or more multiples of 10, the zeros are chopped off and added to the diff --git a/coins/validate_test.go b/coins/validate_test.go index e3f48959..60cab7d1 100644 --- a/coins/validate_test.go +++ b/coins/validate_test.go @@ -66,6 +66,16 @@ func TestValidatePositive_errors(t *testing.T) { value: StrToDecimal("1.12345678901234567890000000000000000000"), // zeros at end ignored errContains: `"testValue" has too many decimal points; found=19 max=18`, }, + { + numDecPlaces: NumEtherDecimals, + value: &apd.Decimal{Form: apd.Infinite}, + errContains: `"testValue" must be finite`, + }, + { + numDecPlaces: NumEtherDecimals, + value: &apd.Decimal{Form: apd.NaN}, + errContains: `"testValue" must be finite`, + }, } for _, entry := range testEntries { err := ValidatePositive(fieldName, entry.numDecPlaces, entry.value) diff --git a/common/rpctypes/types.go b/common/rpctypes/types.go index 43696507..fe9377b8 100644 --- a/common/rpctypes/types.go +++ b/common/rpctypes/types.go @@ -115,14 +115,30 @@ type SignerTxSigned struct { TxHash ethcommon.Hash `json:"txHash" validate:"required"` } -// BalancesResponse holds the response for the combined Monero and Ethereum Balances request +// TokenInfoRequest is used to request lookup of the token's metadata. +type TokenInfoRequest struct { + TokenAddr ethcommon.Address `json:"tokenAddr" validate:"required"` +} + +// TokenInfoResponse contains the metadata for the requested token +type TokenInfoResponse = coins.ERC20TokenInfo + +// BalancesRequest is used to request the combined Monero and Ethereum balances +// as well as the balances of any tokens included in the request. +type BalancesRequest struct { + TokenAddrs []ethcommon.Address `json:"tokensAddrs" validate:"dive,required"` +} + +// BalancesResponse holds the response for the combined Monero, Ethereum and +// optional token Balances request type BalancesResponse struct { - MoneroAddress *mcrypto.Address `json:"moneroAddress" validate:"required"` - PiconeroBalance *coins.PiconeroAmount `json:"piconeroBalance" validate:"required"` - PiconeroUnlockedBalance *coins.PiconeroAmount `json:"piconeroUnlockedBalance" validate:"required"` - BlocksToUnlock uint64 `json:"blocksToUnlock"` - EthAddress ethcommon.Address `json:"ethAddress" validate:"required"` - WeiBalance *coins.WeiAmount `json:"weiBalance" validate:"required"` + MoneroAddress *mcrypto.Address `json:"moneroAddress" validate:"required"` + PiconeroBalance *coins.PiconeroAmount `json:"piconeroBalance" validate:"required"` + PiconeroUnlockedBalance *coins.PiconeroAmount `json:"piconeroUnlockedBalance" validate:"required"` + BlocksToUnlock uint64 `json:"blocksToUnlock"` + EthAddress ethcommon.Address `json:"ethAddress" validate:"required"` + WeiBalance *coins.WeiAmount `json:"weiBalance" validate:"required"` + TokenBalances []*coins.ERC20TokenAmount `json:"tokenBalances" validate:"dive,required"` } // AddressesResponse ... diff --git a/common/types/ethasset.go b/common/types/ethasset.go index 57b1394d..259fe67b 100644 --- a/common/types/ethasset.go +++ b/common/types/ethasset.go @@ -13,10 +13,20 @@ import ( // EthAsset represents an Ethereum asset (ETH or a token address) type EthAsset ethcommon.Address +// IsETH returns true of the asset is ETH, otherwise false +func (asset EthAsset) IsETH() bool { + return asset == EthAssetETH +} + +// IsToken returns true if the asset is an ERC20 token, otherwise false +func (asset EthAsset) IsToken() bool { + return asset != EthAssetETH +} + // String implements fmt.Stringer, returning the asset's address in hex // prefixed by `ERC20@` if it's an ERC20 token, or ETH for ether. func (asset EthAsset) String() string { - if asset == EthAssetETH { + if asset.IsETH() { return "ETH" } @@ -27,7 +37,7 @@ func (asset EthAsset) String() string { // MarshalText returns the hex representation of the EthAsset or, // in some cases, a short string. func (asset EthAsset) MarshalText() ([]byte, error) { - if asset == EthAssetETH { + if asset.IsETH() { return []byte("ETH"), nil } return []byte(ethcommon.Address(asset).Hex()), nil @@ -36,16 +46,18 @@ func (asset EthAsset) MarshalText() ([]byte, error) { // UnmarshalText assigns the EthAsset from the input text func (asset *EthAsset) UnmarshalText(input []byte) error { inputStr := string(input) - switch { - case strings.EqualFold(inputStr, "ETH"): - *asset = EthAsset{} + if inputStr == "ETH" { + *asset = EthAssetETH return nil - case ethcommon.IsHexAddress(inputStr): + } + + inputStr = strings.TrimPrefix(inputStr, "ERC20@") + if ethcommon.IsHexAddress(inputStr) { *asset = EthAsset(ethcommon.HexToAddress(inputStr)) return nil - default: - return fmt.Errorf("invalid asset value %q", inputStr) } + + return fmt.Errorf("invalid asset value %q", inputStr) } // Address ... diff --git a/common/types/ethasset_test.go b/common/types/ethasset_test.go index da51b08d..e1691e6d 100644 --- a/common/types/ethasset_test.go +++ b/common/types/ethasset_test.go @@ -26,11 +26,13 @@ func TestEthAsset_MarshalText(t *testing.T) { } func TestEthAsset_UnmarshalText(t *testing.T) { + // Unmarshal string ETH asset := EthAsset(ethcommon.Address{0x1}) // any non-zero initial value to make sure we overwrite it err := json.Unmarshal([]byte(`"ETH"`), &asset) require.NoError(t, err) require.Equal(t, EthAssetETH, asset) + // Unmarshal 0x prefixed address addr := "0xADd47138bb89c3013B39F2e3B062B408c90E5179" quotedAddr := fmt.Sprintf("%q", addr) err = json.Unmarshal([]byte(quotedAddr), &asset) @@ -38,12 +40,20 @@ func TestEthAsset_UnmarshalText(t *testing.T) { expected := EthAsset(ethcommon.HexToAddress(addr)) require.Equal(t, expected, asset) - // Same exact test as above, but without the 0x prefix + // Unmarshal address without the 0x prefix quotedAddr = fmt.Sprintf("%q", addr[2:]) err = json.Unmarshal([]byte(quotedAddr), &asset) require.NoError(t, err) expected = EthAsset(ethcommon.HexToAddress(addr)) require.Equal(t, expected, asset) + + // Unmarshal addresses with the ERC20@ prefix that our String() method + // generates + expected = EthAsset(ethcommon.HexToAddress("0xa1E32d14AC4B6d8c1791CAe8E9baD46a1E15B7a8")) + quotedAddr = fmt.Sprintf("%q", expected.String()) // will have ERC20@ prefix + err = json.Unmarshal([]byte(quotedAddr), &asset) + require.NoError(t, err) + require.Equal(t, expected, asset) } func TestEthAsset_UnmarshalText_fail(t *testing.T) { @@ -52,3 +62,12 @@ func TestEthAsset_UnmarshalText_fail(t *testing.T) { err := json.Unmarshal([]byte(tooShortQuotedAddr), &asset) require.ErrorContains(t, err, "invalid asset value") } + +func TestEthAsset_IsToken(t *testing.T) { + require.True(t, EthAssetETH.IsETH()) + require.False(t, EthAssetETH.IsToken()) + + token := EthAsset(ethcommon.HexToAddress("0xa1E32d14AC4B6d8c1791CAe8E9baD46a1E15B7a8")) + require.False(t, token.IsETH()) + require.True(t, token.IsToken()) +} diff --git a/daemon/swap_daemon.go b/daemon/swap_daemon.go index a4ddcb05..ae2542d9 100644 --- a/daemon/swap_daemon.go +++ b/daemon/swap_daemon.go @@ -157,15 +157,11 @@ func RunSwapDaemon(ctx context.Context, conf *SwapdConfig) (err error) { }) log.Infof("starting swapd with data-dir %s", conf.EnvConf.DataDir) - err = rpcServer.Start() if errors.Is(err, http.ErrServerClosed) { - // Set err to nil to exit the program with exit code 0 - // and to avoid a FATAL ERROR entry in log files - // - // NOTE: The ErrServerClosed error happens only when the server is told - // to shutdown or close + // Remove the error for a clean program exit, as ErrServerClosed only + // happens when the server is told to shut down err = nil } diff --git a/daemon/swap_daemon_erc20_test.go b/daemon/swap_daemon_erc20_test.go new file mode 100644 index 00000000..f673a75f --- /dev/null +++ b/daemon/swap_daemon_erc20_test.go @@ -0,0 +1,119 @@ +package daemon + +import ( + "fmt" + "sync" + "testing" + "time" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/athanorlabs/atomic-swap/coins" + "github.com/athanorlabs/atomic-swap/common/rpctypes" + "github.com/athanorlabs/atomic-swap/common/types" + "github.com/athanorlabs/atomic-swap/monero" + "github.com/athanorlabs/atomic-swap/rpcclient" + "github.com/athanorlabs/atomic-swap/rpcclient/wsclient" + "github.com/athanorlabs/atomic-swap/tests" +) + +// Tests the scenario where Bob has XMR and enough ETH to pay gas fees for the token claim. He +// exchanges 2 XMR for 3 of Alice's ERC20 tokens. +func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) { + minXMR := coins.StrToDecimal("1") + maxXMR := coins.StrToDecimal("2") + exRate := coins.StrToExchangeRate("1.5") + + bobConf := CreateTestConf(t, tests.GetMakerTestKey(t)) + monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR)) + + aliceConf := CreateTestConf(t, tests.GetTakerTestKey(t)) + + tokenAddr := GetMockTokens(t, aliceConf.EthereumClient)[MockTether] + tokenAsset := types.EthAsset(tokenAddr) + + timeout := 7 * time.Minute + ctx := LaunchDaemons(t, timeout, aliceConf, bobConf) + + bc, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", bobConf.RPCPort)) + require.NoError(t, err) + ac, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", aliceConf.RPCPort)) + require.NoError(t, err) + + _, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, tokenAsset, false) + require.NoError(t, err) + time.Sleep(250 * time.Millisecond) // offer propagation time + + // Have Alice query all the offer information back + aRPC := rpcclient.NewClient(ctx, fmt.Sprintf("http://127.0.0.1:%d", aliceConf.RPCPort)) + peersWithOffers, err := aRPC.QueryAll(coins.ProvidesXMR, 3) + require.NoError(t, err) + require.Len(t, peersWithOffers, 1) + require.Len(t, peersWithOffers[0].Offers, 1) + peerID := peersWithOffers[0].PeerID + offer := peersWithOffers[0].Offers[0] + tokenInfo, err := aRPC.TokenInfo(offer.EthAsset.Address()) + require.NoError(t, err) + providesAmt, err := exRate.ToERC20Amount(offer.MaxAmount, tokenInfo) + require.NoError(t, err) + + aliceStatusCh, err := ac.TakeOfferAndSubscribe(peerID, offer.ID, providesAmt) + require.NoError(t, err) + + var statusWG sync.WaitGroup + statusWG.Add(2) + + // Ensure Alice completes the swap successfully + go func() { + defer statusWG.Done() + for { + select { + case status := <-aliceStatusCh: + t.Log("> Alice got status:", status) + if !status.IsOngoing() { + assert.Equal(t, types.CompletedSuccess.String(), status.String()) + return + } + case <-ctx.Done(): + t.Errorf("Alice's context cancelled before she completed the swap") + return + } + } + }() + + // Test that Bob completes the swap successfully + go func() { + defer statusWG.Done() + for { + select { + case status := <-bobStatusCh: + t.Log("> Bob got status:", status) + if !status.IsOngoing() { + assert.Equal(t, types.CompletedSuccess.String(), status.String()) + return + } + case <-ctx.Done(): + t.Errorf("Bob's context cancelled before he completed the swap") + return + } + } + }() + + statusWG.Wait() + if t.Failed() { + return + } + + // + // Check Bob's token balance via RPC method instead of doing it directly + // + bRPC := rpcclient.NewClient(ctx, fmt.Sprintf("http://127.0.0.1:%d", bobConf.RPCPort)) + balances, err := bRPC.Balances(&rpctypes.BalancesRequest{TokenAddrs: []ethcommon.Address{tokenAddr}}) + require.NoError(t, err) + t.Logf("Balances: %#v", balances) + + require.NotEmpty(t, balances.TokenBalances) + require.Equal(t, providesAmt.Text('f'), balances.TokenBalances[0].AsStandardString()) +} diff --git a/daemon/swap_daemon_test.go b/daemon/swap_daemon_test.go index 77f9f2cc..fbc55d41 100644 --- a/daemon/swap_daemon_test.go +++ b/daemon/swap_daemon_test.go @@ -17,15 +17,12 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" logging "github.com/ipfs/go-log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/athanorlabs/atomic-swap/coins" - "github.com/athanorlabs/atomic-swap/common" "github.com/athanorlabs/atomic-swap/common/types" - contracts "github.com/athanorlabs/atomic-swap/ethereum" "github.com/athanorlabs/atomic-swap/ethereum/block" "github.com/athanorlabs/atomic-swap/ethereum/extethclient" "github.com/athanorlabs/atomic-swap/monero" @@ -60,30 +57,11 @@ func init() { _ = logging.SetLogLevel("protocol", level) _ = logging.SetLogLevel("relayer", level) // external and internal _ = logging.SetLogLevel("rpc", level) + _ = logging.SetLogLevel("txsender", level) _ = logging.SetLogLevel("xmrmaker", level) _ = logging.SetLogLevel("xmrtaker", level) } -var _swapCreatorAddr *ethcommon.Address - -func getSwapCreatorAddress(t *testing.T, ec *ethclient.Client) ethcommon.Address { - if _swapCreatorAddr != nil { - return *_swapCreatorAddr - } - - ctx := context.Background() - ethKey := tests.GetTakerTestKey(t) // requester might not have ETH, so we don't pass the key in - - forwarderAddr, err := contracts.DeployGSNForwarderWithKey(ctx, ec, ethKey) - require.NoError(t, err) - - swapCreatorAddr, _, err := contracts.DeploySwapCreatorWithKey(ctx, ec, ethKey, forwarderAddr) - require.NoError(t, err) - - _swapCreatorAddr = &swapCreatorAddr - return swapCreatorAddr -} - func privKeyToAddr(privKey *ecdsa.PrivateKey) ethcommon.Address { return crypto.PubkeyToAddress(*privKey.Public().(*ecdsa.PublicKey)) } @@ -144,73 +122,7 @@ func minimumFundAlice(t *testing.T, ec extethclient.EthClient, providesAmt *apd. bal, err := ec.Balance(context.Background()) require.NoError(t, err) - t.Logf("Alice's start balance is: %s ETH", coins.FmtWeiAsETH(bal)) -} - -func createTestConf(t *testing.T, ethKey *ecdsa.PrivateKey) *SwapdConfig { - ctx := context.Background() - ec, err := extethclient.NewEthClient(ctx, common.Development, common.DefaultEthEndpoint, ethKey) - require.NoError(t, err) - t.Cleanup(func() { - ec.Close() - }) - - rpcPort, err := common.GetFreeTCPPort() - require.NoError(t, err) - - // We need a copy of the environment conf, as it is no longer a singleton - // when we are testing it here. - envConf := new(common.Config) - *envConf = *common.ConfigDefaultsForEnv(common.Development) - envConf.DataDir = t.TempDir() - envConf.SwapCreatorAddr = getSwapCreatorAddress(t, ec.Raw()) - - return &SwapdConfig{ - EnvConf: envConf, - MoneroClient: monero.CreateWalletClient(t), - EthereumClient: ec, - Libp2pPort: 0, - Libp2pKeyfile: "", - RPCPort: uint16(rpcPort), - IsRelayer: false, - NoTransferBack: false, - } -} - -func launchDaemons(t *testing.T, timeout time.Duration, configs ...*SwapdConfig) context.Context { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - - var wg sync.WaitGroup - t.Cleanup(func() { - cancel() - wg.Wait() - }) - - var bootNodes []string // First daemon to launch has no bootnodes - - for n, conf := range configs { - - conf.EnvConf.Bootnodes = bootNodes - - wg.Add(1) - go func() { - defer wg.Done() - err := RunSwapDaemon(ctx, conf) - require.ErrorIs(t, err, context.Canceled) - }() - WaitForSwapdStart(t, conf.RPCPort) - - // Configure remaining daemons to use the first one a bootnode - if n == 0 { - c := rpcclient.NewClient(ctx, fmt.Sprintf("http://127.0.0.1:%d", conf.RPCPort)) - addresses, err := c.Addresses() - require.NoError(t, err) - require.Greater(t, len(addresses.Addrs), 1) - bootNodes = []string{addresses.Addrs[0]} - } - } - - return ctx + t.Logf("Alice's start balance is: %s ETH", bal.AsEtherString()) } // Tests the scenario, where Bob has no ETH, there are no advertised relayers in @@ -224,13 +136,13 @@ func TestRunSwapDaemon_SwapBobHasNoEth_AliceRelaysClaim(t *testing.T) { bobEthKey, err := crypto.GenerateKey() // Bob has no ETH (not a ganache key) require.NoError(t, err) - bobConf := createTestConf(t, bobEthKey) + bobConf := CreateTestConf(t, bobEthKey) monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR)) - aliceConf := createTestConf(t, tests.GetTakerTestKey(t)) + aliceConf := CreateTestConf(t, tests.GetTakerTestKey(t)) timeout := 7 * time.Minute - ctx := launchDaemons(t, timeout, bobConf, aliceConf) + ctx := LaunchDaemons(t, timeout, bobConf, aliceConf) bc, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", bobConf.RPCPort)) require.NoError(t, err) @@ -298,7 +210,7 @@ func TestRunSwapDaemon_SwapBobHasNoEth_AliceRelaysClaim(t *testing.T) { bobBalance, err := bobConf.EthereumClient.Balance(ctx) require.NoError(t, err) - require.Equal(t, expectedBal.Text('f'), coins.FmtWeiAsETH(bobBalance)) + require.Equal(t, expectedBal.Text('f'), bobBalance.AsEtherString()) } // Tests the scenario where Bob has no ETH, he can't find an advertised relayer, @@ -314,16 +226,16 @@ func TestRunSwapDaemon_NoRelayersAvailable_Refund(t *testing.T) { bobEthKey, err := crypto.GenerateKey() // Bob has no ETH (not a ganache key) require.NoError(t, err) - bobConf := createTestConf(t, bobEthKey) + bobConf := CreateTestConf(t, bobEthKey) monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR)) aliceEthKey, err := crypto.GenerateKey() // Alice has non-ganache key that we fund require.NoError(t, err) - aliceConf := createTestConf(t, aliceEthKey) + aliceConf := CreateTestConf(t, aliceEthKey) minimumFundAlice(t, aliceConf.EthereumClient, providesAmt) timeout := 8 * time.Minute - ctx := launchDaemons(t, timeout, bobConf, aliceConf) + ctx := LaunchDaemons(t, timeout, bobConf, aliceConf) bc, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", bobConf.RPCPort)) require.NoError(t, err) @@ -392,23 +304,23 @@ func TestRunSwapDaemon_CharlieRelays(t *testing.T) { bobEthKey, err := crypto.GenerateKey() // Bob has no ETH (not a ganache key) require.NoError(t, err) - bobConf := createTestConf(t, bobEthKey) + bobConf := CreateTestConf(t, bobEthKey) monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR)) // Configure Alice with enough funds to complete the swap, but not to relay Bob's claim aliceEthKey, err := crypto.GenerateKey() // Alice gets a key without enough funds to relay require.NoError(t, err) - aliceConf := createTestConf(t, aliceEthKey) + aliceConf := CreateTestConf(t, aliceEthKey) minimumFundAlice(t, aliceConf.EthereumClient, providesAmt) // Charlie can safely use the taker key, as Alice is not using it. - charlieConf := createTestConf(t, tests.GetTakerTestKey(t)) + charlieConf := CreateTestConf(t, tests.GetTakerTestKey(t)) charlieConf.IsRelayer = true charlieStartBal, err := charlieConf.EthereumClient.Balance(context.Background()) require.NoError(t, err) timeout := 7 * time.Minute - ctx := launchDaemons(t, timeout, bobConf, aliceConf, charlieConf) + ctx := LaunchDaemons(t, timeout, bobConf, aliceConf, charlieConf) bc, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", bobConf.RPCPort)) require.NoError(t, err) @@ -474,7 +386,7 @@ func TestRunSwapDaemon_CharlieRelays(t *testing.T) { require.NoError(t, err) bobBalance, err := bobConf.EthereumClient.Balance(ctx) require.NoError(t, err) - require.Equal(t, bobExpectedBal.Text('f'), coins.FmtWeiAsETH(bobBalance)) + require.Equal(t, bobExpectedBal.Text('f'), bobBalance.AsEtherString()) // // Charlie should be wealthier now than at the start, despite paying the claim @@ -484,8 +396,8 @@ func TestRunSwapDaemon_CharlieRelays(t *testing.T) { charlieBal, err := charlieEC.Balance(ctx) require.NoError(t, err) require.Greater(t, charlieBal.Cmp(charlieStartBal), 0) - charlieProfitWei := new(big.Int).Sub(charlieBal, charlieStartBal) - t.Logf("Charlie earned %s ETH", coins.FmtWeiAsETH(charlieProfitWei)) + charlieProfitWei := charlieBal.Sub(charlieStartBal) + t.Logf("Charlie earned %s ETH", charlieProfitWei.AsEtherString()) } // Tests the scenario where Charlie, an advertised relayer, has run out of ETH @@ -500,20 +412,20 @@ func TestRunSwapDaemon_CharlieIsBroke_AliceRelays(t *testing.T) { bobEthKey, err := crypto.GenerateKey() // Bob has no ETH (not a ganache key) require.NoError(t, err) - bobConf := createTestConf(t, bobEthKey) + bobConf := CreateTestConf(t, bobEthKey) monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR)) // Alice is fully funded with the taker key - aliceConf := createTestConf(t, tests.GetTakerTestKey(t)) + aliceConf := CreateTestConf(t, tests.GetTakerTestKey(t)) // Charlie is a relayer, but he has no ETH charlieEthKey, err := crypto.GenerateKey() require.NoError(t, err) - charlieConf := createTestConf(t, charlieEthKey) + charlieConf := CreateTestConf(t, charlieEthKey) charlieConf.IsRelayer = true timeout := 7 * time.Minute - ctx := launchDaemons(t, timeout, bobConf, aliceConf, charlieConf) + ctx := LaunchDaemons(t, timeout, bobConf, aliceConf, charlieConf) bc, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", bobConf.RPCPort)) require.NoError(t, err) @@ -579,15 +491,15 @@ func TestRunSwapDaemon_CharlieIsBroke_AliceRelays(t *testing.T) { require.NoError(t, err) bobBalance, err := bobConf.EthereumClient.Balance(ctx) require.NoError(t, err) - require.Equal(t, bobExpectedBal.Text('f'), coins.FmtWeiAsETH(bobBalance)) + require.Equal(t, bobExpectedBal.Text('f'), bobBalance.AsEtherString()) } // Tests the version and shutdown RPC methods func TestRunSwapDaemon_RPC_Version(t *testing.T) { - conf := createTestConf(t, tests.GetMakerTestKey(t)) + conf := CreateTestConf(t, tests.GetMakerTestKey(t)) protocolVersion := fmt.Sprintf("%s/%d", net.ProtocolID, conf.EthereumClient.ChainID()) timeout := time.Minute - ctx := launchDaemons(t, timeout, conf) + ctx := LaunchDaemons(t, timeout, conf) c := rpcclient.NewClient(ctx, fmt.Sprintf("http://127.0.0.1:%d", conf.RPCPort)) versionResp, err := c.Version() @@ -601,9 +513,9 @@ func TestRunSwapDaemon_RPC_Version(t *testing.T) { // Tests the shutdown RPC method func TestRunSwapDaemon_RPC_Shutdown(t *testing.T) { - conf := createTestConf(t, tests.GetMakerTestKey(t)) + conf := CreateTestConf(t, tests.GetMakerTestKey(t)) timeout := time.Minute - ctx := launchDaemons(t, timeout, conf) + ctx := LaunchDaemons(t, timeout, conf) c := rpcclient.NewClient(ctx, fmt.Sprintf("http://127.0.0.1:%d", conf.RPCPort)) err := c.Shutdown() diff --git a/daemon/test_support.go b/daemon/test_support.go index be5558ed..14b9f45a 100644 --- a/daemon/test_support.go +++ b/daemon/test_support.go @@ -6,18 +6,109 @@ package daemon import ( + "context" + "crypto/ecdsa" "fmt" + "math/big" "net" + "sync" "syscall" "testing" "time" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" + + "github.com/athanorlabs/atomic-swap/common" + contracts "github.com/athanorlabs/atomic-swap/ethereum" + "github.com/athanorlabs/atomic-swap/ethereum/extethclient" + "github.com/athanorlabs/atomic-swap/monero" + "github.com/athanorlabs/atomic-swap/rpcclient" + "github.com/athanorlabs/atomic-swap/tests" +) + +// map indexes for our mock tokens +const ( + MockDAI = "DAI" + MockTether = "USDT" ) // This file is only for test support. Use the build tag "prod" to prevent // symbols in this file from consuming space in production binaries. +// CreateTestConf creates a localhost-only dev environment SwapdConfig config +// for testing +func CreateTestConf(t *testing.T, ethKey *ecdsa.PrivateKey) *SwapdConfig { + ctx := context.Background() + ec, err := extethclient.NewEthClient(ctx, common.Development, common.DefaultEthEndpoint, ethKey) + require.NoError(t, err) + t.Cleanup(func() { + ec.Close() + }) + + rpcPort, err := common.GetFreeTCPPort() + require.NoError(t, err) + + // We need a copy of the environment conf, as it is no longer a singleton + // when we are testing it here. + envConf := new(common.Config) + *envConf = *common.ConfigDefaultsForEnv(common.Development) + envConf.DataDir = t.TempDir() + envConf.SwapCreatorAddr = getSwapCreatorAddress(t, ec.Raw()) + + return &SwapdConfig{ + EnvConf: envConf, + MoneroClient: monero.CreateWalletClient(t), + EthereumClient: ec, + Libp2pPort: 0, + Libp2pKeyfile: "", + RPCPort: uint16(rpcPort), + IsRelayer: false, + NoTransferBack: false, + } +} + +// LaunchDaemons launches one or more swapd daemons and blocks until they are +// started. If more than one config is passed, the bootnode settings of the +// passed config are modified to make the first daemon the bootnode for the +// remaining daemons. +func LaunchDaemons(t *testing.T, timeout time.Duration, configs ...*SwapdConfig) context.Context { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + + var wg sync.WaitGroup + t.Cleanup(func() { + cancel() + wg.Wait() + }) + + var bootNodes []string // First daemon to launch has no bootnodes + + for n, conf := range configs { + + conf.EnvConf.Bootnodes = bootNodes + + wg.Add(1) + go func() { + defer wg.Done() + err := RunSwapDaemon(ctx, conf) + require.ErrorIs(t, err, context.Canceled) + }() + WaitForSwapdStart(t, conf.RPCPort) + + // Configure remaining daemons to use the first one a bootnode + if n == 0 { + c := rpcclient.NewClient(ctx, fmt.Sprintf("http://127.0.0.1:%d", conf.RPCPort)) + addresses, err := c.Addresses() + require.NoError(t, err) + require.Greater(t, len(addresses.Addrs), 1) + bootNodes = []string{addresses.Addrs[0]} + } + } + + return ctx +} + // WaitForSwapdStart takes the rpcPort of a swapd instance and waits for it to // be in a listening state. Fails the test if the server isn't listening after a // little over 60 seconds. @@ -42,3 +133,83 @@ func WaitForSwapdStart(t *testing.T, rpcPort uint16) { } t.Fatalf("giving up, swapd RPC port %d is not listening after %d seconds", rpcPort, maxSeconds) } + +// these variables are only for use by getSwapCreatorAddress +var _swapCreatorAddr *ethcommon.Address +var _swapCreatorAddrMu sync.Mutex + +func getSwapCreatorAddress(t *testing.T, ec *ethclient.Client) ethcommon.Address { + _swapCreatorAddrMu.Lock() + defer _swapCreatorAddrMu.Unlock() + + if _swapCreatorAddr != nil { + return *_swapCreatorAddr + } + + ctx := context.Background() + ethKey := tests.GetTakerTestKey(t) // requester might not have ETH, so we don't pass the key in + + forwarderAddr, err := contracts.DeployGSNForwarderWithKey(ctx, ec, ethKey) + require.NoError(t, err) + + swapCreatorAddr, _, err := contracts.DeploySwapCreatorWithKey(ctx, ec, ethKey, forwarderAddr) + require.NoError(t, err) + + _swapCreatorAddr = &swapCreatorAddr + return swapCreatorAddr +} + +// these variables are only for use by GetMockTokens +var _mockTokens map[string]ethcommon.Address +var _mockTokensMu sync.Mutex + +// GetMockTokens returns a symbol=>address map of our mock ERC20 tokens, +// deploying them if they haven't already been deployed. Use the constants +// defined earlier to access the map elements. +func GetMockTokens(t *testing.T, ec extethclient.EthClient) map[string]ethcommon.Address { + _mockTokensMu.Lock() + defer _mockTokensMu.Unlock() + + if _mockTokens == nil { + _mockTokens = make(map[string]ethcommon.Address) + } + + calcSupply := func(numStdUnits int64, decimals int64) *big.Int { + return new(big.Int).Mul(big.NewInt(numStdUnits), new(big.Int).Exp(big.NewInt(10), big.NewInt(decimals), nil)) + } + + ctx := context.Background() + txOpts, err := ec.TxOpts(ctx) + require.NoError(t, err) + + mockDaiAddr, mockDaiTx, _, err := contracts.DeployERC20Mock( + txOpts, + ec.Raw(), + "Dai Stablecoin", + "DAI", + 18, + ec.Address(), + calcSupply(1000, 18), + ) + require.NoError(t, err) + tests.MineTransaction(t, ec.Raw(), mockDaiTx) + _mockTokens[MockDAI] = mockDaiAddr + + txOpts, err = ec.TxOpts(ctx) + require.NoError(t, err) + + mockTetherAddr, mockTetherTx, _, err := contracts.DeployERC20Mock( + txOpts, + ec.Raw(), + "Tether USD", + "USDT", + 6, + ec.Address(), + calcSupply(1000, 6), + ) + require.NoError(t, err) + tests.MineTransaction(t, ec.Raw(), mockTetherTx) + _mockTokens[MockTether] = mockTetherAddr + + return _mockTokens +} diff --git a/db/database_test.go b/db/database_test.go index 65b40fbc..086eaee2 100644 --- a/db/database_test.go +++ b/db/database_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ChainSafe/chaindb" + ethcommon "github.com/ethereum/go-ethereum/common" logging "github.com/ipfs/go-log" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/require" @@ -174,8 +175,9 @@ func TestDatabase_SwapTable(t *testing.T) { one := coins.StrToDecimal("1") oneEx := coins.ToExchangeRate(one) + ethAsset := types.EthAsset(ethcommon.HexToAddress("0xa1E32d14AC4B6d8c1791CAe8E9baD46a1E15B7a8")) - offerA := types.NewOffer(coins.ProvidesXMR, one, one, oneEx, types.EthAssetETH) + offerA := types.NewOffer(coins.ProvidesXMR, one, one, oneEx, ethAsset) err = db.PutOffer(offerA) require.NoError(t, err) @@ -211,7 +213,7 @@ func TestDatabase_SwapTable(t *testing.T) { ProvidedAmount: coins.StrToDecimal("1.5"), ExpectedAmount: coins.StrToDecimal("0.15"), ExchangeRate: coins.ToExchangeRate(coins.StrToDecimal("0.1")), - EthAsset: types.EthAsset{}, + EthAsset: ethAsset, Status: types.XMRLocked, LastStatusUpdateTime: time.Now(), MoneroStartHeight: 12345, diff --git a/ethereum/check_swap_creator_contract_test.go b/ethereum/check_swap_creator_contract_test.go index 42f3603b..c0d0309a 100644 --- a/ethereum/check_swap_creator_contract_test.go +++ b/ethereum/check_swap_creator_contract_test.go @@ -8,6 +8,7 @@ import ( "context" "crypto/ecdsa" "errors" + "os" "testing" ethcrypto "github.com/ethereum/go-ethereum/crypto" @@ -123,8 +124,7 @@ func TestCheckSwapCreatorContractCode_fail(t *testing.T) { } func TestSepoliaContract(t *testing.T) { - // TODO: CI's ETH_SEPOLIA_ENDPOINT is giving 404 errors - endpoint := "" // os.Getenv("ETH_SEPOLIA_ENDPOINT") + endpoint := os.Getenv("ETH_SEPOLIA_ENDPOINT") if endpoint == "" { endpoint = "https://rpc.sepolia.org/" } diff --git a/ethereum/contracts/ERC20Mock.sol b/ethereum/contracts/ERC20Mock.sol index 7a5dcb57..9e178b36 100644 --- a/ethereum/contracts/ERC20Mock.sol +++ b/ethereum/contracts/ERC20Mock.sol @@ -6,15 +6,23 @@ import {ERC20} from "./ERC20.sol"; // mock class using ERC20 contract ERC20Mock is ERC20 { + uint8 private _decimals; + constructor( string memory name, string memory symbol, + uint8 numDecimals, address initialAccount, uint256 initialBalance ) payable ERC20(name, symbol) { + _decimals = numDecimals; _mint(initialAccount, initialBalance); } + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + function mint(address account, uint256 amount) public { _mint(account, amount); } diff --git a/ethereum/erc20_mock.go b/ethereum/erc20_mock.go index 136c77f4..0fb8f1d6 100644 --- a/ethereum/erc20_mock.go +++ b/ethereum/erc20_mock.go @@ -31,8 +31,8 @@ var ( // ERC20MockMetaData contains all meta data concerning the ERC20Mock contract. var ERC20MockMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"symbol\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"initialAccount\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"initialBalance\",\"type\":\"uint256\"}],\"stateMutability\":\"payable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"approveInternal\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"burn\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"subtractedValue\",\"type\":\"uint256\"}],\"name\":\"decreaseAllowance\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"addedValue\",\"type\":\"uint256\"}],\"name\":\"increaseAllowance\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"mint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transferInternal\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", - Bin: "0x6080604052604051620020f4380380620020f4833981810160405281019062000029919062000417565b838381600390816200003c919062000708565b5080600490816200004e919062000708565b5050506200006382826200006d60201b60201c565b505050506200090a565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603620000df576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401620000d69062000850565b60405180910390fd5b620000f360008383620001da60201b60201c565b8060026000828254620001079190620008a1565b92505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051620001ba9190620008ed565b60405180910390a3620001d660008383620001df60201b60201c565b5050565b505050565b505050565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6200024d8262000202565b810181811067ffffffffffffffff821117156200026f576200026e62000213565b5b80604052505050565b600062000284620001e4565b905062000292828262000242565b919050565b600067ffffffffffffffff821115620002b557620002b462000213565b5b620002c08262000202565b9050602081019050919050565b60005b83811015620002ed578082015181840152602081019050620002d0565b60008484015250505050565b6000620003106200030a8462000297565b62000278565b9050828152602081018484840111156200032f576200032e620001fd565b5b6200033c848285620002cd565b509392505050565b600082601f8301126200035c576200035b620001f8565b5b81516200036e848260208601620002f9565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000620003a48262000377565b9050919050565b620003b68162000397565b8114620003c257600080fd5b50565b600081519050620003d681620003ab565b92915050565b6000819050919050565b620003f181620003dc565b8114620003fd57600080fd5b50565b6000815190506200041181620003e6565b92915050565b60008060008060808587031215620004345762000433620001ee565b5b600085015167ffffffffffffffff811115620004555762000454620001f3565b5b620004638782880162000344565b945050602085015167ffffffffffffffff811115620004875762000486620001f3565b5b620004958782880162000344565b9350506040620004a887828801620003c5565b9250506060620004bb8782880162000400565b91505092959194509250565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200051a57607f821691505b60208210810362000530576200052f620004d2565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026200059a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff826200055b565b620005a686836200055b565b95508019841693508086168417925050509392505050565b6000819050919050565b6000620005e9620005e3620005dd84620003dc565b620005be565b620003dc565b9050919050565b6000819050919050565b6200060583620005c8565b6200061d6200061482620005f0565b84845462000568565b825550505050565b600090565b6200063462000625565b62000641818484620005fa565b505050565b5b8181101562000669576200065d6000826200062a565b60018101905062000647565b5050565b601f821115620006b857620006828162000536565b6200068d846200054b565b810160208510156200069d578190505b620006b5620006ac856200054b565b83018262000646565b50505b505050565b600082821c905092915050565b6000620006dd60001984600802620006bd565b1980831691505092915050565b6000620006f88383620006ca565b9150826002028217905092915050565b6200071382620004c7565b67ffffffffffffffff8111156200072f576200072e62000213565b5b6200073b825462000501565b620007488282856200066d565b600060209050601f8311600181146200078057600084156200076b578287015190505b620007778582620006ea565b865550620007e7565b601f198416620007908662000536565b60005b82811015620007ba5784890151825560018201915060208501945060208101905062000793565b86831015620007da5784890151620007d6601f891682620006ca565b8355505b6001600288020188555050505b505050505050565b600082825260208201905092915050565b7f45524332303a206d696e7420746f20746865207a65726f206164647265737300600082015250565b600062000838601f83620007ef565b9150620008458262000800565b602082019050919050565b600060208201905081810360008301526200086b8162000829565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000620008ae82620003dc565b9150620008bb83620003dc565b9250828201905080821115620008d657620008d562000872565b5b92915050565b620008e781620003dc565b82525050565b6000602082019050620009046000830184620008dc565b92915050565b6117da806200091a6000396000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c806340c10f19116100975780639dc29fac116100665780639dc29fac14610286578063a457c2d7146102a2578063a9059cbb146102d2578063dd62ed3e14610302576100f5565b806340c10f191461020057806356189cb41461021c57806370a082311461023857806395d89b4114610268576100f5565b8063222f5be0116100d3578063222f5be01461016657806323b872dd14610182578063313ce567146101b257806339509351146101d0576100f5565b806306fdde03146100fa578063095ea7b31461011857806318160ddd14610148575b600080fd5b610102610332565b60405161010f9190610f27565b60405180910390f35b610132600480360381019061012d9190610fe2565b6103c4565b60405161013f919061103d565b60405180910390f35b6101506103e7565b60405161015d9190611067565b60405180910390f35b610180600480360381019061017b9190611082565b6103f1565b005b61019c60048036038101906101979190611082565b610401565b6040516101a9919061103d565b60405180910390f35b6101ba610430565b6040516101c791906110f1565b60405180910390f35b6101ea60048036038101906101e59190610fe2565b610439565b6040516101f7919061103d565b60405180910390f35b61021a60048036038101906102159190610fe2565b610470565b005b61023660048036038101906102319190611082565b61047e565b005b610252600480360381019061024d919061110c565b61048e565b60405161025f9190611067565b60405180910390f35b6102706104d6565b60405161027d9190610f27565b60405180910390f35b6102a0600480360381019061029b9190610fe2565b610568565b005b6102bc60048036038101906102b79190610fe2565b610576565b6040516102c9919061103d565b60405180910390f35b6102ec60048036038101906102e79190610fe2565b6105ed565b6040516102f9919061103d565b60405180910390f35b61031c60048036038101906103179190611139565b610610565b6040516103299190611067565b60405180910390f35b606060038054610341906111a8565b80601f016020809104026020016040519081016040528092919081815260200182805461036d906111a8565b80156103ba5780601f1061038f576101008083540402835291602001916103ba565b820191906000526020600020905b81548152906001019060200180831161039d57829003601f168201915b5050505050905090565b6000806103cf610697565b90506103dc81858561069f565b600191505092915050565b6000600254905090565b6103fc838383610868565b505050565b60008061040c610697565b9050610419858285610ade565b610424858585610868565b60019150509392505050565b60006012905090565b600080610444610697565b90506104658185856104568589610610565b6104609190611208565b61069f565b600191505092915050565b61047a8282610b6a565b5050565b61048983838361069f565b505050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6060600480546104e5906111a8565b80601f0160208091040260200160405190810160405280929190818152602001828054610511906111a8565b801561055e5780601f106105335761010080835404028352916020019161055e565b820191906000526020600020905b81548152906001019060200180831161054157829003601f168201915b5050505050905090565b6105728282610cc0565b5050565b600080610581610697565b9050600061058f8286610610565b9050838110156105d4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016105cb906112ae565b60405180910390fd5b6105e1828686840361069f565b60019250505092915050565b6000806105f8610697565b9050610605818585610868565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361070e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161070590611340565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361077d576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610774906113d2565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258360405161085b9190611067565b60405180910390a3505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108d7576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108ce90611464565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610946576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161093d906114f6565b60405180910390fd5b610951838383610e8d565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156109d7576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109ce90611588565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610ac59190611067565b60405180910390a3610ad8848484610e92565b50505050565b6000610aea8484610610565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610b645781811015610b56576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b4d906115f4565b60405180910390fd5b610b63848484840361069f565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610bd9576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610bd090611660565b60405180910390fd5b610be560008383610e8d565b8060026000828254610bf79190611208565b92505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610ca89190611067565b60405180910390a3610cbc60008383610e92565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610d2f576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610d26906116f2565b60405180910390fd5b610d3b82600083610e8d565b60008060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610dc1576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610db890611784565b60405180910390fd5b8181036000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555081600260008282540392505081905550600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610e749190611067565b60405180910390a3610e8883600084610e92565b505050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610ed1578082015181840152602081019050610eb6565b60008484015250505050565b6000601f19601f8301169050919050565b6000610ef982610e97565b610f038185610ea2565b9350610f13818560208601610eb3565b610f1c81610edd565b840191505092915050565b60006020820190508181036000830152610f418184610eee565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610f7982610f4e565b9050919050565b610f8981610f6e565b8114610f9457600080fd5b50565b600081359050610fa681610f80565b92915050565b6000819050919050565b610fbf81610fac565b8114610fca57600080fd5b50565b600081359050610fdc81610fb6565b92915050565b60008060408385031215610ff957610ff8610f49565b5b600061100785828601610f97565b925050602061101885828601610fcd565b9150509250929050565b60008115159050919050565b61103781611022565b82525050565b6000602082019050611052600083018461102e565b92915050565b61106181610fac565b82525050565b600060208201905061107c6000830184611058565b92915050565b60008060006060848603121561109b5761109a610f49565b5b60006110a986828701610f97565b93505060206110ba86828701610f97565b92505060406110cb86828701610fcd565b9150509250925092565b600060ff82169050919050565b6110eb816110d5565b82525050565b600060208201905061110660008301846110e2565b92915050565b60006020828403121561112257611121610f49565b5b600061113084828501610f97565b91505092915050565b600080604083850312156111505761114f610f49565b5b600061115e85828601610f97565b925050602061116f85828601610f97565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806111c057607f821691505b6020821081036111d3576111d2611179565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061121382610fac565b915061121e83610fac565b9250828201905080821115611236576112356111d9565b5b92915050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000611298602583610ea2565b91506112a38261123c565b604082019050919050565b600060208201905081810360008301526112c78161128b565b9050919050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b600061132a602483610ea2565b9150611335826112ce565b604082019050919050565b600060208201905081810360008301526113598161131d565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b60006113bc602283610ea2565b91506113c782611360565b604082019050919050565b600060208201905081810360008301526113eb816113af565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b600061144e602583610ea2565b9150611459826113f2565b604082019050919050565b6000602082019050818103600083015261147d81611441565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b60006114e0602383610ea2565b91506114eb82611484565b604082019050919050565b6000602082019050818103600083015261150f816114d3565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b6000611572602683610ea2565b915061157d82611516565b604082019050919050565b600060208201905081810360008301526115a181611565565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b60006115de601d83610ea2565b91506115e9826115a8565b602082019050919050565b6000602082019050818103600083015261160d816115d1565b9050919050565b7f45524332303a206d696e7420746f20746865207a65726f206164647265737300600082015250565b600061164a601f83610ea2565b915061165582611614565b602082019050919050565b600060208201905081810360008301526116798161163d565b9050919050565b7f45524332303a206275726e2066726f6d20746865207a65726f2061646472657360008201527f7300000000000000000000000000000000000000000000000000000000000000602082015250565b60006116dc602183610ea2565b91506116e782611680565b604082019050919050565b6000602082019050818103600083015261170b816116cf565b9050919050565b7f45524332303a206275726e20616d6f756e7420657863656564732062616c616e60008201527f6365000000000000000000000000000000000000000000000000000000000000602082015250565b600061176e602283610ea2565b915061177982611712565b604082019050919050565b6000602082019050818103600083015261179d81611761565b905091905056fea2646970667358221220af9e01ce81a57fddb609151c618fc94dec5e4a2921f9625373b205f1b2b92fc764736f6c63430008130033", + ABI: "[{\"inputs\":[{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"symbol\",\"type\":\"string\"},{\"internalType\":\"uint8\",\"name\":\"numDecimals\",\"type\":\"uint8\"},{\"internalType\":\"address\",\"name\":\"initialAccount\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"initialBalance\",\"type\":\"uint256\"}],\"stateMutability\":\"payable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"approveInternal\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"burn\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"subtractedValue\",\"type\":\"uint256\"}],\"name\":\"decreaseAllowance\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"addedValue\",\"type\":\"uint256\"}],\"name\":\"increaseAllowance\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"mint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"transferInternal\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + Bin: "0x60806040526040516200217238038062002172833981810160405281019062000029919062000471565b848481600390816200003c919062000778565b5080600490816200004e919062000778565b50505082600560006101000a81548160ff021916908360ff1602179055506200007e82826200008960201b60201c565b50505050506200097a565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603620000fb576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401620000f290620008c0565b60405180910390fd5b6200010f60008383620001f660201b60201c565b806002600082825462000123919062000911565b92505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051620001d691906200095d565b60405180910390a3620001f260008383620001fb60201b60201c565b5050565b505050565b505050565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b62000269826200021e565b810181811067ffffffffffffffff821117156200028b576200028a6200022f565b5b80604052505050565b6000620002a062000200565b9050620002ae82826200025e565b919050565b600067ffffffffffffffff821115620002d157620002d06200022f565b5b620002dc826200021e565b9050602081019050919050565b60005b8381101562000309578082015181840152602081019050620002ec565b60008484015250505050565b60006200032c6200032684620002b3565b62000294565b9050828152602081018484840111156200034b576200034a62000219565b5b62000358848285620002e9565b509392505050565b600082601f83011262000378576200037762000214565b5b81516200038a84826020860162000315565b91505092915050565b600060ff82169050919050565b620003ab8162000393565b8114620003b757600080fd5b50565b600081519050620003cb81620003a0565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000620003fe82620003d1565b9050919050565b6200041081620003f1565b81146200041c57600080fd5b50565b600081519050620004308162000405565b92915050565b6000819050919050565b6200044b8162000436565b81146200045757600080fd5b50565b6000815190506200046b8162000440565b92915050565b600080600080600060a0868803121562000490576200048f6200020a565b5b600086015167ffffffffffffffff811115620004b157620004b06200020f565b5b620004bf8882890162000360565b955050602086015167ffffffffffffffff811115620004e357620004e26200020f565b5b620004f18882890162000360565b94505060406200050488828901620003ba565b935050606062000517888289016200041f565b92505060806200052a888289016200045a565b9150509295509295909350565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200058a57607f821691505b602082108103620005a0576200059f62000542565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b6000600883026200060a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82620005cb565b620006168683620005cb565b95508019841693508086168417925050509392505050565b6000819050919050565b600062000659620006536200064d8462000436565b6200062e565b62000436565b9050919050565b6000819050919050565b620006758362000638565b6200068d620006848262000660565b848454620005d8565b825550505050565b600090565b620006a462000695565b620006b18184846200066a565b505050565b5b81811015620006d957620006cd6000826200069a565b600181019050620006b7565b5050565b601f8211156200072857620006f281620005a6565b620006fd84620005bb565b810160208510156200070d578190505b620007256200071c85620005bb565b830182620006b6565b50505b505050565b600082821c905092915050565b60006200074d600019846008026200072d565b1980831691505092915050565b60006200076883836200073a565b9150826002028217905092915050565b620007838262000537565b67ffffffffffffffff8111156200079f576200079e6200022f565b5b620007ab825462000571565b620007b8828285620006dd565b600060209050601f831160018114620007f05760008415620007db578287015190505b620007e785826200075a565b86555062000857565b601f1984166200080086620005a6565b60005b828110156200082a5784890151825560018201915060208501945060208101905062000803565b868310156200084a578489015162000846601f8916826200073a565b8355505b6001600288020188555050505b505050505050565b600082825260208201905092915050565b7f45524332303a206d696e7420746f20746865207a65726f206164647265737300600082015250565b6000620008a8601f836200085f565b9150620008b58262000870565b602082019050919050565b60006020820190508181036000830152620008db8162000899565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006200091e8262000436565b91506200092b8362000436565b9250828201905080821115620009465762000945620008e2565b5b92915050565b620009578162000436565b82525050565b60006020820190506200097460008301846200094c565b92915050565b6117e8806200098a6000396000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c806340c10f19116100975780639dc29fac116100665780639dc29fac14610286578063a457c2d7146102a2578063a9059cbb146102d2578063dd62ed3e14610302576100f5565b806340c10f191461020057806356189cb41461021c57806370a082311461023857806395d89b4114610268576100f5565b8063222f5be0116100d3578063222f5be01461016657806323b872dd14610182578063313ce567146101b257806339509351146101d0576100f5565b806306fdde03146100fa578063095ea7b31461011857806318160ddd14610148575b600080fd5b610102610332565b60405161010f9190610f35565b60405180910390f35b610132600480360381019061012d9190610ff0565b6103c4565b60405161013f919061104b565b60405180910390f35b6101506103e7565b60405161015d9190611075565b60405180910390f35b610180600480360381019061017b9190611090565b6103f1565b005b61019c60048036038101906101979190611090565b610401565b6040516101a9919061104b565b60405180910390f35b6101ba610430565b6040516101c791906110ff565b60405180910390f35b6101ea60048036038101906101e59190610ff0565b610447565b6040516101f7919061104b565b60405180910390f35b61021a60048036038101906102159190610ff0565b61047e565b005b61023660048036038101906102319190611090565b61048c565b005b610252600480360381019061024d919061111a565b61049c565b60405161025f9190611075565b60405180910390f35b6102706104e4565b60405161027d9190610f35565b60405180910390f35b6102a0600480360381019061029b9190610ff0565b610576565b005b6102bc60048036038101906102b79190610ff0565b610584565b6040516102c9919061104b565b60405180910390f35b6102ec60048036038101906102e79190610ff0565b6105fb565b6040516102f9919061104b565b60405180910390f35b61031c60048036038101906103179190611147565b61061e565b6040516103299190611075565b60405180910390f35b606060038054610341906111b6565b80601f016020809104026020016040519081016040528092919081815260200182805461036d906111b6565b80156103ba5780601f1061038f576101008083540402835291602001916103ba565b820191906000526020600020905b81548152906001019060200180831161039d57829003601f168201915b5050505050905090565b6000806103cf6106a5565b90506103dc8185856106ad565b600191505092915050565b6000600254905090565b6103fc838383610876565b505050565b60008061040c6106a5565b9050610419858285610aec565b610424858585610876565b60019150509392505050565b6000600560009054906101000a900460ff16905090565b6000806104526106a5565b9050610473818585610464858961061e565b61046e9190611216565b6106ad565b600191505092915050565b6104888282610b78565b5050565b6104978383836106ad565b505050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6060600480546104f3906111b6565b80601f016020809104026020016040519081016040528092919081815260200182805461051f906111b6565b801561056c5780601f106105415761010080835404028352916020019161056c565b820191906000526020600020905b81548152906001019060200180831161054f57829003601f168201915b5050505050905090565b6105808282610cce565b5050565b60008061058f6106a5565b9050600061059d828661061e565b9050838110156105e2576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016105d9906112bc565b60405180910390fd5b6105ef82868684036106ad565b60019250505092915050565b6000806106066106a5565b9050610613818585610876565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361071c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107139061134e565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361078b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610782906113e0565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040516108699190611075565b60405180910390a3505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108e5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108dc90611472565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610954576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161094b90611504565b60405180910390fd5b61095f838383610e9b565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156109e5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109dc90611596565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610ad39190611075565b60405180910390a3610ae6848484610ea0565b50505050565b6000610af8848461061e565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610b725781811015610b64576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b5b90611602565b60405180910390fd5b610b7184848484036106ad565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610be7576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610bde9061166e565b60405180910390fd5b610bf360008383610e9b565b8060026000828254610c059190611216565b92505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610cb69190611075565b60405180910390a3610cca60008383610ea0565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610d3d576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610d3490611700565b60405180910390fd5b610d4982600083610e9b565b60008060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015610dcf576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610dc690611792565b60405180910390fd5b8181036000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555081600260008282540392505081905550600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610e829190611075565b60405180910390a3610e9683600084610ea0565b505050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610edf578082015181840152602081019050610ec4565b60008484015250505050565b6000601f19601f8301169050919050565b6000610f0782610ea5565b610f118185610eb0565b9350610f21818560208601610ec1565b610f2a81610eeb565b840191505092915050565b60006020820190508181036000830152610f4f8184610efc565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610f8782610f5c565b9050919050565b610f9781610f7c565b8114610fa257600080fd5b50565b600081359050610fb481610f8e565b92915050565b6000819050919050565b610fcd81610fba565b8114610fd857600080fd5b50565b600081359050610fea81610fc4565b92915050565b6000806040838503121561100757611006610f57565b5b600061101585828601610fa5565b925050602061102685828601610fdb565b9150509250929050565b60008115159050919050565b61104581611030565b82525050565b6000602082019050611060600083018461103c565b92915050565b61106f81610fba565b82525050565b600060208201905061108a6000830184611066565b92915050565b6000806000606084860312156110a9576110a8610f57565b5b60006110b786828701610fa5565b93505060206110c886828701610fa5565b92505060406110d986828701610fdb565b9150509250925092565b600060ff82169050919050565b6110f9816110e3565b82525050565b600060208201905061111460008301846110f0565b92915050565b6000602082840312156111305761112f610f57565b5b600061113e84828501610fa5565b91505092915050565b6000806040838503121561115e5761115d610f57565b5b600061116c85828601610fa5565b925050602061117d85828601610fa5565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806111ce57607f821691505b6020821081036111e1576111e0611187565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061122182610fba565b915061122c83610fba565b9250828201905080821115611244576112436111e7565b5b92915050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b60006112a6602583610eb0565b91506112b18261124a565b604082019050919050565b600060208201905081810360008301526112d581611299565b9050919050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b6000611338602483610eb0565b9150611343826112dc565b604082019050919050565b600060208201905081810360008301526113678161132b565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b60006113ca602283610eb0565b91506113d58261136e565b604082019050919050565b600060208201905081810360008301526113f9816113bd565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b600061145c602583610eb0565b915061146782611400565b604082019050919050565b6000602082019050818103600083015261148b8161144f565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b60006114ee602383610eb0565b91506114f982611492565b604082019050919050565b6000602082019050818103600083015261151d816114e1565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b6000611580602683610eb0565b915061158b82611524565b604082019050919050565b600060208201905081810360008301526115af81611573565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b60006115ec601d83610eb0565b91506115f7826115b6565b602082019050919050565b6000602082019050818103600083015261161b816115df565b9050919050565b7f45524332303a206d696e7420746f20746865207a65726f206164647265737300600082015250565b6000611658601f83610eb0565b915061166382611622565b602082019050919050565b600060208201905081810360008301526116878161164b565b9050919050565b7f45524332303a206275726e2066726f6d20746865207a65726f2061646472657360008201527f7300000000000000000000000000000000000000000000000000000000000000602082015250565b60006116ea602183610eb0565b91506116f58261168e565b604082019050919050565b60006020820190508181036000830152611719816116dd565b9050919050565b7f45524332303a206275726e20616d6f756e7420657863656564732062616c616e60008201527f6365000000000000000000000000000000000000000000000000000000000000602082015250565b600061177c602283610eb0565b915061178782611720565b604082019050919050565b600060208201905081810360008301526117ab8161176f565b905091905056fea264697066735822122082c230d50a21871fc6f0e7d5819d9623254c94806da38937edd6a7f483027abc64736f6c63430008130033", } // ERC20MockABI is the input ABI used to generate the binding from. @@ -44,7 +44,7 @@ var ERC20MockABI = ERC20MockMetaData.ABI var ERC20MockBin = ERC20MockMetaData.Bin // DeployERC20Mock deploys a new Ethereum contract, binding an instance of ERC20Mock to it. -func DeployERC20Mock(auth *bind.TransactOpts, backend bind.ContractBackend, name string, symbol string, initialAccount common.Address, initialBalance *big.Int) (common.Address, *types.Transaction, *ERC20Mock, error) { +func DeployERC20Mock(auth *bind.TransactOpts, backend bind.ContractBackend, name string, symbol string, numDecimals uint8, initialAccount common.Address, initialBalance *big.Int) (common.Address, *types.Transaction, *ERC20Mock, error) { parsed, err := ERC20MockMetaData.GetAbi() if err != nil { return common.Address{}, nil, nil, err @@ -53,7 +53,7 @@ func DeployERC20Mock(auth *bind.TransactOpts, backend bind.ContractBackend, name return common.Address{}, nil, nil, errors.New("GetABI returned nil") } - address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(ERC20MockBin), backend, name, symbol, initialAccount, initialBalance) + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(ERC20MockBin), backend, name, symbol, numDecimals, initialAccount, initialBalance) if err != nil { return common.Address{}, nil, nil, err } diff --git a/ethereum/erc20_mock_test.go b/ethereum/erc20_mock_test.go index ac7e3195..ec920f68 100644 --- a/ethereum/erc20_mock_test.go +++ b/ethereum/erc20_mock_test.go @@ -21,7 +21,7 @@ func TestSwapCreator_NewSwap_ERC20(t *testing.T) { addr := crypto.PubkeyToAddress(*pub) // deploy ERC20Mock - erc20Addr, erc20Tx, _, err := DeployERC20Mock(auth, conn, "ERC20Mock", "MOCK", addr, big.NewInt(9999)) + erc20Addr, erc20Tx, _, err := DeployERC20Mock(auth, conn, "ERC20Mock", "MOCK", 18, addr, big.NewInt(9999)) require.NoError(t, err) receipt, err := block.WaitForReceipt(context.Background(), conn, erc20Tx.Hash()) require.NoError(t, err) @@ -35,7 +35,7 @@ func TestSwapCreator_Claim_ERC20(t *testing.T) { pub := pkA.Public().(*ecdsa.PublicKey) addr := crypto.PubkeyToAddress(*pub) - erc20Addr, erc20Tx, erc20Contract, err := DeployERC20Mock(auth, conn, "ERC20Mock", "MOCK", addr, big.NewInt(9999)) + erc20Addr, erc20Tx, erc20Contract, err := DeployERC20Mock(auth, conn, "ERC20Mock", "MOCK", 18, addr, big.NewInt(9999)) require.NoError(t, err) receipt, err := block.WaitForReceipt(context.Background(), conn, erc20Tx.Hash()) require.NoError(t, err) @@ -53,7 +53,7 @@ func TestSwapCreator_RefundBeforeT0_ERC20(t *testing.T) { pub := pkA.Public().(*ecdsa.PublicKey) addr := crypto.PubkeyToAddress(*pub) - erc20Addr, erc20Tx, _, err := DeployERC20Mock(auth, conn, "ERC20Mock", "MOCK", addr, big.NewInt(9999)) + erc20Addr, erc20Tx, _, err := DeployERC20Mock(auth, conn, "ERC20Mock", "MOCK", 18, addr, big.NewInt(9999)) require.NoError(t, err) receipt, err := block.WaitForReceipt(context.Background(), conn, erc20Tx.Hash()) require.NoError(t, err) @@ -67,7 +67,7 @@ func TestSwapCreator_RefundAfterT1_ERC20(t *testing.T) { pub := pkA.Public().(*ecdsa.PublicKey) addr := crypto.PubkeyToAddress(*pub) - erc20Addr, erc20Tx, _, err := DeployERC20Mock(auth, conn, "ERC20Mock", "MOCK", addr, big.NewInt(9999)) + erc20Addr, erc20Tx, _, err := DeployERC20Mock(auth, conn, "ERC20Mock", "MOCK", 18, addr, big.NewInt(9999)) require.NoError(t, err) receipt, err := block.WaitForReceipt(context.Background(), conn, erc20Tx.Hash()) require.NoError(t, err) diff --git a/ethereum/extethclient/eth_wallet_client.go b/ethereum/extethclient/eth_wallet_client.go index 5eee9ed8..67a45637 100644 --- a/ethereum/extethclient/eth_wallet_client.go +++ b/ethereum/extethclient/eth_wallet_client.go @@ -19,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" logging "github.com/ipfs/go-log" + "github.com/athanorlabs/atomic-swap/coins" "github.com/athanorlabs/atomic-swap/common" contracts "github.com/athanorlabs/atomic-swap/ethereum" "github.com/athanorlabs/atomic-swap/ethereum/block" @@ -36,10 +37,10 @@ type EthClient interface { HasPrivateKey() bool Endpoint() string - Balance(ctx context.Context) (*big.Int, error) - ERC20Balance(ctx context.Context, token ethcommon.Address) (*big.Int, error) + Balance(ctx context.Context) (*coins.WeiAmount, error) + ERC20Balance(ctx context.Context, token ethcommon.Address) (*coins.ERC20TokenAmount, error) - ERC20Info(ctx context.Context, token ethcommon.Address) (name string, symbol string, decimals uint8, err error) + ERC20Info(ctx context.Context, tokenAddr ethcommon.Address) (*coins.ERC20TokenInfo, error) SetGasPrice(uint64) SetGasLimit(uint64) @@ -129,13 +130,13 @@ func (c *ethClient) Endpoint() string { return c.endpoint } -func (c *ethClient) Balance(ctx context.Context) (*big.Int, error) { +func (c *ethClient) Balance(ctx context.Context) (*coins.WeiAmount, error) { addr := c.Address() bal, err := c.ec.BalanceAt(ctx, addr, nil) if err != nil { return nil, err } - return bal, nil + return coins.NewWeiAmount(bal), nil } // SuggestGasPrice returns the underlying eth client's suggested gas price @@ -148,37 +149,56 @@ func (c *ethClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { return c.Raw().SuggestGasPrice(ctx) } -func (c *ethClient) ERC20Balance(ctx context.Context, token ethcommon.Address) (*big.Int, error) { - tokenContract, err := contracts.NewIERC20(token, c.ec) +func (c *ethClient) ERC20Balance(ctx context.Context, tokenAddr ethcommon.Address) (*coins.ERC20TokenAmount, error) { + tokenContract, err := contracts.NewIERC20(tokenAddr, c.ec) if err != nil { - return big.NewInt(0), err + return nil, err } - return tokenContract.BalanceOf(c.CallOpts(ctx), c.Address()) + + bal, err := tokenContract.BalanceOf(c.CallOpts(ctx), c.Address()) + if err != nil { + return nil, err + } + + tokenInfo, err := c.erc20Info(ctx, tokenAddr, tokenContract) + if err != nil { + return nil, err + } + + return coins.NewERC20TokenAmountFromBigInt(bal, tokenInfo), nil } -func (c *ethClient) ERC20Info(ctx context.Context, token ethcommon.Address) ( - name string, - symbol string, - decimals uint8, - err error, -) { - tokenContract, err := contracts.NewIERC20(token, c.ec) +func (c *ethClient) erc20Info( + ctx context.Context, + tokenAddr ethcommon.Address, + tokenContract *contracts.IERC20, +) (*coins.ERC20TokenInfo, error) { + name, err := tokenContract.Name(c.CallOpts(ctx)) if err != nil { - return "", "", 18, err + return nil, err } - name, err = tokenContract.Name(c.CallOpts(ctx)) + + symbol, err := tokenContract.Symbol(c.CallOpts(ctx)) if err != nil { - return "", "", 18, err + return nil, err } - symbol, err = tokenContract.Symbol(c.CallOpts(ctx)) + + // TODO: Do we support ERC20 tokens that do not have this method? + decimals, err := tokenContract.Decimals(c.CallOpts(ctx)) if err != nil { - return "", "", 18, err + return nil, err } - decimals, err = tokenContract.Decimals(c.CallOpts(ctx)) + + return coins.NewERC20TokenInfo(tokenAddr, decimals, name, symbol), nil +} + +func (c *ethClient) ERC20Info(ctx context.Context, tokenAddr ethcommon.Address) (*coins.ERC20TokenInfo, error) { + tokenContract, err := contracts.NewIERC20(tokenAddr, c.ec) if err != nil { - return "", "", 18, err + return nil, err } - return name, symbol, decimals, nil + + return c.erc20Info(ctx, tokenAddr, tokenContract) } // SetGasPrice sets the ethereum gas price (in wei) for use in transactions. In most diff --git a/net/initiate.go b/net/initiate.go index d21662ed..021b7ea7 100644 --- a/net/initiate.go +++ b/net/initiate.go @@ -175,7 +175,7 @@ func (h *Host) handleProtocolStreamInner(stream libp2pnetwork.Stream, s SwapStat err = s.HandleProtocolMessage(msg) if err != nil { - log.Warnf("failed to handle protocol message: err=%s", err) + log.Warnf("failed to handle protocol message: %s", err) return } } diff --git a/protocol/backend/backend.go b/protocol/backend/backend.go index 79d6f934..250a6123 100644 --- a/protocol/backend/backend.go +++ b/protocol/backend/backend.go @@ -160,7 +160,7 @@ func (b *backend) NewTxSender(asset ethcommon.Address, erc20Contract *contracts. return txsender.NewExternalSender(b.ctx, b.env, b.ethClient.Raw(), b.swapCreatorAddr, asset) } - return txsender.NewSenderWithPrivateKey(b.ctx, b.ETHClient(), b.swapCreator, erc20Contract), nil + return txsender.NewSenderWithPrivateKey(b.ctx, b.ETHClient(), b.swapCreatorAddr, b.swapCreator, erc20Contract), nil } func (b *backend) RecoveryDB() RecoveryDB { diff --git a/protocol/ethereum_asset_amount.go b/protocol/ethereum_asset_amount.go index e8bfc308..54354c40 100644 --- a/protocol/ethereum_asset_amount.go +++ b/protocol/ethereum_asset_amount.go @@ -6,7 +6,6 @@ package protocol import ( "context" "fmt" - "math/big" "github.com/cockroachdb/apd/v3" @@ -15,26 +14,21 @@ import ( "github.com/athanorlabs/atomic-swap/ethereum/extethclient" ) -// EthereumAssetAmount represents an amount of an Ethereum asset (ie. ether or an ERC20) -type EthereumAssetAmount interface { - BigInt() *big.Int - AsStandard() *apd.Decimal -} - -// GetEthereumAssetAmount returns an EthereumAssetAmount (ie WeiAmount or ERC20TokenAmount) -func GetEthereumAssetAmount( +// GetEthAssetAmount converts the passed asset amt (in standard units) to +// EthAssetAmount (ie WeiAmount or ERC20TokenAmount) +func GetEthAssetAmount( ctx context.Context, ec extethclient.EthClient, - amt *apd.Decimal, + amt *apd.Decimal, // in standard units asset types.EthAsset, -) (EthereumAssetAmount, error) { +) (coins.EthAssetAmount, error) { if asset != types.EthAssetETH { - _, _, decimals, err := ec.ERC20Info(ctx, asset.Address()) + tokenInfo, err := ec.ERC20Info(ctx, asset.Address()) if err != nil { return nil, fmt.Errorf("failed to get ERC20 info: %w", err) } - return coins.NewERC20TokenAmountFromDecimals(amt, decimals), nil + return coins.NewERC20TokenAmountFromDecimals(amt, tokenInfo), nil } return coins.EtherToWei(amt), nil diff --git a/protocol/txsender/external_sender.go b/protocol/txsender/external_sender.go index 189af140..cfeb28a6 100644 --- a/protocol/txsender/external_sender.go +++ b/protocol/txsender/external_sender.go @@ -76,11 +76,11 @@ func NewExternalSender( }, nil } -// SetContract ... -func (s *ExternalSender) SetContract(_ *contracts.SwapCreator) {} +// SetSwapCreator sets the bound contract for the SwapCreator +func (s *ExternalSender) SetSwapCreator(_ *contracts.SwapCreator) {} -// SetContractAddress ... -func (s *ExternalSender) SetContractAddress(addr ethcommon.Address) { +// SetSwapCreatorAddr sets the address of the SwapCreator contract +func (s *ExternalSender) SetSwapCreatorAddr(addr ethcommon.Address) { s.contractAddr = addr } @@ -94,8 +94,10 @@ func (s *ExternalSender) IncomingCh(id types.Hash) chan<- ethcommon.Hash { return s.in } -// Approve prompts the external sender to sign an ERC20 Approve transaction -func (s *ExternalSender) Approve( +// approve prompts the external sender to sign an ERC20 approve transaction +// +//nolint:unused // not used because external sender's NewSwap doesn't support ERC20 tokens +func (s *ExternalSender) approve( spender ethcommon.Address, amount *big.Int, ) (*ethtypes.Receipt, error) { @@ -114,20 +116,23 @@ func (s *ExternalSender) NewSwap( claimer ethcommon.Address, timeoutDuration *big.Int, nonce *big.Int, - ethAsset types.EthAsset, - value *big.Int, + amount coins.EthAssetAmount, ) (*ethtypes.Receipt, error) { + // TODO: Add ERC20 token support and approve new_swap for the token transfer + if amount.IsToken() { + return nil, errors.New("external sender does not support ERC20 token swaps") + } + input, err := s.abi.Pack("new_swap", pubKeyClaim, pubKeyRefund, claimer, timeoutDuration, - ethAsset, value, nonce) + amount.TokenAddress(), amount.BigInt(), nonce) if err != nil { return nil, err } - valueWei := coins.NewWeiAmount(value) tx := &Transaction{ To: s.contractAddr, Data: input, - Value: valueWei.AsEther(), + Value: amount.AsStandard(), } s.Lock() diff --git a/protocol/txsender/sender.go b/protocol/txsender/sender.go index 7f3e3dcf..5e22d786 100644 --- a/protocol/txsender/sender.go +++ b/protocol/txsender/sender.go @@ -13,28 +13,32 @@ import ( "fmt" "math/big" - "github.com/athanorlabs/atomic-swap/common/types" + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + logging "github.com/ipfs/go-log" + + "github.com/athanorlabs/atomic-swap/coins" + "github.com/athanorlabs/atomic-swap/common" contracts "github.com/athanorlabs/atomic-swap/ethereum" "github.com/athanorlabs/atomic-swap/ethereum/block" "github.com/athanorlabs/atomic-swap/ethereum/extethclient" +) - ethcommon "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" +var ( + log = logging.Logger("txsender") ) // Sender signs and submits transactions to the chain type Sender interface { - SetContract(*contracts.SwapCreator) - SetContractAddress(ethcommon.Address) - Approve(spender ethcommon.Address, amount *big.Int) (*ethtypes.Receipt, error) // for ERC20 swaps + SetSwapCreator(*contracts.SwapCreator) + SetSwapCreatorAddr(ethcommon.Address) NewSwap( pubKeyClaim [32]byte, pubKeyRefund [32]byte, claimer ethcommon.Address, timeoutDuration *big.Int, nonce *big.Int, - ethAsset types.EthAsset, - amount *big.Int, + amount coins.EthAssetAmount, ) (*ethtypes.Receipt, error) SetReady(swap *contracts.SwapCreatorSwap) (*ethtypes.Receipt, error) Claim(swap *contracts.SwapCreatorSwap, secret [32]byte) (*ethtypes.Receipt, error) @@ -42,58 +46,35 @@ type Sender interface { } type privateKeySender struct { - ctx context.Context - ethClient extethclient.EthClient - swapContract *contracts.SwapCreator - erc20Contract *contracts.IERC20 + ctx context.Context + ethClient extethclient.EthClient + swapCreatorAddr ethcommon.Address + swapCreator *contracts.SwapCreator + erc20Contract *contracts.IERC20 } // NewSenderWithPrivateKey returns a new *privateKeySender func NewSenderWithPrivateKey( ctx context.Context, ethClient extethclient.EthClient, - swapContract *contracts.SwapCreator, + swapCreatorAddr ethcommon.Address, + swapCreator *contracts.SwapCreator, erc20Contract *contracts.IERC20, ) Sender { return &privateKeySender{ - ctx: ctx, - ethClient: ethClient, - swapContract: swapContract, - erc20Contract: erc20Contract, + ctx: ctx, + ethClient: ethClient, + swapCreatorAddr: swapCreatorAddr, + swapCreator: swapCreator, + erc20Contract: erc20Contract, } } -func (s *privateKeySender) SetContract(contract *contracts.SwapCreator) { - s.swapContract = contract +func (s *privateKeySender) SetSwapCreator(contract *contracts.SwapCreator) { + s.swapCreator = contract } -func (s *privateKeySender) SetContractAddress(_ ethcommon.Address) {} - -func (s *privateKeySender) Approve( - spender ethcommon.Address, - amount *big.Int, -) (*ethtypes.Receipt, error) { - s.ethClient.Lock() - defer s.ethClient.Unlock() - txOpts, err := s.ethClient.TxOpts(s.ctx) - if err != nil { - return nil, err - } - - tx, err := s.erc20Contract.Approve(txOpts, spender, amount) - if err != nil { - err = fmt.Errorf("set_ready tx creation failed, %w", err) - return nil, err - } - - receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash()) - if err != nil { - err = fmt.Errorf("set_ready failed, %w", err) - return nil, err - } - - return receipt, nil -} +func (s *privateKeySender) SetSwapCreatorAddr(_ ethcommon.Address) {} func (s *privateKeySender) NewSwap( pubKeyClaim [32]byte, @@ -101,23 +82,50 @@ func (s *privateKeySender) NewSwap( claimer ethcommon.Address, timeoutDuration *big.Int, nonce *big.Int, - ethAsset types.EthAsset, - value *big.Int, + amount coins.EthAssetAmount, ) (*ethtypes.Receipt, error) { s.ethClient.Lock() defer s.ethClient.Unlock() + + value := amount.BigInt() + + // For token swaps, approving our contract to transfer tokens, and calling + // NewSwap which performs the transfer, needs to be inside the same wallet + // lock grab in case there are other simultaneous swaps happening with the + // same token. + if amount.IsToken() { + txOpts, err := s.ethClient.TxOpts(s.ctx) + if err != nil { + return nil, err + } + + tx, err := s.erc20Contract.Approve(txOpts, s.swapCreatorAddr, value) + if err != nil { + return nil, fmt.Errorf("approve tx creation failed, %w", err) + } + + receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash()) + if err != nil { + return nil, fmt.Errorf("approve failed, %w", err) + } + + log.Debugf("approve transaction included %s", common.ReceiptInfo(receipt)) + log.Infof("%s %s approved for use by SwapCreator's new_swap", + amount.AsStandard().Text('f'), amount.StandardSymbol()) + } + txOpts, err := s.ethClient.TxOpts(s.ctx) if err != nil { return nil, err } // transfer ETH if we're not doing an ERC20 swap - if ethAsset == types.EthAssetETH { + if !amount.IsToken() { txOpts.Value = value } - tx, err := s.swapContract.NewSwap(txOpts, pubKeyClaim, pubKeyRefund, claimer, timeoutDuration, timeoutDuration, - ethcommon.Address(ethAsset), value, nonce) + tx, err := s.swapCreator.NewSwap(txOpts, pubKeyClaim, pubKeyRefund, claimer, timeoutDuration, timeoutDuration, + amount.TokenAddress(), value, nonce) if err != nil { err = fmt.Errorf("new_swap tx creation failed, %w", err) return nil, err @@ -140,7 +148,7 @@ func (s *privateKeySender) SetReady(swap *contracts.SwapCreatorSwap) (*ethtypes. return nil, err } - tx, err := s.swapContract.SetReady(txOpts, *swap) + tx, err := s.swapCreator.SetReady(txOpts, *swap) if err != nil { err = fmt.Errorf("set_ready tx creation failed, %w", err) return nil, err @@ -166,7 +174,7 @@ func (s *privateKeySender) Claim( return nil, err } - tx, err := s.swapContract.Claim(txOpts, *swap, secret) + tx, err := s.swapCreator.Claim(txOpts, *swap, secret) if err != nil { err = fmt.Errorf("claim tx creation failed, %w", err) return nil, err @@ -192,7 +200,7 @@ func (s *privateKeySender) Refund( return nil, err } - tx, err := s.swapContract.Refund(txOpts, *swap, secret) + tx, err := s.swapCreator.Refund(txOpts, *swap, secret) if err != nil { err = fmt.Errorf("refund tx creation failed, %w", err) return nil, err diff --git a/protocol/utils.go b/protocol/utils.go index ebb6e233..d42f5af9 100644 --- a/protocol/utils.go +++ b/protocol/utils.go @@ -5,6 +5,7 @@ package protocol import ( "fmt" + "strconv" "github.com/athanorlabs/atomic-swap/common/types" "github.com/athanorlabs/atomic-swap/protocol/backend" @@ -17,12 +18,12 @@ const etherSymbol = "ETH" // AssetSymbol returns the symbol for the given asset. func AssetSymbol(b backend.Backend, asset types.EthAsset) (string, error) { if asset != types.EthAssetETH { - _, symbol, _, err := b.ETHClient().ERC20Info(b.Ctx(), asset.Address()) + tokenInfo, err := b.ETHClient().ERC20Info(b.Ctx(), asset.Address()) if err != nil { return "", fmt.Errorf("failed to get ERC20 info: %w", err) } - return symbol, nil + return strconv.QuoteToASCII(tokenInfo.Symbol), nil } return etherSymbol, nil diff --git a/protocol/xmrmaker/backend_offers.go b/protocol/xmrmaker/backend_offers.go index 4ba5f16a..598d4102 100644 --- a/protocol/xmrmaker/backend_offers.go +++ b/protocol/xmrmaker/backend_offers.go @@ -9,12 +9,12 @@ import ( ) // MakeOffer makes a new swap offer. -func (b *Instance) MakeOffer( +func (inst *Instance) MakeOffer( o *types.Offer, useRelayer bool, ) (*types.OfferExtra, error) { // get monero balance - balance, err := b.backend.XMRClient().GetBalance(0) + balance, err := inst.backend.XMRClient().GetBalance(0) if err != nil { return nil, err } @@ -24,29 +24,29 @@ func (b *Instance) MakeOffer( return nil, errUnlockedBalanceTooLow{o.MaxAmount, unlockedBalance} } - if useRelayer && o.EthAsset != types.EthAssetETH { + if useRelayer && o.EthAsset.IsToken() { return nil, errRelayingWithNonEthAsset } - extra, err := b.offerManager.AddOffer(o, useRelayer) + extra, err := inst.offerManager.AddOffer(o, useRelayer) if err != nil { return nil, err } - b.net.Advertise() + inst.net.Advertise() log.Infof("created new offer: %v", o) return extra, nil } // GetOffers returns all current offers. -func (b *Instance) GetOffers() []*types.Offer { - return b.offerManager.GetOffers() +func (inst *Instance) GetOffers() []*types.Offer { + return inst.offerManager.GetOffers() } // ClearOffers clears all offers. -func (b *Instance) ClearOffers(offerIDs []types.Hash) error { +func (inst *Instance) ClearOffers(offerIDs []types.Hash) error { if len(offerIDs) == 0 { - return b.offerManager.ClearAllOffers() + return inst.offerManager.ClearAllOffers() } - return b.offerManager.ClearOfferIDs(offerIDs) + return inst.offerManager.ClearOfferIDs(offerIDs) } diff --git a/protocol/xmrmaker/checks.go b/protocol/xmrmaker/checks.go index a2b9f417..123ec7b7 100644 --- a/protocol/xmrmaker/checks.go +++ b/protocol/xmrmaker/checks.go @@ -82,7 +82,7 @@ func (s *swapState) checkContract(txHash ethcommon.Hash) error { return fmt.Errorf("swap value and event value don't match: got %v, expected %v", event.Value, s.contractSwap.Value) } - expectedAmount, err := pcommon.GetEthereumAssetAmount( + expectedAmount, err := pcommon.GetEthAssetAmount( s.ctx, s.ETHClient(), s.info.ExpectedAmount, diff --git a/protocol/xmrmaker/claim.go b/protocol/xmrmaker/claim.go index 5ea6b1fe..016d5e07 100644 --- a/protocol/xmrmaker/claim.go +++ b/protocol/xmrmaker/claim.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "math/big" "time" "github.com/ethereum/go-ethereum" @@ -16,7 +15,6 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" - "github.com/athanorlabs/atomic-swap/coins" "github.com/athanorlabs/atomic-swap/common" "github.com/athanorlabs/atomic-swap/common/types" "github.com/athanorlabs/atomic-swap/ethereum/block" @@ -26,40 +24,25 @@ import ( // claimFunds redeems XMRMaker's ETH funds by calling Claim() on the contract func (s *swapState) claimFunds() (*ethtypes.Receipt, error) { - var ( - symbol string - decimals uint8 - err error - ) - if types.EthAsset(s.contractSwap.Asset) != types.EthAssetETH { - _, symbol, decimals, err = s.ETHClient().ERC20Info(s.ctx, s.contractSwap.Asset) - if err != nil { - return nil, fmt.Errorf("failed to get ERC20 info: %w", err) - } - } - weiBalance, err := s.ETHClient().Balance(s.ctx) if err != nil { return nil, err } if types.EthAsset(s.contractSwap.Asset) == types.EthAssetETH { - log.Infof("balance before claim: %s ETH", coins.NewWeiAmount(weiBalance).AsEther()) + log.Infof("balance before claim: %s ETH", weiBalance.AsEtherString()) } else { balance, err := s.ETHClient().ERC20Balance(s.ctx, s.contractSwap.Asset) //nolint:govet if err != nil { return nil, err } - log.Infof("balance before claim: %v %s", - coins.NewERC20TokenAmountFromBigInt(balance, decimals).AsStandard().Text('f'), - symbol, - ) + log.Infof("balance before claim: %s %s", balance.AsStandardString(), balance.StandardSymbol()) } var receipt *ethtypes.Receipt // call swap.Swap.Claim() w/ b.privkeys.sk, revealing XMRMaker's secret spend key - if s.offerExtra.UseRelayer || weiBalance.Cmp(big.NewInt(0)) == 0 { + if s.offerExtra.UseRelayer || weiBalance.Decimal().IsZero() { // relayer fee was set or we had insufficient funds to claim without a relayer // TODO: Sufficient funds check above should be more specific receipt, err = s.claimWithRelay() @@ -85,17 +68,14 @@ func (s *swapState) claimFunds() (*ethtypes.Receipt, error) { if err != nil { return nil, err } - log.Infof("balance after claim: %s ETH", coins.FmtWeiAsETH(balance)) + log.Infof("balance after claim: %s ETH", balance.AsEtherString()) } else { balance, err := s.ETHClient().ERC20Balance(s.ctx, s.contractSwap.Asset) if err != nil { return nil, err } - log.Infof("balance after claim: %s %s", - coins.NewERC20TokenAmountFromBigInt(balance, decimals).AsStandard().Text('f'), - symbol, - ) + log.Infof("balance after claim: %s %s", balance.AsStandardString(), balance.StandardSymbol()) } return receipt, nil diff --git a/protocol/xmrmaker/claim_test.go b/protocol/xmrmaker/claim_test.go index 7102e691..13e7b6fe 100644 --- a/protocol/xmrmaker/claim_test.go +++ b/protocol/xmrmaker/claim_test.go @@ -47,6 +47,7 @@ func TestSwapState_ClaimRelayer_ERC20(t *testing.T) { conn, "Mock", "MOCK", + 18, addr, initialBalance, ) diff --git a/protocol/xmrmaker/instance.go b/protocol/xmrmaker/instance.go index 7b0ff64d..4a5a5fb0 100644 --- a/protocol/xmrmaker/instance.go +++ b/protocol/xmrmaker/instance.go @@ -195,7 +195,7 @@ func (inst *Instance) createOngoingSwap(s *swap.Info) error { } // completeSwap is called in the case where we find an ongoing swap in the db on startup, -// and the swap already has the counterpary's swap secret stored. +// and the swap already has the counterparty's swap secret stored. // In this case, we simply re-claim the XMR we locked, as we have both secrets required. // It's unlikely for this case to ever be hit, unless the daemon was shut down in-between // us finding the counterparty's secret and claiming the XMR. diff --git a/protocol/xmrmaker/net.go b/protocol/xmrmaker/net.go index 54f7d0cf..063b6650 100644 --- a/protocol/xmrmaker/net.go +++ b/protocol/xmrmaker/net.go @@ -4,9 +4,6 @@ package xmrmaker import ( - "math/big" - - "github.com/cockroachdb/apd/v3" "github.com/libp2p/go-libp2p/core/peer" "github.com/athanorlabs/atomic-swap/coins" @@ -19,12 +16,6 @@ import ( "github.com/fatih/color" ) -// EthereumAssetAmount represents an amount of an Ethereum asset (ie. ether or an ERC20) -type EthereumAssetAmount interface { - BigInt() *big.Int - AsStandard() *apd.Decimal -} - // Provides returns types.ProvidesXMR func (inst *Instance) Provides() coins.ProvidesCoin { return coins.ProvidesXMR @@ -35,7 +26,7 @@ func (inst *Instance) initiate( offer *types.Offer, offerExtra *types.OfferExtra, providesAmount *coins.PiconeroAmount, - desiredAmount EthereumAssetAmount, + desiredAmount coins.EthAssetAmount, ) (*swapState, error) { if inst.swapStates[offer.ID] != nil { return nil, errProtocolAlreadyInProgress @@ -146,7 +137,7 @@ func (inst *Instance) HandleInitiateMessage( // check decimals if ERC20 // note: this is our counterparty's provided amount, ie. how much we're receiving - expectedAmount, err := pcommon.GetEthereumAssetAmount( + expectedAmount, err := pcommon.GetEthAssetAmount( inst.backend.Ctx(), inst.backend.ETHClient(), msg.ProvidedAmount, diff --git a/protocol/xmrmaker/swap_state.go b/protocol/xmrmaker/swap_state.go index e23ca02e..521f3e51 100644 --- a/protocol/xmrmaker/swap_state.go +++ b/protocol/xmrmaker/swap_state.go @@ -63,7 +63,7 @@ type swapState struct { pubkeys *mcrypto.PublicKeyPair // swap contract and timeouts in it - contract *contracts.SwapCreator + swapCreator *contracts.SwapCreator swapCreatorAddr ethcommon.Address contractSwapID [32]byte contractSwap *contracts.SwapCreatorSwap @@ -105,7 +105,7 @@ func newSwapStateFromStart( offerExtra *types.OfferExtra, om *offers.Manager, providesAmount *coins.PiconeroAmount, - desiredAmount EthereumAssetAmount, + desiredAmount coins.EthAssetAmount, ) (*swapState, error) { // at this point, we've received the counterparty's keys, // and will send our own after this function returns. @@ -332,7 +332,7 @@ func newSwapState( info *pswap.Info, ) (*swapState, error) { var sender txsender.Sender - if offer.EthAsset != types.EthAssetETH { + if offer.EthAsset.IsToken() { erc20Contract, err := contracts.NewIERC20(offer.EthAsset.Address(), b.ETHClient().Raw()) if err != nil { return nil, err @@ -624,18 +624,18 @@ func (s *swapState) setXMRTakerKeys( return s.RecoveryDB().PutCounterpartySwapKeys(s.OfferID(), sk, vk) } -// setContract sets the contract in which XMRTaker has locked her ETH. +// setContract sets the swapCreator in which XMRTaker has locked her ETH. func (s *swapState) setContract(address ethcommon.Address) error { s.swapCreatorAddr = address var err error - s.contract, err = s.NewSwapCreator(address) + s.swapCreator, err = s.NewSwapCreator(address) if err != nil { return err } - s.sender.SetContractAddress(address) - s.sender.SetContract(s.contract) + s.sender.SetSwapCreatorAddr(address) + s.sender.SetSwapCreator(s.swapCreator) return nil } diff --git a/protocol/xmrmaker/watcher.go b/protocol/xmrmaker/watcher.go index 28717d37..18fe9528 100644 --- a/protocol/xmrmaker/watcher.go +++ b/protocol/xmrmaker/watcher.go @@ -23,7 +23,6 @@ func (s *swapState) runContractEventWatcher() { log.Errorf("failed to handle ready logs: %s", err) } case l := <-s.logRefundedCh: - log.Infof("got refunded logs: %s", l) err := s.handleRefundLogs(&l) if err != nil { log.Errorf("failed to handle refund logs: %s", err) diff --git a/protocol/xmrtaker/errors.go b/protocol/xmrtaker/errors.go index 9410dda2..7ba31852 100644 --- a/protocol/xmrtaker/errors.go +++ b/protocol/xmrtaker/errors.go @@ -6,6 +6,8 @@ package xmrtaker import ( "errors" "fmt" + + "github.com/cockroachdb/apd/v3" ) var ( @@ -26,10 +28,22 @@ var ( // initiation errors errProtocolAlreadyInProgress = errors.New("protocol already in progress") - errBalanceTooLow = errors.New("eth balance lower than amount to be provided") errInvalidStageForRecovery = errors.New("cannot create ongoing swap state if stage is not ETHLocked or ContractReady") //nolint:lll ) +type errAssetBalanceTooLow struct { + providedAmount *apd.Decimal + balance *apd.Decimal + symbol string +} + +func (e errAssetBalanceTooLow) Error() string { + return fmt.Sprintf("balance of %s %s is below provided %s %s", + e.balance.Text('f'), e.symbol, + e.providedAmount.Text('f'), e.symbol, + ) +} + func errContractAddrMismatch(addr string) error { //nolint:lll return fmt.Errorf("cannot recover from swap where contract address is not the one loaded at start-up; please restart with --contract-address=%s", addr) diff --git a/protocol/xmrtaker/net.go b/protocol/xmrtaker/net.go index ab4d17d6..71eea01a 100644 --- a/protocol/xmrtaker/net.go +++ b/protocol/xmrtaker/net.go @@ -4,26 +4,17 @@ package xmrtaker import ( - "math/big" - "github.com/cockroachdb/apd/v3" "github.com/libp2p/go-libp2p/core/peer" "github.com/athanorlabs/atomic-swap/coins" "github.com/athanorlabs/atomic-swap/common" "github.com/athanorlabs/atomic-swap/common/types" - contracts "github.com/athanorlabs/atomic-swap/ethereum" pcommon "github.com/athanorlabs/atomic-swap/protocol" "github.com/fatih/color" ) -// EthereumAssetAmount represents an amount of an Ethereum asset (ie. ether or an ERC20) -type EthereumAssetAmount interface { - BigInt() *big.Int - AsStandard() *apd.Decimal -} - // Provides returns types.ProvidesETH func (inst *Instance) Provides() coins.ProvidesCoin { return coins.ProvidesETH @@ -40,7 +31,7 @@ func (inst *Instance) InitiateProtocol( if err != nil { return nil, err } - providedAmount, err := pcommon.GetEthereumAssetAmount( + providedAmount, err := pcommon.GetEthAssetAmount( inst.backend.Ctx(), inst.backend.ETHClient(), providesAmount, @@ -61,7 +52,7 @@ func (inst *Instance) InitiateProtocol( func (inst *Instance) initiate( makerPeerID peer.ID, - providesAmount EthereumAssetAmount, + providesAmount coins.EthAssetAmount, expectedAmount *coins.PiconeroAmount, exchangeRate *coins.ExchangeRate, ethAsset types.EthAsset, @@ -74,31 +65,34 @@ func (inst *Instance) initiate( return nil, errProtocolAlreadyInProgress } - balance, err := inst.backend.ETHClient().Balance(inst.backend.Ctx()) + ethBalance, err := inst.backend.ETHClient().Balance(inst.backend.Ctx()) if err != nil { return nil, err } // Ensure the user's balance is strictly greater than the amount they will provide - if ethAsset == types.EthAssetETH && balance.Cmp(providesAmount.BigInt()) <= 0 { + if ethAsset.IsETH() && ethBalance.Cmp(providesAmount.(*coins.WeiAmount)) <= 0 { log.Warnf("Account %s needs additional funds for swap balance=%s ETH providesAmount=%s ETH", - inst.backend.ETHClient().Address(), coins.FmtWeiAsETH(balance), providesAmount.AsStandard()) - return nil, errBalanceTooLow + inst.backend.ETHClient().Address(), ethBalance.AsEtherString(), providesAmount.AsStandard()) + return nil, errAssetBalanceTooLow{ + providedAmount: providesAmount.AsStandard(), + balance: ethBalance.AsEther(), + symbol: "ETH", + } } - if ethAsset != types.EthAssetETH { - erc20Contract, err := contracts.NewIERC20(ethAsset.Address(), inst.backend.ETHClient().Raw()) //nolint:govet + if ethAsset.IsToken() { + tokenBalance, err := inst.backend.ETHClient().ERC20Balance(inst.backend.Ctx(), ethAsset.Address()) //nolint:govet if err != nil { return nil, err } - balance, err := erc20Contract.BalanceOf(inst.backend.ETHClient().CallOpts(inst.backend.Ctx()), inst.backend.ETHClient().Address()) //nolint:lll - if err != nil { - return nil, err - } - - if balance.Cmp(providesAmount.BigInt()) <= 0 { - return nil, errBalanceTooLow + if tokenBalance.AsStandard().Cmp(providesAmount.AsStandard()) <= 0 { + return nil, errAssetBalanceTooLow{ + providedAmount: providesAmount.AsStandard(), + balance: tokenBalance.AsStandard(), + symbol: tokenBalance.StandardSymbol(), + } } } diff --git a/protocol/xmrtaker/swap_state.go b/protocol/xmrtaker/swap_state.go index 5be7f58a..18df54e5 100644 --- a/protocol/xmrtaker/swap_state.go +++ b/protocol/xmrtaker/swap_state.go @@ -53,7 +53,7 @@ type swapState struct { info *pswap.Info statusCh chan types.Status - providedAmount EthereumAssetAmount + providedAmount coins.EthAssetAmount // our keys for this session dleqProof *dleq.Proof @@ -100,7 +100,7 @@ func newSwapStateFromStart( makerPeerID peer.ID, offerID types.Hash, noTransferBack bool, - providedAmount EthereumAssetAmount, + providedAmount coins.EthAssetAmount, expectedAmount *coins.PiconeroAmount, exchangeRate *coins.ExchangeRate, ethAsset types.EthAsset, @@ -216,7 +216,7 @@ func newSwapState( } var sender txsender.Sender - if info.EthAsset != types.EthAssetETH { + if info.EthAsset.IsToken() { erc20Contract, err := contracts.NewIERC20(info.EthAsset.Address(), b.ETHClient().Raw()) if err != nil { return nil, err @@ -255,6 +255,18 @@ func newSwapState( return nil, err } + var providedAmt coins.EthAssetAmount + if info.EthAsset.IsETH() { + providedAmt = coins.EtherToWei(info.ProvidedAmount) + } else { + tokenInfo, err := b.ETHClient().ERC20Info(b.Ctx(), info.EthAsset.Address()) + if err != nil { + cancel() + return nil, err + } + providedAmt = coins.NewERC20TokenAmountFromDecimals(info.ProvidedAmount, tokenInfo) + } + // note: if this is recovering an ongoing swap, this will only // be invoked if our status is ETHLocked or ContractReady; ie. // we've locked ETH, but not yet claimed or refunded. @@ -278,11 +290,12 @@ func newSwapState( claimedCh: make(chan struct{}), done: make(chan struct{}), info: info, - providedAmount: coins.EtherToWei(info.ProvidedAmount), + providedAmount: providedAmt, statusCh: info.StatusCh(), } if err := s.generateAndSetKeys(); err != nil { + cancel() return nil, err } @@ -529,49 +542,17 @@ func (s *swapState) setXMRMakerKeys( return s.Backend.RecoveryDB().PutCounterpartySwapKeys(s.info.OfferID, sk, vk) } -func (s *swapState) approveToken() error { - token, err := contracts.NewIERC20(s.info.EthAsset.Address(), s.ETHClient().Raw()) - if err != nil { - return fmt.Errorf("failed to instantiate IERC20: %w", err) - } - - balance, err := token.BalanceOf(s.ETHClient().CallOpts(s.ctx), s.ETHClient().Address()) - if err != nil { - return fmt.Errorf("failed to get balance for token: %w", err) - } - - log.Info("approving token for use by the swap contract...") - _, err = s.sender.Approve(s.SwapCreatorAddr(), balance) - if err != nil { - return fmt.Errorf("failed to approve token: %w", err) - } - - log.Info("approved token for use by the swap contract") - return nil -} - // lockAsset calls the Swap contract function new_swap and locks `amount` ether in it. func (s *swapState) lockAsset() (*ethtypes.Receipt, error) { if s.xmrmakerPublicSpendKey == nil || s.xmrmakerPrivateViewKey == nil { panic(errCounterpartyKeysNotSet) } - symbol, err := pcommon.AssetSymbol(s.Backend, s.info.EthAsset) - if err != nil { - return nil, err - } - - if s.info.EthAsset != types.EthAssetETH { - err = s.approveToken() - if err != nil { - return nil, err - } - } - cmtXMRTaker := s.secp256k1Pub.Keccak256() cmtXMRMaker := s.xmrmakerSecp256k1PublicKey.Keccak256() + providedAmt := s.providedAmount - log.Debugf("locking %s in contract", symbol) + log.Debugf("locking %s %s in contract", providedAmt.AsStandard(), providedAmt.StandardSymbol()) nonce := generateNonce() receipt, err := s.sender.NewSwap( @@ -580,8 +561,7 @@ func (s *swapState) lockAsset() (*ethtypes.Receipt, error) { s.xmrmakerAddress, big.NewInt(int64(s.SwapTimeout().Seconds())), nonce, - s.info.EthAsset, - s.providedAmount.BigInt(), + providedAmt, ) if err != nil { return nil, fmt.Errorf("failed to instantiate swap on-chain: %w", err) @@ -642,7 +622,7 @@ func (s *swapState) lockAsset() (*ethtypes.Receipt, error) { return nil, err } - log.Infof("locked %s in swap contract, waiting for XMR to be locked", symbol) + log.Infof("locked %s in swap contract, waiting for XMR to be locked", providedAmt.StandardSymbol()) return receipt, nil } diff --git a/protocol/xmrtaker/swap_state_test.go b/protocol/xmrtaker/swap_state_test.go index 27ac4517..2dad5ab3 100644 --- a/protocol/xmrtaker/swap_state_test.go +++ b/protocol/xmrtaker/swap_state_test.go @@ -34,11 +34,15 @@ import ( ) var ( - _ = logging.SetLogLevel("protocol", "debug") - _ = logging.SetLogLevel("xmrtaker", "debug") testPeerID, _ = peer.Decode("12D3KooWQQRJuKTZ35eiHGNPGDpQqjpJSdaxEMJRxi6NWFrrvQVi") ) +func init() { + logging.SetLogLevel("xmrtaker", "debug") + logging.SetLogLevel("protocol", "debug") + logging.SetLogLevel("txsender", "debug") +} + type mockNet struct { msgMu sync.Mutex // lock needed, as SendSwapMessage is called async from timeout handlers msg common.Message // last value passed to SendSwapMessage @@ -144,8 +148,22 @@ func newTestSwapState(t *testing.T) *swapState { return s } -func newTestSwapStateWithERC20(t *testing.T, initialBalance *big.Int) (*swapState, *contracts.ERC20Mock) { +func newTestSwapStateWithERC20(t *testing.T, providesAmt *apd.Decimal) (*swapState, *contracts.ERC20Mock) { b := newBackend(t) + const numDecimals = 13 + + // Increase the provided amount to token units, by adding to the exponent + providesAmtTokenUnits := apd.NewWithBigInt(&providesAmt.Coeff, providesAmt.Exponent+numDecimals) + require.Positive(t, providesAmtTokenUnits.Exponent) + + // Set the exponent to zero pushing everything into the coefficient + _, err := coins.DecimalCtx().Quantize(providesAmtTokenUnits, providesAmtTokenUnits, 0) + if err != nil { + panic(err) + } + + // Now that everything is in the coefficient, we can convert to big.Int + providesAmtTokenUnitsBI := new(big.Int).SetBytes(providesAmtTokenUnits.Coeff.Bytes()) txOpts, err := b.ETHClient().TxOpts(b.Ctx()) require.NoError(t, err) @@ -153,19 +171,25 @@ func newTestSwapStateWithERC20(t *testing.T, initialBalance *big.Int) (*swapStat _, tx, contract, err := contracts.DeployERC20Mock( txOpts, b.ETHClient().Raw(), - "Mock", - "MOCK", + "☢☣☠\a Obnoxious Token \a☠☣☢", + "\a☢\n☣\a☠", // ensure we escape this + 13, b.ETHClient().Address(), - initialBalance, + providesAmtTokenUnitsBI, ) require.NoError(t, err) addr, err := bind.WaitDeployed(b.Ctx(), b.ETHClient().Raw(), tx) require.NoError(t, err) + tokenInfo, err := b.ETHClient().ERC20Info(b.Ctx(), addr) + require.NoError(t, err) + + providesEthAssetAmt := coins.NewERC20TokenAmountFromDecimals(providesAmt, tokenInfo) + exchangeRate := coins.ToExchangeRate(apd.New(1, 0)) // 100% zeroPiconeros := coins.NewPiconeroAmount(0) swapState, err := newSwapStateFromStart(b, testPeerID, types.Hash{}, false, - coins.IntToWei(1), zeroPiconeros, exchangeRate, types.EthAsset(addr)) + providesEthAssetAmt, zeroPiconeros, exchangeRate, types.EthAsset(addr)) require.NoError(t, err) return swapState, contract } @@ -425,11 +449,30 @@ func TestExit_invalidNextMessageType(t *testing.T) { } func TestSwapState_ApproveToken(t *testing.T) { - initialBalance := big.NewInt(999999) - s, contract := newTestSwapStateWithERC20(t, initialBalance) - err := s.approveToken() + const expectedAmtStr = "5678" + providesAmt := coins.StrToDecimal(expectedAmtStr) + + s, contract := newTestSwapStateWithERC20(t, providesAmt) + + xmrmakerKeysAndProof, err := generateKeys() require.NoError(t, err) - allowance, err := contract.Allowance(&bind.CallOpts{}, s.ETHClient().Address(), s.SwapCreatorAddr()) + + err = s.setXMRMakerKeys( + xmrmakerKeysAndProof.PublicKeyPair.SpendKey(), + xmrmakerKeysAndProof.PrivateKeyPair.ViewKey(), + xmrmakerKeysAndProof.Secp256k1PublicKey, + ) require.NoError(t, err) - require.Equal(t, initialBalance, allowance) + + // approve is called by NewSwap() in lockAsset() + _, err = s.lockAsset() + require.NoError(t, err) + + // Now that the tokens are locked in the contract, validate that + // the contract is no longer approved to transfer additional tokens + // from us. + callOpts := &bind.CallOpts{Context: s.ctx} + allowance, err := contract.Allowance(callOpts, s.ETHClient().Address(), s.SwapCreatorAddr()) + require.NoError(t, err) + require.Equal(t, "0", allowance.String()) } diff --git a/relayer/submit_transaction.go b/relayer/submit_transaction.go index 0e6378e9..ced6f316 100644 --- a/relayer/submit_transaction.go +++ b/relayer/submit_transaction.go @@ -112,9 +112,9 @@ func checkForMinClaimBalance(ctx context.Context, ec extethclient.EthClient) (*b } txCost := new(big.Int).Mul(gasPrice, big.NewInt(forwarderClaimGas)) - if balance.Cmp(txCost) < 0 { + if balance.BigInt().Cmp(txCost) < 0 { return nil, fmt.Errorf("balance %s ETH is under the minimum %s ETH to relay claim", - coins.FmtWeiAsETH(balance), coins.FmtWeiAsETH(txCost)) + balance.AsEtherString(), coins.FmtWeiAsETH(txCost)) } return gasPrice, nil diff --git a/rpc/personal.go b/rpc/personal.go index d9dbbcab..22b504cf 100644 --- a/rpc/personal.go +++ b/rpc/personal.go @@ -5,6 +5,7 @@ package rpc import ( "context" + "fmt" "net/http" "time" @@ -56,15 +57,34 @@ type SetGasPriceRequest struct { GasPrice uint64 `json:"gasPrice" validate:"required"` } -// SetGasPrice sets the gas price (in wei) to be used for ethereum transactions. +// SetGasPrice sets the gas price (in Wei) to be used for ethereum transactions. func (s *PersonalService) SetGasPrice(_ *http.Request, req *SetGasPriceRequest, _ *interface{}) error { s.pb.ETHClient().SetGasPrice(req.GasPrice) return nil } +// TokenInfo looks up the ERC20 token's metadata +func (s *PersonalService) TokenInfo( + _ *http.Request, + req *rpctypes.TokenInfoRequest, + resp *rpctypes.TokenInfoResponse, +) error { + tokenInfo, err := s.pb.ETHClient().ERC20Info(s.ctx, req.TokenAddr) + if err != nil { + return err + } + + *resp = *tokenInfo + return nil +} + // Balances returns combined information of both the Monero and Ethereum account addresses // and balances. -func (s *PersonalService) Balances(_ *http.Request, _ *interface{}, resp *rpctypes.BalancesResponse) error { +func (s *PersonalService) Balances( + _ *http.Request, + req *rpctypes.BalancesRequest, // optional, can be nil + resp *rpctypes.BalancesResponse, +) error { mAddr, mBal, err := s.xmrmaker.GetMoneroBalance() if err != nil { return err @@ -75,13 +95,27 @@ func (s *PersonalService) Balances(_ *http.Request, _ *interface{}, resp *rpctyp return err } + var tokenBalances []*coins.ERC20TokenAmount + if req != nil { + ec := s.pb.ETHClient() + for _, tokenAddr := range req.TokenAddrs { + balance, err := ec.ERC20Balance(s.ctx, tokenAddr) + if err != nil { + return fmt.Errorf("unable to get balance for %s: %w", tokenAddr, err) + } + + tokenBalances = append(tokenBalances, balance) + } + } + *resp = rpctypes.BalancesResponse{ MoneroAddress: mAddr, PiconeroBalance: coins.NewPiconeroAmount(mBal.Balance), PiconeroUnlockedBalance: coins.NewPiconeroAmount(mBal.UnlockedBalance), BlocksToUnlock: mBal.BlocksToUnlock, EthAddress: s.pb.ETHClient().Address(), - WeiBalance: coins.NewWeiAmount(eBal), + WeiBalance: eBal, + TokenBalances: tokenBalances, } return nil } diff --git a/rpc/swap.go b/rpc/swap.go index 06167ef4..7d14a84c 100644 --- a/rpc/swap.go +++ b/rpc/swap.go @@ -53,6 +53,7 @@ func NewSwapService( type PastSwap struct { ID types.Hash `json:"id" validate:"required"` Provided coins.ProvidesCoin `json:"provided" validate:"required"` + EthAsset types.EthAsset `json:"ethAsset"` ProvidedAmount *apd.Decimal `json:"providedAmount" validate:"required"` ExpectedAmount *apd.Decimal `json:"expectedAmount" validate:"required"` ExchangeRate *coins.ExchangeRate `json:"exchangeRate" validate:"required"` @@ -105,6 +106,7 @@ func (s *SwapService) GetPast(_ *http.Request, req *GetPastRequest, resp *GetPas resp.Swaps[i] = &PastSwap{ ID: info.OfferID, Provided: info.Provides, + EthAsset: info.EthAsset, ProvidedAmount: info.ProvidedAmount, ExpectedAmount: info.ExpectedAmount, ExchangeRate: info.ExchangeRate, @@ -125,6 +127,7 @@ func (s *SwapService) GetPast(_ *http.Request, req *GetPastRequest, resp *GetPas type OngoingSwap struct { ID types.Hash `json:"id" validate:"required"` Provided coins.ProvidesCoin `json:"provided" validate:"required"` + EthAsset types.EthAsset `json:"ethAsset"` ProvidedAmount *apd.Decimal `json:"providedAmount" validate:"required"` ExpectedAmount *apd.Decimal `json:"expectedAmount" validate:"required"` ExchangeRate *coins.ExchangeRate `json:"exchangeRate" validate:"required"` @@ -174,6 +177,7 @@ func (s *SwapService) GetOngoing(_ *http.Request, req *GetOngoingRequest, resp * swap := new(OngoingSwap) swap.ID = info.OfferID swap.Provided = info.Provides + swap.EthAsset = info.EthAsset swap.ProvidedAmount = info.ProvidedAmount swap.ExpectedAmount = info.ExpectedAmount swap.ExchangeRate = info.ExchangeRate @@ -279,7 +283,7 @@ func (s *SwapService) Cancel(_ *http.Request, req *CancelRequest, resp *CancelRe return fmt.Errorf("failed to find swap state with ID %s", req.OfferID) } - // Exit() is safe to be called concurrently, since it since it puts an exit event + // Exit() is safe to be called concurrently, as it puts an exit event // into the swap state's eventCh, and events are handled sequentially. if err = ss.Exit(); err != nil { return err @@ -334,7 +338,7 @@ func (s *SwapService) SuggestedExchangeRate(_ *http.Request, _ *interface{}, res return nil } -// estimatedTimeToCompletionreturns the estimated time for the swap to complete +// estimatedTimeToCompletion returns the estimated time for the swap to complete // in the optimistic case based on the given status and the time the status was updated. func estimatedTimeToCompletion( env common.Environment, diff --git a/rpcclient/personal.go b/rpcclient/personal.go index ba614ed1..bbf69c43 100644 --- a/rpcclient/personal.go +++ b/rpcclient/personal.go @@ -4,6 +4,9 @@ package rpcclient import ( + ethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/athanorlabs/atomic-swap/coins" "github.com/athanorlabs/atomic-swap/common/rpctypes" "github.com/athanorlabs/atomic-swap/rpc" ) @@ -39,14 +42,31 @@ func (c *Client) GetSwapTimeout() (*rpc.GetSwapTimeoutResponse, error) { return swapTimeout, nil } +// TokenInfo calls personal_tokenInfo +func (c *Client) TokenInfo(tokenAddr ethcommon.Address) (*coins.ERC20TokenInfo, error) { + const ( + method = "personal_tokenInfo" + ) + + // Note: coins.ERC20TokenInfo and rpctypes.TokenInfoRequest are aliases + request := &rpctypes.TokenInfoRequest{TokenAddr: tokenAddr} + tokenInfo := new(rpctypes.TokenInfoResponse) + + if err := c.Post(method, request, tokenInfo); err != nil { + return nil, err + } + + return tokenInfo, nil +} + // Balances calls personal_balances. -func (c *Client) Balances() (*rpctypes.BalancesResponse, error) { +func (c *Client) Balances(request *rpctypes.BalancesRequest) (*rpctypes.BalancesResponse, error) { const ( method = "personal_balances" ) balances := &rpctypes.BalancesResponse{} - if err := c.Post(method, nil, balances); err != nil { + if err := c.Post(method, request, balances); err != nil { return nil, err } diff --git a/scripts/run-integration-tests.sh b/scripts/run-integration-tests.sh index e1eae365..10d68377 100755 --- a/scripts/run-integration-tests.sh +++ b/scripts/run-integration-tests.sh @@ -10,13 +10,12 @@ source "scripts/testlib.sh" check-set-swap-test-data-dir # The first 5 ganache keys are reserved for use by integration tests and dev swapd -# instances. For now, we are only using 4: Alice, Bob, Charlie and the relayer. +# instances. For now, we are only using 3: Alice, Bob, Charlie. GANACHE_KEYS=( "4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" # Key 0 "6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1" # Key 1 "6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c" # Key 2 - "646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913" # Key 3 - "add53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743" # Key 4 (placeholder) + "646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913" # Key 3 (placeholder) ) KEY_USERS=( diff --git a/tests/erc20_integration_test.go b/tests/erc20_integration_test.go index e55c2917..620bbc58 100644 --- a/tests/erc20_integration_test.go +++ b/tests/erc20_integration_test.go @@ -5,70 +5,106 @@ package tests import ( "context" - "crypto/ecdsa" "math/big" "testing" - "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" ethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" "github.com/athanorlabs/atomic-swap/common" - "github.com/athanorlabs/atomic-swap/common/types" + "github.com/athanorlabs/atomic-swap/common/rpctypes" contracts "github.com/athanorlabs/atomic-swap/ethereum" - "github.com/athanorlabs/atomic-swap/ethereum/block" + "github.com/athanorlabs/atomic-swap/ethereum/extethclient" + "github.com/athanorlabs/atomic-swap/rpcclient" ) -func setupXMRTakerAuth(t *testing.T) (*bind.TransactOpts, *ethclient.Client, *ecdsa.PrivateKey) { - conn, chainID := NewEthClient(t) - pk, err := ethcrypto.HexToECDSA(common.DefaultPrivKeyXMRTaker) - require.NoError(t, err) - auth, err := bind.NewKeyedTransactorWithChainID(pk, chainID) - require.NoError(t, err) - return auth, conn, pk -} - // deploys ERC20Mock.sol and assigns the whole token balance to the XMRTaker default address. func deployERC20Mock(t *testing.T) ethcommon.Address { - auth, conn, pkA := setupXMRTakerAuth(t) - pub := pkA.Public().(*ecdsa.PublicKey) - addr := ethcrypto.PubkeyToAddress(*pub) + ctx := context.Background() + aliceKey, err := ethcrypto.HexToECDSA(common.DefaultPrivKeyXMRTaker) + require.NoError(t, err) - decimals := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) - balance := new(big.Int).Mul(big.NewInt(9999999), decimals) - erc20Addr, erc20Tx, _, err := contracts.DeployERC20Mock(auth, conn, "ERC20Mock", "MOCK", addr, balance) + ec := extethclient.CreateTestClient(t, aliceKey) + txOpts, err := ec.TxOpts(ctx) require.NoError(t, err) - _, err = block.WaitForReceipt(context.Background(), conn, erc20Tx.Hash()) + + const ( + initialTokenBalance = 1000 // standard units + decimals = 18 + ) + tenToDecimals := new(big.Int).Exp(big.NewInt(10), big.NewInt(decimals), nil) + totalSupply := new(big.Int).Mul(big.NewInt(initialTokenBalance*2), tenToDecimals) + halfSupply := new(big.Int).Mul(big.NewInt(initialTokenBalance), tenToDecimals) + + erc20Addr, erc20Tx, tokenContract, err := contracts.DeployERC20Mock( + txOpts, + ec.Raw(), + "ERC20Mock", + "MOCK", + decimals, + ec.Address(), + totalSupply, + ) require.NoError(t, err) + MineTransaction(t, ec.Raw(), erc20Tx) + + // Query Charlie's Ethereum address + charlieCli := rpcclient.NewClient(ctx, defaultCharlieSwapdEndpoint) + balResp, err := charlieCli.Balances(nil) + require.NoError(t, err) + charlieAddr := balResp.EthAddress + + // Transfer half of the supply to Charlie (using Alice's extended ethereum client) + txOpts, err = ec.TxOpts(ctx) + require.NoError(t, err) + tx, err := tokenContract.Transfer(txOpts, charlieAddr, halfSupply) + require.NoError(t, err) + MineTransaction(t, ec.Raw(), tx) + + tokenBalReq := &rpctypes.BalancesRequest{ + TokenAddrs: []ethcommon.Address{erc20Addr}, + } + + // verify that the XMR Taker has exactly 1000 tokens + aliceCli := rpcclient.NewClient(ctx, defaultXMRTakerSwapdEndpoint) + balResp, err = aliceCli.Balances(tokenBalReq) + require.NoError(t, err) + require.Equal(t, "1000", balResp.TokenBalances[0].AsStandardString()) + + // verify that Charlie also has exactly 1000 tokens + balResp, err = charlieCli.Balances(tokenBalReq) + require.NoError(t, err) + require.NoError(t, err) + require.Equal(t, "1000", balResp.TokenBalances[0].AsStandardString()) + return erc20Addr } func (s *IntegrationTestSuite) TestXMRTaker_ERC20_Query() { - s.testXMRTakerQuery(types.EthAsset(deployERC20Mock(s.T()))) + s.testXMRTakerQuery(s.testToken) } func (s *IntegrationTestSuite) TestSuccess_ERC20_OneSwap() { - s.testSuccessOneSwap(types.EthAsset(deployERC20Mock(s.T())), false) + s.testSuccessOneSwap(s.testToken, false) } func (s *IntegrationTestSuite) TestRefund_ERC20_XMRTakerCancels() { - s.testRefundXMRTakerCancels(types.EthAsset(deployERC20Mock(s.T()))) + s.testRefundXMRTakerCancels(s.testToken) } func (s *IntegrationTestSuite) TestAbort_ERC20_XMRTakerCancels() { - s.testAbortXMRTakerCancels(types.EthAsset(deployERC20Mock(s.T()))) + s.testAbortXMRTakerCancels(s.testToken) } func (s *IntegrationTestSuite) TestAbort_ERC20_XMRMakerCancels() { - s.testAbortXMRMakerCancels(types.EthAsset(deployERC20Mock(s.T()))) + s.testAbortXMRMakerCancels(s.testToken) } func (s *IntegrationTestSuite) TestError_ERC20_ShouldOnlyTakeOfferOnce() { - s.testErrorShouldOnlyTakeOfferOnce(types.EthAsset(deployERC20Mock(s.T()))) + s.testErrorShouldOnlyTakeOfferOnce(s.testToken) } func (s *IntegrationTestSuite) TestSuccess_ERC20_ConcurrentSwaps() { - s.testSuccessConcurrentSwaps(types.EthAsset(deployERC20Mock(s.T()))) + s.testSuccessConcurrentSwaps(s.testToken) } diff --git a/tests/ganache.go b/tests/ganache.go index 057b0f7f..186722d4 100644 --- a/tests/ganache.go +++ b/tests/ganache.go @@ -35,6 +35,7 @@ var testPackages = []struct { name string numKeys int }{ + {"cmd/swapcli", 2}, {"cmd/swapd", 2}, {"daemon", 2}, {"ethereum", 16}, diff --git a/tests/integration_test.go b/tests/integration_test.go index a70028b4..1182bb26 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -41,6 +41,7 @@ const ( defaultXMRTakerSwapdWSEndpoint = "ws://localhost:5000/ws" defaultXMRMakerSwapdEndpoint = "http://localhost:5001" defaultXMRMakerSwapdWSEndpoint = "ws://localhost:5001/ws" + defaultCharlieSwapdEndpoint = "http://localhost:5002" defaultCharlieSwapdWSEndpoint = "ws://localhost:5002/ws" defaultDiscoverTimeout = 2 // 2 seconds @@ -55,13 +56,16 @@ var ( type IntegrationTestSuite struct { suite.Suite + testToken types.EthAsset } func TestRunIntegrationTests(t *testing.T) { if testing.Short() || os.Getenv(testsEnv) != integrationMode { t.Skip() } - suite.Run(t, new(IntegrationTestSuite)) + s := new(IntegrationTestSuite) + s.testToken = types.EthAsset(deployERC20Mock(t)) + suite.Run(t, s) } func (s *IntegrationTestSuite) SetupTest() { @@ -90,7 +94,7 @@ func mineMinXMRMakerBalance(t *testing.T, minBalance *coins.PiconeroAmount) { daemonCli := monerorpc.New(monero.MonerodRegtestEndpoint, nil).Daemon ctx := context.Background() for { - balances, err := rpcclient.NewClient(ctx, defaultXMRMakerSwapdEndpoint).Balances() + balances, err := rpcclient.NewClient(ctx, defaultXMRMakerSwapdEndpoint).Balances(nil) require.NoError(t, err) if balances.PiconeroUnlockedBalance.Cmp(minBalance) >= 0 { break