From 805e2a71f47f00ee12930521b0fde18ee68f6dd3 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Tue, 4 May 2021 18:40:53 +0900 Subject: [PATCH] multi: Add ETHSwapV0. Have the eth harness start with a contract. Add contract bindings and basic calls to clients. --- client/asset/eth/eth.go | 5 + client/asset/eth/eth_test.go | 14 + client/asset/eth/rpcclient.go | 63 ++++ client/asset/eth/rpcclient_harness_test.go | 351 +++++++++++++++++++ server/asset/eth/contract.go | 379 +++++++++++++++++++++ server/asset/eth/contracts/ETHSwapV0.sol | 84 +++++ server/asset/eth/contracts/README.md | 13 + 7 files changed, 909 insertions(+) create mode 100644 server/asset/eth/contract.go create mode 100644 server/asset/eth/contracts/ETHSwapV0.sol create mode 100644 server/asset/eth/contracts/README.md diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index f07f1fc403..4aa1c9012f 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -19,6 +19,7 @@ import ( "decred.org/dcrdex/server/asset/eth" "github.com/decred/dcrd/dcrutil/v3" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/node" @@ -103,13 +104,17 @@ type ethFetcher interface { blockNumber(ctx context.Context) (uint64, error) connect(ctx context.Context, node *node.Node, contractAddr common.Address) error importAccount(pw string, privKeyB []byte) (*accounts.Account, error) + initiate(opts *bind.TransactOpts, netID int64, refundTimestamp int64, secretHash [32]byte, participant common.Address) (*types.Transaction, error) lock(ctx context.Context, acct *accounts.Account) error locked(ctx context.Context, acct *accounts.Account) (bool, error) nodeInfo(ctx context.Context) (*p2p.NodeInfo, error) pendingTransactions(ctx context.Context) ([]*types.Transaction, error) transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) + redeem(opts *bind.TransactOpts, netID int64, secret, secretHash [32]byte) (*types.Transaction, error) + refund(opts *bind.TransactOpts, netID int64, secretHash [32]byte) (*types.Transaction, error) sendToAddr(ctx context.Context, acct *accounts.Account, addr common.Address, amt, gasFee *big.Int) (common.Hash, error) shutdown() + swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*eth.ETHSwapSwap, error) syncStatus(ctx context.Context) (bool, float32, error) unlock(ctx context.Context, pw string, acct *accounts.Account) error } diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 6aa12d3c56..f8a9db2d68 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -11,7 +11,9 @@ import ( "testing" "decred.org/dcrdex/dex" + "decred.org/dcrdex/server/asset/eth" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/node" @@ -77,6 +79,18 @@ func (n *testNode) blockNumber(ctx context.Context) (uint64, error) { func (n *testNode) pendingTransactions(ctx context.Context) ([]*types.Transaction, error) { return nil, nil } +func (n *testNode) initiate(opts *bind.TransactOpts, netID int64, refundTimestamp int64, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return nil, nil +} +func (n *testNode) redeem(opts *bind.TransactOpts, netID int64, secret, secretHash [32]byte) (*types.Transaction, error) { + return nil, nil +} +func (n *testNode) refund(opts *bind.TransactOpts, netID int64, secretHash [32]byte) (*types.Transaction, error) { + return nil, nil +} +func (n *testNode) swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*eth.ETHSwapSwap, error) { + return nil, nil +} func (n *testNode) transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { return nil, nil } diff --git a/client/asset/eth/rpcclient.go b/client/asset/eth/rpcclient.go index 15af6b3cba..3b41da3784 100644 --- a/client/asset/eth/rpcclient.go +++ b/client/asset/eth/rpcclient.go @@ -10,7 +10,9 @@ import ( "fmt" "math/big" + "decred.org/dcrdex/server/asset/eth" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -32,6 +34,7 @@ type rpcclient struct { // ec wraps the client with some useful calls. ec *ethclient.Client n *node.Node + es *eth.ETHSwap } // connect connects to a node. It then wraps ethclient's client and @@ -44,6 +47,10 @@ func (c *rpcclient) connect(ctx context.Context, node *node.Node, contractAddr c c.c = client c.ec = ethclient.NewClient(client) c.n = node + c.es, err = eth.NewETHSwap(contractAddr, c.ec) + if err != nil { + return fmt.Errorf("unable to find swap contract: %v", err) + } return nil } @@ -242,3 +249,59 @@ func (c *rpcclient) wallet(acct accounts.Account) (accounts.Wallet, error) { } return wallet, nil } + +// swap gets a swap keyed by secretHash in the contract. +func (c *rpcclient) swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*eth.ETHSwapSwap, error) { + callOpts := &bind.CallOpts{ + Pending: true, + From: from.Address, + Context: ctx, + } + swap, err := c.es.Swap(callOpts, secretHash) + if err != nil { + return nil, err + } + return &swap, nil +} + +// initiate creates a swap contract. The initiator will be the account at +// txOpts.From. Any on-chain failure, such as this secret hash already existing +// in the swaps map, will not cause this to error. +func (c *rpcclient) initiate(txOpts *bind.TransactOpts, netID int64, refundTimestamp int64, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + wallet, err := c.wallet(accounts.Account{Address: txOpts.From}) + if err != nil { + return nil, err + } + txOpts.Signer = func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { + return wallet.SignTx(accounts.Account{Address: addr}, tx, big.NewInt(netID)) + } + return c.es.Initiate(txOpts, big.NewInt(refundTimestamp), secretHash, participant) +} + +// redeem redeems a swap contract. The redeemer will be the account at txOpts.From. +// Any on-chain failure, such as this secret not mathing the hash, will not cause +// this to error. +func (c *rpcclient) redeem(txOpts *bind.TransactOpts, netID int64, secret, secretHash [32]byte) (*types.Transaction, error) { + wallet, err := c.wallet(accounts.Account{Address: txOpts.From}) + if err != nil { + return nil, err + } + txOpts.Signer = func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { + return wallet.SignTx(accounts.Account{Address: addr}, tx, big.NewInt(netID)) + } + return c.es.Redeem(txOpts, secret, secretHash) +} + +// refund refunds a swap contract. The refunder will be the account at txOpts.From. +// Any on-chain failure, such as the locktime not being past, will not cause +// this to error. +func (c *rpcclient) refund(txOpts *bind.TransactOpts, netID int64, secretHash [32]byte) (*types.Transaction, error) { + wallet, err := c.wallet(accounts.Account{Address: txOpts.From}) + if err != nil { + return nil, err + } + txOpts.Signer = func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { + return wallet.SignTx(accounts.Account{Address: addr}, tx, big.NewInt(netID)) + } + return c.es.Refund(txOpts, secretHash) +} diff --git a/client/asset/eth/rpcclient_harness_test.go b/client/asset/eth/rpcclient_harness_test.go index ef7b1128e2..36c936abc8 100644 --- a/client/asset/eth/rpcclient_harness_test.go +++ b/client/asset/eth/rpcclient_harness_test.go @@ -16,6 +16,7 @@ package eth import ( "context" + "crypto/sha256" "encoding/hex" "errors" "fmt" @@ -29,6 +30,8 @@ import ( "decred.org/dcrdex/client/asset" "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/encode" + "decred.org/dcrdex/server/asset/eth" "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -323,6 +326,17 @@ func TestPendingTransactions(t *testing.T) { spew.Dump(txs) } +func TestSwap(t *testing.T) { + var secretHash [32]byte + copy(secretHash[:], encode.RandomBytes(32)) + swap, err := ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatal(err) + } + // Should be empty. + spew.Dump(swap) +} + func TestSyncStatus(t *testing.T) { synced, ratio, err := ethClient.syncStatus(ctx) if err != nil { @@ -331,3 +345,340 @@ func TestSyncStatus(t *testing.T) { spew.Dump(synced) spew.Dump(ratio) } + +func TestInitiate(t *testing.T) { + now := time.Now().Unix() + var secretHash [32]byte + copy(secretHash[:], encode.RandomBytes(32)) + err := ethClient.unlock(ctx, pw, simnetAcct) + if err != nil { + t.Fatal(err) + } + amt := big.NewInt(1e18) + txOpts := newTXOpts(ctx, simnetAddr, amt) + swap, err := ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatal("unable to get swap state") + } + spew.Dump(swap) + state := eth.SwapState(swap.State) + if state != eth.None { + t.Fatalf("unexpeted swap state: want %s got %s", eth.None, state) + } + + tests := []struct { + name string + subAmt bool + finalState eth.SwapState + }{{ + name: "ok", + finalState: eth.Initiated, + subAmt: true, + }, { + // If the hash already exists, the contract should subtract only + // the tx fee from the account. + name: "secret hash already exists", + finalState: eth.Initiated, + }} + + for _, test := range tests { + originalBal, err := ethClient.balance(ctx, simnetAcct) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + + tx, err := ethClient.initiate(txOpts, simnetID, now, secretHash, participantAddr) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + spew.Dump(tx) + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + + // It appears the receipt is only accessable after the tx is mined. + receipt, err := ethClient.transactionReceipt(ctx, tx.Hash()) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + spew.Dump(receipt) + + // Balance should be reduced by a certain amount depending on + // whether initiate completed successfully on-chain. If + // unsuccessful the fee it subtracted. If successful, amt is + // also subtracted. + bal, err := ethClient.balance(ctx, simnetAcct) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + txFee := big.NewInt(0).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice) + wantBal := big.NewInt(0).Sub(originalBal, txFee) + if test.subAmt { + wantBal.Sub(wantBal, amt) + } + if bal.Cmp(wantBal) != 0 { + t.Fatalf("unexpeted balance change for test %v: want %v got %v", test.name, wantBal, bal) + } + + swap, err = ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + state := eth.SwapState(swap.State) + if state != test.finalState { + t.Fatalf("unexpeted swap state for test %v: want %s got %s", test.name, test.finalState, state) + } + } +} + +func TestRedeem(t *testing.T) { + amt := big.NewInt(1e18) + locktime := time.Second * 12 + tests := []struct { + name string + sleep time.Duration + redeemer *accounts.Account + finalState eth.SwapState + addAmt, badSecret bool + }{{ + name: "ok before locktime", + sleep: time.Second * 8, + redeemer: participantAcct, + finalState: eth.Redeemed, + addAmt: true, + }, { + name: "ok after locktime", + sleep: time.Second * 16, + redeemer: participantAcct, + finalState: eth.Redeemed, + addAmt: true, + }, { + name: "bad secred", + sleep: time.Second * 8, + redeemer: participantAcct, + finalState: eth.Initiated, + badSecret: true, + }, { + name: "wrong redeemer", + sleep: time.Second * 8, + finalState: eth.Initiated, + redeemer: simnetAcct, + }} + + for _, test := range tests { + err := ethClient.unlock(ctx, pw, simnetAcct) + if err != nil { + t.Fatal(err) + } + err = ethClient.unlock(ctx, pw, participantAcct) + if err != nil { + t.Fatal(err) + } + txOpts := newTXOpts(ctx, simnetAddr, amt) + var secret [32]byte + copy(secret[:], encode.RandomBytes(32)) + secretHash := sha256.Sum256(secret[:]) + + swap, err := ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatal("unable to get swap state") + } + state := eth.SwapState(swap.State) + if state != eth.None { + t.Fatalf("unexpeted swap state for test %v: want %s got %s", test.name, eth.None, state) + } + + // Create a secret that doesn't has to secredHash. + if test.badSecret { + copy(secret[:], encode.RandomBytes(32)) + } + + inLocktime := time.Now().Add(locktime).Unix() + _, err = ethClient.initiate(txOpts, simnetID, inLocktime, secretHash, participantAcct.Address) + if err != nil { + t.Fatalf("unable to initiate swap for test %v: %v ", test.name, err) + } + + // This waitForMined will always take test.sleep to complete. + if err := waitForMined(t, test.sleep, true); err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + originalBal, err := ethClient.balance(ctx, test.redeemer) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + txOpts = newTXOpts(ctx, test.redeemer.Address, nil) + tx, err := ethClient.redeem(txOpts, simnetID, secret, secretHash) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + spew.Dump(tx) + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + + // It appears the receipt is only accessable after the tx is mined. + receipt, err := ethClient.transactionReceipt(ctx, tx.Hash()) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + spew.Dump(receipt) + + // Balance should increase or decrease by a certain amount + // depending on whether redeem completed successfully on-chain. + // If unsuccessful the fee it subtracted. If successful, amt is + // added. + bal, err := ethClient.balance(ctx, test.redeemer) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + txFee := big.NewInt(0).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice) + wantBal := big.NewInt(0).Sub(originalBal, txFee) + if test.addAmt { + wantBal.Add(wantBal, amt) + } + if bal.Cmp(wantBal) != 0 { + t.Fatalf("unexpeted balance change for test %v: want %v got %v", test.name, wantBal, bal) + } + + swap, err = ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + state = eth.SwapState(swap.State) + if state != test.finalState { + t.Fatalf("unexpeted swap state for test %v: want %s got %s", test.name, test.finalState, state) + } + } +} + +func TestRefund(t *testing.T) { + amt := big.NewInt(1e18) + locktime := time.Second * 12 + tests := []struct { + name string + sleep time.Duration + refunder *accounts.Account + finalState eth.SwapState + addAmt, redeem bool + }{{ + name: "ok", + sleep: time.Second * 16, + refunder: simnetAcct, + addAmt: true, + finalState: eth.Refunded, + }, { + name: "before locktime", + sleep: time.Second * 8, + refunder: simnetAcct, + finalState: eth.Initiated, + }, { + name: "wrong refunder", + sleep: time.Second * 16, + refunder: participantAcct, + finalState: eth.Initiated, + }, { + name: "already redeemed", + sleep: time.Second * 16, + refunder: simnetAcct, + redeem: true, + finalState: eth.Redeemed, + }} + + for _, test := range tests { + err := ethClient.unlock(ctx, pw, simnetAcct) + if err != nil { + t.Fatal(err) + } + err = ethClient.unlock(ctx, pw, participantAcct) + if err != nil { + t.Fatal(err) + } + txOpts := newTXOpts(ctx, simnetAddr, amt) + var secret [32]byte + copy(secret[:], encode.RandomBytes(32)) + secretHash := sha256.Sum256(secret[:]) + + swap, err := ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatal("unable to get swap state") + } + state := eth.SwapState(swap.State) + if state != eth.None { + t.Fatalf("unexpeted swap state for test %v: want %s got %s", test.name, eth.None, state) + } + + inLocktime := time.Now().Add(locktime).Unix() + _, err = ethClient.initiate(txOpts, simnetID, inLocktime, secretHash, participantAcct.Address) + if err != nil { + t.Fatalf("unable to initiate swap for test %v: %v ", test.name, err) + } + + if test.redeem { + if err := waitForMined(t, time.Second*8, false); err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + txOpts = newTXOpts(ctx, participantAddr, nil) + _, err := ethClient.redeem(txOpts, simnetID, secret, secretHash) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + } + + // This waitForMined will always take test.sleep to complete. + if err := waitForMined(t, test.sleep, true); err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + + originalBal, err := ethClient.balance(ctx, test.refunder) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + + txOpts = newTXOpts(ctx, test.refunder.Address, nil) + tx, err := ethClient.refund(txOpts, simnetID, secretHash) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + spew.Dump(tx) + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + + // It appears the receipt is only accessable after the tx is mined. + receipt, err := ethClient.transactionReceipt(ctx, tx.Hash()) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + spew.Dump(receipt) + + // Balance should increase or decrease by a certain amount + // depending on whether redeem completed successfully on-chain. + // If unsuccessful the fee it subtracted. If successful, amt is + // added. + bal, err := ethClient.balance(ctx, test.refunder) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + txFee := big.NewInt(0).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice) + wantBal := big.NewInt(0).Sub(originalBal, txFee) + if test.addAmt { + wantBal.Add(wantBal, amt) + } + if bal.Cmp(wantBal) != 0 { + t.Fatalf("unexpeted balance change for test %v: want %v got %v", test.name, wantBal, bal) + } + + swap, err = ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + state = eth.SwapState(swap.State) + if state != test.finalState { + t.Fatalf("unexpeted swap state for test %v: want %s got %s", test.name, test.finalState, state) + } + } +} diff --git a/server/asset/eth/contract.go b/server/asset/eth/contract.go new file mode 100644 index 0000000000..891a467061 --- /dev/null +++ b/server/asset/eth/contract.go @@ -0,0 +1,379 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package eth + +import ( + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// ETHSwapSwap is an auto generated low-level Go binding around an user-defined struct. +type ETHSwapSwap struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 +} + +// ETHSwapABI is the input ABI used to generate the binding from. +const ETHSwapABI = "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"refundTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"}],\"name\":\"initiate\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"redeem\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"refund\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"swap\",\"outputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"initBlockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"refundBlockTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"enumETHSwap.State\",\"name\":\"state\",\"type\":\"uint8\"}],\"internalType\":\"structETHSwap.Swap\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"swaps\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"initBlockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"refundBlockTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"enumETHSwap.State\",\"name\":\"state\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]" + +// ETHSwapFuncSigs maps the 4-byte function signature to its string representation. +var ETHSwapFuncSigs = map[string]string{ + "ae052147": "initiate(uint256,bytes32,address)", + "b31597ad": "redeem(bytes32,bytes32)", + "7249fbb6": "refund(bytes32)", + "76467cbd": "swap(bytes32)", + "eb84e7f2": "swaps(bytes32)", +} + +// ETHSwapBin is the compiled bytecode used for deploying new contracts. +var ETHSwapBin = "0x608060405234801561001057600080fd5b50610758806100206000396000f3fe60806040526004361061004a5760003560e01c80637249fbb61461004f57806376467cbd14610071578063ae052147146100a7578063b31597ad146100ba578063eb84e7f2146100da575b600080fd5b34801561005b57600080fd5b5061006f61006a366004610565565b61010e565b005b34801561007d57600080fd5b5061009161008c366004610565565b6101f7565b60405161009e919061065d565b60405180910390f35b61006f6100b53660046105b6565b6102c1565b3480156100c657600080fd5b5061006f6100d5366004610595565b610388565b3480156100e657600080fd5b506100fa6100f5366004610565565b6104d0565b60405161009e9897969594939291906106cd565b8033600160008381526020819052604090206007015460ff16600381111561014657634e487b7160e01b600052602160045260246000fd5b1461015057600080fd5b6000828152602081905260409020600401546001600160a01b0382811691161461017957600080fd5b6000828152602081905260409020600101544281111561019857600080fd5b600084815260208190526040808220600601549051339282156108fc02929190818181858888f193505050501580156101d5573d6000803e3d6000fd5b505050600091825250602081905260409020600701805460ff19166003179055565b6101ff610523565b6000828152602081815260409182902082516101008101845281548152600182015492810192909252600281015492820192909252600380830154606083015260048301546001600160a01b03908116608084015260058401541660a0830152600683015460c0830152600783015491929160e084019160ff9091169081111561029957634e487b7160e01b600052602160045260246000fd5b60038111156102b857634e487b7160e01b600052602160045260246000fd5b90525092915050565b82600034116102cf57600080fd5b600081116102dc57600080fd5b826000808281526020819052604090206007015460ff16600381111561031257634e487b7160e01b600052602160045260246000fd5b1461031c57600080fd5b6000848152602081905260409020438155600180820187905560028201869055600482018054336001600160a01b0319918216179091556005830180549091166001600160a01b0387161790553460068301556007909101805460ff1916828002179055505050505050565b808233600160008481526020819052604090206007015460ff1660038111156103c157634e487b7160e01b600052602160045260246000fd5b146103cb57600080fd5b6000838152602081905260409020600501546001600160a01b038281169116146103f457600080fd5b82600283604051602001610408919061061b565b60408051601f198184030181529082905261042291610624565b602060405180830381855afa15801561043f573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610462919061057d565b1461046c57600080fd5b600084815260208190526040808220600601549051339282156108fc02929190818181858888f193505050501580156104a9573d6000803e3d6000fd5b50505060009182525060208190526040902060078101805460ff1916600217905560030155565b6000602081905290815260409020805460018201546002830154600384015460048501546005860154600687015460079097015495969495939492936001600160a01b0392831693919092169160ff1688565b6040805161010081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c081018290529060e082015290565b600060208284031215610576578081fd5b5035919050565b60006020828403121561058e578081fd5b5051919050565b600080604083850312156105a7578081fd5b50508035926020909101359150565b6000806000606084860312156105ca578081fd5b833592506020840135915060408401356001600160a01b03811681146105ee578182fd5b809150509250925092565b6004811061061757634e487b7160e01b600052602160045260246000fd5b9052565b90815260200190565b60008251815b81811015610644576020818601810151858301520161062a565b818111156106525782828501525b509190910192915050565b60006101008201905082518252602083015160208301526040830151604083015260608301516060830152608083015160018060a01b0380821660808501528060a08601511660a0850152505060c083015160c083015260e08301516106c660e08401826105f9565b5092915050565b8881526020810188905260408101879052606081018690526001600160a01b038581166080830152841660a082015260c08101839052610100810161071560e08301846105f9565b999850505050505050505056fea26469706673582212204ae7685f717de45f989d837f9275de6e36bef2be566fd8950bde0a539301c70064736f6c63430008010033" + +// DeployETHSwap deploys a new Ethereum contract, binding an instance of ETHSwap to it. +func DeployETHSwap(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *ETHSwap, error) { + parsed, err := abi.JSON(strings.NewReader(ETHSwapABI)) + if err != nil { + return common.Address{}, nil, nil, err + } + + address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(ETHSwapBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, ÐSwap{ETHSwapCaller: ETHSwapCaller{contract: contract}, ETHSwapTransactor: ETHSwapTransactor{contract: contract}, ETHSwapFilterer: ETHSwapFilterer{contract: contract}}, nil +} + +// ETHSwap is an auto generated Go binding around an Ethereum contract. +type ETHSwap struct { + ETHSwapCaller // Read-only binding to the contract + ETHSwapTransactor // Write-only binding to the contract + ETHSwapFilterer // Log filterer for contract events +} + +// ETHSwapCaller is an auto generated read-only Go binding around an Ethereum contract. +type ETHSwapCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ETHSwapTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ETHSwapTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ETHSwapFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ETHSwapFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ETHSwapSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ETHSwapSession struct { + Contract *ETHSwap // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ETHSwapCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ETHSwapCallerSession struct { + Contract *ETHSwapCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ETHSwapTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ETHSwapTransactorSession struct { + Contract *ETHSwapTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ETHSwapRaw is an auto generated low-level Go binding around an Ethereum contract. +type ETHSwapRaw struct { + Contract *ETHSwap // Generic contract binding to access the raw methods on +} + +// ETHSwapCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ETHSwapCallerRaw struct { + Contract *ETHSwapCaller // Generic read-only contract binding to access the raw methods on +} + +// ETHSwapTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ETHSwapTransactorRaw struct { + Contract *ETHSwapTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewETHSwap creates a new instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwap(address common.Address, backend bind.ContractBackend) (*ETHSwap, error) { + contract, err := bindETHSwap(address, backend, backend, backend) + if err != nil { + return nil, err + } + return ÐSwap{ETHSwapCaller: ETHSwapCaller{contract: contract}, ETHSwapTransactor: ETHSwapTransactor{contract: contract}, ETHSwapFilterer: ETHSwapFilterer{contract: contract}}, nil +} + +// NewETHSwapCaller creates a new read-only instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwapCaller(address common.Address, caller bind.ContractCaller) (*ETHSwapCaller, error) { + contract, err := bindETHSwap(address, caller, nil, nil) + if err != nil { + return nil, err + } + return ÐSwapCaller{contract: contract}, nil +} + +// NewETHSwapTransactor creates a new write-only instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwapTransactor(address common.Address, transactor bind.ContractTransactor) (*ETHSwapTransactor, error) { + contract, err := bindETHSwap(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return ÐSwapTransactor{contract: contract}, nil +} + +// NewETHSwapFilterer creates a new log filterer instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwapFilterer(address common.Address, filterer bind.ContractFilterer) (*ETHSwapFilterer, error) { + contract, err := bindETHSwap(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return ÐSwapFilterer{contract: contract}, nil +} + +// bindETHSwap binds a generic wrapper to an already deployed contract. +func bindETHSwap(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(ETHSwapABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ETHSwap *ETHSwapRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ETHSwap.Contract.ETHSwapCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ETHSwap *ETHSwapRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ETHSwap.Contract.ETHSwapTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ETHSwap *ETHSwapRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ETHSwap.Contract.ETHSwapTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ETHSwap *ETHSwapCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ETHSwap.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ETHSwap *ETHSwapTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ETHSwap.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ETHSwap *ETHSwapTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ETHSwap.Contract.contract.Transact(opts, method, params...) +} + +// Swap is a free data retrieval call binding the contract method 0x76467cbd. +// +// Solidity: function swap(bytes32 secretHash) view returns((uint256,uint256,bytes32,bytes32,address,address,uint256,uint8)) +func (_ETHSwap *ETHSwapCaller) Swap(opts *bind.CallOpts, secretHash [32]byte) (ETHSwapSwap, error) { + var out []interface{} + err := _ETHSwap.contract.Call(opts, &out, "swap", secretHash) + + if err != nil { + return *new(ETHSwapSwap), err + } + + out0 := *abi.ConvertType(out[0], new(ETHSwapSwap)).(*ETHSwapSwap) + + return out0, err + +} + +// Swap is a free data retrieval call binding the contract method 0x76467cbd. +// +// Solidity: function swap(bytes32 secretHash) view returns((uint256,uint256,bytes32,bytes32,address,address,uint256,uint8)) +func (_ETHSwap *ETHSwapSession) Swap(secretHash [32]byte) (ETHSwapSwap, error) { + return _ETHSwap.Contract.Swap(&_ETHSwap.CallOpts, secretHash) +} + +// Swap is a free data retrieval call binding the contract method 0x76467cbd. +// +// Solidity: function swap(bytes32 secretHash) view returns((uint256,uint256,bytes32,bytes32,address,address,uint256,uint8)) +func (_ETHSwap *ETHSwapCallerSession) Swap(secretHash [32]byte) (ETHSwapSwap, error) { + return _ETHSwap.Contract.Swap(&_ETHSwap.CallOpts, secretHash) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(uint256 initBlockNumber, uint256 refundBlockTimestamp, bytes32 secretHash, bytes32 secret, address initiator, address participant, uint256 value, uint8 state) +func (_ETHSwap *ETHSwapCaller) Swaps(opts *bind.CallOpts, arg0 [32]byte) (struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 +}, error) { + var out []interface{} + err := _ETHSwap.contract.Call(opts, &out, "swaps", arg0) + + outstruct := new(struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 + }) + if err != nil { + return *outstruct, err + } + + outstruct.InitBlockNumber = *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + outstruct.RefundBlockTimestamp = *abi.ConvertType(out[1], new(*big.Int)).(**big.Int) + outstruct.SecretHash = *abi.ConvertType(out[2], new([32]byte)).(*[32]byte) + outstruct.Secret = *abi.ConvertType(out[3], new([32]byte)).(*[32]byte) + outstruct.Initiator = *abi.ConvertType(out[4], new(common.Address)).(*common.Address) + outstruct.Participant = *abi.ConvertType(out[5], new(common.Address)).(*common.Address) + outstruct.Value = *abi.ConvertType(out[6], new(*big.Int)).(**big.Int) + outstruct.State = *abi.ConvertType(out[7], new(uint8)).(*uint8) + + return *outstruct, err + +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(uint256 initBlockNumber, uint256 refundBlockTimestamp, bytes32 secretHash, bytes32 secret, address initiator, address participant, uint256 value, uint8 state) +func (_ETHSwap *ETHSwapSession) Swaps(arg0 [32]byte) (struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 +}, error) { + return _ETHSwap.Contract.Swaps(&_ETHSwap.CallOpts, arg0) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(uint256 initBlockNumber, uint256 refundBlockTimestamp, bytes32 secretHash, bytes32 secret, address initiator, address participant, uint256 value, uint8 state) +func (_ETHSwap *ETHSwapCallerSession) Swaps(arg0 [32]byte) (struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 +}, error) { + return _ETHSwap.Contract.Swaps(&_ETHSwap.CallOpts, arg0) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns() +func (_ETHSwap *ETHSwapTransactor) Initiate(opts *bind.TransactOpts, refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _ETHSwap.contract.Transact(opts, "initiate", refundTimestamp, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns() +func (_ETHSwap *ETHSwapSession) Initiate(refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _ETHSwap.Contract.Initiate(&_ETHSwap.TransactOpts, refundTimestamp, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns() +func (_ETHSwap *ETHSwapTransactorSession) Initiate(refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _ETHSwap.Contract.Initiate(&_ETHSwap.TransactOpts, refundTimestamp, secretHash, participant) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(bytes32 secret, bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapTransactor) Redeem(opts *bind.TransactOpts, secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.contract.Transact(opts, "redeem", secret, secretHash) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(bytes32 secret, bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.Contract.Redeem(&_ETHSwap.TransactOpts, secret, secretHash) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(bytes32 secret, bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapTransactorSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.Contract.Redeem(&_ETHSwap.TransactOpts, secret, secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapTransactor) Refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.contract.Transact(opts, "refund", secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.Contract.Refund(&_ETHSwap.TransactOpts, secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapTransactorSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.Contract.Refund(&_ETHSwap.TransactOpts, secretHash) +} diff --git a/server/asset/eth/contracts/ETHSwapV0.sol b/server/asset/eth/contracts/ETHSwapV0.sol new file mode 100644 index 0000000000..5e03179f4d --- /dev/null +++ b/server/asset/eth/contracts/ETHSwapV0.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity >=0.7.0 <0.9.0; +contract ETHSwap { + enum State { Empty, Filled, Redeemed, Refunded } + + struct Swap { + uint initBlockNumber; + uint refundBlockTimestamp; + bytes32 secretHash; + bytes32 secret; + address initiator; + address participant; + uint256 value; + State state; + } + + mapping(bytes32 => Swap) public swaps; + + constructor() {} + + modifier isRefundable(bytes32 secretHash, address refunder) { + require(swaps[secretHash].state == State.Filled); + require(swaps[secretHash].initiator == refunder); + uint refundBlockTimestamp = swaps[secretHash].refundBlockTimestamp; + require(block.timestamp >= refundBlockTimestamp); + _; + } + + modifier isRedeemable(bytes32 secretHash, bytes32 secret, address redeemer) { + require(swaps[secretHash].state == State.Filled); + require(swaps[secretHash].participant == redeemer); + require(sha256(abi.encodePacked(secret)) == secretHash); + _; + } + + modifier isNotInitiated(bytes32 secretHash) { + require(swaps[secretHash].state == State.Empty); + _; + } + + modifier hasNoNilValues(uint refundTime) { + require(msg.value > 0); + require(refundTime > 0); + _; + } + + function swap(bytes32 secretHash) + public view returns(Swap memory) + { + return swaps[secretHash]; + } + + function initiate(uint refundTimestamp, bytes32 secretHash, address participant) + public + payable + hasNoNilValues(refundTimestamp) + isNotInitiated(secretHash) + { + swaps[secretHash].initBlockNumber = block.number; + swaps[secretHash].refundBlockTimestamp = refundTimestamp; + swaps[secretHash].secretHash = secretHash; + swaps[secretHash].initiator = msg.sender; + swaps[secretHash].participant = participant; + swaps[secretHash].value = msg.value; + swaps[secretHash].state = State.Filled; + } + + function redeem(bytes32 secret, bytes32 secretHash) + public + isRedeemable(secretHash, secret, msg.sender) + { + payable(msg.sender).transfer(swaps[secretHash].value); + swaps[secretHash].state = State.Redeemed; + swaps[secretHash].secret = secret; + } + + function refund(bytes32 secretHash) + public + isRefundable(secretHash, msg.sender) + { + payable(msg.sender).transfer(swaps[secretHash].value); + swaps[secretHash].state = State.Refunded; + } +} diff --git a/server/asset/eth/contracts/README.md b/server/asset/eth/contracts/README.md new file mode 100644 index 0000000000..25882d57e3 --- /dev/null +++ b/server/asset/eth/contracts/README.md @@ -0,0 +1,13 @@ +## Eth Contract Creation + +Have `solc` and `abigen` installed on your system and run from this directory: + +`abigen --sol ETHSwapV{version}.sol --pkg eth --out ../contract.go` + +## History + +### V0 + +mainnet addres is `to be determined` + +ETHSwapV0.sol is the first interation of the the eth swap smart contract.