// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only

package contracts

import (
	"context"
	"crypto/ecdsa"
	"crypto/rand"
	"encoding/hex"
	"math/big"
	"sync"
	"testing"
	"time"

	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	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"
	"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"
	"github.com/athanorlabs/atomic-swap/ethereum/block"
	"github.com/athanorlabs/atomic-swap/tests"
)

var (
	defaultTimeoutDuration = big.NewInt(60) // 60 seconds
	defaultSwapValue       = big.NewInt(100)
	dummySwapKey           = [32]byte{1} // dummy non-zero value for claim/refund key
)

func getAuth(t *testing.T, pk *ecdsa.PrivateKey) *bind.TransactOpts {
	txOpts, err := bind.NewKeyedTransactorWithChainID(pk, big.NewInt(common.GanacheChainID))
	require.NoError(t, err)
	return txOpts
}

func getReceipt(t *testing.T, ec *ethclient.Client, tx *ethtypes.Transaction) *ethtypes.Receipt {
	receipt, err := block.WaitForReceipt(context.Background(), ec, tx.Hash())
	require.NoError(t, err)
	return receipt
}

func approveERC20(t *testing.T,
	ec *ethclient.Client,
	pk *ecdsa.PrivateKey,
	erc20Contract *TestERC20,
	swapCreatorAddress ethcommon.Address,
	value *big.Int,
) {
	require.NotNil(t, erc20Contract)

	tx, err := erc20Contract.Approve(getAuth(t, pk), swapCreatorAddress, value)
	require.NoError(t, err)
	receipt := getReceipt(t, ec, tx)

	t.Logf("gas cost to call Approve %d (delta %d)", receipt.GasUsed, MaxTokenApproveGas-int(receipt.GasUsed))
	require.GreaterOrEqual(t, MaxTokenApproveGas, int(receipt.GasUsed), "Token Approve")
}

func testNewSwap(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20) {
	pk := tests.GetTakerTestKey(t)
	ec, _ := tests.NewEthClient(t)

	swapCreatorAddr, swapCreator := DevDeploySwapCreator(t, ec, pk)

	owner := crypto.PubkeyToAddress(pk.PublicKey)
	claimer := common.EthereumPrivateKeyToAddress(tests.GetMakerTestKey(t))

	var claimCommitment, refundCommitment [32]byte
	_, err := rand.Read(claimCommitment[:])
	require.NoError(t, err)
	_, err = rand.Read(refundCommitment[:])
	require.NoError(t, err)

	nonce, err := rand.Prime(rand.Reader, 256)
	require.NoError(t, err)

	txOpts := getAuth(t, pk)
	value := defaultSwapValue
	if asset.IsETH() {
		txOpts.Value = value
	} else {
		approveERC20(t, ec, pk, erc20Contract, swapCreatorAddr, value)
	}

	tx, err := swapCreator.NewSwap(
		txOpts,
		claimCommitment,
		refundCommitment,
		claimer,
		defaultTimeoutDuration,
		defaultTimeoutDuration,
		asset.Address(),
		value,
		nonce,
	)
	require.NoError(t, err)

	receipt := getReceipt(t, ec, tx)
	if asset.IsETH() {
		t.Logf("gas cost to call ETH NewSwap: %d (delta %d)",
			receipt.GasUsed, MaxNewSwapETHGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxNewSwapETHGas, int(receipt.GasUsed), "ETH NewSwap")
	} else {
		t.Logf("gas cost to call token NewSwap: %d (delta %d)",
			receipt.GasUsed, MaxNewSwapTokenGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxNewSwapTokenGas, int(receipt.GasUsed), "Token NewSwap")
	}

	newSwapLogIndex := 0
	if asset.IsToken() {
		newSwapLogIndex = 2
	}
	require.Equal(t, newSwapLogIndex+1, len(receipt.Logs))

	swapID, err := GetIDFromLog(receipt.Logs[newSwapLogIndex])
	require.NoError(t, err)

	t1, t2, err := GetTimeoutsFromLog(receipt.Logs[newSwapLogIndex])
	require.NoError(t, err)

	// validate that off-chain swapID calculation matches the on-chain value
	swap := SwapCreatorSwap{
		Owner:            owner,
		Claimer:          claimer,
		ClaimCommitment:  claimCommitment,
		RefundCommitment: refundCommitment,
		Timeout1:         t1,
		Timeout2:         t2,
		Asset:            asset.Address(),
		Value:            value,
		Nonce:            nonce,
	}

	// validate our off-net calculation of the SwapID
	require.Equal(t, types.Hash(swapID).Hex(), swap.SwapID().Hex())
}

func TestSwapCreator_NewSwap(t *testing.T) {
	testNewSwap(t, types.EthAssetETH, nil)
}

func TestSwapCreator_Claim_vec(t *testing.T) {
	secret, err := hex.DecodeString("D30519BCAE8D180DBFCC94FE0B8383DC310185B0BE97B4365083EBCECCD75759")
	require.NoError(t, err)
	pubX, err := hex.DecodeString("3AF1E1EFA4D1E1AD5CB9E3967E98E901DAFCD37C44CF0BFB6C216997F5EE51DF")
	require.NoError(t, err)
	pubY, err := hex.DecodeString("E4ACAC3E6F139E0C7DB2BD736824F51392BDA176965A1C59EB9C3C5FF9E85D7A")
	require.NoError(t, err)

	var s, x, y [32]byte
	copy(s[:], secret)
	copy(x[:], pubX)
	copy(y[:], pubY)

	pk := secp256k1.NewPublicKey(x, y)
	cmt := pk.Keccak256()

	// deploy swap contract with claim key hash
	pkA := tests.GetTakerTestKey(t)
	ec, _ := tests.NewEthClient(t)
	addr := crypto.PubkeyToAddress(pkA.PublicKey)

	_, swapCreator := DevDeploySwapCreator(t, ec, pkA)

	txOpts := getAuth(t, pkA)
	txOpts.Value = defaultSwapValue

	nonce := big.NewInt(0)
	tx, err := swapCreator.NewSwap(txOpts, cmt, dummySwapKey, addr, defaultTimeoutDuration,
		defaultTimeoutDuration, ethcommon.Address(types.EthAssetETH), defaultSwapValue, nonce)
	require.NoError(t, err)

	receipt := getReceipt(t, ec, tx)
	t.Logf("gas cost to call ETH NewSwap: %d (delta %d)", receipt.GasUsed, MaxNewSwapETHGas-int(receipt.GasUsed))
	require.GreaterOrEqual(t, MaxNewSwapETHGas, int(receipt.GasUsed), "ETH NewSwap")

	require.Equal(t, 1, len(receipt.Logs))
	id, err := GetIDFromLog(receipt.Logs[0])
	require.NoError(t, err)

	t1, t2, err := GetTimeoutsFromLog(receipt.Logs[0])
	require.NoError(t, err)

	swap := SwapCreatorSwap{
		Owner:            addr,
		Claimer:          addr,
		ClaimCommitment:  cmt,
		RefundCommitment: dummySwapKey,
		Timeout1:         t1,
		Timeout2:         t2,
		Asset:            ethcommon.Address(types.EthAssetETH),
		Value:            defaultSwapValue,
		Nonce:            nonce,
	}

	// set contract to Ready
	tx, err = swapCreator.SetReady(getAuth(t, pkA), swap)
	require.NoError(t, err)
	receipt = getReceipt(t, ec, tx)

	t.Logf("gas cost to call SetReady: %d (delta %d)", receipt.GasUsed, MaxSetReadyGas-int(receipt.GasUsed))
	require.GreaterOrEqual(t, MaxSetReadyGas, int(receipt.GasUsed), "SetReady")

	// now let's try to claim
	tx, err = swapCreator.Claim(getAuth(t, pkA), swap, s)
	require.NoError(t, err)
	receipt = getReceipt(t, ec, tx)
	t.Logf("gas cost to call ETH Claim: %d (delta %d)", receipt.GasUsed, MaxClaimETHGas-int(receipt.GasUsed))
	require.GreaterOrEqual(t, MaxClaimETHGas, int(receipt.GasUsed), "ETH Claim")

	stage, err := swapCreator.Swaps(nil, id)
	require.NoError(t, err)
	require.Equal(t, StageCompleted, stage)
}

func testClaim(t *testing.T, asset types.EthAsset, newLogIndex int, value *big.Int, erc20Contract *TestERC20) {
	// generate claim secret and public key
	dleq := &dleq.DefaultDLEq{}
	proof, err := dleq.Prove()
	require.NoError(t, err)
	res, err := dleq.Verify(proof)
	require.NoError(t, err)

	// hash public key
	cmt := res.Secp256k1PublicKey().Keccak256()

	// deploy swap contract with claim key hash
	pkA := tests.GetTakerTestKey(t)
	ec, _ := tests.NewEthClient(t)
	addr := crypto.PubkeyToAddress(pkA.PublicKey)

	swapCreatorAddr, swapCreator := DevDeploySwapCreator(t, ec, pkA)

	if asset.IsToken() {
		approveERC20(t, ec, pkA, erc20Contract, swapCreatorAddr, value)
	}

	txOpts := getAuth(t, pkA)
	require.NoError(t, err)
	if asset.IsETH() {
		txOpts.Value = value
	}

	nonce := GenerateNewSwapNonce()
	tx, err := swapCreator.NewSwap(txOpts, cmt, dummySwapKey, addr,
		defaultTimeoutDuration, defaultTimeoutDuration, asset.Address(), value, nonce)
	require.NoError(t, err)

	receipt := getReceipt(t, ec, tx)
	if asset.IsETH() {
		t.Logf("gas cost to call ETH NewSwap: %d (delta %d)",
			receipt.GasUsed, MaxNewSwapETHGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxNewSwapETHGas, int(receipt.GasUsed), "ETH NewSwap")
	} else {
		t.Logf("gas cost to call token NewSwap: %d (delta %d)",
			receipt.GasUsed, MaxNewSwapTokenGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxNewSwapTokenGas, int(receipt.GasUsed), "Token NewSwap")
	}

	require.Equal(t, newLogIndex+1, len(receipt.Logs))
	id, err := GetIDFromLog(receipt.Logs[newLogIndex])
	require.NoError(t, err)

	t1, t2, err := GetTimeoutsFromLog(receipt.Logs[newLogIndex])
	require.NoError(t, err)

	swap := SwapCreatorSwap{
		Owner:            addr,
		Claimer:          addr,
		ClaimCommitment:  cmt,
		RefundCommitment: dummySwapKey,
		Timeout1:         t1,
		Timeout2:         t2,
		Asset:            asset.Address(),
		Value:            value,
		Nonce:            nonce,
	}

	// ensure we can't claim before setting contract to Ready
	_, err = swapCreator.Claim(getAuth(t, pkA), swap, proof.Secret())
	require.ErrorContains(t, err, "VM Exception while processing transaction: revert")

	// set contract to Ready
	tx, err = swapCreator.SetReady(getAuth(t, pkA), swap)
	require.NoError(t, err)
	receipt = getReceipt(t, ec, tx)
	t.Logf("gas cost to call SetReady: %d (delta %d)", receipt.GasUsed, MaxSetReadyGas-int(receipt.GasUsed))
	require.GreaterOrEqual(t, MaxSetReadyGas, int(receipt.GasUsed), "SetReady")

	// now let's try to claim
	tx, err = swapCreator.Claim(getAuth(t, pkA), swap, proof.Secret())
	require.NoError(t, err)
	receipt = getReceipt(t, ec, tx)

	if asset.IsETH() {
		t.Logf("gas cost to call ETH Claim: %d (delta %d)", receipt.GasUsed, MaxClaimETHGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxClaimETHGas, int(receipt.GasUsed), "ETH Claim")
	} else {
		t.Logf("gas cost to call token Claim: %d (delta %d)", receipt.GasUsed, MaxClaimTokenGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxClaimTokenGas, int(receipt.GasUsed), "Token Claim")
	}

	stage, err := swapCreator.Swaps(nil, id)
	require.NoError(t, err)
	require.Equal(t, StageCompleted, stage)
}

func TestSwapCreator_Claim_random(t *testing.T) {
	testClaim(t, types.EthAssetETH, 0, defaultSwapValue, nil)
}

func testRefundBeforeT1(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20, newLogIndex int) {
	// generate refund secret and public key
	dleq := &dleq.DefaultDLEq{}
	proof, err := dleq.Prove()
	require.NoError(t, err)
	res, err := dleq.Verify(proof)
	require.NoError(t, err)

	// hash public key
	cmt := res.Secp256k1PublicKey().Keccak256()

	// deploy swap contract with refund key hash
	pkA := tests.GetTakerTestKey(t)
	ec, _ := tests.NewEthClient(t)
	addr := crypto.PubkeyToAddress(pkA.PublicKey)

	swapCreatorAddr, swapCreator := DevDeploySwapCreator(t, ec, pkA)

	if asset.IsToken() {
		approveERC20(t, ec, pkA, erc20Contract, swapCreatorAddr, defaultSwapValue)
	}

	txOpts := getAuth(t, pkA)
	txOpts.Value = defaultSwapValue

	nonce := GenerateNewSwapNonce()
	tx, err := swapCreator.NewSwap(txOpts, dummySwapKey, cmt, addr, defaultTimeoutDuration, defaultTimeoutDuration,
		asset.Address(), defaultSwapValue, nonce)
	require.NoError(t, err)

	receipt := getReceipt(t, ec, tx)
	if asset.IsETH() {
		t.Logf("gas cost to call ETH NewSwap: %d (delta %d)",
			receipt.GasUsed, MaxNewSwapETHGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxNewSwapETHGas, int(receipt.GasUsed), "ETH NewSwap")
	} else {
		t.Logf("gas cost to call token NewSwap: %d (delta %d)",
			receipt.GasUsed, MaxNewSwapTokenGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxNewSwapTokenGas, int(receipt.GasUsed), "ETH NewSwap")
	}

	require.Equal(t, newLogIndex+1, len(receipt.Logs))
	id, err := GetIDFromLog(receipt.Logs[newLogIndex])
	require.NoError(t, err)

	t1, t2, err := GetTimeoutsFromLog(receipt.Logs[newLogIndex])
	require.NoError(t, err)

	swap := SwapCreatorSwap{
		Owner:            addr,
		Claimer:          addr,
		ClaimCommitment:  dummySwapKey,
		RefundCommitment: cmt,
		Timeout1:         t1,
		Timeout2:         t2,
		Asset:            asset.Address(),
		Value:            defaultSwapValue,
		Nonce:            nonce,
	}

	// now let's try to refund
	tx, err = swapCreator.Refund(getAuth(t, pkA), swap, proof.Secret())
	require.NoError(t, err)
	receipt = getReceipt(t, ec, tx)

	if asset.IsETH() {
		t.Logf("gas cost to call ETH Refund: %d (delta %d)",
			receipt.GasUsed, MaxRefundETHGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxRefundETHGas, int(receipt.GasUsed), "ETH Refund")
	} else {
		t.Logf("gas cost to call token Refund: %d (delta %d)",
			receipt.GasUsed, MaxRefundTokenGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxRefundTokenGas, int(receipt.GasUsed), "Token Refund")
	}

	stage, err := swapCreator.Swaps(nil, id)
	require.NoError(t, err)
	require.Equal(t, StageCompleted, stage)
}

func TestSwapCreator_Refund_beforeT1(t *testing.T) {
	testRefundBeforeT1(t, types.EthAssetETH, nil, 0)
}

func testRefundAfterT2(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20, newLogIndex int) {
	ctx := context.Background()

	// generate refund secret and public key
	dleq := &dleq.DefaultDLEq{}
	proof, err := dleq.Prove()
	require.NoError(t, err)
	res, err := dleq.Verify(proof)
	require.NoError(t, err)

	// hash public key
	cmt := res.Secp256k1PublicKey().Keccak256()

	// deploy swap contract with refund key hash
	pkA := tests.GetTakerTestKey(t)
	ec, _ := tests.NewEthClient(t)
	addr := crypto.PubkeyToAddress(pkA.PublicKey)

	swapCreatorAddr, swapCreator := DevDeploySwapCreator(t, ec, pkA)

	if asset.IsToken() {
		approveERC20(t, ec, pkA, erc20Contract, swapCreatorAddr, defaultSwapValue)
	}

	txOpts := getAuth(t, pkA)
	txOpts.Value = defaultSwapValue

	nonce := GenerateNewSwapNonce()
	timeout := big.NewInt(3)
	tx, err := swapCreator.NewSwap(txOpts, dummySwapKey, cmt, addr, timeout, timeout,
		asset.Address(), defaultSwapValue, nonce)
	require.NoError(t, err)
	receipt := getReceipt(t, ec, tx)

	if asset.IsETH() {
		t.Logf("gas cost to call ETH NewSwap: %d (delta %d)",
			receipt.GasUsed, MaxNewSwapETHGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxNewSwapETHGas, int(receipt.GasUsed), "ETH NewSwap")
	} else {
		t.Logf("gas cost to call token NewSwap: %d (delta %d)",
			receipt.GasUsed, MaxNewSwapTokenGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxNewSwapTokenGas, int(receipt.GasUsed), "Token NewSwap")
	}

	require.Equal(t, newLogIndex+1, len(receipt.Logs))
	id, err := GetIDFromLog(receipt.Logs[newLogIndex])
	require.NoError(t, err)

	t1, t2, err := GetTimeoutsFromLog(receipt.Logs[newLogIndex])
	require.NoError(t, err)

	// ensure we can't refund between T1 and T2
	<-time.After(time.Until(time.Unix(t1.Int64()+1, 0)))
	swap := SwapCreatorSwap{
		Owner:            addr,
		Claimer:          addr,
		ClaimCommitment:  dummySwapKey,
		RefundCommitment: cmt,
		Timeout1:         t1,
		Timeout2:         t2,
		Asset:            asset.Address(),
		Value:            defaultSwapValue,
		Nonce:            nonce,
	}

	secret := proof.Secret()
	tx, err = swapCreator.Refund(getAuth(t, pkA), swap, secret)
	require.NoError(t, err)
	_, err = block.WaitForReceipt(ctx, ec, tx.Hash())
	require.ErrorContains(t, err, "VM Exception while processing transaction: revert")

	<-time.After(time.Until(time.Unix(t2.Int64()+1, 0)))

	// now let's try to refund
	tx, err = swapCreator.Refund(getAuth(t, pkA), swap, secret)
	require.NoError(t, err)
	receipt = getReceipt(t, ec, tx)

	if asset.IsETH() {
		t.Logf("gas cost to call ETH Refund: %d (delta %d)",
			receipt.GasUsed, MaxRefundETHGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxRefundETHGas, int(receipt.GasUsed), "ETH Refund")
	} else {
		t.Logf("gas cost to call token Refund: %d (delta %d)",
			receipt.GasUsed, MaxRefundTokenGas-int(receipt.GasUsed))
		require.GreaterOrEqual(t, MaxRefundTokenGas, int(receipt.GasUsed), "Token Refund")
	}

	callOpts := &bind.CallOpts{Context: ctx}
	stage, err := swapCreator.Swaps(callOpts, id)
	require.NoError(t, err)
	require.Equal(t, StageCompleted, stage)
}

func TestSwapCreator_Refund_afterT2(t *testing.T) {
	testRefundAfterT2(t, types.EthAssetETH, nil, 0)
}

// test case where contract has multiple swaps happening at once
func TestSwapCreator_MultipleSwaps(t *testing.T) {
	pkContractCreator := tests.GetTestKeyByIndex(t, 0)
	ec, _ := tests.NewEthClient(t)

	_, swapCreator := DevDeploySwapCreator(t, ec, pkContractCreator)

	const numSwaps = 16
	type swapCase struct {
		index     int // index in the swap array
		walletKey *ecdsa.PrivateKey
		id        [32]byte
		secret    [32]byte
		swap      SwapCreatorSwap
	}

	swapCases := [numSwaps]swapCase{}

	// setup all swap instances
	for i := 0; i < numSwaps; i++ {
		sc := &swapCases[i]
		sc.index = i

		// generate claim secret and public key
		dleq := &dleq.DefaultDLEq{}
		proof, err := dleq.Prove()
		require.NoError(t, err)
		res, err := dleq.Verify(proof)
		require.NoError(t, err)

		sc.secret = proof.Secret()
		sc.walletKey = tests.GetTestKeyByIndex(t, i)
		addrSwap := crypto.PubkeyToAddress(*sc.walletKey.Public().(*ecdsa.PublicKey))

		sc.swap = SwapCreatorSwap{
			Owner:            addrSwap,
			Claimer:          addrSwap,
			ClaimCommitment:  res.Secp256k1PublicKey().Keccak256(),
			RefundCommitment: dummySwapKey, // no one calls refund in this test
			Timeout1:         nil,          // timeouts initialised when swap is created
			Timeout2:         nil,
			Asset:            ethcommon.Address(types.EthAssetETH),
			Value:            defaultSwapValue,
			Nonce:            big.NewInt(int64(i)),
		}
	}

	// We create all transactions in parallel, so the transactions of each swap stage can get bundled up
	// into one or two blocks and greatly speed up the test.
	var wg sync.WaitGroup

	// create swap instances
	wg.Add(numSwaps)
	for i := 0; i < numSwaps; i++ {
		go func(sc *swapCase) {
			defer wg.Done()

			auth := getAuth(t, sc.walletKey)
			auth.Value = sc.swap.Value
			tx, err := swapCreator.NewSwap(
				auth,
				sc.swap.ClaimCommitment,
				sc.swap.RefundCommitment,
				sc.swap.Claimer,
				defaultTimeoutDuration,
				defaultTimeoutDuration,
				types.EthAssetETH.Address(),
				sc.swap.Value,
				sc.swap.Nonce,
			)
			require.NoError(t, err)
			receipt := getReceipt(t, ec, tx)

			t.Logf("gas cost to call ETH NewSwap[%d]: %d (delta %d)",
				sc.index, receipt.GasUsed, MaxNewSwapETHGas-int(receipt.GasUsed))
			require.GreaterOrEqual(t, MaxNewSwapETHGas, int(receipt.GasUsed), "ETH NewSwap")

			require.Equal(t, 1, len(receipt.Logs))
			sc.id, err = GetIDFromLog(receipt.Logs[0])
			require.NoError(t, err)

			sc.swap.Timeout1, sc.swap.Timeout2, err = GetTimeoutsFromLog(receipt.Logs[0])
			require.NoError(t, err)
		}(&swapCases[i])
	}
	wg.Wait() // all swaps created

	// set all swaps to Ready
	wg.Add(numSwaps)
	for i := 0; i < numSwaps; i++ {
		go func(sc *swapCase) {
			defer wg.Done()
			tx, err := swapCreator.SetReady(getAuth(t, sc.walletKey), sc.swap)
			require.NoError(t, err)
			receipt := getReceipt(t, ec, tx)
			t.Logf("gas cost to call SetReady[%d]: %d (delta %d)",
				sc.index, receipt.GasUsed, MaxSetReadyGas-int(receipt.GasUsed))
			require.GreaterOrEqual(t, MaxSetReadyGas, int(receipt.GasUsed), "SetReady")
		}(&swapCases[i])
	}
	wg.Wait() // set_ready called on all swaps

	// call claim on all the swaps
	wg.Add(numSwaps)
	for i := 0; i < numSwaps; i++ {
		go func(sc *swapCase) {
			defer wg.Done()
			tx, err := swapCreator.Claim(getAuth(t, sc.walletKey), sc.swap, sc.secret)
			require.NoError(t, err)
			receipt := getReceipt(t, ec, tx)
			t.Logf("gas cost to call ETH Claim[%d]: %d (delta %d)",
				sc.index, receipt.GasUsed, MaxClaimETHGas-int(receipt.GasUsed))
			require.GreaterOrEqual(t, MaxClaimETHGas, int(receipt.GasUsed), "ETH Claim")
		}(&swapCases[i])
	}
	wg.Wait() // claim called on all swaps

	// ensure all swaps are completed
	wg.Add(numSwaps)
	for i := 0; i < numSwaps; i++ {
		go func(sc *swapCase) {
			defer wg.Done()
			stage, err := swapCreator.Swaps(nil, sc.id)
			require.NoError(t, err)
			require.Equal(t, StageToString(StageCompleted), StageToString(stage))
		}(&swapCases[i])
	}
	wg.Wait() // status of all swaps checked
}
