mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-08 21:58:07 -05:00
150 lines
5.0 KiB
Go
150 lines
5.0 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
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/coins"
|
|
"github.com/athanorlabs/atomic-swap/common/rpctypes"
|
|
"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/rpcclient"
|
|
"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) {
|
|
fundingEC := extethclient.CreateTestClient(t, tests.GetTakerTestKey(t))
|
|
tokenAsset := getMockTetherAsset(t, fundingEC)
|
|
tokenAddr := tokenAsset.Address()
|
|
token, err := fundingEC.ERC20Info(context.Background(), tokenAddr)
|
|
require.NoError(t, err)
|
|
|
|
minXMR := coins.StrToDecimal("0.1")
|
|
maxXMR := coins.StrToDecimal("0.25")
|
|
exRate := coins.StrToExchangeRate("140")
|
|
providesAmt := coins.NewEthAssetAmount(coins.StrToDecimal("33.999994"), token) // 33.999994 USDT / 140 = 0.2428571 XMR
|
|
gasMoney := coins.EtherToWei(coins.StrToDecimal("0.1"))
|
|
|
|
bobEthKey, err := crypto.GenerateKey()
|
|
require.NoError(t, err)
|
|
bobConf := CreateTestConf(t, bobEthKey)
|
|
monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR))
|
|
|
|
// Ensure that Alice has no tokens and definitely no pre-approval to spend
|
|
// any of those tokens by giving her a brand-new ETH key.
|
|
aliceEthKey, err := crypto.GenerateKey()
|
|
require.NoError(t, err)
|
|
aliceConf := CreateTestConf(t, aliceEthKey)
|
|
timeout := 7 * time.Minute
|
|
|
|
// Fund Alice and Bob with a little ether for gas. Bob needs gas to claim,
|
|
// as ERC20 token swaps cannot use a relayer.
|
|
_, err = fundingEC.Transfer(context.Background(), aliceConf.EthereumClient.Address(), gasMoney, nil)
|
|
require.NoError(t, err)
|
|
_, err = fundingEC.Transfer(context.Background(), bobConf.EthereumClient.Address(), gasMoney, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Fund Alice with the exact amount of token that she'll provide in the swap
|
|
// with Bob. After the swap is over, her token balance should be exactly
|
|
// zero.
|
|
erc20Iface, err := contracts.NewIERC20(tokenAddr, fundingEC.Raw())
|
|
require.NoError(t, err)
|
|
txOpts, err := fundingEC.TxOpts(context.Background())
|
|
require.NoError(t, err)
|
|
_, err = erc20Iface.Transfer(txOpts, aliceConf.EthereumClient.Address(), providesAmt.BigInt())
|
|
require.NoError(t, err)
|
|
|
|
ctx, _ := LaunchDaemons(t, timeout, aliceConf, bobConf)
|
|
|
|
bc := rpcclient.NewClient(ctx, bobConf.RPCPort)
|
|
ac := rpcclient.NewClient(ctx, aliceConf.RPCPort)
|
|
|
|
_, 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
|
|
peersWithOffers, err := ac.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]
|
|
require.Equal(t, tokenAddr.String(), offer.EthAsset.Address().String())
|
|
|
|
aliceStatusCh, err := ac.TakeOfferAndSubscribe(peerID, offer.ID, providesAmt.AsStd())
|
|
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 final token balances via RPC method instead of doing it directly on
|
|
// the eth client. Bob should have exactly the provided amount and Alice's
|
|
// token balance should now be zero.
|
|
//
|
|
balReq := &rpctypes.BalancesRequest{TokenAddrs: []ethcommon.Address{tokenAddr}}
|
|
|
|
bobBal, err := bc.Balances(balReq)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, bobBal.TokenBalances)
|
|
require.Equal(t, providesAmt.AsStdString(), bobBal.TokenBalances[0].AsStdString())
|
|
|
|
aliceBal, err := ac.Balances(balReq)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, aliceBal.TokenBalances)
|
|
require.Equal(t, "0", aliceBal.TokenBalances[0].AsStdString())
|
|
}
|