diff --git a/.travis.yml b/.travis.yml index 8f74195..13ee162 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,27 @@ -language: go -go: - - 1.9.x - - 1.10.x -sudo: false -install: - - go get -v github.com/golang/dep/cmd/dep - - dep ensure - - go install ./cmd/... - - go get -v github.com/alecthomas/gometalinter - - gometalinter --install -script: - - export PATH=$PATH:$HOME/gopath/bin - - ./goclean.sh +jobs: + include: + - stage: test + language: go + go: + - 1.9.x + - 1.10.x + sudo: false + install: + - go get -v github.com/golang/dep/cmd/dep + - dep ensure + - go install ./cmd/... + - go get -v github.com/alecthomas/gometalinter + - gometalinter --install + script: + - export PATH=$PATH:$HOME/gopath/bin + - ./goclean.sh + - go test -v -race ./cmd/ethatomicswap + - stage: test + language: node_js + node_js: + - "node" + install: + - npm install -g truffle + script: + - cd cmd/ethatomicswap/contract/src + - truffle test diff --git a/Gopkg.lock b/Gopkg.lock index cf60660..35b9b73 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,18 @@ ] revision = "5312a61534124124185d41f09206b9fef1d88403" +[[projects]] + branch = "master" + name = "github.com/aristanetworks/goarista" + packages = ["monotime"] + revision = "625ff285aa35926943df199ab15deba045716206" + +[[projects]] + name = "github.com/bgentry/speakeasy" + packages = ["."] + revision = "4aabc24848ce5fd31929f7d1e4ea74d3709c14cd" + version = "v0.1.0" + [[projects]] branch = "master" name = "github.com/bitgoin/lyra2rev2" @@ -136,6 +148,43 @@ ] revision = "1dc40f82683013a2111dd6bcfbd22d3dd219bc34" +[[projects]] + name = "github.com/ethereum/go-ethereum" + packages = [ + ".", + "accounts", + "accounts/abi", + "accounts/abi/bind", + "accounts/keystore", + "common", + "common/hexutil", + "common/math", + "common/mclock", + "core/types", + "crypto", + "crypto/randentropy", + "crypto/secp256k1", + "crypto/sha3", + "ethclient", + "ethdb", + "event", + "log", + "metrics", + "p2p/netutil", + "params", + "rlp", + "rpc", + "trie" + ] + revision = "37685930d953bcbe023f9bc65b135a8d8b8f1488" + version = "v1.8.12" + +[[projects]] + name = "github.com/go-stack/stack" + packages = ["."] + revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc" + version = "v1.7.0" + [[projects]] branch = "master" name = "github.com/golang/protobuf" @@ -148,6 +197,12 @@ ] revision = "bbd03ef6da3a115852eaf24c8a1c46aeb39aa175" +[[projects]] + branch = "master" + name = "github.com/golang/snappy" + packages = ["."] + revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" + [[projects]] branch = "ltcatomicswap" name = "github.com/ltcsuite/ltcd" @@ -211,6 +266,12 @@ packages = ["wallet/txrules"] revision = "970e161f293ff12ebbadae51db1f9d47f7414967" +[[projects]] + name = "github.com/pborman/uuid" + packages = ["."] + revision = "e790cca94e6cc75c7064b1332e63811d4aae1a53" + version = "v1.1" + [[projects]] branch = "master" name = "github.com/polissuite/gopolis" @@ -247,6 +308,37 @@ packages = ["."] revision = "66b5c7eba786ac6ce9ab51a24e24bb2d319b488c" +[[projects]] + name = "github.com/rjeczalik/notify" + packages = ["."] + revision = "52ae50d8490436622a8941bd70c3dbe0acdd4bbf" + version = "v0.9.0" + +[[projects]] + name = "github.com/rs/cors" + packages = ["."] + revision = "ca016a06a5753f8ba03029c0aa5e54afb1bf713f" + version = "v1.4.0" + +[[projects]] + branch = "master" + name = "github.com/syndtr/goleveldb" + packages = [ + "leveldb", + "leveldb/cache", + "leveldb/comparer", + "leveldb/errors", + "leveldb/filter", + "leveldb/iterator", + "leveldb/journal", + "leveldb/memdb", + "leveldb/opt", + "leveldb/storage", + "leveldb/table", + "leveldb/util" + ] + revision = "c4c61651e9e37fa117f53c5a906d3b63090d8445" + [[projects]] branch = "master" name = "github.com/vertcoin/vtcd" @@ -394,7 +486,8 @@ "idna", "internal/timeseries", "lex/httplex", - "trace" + "trace", + "websocket" ] revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" @@ -428,6 +521,16 @@ revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" +[[projects]] + branch = "master" + name = "golang.org/x/tools" + packages = [ + "go/ast/astutil", + "imports", + "internal/fastwalk" + ] + revision = "32950ab3be12acf6d472893021373669979907ab" + [[projects]] branch = "master" name = "google.golang.org/genproto" @@ -464,9 +567,27 @@ revision = "8e4536a86ab602859c20df5ebfd0bd4228d08655" version = "v1.10.0" +[[projects]] + name = "gopkg.in/fatih/set.v0" + packages = ["."] + revision = "57907de300222151a123d29255ed17f5ed43fad3" + version = "v0.1.0" + +[[projects]] + branch = "v2" + name = "gopkg.in/karalabe/cookiejar.v2" + packages = ["collections/prque"] + revision = "8dcd6a7f4951f6ff3ee9cbb919a06d8925822e57" + +[[projects]] + branch = "v2" + name = "gopkg.in/natefinch/npipe.v2" + packages = ["."] + revision = "c1b8fa8bdccecb0b8db834ee0b92fdbcfa606dd6" + [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "b403e3758f7b9d3985451de9323dbf395cc4ab3e22b068e87d291fec5d05ae02" + inputs-digest = "69b21e03f99e857dd0d1f4978f569bbf1c3d515d359bf9e48144d3dd14e41619" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index b598d16..ba9a98c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ exists for the following coins and wallets: * Bitcoin ([Bitcoin Core](https://github.com/bitcoin/bitcoin)) * Bitcoin Cash ([Bitcoin ABC](https://github.com/Bitcoin-ABC/bitcoin-abc), [Bitcoin Unlimited](https://github.com/BitcoinUnlimited/BitcoinUnlimited), [Bitcoin XT](https://github.com/bitcoinxt/bitcoinxt)) * Decred ([dcrwallet](https://github.com/decred/dcrwallet)) +* Ethereum ([Go Ethereum](https://github.com/ethereum/go-ethereum)) * Litecoin ([Litecoin Core](https://github.com/litecoin-project/litecoin)) * Monacoin ([Monacoin Core](https://github.com/monacoinproject/monacoin)) * Particl ([Particl Core](https://github.com/particl/particl-core)) diff --git a/cmd/ethatomicswap/contract/.gitignore b/cmd/ethatomicswap/contract/.gitignore new file mode 100644 index 0000000..a101c15 --- /dev/null +++ b/cmd/ethatomicswap/contract/.gitignore @@ -0,0 +1,2 @@ +*.abi +*.bin diff --git a/cmd/ethatomicswap/contract/atomicswap.go b/cmd/ethatomicswap/contract/atomicswap.go new file mode 100644 index 0000000..a786af6 --- /dev/null +++ b/cmd/ethatomicswap/contract/atomicswap.go @@ -0,0 +1,830 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package contract + +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" +) + +// ContractABI is the input ABI used to generate the binding from. +const ContractABI = "[{\"constant\":false,\"inputs\":[{\"name\":\"refundTime\",\"type\":\"uint256\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"name\":\"initiator\",\"type\":\"address\"}],\"name\":\"participate\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"refund\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"refundTime\",\"type\":\"uint256\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"name\":\"participant\",\"type\":\"address\"}],\"name\":\"initiate\",\"outputs\":[],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"secret\",\"type\":\"bytes32\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"redeem\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"swaps\",\"outputs\":[{\"name\":\"initTimestamp\",\"type\":\"uint256\"},{\"name\":\"refundTime\",\"type\":\"uint256\"},{\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"name\":\"secret\",\"type\":\"bytes32\"},{\"name\":\"initiator\",\"type\":\"address\"},{\"name\":\"participant\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"kind\",\"type\":\"uint8\"},{\"name\":\"state\",\"type\":\"uint8\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"refundTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"refunder\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Refunded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"redeemTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"secret\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"redeemer\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Redeemed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"initTimestamp\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"refundTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"initiator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"participant\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Participated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"initTimestamp\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"refundTime\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"initiator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"participant\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Initiated\",\"type\":\"event\"}]" + +// ContractBin is the compiled bytecode used for deploying new contracts. +const ContractBin = `608060405234801561001057600080fd5b506110ac806100206000396000f30060806040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680631aa02853146100725780637249fbb6146100c0578063ae052147146100f1578063b31597ad1461013f578063eb84e7f21461017e575b600080fd5b6100be600480360381019080803590602001909291908035600019169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919050505061027f565b005b3480156100cc57600080fd5b506100ef6004803603810190808035600019169060200190929190505050610576565b005b61013d600480360381019080803590602001909291908035600019169060200190929190803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506108bb565b005b34801561014b57600080fd5b5061017c60048036038101908080356000191690602001909291908035600019169060200190929190505050610bb2565b005b34801561018a57600080fd5b506101ad6004803603810190808035600019169060200190929190505050610fd8565b604051808a8152602001898152602001886000191660001916815260200187600019166000191681526020018673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200184815260200183600181111561024f57fe5b60ff16815260200182600381111561026357fe5b60ff168152602001995050505050505050505060405180910390f35b8260003411151561028f57600080fd5b60008111151561029e57600080fd5b82600060038111156102ac57fe5b600080836000191660001916815260200190815260200160002060070160019054906101000a900460ff1660038111156102e257fe5b1415156102ee57600080fd5b4260008086600019166000191681526020019081526020016000206000018190555084600080866000191660001916815260200190815260200160002060010181905550836000808660001916600019168152602001908152602001600020600201816000191690555082600080866000191660001916815260200190815260200160002060040160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555033600080866000191660001916815260200190815260200160002060050160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550346000808660001916600019168152602001908152602001600020600601819055506001600080866000191660001916815260200190815260200160002060070160006101000a81548160ff0219169083600181111561046c57fe5b02179055506001600080866000191660001916815260200190815260200160002060070160016101000a81548160ff021916908360038111156104ab57fe5b02179055507fe5571d467a528d7481c0e3bdd55ad528d0df6b457b07bab736c3e245c3aa16f44286868633346040518087815260200186815260200185600019166000191681526020018473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828152602001965050505050505060405180910390a15050505050565b803360006001600381111561058757fe5b600080856000191660001916815260200190815260200160002060070160019054906101000a900460ff1660038111156105bd57fe5b1415156105c957600080fd5b6001808111156105d557fe5b600080856000191660001916815260200190815260200160002060070160009054906101000a900460ff16600181111561060b57fe5b141561068d578173ffffffffffffffffffffffffffffffffffffffff16600080856000191660001916815260200190815260200160002060050160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561068857600080fd5b610705565b8173ffffffffffffffffffffffffffffffffffffffff16600080856000191660001916815260200190815260200160002060040160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561070457600080fd5b5b600080846000191660001916815260200190815260200160002060000154905060008084600019166000191681526020019081526020016000206001015481019050804211151561075557600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc6000808760001916600019168152602001908152602001600020600601549081150290604051600060405180830381858888f193505050501580156107b8573d6000803e3d6000fd5b506003600080866000191660001916815260200190815260200160002060070160016101000a81548160ff021916908360038111156107f357fe5b02179055507fadb1dca52dfad065e50a1e25c2ee47ae54013a1f2d6f8ea5abace52eb4b7a4c842600080876000191660001916815260200190815260200160002060020154336000808960001916600019168152602001908152602001600020600601546040518085815260200184600019166000191681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200194505050505060405180910390a150505050565b826000341115156108cb57600080fd5b6000811115156108da57600080fd5b82600060038111156108e857fe5b600080836000191660001916815260200190815260200160002060070160019054906101000a900460ff16600381111561091e57fe5b14151561092a57600080fd5b4260008086600019166000191681526020019081526020016000206000018190555084600080866000191660001916815260200190815260200160002060010181905550836000808660001916600019168152602001908152602001600020600201816000191690555033600080866000191660001916815260200190815260200160002060040160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555082600080866000191660001916815260200190815260200160002060050160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550346000808660001916600019168152602001908152602001600020600601819055506000806000866000191660001916815260200190815260200160002060070160006101000a81548160ff02191690836001811115610aa857fe5b02179055506001600080866000191660001916815260200190815260200160002060070160016101000a81548160ff02191690836003811115610ae757fe5b02179055507f75501a491c11746724d18ea6e5ac6a53864d886d653da6b846fdecda837cf5764286863387346040518087815260200186815260200185600019166000191681526020018473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828152602001965050505050505060405180910390a15050505050565b80823360016003811115610bc257fe5b600080856000191660001916815260200190815260200160002060070160019054906101000a900460ff166003811115610bf857fe5b141515610c0457600080fd5b600180811115610c1057fe5b600080856000191660001916815260200190815260200160002060070160009054906101000a900460ff166001811115610c4657fe5b1415610cc8578073ffffffffffffffffffffffffffffffffffffffff16600080856000191660001916815260200190815260200160002060040160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515610cc357600080fd5b610d40565b8073ffffffffffffffffffffffffffffffffffffffff16600080856000191660001916815260200190815260200160002060050160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515610d3f57600080fd5b5b82600019166002836040516020018082600019166000191681526020019150506040516020818303038152906040526040518082805190602001908083835b602083101515610da45780518252602082019150602081019050602083039250610d7f565b6001836020036101000a0380198251168184511680821785525050505050509050019150506020604051808303816000865af1158015610de8573d6000803e3d6000fd5b5050506040513d6020811015610dfd57600080fd5b810190808051906020019092919050505060001916141515610e1e57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc6000808760001916600019168152602001908152602001600020600601549081150290604051600060405180830381858888f19350505050158015610e81573d6000803e3d6000fd5b506002600080866000191660001916815260200190815260200160002060070160016101000a81548160ff02191690836003811115610ebc57fe5b021790555084600080866000191660001916815260200190815260200160002060030181600019169055507fe4da013d8c42cdfa76ab1d5c08edcdc1503d2da88d7accc854f0e57ebe45c59142600080876000191660001916815260200190815260200160002060020154600080886000191660001916815260200190815260200160002060030154336000808a600019166000191681526020019081526020016000206006015460405180868152602001856000191660001916815260200184600019166000191681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019550505050505060405180910390a15050505050565b60006020528060005260406000206000915090508060000154908060010154908060020154908060030154908060040160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060050160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060060154908060070160009054906101000a900460ff16908060070160019054906101000a900460ff169050895600a165627a7a72305820081a82269020bc584dfffd0f5cad637d66d08ba4234a58c31e0dc6329fbe966b0029` + +// DeployContract deploys a new Ethereum contract, binding an instance of Contract to it. +func DeployContract(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Contract, error) { + parsed, err := abi.JSON(strings.NewReader(ContractABI)) + if err != nil { + return common.Address{}, nil, nil, err + } + address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(ContractBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Contract{ContractCaller: ContractCaller{contract: contract}, ContractTransactor: ContractTransactor{contract: contract}, ContractFilterer: ContractFilterer{contract: contract}}, nil +} + +// Contract is an auto generated Go binding around an Ethereum contract. +type Contract struct { + ContractCaller // Read-only binding to the contract + ContractTransactor // Write-only binding to the contract + ContractFilterer // Log filterer for contract events +} + +// ContractCaller is an auto generated read-only Go binding around an Ethereum contract. +type ContractCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ContractTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ContractTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ContractFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ContractFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ContractSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ContractSession struct { + Contract *Contract // 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 +} + +// ContractCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ContractCallerSession struct { + Contract *ContractCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ContractTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ContractTransactorSession struct { + Contract *ContractTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ContractRaw is an auto generated low-level Go binding around an Ethereum contract. +type ContractRaw struct { + Contract *Contract // Generic contract binding to access the raw methods on +} + +// ContractCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ContractCallerRaw struct { + Contract *ContractCaller // Generic read-only contract binding to access the raw methods on +} + +// ContractTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ContractTransactorRaw struct { + Contract *ContractTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewContract creates a new instance of Contract, bound to a specific deployed contract. +func NewContract(address common.Address, backend bind.ContractBackend) (*Contract, error) { + contract, err := bindContract(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Contract{ContractCaller: ContractCaller{contract: contract}, ContractTransactor: ContractTransactor{contract: contract}, ContractFilterer: ContractFilterer{contract: contract}}, nil +} + +// NewContractCaller creates a new read-only instance of Contract, bound to a specific deployed contract. +func NewContractCaller(address common.Address, caller bind.ContractCaller) (*ContractCaller, error) { + contract, err := bindContract(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ContractCaller{contract: contract}, nil +} + +// NewContractTransactor creates a new write-only instance of Contract, bound to a specific deployed contract. +func NewContractTransactor(address common.Address, transactor bind.ContractTransactor) (*ContractTransactor, error) { + contract, err := bindContract(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ContractTransactor{contract: contract}, nil +} + +// NewContractFilterer creates a new log filterer instance of Contract, bound to a specific deployed contract. +func NewContractFilterer(address common.Address, filterer bind.ContractFilterer) (*ContractFilterer, error) { + contract, err := bindContract(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ContractFilterer{contract: contract}, nil +} + +// bindContract binds a generic wrapper to an already deployed contract. +func bindContract(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(ContractABI)) + 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 (_Contract *ContractRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { + return _Contract.Contract.ContractCaller.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 (_Contract *ContractRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Contract.Contract.ContractTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Contract *ContractRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Contract.Contract.ContractTransactor.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 (_Contract *ContractCallerRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { + return _Contract.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 (_Contract *ContractTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Contract.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Contract *ContractTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Contract.Contract.contract.Transact(opts, method, params...) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps( bytes32) constant returns(initTimestamp uint256, refundTime uint256, secretHash bytes32, secret bytes32, initiator address, participant address, value uint256, kind uint8, state uint8) +func (_Contract *ContractCaller) Swaps(opts *bind.CallOpts, arg0 [32]byte) (struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 +}, error) { + ret := new(struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 + }) + out := ret + err := _Contract.contract.Call(opts, out, "swaps", arg0) + return *ret, err +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps( bytes32) constant returns(initTimestamp uint256, refundTime uint256, secretHash bytes32, secret bytes32, initiator address, participant address, value uint256, kind uint8, state uint8) +func (_Contract *ContractSession) Swaps(arg0 [32]byte) (struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 +}, error) { + return _Contract.Contract.Swaps(&_Contract.CallOpts, arg0) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps( bytes32) constant returns(initTimestamp uint256, refundTime uint256, secretHash bytes32, secret bytes32, initiator address, participant address, value uint256, kind uint8, state uint8) +func (_Contract *ContractCallerSession) Swaps(arg0 [32]byte) (struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 +}, error) { + return _Contract.Contract.Swaps(&_Contract.CallOpts, arg0) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(refundTime uint256, secretHash bytes32, participant address) returns() +func (_Contract *ContractTransactor) Initiate(opts *bind.TransactOpts, refundTime *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _Contract.contract.Transact(opts, "initiate", refundTime, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(refundTime uint256, secretHash bytes32, participant address) returns() +func (_Contract *ContractSession) Initiate(refundTime *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _Contract.Contract.Initiate(&_Contract.TransactOpts, refundTime, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(refundTime uint256, secretHash bytes32, participant address) returns() +func (_Contract *ContractTransactorSession) Initiate(refundTime *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _Contract.Contract.Initiate(&_Contract.TransactOpts, refundTime, secretHash, participant) +} + +// Participate is a paid mutator transaction binding the contract method 0x1aa02853. +// +// Solidity: function participate(refundTime uint256, secretHash bytes32, initiator address) returns() +func (_Contract *ContractTransactor) Participate(opts *bind.TransactOpts, refundTime *big.Int, secretHash [32]byte, initiator common.Address) (*types.Transaction, error) { + return _Contract.contract.Transact(opts, "participate", refundTime, secretHash, initiator) +} + +// Participate is a paid mutator transaction binding the contract method 0x1aa02853. +// +// Solidity: function participate(refundTime uint256, secretHash bytes32, initiator address) returns() +func (_Contract *ContractSession) Participate(refundTime *big.Int, secretHash [32]byte, initiator common.Address) (*types.Transaction, error) { + return _Contract.Contract.Participate(&_Contract.TransactOpts, refundTime, secretHash, initiator) +} + +// Participate is a paid mutator transaction binding the contract method 0x1aa02853. +// +// Solidity: function participate(refundTime uint256, secretHash bytes32, initiator address) returns() +func (_Contract *ContractTransactorSession) Participate(refundTime *big.Int, secretHash [32]byte, initiator common.Address) (*types.Transaction, error) { + return _Contract.Contract.Participate(&_Contract.TransactOpts, refundTime, secretHash, initiator) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(secret bytes32, secretHash bytes32) returns() +func (_Contract *ContractTransactor) Redeem(opts *bind.TransactOpts, secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _Contract.contract.Transact(opts, "redeem", secret, secretHash) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(secret bytes32, secretHash bytes32) returns() +func (_Contract *ContractSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _Contract.Contract.Redeem(&_Contract.TransactOpts, secret, secretHash) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(secret bytes32, secretHash bytes32) returns() +func (_Contract *ContractTransactorSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _Contract.Contract.Redeem(&_Contract.TransactOpts, secret, secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(secretHash bytes32) returns() +func (_Contract *ContractTransactor) Refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { + return _Contract.contract.Transact(opts, "refund", secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(secretHash bytes32) returns() +func (_Contract *ContractSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _Contract.Contract.Refund(&_Contract.TransactOpts, secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(secretHash bytes32) returns() +func (_Contract *ContractTransactorSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _Contract.Contract.Refund(&_Contract.TransactOpts, secretHash) +} + +// ContractInitiatedIterator is returned from FilterInitiated and is used to iterate over the raw logs and unpacked data for Initiated events raised by the Contract contract. +type ContractInitiatedIterator struct { + Event *ContractInitiated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ContractInitiatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ContractInitiated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ContractInitiated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ContractInitiatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ContractInitiatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ContractInitiated represents a Initiated event raised by the Contract contract. +type ContractInitiated struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInitiated is a free log retrieval operation binding the contract event 0x75501a491c11746724d18ea6e5ac6a53864d886d653da6b846fdecda837cf576. +// +// Solidity: e Initiated(initTimestamp uint256, refundTime uint256, secretHash bytes32, initiator address, participant address, value uint256) +func (_Contract *ContractFilterer) FilterInitiated(opts *bind.FilterOpts) (*ContractInitiatedIterator, error) { + + logs, sub, err := _Contract.contract.FilterLogs(opts, "Initiated") + if err != nil { + return nil, err + } + return &ContractInitiatedIterator{contract: _Contract.contract, event: "Initiated", logs: logs, sub: sub}, nil +} + +// WatchInitiated is a free log subscription operation binding the contract event 0x75501a491c11746724d18ea6e5ac6a53864d886d653da6b846fdecda837cf576. +// +// Solidity: e Initiated(initTimestamp uint256, refundTime uint256, secretHash bytes32, initiator address, participant address, value uint256) +func (_Contract *ContractFilterer) WatchInitiated(opts *bind.WatchOpts, sink chan<- *ContractInitiated) (event.Subscription, error) { + + logs, sub, err := _Contract.contract.WatchLogs(opts, "Initiated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ContractInitiated) + if err := _Contract.contract.UnpackLog(event, "Initiated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ContractParticipatedIterator is returned from FilterParticipated and is used to iterate over the raw logs and unpacked data for Participated events raised by the Contract contract. +type ContractParticipatedIterator struct { + Event *ContractParticipated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ContractParticipatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ContractParticipated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ContractParticipated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ContractParticipatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ContractParticipatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ContractParticipated represents a Participated event raised by the Contract contract. +type ContractParticipated struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterParticipated is a free log retrieval operation binding the contract event 0xe5571d467a528d7481c0e3bdd55ad528d0df6b457b07bab736c3e245c3aa16f4. +// +// Solidity: e Participated(initTimestamp uint256, refundTime uint256, secretHash bytes32, initiator address, participant address, value uint256) +func (_Contract *ContractFilterer) FilterParticipated(opts *bind.FilterOpts) (*ContractParticipatedIterator, error) { + + logs, sub, err := _Contract.contract.FilterLogs(opts, "Participated") + if err != nil { + return nil, err + } + return &ContractParticipatedIterator{contract: _Contract.contract, event: "Participated", logs: logs, sub: sub}, nil +} + +// WatchParticipated is a free log subscription operation binding the contract event 0xe5571d467a528d7481c0e3bdd55ad528d0df6b457b07bab736c3e245c3aa16f4. +// +// Solidity: e Participated(initTimestamp uint256, refundTime uint256, secretHash bytes32, initiator address, participant address, value uint256) +func (_Contract *ContractFilterer) WatchParticipated(opts *bind.WatchOpts, sink chan<- *ContractParticipated) (event.Subscription, error) { + + logs, sub, err := _Contract.contract.WatchLogs(opts, "Participated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ContractParticipated) + if err := _Contract.contract.UnpackLog(event, "Participated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ContractRedeemedIterator is returned from FilterRedeemed and is used to iterate over the raw logs and unpacked data for Redeemed events raised by the Contract contract. +type ContractRedeemedIterator struct { + Event *ContractRedeemed // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ContractRedeemedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ContractRedeemed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ContractRedeemed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ContractRedeemedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ContractRedeemedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ContractRedeemed represents a Redeemed event raised by the Contract contract. +type ContractRedeemed struct { + RedeemTime *big.Int + SecretHash [32]byte + Secret [32]byte + Redeemer common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRedeemed is a free log retrieval operation binding the contract event 0xe4da013d8c42cdfa76ab1d5c08edcdc1503d2da88d7accc854f0e57ebe45c591. +// +// Solidity: e Redeemed(redeemTime uint256, secretHash bytes32, secret bytes32, redeemer address, value uint256) +func (_Contract *ContractFilterer) FilterRedeemed(opts *bind.FilterOpts) (*ContractRedeemedIterator, error) { + + logs, sub, err := _Contract.contract.FilterLogs(opts, "Redeemed") + if err != nil { + return nil, err + } + return &ContractRedeemedIterator{contract: _Contract.contract, event: "Redeemed", logs: logs, sub: sub}, nil +} + +// WatchRedeemed is a free log subscription operation binding the contract event 0xe4da013d8c42cdfa76ab1d5c08edcdc1503d2da88d7accc854f0e57ebe45c591. +// +// Solidity: e Redeemed(redeemTime uint256, secretHash bytes32, secret bytes32, redeemer address, value uint256) +func (_Contract *ContractFilterer) WatchRedeemed(opts *bind.WatchOpts, sink chan<- *ContractRedeemed) (event.Subscription, error) { + + logs, sub, err := _Contract.contract.WatchLogs(opts, "Redeemed") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ContractRedeemed) + if err := _Contract.contract.UnpackLog(event, "Redeemed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ContractRefundedIterator is returned from FilterRefunded and is used to iterate over the raw logs and unpacked data for Refunded events raised by the Contract contract. +type ContractRefundedIterator struct { + Event *ContractRefunded // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *ContractRefundedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(ContractRefunded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(ContractRefunded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *ContractRefundedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *ContractRefundedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// ContractRefunded represents a Refunded event raised by the Contract contract. +type ContractRefunded struct { + RefundTime *big.Int + SecretHash [32]byte + Refunder common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRefunded is a free log retrieval operation binding the contract event 0xadb1dca52dfad065e50a1e25c2ee47ae54013a1f2d6f8ea5abace52eb4b7a4c8. +// +// Solidity: e Refunded(refundTime uint256, secretHash bytes32, refunder address, value uint256) +func (_Contract *ContractFilterer) FilterRefunded(opts *bind.FilterOpts) (*ContractRefundedIterator, error) { + + logs, sub, err := _Contract.contract.FilterLogs(opts, "Refunded") + if err != nil { + return nil, err + } + return &ContractRefundedIterator{contract: _Contract.contract, event: "Refunded", logs: logs, sub: sub}, nil +} + +// WatchRefunded is a free log subscription operation binding the contract event 0xadb1dca52dfad065e50a1e25c2ee47ae54013a1f2d6f8ea5abace52eb4b7a4c8. +// +// Solidity: e Refunded(refundTime uint256, secretHash bytes32, refunder address, value uint256) +func (_Contract *ContractFilterer) WatchRefunded(opts *bind.WatchOpts, sink chan<- *ContractRefunded) (event.Subscription, error) { + + logs, sub, err := _Contract.contract.WatchLogs(opts, "Refunded") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(ContractRefunded) + if err := _Contract.contract.UnpackLog(event, "Refunded", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} diff --git a/cmd/ethatomicswap/contract/generate.go b/cmd/ethatomicswap/contract/generate.go new file mode 100644 index 0000000..06d3af4 --- /dev/null +++ b/cmd/ethatomicswap/contract/generate.go @@ -0,0 +1,16 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + +package contract + +// prerequisite: install ethereum devtools +// +// go get -u github.com/ethereum/go-ethereum +// cd $GOPATH/src/github.com/ethereum/go-ethereum/ +// make +// make devtools + +//go:generate sh -c "solc --abi src/contracts/AtomicSwap.sol | awk '/JSON ABI/{x=1;next}x' > AtomicSwap.abi" +//go:generate sh -c "solc --bin src/contracts/AtomicSwap.sol | awk '/Binary:/{x=1;next}x' > AtomicSwap.bin" +//go:generate abigen --bin=AtomicSwap.bin --abi=AtomicSwap.abi --pkg=contract --out=atomicswap.go diff --git a/cmd/ethatomicswap/contract/src/.gitignore b/cmd/ethatomicswap/contract/src/.gitignore new file mode 100644 index 0000000..d163863 --- /dev/null +++ b/cmd/ethatomicswap/contract/src/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/cmd/ethatomicswap/contract/src/README.md b/cmd/ethatomicswap/contract/src/README.md new file mode 100644 index 0000000..5dafea4 --- /dev/null +++ b/cmd/ethatomicswap/contract/src/README.md @@ -0,0 +1,30 @@ +# AtomicSwap Smart Contract for the EVM + +In this directory you can find the smart contract, written in Solidity, +to be used together with the `ethatomicswap` tool. + +## WARNING + +This contract has only recently been developed, and has not received any external audits yet. Please use common sense when doing anything that deals with real money! We take no responsibility for any security problem you might experience while using this contract. + +## Test + +You can test the AtomicSwap smart contract, +found as [/cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol](/cmd/ethatomicswap/solidity/contracts/AtomicSwap.sol) using a single command. It has however following prerequisites: + +* Install NodeJS (10.5.0), which bundles _npm_ as well; +* Install truffle: `npm install -g truffle`; + +Optionally you can also install and run +Ganache ( ). + +Once you have fulfilled all prerequisites listed above, +you can run the unit tests provided with the AtomicSwap contract, using: + +``` +truffle test +``` + +## Deploy + +// TODO diff --git a/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol b/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol new file mode 100644 index 0000000..a90a181 --- /dev/null +++ b/cmd/ethatomicswap/contract/src/contracts/AtomicSwap.sol @@ -0,0 +1,187 @@ +// Copyright (c) 2017 Altcoin Exchange, Inc + +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + +pragma solidity ^0.4.23; + +// Notes on security warnings: +// + block.timestamp is safe to use, +// given that our timestamp can tolerate a 30-second drift in time; + +contract AtomicSwap { + enum Kind { Initiator, Participant } + enum State { Empty, Filled, Redeemed, Refunded } + + struct Swap { + uint initTimestamp; + uint refundTime; + bytes32 secretHash; + bytes32 secret; + address initiator; + address participant; + uint256 value; + Kind kind; + State state; + } + + mapping(bytes32 => Swap) public swaps; + + event Refunded( + uint refundTime, + bytes32 secretHash, + address refunder, + uint256 value + ); + + event Redeemed( + uint redeemTime, + bytes32 secretHash, + bytes32 secret, + address redeemer, + uint256 value + ); + + event Participated( + uint initTimestamp, + uint refundTime, + bytes32 secretHash, + address initiator, + address participant, + uint256 value + ); + + event Initiated( + uint initTimestamp, + uint refundTime, + bytes32 secretHash, + address initiator, + address participant, + uint256 value + ); + + constructor() public {} + + modifier isRefundable(bytes32 secretHash, address refunder) { + require(swaps[secretHash].state == State.Filled); + if (swaps[secretHash].kind == Kind.Participant) { + require(swaps[secretHash].participant == refunder); + } else { + require(swaps[secretHash].initiator == refunder); + } + uint preRefundTimestamp = swaps[secretHash].initTimestamp; + preRefundTimestamp += swaps[secretHash].refundTime; + require(block.timestamp > preRefundTimestamp); + _; + } + + modifier isRedeemable(bytes32 secretHash, bytes32 secret, address redeemer) { + require(swaps[secretHash].state == State.Filled); + if (swaps[secretHash].kind == Kind.Participant) { + require(swaps[secretHash].initiator == redeemer); + } else { + require(swaps[secretHash].participant == redeemer); + } + require(sha256(abi.encodePacked(secret)) == secretHash); + _; + } + + modifier isInitiator(bytes32 secretHash) { + require(msg.sender == swaps[secretHash].initiator); + _; + } + + modifier isNotInitiated(bytes32 secretHash) { + require(swaps[secretHash].state == State.Empty); + _; + } + + modifier hasNoNilValues(uint refundTime) { + require(msg.value > 0); + require(refundTime > 0); + _; + } + + function initiate(uint refundTime, bytes32 secretHash, address participant) + public + payable + hasNoNilValues(refundTime) + isNotInitiated(secretHash) + { + swaps[secretHash].initTimestamp = block.timestamp; + swaps[secretHash].refundTime = refundTime; + swaps[secretHash].secretHash = secretHash; + swaps[secretHash].initiator = msg.sender; + swaps[secretHash].participant = participant; + swaps[secretHash].value = msg.value; + swaps[secretHash].kind = Kind.Initiator; + swaps[secretHash].state = State.Filled; + emit Initiated( + block.timestamp, + refundTime, + secretHash, + msg.sender, + participant, + msg.value + ); + } + + function participate(uint refundTime, bytes32 secretHash, address initiator) + public + payable + hasNoNilValues(refundTime) + isNotInitiated(secretHash) + { + swaps[secretHash].initTimestamp = block.timestamp; + swaps[secretHash].refundTime = refundTime; + swaps[secretHash].secretHash = secretHash; + swaps[secretHash].initiator = initiator; + swaps[secretHash].participant = msg.sender; + swaps[secretHash].value = msg.value; + swaps[secretHash].kind = Kind.Participant; + swaps[secretHash].state = State.Filled; + emit Participated( + block.timestamp, + refundTime, + secretHash, + initiator, + msg.sender, + msg.value + ); + } + + function redeem(bytes32 secret, bytes32 secretHash) + public + isRedeemable(secretHash, secret, msg.sender) + { + msg.sender.transfer(swaps[secretHash].value); + + swaps[secretHash].state = State.Redeemed; + swaps[secretHash].secret = secret; + + emit Redeemed( + block.timestamp, + swaps[secretHash].secretHash, + swaps[secretHash].secret, + msg.sender, + swaps[secretHash].value + ); + } + + function refund(bytes32 secretHash) + public + isRefundable(secretHash, msg.sender) + { + msg.sender.transfer(swaps[secretHash].value); + + swaps[secretHash].state = State.Refunded; + + emit Refunded( + block.timestamp, + swaps[secretHash].secretHash, + msg.sender, + swaps[secretHash].value + ); + } +} diff --git a/cmd/ethatomicswap/contract/src/contracts/Migrations.sol b/cmd/ethatomicswap/contract/src/contracts/Migrations.sol new file mode 100644 index 0000000..97c708c --- /dev/null +++ b/cmd/ethatomicswap/contract/src/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.4.23; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + constructor() public { + owner = msg.sender; + } + + modifier restricted() { + if (msg.sender == owner) _; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) public restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/cmd/ethatomicswap/contract/src/migrations/1_initial_migration.js b/cmd/ethatomicswap/contract/src/migrations/1_initial_migration.js new file mode 100644 index 0000000..4d5f3f9 --- /dev/null +++ b/cmd/ethatomicswap/contract/src/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +var Migrations = artifacts.require("./Migrations.sol"); + +module.exports = function(deployer) { + deployer.deploy(Migrations); +}; diff --git a/cmd/ethatomicswap/contract/src/migrations/2_atomicswap_migration.js b/cmd/ethatomicswap/contract/src/migrations/2_atomicswap_migration.js new file mode 100644 index 0000000..9854c67 --- /dev/null +++ b/cmd/ethatomicswap/contract/src/migrations/2_atomicswap_migration.js @@ -0,0 +1,10 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + +var AtomicSwap = artifacts.require("AtomicSwap"); + +module.exports = function(deployer) { + // deployment steps + deployer.deploy(AtomicSwap); +}; \ No newline at end of file diff --git a/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js b/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js new file mode 100644 index 0000000..d7fcea2 --- /dev/null +++ b/cmd/ethatomicswap/contract/src/test/TestAtomicSwap.js @@ -0,0 +1,857 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + +const AtomicSwap = artifacts.require("AtomicSwap"); + +contract("AtomicSwap tests", accounts => { + let tryCatch = require("./exceptions.js").tryCatch; + let errTypes = require("./exceptions.js").errTypes; + let utils = require("./utils.js"); + + // Solidity Enums aren't exported, + // a manual integer mapping is therefore required + const [kindInitiator, kindParticipant] = [0, 1]; + const [stateEmpty, stateFilled, stateRedeemed, stateRefunded] = [0, 1, 2, 3]; + + // contractAccount is used only to deploy the contracts, + // firstAccount and secondAccount are used for valid and invalid transfers, + // and thridAccount and fourthAccount are used only for invalid transfers + const [contractAccount, firstAccount, secondAccount, thirdAccount, fourthAccount] = accounts; + + // atomicSwap gets assigned, before each unit test, + // the instance of a newly deployed AtomicSwap smart contract + let atomicSwap; + + // define constant hash+secretHash + const secret = "0x64f1ddd4cc83a3aaf37a7f290ec922dc764de023acdd11bf76c24378b086a017"; + const secretHash = "0xd4ebb2bf3e7898c18f6fe07d8eb8e7084e0bae52ae44a42ca6cdba240f58549f"; + // wrong hash+secretHash + const wrongSecretHash = "0xe3b25a963d024e7788d97ae1030bdb279731edb190f25d4aa5d38c400e08634e"; + const wrongSecret = "0x686f661e0c2f7678d2751db8662cc56cb9b6a7bdfd0524f0a841006c244cfc37"; + // empty secret + const emptySecret = "0x0000000000000000000000000000000000000000000000000000000000000000"; + + beforeEach(async () => { + atomicSwap = await AtomicSwap.new(); + + console.log("balance of accounts before test:") + console.log(" * balance of account #1: " + web3.eth.getBalance(firstAccount).toString()); + console.log(" * balance of account #2: " + web3.eth.getBalance(secondAccount).toString()); + console.log(" * balance of account #3: " + web3.eth.getBalance(thirdAccount).toString()); + console.log(" * balance of account #4: " + web3.eth.getBalance(fourthAccount).toString()); + }); + + afterEach(async () => { + console.log("balance of accounts after test:") + console.log(" * balance of account #1: " + web3.eth.getBalance(firstAccount).toString()); + console.log(" * balance of account #2: " + web3.eth.getBalance(secondAccount).toString()); + console.log(" * balance of account #3: " + web3.eth.getBalance(thirdAccount).toString()); + console.log(" * balance of account #4: " + web3.eth.getBalance(fourthAccount).toString()); + }); + + it("should be able to redeem a participation contract", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 60; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create participation contract + await atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Participated", "Expected Participated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.initiator, secondAccount, "Initiator should equal secondAccount"); + assert.equal(firstLog.args.participant, firstAccount, "Participant should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // assert all contract details + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateFilled, "state should equal Filled"); + + // creating another contract using the same secretHash should fail + await tryCatch(atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + // even when using different accounts + await tryCatch(atomicSwap.participate(refundTime, secretHash, fourthAccount, + {from: thirdAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + // and even when trying to create an initiation contract, instead of an participation contract + await tryCatch(atomicSwap.initiate(refundTime, secretHash, fourthAccount, + {from: thirdAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + + // only the initiator can refund a contract + await tryCatch(atomicSwap.refund(secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + // but even the initiator cannot refund, given the refundTime has not yet been reached + await tryCatch(atomicSwap.refund(secretHash, + {from: firstAccount, gasPrice: 0}), errTypes.revert); + + // only the the participant can redeem a contract + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: firstAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + // the participant has to give however give the correct secret hash + await tryCatch(atomicSwap.redeem(secret, wrongSecretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + // and the correct secret + await tryCatch(atomicSwap.redeem(wrongSecret, secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + // in fact, the secretHash has to be the correct one and the secretHash has to equal sha256(secret) + await tryCatch(atomicSwap.redeem(wrongSecret, wrongSecretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + + // redeem the participation contract as the the participant + await atomicSwap.redeem(secret, secretHash, {from: secondAccount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Redeemed", "Expected Redeemed event"); + assert.isAtLeast(firstLog.args.redeemTime.toNumber(), initTimestamp, + "redeem time " + firstLog.args.redeemTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.secret, secret, "secret should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.redeemer, secondAccount, "redeemer should equal secondAccount"); + }); + + // ensure balance updates of second account + balanceSecondAccount = web3.eth.getBalance(secondAccount); + expectedBalanceSecondAccount = expectedBalanceSecondAccount.add(contractAmount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should have decreased by txn cost, " + + "and should have received contract amount"); + + // assert state has now been updated, + // and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, secret, "secret should no longer be nil and instead be as expected"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateRedeemed, "state should equal Redeemed"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to redeem a participation contract even when refunding is already possible", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 1; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create participation contract + await atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Participated", "Expected Participated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.initiator, secondAccount, "Initiator should equal secondAccount"); + assert.equal(firstLog.args.participant, firstAccount, "Participant should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // sleep so that the refund-period gets reached + await utils.sleep(refundTime * 2000); + + // redeem the participation contract as the the participant + await atomicSwap.redeem(secret, secretHash, {from: secondAccount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Redeemed", "Expected Redeemed event"); + assert.isAtLeast(firstLog.args.redeemTime.toNumber(), initTimestamp, + "redeem time " + firstLog.args.redeemTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.secret, secret, "secret should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.redeemer, secondAccount, "redeemer should equal secondAccount"); + }); + + // ensure balance updates of second account + balanceSecondAccount = web3.eth.getBalance(secondAccount); + expectedBalanceSecondAccount = expectedBalanceSecondAccount.add(contractAmount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should have decreased by txn cost, " + + "and should have received contract amount"); + + // assert state and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, secret, "secret should no longer be nil and instead be as expected"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateRedeemed, "state should equal Redeemed"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to redeem an initiation contract", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 60; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create initiation contract + await atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Initiated", "Expected Initiated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.participant, secondAccount, "Participant should equal secondAccount"); + assert.equal(firstLog.args.initiator, firstAccount, "Initiator should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // assert all contract details + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateFilled, "state should equal Filled"); + + // creating another contract using the same secretHash should fail + await tryCatch(atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + // even when using different accounts + await tryCatch(atomicSwap.initiate(refundTime, secretHash, fourthAccount, + {from: thirdAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + // and even when trying to create a participation contract, instead of an initiation contract + await tryCatch(atomicSwap.participate(refundTime, secretHash, fourthAccount, + {from: thirdAccount, value: contractAmount, gasPrice: 0}), errTypes.revert); + + // only the participant can refund a contract + await tryCatch(atomicSwap.refund(secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + // but even the participant cannot refund, given the refundTime has not yet been reached + await tryCatch(atomicSwap.refund(secretHash, + {from: firstAccount, gasPrice: 0}), errTypes.revert); + + // only the the initiator can redeem a contract + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: firstAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.redeem(secret, secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + // the initiator has to give however give the correct secret hash + await tryCatch(atomicSwap.redeem(secret, wrongSecretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + // and the correct secret + await tryCatch(atomicSwap.redeem(wrongSecret, secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + // in fact, the secretHash has to be the correct one and the secretHash has to equal sha256(secret) + await tryCatch(atomicSwap.redeem(wrongSecret, wrongSecretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + + // redeem the initiation contract + await atomicSwap.redeem(secret, secretHash, {from: secondAccount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Redeemed", "Expected Redeemed event"); + assert.isAtLeast(firstLog.args.redeemTime.toNumber(), initTimestamp, + "redeem time " + firstLog.args.redeemTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.secret, secret, "secret should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.redeemer, secondAccount, "redeemer should equal secondAccount"); + }); + + // ensure balance updates of second account + balanceSecondAccount = web3.eth.getBalance(secondAccount); + expectedBalanceSecondAccount = expectedBalanceSecondAccount.add(contractAmount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should have decreased by txn cost, " + + "and should have received contract amount"); + + // assert state has now been updated, + // and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, secret, "secret should no longer be nil and instead be as expected"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateRedeemed, "state should equal Redeemed"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to redeem an initiation contract even when refunding is already possible", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 1; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create initiation contract + await atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Initiated", "Expected Initiated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.participant, secondAccount, "Participant should equal secondAccount"); + assert.equal(firstLog.args.initiator, firstAccount, "Initiator should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // sleep so that the refund-period gets reached + await utils.sleep(refundTime * 2000); + + // redeem the initiation contract + await atomicSwap.redeem(secret, secretHash, {from: secondAccount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Redeemed", "Expected Redeemed event"); + assert.isAtLeast(firstLog.args.redeemTime.toNumber(), initTimestamp, + "redeem time " + firstLog.args.redeemTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.secret, secret, "secret should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.redeemer, secondAccount, "redeemer should equal secondAccount"); + }); + + // ensure balance updates of second account + balanceSecondAccount = web3.eth.getBalance(secondAccount); + expectedBalanceSecondAccount = expectedBalanceSecondAccount.add(contractAmount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should have decreased by txn cost, " + + "and should have received contract amount"); + + // assert state and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, secret, "secret should no longer be nil and instead be as expected"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateRedeemed, "state should equal Redeemed"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to refund a participation contract", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 1; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create participation contract + await atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Participated", "Expected Participated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.initiator, secondAccount, "Initiator should equal secondAccount"); + assert.equal(firstLog.args.participant, firstAccount, "Participant should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // assert all contract details + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateFilled, "state should equal Filled"); + + await utils.sleep(refundTime * 2000); + + // only the participant can refund a contract + await tryCatch(atomicSwap.refund(secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + + atomicSwap.refund(secretHash, {from: firstAccount, gasPrice: 0}).then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Refunded", "Expected Refunded event"); + assert.isAtLeast(firstLog.args.refundTime.toNumber(), initTimestamp, + "refund time " + firstLog.args.refundTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.refunder, firstAccount, "refunder should equal firstAccount"); + }); + + await utils.sleep(1000); // refund balance updates seem to take longer for some reason + + // ensure balance updates of first account + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should have decreased by txn cost, " + + "and should have received contract amount back"); + + // assert state has now been updated, + // and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, secondAccount, "initiator should equal secondAccount"); + assert.equal(contractParticipant, firstAccount, "participant should equal firstAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindParticipant, "kind should equal Participant"); + assert.equal(contractState, stateRefunded, "state should equal Refunded"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("should be able to refund an initiation contract", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + const refundTime = 1; + + let initTimestamp; + + // store initial balance of our accounts, + // so that we can check if the locked value + // transfers indeed between accounts + let balanceFirstAccount = web3.eth.getBalance(firstAccount); + let balanceSecondAccount = web3.eth.getBalance(secondAccount); + let expectedBalanceFirstAccount = balanceFirstAccount; + let expectedBalanceSecondAccount = balanceSecondAccount; + + // ensure our contract does not exist yet + var [,,,,,,,,contractState] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractState, stateEmpty, "state should equal Empty"); + + // sanity balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // create initiation contract + await atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}). + then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Initiated", "Expected Initiated event"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "Value should equal contractAmount"); + assert.equal(firstLog.args.secretHash, secretHash, "SecretHash should be as expected"); + assert.equal(firstLog.args.refundTime, refundTime, "RefundTime should be as expected"); + assert.equal(firstLog.args.initiator, firstAccount, "Initiator should equal secondAccount"); + assert.equal(firstLog.args.participant, secondAccount, "Participant should equal firstAccount"); + initTimestamp = firstLog.args.initTimestamp.toNumber(); + }); + + // update balance and check it again + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount.negated()); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + + // assert all contract details + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateFilled, "state should equal Filled"); + + await utils.sleep(refundTime * 2000); + + // only the initiator can refund a contract + await tryCatch(atomicSwap.refund(secretHash, + {from: secondAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: thirdAccount, gasPrice: 0}), errTypes.revert); + await tryCatch(atomicSwap.refund(secretHash, + {from: fourthAccount, gasPrice: 0}), errTypes.revert); + atomicSwap.refund(secretHash, {from: firstAccount, gasPrice: 0}).then(result => { + const firstLog = result.logs[0]; + assert.equal(firstLog.event, "Refunded", "Expected Refunded event"); + assert.isAtLeast(firstLog.args.refundTime.toNumber(), initTimestamp, + "refund time " + firstLog.args.refundTime + + " should be atleast equal to the init timestamp " + + initTimestamp.toString()); + assert.equal(firstLog.args.secretHash, secretHash, "secretHash should be as expected"); + assert.equal(firstLog.args.value.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(firstLog.args.refunder, firstAccount, "refunder should equal firstAccount"); + }); + + await utils.sleep(1000); // refund balance updates seem to take longer for some reason + + // ensure balance updates of first account + balanceFirstAccount = web3.eth.getBalance(firstAccount); + expectedBalanceFirstAccount = expectedBalanceFirstAccount.add(contractAmount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should have decreased by txn cost, " + + "and should have received contract amount back"); + + // assert state has now been updated, + // and that our contract still exists + var [ + contractTime, + contractRefundTime, + contractSecretHash, + contractSecret, + contractInitiator, + contractParticipant, + contractValue, + contractKind, + contractState, + ] = await atomicSwap.swaps(secretHash, {gasPrice: 0}); + assert.equal(contractSecretHash, secretHash, "secretHash should be as expected"); + assert.equal(contractSecret, emptySecret, "secret should still be nil"); + assert.equal(contractInitiator, firstAccount, "initiator should equal firstAccount"); + assert.equal(contractParticipant, secondAccount, "participant should equal secondAccount"); + assert.equal(contractValue.toString(), contractAmount.toString(), "value should equal contractAmount"); + assert.equal(contractKind, kindInitiator, "kind should equal Initiator"); + assert.equal(contractState, stateRefunded, "state should equal Refunded"); + + // last balance check + balanceFirstAccount = web3.eth.getBalance(firstAccount); + assert.equal(balanceFirstAccount.toString(), expectedBalanceFirstAccount.toString(), + "balance of first account should be as expected"); + balanceSecondAccount = web3.eth.getBalance(secondAccount); + assert.equal(balanceSecondAccount.toString(), expectedBalanceSecondAccount.toString(), + "balance of second account should be as expected"); + }); + + it("shouldn't be possible to create a contract with no value", async () => { + const refundTime = 60; + + await tryCatch(atomicSwap.participate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: 0, gasPrice: 0}), errTypes.revert) + await tryCatch(atomicSwap.initiate(refundTime, secretHash, secondAccount, + {from: firstAccount, value: 0, gasPrice: 0}), errTypes.revert) + }); + + it("shouldn't be possible to create a contract with no refundTime", async () => { + const contractAmount = web3.toBigNumber(web3.toWei('0.01', 'ether')); + + await tryCatch(atomicSwap.participate(0, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}), errTypes.revert) + await tryCatch(atomicSwap.initiate(0, secretHash, secondAccount, + {from: firstAccount, value: contractAmount, gasPrice: 0}), errTypes.revert) + }); +}); diff --git a/cmd/ethatomicswap/contract/src/test/exceptions.js b/cmd/ethatomicswap/contract/src/test/exceptions.js new file mode 100644 index 0000000..3e1c881 --- /dev/null +++ b/cmd/ethatomicswap/contract/src/test/exceptions.js @@ -0,0 +1,26 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + +module.exports.errTypes = { + revert : "revert", + outOfGas : "out of gas", + invalidJump : "invalid JUMP", + invalidOpcode : "invalid opcode", + stackOverflow : "stack overflow", + stackUnderflow : "stack underflow", + staticStateChange : "static state change" +} + +module.exports.tryCatch = async function(promise, errType) { + try { + await promise; + throw null; + } + catch (error) { + assert(error, "Expected an error but did not get one"); + assert(error.message.startsWith(PREFIX + errType), "Expected an error starting with '" + PREFIX + errType + "' but got '" + error.message + "' instead"); + } +}; + +const PREFIX = "VM Exception while processing transaction: "; \ No newline at end of file diff --git a/cmd/ethatomicswap/contract/src/test/utils.js b/cmd/ethatomicswap/contract/src/test/utils.js new file mode 100644 index 0000000..d89a39e --- /dev/null +++ b/cmd/ethatomicswap/contract/src/test/utils.js @@ -0,0 +1,7 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + +module.exports.sleep = async function(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +}; diff --git a/cmd/ethatomicswap/contract/src/truffle-config.js b/cmd/ethatomicswap/contract/src/truffle-config.js new file mode 100644 index 0000000..0855df1 --- /dev/null +++ b/cmd/ethatomicswap/contract/src/truffle-config.js @@ -0,0 +1,18 @@ +/* + * NB: since truffle-hdwallet-provider 0.0.5 you must wrap HDWallet providers in a + * function when declaring them. Failure to do so will cause commands to hang. ex: + * ``` + * mainnet: { + * provider: function() { + * return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/') + * }, + * network_id: '1', + * gas: 4500000, + * gasPrice: 10000000000, + * }, + */ + +module.exports = { + // See + // to customize your Truffle configuration! +}; diff --git a/cmd/ethatomicswap/contract/src/truffle.js b/cmd/ethatomicswap/contract/src/truffle.js new file mode 100644 index 0000000..0855df1 --- /dev/null +++ b/cmd/ethatomicswap/contract/src/truffle.js @@ -0,0 +1,18 @@ +/* + * NB: since truffle-hdwallet-provider 0.0.5 you must wrap HDWallet providers in a + * function when declaring them. Failure to do so will cause commands to hang. ex: + * ``` + * mainnet: { + * provider: function() { + * return new HDWalletProvider(mnemonic, 'https://mainnet.infura.io/') + * }, + * network_id: '1', + * gas: 4500000, + * gasPrice: 10000000000, + * }, + */ + +module.exports = { + // See + // to customize your Truffle configuration! +}; diff --git a/cmd/ethatomicswap/main.go b/cmd/ethatomicswap/main.go new file mode 100644 index 0000000..b90cb3f --- /dev/null +++ b/cmd/ethatomicswap/main.go @@ -0,0 +1,1368 @@ +// Copyright (c) 2017 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. + +package main + +import ( + "bufio" + "bytes" + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "flag" + "fmt" + "io/ioutil" + "math/big" + "os" + "strings" + "time" + + "github.com/bgentry/speakeasy" + + 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/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/decred/atomicswap/cmd/ethatomicswap/contract" +) + +var ( + chainConfig = params.MainnetChainConfig +) + +const ( + initiateLockPeriodInSeconds = 48 * 60 * 60 + participateLockPeriodInSeconds = 24 * 60 * 60 + + maxGasLimit = 210000 +) + +var ( + flagset = flag.NewFlagSet("", flag.ExitOnError) + connectFlag = flagset.String("s", "http://localhost:8545", "endpoint of Ethereum RPC server") + contractFlag = flagset.String("c", "", "hex-enoded address of the deployed contract") + accountFlag = flagset.String("account", "", "account file, account address or nothing for the daemon's first account") + timeoutFlag = flagset.Duration("t", 0, "optional timeout of any call made") + testnetFlag = flagset.Bool("testnet", false, "use testnet (Rinkeby) network") +) + +// There are two directions that the atomic swap can be performed, as the +// initiator can be on either chain. This tool only deals with creating the +// Bitcoin transactions for these swaps. A second tool should be used for the +// transaction on the other chain. Any chain can be used so long as it supports +// OP_SHA256 and OP_CHECKLOCKTIMEVERIFY. +// +// Example scenerios using bitcoin as the second chain: +// +// Scenerio 1: +// cp1 initiates (dcr) +// cp2 participates with cp1 H(S) (eth) +// cp1 redeems eth revealing S +// - must verify H(S) in contract is hash of known secret +// cp2 redeems dcr with S +// +// Scenerio 2: +// cp1 initiates (eth) +// cp2 participates with cp1 H(S) (dcr) +// cp1 redeems dcr revealing S +// - must verify H(S) in contract is hash of known secret +// cp2 redeems eth with S + +func init() { + flagset.Usage = func() { + fmt.Println("Usage: ethatomicswap [flags] cmd [cmd args]") + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" initiate ") + fmt.Println(" participate ") + fmt.Println(" redeem ") + fmt.Println(" refund ") + fmt.Println(" extractsecret ") + fmt.Println(" auditcontract ") + fmt.Println() + fmt.Println("Extra Commands:") + fmt.Println(" deploycontract") + fmt.Println(" validatedeployedcontract ") + fmt.Println() + fmt.Println("Flags:") + flagset.PrintDefaults() + } +} + +type command interface { + runCommand(swapContractTransactor) error +} + +// offline commands don't require wallet RPC. +type offlineCommand interface { + command + runOfflineCommand() error +} + +type initiateCmd struct { + cp2Addr common.Address + amount *big.Int // in wei +} + +type participateCmd struct { + cp1Addr common.Address + amount *big.Int // in wei + secretHash [32]byte +} + +type redeemCmd struct { + contractTx *types.Transaction + secret [32]byte +} + +type refundCmd struct { + contractTx *types.Transaction +} + +type extractSecretCmd struct { + redemptionTx *types.Transaction + secretHash [32]byte +} + +type auditContractCmd struct { + contractTx *types.Transaction +} + +type deployContractCmd struct{} + +type validateDeployedContractCmd struct { + deployTx *types.Transaction +} + +func main() { + err, showUsage := run() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + if showUsage { + flagset.Usage() + } + if err != nil || showUsage { + os.Exit(1) + } +} + +func checkCmdArgLength(args []string, required int) (nArgs int) { + if len(args) < required { + return 0 + } + for i, arg := range args[:required] { + if len(arg) != 1 && strings.HasPrefix(arg, "-") { + return i + } + } + return required +} + +const ( + weiPrecision = 18 +) + +func parseEthAsWei(str string) (*big.Int, error) { + initialParts := strings.SplitN(str, ".", 2) + if len(initialParts) == 1 { + // a round number, simply multiply and go + i, ok := big.NewInt(0).SetString(initialParts[0], 10) + if !ok { + return nil, errors.New("invalid round amount") + } + switch i.Cmp(big.NewInt(0)) { + case -1: + return nil, errors.New("invalid round amount: cannot be negative") + case 0: + return nil, errors.New("invalid round amount: cannot be nil") + } + return i.Mul(i, new(big.Int).Exp(big.NewInt(10), big.NewInt(weiPrecision), nil)), nil + } + + whole := initialParts[0] + dac := initialParts[1] + sn := uint(weiPrecision) + if l := uint(len(dac)); l < sn { + sn = l + } + whole += initialParts[1][:sn] + dac = dac[sn:] + for i := range dac { + if dac[i] != '0' { + return nil, errors.New("invalid or too precise amount") + } + } + i, ok := big.NewInt(0).SetString(whole, 10) + if !ok { + return nil, errors.New("invalid amount") + } + switch i.Cmp(big.NewInt(0)) { + case -1: + return nil, errors.New("invalid round amount: cannot be negative") + case 0: + return nil, errors.New("invalid round amount: cannot be nil") + } + i.Mul(i, big.NewInt(0).Exp( + big.NewInt(10), big.NewInt(int64(weiPrecision-sn)), nil)) + + switch i.Cmp(big.NewInt(0)) { + case -1: + return nil, errors.New("invalid round amount: cannot be negative") + case 0: + return nil, errors.New("invalid round amount: cannot be nil") + } + return i, nil +} + +func formatWeiAsEthString(w *big.Int) string { + if w.Cmp(big.NewInt(0)) == 0 { + return "0" + } + + str := w.String() + l := uint(len(str)) + if l > weiPrecision { + idx := l - weiPrecision + str = strings.TrimRight(str[:idx]+"."+str[idx:], "0") + str = strings.TrimRight(str, ".") + if len(str) == 0 { + return "0" + } + return str + } + str = "0." + strings.Repeat("0", int(weiPrecision-l)) + str + str = strings.TrimRight(str, "0") + str = strings.TrimRight(str, ".") + return str +} + +func run() (err error, showUsage bool) { + flagset.Parse(os.Args[1:]) + args := flagset.Args() + if len(args) == 0 { + return nil, true + } + cmdArgs := 0 + switch args[0] { + case "initiate": + cmdArgs = 2 + case "participate": + cmdArgs = 3 + case "redeem": + cmdArgs = 2 + case "refund": + cmdArgs = 1 + case "extractsecret": + cmdArgs = 2 + case "auditcontract": + cmdArgs = 1 + case "deploycontract": + cmdArgs = 0 + case "validatedeployedcontract": + cmdArgs = 1 + default: + return fmt.Errorf("unknown command %v", args[0]), true + } + nArgs := checkCmdArgLength(args[1:], cmdArgs) + flagset.Parse(args[1+nArgs:]) + if nArgs < cmdArgs { + return fmt.Errorf("%s: too few arguments", args[0]), true + } + if flagset.NArg() != 0 { + return fmt.Errorf("unexpected argument: %s", flagset.Arg(0)), true + } + + if *testnetFlag { + chainConfig = params.RinkebyChainConfig + } + + var cmd command + switch args[0] { + case "initiate": + cp2Addr := common.HexToAddress(args[1]) + amount, err := parseEthAsWei(args[2]) + if err != nil { + return fmt.Errorf("unexpected amount argument (%v): %v", args[2], err), true + } + cmd = &initiateCmd{ + cp2Addr: cp2Addr, + amount: amount, + } + + case "participate": + cp1Addr := common.HexToAddress(args[1]) + amount, err := parseEthAsWei(args[2]) + if err != nil { + return fmt.Errorf("unexpected amount argument (%v): %v", args[2], err), true + } + secretHash, err := hexDecodeSha256Hash("secret hash", args[3]) + if err != nil { + return err, true + } + cmd = &participateCmd{ + cp1Addr: cp1Addr, + amount: amount, + secretHash: secretHash, + } + + case "redeem": + contractTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + secret, err := hexDecodeSha256Hash("secret", args[2]) + if err != nil { + return err, true + } + cmd = &redeemCmd{ + contractTx: contractTx, + secret: secret, + } + + case "refund": + contractTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + cmd = &refundCmd{ + contractTx: contractTx, + } + + case "extractsecret": + redemptionTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + secretHash, err := hexDecodeSha256Hash("secret hash", args[2]) + if err != nil { + return err, true + } + cmd = &extractSecretCmd{ + redemptionTx: redemptionTx, + secretHash: secretHash, + } + + case "auditcontract": + contractTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + cmd = &auditContractCmd{ + contractTx: contractTx, + } + + case "deploycontract": + cmd = new(deployContractCmd) + + case "validatedeployedcontract": + deployTx, err := hexDecodeTransaction(args[1]) + if err != nil { + return err, true + } + cmd = &validateDeployedContractCmd{ + deployTx: deployTx, + } + + default: + panic(fmt.Sprintf("unknown command %v", args[0])) + } + + // Offline commands don't need to talk to the wallet. + if cmd, ok := cmd.(offlineCommand); ok { + return cmd.runOfflineCommand(), false + } + + client, err := dialClient() + if err != nil { + return fmt.Errorf("rpc connect: %v", err), false + } + defer client.Close() + + // create (swap) contract transactor + contractAddr, err := getDeployedContractAddress() + if err != nil { + return fmt.Errorf("failed to get contract address: %v", err), false + } + sct, err := newSwapContractTransactor(client, contractAddr) + if err != nil { + return err, false + } + + err = cmd.runCommand(sct) + return err, false +} + +func getDeployedContractAddress() (common.Address, error) { + contractAddress := *contractFlag + if contractAddress != "" { + return common.HexToAddress(contractAddress), nil + } + switch chainConfig { + case params.MainnetChainConfig: + return common.Address{}, errors.New("no default contract exist yet for the main net") + case params.RinkebyChainConfig: + return common.HexToAddress("2661CBAa149721f7c5FAB3FA88C1EA564A683631"), nil + } + + panic("unknown chain config for chain ID: " + chainConfig.ChainID.String()) +} + +func sha256Hash(x []byte) [sha256.Size]byte { + h := sha256.Sum256(x) + return h +} + +func hexDecodeSha256Hash(name, str string) (hash [sha256.Size]byte, err error) { + slice, err := hex.DecodeString(strings.TrimPrefix(str, "0x")) + if err != nil { + err = errors.New(name + " must be hex encoded") + return + } + if len(slice) != sha256.Size { + err = errors.New(name + " has wrong size") + return + } + copy(hash[:], slice) + return +} + +func hexDecodeTransaction(str string) (*types.Transaction, error) { + slice, err := hex.DecodeString(strings.TrimPrefix(str, "0x")) + if err != nil { + return nil, errors.New("transaction must be hex encoded") + } + var tx types.Transaction + err = rlp.DecodeBytes(slice, &tx) + if err != nil { + return nil, fmt.Errorf("failed to decode transaction: %v", err) + } + return &tx, nil +} + +func generateSecretHashPair() (secret, secretHash [sha256.Size]byte) { + rand.Read(secret[:]) + secretHash = sha256Hash(secret[:]) + return +} + +func promptPublishTx(name string) (bool, error) { + reader := bufio.NewReader(os.Stdin) + for { + fmt.Printf("Publish %s transaction? [y/N] ", name) + answer, err := reader.ReadString('\n') + if err != nil { + return false, err + } + answer = strings.TrimSpace(strings.ToLower(answer)) + + switch answer { + case "y", "yes": + return true, nil + case "n", "no", "": + return false, nil + default: + fmt.Println("please answer y or n") + continue + } + } +} + +func calcGasCost(limit uint64, c *ethclient.Client) (*big.Int, error) { + price, err := c.SuggestGasPrice(context.Background()) + if err != nil { + return nil, err + } + return price.Mul(price, big.NewInt(int64(limit))), nil +} + +func unpackContractInputParams(abi abi.ABI, tx *types.Transaction) (params struct { + LockDuration *big.Int + SecretHash [sha256.Size]byte + ToAddress common.Address +}, err error) { + txData := tx.Data() + + // first 4 bytes contain the id, so let's get method using that ID + method, err := abi.MethodById(txData[:4]) + if err != nil { + err = fmt.Errorf("failed to get method using its parsed id: %v", err) + return + } + + // unpack and return the params + paramSlice := []interface{}{ // unpack as slice, so we don't enforce field names + ¶ms.LockDuration, + ¶ms.SecretHash, + ¶ms.ToAddress, + } + err = method.Inputs.Unpack(¶mSlice, txData[4:]) + if err != nil { + err = fmt.Errorf("failed to unpack method's input params: %v", err) + } + return +} + +func (cmd *initiateCmd) runCommand(sct swapContractTransactor) error { + secret, secretHash := generateSecretHashPair() + tx, err := sct.initiateTx(cmd.amount, secretHash, cmd.cp2Addr) + if err != nil { + return fmt.Errorf("failed to create initiate TX: %v", err) + } + + fmt.Printf("Amount: %s Wei (%s ETH)\n\n", + cmd.amount.String(), formatWeiAsEthString(cmd.amount)) + + fmt.Printf("Secret: %x\n", secret) + fmt.Printf("Secret hash: %x\n\n", secretHash) + + if sct.autoAccount { + fmt.Printf("Author's refund address: %x\n\n", sct.fromAddr) + } + + initiateTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Contract fee: %s ETH\n", formatWeiAsEthString(initiateTxCost)) + refundTxCost, err := sct.maxGasCost() + if err != nil { + return fmt.Errorf("failed to estimate max gas cost for refund tx: %v", err) + } + fmt.Printf("Refund fee: %s ETH (max)\n\n", formatWeiAsEthString(refundTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Contract transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode contract TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("contract") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published contract transaction (%x)\n", tx.Hash()) + return nil +} + +func (cmd *participateCmd) runCommand(sct swapContractTransactor) error { + tx, err := sct.participateTx(cmd.amount, cmd.secretHash, cmd.cp1Addr) + if err != nil { + return fmt.Errorf("failed to create participate TX: %v", err) + } + + fmt.Printf("Amount: %s Wei (%s ETH)\n\n", + cmd.amount.String(), formatWeiAsEthString(cmd.amount)) + + if sct.autoAccount { + fmt.Printf("Author's refund address: %x\n\n", sct.fromAddr) + } + + participateTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Contract fee: %s ETH\n", formatWeiAsEthString(participateTxCost)) + refundTxCost, err := sct.maxGasCost() + if err != nil { + return fmt.Errorf("failed to estimate max gas cost for refund tx: %v", err) + } + fmt.Printf("Refund fee: %s ETH (max)\n\n", formatWeiAsEthString(refundTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Contract transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode contract TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("contract") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published contract transaction (%x)\n", tx.Hash()) + return nil +} + +func (cmd *redeemCmd) runCommand(sct swapContractTransactor) error { + params, err := unpackContractInputParams(sct.abi, cmd.contractTx) + if err != nil { + return err + } + tx, err := sct.redeemTx(params.SecretHash, cmd.secret) + if err != nil { + return fmt.Errorf("failed to create redeem TX: %v", err) + } + + redeemTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Redeem fee: %s ETH\n\n", formatWeiAsEthString(redeemTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Redeem transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode redeem TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("redeem") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published redeem transaction (%x)\n", tx.Hash()) + return nil +} + +func (cmd *refundCmd) runCommand(sct swapContractTransactor) error { + params, err := unpackContractInputParams(sct.abi, cmd.contractTx) + if err != nil { + return err + } + tx, err := sct.refundTx(params.SecretHash) + if err != nil { + return fmt.Errorf("failed to create refund TX: %v", err) + } + + refundTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Refund fee: %s ETH\n\n", formatWeiAsEthString(refundTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Refund transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode refund TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("refund") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published refund transaction (%x)\n", tx.Hash()) + return nil +} + +func (cmd *extractSecretCmd) runCommand(swapContractTransactor) error { + return cmd.runOfflineCommand() +} + +func (cmd *extractSecretCmd) runOfflineCommand() error { + abi, err := abi.JSON(strings.NewReader(contract.ContractABI)) + if err != nil { + return fmt.Errorf("failed to read (smart) contract ABI: %v", err) + } + + txData := cmd.redemptionTx.Data() + + // first 4 bytes contain the id, so let's get method using that ID + method, err := abi.MethodById(txData[:4]) + if err != nil { + return fmt.Errorf("failed to get method using its parsed id: %v", err) + } + if method.Name != "redeem" { + return fmt.Errorf("unexpected name for unpacked method ID: %s", method.Name) + } + + // prepare the params + params := struct { + Secret [sha256.Size]byte + SecretHash [sha256.Size]byte + }{} + + // unpack the params + err = method.Inputs.Unpack(¶ms, txData[4:]) + if err != nil { + return fmt.Errorf("failed to unpack method's input params: %v", err) + } + + // ensure secret hash is the same as the given one + if cmd.secretHash != params.SecretHash { + return fmt.Errorf("unexpected secret hash found: %x", params.SecretHash) + } + secretHash := sha256Hash(params.Secret[:]) + if params.SecretHash != secretHash { + return fmt.Errorf("unexpected secret found: %x", params.Secret) + } + + // print secret + fmt.Printf("Secret: %x\n", params.Secret) + return nil +} + +func (cmd *auditContractCmd) runCommand(sct swapContractTransactor) error { + // unpack input params from contract tx + params, err := unpackContractInputParams(sct.abi, cmd.contractTx) + if err != nil { + return err + } + + rpcTransaction := struct { + tx *types.Transaction + BlockNumber *string + BlockHash *common.Hash + From *common.Address + }{} + + // get transaction by hash + contractHash := cmd.contractTx.Hash() + ctx := newContext() + err = sct.client.rpcClient.CallContext(ctx, + &rpcTransaction, "eth_getTransactionByHash", contractHash) + ctx.Cancel() + if err != nil { + return fmt.Errorf( + "failed to find transaction (%x): %v", contractHash, err) + } + if rpcTransaction.BlockNumber == nil || *rpcTransaction.BlockNumber == "" || *rpcTransaction.BlockNumber == "0" { + return fmt.Errorf("transaction (%x) is pending", contractHash) + } + + // get block in order to know the timestamp of the txn + ctx = newContext() + block, err := sct.client.BlockByHash(ctx, *rpcTransaction.BlockHash) + ctx.Cancel() + if err != nil { + return fmt.Errorf( + "failed to find block (%x): %v", rpcTransaction.BlockHash, err) + } + + // compute the locktime + lockTime := time.Unix(block.Time().Int64()+params.LockDuration.Int64(), 0) + + // print contract info + + fmt.Printf("Contract address: %x\n", cmd.contractTx.To()) + fmt.Printf("Contract value: %s ETH\n", formatWeiAsEthString(cmd.contractTx.Value())) + fmt.Printf("Recipient address: %x\n", params.ToAddress) + fmt.Printf("Author's refund address: %x\n\n", rpcTransaction.From) + + fmt.Printf("Secret hash: %x\n\n", params.SecretHash) + + // NOTE: + // the reason we require th node for this method, + // is because we need to be able to know the transaction's timestamp + + fmt.Printf("Locktime: %v\n", lockTime.UTC()) + reachedAt := lockTime.Sub(time.Now().UTC()).Truncate(time.Second) + if reachedAt > 0 { + fmt.Printf("Locktime reached in %v\n", reachedAt) + } else { + fmt.Printf("Contract refund time lock has expired\n") + } + return nil +} + +func (cmd *deployContractCmd) runCommand(sct swapContractTransactor) error { + tx, err := sct.deployTx() + if err != nil { + return fmt.Errorf("failed to create deploy TX: %v", err) + } + + deployTxCost := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + fmt.Printf("Deploy fee: %s ETH\n\n", formatWeiAsEthString(deployTxCost)) + + fmt.Printf("Chain ID: %s\n", chainConfig.ChainID.String()) + fmt.Printf("Contract Address: %x\n", sct.contractAddr) + + fmt.Printf("Deploy transaction (%x):\n", tx.Hash()) + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return fmt.Errorf("failed to encode deploy TX: %v", err) + } + fmt.Printf("%x\n\n", txBytes) + + publish, err := promptPublishTx("deploy") + if err != nil || !publish { + return err + } + + err = tx.Send() + if err != nil { + return err + } + fmt.Printf("Published deploy transaction (%x)\n", tx.Hash()) + return nil +} + +func (cmd *validateDeployedContractCmd) runCommand(swapContractTransactor) error { + return cmd.runOfflineCommand() +} + +func (cmd *validateDeployedContractCmd) runOfflineCommand() error { + if !bytes.Equal(cmd.deployTx.Data(), contractBin) { + return errors.New("deployed contract is invalid (make sure to use the same Solidity contract source code and Compiler version (0.4.24))") + } + fmt.Println("Contract is valid") + return nil +} + +// newSwapContractTransactor creates a new swapContract instance, +// see swapContractTransactor for more information +func newSwapContractTransactor(c *ethClient, contractAddr common.Address) (swapContractTransactor, error) { + parsed, err := abi.JSON(strings.NewReader(contract.ContractABI)) + if err != nil { + return swapContractTransactor{}, fmt.Errorf("failed to read (smart) contract ABI: %v", err) + } + switch account := *accountFlag; { + case account == "": + var accounts []common.Address + ctx := newContext() + err := c.rpcClient.CallContext(ctx, &accounts, "eth_accounts") + ctx.Cancel() + if err != nil { + return swapContractTransactor{}, fmt.Errorf("failed to list unlocked accounts: %v", err) + } + if len(accounts) == 0 { + return swapContractTransactor{}, errors.New("no unlocked accounts were found") + } + // sign using daemon with a random account + return swapContractTransactor{ + abi: parsed, + client: c, + contractAddr: contractAddr, + fromAddr: accounts[0], + autoAccount: true, + }, nil + + case common.IsHexAddress(account): + // sign using daemon + return swapContractTransactor{ + abi: parsed, + client: c, + contractAddr: contractAddr, + fromAddr: common.HexToAddress(account), + }, nil + + default: + // sign using given key + signer, fromAddr, err := newSigner(account) + if err != nil { + return swapContractTransactor{}, fmt.Errorf("failed to create tx signer: %v", err) + } + return swapContractTransactor{ + abi: parsed, + signer: signer, + client: c, + fromAddr: fromAddr, + contractAddr: contractAddr, + }, nil + } +} + +// newSigner creates a signer func using the flag-passed +// private credentials of the sender +func newSigner(path string) (bind.SignerFn, common.Address, error) { + json, err := ioutil.ReadFile(path) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to read encrypted account/key file (%s) content: %v", path, err) + } + passphrase, err := speakeasy.Ask("Account passphrase: ") + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to get passphrase from STDIN: %v", err) + } + key, err := keystore.DecryptKey(json, passphrase) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to decrypt (JSON) account/key file (%s): %v", path, err) + } + privKey := key.PrivateKey + keyAddr := crypto.PubkeyToAddress(privKey.PublicKey) + return func(signer types.Signer, address common.Address, tx *types.Transaction) (*types.Transaction, error) { + if address != keyAddr { + return nil, errors.New("not authorized to sign this account") + } + signature, err := crypto.Sign(signer.Hash(tx).Bytes(), privKey) + if err != nil { + return nil, err + } + return tx.WithSignature(signer, signature) + }, keyAddr, nil +} + +type ( + // swapContractTransactor allows the creation of transactions for the different + // atomic swap actions + swapContractTransactor struct { + abi abi.ABI + signer bind.SignerFn + client *ethClient + fromAddr common.Address + contractAddr common.Address + autoAccount bool // defines if an account is automatically selected + + _contract *contract.Contract // created only once + } + + // swapTransaction adds send functionality to the transaction, + // such that it can be send in an easy way + swapTransaction struct { + *types.Transaction + client *ethClient + } +) + +func (sct *swapContractTransactor) initiateTx(amount *big.Int, secretHash [sha256.Size]byte, participant common.Address) (*swapTransaction, error) { + // validate tx does not exist yet, + // as to provide more meaningful error messages + switch _, err := sct.getSwapContract(secretHash); err { + case errNotExists: + // this is what we want + case nil: + return nil, errors.New("secret hash is already used for another atomic swap contract") + default: + return nil, fmt.Errorf("unexpected error while checking for an existing contract: %v", err) + } + // create initiate tx + return sct.newTransaction( + amount, "initiate", + // lock duration + big.NewInt(initiateLockPeriodInSeconds), + // secret hash + secretHash, + // participant + participant, + ) +} + +func (sct *swapContractTransactor) participateTx(amount *big.Int, secretHash [sha256.Size]byte, initiator common.Address) (*swapTransaction, error) { + // validate tx does not exist yet, + // as to provide more meaningful error messages + switch _, err := sct.getSwapContract(secretHash); err { + case errNotExists: + // this is what we want + case nil: + return nil, errors.New("secret hash is already used for another atomic swap contract") + default: + return nil, fmt.Errorf("unexpected error while checking for an existing contract: %v", err) + } + return sct.newTransaction( + amount, "participate", + // lock duration + big.NewInt(participateLockPeriodInSeconds), + // secret hash + secretHash, + // participant + initiator, + ) +} + +func (sct *swapContractTransactor) redeemTx(secretHash, secret [sha256.Size]byte) (*swapTransaction, error) { + // validate swap contract, + // as to provide more meaningful errors + sc, err := sct.getSwapContract(secretHash) + if err != nil { + return nil, err + } + if sc.SecretHash != secretHash { + return nil, errors.New("invalid secret hash registered") + } + if userSecretHash := sha256Hash(secret[:]); sc.SecretHash != userSecretHash { + return nil, errors.New("secret does not match secret hash") + } + switch sc.Kind { + case swapKindInitiator: + if sc.Participant != sct.fromAddr { + return nil, fmt.Errorf("only the participant can redeem: unexpected address: %x", sct.fromAddr) + } + case swapKindParticipant: + if sc.Initiator != sct.fromAddr { + return nil, fmt.Errorf("only the initiator can redeem: unexpected address: %x", sct.fromAddr) + } + default: + return nil, fmt.Errorf("invalid atomic swap contract kind: %d", sc.Kind) + } + if sc.State != swapStateFilled { + return nil, errors.New("inactive atomic swap contract") + } + // create redeem tx + return sct.newTransaction( + nil, "redeem", + // secret, + secret, + // secret hash + secretHash, + ) +} + +func (sct *swapContractTransactor) refundTx(secretHash [sha256.Size]byte) (*swapTransaction, error) { + // validate swap contract, + // as to provide more meaningful errors + sc, err := sct.getSwapContract(secretHash) + if err != nil { + return nil, err + } + if sc.SecretHash != secretHash { + return nil, errors.New("invalid secret hash registered") + } + switch sc.Kind { + case swapKindInitiator: + if sc.Initiator != sct.fromAddr { + return nil, fmt.Errorf("only the participant can refund: unexpected address: %x", sct.fromAddr) + } + case swapKindParticipant: + if sc.Participant != sct.fromAddr { + return nil, fmt.Errorf("only the initiator can refund: unexpected address: %x", sct.fromAddr) + } + default: + return nil, fmt.Errorf("invalid atomic swap contract kind: %d", sc.Kind) + } + if sc.State != swapStateFilled { + return nil, errors.New("inactive atomic swap contract") + } + lockTime := time.Unix(bigIntPtrToUint64(sc.InitTimestamp)+bigIntPtrToUint64(sc.RefundTime), 0) + if dur := time.Until(lockTime).Truncate(time.Second); dur >= 0 { + return nil, fmt.Errorf("contract is still locked for %v", dur+time.Second) + } + // create refund tx + return sct.newTransaction( + nil, "refund", + // secret hash + secretHash, + ) +} + +func bigIntPtrToUint64(i *big.Int) int64 { + if i == nil { + return 0 + } + return i.Int64() +} + +func (sct *swapContractTransactor) deployTx() (*swapTransaction, error) { + return sct.newTransactionWithInput(nil, false, common.FromHex(contract.ContractBin)) +} + +func (sct *swapContractTransactor) maxGasCost() (*big.Int, error) { + ctx := newContext() + gasPrice, err := sct.client.SuggestGasPrice(ctx) + ctx.Cancel() + if err != nil { + return nil, fmt.Errorf("failed to suggest gas price: %v", err) + } + return gasPrice.Mul(gasPrice, big.NewInt(maxGasLimit)), nil +} + +// states have to be mapped 1-to-1 with Enum AtomicSwap.State, +// as found in ./contract/src/contracts/AtomicSwap.sol +// +// This isn't part of the Ethereum-generated Go code found in the child "contract" pkg, +// given that the ABI does not export Enums. +const ( + swapStateEmpty uint8 = iota + swapStateFilled + swapStateRedeemed + swapStateRefunded +) + +// kinds have to be mapped 1-to-1 with Enum AtomicSwap.Kind, +// as found in ./contract/src/contracts/AtomicSwap.sol +// +// This isn't part of the Ethereum-generated Go code found in the child "contract" pkg, +// given that the ABI does not export Enums. +const ( + swapKindInitiator uint8 = iota + swapKindParticipant +) + +var ( + // error reported when an atomic swap contract (identified by a secret hash), + // has the state Empty, indicating it doesn't exist yet. + errNotExists = errors.New("atomic swap contract does not exist") +) + +// getSwapContract is a free contract call, +// which allows us to retrieve an atomic swap contract from a deployed AtomicSwap smart contract, +// using the secret hash used in that atomic swap contract as this contract's identifier. +func (sct *swapContractTransactor) getSwapContract(secretHash [32]byte) (*struct { + InitTimestamp *big.Int + RefundTime *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + Kind uint8 + State uint8 +}, error) { + if sct._contract == nil { + var err error + sct._contract, err = contract.NewContract(sct.contractAddr, sct.client.Client) + if err != nil { + return nil, fmt.Errorf("failed to bind smart contract (at %x): %v", sct.contractAddr, err) + } + } + ctx := newContext() + sc, err := sct._contract.Swaps(&bind.CallOpts{ + Pending: false, + From: sct.fromAddr, + Context: ctx, + }, secretHash) + ctx.Cancel() + if err != nil { + return nil, fmt.Errorf("failed to get swap contract from smart contract (at %x): %v", sct.contractAddr, err) + } + if sc.State == swapStateEmpty { + return nil, errNotExists + } + return &sc, nil +} + +func (sct *swapContractTransactor) newTransaction(amount *big.Int, name string, params ...interface{}) (*swapTransaction, error) { + // pack up the parameters and contract name + input, err := sct.abi.Pack(name, params...) + if err != nil { + return nil, fmt.Errorf("failed to pack input") + } + return sct.newTransactionWithInput(amount, true, input) +} + +func (sct *swapContractTransactor) newTransactionWithInput(amount *big.Int, contractCall bool, input []byte) (*swapTransaction, error) { + // define the TransactOpts for binding + opts, err := sct.calcBaseOpts(amount) + if err != nil { + return nil, err + } + opts.GasLimit, err = sct.calcGasLimit(opts.Value, opts.GasPrice, contractCall, input) + if err != nil { + return nil, err + } + + // sign using daemon or do it client-side if desired + var signedTx *types.Transaction + if opts.Signer == nil { + var toAddr *common.Address + if contractCall { + toAddr = &sct.contractAddr + } + // sign transaction using the daemon + var result struct { + Raw string `json:"raw"` + Tx types.Transaction `json:"tx"` + } + ctx := newContext() + err = sct.client.rpcClient.CallContext(ctx, &result, "eth_signTransaction", struct { + From common.Address `json:"from"` + To *common.Address `json:"to"` + Gas hexutil.Uint64 `json:"gas"` + GasPrice hexutil.Big `json:"gasPrice"` + Value hexutil.Big `json:"value"` + Nonce hexutil.Uint64 `json:"nonce"` + Data hexutil.Bytes `json:"data"` + }{ + From: opts.From, + To: toAddr, + Gas: hexutil.Uint64(opts.GasLimit), + GasPrice: hexutil.Big(*opts.GasPrice), + Value: func() hexutil.Big { + if amount == nil { + return hexutil.Big{} + } + return hexutil.Big(*amount) + }(), + Nonce: hexutil.Uint64(opts.Nonce.Uint64()), + Data: hexutil.Bytes(input), + }) + ctx.Cancel() + if err != nil { + return nil, fmt.Errorf("failed to sign transaction from daemon: %v", err) + } + signedTx = &result.Tx + } else { + var rawTx *types.Transaction + if contractCall { + rawTx = types.NewTransaction( + opts.Nonce.Uint64(), + sct.contractAddr, + opts.Value, + opts.GasLimit, + opts.GasPrice, + input, + ) + } else { + rawTx = types.NewContractCreation( + opts.Nonce.Uint64(), + opts.Value, + opts.GasLimit, + opts.GasPrice, + input, + ) + } + // sign ourselves + signedTx, err = opts.Signer(types.HomesteadSigner{}, opts.From, rawTx) + if err != nil { + return nil, fmt.Errorf("failed to sign transaction from client: %v", err) + } + } + return &swapTransaction{ + Transaction: signedTx, + client: sct.client, + }, nil +} + +func (sct *swapContractTransactor) calcBaseOpts(amount *big.Int) (*bind.TransactOpts, error) { + ctx := newContext() + nonce, err := sct.client.PendingNonceAt(ctx, sct.fromAddr) + ctx.Cancel() + if err != nil { + return nil, fmt.Errorf( + "failed to retrieve account (%x) nonce: %v", + sct.fromAddr, err) + } + ctx = newContext() + gasPrice, err := sct.client.SuggestGasPrice(ctx) + ctx.Cancel() + if err != nil { + return nil, fmt.Errorf("failed to suggest gas price: %v", err) + } + if amount == nil { + amount = new(big.Int) + } + return &bind.TransactOpts{ + From: sct.fromAddr, + Nonce: new(big.Int).SetUint64(nonce), + Signer: sct.signer, + Value: amount, + GasPrice: gasPrice, + }, nil +} + +func (sct *swapContractTransactor) calcGasLimit(amount, gasPrice *big.Int, contractCall bool, input []byte) (uint64, error) { + if contractCall { + ctx := newContext() + code, err := sct.client.PendingCodeAt(ctx, sct.contractAddr) + ctx.Cancel() + if err != nil { + return 0, fmt.Errorf("failed to estimate gas needed: %v", err) + } else if len(code) == 0 { + return 0, fmt.Errorf("failed to estimate gas needed: %v", bind.ErrNoCode) + } + } + // If the contract surely has code (or code is not needed), estimate the transaction + msg := ethereum.CallMsg{ + From: sct.fromAddr, + Value: amount, + Data: input, + } + if contractCall { + msg.To = &sct.contractAddr + } + ctx := newContext() + gasLimit, err := sct.client.EstimateGas(ctx, msg) + ctx.Cancel() + if err != nil { + return 0, fmt.Errorf("failed to estimate gas needed: %v", err) + } + if contractCall && gasLimit > maxGasLimit { + return 0, fmt.Errorf("%d exceeds the hardcoded code-call gas limit of %d", gasLimit, maxGasLimit) + } + return gasLimit, nil +} + +func (st *swapTransaction) Send() error { + ctx := newContext() + err := st.client.SendTransaction(ctx, st.Transaction) + ctx.Cancel() + if err != nil { + return fmt.Errorf("failed to send transaction: %v", err) + } + return nil +} + +func dialClient() (*ethClient, error) { + c, err := rpc.DialContext(context.Background(), *connectFlag) + if err != nil { + return nil, err + } + return ðClient{ + Client: ethclient.NewClient(c), + rpcClient: c, + }, nil +} + +type ethClient struct { + *ethclient.Client + rpcClient *rpc.Client +} + +// newContext creates a context which HAS +// to be manually cancelled, as to not leak any resources +func newContext() *cancelableContext { + if *timeoutFlag == 0 { + ctx, cancelFn := context.WithCancel(context.Background()) + return &cancelableContext{ + Context: ctx, + cancelFn: cancelFn, + } + } + ctx, cancelFn := context.WithTimeout(context.Background(), *timeoutFlag) + return &cancelableContext{ + Context: ctx, + cancelFn: cancelFn, + } +} + +type cancelableContext struct { + context.Context + cancelFn context.CancelFunc +} + +func (cc *cancelableContext) Cancel() { + cc.cancelFn() +} + +var ( + // decode the byte code of the smart contract used + // during the initialisation phase of this CLI tool, + // as to ensure the hex-encoded string is valid at all times. + // + // This prevents of having a hidden error, + // due to the fact that it is only ever used in + // our extra smart-contract-related commands. + contractBin = func() []byte { + b, err := hex.DecodeString(contract.ContractBin) + if err != nil { + panic("invalid binary contract: " + err.Error()) + } + return b + }() +) diff --git a/cmd/ethatomicswap/main_test.go b/cmd/ethatomicswap/main_test.go new file mode 100644 index 0000000..2036e9d --- /dev/null +++ b/cmd/ethatomicswap/main_test.go @@ -0,0 +1,62 @@ +// Copyright (c) 2018 BetterToken BVBA +// Use of this source code is governed by an MIT +// license that can be found at https://github.com/rivine/rivine/blob/master/LICENSE. +package main + +import ( + "math/big" + "strings" + "testing" +) + +func TestParseEthAsWei(t *testing.T) { + bi := func(str string) *big.Int { + i, ok := new(big.Int).SetString(str, 10) + if !ok { + t.Fatal("failed to turn " + str + " into a big.Int") + } + return i + } + testCases := []struct { + Input string + ExpectedOutput *big.Int + }{ + {"-0", nil}, // nil isn't allowed + {"0", nil}, // nil isn't allowed + {"-123", nil}, // negative numbers aren't allowed + {"0.0000000000000000001", nil}, // too precise + {"1", big.NewInt(1000000000000000000)}, + {"1.1", big.NewInt(1100000000000000000)}, + {"1.123", big.NewInt(1123000000000000000)}, + {"0.001", big.NewInt(1000000000000000)}, + {"0.000000000000000001", big.NewInt(1)}, + {"123456789.987654321", bi("123456789987654321000000000")}, + {"0.00100", big.NewInt(1000000000000000)}, + {"0001", big.NewInt(1000000000000000000)}, + {"0001.100", big.NewInt(1100000000000000000)}, + } + for idx, testCase := range testCases { + x, err := parseEthAsWei(testCase.Input) + if testCase.ExpectedOutput == nil { + if err == nil { + t.Error(idx, "expected fail parsing, but it didn't") + } + continue + } + if err != nil { + t.Error(idx, "expected to parse, but it didn't", err) + continue + } + if testCase.ExpectedOutput.Cmp(x) != 0 { + t.Error(idx, testCase.ExpectedOutput.String(), "!=", x.String()) + } + str := formatWeiAsEthString(x) + strippedTestCase := strings.Trim(testCase.Input, "0") + if strippedTestCase[0] == '.' { + strippedTestCase = "0" + strippedTestCase + } + if str != strippedTestCase { + t.Error(idx, str, "!=", strippedTestCase) + } + } +}