├── .gitignore ├── LICENSE ├── README.md ├── abi ├── PaymentProtectionv2.abi └── PaymentProtectionv2.bin ├── cmd └── main.go ├── configuration.yaml ├── env-sample ├── util ├── util.go └── util_test.go └── wallet ├── account.go ├── account_test.go ├── client.go ├── client_test.go ├── contract_manager.go ├── erc20.go ├── erc20_wallet.go ├── erc20_wallet_test.go ├── escrow_v1.go ├── exchange_rates.go ├── service.go ├── wallet.go └── wallet_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | UTC-* 2 | *.txt 3 | .env 4 | .dat -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 OpenBazaar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-ethwallet 2 | ![banner](https://i.imgur.com/iOnXDXK.png) 3 | OpenBazaar Ethereum Wallet in Go 4 | 5 | [![Build Status](https://travis-ci.org/OpenBazaar/go-ethwallet.svg?branch=master)](https://travis-ci.org/OpenBazaar/go-ethwallet) 6 | [![Coverage Status](https://coveralls.io/repos/github/OpenBazaar/go-ethwallet/badge.svg?branch=master)](https://coveralls.io/github/OpenBazaar/go-ethwallet?branch=master) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/OpenBazaar/go-ethwallet)](https://goreportcard.com/report/github.com/OpenBazaar/go-ethwallet) 8 | 9 | 10 | This is an Ethereum wallet implementation which uses the Infura API. 11 | 12 | Your Infura API key is required as an environment variable. Refer to the 13 | env-sample for adding a `.env` file to the project root. 14 | 15 | To use this, you need to have an existing Ethereum JSON keystore. 16 | 17 | There is an option of creating one but it has not been integrated yet. 18 | 19 | To execute the wallet: 20 | 21 | >$ go run cmd/main.go -p < wallet_password > -f < path-to-keystore-file > 22 | 23 | eg: 24 | 25 | >$ go run cmd/main.go -p odetojoy -f ./UTC--2018-06-16T18-41-19.615987160Z--c0b4ef9e6d2806f643be94b2434f5c3d6cecd255 26 | 27 | Where the wallet password is odetojoy and the keystore file is in the same directory 28 | as the code. -------------------------------------------------------------------------------- /abi/PaymentProtectionv2.abi: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"scriptHash","type":"bytes32"}],"name":"addFundsToTransaction","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"transactions","outputs":[{"name":"scriptHash","type":"bytes32"},{"name":"buyer","type":"address"},{"name":"seller","type":"address"},{"name":"value","type":"uint256"},{"name":"status","type":"uint8"},{"name":"ipfsHash","type":"string"},{"name":"lastFunded","type":"uint256"},{"name":"timeoutHours","type":"uint32"},{"name":"threshold","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"transactionCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_buyer","type":"address"},{"name":"_seller","type":"address"},{"name":"_moderators","type":"address[]"},{"name":"threshold","type":"uint8"},{"name":"timeoutHours","type":"uint32"},{"name":"scriptHash","type":"bytes32"}],"name":"addTransaction","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"_partyAddress","type":"address"}],"name":"getAllTransactionsForParty","outputs":[{"name":"scriptHashes","type":"bytes32[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"sigV","type":"uint8[]"},{"name":"sigR","type":"bytes32[]"},{"name":"sigS","type":"bytes32[]"},{"name":"scriptHash","type":"bytes32"},{"name":"uniqueId","type":"bytes20"},{"name":"destinations","type":"address[]"},{"name":"amounts","type":"uint256[]"}],"name":"execute","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"scriptHash","type":"bytes32"},{"indexed":false,"name":"destinations","type":"address[]"},{"indexed":false,"name":"amounts","type":"uint256[]"}],"name":"Executed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"scriptHash","type":"bytes32"},{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"valueAdded","type":"uint256"}],"name":"FundAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"scriptHash","type":"bytes32"},{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Funded","type":"event"}] 2 | 3 | ======= openzeppelin-solidity/contracts/math/SafeMath.sol:SafeMath ======= 4 | [] 5 | -------------------------------------------------------------------------------- /abi/PaymentProtectionv2.bin: -------------------------------------------------------------------------------- 1 | 6080604052600060015534801561001557600080fd5b5061219d806100256000396000f300608060405260043610610078576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632d9ef96e1461007d578063642f2eaf146100a1578063b77bf60014610203578063b8310cac1461022e578063be84ceaf146102f2578063c8331de61461038a575b600080fd5b61009f6004803603810190808035600019169060200190929190505050610523565b005b3480156100ad57600080fd5b506100d0600480360381019080803560001916906020019092919050505061071b565b604051808a600019166000191681526020018973ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200187815260200186600181111561015857fe5b60ff168152602001806020018581526020018463ffffffff1663ffffffff1681526020018360ff1660ff168152602001828103825286818151815260200191508051906020019080838360005b838110156101c05780820151818401526020810190506101a5565b50505050905090810190601f1680156101ed5780820380516001836020036101000a031916815260200191505b509a505050505050505050505060405180910390f35b34801561020f57600080fd5b5061021861086b565b6040518082815260200191505060405180910390f35b6102f0600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190820180359060200190808060200260200160405190810160405280939291908181526020018383602002808284378201915050505050509192919290803560ff169060200190929190803563ffffffff1690602001909291908035600019169060200190929190505050610871565b005b3480156102fe57600080fd5b50610333600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050611076565b6040518080602001828103825283818151815260200191508051906020019060200280838360005b8381101561037657808201518184015260208101905061035b565b505050509050019250505060405180910390f35b34801561039657600080fd5b50610521600480360381019080803590602001908201803590602001908080602002602001604051908101604052809392919081815260200183836020028082843782019150505050505091929192908035906020019082018035906020019080806020026020016040519081016040528093929190818152602001838360200280828437820191505050505050919291929080359060200190820180359060200190808060200260200160405190810160405280939291908181526020018383602002808284378201915050505050509192919290803560001916906020019092919080356bffffffffffffffffffffffff191690602001909291908035906020019082018035906020019080806020026020016040519081016040528093929190818152602001838360200280828437820191505050505050919291929080359060200190820180359060200190808060200260200160405190810160405280939291908181526020018383602002808284378201915050505050509192919290505050611111565b005b60008160008060008360001916600019168152602001908152602001600020600401541415151561055357600080fd5b826000600181111561056157fe5b600080836000191660001916815260200190815260200160002060050160009054906101000a900460ff16600181111561059757fe5b141515610632576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260328152602001807f5472616e73616374696f6e2069732065697468657220696e206469737075746581526020017f206f722072656c6561736564207374617465000000000000000000000000000081525060400191505060405180910390fd5b34925060008311151561064457600080fd5b6106748360008087600019166000191681526020019081526020016000206004015461199490919063ffffffff16565b600080866000191660001916815260200190815260200160002060040181905550426000808660001916600019168152602001908152602001600020600701819055503373ffffffffffffffffffffffffffffffffffffffff167ff66fd2ae9e24a6a24b02e1b5b7512ffde5149a4176085fc0298ae228c9b9d72985856040518083600019166000191681526020018281526020019250505060405180910390a250505050565b60006020528060005260406000206000915090508060000154908060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060040154908060050160009054906101000a900460ff1690806006018054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156108325780601f1061080757610100808354040283529160200191610832565b820191906000526020600020905b81548152906001019060200180831161081557829003601f168201915b5050505050908060070154908060080160009054906101000a900463ffffffff16908060080160049054906101000a900460ff16905089565b60015481565b6000808260008060008360001916600019168152602001908152602001600020600401541415156108a157600080fd5b34925060008311151561091c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260118152602001807f56616c756520706173736564206973203000000000000000000000000000000081525060200191505060405180910390fd5b600087511115156109bb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001807f54686572652073686f756c642062652061746c656173742031206d6f6465726181526020017f746f72000000000000000000000000000000000000000000000000000000000081525060400191505060405180910390fd5b60028751018660ff1611151515610a60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001807f5468726573686f6c642069732067726561746572207468616e20746f74616c2081526020017f6f776e657273000000000000000000000000000000000000000000000000000081525060400191505060405180910390fd5b61014060405190810160405280856000191681526020018a73ffffffffffffffffffffffffffffffffffffffff1681526020018973ffffffffffffffffffffffffffffffffffffffff16815260200188815260200184815260200160006001811115610ac857fe5b8152602001602060405190810160405280600081525081526020014281526020018663ffffffff1681526020018760ff1681525060008086600019166000191681526020019081526020016000206000820151816000019060001916905560208201518160010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060408201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506060820151816003019080519060200190610bd0929190611fff565b506080820151816004015560a08201518160050160006101000a81548160ff02191690836001811115610bff57fe5b021790555060c0820151816006019080519060200190610c20929190612089565b5060e082015181600701556101008201518160080160006101000a81548163ffffffff021916908363ffffffff1602179055506101208201518160080160046101000a81548160ff021916908360ff1602179055509050506001600080866000191660001916815260200190815260200160002060090160008a73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055506001600080866000191660001916815260200190815260200160002060090160008b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff021916908315150217905550600091505b86518260ff161015610f005760008085600019166000191681526020019081526020016000206009016000888460ff16815181101515610d9f57fe5b9060200190602002015173ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16151515610e66576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601b8152602001807f4d6f64657261746f7220697320626569676e207265706561746564000000000081525060200191505060405180910390fd5b600160008086600019166000191681526020019081526020016000206009016000898560ff16815181101515610e9857fe5b9060200190602002015173ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055508180600101925050610d63565b600160008154809291906001019190505550600260008a73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020849080600181540180825580915050906001820390600052602060002001600090919290919091509060001916905550600260008973ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208490806001815401808255809150509060018203906000526020600020016000909192909190915090600019169055503373ffffffffffffffffffffffffffffffffffffffff167fce7089d0668849fb9ca29778c0cbf1e764d9efb048d81fd71fb34c94f26db368600080876000191660001916815260200190815260200160002060000154856040518083600019166000191681526020018281526020019250505060405180910390a2505050505050505050565b6060600260008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002080548060200260200160405190810160405280929190818152602001828054801561110557602002820191906000526020600020905b815460001916815260200190600101908083116110ed575b50505050509050919050565b6000806000806000808960008060008360001916600019168152602001908152602001600020600401541415151561114857600080fd5b8a6000600181111561115657fe5b600080836000191660001916815260200190815260200160002060050160009054906101000a900460ff16600181111561118c57fe5b141515611227576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260328152602001807f5472616e73616374696f6e2069732065697468657220696e206469737075746581526020017f206f722072656c6561736564207374617465000000000000000000000000000081525060400191505060405180910390fd5b60008a51118015611239575088518a51145b151561124457600080fd5b6000808d6000191660001916815260200190815260200160002097508a8860080160049054906101000a900460ff168960080160009054906101000a900463ffffffff168a60010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff168b60020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff168c60030160008154811015156112e457fe5b9060005260206000200160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1660405160200180876bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018660ff1660ff167f01000000000000000000000000000000000000000000000000000000000000000281526001018563ffffffff1663ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166c010000000000000000000000000281526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166c010000000000000000000000000281526014018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166c0100000000000000000000000002815260140196505050505050506040516020818303038152906040526040518082805190602001908083835b6020831015156114a85780518252602082019150602081019050602083039250611483565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040518091039020965086600019168c6000191614151561157c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260388152602001807f43616c63756c6174656420736372697074206861736820646f6573206e6f742081526020017f6d6174636820706173736564207363726970742068617368000000000000000081525060400191505060405180910390fd5b61158a8f8f8f8f8e8e6119b0565b95506115ae8860080160009054906101000a900463ffffffff168960070154611fb8565b94508760080160049054906101000a900460ff1660ff168f511080156115d2575084155b156115dc57600080fd5b60018f511480156115ea5750845b801561164657508760020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff1614155b1561165057600080fd5b8760080160049054906101000a900460ff1660ff168f51101561167257600080fd5b60016000808e6000191660001916815260200190815260200160002060050160006101000a81548160ff021916908360018111156116ac57fe5b021790555060009350600092505b89518360ff16101561188657600073ffffffffffffffffffffffffffffffffffffffff168a8460ff168151811015156116ef57fe5b9060200190602002015173ffffffffffffffffffffffffffffffffffffffff161415801561179b57506000808d6000191660001916815260200190815260200160002060090160008b8560ff1681518110151561174857fe5b9060200190602002015173ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff165b15156117a657600080fd5b6000898460ff168151811015156117b957fe5b906020019060200201511115156117cf57600080fd5b6117fc898460ff168151811015156117e357fe5b906020019060200201518561199490919063ffffffff16565b9350898360ff1681518110151561180f57fe5b9060200190602002015173ffffffffffffffffffffffffffffffffffffffff166108fc8a8560ff1681518110151561184357fe5b906020019060200201519081150290604051600060405180830381858888f19350505050158015611878573d6000803e3d6000fd5b5082806001019350506116ba565b6000808d600019166000191681526020019081526020016000206004015484111515156118b257600080fd5b7f688e2a1b34445bcd47b0e11ba2a9c8c4d850a1831b64199b59d1c70e297015458c8b8b6040518084600019166000191681526020018060200180602001838103835285818151815260200191508051906020019060200280838360005b8381101561192b578082015181840152602081019050611910565b50505050905001838103825284818151815260200191508051906020019060200280838360005b8381101561196d578082015181840152602081019050611952565b505050509050019550505050505060405180910390a1505050505050505050505050505050565b600081830190508281101515156119a757fe5b80905092915050565b600080600080875189511480156119c8575089518951145b15156119d357600080fd5b60197f01000000000000000000000000000000000000000000000000000000000000000260007f0100000000000000000000000000000000000000000000000000000000000000023088888b60405160200180877effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff19167effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff19168152600101867effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff19167effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191681526001018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166c01000000000000000000000000028152601401848051906020019060200280838360005b83811015611b27578082015181840152602081019050611b0c565b50505050905001838051906020019060200280838360005b83811015611b5a578082015181840152602081019050611b3f565b50505050905001826000191660001916815260200196505050505050506040516020818303038152906040526040518082805190602001908083835b602083101515611bbb5780518252602082019150602081019050602083039250611b96565b6001836020036101000a038019825116818451168082178552505050505050905001915050604051809103902060405160200180807f19457468657265756d205369676e6564204d6573736167653a0a333200000000815250601c0182600019166000191681526020019150506040516020818303038152906040526040518082805190602001908083835b602083101515611c6c5780518252602082019150602081019050602083039250611c47565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390209250600091505b8851821015611fab576001838b84815181101515611cba57fe5b906020019060200201518b85815181101515611cd257fe5b906020019060200201518b86815181101515611cea57fe5b90602001906020020151604051600081526020016040526040518085600019166000191681526020018460ff1660ff168152602001836000191660001916815260200182600019166000191681526020019450505050506020604051602081039080840390855afa158015611d63573d6000803e3d6000fd5b505050602060405103519050600080886000191660001916815260200190815260200160002060090160008273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff161515611e4b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260118152602001807f496e76616c6964207369676e617475726500000000000000000000000000000081525060200191505060405180910390fd5b6000808860001916600019168152602001908152602001600020600a0160008273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16151515611f28576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260198152602001807f53616d65207369676e61747572652073656e742074776963650000000000000081525060200191505060405180910390fd5b60016000808960001916600019168152602001908152602001600020600a0160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055508093508180600101925050611ca0565b5050509695505050505050565b600080611fce8342611fe690919063ffffffff16565b9050610e10840263ffffffff16811191505092915050565b6000828211151515611ff457fe5b818303905092915050565b828054828255906000526020600020908101928215612078579160200282015b828111156120775782518260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055509160200191906001019061201f565b5b5090506120859190612109565b5090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106120ca57805160ff19168380011785556120f8565b828001600101855582156120f8579182015b828111156120f75782518255916020019190600101906120dc565b5b509050612105919061214c565b5090565b61214991905b8082111561214557600081816101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690555060010161210f565b5090565b90565b61216e91905b8082111561216a576000816000905550600101612152565b5090565b905600a165627a7a7230582028c6a45a5a30ac8978c682848de6ff562f486e5a22b8e9e882164ab016e1f6480029 2 | 3 | ======= openzeppelin-solidity/contracts/math/SafeMath.sol:SafeMath ======= 4 | 604c602c600b82828239805160001a60731460008114601c57601e565bfe5b5030600052607381538281f30073000000000000000000000000000000000000000030146080604052600080fd00a165627a7a723058201f11c3b9646e4f7d728669bbc871ca0f5ca20db4a8a9ff4bb95c01848b9cc2620029 5 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "math/big" 8 | "os" 9 | 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/ethereum/go-ethereum/ethclient" 12 | _ "github.com/joho/godotenv/autoload" 13 | log "github.com/sirupsen/logrus" 14 | 15 | "github.com/OpenBazaar/go-ethwallet/wallet" 16 | ) 17 | 18 | var ( 19 | password string 20 | keyDir string 21 | keyFile string 22 | ) 23 | 24 | const ethToWei = 1 << 17 25 | 26 | // InfuraRopstenBase is the base URL for Infura Ropsten network 27 | const InfuraRopstenBase string = "https://ropsten.infura.io/" 28 | 29 | func init() { 30 | flag.StringVar(&password, "p", "", "password for keystore") 31 | flag.StringVar(&keyDir, "d", "", "key dir to generate key") 32 | flag.StringVar(&keyFile, "f", "", "key file path") 33 | } 34 | 35 | func main() { 36 | fmt.Println(os.Getenv("INFURA_KEY")) 37 | 38 | ropstenURL := InfuraRopstenBase + os.Getenv("INFURA_KEY") 39 | 40 | flag.Parse() 41 | 42 | fmt.Println("Password is : ", password) 43 | fmt.Println("keydir is: ", keyDir) 44 | fmt.Println("keyfile is : ", keyFile) 45 | 46 | client, err := ethclient.Dial("https://mainnet.infura.io") 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | fmt.Println("we have a connection") 51 | _ = client 52 | 53 | address := common.HexToAddress("0x71c7656ec7ab88b098defb751b7401b5f6d8976f") 54 | fmt.Println(address.Hex()) 55 | // 0x71C7656EC7ab88b098defB751B7401B5f6d8976F 56 | fmt.Println(address.Hash().Hex()) 57 | // 0x00000000000000000000000071c7656ec7ab88b098defb751b7401b5f6d8976f 58 | fmt.Println(address.Bytes()) 59 | 60 | account := common.HexToAddress("0x71c7656ec7ab88b098defb751b7401b5f6d8976f") 61 | balance, err := client.BalanceAt(context.Background(), account, nil) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | fmt.Println(balance) 66 | 67 | // Get the balance at a particular instance of time expressed as block number 68 | blockNumber := big.NewInt(5532993) 69 | balance, err = client.BalanceAt(context.Background(), account, blockNumber) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | fmt.Println(balance) 74 | 75 | //wallet.GenWallet() 76 | 77 | //wallet.GenDefaultKeyStore(password) 78 | var myAccount *wallet.Account 79 | myAccount, err = wallet.NewAccountFromKeyfile(keyFile, password) 80 | if err != nil { 81 | log.Fatal("key file validation failed:%s", err.Error()) 82 | } 83 | fmt.Println(myAccount.Address().String()) 84 | 85 | // create the source wallet obj for Infura Ropsten 86 | myWallet := wallet.NewEthereumWalletWithKeyfile(ropstenURL, keyFile, password) 87 | fmt.Println(myWallet.GetBalance()) 88 | 89 | // create dest account 90 | //wallet.GenDefaultKeyStore(password) 91 | var destAccount *wallet.Account 92 | destKeyFile := "./UTC--2018-06-16T20-09-33.726552102Z--cecb952de5b23950b15bfd49302d1bdd25f9ee67" 93 | destAccount, err = wallet.NewAccountFromKeyfile(destKeyFile, password) 94 | if err != nil { 95 | log.Fatal("key file validation failed:%s", err.Error()) 96 | } 97 | fmt.Println(destAccount.Address().String()) 98 | 99 | // create the destination wallet obj for Infura Ropsten 100 | destWallet := wallet.NewEthereumWalletWithKeyfile(ropstenURL, destKeyFile, password) 101 | fmt.Println(destWallet.GetBalance()) 102 | 103 | // lets transfer 104 | //err = myWallet.Transfer(destAccount.Address().String(), big.NewInt(3344556677)) 105 | //if err != nil { 106 | // fmt.Println("what happened here : ", err) 107 | //} 108 | fmt.Println("after transfer : ") 109 | fmt.Println("Dest balance ") 110 | fmt.Println(destWallet.GetBalance()) 111 | fmt.Println(destWallet.Balance()) 112 | fmt.Println("Source balance ") 113 | fmt.Println(myWallet.GetBalance()) 114 | fmt.Println(myWallet.Balance()) 115 | 116 | fmt.Println(myWallet.CreateAddress()) 117 | } 118 | -------------------------------------------------------------------------------- /configuration.yaml: -------------------------------------------------------------------------------- 1 | ROPSTEN_PPv2_ADDRESS : 0x0b944d65dc9f06d756ee2845061c0948eb938129 2 | ROPSTEN_REGISTRY : 0x029d6a0cd4ce98315690f4ea52945545d9c0f460 3 | -------------------------------------------------------------------------------- /env-sample: -------------------------------------------------------------------------------- 1 | INFURA_KEY=YOUR_INFURA_KEY -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/big" 5 | "reflect" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/btcsuite/btcd/chaincfg/chainhash" 11 | "github.com/btcsuite/btcutil/base58" 12 | "github.com/btcsuite/btcutil/hdkeychain" 13 | "github.com/ethereum/go-ethereum/common" 14 | "github.com/ethereum/go-ethereum/common/hexutil" 15 | "github.com/op/go-logging" 16 | "github.com/shopspring/decimal" 17 | ) 18 | 19 | var log = logging.MustGetLogger("ethwallet-util") 20 | 21 | // ExtractChaincode used to get the chaincode out of extended key 22 | func ExtractChaincode(key *hdkeychain.ExtendedKey) []byte { 23 | return base58.Decode(key.String())[13:45] 24 | } 25 | 26 | // IsValidAddress validate hex address 27 | func IsValidAddress(iaddress interface{}) bool { 28 | re := regexp.MustCompile("^0x[0-9a-fA-F]{40}$") 29 | switch v := iaddress.(type) { 30 | case string: 31 | return re.MatchString(v) 32 | case common.Address: 33 | return re.MatchString(v.Hex()) 34 | default: 35 | return false 36 | } 37 | } 38 | 39 | // IsZeroAddress validate if it's a 0 address 40 | func IsZeroAddress(iaddress interface{}) bool { 41 | var address common.Address 42 | switch v := iaddress.(type) { 43 | case string: 44 | address = common.HexToAddress(v) 45 | case common.Address: 46 | address = v 47 | default: 48 | return false 49 | } 50 | 51 | zeroAddressBytes := common.FromHex("0x0000000000000000000000000000000000000000") 52 | addressBytes := address.Bytes() 53 | return reflect.DeepEqual(addressBytes, zeroAddressBytes) 54 | } 55 | 56 | // ToDecimal wei to decimals 57 | func ToDecimal(ivalue interface{}, decimals int) decimal.Decimal { 58 | value := new(big.Int) 59 | switch v := ivalue.(type) { 60 | case string: 61 | value.SetString(v, 10) 62 | case *big.Int: 63 | value = v 64 | } 65 | 66 | mul := decimal.NewFromFloat(float64(10)).Pow(decimal.NewFromFloat(float64(decimals))) 67 | num, _ := decimal.NewFromString(value.String()) 68 | result := num.Div(mul) 69 | 70 | return result 71 | } 72 | 73 | // ToWei decimals to wei 74 | func ToWei(iamount interface{}, decimals int) *big.Int { 75 | amount := decimal.NewFromFloat(0) 76 | switch v := iamount.(type) { 77 | case string: 78 | amount, _ = decimal.NewFromString(v) 79 | case float64: 80 | amount = decimal.NewFromFloat(v) 81 | case int64: 82 | amount = decimal.NewFromFloat(float64(v)) 83 | case decimal.Decimal: 84 | amount = v 85 | case *decimal.Decimal: 86 | amount = *v 87 | } 88 | 89 | mul := decimal.NewFromFloat(float64(10)).Pow(decimal.NewFromFloat(float64(decimals))) 90 | result := amount.Mul(mul) 91 | 92 | wei := new(big.Int) 93 | wei.SetString(result.String(), 10) 94 | 95 | return wei 96 | } 97 | 98 | // CalcGasCost calculate gas cost given gas limit (units) and gas price (wei) 99 | func CalcGasCost(gasLimit uint64, gasPrice *big.Int) *big.Int { 100 | gasLimitBig := big.NewInt(int64(gasLimit)) 101 | return gasLimitBig.Mul(gasLimitBig, gasPrice) 102 | } 103 | 104 | // SigRSV signatures R S V returned as arrays 105 | func SigRSV(isig interface{}) ([32]byte, [32]byte, uint8) { 106 | var sig []byte 107 | switch v := isig.(type) { 108 | case []byte: 109 | sig = v 110 | case string: 111 | sig, _ = hexutil.Decode(v) 112 | } 113 | 114 | sigstr := common.Bytes2Hex(sig) 115 | rS := sigstr[0:64] 116 | sS := sigstr[64:128] 117 | R := [32]byte{} 118 | S := [32]byte{} 119 | copy(R[:], common.FromHex(rS)) 120 | copy(S[:], common.FromHex(sS)) 121 | vStr := sigstr[128:130] 122 | vI, _ := strconv.Atoi(vStr) 123 | V := uint8(vI + 27) 124 | 125 | return R, S, V 126 | } 127 | 128 | // EnsureCorrectPrefix ensures we have 0x prefix 129 | func EnsureCorrectPrefix(str string) string { 130 | if strings.HasPrefix(str, "0x") { 131 | return str 132 | } 133 | return "0x" + str 134 | } 135 | 136 | // CreateChainHash is a wrapper to the chainhash.new hash function 137 | // this allows for a cleaner way to check if we are not in any way 138 | // letting the 0x prefix hinder the chainhash generation 139 | func CreateChainHash(str string) (*chainhash.Hash, error) { 140 | hash, err := chainhash.NewHashFromStr(str) 141 | if err == chainhash.ErrHashStrSize { 142 | hash, err = chainhash.NewHashFromStr(strings.TrimPrefix(str, "0x")) 143 | if err != nil { 144 | log.Errorf("err creating chainhash : %v", err) 145 | return nil, err 146 | } 147 | } 148 | return hash, nil 149 | } 150 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/btcsuite/btcutil/hdkeychain" 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/ethereum/go-ethereum/common/hexutil" 11 | "github.com/shopspring/decimal" 12 | ) 13 | 14 | func TestIsValidAddress(t *testing.T) { 15 | t.Parallel() 16 | validAddress := "0x323b5d4c32345ced77393b3530b1eed0f346429d" 17 | invalidAddress := "0xabc" 18 | invalidAddress2 := "323b5d4c32345ced77393b3530b1eed0f346429d" 19 | { 20 | got := IsValidAddress(validAddress) 21 | expected := true 22 | 23 | if got != expected { 24 | t.Errorf("Expected %v, got %v", expected, got) 25 | } 26 | } 27 | 28 | { 29 | got := IsValidAddress(invalidAddress) 30 | expected := false 31 | 32 | if got != expected { 33 | t.Errorf("Expected %v, got %v", expected, got) 34 | } 35 | } 36 | 37 | { 38 | got := IsValidAddress(invalidAddress2) 39 | expected := false 40 | 41 | if got != expected { 42 | t.Errorf("Expected %v, got %v", expected, got) 43 | } 44 | } 45 | } 46 | 47 | func TestIsZeroAddress(t *testing.T) { 48 | t.Parallel() 49 | validAddress := common.HexToAddress("0x323b5d4c32345ced77393b3530b1eed0f346429d") 50 | zeroAddress := common.HexToAddress("0x0000000000000000000000000000000000000000") 51 | 52 | { 53 | isZeroAddress := IsZeroAddress(validAddress) 54 | 55 | if isZeroAddress { 56 | t.Error("Expected to be false") 57 | } 58 | } 59 | 60 | { 61 | isZeroAddress := IsZeroAddress(zeroAddress) 62 | 63 | if !isZeroAddress { 64 | t.Error("Expected to be true") 65 | } 66 | } 67 | 68 | { 69 | isZeroAddress := IsZeroAddress(validAddress.Hex()) 70 | 71 | if isZeroAddress { 72 | t.Error("Expected to be false") 73 | } 74 | } 75 | 76 | { 77 | isZeroAddress := IsZeroAddress(zeroAddress.Hex()) 78 | 79 | if !isZeroAddress { 80 | t.Error("Expected to be true") 81 | } 82 | } 83 | } 84 | 85 | func TestToWei(t *testing.T) { 86 | t.Parallel() 87 | amount := decimal.NewFromFloat(0.02) 88 | got := ToWei(amount, 18) 89 | expected := new(big.Int) 90 | expected.SetString("20000000000000000", 10) 91 | if got.Cmp(expected) != 0 { 92 | t.Errorf("Expected %s, got %s", expected, got) 93 | } 94 | } 95 | 96 | func TestToDecimal(t *testing.T) { 97 | t.Parallel() 98 | weiAmount := big.NewInt(0) 99 | weiAmount.SetString("20000000000000000", 10) 100 | ethAmount := ToDecimal(weiAmount, 18) 101 | f64, _ := ethAmount.Float64() 102 | expected := 0.02 103 | if f64 != expected { 104 | t.Errorf("%v does not equal expected %v", ethAmount, expected) 105 | } 106 | } 107 | 108 | func TestCalcGasLimit(t *testing.T) { 109 | t.Parallel() 110 | gasPrice := big.NewInt(0) 111 | gasPrice.SetString("2000000000", 10) 112 | gasLimit := uint64(21000) 113 | expected := big.NewInt(0) 114 | expected.SetString("42000000000000", 10) 115 | gasCost := CalcGasCost(gasLimit, gasPrice) 116 | if gasCost.Cmp(expected) != 0 { 117 | t.Errorf("expected %s, got %s", gasCost, expected) 118 | } 119 | } 120 | 121 | func TestSigRSV(t *testing.T) { 122 | t.Parallel() 123 | 124 | sig := "0x789a80053e4927d0a898db8e065e948f5cf086e32f9ccaa54c1908e22ac430c62621578113ddbb62d509bf6049b8fb544ab06d36f916685a2eb8e57ffadde02301" 125 | r, s, v := SigRSV(sig) 126 | expectedR := "789a80053e4927d0a898db8e065e948f5cf086e32f9ccaa54c1908e22ac430c6" 127 | expectedS := "2621578113ddbb62d509bf6049b8fb544ab06d36f916685a2eb8e57ffadde023" 128 | expectedV := uint8(28) 129 | if hexutil.Encode(r[:])[2:] != expectedR { 130 | t.FailNow() 131 | } 132 | if hexutil.Encode(s[:])[2:] != expectedS { 133 | t.FailNow() 134 | } 135 | if v != expectedV { 136 | t.FailNow() 137 | } 138 | } 139 | 140 | func TestExtractChaincode(t *testing.T) { 141 | t.Parallel() 142 | 143 | version := []byte{0x05, 0x06, 0x07, 0x08} 144 | validAddress := []byte("323b5d4c32345ced77393b3530b1eed0f346429d") 145 | chaincode := []byte("423b5d4c32345ced77393b3530b1eed1") 146 | parentFP := []byte{0x00, 0x00, 0x00, 0x00} 147 | 148 | fmt.Println(version, validAddress, chaincode) 149 | 150 | key := hdkeychain.NewExtendedKey(version, validAddress, chaincode[:32], parentFP, 0, 0, false) 151 | 152 | actualChaincode := ExtractChaincode(key) 153 | 154 | fmt.Println(actualChaincode) 155 | 156 | fmt.Println("Org Chaincode : ", string(chaincode[:32])) 157 | fmt.Println("Extracted Chaincode : ", string(actualChaincode)) 158 | 159 | if string(chaincode[:32]) != string(actualChaincode) { 160 | t.Error("chaincodes dont match") 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /wallet/account.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/btcsuite/btcd/chaincfg" 9 | hd "github.com/btcsuite/btcutil/hdkeychain" 10 | "github.com/ethereum/go-ethereum/accounts/keystore" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/core/types" 13 | "github.com/ethereum/go-ethereum/crypto" 14 | "github.com/tyler-smith/go-bip39" 15 | ) 16 | 17 | // EthAddress implements the WalletAddress interface 18 | type EthAddress struct { 19 | address *common.Address 20 | } 21 | 22 | // String representation of eth address 23 | func (addr EthAddress) String() string { 24 | return addr.address.Hex() // [2:] //String()[2:] 25 | } 26 | 27 | // EncodeAddress returns hex representation of the address 28 | func (addr EthAddress) EncodeAddress() string { 29 | return addr.address.Hex() // [2:] 30 | } 31 | 32 | // ScriptAddress returns byte representation of address 33 | func (addr EthAddress) ScriptAddress() []byte { 34 | return addr.address.Bytes() 35 | } 36 | 37 | // IsForNet returns true because EthAddress has to become btc.Address 38 | func (addr EthAddress) IsForNet(params *chaincfg.Params) bool { 39 | return true 40 | } 41 | 42 | // Account represents ethereum keystore 43 | type Account struct { 44 | // key *keystore.Key 45 | privateKey *ecdsa.PrivateKey 46 | address common.Address 47 | } 48 | 49 | // NewAccountFromKeyfile returns the account imported 50 | func NewAccountFromKeyfile(keyFile, password string) (*Account, error) { 51 | key, err := importKey(keyFile, password) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return &Account{ 57 | privateKey: key.PrivateKey, 58 | address: crypto.PubkeyToAddress(key.PrivateKey.PublicKey), 59 | }, nil 60 | } 61 | 62 | // NewAccountFromMnemonic returns generated account 63 | func NewAccountFromMnemonic(mnemonic, password string, params *chaincfg.Params) (*Account, error) { 64 | seed := bip39.NewSeed(mnemonic, password) 65 | 66 | /* 67 | fmt.Println(len(seed)) 68 | fmt.Println(seed) 69 | 70 | priv := new(ecdsa.PrivateKey) 71 | priv.PublicKey.Curve = btcec.S256() 72 | 73 | if 8*len(seed[:32]) != priv.Params().BitSize { 74 | fmt.Println("whoa....", 8*len(seed[:32]), priv.Params().BitSize) 75 | //return nil, fmt.Errorf("invalid length, need %d bits", priv.Params().BitSize) 76 | } 77 | 78 | */ 79 | 80 | // This is no longer used. 31 Dec 2018 81 | /* 82 | privateKeyECDSA, err := crypto.ToECDSA(seed[:32]) 83 | if err != nil { 84 | return nil, err 85 | } 86 | */ 87 | 88 | mPrivKey, err := hd.NewMaster(seed, params) 89 | if err != nil { 90 | log.Errorf("err initializing btc priv key : %v", err) 91 | return nil, err 92 | } 93 | 94 | exPrivKey, err := mPrivKey.ECPrivKey() 95 | if err != nil { 96 | log.Errorf("err extracting btcec priv key : %v", err) 97 | return nil, err 98 | } 99 | 100 | privateKeyECDSA := exPrivKey.ToECDSA() 101 | 102 | /* 103 | fmt.Println(privateKeyECDSA) 104 | fmt.Println(privateKeyECDSA.Public()) 105 | 106 | privateKeyBytes := crypto.FromECDSA(privateKeyECDSA) 107 | fmt.Println(hexutil.Encode(privateKeyBytes)[2:]) 108 | 109 | publicKey := privateKeyECDSA.Public() 110 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 111 | if !ok { 112 | log.Fatal("error casting public key to ECDSA") 113 | } 114 | 115 | publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) 116 | fmt.Println(hexutil.Encode(publicKeyBytes)[4:]) 117 | 118 | address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() 119 | fmt.Println("address : ", address) 120 | */ 121 | 122 | return &Account{privateKey: privateKeyECDSA, address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey)}, nil 123 | } 124 | 125 | func importKey(keyFile, password string) (*keystore.Key, error) { 126 | f, err := os.Open(keyFile) 127 | if err != nil { 128 | return nil, err 129 | } 130 | defer f.Close() 131 | json, err := ioutil.ReadAll(f) 132 | if err != nil { 133 | return nil, err 134 | } 135 | return keystore.DecryptKey(json, password) 136 | } 137 | 138 | // Address returns the eth address 139 | func (account *Account) Address() common.Address { 140 | return account.address 141 | } 142 | 143 | // SignTransaction will sign the txn 144 | func (account *Account) SignTransaction(signer types.Signer, tx *types.Transaction) (*types.Transaction, error) { 145 | signature, err := crypto.Sign(signer.Hash(tx).Bytes(), account.privateKey) 146 | if err != nil { 147 | return nil, err 148 | } 149 | return tx.WithSignature(signer, signature) 150 | } 151 | -------------------------------------------------------------------------------- /wallet/account_test.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | 9 | "github.com/OpenBazaar/go-ethwallet/util" 10 | ) 11 | 12 | const ( 13 | validKeyFile = "../test/UTC--2018-06-16T18-41-19.615987160Z--c0b4ef9e9d2806f643be94d2434e5c3d5cecd255" 14 | validPassword = "hotpotato" 15 | invalidKeyFile = "../test/UTC--IDontExist" 16 | invalidPassword = "lookout" 17 | mnemonicStr = "soup arch join universe table nasty fiber solve hotel luggage double clean tell oppose hurry weather isolate decline quick dune song enforce curious menu" // "wolf dragon lion stage rose snow sand snake kingdom hand daring flower foot walk sword" 18 | mnemonicStrAddress = "0x44Ae1C0955C7ad96700088Fb96906C72102c51E3" 19 | ) 20 | 21 | var validEthAddr EthAddress 22 | 23 | func setupAddr() { 24 | addr := common.HexToAddress(validSourceAddress) 25 | validEthAddr = EthAddress{ 26 | address: &addr, 27 | } 28 | } 29 | 30 | func TestNewAccountWithValidCredentials(t *testing.T) { 31 | account, err := NewAccountFromKeyfile(validKeyFile, validPassword) 32 | if err != nil { 33 | t.Errorf("valid keyfile should open properly") 34 | } 35 | if account.Address().String() == "" { 36 | t.Errorf("the account should have a valid address") 37 | } 38 | if account.Address().String() != validSourceAddress { 39 | t.Errorf("the account address is wrong") 40 | } 41 | } 42 | 43 | func TestNewAccountWithInValidKeyFile(t *testing.T) { 44 | account, err := NewAccountFromKeyfile(invalidKeyFile, validPassword) 45 | if err == nil { 46 | t.Errorf("invalid keyfile should not open properly") 47 | } 48 | if account != nil { 49 | t.Errorf("the account should not be returned") 50 | } 51 | } 52 | 53 | func TestNewAccountWithInValidPassword(t *testing.T) { 54 | account, err := NewAccountFromKeyfile(validKeyFile, invalidPassword) 55 | if err == nil { 56 | t.Errorf("invalid keyfile should not open properly") 57 | } 58 | if account != nil { 59 | t.Errorf("the account should not be returned") 60 | } 61 | } 62 | 63 | func TestNewAccountWithInValidCredentials(t *testing.T) { 64 | account, err := NewAccountFromKeyfile(invalidKeyFile, invalidPassword) 65 | if err == nil { 66 | t.Errorf("invalid keyfile should not open properly") 67 | } 68 | if account != nil { 69 | t.Errorf("the account should not be returned") 70 | } 71 | } 72 | 73 | func TestNewAccountWithMnemonic(t *testing.T) { 74 | account, err := NewAccountFromMnemonic(mnemonicStr, "", nil) 75 | if err != nil { 76 | t.Errorf("failed to open account from mnemonic: %v", err) 77 | } 78 | //fmt.Println(account.key.PrivateKey) 79 | //fmt.Println(account.Address()) 80 | //fmt.Println("account address : ", account.Address().String()) 81 | if account.Address().String() != mnemonicStrAddress { 82 | t.Errorf("failed to gen correct address from mnemonic: %v", err) 83 | } 84 | if !util.IsValidAddress(account.Address()) { 85 | t.Errorf("failed to gen valid address from mnemonic: %v", err) 86 | } 87 | } 88 | 89 | func TestEthAddress(t *testing.T) { 90 | setupAddr() 91 | //fmt.Println(validEthAddr.String()) 92 | if validEthAddr.String() != validSourceAddress { 93 | t.Errorf("address not initialized correctly") 94 | } 95 | if !bytes.Equal(validEthAddr.ScriptAddress(), []byte{192, 180, 239, 158, 157, 40, 6, 246, 67, 190, 148, 210, 67, 78, 92, 61, 92, 236, 210, 85}) { 96 | t.Errorf("address not initialized correctly") 97 | } 98 | if validEthAddr.EncodeAddress() != validSourceAddress[2:] { 99 | t.Errorf("address not initialized correctly") 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /wallet/client.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "math/big" 10 | "net/http" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | wi "github.com/OpenBazaar/wallet-interface" 16 | "github.com/ethereum/go-ethereum" 17 | "github.com/ethereum/go-ethereum/common" 18 | "github.com/ethereum/go-ethereum/core/types" 19 | "github.com/ethereum/go-ethereum/crypto" 20 | "github.com/ethereum/go-ethereum/ethclient" 21 | "github.com/gorilla/websocket" 22 | "github.com/hunterlong/tokenbalance" 23 | "github.com/nanmu42/etherscan-api" 24 | 25 | "github.com/OpenBazaar/go-ethwallet/util" 26 | ) 27 | 28 | /* 29 | !! Important URL information from Infura 30 | Mainnet JSON-RPC over HTTPs https://mainnet.infura.io/v3/YOUR-PROJECT-ID 31 | Mainnet JSON-RPC over websockets wss://mainnet.infura.io/ws/v3/YOUR-PROJECT-ID 32 | Ropsten JSON-RPC over HTTPS https://ropsten.infura.io/v3/YOUR-PROJECT-ID 33 | Ropsten JSON-RPC over websockets wss://ropsten.infura.io/ws/v3/YOUR-PROJECT-ID 34 | Rinkeby JSON-RPC over HTTPS https://rinkeby.infura.io/v3/YOUR-PROJECT-ID 35 | Rinkeby JSON-RPC over websockets wss://rinkeby.infura.io/ws/v3/YOUR-PROJECT-ID 36 | Kovan JSON-RPC over HTTPS https://kovan.infura.io/v3/YOUR-PROJECT-ID 37 | Kovan JSON-RPC over websockets wss://kovan.infura.io/ws/v3/YOUR-PROJECT-ID 38 | Görli JSON-RPC over HTTPS https://goerli.infura.io/v3/YOUR-PROJECT-ID 39 | Görli JSON-RPC over websockets wss://goerli.infura.io/ws/v3/YOUR-PROJECT-ID 40 | */ 41 | 42 | var wsURLTemplate = "wss://%s.infura.io/ws/%s" 43 | 44 | // EthClient represents the eth client 45 | type EthClient struct { 46 | *ethclient.Client 47 | eClient *etherscan.Client 48 | ws *websocket.Conn 49 | url string 50 | } 51 | 52 | var txns []wi.Txn 53 | var txnsLock sync.RWMutex 54 | 55 | // NewEthClient returns a new eth client 56 | // wss://mainnet.infura.io/ws/v3/YOUR-PROJECT-ID 57 | func NewEthClient(url string) (*EthClient, error) { 58 | var conn *ethclient.Client 59 | var econn *etherscan.Client 60 | var wsURL string 61 | if strings.Contains(url, "rinkeby") { 62 | econn = etherscan.New(etherscan.Rinkby, EtherScanAPIKey) 63 | wsURL = fmt.Sprintf(wsURLTemplate, "rinkeby", InfuraAPIKey) 64 | } else if strings.Contains(url, "ropsten") { 65 | econn = etherscan.New(etherscan.Ropsten, EtherScanAPIKey) 66 | wsURL = fmt.Sprintf(wsURLTemplate, "ropsten", InfuraAPIKey) 67 | } else { 68 | econn = etherscan.New(etherscan.Mainnet, EtherScanAPIKey) 69 | wsURL = fmt.Sprintf(wsURLTemplate, "mainnet", InfuraAPIKey) 70 | } 71 | var err error 72 | if conn, err = ethclient.Dial(url); err != nil { 73 | return nil, err 74 | } 75 | ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil) 76 | if err != nil { 77 | log.Errorf("eth wallet unable to open ws conn: %v", err) 78 | ws = nil 79 | } 80 | return &EthClient{ 81 | Client: conn, 82 | eClient: econn, 83 | url: url, 84 | ws: ws, 85 | }, nil 86 | 87 | } 88 | 89 | // Transfer will transfer eth from this user account to dest address 90 | func (client *EthClient) Transfer(from *Account, destAccount common.Address, value *big.Int, spendAll bool, fee big.Int) (common.Hash, error) { 91 | var err error 92 | fromAddress := from.Address() 93 | nonce, err := client.PendingNonceAt(context.Background(), fromAddress) 94 | if err != nil { 95 | return common.BytesToHash([]byte{}), err 96 | } 97 | 98 | gasPrice, err := client.SuggestGasPrice(context.Background()) 99 | if err != nil { 100 | return common.BytesToHash([]byte{}), err 101 | } 102 | 103 | if gasPrice.Int64() < fee.Int64() { 104 | gasPrice = &fee 105 | } 106 | 107 | tvalue := value 108 | 109 | msg := ethereum.CallMsg{From: fromAddress, Value: tvalue} 110 | gasLimit, err := client.EstimateGas(context.Background(), msg) 111 | if err != nil { 112 | return common.BytesToHash([]byte{}), err 113 | } 114 | 115 | // if spend all then we need to set the value = confirmedBalance - gas 116 | if spendAll { 117 | currentBalance, err := client.GetBalance(fromAddress) 118 | if err != nil { 119 | //currentBalance = big.NewInt(0) 120 | return common.BytesToHash([]byte{}), err 121 | } 122 | gas := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasLimit))) 123 | 124 | if currentBalance.Cmp(gas) >= 0 { 125 | tvalue = new(big.Int).Sub(currentBalance, gas) 126 | } 127 | } 128 | 129 | rawTx := types.NewTransaction(nonce, destAccount, tvalue, gasLimit, gasPrice, nil) 130 | signedTx, err := from.SignTransaction(types.HomesteadSigner{}, rawTx) 131 | if err != nil { 132 | return common.BytesToHash([]byte{}), err 133 | } 134 | txns = append(txns, wi.Txn{ 135 | Txid: signedTx.Hash().Hex(), 136 | Value: tvalue.String(), 137 | Height: int32(nonce), 138 | Timestamp: time.Now(), 139 | WatchOnly: false, 140 | Bytes: rawTx.Data()}) 141 | return signedTx.Hash(), client.SendTransaction(context.Background(), signedTx) 142 | } 143 | 144 | // TransferToken will transfer erc20 token from this user account to dest address 145 | func (client *EthClient) TransferToken(from *Account, toAddress common.Address, tokenAddress common.Address, value *big.Int) (common.Hash, error) { 146 | var err error 147 | fromAddress := from.Address() 148 | nonce, err := client.PendingNonceAt(context.Background(), fromAddress) 149 | if err != nil { 150 | return common.BytesToHash([]byte{}), err 151 | } 152 | 153 | gasPrice, err := client.SuggestGasPrice(context.Background()) 154 | if err != nil { 155 | return common.BytesToHash([]byte{}), err 156 | } 157 | 158 | transferFnSignature := []byte("transfer(address,uint256)") 159 | methodID := crypto.Keccak256(transferFnSignature)[:4] 160 | paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32) 161 | paddedAmount := common.LeftPadBytes(value.Bytes(), 32) 162 | 163 | var data []byte 164 | data = append(data, methodID...) 165 | data = append(data, paddedAddress...) 166 | data = append(data, paddedAmount...) 167 | 168 | gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{ 169 | To: &toAddress, 170 | Data: data, 171 | }) 172 | if err != nil { 173 | return common.BytesToHash([]byte{}), err 174 | } 175 | rawTx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data) 176 | signedTx, err := from.SignTransaction(types.HomesteadSigner{}, rawTx) //types.SignTx(tx, types.HomesteadSigner{}, privateKey) 177 | if err != nil { 178 | return common.BytesToHash([]byte{}), err 179 | } 180 | txns = append(txns, wi.Txn{ 181 | Txid: signedTx.Hash().Hex(), 182 | Value: value.String(), 183 | Height: int32(nonce), 184 | Timestamp: time.Now(), 185 | WatchOnly: false, 186 | Bytes: rawTx.Data()}) 187 | return signedTx.Hash(), client.SendTransaction(context.Background(), signedTx) 188 | } 189 | 190 | // GetBalance - returns the balance for this account 191 | func (client *EthClient) GetBalance(destAccount common.Address) (*big.Int, error) { 192 | return client.BalanceAt(context.Background(), destAccount, nil) 193 | } 194 | 195 | // GetTokenBalance - returns the erc20 token balance for this account 196 | func (client *EthClient) GetTokenBalance(destAccount, tokenAddress common.Address) (*big.Int, error) { 197 | configs := &tokenbalance.Config{ 198 | GethLocation: client.url, 199 | Logs: true, 200 | } 201 | if err := configs.Connect(); err != nil { 202 | return nil, err 203 | } 204 | 205 | // insert a Token Contract address and Wallet address 206 | contract := tokenAddress.String() 207 | wallet := destAccount.String() 208 | 209 | // query the blockchain and wallet details 210 | token, err := tokenbalance.New(contract, wallet) 211 | return token.Balance, err 212 | } 213 | 214 | // GetUnconfirmedBalance - returns the unconfirmed balance for this account 215 | func (client *EthClient) GetUnconfirmedBalance(destAccount common.Address) (*big.Int, error) { 216 | return client.PendingBalanceAt(context.Background(), destAccount) 217 | } 218 | 219 | // GetTransaction - returns a eth txn for the specified hash 220 | func (client *EthClient) GetTransaction(hash common.Hash) (*types.Transaction, bool, error) { 221 | return client.TransactionByHash(context.Background(), hash) 222 | } 223 | 224 | // GetLatestBlock - returns the latest block 225 | func (client *EthClient) GetLatestBlock() (uint32, common.Hash, error) { 226 | header, err := client.HeaderByNumber(context.Background(), nil) 227 | if err != nil { 228 | return 0, common.BytesToHash([]byte{}), err 229 | } 230 | return uint32(header.Number.Int64()), header.Hash(), nil 231 | } 232 | 233 | // EstimateTxnGas - returns estimated gas 234 | func (client *EthClient) EstimateTxnGas(from, to common.Address, value *big.Int) (*big.Int, error) { 235 | gas := big.NewInt(0) 236 | if !(util.IsValidAddress(from.String()) && util.IsValidAddress(to.String())) { 237 | return gas, errors.New("invalid address") 238 | } 239 | 240 | gasPrice, err := client.SuggestGasPrice(context.Background()) 241 | if err != nil { 242 | return gas, err 243 | } 244 | msg := ethereum.CallMsg{From: from, To: &to, Value: value} 245 | gasLimit, err := client.EstimateGas(context.Background(), msg) 246 | if err != nil { 247 | return gas, err 248 | } 249 | return gas.Mul(big.NewInt(int64(gasLimit)), gasPrice), nil 250 | } 251 | 252 | // EstimateGasSpend - returns estimated gas 253 | func (client *EthClient) EstimateGasSpend(from common.Address, value *big.Int) (*big.Int, error) { 254 | gas := big.NewInt(0) 255 | gasPrice, err := client.SuggestGasPrice(context.Background()) 256 | if err != nil { 257 | return gas, err 258 | } 259 | msg := ethereum.CallMsg{From: from, Value: value} 260 | gasLimit, err := client.EstimateGas(context.Background(), msg) 261 | if err != nil { 262 | return gas, err 263 | } 264 | return gas.Mul(big.NewInt(int64(gasLimit)), gasPrice), nil 265 | } 266 | 267 | // GetTxnNonce - used to fetch nonce for a submitted txn 268 | func (client *EthClient) GetTxnNonce(txID string) (int32, error) { 269 | txnsLock.Lock() 270 | defer txnsLock.Unlock() 271 | for _, txn := range txns { 272 | if txn.Txid == txID { 273 | return txn.Height, nil 274 | } 275 | } 276 | return 0, errors.New("nonce not found") 277 | } 278 | 279 | /* 280 | func getClient() (*ethclient.Client, error) { 281 | client, err := ethclient.Dial("https://mainnet.infura.io") 282 | if err != nil { 283 | log.Info("error initializing client") 284 | } 285 | 286 | return client, err 287 | } 288 | */ 289 | 290 | // EthGasStationData represents ethgasstation api data 291 | // https://ethgasstation.info/json/ethgasAPI.json 292 | // {"average": 20.0, "fastestWait": 0.4, "fastWait": 0.4, "fast": 200.0, 293 | // "safeLowWait": 10.6, "blockNum": 6684733, "avgWait": 2.0, 294 | // "block_time": 13.056701030927835, "speed": 0.7529715304081577, 295 | // "fastest": 410.0, "safeLow": 17.0} 296 | type EthGasStationData struct { 297 | Average float64 `json:"average"` 298 | FastestWait float64 `json:"fastestWait"` 299 | FastWait float64 `json:"fastWeight"` 300 | Fast float64 `json:"Fast"` 301 | SafeLowWait float64 `json:"safeLowWait"` 302 | BlockNum int64 `json:"blockNum"` 303 | AvgWait float64 `json:"avgWait"` 304 | BlockTime float64 `json:"block_time"` 305 | Speed float64 `json:"speed"` 306 | Fastest float64 `json:"fastest"` 307 | SafeLow float64 `json:"safeLow"` 308 | } 309 | 310 | // GetEthGasStationEstimate get the latest data 311 | // from https://ethgasstation.info/json/ethgasAPI.json 312 | func (client *EthClient) GetEthGasStationEstimate() (*EthGasStationData, error) { 313 | res, err := http.Get("https://ethgasstation.info/json/ethgasAPI.json") 314 | if err != nil { 315 | return nil, err 316 | } 317 | body, err := ioutil.ReadAll(res.Body) 318 | if err != nil { 319 | return nil, err 320 | } 321 | var s = new(EthGasStationData) 322 | err = json.Unmarshal(body, &s) 323 | if err != nil { 324 | return nil, err 325 | } 326 | return s, nil 327 | } 328 | 329 | func init() { 330 | txns = []wi.Txn{} 331 | } 332 | -------------------------------------------------------------------------------- /wallet/client_test.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | "testing" 8 | "time" 9 | 10 | "github.com/davecgh/go-spew/spew" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/core/types" 13 | "github.com/joho/godotenv" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | const invalidInfuraKey = "IAMNOTREAL" 18 | const validSourceAddress = "0xc0B4ef9E9d2806F643be94d2434e5C3d5cEcd255" 19 | const validDestinationAddress = "0xcecb952de5b23950b15bfd49302d1bdd25f9ee67" 20 | 21 | const validTxn1 = "0xae818b782ce2d5ef8160de1d022440fdaf92cf91d4cd444eb23c6b6a55240c5b" 22 | 23 | var client *EthClient 24 | var err error 25 | 26 | var validInfuraKey string 27 | var validurlsTest map[string]bool 28 | var invalidurlsTest map[string]bool 29 | var logicallyinvalidurlsTest map[string]bool 30 | var plainurlsTest map[string]bool 31 | 32 | var n uint32 33 | var bal *big.Int 34 | var ropstenURL string 35 | 36 | var validRopstenTxn types.Transaction 37 | 38 | func init() { 39 | err := godotenv.Load("../.env") 40 | if err != nil { 41 | log.Fatal("Error loading .env file") 42 | } 43 | validInfuraKey = InfuraAPIKey // os.Getenv("INFURA_KEY") 44 | fmt.Println("valid infura key is : ", validInfuraKey) 45 | 46 | ropstenURL = fmt.Sprintf("https://ropsten.infura.io/%s", validInfuraKey) 47 | 48 | validRopstenTxn = *types.NewTransaction(0, common.HexToAddress(validDestinationAddress), 49 | big.NewInt(3344556677), 53000, big.NewInt(1000000000), 50 | []byte("f86780843b9aca0082cf0894cecb952de5b23950b15bfd49302d1bdd25f9ee6784c759e285801ca056dfa0ed4e028d2f2307c421bbe0ebe7516e5fdd140ff080091cb03137a885f8a03197486094c53edb2aaefb6ba5059dd6c82fc709134c93c0c422cb671a139352")) 51 | } 52 | 53 | func setup() { 54 | 55 | validurlsTest = map[string]bool{ 56 | fmt.Sprintf("https://ropsten.infura.io/%s", validInfuraKey): true, 57 | fmt.Sprintf("https://rinkeby.infura.io/%s", validInfuraKey): true, 58 | fmt.Sprintf("https://mainnet.infura.io/%s", validInfuraKey): true, 59 | fmt.Sprintf("https://ropsten.infura.io/%s", invalidInfuraKey): false, 60 | fmt.Sprintf("https://rinkeby.infura.io/%s", invalidInfuraKey): false, 61 | fmt.Sprintf("https://mainnet.infura.io/%s", invalidInfuraKey): false, 62 | } 63 | 64 | invalidurlsTest = map[string]bool{ 65 | fmt.Sprintf("innet.infura.io/%s", invalidInfuraKey): false, 66 | "innet.infura.io/": false, 67 | } 68 | 69 | logicallyinvalidurlsTest = map[string]bool{ 70 | fmt.Sprintf("https://ropstenFTW.infura.io/%s", validInfuraKey): true, 71 | } 72 | 73 | plainurlsTest = map[string]bool{ 74 | "https://ropsten.infura.io/": false, 75 | "https://rinkeby.infura.io/": false, 76 | "https://mainnet.infura.io/": false, 77 | } 78 | 79 | n = 0 80 | 81 | } 82 | 83 | func TestNewClient(t *testing.T) { 84 | setup() 85 | t.Parallel() 86 | 87 | if validInfuraKey == "" { 88 | t.Error("no infura key specified") 89 | } 90 | 91 | for baseURL := range validurlsTest { 92 | _, err = NewEthClient(baseURL) 93 | if err != nil { 94 | t.Errorf("client should have initialized") 95 | } 96 | } 97 | 98 | for baseURL := range plainurlsTest { 99 | _, err = NewEthClient(baseURL) 100 | if err != nil { 101 | t.Errorf("client should have initialized") 102 | } 103 | } 104 | 105 | for baseURL := range logicallyinvalidurlsTest { 106 | _, err = NewEthClient(baseURL) 107 | if err != nil { 108 | t.Errorf("client should have initialized") 109 | } 110 | } 111 | 112 | for baseURL := range invalidurlsTest { 113 | _, err = NewEthClient(baseURL) 114 | if err == nil { 115 | t.Errorf(baseURL + " client should not have initialized") 116 | } 117 | } 118 | } 119 | 120 | func TestGetLatestBlock(t *testing.T) { 121 | setup() 122 | t.Parallel() 123 | 124 | for baseURL := range validurlsTest { 125 | client, err = NewEthClient(baseURL) 126 | if err != nil { 127 | t.Errorf("client should have initialized") 128 | } 129 | n, _, err = client.GetLatestBlock() 130 | if err != nil || n <= 0 { 131 | t.Errorf("client should have fetched block number") 132 | } 133 | } 134 | 135 | for baseURL := range plainurlsTest { 136 | client, err = NewEthClient(baseURL) 137 | if err != nil { 138 | t.Errorf("client should have initialized") 139 | } 140 | n, _, err = client.GetLatestBlock() 141 | if err != nil || n <= 0 { 142 | t.Errorf("client should have fetched block number") 143 | } 144 | } 145 | 146 | for baseURL := range logicallyinvalidurlsTest { 147 | client, err = NewEthClient(baseURL) 148 | if err != nil { 149 | t.Errorf("client should have initialized") 150 | } 151 | n, _, err = client.GetLatestBlock() 152 | if err == nil || n > 0 { 153 | t.Errorf("client should not have fetched block number") 154 | } 155 | } 156 | } 157 | 158 | func TestGetBalance(t *testing.T) { 159 | setup() 160 | t.Parallel() 161 | 162 | addr := common.HexToAddress(validSourceAddress) 163 | 164 | for baseURL := range validurlsTest { 165 | client, err = NewEthClient(baseURL) 166 | if err != nil { 167 | t.Errorf("client should have initialized") 168 | } 169 | bal, err = client.GetBalance(addr) 170 | if err != nil || bal == nil { 171 | t.Errorf("client should have fetched balance") 172 | } 173 | } 174 | 175 | for baseURL := range plainurlsTest { 176 | client, err = NewEthClient(baseURL) 177 | if err != nil { 178 | t.Errorf("client should have initialized") 179 | } 180 | bal, err = client.GetBalance(addr) 181 | if err != nil || bal == nil { 182 | t.Errorf("client should have fetched balance") 183 | } 184 | } 185 | 186 | for baseURL := range logicallyinvalidurlsTest { 187 | client, err = NewEthClient(baseURL) 188 | if err != nil { 189 | t.Errorf("client should have initialized") 190 | } 191 | bal, err = client.GetBalance(addr) 192 | if err == nil && bal != nil { 193 | t.Errorf("client should not have fetched balance") 194 | } 195 | } 196 | } 197 | 198 | func TestGetUnconfirmedBalance(t *testing.T) { 199 | setup() 200 | t.Parallel() 201 | 202 | addr := common.HexToAddress(validSourceAddress) 203 | 204 | for baseURL := range validurlsTest { 205 | client, err = NewEthClient(baseURL) 206 | if err != nil { 207 | t.Errorf("client should have initialized") 208 | } 209 | bal, err = client.GetUnconfirmedBalance(addr) 210 | if err != nil || bal == nil { 211 | t.Errorf("client should have fetched unconfirmed balance") 212 | } 213 | } 214 | 215 | for baseURL := range plainurlsTest { 216 | client, err = NewEthClient(baseURL) 217 | if err != nil { 218 | t.Errorf("client should have initialized") 219 | } 220 | bal, err = client.GetUnconfirmedBalance(addr) 221 | if err != nil || bal == nil { 222 | t.Errorf("client should have fetched unconfirmed balance") 223 | } 224 | } 225 | 226 | for baseURL := range logicallyinvalidurlsTest { 227 | client, err = NewEthClient(baseURL) 228 | if err != nil { 229 | t.Errorf("client should have initialized") 230 | } 231 | bal, err = client.GetUnconfirmedBalance(addr) 232 | if err == nil && bal != nil { 233 | t.Errorf("client should not have fetched unconfirmed balance") 234 | } 235 | } 236 | } 237 | 238 | func TestEstimateTxnGas(t *testing.T) { 239 | setup() 240 | t.Parallel() 241 | 242 | addr := common.HexToAddress(validSourceAddress) 243 | dest := common.HexToAddress(validDestinationAddress) 244 | value := big.NewInt(200000) 245 | 246 | for baseURL := range validurlsTest { 247 | client, err = NewEthClient(baseURL) 248 | if err != nil { 249 | t.Errorf("client should have initialized") 250 | } 251 | bal, err = client.EstimateTxnGas(addr, dest, value) 252 | if err != nil || bal == nil { 253 | t.Errorf("client should have estimated txn gas") 254 | } 255 | } 256 | 257 | for baseURL := range plainurlsTest { 258 | client, err = NewEthClient(baseURL) 259 | if err != nil { 260 | t.Errorf("client should have initialized") 261 | } 262 | bal, err = client.EstimateTxnGas(addr, dest, value) 263 | if err != nil || bal == nil { 264 | t.Errorf("client should have estimated txn gas") 265 | } 266 | } 267 | 268 | for baseURL := range logicallyinvalidurlsTest { 269 | client, err = NewEthClient(baseURL) 270 | if err != nil { 271 | t.Errorf("client should have initialized") 272 | } 273 | bal, err = client.EstimateTxnGas(addr, dest, value) 274 | if err == nil && bal != nil { 275 | t.Errorf("client should not have estimated txn gas") 276 | } 277 | } 278 | } 279 | 280 | func TestEstimateGasSpend(t *testing.T) { 281 | setup() 282 | t.Parallel() 283 | 284 | addr := common.HexToAddress(validSourceAddress) 285 | value := big.NewInt(200000) 286 | 287 | for baseURL := range validurlsTest { 288 | client, err = NewEthClient(baseURL) 289 | if err != nil { 290 | t.Errorf("client should have initialized") 291 | } 292 | bal, err = client.EstimateGasSpend(addr, value) 293 | if err != nil || bal == nil { 294 | t.Errorf("client should have estimated txn gas") 295 | } 296 | } 297 | 298 | for baseURL := range plainurlsTest { 299 | client, err = NewEthClient(baseURL) 300 | if err != nil { 301 | t.Errorf("client should have initialized") 302 | } 303 | bal, err = client.EstimateGasSpend(addr, value) 304 | if err != nil || bal == nil { 305 | t.Errorf("client should have estimated txn gas") 306 | } 307 | } 308 | 309 | for baseURL := range logicallyinvalidurlsTest { 310 | client, err = NewEthClient(baseURL) 311 | if err != nil { 312 | t.Errorf("client should have initialized") 313 | } 314 | bal, err = client.EstimateGasSpend(addr, value) 315 | if err == nil && bal != nil { 316 | t.Errorf("client should not have estimated txn gas") 317 | } 318 | } 319 | } 320 | 321 | func TestValidGetTransaction(t *testing.T) { 322 | t.Parallel() 323 | 324 | client, err := NewEthClient(ropstenURL) 325 | if err != nil { 326 | t.Errorf("client should have initialized") 327 | } 328 | txn, isPending, err := client.GetTransaction(common.HexToHash(validTxn1)) 329 | spew.Println(txn) 330 | 331 | if err != nil { 332 | t.Errorf("txn should have been correctly fetched") 333 | } 334 | 335 | if isPending { 336 | t.Errorf("non pending txn should have been correctly fetched") 337 | } 338 | 339 | if validRopstenTxn.Nonce() != txn.Nonce() { 340 | t.Errorf("txn should have been correctly fetched") 341 | } 342 | 343 | if validRopstenTxn.Gas() != txn.Gas() { 344 | t.Errorf("txn should have been correctly fetched") 345 | } 346 | 347 | if validRopstenTxn.GasPrice().Cmp(txn.GasPrice()) != 0 { 348 | t.Errorf("txn should have been correctly fetched") 349 | } 350 | 351 | } 352 | 353 | func TestInvalidGetTransaction(t *testing.T) { 354 | t.Parallel() 355 | 356 | client, err := NewEthClient(ropstenURL) 357 | if err != nil { 358 | t.Errorf("client should have initialized") 359 | } 360 | txn, _, err := client.GetTransaction(common.HexToHash("ooommm")) 361 | if err == nil || txn != nil { 362 | t.Errorf("invalid txn should not have been fetched") 363 | } 364 | } 365 | 366 | func TestTransfer(t *testing.T) { 367 | //t.SkipNow() 368 | client, err := NewEthClient(ropstenURL) 369 | if err != nil { 370 | t.Errorf("client should have initialized") 371 | } 372 | addr := common.HexToAddress(validSourceAddress) 373 | account, err := NewAccountFromKeyfile("../test/UTC--2018-06-16T18-41-19.615987160Z--c0b4ef9e9d2806f643be94d2434e5c3d5cecd255", "hotpotato") 374 | if err != nil { 375 | t.Errorf("account should have initialized") 376 | } 377 | dest := common.HexToAddress(validDestinationAddress) 378 | value := big.NewInt(20000000000000) 379 | 380 | var bal1, bal2, sbal1, dbal1, sbal2, dbal2, val *big.Int 381 | var txn *types.Transaction 382 | 383 | bal1, err = client.GetBalance(addr) 384 | if err != nil || bal1 == nil { 385 | t.Errorf("client should have fetched balance") 386 | } 387 | 388 | bal2, err = client.GetUnconfirmedBalance(addr) 389 | if err != nil || bal2 == nil { 390 | t.Errorf("client should have fetched balance") 391 | } 392 | 393 | sbal1 = big.NewInt(0) 394 | 395 | // get the source balance 396 | sbal1.Add(bal1, big.NewInt(0)) 397 | 398 | bal1, err = client.GetBalance(dest) 399 | if err != nil { 400 | t.Errorf("client should have fetched balance") 401 | } 402 | 403 | bal2, err = client.GetUnconfirmedBalance(dest) 404 | if err != nil { 405 | t.Errorf("client should have fetched balance") 406 | } 407 | 408 | dbal1 = big.NewInt(0) 409 | 410 | // get the dest balance 411 | dbal1.Add(bal1, big.NewInt(0)) 412 | 413 | hash, err := client.Transfer(account, dest, value, false) 414 | if err != nil { 415 | t.Errorf("client should transfer : %v", err) 416 | } 417 | 418 | txn, _, err = client.GetTransaction(hash) 419 | if err != nil { 420 | t.Errorf("txn should have been correctly fetched") 421 | } 422 | 423 | if txn.Value().Cmp(value) != 0 { 424 | t.Errorf("client should transfer correct amount") 425 | } 426 | 427 | bal1, err = client.GetBalance(addr) 428 | if err != nil { 429 | t.Errorf("client should have fetched balance") 430 | } 431 | 432 | bal2, err = client.GetUnconfirmedBalance(addr) 433 | if err != nil { 434 | t.Errorf("client should have fetched balance") 435 | } 436 | 437 | time.Sleep(2 * time.Minute) 438 | 439 | sbal2 = big.NewInt(0) 440 | 441 | // get the source balance 442 | sbal2.Add(bal1, big.NewInt(0)) 443 | 444 | bal1, err = client.GetBalance(dest) 445 | if err != nil { 446 | t.Errorf("client should have fetched balance") 447 | } 448 | 449 | bal2, err = client.GetUnconfirmedBalance(dest) 450 | if err != nil { 451 | t.Errorf("client should have fetched balance") 452 | } 453 | 454 | dbal2 = big.NewInt(0) 455 | val = big.NewInt(0) 456 | 457 | // get the dest balance 458 | dbal2.Add(bal1, big.NewInt(0)) 459 | 460 | val.Sub(dbal2, dbal1) 461 | 462 | fmt.Println("before : ", dbal1.Int64(), " after : ", dbal2.Int64(), " value : ", value.Int64()) 463 | 464 | if val.Cmp(value) != 0 { 465 | t.Errorf("client should have transferred balance") 466 | } 467 | } 468 | 469 | func TestEthGasStationFetch(t *testing.T) { 470 | client, err := NewEthClient(validRinkebyURL) 471 | if err != nil { 472 | t.Errorf("client should have initialized") 473 | } 474 | 475 | data, err := client.GetEthGasStationEstimate() 476 | 477 | if err != nil { 478 | fmt.Println(err) 479 | return 480 | } 481 | 482 | spew.Dump(data) 483 | fmt.Println("avg : ", int64(data.Average*1000000000)) 484 | 485 | est, err := client.SuggestGasPrice(context.Background()) 486 | 487 | if err != nil { 488 | fmt.Println(err) 489 | return 490 | } 491 | 492 | fmt.Println(est.Int64()) 493 | 494 | } 495 | -------------------------------------------------------------------------------- /wallet/erc20.go: -------------------------------------------------------------------------------- 1 | // Code generated - DO NOT EDIT. 2 | // This file is a generated binding and any manual changes will be lost. 3 | 4 | package wallet 5 | 6 | import ( 7 | "math/big" 8 | "strings" 9 | 10 | ethereum "github.com/ethereum/go-ethereum" 11 | "github.com/ethereum/go-ethereum/accounts/abi" 12 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 13 | "github.com/ethereum/go-ethereum/common" 14 | "github.com/ethereum/go-ethereum/core/types" 15 | "github.com/ethereum/go-ethereum/event" 16 | ) 17 | 18 | // TokenABI is the input ABI used to generate the binding from. 19 | const TokenABI = "[{\"constant\":true,\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_spender\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"name\":\"\",\"type\":\"uint8\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"},{\"name\":\"_spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"inputs\":[],\"payable\":false,\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_spender\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"}]" 20 | 21 | // Token is an auto generated Go binding around an Ethereum contract. 22 | type Token struct { 23 | TokenCaller // Read-only binding to the contract 24 | TokenTransactor // Write-only binding to the contract 25 | TokenFilterer // Log filterer for contract events 26 | } 27 | 28 | // TokenCaller is an auto generated read-only Go binding around an Ethereum contract. 29 | type TokenCaller struct { 30 | contract *bind.BoundContract // Generic contract wrapper for the low level calls 31 | } 32 | 33 | // TokenTransactor is an auto generated write-only Go binding around an Ethereum contract. 34 | type TokenTransactor struct { 35 | contract *bind.BoundContract // Generic contract wrapper for the low level calls 36 | } 37 | 38 | // TokenFilterer is an auto generated log filtering Go binding around an Ethereum contract events. 39 | type TokenFilterer struct { 40 | contract *bind.BoundContract // Generic contract wrapper for the low level calls 41 | } 42 | 43 | // TokenSession is an auto generated Go binding around an Ethereum contract, 44 | // with pre-set call and transact options. 45 | type TokenSession struct { 46 | Contract *Token // Generic contract binding to set the session for 47 | CallOpts bind.CallOpts // Call options to use throughout this session 48 | TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session 49 | } 50 | 51 | // TokenCallerSession is an auto generated read-only Go binding around an Ethereum contract, 52 | // with pre-set call options. 53 | type TokenCallerSession struct { 54 | Contract *TokenCaller // Generic contract caller binding to set the session for 55 | CallOpts bind.CallOpts // Call options to use throughout this session 56 | } 57 | 58 | // TokenTransactorSession is an auto generated write-only Go binding around an Ethereum contract, 59 | // with pre-set transact options. 60 | type TokenTransactorSession struct { 61 | Contract *TokenTransactor // Generic contract transactor binding to set the session for 62 | TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session 63 | } 64 | 65 | // TokenRaw is an auto generated low-level Go binding around an Ethereum contract. 66 | type TokenRaw struct { 67 | Contract *Token // Generic contract binding to access the raw methods on 68 | } 69 | 70 | // TokenCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. 71 | type TokenCallerRaw struct { 72 | Contract *TokenCaller // Generic read-only contract binding to access the raw methods on 73 | } 74 | 75 | // TokenTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. 76 | type TokenTransactorRaw struct { 77 | Contract *TokenTransactor // Generic write-only contract binding to access the raw methods on 78 | } 79 | 80 | // NewToken creates a new instance of Token, bound to a specific deployed contract. 81 | func NewToken(address common.Address, backend bind.ContractBackend) (*Token, error) { 82 | contract, err := bindToken(address, backend, backend, backend) 83 | if err != nil { 84 | return nil, err 85 | } 86 | return &Token{TokenCaller: TokenCaller{contract: contract}, TokenTransactor: TokenTransactor{contract: contract}, TokenFilterer: TokenFilterer{contract: contract}}, nil 87 | } 88 | 89 | // NewTokenCaller creates a new read-only instance of Token, bound to a specific deployed contract. 90 | func NewTokenCaller(address common.Address, caller bind.ContractCaller) (*TokenCaller, error) { 91 | contract, err := bindToken(address, caller, nil, nil) 92 | if err != nil { 93 | return nil, err 94 | } 95 | return &TokenCaller{contract: contract}, nil 96 | } 97 | 98 | // NewTokenTransactor creates a new write-only instance of Token, bound to a specific deployed contract. 99 | func NewTokenTransactor(address common.Address, transactor bind.ContractTransactor) (*TokenTransactor, error) { 100 | contract, err := bindToken(address, nil, transactor, nil) 101 | if err != nil { 102 | return nil, err 103 | } 104 | return &TokenTransactor{contract: contract}, nil 105 | } 106 | 107 | // NewTokenFilterer creates a new log filterer instance of Token, bound to a specific deployed contract. 108 | func NewTokenFilterer(address common.Address, filterer bind.ContractFilterer) (*TokenFilterer, error) { 109 | contract, err := bindToken(address, nil, nil, filterer) 110 | if err != nil { 111 | return nil, err 112 | } 113 | return &TokenFilterer{contract: contract}, nil 114 | } 115 | 116 | // bindToken binds a generic wrapper to an already deployed contract. 117 | func bindToken(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { 118 | parsed, err := abi.JSON(strings.NewReader(TokenABI)) 119 | if err != nil { 120 | return nil, err 121 | } 122 | return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil 123 | } 124 | 125 | // Call invokes the (constant) contract method with params as input values and 126 | // sets the output to result. The result type might be a single field for simple 127 | // returns, a slice of interfaces for anonymous returns and a struct for named 128 | // returns. 129 | func (_Token *TokenRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { 130 | return _Token.Contract.TokenCaller.contract.Call(opts, result, method, params...) 131 | } 132 | 133 | // Transfer initiates a plain transaction to move funds to the contract, calling 134 | // its default method if one is available. 135 | func (_Token *TokenRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { 136 | return _Token.Contract.TokenTransactor.contract.Transfer(opts) 137 | } 138 | 139 | // Transact invokes the (paid) contract method with params as input values. 140 | func (_Token *TokenRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { 141 | return _Token.Contract.TokenTransactor.contract.Transact(opts, method, params...) 142 | } 143 | 144 | // Call invokes the (constant) contract method with params as input values and 145 | // sets the output to result. The result type might be a single field for simple 146 | // returns, a slice of interfaces for anonymous returns and a struct for named 147 | // returns. 148 | func (_Token *TokenCallerRaw) Call(opts *bind.CallOpts, result interface{}, method string, params ...interface{}) error { 149 | return _Token.Contract.contract.Call(opts, result, method, params...) 150 | } 151 | 152 | // Transfer initiates a plain transaction to move funds to the contract, calling 153 | // its default method if one is available. 154 | func (_Token *TokenTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { 155 | return _Token.Contract.contract.Transfer(opts) 156 | } 157 | 158 | // Transact invokes the (paid) contract method with params as input values. 159 | func (_Token *TokenTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { 160 | return _Token.Contract.contract.Transact(opts, method, params...) 161 | } 162 | 163 | // Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. 164 | // 165 | // Solidity: function allowance(_owner address, _spender address) constant returns(uint256) 166 | func (_Token *TokenCaller) Allowance(opts *bind.CallOpts, _owner common.Address, _spender common.Address) (*big.Int, error) { 167 | var ( 168 | ret0 = new(*big.Int) 169 | ) 170 | out := ret0 171 | err := _Token.contract.Call(opts, out, "allowance", _owner, _spender) 172 | return *ret0, err 173 | } 174 | 175 | // Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. 176 | // 177 | // Solidity: function allowance(_owner address, _spender address) constant returns(uint256) 178 | func (_Token *TokenSession) Allowance(_owner common.Address, _spender common.Address) (*big.Int, error) { 179 | return _Token.Contract.Allowance(&_Token.CallOpts, _owner, _spender) 180 | } 181 | 182 | // Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. 183 | // 184 | // Solidity: function allowance(_owner address, _spender address) constant returns(uint256) 185 | func (_Token *TokenCallerSession) Allowance(_owner common.Address, _spender common.Address) (*big.Int, error) { 186 | return _Token.Contract.Allowance(&_Token.CallOpts, _owner, _spender) 187 | } 188 | 189 | // BalanceOf is a free data retrieval call binding the contract method 0x70a08231. 190 | // 191 | // Solidity: function balanceOf(_owner address) constant returns(uint256) 192 | func (_Token *TokenCaller) BalanceOf(opts *bind.CallOpts, _owner common.Address) (*big.Int, error) { 193 | var ( 194 | ret0 = new(*big.Int) 195 | ) 196 | out := ret0 197 | err := _Token.contract.Call(opts, out, "balanceOf", _owner) 198 | return *ret0, err 199 | } 200 | 201 | // BalanceOf is a free data retrieval call binding the contract method 0x70a08231. 202 | // 203 | // Solidity: function balanceOf(_owner address) constant returns(uint256) 204 | func (_Token *TokenSession) BalanceOf(_owner common.Address) (*big.Int, error) { 205 | return _Token.Contract.BalanceOf(&_Token.CallOpts, _owner) 206 | } 207 | 208 | // BalanceOf is a free data retrieval call binding the contract method 0x70a08231. 209 | // 210 | // Solidity: function balanceOf(_owner address) constant returns(uint256) 211 | func (_Token *TokenCallerSession) BalanceOf(_owner common.Address) (*big.Int, error) { 212 | return _Token.Contract.BalanceOf(&_Token.CallOpts, _owner) 213 | } 214 | 215 | // Decimals is a free data retrieval call binding the contract method 0x313ce567. 216 | // 217 | // Solidity: function decimals() constant returns(uint8) 218 | func (_Token *TokenCaller) Decimals(opts *bind.CallOpts) (uint8, error) { 219 | var ( 220 | ret0 = new(uint8) 221 | ) 222 | out := ret0 223 | err := _Token.contract.Call(opts, out, "decimals") 224 | return *ret0, err 225 | } 226 | 227 | // Decimals is a free data retrieval call binding the contract method 0x313ce567. 228 | // 229 | // Solidity: function decimals() constant returns(uint8) 230 | func (_Token *TokenSession) Decimals() (uint8, error) { 231 | return _Token.Contract.Decimals(&_Token.CallOpts) 232 | } 233 | 234 | // Decimals is a free data retrieval call binding the contract method 0x313ce567. 235 | // 236 | // Solidity: function decimals() constant returns(uint8) 237 | func (_Token *TokenCallerSession) Decimals() (uint8, error) { 238 | return _Token.Contract.Decimals(&_Token.CallOpts) 239 | } 240 | 241 | // Name is a free data retrieval call binding the contract method 0x06fdde03. 242 | // 243 | // Solidity: function name() constant returns(string) 244 | func (_Token *TokenCaller) Name(opts *bind.CallOpts) (string, error) { 245 | var ( 246 | ret0 = new(string) 247 | ) 248 | out := ret0 249 | err := _Token.contract.Call(opts, out, "name") 250 | return *ret0, err 251 | } 252 | 253 | // Name is a free data retrieval call binding the contract method 0x06fdde03. 254 | // 255 | // Solidity: function name() constant returns(string) 256 | func (_Token *TokenSession) Name() (string, error) { 257 | return _Token.Contract.Name(&_Token.CallOpts) 258 | } 259 | 260 | // Name is a free data retrieval call binding the contract method 0x06fdde03. 261 | // 262 | // Solidity: function name() constant returns(string) 263 | func (_Token *TokenCallerSession) Name() (string, error) { 264 | return _Token.Contract.Name(&_Token.CallOpts) 265 | } 266 | 267 | // Symbol is a free data retrieval call binding the contract method 0x95d89b41. 268 | // 269 | // Solidity: function symbol() constant returns(string) 270 | func (_Token *TokenCaller) Symbol(opts *bind.CallOpts) (string, error) { 271 | var ( 272 | ret0 = new(string) 273 | ) 274 | out := ret0 275 | err := _Token.contract.Call(opts, out, "symbol") 276 | return *ret0, err 277 | } 278 | 279 | // Symbol is a free data retrieval call binding the contract method 0x95d89b41. 280 | // 281 | // Solidity: function symbol() constant returns(string) 282 | func (_Token *TokenSession) Symbol() (string, error) { 283 | return _Token.Contract.Symbol(&_Token.CallOpts) 284 | } 285 | 286 | // Symbol is a free data retrieval call binding the contract method 0x95d89b41. 287 | // 288 | // Solidity: function symbol() constant returns(string) 289 | func (_Token *TokenCallerSession) Symbol() (string, error) { 290 | return _Token.Contract.Symbol(&_Token.CallOpts) 291 | } 292 | 293 | // TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. 294 | // 295 | // Solidity: function totalSupply() constant returns(uint256) 296 | func (_Token *TokenCaller) TotalSupply(opts *bind.CallOpts) (*big.Int, error) { 297 | var ( 298 | ret0 = new(*big.Int) 299 | ) 300 | out := ret0 301 | err := _Token.contract.Call(opts, out, "totalSupply") 302 | return *ret0, err 303 | } 304 | 305 | // TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. 306 | // 307 | // Solidity: function totalSupply() constant returns(uint256) 308 | func (_Token *TokenSession) TotalSupply() (*big.Int, error) { 309 | return _Token.Contract.TotalSupply(&_Token.CallOpts) 310 | } 311 | 312 | // TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. 313 | // 314 | // Solidity: function totalSupply() constant returns(uint256) 315 | func (_Token *TokenCallerSession) TotalSupply() (*big.Int, error) { 316 | return _Token.Contract.TotalSupply(&_Token.CallOpts) 317 | } 318 | 319 | // Approve is a paid mutator transaction binding the contract method 0x095ea7b3. 320 | // 321 | // Solidity: function approve(_spender address, _value uint256) returns(bool) 322 | func (_Token *TokenTransactor) Approve(opts *bind.TransactOpts, _spender common.Address, _value *big.Int) (*types.Transaction, error) { 323 | return _Token.contract.Transact(opts, "approve", _spender, _value) 324 | } 325 | 326 | // Approve is a paid mutator transaction binding the contract method 0x095ea7b3. 327 | // 328 | // Solidity: function approve(_spender address, _value uint256) returns(bool) 329 | func (_Token *TokenSession) Approve(_spender common.Address, _value *big.Int) (*types.Transaction, error) { 330 | return _Token.Contract.Approve(&_Token.TransactOpts, _spender, _value) 331 | } 332 | 333 | // Approve is a paid mutator transaction binding the contract method 0x095ea7b3. 334 | // 335 | // Solidity: function approve(_spender address, _value uint256) returns(bool) 336 | func (_Token *TokenTransactorSession) Approve(_spender common.Address, _value *big.Int) (*types.Transaction, error) { 337 | return _Token.Contract.Approve(&_Token.TransactOpts, _spender, _value) 338 | } 339 | 340 | // Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. 341 | // 342 | // Solidity: function transfer(_to address, _value uint256) returns(bool) 343 | func (_Token *TokenTransactor) Transfer(opts *bind.TransactOpts, _to common.Address, _value *big.Int) (*types.Transaction, error) { 344 | return _Token.contract.Transact(opts, "transfer", _to, _value) 345 | } 346 | 347 | // Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. 348 | // 349 | // Solidity: function transfer(_to address, _value uint256) returns(bool) 350 | func (_Token *TokenSession) Transfer(_to common.Address, _value *big.Int) (*types.Transaction, error) { 351 | return _Token.Contract.Transfer(&_Token.TransactOpts, _to, _value) 352 | } 353 | 354 | // Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. 355 | // 356 | // Solidity: function transfer(_to address, _value uint256) returns(bool) 357 | func (_Token *TokenTransactorSession) Transfer(_to common.Address, _value *big.Int) (*types.Transaction, error) { 358 | return _Token.Contract.Transfer(&_Token.TransactOpts, _to, _value) 359 | } 360 | 361 | // TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. 362 | // 363 | // Solidity: function transferFrom(_from address, _to address, _value uint256) returns(bool) 364 | func (_Token *TokenTransactor) TransferFrom(opts *bind.TransactOpts, _from common.Address, _to common.Address, _value *big.Int) (*types.Transaction, error) { 365 | return _Token.contract.Transact(opts, "transferFrom", _from, _to, _value) 366 | } 367 | 368 | // TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. 369 | // 370 | // Solidity: function transferFrom(_from address, _to address, _value uint256) returns(bool) 371 | func (_Token *TokenSession) TransferFrom(_from common.Address, _to common.Address, _value *big.Int) (*types.Transaction, error) { 372 | return _Token.Contract.TransferFrom(&_Token.TransactOpts, _from, _to, _value) 373 | } 374 | 375 | // TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. 376 | // 377 | // Solidity: function transferFrom(_from address, _to address, _value uint256) returns(bool) 378 | func (_Token *TokenTransactorSession) TransferFrom(_from common.Address, _to common.Address, _value *big.Int) (*types.Transaction, error) { 379 | return _Token.Contract.TransferFrom(&_Token.TransactOpts, _from, _to, _value) 380 | } 381 | 382 | // TokenApprovalIterator is returned from FilterApproval and is used to iterate over the raw logs and unpacked data for Approval events raised by the Token contract. 383 | type TokenApprovalIterator struct { 384 | Event *TokenApproval // Event containing the contract specifics and raw log 385 | 386 | contract *bind.BoundContract // Generic contract to use for unpacking event data 387 | event string // Event name to use for unpacking event data 388 | 389 | logs chan types.Log // Log channel receiving the found contract events 390 | sub ethereum.Subscription // Subscription for errors, completion and termination 391 | done bool // Whether the subscription completed delivering logs 392 | fail error // Occurred error to stop iteration 393 | } 394 | 395 | // Next advances the iterator to the subsequent event, returning whether there 396 | // are any more events found. In case of a retrieval or parsing error, false is 397 | // returned and Error() can be queried for the exact failure. 398 | func (it *TokenApprovalIterator) Next() bool { 399 | // If the iterator failed, stop iterating 400 | if it.fail != nil { 401 | return false 402 | } 403 | // If the iterator completed, deliver directly whatever's available 404 | if it.done { 405 | select { 406 | case log := <-it.logs: 407 | it.Event = new(TokenApproval) 408 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 409 | it.fail = err 410 | return false 411 | } 412 | it.Event.Raw = log 413 | return true 414 | 415 | default: 416 | return false 417 | } 418 | } 419 | // Iterator still in progress, wait for either a data or an error event 420 | select { 421 | case log := <-it.logs: 422 | it.Event = new(TokenApproval) 423 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 424 | it.fail = err 425 | return false 426 | } 427 | it.Event.Raw = log 428 | return true 429 | 430 | case err := <-it.sub.Err(): 431 | it.done = true 432 | it.fail = err 433 | return it.Next() 434 | } 435 | } 436 | 437 | // Error returns any retrieval or parsing error occurred during filtering. 438 | func (it *TokenApprovalIterator) Error() error { 439 | return it.fail 440 | } 441 | 442 | // Close terminates the iteration process, releasing any pending underlying 443 | // resources. 444 | func (it *TokenApprovalIterator) Close() error { 445 | it.sub.Unsubscribe() 446 | return nil 447 | } 448 | 449 | // TokenApproval represents a Approval event raised by the Token contract. 450 | type TokenApproval struct { 451 | Owner common.Address 452 | Spender common.Address 453 | Value *big.Int 454 | Raw types.Log // Blockchain specific contextual infos 455 | } 456 | 457 | // FilterApproval is a free log retrieval operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. 458 | // 459 | // Solidity: event Approval(_owner indexed address, _spender indexed address, _value uint256) 460 | func (_Token *TokenFilterer) FilterApproval(opts *bind.FilterOpts, _owner []common.Address, _spender []common.Address) (*TokenApprovalIterator, error) { 461 | 462 | var _ownerRule []interface{} 463 | for _, _ownerItem := range _owner { 464 | _ownerRule = append(_ownerRule, _ownerItem) 465 | } 466 | var _spenderRule []interface{} 467 | for _, _spenderItem := range _spender { 468 | _spenderRule = append(_spenderRule, _spenderItem) 469 | } 470 | 471 | logs, sub, err := _Token.contract.FilterLogs(opts, "Approval", _ownerRule, _spenderRule) 472 | if err != nil { 473 | return nil, err 474 | } 475 | return &TokenApprovalIterator{contract: _Token.contract, event: "Approval", logs: logs, sub: sub}, nil 476 | } 477 | 478 | // WatchApproval is a free log subscription operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. 479 | // 480 | // Solidity: event Approval(_owner indexed address, _spender indexed address, _value uint256) 481 | func (_Token *TokenFilterer) WatchApproval(opts *bind.WatchOpts, sink chan<- *TokenApproval, _owner []common.Address, _spender []common.Address) (event.Subscription, error) { 482 | 483 | var _ownerRule []interface{} 484 | for _, _ownerItem := range _owner { 485 | _ownerRule = append(_ownerRule, _ownerItem) 486 | } 487 | var _spenderRule []interface{} 488 | for _, _spenderItem := range _spender { 489 | _spenderRule = append(_spenderRule, _spenderItem) 490 | } 491 | 492 | logs, sub, err := _Token.contract.WatchLogs(opts, "Approval", _ownerRule, _spenderRule) 493 | if err != nil { 494 | return nil, err 495 | } 496 | return event.NewSubscription(func(quit <-chan struct{}) error { 497 | defer sub.Unsubscribe() 498 | for { 499 | select { 500 | case log := <-logs: 501 | // New log arrived, parse the event and forward to the user 502 | event := new(TokenApproval) 503 | if err := _Token.contract.UnpackLog(event, "Approval", log); err != nil { 504 | return err 505 | } 506 | event.Raw = log 507 | 508 | select { 509 | case sink <- event: 510 | case err := <-sub.Err(): 511 | return err 512 | case <-quit: 513 | return nil 514 | } 515 | case err := <-sub.Err(): 516 | return err 517 | case <-quit: 518 | return nil 519 | } 520 | } 521 | }), nil 522 | } 523 | 524 | // TokenTransferIterator is returned from FilterTransfer and is used to iterate over the raw logs and unpacked data for Transfer events raised by the Token contract. 525 | type TokenTransferIterator struct { 526 | Event *TokenTransfer // Event containing the contract specifics and raw log 527 | 528 | contract *bind.BoundContract // Generic contract to use for unpacking event data 529 | event string // Event name to use for unpacking event data 530 | 531 | logs chan types.Log // Log channel receiving the found contract events 532 | sub ethereum.Subscription // Subscription for errors, completion and termination 533 | done bool // Whether the subscription completed delivering logs 534 | fail error // Occurred error to stop iteration 535 | } 536 | 537 | // Next advances the iterator to the subsequent event, returning whether there 538 | // are any more events found. In case of a retrieval or parsing error, false is 539 | // returned and Error() can be queried for the exact failure. 540 | func (it *TokenTransferIterator) Next() bool { 541 | // If the iterator failed, stop iterating 542 | if it.fail != nil { 543 | return false 544 | } 545 | // If the iterator completed, deliver directly whatever's available 546 | if it.done { 547 | select { 548 | case log := <-it.logs: 549 | it.Event = new(TokenTransfer) 550 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 551 | it.fail = err 552 | return false 553 | } 554 | it.Event.Raw = log 555 | return true 556 | 557 | default: 558 | return false 559 | } 560 | } 561 | // Iterator still in progress, wait for either a data or an error event 562 | select { 563 | case log := <-it.logs: 564 | it.Event = new(TokenTransfer) 565 | if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { 566 | it.fail = err 567 | return false 568 | } 569 | it.Event.Raw = log 570 | return true 571 | 572 | case err := <-it.sub.Err(): 573 | it.done = true 574 | it.fail = err 575 | return it.Next() 576 | } 577 | } 578 | 579 | // Error returns any retrieval or parsing error occurred during filtering. 580 | func (it *TokenTransferIterator) Error() error { 581 | return it.fail 582 | } 583 | 584 | // Close terminates the iteration process, releasing any pending underlying 585 | // resources. 586 | func (it *TokenTransferIterator) Close() error { 587 | it.sub.Unsubscribe() 588 | return nil 589 | } 590 | 591 | // TokenTransfer represents a Transfer event raised by the Token contract. 592 | type TokenTransfer struct { 593 | From common.Address 594 | To common.Address 595 | Value *big.Int 596 | Raw types.Log // Blockchain specific contextual infos 597 | } 598 | 599 | // FilterTransfer is a free log retrieval operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. 600 | // 601 | // Solidity: event Transfer(_from indexed address, _to indexed address, _value uint256) 602 | func (_Token *TokenFilterer) FilterTransfer(opts *bind.FilterOpts, _from []common.Address, _to []common.Address) (*TokenTransferIterator, error) { 603 | 604 | var _fromRule []interface{} 605 | for _, _fromItem := range _from { 606 | _fromRule = append(_fromRule, _fromItem) 607 | } 608 | var _toRule []interface{} 609 | for _, _toItem := range _to { 610 | _toRule = append(_toRule, _toItem) 611 | } 612 | 613 | logs, sub, err := _Token.contract.FilterLogs(opts, "Transfer", _fromRule, _toRule) 614 | if err != nil { 615 | return nil, err 616 | } 617 | return &TokenTransferIterator{contract: _Token.contract, event: "Transfer", logs: logs, sub: sub}, nil 618 | } 619 | 620 | // WatchTransfer is a free log subscription operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. 621 | // 622 | // Solidity: event Transfer(_from indexed address, _to indexed address, _value uint256) 623 | func (_Token *TokenFilterer) WatchTransfer(opts *bind.WatchOpts, sink chan<- *TokenTransfer, _from []common.Address, _to []common.Address) (event.Subscription, error) { 624 | 625 | var _fromRule []interface{} 626 | for _, _fromItem := range _from { 627 | _fromRule = append(_fromRule, _fromItem) 628 | } 629 | var _toRule []interface{} 630 | for _, _toItem := range _to { 631 | _toRule = append(_toRule, _toItem) 632 | } 633 | 634 | logs, sub, err := _Token.contract.WatchLogs(opts, "Transfer", _fromRule, _toRule) 635 | if err != nil { 636 | return nil, err 637 | } 638 | return event.NewSubscription(func(quit <-chan struct{}) error { 639 | defer sub.Unsubscribe() 640 | for { 641 | select { 642 | case log := <-logs: 643 | // New log arrived, parse the event and forward to the user 644 | event := new(TokenTransfer) 645 | if err := _Token.contract.UnpackLog(event, "Transfer", log); err != nil { 646 | return err 647 | } 648 | event.Raw = log 649 | 650 | select { 651 | case sink <- event: 652 | case err := <-sub.Err(): 653 | return err 654 | case <-quit: 655 | return nil 656 | } 657 | case err := <-sub.Err(): 658 | return err 659 | case <-quit: 660 | return nil 661 | } 662 | } 663 | }), nil 664 | } 665 | -------------------------------------------------------------------------------- /wallet/erc20_wallet.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/ecdsa" 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | "math/big" 11 | "time" 12 | 13 | "github.com/OpenBazaar/multiwallet/config" 14 | wi "github.com/OpenBazaar/wallet-interface" 15 | "github.com/btcsuite/btcd/chaincfg" 16 | "github.com/btcsuite/btcd/chaincfg/chainhash" 17 | "github.com/btcsuite/btcutil" 18 | hd "github.com/btcsuite/btcutil/hdkeychain" 19 | "github.com/davecgh/go-spew/spew" 20 | ethereum "github.com/ethereum/go-ethereum" 21 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 22 | "github.com/ethereum/go-ethereum/common" 23 | "github.com/ethereum/go-ethereum/common/hexutil" 24 | "github.com/ethereum/go-ethereum/core/types" 25 | "github.com/ethereum/go-ethereum/crypto" 26 | "github.com/ethereum/go-ethereum/ethclient" 27 | "golang.org/x/net/proxy" 28 | 29 | "github.com/OpenBazaar/go-ethwallet/util" 30 | ) 31 | 32 | // ERC20Wallet is the wallet implementation for ethereum 33 | type ERC20Wallet struct { 34 | client *EthClient 35 | account *Account 36 | address *EthAddress 37 | service *Service 38 | registry *Registry 39 | ppsct *Escrow 40 | db wi.Datastore 41 | exchangeRates wi.ExchangeRates 42 | symbol string 43 | name string 44 | deployAddressMain common.Address 45 | deployAddressRopsten common.Address 46 | deployAddressRinkeby common.Address 47 | token *Token 48 | listeners []func(wi.TransactionCallback) 49 | } 50 | 51 | // GenTokenScriptHash - used to generate script hash for erc20 token as per 52 | // escrow smart contract 53 | func GenTokenScriptHash(script EthRedeemScript) ([32]byte, string, error) { 54 | //ahash := sha3.NewKeccak256() 55 | a := make([]byte, 4) 56 | binary.BigEndian.PutUint32(a, script.Timeout) 57 | arr := append(script.TxnID.Bytes(), append([]byte{script.Threshold}, 58 | append(a[:], append(script.Buyer.Bytes(), 59 | append(script.Seller.Bytes(), append(script.Moderator.Bytes(), 60 | append(script.MultisigAddress.Bytes(), 61 | script.TokenAddress.Bytes()...)...)...)...)...)...)...) 62 | //ahash.Write(arr) 63 | var retHash [32]byte 64 | 65 | //copy(retHash[:], ahash.Sum(nil)[:]) 66 | copy(retHash[:], crypto.Keccak256(arr)) 67 | ahashStr := hexutil.Encode(retHash[:]) 68 | 69 | return retHash, ahashStr, nil 70 | } 71 | 72 | // TokenDetail is used to capture ERC20 token details 73 | type TokenDetail struct { 74 | name string 75 | symbol string 76 | deployAddressMain common.Address 77 | deployAddressRopsten common.Address 78 | deployAddressRinkeby common.Address 79 | } 80 | 81 | // NewERC20Wallet will return a reference to the ERC20 Wallet 82 | func NewERC20Wallet(cfg config.CoinConfig, params *chaincfg.Params, mnemonic string, proxy proxy.Dialer) (*ERC20Wallet, error) { 83 | client, err := NewEthClient(cfg.ClientAPIs[0] + "/" + InfuraAPIKey) 84 | if err != nil { 85 | log.Errorf("error initializing wallet: %v", err) 86 | return nil, err 87 | } 88 | var myAccount *Account 89 | 90 | myAccount, err = NewAccountFromMnemonic(mnemonic, "", params) 91 | if err != nil { 92 | log.Errorf("mnemonic based pk generation failed: %s", err.Error()) 93 | return nil, err 94 | } 95 | addr := myAccount.Address() 96 | 97 | ethConfig := EthConfiguration{} 98 | 99 | var regAddr interface{} 100 | var ok bool 101 | if regAddr, ok = cfg.Options["RegistryAddress"]; !ok { 102 | log.Errorf("ethereum registry not found: %s", cfg.Options["RegistryAddress"]) 103 | return nil, err 104 | } 105 | 106 | ethConfig.RegistryAddress = regAddr.(string) 107 | 108 | reg, err := NewRegistry(common.HexToAddress(ethConfig.RegistryAddress), client) 109 | if err != nil { 110 | log.Errorf("error initilaizing contract failed: %s", err.Error()) 111 | return nil, err 112 | } 113 | 114 | er := NewEthereumPriceFetcher(proxy) 115 | 116 | token := TokenDetail{} 117 | 118 | var name, symbol, deployAddrMain, deployAddrRopsten, deployAddrRinkeby interface{} 119 | if name, ok = cfg.Options["Name"]; !ok { 120 | log.Errorf("erc20 token name not found: %s", cfg.Options["Name"]) 121 | return nil, err 122 | } 123 | 124 | token.name = name.(string) 125 | 126 | if symbol, ok = cfg.Options["Symbol"]; !ok { 127 | log.Errorf("erc20 token symbol not found: %s", cfg.Options["Symbol"]) 128 | return nil, err 129 | } 130 | 131 | token.symbol = symbol.(string) 132 | 133 | if deployAddrMain, ok = cfg.Options["MainNetAddress"]; !ok { 134 | log.Errorf("erc20 token address not found: %s", cfg.Options["MainNetAddress"]) 135 | return nil, err 136 | } 137 | 138 | token.deployAddressMain = common.HexToAddress(deployAddrMain.(string)) 139 | 140 | if deployAddrRopsten, ok = cfg.Options["RopstenAddress"]; ok { 141 | token.deployAddressMain = common.HexToAddress(deployAddrRopsten.(string)) 142 | } 143 | 144 | if deployAddrRinkeby, ok = cfg.Options["RinkebyAddress"]; ok { 145 | token.deployAddressRinkeby = common.HexToAddress(deployAddrRinkeby.(string)) 146 | } 147 | 148 | erc20Token, err := NewToken(token.deployAddressMain, client) 149 | if err != nil { 150 | log.Errorf("error initilaizing erc20 token failed: %s", err.Error()) 151 | return nil, err 152 | } 153 | 154 | return &ERC20Wallet{ 155 | client: client, 156 | account: myAccount, 157 | address: &EthAddress{&addr}, 158 | service: &Service{}, 159 | registry: reg, 160 | ppsct: nil, 161 | db: cfg.DB, 162 | exchangeRates: er, 163 | symbol: token.symbol, 164 | name: token.name, 165 | deployAddressMain: token.deployAddressMain, 166 | deployAddressRopsten: token.deployAddressRopsten, 167 | deployAddressRinkeby: token.deployAddressRinkeby, 168 | token: erc20Token, 169 | listeners: []func(wi.TransactionCallback){}, 170 | }, nil 171 | } 172 | 173 | // Params - return nil to comply 174 | func (wallet *ERC20Wallet) Params() *chaincfg.Params { 175 | return nil 176 | } 177 | 178 | // GetBalance returns the balance for the wallet 179 | func (wallet *ERC20Wallet) GetBalance() (*big.Int, error) { 180 | return wallet.client.GetTokenBalance(wallet.account.Address(), wallet.deployAddressMain) 181 | } 182 | 183 | // GetUnconfirmedBalance returns the unconfirmed balance for the wallet 184 | func (wallet *ERC20Wallet) GetUnconfirmedBalance() (*big.Int, error) { 185 | return big.NewInt(0), nil 186 | } 187 | 188 | // Transfer will transfer the amount from this wallet to the spec address 189 | func (wallet *ERC20Wallet) Transfer(to string, value *big.Int) (common.Hash, error) { 190 | toAddress := common.HexToAddress(to) 191 | //wallet.token.Transfer() 192 | return wallet.client.TransferToken(wallet.account, toAddress, wallet.deployAddressMain, value) 193 | } 194 | 195 | // Start will start the wallet daemon 196 | func (wallet *ERC20Wallet) Start() { 197 | // daemonize the wallet 198 | } 199 | 200 | // CurrencyCode returns symbol 201 | func (wallet *ERC20Wallet) CurrencyCode() string { 202 | return wallet.symbol 203 | } 204 | 205 | // IsDust Check if this amount is considered dust - 10000 wei 206 | func (wallet *ERC20Wallet) IsDust(amount int64) bool { 207 | return amount < 10000 208 | } 209 | 210 | // MasterPrivateKey - Get the master private key 211 | func (wallet *ERC20Wallet) MasterPrivateKey() *hd.ExtendedKey { 212 | return hd.NewExtendedKey([]byte{0x00, 0x00, 0x00, 0x00}, wallet.account.privateKey.D.Bytes(), 213 | wallet.account.address.Bytes(), wallet.account.address.Bytes(), 0, 0, true) 214 | } 215 | 216 | // MasterPublicKey - Get the master public key 217 | func (wallet *ERC20Wallet) MasterPublicKey() *hd.ExtendedKey { 218 | publicKey := wallet.account.privateKey.Public() 219 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 220 | if !ok { 221 | log.Fatal("error casting public key to ECDSA") 222 | } 223 | 224 | publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) 225 | return hd.NewExtendedKey([]byte{0x00, 0x00, 0x00, 0x00}, publicKeyBytes, 226 | wallet.account.address.Bytes(), wallet.account.address.Bytes(), 0, 0, false) 227 | } 228 | 229 | // ChildKey Generate a child key using the given chaincode. The key is used in multisig transactions. 230 | // For most implementations this should just be child key 0. 231 | func (wallet *ERC20Wallet) ChildKey(keyBytes []byte, chaincode []byte, isPrivateKey bool) (*hd.ExtendedKey, error) { 232 | if isPrivateKey { 233 | return wallet.MasterPrivateKey(), nil 234 | } 235 | return wallet.MasterPublicKey(), nil 236 | } 237 | 238 | // CurrentAddress - Get the current address for the given purpose 239 | func (wallet *ERC20Wallet) CurrentAddress(purpose wi.KeyPurpose) btcutil.Address { 240 | return *wallet.address 241 | } 242 | 243 | // NewAddress - Returns a fresh address that has never been returned by this function 244 | func (wallet *ERC20Wallet) NewAddress(purpose wi.KeyPurpose) btcutil.Address { 245 | return *wallet.address 246 | } 247 | 248 | // DecodeAddress - Parse the address string and return an address interface 249 | func (wallet *ERC20Wallet) DecodeAddress(addr string) (btcutil.Address, error) { 250 | ethAddr := common.HexToAddress(addr) 251 | return EthAddress{ðAddr}, nil 252 | } 253 | 254 | // ScriptToAddress - ? 255 | func (wallet *ERC20Wallet) ScriptToAddress(script []byte) (btcutil.Address, error) { 256 | return wallet.address, nil 257 | } 258 | 259 | // HasKey - Returns if the wallet has the key for the given address 260 | func (wallet *ERC20Wallet) HasKey(addr btcutil.Address) bool { 261 | if !util.IsValidAddress(addr.String()) { 262 | return false 263 | } 264 | return wallet.account.Address().String() == addr.String() 265 | } 266 | 267 | // Balance - Get the confirmed and unconfirmed balances 268 | func (wallet *ERC20Wallet) Balance() (confirmed, unconfirmed int64) { 269 | var balance, ucbalance int64 270 | bal, err := wallet.GetBalance() 271 | if err == nil { 272 | balance = bal.Int64() 273 | } 274 | ucbal, err := wallet.GetUnconfirmedBalance() 275 | if err == nil { 276 | ucbalance = ucbal.Int64() 277 | } 278 | ucb := int64(0) 279 | if ucbalance > balance { 280 | ucb = ucbalance - balance 281 | } 282 | return balance, ucb 283 | } 284 | 285 | // Transactions - Returns a list of transactions for this wallet 286 | func (wallet *ERC20Wallet) Transactions() ([]wi.Txn, error) { 287 | txns, err := wallet.client.eClient.NormalTxByAddress(wallet.account.Address().String(), nil, nil, 288 | 1, 0, true) 289 | if err != nil { 290 | return []wi.Txn{}, err 291 | } 292 | 293 | ret := []wi.Txn{} 294 | for _, t := range txns { 295 | status := wi.StatusConfirmed 296 | if t.IsError != 0 { 297 | status = wi.StatusError 298 | } 299 | tnew := wi.Txn{ 300 | Txid: t.Hash, 301 | Value: t.Value.Int().String(), 302 | Height: int32(t.BlockNumber), 303 | Timestamp: t.TimeStamp.Time(), 304 | WatchOnly: false, 305 | Confirmations: int64(t.Confirmations), 306 | Status: wi.StatusCode(status), 307 | Bytes: []byte(t.Input), 308 | } 309 | ret = append(ret, tnew) 310 | } 311 | 312 | return ret, nil 313 | } 314 | 315 | // GetTransaction - Get info on a specific transaction 316 | func (wallet *ERC20Wallet) GetTransaction(txid chainhash.Hash) (wi.Txn, error) { 317 | tx, _, err := wallet.client.GetTransaction(common.HexToHash(txid.String())) 318 | if err != nil { 319 | return wi.Txn{}, err 320 | } 321 | return wi.Txn{ 322 | Txid: tx.Hash().String(), 323 | Value: tx.Value().String(), 324 | Height: 0, 325 | Timestamp: time.Now(), 326 | WatchOnly: false, 327 | Bytes: tx.Data(), 328 | }, nil 329 | } 330 | 331 | // ChainTip - Get the height and best hash of the blockchain 332 | func (wallet *ERC20Wallet) ChainTip() (uint32, chainhash.Hash) { 333 | num, hash, err := wallet.client.GetLatestBlock() 334 | h, _ := chainhash.NewHashFromStr("") 335 | if err != nil { 336 | return 0, *h 337 | } 338 | h, _ = chainhash.NewHashFromStr(hash.Hex()[2:]) 339 | return num, *h 340 | } 341 | 342 | // GetFeePerByte - Get the current fee per byte 343 | func (wallet *ERC20Wallet) GetFeePerByte(feeLevel wi.FeeLevel) uint64 { 344 | return 0 345 | } 346 | 347 | // Spend - Send ether to an external wallet 348 | func (wallet *ERC20Wallet) Spend(amount big.Int, addr btcutil.Address, feeLevel wi.FeeLevel, referenceID string) (*chainhash.Hash, error) { 349 | var hash common.Hash 350 | var h *chainhash.Hash 351 | var err error 352 | 353 | // check if the addr is a multisig addr 354 | scripts, err := wallet.db.WatchedScripts().GetAll() 355 | if err != nil { 356 | return nil, err 357 | } 358 | isScript := false 359 | key := []byte(addr.String()) 360 | redeemScript := []byte{} 361 | 362 | for _, script := range scripts { 363 | if bytes.Equal(key, script[:common.AddressLength]) { 364 | isScript = true 365 | redeemScript = script[common.AddressLength:] 366 | break 367 | } 368 | } 369 | 370 | if isScript { 371 | ethScript, err := DeserializeEthScript(redeemScript) 372 | if err != nil { 373 | return nil, err 374 | } 375 | hash, err = wallet.callAddTokenTransaction(ethScript, &amount) 376 | if err != nil { 377 | log.Errorf("error call add token txn: %v", err) 378 | } 379 | } else { 380 | hash, err = wallet.Transfer(addr.String(), &amount) 381 | } 382 | 383 | if err != nil { 384 | return nil, err 385 | } 386 | start := time.Now() 387 | flag := false 388 | var rcpt *types.Receipt 389 | for !flag { 390 | rcpt, err = wallet.client.TransactionReceipt(context.Background(), hash) 391 | if rcpt != nil { 392 | flag = true 393 | } 394 | if time.Since(start).Seconds() > 120 { 395 | flag = true 396 | } 397 | } 398 | if rcpt != nil { 399 | // good. so the txn has been processed but we have to account for failed 400 | // but valid txn like some contract condition causing revert 401 | if rcpt.Status > 0 { 402 | // all good to update order state 403 | go wallet.callListeners(wallet.createTxnCallback(hash.String(), referenceID, addr, amount, time.Now())) 404 | } else { 405 | // there was some error processing this txn 406 | return nil, errors.New("problem processing this transaction") 407 | } 408 | } 409 | 410 | if err == nil { 411 | h, err = chainhash.NewHashFromStr(hash.Hex()[2:]) 412 | } 413 | return h, err 414 | } 415 | 416 | func (wallet *ERC20Wallet) createTxnCallback(txID, orderID string, toAddress btcutil.Address, value big.Int, bTime time.Time) wi.TransactionCallback { 417 | output := wi.TransactionOutput{ 418 | Address: toAddress, 419 | Value: value, 420 | Index: 1, 421 | OrderID: orderID, 422 | } 423 | 424 | input := wi.TransactionInput{ 425 | OutpointHash: []byte(txID), 426 | OutpointIndex: 1, 427 | LinkedAddress: wallet.address, 428 | Value: value, 429 | OrderID: orderID, 430 | } 431 | 432 | return wi.TransactionCallback{ 433 | Txid: txID[2:], 434 | Outputs: []wi.TransactionOutput{output}, 435 | Inputs: []wi.TransactionInput{input}, 436 | Height: 1, 437 | Timestamp: time.Now(), 438 | Value: value, 439 | WatchOnly: false, 440 | BlockTime: bTime, 441 | } 442 | } 443 | 444 | func (wallet *ERC20Wallet) callListeners(txnCB wi.TransactionCallback) { 445 | for _, l := range wallet.listeners { 446 | go l(txnCB) 447 | } 448 | } 449 | 450 | // BumpFee - Bump the fee for the given transaction 451 | func (wallet *ERC20Wallet) BumpFee(txid chainhash.Hash) (*chainhash.Hash, error) { 452 | return chainhash.NewHashFromStr(txid.String()) 453 | } 454 | 455 | // EstimateFee - Calculates the estimated size of the transaction and returns the total fee for the given feePerByte 456 | func (wallet *ERC20Wallet) EstimateFee(ins []wi.TransactionInput, outs []wi.TransactionOutput, feePerByte uint64) uint64 { 457 | sum := big.NewInt(0) 458 | for _, out := range outs { 459 | gas, err := wallet.client.EstimateTxnGas(wallet.account.Address(), 460 | common.HexToAddress(out.Address.String()), &out.Value) 461 | if err != nil { 462 | return sum.Uint64() 463 | } 464 | sum.Add(sum, gas) 465 | } 466 | return sum.Uint64() 467 | } 468 | 469 | // EstimateSpendFee - Build a spend transaction for the amount and return the transaction fee 470 | func (wallet *ERC20Wallet) EstimateSpendFee(amount int64, feeLevel wi.FeeLevel) (uint64, error) { 471 | gas, err := wallet.client.EstimateGasSpend(wallet.account.Address(), big.NewInt(amount)) 472 | return gas.Uint64(), err 473 | } 474 | 475 | // SweepAddress - Build and broadcast a transaction that sweeps all coins from an address. If it is a p2sh multisig, the redeemScript must be included 476 | func (wallet *ERC20Wallet) SweepAddress(utxos []wi.TransactionInput, address *btcutil.Address, key *hd.ExtendedKey, redeemScript *[]byte, feeLevel wi.FeeLevel) (*chainhash.Hash, error) { 477 | return chainhash.NewHashFromStr("") 478 | } 479 | 480 | // ExchangeRates - return the exchangerates 481 | func (wallet *ERC20Wallet) ExchangeRates() wi.ExchangeRates { 482 | return wallet.exchangeRates 483 | } 484 | 485 | func (wallet *ERC20Wallet) callAddTokenTransaction(script EthRedeemScript, value *big.Int) (common.Hash, error) { 486 | 487 | h := common.BigToHash(big.NewInt(0)) 488 | 489 | // call registry to get the deployed address for the escrow ct 490 | fromAddress := wallet.account.Address() 491 | nonce, err := wallet.client.PendingNonceAt(context.Background(), fromAddress) 492 | if err != nil { 493 | log.Fatal(err) 494 | } 495 | gasPrice, err := wallet.client.SuggestGasPrice(context.Background()) 496 | if err != nil { 497 | log.Fatal(err) 498 | } 499 | auth := bind.NewKeyedTransactor(wallet.account.privateKey) 500 | 501 | auth.Nonce = big.NewInt(int64(nonce)) 502 | auth.Value = big.NewInt(0) // in wei 503 | auth.GasLimit = 4000000 // in units 504 | auth.GasPrice = gasPrice 505 | 506 | shash, _, err := GenTokenScriptHash(script) 507 | if err != nil { 508 | return h, err 509 | } 510 | 511 | smtct, err := NewEscrow(script.MultisigAddress, wallet.client) 512 | if err != nil { 513 | log.Fatalf("error initilaizing contract failed: %s", err.Error()) 514 | } 515 | 516 | var tx *types.Transaction 517 | 518 | tx, err = wallet.token.Approve(auth, script.MultisigAddress, value) 519 | 520 | if err != nil { 521 | return common.BigToHash(big.NewInt(0)), err 522 | } 523 | 524 | //time.Sleep(2 * time.Minute) 525 | header, err := wallet.client.HeaderByNumber(context.Background(), nil) 526 | if err != nil { 527 | log.Errorf("error fetching latest blk: %v", err) 528 | } 529 | tclient, err := ethclient.Dial("wss://rinkeby.infura.io/ws") 530 | if err != nil { 531 | log.Errorf("error establishing ws conn: %v", err) 532 | } 533 | 534 | query := ethereum.FilterQuery{ 535 | Addresses: []common.Address{script.TokenAddress}, 536 | FromBlock: header.Number, 537 | Topics: [][]common.Hash{{common.HexToHash("0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925")}}, 538 | } 539 | logs := make(chan types.Log) 540 | sub1, err := tclient.SubscribeFilterLogs(context.Background(), query, logs) 541 | if err != nil { 542 | return common.BigToHash(big.NewInt(0)), err 543 | } 544 | defer sub1.Unsubscribe() 545 | flag := false 546 | for !flag { 547 | select { 548 | case err := <-sub1.Err(): 549 | log.Fatal(err) 550 | case vLog := <-logs: 551 | //fmt.Println(vLog) // pointer to event log 552 | //spew.Dump(vLog) 553 | //fmt.Println(vLog.Topics[0]) 554 | fmt.Println(vLog.Address.String()) 555 | if tx.Hash() == vLog.TxHash { 556 | fmt.Println("we have found the approval ...") 557 | //time.Sleep(2 * time.Minute) 558 | spew.Dump(vLog) 559 | nonce, _ = wallet.client.PendingNonceAt(context.Background(), fromAddress) 560 | auth.Nonce = big.NewInt(int64(nonce)) 561 | auth.Value = big.NewInt(0) // in wei 562 | auth.GasLimit = 4000000 // in units 563 | auth.GasPrice = gasPrice 564 | 565 | tx, err = smtct.AddTokenTransaction(auth, script.Buyer, script.Seller, 566 | script.Moderator, script.Threshold, script.Timeout, shash, 567 | value, script.TxnID, wallet.deployAddressMain) 568 | 569 | if err == nil { 570 | h = tx.Hash() 571 | } 572 | flag = true 573 | break 574 | } 575 | } 576 | } 577 | 578 | /* 579 | auth.Nonce = big.NewInt(int64(nonce)) 580 | auth.Value = value // in wei 581 | auth.GasLimit = 4000000 // in units 582 | auth.GasPrice = gasPrice 583 | 584 | tx, err = smtct.AddTokenTransaction(auth, script.Buyer, script.Seller, 585 | script.Moderator, script.Threshold, script.Timeout, shash, 586 | value, script.TxnID, wallet.deployAddressMain) 587 | 588 | if err == nil { 589 | h = tx.Hash() 590 | } 591 | */ 592 | 593 | return h, err 594 | 595 | } 596 | 597 | // GenerateMultisigScript - Generate a multisig script from public keys. If a timeout is included the returned script should be a timelocked escrow which releases using the timeoutKey. 598 | func (wallet *ERC20Wallet) GenerateMultisigScript(keys []hd.ExtendedKey, threshold int, timeout time.Duration, timeoutKey *hd.ExtendedKey) (btcutil.Address, []byte, error) { 599 | if uint32(timeout.Hours()) > 0 && timeoutKey == nil { 600 | return nil, nil, errors.New("timeout key must be non nil when using an escrow timeout") 601 | } 602 | 603 | if len(keys) < threshold { 604 | return nil, nil, fmt.Errorf("unable to generate multisig script with "+ 605 | "%d required signatures when there are only %d public "+ 606 | "keys available", threshold, len(keys)) 607 | } 608 | 609 | if len(keys) < 2 { 610 | return nil, nil, fmt.Errorf("unable to generate multisig script with "+ 611 | "%d required signatures when there are only %d public "+ 612 | "keys available", threshold, len(keys)) 613 | } 614 | 615 | var ecKeys []common.Address 616 | for _, key := range keys { 617 | ecKey, err := key.ECPubKey() 618 | if err != nil { 619 | return nil, nil, err 620 | } 621 | ecKeys = append(ecKeys, common.BytesToAddress(ecKey.SerializeUncompressed())) 622 | } 623 | 624 | ver, err := wallet.registry.GetRecommendedVersion(nil, "escrow") 625 | if err != nil { 626 | log.Fatal(err) 627 | } 628 | 629 | if util.IsZeroAddress(ver.Implementation) { 630 | return nil, nil, errors.New("no escrow contract available") 631 | } 632 | 633 | builder := EthRedeemScript{} 634 | 635 | builder.TxnID = common.BytesToAddress(util.ExtractChaincode(&keys[0])) 636 | builder.Timeout = uint32(timeout.Hours()) 637 | builder.Threshold = uint8(threshold) 638 | builder.Buyer = ecKeys[0] 639 | builder.Seller = ecKeys[1] 640 | builder.MultisigAddress = ver.Implementation 641 | builder.TokenAddress = wallet.deployAddressMain 642 | 643 | if threshold > 1 { 644 | builder.Moderator = ecKeys[2] 645 | } 646 | switch threshold { 647 | case 1: 648 | { 649 | // Seller is offline 650 | } 651 | case 2: 652 | { 653 | // Moderated payment 654 | } 655 | default: 656 | { 657 | // handle this 658 | } 659 | } 660 | 661 | redeemScript, err := SerializeEthScript(builder) 662 | if err != nil { 663 | return nil, nil, err 664 | } 665 | 666 | //hash := sha3.NewKeccak256() 667 | //hash.Write(redeemScript) 668 | addr := common.HexToAddress(hexutil.Encode(crypto.Keccak256(redeemScript))) 669 | retAddr := EthAddress{&addr} 670 | 671 | scriptKey := append(addr.Bytes(), redeemScript...) 672 | wallet.db.WatchedScripts().Put(scriptKey) 673 | 674 | return retAddr, redeemScript, nil 675 | } 676 | 677 | // CreateMultisigSignature - Create a signature for a multisig transaction 678 | func (wallet *ERC20Wallet) CreateMultisigSignature(ins []wi.TransactionInput, outs []wi.TransactionOutput, key *hd.ExtendedKey, redeemScript []byte, feePerByte uint64) ([]wi.Signature, error) { 679 | 680 | var sigs []wi.Signature 681 | 682 | payables := make(map[string]*big.Int) 683 | for _, out := range outs { 684 | if out.Value.Cmp(big.NewInt(0)) <= 0 { 685 | continue 686 | } 687 | val := &out.Value 688 | if p, ok := payables[out.Address.String()]; ok { 689 | sum := big.NewInt(0) 690 | sum.Add(val, p) 691 | payables[out.Address.String()] = sum 692 | } else { 693 | payables[out.Address.String()] = val 694 | } 695 | } 696 | 697 | destArr := []byte{} 698 | amountArr := []byte{} 699 | 700 | for k, v := range payables { 701 | addr := common.HexToAddress(k) 702 | sample := [32]byte{} 703 | sampleDest := [32]byte{} 704 | copy(sampleDest[12:], addr.Bytes()) 705 | a := make([]byte, 8) 706 | binary.BigEndian.PutUint64(a, v.Uint64()) 707 | 708 | copy(sample[24:], a) 709 | destArr = append(destArr, sampleDest[:]...) 710 | amountArr = append(amountArr, sample[:]...) 711 | } 712 | 713 | rScript, err := DeserializeEthScript(redeemScript) 714 | if err != nil { 715 | return nil, err 716 | } 717 | 718 | shash, _, err := GenTokenScriptHash(rScript) 719 | if err != nil { 720 | return nil, err 721 | } 722 | 723 | var txHash [32]byte 724 | var payloadHash [32]byte 725 | 726 | /* 727 | // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191 728 | bytes32 txHash = keccak256( 729 | abi.encodePacked( 730 | "\x19Ethereum Signed Message:\n32", 731 | keccak256( 732 | abi.encodePacked( 733 | byte(0x19), 734 | byte(0), 735 | this, 736 | destinations, 737 | amounts, 738 | scriptHash 739 | ) 740 | ) 741 | ) 742 | ); 743 | 744 | */ 745 | 746 | payload := []byte{byte(0x19), byte(0)} 747 | payload = append(payload, rScript.MultisigAddress.Bytes()...) 748 | payload = append(payload, destArr...) 749 | payload = append(payload, amountArr...) 750 | payload = append(payload, shash[:]...) 751 | 752 | pHash := crypto.Keccak256(payload) 753 | copy(payloadHash[:], pHash) 754 | 755 | txData := []byte{byte(0x19)} 756 | txData = append(txData, []byte("Ethereum Signed Message:\n32")...) 757 | txData = append(txData, payloadHash[:]...) 758 | txnHash := crypto.Keccak256(txData) 759 | copy(txHash[:], txnHash) 760 | 761 | sig, err := crypto.Sign(txHash[:], wallet.account.privateKey) 762 | if err != nil { 763 | log.Errorf("error signing in createmultisig : %v", err) 764 | } 765 | sigs = append(sigs, wi.Signature{InputIndex: 1, Signature: sig}) 766 | 767 | return sigs, err 768 | } 769 | 770 | // Multisign - Combine signatures and optionally broadcast 771 | func (wallet *ERC20Wallet) Multisign(ins []wi.TransactionInput, outs []wi.TransactionOutput, sigs1 []wi.Signature, sigs2 []wi.Signature, redeemScript []byte, feePerByte uint64, broadcast bool) ([]byte, error) { 772 | 773 | //var buf bytes.Buffer 774 | 775 | payables := make(map[string]*big.Int) 776 | for _, out := range outs { 777 | if out.Value.Cmp(big.NewInt(0)) <= 0 { 778 | continue 779 | } 780 | val := &out.Value 781 | if p, ok := payables[out.Address.String()]; ok { 782 | sum := big.NewInt(0) 783 | sum.Add(val, p) 784 | payables[out.Address.String()] = sum 785 | } else { 786 | payables[out.Address.String()] = val 787 | } 788 | } 789 | 790 | rSlice := [][32]byte{} //, 2) 791 | sSlice := [][32]byte{} //, 2) 792 | vSlice := []uint8{} //, 2) 793 | 794 | var r [32]byte 795 | var s [32]byte 796 | var v uint8 797 | 798 | if len(sigs1[0].Signature) > 0 { 799 | r, s, v = util.SigRSV(sigs1[0].Signature) 800 | rSlice = append(rSlice, r) 801 | sSlice = append(sSlice, s) 802 | vSlice = append(vSlice, v) 803 | } 804 | 805 | //r = [32]byte{} 806 | //s = [32]byte{} 807 | //v = uint8(0) 808 | 809 | if len(sigs2[0].Signature) > 0 { 810 | r, s, v = util.SigRSV(sigs2[0].Signature) 811 | rSlice = append(rSlice, r) 812 | sSlice = append(sSlice, s) 813 | vSlice = append(vSlice, v) 814 | } 815 | 816 | rScript, err := DeserializeEthScript(redeemScript) 817 | if err != nil { 818 | return nil, err 819 | } 820 | 821 | shash, _, err := GenTokenScriptHash(rScript) 822 | if err != nil { 823 | return nil, err 824 | } 825 | 826 | smtct, err := NewEscrow(rScript.MultisigAddress, wallet.client) 827 | if err != nil { 828 | log.Fatalf("error initilaizing contract failed: %s", err.Error()) 829 | } 830 | 831 | destinations := []common.Address{} 832 | amounts := []*big.Int{} 833 | 834 | for k, v := range payables { 835 | destinations = append(destinations, common.HexToAddress(k)) 836 | amounts = append(amounts, v) 837 | } 838 | 839 | fromAddress := wallet.account.Address() 840 | nonce, err := wallet.client.PendingNonceAt(context.Background(), fromAddress) 841 | if err != nil { 842 | log.Fatal(err) 843 | } 844 | gasPrice, err := wallet.client.SuggestGasPrice(context.Background()) 845 | if err != nil { 846 | log.Fatal(err) 847 | } 848 | auth := bind.NewKeyedTransactor(wallet.account.privateKey) 849 | 850 | auth.Nonce = big.NewInt(int64(nonce)) 851 | auth.Value = big.NewInt(0) // in wei 852 | auth.GasLimit = 4000000 // in units 853 | auth.GasPrice = gasPrice 854 | 855 | var tx *types.Transaction 856 | 857 | tx, err = smtct.Execute(auth, vSlice, rSlice, sSlice, shash, destinations, amounts) 858 | 859 | //fmt.Println(tx) 860 | //fmt.Println(err) 861 | 862 | if err != nil { 863 | return nil, err 864 | } 865 | 866 | ret, err := tx.MarshalJSON() 867 | 868 | return ret, err 869 | } 870 | 871 | // AddTransactionListener - add a txn listener 872 | func (wallet *ERC20Wallet) AddTransactionListener(callback func(wi.TransactionCallback)) { 873 | // add incoming txn listener using service 874 | wallet.listeners = append(wallet.listeners, callback) 875 | } 876 | 877 | // ReSyncBlockchain - Use this to re-download merkle blocks in case of missed transactions 878 | func (wallet *ERC20Wallet) ReSyncBlockchain(fromTime time.Time) { 879 | // use service here 880 | } 881 | 882 | // GetConfirmations - Return the number of confirmations and the height for a transaction 883 | func (wallet *ERC20Wallet) GetConfirmations(txid chainhash.Hash) (confirms, atHeight uint32, err error) { 884 | return 0, 0, nil 885 | } 886 | 887 | // Close will stop the wallet daemon 888 | func (wallet *ERC20Wallet) Close() { 889 | // stop the wallet daemon 890 | } 891 | 892 | // CreateAddress - used to generate a new address 893 | func (wallet *ERC20Wallet) CreateAddress() (common.Address, error) { 894 | fromAddress := wallet.account.Address() 895 | nonce, err := wallet.client.PendingNonceAt(context.Background(), fromAddress) 896 | if err != nil { 897 | fmt.Println(err) 898 | } 899 | addr := crypto.CreateAddress(fromAddress, nonce) 900 | //fmt.Println("Addr : ", addr.String()) 901 | return addr, err 902 | } 903 | -------------------------------------------------------------------------------- /wallet/erc20_wallet_test.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/binary" 7 | "fmt" 8 | "math/big" 9 | "net/url" 10 | "testing" 11 | "time" 12 | 13 | "github.com/OpenBazaar/multiwallet/config" 14 | wi "github.com/OpenBazaar/wallet-interface" 15 | "github.com/btcsuite/btcd/chaincfg/chainhash" 16 | hd "github.com/btcsuite/btcutil/hdkeychain" 17 | "github.com/davecgh/go-spew/spew" 18 | ethereum "github.com/ethereum/go-ethereum" 19 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 20 | "github.com/ethereum/go-ethereum/common" 21 | "github.com/ethereum/go-ethereum/common/hexutil" 22 | "github.com/ethereum/go-ethereum/core/types" 23 | "github.com/ethereum/go-ethereum/crypto" 24 | "github.com/ethereum/go-ethereum/ethclient" 25 | log "github.com/sirupsen/logrus" 26 | 27 | "github.com/OpenBazaar/go-ethwallet/util" 28 | ) 29 | 30 | var validTokenWallet *ERC20Wallet 31 | var destTokenWallet *ERC20Wallet 32 | 33 | var tokenCfg config.CoinConfig 34 | var tscript EthRedeemScript 35 | 36 | func setupTokenConfigRinkeby() { 37 | clientURL, _ := url.Parse("https://rinkeby.infura.io") 38 | tokenCfg.ClientAPIs = []string{(*clientURL).String()} 39 | tokenCfg.CoinType = wi.Ethereum 40 | tokenCfg.Options = make(map[string]interface{}) 41 | tokenCfg.Options["RegistryAddress"] = "0x403d907982474cdd51687b09a8968346159378f3" 42 | tokenCfg.Options["Name"] = "OBToken" 43 | tokenCfg.Options["Symbol"] = "OBT" 44 | tokenCfg.Options["MainNetAddress"] = "0xe46ea07736e68df951df7b987dda453962ba7d5a" 45 | } 46 | 47 | func setupERCTokenRedeemScript(timeout time.Duration, threshold int) { 48 | 49 | chaincode := make([]byte, 32) 50 | _, err := rand.Read(chaincode) 51 | fmt.Println("chiancode : ", chaincode) 52 | if err != nil { 53 | fmt.Println(err) 54 | chaincode = []byte("423b5d4c32345ced77393b3530b1eed1") 55 | } 56 | tscript.TxnID = common.BytesToAddress(chaincode) 57 | tscript.Timeout = uint32(timeout.Hours()) 58 | tscript.Threshold = uint8(threshold) 59 | tscript.Buyer = common.HexToAddress(mnemonicStrAddress) 60 | tscript.Seller = common.HexToAddress(validDestinationAddress) 61 | tscript.Moderator = common.BigToAddress(big.NewInt(0)) 62 | //tscript.Moderator = common.HexToAddress("0xa6Ac51BB2593e833C629A3352c4c432267714385") 63 | tscript.MultisigAddress = common.HexToAddress("0x36e19e91DFFCA4251f4fB541f5c3a596252eA4BB") 64 | tscript.TokenAddress = common.HexToAddress("0xe46ea07736e68df951df7b987dda453962ba7d5a") 65 | 66 | //fmt.Println("in setup tscript: ") 67 | //spew.Dump(tscript) 68 | } 69 | 70 | func setupSourceErc20Wallet() { 71 | setupTokenConfigRinkeby() 72 | validTokenWallet, _ = NewERC20Wallet(tokenCfg, nil, mnemonicStr, nil) 73 | } 74 | 75 | func TestNewErc20WalletWithValidCoinConfigValues(t *testing.T) { 76 | setupTokenConfigRinkeby() 77 | wallet, err := NewERC20Wallet(tokenCfg, nil, mnemonicStr, nil) 78 | if err != nil || wallet == nil { 79 | t.Errorf("valid credentials should return a wallet") 80 | } 81 | fmt.Println(wallet.address.String()) 82 | fmt.Println(validSourceAddress) 83 | if wallet.address.String() != mnemonicStrAddress { 84 | t.Errorf("valid credentials should return a wallet with proper initialization") 85 | } 86 | } 87 | 88 | func TestTokenWalletChainTip(t *testing.T) { 89 | setupSourceErc20Wallet() 90 | 91 | emptyHash, _ := chainhash.NewHashFromStr("") 92 | 93 | tip, hash := validTokenWallet.ChainTip() 94 | 95 | if hash.String() == emptyHash.String() { 96 | t.Errorf("valid wallet should return chaintip") 97 | } 98 | fmt.Println("Chaintip is : ", tip) 99 | } 100 | 101 | func TestWalletGetTokenBalance(t *testing.T) { 102 | setupSourceErc20Wallet() 103 | 104 | if _, err := validTokenWallet.GetBalance(); err != nil { 105 | t.Errorf("valid wallet should return balance") 106 | } 107 | } 108 | 109 | func TestWalletGetTokenUnconfirmedBalance(t *testing.T) { 110 | setupSourceErc20Wallet() 111 | 112 | if _, err := validTokenWallet.GetUnconfirmedBalance(); err != nil { 113 | t.Errorf("valid wallet should return unconfirmed balance") 114 | } 115 | } 116 | 117 | func TestTokenWalletCurrencyCode(t *testing.T) { 118 | setupSourceErc20Wallet() 119 | 120 | if validTokenWallet.CurrencyCode() != "OBT" { 121 | t.Errorf("wallet should return proper currency code") 122 | } 123 | } 124 | 125 | func TestTokenWalletIsDust(t *testing.T) { 126 | setupSourceErc20Wallet() 127 | 128 | if validTokenWallet.IsDust(int64(10000 + 10000)) { 129 | t.Errorf("wallet should not indicate wrong dust") 130 | } 131 | 132 | if !validTokenWallet.IsDust(int64(10000 - 100)) { 133 | t.Errorf("wallet should not indicate wrong dust") 134 | } 135 | } 136 | 137 | func TestTokenWalletCurrentAddress(t *testing.T) { 138 | setupSourceErc20Wallet() 139 | 140 | addr := validTokenWallet.CurrentAddress(wi.EXTERNAL) 141 | 142 | if addr.String() != mnemonicStrAddress { 143 | t.Errorf("wallet should return correct current address") 144 | } 145 | } 146 | 147 | func TestTokenWalletNewAddress(t *testing.T) { 148 | setupSourceErc20Wallet() 149 | 150 | addr := validTokenWallet.NewAddress(wi.EXTERNAL) 151 | 152 | if addr.String() != mnemonicStrAddress { 153 | t.Errorf("wallet should return correct new address") 154 | } 155 | } 156 | 157 | func TestTokenWalletContractAddTransaction(t *testing.T) { 158 | setupSourceErc20Wallet() 159 | 160 | ver, err := validTokenWallet.registry.GetRecommendedVersion(nil, "escrow") 161 | if err != nil { 162 | t.Error("error fetching escrow from registry") 163 | } 164 | 165 | if util.IsZeroAddress(ver.Implementation) { 166 | log.Infof("escrow not available") 167 | return 168 | } 169 | 170 | d, _ := time.ParseDuration("1h") 171 | setupERCTokenRedeemScript(d, 1) 172 | 173 | tscript.MultisigAddress = ver.Implementation 174 | 175 | redeemScript, err := SerializeEthScript(tscript) 176 | if err != nil { 177 | t.Error("error serializing redeem script") 178 | } 179 | 180 | fmt.Println(redeemScript) 181 | 182 | spew.Dump(tscript) 183 | 184 | orderValue := big.NewInt(345678123478789123) 185 | 186 | hash, err := validTokenWallet.callAddTokenTransaction(tscript, orderValue) 187 | 188 | fmt.Println("returned hash : ", hash) 189 | fmt.Println(err) 190 | 191 | chash, err := chainhash.NewHashFromStr(hash.String()) 192 | 193 | fmt.Println("err : ", err) 194 | 195 | if err == nil { 196 | txn, err := validTokenWallet.GetTransaction(*chash) 197 | 198 | spew.Dump(txn) 199 | fmt.Println(err) 200 | } 201 | 202 | output := wi.TransactionOutput{ 203 | Address: EthAddress{&tscript.Seller}, 204 | Value: *orderValue, 205 | Index: 1, 206 | } 207 | 208 | hkey := hd.NewExtendedKey([]byte{}, []byte{}, []byte{}, []byte{}, 0, 0, false) 209 | 210 | sig, err := validTokenWallet.CreateMultisigSignature([]wi.TransactionInput{}, []wi.TransactionOutput{output}, 211 | hkey, redeemScript, 2000) 212 | 213 | if err != nil { 214 | fmt.Println(err) 215 | } 216 | 217 | fmt.Println(sig) 218 | 219 | time.Sleep(5 * time.Minute) 220 | 221 | txBytes, err := validTokenWallet.Multisign([]wi.TransactionInput{}, 222 | []wi.TransactionOutput{output}, 223 | sig, []wi.Signature{wi.Signature{InputIndex: 1, Signature: []byte{}}}, redeemScript, 224 | 20000, true) 225 | //fmt.Println("after multisign") 226 | //fmt.Println(txBytes) 227 | fmt.Println("err : ", err) 228 | 229 | mtx := &types.Transaction{} 230 | 231 | mtx.UnmarshalJSON(txBytes) 232 | 233 | spew.Dump(mtx) 234 | 235 | sshh, sshhstr, _ := GenTokenScriptHash(tscript) 236 | 237 | fmt.Println("script hash for ct : ", sshh) 238 | fmt.Println(sshhstr) 239 | 240 | } 241 | 242 | func TestTokenWalletContractApproveEvent(t *testing.T) { 243 | setupSourceErc20Wallet() 244 | 245 | ver, err := validTokenWallet.registry.GetRecommendedVersion(nil, "escrow") 246 | if err != nil { 247 | t.Error("error fetching escrow from registry") 248 | } 249 | 250 | if util.IsZeroAddress(ver.Implementation) { 251 | log.Infof("escrow not available") 252 | return 253 | } 254 | 255 | d, _ := time.ParseDuration("1h") 256 | setupERCTokenRedeemScript(d, 1) 257 | 258 | tscript.MultisigAddress = ver.Implementation 259 | 260 | redeemScript, err := SerializeEthScript(tscript) 261 | if err != nil { 262 | t.Error("error serializing redeem script") 263 | } 264 | 265 | fmt.Println(redeemScript) 266 | 267 | spew.Dump(tscript) 268 | 269 | orderValue := big.NewInt(34567812347878) 270 | 271 | fromAddress := validTokenWallet.account.Address() 272 | nonce, err := validTokenWallet.client.PendingNonceAt(context.Background(), fromAddress) 273 | if err != nil { 274 | log.Fatal(err) 275 | } 276 | gasPrice, err := validTokenWallet.client.SuggestGasPrice(context.Background()) 277 | if err != nil { 278 | log.Fatal(err) 279 | } 280 | auth := bind.NewKeyedTransactor(validTokenWallet.account.privateKey) 281 | 282 | auth.Nonce = big.NewInt(int64(nonce)) 283 | auth.Value = big.NewInt(0) // in wei 284 | auth.GasLimit = 4000000 // in units 285 | auth.GasPrice = gasPrice 286 | 287 | header, err := validTokenWallet.client.HeaderByNumber(context.Background(), nil) 288 | fmt.Println("current header no : ", header.Number.Int64()) 289 | 290 | var tx *types.Transaction 291 | 292 | tx, err = validTokenWallet.token.Approve(auth, script.MultisigAddress, orderValue) 293 | 294 | if err != nil { 295 | log.Error(err) 296 | return 297 | } 298 | 299 | spew.Dump(tx) 300 | 301 | tclient, err := ethclient.Dial("wss://rinkeby.infura.io/ws") 302 | if err != nil { 303 | log.Error(err) 304 | } 305 | 306 | //tchan := make(chan *TokenApproval) 307 | 308 | validTokenWallet.client, _ = NewEthClient("wss://rinkeby.infura.io/ws") // &EthClient{tclient, ""} 309 | 310 | //var startBlock *uint64 311 | startBlock := new(uint64) //(*uint64)(unsafe.Pointer(&uint64(0))) 312 | 313 | *startBlock = header.Number.Uint64() 314 | 315 | /* 316 | wopts := &bind.WatchOpts{ 317 | Start: startBlock, 318 | Context: context.Background(), 319 | } 320 | 321 | sub, err := validTokenWallet.token.WatchApproval(wopts, tchan, 322 | []common.Address{validTokenWallet.account.Address()}, 323 | []common.Address{script.MultisigAddress}) 324 | if err != nil { 325 | fmt.Println("cannot watch") 326 | log.Error(err) 327 | return 328 | } 329 | 330 | for { 331 | select { 332 | case err := <-sub.Err(): 333 | log.Error(err) 334 | break 335 | case tlog := <-tchan: 336 | fmt.Println("yyyyyyyyyyy") 337 | fmt.Println(tlog) 338 | } 339 | } 340 | */ 341 | 342 | validTokenWallet.token.FilterApproval(nil, 343 | []common.Address{validTokenWallet.account.Address()}, 344 | []common.Address{script.MultisigAddress}) 345 | 346 | query := ethereum.FilterQuery{ 347 | Addresses: []common.Address{tscript.TokenAddress}, 348 | FromBlock: header.Number, 349 | Topics: [][]common.Hash{{common.HexToHash("0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925")}}, 350 | } 351 | logs := make(chan types.Log) 352 | sub1, err := tclient.SubscribeFilterLogs(context.Background(), query, logs) 353 | if err != nil { 354 | log.Fatal(err) 355 | } 356 | defer sub1.Unsubscribe() 357 | flag := false 358 | for !flag { 359 | select { 360 | case err := <-sub1.Err(): 361 | log.Fatal(err) 362 | case vLog := <-logs: 363 | //fmt.Println(vLog) // pointer to event log 364 | //spew.Dump(vLog) 365 | //fmt.Println(vLog.Topics[0]) 366 | fmt.Println(vLog.Address.String()) 367 | if tx.Hash() == vLog.TxHash { 368 | fmt.Println("we have found the approval ...") 369 | spew.Dump(vLog) 370 | flag = true 371 | break 372 | } 373 | } 374 | } 375 | 376 | } 377 | 378 | func TestTokenWalletContractScriptHash(t *testing.T) { 379 | setupSourceErc20Wallet() 380 | 381 | ver, err := validTokenWallet.registry.GetRecommendedVersion(nil, "escrow") 382 | if err != nil { 383 | t.Error("error fetching escrow from registry") 384 | } 385 | 386 | if util.IsZeroAddress(ver.Implementation) { 387 | log.Infof("escrow not available") 388 | return 389 | } 390 | 391 | d, _ := time.ParseDuration("1h") 392 | setupERCTokenRedeemScript(d, 1) 393 | 394 | chaincode := []byte("423b5d4c32345ced77393b3530b1eed1") 395 | 396 | tscript.TxnID = common.BytesToAddress(chaincode) 397 | 398 | tscript.MultisigAddress = ver.Implementation 399 | 400 | /* 401 | fmt.Println("buyer : ", script.Buyer) 402 | fmt.Println("seller : ", script.Seller) 403 | fmt.Println("moderator : ", script.Moderator) 404 | fmt.Println("threshold : ", script.Threshold) 405 | fmt.Println("timeout : ", script.Timeout) 406 | fmt.Println("scrptHash : ", shash) 407 | */ 408 | 409 | spew.Dump(tscript) 410 | 411 | fmt.Println("escrow address : ", ver.Implementation.String()) 412 | 413 | smtct, err := NewEscrow(ver.Implementation, validTokenWallet.client) 414 | if err != nil { 415 | t.Errorf("error initilaizing contract failed: %s", err.Error()) 416 | } 417 | 418 | retHash, err := smtct.CalculateRedeemScriptHash(nil, tscript.TxnID, tscript.Threshold, 419 | tscript.Timeout, tscript.Buyer, tscript.Seller, tscript.Moderator, tscript.TokenAddress) 420 | 421 | fmt.Println(err) 422 | fmt.Println("from smtct : ", retHash) 423 | 424 | rethash1Str := hexutil.Encode(retHash[:]) 425 | fmt.Println("rethash1Str : ", rethash1Str) 426 | 427 | ahash := crypto.NewKeccak256() 428 | a := make([]byte, 4) 429 | binary.BigEndian.PutUint32(a, tscript.Timeout) 430 | arr := append(tscript.TxnID.Bytes(), append([]byte{tscript.Threshold}, 431 | append(a[:], append(tscript.Buyer.Bytes(), 432 | append(tscript.Seller.Bytes(), append(tscript.Moderator.Bytes(), 433 | append(tscript.MultisigAddress.Bytes(), 434 | append(tscript.TokenAddress.Bytes())...)...)...)...)...)...)...) 435 | ahash.Write(arr) 436 | ahashStr := hexutil.Encode(ahash.Sum(nil)[:]) 437 | 438 | fmt.Println("computed : ", ahashStr) 439 | 440 | if rethash1Str == ahashStr { 441 | fmt.Println("yay!!!!!!!!!!!!") 442 | } 443 | 444 | fmt.Println("priv key : ", validTokenWallet.account.privateKey) 445 | 446 | b := []byte{161, 162, 209, 139, 227, 101, 186, 196, 93, 247, 64, 186, 79, 166, 235, 225, 191, 123, 139, 89, 247, 48, 49, 71, 46, 130, 125, 221, 137, 35, 41, 51} 447 | 448 | fmt.Println(hexutil.Encode(b)) 449 | 450 | privateKeyBytes := crypto.FromECDSA(validTokenWallet.account.privateKey) 451 | fmt.Println(hexutil.Encode(privateKeyBytes)[2:]) 452 | 453 | fmt.Println("dest : ", tscript.MultisigAddress.String()[2:]) 454 | fmt.Println("dest : ", string(tscript.MultisigAddress.Bytes())) 455 | fmt.Println("dest : ", tscript.MultisigAddress.Hex()) 456 | fmt.Println("dest : ", []byte(tscript.MultisigAddress.String())[2:]) 457 | 458 | a1, b1, c1 := GenTokenScriptHash(tscript) 459 | fmt.Println("scrpt hash : ", a1, " ", b1[2:], " ", c1) 460 | } 461 | -------------------------------------------------------------------------------- /wallet/exchange_rates.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net" 7 | "net/http" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | exchange "github.com/OpenBazaar/spvwallet/exchangerates" 15 | "golang.org/x/net/proxy" 16 | ) 17 | 18 | // ExchangeRateProvider - used for looking up exchange rates for ETH 19 | type ExchangeRateProvider struct { 20 | fetchURL string 21 | cache map[string]float64 22 | client *http.Client 23 | decoder ExchangeRateDecoder 24 | bitcoinProvider *exchange.BitcoinPriceFetcher 25 | } 26 | 27 | // ExchangeRateDecoder - used for serializing/deserializing provider struct 28 | type ExchangeRateDecoder interface { 29 | decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) 30 | } 31 | 32 | // OpenBazaarDecoder - decoder to be used by OB 33 | type OpenBazaarDecoder struct{} 34 | 35 | // KrakenDecoder - decoder with Kraken exchange as provider 36 | type KrakenDecoder struct{} 37 | 38 | // PoloniexDecoder - decoder with Poloniex exchange as provider 39 | type PoloniexDecoder struct{} 40 | 41 | // BitfinexDecoder - decoder with Bitfinex exchange as provider 42 | type BitfinexDecoder struct{} 43 | 44 | // BittrexDecoder - decoder with Bittrex exchange as provider 45 | type BittrexDecoder struct{} 46 | 47 | // EthereumPriceFetcher - get ETH prices from the providers (exchanges) 48 | type EthereumPriceFetcher struct { 49 | sync.Mutex 50 | cache map[string]float64 51 | providers []*ExchangeRateProvider 52 | } 53 | 54 | // NewEthereumPriceFetcher - instantiate a eth price fetcher 55 | func NewEthereumPriceFetcher(dialer proxy.Dialer) *EthereumPriceFetcher { 56 | bp := exchange.NewBitcoinPriceFetcher(dialer) 57 | z := EthereumPriceFetcher{ 58 | cache: make(map[string]float64), 59 | } 60 | dial := net.Dial 61 | if dialer != nil { 62 | dial = dialer.Dial 63 | } 64 | tbTransport := &http.Transport{Dial: dial} 65 | client := &http.Client{Transport: tbTransport, Timeout: time.Minute} 66 | 67 | z.providers = []*ExchangeRateProvider{ 68 | {"https://api.kraken.com/0/public/Ticker?pair=ETHXBT", z.cache, client, KrakenDecoder{}, bp}, 69 | } 70 | go z.run() 71 | return &z 72 | } 73 | 74 | // GetExchangeRate - fetch the exchange rate for the specified currency 75 | func (z *EthereumPriceFetcher) GetExchangeRate(currencyCode string) (float64, error) { 76 | currencyCode = NormalizeCurrencyCode(currencyCode) 77 | 78 | z.Lock() 79 | defer z.Unlock() 80 | price, ok := z.cache[currencyCode] 81 | if !ok { 82 | return 0, errors.New("currency not tracked") 83 | } 84 | return price, nil 85 | } 86 | 87 | // GetLatestRate - refresh the cache and return the latest exchange rate for the specified currency 88 | func (z *EthereumPriceFetcher) GetLatestRate(currencyCode string) (float64, error) { 89 | currencyCode = NormalizeCurrencyCode(currencyCode) 90 | 91 | z.fetchCurrentRates() 92 | z.Lock() 93 | defer z.Unlock() 94 | price, ok := z.cache[currencyCode] 95 | if !ok { 96 | return 0, errors.New("currency not tracked") 97 | } 98 | return price, nil 99 | } 100 | 101 | // GetAllRates - refresh the cache 102 | func (z *EthereumPriceFetcher) GetAllRates(cacheOK bool) (map[string]float64, error) { 103 | if !cacheOK { 104 | err := z.fetchCurrentRates() 105 | if err != nil { 106 | return nil, err 107 | } 108 | } 109 | z.Lock() 110 | defer z.Unlock() 111 | copy := make(map[string]float64, len(z.cache)) 112 | for k, v := range z.cache { 113 | copy[k] = v 114 | } 115 | return copy, nil 116 | } 117 | 118 | // UnitsPerCoin - return weis in 1 ETH 119 | func (z *EthereumPriceFetcher) UnitsPerCoin() int64 { 120 | return 1000000000000000000 121 | } 122 | 123 | func (z *EthereumPriceFetcher) fetchCurrentRates() error { 124 | z.Lock() 125 | defer z.Unlock() 126 | for _, provider := range z.providers { 127 | err := provider.fetch() 128 | if err == nil { 129 | return nil 130 | } 131 | } 132 | return errors.New("all exchange rate API queries failed") 133 | } 134 | 135 | func (z *EthereumPriceFetcher) run() { 136 | z.fetchCurrentRates() 137 | ticker := time.NewTicker(time.Minute * 15) 138 | defer ticker.Stop() 139 | for range ticker.C { 140 | z.fetchCurrentRates() 141 | } 142 | } 143 | 144 | func (provider *ExchangeRateProvider) fetch() (err error) { 145 | if len(provider.fetchURL) == 0 { 146 | err = errors.New("provider has no fetchUrl") 147 | return err 148 | } 149 | resp, err := provider.client.Get(provider.fetchURL) 150 | if err != nil { 151 | return err 152 | } 153 | decoder := json.NewDecoder(resp.Body) 154 | var dataMap interface{} 155 | err = decoder.Decode(&dataMap) 156 | if err != nil { 157 | return err 158 | } 159 | return provider.decoder.decode(dataMap, provider.cache, provider.bitcoinProvider) 160 | } 161 | 162 | func (b OpenBazaarDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { 163 | //data := dat.(map[string]interface{}) 164 | data, ok := dat.(map[string]interface{}) 165 | if !ok { 166 | return errors.New(reflect.TypeOf(b).Name() + ".decode: type assertion failed invalid json") 167 | } 168 | 169 | eth, ok := data["ETH"] 170 | if !ok { 171 | return errors.New(reflect.TypeOf(b).Name() + ".decode: type assertion failed, missing 'ETH' field") 172 | } 173 | val, ok := eth.(map[string]interface{}) 174 | if !ok { 175 | return errors.New(reflect.TypeOf(b).Name() + ".decode: type assertion failed") 176 | } 177 | ethRate, ok := val["last"].(float64) 178 | if !ok { 179 | return errors.New(reflect.TypeOf(b).Name() + ".decode: type assertion failed, missing 'last' (float) field") 180 | } 181 | for k, v := range data { 182 | if k != "timestamp" { 183 | val, ok := v.(map[string]interface{}) 184 | if !ok { 185 | return errors.New(reflect.TypeOf(b).Name() + ".decode: type assertion failed") 186 | } 187 | price, ok := val["last"].(float64) 188 | if !ok { 189 | return errors.New(reflect.TypeOf(b).Name() + ".decode: type assertion failed, missing 'last' (float) field") 190 | } 191 | cache[k] = price * (1 / ethRate) 192 | } 193 | } 194 | return nil 195 | } 196 | 197 | func (b KrakenDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { 198 | rates, err := bp.GetAllRates(false) 199 | if err != nil { 200 | return err 201 | } 202 | obj, ok := dat.(map[string]interface{}) 203 | if !ok { 204 | return errors.New("krakenDecoder type assertion failure") 205 | } 206 | result, ok := obj["result"] 207 | if !ok { 208 | return errors.New("krakenDecoder: field `result` not found") 209 | } 210 | resultMap, ok := result.(map[string]interface{}) 211 | if !ok { 212 | return errors.New("KrakenDecoder type assertion failure") 213 | } 214 | pair, ok := resultMap["XETHXXBT"] 215 | if !ok { 216 | return errors.New("krakenDecoder: field `ETHXBT` not found") 217 | } 218 | pairMap, ok := pair.(map[string]interface{}) 219 | if !ok { 220 | return errors.New("krakenDecoder type assertion failure") 221 | } 222 | c, ok := pairMap["c"] 223 | if !ok { 224 | return errors.New("krakenDecoder: field `c` not found") 225 | } 226 | cList, ok := c.([]interface{}) 227 | if !ok { 228 | return errors.New("krakenDecoder type assertion failure") 229 | } 230 | rateStr, ok := cList[0].(string) 231 | if !ok { 232 | return errors.New("krakenDecoder type assertion failure") 233 | } 234 | price, err := strconv.ParseFloat(rateStr, 64) 235 | if err != nil { 236 | return err 237 | } 238 | rate := price 239 | 240 | if rate == 0 { 241 | return errors.New("bitcoin-ethereum price data not available") 242 | } 243 | for k, v := range rates { 244 | cache[k] = v * rate 245 | } 246 | return nil 247 | } 248 | 249 | func (b BitfinexDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { 250 | rates, err := bp.GetAllRates(false) 251 | if err != nil { 252 | return err 253 | } 254 | obj, ok := dat.(map[string]interface{}) 255 | if !ok { 256 | return errors.New("bitfinexDecoder: type assertion failure") 257 | } 258 | r, ok := obj["last_price"] 259 | if !ok { 260 | return errors.New("bitfinexDecoder: field `last_price` not found") 261 | } 262 | rateStr, ok := r.(string) 263 | if !ok { 264 | return errors.New("bitfinexDecoder: type assertion failure") 265 | } 266 | price, err := strconv.ParseFloat(rateStr, 64) 267 | if err != nil { 268 | return err 269 | } 270 | rate := price 271 | 272 | if rate == 0 { 273 | return errors.New("bitcoin-ethereum price data not available") 274 | } 275 | for k, v := range rates { 276 | cache[k] = v * rate 277 | } 278 | return nil 279 | } 280 | 281 | func (b BittrexDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { 282 | rates, err := bp.GetAllRates(false) 283 | if err != nil { 284 | return err 285 | } 286 | obj, ok := dat.(map[string]interface{}) 287 | if !ok { 288 | return errors.New("bittrexDecoder: type assertion failure") 289 | } 290 | result, ok := obj["result"] 291 | if !ok { 292 | return errors.New("bittrexDecoder: field `result` not found") 293 | } 294 | resultMap, ok := result.(map[string]interface{}) 295 | if !ok { 296 | return errors.New("bittrexDecoder: type assertion failure") 297 | } 298 | exRate, ok := resultMap["Last"] 299 | if !ok { 300 | return errors.New("bittrexDecoder: field `Last` not found") 301 | } 302 | rate, ok := exRate.(float64) 303 | if !ok { 304 | return errors.New("bittrexDecoder type assertion failure") 305 | } 306 | 307 | if rate == 0 { 308 | return errors.New("bitcoin-ethereum price data not available") 309 | } 310 | for k, v := range rates { 311 | cache[k] = v * rate 312 | } 313 | return nil 314 | } 315 | 316 | func (b PoloniexDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { 317 | rates, err := bp.GetAllRates(false) 318 | if err != nil { 319 | return err 320 | } 321 | data, ok := dat.(map[string]interface{}) 322 | if !ok { 323 | return errors.New(reflect.TypeOf(b).Name() + ".decode: type assertion failed") 324 | } 325 | var rate float64 326 | v := data["BTC_ETH"] 327 | //data := dat.(map[string]interface{}) 328 | //var rate float64 329 | 330 | val, ok := v.(map[string]interface{}) 331 | if !ok { 332 | return errors.New(reflect.TypeOf(b).Name() + ".decode: type assertion failed") 333 | } 334 | s, ok := val["last"].(string) 335 | if !ok { 336 | return errors.New(reflect.TypeOf(b).Name() + ".decode: type assertion failed, missing 'last' (string) field") 337 | } 338 | price, err := strconv.ParseFloat(s, 64) 339 | if err != nil { 340 | return err 341 | } 342 | rate = price 343 | 344 | if rate == 0 { 345 | return errors.New("bitcoin-ethereum price data not available") 346 | } 347 | for k, v := range rates { 348 | cache[k] = v * rate 349 | } 350 | return nil 351 | } 352 | 353 | // NormalizeCurrencyCode standardizes the format for the given currency code 354 | func NormalizeCurrencyCode(currencyCode string) string { 355 | return strings.ToUpper(currencyCode) 356 | } 357 | -------------------------------------------------------------------------------- /wallet/service.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/OpenBazaar/wallet-interface" 8 | "github.com/btcsuite/btcd/chaincfg/chainhash" 9 | "github.com/ethereum/go-ethereum/ethclient" 10 | ) 11 | 12 | // Service - used to represent WalletService 13 | type Service struct { 14 | db wallet.Datastore 15 | client *ethclient.Client 16 | coinType wallet.CoinType 17 | 18 | chainHeight uint32 19 | bestBlock string 20 | 21 | lock sync.RWMutex 22 | 23 | doneChan chan struct{} 24 | } 25 | 26 | const nullHash = "0000000000000000000000000000000000000000000000000000000000000000" 27 | 28 | // NewWalletService - used to create new wallet service 29 | func NewWalletService(db wallet.Datastore, client *ethclient.Client, coinType wallet.CoinType) *Service { 30 | return &Service{db, client, coinType, 0, nullHash, sync.RWMutex{}, make(chan struct{})} 31 | } 32 | 33 | // Start - the wallet daemon 34 | func (ws *Service) Start() { 35 | log.Infof("Starting %s WalletService", ws.coinType.String()) 36 | go ws.UpdateState() 37 | } 38 | 39 | // Stop - the wallet daemon 40 | func (ws *Service) Stop() { 41 | ws.doneChan <- struct{}{} 42 | } 43 | 44 | // ChainTip - get the chain tip 45 | func (ws *Service) ChainTip() (uint32, chainhash.Hash) { 46 | ws.lock.RLock() 47 | defer ws.lock.RUnlock() 48 | ch, _ := chainhash.NewHashFromStr(ws.bestBlock) 49 | return ws.chainHeight, *ch 50 | } 51 | 52 | // UpdateState - updates state 53 | func (ws *Service) UpdateState() { 54 | // Start by fetching the chain height from the API 55 | log.Debugf("querying for %s chain height", ws.coinType.String()) 56 | best, err := ws.client.HeaderByNumber(context.Background(), nil) 57 | if err == nil { 58 | log.Debugf("%s chain height: %d", ws.coinType.String(), best.Nonce) 59 | ws.lock.Lock() 60 | ws.chainHeight = uint32(best.Number.Uint64()) 61 | ws.bestBlock = best.TxHash.String() 62 | ws.lock.Unlock() 63 | } else { 64 | log.Errorf("error querying API for chain height: %s", err.Error()) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /wallet/wallet.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/ecdsa" 7 | "encoding/binary" 8 | "encoding/gob" 9 | "encoding/hex" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "io/ioutil" 14 | "math/big" 15 | "net/http" 16 | "path" 17 | "runtime" 18 | "sort" 19 | "strconv" 20 | "strings" 21 | "time" 22 | 23 | "github.com/OpenBazaar/multiwallet/config" 24 | wi "github.com/OpenBazaar/wallet-interface" 25 | "github.com/btcsuite/btcd/chaincfg" 26 | "github.com/btcsuite/btcd/chaincfg/chainhash" 27 | "github.com/btcsuite/btcutil" 28 | hd "github.com/btcsuite/btcutil/hdkeychain" 29 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 30 | "github.com/ethereum/go-ethereum/accounts/keystore" 31 | "github.com/ethereum/go-ethereum/common" 32 | "github.com/ethereum/go-ethereum/common/hexutil" 33 | "github.com/ethereum/go-ethereum/core/types" 34 | "github.com/ethereum/go-ethereum/crypto" 35 | "github.com/nanmu42/etherscan-api" 36 | "github.com/op/go-logging" 37 | "golang.org/x/net/proxy" 38 | "gopkg.in/yaml.v2" 39 | 40 | "github.com/OpenBazaar/go-ethwallet/util" 41 | ut "github.com/OpenBazaar/openbazaar-go/util" 42 | ) 43 | 44 | var _ = wi.Wallet(&EthereumWallet{}) 45 | var done, doneBalanceTicker chan bool 46 | 47 | const ( 48 | // InfuraAPIKey is the hard coded Infura API key 49 | InfuraAPIKey = "v3/91c82af0169c4115940c76d331410749" 50 | // EtherScanAPIKey is needed for all Eherscan requests 51 | EtherScanAPIKey = "KA15D8FCHGBFZ4CQ25Y4NZM24417AXWF7M" 52 | maxGasLimit = 400000 53 | ) 54 | 55 | var ( 56 | emptyChainHash *chainhash.Hash 57 | 58 | // EthCurrencyDefinition is eth defaults 59 | EthCurrencyDefinition = wi.CurrencyDefinition{ 60 | Code: "ETH", 61 | Divisibility: 18, 62 | } 63 | log = logging.MustGetLogger("ethwallet") 64 | ) 65 | 66 | func init() { 67 | mustInitEmptyChainHash() 68 | } 69 | 70 | func mustInitEmptyChainHash() { 71 | hash, err := chainhash.NewHashFromStr("") 72 | if err != nil { 73 | panic(fmt.Sprintf("creating emptyChainHash: %s", err.Error())) 74 | } 75 | emptyChainHash = hash 76 | } 77 | 78 | // EthConfiguration - used for eth specific configuration 79 | type EthConfiguration struct { 80 | RopstenPPAddress string `yaml:"ROPSTEN_PPv2_ADDRESS"` 81 | RegistryAddress string `yaml:"ROPSTEN_REGISTRY"` 82 | } 83 | 84 | // EthRedeemScript - used to represent redeem script for eth wallet 85 | // 86 | // 87 | type EthRedeemScript struct { 88 | TxnID common.Address 89 | Threshold uint8 90 | Timeout uint32 91 | Buyer common.Address 92 | Seller common.Address 93 | Moderator common.Address 94 | MultisigAddress common.Address 95 | TokenAddress common.Address 96 | } 97 | 98 | // SerializeEthScript - used to serialize eth redeem script 99 | func SerializeEthScript(scrpt EthRedeemScript) ([]byte, error) { 100 | b := bytes.Buffer{} 101 | e := gob.NewEncoder(&b) 102 | err := e.Encode(scrpt) 103 | return b.Bytes(), err 104 | } 105 | 106 | // DeserializeEthScript - used to deserialize eth redeem script 107 | func DeserializeEthScript(b []byte) (EthRedeemScript, error) { 108 | scrpt := EthRedeemScript{} 109 | buf := bytes.NewBuffer(b) 110 | d := gob.NewDecoder(buf) 111 | err := d.Decode(&scrpt) 112 | return scrpt, err 113 | } 114 | 115 | // PendingTxn used to record a pending eth txn 116 | type PendingTxn struct { 117 | TxnID common.Hash 118 | OrderID string 119 | Amount string 120 | Nonce int32 121 | From string 122 | To string 123 | WithInput bool 124 | } 125 | 126 | // SerializePendingTxn - used to serialize eth pending txn 127 | func SerializePendingTxn(pTxn PendingTxn) ([]byte, error) { 128 | b := bytes.Buffer{} 129 | e := gob.NewEncoder(&b) 130 | err := e.Encode(pTxn) 131 | return b.Bytes(), err 132 | } 133 | 134 | // DeserializePendingTxn - used to deserialize eth pending txn 135 | func DeserializePendingTxn(b []byte) (PendingTxn, error) { 136 | pTxn := PendingTxn{} 137 | buf := bytes.NewBuffer(b) 138 | d := gob.NewDecoder(buf) 139 | err := d.Decode(&pTxn) 140 | return pTxn, err 141 | } 142 | 143 | // GenScriptHash - used to generate script hash for eth as per 144 | // escrow smart contract 145 | func GenScriptHash(script EthRedeemScript) ([32]byte, string, error) { 146 | a := make([]byte, 4) 147 | binary.BigEndian.PutUint32(a, script.Timeout) 148 | arr := append(script.TxnID.Bytes(), append([]byte{script.Threshold}, 149 | append(a[:], append(script.Buyer.Bytes(), 150 | append(script.Seller.Bytes(), append(script.Moderator.Bytes(), 151 | append(script.MultisigAddress.Bytes())...)...)...)...)...)...) 152 | var retHash [32]byte 153 | 154 | copy(retHash[:], crypto.Keccak256(arr)[:]) 155 | ahashStr := hexutil.Encode(retHash[:]) 156 | 157 | return retHash, ahashStr, nil 158 | } 159 | 160 | // EthereumWallet is the wallet implementation for ethereum 161 | type EthereumWallet struct { 162 | client *EthClient 163 | account *Account 164 | address *EthAddress 165 | service *Service 166 | registry *Registry 167 | ppsct *Escrow 168 | db wi.Datastore 169 | exchangeRates wi.ExchangeRates 170 | params *chaincfg.Params 171 | listeners []func(wi.TransactionCallback) 172 | } 173 | 174 | // NewEthereumWalletWithKeyfile will return a reference to the Eth Wallet 175 | func NewEthereumWalletWithKeyfile(url, keyFile, passwd string) *EthereumWallet { 176 | client, err := NewEthClient(url) 177 | if err != nil { 178 | log.Fatalf("error initializing wallet: %v", err) 179 | } 180 | var myAccount *Account 181 | myAccount, err = NewAccountFromKeyfile(keyFile, passwd) 182 | if err != nil { 183 | log.Fatalf("key file validation failed: %s", err.Error()) 184 | } 185 | addr := myAccount.Address() 186 | 187 | _, filename, _, _ := runtime.Caller(0) 188 | conf, err := ioutil.ReadFile(path.Join(path.Dir(filename), "../configuration.yaml")) 189 | 190 | if err != nil { 191 | log.Fatalf("ethereum config not found: %s", err.Error()) 192 | } 193 | ethConfig := EthConfiguration{} 194 | err = yaml.Unmarshal(conf, ðConfig) 195 | if err != nil { 196 | log.Fatalf("ethereum config not valid: %s", err.Error()) 197 | } 198 | 199 | reg, err := NewRegistry(common.HexToAddress(ethConfig.RegistryAddress), client) 200 | if err != nil { 201 | log.Fatalf("error initilaizing contract failed: %s", err.Error()) 202 | } 203 | 204 | return &EthereumWallet{client, myAccount, &EthAddress{&addr}, &Service{}, reg, nil, nil, nil, nil, []func(wi.TransactionCallback){}} 205 | } 206 | 207 | // NewEthereumWallet will return a reference to the Eth Wallet 208 | func NewEthereumWallet(cfg config.CoinConfig, params *chaincfg.Params, mnemonic string, proxy proxy.Dialer) (*EthereumWallet, error) { 209 | client, err := NewEthClient(cfg.ClientAPIs[0] + "/" + InfuraAPIKey) 210 | if err != nil { 211 | log.Errorf("error initializing wallet: %v", err) 212 | return nil, err 213 | } 214 | var myAccount *Account 215 | myAccount, err = NewAccountFromMnemonic(mnemonic, "", params) 216 | if err != nil { 217 | log.Errorf("mnemonic based pk generation failed: %s", err.Error()) 218 | return nil, err 219 | } 220 | addr := myAccount.Address() 221 | 222 | ethConfig := EthConfiguration{} 223 | 224 | var regAddr interface{} 225 | var ok bool 226 | registryKey := "RegistryAddress" 227 | if strings.Contains(cfg.ClientAPIs[0], "rinkeby") { 228 | registryKey = "RinkebyRegistryAddress" 229 | } else if strings.Contains(cfg.ClientAPIs[0], "ropsten") { 230 | registryKey = "RopstenRegistryAddress" 231 | } 232 | if regAddr, ok = cfg.Options[registryKey]; !ok { 233 | log.Errorf("ethereum registry not found: %s", cfg.Options[registryKey]) 234 | return nil, err 235 | } 236 | 237 | ethConfig.RegistryAddress = regAddr.(string) 238 | 239 | reg, err := NewRegistry(common.HexToAddress(ethConfig.RegistryAddress), client) 240 | if err != nil { 241 | log.Errorf("error initilaizing contract failed: %s", err.Error()) 242 | return nil, err 243 | } 244 | er := NewEthereumPriceFetcher(proxy) 245 | 246 | return &EthereumWallet{client, myAccount, &EthAddress{&addr}, &Service{}, reg, nil, cfg.DB, er, params, []func(wi.TransactionCallback){}}, nil 247 | } 248 | 249 | // Params - return nil to comply 250 | func (wallet *EthereumWallet) Params() *chaincfg.Params { 251 | return wallet.params 252 | } 253 | 254 | // GetBalance returns the balance for the wallet 255 | func (wallet *EthereumWallet) GetBalance() (*big.Int, error) { 256 | return wallet.client.GetBalance(wallet.account.Address()) 257 | } 258 | 259 | // GetUnconfirmedBalance returns the unconfirmed balance for the wallet 260 | func (wallet *EthereumWallet) GetUnconfirmedBalance() (*big.Int, error) { 261 | return wallet.client.GetUnconfirmedBalance(wallet.account.Address()) 262 | } 263 | 264 | // Transfer will transfer the amount from this wallet to the spec address 265 | func (wallet *EthereumWallet) Transfer(to string, value *big.Int, spendAll bool, fee big.Int) (common.Hash, error) { 266 | toAddress := common.HexToAddress(to) 267 | return wallet.client.Transfer(wallet.account, toAddress, value, spendAll, fee) 268 | } 269 | 270 | // Start will start the wallet daemon 271 | func (wallet *EthereumWallet) Start() { 272 | done = make(chan bool) 273 | doneBalanceTicker = make(chan bool) 274 | // start the ticker to check for pending txn rcpts 275 | go func(wallet *EthereumWallet) { 276 | ticker := time.NewTicker(5 * time.Second) 277 | defer ticker.Stop() 278 | 279 | for { 280 | select { 281 | case <-done: 282 | return 283 | case <-ticker.C: 284 | // get the pending txns 285 | txns, err := wallet.db.Txns().GetAll(true) 286 | if err != nil { 287 | continue 288 | } 289 | for _, txn := range txns { 290 | hash := common.HexToHash(txn.Txid) 291 | go func(txnData []byte) { 292 | _, err := wallet.checkTxnRcpt(&hash, txnData) 293 | if err != nil { 294 | log.Errorf(err.Error()) 295 | } 296 | }(txn.Bytes) 297 | } 298 | } 299 | } 300 | }(wallet) 301 | 302 | // start the ticker to check for balance 303 | go func(wallet *EthereumWallet) { 304 | ticker := time.NewTicker(15 * time.Second) 305 | defer ticker.Stop() 306 | 307 | currentBalance, err := wallet.GetBalance() 308 | if err != nil { 309 | log.Infof("err fetching initial balance: %v", err) 310 | } 311 | currentTip, _ := wallet.ChainTip() 312 | 313 | for { 314 | select { 315 | case <-doneBalanceTicker: 316 | return 317 | case <-ticker.C: 318 | // fetch the current balance 319 | fetchedBalance, err := wallet.GetBalance() 320 | if err != nil { 321 | log.Infof("err fetching balance at %v: %v", time.Now(), err) 322 | continue 323 | } 324 | if fetchedBalance.Cmp(currentBalance) != 0 { 325 | // process balance change 326 | go wallet.processBalanceChange(currentBalance, fetchedBalance, currentTip) 327 | currentTip, _ = wallet.ChainTip() 328 | currentBalance = fetchedBalance 329 | } 330 | } 331 | } 332 | }(wallet) 333 | } 334 | 335 | func (wallet *EthereumWallet) processBalanceChange(previousBalance, currentBalance *big.Int, currentHead uint32) { 336 | count := 0 337 | cTip := int(currentHead) 338 | value := new(big.Int).Sub(currentBalance, previousBalance) 339 | for count < 30 { 340 | txns, err := wallet.TransactionsFromBlock(&cTip) 341 | if err == nil && len(txns) > 0 { 342 | count = 30 343 | txncb := wi.TransactionCallback{ 344 | Txid: util.EnsureCorrectPrefix(txns[0].Txid), 345 | Outputs: []wi.TransactionOutput{}, 346 | Inputs: []wi.TransactionInput{}, 347 | Height: txns[0].Height, 348 | Timestamp: time.Now(), 349 | Value: *value, 350 | WatchOnly: false, 351 | } 352 | for _, l := range wallet.listeners { 353 | go l(txncb) 354 | } 355 | continue 356 | } 357 | 358 | time.Sleep(2 * time.Second) 359 | count++ 360 | } 361 | } 362 | 363 | func (wallet *EthereumWallet) invokeTxnCB(txnID string, value *big.Int) { 364 | txncb := wi.TransactionCallback{ 365 | Txid: util.EnsureCorrectPrefix(txnID), 366 | Outputs: []wi.TransactionOutput{}, 367 | Inputs: []wi.TransactionInput{}, 368 | Height: 0, 369 | Timestamp: time.Now(), 370 | Value: *value, 371 | WatchOnly: false, 372 | } 373 | for _, l := range wallet.listeners { 374 | go l(txncb) 375 | } 376 | } 377 | 378 | // CurrencyCode returns ETH 379 | func (wallet *EthereumWallet) CurrencyCode() string { 380 | if wallet.params == nil { 381 | return "ETH" 382 | } 383 | if wallet.params.Name == chaincfg.MainNetParams.Name { 384 | return "ETH" 385 | } else { 386 | return "TETH" 387 | } 388 | //return "ETH" 389 | } 390 | 391 | // IsDust Check if this amount is considered dust - 10000 wei 392 | func (wallet *EthereumWallet) IsDust(amount big.Int) bool { 393 | return amount.Cmp(big.NewInt(10000)) <= 0 394 | } 395 | 396 | // MasterPrivateKey - Get the master private key 397 | func (wallet *EthereumWallet) MasterPrivateKey() *hd.ExtendedKey { 398 | return hd.NewExtendedKey([]byte{0x00, 0x00, 0x00, 0x00}, wallet.account.privateKey.D.Bytes(), 399 | wallet.account.address.Bytes(), wallet.account.address.Bytes(), 0, 0, true) 400 | } 401 | 402 | // MasterPublicKey - Get the master public key 403 | func (wallet *EthereumWallet) MasterPublicKey() *hd.ExtendedKey { 404 | publicKey := wallet.account.privateKey.Public() 405 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 406 | if !ok { 407 | log.Fatal("error casting public key to ECDSA") 408 | } 409 | 410 | publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) 411 | return hd.NewExtendedKey([]byte{0x00, 0x00, 0x00, 0x00}, publicKeyBytes, 412 | wallet.account.address.Bytes(), wallet.account.address.Bytes(), 0, 0, false) 413 | } 414 | 415 | // ChildKey Generate a child key using the given chaincode. The key is used in multisig transactions. 416 | // For most implementations this should just be child key 0. 417 | func (wallet *EthereumWallet) ChildKey(keyBytes []byte, chaincode []byte, isPrivateKey bool) (*hd.ExtendedKey, error) { 418 | 419 | parentFP := []byte{0x00, 0x00, 0x00, 0x00} 420 | version := []byte{0x04, 0x88, 0xad, 0xe4} // starts with xprv 421 | if !isPrivateKey { 422 | version = []byte{0x04, 0x88, 0xb2, 0x1e} 423 | } 424 | /* 425 | hdKey := hd.NewExtendedKey( 426 | version, 427 | keyBytes, 428 | chaincode, 429 | parentFP, 430 | 0, 431 | 0, 432 | isPrivateKey) 433 | return hdKey.Child(0) 434 | */ 435 | 436 | return hd.NewExtendedKey(version, keyBytes, chaincode, parentFP, 0, 0, isPrivateKey), nil 437 | } 438 | 439 | // CurrentAddress - Get the current address for the given purpose 440 | func (wallet *EthereumWallet) CurrentAddress(purpose wi.KeyPurpose) btcutil.Address { 441 | return *wallet.address 442 | } 443 | 444 | // NewAddress - Returns a fresh address that has never been returned by this function 445 | func (wallet *EthereumWallet) NewAddress(purpose wi.KeyPurpose) btcutil.Address { 446 | return *wallet.address 447 | } 448 | 449 | // DecodeAddress - Parse the address string and return an address interface 450 | func (wallet *EthereumWallet) DecodeAddress(addr string) (btcutil.Address, error) { 451 | var ( 452 | ethAddr common.Address 453 | err error 454 | ) 455 | if len(addr) > 64 { 456 | ethAddr, err = ethScriptToAddr(addr) 457 | if err != nil { 458 | log.Error(err.Error()) 459 | } 460 | } else { 461 | ethAddr = common.HexToAddress(addr) 462 | } 463 | 464 | return EthAddress{ðAddr}, err 465 | } 466 | 467 | func ethScriptToAddr(addr string) (common.Address, error) { 468 | rScriptBytes, err := hex.DecodeString(addr) 469 | if err != nil { 470 | return common.Address{}, err 471 | } 472 | rScript, err := DeserializeEthScript(rScriptBytes) 473 | if err != nil { 474 | return common.Address{}, err 475 | } 476 | _, sHash, err := GenScriptHash(rScript) 477 | if err != nil { 478 | return common.Address{}, err 479 | } 480 | return common.HexToAddress(sHash), nil 481 | } 482 | 483 | // ScriptToAddress - ? 484 | func (wallet *EthereumWallet) ScriptToAddress(script []byte) (btcutil.Address, error) { 485 | return wallet.address, nil 486 | } 487 | 488 | // HasKey - Returns if the wallet has the key for the given address 489 | func (wallet *EthereumWallet) HasKey(addr btcutil.Address) bool { 490 | if !util.IsValidAddress(addr.String()) { 491 | return false 492 | } 493 | return wallet.account.Address().String() == addr.String() 494 | } 495 | 496 | // Balance - Get the confirmed and unconfirmed balances 497 | func (wallet *EthereumWallet) Balance() (confirmed, unconfirmed wi.CurrencyValue) { 498 | var balance, ucbalance wi.CurrencyValue 499 | bal, err := wallet.GetBalance() 500 | if err == nil { 501 | balance = wi.CurrencyValue{ 502 | Value: *bal, 503 | Currency: EthCurrencyDefinition, 504 | } 505 | } 506 | ucbal, err := wallet.GetUnconfirmedBalance() 507 | ucb := big.NewInt(0) 508 | if err == nil { 509 | if ucbal.Cmp(bal) > 0 { 510 | ucb.Sub(ucbal, bal) 511 | } 512 | } 513 | ucbalance = wi.CurrencyValue{ 514 | Value: *ucb, 515 | Currency: EthCurrencyDefinition, 516 | } 517 | return balance, ucbalance 518 | } 519 | 520 | // TransactionsFromBlock - Returns a list of transactions for this wallet begining from the specified block 521 | func (wallet *EthereumWallet) TransactionsFromBlock(startBlock *int) ([]wi.Txn, error) { 522 | txns, err := wallet.client.eClient.NormalTxByAddress(util.EnsureCorrectPrefix(wallet.account.Address().String()), startBlock, nil, 523 | 1, 0, false) 524 | if err != nil { 525 | log.Error("err fetching transactions : ", err) 526 | return []wi.Txn{}, nil 527 | } 528 | 529 | ret := []wi.Txn{} 530 | for _, t := range txns { 531 | status := wi.StatusConfirmed 532 | prefix := "" 533 | if t.IsError != 0 { 534 | status = wi.StatusError 535 | } 536 | if strings.ToLower(t.From) == strings.ToLower(wallet.address.String()) { 537 | prefix = "-" 538 | } 539 | tnew := wi.Txn{ 540 | Txid: util.EnsureCorrectPrefix(t.Hash), 541 | Value: prefix + t.Value.Int().String(), 542 | Height: int32(t.BlockNumber), 543 | Timestamp: t.TimeStamp.Time(), 544 | WatchOnly: false, 545 | Confirmations: int64(t.Confirmations), 546 | Status: wi.StatusCode(status), 547 | Bytes: []byte(t.Input), 548 | } 549 | ret = append(ret, tnew) 550 | } 551 | 552 | return ret, nil 553 | } 554 | 555 | // Transactions - Returns a list of transactions for this wallet 556 | func (wallet *EthereumWallet) Transactions() ([]wi.Txn, error) { 557 | return wallet.TransactionsFromBlock(nil) 558 | } 559 | 560 | // GetTransaction - Get info on a specific transaction 561 | func (wallet *EthereumWallet) GetTransaction(txid chainhash.Hash) (wi.Txn, error) { 562 | tx, _, err := wallet.client.GetTransaction(common.HexToHash(util.EnsureCorrectPrefix(txid.String()))) 563 | if err != nil { 564 | return wi.Txn{}, err 565 | } 566 | 567 | chainID, err := wallet.client.NetworkID(context.Background()) 568 | if err != nil { 569 | return wi.Txn{}, err 570 | } 571 | 572 | msg, err := tx.AsMessage(types.NewEIP155Signer(chainID)) // HomesteadSigner{}) 573 | if err != nil { 574 | return wi.Txn{}, err 575 | } 576 | 577 | //value := tx.Value().String() 578 | fromAddr := msg.From() 579 | toAddr := msg.To() 580 | valueSub := big.NewInt(5000000) 581 | 582 | v, err := wallet.registry.GetRecommendedVersion(nil, "escrow") 583 | if err == nil { 584 | if tx.To().String() == v.Implementation.String() { 585 | toAddr = wallet.address.address 586 | } 587 | if msg.Value().Cmp(valueSub) > 0 { 588 | valueSub = msg.Value() 589 | } 590 | } 591 | 592 | return wi.Txn{ 593 | Txid: util.EnsureCorrectPrefix(tx.Hash().Hex()), 594 | Value: tx.Value().String(), 595 | Height: 0, 596 | Timestamp: time.Now(), 597 | WatchOnly: false, 598 | Bytes: tx.Data(), 599 | ToAddress: util.EnsureCorrectPrefix(tx.To().String()), 600 | FromAddress: util.EnsureCorrectPrefix(msg.From().Hex()), 601 | Outputs: []wi.TransactionOutput{ 602 | { 603 | Address: EthAddress{toAddr}, 604 | Value: *valueSub, 605 | Index: 0, 606 | }, 607 | { 608 | Address: EthAddress{&fromAddr}, 609 | Value: *valueSub, 610 | Index: 1, 611 | }, 612 | { 613 | Address: EthAddress{msg.To()}, 614 | Value: *valueSub, 615 | Index: 2, 616 | }, 617 | }, 618 | }, nil 619 | } 620 | 621 | // ChainTip - Get the height and best hash of the blockchain 622 | func (wallet *EthereumWallet) ChainTip() (uint32, chainhash.Hash) { 623 | num, hash, err := wallet.client.GetLatestBlock() 624 | if err != nil { 625 | return 0, *emptyChainHash 626 | } 627 | h, err := util.CreateChainHash(hash.Hex()) 628 | if err != nil { 629 | log.Error(err.Error()) 630 | h = emptyChainHash 631 | } 632 | return num, *h 633 | } 634 | 635 | // GetFeePerByte - Get the current fee per byte 636 | func (wallet *EthereumWallet) GetFeePerByte(feeLevel wi.FeeLevel) big.Int { 637 | est, err := wallet.client.GetEthGasStationEstimate() 638 | ret := big.NewInt(0) 639 | if err != nil { 640 | log.Errorf("err fetching ethgas station data: %v", err) 641 | return *ret 642 | } 643 | switch feeLevel { 644 | case wi.NORMAL: 645 | ret, _ = big.NewFloat(est.Average * 100000000).Int(nil) 646 | case wi.ECONOMIC, wi.SUPER_ECONOMIC: 647 | ret, _ = big.NewFloat(est.SafeLow * 100000000).Int(nil) 648 | case wi.PRIOIRTY, wi.FEE_BUMP: 649 | ret, _ = big.NewFloat(est.Fast * 100000000).Int(nil) 650 | } 651 | return *ret 652 | } 653 | 654 | // Spend - Send ether to an external wallet 655 | func (wallet *EthereumWallet) Spend(amount big.Int, addr btcutil.Address, feeLevel wi.FeeLevel, referenceID string, spendAll bool) (*chainhash.Hash, error) { 656 | var hash common.Hash 657 | var h *chainhash.Hash 658 | var err error 659 | actualRecipient := addr 660 | 661 | if referenceID == "" { 662 | // no referenceID means this is a direct transfer 663 | hash, err = wallet.Transfer(util.EnsureCorrectPrefix(addr.String()), &amount, spendAll, wallet.GetFeePerByte(feeLevel)) 664 | } else { 665 | // this is a spend which means it has to be linked to an order 666 | // specified using the referenceID 667 | 668 | // check if the addr is a multisig addr 669 | scripts, err := wallet.db.WatchedScripts().GetAll() 670 | if err != nil { 671 | return nil, err 672 | } 673 | isScript := false 674 | addrEth := common.HexToAddress(addr.String()) 675 | key := addrEth.Bytes() 676 | redeemScript := []byte{} 677 | 678 | for _, script := range scripts { 679 | if bytes.Equal(key, script[:common.AddressLength]) { 680 | isScript = true 681 | redeemScript = script[common.AddressLength:] 682 | break 683 | } 684 | } 685 | 686 | if isScript { 687 | ethScript, err := DeserializeEthScript(redeemScript) 688 | if err != nil { 689 | return nil, err 690 | } 691 | _, scrHash, err := GenScriptHash(ethScript) 692 | if err != nil { 693 | log.Error(err.Error()) 694 | } 695 | addrScrHash := common.HexToAddress(scrHash) 696 | actualRecipient = EthAddress{address: &addrScrHash} 697 | hash, _, err = wallet.callAddTransaction(ethScript, &amount, feeLevel) 698 | if err != nil { 699 | log.Errorf("error call add txn: %v", err) 700 | return nil, wi.ErrInsufficientFunds 701 | } 702 | } else { 703 | if !wallet.balanceCheck(feeLevel, amount) { 704 | return nil, wi.ErrInsufficientFunds 705 | } 706 | hash, err = wallet.Transfer(util.EnsureCorrectPrefix(addr.String()), &amount, spendAll, wallet.GetFeePerByte(feeLevel)) 707 | } 708 | if err != nil { 709 | return nil, err 710 | } 711 | 712 | // txn is pending 713 | nonce, err := wallet.client.GetTxnNonce(util.EnsureCorrectPrefix(hash.Hex())) 714 | if err == nil { 715 | data, err := SerializePendingTxn(PendingTxn{ 716 | TxnID: hash, 717 | Amount: amount.String(), 718 | OrderID: referenceID, 719 | Nonce: nonce, 720 | From: wallet.address.EncodeAddress(), 721 | To: actualRecipient.EncodeAddress(), 722 | WithInput: false, 723 | }) 724 | if err == nil { 725 | err0 := wallet.db.Txns().Put(data, ut.NormalizeAddress(hash.Hex()), "0", 0, time.Now(), true) 726 | if err0 != nil { 727 | log.Error(err0.Error()) 728 | } 729 | } 730 | } 731 | } 732 | 733 | if err == nil { 734 | h, err = util.CreateChainHash(hash.Hex()) 735 | if err == nil { 736 | wallet.invokeTxnCB(h.String(), &amount) 737 | } 738 | } 739 | return h, err 740 | } 741 | 742 | func (wallet *EthereumWallet) createTxnCallback(txID, orderID string, toAddress btcutil.Address, value big.Int, bTime time.Time, withInput bool) wi.TransactionCallback { 743 | output := wi.TransactionOutput{ 744 | Address: toAddress, 745 | Value: value, 746 | Index: 1, 747 | OrderID: orderID, 748 | } 749 | 750 | input := wi.TransactionInput{} 751 | 752 | if withInput { 753 | input = wi.TransactionInput{ 754 | OutpointHash: []byte(util.EnsureCorrectPrefix(txID)), 755 | OutpointIndex: 1, 756 | LinkedAddress: toAddress, 757 | Value: value, 758 | OrderID: orderID, 759 | } 760 | 761 | } 762 | 763 | return wi.TransactionCallback{ 764 | Txid: util.EnsureCorrectPrefix(txID), 765 | Outputs: []wi.TransactionOutput{output}, 766 | Inputs: []wi.TransactionInput{input}, 767 | Height: 1, 768 | Timestamp: time.Now(), 769 | Value: value, 770 | WatchOnly: false, 771 | BlockTime: bTime, 772 | } 773 | } 774 | 775 | func (wallet *EthereumWallet) AssociateTransactionWithOrder(txnCB wi.TransactionCallback) { 776 | for _, l := range wallet.listeners { 777 | go l(txnCB) 778 | } 779 | } 780 | 781 | // checkTxnRcpt check the txn rcpt status 782 | func (wallet *EthereumWallet) checkTxnRcpt(hash *common.Hash, data []byte) (*common.Hash, error) { 783 | var rcpt *types.Receipt 784 | pTxn, err := DeserializePendingTxn(data) 785 | if err != nil { 786 | return nil, err 787 | } 788 | 789 | rcpt, err = wallet.client.TransactionReceipt(context.Background(), *hash) 790 | if err != nil { 791 | log.Infof("fetching txn rcpt: %v", err) 792 | } 793 | 794 | if rcpt != nil { 795 | // good. so the txn has been processed but we have to account for failed 796 | // but valid txn like some contract condition causing revert 797 | if rcpt.Status > 0 { 798 | // all good to update order state 799 | chash, err := util.CreateChainHash((*hash).Hex()) 800 | if err != nil { 801 | return nil, err 802 | } 803 | err = wallet.db.Txns().Delete(chash) 804 | if err != nil { 805 | log.Errorf("err deleting the pending txn : %v", err) 806 | } 807 | n := new(big.Int) 808 | n, _ = n.SetString(pTxn.Amount, 10) 809 | toAddr := common.HexToAddress(pTxn.To) 810 | withInput := true 811 | if pTxn.Amount != "0" { 812 | toAddr = common.HexToAddress(util.EnsureCorrectPrefix(pTxn.To)) 813 | withInput = pTxn.WithInput 814 | } 815 | go wallet.AssociateTransactionWithOrder( 816 | wallet.createTxnCallback(util.EnsureCorrectPrefix(hash.Hex()), pTxn.OrderID, EthAddress{&toAddr}, 817 | *n, time.Now(), withInput)) 818 | } 819 | } 820 | return hash, nil 821 | 822 | } 823 | 824 | // BumpFee - Bump the fee for the given transaction 825 | func (wallet *EthereumWallet) BumpFee(txid chainhash.Hash) (*chainhash.Hash, error) { 826 | return util.CreateChainHash(txid.String()) 827 | } 828 | 829 | // EstimateFee - Calculates the estimated size of the transaction and returns the total fee for the given feePerByte 830 | func (wallet *EthereumWallet) EstimateFee(ins []wi.TransactionInput, outs []wi.TransactionOutput, feePerByte big.Int) big.Int { 831 | sum := big.NewInt(0) 832 | for _, out := range outs { 833 | gas, err := wallet.client.EstimateTxnGas(wallet.account.Address(), 834 | common.HexToAddress(out.Address.String()), &out.Value) 835 | if err != nil { 836 | return *sum 837 | } 838 | sum.Add(sum, gas) 839 | } 840 | return *sum 841 | } 842 | 843 | func (wallet *EthereumWallet) balanceCheck(feeLevel wi.FeeLevel, amount big.Int) bool { 844 | fee := wallet.GetFeePerByte(feeLevel) 845 | if fee.Int64() == 0 { 846 | return false 847 | } 848 | // lets check if the caller has enough balance to make the 849 | // multisign call 850 | requiredBalance := new(big.Int).Mul(&fee, big.NewInt(maxGasLimit)) 851 | requiredBalance = new(big.Int).Add(requiredBalance, &amount) 852 | currentBalance, err := wallet.GetBalance() 853 | if err != nil { 854 | log.Error("err fetching eth wallet balance") 855 | currentBalance = big.NewInt(0) 856 | } 857 | if requiredBalance.Cmp(currentBalance) > 0 { 858 | // the wallet does not have the required balance 859 | return false 860 | } 861 | return true 862 | } 863 | 864 | // EstimateSpendFee - Build a spend transaction for the amount and return the transaction fee 865 | func (wallet *EthereumWallet) EstimateSpendFee(amount big.Int, feeLevel wi.FeeLevel) (big.Int, error) { 866 | if !wallet.balanceCheck(feeLevel, amount) { 867 | return *big.NewInt(0), wi.ErrInsufficientFunds 868 | } 869 | gas, err := wallet.client.EstimateGasSpend(wallet.account.Address(), &amount) 870 | return *gas, err 871 | } 872 | 873 | // SweepAddress - Build and broadcast a transaction that sweeps all coins from an address. If it is a p2sh multisig, the redeemScript must be included 874 | func (wallet *EthereumWallet) SweepAddress(utxos []wi.TransactionInput, address *btcutil.Address, key *hd.ExtendedKey, redeemScript *[]byte, feeLevel wi.FeeLevel) (*chainhash.Hash, error) { 875 | 876 | outs := []wi.TransactionOutput{} 877 | for i, in := range utxos { 878 | out := wi.TransactionOutput{ 879 | Address: wallet.address, 880 | Value: in.Value, 881 | Index: uint32(i), 882 | OrderID: in.OrderID, 883 | } 884 | outs = append(outs, out) 885 | } 886 | 887 | sigs, err := wallet.CreateMultisigSignature([]wi.TransactionInput{}, outs, key, *redeemScript, *big.NewInt(1)) 888 | if err != nil { 889 | return nil, err 890 | } 891 | 892 | data, err := wallet.Multisign([]wi.TransactionInput{}, outs, sigs, []wi.Signature{}, *redeemScript, *big.NewInt(1), false) 893 | if err != nil { 894 | return nil, err 895 | } 896 | hash := common.BytesToHash(data) 897 | 898 | return util.CreateChainHash(hash.Hex()) 899 | } 900 | 901 | // ExchangeRates - return the exchangerates 902 | func (wallet *EthereumWallet) ExchangeRates() wi.ExchangeRates { 903 | return wallet.exchangeRates 904 | } 905 | 906 | func (wallet *EthereumWallet) callAddTransaction(script EthRedeemScript, value *big.Int, feeLevel wi.FeeLevel) (common.Hash, uint64, error) { 907 | 908 | h := common.BigToHash(big.NewInt(0)) 909 | 910 | // call registry to get the deployed address for the escrow ct 911 | fromAddress := wallet.account.Address() 912 | nonce, err := wallet.client.PendingNonceAt(context.Background(), fromAddress) 913 | if err != nil { 914 | log.Fatal(err) 915 | } 916 | gasPrice, err := wallet.client.SuggestGasPrice(context.Background()) 917 | if err != nil { 918 | log.Fatal(err) 919 | } 920 | gasPriceETHGAS := wallet.GetFeePerByte(feeLevel) 921 | if gasPriceETHGAS.Int64() < gasPrice.Int64() { 922 | gasPriceETHGAS = *gasPrice 923 | } 924 | auth := bind.NewKeyedTransactor(wallet.account.privateKey) 925 | 926 | auth.Nonce = big.NewInt(int64(nonce)) 927 | auth.Value = value // in wei 928 | auth.GasLimit = maxGasLimit // in units 929 | auth.GasPrice = gasPrice 930 | 931 | // lets check if the caller has enough balance to make the 932 | // multisign call 933 | if !wallet.balanceCheck(feeLevel, *big.NewInt(0)) { 934 | // the wallet does not have the required balance 935 | return h, nonce, wi.ErrInsufficientFunds 936 | } 937 | 938 | shash, _, err := GenScriptHash(script) 939 | if err != nil { 940 | return h, nonce, err 941 | } 942 | 943 | smtct, err := NewEscrow(script.MultisigAddress, wallet.client) 944 | if err != nil { 945 | log.Fatalf("error initilaizing contract failed: %s", err.Error()) 946 | } 947 | 948 | var tx *types.Transaction 949 | tx, err = smtct.AddTransaction(auth, script.Buyer, script.Seller, 950 | script.Moderator, script.Threshold, script.Timeout, shash, script.TxnID) 951 | if err == nil { 952 | h = tx.Hash() 953 | } else { 954 | return h, 0, err 955 | } 956 | 957 | txns = append(txns, wi.Txn{ 958 | Txid: tx.Hash().Hex(), 959 | Value: value.String(), 960 | Height: int32(nonce), 961 | Timestamp: time.Now(), 962 | WatchOnly: false, 963 | Bytes: tx.Data()}) 964 | 965 | return h, nonce, err 966 | 967 | } 968 | 969 | // GenerateMultisigScript - Generate a multisig script from public keys. If a timeout is included the returned script should be a timelocked escrow which releases using the timeoutKey. 970 | func (wallet *EthereumWallet) GenerateMultisigScript(keys []hd.ExtendedKey, threshold int, timeout time.Duration, timeoutKey *hd.ExtendedKey) (btcutil.Address, []byte, error) { 971 | if uint32(timeout.Hours()) > 0 && timeoutKey == nil { 972 | return nil, nil, errors.New("timeout key must be non nil when using an escrow timeout") 973 | } 974 | 975 | if len(keys) < threshold { 976 | return nil, nil, fmt.Errorf("unable to generate multisig script with "+ 977 | "%d required signatures when there are only %d public "+ 978 | "keys available", threshold, len(keys)) 979 | } 980 | 981 | if len(keys) < 2 { 982 | return nil, nil, fmt.Errorf("unable to generate multisig script with "+ 983 | "%d required signatures when there are only %d public "+ 984 | "keys available", threshold, len(keys)) 985 | } 986 | 987 | var ecKeys []common.Address 988 | for _, key := range keys { 989 | ecKey, err := key.ECPubKey() 990 | if err != nil { 991 | return nil, nil, err 992 | } 993 | ePubkey := ecKey.ToECDSA() 994 | ecKeys = append(ecKeys, crypto.PubkeyToAddress(*ePubkey)) 995 | } 996 | 997 | ver, err := wallet.registry.GetRecommendedVersion(nil, "escrow") 998 | if err != nil { 999 | log.Fatal(err) 1000 | } 1001 | 1002 | if util.IsZeroAddress(ver.Implementation) { 1003 | return nil, nil, errors.New("no escrow contract available") 1004 | } 1005 | 1006 | builder := EthRedeemScript{} 1007 | 1008 | builder.TxnID = common.BytesToAddress(util.ExtractChaincode(&keys[0])) 1009 | builder.Timeout = uint32(timeout.Hours()) 1010 | builder.Threshold = uint8(threshold) 1011 | builder.Buyer = ecKeys[0] 1012 | builder.Seller = ecKeys[1] 1013 | builder.MultisigAddress = ver.Implementation 1014 | 1015 | if threshold > 1 { 1016 | builder.Moderator = ecKeys[2] 1017 | } 1018 | switch threshold { 1019 | case 1: 1020 | { 1021 | // Seller is offline 1022 | } 1023 | case 2: 1024 | { 1025 | // Moderated payment 1026 | } 1027 | default: 1028 | { 1029 | // handle this 1030 | } 1031 | } 1032 | 1033 | redeemScript, err := SerializeEthScript(builder) 1034 | if err != nil { 1035 | return nil, nil, err 1036 | } 1037 | 1038 | addr := common.HexToAddress(hexutil.Encode(crypto.Keccak256(redeemScript))) //hash.Sum(nil)[:])) 1039 | retAddr := EthAddress{&addr} 1040 | 1041 | scriptKey := append(addr.Bytes(), redeemScript...) 1042 | err = wallet.db.WatchedScripts().Put(scriptKey) 1043 | if err != nil { 1044 | log.Errorf("err saving the redeemscript: %v", err) 1045 | } 1046 | 1047 | return retAddr, redeemScript, nil 1048 | } 1049 | 1050 | // CreateMultisigSignature - Create a signature for a multisig transaction 1051 | func (wallet *EthereumWallet) CreateMultisigSignature(ins []wi.TransactionInput, outs []wi.TransactionOutput, key *hd.ExtendedKey, redeemScript []byte, feePerByte big.Int) ([]wi.Signature, error) { 1052 | 1053 | payouts := []wi.TransactionOutput{} 1054 | difference := new(big.Int) 1055 | 1056 | if len(ins) > 0 { 1057 | totalVal := ins[0].Value 1058 | outVal := new(big.Int) 1059 | for _, out := range outs { 1060 | outVal = new(big.Int).Add(outVal, &out.Value) 1061 | } 1062 | if totalVal.Cmp(outVal) != 0 { 1063 | if totalVal.Cmp(outVal) < 0 { 1064 | return nil, errors.New("payout greater than initial amount") 1065 | } 1066 | difference = new(big.Int).Sub(&totalVal, outVal) 1067 | } 1068 | } 1069 | 1070 | rScript, err := DeserializeEthScript(redeemScript) 1071 | if err != nil { 1072 | return nil, err 1073 | } 1074 | 1075 | indx := []int{} 1076 | mbvAddresses := make([]string, 3) 1077 | 1078 | for i, out := range outs { 1079 | if out.Value.Cmp(new(big.Int)) > 0 { 1080 | indx = append(indx, i) 1081 | } 1082 | if out.Address.String() == rScript.Moderator.Hex() { 1083 | mbvAddresses[0] = out.Address.String() 1084 | } else if out.Address.String() == rScript.Buyer.Hex() && (out.Value.Cmp(new(big.Int)) > 0) { 1085 | mbvAddresses[1] = out.Address.String() 1086 | } else { 1087 | mbvAddresses[2] = out.Address.String() 1088 | } 1089 | p := wi.TransactionOutput{ 1090 | Address: out.Address, 1091 | Value: out.Value, 1092 | Index: out.Index, 1093 | OrderID: out.OrderID, 1094 | } 1095 | payouts = append(payouts, p) 1096 | } 1097 | 1098 | if len(indx) > 0 { 1099 | diff := new(big.Int) 1100 | delta := new(big.Int) 1101 | diff.DivMod(difference, big.NewInt(int64(len(indx))), delta) 1102 | for _, i := range indx { 1103 | payouts[i].Value.Add(&payouts[i].Value, diff) 1104 | } 1105 | payouts[indx[0]].Value.Add(&payouts[indx[0]].Value, delta) 1106 | } 1107 | 1108 | sort.Slice(payouts, func(i, j int) bool { 1109 | return strings.Compare(payouts[i].Address.String(), payouts[j].Address.String()) == -1 1110 | }) 1111 | 1112 | var sigs []wi.Signature 1113 | 1114 | payables := make(map[string]big.Int) 1115 | addresses := []string{} 1116 | for _, out := range payouts { 1117 | if out.Value.Cmp(big.NewInt(0)) <= 0 { 1118 | continue 1119 | } 1120 | val := new(big.Int).SetBytes(out.Value.Bytes()) // &out.Value 1121 | if p, ok := payables[out.Address.String()]; ok { 1122 | sum := new(big.Int).Add(val, &p) 1123 | payables[out.Address.String()] = *sum 1124 | } else { 1125 | payables[out.Address.String()] = *val 1126 | addresses = append(addresses, out.Address.String()) 1127 | } 1128 | } 1129 | 1130 | sort.Strings(addresses) 1131 | destArr := []byte{} 1132 | amountArr := []byte{} 1133 | 1134 | for _, k := range mbvAddresses { 1135 | v := payables[k] 1136 | if v.Cmp(big.NewInt(0)) != 1 { 1137 | continue 1138 | } 1139 | addr := common.HexToAddress(k) 1140 | sample := [32]byte{} 1141 | sampleDest := [32]byte{} 1142 | copy(sampleDest[12:], addr.Bytes()) 1143 | val := v.Bytes() 1144 | l := len(val) 1145 | 1146 | copy(sample[32-l:], val) 1147 | destArr = append(destArr, sampleDest[:]...) 1148 | amountArr = append(amountArr, sample[:]...) 1149 | } 1150 | 1151 | shash, _, err := GenScriptHash(rScript) 1152 | if err != nil { 1153 | return nil, err 1154 | } 1155 | 1156 | var txHash [32]byte 1157 | var payloadHash [32]byte 1158 | 1159 | /* 1160 | // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191 1161 | bytes32 txHash = keccak256( 1162 | abi.encodePacked( 1163 | "\x19Ethereum Signed Message:\n32", 1164 | keccak256( 1165 | abi.encodePacked( 1166 | byte(0x19), 1167 | byte(0), 1168 | this, 1169 | destinations, 1170 | amounts, 1171 | scriptHash 1172 | ) 1173 | ) 1174 | ) 1175 | ); 1176 | 1177 | */ 1178 | 1179 | payload := []byte{byte(0x19), byte(0)} 1180 | payload = append(payload, rScript.MultisigAddress.Bytes()...) 1181 | payload = append(payload, destArr...) 1182 | payload = append(payload, amountArr...) 1183 | payload = append(payload, shash[:]...) 1184 | 1185 | pHash := crypto.Keccak256(payload) 1186 | copy(payloadHash[:], pHash) 1187 | 1188 | txData := []byte{byte(0x19)} 1189 | txData = append(txData, []byte("Ethereum Signed Message:\n32")...) 1190 | txData = append(txData, payloadHash[:]...) 1191 | txnHash := crypto.Keccak256(txData) 1192 | log.Debugf("txnHash : %s", hexutil.Encode(txnHash)) 1193 | log.Debugf("phash : %s", hexutil.Encode(payloadHash[:])) 1194 | copy(txHash[:], txnHash) 1195 | 1196 | sig, err := crypto.Sign(txHash[:], wallet.account.privateKey) 1197 | if err != nil { 1198 | log.Errorf("error signing in createmultisig : %v", err) 1199 | } 1200 | sigs = append(sigs, wi.Signature{InputIndex: 1, Signature: sig}) 1201 | 1202 | return sigs, err 1203 | } 1204 | 1205 | // Multisign - Combine signatures and optionally broadcast 1206 | func (wallet *EthereumWallet) Multisign(ins []wi.TransactionInput, outs []wi.TransactionOutput, sigs1 []wi.Signature, sigs2 []wi.Signature, redeemScript []byte, feePerByte big.Int, broadcast bool) ([]byte, error) { 1207 | 1208 | payouts := []wi.TransactionOutput{} 1209 | difference := new(big.Int) 1210 | 1211 | if len(ins) > 0 { 1212 | totalVal := &ins[0].Value 1213 | outVal := new(big.Int) 1214 | for _, out := range outs { 1215 | outVal.Add(outVal, &out.Value) 1216 | } 1217 | if totalVal.Cmp(outVal) != 0 { 1218 | if totalVal.Cmp(outVal) < 0 { 1219 | return nil, errors.New("payout greater than initial amount") 1220 | } 1221 | difference.Sub(totalVal, outVal) 1222 | } 1223 | } 1224 | 1225 | rScript, err := DeserializeEthScript(redeemScript) 1226 | if err != nil { 1227 | return nil, err 1228 | } 1229 | 1230 | indx := []int{} 1231 | referenceID := "" 1232 | mbvAddresses := make([]string, 3) 1233 | 1234 | for i, out := range outs { 1235 | if out.Value.Cmp(new(big.Int)) > 0 { 1236 | indx = append(indx, i) 1237 | } 1238 | if out.Address.String() == rScript.Moderator.Hex() { 1239 | indx = append(indx, i) 1240 | mbvAddresses[0] = out.Address.String() 1241 | } else if out.Address.String() == rScript.Buyer.Hex() { 1242 | mbvAddresses[1] = out.Address.String() 1243 | } else { 1244 | mbvAddresses[2] = out.Address.String() 1245 | } 1246 | p := wi.TransactionOutput{ 1247 | Address: out.Address, 1248 | Value: out.Value, 1249 | Index: out.Index, 1250 | OrderID: out.OrderID, 1251 | } 1252 | referenceID = out.OrderID 1253 | payouts = append(payouts, p) 1254 | } 1255 | 1256 | if len(indx) > 0 { 1257 | diff := new(big.Int) 1258 | delta := new(big.Int) 1259 | diff.DivMod(difference, big.NewInt(int64(len(indx))), delta) 1260 | for _, i := range indx { 1261 | payouts[i].Value.Add(&payouts[i].Value, diff) 1262 | } 1263 | payouts[indx[0]].Value.Add(&payouts[indx[0]].Value, delta) 1264 | } 1265 | 1266 | sort.Slice(payouts, func(i, j int) bool { 1267 | return strings.Compare(payouts[i].Address.String(), payouts[j].Address.String()) == -1 1268 | }) 1269 | 1270 | payables := make(map[string]big.Int) 1271 | for _, out := range payouts { 1272 | if out.Value.Cmp(big.NewInt(0)) <= 0 { 1273 | continue 1274 | } 1275 | val := new(big.Int).SetBytes(out.Value.Bytes()) // &out.Value 1276 | if p, ok := payables[out.Address.String()]; ok { 1277 | sum := new(big.Int).Add(val, &p) 1278 | payables[out.Address.String()] = *sum 1279 | } else { 1280 | payables[out.Address.String()] = *val 1281 | } 1282 | } 1283 | 1284 | rSlice := [][32]byte{} 1285 | sSlice := [][32]byte{} 1286 | vSlice := []uint8{} 1287 | 1288 | var r [32]byte 1289 | var s [32]byte 1290 | var v uint8 1291 | 1292 | if len(sigs1) > 0 && len(sigs1[0].Signature) > 0 { 1293 | r, s, v = util.SigRSV(sigs1[0].Signature) 1294 | rSlice = append(rSlice, r) 1295 | sSlice = append(sSlice, s) 1296 | vSlice = append(vSlice, v) 1297 | } 1298 | 1299 | if len(sigs2) > 0 && len(sigs2[0].Signature) > 0 { 1300 | r, s, v = util.SigRSV(sigs2[0].Signature) 1301 | rSlice = append(rSlice, r) 1302 | sSlice = append(sSlice, s) 1303 | vSlice = append(vSlice, v) 1304 | } 1305 | 1306 | shash, _, err := GenScriptHash(rScript) 1307 | if err != nil { 1308 | return nil, err 1309 | } 1310 | 1311 | smtct, err := NewEscrow(rScript.MultisigAddress, wallet.client) 1312 | if err != nil { 1313 | log.Fatalf("error initializing contract failed: %s", err.Error()) 1314 | } 1315 | 1316 | destinations := []common.Address{} 1317 | amounts := []*big.Int{} 1318 | 1319 | for _, k := range mbvAddresses { 1320 | v := payables[k] 1321 | if v.Cmp(big.NewInt(0)) == 1 { 1322 | destinations = append(destinations, common.HexToAddress(k)) 1323 | amounts = append(amounts, new(big.Int).SetBytes(v.Bytes())) 1324 | } 1325 | } 1326 | 1327 | fromAddress := wallet.account.Address() 1328 | nonce, err := wallet.client.PendingNonceAt(context.Background(), fromAddress) 1329 | if err != nil { 1330 | log.Fatal(err) 1331 | } 1332 | gasPrice, err := wallet.client.SuggestGasPrice(context.Background()) 1333 | if err != nil { 1334 | log.Fatal(err) 1335 | } 1336 | auth := bind.NewKeyedTransactor(wallet.account.privateKey) 1337 | 1338 | auth.Nonce = big.NewInt(int64(nonce)) 1339 | auth.Value = big.NewInt(0) // in wei 1340 | auth.GasLimit = maxGasLimit // in units 1341 | auth.GasPrice = gasPrice 1342 | 1343 | // lets check if the caller has enough balance to make the 1344 | // multisign call 1345 | requiredBalance := new(big.Int).Mul(gasPrice, big.NewInt(maxGasLimit)) 1346 | currentBalance, err := wallet.GetBalance() 1347 | if err != nil { 1348 | log.Error("err fetching eth wallet balance") 1349 | currentBalance = big.NewInt(0) 1350 | } 1351 | 1352 | if requiredBalance.Cmp(currentBalance) > 0 { 1353 | // the wallet does not have the required balance 1354 | return nil, wi.ErrInsufficientFunds 1355 | } 1356 | 1357 | tx, txnErr := smtct.Execute(auth, vSlice, rSlice, sSlice, shash, destinations, amounts) 1358 | 1359 | if txnErr != nil { 1360 | return nil, txnErr 1361 | } 1362 | 1363 | txns = append(txns, wi.Txn{ 1364 | Txid: tx.Hash().Hex(), 1365 | Value: "0", 1366 | Height: int32(nonce), 1367 | Timestamp: time.Now(), 1368 | WatchOnly: false, 1369 | Bytes: tx.Data()}) 1370 | 1371 | // this is a pending txn 1372 | _, scrHash, err := GenScriptHash(rScript) 1373 | if err != nil { 1374 | log.Error(err.Error()) 1375 | } 1376 | data, err := SerializePendingTxn(PendingTxn{ 1377 | TxnID: tx.Hash(), 1378 | Amount: "0", 1379 | OrderID: referenceID, 1380 | Nonce: int32(nonce), 1381 | From: wallet.address.EncodeAddress(), 1382 | To: scrHash, 1383 | }) 1384 | if err == nil { 1385 | err0 := wallet.db.Txns().Put(data, ut.NormalizeAddress(tx.Hash().Hex()), "0", 0, time.Now(), true) 1386 | if err0 != nil { 1387 | log.Error(err0.Error()) 1388 | } 1389 | } 1390 | 1391 | return tx.Hash().Bytes(), nil 1392 | } 1393 | 1394 | // AddWatchedAddresses - Add a script to the wallet and get notifications back when coins are received or spent from it 1395 | func (wallet *EthereumWallet) AddWatchedAddresses(addrs ...btcutil.Address) error { 1396 | // the reason eth wallet cannot use this as of now is because only the address 1397 | // is insufficient, the redeemScript is also required 1398 | return nil 1399 | } 1400 | 1401 | // AddTransactionListener will call the function callback when new transactions are discovered 1402 | func (wallet *EthereumWallet) AddTransactionListener(callback func(wi.TransactionCallback)) { 1403 | // add incoming txn listener using service 1404 | wallet.listeners = append(wallet.listeners, callback) 1405 | } 1406 | 1407 | // ReSyncBlockchain - Use this to re-download merkle blocks in case of missed transactions 1408 | func (wallet *EthereumWallet) ReSyncBlockchain(fromTime time.Time) { 1409 | // use service here 1410 | } 1411 | 1412 | // GetConfirmations - Return the number of confirmations and the height for a transaction 1413 | func (wallet *EthereumWallet) GetConfirmations(txid chainhash.Hash) (confirms, atHeight uint32, err error) { 1414 | // TODO: etherscan api is being used 1415 | // when mainnet is activated we may need a way to set the 1416 | // url correctly - done 6 April 2019 1417 | hash := common.HexToHash(util.EnsureCorrectPrefix(txid.String())) 1418 | network := etherscan.Rinkby 1419 | if strings.Contains(wallet.client.url, "mainnet") { 1420 | network = etherscan.Mainnet 1421 | } 1422 | urlStr := fmt.Sprintf("https://%s.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=%s&apikey=%s", 1423 | network, hash.String(), EtherScanAPIKey) 1424 | res, err := http.Get(urlStr) 1425 | if err != nil { 1426 | return 0, 0, err 1427 | } 1428 | body, err := ioutil.ReadAll(res.Body) 1429 | if err != nil { 1430 | return 0, 0, err 1431 | } 1432 | if len(body) == 0 { 1433 | return 0, 0, errors.New("invalid txn hash") 1434 | } 1435 | var s map[string]interface{} 1436 | err = json.Unmarshal(body, &s) 1437 | if err != nil { 1438 | return 0, 0, err 1439 | } 1440 | 1441 | if s["result"] == nil { 1442 | return 0, 0, errors.New("invalid txn hash") 1443 | } 1444 | 1445 | if s["message"] != nil { 1446 | return 0, 0, nil 1447 | } 1448 | 1449 | result := s["result"].(map[string]interface{}) 1450 | 1451 | var d, conf int64 1452 | if result["blockNumber"] != nil { 1453 | d, _ = strconv.ParseInt(result["blockNumber"].(string), 0, 64) 1454 | } else { 1455 | d = 0 1456 | } 1457 | 1458 | n, err := wallet.client.HeaderByNumber(context.Background(), nil) 1459 | if err != nil { 1460 | return 0, 0, err 1461 | } 1462 | 1463 | if d != 0 { 1464 | conf = n.Number.Int64() - d + 1 1465 | } else { 1466 | conf = 0 1467 | } 1468 | 1469 | return uint32(conf), uint32(d), nil 1470 | } 1471 | 1472 | // Close will stop the wallet daemon 1473 | func (wallet *EthereumWallet) Close() { 1474 | // stop the wallet daemon 1475 | done <- true 1476 | doneBalanceTicker <- true 1477 | } 1478 | 1479 | // CreateAddress - used to generate a new address 1480 | func (wallet *EthereumWallet) CreateAddress() (common.Address, error) { 1481 | fromAddress := wallet.account.Address() 1482 | nonce, err := wallet.client.PendingNonceAt(context.Background(), fromAddress) 1483 | if err != nil { 1484 | log.Error(err.Error()) 1485 | } 1486 | addr := crypto.CreateAddress(fromAddress, nonce) 1487 | return addr, err 1488 | } 1489 | 1490 | // PrintKeys - used to print the keys for this wallet 1491 | func (wallet *EthereumWallet) PrintKeys() { 1492 | privateKeyBytes := crypto.FromECDSA(wallet.account.privateKey) 1493 | log.Debug(string(privateKeyBytes)) 1494 | publicKey := wallet.account.privateKey.Public() 1495 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 1496 | if !ok { 1497 | log.Fatal("error casting public key to ECDSA") 1498 | } 1499 | 1500 | publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) 1501 | address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() 1502 | log.Debug(address) 1503 | log.Debug(string(publicKeyBytes)) 1504 | } 1505 | 1506 | // GenWallet creates a wallet 1507 | func GenWallet() { 1508 | } 1509 | 1510 | // GenDefaultKeyStore will generate a default keystore 1511 | func GenDefaultKeyStore(passwd string) (*Account, error) { 1512 | ks := keystore.NewKeyStore("./", keystore.StandardScryptN, keystore.StandardScryptP) 1513 | account, err := ks.NewAccount(passwd) 1514 | if err != nil { 1515 | return nil, err 1516 | } 1517 | return NewAccountFromKeyfile(account.URL.Path, passwd) 1518 | } 1519 | -------------------------------------------------------------------------------- /wallet/wallet_test.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "encoding/binary" 8 | "fmt" 9 | "math/big" 10 | "net/url" 11 | "testing" 12 | "time" 13 | 14 | "github.com/OpenBazaar/multiwallet/config" 15 | wi "github.com/OpenBazaar/wallet-interface" 16 | "github.com/btcsuite/btcd/chaincfg/chainhash" 17 | hd "github.com/btcsuite/btcutil/hdkeychain" 18 | "github.com/davecgh/go-spew/spew" 19 | "github.com/ethereum/go-ethereum/common" 20 | "github.com/ethereum/go-ethereum/common/hexutil" 21 | "github.com/ethereum/go-ethereum/core/types" 22 | "github.com/ethereum/go-ethereum/crypto" 23 | log "github.com/sirupsen/logrus" 24 | 25 | "github.com/OpenBazaar/go-ethwallet/util" 26 | ) 27 | 28 | const ( 29 | magicOrderID = "iamanorderid" 30 | ) 31 | 32 | var validRopstenURL = fmt.Sprintf("https://ropsten.infura.io/%s", validInfuraKey) 33 | var validRinkebyURL = fmt.Sprintf("https://rinkeby.infura.io/%s", validInfuraKey) 34 | 35 | var validSampleWallet *EthereumWallet 36 | var destWallet *EthereumWallet 37 | 38 | var script EthRedeemScript 39 | 40 | var cfg config.CoinConfig 41 | 42 | func setupSourceWallet() { 43 | //validRopstenWallet = NewEthereumWalletWithKeyfile(validRopstenURL, validKeyFile, validPassword) 44 | setupCoinConfigRinkeby() 45 | validSampleWallet, _ = NewEthereumWallet(cfg, mnemonicStr, nil) 46 | } 47 | 48 | func setupDestWallet() { 49 | destWallet = NewEthereumWalletWithKeyfile(validRinkebyURL, 50 | "../test/UTC--2018-06-16T20-09-33.726552102Z--cecb952de5b23950b15bfd49302d1bdd25f9ee67", validPassword) 51 | } 52 | 53 | func setupEthRedeemScript(timeout time.Duration, threshold int) { 54 | 55 | chaincode := make([]byte, 32) 56 | _, err := rand.Read(chaincode) 57 | fmt.Println("chiancode : ", chaincode) 58 | if err != nil { 59 | fmt.Println(err) 60 | chaincode = []byte("423b5d4c32345ced77393b3530b1eed1") 61 | } 62 | //chaincode := []byte("423b5d4c32345ced77393b3530b1eed1") 63 | script.TxnID = common.BytesToAddress(chaincode) // .HexToAddress(string(chaincode)) // common.HexToAddress(xid.New().String() + xid.New().String()) 64 | script.Timeout = uint32(timeout.Hours()) 65 | script.Threshold = uint8(threshold) 66 | script.Buyer = common.HexToAddress(mnemonicStrAddress) 67 | script.Seller = common.HexToAddress(validDestinationAddress) 68 | script.Moderator = common.BigToAddress(big.NewInt(0)) 69 | script.MultisigAddress = common.HexToAddress("0x36e19e91DFFCA4251f4fB541f5c3a596252eA4BB") 70 | 71 | //fmt.Println("in setup script: ") 72 | //spew.Dump(script) 73 | } 74 | 75 | func setupCoinConfigRopsten() { 76 | clientURL, _ := url.Parse("https://ropsten.infura.io") 77 | cfg.ClientAPIs = []string{(*clientURL).String()} 78 | cfg.CoinType = wi.Ethereum 79 | cfg.Options = make(map[string]interface{}) 80 | //cfg.Options["RegistryAddress"] = "0x029d6a0cd4ce98315690f4ea52945545d9c0f460" 81 | cfg.Options["RegistryAddress"] = "0x403d907982474cdd51687b09a8968346159378f3" 82 | } 83 | 84 | func setupCoinConfigRinkeby() { 85 | clientURL, _ := url.Parse("https://rinkeby.infura.io") 86 | cfg.ClientAPIs = []string{(*clientURL).String()} 87 | cfg.CoinType = wi.Ethereum 88 | cfg.Options = make(map[string]interface{}) 89 | cfg.Options["RegistryAddress"] = "0x403d907982474cdd51687b09a8968346159378f3" //"0xab8dd0e05b73529b440d9c9df00b5f490c8596ff" 90 | } 91 | 92 | type MockWatchedScripts struct { 93 | Name string 94 | } 95 | 96 | func (m MockWatchedScripts) Put(script []byte) error { 97 | return nil 98 | } 99 | 100 | func (m MockWatchedScripts) GetAll() ([][]byte, error) { 101 | return [][]byte{}, nil 102 | } 103 | 104 | func (m MockWatchedScripts) Delete([]byte) error { 105 | return nil 106 | } 107 | 108 | type MockDatastore struct { 109 | keys wi.Keys 110 | utxos wi.Utxos 111 | stxos wi.Stxos 112 | txns wi.Txns 113 | watchedScripts wi.WatchedScripts 114 | } 115 | 116 | func (m MockDatastore) Keys() wi.Keys { 117 | return m.keys 118 | } 119 | 120 | func (m MockDatastore) Utxos() wi.Utxos { 121 | return m.utxos 122 | } 123 | 124 | func (m MockDatastore) Stxos() wi.Stxos { 125 | return m.stxos 126 | } 127 | 128 | func (m MockDatastore) Txns() wi.Txns { 129 | return m.txns 130 | } 131 | 132 | func (m MockDatastore) WatchedScripts() wi.WatchedScripts { 133 | return m.watchedScripts 134 | } 135 | 136 | func TestNewWalletWithValidKeyfileValues(t *testing.T) { 137 | wallet := NewEthereumWalletWithKeyfile(validRopstenURL, validKeyFile, validPassword) 138 | if wallet == nil { 139 | t.Errorf("valid credentials should return a wallet") 140 | } 141 | if wallet.address.String() != validSourceAddress { 142 | t.Errorf("valid credentials should return a wallet with proper initialization") 143 | } 144 | } 145 | 146 | func TestNewWalletWithInValidValues(t *testing.T) { 147 | t.SkipNow() 148 | wallet := NewEthereumWalletWithKeyfile(validRopstenURL, validKeyFile, invalidPassword) 149 | if wallet != nil { 150 | t.Errorf("invalid credentials should return a wallet") 151 | } 152 | } 153 | 154 | func TestNewWalletWithValidCoinConfigValues(t *testing.T) { 155 | setupCoinConfigRinkeby() 156 | wallet, err := NewEthereumWallet(cfg, mnemonicStr, nil) 157 | if err != nil || wallet == nil { 158 | t.Errorf("valid credentials should return a wallet") 159 | } 160 | fmt.Println(wallet.address.String()) 161 | fmt.Println(validSourceAddress) 162 | if wallet.address.String() != mnemonicStrAddress { 163 | t.Errorf("valid credentials should return a wallet with proper initialization") 164 | } 165 | } 166 | 167 | func TestWalletChainTip(t *testing.T) { 168 | setupSourceWallet() 169 | 170 | emptyHash, _ := chainhash.NewHashFromStr("") 171 | 172 | tip, hash := validSampleWallet.ChainTip() 173 | 174 | if hash.String() == emptyHash.String() { 175 | t.Errorf("valid wallet should return chaintip") 176 | } 177 | fmt.Println("Chaintip is : ", tip) 178 | } 179 | 180 | func TestWalletGetBalance(t *testing.T) { 181 | setupSourceWallet() 182 | 183 | if _, err := validSampleWallet.GetBalance(); err != nil { 184 | t.Errorf("valid wallet should return balance") 185 | } 186 | } 187 | 188 | func TestWalletGetUnconfirmedBalance(t *testing.T) { 189 | setupSourceWallet() 190 | 191 | if _, err := validSampleWallet.GetUnconfirmedBalance(); err != nil { 192 | t.Errorf("valid wallet should return unconfirmed balance") 193 | } 194 | } 195 | 196 | //$ GOCACHE=off go test -v ./... -run TestWalletGetTransaction -count=1 197 | func TestWalletGetTransaction(t *testing.T) { 198 | setupSourceWallet() 199 | 200 | txID := "8a0f98762bd7be13a7a17ce45540110f2ca7cf7bda7397daff1532028a9bbe4d" 201 | cHash, err := chainhash.NewHashFromStr(txID) 202 | 203 | if err != nil { 204 | t.Errorf("chainhash should be created froma 32 byte string") 205 | } 206 | 207 | txn, err := validSampleWallet.GetTransaction(*cHash) 208 | if err != nil { 209 | t.Errorf("wallet should fetch a txn from valid chainhash") 210 | } 211 | 212 | spew.Dump(txn) 213 | 214 | } 215 | 216 | func TestWalletTransfer(t *testing.T) { 217 | //t.SkipNow() 218 | setupSourceWallet() 219 | setupDestWallet() 220 | 221 | value := big.NewInt(99999000000) 222 | 223 | sbal1 := big.NewInt(0) 224 | dbal1 := big.NewInt(0) 225 | 226 | cbal1, _ := validSampleWallet.GetBalance() 227 | ucbal1, _ := validSampleWallet.GetUnconfirmedBalance() 228 | 229 | cbal2, _ := destWallet.GetBalance() 230 | ucbal2, _ := destWallet.GetUnconfirmedBalance() 231 | 232 | sbal1.Add(cbal1, ucbal1) 233 | dbal1.Add(cbal2, ucbal2) 234 | 235 | h, err := validSampleWallet.Transfer(validDestinationAddress, value) 236 | 237 | if err != nil { 238 | fmt.Println("err in transfer : ", err) 239 | return 240 | } 241 | 242 | flag := false 243 | var rcpt *types.Receipt 244 | for !flag { 245 | rcpt, err = validSampleWallet.client.TransactionReceipt(context.Background(), h) 246 | if rcpt != nil { 247 | flag = true 248 | } 249 | } 250 | 251 | if err != nil { 252 | t.Errorf("valid wallet should allow transfer : %v", err) 253 | } 254 | 255 | fmt.Println("rcpt") 256 | spew.Dump(rcpt) 257 | 258 | //_, err = chainhash.NewHashFromStr(hash.Hex()[2:]) 259 | 260 | //if err != nil { 261 | // t.Errorf("wallet should return a valid transaction") 262 | //} 263 | 264 | //txn, err := validRopstenWallet.GetTransaction(*chash) 265 | 266 | //if err != nil { 267 | // t.Errorf("wallet should return a valid transaction") 268 | //} 269 | 270 | //if txn.Value != value.Int64() { 271 | // t.Errorf("wallet is not forming the correct txn") 272 | //} 273 | 274 | sbal2 := big.NewInt(0) 275 | dbal2 := big.NewInt(0) 276 | 277 | cbal1, _ = validSampleWallet.GetBalance() 278 | ucbal1, _ = validSampleWallet.GetUnconfirmedBalance() 279 | 280 | cbal2, _ = destWallet.GetBalance() 281 | ucbal2, _ = destWallet.GetUnconfirmedBalance() 282 | 283 | sbal2.Add(cbal1, ucbal1) 284 | dbal2.Add(cbal2, ucbal2) 285 | 286 | val := big.NewInt(0) 287 | 288 | val.Sub(dbal2, dbal1) 289 | 290 | if val.Cmp(value) != 0 && rcpt.Status == 0 { 291 | t.Errorf("client should have transferred balance") 292 | } 293 | 294 | } 295 | 296 | func TestWalletCurrencyCode(t *testing.T) { 297 | setupSourceWallet() 298 | 299 | if validSampleWallet.CurrencyCode() != "ETH" { 300 | t.Errorf("wallet should return proper currency code") 301 | } 302 | } 303 | 304 | func TestWalletIsDust(t *testing.T) { 305 | setupSourceWallet() 306 | 307 | if validSampleWallet.IsDust(int64(10000 + 10000)) { 308 | t.Errorf("wallet should not indicate wrong dust") 309 | } 310 | 311 | if !validSampleWallet.IsDust(int64(10000 - 100)) { 312 | t.Errorf("wallet should not indicate wrong dust") 313 | } 314 | } 315 | 316 | func TestWalletCurrentAddress(t *testing.T) { 317 | setupSourceWallet() 318 | 319 | addr := validSampleWallet.CurrentAddress(wi.EXTERNAL) 320 | 321 | if addr.String() != mnemonicStrAddress { 322 | t.Errorf("wallet should return correct current address") 323 | } 324 | } 325 | 326 | func TestWalletNewAddress(t *testing.T) { 327 | setupSourceWallet() 328 | 329 | addr := validSampleWallet.NewAddress(wi.EXTERNAL) 330 | 331 | if addr.String() != mnemonicStrAddress { 332 | t.Errorf("wallet should return correct new address") 333 | } 334 | } 335 | 336 | func TestWalletContractAddTransaction(t *testing.T) { 337 | setupSourceWallet() 338 | 339 | ver, err := validSampleWallet.registry.GetRecommendedVersion(nil, "escrow") 340 | if err != nil { 341 | t.Error("error fetching escrow from registry") 342 | } 343 | 344 | if util.IsZeroAddress(ver.Implementation) { 345 | log.Infof("escrow not available") 346 | return 347 | } 348 | 349 | d, _ := time.ParseDuration("1h") 350 | setupEthRedeemScript(d, 1) 351 | 352 | script.MultisigAddress = ver.Implementation 353 | 354 | redeemScript, err := SerializeEthScript(script) 355 | if err != nil { 356 | t.Error("error serializing redeem script") 357 | } 358 | 359 | fmt.Println(redeemScript) 360 | 361 | spew.Dump(script) 362 | 363 | orderValue := big.NewInt(34567812347878) 364 | 365 | hash, err := validSampleWallet.callAddTransaction(script, orderValue) 366 | 367 | fmt.Println("returned hash : ", hash) 368 | fmt.Println(err) 369 | 370 | chash, err := chainhash.NewHashFromStr(hash.Hex()[2:]) 371 | 372 | fmt.Println("err : ", err) 373 | 374 | if err == nil { 375 | txn, err := validSampleWallet.GetTransaction(*chash) 376 | 377 | spew.Dump(txn) 378 | fmt.Println(err) 379 | } 380 | 381 | output := wi.TransactionOutput{ 382 | Address: EthAddress{&script.Seller}, 383 | Value: orderValue.Int64(), 384 | Index: 1, 385 | } 386 | 387 | hkey := hd.NewExtendedKey([]byte{}, []byte{}, []byte{}, []byte{}, 0, 0, false) 388 | 389 | sig, err := validSampleWallet.CreateMultisigSignature([]wi.TransactionInput{}, []wi.TransactionOutput{output}, 390 | hkey, redeemScript, 2000) 391 | 392 | if err != nil { 393 | fmt.Println(err) 394 | } 395 | 396 | fmt.Println(sig) 397 | 398 | time.Sleep(5 * time.Minute) 399 | 400 | txBytes, err := validSampleWallet.Multisign([]wi.TransactionInput{}, 401 | []wi.TransactionOutput{output}, 402 | sig, []wi.Signature{wi.Signature{InputIndex: 1, Signature: []byte{}}}, redeemScript, 403 | 20000, true) 404 | //fmt.Println("after multisign") 405 | //fmt.Println(txBytes) 406 | fmt.Println("err : ", err) 407 | 408 | mtx := &types.Transaction{} 409 | 410 | mtx.UnmarshalJSON(txBytes) 411 | 412 | spew.Dump(mtx) 413 | 414 | sshh, sshhstr, _ := GenScriptHash(script) 415 | 416 | fmt.Println("script hash for ct : ", sshh) 417 | fmt.Println(sshhstr) 418 | 419 | } 420 | 421 | func TestWalletContractScriptHash(t *testing.T) { 422 | setupSourceWallet() 423 | 424 | ver, err := validSampleWallet.registry.GetRecommendedVersion(nil, "escrow") 425 | if err != nil { 426 | t.Error("error fetching escrow from registry") 427 | } 428 | 429 | if util.IsZeroAddress(ver.Implementation) { 430 | log.Infof("escrow not available") 431 | return 432 | } 433 | 434 | d, _ := time.ParseDuration("1h") 435 | setupEthRedeemScript(d, 1) 436 | 437 | chaincode := []byte("423b5d4c32345ced77393b3530b1eed1") 438 | 439 | script.TxnID = common.BytesToAddress(chaincode) 440 | 441 | script.MultisigAddress = ver.Implementation 442 | 443 | /* 444 | fmt.Println("buyer : ", script.Buyer) 445 | fmt.Println("seller : ", script.Seller) 446 | fmt.Println("moderator : ", script.Moderator) 447 | fmt.Println("threshold : ", script.Threshold) 448 | fmt.Println("timeout : ", script.Timeout) 449 | fmt.Println("scrptHash : ", shash) 450 | */ 451 | 452 | spew.Dump(script) 453 | 454 | smtct, err := NewEscrow(ver.Implementation, validSampleWallet.client) 455 | if err != nil { 456 | t.Errorf("error initilaizing contract failed: %s", err.Error()) 457 | } 458 | 459 | retHash, err := smtct.CalculateRedeemScriptHash(nil, script.TxnID, script.Threshold, script.Timeout, script.Buyer, 460 | script.Seller, script.Moderator, script.TokenAddress) 461 | 462 | fmt.Println(err) 463 | fmt.Println("from smtct : ", retHash) 464 | 465 | rethash1Str := hexutil.Encode(retHash[:]) 466 | fmt.Println("rethash1Str : ", rethash1Str) 467 | 468 | ahash := crypto.NewKeccak256() 469 | a := make([]byte, 4) 470 | binary.BigEndian.PutUint32(a, script.Timeout) 471 | arr := append(script.TxnID.Bytes(), append([]byte{script.Threshold}, 472 | append(a[:], append(script.Buyer.Bytes(), 473 | append(script.Seller.Bytes(), append(script.Moderator.Bytes(), 474 | append(script.MultisigAddress.Bytes())...)...)...)...)...)...) 475 | ahash.Write(arr) 476 | ahashStr := hexutil.Encode(ahash.Sum(nil)[:]) 477 | 478 | fmt.Println("computed : ", ahashStr) 479 | 480 | fmt.Println("priv key : ", validSampleWallet.account.privateKey) 481 | 482 | b := []byte{161, 162, 209, 139, 227, 101, 186, 196, 93, 247, 64, 186, 79, 166, 235, 225, 191, 123, 139, 89, 247, 48, 49, 71, 46, 130, 125, 221, 137, 35, 41, 51} 483 | 484 | fmt.Println(hexutil.Encode(b)) 485 | 486 | privateKeyBytes := crypto.FromECDSA(validSampleWallet.account.privateKey) 487 | fmt.Println(hexutil.Encode(privateKeyBytes)[2:]) 488 | 489 | fmt.Println("dest : ", script.MultisigAddress.String()[2:]) 490 | fmt.Println("dest : ", string(script.MultisigAddress.Bytes())) 491 | fmt.Println("dest : ", script.MultisigAddress.Hex()) 492 | fmt.Println("dest : ", []byte(script.MultisigAddress.String())[2:]) 493 | 494 | a1, b1, c1 := GenScriptHash(script) 495 | fmt.Println("scrpt hash : ", a1, " ", b1[2:], " ", c1) 496 | } 497 | 498 | func TestWalletContractTxnHash(t *testing.T) { 499 | t.Parallel() 500 | 501 | val := uint64(34567812347878) 502 | //destStr := fmt.Sprintf("%064s", validDestinationAddress[2:]) 503 | destAddress := common.HexToAddress(validDestinationAddress) 504 | 505 | orderValue := big.NewInt(34567812347878) 506 | sample := [32]byte{} 507 | sampleDest := [32]byte{} 508 | atq := make([]byte, 8) 509 | binary.BigEndian.PutUint64(atq, orderValue.Uint64()) 510 | copy(sample[24:], atq) 511 | 512 | fmt.Println("sample : ", sample) 513 | fmt.Println("val : ", orderValue.Bytes()) 514 | fmt.Println("val2 : ", atq) 515 | fmt.Println("dest : ", destAddress.Bytes()) 516 | fmt.Println("len dest : ", len(destAddress.Bytes())) 517 | copy(sampleDest[12:], destAddress.Bytes()) 518 | 519 | fmt.Println("sdest : ", sampleDest) 520 | 521 | var amountStr string 522 | amountStr = fmt.Sprintf("%064s", fmt.Sprintf("%x", orderValue.Int64())) 523 | 524 | //fmt.Println("dest str : ", destStr) 525 | fmt.Println("amnt str : ", amountStr) 526 | 527 | setupSourceWallet() 528 | 529 | d, _ := time.ParseDuration("1h") 530 | setupEthRedeemScript(d, 1) 531 | 532 | //a1, b1, c1 := GenScriptHash(script) 533 | //fmt.Println("scrpt hash : ", a1, " ", b1[2:], " ", c1) 534 | 535 | b1, err := hexutil.Decode("0x66cfea37109f1240d9d2f88643be076dc757883113a00a65ae8cd53d1e8411b4") 536 | 537 | b2 := byte(0x19) 538 | b3 := byte(0) 539 | 540 | //payloadStr := string(b2) + string(b3) + script.MultisigAddress.String()[2:] + destStr + amountStr + 541 | // b1[2:] 542 | 543 | //trialPayloadStr := "0x190036e19e91dffca4251f4fb541f5c3a596252ea4bb000000000000000000000000cecb952de5b23950b15bfd49302d1bdd25f9ee6700000000000000000000000000000000000000000000000000001f70722cf7e666cfea37109f1240d9d2f88643be076dc757883113a00a65ae8cd53d1e8411b4" 544 | 545 | //fmt.Println("payload str : ", payloadStr) 546 | //fmt.Println("trial payload str : ", trialPayloadStr) 547 | 548 | at := make([]byte, 8) 549 | binary.BigEndian.PutUint64(at, orderValue.Uint64()) 550 | 551 | at1 := make([]byte, 8) 552 | binary.LittleEndian.PutUint64(at1, val) 553 | 554 | at2 := make([]byte, 8) 555 | binary.BigEndian.PutUint64(at2, val) 556 | 557 | p1 := []byte{b2, b3} 558 | p1 = append(p1, script.MultisigAddress.Bytes()...) 559 | p1 = append(p1, sampleDest[:]...) 560 | p1 = append(p1, sample[:]...) 561 | //p1 = append(p1, destAddress.Bytes()...) 562 | //p1 = append(p1, orderValue.Bytes()...) 563 | //p1 = append(p1, at...) 564 | //p1 = append(p1, []byte(amountStr)...) 565 | //p1 = append(p1, at1...) 566 | //p1 = append(p1, at2...) 567 | p1 = append(p1, b1...) 568 | 569 | ahash := crypto.NewKeccak256() 570 | //a := make([]byte, 4) 571 | //binary.BigEndian.PutUint32(a, script.Timeout) 572 | //arr := append(script.TxnID.Bytes(), append([]byte{script.Threshold}, 573 | // append(a[:], append(script.Buyer.Bytes(), 574 | // append(script.Seller.Bytes(), append(script.Moderator.Bytes(), 575 | // append(script.MultisigAddress.Bytes())...)...)...)...)...)...) 576 | 577 | p11 := append([]byte{b2}, append([]byte{b3}, append(script.MultisigAddress.Bytes(), 578 | append(destAddress.Bytes(), append(orderValue.Bytes(), 579 | append(b1)...)...)...)...)...) 580 | 581 | ahash.Write(p11) 582 | p11hash := ahash.Sum(nil)[:] 583 | fmt.Println("www aaa : ", hexutil.Encode(p11hash)) 584 | 585 | pHash := crypto.Keccak256(p1) 586 | var payloadHash [32]byte 587 | copy(payloadHash[:], pHash) 588 | 589 | phash2, err := hexutil.Decode("0x7037f184ba846ff842222df065da84f5de500ad7ba0f996a4c7cdeff3520f4be") 590 | 591 | //phash2, err := hexutil.Decode("0x3a0312b6d025a3d21a257ad0a501a75026f9a3180c6d8c4fa2e92f7c12097310") 592 | 593 | if bytes.Equal(phash2, payloadHash[:]) { 594 | fmt.Println("yes .... sssssssss .......") 595 | } else { 596 | fmt.Println("still not there yet ....") 597 | fmt.Println("got payloadHash : ", hexutil.Encode(pHash)) 598 | fmt.Println("wanted : ", "0x7037f184ba846ff842222df065da84f5de500ad7ba0f996a4c7cdeff3520f4be") 599 | } 600 | 601 | //phash2 := []byte("7037f184ba846ff842222df065da84f5de500ad7ba0f996a4c7cdeff3520f4be") 602 | 603 | txData := []byte{byte(0x19)} 604 | txData = append(txData, []byte("Ethereum Signed Message:\n32")...) 605 | //txData = append(txData, byte(32)) 606 | txData = append(txData, phash2...) 607 | txnHash := crypto.Keccak256(txData) 608 | fmt.Println("txnHash : ", hexutil.Encode(txnHash)) 609 | var txHash [32]byte 610 | copy(txHash[:], txnHash) 611 | 612 | sig, err := crypto.Sign(txHash[:], validSampleWallet.account.privateKey) 613 | if err != nil { 614 | log.Errorf("error signing in createmultisig : %v", err) 615 | } 616 | 617 | spew.Dump(sig) 618 | 619 | r, s, v := util.SigRSV(sig) 620 | 621 | fmt.Println("r : ", hexutil.Encode(r[:])) 622 | fmt.Println("s : ", hexutil.Encode(s[:])) 623 | fmt.Println("v : ", v) 624 | 625 | } 626 | 627 | var listenerCallbackFlag bool 628 | 629 | func sampleListener(cb wi.TransactionCallback) { 630 | fmt.Println("in sample listener ....") 631 | spew.Dump(cb) 632 | if len(cb.Outputs) > 0 && cb.Outputs[0].OrderID == magicOrderID { 633 | listenerCallbackFlag = true 634 | } 635 | } 636 | 637 | func TestWalletSpend(t *testing.T) { 638 | //t.SkipNow() 639 | setupSourceWallet() 640 | setupDestWallet() 641 | 642 | value := big.NewInt(99999000000) 643 | 644 | validSampleWallet.AddTransactionListener(sampleListener) 645 | validSampleWallet.db = MockDatastore{watchedScripts: MockWatchedScripts{}} 646 | listenerCallbackFlag = false 647 | 648 | h, err := validSampleWallet.Spend(value.Int64(), destWallet.address, 1, magicOrderID) 649 | 650 | if err != nil { 651 | fmt.Println("err in transfer : ", err) 652 | return 653 | } 654 | 655 | spew.Dump(h) 656 | 657 | time.Sleep(1 * time.Minute) 658 | 659 | if !listenerCallbackFlag { 660 | t.Errorf("spend is not calling back correctly") 661 | } 662 | 663 | } 664 | 665 | // GOCACHE=off go test -v ./... -run TestWalletGetConfirmations -count=1 666 | func TestWalletGetConfirmations(t *testing.T) { 667 | setupSourceWallet() 668 | thash := "0x5315bbdb8b6370244ffb6fc41bf275e785355e567759fb15e85df6508eff9b35" 669 | chainHash, err := chainhash.NewHashFromStr(thash[2:]) 670 | if err != nil { 671 | t.Error("chainhash not initialized properly") 672 | } 673 | conf, ht, err := validSampleWallet.GetConfirmations(*chainHash) 674 | if err != nil { 675 | t.Error("chainhash not initialized properly") 676 | } 677 | fmt.Println("confs : ", conf, " height : ", ht) 678 | 679 | } 680 | 681 | func TestWalletDecodeAddress(t *testing.T) { 682 | setupSourceWallet() 683 | d, _ := time.ParseDuration("1h") 684 | setupEthRedeemScript(d, 1) 685 | 686 | script.MultisigAddress = ver.Implementation 687 | 688 | redeemScript, err := SerializeEthScript(script) 689 | if err != nil { 690 | t.Error("error serializing redeem script") 691 | } 692 | _, sHash, err := GenScriptHash(script) 693 | if err != nil { 694 | t.Error("error generating script hash for redeem script") 695 | } 696 | 697 | addr, err := validSampleWallet.DecodeAddress(validDestinationAddress) 698 | if err != nil || addr.String() != validDestinationAddress { 699 | t.Error("error decoding valid address") 700 | } 701 | 702 | notAddr, err := validSampleWallet.DecodeAddress(string(redeemScript)) 703 | if err != nil || notAddr.String() != sHash { 704 | t.Error("error decoding redeem script hash address") 705 | } 706 | 707 | } 708 | --------------------------------------------------------------------------------