mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-07 21:34:05 -05:00
XMR taker is relayer of last resort for the maker (#366)
Co-authored-by: noot <36753753+noot@users.noreply.github.com>
This commit is contained in:
@@ -253,6 +253,7 @@ func setLogLevels(level string) {
|
||||
_ = logging.SetLogLevel("cmd", level)
|
||||
_ = logging.SetLogLevel("extethclient", level)
|
||||
_ = logging.SetLogLevel("ethereum/watcher", level)
|
||||
_ = logging.SetLogLevel("ethereum/block", level)
|
||||
_ = logging.SetLogLevel("monero", level)
|
||||
_ = logging.SetLogLevel("net", level)
|
||||
_ = logging.SetLogLevel("offers", level)
|
||||
|
||||
@@ -24,13 +24,13 @@ type SwapState interface {
|
||||
// It is implemented by *xmrtaker.swapState and *xmrmaker.swapState
|
||||
type SwapStateNet interface {
|
||||
HandleProtocolMessage(msg Message) error
|
||||
ID() types.Hash
|
||||
OfferID() types.Hash
|
||||
Exit() error
|
||||
}
|
||||
|
||||
// SwapStateRPC contains the methods used by the RPC server into the SwapState.
|
||||
type SwapStateRPC interface {
|
||||
SendKeysMessage() Message
|
||||
ID() types.Hash
|
||||
OfferID() types.Hash
|
||||
Exit() error
|
||||
}
|
||||
|
||||
@@ -7,12 +7,16 @@ import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
)
|
||||
|
||||
// Reverse returns a copy of the slice with the bytes in reverse order
|
||||
@@ -91,3 +95,10 @@ func GetFreeTCPPort() (uint, error) {
|
||||
|
||||
return uint(ln.Addr().(*net.TCPAddr).Port), nil
|
||||
}
|
||||
|
||||
// ReceiptInfo creates a string for logging from an ethereum transaction receipt
|
||||
func ReceiptInfo(receipt *ethtypes.Receipt) string {
|
||||
txCostWei := new(big.Int).Mul(receipt.EffectiveGasPrice, big.NewInt(int64(receipt.GasUsed)))
|
||||
return fmt.Sprintf("gas-used: %d, gas-price: %s WEI, tx-cost: %s ETH, block: %s, txID: %s",
|
||||
receipt.GasUsed, receipt.EffectiveGasPrice, coins.FmtWeiAsETH(txCostWei), receipt.BlockNumber, receipt.TxHash)
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"math"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
@@ -106,3 +108,15 @@ func TestGetFreeTCPPort(t *testing.T) {
|
||||
require.GreaterOrEqual(t, port, uint(1024))
|
||||
require.LessOrEqual(t, port, uint(math.MaxUint16))
|
||||
}
|
||||
|
||||
func TestReceiptInfo(t *testing.T) {
|
||||
receipt := ðtypes.Receipt{
|
||||
GasUsed: 100000,
|
||||
EffectiveGasPrice: big.NewInt(1000000),
|
||||
BlockNumber: big.NewInt(99),
|
||||
TxHash: ethcommon.Hash{1, 2, 3},
|
||||
}
|
||||
logStr := ReceiptInfo(receipt)
|
||||
const expectedStr = "gas-used: 100000, gas-price: 1000000 WEI, tx-cost: 0.0000001 ETH, block: 99, txID: 0x0102030000000000000000000000000000000000000000000000000000000000" //nolint:lll
|
||||
require.Equal(t, expectedStr, logStr)
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func RunSwapDaemon(ctx context.Context, conf *SwapdConfig) (err error) {
|
||||
}
|
||||
|
||||
// connect the maker/taker handlers to the p2p network host
|
||||
host.SetHandlers(xmrMaker, xmrTaker)
|
||||
host.SetHandlers(xmrMaker, swapBackend)
|
||||
if err = host.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
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"
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
"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"
|
||||
"github.com/athanorlabs/atomic-swap/relayer"
|
||||
@@ -31,6 +34,11 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/tests"
|
||||
)
|
||||
|
||||
const (
|
||||
// transferGas is the amount of gas to perform a standard ETH transfer
|
||||
transferGas = 21000
|
||||
)
|
||||
|
||||
func init() {
|
||||
// alphabetically ordered
|
||||
level := "debug"
|
||||
@@ -41,6 +49,7 @@ func init() {
|
||||
_ = logging.SetLogLevel("cmd", level)
|
||||
_ = logging.SetLogLevel("extethclient", level)
|
||||
_ = logging.SetLogLevel("ethereum/watcher", level)
|
||||
_ = logging.SetLogLevel("ethereum/block", level)
|
||||
_ = logging.SetLogLevel("monero", level)
|
||||
_ = logging.SetLogLevel("net", level)
|
||||
_ = logging.SetLogLevel("offers", level)
|
||||
@@ -73,6 +82,69 @@ func getSwapFactoryAddress(t *testing.T, ec *ethclient.Client) ethcommon.Address
|
||||
return swapFactoryAddr
|
||||
}
|
||||
|
||||
func privKeyToAddr(privKey *ecdsa.PrivateKey) ethcommon.Address {
|
||||
return crypto.PubkeyToAddress(*privKey.Public().(*ecdsa.PublicKey))
|
||||
}
|
||||
|
||||
func transfer(t *testing.T, fromKey *ecdsa.PrivateKey, toAddress ethcommon.Address, ethAmount *apd.Decimal) {
|
||||
ctx := context.Background()
|
||||
ec, chainID := tests.NewEthClient(t)
|
||||
fromAddress := privKeyToAddr(fromKey)
|
||||
|
||||
gasPrice, err := ec.SuggestGasPrice(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
nonce, err := ec.PendingNonceAt(ctx, fromAddress)
|
||||
require.NoError(t, err)
|
||||
|
||||
weiAmount := coins.EtherToWei(ethAmount).BigInt()
|
||||
|
||||
tx := ethtypes.NewTx(ðtypes.LegacyTx{
|
||||
Nonce: nonce,
|
||||
To: &toAddress,
|
||||
Value: weiAmount,
|
||||
Gas: transferGas,
|
||||
GasPrice: gasPrice,
|
||||
})
|
||||
signedTx, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainID), fromKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ec.SendTransaction(ctx, signedTx)
|
||||
require.NoError(t, err)
|
||||
_, err = block.WaitForReceipt(ctx, ec, signedTx.Hash())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// minimumFundAlice gives Alice enough ETH to do everything but relay a claim
|
||||
func minimumFundAlice(t *testing.T, ec extethclient.EthClient, providesAmt *apd.Decimal) {
|
||||
fundingKey := tests.GetTakerTestKey(t)
|
||||
|
||||
// When this comment was written, sample gas costs were:
|
||||
// newSwap: 53787
|
||||
// setReady: 34452
|
||||
// refund: 46692
|
||||
// relayClaim: 130507
|
||||
//
|
||||
const (
|
||||
aliceGasRation = 150000 // roughly 10% more than newSwap+setRead+refund
|
||||
)
|
||||
// We give Alice enough gas money to refund if needed, but not enough to
|
||||
// relay a claim:
|
||||
// 150000 - (53787 + 34452) = 61761
|
||||
//
|
||||
suggestedGasPrice, err := ec.Raw().SuggestGasPrice(context.Background())
|
||||
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)
|
||||
transfer(t, fundingKey, ec.Address(), fundAmt)
|
||||
|
||||
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)
|
||||
@@ -103,13 +175,45 @@ func createTestConf(t *testing.T, ethKey *ecdsa.PrivateKey) *SwapdConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario, where Bob has no ETH and Alice relays his claim.
|
||||
func TestRunSwapDaemon_SwapBobHasNoEth(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
@@ -121,45 +225,18 @@ func TestRunSwapDaemon_SwapBobHasNoEth(t *testing.T) {
|
||||
bobConf := createTestConf(t, bobEthKey)
|
||||
monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR))
|
||||
|
||||
var stoppedWG sync.WaitGroup
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
stoppedWG.Wait() // ensure daemons are stopped even if require fails
|
||||
})
|
||||
|
||||
stoppedWG.Add(1)
|
||||
go func() {
|
||||
defer stoppedWG.Done()
|
||||
err := RunSwapDaemon(ctx, bobConf) //nolint:govet
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}()
|
||||
WaitForSwapdStart(t, bobConf.RPCPort)
|
||||
|
||||
bc := rpcclient.NewClient(ctx, fmt.Sprintf("http://127.0.0.1:%d", bobConf.RPCPort))
|
||||
bws, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", bobConf.RPCPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Configure Alice to be a relayer and to use Bob as a bootnode
|
||||
aliceConf := createTestConf(t, tests.GetTakerTestKey(t))
|
||||
aliceConf.IsRelayer = true
|
||||
bobAddrs, err := bc.Addresses()
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(bobAddrs.Addrs), 1)
|
||||
aliceConf.EnvConf.Bootnodes = []string{bobAddrs.Addrs[0]}
|
||||
|
||||
stoppedWG.Add(1)
|
||||
go func() {
|
||||
defer stoppedWG.Done()
|
||||
err := RunSwapDaemon(ctx, aliceConf) //nolint:govet
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}()
|
||||
WaitForSwapdStart(t, aliceConf.RPCPort)
|
||||
timeout := 5 * time.Minute
|
||||
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)
|
||||
ac, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", aliceConf.RPCPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
useRelayer := false // Bob will use the relayer regardless, because he has no ETH
|
||||
makeResp, bobStatusCh, err := bws.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, types.EthAssetETH, useRelayer)
|
||||
require.NoError(t, err)
|
||||
|
||||
ac, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", aliceConf.RPCPort))
|
||||
makeResp, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, types.EthAssetETH, useRelayer)
|
||||
require.NoError(t, err)
|
||||
|
||||
aliceStatusCh, err := ac.TakeOfferAndSubscribe(makeResp.PeerID, makeResp.OfferID, providesAmt)
|
||||
@@ -221,3 +298,284 @@ func TestRunSwapDaemon_SwapBobHasNoEth(t *testing.T) {
|
||||
|
||||
require.Equal(t, expectedBal.Text('f'), coins.FmtWeiAsETH(bobBalance))
|
||||
}
|
||||
|
||||
// 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 T1 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, providesAmt)
|
||||
|
||||
timeout := 7 * time.Minute
|
||||
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)
|
||||
ac, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", aliceConf.RPCPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
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, 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 := 5 * time.Minute
|
||||
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)
|
||||
ac, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", aliceConf.RPCPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
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, relayer.FeeEth)
|
||||
require.NoError(t, err)
|
||||
bobBalance, err := bobConf.EthereumClient.Balance(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, bobExpectedBal.Text('f'), coins.FmtWeiAsETH(bobBalance))
|
||||
|
||||
//
|
||||
// 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 := new(big.Int).Sub(charlieBal, charlieStartBal)
|
||||
t.Logf("Charlie earned %s ETH", coins.FmtWeiAsETH(charlieProfitWei))
|
||||
}
|
||||
|
||||
// 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 := 5 * time.Minute
|
||||
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)
|
||||
ac, err := wsclient.NewWsClient(ctx, fmt.Sprintf("ws://127.0.0.1:%d/ws", aliceConf.RPCPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
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, relayer.FeeEth)
|
||||
require.NoError(t, err)
|
||||
bobBalance, err := bobConf.EthereumClient.Balance(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, bobExpectedBal.Text('f'), coins.FmtWeiAsETH(bobBalance))
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ func (db *Database) PutSwap(s *swap.Info) error {
|
||||
return err
|
||||
}
|
||||
|
||||
key := s.ID
|
||||
key := s.OfferID
|
||||
err = db.swapTable.Put(key[:], val)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/ChainSafe/chaindb"
|
||||
logging "github.com/ipfs/go-log"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
@@ -22,6 +23,8 @@ func init() {
|
||||
_ = logging.SetLogLevel("db", "debug")
|
||||
}
|
||||
|
||||
var testPeerID, _ = peer.Decode("12D3KooWQQRJuKTZ35eiHGNPGDpQqjpJSdaxEMJRxi6NWFrrvQVi")
|
||||
|
||||
// infoAsJSON converts an Info object to a JSON string. Converting
|
||||
// the struct to JSON is the easiest way to compare 2 structs for
|
||||
// equality, as there are many pointer fields.
|
||||
@@ -41,7 +44,8 @@ func TestDatabase_OfferTable(t *testing.T) {
|
||||
// put swap to ensure iterator over offers is ok
|
||||
infoA := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: types.Hash{0x1},
|
||||
PeerID: testPeerID,
|
||||
OfferID: types.Hash{0x1},
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("0.1"),
|
||||
ExpectedAmount: coins.StrToDecimal("1"),
|
||||
@@ -108,7 +112,8 @@ func TestDatabase_GetAllOffers_InvalidEntry(t *testing.T) {
|
||||
// Put a swap entry tied to the bad offer in the database
|
||||
swapEntry := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: badOfferID,
|
||||
PeerID: testPeerID,
|
||||
OfferID: badOfferID,
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("0.1"),
|
||||
ExpectedAmount: coins.StrToDecimal("1"),
|
||||
@@ -180,7 +185,8 @@ func TestDatabase_SwapTable(t *testing.T) {
|
||||
|
||||
infoA := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: offerA.ID,
|
||||
PeerID: testPeerID,
|
||||
OfferID: offerA.ID,
|
||||
Provides: offerA.Provides,
|
||||
ProvidedAmount: offerA.MinAmount,
|
||||
ExpectedAmount: offerA.MinAmount,
|
||||
@@ -199,7 +205,8 @@ func TestDatabase_SwapTable(t *testing.T) {
|
||||
|
||||
infoB := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: types.Hash{0x2},
|
||||
PeerID: testPeerID,
|
||||
OfferID: types.Hash{0x2},
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("1.5"),
|
||||
ExpectedAmount: coins.StrToDecimal("0.15"),
|
||||
@@ -238,7 +245,8 @@ func TestDatabase_GetAllSwaps_InvalidEntry(t *testing.T) {
|
||||
|
||||
goodInfo := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: types.Hash{0x1, 0x2, 0x3},
|
||||
PeerID: testPeerID,
|
||||
OfferID: types.Hash{0x1, 0x2, 0x3},
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("1.5"),
|
||||
ExpectedAmount: coins.StrToDecimal("0.15"),
|
||||
@@ -261,7 +269,7 @@ func TestDatabase_GetAllSwaps_InvalidEntry(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Establish a baseline that both the good and bad entries exist before calling GetAllSwaps
|
||||
exists, err := db.swapTable.Has(goodInfo.ID[:])
|
||||
exists, err := db.swapTable.Has(goodInfo.OfferID[:])
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists)
|
||||
|
||||
@@ -273,10 +281,10 @@ func TestDatabase_GetAllSwaps_InvalidEntry(t *testing.T) {
|
||||
swaps, err := db.GetAllSwaps()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(swaps))
|
||||
require.EqualValues(t, goodInfo.ID[:], swaps[0].ID[:])
|
||||
require.EqualValues(t, goodInfo.OfferID[:], swaps[0].OfferID[:])
|
||||
|
||||
// GetAllSwaps should have pruned the bad swap info entry, but left the good entry
|
||||
exists, err = db.swapTable.Has(goodInfo.ID[:])
|
||||
exists, err = db.swapTable.Has(goodInfo.OfferID[:])
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists) // entry still exists
|
||||
|
||||
@@ -301,7 +309,8 @@ func TestDatabase_SwapTable_Update(t *testing.T) {
|
||||
|
||||
infoA := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: id,
|
||||
PeerID: testPeerID,
|
||||
OfferID: id,
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("0.1"),
|
||||
ExpectedAmount: coins.StrToDecimal("1"),
|
||||
|
||||
@@ -45,16 +45,11 @@ func WaitForReceipt(ctx context.Context, ec *ethclient.Client, txHash ethcommon.
|
||||
continue
|
||||
}
|
||||
if receipt.Status != ethtypes.ReceiptStatusSuccessful {
|
||||
err = fmt.Errorf("transaction failed (gas-lost=%d tx=%s block=%d), %w",
|
||||
receipt.GasUsed, txHash, receipt.BlockNumber, ErrorFromBlock(ctx, ec, receipt))
|
||||
err = fmt.Errorf("failed transaction included in block (%s): %w",
|
||||
common.ReceiptInfo(receipt), ErrorFromBlock(ctx, ec, receipt))
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("transaction %s included in chain, block hash=%s, block number=%d, gas used=%d",
|
||||
txHash,
|
||||
receipt.BlockHash,
|
||||
receipt.BlockNumber,
|
||||
receipt.CumulativeGasUsed,
|
||||
)
|
||||
log.Debugf("transaction included in chain %s", common.ReceiptInfo(receipt))
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -108,5 +108,5 @@ func TestWaitForReceipt_failWhenTransactionIsMined(t *testing.T) {
|
||||
// Ensure that we got the expected error
|
||||
require.Contains(t, err.Error(), "revert block.timestamp was not less than stamp")
|
||||
// Ensure that the expected error happened when the transaction was mined and not earlier
|
||||
require.Contains(t, err.Error(), "gas-lost=")
|
||||
require.Contains(t, err.Error(), "failed transaction included in block")
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ type EthClient interface {
|
||||
|
||||
SetGasPrice(uint64)
|
||||
SetGasLimit(uint64)
|
||||
SuggestGasPrice(ctx context.Context) (*big.Int, error)
|
||||
CallOpts(ctx context.Context) *bind.CallOpts
|
||||
TxOpts(ctx context.Context) (*bind.TransactOpts, error)
|
||||
ChainID() *big.Int
|
||||
@@ -137,6 +138,16 @@ func (c *ethClient) Balance(ctx context.Context) (*big.Int, error) {
|
||||
return bal, nil
|
||||
}
|
||||
|
||||
// SuggestedGasPrice returns the underlying eth client's suggested gas price
|
||||
// unless the user specified a fixed gas price to use, in which case the user
|
||||
// supplied value is returned.
|
||||
func (c *ethClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
|
||||
if c.gasPrice != nil {
|
||||
return c.gasPrice, nil
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ package contracts
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"sync"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"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/crypto/secp256k1"
|
||||
"github.com/athanorlabs/atomic-swap/dleq"
|
||||
@@ -47,18 +49,77 @@ func testNewSwap(t *testing.T, asset ethcommon.Address) {
|
||||
require.NotEqual(t, ethcommon.Address{}, address)
|
||||
require.NotNil(t, tx)
|
||||
require.NotNil(t, contract)
|
||||
|
||||
receipt, err := block.WaitForReceipt(context.Background(), conn, tx.Hash())
|
||||
require.NoError(t, err)
|
||||
t.Logf("gas cost to deploy SwapFactory.sol: %d", receipt.GasUsed)
|
||||
|
||||
nonce := big.NewInt(0)
|
||||
tx, err = contract.NewSwap(auth, [32]byte{}, [32]byte{},
|
||||
ethcommon.Address{}, defaultTimeoutDuration, defaultTimeoutDuration, asset, big.NewInt(0), nonce)
|
||||
owner := auth.From
|
||||
claimer := common.EthereumPrivateKeyToAddress(tests.GetMakerTestKey(t))
|
||||
|
||||
var pubKeyClaim, pubKeyRefund [32]byte
|
||||
_, err = rand.Read(pubKeyClaim[:])
|
||||
require.NoError(t, err)
|
||||
receipt, err = block.WaitForReceipt(context.Background(), conn, tx.Hash())
|
||||
_, err = rand.Read(pubKeyRefund[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
nonce, err := rand.Prime(rand.Reader, 256)
|
||||
require.NoError(t, err)
|
||||
value, err := rand.Prime(rand.Reader, 53) // (2^54 - 1) / 10^18 ~= .018 ETH
|
||||
require.NoError(t, err)
|
||||
|
||||
isEthAsset := asset == ethAssetAddress
|
||||
|
||||
if isEthAsset {
|
||||
auth.Value = value
|
||||
} else {
|
||||
value = big.NewInt(0)
|
||||
}
|
||||
|
||||
tx, err = contract.NewSwap(
|
||||
auth,
|
||||
pubKeyClaim,
|
||||
pubKeyRefund,
|
||||
claimer,
|
||||
defaultTimeoutDuration,
|
||||
defaultTimeoutDuration,
|
||||
asset,
|
||||
value,
|
||||
nonce,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
receipt, err = block.WaitForReceipt(context.Background(), conn, tx.Hash())
|
||||
require.NoError(t, err)
|
||||
t.Logf("gas cost to call new_swap: %d", receipt.GasUsed)
|
||||
|
||||
newSwapLogIndex := 0
|
||||
if !isEthAsset {
|
||||
newSwapLogIndex = 2
|
||||
}
|
||||
require.Equal(t, newSwapLogIndex+1, len(receipt.Logs))
|
||||
|
||||
swapID, err := GetIDFromLog(receipt.Logs[newSwapLogIndex])
|
||||
require.NoError(t, err)
|
||||
|
||||
t0, t1, err := GetTimeoutsFromLog(receipt.Logs[newSwapLogIndex])
|
||||
require.NoError(t, err)
|
||||
|
||||
// validate that off-chain swapID calculation matches the on-chain value
|
||||
swap := SwapFactorySwap{
|
||||
Owner: owner,
|
||||
Claimer: claimer,
|
||||
PubKeyClaim: pubKeyClaim,
|
||||
PubKeyRefund: pubKeyRefund,
|
||||
Timeout0: t0,
|
||||
Timeout1: t1,
|
||||
Asset: asset,
|
||||
Value: value,
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
// validate our off-net calculation of the SwapID
|
||||
require.Equal(t, types.Hash(swapID).Hex(), swap.SwapID().Hex())
|
||||
}
|
||||
|
||||
func TestSwapFactory_NewSwap(t *testing.T) {
|
||||
|
||||
@@ -11,9 +11,12 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
|
||||
)
|
||||
|
||||
@@ -52,6 +55,75 @@ func StageToString(stage byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
// SwapID calculates and returns the same hashed swap identifier that newSwap
|
||||
// emits and that is used to track the on-chain stage of a swap.
|
||||
func (sfs *SwapFactorySwap) SwapID() types.Hash {
|
||||
uint256Ty, err := abi.NewType("uint256", "", nil)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create uint256 type: %s", err))
|
||||
}
|
||||
|
||||
bytes32Ty, err := abi.NewType("bytes32", "", nil)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create bytes32 type: %s", err))
|
||||
}
|
||||
|
||||
addressTy, err := abi.NewType("address", "", nil)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create address type: %s", err))
|
||||
}
|
||||
|
||||
arguments := abi.Arguments{
|
||||
{
|
||||
Type: addressTy,
|
||||
},
|
||||
{
|
||||
Type: addressTy,
|
||||
},
|
||||
{
|
||||
Type: bytes32Ty,
|
||||
},
|
||||
{
|
||||
Type: bytes32Ty,
|
||||
},
|
||||
{
|
||||
Type: uint256Ty,
|
||||
},
|
||||
{
|
||||
Type: uint256Ty,
|
||||
},
|
||||
{
|
||||
Type: addressTy,
|
||||
},
|
||||
{
|
||||
Type: uint256Ty,
|
||||
},
|
||||
{
|
||||
Type: uint256Ty,
|
||||
},
|
||||
}
|
||||
|
||||
args, err := arguments.Pack(
|
||||
sfs.Owner,
|
||||
sfs.Claimer,
|
||||
sfs.PubKeyClaim,
|
||||
sfs.PubKeyRefund,
|
||||
sfs.Timeout0,
|
||||
sfs.Timeout1,
|
||||
sfs.Asset,
|
||||
sfs.Value,
|
||||
sfs.Nonce,
|
||||
)
|
||||
if err != nil {
|
||||
// As long as none of the *big.Int fields are nil, this cannot fail.
|
||||
// When receiving SwapFactorySwap objects from the database or peers in
|
||||
// JSON, all *big.Int values are pre-validated to be non-nil.
|
||||
panic(fmt.Sprintf("failed to pack arguments: %s", err))
|
||||
}
|
||||
|
||||
return crypto.Keccak256Hash(args)
|
||||
}
|
||||
|
||||
// GetSecretFromLog returns the secret from a Claimed or Refunded log
|
||||
func GetSecretFromLog(log *ethtypes.Log, eventTopic [32]byte) (*mcrypto.PrivateSpendKey, error) {
|
||||
if eventTopic != claimedTopic && eventTopic != refundedTopic {
|
||||
|
||||
29
net/host.go
29
net/host.go
@@ -60,10 +60,10 @@ type Host struct {
|
||||
isRelayer bool
|
||||
|
||||
makerHandler MakerHandler
|
||||
takerHandler TakerHandler
|
||||
relayHandler RelayHandler
|
||||
|
||||
// swap instance info
|
||||
swapMu sync.Mutex
|
||||
swapMu sync.RWMutex
|
||||
swaps map[types.Hash]*swap
|
||||
}
|
||||
|
||||
@@ -108,11 +108,6 @@ func NewHost(cfg *Config) (*Host, error) {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// P2pHost returns the underlying go-p2p-net host.
|
||||
func (h *Host) P2pHost() P2pHost {
|
||||
return h.h
|
||||
}
|
||||
|
||||
func (h *Host) advertisedNamespaces() []string {
|
||||
provides := []string{""}
|
||||
|
||||
@@ -129,20 +124,18 @@ func (h *Host) advertisedNamespaces() []string {
|
||||
|
||||
// SetHandlers sets the maker and taker instances used by the host, and configures
|
||||
// the stream handlers.
|
||||
func (h *Host) SetHandlers(makerHandler MakerHandler, takerHandler TakerHandler) {
|
||||
func (h *Host) SetHandlers(makerHandler MakerHandler, relayHandler RelayHandler) {
|
||||
h.makerHandler = makerHandler
|
||||
h.takerHandler = takerHandler
|
||||
h.relayHandler = relayHandler
|
||||
|
||||
h.h.SetStreamHandler(queryProtocolID, h.handleQueryStream)
|
||||
if h.isRelayer {
|
||||
h.h.SetStreamHandler(relayProtocolID, h.handleRelayStream)
|
||||
}
|
||||
h.h.SetStreamHandler(relayProtocolID, h.handleRelayStream)
|
||||
h.h.SetStreamHandler(swapID, h.handleProtocolStream)
|
||||
}
|
||||
|
||||
// Start starts the bootstrap and discovery process.
|
||||
func (h *Host) Start() error {
|
||||
if h.makerHandler == nil || h.takerHandler == nil {
|
||||
if h.makerHandler == nil || h.relayHandler == nil {
|
||||
return errNilHandler
|
||||
}
|
||||
|
||||
@@ -161,8 +154,8 @@ func (h *Host) Stop() error {
|
||||
|
||||
// SendSwapMessage sends a message to the peer who we're currently doing a swap with.
|
||||
func (h *Host) SendSwapMessage(msg Message, id types.Hash) error {
|
||||
h.swapMu.Lock()
|
||||
defer h.swapMu.Unlock()
|
||||
h.swapMu.RLock()
|
||||
defer h.swapMu.RUnlock()
|
||||
|
||||
swap, has := h.swaps[id]
|
||||
if !has {
|
||||
@@ -173,8 +166,10 @@ func (h *Host) SendSwapMessage(msg Message, id types.Hash) error {
|
||||
}
|
||||
|
||||
// CloseProtocolStream closes the current swap protocol stream.
|
||||
func (h *Host) CloseProtocolStream(id types.Hash) {
|
||||
swap, has := h.swaps[id]
|
||||
func (h *Host) CloseProtocolStream(offerID types.Hash) {
|
||||
h.swapMu.RLock()
|
||||
swap, has := h.swaps[offerID]
|
||||
h.swapMu.RUnlock()
|
||||
if !has {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
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"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
@@ -35,30 +36,33 @@ func (h *mockMakerHandler) GetOffers() []*types.Offer {
|
||||
return []*types.Offer{}
|
||||
}
|
||||
|
||||
func (h *mockMakerHandler) HandleInitiateMessage(msg *message.SendKeysMessage) (s SwapState, resp Message, err error) {
|
||||
func (h *mockMakerHandler) HandleInitiateMessage(
|
||||
_ peer.ID,
|
||||
msg *message.SendKeysMessage,
|
||||
) (s SwapState, resp Message, err error) {
|
||||
if (h.id != types.Hash{}) {
|
||||
return &mockSwapState{h.id}, createSendKeysMessage(h.t), nil
|
||||
}
|
||||
return &mockSwapState{}, msg, nil
|
||||
}
|
||||
|
||||
type mockTakerHandler struct {
|
||||
type mockRelayHandler struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (h *mockTakerHandler) HandleRelayClaimRequest(_ *RelayClaimRequest) (*RelayClaimResponse, error) {
|
||||
func (h *mockRelayHandler) HandleRelayClaimRequest(_ *RelayClaimRequest) (*RelayClaimResponse, error) {
|
||||
return &RelayClaimResponse{
|
||||
TxHash: mockEthTXHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type mockSwapState struct {
|
||||
id types.Hash
|
||||
offerID types.Hash
|
||||
}
|
||||
|
||||
func (s *mockSwapState) ID() types.Hash {
|
||||
if (s.id != types.Hash{}) {
|
||||
return s.id
|
||||
func (s *mockSwapState) OfferID() types.Hash {
|
||||
if (s.offerID != types.Hash{}) {
|
||||
return s.offerID
|
||||
}
|
||||
|
||||
return testID
|
||||
@@ -90,7 +94,7 @@ func basicTestConfig(t *testing.T) *Config {
|
||||
func newHost(t *testing.T, cfg *Config) *Host {
|
||||
h, err := NewHost(cfg)
|
||||
require.NoError(t, err)
|
||||
h.SetHandlers(&mockMakerHandler{t: t}, &mockTakerHandler{t: t})
|
||||
h.SetHandlers(&mockMakerHandler{t: t}, &mockRelayHandler{t: t})
|
||||
t.Cleanup(func() {
|
||||
err = h.Stop()
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -30,7 +30,7 @@ func (h *Host) Initiate(who peer.AddrInfo, sendKeysMessage common.Message, s com
|
||||
h.swapMu.Lock()
|
||||
defer h.swapMu.Unlock()
|
||||
|
||||
id := s.ID()
|
||||
id := s.OfferID()
|
||||
|
||||
if h.swaps[id] != nil {
|
||||
return errSwapAlreadyInProgress
|
||||
@@ -63,6 +63,7 @@ func (h *Host) Initiate(who peer.AddrInfo, sendKeysMessage common.Message, s com
|
||||
h.swaps[id] = &swap{
|
||||
swapState: s,
|
||||
stream: stream,
|
||||
isTaker: true,
|
||||
}
|
||||
|
||||
go h.handleProtocolStreamInner(stream, s)
|
||||
@@ -87,12 +88,9 @@ func (h *Host) handleProtocolStream(stream libp2pnetwork.Stream) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug(
|
||||
"received message from peer, peer=",
|
||||
stream.Conn().RemotePeer(),
|
||||
" type=",
|
||||
message.TypeToString(msg.Type()),
|
||||
)
|
||||
curPeer := stream.Conn().RemotePeer()
|
||||
|
||||
log.Debugf("received message from peer=%s type=%s", curPeer, message.TypeToString(msg.Type()))
|
||||
|
||||
im, ok := msg.(*SendKeysMessage)
|
||||
if !ok {
|
||||
@@ -102,7 +100,7 @@ func (h *Host) handleProtocolStream(stream libp2pnetwork.Stream) {
|
||||
}
|
||||
|
||||
var s SwapState
|
||||
s, resp, err := h.makerHandler.HandleInitiateMessage(im)
|
||||
s, resp, err := h.makerHandler.HandleInitiateMessage(curPeer, im)
|
||||
if err != nil {
|
||||
log.Warnf("failed to handle protocol message: err=%s", err)
|
||||
_ = stream.Close()
|
||||
@@ -119,9 +117,10 @@ func (h *Host) handleProtocolStream(stream libp2pnetwork.Stream) {
|
||||
}
|
||||
|
||||
h.swapMu.Lock()
|
||||
h.swaps[s.ID()] = &swap{
|
||||
h.swaps[s.OfferID()] = &swap{
|
||||
swapState: s,
|
||||
stream: stream,
|
||||
isTaker: false,
|
||||
}
|
||||
h.swapMu.Unlock()
|
||||
|
||||
@@ -139,7 +138,7 @@ func (h *Host) handleProtocolStreamInner(stream libp2pnetwork.Stream, s SwapStat
|
||||
log.Errorf("failed to exit protocol: err=%s", err)
|
||||
}
|
||||
h.swapMu.Lock()
|
||||
delete(h.swaps, s.ID())
|
||||
delete(h.swaps, s.OfferID())
|
||||
h.swapMu.Unlock()
|
||||
}()
|
||||
|
||||
|
||||
@@ -48,13 +48,13 @@ func TestHost_Initiate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
|
||||
ha.swapMu.Lock()
|
||||
ha.swapMu.RLock()
|
||||
require.NotNil(t, ha.swaps[testID])
|
||||
ha.swapMu.Unlock()
|
||||
ha.swapMu.RUnlock()
|
||||
|
||||
hb.swapMu.Lock()
|
||||
hb.swapMu.RLock()
|
||||
require.NotNil(t, hb.swaps[testID])
|
||||
hb.swapMu.Unlock()
|
||||
hb.swapMu.RUnlock()
|
||||
}
|
||||
|
||||
func TestHost_ConcurrentSwaps(t *testing.T) {
|
||||
@@ -77,13 +77,13 @@ func TestHost_ConcurrentSwaps(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
|
||||
ha.swapMu.Lock()
|
||||
ha.swapMu.RLock()
|
||||
require.NotNil(t, ha.swaps[testID])
|
||||
ha.swapMu.Unlock()
|
||||
ha.swapMu.RUnlock()
|
||||
|
||||
hb.swapMu.Lock()
|
||||
hb.swapMu.RLock()
|
||||
require.NotNil(t, hb.swaps[testID])
|
||||
hb.swapMu.Unlock()
|
||||
hb.swapMu.RUnlock()
|
||||
|
||||
hb.makerHandler.(*mockMakerHandler).id = testID2
|
||||
|
||||
@@ -91,11 +91,11 @@ func TestHost_ConcurrentSwaps(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Millisecond * 1500)
|
||||
|
||||
ha.swapMu.Lock()
|
||||
ha.swapMu.RLock()
|
||||
require.NotNil(t, ha.swaps[testID])
|
||||
ha.swapMu.Unlock()
|
||||
ha.swapMu.RUnlock()
|
||||
|
||||
hb.swapMu.Lock()
|
||||
hb.swapMu.RLock()
|
||||
require.NotNil(t, hb.swaps[testID])
|
||||
hb.swapMu.Unlock()
|
||||
hb.swapMu.RUnlock()
|
||||
}
|
||||
|
||||
@@ -8,12 +8,17 @@ import (
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
"github.com/athanorlabs/atomic-swap/common/vjson"
|
||||
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
||||
)
|
||||
|
||||
// RelayClaimRequest implements common.Message for our p2p relay claim requests
|
||||
// RelayClaimRequest implements common.Message for our p2p relay claim requests.
|
||||
type RelayClaimRequest struct {
|
||||
// OfferID is non-nil, if the request is from a maker to the taker of an
|
||||
// active swap. It is nil, if the request is being sent to a relay node,
|
||||
// because it advertised in the DHT.
|
||||
OfferID *types.Hash `json:"offerID"`
|
||||
SwapFactoryAddress ethcommon.Address `json:"swapFactoryAddress" validate:"required"`
|
||||
Swap *contracts.SwapFactorySwap `json:"swap" validate:"required"`
|
||||
Secret []byte `json:"secret" validate:"required,len=32"`
|
||||
|
||||
49
net/relay.go
49
net/relay.go
@@ -34,27 +34,52 @@ func (h *Host) DiscoverRelayers() ([]peer.ID, error) {
|
||||
func (h *Host) handleRelayStream(stream libp2pnetwork.Stream) {
|
||||
defer func() { _ = stream.Close() }()
|
||||
|
||||
// TODO: If the request is from a Maker/OfferID combo that we did a swap with, we
|
||||
// should always be willing to relay.
|
||||
if !h.isRelayer {
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := readStreamMessage(stream, maxRelayMessageSize)
|
||||
if err != nil {
|
||||
log.Debugf("error reading RelayClaimRequest: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
curPeer := stream.Conn().RemotePeer()
|
||||
|
||||
req, ok := msg.(*RelayClaimRequest)
|
||||
if !ok {
|
||||
log.Debugf("ignoring wrong message type=%s sent to relay stream", message.TypeToString(msg.Type()))
|
||||
log.Debugf("ignoring wrong message type=%s sent to relay stream from %s",
|
||||
message.TypeToString(msg.Type()), curPeer)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.takerHandler.HandleRelayClaimRequest(req)
|
||||
// Handle case where we are not a relayer, and the request didn't set the offerID
|
||||
// to indicate that it make from a running swap partner.
|
||||
|
||||
// While HandleRelayClaimRequest(...) will do lower level validation on the
|
||||
// claim request, there are 2 validations best handled here:
|
||||
// (1) If the network layer is not advertising that we are a relayer to the
|
||||
// DHT, we should not be getting claim requests targeted for open
|
||||
// relayers (i.e. requests that do not have the OfferID set).
|
||||
// (2) If the request is purportedly from a maker to a taker of a current
|
||||
// swap, then:
|
||||
// (a) The swap should exist in our swaps map
|
||||
// (b) The peerID who sent us the request much match the peerID with
|
||||
// whom we are performing the swap.
|
||||
if req.OfferID == nil && !h.isRelayer {
|
||||
return
|
||||
} else if req.OfferID != nil {
|
||||
h.swapMu.RLock()
|
||||
swap, ok := h.swaps[*req.OfferID]
|
||||
h.swapMu.RUnlock()
|
||||
|
||||
found := ok && swap.isTaker
|
||||
if !found || curPeer != swap.stream.Conn().RemotePeer() {
|
||||
log.Debugf("received invalid taker-specific claim request from peer=%s offerID=%s swap-found=%t",
|
||||
curPeer, req.OfferID, found)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := h.relayHandler.HandleRelayClaimRequest(req)
|
||||
if err != nil {
|
||||
log.Debugf("Did not handle relay request: %s", err)
|
||||
log.Debugf("did not handle relay request: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,6 +93,12 @@ func (h *Host) handleRelayStream(stream libp2pnetwork.Stream) {
|
||||
|
||||
// SubmitClaimToRelayer sends a request to relay a swap claim to a peer.
|
||||
func (h *Host) SubmitClaimToRelayer(relayerID peer.ID, request *RelayClaimRequest) (*RelayClaimResponse, error) {
|
||||
// The timeout should be short enough, that the Maker can try multiple relayers
|
||||
// before T1 expires even if the receiving node accepts the relay request and
|
||||
// just sits on it without doing anything.
|
||||
// TODO: https://github.com/AthanorLabs/atomic-swap/issues/375
|
||||
// The context below needs extension to cover the response. Right now
|
||||
// only covers the Connect(...).
|
||||
ctx, cancel := context.WithTimeout(h.ctx, relayClaimTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
func twoHostRelayerSetup(t *testing.T) (*Host, *Host) {
|
||||
// ha is not a relayer
|
||||
haCfg := basicTestConfig(t)
|
||||
haCfg.IsRelayer = true
|
||||
haCfg.IsRelayer = false
|
||||
ha := newHost(t, haCfg)
|
||||
err := ha.Start()
|
||||
require.NoError(t, err)
|
||||
@@ -44,13 +45,14 @@ func TestHost_DiscoverRelayers(t *testing.T) {
|
||||
|
||||
peerIDs, err := ha.DiscoverRelayers()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, peerIDs, 1)
|
||||
require.True(t, hb.isRelayer)
|
||||
require.Len(t, peerIDs, 1) // discovers hb
|
||||
require.Equal(t, hb.PeerID(), peerIDs[0])
|
||||
|
||||
peerIDs, err = hb.DiscoverRelayers()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, peerIDs, 1)
|
||||
require.Equal(t, ha.PeerID(), peerIDs[0])
|
||||
require.False(t, ha.isRelayer)
|
||||
require.Len(t, peerIDs, 0) // ha is not a relayer and not discovered
|
||||
}
|
||||
|
||||
func createTestClaimRequest() *message.RelayClaimRequest {
|
||||
@@ -77,12 +79,43 @@ func createTestClaimRequest() *message.RelayClaimRequest {
|
||||
return req
|
||||
}
|
||||
|
||||
func TestHost_SubmitClaimToRelayer(t *testing.T) {
|
||||
func TestHost_SubmitClaimToRelayer_dhtRelayer(t *testing.T) {
|
||||
ha, hb := twoHostRelayerSetup(t)
|
||||
|
||||
// success path ha->hb, hb is a DHT relayer
|
||||
resp, err := ha.SubmitClaimToRelayer(hb.PeerID(), createTestClaimRequest())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockEthTXHash.Hex(), resp.TxHash.Hex())
|
||||
|
||||
// failure path hb->ha, ha is NOT a DHT relayer. Note that the remote end
|
||||
// does not pass back the exact reason for rejecting a claim to avoid
|
||||
// possible privacy data leaks, but in this case it is because hb is not
|
||||
// a DHT advertising relayer.
|
||||
_, err = hb.SubmitClaimToRelayer(ha.PeerID(), createTestClaimRequest())
|
||||
require.ErrorContains(t, err, "failed to read RelayClaimResponse: EOF")
|
||||
}
|
||||
|
||||
func TestHost_SubmitClaimToRelayer_xmrTakerRelayer(t *testing.T) {
|
||||
ha, hb := twoHostRelayerSetup(t)
|
||||
|
||||
request := createTestClaimRequest()
|
||||
offerID := types.Hash{0x1}
|
||||
request.OfferID = &offerID
|
||||
|
||||
// fail, because there is no ongoing swap between ha and hb
|
||||
_, err := hb.SubmitClaimToRelayer(ha.PeerID(), request)
|
||||
require.ErrorContains(t, err, "failed to read RelayClaimResponse: EOF")
|
||||
|
||||
// create an ongoing swap between ha and hb
|
||||
swapState := &mockSwapState{offerID: offerID}
|
||||
err = ha.Initiate(peer.AddrInfo{ID: hb.PeerID()}, createSendKeysMessage(t), swapState)
|
||||
require.NoError(t, err)
|
||||
defer ha.CloseProtocolStream(offerID)
|
||||
|
||||
// same steps will succeed now, because we started a swap first
|
||||
response, err := hb.SubmitClaimToRelayer(ha.PeerID(), request)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockEthTXHash, response.TxHash)
|
||||
}
|
||||
|
||||
func TestHost_SubmitClaimToRelayer_fail(t *testing.T) {
|
||||
|
||||
12
net/types.go
12
net/types.go
@@ -4,6 +4,8 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
"github.com/athanorlabs/atomic-swap/net/message"
|
||||
@@ -27,16 +29,18 @@ type (
|
||||
// implemented by *xmrmaker.Instance.
|
||||
type MakerHandler interface {
|
||||
GetOffers() []*types.Offer
|
||||
HandleInitiateMessage(msg *SendKeysMessage) (SwapState, Message, error)
|
||||
HandleInitiateMessage(peerID peer.ID, msg *SendKeysMessage) (SwapState, Message, error)
|
||||
}
|
||||
|
||||
// TakerHandler handles relay claim requests. It is implemented by
|
||||
// *xmrtaker.xmrtaker.
|
||||
type TakerHandler interface {
|
||||
// RelayHandler handles relay claim requests. It is implemented by
|
||||
// *backend.backend.
|
||||
type RelayHandler interface {
|
||||
HandleRelayClaimRequest(msg *RelayClaimRequest) (*RelayClaimResponse, error)
|
||||
}
|
||||
|
||||
type swap struct {
|
||||
swapState SwapState
|
||||
stream libp2pnetwork.Stream
|
||||
// isTaker is true if we initiated the swap (created the outbound stream)
|
||||
isTaker bool
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/net/message"
|
||||
"github.com/athanorlabs/atomic-swap/protocol/swap"
|
||||
"github.com/athanorlabs/atomic-swap/protocol/txsender"
|
||||
"github.com/athanorlabs/atomic-swap/relayer"
|
||||
)
|
||||
|
||||
// NetSender consists of Host methods invoked by the Maker/Taker
|
||||
@@ -62,6 +65,7 @@ type Backend interface {
|
||||
|
||||
// helpers
|
||||
NewSwapFactory(addr ethcommon.Address) (*contracts.SwapFactory, error)
|
||||
HandleRelayClaimRequest(request *message.RelayClaimRequest) (*message.RelayClaimResponse, error)
|
||||
|
||||
// getters
|
||||
Ctx() context.Context
|
||||
@@ -231,3 +235,29 @@ func (b *backend) ClearXMRDepositAddress(offerID types.Hash) {
|
||||
defer b.perSwapXMRDepositAddrRWMu.Unlock()
|
||||
delete(b.perSwapXMRDepositAddr, offerID)
|
||||
}
|
||||
|
||||
// HandleRelayClaimRequest validates and sends the transaction for a relay claim request
|
||||
func (b *backend) HandleRelayClaimRequest(request *message.RelayClaimRequest) (*message.RelayClaimResponse, error) {
|
||||
// In the taker relay scenario, the net layer has already validated that we
|
||||
// have an ongoing swap with the requesting peer that uses the passed
|
||||
// offerID, but we have not verified that the claim in the swap matches the
|
||||
// offerID. The backend, with its access to the recovery DB, is in the best
|
||||
// position to perform this check. The remaining validations will be in the
|
||||
// relayer library.
|
||||
if request.OfferID != nil {
|
||||
swapInfo, err := b.recoveryDB.GetContractSwapInfo(*request.OfferID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("swap info for taker claim request not found: %w", err)
|
||||
}
|
||||
if swapInfo.SwapID != request.Swap.SwapID() {
|
||||
return nil, errors.New("counterparty claim request has invalid swap ID")
|
||||
}
|
||||
}
|
||||
|
||||
return relayer.ValidateAndSendTransaction(
|
||||
b.Ctx(),
|
||||
request,
|
||||
b.ETHClient(),
|
||||
b.ContractAddr(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func NewManager(db Database) (Manager, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
ongoing[s.ID] = s
|
||||
ongoing[s.OfferID] = s
|
||||
}
|
||||
|
||||
return &manager{
|
||||
@@ -74,9 +74,9 @@ func (m *manager) AddSwap(info *Info) error {
|
||||
|
||||
switch info.Status.IsOngoing() {
|
||||
case true:
|
||||
m.ongoing[info.ID] = info
|
||||
m.ongoing[info.OfferID] = info
|
||||
default:
|
||||
m.past[info.ID] = info
|
||||
m.past[info.OfferID] = info
|
||||
}
|
||||
|
||||
return m.db.PutSwap(info)
|
||||
@@ -107,7 +107,7 @@ func (m *manager) GetPastIDs() ([]types.Hash, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
ids[s.ID] = struct{}{}
|
||||
ids[s.OfferID] = struct{}{}
|
||||
}
|
||||
|
||||
idArr := make([]types.Hash, len(ids))
|
||||
@@ -135,7 +135,7 @@ func (m *manager) GetPastSwap(id types.Hash) (*Info, error) {
|
||||
}
|
||||
|
||||
// cache the swap, since it's recently accessed
|
||||
m.past[s.ID] = s
|
||||
m.past[s.OfferID] = s
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ func (m *manager) GetOngoingSwaps() ([]*Info, error) {
|
||||
func (m *manager) CompleteOngoingSwap(info *Info) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
_, has := m.ongoing[info.ID]
|
||||
_, has := m.ongoing[info.OfferID]
|
||||
if !has {
|
||||
return errNoSwapWithID
|
||||
}
|
||||
@@ -178,8 +178,8 @@ func (m *manager) CompleteOngoingSwap(info *Info) error {
|
||||
now := time.Now()
|
||||
info.EndTime = &now
|
||||
|
||||
m.past[info.ID] = info
|
||||
delete(m.ongoing, info.ID)
|
||||
m.past[info.OfferID] = info
|
||||
delete(m.ongoing, info.OfferID)
|
||||
|
||||
// re-write to db, as status has changed
|
||||
return m.db.PutSwap(info)
|
||||
|
||||
@@ -29,6 +29,7 @@ func TestNewManager(t *testing.T) {
|
||||
|
||||
hashA := types.Hash{0x1}
|
||||
infoA := NewInfo(
|
||||
testPeerID,
|
||||
hashA,
|
||||
coins.ProvidesXMR,
|
||||
apd.New(1, 0),
|
||||
@@ -44,6 +45,7 @@ func TestNewManager(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
infoB := NewInfo(
|
||||
testPeerID,
|
||||
types.Hash{2},
|
||||
coins.ProvidesXMR,
|
||||
apd.New(1, 0),
|
||||
@@ -77,6 +79,7 @@ func TestManager_AddSwap_Ongoing(t *testing.T) {
|
||||
m := mgr.(*manager)
|
||||
require.NoError(t, err)
|
||||
info := NewInfo(
|
||||
testPeerID,
|
||||
types.Hash{},
|
||||
coins.ProvidesXMR,
|
||||
apd.New(1, 0),
|
||||
@@ -125,34 +128,34 @@ func TestManager_AddSwap_Past(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
info := &Info{
|
||||
ID: types.Hash{1},
|
||||
Status: types.CompletedSuccess,
|
||||
OfferID: types.Hash{1},
|
||||
Status: types.CompletedSuccess,
|
||||
}
|
||||
|
||||
db.EXPECT().PutSwap(info)
|
||||
err = m.AddSwap(info)
|
||||
require.NoError(t, err)
|
||||
|
||||
s, err := m.GetPastSwap(info.ID)
|
||||
s, err := m.GetPastSwap(info.OfferID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, s)
|
||||
|
||||
info = &Info{
|
||||
ID: types.Hash{2},
|
||||
Status: types.CompletedSuccess,
|
||||
OfferID: types.Hash{2},
|
||||
Status: types.CompletedSuccess,
|
||||
}
|
||||
|
||||
db.EXPECT().PutSwap(info)
|
||||
err = m.AddSwap(info)
|
||||
require.NoError(t, err)
|
||||
|
||||
s, err = m.GetPastSwap(info.ID)
|
||||
s, err = m.GetPastSwap(info.OfferID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, s)
|
||||
|
||||
info = &Info{
|
||||
ID: types.Hash{3},
|
||||
Status: types.ExpectingKeys,
|
||||
OfferID: types.Hash{3},
|
||||
Status: types.ExpectingKeys,
|
||||
}
|
||||
|
||||
db.EXPECT().PutSwap(info)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
@@ -31,7 +32,8 @@ type (
|
||||
// Info contains the details of the swap as well as its status.
|
||||
type Info struct {
|
||||
Version *semver.Version `json:"version"`
|
||||
ID types.Hash `json:"offerID" validate:"required"` // swap offer ID
|
||||
PeerID peer.ID `json:"peerID" validate:"required"`
|
||||
OfferID types.Hash `json:"offerID" validate:"required"`
|
||||
Provides coins.ProvidesCoin `json:"provides" validate:"required"`
|
||||
ProvidedAmount *apd.Decimal `json:"providedAmount" validate:"required"`
|
||||
ExpectedAmount *apd.Decimal `json:"expectedAmount" validate:"required"`
|
||||
@@ -66,7 +68,8 @@ type Info struct {
|
||||
// NewInfo creates a new *Info from the given parameters.
|
||||
// Note that the swap ID is the same as the offer ID.
|
||||
func NewInfo(
|
||||
id types.Hash,
|
||||
peerID peer.ID,
|
||||
offerID types.Hash,
|
||||
provides coins.ProvidesCoin,
|
||||
providedAmount, expectedAmount *apd.Decimal,
|
||||
exchangeRate *coins.ExchangeRate,
|
||||
@@ -77,7 +80,8 @@ func NewInfo(
|
||||
) *Info {
|
||||
info := &Info{
|
||||
Version: CurInfoVersion,
|
||||
ID: id,
|
||||
PeerID: peerID,
|
||||
OfferID: offerID,
|
||||
Provides: provides,
|
||||
ProvidedAmount: providedAmount,
|
||||
ExpectedAmount: expectedAmount,
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
@@ -16,10 +17,13 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/common/vjson"
|
||||
)
|
||||
|
||||
var testPeerID, _ = peer.Decode("12D3KooWQQRJuKTZ35eiHGNPGDpQqjpJSdaxEMJRxi6NWFrrvQVi")
|
||||
|
||||
func Test_InfoMarshal(t *testing.T) {
|
||||
offerIDStr := "0x0102030405060708091011121314151617181920212223242526272829303132"
|
||||
offerID := ethcommon.HexToHash(offerIDStr)
|
||||
info := NewInfo(
|
||||
testPeerID,
|
||||
offerID,
|
||||
coins.ProvidesXMR,
|
||||
apd.New(125, -2), // 1.25
|
||||
@@ -39,6 +43,7 @@ func Test_InfoMarshal(t *testing.T) {
|
||||
|
||||
expectedJSON := `{
|
||||
"version": "0.2.0",
|
||||
"peerID": "12D3KooWQQRJuKTZ35eiHGNPGDpQqjpJSdaxEMJRxi6NWFrrvQVi",
|
||||
"offerID": "0x0102030405060708091011121314151617181920212223242526272829303132",
|
||||
"provides": "XMR",
|
||||
"providedAmount": "1.25",
|
||||
|
||||
@@ -98,10 +98,10 @@ func (s *ExternalSender) IncomingCh(id types.Hash) chan<- ethcommon.Hash {
|
||||
func (s *ExternalSender) Approve(
|
||||
spender ethcommon.Address,
|
||||
amount *big.Int,
|
||||
) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
) (*ethtypes.Receipt, error) {
|
||||
input, err := s.abi.Pack("approve", spender, amount)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.sendAndReceive(input, s.erc20Addr)
|
||||
@@ -116,11 +116,11 @@ func (s *ExternalSender) NewSwap(
|
||||
nonce *big.Int,
|
||||
ethAsset types.EthAsset,
|
||||
value *big.Int,
|
||||
) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
) (*ethtypes.Receipt, error) {
|
||||
input, err := s.abi.Pack("new_swap", pubKeyClaim, pubKeyRefund, claimer, timeoutDuration,
|
||||
ethAsset, value, nonce)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valueWei := coins.NewWeiAmount(value)
|
||||
@@ -137,23 +137,23 @@ func (s *ExternalSender) NewSwap(
|
||||
var txHash ethcommon.Hash
|
||||
select {
|
||||
case <-time.After(transactionTimeout):
|
||||
return ethcommon.Hash{}, nil, errTransactionTimeout
|
||||
return nil, errTransactionTimeout
|
||||
case txHash = <-s.in:
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ec, txHash)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txHash, receipt, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
// SetReady prompts the external sender to sign a set_ready transaction
|
||||
func (s *ExternalSender) SetReady(swap *contracts.SwapFactorySwap) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
func (s *ExternalSender) SetReady(swap *contracts.SwapFactorySwap) (*ethtypes.Receipt, error) {
|
||||
input, err := s.abi.Pack("set_ready", swap)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.sendAndReceive(input, s.contractAddr)
|
||||
@@ -163,10 +163,10 @@ func (s *ExternalSender) SetReady(swap *contracts.SwapFactorySwap) (ethcommon.Ha
|
||||
func (s *ExternalSender) Claim(
|
||||
swap *contracts.SwapFactorySwap,
|
||||
secret [32]byte,
|
||||
) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
) (*ethtypes.Receipt, error) {
|
||||
input, err := s.abi.Pack("claim", swap, secret)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.sendAndReceive(input, s.contractAddr)
|
||||
@@ -176,16 +176,16 @@ func (s *ExternalSender) Claim(
|
||||
func (s *ExternalSender) Refund(
|
||||
swap *contracts.SwapFactorySwap,
|
||||
secret [32]byte,
|
||||
) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
) (*ethtypes.Receipt, error) {
|
||||
input, err := s.abi.Pack("refund", swap, secret)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.sendAndReceive(input, s.contractAddr)
|
||||
}
|
||||
|
||||
func (s *ExternalSender) sendAndReceive(input []byte, to ethcommon.Address) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
func (s *ExternalSender) sendAndReceive(input []byte, to ethcommon.Address) (*ethtypes.Receipt, error) {
|
||||
tx := &Transaction{To: to, Data: input}
|
||||
|
||||
s.Lock()
|
||||
@@ -195,14 +195,14 @@ func (s *ExternalSender) sendAndReceive(input []byte, to ethcommon.Address) (eth
|
||||
var txHash ethcommon.Hash
|
||||
select {
|
||||
case <-time.After(transactionTimeout):
|
||||
return ethcommon.Hash{}, nil, errTransactionTimeout
|
||||
return nil, errTransactionTimeout
|
||||
case txHash = <-s.in:
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ec, txHash)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txHash, receipt, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
type Sender interface {
|
||||
SetContract(*contracts.SwapFactory)
|
||||
SetContractAddress(ethcommon.Address)
|
||||
Approve(spender ethcommon.Address, amount *big.Int) (ethcommon.Hash, *ethtypes.Receipt, error) // for ERC20 swaps
|
||||
Approve(spender ethcommon.Address, amount *big.Int) (*ethtypes.Receipt, error) // for ERC20 swaps
|
||||
NewSwap(
|
||||
pubKeyClaim [32]byte,
|
||||
pubKeyRefund [32]byte,
|
||||
@@ -35,10 +35,10 @@ type Sender interface {
|
||||
nonce *big.Int,
|
||||
ethAsset types.EthAsset,
|
||||
amount *big.Int,
|
||||
) (ethcommon.Hash, *ethtypes.Receipt, error)
|
||||
SetReady(swap *contracts.SwapFactorySwap) (ethcommon.Hash, *ethtypes.Receipt, error)
|
||||
Claim(swap *contracts.SwapFactorySwap, secret [32]byte) (ethcommon.Hash, *ethtypes.Receipt, error)
|
||||
Refund(swap *contracts.SwapFactorySwap, secret [32]byte) (ethcommon.Hash, *ethtypes.Receipt, error)
|
||||
) (*ethtypes.Receipt, error)
|
||||
SetReady(swap *contracts.SwapFactorySwap) (*ethtypes.Receipt, error)
|
||||
Claim(swap *contracts.SwapFactorySwap, secret [32]byte) (*ethtypes.Receipt, error)
|
||||
Refund(swap *contracts.SwapFactorySwap, secret [32]byte) (*ethtypes.Receipt, error)
|
||||
}
|
||||
|
||||
type privateKeySender struct {
|
||||
@@ -72,27 +72,27 @@ func (s *privateKeySender) SetContractAddress(_ ethcommon.Address) {}
|
||||
func (s *privateKeySender) Approve(
|
||||
spender ethcommon.Address,
|
||||
amount *big.Int,
|
||||
) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
) (*ethtypes.Receipt, error) {
|
||||
s.ethClient.Lock()
|
||||
defer s.ethClient.Unlock()
|
||||
txOpts, err := s.ethClient.TxOpts(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
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 ethcommon.Hash{}, nil, 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 ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx.Hash(), receipt, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
func (s *privateKeySender) NewSwap(
|
||||
@@ -103,12 +103,12 @@ func (s *privateKeySender) NewSwap(
|
||||
nonce *big.Int,
|
||||
ethAsset types.EthAsset,
|
||||
value *big.Int,
|
||||
) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
) (*ethtypes.Receipt, error) {
|
||||
s.ethClient.Lock()
|
||||
defer s.ethClient.Unlock()
|
||||
txOpts, err := s.ethClient.TxOpts(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// transfer ETH if we're not doing an ERC20 swap
|
||||
@@ -120,89 +120,89 @@ func (s *privateKeySender) NewSwap(
|
||||
ethcommon.Address(ethAsset), value, nonce)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("new_swap tx creation failed, %w", err)
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("new_swap failed, %w", err)
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx.Hash(), receipt, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
func (s *privateKeySender) SetReady(swap *contracts.SwapFactorySwap) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
func (s *privateKeySender) SetReady(swap *contracts.SwapFactorySwap) (*ethtypes.Receipt, error) {
|
||||
s.ethClient.Lock()
|
||||
defer s.ethClient.Unlock()
|
||||
txOpts, err := s.ethClient.TxOpts(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx, err := s.swapContract.SetReady(txOpts, *swap)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("set_ready tx creation failed, %w", err)
|
||||
return ethcommon.Hash{}, nil, 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 ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx.Hash(), receipt, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
func (s *privateKeySender) Claim(
|
||||
swap *contracts.SwapFactorySwap,
|
||||
secret [32]byte,
|
||||
) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
) (*ethtypes.Receipt, error) {
|
||||
s.ethClient.Lock()
|
||||
defer s.ethClient.Unlock()
|
||||
txOpts, err := s.ethClient.TxOpts(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx, err := s.swapContract.Claim(txOpts, *swap, secret)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("claim tx creation failed, %w", err)
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("claim failed, %w", err)
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx.Hash(), receipt, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
func (s *privateKeySender) Refund(
|
||||
swap *contracts.SwapFactorySwap,
|
||||
secret [32]byte,
|
||||
) (ethcommon.Hash, *ethtypes.Receipt, error) {
|
||||
) (*ethtypes.Receipt, error) {
|
||||
s.ethClient.Lock()
|
||||
defer s.ethClient.Unlock()
|
||||
txOpts, err := s.ethClient.TxOpts(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx, err := s.swapContract.Refund(txOpts, *swap, secret)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("refund tx creation failed, %w", err)
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("refund failed, %w", err)
|
||||
return ethcommon.Hash{}, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx.Hash(), receipt, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
@@ -9,88 +9,14 @@ import (
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"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/net/message"
|
||||
pcommon "github.com/athanorlabs/atomic-swap/protocol"
|
||||
)
|
||||
|
||||
// checkContractSwapID checks that the `Swap` type sent matches the swap ID when hashed
|
||||
func checkContractSwapID(msg *message.NotifyETHLocked) error {
|
||||
uint256Ty, err := abi.NewType("uint256", "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create uint256 type: %w", err)
|
||||
}
|
||||
|
||||
bytes32Ty, err := abi.NewType("bytes32", "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create bytes32 type: %w", err)
|
||||
}
|
||||
|
||||
addressTy, err := abi.NewType("address", "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create address type: %w", err)
|
||||
}
|
||||
|
||||
arguments := abi.Arguments{
|
||||
{
|
||||
Type: addressTy,
|
||||
},
|
||||
{
|
||||
Type: addressTy,
|
||||
},
|
||||
{
|
||||
Type: bytes32Ty,
|
||||
},
|
||||
{
|
||||
Type: bytes32Ty,
|
||||
},
|
||||
{
|
||||
Type: uint256Ty,
|
||||
},
|
||||
{
|
||||
Type: uint256Ty,
|
||||
},
|
||||
{
|
||||
Type: addressTy,
|
||||
},
|
||||
{
|
||||
Type: uint256Ty,
|
||||
},
|
||||
{
|
||||
Type: uint256Ty,
|
||||
},
|
||||
}
|
||||
|
||||
args, err := arguments.Pack(
|
||||
msg.ContractSwap.Owner,
|
||||
msg.ContractSwap.Claimer,
|
||||
msg.ContractSwap.PubKeyClaim,
|
||||
msg.ContractSwap.PubKeyRefund,
|
||||
msg.ContractSwap.Timeout0,
|
||||
msg.ContractSwap.Timeout1,
|
||||
msg.ContractSwap.Asset,
|
||||
msg.ContractSwap.Value,
|
||||
msg.ContractSwap.Nonce,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pack arguments: %w", err)
|
||||
}
|
||||
|
||||
hash := crypto.Keccak256Hash(args)
|
||||
if !bytes.Equal(hash[:], msg.ContractSwapID[:]) {
|
||||
log.Debugf("swap hash mismatch, expected args=%v\n", args)
|
||||
return errSwapIDMismatch
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkContract checks the contract's balance and Claim/Refund keys.
|
||||
// if the balance doesn't match what we're expecting to receive, or the public keys in the contract
|
||||
// aren't what we expect, we error and abort the swap.
|
||||
|
||||
@@ -20,11 +20,12 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
"github.com/athanorlabs/atomic-swap/ethereum/block"
|
||||
"github.com/athanorlabs/atomic-swap/net/message"
|
||||
"github.com/athanorlabs/atomic-swap/relayer"
|
||||
)
|
||||
|
||||
// claimFunds redeems XMRMaker's ETH funds by calling Claim() on the contract
|
||||
func (s *swapState) claimFunds() (ethcommon.Hash, error) {
|
||||
func (s *swapState) claimFunds() (*ethtypes.Receipt, error) {
|
||||
var (
|
||||
symbol string
|
||||
decimals uint8
|
||||
@@ -33,13 +34,13 @@ func (s *swapState) claimFunds() (ethcommon.Hash, error) {
|
||||
if types.EthAsset(s.contractSwap.Asset) != types.EthAssetETH {
|
||||
_, symbol, decimals, err = s.ETHClient().ERC20Info(s.ctx, s.contractSwap.Asset)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, fmt.Errorf("failed to get ERC20 info: %w", err)
|
||||
return nil, fmt.Errorf("failed to get ERC20 info: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
weiBalance, err := s.ETHClient().Balance(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if types.EthAsset(s.contractSwap.Asset) == types.EthAssetETH {
|
||||
@@ -47,7 +48,7 @@ func (s *swapState) claimFunds() (ethcommon.Hash, error) {
|
||||
} else {
|
||||
balance, err := s.ETHClient().ERC20Balance(s.ctx, s.contractSwap.Asset) //nolint:govet
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("balance before claim: %v %s",
|
||||
coins.NewERC20TokenAmountFromBigInt(balance, decimals).AsStandard().Text('f'),
|
||||
@@ -55,37 +56,40 @@ func (s *swapState) claimFunds() (ethcommon.Hash, error) {
|
||||
)
|
||||
}
|
||||
|
||||
var txHash ethcommon.Hash
|
||||
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 {
|
||||
// relayer fee was set or we had insufficient funds to claim without a relayer
|
||||
// TODO: Sufficient funds check above should be more specific
|
||||
txHash, err = s.discoverRelayersAndClaim()
|
||||
receipt, err = s.claimWithRelay()
|
||||
if err != nil {
|
||||
log.Warnf("failed to claim using relayers: %s", err)
|
||||
return nil, fmt.Errorf("failed to claim using relayers: %w", err)
|
||||
}
|
||||
log.Infof("claim transaction was relayed: %s", common.ReceiptInfo(receipt))
|
||||
} else {
|
||||
// claim and wait for tx to be included
|
||||
sc := s.getSecret()
|
||||
txHash, _, err = s.sender.Claim(s.contractSwap, sc)
|
||||
receipt, err = s.sender.Claim(s.contractSwap, sc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("claim transaction %s", common.ReceiptInfo(receipt))
|
||||
}
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("sent claim transaction, tx hash=%s", txHash)
|
||||
|
||||
if types.EthAsset(s.contractSwap.Asset) == types.EthAssetETH {
|
||||
balance, err := s.ETHClient().Balance(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("balance after claim: %s ETH", coins.FmtWeiAsETH(balance))
|
||||
} else {
|
||||
balance, err := s.ETHClient().ERC20Balance(s.ctx, s.contractSwap.Asset)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("balance after claim: %s %s",
|
||||
@@ -94,50 +98,64 @@ func (s *swapState) claimFunds() (ethcommon.Hash, error) {
|
||||
)
|
||||
}
|
||||
|
||||
return txHash, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
// discoverRelayersAndClaim discovers available relayers on the network,
|
||||
func (s *swapState) discoverRelayersAndClaim() (ethcommon.Hash, error) {
|
||||
// relayClaimWithXMRTaker relays the claim to the swap's XMR taker, who should
|
||||
// process the claim even if they are not relaying claims for everyone.
|
||||
func (s *swapState) relayClaimWithXMRTaker(request *message.RelayClaimRequest) (*ethtypes.Receipt, error) {
|
||||
// only requests to the XMR taker set the offerID field
|
||||
request.OfferID = &s.offer.ID
|
||||
defer func() { request.OfferID = nil }()
|
||||
|
||||
response, err := s.Backend.SubmitClaimToRelayer(s.info.PeerID, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receipt, err := waitForClaimReceipt(
|
||||
s.ctx,
|
||||
s.ETHClient().Raw(),
|
||||
response.TxHash,
|
||||
s.contractAddr,
|
||||
s.contractSwapID,
|
||||
s.getSecret(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get receipt of relayer's tx: %s", err)
|
||||
}
|
||||
|
||||
log.Infof("relayer's claim via counterparty included and validated %s", common.ReceiptInfo(receipt))
|
||||
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
// claimWithAdvertisedRelayers relays the claim to nodes that advertise
|
||||
// themselves as relayers in the DHT until the claim succeeds, all relayers have
|
||||
// been tried, or the context is cancelled.
|
||||
func (s *swapState) claimWithAdvertisedRelayers(request *message.RelayClaimRequest) (*ethtypes.Receipt, error) {
|
||||
relayers, err := s.Backend.DiscoverRelayers()
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(relayers) == 0 {
|
||||
return ethcommon.Hash{}, errors.New("no relayers found to submit claim to")
|
||||
return nil, errors.New("no relayers found to submit claim to")
|
||||
}
|
||||
log.Debugf("Found %d relayers to submit claim to", len(relayers))
|
||||
|
||||
forwarderAddress, err := s.Contract().TrustedForwarder(&bind.CallOpts{Context: s.ctx})
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
}
|
||||
|
||||
secret := s.getSecret()
|
||||
|
||||
req, err := relayer.CreateRelayClaimRequest(
|
||||
s.ctx,
|
||||
s.ETHClient().PrivateKey(),
|
||||
s.ETHClient().Raw(),
|
||||
s.contractAddr,
|
||||
forwarderAddress,
|
||||
s.contractSwap,
|
||||
&secret,
|
||||
)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
}
|
||||
|
||||
for _, relayer := range relayers {
|
||||
log.Debugf("submitting claim to relayer with peer ID %s", relayer)
|
||||
resp, err := s.Backend.SubmitClaimToRelayer(relayer, req)
|
||||
for _, relayerPeerID := range relayers {
|
||||
if relayerPeerID == s.info.PeerID {
|
||||
log.Debugf("skipping DHT-advertised relayer that is our swap counterparty")
|
||||
continue
|
||||
}
|
||||
log.Debugf("submitting claim to relayer with peer ID %s", relayerPeerID)
|
||||
resp, err := s.Backend.SubmitClaimToRelayer(relayerPeerID, request)
|
||||
if err != nil {
|
||||
log.Warnf("failed to submit tx to relayer: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = waitForClaimReceipt(
|
||||
receipt, err := waitForClaimReceipt(
|
||||
s.ctx,
|
||||
s.ETHClient().Raw(),
|
||||
resp.TxHash,
|
||||
@@ -150,10 +168,48 @@ func (s *swapState) discoverRelayersAndClaim() (ethcommon.Hash, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
return resp.TxHash, nil
|
||||
log.Infof("DHT relayer's claim included and validated %s", common.ReceiptInfo(receipt))
|
||||
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
return ethcommon.Hash{}, errors.New("failed to submit transaction to any relayer")
|
||||
return nil, errors.New("failed to relay claim with any non-counterparty relayer")
|
||||
}
|
||||
|
||||
// claimWithRelay first tries to relay sequentially with all relayers
|
||||
// advertising in the DHT that are not the XMR taker and, if that fails, falls
|
||||
// back to the XMR taker who, if using our software, will act as a relayer of
|
||||
// last resort for their own swap, even if they are not performing relay
|
||||
// operations more generally. Note that the receipt returned is for a
|
||||
// transaction created by the remote relayer, not by us.
|
||||
func (s *swapState) claimWithRelay() (*ethtypes.Receipt, error) {
|
||||
forwarderAddress, err := s.Contract().TrustedForwarder(&bind.CallOpts{Context: s.ctx})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secret := s.getSecret()
|
||||
|
||||
request, err := relayer.CreateRelayClaimRequest(
|
||||
s.ctx,
|
||||
s.ETHClient().PrivateKey(),
|
||||
s.ETHClient().Raw(),
|
||||
s.contractAddr,
|
||||
forwarderAddress,
|
||||
s.contractSwap,
|
||||
&secret,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receipt, err := s.claimWithAdvertisedRelayers(request)
|
||||
if err != nil {
|
||||
log.Warnf("failed to relay with DHT-advertised relayers: %s", err)
|
||||
log.Infof("falling back to swap counterparty as relayer")
|
||||
return s.relayClaimWithXMRTaker(request)
|
||||
}
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
func waitForClaimReceipt(
|
||||
@@ -161,8 +217,9 @@ func waitForClaimReceipt(
|
||||
ec *ethclient.Client,
|
||||
txHash ethcommon.Hash,
|
||||
contractAddr ethcommon.Address,
|
||||
contractSwapID, secret [32]byte,
|
||||
) error {
|
||||
contractSwapID [32]byte,
|
||||
secret [32]byte,
|
||||
) (*ethtypes.Receipt, error) {
|
||||
const (
|
||||
checkInterval = time.Second // time between transaction polls
|
||||
maxWait = time.Minute // max wait for the tx to be included in a block
|
||||
@@ -178,7 +235,7 @@ func waitForClaimReceipt(
|
||||
// into the node we're using
|
||||
err := common.SleepWithContext(ctx, checkInterval)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, isPending, err := ec.TransactionByHash(ctx, txHash)
|
||||
@@ -189,12 +246,12 @@ func waitForClaimReceipt(
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if time.Since(start) > maxWait {
|
||||
// the tx is taking too long, return an error so we try with another relayer
|
||||
return errRelayedTransactionTimeout
|
||||
return nil, errRelayedTransactionTimeout
|
||||
}
|
||||
|
||||
if !isPending {
|
||||
@@ -204,28 +261,26 @@ func waitForClaimReceipt(
|
||||
|
||||
receipt, err := ec.TransactionReceipt(ctx, txHash)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if receipt.Status != ethtypes.ReceiptStatusSuccessful {
|
||||
err = fmt.Errorf("relayer's claim transaction failed (gas-lost=%d tx=%s block=%d), %w",
|
||||
receipt.GasUsed, txHash, receipt.BlockNumber, block.ErrorFromBlock(ctx, ec, receipt))
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(receipt.Logs) == 0 {
|
||||
return fmt.Errorf("relayer's claim transaction had no logs (tx=%s block=%d)",
|
||||
return nil, fmt.Errorf("relayer's claim transaction had no logs (tx=%s block=%d)",
|
||||
txHash, receipt.BlockNumber)
|
||||
}
|
||||
|
||||
if err = checkClaimedLog(receipt.Logs[0], contractAddr, contractSwapID, secret); err != nil {
|
||||
return fmt.Errorf("relayer's claim had logs error (tx=%s block=%d): %w",
|
||||
return nil, fmt.Errorf("relayer's claim had logs error (tx=%s block=%d): %w",
|
||||
txHash, receipt.BlockNumber, err)
|
||||
}
|
||||
|
||||
log.Infof("relayer's claim tx=%s in block=%d validated, gas used: %d",
|
||||
receipt.TxHash, receipt.BlockNumber, receipt.GasUsed)
|
||||
return nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
func checkClaimedLog(log *ethtypes.Log, contractAddr ethcommon.Address, contractSwapID, secret [32]byte) error {
|
||||
|
||||
@@ -259,7 +259,7 @@ func (s *swapState) handleEventContractReady() error {
|
||||
s.readyWatcher.Stop()
|
||||
|
||||
// contract ready, let's claim our ether
|
||||
txHash, err := s.claimFunds()
|
||||
receipt, err := s.claimFunds()
|
||||
if err != nil {
|
||||
log.Warnf("failed to claim funds from contract, attempting to safely exit: %s", err)
|
||||
|
||||
@@ -271,7 +271,7 @@ func (s *swapState) handleEventContractReady() error {
|
||||
return fmt.Errorf("failed to claim: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("funds claimed, tx: %s", txHash)
|
||||
log.Debugf("funds claimed, tx: %s", receipt.TxHash)
|
||||
s.clearNextExpectedEvent(types.CompletedSuccess)
|
||||
return nil
|
||||
}
|
||||
@@ -284,6 +284,6 @@ func (s *swapState) handleEventETHRefunded(e *EventETHRefunded) error {
|
||||
}
|
||||
|
||||
s.clearNextExpectedEvent(types.CompletedRefund)
|
||||
s.CloseProtocolStream(s.ID())
|
||||
s.CloseProtocolStream(s.OfferID())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func (inst *Instance) checkForOngoingSwaps() error {
|
||||
}
|
||||
|
||||
if s.Status == types.KeysExchanged || s.Status == types.ExpectingKeys {
|
||||
log.Infof("found ongoing swap %s in DB, aborting since no funds were locked", s.ID)
|
||||
log.Infof("found ongoing swap %s in DB, aborting since no funds were locked", s.OfferID)
|
||||
|
||||
// for these two cases, no funds have been locked, so we can safely
|
||||
// abort the swap.
|
||||
@@ -125,32 +125,34 @@ func (inst *Instance) abortOngoingSwap(s *swap.Info) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return inst.backend.RecoveryDB().DeleteSwap(s.ID)
|
||||
return inst.backend.RecoveryDB().DeleteSwap(s.OfferID)
|
||||
}
|
||||
|
||||
func (inst *Instance) createOngoingSwap(s *swap.Info) error {
|
||||
log.Infof("found ongoing swap %s in DB, restarting swap", s.ID)
|
||||
log.Infof("found ongoing swap %s in DB, restarting swap", s.OfferID)
|
||||
|
||||
// check if we have shared secret key in db; if so, recover XMR from that
|
||||
// otherwise, create new swap state from recovery info
|
||||
skA, err := inst.backend.RecoveryDB().GetCounterpartySwapPrivateKey(s.ID)
|
||||
skA, err := inst.backend.RecoveryDB().GetCounterpartySwapPrivateKey(s.OfferID)
|
||||
if err == nil {
|
||||
return inst.completeSwap(s, skA)
|
||||
}
|
||||
|
||||
offer, _, err := inst.offerManager.GetOffer(s.ID)
|
||||
offer, _, err := inst.offerManager.GetOffer(s.OfferID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get offer for ongoing swap, id %s: %s", s.ID, err)
|
||||
return fmt.Errorf("failed to get offer for ongoing swap, offer ID %s: %s", s.OfferID, err)
|
||||
}
|
||||
|
||||
ethSwapInfo, err := inst.backend.RecoveryDB().GetContractSwapInfo(s.ID)
|
||||
ethSwapInfo, err := inst.backend.RecoveryDB().GetContractSwapInfo(s.OfferID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get contract info for ongoing swap from db with swap id %s: %w", s.ID, err)
|
||||
return fmt.Errorf("failed to get contract info for ongoing swap from db with offer ID %s: %s",
|
||||
s.OfferID, err)
|
||||
}
|
||||
|
||||
sk, err := inst.backend.RecoveryDB().GetSwapPrivateKey(s.ID)
|
||||
sk, err := inst.backend.RecoveryDB().GetSwapPrivateKey(s.OfferID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get private key for ongoing swap from db with swap id %s: %w", s.ID, err)
|
||||
return fmt.Errorf("failed to get private key for ongoing swap from db with offer ID %s: %s",
|
||||
s.OfferID, err)
|
||||
}
|
||||
|
||||
kp, err := sk.AsPrivateKeyPair()
|
||||
@@ -158,7 +160,7 @@ func (inst *Instance) createOngoingSwap(s *swap.Info) error {
|
||||
return err
|
||||
}
|
||||
|
||||
relayerInfo, err := inst.backend.RecoveryDB().GetSwapRelayerInfo(s.ID)
|
||||
relayerInfo, err := inst.backend.RecoveryDB().GetSwapRelayerInfo(s.OfferID)
|
||||
if err != nil {
|
||||
// we can ignore the error; if the key doesn't exist,
|
||||
// then no relayer was set for this swap.
|
||||
@@ -175,11 +177,11 @@ func (inst *Instance) createOngoingSwap(s *swap.Info) error {
|
||||
kp,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new swap state for ongoing swap, id %s: %w", s.ID, err)
|
||||
return fmt.Errorf("failed to create new swap state for ongoing swap, offer id %s: %w", s.OfferID, err)
|
||||
}
|
||||
|
||||
inst.swapMu.Lock()
|
||||
inst.swapStates[s.ID] = ss
|
||||
inst.swapStates[s.OfferID] = ss
|
||||
inst.swapMu.Unlock()
|
||||
|
||||
go func() {
|
||||
@@ -203,7 +205,7 @@ func (inst *Instance) createOngoingSwap(s *swap.Info) error {
|
||||
// address, not whatever address was used when the swap was started.
|
||||
func (inst *Instance) completeSwap(s *swap.Info, skA *mcrypto.PrivateSpendKey) error {
|
||||
// fetch our swap private spend key
|
||||
skB, err := inst.backend.RecoveryDB().GetSwapPrivateKey(s.ID)
|
||||
skB, err := inst.backend.RecoveryDB().GetSwapPrivateKey(s.OfferID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -216,7 +218,7 @@ func (inst *Instance) completeSwap(s *swap.Info, skA *mcrypto.PrivateSpendKey) e
|
||||
|
||||
// we save the counterparty's public keys in case they send public keys derived
|
||||
// in a non-standard way.
|
||||
_, vkA, err := inst.backend.RecoveryDB().GetCounterpartySwapKeys(s.ID)
|
||||
_, vkA, err := inst.backend.RecoveryDB().GetCounterpartySwapKeys(s.OfferID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -229,7 +231,7 @@ func (inst *Instance) completeSwap(s *swap.Info, skA *mcrypto.PrivateSpendKey) e
|
||||
err = pcommon.ClaimMonero(
|
||||
inst.backend.Ctx(),
|
||||
inst.backend.Env(),
|
||||
s.ID,
|
||||
s.OfferID,
|
||||
inst.backend.XMRClient(),
|
||||
s.MoneroStartHeight,
|
||||
kpAB,
|
||||
@@ -243,7 +245,7 @@ func (inst *Instance) completeSwap(s *swap.Info, skA *mcrypto.PrivateSpendKey) e
|
||||
s.Status = types.CompletedRefund
|
||||
err = inst.backend.SwapManager().CompleteOngoingSwap(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark swap %s as completed: %w", s.ID, err)
|
||||
return fmt.Errorf("failed to mark swap %s as completed: %w", s.OfferID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -188,7 +188,7 @@ func TestInstance_createOngoingSwap(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
s := &pswap.Info{
|
||||
ID: offer.ID,
|
||||
OfferID: offer.ID,
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: one,
|
||||
ExpectedAmount: one,
|
||||
@@ -200,9 +200,9 @@ func TestInstance_createOngoingSwap(t *testing.T) {
|
||||
sk, err := mcrypto.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
rdb.EXPECT().GetSwapRelayerInfo(s.ID).Return(nil, errors.New("some error"))
|
||||
rdb.EXPECT().GetCounterpartySwapPrivateKey(s.ID).Return(nil, errors.New("some error"))
|
||||
rdb.EXPECT().GetContractSwapInfo(s.ID).Return(&db.EthereumSwapInfo{
|
||||
rdb.EXPECT().GetSwapRelayerInfo(s.OfferID).Return(nil, errors.New("some error"))
|
||||
rdb.EXPECT().GetCounterpartySwapPrivateKey(s.OfferID).Return(nil, errors.New("some error"))
|
||||
rdb.EXPECT().GetContractSwapInfo(s.OfferID).Return(&db.EthereumSwapInfo{
|
||||
StartNumber: big.NewInt(1),
|
||||
ContractAddress: inst.backend.ContractAddr(),
|
||||
SwapID: contractSwapID,
|
||||
@@ -211,17 +211,17 @@ func TestInstance_createOngoingSwap(t *testing.T) {
|
||||
Timeout1: big.NewInt(2),
|
||||
},
|
||||
}, nil)
|
||||
rdb.EXPECT().GetSwapPrivateKey(s.ID).Return(
|
||||
rdb.EXPECT().GetSwapPrivateKey(s.OfferID).Return(
|
||||
sk.SpendKey(), nil,
|
||||
)
|
||||
offerDB.EXPECT().GetOffer(s.ID).Return(offer, nil)
|
||||
offerDB.EXPECT().GetOffer(s.OfferID).Return(offer, nil)
|
||||
|
||||
err = inst.createOngoingSwap(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
inst.swapMu.Lock()
|
||||
defer inst.swapMu.Unlock()
|
||||
close(inst.swapStates[s.ID].done)
|
||||
close(inst.swapStates[s.OfferID].done)
|
||||
}
|
||||
|
||||
func TestInstance_CompleteSwap(t *testing.T) {
|
||||
@@ -245,7 +245,7 @@ func TestInstance_CompleteSwap(t *testing.T) {
|
||||
height, err := inst.backend.XMRClient().GetHeight()
|
||||
require.NoError(t, err)
|
||||
sinfo := &pswap.Info{
|
||||
ID: id,
|
||||
OfferID: id,
|
||||
MoneroStartHeight: height,
|
||||
Status: types.XMRLocked,
|
||||
}
|
||||
|
||||
@@ -101,8 +101,8 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) error {
|
||||
log.Infof("got NotifyETHLocked; address=%s contract swap ID=%s", msg.Address, msg.ContractSwapID)
|
||||
|
||||
// validate that swap ID == keccak256(swap struct)
|
||||
if err := checkContractSwapID(msg); err != nil {
|
||||
return err
|
||||
if msg.ContractSwap.SwapID() != msg.ContractSwapID {
|
||||
return errSwapIDMismatch
|
||||
}
|
||||
|
||||
s.contractSwapID = msg.ContractSwapID
|
||||
@@ -133,11 +133,11 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) error {
|
||||
ContractAddress: contractAddr,
|
||||
}
|
||||
|
||||
if err = s.Backend.RecoveryDB().PutContractSwapInfo(s.ID(), ethInfo); err != nil {
|
||||
if err = s.Backend.RecoveryDB().PutContractSwapInfo(s.OfferID(), ethInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("stored ContractSwapInfo: id=%s", s.ID())
|
||||
log.Infof("stored ContractSwapInfo: id=%s", s.OfferID())
|
||||
|
||||
if err = s.checkContract(msg.TxHash); err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,6 +7,7 @@ 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"
|
||||
@@ -30,6 +31,7 @@ func (inst *Instance) Provides() coins.ProvidesCoin {
|
||||
}
|
||||
|
||||
func (inst *Instance) initiate(
|
||||
takerPeerID peer.ID,
|
||||
offer *types.Offer,
|
||||
offerExtra *types.OfferExtra,
|
||||
providesAmount *coins.PiconeroAmount,
|
||||
@@ -62,6 +64,7 @@ func (inst *Instance) initiate(
|
||||
|
||||
s, err := newSwapStateFromStart(
|
||||
inst.backend,
|
||||
takerPeerID,
|
||||
offer,
|
||||
offerExtra,
|
||||
inst.offerManager,
|
||||
@@ -84,7 +87,7 @@ func (inst *Instance) initiate(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info(color.New(color.Bold).Sprintf("**initiated swap with offer ID=%s**", s.info.ID))
|
||||
log.Info(color.New(color.Bold).Sprintf("**initiated swap with offer ID=%s**", s.info.OfferID))
|
||||
log.Info(color.New(color.Bold).Sprint("DO NOT EXIT THIS PROCESS OR THE SWAP MAY BE CANCELLED!"))
|
||||
log.Infof(color.New(color.Bold).Sprintf("receiving %v %s for %v XMR",
|
||||
s.info.ExpectedAmount,
|
||||
@@ -96,7 +99,10 @@ func (inst *Instance) initiate(
|
||||
}
|
||||
|
||||
// HandleInitiateMessage is called when we receive a network message from a peer that they wish to initiate a swap.
|
||||
func (inst *Instance) HandleInitiateMessage(msg *message.SendKeysMessage) (net.SwapState, common.Message, error) {
|
||||
func (inst *Instance) HandleInitiateMessage(
|
||||
takerPeerID peer.ID,
|
||||
msg *message.SendKeysMessage,
|
||||
) (net.SwapState, common.Message, error) {
|
||||
inst.swapMu.Lock()
|
||||
defer inst.swapMu.Unlock()
|
||||
|
||||
@@ -150,7 +156,7 @@ func (inst *Instance) HandleInitiateMessage(msg *message.SendKeysMessage) (net.S
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
state, err := inst.initiate(offer, offerExtra, providedPiconero, expectedAmount)
|
||||
state, err := inst.initiate(takerPeerID, offer, offerExtra, providedPiconero, expectedAmount)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestXMRMaker_HandleInitiateMessage(t *testing.T) {
|
||||
msg.ProvidedAmount, err = offer.ExchangeRate.ToETH(offer.MinAmount)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err := b.HandleInitiateMessage(msg)
|
||||
_, resp, err := b.HandleInitiateMessage("", msg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, message.SendKeysType, resp.Type())
|
||||
require.NotNil(t, b.swapStates[offer.ID])
|
||||
|
||||
@@ -13,10 +13,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
ethereum "github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/fatih/color"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
@@ -99,6 +100,7 @@ type swapState struct {
|
||||
// newSwapStateFromStart returns a new *swapState for a fresh swap.
|
||||
func newSwapStateFromStart(
|
||||
b backend.Backend,
|
||||
takerPeerID peer.ID,
|
||||
offer *types.Offer,
|
||||
offerExtra *types.OfferExtra,
|
||||
om *offers.Manager,
|
||||
@@ -134,6 +136,7 @@ func newSwapStateFromStart(
|
||||
}
|
||||
|
||||
info := pswap.NewInfo(
|
||||
takerPeerID,
|
||||
offer.ID,
|
||||
coins.ProvidesXMR,
|
||||
providesAmount.AsMonero(),
|
||||
@@ -247,20 +250,20 @@ func completeSwap(info *swap.Info, b backend.Backend, om *offers.Manager) error
|
||||
info.SetStatus(types.CompletedSuccess)
|
||||
err := b.SwapManager().CompleteOngoingSwap(info)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark swap %s as completed: %s", info.ID, err)
|
||||
return fmt.Errorf("failed to mark swap %s as completed: %s", info.OfferID, err)
|
||||
}
|
||||
|
||||
err = om.DeleteOffer(info.ID)
|
||||
err = om.DeleteOffer(info.OfferID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete offer %s from db: %s", info.ID, err)
|
||||
return fmt.Errorf("failed to delete offer %s from db: %s", info.OfferID, err)
|
||||
}
|
||||
|
||||
err = b.RecoveryDB().DeleteSwap(info.ID)
|
||||
err = b.RecoveryDB().DeleteSwap(info.OfferID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete temporary swap info %s from db: %s", info.ID, err)
|
||||
return fmt.Errorf("failed to delete temporary swap info %s from db: %s", info.OfferID, err)
|
||||
}
|
||||
|
||||
exitLog := color.New(color.Bold).Sprintf("**swap completed successfully: id=%s**", info.ID)
|
||||
exitLog := color.New(color.Bold).Sprintf("**swap completed successfully: id=%s**", info.OfferID)
|
||||
log.Info(exitLog)
|
||||
return nil
|
||||
}
|
||||
@@ -436,9 +439,9 @@ func (s *swapState) ExpectedAmount() *apd.Decimal {
|
||||
return s.info.ExpectedAmount
|
||||
}
|
||||
|
||||
// ID returns the ID of the swap
|
||||
func (s *swapState) ID() types.Hash {
|
||||
return s.info.ID
|
||||
// OfferID returns the ID of the swap
|
||||
func (s *swapState) OfferID() types.Hash {
|
||||
return s.info.OfferID
|
||||
}
|
||||
|
||||
// Exit is called by the network when the protocol stream closes, or if the swap_refund RPC endpoint is called.
|
||||
@@ -490,11 +493,11 @@ func (s *swapState) exit() error {
|
||||
var exitLog string
|
||||
switch s.info.Status {
|
||||
case types.CompletedSuccess:
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap completed successfully: id=%s**", s.ID())
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap completed successfully: id=%s**", s.OfferID())
|
||||
case types.CompletedRefund:
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap refunded successfully: id=%s**", s.ID())
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap refunded successfully: id=%s**", s.OfferID())
|
||||
case types.CompletedAbort:
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap aborted: id=%s**", s.ID())
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap aborted: id=%s**", s.OfferID())
|
||||
}
|
||||
|
||||
log.Info(exitLog)
|
||||
@@ -542,13 +545,13 @@ func (s *swapState) exit() error {
|
||||
|
||||
func (s *swapState) reclaimMonero(skA *mcrypto.PrivateSpendKey) error {
|
||||
// write counterparty swap privkey to disk in case something goes wrong
|
||||
err := s.Backend.RecoveryDB().PutCounterpartySwapPrivateKey(s.ID(), skA)
|
||||
err := s.Backend.RecoveryDB().PutCounterpartySwapPrivateKey(s.OfferID(), skA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.xmrtakerPublicSpendKey == nil || s.xmrtakerPrivateViewKey == nil {
|
||||
s.xmrtakerPublicSpendKey, s.xmrtakerPrivateViewKey, err = s.RecoveryDB().GetCounterpartySwapKeys(s.ID())
|
||||
s.xmrtakerPublicSpendKey, s.xmrtakerPrivateViewKey, err = s.RecoveryDB().GetCounterpartySwapKeys(s.OfferID())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get counterparty public keypair: %w", err)
|
||||
}
|
||||
@@ -562,7 +565,7 @@ func (s *swapState) reclaimMonero(skA *mcrypto.PrivateSpendKey) error {
|
||||
return pcommon.ClaimMonero(
|
||||
s.ctx,
|
||||
s.Env(),
|
||||
s.ID(),
|
||||
s.OfferID(),
|
||||
s.XMRClient(),
|
||||
s.moneroStartHeight,
|
||||
kpAB,
|
||||
@@ -589,7 +592,7 @@ func (s *swapState) generateAndSetKeys() error {
|
||||
s.privkeys = keysAndProof.PrivateKeyPair
|
||||
s.pubkeys = keysAndProof.PublicKeyPair
|
||||
|
||||
return s.Backend.RecoveryDB().PutSwapPrivateKey(s.ID(), s.privkeys.SpendKey())
|
||||
return s.Backend.RecoveryDB().PutSwapPrivateKey(s.OfferID(), s.privkeys.SpendKey())
|
||||
}
|
||||
|
||||
func generateKeys() (*pcommon.KeysAndProof, error) {
|
||||
@@ -616,7 +619,7 @@ func (s *swapState) setXMRTakerKeys(
|
||||
s.xmrtakerPublicSpendKey = sk
|
||||
s.xmrtakerPrivateViewKey = vk
|
||||
s.xmrtakerSecp256K1PublicKey = secp256k1Pub
|
||||
return s.RecoveryDB().PutCounterpartySwapKeys(s.ID(), sk, vk)
|
||||
return s.RecoveryDB().PutCounterpartySwapKeys(s.OfferID(), sk, vk)
|
||||
}
|
||||
|
||||
// setContract sets the contract in which XMRTaker has locked her ETH.
|
||||
|
||||
@@ -116,7 +116,7 @@ func TestSwapStateOngoing_Refund(t *testing.T) {
|
||||
|
||||
s.info.Status = types.XMRLocked
|
||||
rdb := inst.backend.RecoveryDB().(*backend.MockRecoveryDB)
|
||||
rdb.EXPECT().GetCounterpartySwapKeys(s.ID()).Return(
|
||||
rdb.EXPECT().GetCounterpartySwapKeys(s.OfferID()).Return(
|
||||
xmrtakerKeysAndProof.PublicKeyPair.SpendKey(),
|
||||
xmrtakerKeysAndProof.PrivateKeyPair.ViewKey(),
|
||||
nil,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
@@ -33,6 +34,7 @@ var (
|
||||
_ = logging.SetLogLevel("xmrmaker", "debug")
|
||||
desiredAmount = coins.EtherToWei(apd.New(33, -2)) // "0.33"
|
||||
defaultTimeoutDuration, _ = time.ParseDuration("86400s") // 1 day = 60s * 60min * 24hr
|
||||
testPeerID, _ = peer.Decode("12D3KooWQQRJuKTZ35eiHGNPGDpQqjpJSdaxEMJRxi6NWFrrvQVi")
|
||||
)
|
||||
|
||||
func newTestSwapStateAndDB(t *testing.T) (*Instance, *swapState, *offers.MockDatabase) {
|
||||
@@ -40,6 +42,7 @@ func newTestSwapStateAndDB(t *testing.T) (*Instance, *swapState, *offers.MockDat
|
||||
|
||||
swapState, err := newSwapStateFromStart(
|
||||
xmrmaker.backend,
|
||||
testPeerID,
|
||||
types.NewOffer("", new(apd.Decimal), new(apd.Decimal), new(coins.ExchangeRate), types.EthAssetETH),
|
||||
&types.OfferExtra{},
|
||||
xmrmaker.offerManager,
|
||||
@@ -154,9 +157,9 @@ func TestSwapState_ClaimFunds(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
tests.MineTransaction(t, swapState.ETHClient().Raw(), tx)
|
||||
|
||||
txHash, err := swapState.claimFunds()
|
||||
receipt, err := swapState.claimFunds()
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, "", txHash)
|
||||
require.NotNil(t, receipt)
|
||||
require.True(t, swapState.info.Status.IsOngoing())
|
||||
}
|
||||
|
||||
|
||||
@@ -85,12 +85,12 @@ func (s *swapState) claimMonero(skB *mcrypto.PrivateSpendKey) (*mcrypto.Address,
|
||||
}
|
||||
|
||||
// write counterparty swap privkey to disk in case something goes wrong
|
||||
err := s.Backend.RecoveryDB().PutCounterpartySwapPrivateKey(s.ID(), skB)
|
||||
err := s.Backend.RecoveryDB().PutCounterpartySwapPrivateKey(s.OfferID(), skB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := s.ID()
|
||||
id := s.OfferID()
|
||||
depositAddr := s.XMRDepositAddress(&id)
|
||||
if s.noTransferBack {
|
||||
depositAddr = nil
|
||||
@@ -104,7 +104,7 @@ func (s *swapState) claimMonero(skB *mcrypto.PrivateSpendKey) (*mcrypto.Address,
|
||||
err = pcommon.ClaimMonero(
|
||||
s.ctx,
|
||||
s.Env(),
|
||||
s.info.ID,
|
||||
s.info.OfferID,
|
||||
s.XMRClient(),
|
||||
s.walletScanHeight,
|
||||
kpAB,
|
||||
|
||||
@@ -314,7 +314,7 @@ func (s *swapState) handleEventKeysReceived(event *EventKeysReceived) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.SendSwapMessage(resp, s.ID())
|
||||
return s.SendSwapMessage(resp, s.OfferID())
|
||||
}
|
||||
|
||||
func (s *swapState) handleEventETHClaimed(event *EventETHClaimed) error {
|
||||
@@ -324,7 +324,7 @@ func (s *swapState) handleEventETHClaimed(event *EventETHClaimed) error {
|
||||
}
|
||||
|
||||
s.clearNextExpectedEvent(types.CompletedSuccess)
|
||||
s.CloseProtocolStream(s.ID())
|
||||
s.CloseProtocolStream(s.OfferID())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ func (s *swapState) handleEventShouldRefund(event *EventShouldRefund) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
txHash, err := s.refund()
|
||||
receipt, err := s.refund()
|
||||
if err != nil {
|
||||
// TODO: could this ever happen anymore?
|
||||
if !strings.Contains(err.Error(), revertSwapCompleted) {
|
||||
@@ -344,7 +344,7 @@ func (s *swapState) handleEventShouldRefund(event *EventShouldRefund) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("got our ETH back: tx hash=%s", txHash)
|
||||
event.txHashCh <- txHash
|
||||
log.Infof("got our ETH back: tx hash=%s", receipt.TxHash)
|
||||
event.txHashCh <- receipt.TxHash
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,12 +13,10 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
|
||||
"github.com/athanorlabs/atomic-swap/net/message"
|
||||
pcommon "github.com/athanorlabs/atomic-swap/protocol"
|
||||
"github.com/athanorlabs/atomic-swap/protocol/backend"
|
||||
"github.com/athanorlabs/atomic-swap/protocol/swap"
|
||||
"github.com/athanorlabs/atomic-swap/protocol/txsender"
|
||||
"github.com/athanorlabs/atomic-swap/relayer"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -78,10 +76,10 @@ func (inst *Instance) checkForOngoingSwaps() error {
|
||||
|
||||
if s.Status == types.KeysExchanged || s.Status == types.ExpectingKeys {
|
||||
// set status to aborted, delete info from recovery db
|
||||
log.Infof("found ongoing swap %s in DB, aborting since no funds were locked", s.ID)
|
||||
log.Infof("found ongoing swap %s in DB, aborting since no funds were locked", s.OfferID)
|
||||
err = inst.abortOngoingSwap(s)
|
||||
if err != nil {
|
||||
log.Warnf("failed to abort ongoing swap %s: %s", s.ID, err)
|
||||
log.Warnf("failed to abort ongoing swap %s: %s", s.OfferID, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -104,27 +102,28 @@ func (inst *Instance) abortOngoingSwap(s *swap.Info) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return inst.backend.RecoveryDB().DeleteSwap(s.ID)
|
||||
return inst.backend.RecoveryDB().DeleteSwap(s.OfferID)
|
||||
}
|
||||
|
||||
func (inst *Instance) createOngoingSwap(s *swap.Info) error {
|
||||
log.Infof("found ongoing swap %s in DB, restarting swap", s.ID)
|
||||
log.Infof("found ongoing swap %s in DB, restarting swap", s.OfferID)
|
||||
|
||||
// check if we have shared secret key in db; if so, claim XMR from that
|
||||
// otherwise, create new swap state from recovery info
|
||||
skB, err := inst.backend.RecoveryDB().GetCounterpartySwapPrivateKey(s.ID)
|
||||
skB, err := inst.backend.RecoveryDB().GetCounterpartySwapPrivateKey(s.OfferID)
|
||||
if err == nil {
|
||||
return inst.completeSwap(s, skB)
|
||||
}
|
||||
|
||||
ethSwapInfo, err := inst.backend.RecoveryDB().GetContractSwapInfo(s.ID)
|
||||
ethSwapInfo, err := inst.backend.RecoveryDB().GetContractSwapInfo(s.OfferID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get contract info for ongoing swap from db with swap id %s: %w", s.ID, err)
|
||||
return fmt.Errorf("failed to get contract info for ongoing swap from db with offer id %s: %w", s.OfferID, err)
|
||||
}
|
||||
|
||||
sk, err := inst.backend.RecoveryDB().GetSwapPrivateKey(s.ID)
|
||||
sk, err := inst.backend.RecoveryDB().GetSwapPrivateKey(s.OfferID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get private key for ongoing swap from db with swap id %s: %w", s.ID, err)
|
||||
return fmt.Errorf("failed to get private key for ongoing swap from db with offer id %s: %w",
|
||||
s.OfferID, err)
|
||||
}
|
||||
|
||||
kp, err := sk.AsPrivateKeyPair()
|
||||
@@ -142,16 +141,16 @@ func (inst *Instance) createOngoingSwap(s *swap.Info) error {
|
||||
kp,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new swap state for ongoing swap, id %s: %w", s.ID, err)
|
||||
return fmt.Errorf("failed to create new swap state for ongoing swap, offer id %s: %w", s.OfferID, err)
|
||||
}
|
||||
|
||||
inst.swapStates[s.ID] = ss
|
||||
inst.swapStates[s.OfferID] = ss
|
||||
|
||||
go func() {
|
||||
<-ss.done
|
||||
inst.swapMu.Lock()
|
||||
defer inst.swapMu.Unlock()
|
||||
delete(inst.swapStates, s.ID)
|
||||
delete(inst.swapStates, s.OfferID)
|
||||
}()
|
||||
|
||||
return nil
|
||||
@@ -168,7 +167,7 @@ func (inst *Instance) createOngoingSwap(s *swap.Info) error {
|
||||
// wallet address, not whatever address was used when the swap was started.
|
||||
func (inst *Instance) completeSwap(s *swap.Info, skB *mcrypto.PrivateSpendKey) error {
|
||||
// fetch our swap private spend key
|
||||
skA, err := inst.backend.RecoveryDB().GetSwapPrivateKey(s.ID)
|
||||
skA, err := inst.backend.RecoveryDB().GetSwapPrivateKey(s.OfferID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -180,7 +179,7 @@ func (inst *Instance) completeSwap(s *swap.Info, skB *mcrypto.PrivateSpendKey) e
|
||||
}
|
||||
|
||||
// fetch counterparty's private view key
|
||||
_, vkB, err := inst.backend.RecoveryDB().GetCounterpartySwapKeys(s.ID)
|
||||
_, vkB, err := inst.backend.RecoveryDB().GetCounterpartySwapKeys(s.OfferID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -193,7 +192,7 @@ func (inst *Instance) completeSwap(s *swap.Info, skB *mcrypto.PrivateSpendKey) e
|
||||
err = pcommon.ClaimMonero(
|
||||
inst.backend.Ctx(),
|
||||
inst.backend.Env(),
|
||||
s.ID,
|
||||
s.OfferID,
|
||||
inst.backend.XMRClient(),
|
||||
s.MoneroStartHeight,
|
||||
kpAB,
|
||||
@@ -207,7 +206,7 @@ func (inst *Instance) completeSwap(s *swap.Info, skB *mcrypto.PrivateSpendKey) e
|
||||
s.Status = types.CompletedSuccess
|
||||
err = inst.backend.SwapManager().CompleteOngoingSwap(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark swap %s as completed: %w", s.ID, err)
|
||||
return fmt.Errorf("failed to mark swap %s as completed: %w", s.OfferID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -238,13 +237,3 @@ func (inst *Instance) ExternalSender(offerID types.Hash) (*txsender.ExternalSend
|
||||
|
||||
return es, nil
|
||||
}
|
||||
|
||||
// HandleRelayClaimRequest validates and sends the transaction for a relay claim request
|
||||
func (inst *Instance) HandleRelayClaimRequest(request *message.RelayClaimRequest) (*message.RelayClaimResponse, error) {
|
||||
return relayer.ValidateAndSendTransaction(
|
||||
inst.backend.Ctx(),
|
||||
request,
|
||||
inst.backend.ETHClient(),
|
||||
inst.backend.ContractAddr(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestInstance_createOngoingSwap(t *testing.T) {
|
||||
offer := types.NewOffer(coins.ProvidesXMR, one, one, coins.ToExchangeRate(one), types.EthAssetETH)
|
||||
|
||||
s := &pswap.Info{
|
||||
ID: offer.ID,
|
||||
OfferID: offer.ID,
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: one,
|
||||
ExpectedAmount: one,
|
||||
@@ -63,8 +63,8 @@ func TestInstance_createOngoingSwap(t *testing.T) {
|
||||
makerKeys, err := mcrypto.GenerateKeys()
|
||||
require.NoError(t, err)
|
||||
|
||||
rdb.EXPECT().GetCounterpartySwapPrivateKey(s.ID).Return(nil, errors.New("some error"))
|
||||
rdb.EXPECT().GetContractSwapInfo(s.ID).Return(&db.EthereumSwapInfo{
|
||||
rdb.EXPECT().GetCounterpartySwapPrivateKey(s.OfferID).Return(nil, errors.New("some error"))
|
||||
rdb.EXPECT().GetContractSwapInfo(s.OfferID).Return(&db.EthereumSwapInfo{
|
||||
StartNumber: big.NewInt(1),
|
||||
ContractAddress: inst.backend.ContractAddr(),
|
||||
Swap: &contracts.SwapFactorySwap{
|
||||
@@ -72,10 +72,10 @@ func TestInstance_createOngoingSwap(t *testing.T) {
|
||||
Timeout1: big.NewInt(2),
|
||||
},
|
||||
}, nil)
|
||||
rdb.EXPECT().GetSwapPrivateKey(s.ID).Return(
|
||||
rdb.EXPECT().GetSwapPrivateKey(s.OfferID).Return(
|
||||
sk.SpendKey(), nil,
|
||||
)
|
||||
rdb.EXPECT().GetCounterpartySwapKeys(s.ID).Return(
|
||||
rdb.EXPECT().GetCounterpartySwapKeys(s.OfferID).Return(
|
||||
makerKeys.SpendKey().Public(), makerKeys.ViewKey(), nil,
|
||||
)
|
||||
|
||||
@@ -84,5 +84,5 @@ func TestInstance_createOngoingSwap(t *testing.T) {
|
||||
|
||||
inst.swapMu.Lock()
|
||||
defer inst.swapMu.Unlock()
|
||||
close(inst.swapStates[s.ID].done)
|
||||
close(inst.swapStates[s.OfferID].done)
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
mcrypto "github.com/athanorlabs/atomic-swap/crypto/monero"
|
||||
"github.com/athanorlabs/atomic-swap/monero"
|
||||
"github.com/athanorlabs/atomic-swap/net/message"
|
||||
pcommon "github.com/athanorlabs/atomic-swap/protocol"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// HandleProtocolMessage is called by the network to handle an incoming message.
|
||||
@@ -122,10 +122,9 @@ func (s *swapState) handleSendKeysMessage(msg *message.SendKeysMessage) (common.
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set xmrmaker keys: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("stored XMR maker's keys, going to lock ETH")
|
||||
|
||||
txHash, err := s.lockAsset()
|
||||
receipt, err := s.lockAsset()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to lock ethereum asset in contract: %w", err)
|
||||
}
|
||||
@@ -138,7 +137,7 @@ func (s *swapState) handleSendKeysMessage(msg *message.SendKeysMessage) (common.
|
||||
|
||||
out := &message.NotifyETHLocked{
|
||||
Address: s.ContractAddr(),
|
||||
TxHash: txHash,
|
||||
TxHash: receipt.TxHash,
|
||||
ContractSwapID: s.contractSwapID,
|
||||
ContractSwap: s.contractSwap,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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"
|
||||
@@ -30,7 +31,11 @@ func (inst *Instance) Provides() coins.ProvidesCoin {
|
||||
|
||||
// InitiateProtocol is called when an RPC call is made from the user to initiate a swap.
|
||||
// The input units are ether that we will provide.
|
||||
func (inst *Instance) InitiateProtocol(providesAmount *apd.Decimal, offer *types.Offer) (common.SwapState, error) {
|
||||
func (inst *Instance) InitiateProtocol(
|
||||
makerPeerID peer.ID,
|
||||
providesAmount *apd.Decimal,
|
||||
offer *types.Offer,
|
||||
) (common.SwapState, error) {
|
||||
expectedAmount, err := offer.ExchangeRate.ToXMR(providesAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -45,7 +50,7 @@ func (inst *Instance) InitiateProtocol(providesAmount *apd.Decimal, offer *types
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state, err := inst.initiate(providedAmount, coins.MoneroToPiconero(expectedAmount),
|
||||
state, err := inst.initiate(makerPeerID, providedAmount, coins.MoneroToPiconero(expectedAmount),
|
||||
offer.ExchangeRate, offer.EthAsset, offer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -54,8 +59,14 @@ func (inst *Instance) InitiateProtocol(providesAmount *apd.Decimal, offer *types
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (inst *Instance) initiate(providesAmount EthereumAssetAmount, expectedAmount *coins.PiconeroAmount,
|
||||
exchangeRate *coins.ExchangeRate, ethAsset types.EthAsset, offerID types.Hash) (*swapState, error) {
|
||||
func (inst *Instance) initiate(
|
||||
makerPeerID peer.ID,
|
||||
providesAmount EthereumAssetAmount,
|
||||
expectedAmount *coins.PiconeroAmount,
|
||||
exchangeRate *coins.ExchangeRate,
|
||||
ethAsset types.EthAsset,
|
||||
offerID types.Hash,
|
||||
) (*swapState, error) {
|
||||
inst.swapMu.Lock()
|
||||
defer inst.swapMu.Unlock()
|
||||
|
||||
@@ -70,7 +81,8 @@ func (inst *Instance) initiate(providesAmount EthereumAssetAmount, expectedAmoun
|
||||
|
||||
// Ensure the user's balance is strictly greater than the amount they will provide
|
||||
if ethAsset == types.EthAssetETH && balance.Cmp(providesAmount.BigInt()) <= 0 {
|
||||
log.Warnf("Account %s needs additional funds for this transaction", inst.backend.ETHClient().Address())
|
||||
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
|
||||
}
|
||||
|
||||
@@ -92,6 +104,7 @@ func (inst *Instance) initiate(providesAmount EthereumAssetAmount, expectedAmoun
|
||||
|
||||
s, err := newSwapStateFromStart(
|
||||
inst.backend,
|
||||
makerPeerID,
|
||||
offerID,
|
||||
inst.noTransferBack,
|
||||
providesAmount,
|
||||
@@ -110,7 +123,7 @@ func (inst *Instance) initiate(providesAmount EthereumAssetAmount, expectedAmoun
|
||||
delete(inst.swapStates, offerID)
|
||||
}()
|
||||
|
||||
log.Info(color.New(color.Bold).Sprintf("**initiated swap with ID=%s**", s.info.ID))
|
||||
log.Info(color.New(color.Bold).Sprintf("**initiated swap with offer ID=%s**", s.info.OfferID))
|
||||
log.Info(color.New(color.Bold).Sprint("DO NOT EXIT THIS PROCESS OR THE SWAP MAY BE CANCELLED!"))
|
||||
inst.swapStates[offerID] = s
|
||||
return s, nil
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestXMRTaker_InitiateProtocol(t *testing.T) {
|
||||
one := apd.New(1, 0)
|
||||
offer := types.NewOffer(coins.ProvidesETH, zero, zero, coins.ToExchangeRate(one), types.EthAssetETH)
|
||||
providesAmount := apd.New(333, -2) // 3.33
|
||||
s, err := a.InitiateProtocol(providesAmount, offer)
|
||||
s, err := a.InitiateProtocol(testPeerID, providesAmount, offer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, a.swapStates[offer.ID], s)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
@@ -97,6 +98,7 @@ type swapState struct {
|
||||
|
||||
func newSwapStateFromStart(
|
||||
b backend.Backend,
|
||||
makerPeerID peer.ID,
|
||||
offerID types.Hash,
|
||||
noTransferBack bool,
|
||||
providedAmount EthereumAssetAmount,
|
||||
@@ -123,6 +125,7 @@ func newSwapStateFromStart(
|
||||
}
|
||||
|
||||
info := pswap.NewInfo(
|
||||
makerPeerID,
|
||||
offerID,
|
||||
coins.ProvidesETH,
|
||||
providedAmount.AsStandard(),
|
||||
@@ -163,7 +166,7 @@ func newSwapStateFromOngoing(
|
||||
return nil, errInvalidStageForRecovery
|
||||
}
|
||||
|
||||
makerSk, makerVk, err := b.RecoveryDB().GetCounterpartySwapKeys(info.ID)
|
||||
makerSk, makerVk, err := b.RecoveryDB().GetCounterpartySwapKeys(info.OfferID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get xmrmaker swap keys from db: %w", err)
|
||||
}
|
||||
@@ -329,9 +332,9 @@ func (s *swapState) expectedPiconeroAmount() *coins.PiconeroAmount {
|
||||
return coins.MoneroToPiconero(s.info.ExpectedAmount)
|
||||
}
|
||||
|
||||
// ID returns the ID of the swap
|
||||
func (s *swapState) ID() types.Hash {
|
||||
return s.info.ID
|
||||
// OfferID returns the Offer ID of the swap
|
||||
func (s *swapState) OfferID() types.Hash {
|
||||
return s.info.OfferID
|
||||
}
|
||||
|
||||
// Exit is called by the network when the protocol stream closes, or if the swap_refund RPC endpoint is called.
|
||||
@@ -348,13 +351,13 @@ func (s *swapState) exit() error {
|
||||
defer func() {
|
||||
err := s.SwapManager().CompleteOngoingSwap(s.info)
|
||||
if err != nil {
|
||||
log.Warnf("failed to mark swap %s as completed: %s", s.info.ID, err)
|
||||
log.Warnf("failed to mark swap %s as completed: %s", s.info.OfferID, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Backend.RecoveryDB().DeleteSwap(s.ID())
|
||||
err = s.Backend.RecoveryDB().DeleteSwap(s.OfferID())
|
||||
if err != nil {
|
||||
log.Warnf("failed to delete temporary swap info %s from db: %s", s.ID(), err)
|
||||
log.Warnf("failed to delete temporary swap info %s from db: %s", s.OfferID(), err)
|
||||
}
|
||||
|
||||
// Stop all per-swap goroutines
|
||||
@@ -364,11 +367,11 @@ func (s *swapState) exit() error {
|
||||
var exitLog string
|
||||
switch s.info.Status {
|
||||
case types.CompletedSuccess:
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap completed successfully: id=%s**", s.ID())
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap completed successfully: offerID=%s**", s.OfferID())
|
||||
case types.CompletedRefund:
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap refunded successfully: id=%s**", s.ID())
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap refunded successfully: offerID=%s**", s.OfferID())
|
||||
case types.CompletedAbort:
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap aborted: id=%s**", s.ID())
|
||||
exitLog = color.New(color.Bold).Sprintf("**swap aborted: id=%s**", s.OfferID())
|
||||
}
|
||||
|
||||
log.Info(exitLog)
|
||||
@@ -388,7 +391,7 @@ func (s *swapState) exit() error {
|
||||
// for EventETHClaimed, the XMR has been locked, but the
|
||||
// ETH hasn't been claimed, but the contract has been set to ready.
|
||||
// we should also refund in this case, since we might be past t1.
|
||||
txHash, err := s.tryRefund()
|
||||
receipt, err := s.tryRefund()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), revertSwapCompleted) {
|
||||
// note: this should NOT ever error; it could if the ethclient
|
||||
@@ -404,7 +407,7 @@ func (s *swapState) exit() error {
|
||||
}
|
||||
|
||||
s.clearNextExpectedEvent(types.CompletedRefund)
|
||||
log.Infof("refunded ether: transaction hash=%s", txHash)
|
||||
log.Infof("refunded ether: txID=%s", receipt.TxHash)
|
||||
return nil
|
||||
case EventNoneType:
|
||||
// the swap completed already, do nothing
|
||||
@@ -416,17 +419,17 @@ func (s *swapState) exit() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *swapState) tryRefund() (ethcommon.Hash, error) {
|
||||
func (s *swapState) tryRefund() (*ethtypes.Receipt, error) {
|
||||
stage, err := s.Contract().Swaps(s.ETHClient().CallOpts(s.ctx), s.contractSwapID)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch stage {
|
||||
case contracts.StageInvalid:
|
||||
return ethcommon.Hash{}, fmt.Errorf("%w: contract swap ID: %s", errRefundInvalid, s.contractSwapID)
|
||||
return nil, fmt.Errorf("%w: contract swap ID: %s", errRefundInvalid, s.contractSwapID)
|
||||
case contracts.StageCompleted:
|
||||
return ethcommon.Hash{}, errRefundSwapCompleted
|
||||
return nil, errRefundSwapCompleted
|
||||
case contracts.StagePending, contracts.StageReady:
|
||||
// do nothing
|
||||
default:
|
||||
@@ -437,17 +440,17 @@ func (s *swapState) tryRefund() (ethcommon.Hash, error) {
|
||||
|
||||
ts, err := s.ETHClient().LatestBlockTimestamp(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("tryRefund isReady=%v untilT0=%vs untilT1=%vs",
|
||||
isReady, s.t0.Sub(ts).Seconds(), s.t1.Sub(ts).Seconds())
|
||||
|
||||
if ts.Before(s.t0) && !isReady {
|
||||
txHash, err := s.refund() //nolint:govet
|
||||
receipt, err := s.refund() //nolint:govet
|
||||
// TODO: Have refund() return errors that we can use errors.Is to check against
|
||||
if err == nil {
|
||||
return txHash, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
// There is a small, but non-zero chance that our transaction gets placed in a block that is after T0
|
||||
@@ -487,13 +490,13 @@ func (s *swapState) tryRefund() (ethcommon.Hash, error) {
|
||||
case *EventETHClaimed:
|
||||
// we should claim; returning this error
|
||||
// causes the calling function to claim
|
||||
return ethcommon.Hash{}, fmt.Errorf(revertSwapCompleted)
|
||||
return nil, fmt.Errorf(revertSwapCompleted)
|
||||
default:
|
||||
panic(fmt.Sprintf("got unexpected event while waiting for Claimed/T1: %s", event))
|
||||
}
|
||||
case err = <-waitCh:
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, fmt.Errorf("failed to wait for T1: %w", err)
|
||||
return nil, fmt.Errorf("failed to wait for T1: %w", err)
|
||||
}
|
||||
|
||||
return s.refund()
|
||||
@@ -522,7 +525,7 @@ func (s *swapState) generateAndSetKeys() error {
|
||||
s.privkeys = keysAndProof.PrivateKeyPair
|
||||
s.pubkeys = keysAndProof.PublicKeyPair
|
||||
|
||||
return s.Backend.RecoveryDB().PutSwapPrivateKey(s.ID(), s.privkeys.SpendKey())
|
||||
return s.Backend.RecoveryDB().PutSwapPrivateKey(s.OfferID(), s.privkeys.SpendKey())
|
||||
}
|
||||
|
||||
// getSecret secrets returns the current secret scalar used to unlock funds from the contract.
|
||||
@@ -543,7 +546,7 @@ func (s *swapState) setXMRMakerKeys(
|
||||
s.xmrmakerPublicSpendKey = sk
|
||||
s.xmrmakerPrivateViewKey = vk
|
||||
s.xmrmakerSecp256k1PublicKey = secp256k1Pub
|
||||
return s.Backend.RecoveryDB().PutCounterpartySwapKeys(s.info.ID, sk, vk)
|
||||
return s.Backend.RecoveryDB().PutCounterpartySwapKeys(s.info.OfferID, sk, vk)
|
||||
}
|
||||
|
||||
func (s *swapState) approveToken() error {
|
||||
@@ -558,7 +561,7 @@ func (s *swapState) approveToken() error {
|
||||
}
|
||||
|
||||
log.Info("approving token for use by the swap contract...")
|
||||
_, _, err = s.sender.Approve(s.ContractAddr(), balance)
|
||||
_, err = s.sender.Approve(s.ContractAddr(), balance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to approve token: %w", err)
|
||||
}
|
||||
@@ -568,20 +571,20 @@ func (s *swapState) approveToken() error {
|
||||
}
|
||||
|
||||
// lockAsset calls the Swap contract function new_swap and locks `amount` ether in it.
|
||||
func (s *swapState) lockAsset() (ethcommon.Hash, error) {
|
||||
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 ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.info.EthAsset != types.EthAssetETH {
|
||||
err = s.approveToken()
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,7 +594,7 @@ func (s *swapState) lockAsset() (ethcommon.Hash, error) {
|
||||
log.Debugf("locking %s in contract", symbol)
|
||||
|
||||
nonce := generateNonce()
|
||||
txHash, receipt, err := s.sender.NewSwap(
|
||||
receipt, err := s.sender.NewSwap(
|
||||
cmtXMRMaker,
|
||||
cmtXMRTaker,
|
||||
s.xmrmakerAddress,
|
||||
@@ -601,13 +604,14 @@ func (s *swapState) lockAsset() (ethcommon.Hash, error) {
|
||||
s.providedAmount.BigInt(),
|
||||
)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, fmt.Errorf("failed to instantiate swap on-chain: %w", err)
|
||||
return nil, fmt.Errorf("failed to instantiate swap on-chain: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("instantiated swap on-chain: amount=%s asset=%s txHash=%s", s.providedAmount, s.info.EthAsset, txHash)
|
||||
log.Infof("instantiated swap on-chain: amount=%s asset=%s %s",
|
||||
s.providedAmount, s.info.EthAsset, common.ReceiptInfo(receipt))
|
||||
|
||||
if len(receipt.Logs) == 0 {
|
||||
return ethcommon.Hash{}, errSwapInstantiationNoLogs
|
||||
return nil, errSwapInstantiationNoLogs
|
||||
}
|
||||
|
||||
for _, rLog := range receipt.Logs {
|
||||
@@ -617,7 +621,7 @@ func (s *swapState) lockAsset() (ethcommon.Hash, error) {
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, fmt.Errorf("swap ID not found in transaction receipt's logs: %w", err)
|
||||
return nil, fmt.Errorf("swap ID not found in transaction receipt's logs: %w", err)
|
||||
}
|
||||
|
||||
var t0 *big.Int
|
||||
@@ -629,7 +633,7 @@ func (s *swapState) lockAsset() (ethcommon.Hash, error) {
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, fmt.Errorf("timeouts not found in transaction receipt's logs: %w", err)
|
||||
return nil, fmt.Errorf("timeouts not found in transaction receipt's logs: %w", err)
|
||||
}
|
||||
|
||||
s.fundsLocked = true
|
||||
@@ -654,12 +658,12 @@ func (s *swapState) lockAsset() (ethcommon.Hash, error) {
|
||||
ContractAddress: s.Backend.ContractAddr(),
|
||||
}
|
||||
|
||||
if err := s.Backend.RecoveryDB().PutContractSwapInfo(s.ID(), ethInfo); err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
if err := s.Backend.RecoveryDB().PutContractSwapInfo(s.OfferID(), ethInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("locked %s in swap contract, waiting for XMR to be locked", symbol)
|
||||
return txHash, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
// ready calls the Ready() method on the Swap contract, indicating to XMRMaker he has until time t_1 to
|
||||
@@ -675,7 +679,7 @@ func (s *swapState) ready() error {
|
||||
return fmt.Errorf("cannot set contract to ready when swap stage is %s", contracts.StageToString(stage))
|
||||
}
|
||||
|
||||
txHash, receipt, err := s.sender.SetReady(s.contractSwap)
|
||||
receipt, err := s.sender.SetReady(s.contractSwap)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), revertSwapCompleted) && !s.info.Status.IsOngoing() {
|
||||
return nil
|
||||
@@ -683,24 +687,26 @@ func (s *swapState) ready() error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("contract set to ready in block %d, tx %s", receipt.BlockNumber, txHash)
|
||||
log.Infof("contract set to ready %s", common.ReceiptInfo(receipt))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// refund calls the Refund() method in the Swap contract, revealing XMRTaker's secret
|
||||
// and returns to her the ether in the contract.
|
||||
// If time t_1 passes and Claim() has not been called, XMRTaker should call Refund().
|
||||
func (s *swapState) refund() (ethcommon.Hash, error) {
|
||||
func (s *swapState) refund() (*ethtypes.Receipt, error) {
|
||||
sc := s.getSecret()
|
||||
|
||||
log.Infof("attempting to call Refund()...")
|
||||
txHash, _, err := s.sender.Refund(s.contractSwap, sc)
|
||||
receipt, err := s.sender.Refund(s.contractSwap, sc)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("refund succeeded %s", common.ReceiptInfo(receipt))
|
||||
|
||||
s.clearNextExpectedEvent(types.CompletedRefund)
|
||||
return txHash, nil
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
// generateKeys generates XMRTaker's monero spend and view keys (S_b, V_b), a secp256k1 public key,
|
||||
|
||||
@@ -29,11 +29,12 @@ func setupSwapStateUntilETHLocked(t *testing.T) (*swapState, uint64) {
|
||||
|
||||
makerKeys, err := pcommon.GenerateKeysAndProof()
|
||||
require.NoError(t, err)
|
||||
s.setXMRMakerKeys(
|
||||
err = s.setXMRMakerKeys(
|
||||
makerKeys.PublicKeyPair.SpendKey(),
|
||||
makerKeys.PrivateKeyPair.ViewKey(),
|
||||
makerKeys.Secp256k1PublicKey,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.lockAsset()
|
||||
require.NoError(t, err)
|
||||
@@ -43,7 +44,7 @@ func setupSwapStateUntilETHLocked(t *testing.T) (*swapState, uint64) {
|
||||
// shutdown swap state, re-create from ongoing
|
||||
s.cancel()
|
||||
|
||||
rdb.EXPECT().GetCounterpartySwapKeys(s.info.ID).Return(
|
||||
rdb.EXPECT().GetCounterpartySwapKeys(s.info.OfferID).Return(
|
||||
makerKeys.PublicKeyPair.SpendKey(),
|
||||
makerKeys.PrivateKeyPair.ViewKey(),
|
||||
nil,
|
||||
|
||||
@@ -33,8 +33,11 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/tests"
|
||||
)
|
||||
|
||||
var _ = logging.SetLogLevel("protocol", "debug")
|
||||
var _ = logging.SetLogLevel("xmrtaker", "debug")
|
||||
var (
|
||||
_ = logging.SetLogLevel("protocol", "debug")
|
||||
_ = logging.SetLogLevel("xmrtaker", "debug")
|
||||
testPeerID, _ = peer.Decode("12D3KooWQQRJuKTZ35eiHGNPGDpQqjpJSdaxEMJRxi6NWFrrvQVi")
|
||||
)
|
||||
|
||||
type mockNet struct {
|
||||
msgMu sync.Mutex // lock needed, as SendSwapMessage is called async from timeout handlers
|
||||
@@ -79,7 +82,10 @@ func newSwapManager(t *testing.T) pswap.Manager {
|
||||
func newBackendAndNet(t *testing.T) (backend.Backend, *mockNet) {
|
||||
pk := tests.GetTakerTestKey(t)
|
||||
ec := extethclient.CreateTestClient(t, pk)
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
})
|
||||
|
||||
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, ec.ChainID())
|
||||
require.NoError(t, err)
|
||||
@@ -127,7 +133,7 @@ func newTestSwapStateAndNet(t *testing.T) (*swapState, *mockNet) {
|
||||
providedAmt := coins.EtherToWei(coins.StrToDecimal("1"))
|
||||
expectedAmt := coins.MoneroToPiconero(coins.StrToDecimal("1"))
|
||||
exchangeRate := coins.ToExchangeRate(coins.StrToDecimal("1.0")) // 100%
|
||||
swapState, err := newSwapStateFromStart(b, types.Hash{}, true,
|
||||
swapState, err := newSwapStateFromStart(b, testPeerID, types.Hash{}, true,
|
||||
providedAmt, expectedAmt, exchangeRate, types.EthAssetETH)
|
||||
require.NoError(t, err)
|
||||
return swapState, net
|
||||
@@ -158,7 +164,7 @@ func newTestSwapStateWithERC20(t *testing.T, initialBalance *big.Int) (*swapStat
|
||||
|
||||
exchangeRate := coins.ToExchangeRate(apd.New(1, 0)) // 100%
|
||||
zeroPiconeros := coins.NewPiconeroAmount(0)
|
||||
swapState, err := newSwapStateFromStart(b, types.Hash{}, false,
|
||||
swapState, err := newSwapStateFromStart(b, testPeerID, types.Hash{}, false,
|
||||
coins.IntToWei(1), zeroPiconeros, exchangeRate, types.EthAsset(addr))
|
||||
require.NoError(t, err)
|
||||
return swapState, contract
|
||||
@@ -334,7 +340,7 @@ func TestExit_afterSendKeysMessage(t *testing.T) {
|
||||
s.nextExpectedEvent = EventKeysReceivedType
|
||||
err := s.Exit()
|
||||
require.NoError(t, err)
|
||||
info, err := s.SwapManager().GetPastSwap(s.info.ID)
|
||||
info, err := s.SwapManager().GetPastSwap(s.info.OfferID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.CompletedAbort, info.Status)
|
||||
}
|
||||
@@ -360,7 +366,7 @@ func TestExit_afterNotifyXMRLock(t *testing.T) {
|
||||
err = s.Exit()
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := s.SwapManager().GetPastSwap(s.info.ID)
|
||||
info, err := s.SwapManager().GetPastSwap(s.info.OfferID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.CompletedRefund, info.Status)
|
||||
}
|
||||
@@ -386,7 +392,7 @@ func TestExit_afterNotifyClaimed(t *testing.T) {
|
||||
err = s.Exit()
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := s.SwapManager().GetPastSwap(s.info.ID)
|
||||
info, err := s.SwapManager().GetPastSwap(s.info.OfferID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.CompletedRefund, info.Status)
|
||||
}
|
||||
@@ -413,7 +419,7 @@ func TestExit_invalidNextMessageType(t *testing.T) {
|
||||
err = s.Exit()
|
||||
require.True(t, errors.Is(err, errUnexpectedEventType))
|
||||
|
||||
info, err := s.SwapManager().GetPastSwap(s.info.ID)
|
||||
info, err := s.SwapManager().GetPastSwap(s.info.OfferID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.CompletedAbort, info.Status)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
logging "github.com/ipfs/go-log"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
||||
@@ -18,7 +19,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
relayedClaimGas = 70000
|
||||
relayedClaimGas = 70000 // worst case gas usage for the claimRelayer swapFactory call
|
||||
forwarderClaimGas = 156000 // worst case gas usage when using forwarder to claim
|
||||
)
|
||||
|
||||
// FeeWei and FeeEth are the fixed 0.009 ETH fee for using a swap relayer to claim.
|
||||
@@ -27,6 +29,8 @@ var (
|
||||
FeeEth = coins.NewWeiAmount(FeeWei).AsEther()
|
||||
)
|
||||
|
||||
var log = logging.Logger("relayer")
|
||||
|
||||
// CreateRelayClaimRequest fills and returns a RelayClaimRequest ready for
|
||||
// submission to a relayer.
|
||||
func CreateRelayClaimRequest(
|
||||
@@ -53,6 +57,7 @@ func CreateRelayClaimRequest(
|
||||
}
|
||||
|
||||
return &message.RelayClaimRequest{
|
||||
OfferID: nil, // set elsewhere if sending to counterparty
|
||||
SwapFactoryAddress: swapFactoryAddress,
|
||||
Swap: swap,
|
||||
Secret: secret[:],
|
||||
|
||||
@@ -20,17 +20,26 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/tests"
|
||||
)
|
||||
|
||||
// Speed up tests a little by giving deployContracts(...) a package-level cache.
|
||||
// These variables should not be accessed by other functions.
|
||||
var _forwarderAddress *ethcommon.Address
|
||||
var _swapFactoryAddress *ethcommon.Address
|
||||
|
||||
// deployContracts deploys and returns the swapFactory and forwarder addresses.
|
||||
func deployContracts(t *testing.T, ec *ethclient.Client, key *ecdsa.PrivateKey) (ethcommon.Address, ethcommon.Address) {
|
||||
ctx := context.Background()
|
||||
|
||||
forwarderAddr, err := contracts.DeployGSNForwarderWithKey(ctx, ec, key)
|
||||
require.NoError(t, err)
|
||||
if _forwarderAddress == nil || _swapFactoryAddress == nil {
|
||||
forwarderAddr, err := contracts.DeployGSNForwarderWithKey(ctx, ec, key)
|
||||
require.NoError(t, err)
|
||||
_forwarderAddress = &forwarderAddr
|
||||
|
||||
swapFactoryAddr, _, err := contracts.DeploySwapFactoryWithKey(ctx, ec, key, forwarderAddr)
|
||||
require.NoError(t, err)
|
||||
swapFactoryAddr, _, err := contracts.DeploySwapFactoryWithKey(ctx, ec, key, forwarderAddr)
|
||||
require.NoError(t, err)
|
||||
_swapFactoryAddress = &swapFactoryAddr
|
||||
}
|
||||
|
||||
return swapFactoryAddr, forwarderAddr
|
||||
return *_swapFactoryAddress, *_forwarderAddress
|
||||
}
|
||||
|
||||
func createTestSwap(claimer ethcommon.Address) *contracts.SwapFactorySwap {
|
||||
|
||||
@@ -5,11 +5,15 @@ package relayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/athanorlabs/go-relayer/impls/gsnforwarder"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"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"
|
||||
@@ -57,6 +61,11 @@ func ValidateAndSendTransaction(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasPrice, err := checkForMinClaimBalance(ctx, ec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Lock the wallet's nonce until we get a receipt
|
||||
ec.Lock()
|
||||
defer ec.Unlock()
|
||||
@@ -65,6 +74,7 @@ func ValidateAndSendTransaction(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
txOpts.GasPrice = gasPrice
|
||||
|
||||
tx, err := reqForwarder.Execute(
|
||||
txOpts,
|
||||
@@ -78,10 +88,34 @@ func ValidateAndSendTransaction(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = block.WaitForReceipt(ctx, ec.Raw(), tx.Hash())
|
||||
receipt, err := block.WaitForReceipt(ctx, ec.Raw(), tx.Hash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("relayed claim %s", common.ReceiptInfo(receipt))
|
||||
|
||||
return &message.RelayClaimResponse{TxHash: tx.Hash()}, nil
|
||||
}
|
||||
|
||||
// checkForMinClaimBalance verifies that we have enough gas to relay a claim and
|
||||
// returns the gas price that was used for the calculation.
|
||||
func checkForMinClaimBalance(ctx context.Context, ec extethclient.EthClient) (*big.Int, error) {
|
||||
balance, err := ec.Balance(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasPrice, err := ec.SuggestGasPrice(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txCost := new(big.Int).Mul(gasPrice, big.NewInt(forwarderClaimGas))
|
||||
if balance.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))
|
||||
}
|
||||
|
||||
return gasPrice, nil
|
||||
}
|
||||
|
||||
@@ -39,29 +39,35 @@ func validateClaimRequest(
|
||||
// 4. TODO: Validate that the swap exists and is in a claimable state?
|
||||
func validateClaimValues(
|
||||
ctx context.Context,
|
||||
req *message.RelayClaimRequest,
|
||||
request *message.RelayClaimRequest,
|
||||
ec *ethclient.Client,
|
||||
ourSwapFactoryAddr ethcommon.Address,
|
||||
) error {
|
||||
isTakerRelay := request.OfferID != nil
|
||||
|
||||
// Validate the deployed SwapFactory contract, if it is not at the same address
|
||||
// as our own. The CheckSwapFactoryContractCode method validates both the
|
||||
// SwapFactory bytecode and the Forwarder bytecode.
|
||||
if req.SwapFactoryAddress != ourSwapFactoryAddr {
|
||||
_, err := contracts.CheckSwapFactoryContractCode(ctx, ec, req.SwapFactoryAddress)
|
||||
if request.SwapFactoryAddress != ourSwapFactoryAddr {
|
||||
if isTakerRelay {
|
||||
return fmt.Errorf("taker claim swap factory mismatch found=%s expected=%s",
|
||||
request.SwapFactoryAddress, ourSwapFactoryAddr)
|
||||
}
|
||||
_, err := contracts.CheckSwapFactoryContractCode(ctx, ec, request.SwapFactoryAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
asset := types.EthAsset(req.Swap.Asset)
|
||||
asset := types.EthAsset(request.Swap.Asset)
|
||||
if asset != types.EthAssetETH {
|
||||
return fmt.Errorf("relaying for ETH Asset %s is not supported", asset)
|
||||
}
|
||||
|
||||
// The relayer fee must be strictly less than the swap value
|
||||
if FeeWei.Cmp(req.Swap.Value) >= 0 {
|
||||
if FeeWei.Cmp(request.Swap.Value) >= 0 {
|
||||
return fmt.Errorf("swap value of %s ETH is too low to support %s ETH relayer fee",
|
||||
coins.FmtWeiAsETH(req.Swap.Value), coins.FmtWeiAsETH(FeeWei))
|
||||
coins.FmtWeiAsETH(request.Swap.Value), coins.FmtWeiAsETH(FeeWei))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -72,14 +78,14 @@ func validateClaimValues(
|
||||
func validateClaimSignature(
|
||||
ctx context.Context,
|
||||
ec *ethclient.Client,
|
||||
req *message.RelayClaimRequest,
|
||||
request *message.RelayClaimRequest,
|
||||
) error {
|
||||
callOpts := &bind.CallOpts{
|
||||
Context: ctx,
|
||||
From: ethcommon.Address{0xFF}, // can be any value but zero, which will validate all signatures
|
||||
}
|
||||
|
||||
swapFactory, err := contracts.NewSwapFactory(req.SwapFactoryAddress, ec)
|
||||
swapFactory, err := contracts.NewSwapFactory(request.SwapFactoryAddress, ec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -94,17 +100,17 @@ func validateClaimSignature(
|
||||
return err
|
||||
}
|
||||
|
||||
nonce, err := forwarder.GetNonce(callOpts, req.Swap.Claimer)
|
||||
nonce, err := forwarder.GetNonce(callOpts, request.Swap.Claimer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secret := (*[32]byte)(req.Secret)
|
||||
secret := (*[32]byte)(request.Secret)
|
||||
|
||||
forwarderRequest, err := createForwarderRequest(
|
||||
nonce,
|
||||
req.SwapFactoryAddress,
|
||||
req.Swap,
|
||||
request.SwapFactoryAddress,
|
||||
request.Swap,
|
||||
secret,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -117,7 +123,7 @@ func validateClaimSignature(
|
||||
*domainSeparator,
|
||||
gsnforwarder.ForwardRequestTypehash,
|
||||
nil,
|
||||
req.Signature,
|
||||
request.Signature,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify signature: %w", err)
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestValidateRelayerFee(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ec, _ := tests.NewEthClient(t)
|
||||
key := tests.GetTakerTestKey(t)
|
||||
swapFactoryAddr, forwarderAddr := deployContracts(t, ec, key)
|
||||
swapFactoryAddr, _ := deployContracts(t, ec, key)
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
@@ -62,15 +62,13 @@ func TestValidateRelayerFee(t *testing.T) {
|
||||
Nonce: new(big.Int),
|
||||
}
|
||||
|
||||
var secret [32]byte
|
||||
|
||||
request := &message.RelayClaimRequest{
|
||||
SwapFactoryAddress: swapFactoryAddr,
|
||||
Swap: swap,
|
||||
Secret: secret[:],
|
||||
Secret: make([]byte, 32),
|
||||
}
|
||||
|
||||
err := validateClaimValues(ctx, request, ec, forwarderAddr)
|
||||
err := validateClaimValues(ctx, request, ec, swapFactoryAddr)
|
||||
if tc.expectErr != "" {
|
||||
require.ErrorContains(t, err, tc.expectErr, tc.description)
|
||||
} else {
|
||||
@@ -79,6 +77,44 @@ func TestValidateRelayerFee(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// In the taker claim scenario, we need to fail if the contract address is not
|
||||
// identical. If the claim has a different address, the swap was not created by
|
||||
// the taker who is being asked to claim.
|
||||
func Test_validateClaimValues_takerClaim_contractAddressNotEqualFail(t *testing.T) {
|
||||
offerID := types.Hash{0x1} // non-nil offer ID passed to indicate taker claim
|
||||
swapFactoryAddrInClaim := ethcommon.Address{0x1} // address in claim
|
||||
swapFactoryAddrOurs := ethcommon.Address{0x2} // passed to validateClaimValues
|
||||
|
||||
request := &message.RelayClaimRequest{
|
||||
OfferID: &offerID,
|
||||
SwapFactoryAddress: swapFactoryAddrInClaim,
|
||||
Secret: make([]byte, 32),
|
||||
Swap: new(contracts.SwapFactorySwap), // test fails before we validate this
|
||||
}
|
||||
|
||||
err := validateClaimValues(context.Background(), request, nil, swapFactoryAddrOurs)
|
||||
require.ErrorContains(t, err, "taker claim swap factory mismatch")
|
||||
}
|
||||
|
||||
// When validating a claim made to a DHT advertised relayer, the contacts can have
|
||||
// different addresses, but the claim's contract must be byte-code compatible. This
|
||||
// tests for failure when it is not byte-code compatible.
|
||||
func Test_validateClaimValues_dhtClaim_contractAddressNotEqual(t *testing.T) {
|
||||
ec, _ := tests.NewEthClient(t)
|
||||
key := tests.GetTakerTestKey(t)
|
||||
swapFactoryAddr, forwarderAddr := deployContracts(t, ec, key)
|
||||
|
||||
request := &message.RelayClaimRequest{
|
||||
OfferID: nil, // DHT relayer claim
|
||||
SwapFactoryAddress: forwarderAddr, // not a valid swap factory contract
|
||||
Secret: make([]byte, 32),
|
||||
Swap: new(contracts.SwapFactorySwap), // test fails before we validate this
|
||||
}
|
||||
|
||||
err := validateClaimValues(context.Background(), request, ec, swapFactoryAddr)
|
||||
require.ErrorContains(t, err, "contract address does not contain correct SwapFactory code")
|
||||
}
|
||||
|
||||
func Test_validateSignature(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ethKey := tests.GetMakerTestKey(t)
|
||||
@@ -120,6 +156,6 @@ func Test_validateClaimRequest(t *testing.T) {
|
||||
// test failure path by passing a non-eth asset
|
||||
asset := ethcommon.Address{0x1}
|
||||
req.Swap.Asset = asset
|
||||
err = validateClaimRequest(ctx, req, ec, forwarderAddr)
|
||||
err = validateClaimRequest(ctx, req, ec, swapFactoryAddr)
|
||||
require.ErrorContains(t, err, fmt.Sprintf("relaying for ETH Asset %s is not supported", types.EthAsset(asset)))
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ func (*mockSwapManager) GetOngoingSwap(id types.Hash) (swap.Info, error) {
|
||||
|
||||
one := apd.New(1, 0)
|
||||
return *swap.NewInfo(
|
||||
testPeerID,
|
||||
id,
|
||||
coins.ProvidesETH,
|
||||
one,
|
||||
@@ -120,7 +121,7 @@ func (*mockXMRTaker) GetOngoingSwapState(_ types.Hash) common.SwapState {
|
||||
return new(mockSwapState)
|
||||
}
|
||||
|
||||
func (*mockXMRTaker) InitiateProtocol(_ *apd.Decimal, _ *types.Offer) (common.SwapState, error) {
|
||||
func (*mockXMRTaker) InitiateProtocol(_ peer.ID, _ *apd.Decimal, _ *types.Offer) (common.SwapState, error) {
|
||||
return new(mockSwapState), nil
|
||||
}
|
||||
|
||||
@@ -180,7 +181,7 @@ func (*mockSwapState) SendKeysMessage() common.Message {
|
||||
return &message.SendKeysMessage{}
|
||||
}
|
||||
|
||||
func (*mockSwapState) ID() types.Hash {
|
||||
func (*mockSwapState) OfferID() types.Hash {
|
||||
return testSwapID
|
||||
}
|
||||
|
||||
|
||||
@@ -152,11 +152,11 @@ func (s *NetService) TakeOffer(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NetService) takeOffer(who peer.ID, offerID types.Hash, providesAmount *apd.Decimal) (
|
||||
func (s *NetService) takeOffer(makerPeerID peer.ID, offerID types.Hash, providesAmount *apd.Decimal) (
|
||||
<-chan types.Status,
|
||||
error,
|
||||
) {
|
||||
queryResp, err := s.net.Query(who)
|
||||
queryResp, err := s.net.Query(makerPeerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -172,7 +172,7 @@ func (s *NetService) takeOffer(who peer.ID, offerID types.Hash, providesAmount *
|
||||
return nil, errNoOfferWithID
|
||||
}
|
||||
|
||||
swapState, err := s.xmrtaker.InitiateProtocol(providesAmount, offer)
|
||||
swapState, err := s.xmrtaker.InitiateProtocol(makerPeerID, providesAmount, offer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initiate protocol: %w", err)
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func (s *NetService) takeOffer(who peer.ID, offerID types.Hash, providesAmount *
|
||||
skm.OfferID = offerID
|
||||
skm.ProvidedAmount = providesAmount
|
||||
|
||||
if err = s.net.Initiate(peer.AddrInfo{ID: who}, skm, swapState); err != nil {
|
||||
if err = s.net.Initiate(peer.AddrInfo{ID: makerPeerID}, skm, swapState); err != nil {
|
||||
if err = swapState.Exit(); err != nil {
|
||||
log.Warnf("Swap exit failure: %s", err)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/rpc/v2"
|
||||
logging "github.com/ipfs/go-log"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
@@ -175,7 +176,7 @@ type ProtocolBackend interface {
|
||||
// XMRTaker ...
|
||||
type XMRTaker interface {
|
||||
Protocol
|
||||
InitiateProtocol(providesAmount *apd.Decimal, offer *types.Offer) (common.SwapState, error)
|
||||
InitiateProtocol(peerID peer.ID, providesAmount *apd.Decimal, offer *types.Offer) (common.SwapState, error)
|
||||
ExternalSender(offerID types.Hash) (*txsender.ExternalSender, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ func (s *SwapService) GetPast(_ *http.Request, req *GetPastRequest, resp *GetPas
|
||||
resp.Swaps = make([]*PastSwap, len(swaps))
|
||||
for i, info := range swaps {
|
||||
resp.Swaps[i] = &PastSwap{
|
||||
ID: info.ID,
|
||||
ID: info.OfferID,
|
||||
Provided: info.Provides,
|
||||
ProvidedAmount: info.ProvidedAmount,
|
||||
ExpectedAmount: info.ExpectedAmount,
|
||||
@@ -172,7 +172,7 @@ func (s *SwapService) GetOngoing(_ *http.Request, req *GetOngoingRequest, resp *
|
||||
resp.Swaps = make([]*OngoingSwap, len(swaps))
|
||||
for i, info := range swaps {
|
||||
swap := new(OngoingSwap)
|
||||
swap.ID = info.ID
|
||||
swap.ID = info.OfferID
|
||||
swap.Provided = info.Provides
|
||||
swap.ProvidedAmount = info.ProvidedAmount
|
||||
swap.ExpectedAmount = info.ExpectedAmount
|
||||
@@ -184,7 +184,7 @@ func (s *SwapService) GetOngoing(_ *http.Request, req *GetOngoingRequest, resp *
|
||||
swap.Timeout1 = info.Timeout1
|
||||
swap.EstimatedTimeToCompletion, err = estimatedTimeToCompletion(env, info.Status, info.LastStatusUpdateTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to estimate time to completion for swap %s: %w", info.ID, err)
|
||||
return fmt.Errorf("failed to estimate time to completion for swap %s: %w", info.OfferID, err)
|
||||
}
|
||||
|
||||
resp.Swaps[i] = swap
|
||||
@@ -287,7 +287,7 @@ func (s *SwapService) Cancel(_ *http.Request, req *CancelRequest, resp *CancelRe
|
||||
|
||||
s.net.CloseProtocolStream(req.OfferID)
|
||||
|
||||
past, err := s.sm.GetPastSwap(info.ID)
|
||||
past, err := s.sm.GetPastSwap(info.OfferID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ start-ganache
|
||||
# run unit tests
|
||||
echo "running unit tests..."
|
||||
rm -f coverage.txt
|
||||
go test ./... -v -short -timeout=30m -count=1 -covermode=atomic -coverprofile=coverage.txt
|
||||
go test -coverpkg=./... -v -short -timeout=30m -count=1 -covermode=atomic -coverprofile=coverage.txt ./...
|
||||
OK=$?
|
||||
|
||||
if [[ -e coverage.txt ]]; then
|
||||
|
||||
Reference in New Issue
Block a user