Files
atomic-swap/daemon/swap_daemon_test.go
2023-06-19 14:47:27 -05:00

450 lines
14 KiB
Go

// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
package daemon
import (
"context"
"math/big"
"sync"
"syscall"
"testing"
"time"
"github.com/cockroachdb/apd/v3"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/cliutil"
"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/extethclient"
"github.com/athanorlabs/atomic-swap/monero"
"github.com/athanorlabs/atomic-swap/net"
"github.com/athanorlabs/atomic-swap/rpcclient"
"github.com/athanorlabs/atomic-swap/tests"
)
func init() {
cliutil.SetLogLevels("debug")
}
// minimumFundAlice gives Alice enough ETH to do everything but relay a claim
func minimumFundAlice(t *testing.T, aliceAddr ethcommon.Address, providesAmt *apd.Decimal) {
ctx := context.Background()
fundingKey := tests.GetTakerTestKey(t)
ec := extethclient.CreateTestClient(t, fundingKey)
const (
aliceGasRation = contracts.MaxNewSwapETHGas + contracts.MaxSetReadyGas + contracts.MaxRefundETHGas
)
// We give Alice enough gas money to refund if needed, but not enough to
// relay a claim
suggestedGasPrice, err := ec.Raw().SuggestGasPrice(ctx)
require.NoError(t, err)
gasCostWei := new(big.Int).Mul(suggestedGasPrice, big.NewInt(aliceGasRation))
fundAmt := new(apd.Decimal)
_, err = coins.DecimalCtx().Add(fundAmt, providesAmt, coins.NewWeiAmount(gasCostWei).AsEther())
require.NoError(t, err)
_, err = ec.Transfer(ctx, aliceAddr, coins.EtherToWei(fundAmt), nil)
require.NoError(t, err)
bal, err := ec.Balance(ctx)
require.NoError(t, err)
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
// the network, and Alice relays Bob's claim.
func TestRunSwapDaemon_SwapBobHasNoEth_AliceRelaysClaim(t *testing.T) {
minXMR := coins.StrToDecimal("1")
maxXMR := minXMR
exRate := coins.StrToExchangeRate("0.1")
providesAmt, err := exRate.ToETH(minXMR)
require.NoError(t, err)
bobEthKey, err := crypto.GenerateKey() // Bob has no ETH (not a ganache key)
require.NoError(t, err)
bobConf := CreateTestConf(t, bobEthKey)
monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR))
aliceConf := CreateTestConf(t, tests.GetTakerTestKey(t))
timeout := 7 * time.Minute
ctx, _ := LaunchDaemons(t, timeout, bobConf, aliceConf)
bc := rpcclient.NewClient(ctx, bobConf.RPCPort)
ac := rpcclient.NewClient(ctx, aliceConf.RPCPort)
useRelayer := false // Bob will use the relayer regardless, because he has no ETH
makeResp, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, types.EthAssetETH, useRelayer)
require.NoError(t, err)
aliceStatusCh, err := ac.TakeOfferAndSubscribe(makeResp.PeerID, makeResp.OfferID, 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
}
//
// Bob's ending balance should be Alice's provided amount minus the relayer fee
//
expectedBal := new(apd.Decimal)
_, err = coins.DecimalCtx().Sub(expectedBal, providesAmt, coins.RelayerFeeETH)
require.NoError(t, err)
bobBalance, err := bobConf.EthereumClient.Balance(ctx)
require.NoError(t, err)
require.Equal(t, expectedBal.Text('f'), bobBalance.AsEtherString())
}
// Tests the scenario where Bob has no ETH, he can't find an advertised relayer,
// and Alice does not have enough ETH to relay his claim. The end result should
// be a refund. Note that this test has a long pause, as the refund cannot
// happen until T2 expires.
func TestRunSwapDaemon_NoRelayersAvailable_Refund(t *testing.T) {
minXMR := coins.StrToDecimal("1")
maxXMR := minXMR
exRate := coins.StrToExchangeRate("0.1")
providesAmt, err := exRate.ToETH(minXMR)
require.NoError(t, err)
bobEthKey, err := crypto.GenerateKey() // Bob has no ETH (not a ganache key)
require.NoError(t, err)
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)
minimumFundAlice(t, aliceConf.EthereumClient.Address(), providesAmt)
timeout := 8 * time.Minute
ctx, _ := LaunchDaemons(t, timeout, bobConf, aliceConf)
bc := rpcclient.NewClient(ctx, bobConf.RPCPort)
ac := rpcclient.NewClient(ctx, aliceConf.RPCPort)
useRelayer := false // Bob will use unsuccessfully use the relayer regardless, because he has no ETH
makeResp, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, types.EthAssetETH, useRelayer)
require.NoError(t, err)
aliceStatusCh, err := ac.TakeOfferAndSubscribe(makeResp.PeerID, makeResp.OfferID, providesAmt)
require.NoError(t, err)
var statusWG sync.WaitGroup
statusWG.Add(2)
// Ensure Alice completes the swap with a refund
go func() {
defer statusWG.Done()
for {
select {
case status := <-aliceStatusCh:
t.Log("> Alice got status:", status)
if !status.IsOngoing() {
assert.Equal(t, types.CompletedRefund.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 as a refund
go func() {
defer statusWG.Done()
for {
select {
case status := <-bobStatusCh:
t.Log("> Bob got status:", status)
if !status.IsOngoing() {
assert.Equal(t, types.CompletedRefund.String(), status.String())
return
}
case <-ctx.Done():
t.Errorf("Bob's context cancelled before he completed the swap")
return
}
}
}()
statusWG.Wait()
}
// Tests the scenario where Bob has no ETH and Charlie, an advertised relayer,
// performs the relay so Bob can get his ETH. To ensure that the test does not
// succeed by Alice relaying the claim, we ensure that Alice does not have
// enough ETH left over after the swap to relay.
func TestRunSwapDaemon_CharlieRelays(t *testing.T) {
minXMR := coins.StrToDecimal("1")
maxXMR := minXMR
exRate := coins.StrToExchangeRate("0.1")
providesAmt, err := exRate.ToETH(minXMR)
require.NoError(t, err)
bobEthKey, err := crypto.GenerateKey() // Bob has no ETH (not a ganache key)
require.NoError(t, err)
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)
minimumFundAlice(t, aliceConf.EthereumClient.Address(), providesAmt)
// Charlie can safely use the taker key, as Alice is not using it.
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)
bc := rpcclient.NewClient(ctx, bobConf.RPCPort)
ac := rpcclient.NewClient(ctx, aliceConf.RPCPort)
useRelayer := false // Bob will use the relayer regardless, because he has no ETH
makeResp, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, types.EthAssetETH, useRelayer)
require.NoError(t, err)
aliceStatusCh, err := ac.TakeOfferAndSubscribe(makeResp.PeerID, makeResp.OfferID, 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
}
}
}()
// Ensure 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
}
//
// Bob's ending balance should be Alice's provided amount minus the relayer fee
//
bobExpectedBal := new(apd.Decimal)
_, err = coins.DecimalCtx().Sub(bobExpectedBal, providesAmt, coins.RelayerFeeETH)
require.NoError(t, err)
bobBalance, err := bobConf.EthereumClient.Balance(ctx)
require.NoError(t, err)
require.Equal(t, bobExpectedBal.Text('f'), bobBalance.AsEtherString())
//
// Charlie should be wealthier now than at the start, despite paying the claim
// gas, because he received the relayer fee.
//
charlieEC := charlieConf.EthereumClient
charlieBal, err := charlieEC.Balance(ctx)
require.NoError(t, err)
require.Greater(t, charlieBal.Cmp(charlieStartBal), 0)
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
// and cannot relay Alice's request. Bob falls back to Alice as the relayer of
// last resort, and she relays his claim.
func TestRunSwapDaemon_CharlieIsBroke_AliceRelays(t *testing.T) {
minXMR := coins.StrToDecimal("1")
maxXMR := minXMR
exRate := coins.StrToExchangeRate("0.1")
providesAmt, err := exRate.ToETH(minXMR)
require.NoError(t, err)
bobEthKey, err := crypto.GenerateKey() // Bob has no ETH (not a ganache key)
require.NoError(t, err)
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))
// Charlie is a relayer, but he has no ETH
charlieEthKey, err := crypto.GenerateKey()
require.NoError(t, err)
charlieConf := CreateTestConf(t, charlieEthKey)
charlieConf.IsRelayer = true
timeout := 7 * time.Minute
ctx, _ := LaunchDaemons(t, timeout, bobConf, aliceConf, charlieConf)
bc := rpcclient.NewClient(ctx, bobConf.RPCPort)
ac := rpcclient.NewClient(ctx, aliceConf.RPCPort)
useRelayer := false // Bob will use the relayer regardless, because he has no ETH
makeResp, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, types.EthAssetETH, useRelayer)
require.NoError(t, err)
aliceStatusCh, err := ac.TakeOfferAndSubscribe(makeResp.PeerID, makeResp.OfferID, 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
}
}
}()
// Ensure 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
}
//
// Bob's ending balance should be Alice's provided amount minus the relayer fee
//
bobExpectedBal := new(apd.Decimal)
_, err = coins.DecimalCtx().Sub(bobExpectedBal, providesAmt, coins.RelayerFeeETH)
require.NoError(t, err)
bobBalance, err := bobConf.EthereumClient.Balance(ctx)
require.NoError(t, err)
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))
timeout := time.Minute
ctx, _ := LaunchDaemons(t, timeout, conf)
c := rpcclient.NewClient(ctx, conf.RPCPort)
versionResp, err := c.Version()
require.NoError(t, err)
require.Equal(t, conf.EnvConf.Env, versionResp.Env)
require.NotEmpty(t, versionResp.SwapdVersion)
require.Equal(t, conf.EnvConf.SwapCreatorAddr, *versionResp.SwapCreatorAddr)
require.Equal(t, net.ChainProtocolID(common.Development), versionResp.P2PVersion)
}
// Tests the shutdown RPC method
func TestRunSwapDaemon_RPC_Shutdown(t *testing.T) {
conf := CreateTestConf(t, tests.GetMakerTestKey(t))
timeout := time.Minute
ctx, _ := LaunchDaemons(t, timeout, conf)
c := rpcclient.NewClient(ctx, conf.RPCPort)
err := c.Shutdown()
require.NoError(t, err)
err = c.Shutdown()
require.ErrorIs(t, err, syscall.ECONNREFUSED)
}