diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index 38cae788..d627ebed 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -24,6 +24,7 @@ import ( "github.com/trezor/blockbook/bchain/coins/digibyte" "github.com/trezor/blockbook/bchain/coins/divi" "github.com/trezor/blockbook/bchain/coins/dogecoin" + "github.com/trezor/blockbook/bchain/coins/energi" "github.com/trezor/blockbook/bchain/coins/eth" "github.com/trezor/blockbook/bchain/coins/firo" "github.com/trezor/blockbook/bchain/coins/flo" @@ -69,6 +70,7 @@ func init() { BlockChainFactories["Ethereum Classic"] = eth.NewEthereumRPC BlockChainFactories["Ethereum Testnet Ropsten"] = eth.NewEthereumRPC BlockChainFactories["Ethereum Testnet Goerli"] = eth.NewEthereumRPC + BlockChainFactories["Energi"] = energi.NewEnergiRPC BlockChainFactories["Bcash"] = bch.NewBCashRPC BlockChainFactories["Bcash Testnet"] = bch.NewBCashRPC BlockChainFactories["Bgold"] = btg.NewBGoldRPC diff --git a/bchain/coins/energi/erc20.go b/bchain/coins/energi/erc20.go new file mode 100644 index 00000000..48582c99 --- /dev/null +++ b/bchain/coins/energi/erc20.go @@ -0,0 +1,227 @@ +package energi + +import ( + "bytes" + "context" + "encoding/hex" + "math/big" + "strings" + "sync" + "unicode/utf8" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" +) + +// doing the parsing/processing without using go-ethereum/accounts/abi library, it is simple to get data from Transfer event +const erc20TransferMethodSignature = "0xa9059cbb" +const erc20TransferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" +const erc20NameSignature = "0x06fdde03" +const erc20SymbolSignature = "0x95d89b41" +const erc20DecimalsSignature = "0x313ce567" +const erc20BalanceOf = "0x70a08231" + +var cachedContracts = make(map[string]*bchain.Erc20Contract) +var cachedContractsMux sync.Mutex + +func addressFromPaddedHex(s string) (string, error) { + var t big.Int + var ok bool + if has0xPrefix(s) { + _, ok = t.SetString(s[2:], 16) + } else { + _, ok = t.SetString(s, 16) + } + if !ok { + return "", errors.New("Data is not a number") + } + a := ethcommon.BigToAddress(&t) + return a.String(), nil +} + +func erc20GetTransfersFromLog(logs []*rpcLog) ([]bchain.Erc20Transfer, error) { + var r []bchain.Erc20Transfer + for _, l := range logs { + if len(l.Topics) == 3 && l.Topics[0] == erc20TransferEventSignature { + var t big.Int + _, ok := t.SetString(l.Data, 0) + if !ok { + return nil, errors.New("Data is not a number") + } + from, err := addressFromPaddedHex(l.Topics[1]) + if err != nil { + return nil, err + } + to, err := addressFromPaddedHex(l.Topics[2]) + if err != nil { + return nil, err + } + r = append(r, bchain.Erc20Transfer{ + Contract: EIP55AddressFromAddress(l.Address), + From: EIP55AddressFromAddress(from), + To: EIP55AddressFromAddress(to), + Tokens: t, + }) + } + } + return r, nil +} + +func erc20GetTransfersFromTx(tx *rpcTransaction) ([]bchain.Erc20Transfer, error) { + var r []bchain.Erc20Transfer + if len(tx.Payload) == 128+len(erc20TransferMethodSignature) && strings.HasPrefix(tx.Payload, erc20TransferMethodSignature) { + to, err := addressFromPaddedHex(tx.Payload[len(erc20TransferMethodSignature) : 64+len(erc20TransferMethodSignature)]) + if err != nil { + return nil, err + } + var t big.Int + _, ok := t.SetString(tx.Payload[len(erc20TransferMethodSignature)+64:], 16) + if !ok { + return nil, errors.New("Data is not a number") + } + r = append(r, bchain.Erc20Transfer{ + Contract: EIP55AddressFromAddress(tx.To), + From: EIP55AddressFromAddress(tx.From), + To: EIP55AddressFromAddress(to), + Tokens: t, + }) + } + return r, nil +} + +func (b *EnergiRPC) ethCall(data, to string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + var r string + err := b.rpc.CallContext(ctx, &r, "eth_call", map[string]interface{}{ + "data": data, + "to": to, + }, "latest") + if err != nil { + return "", err + } + return r, nil +} + +func parseErc20NumericProperty(contractDesc bchain.AddressDescriptor, data string) *big.Int { + if has0xPrefix(data) { + data = data[2:] + } + if len(data) == 64 { + var n big.Int + _, ok := n.SetString(data, 16) + if ok { + return &n + } + } + if glog.V(1) { + glog.Warning("Cannot parse '", data, "' for contract ", contractDesc) + } + return nil +} + +func parseErc20StringProperty(contractDesc bchain.AddressDescriptor, data string) string { + if has0xPrefix(data) { + data = data[2:] + } + if len(data) > 128 { + n := parseErc20NumericProperty(contractDesc, data[64:128]) + if n != nil { + l := n.Uint64() + if l > 0 && 2*int(l) <= len(data)-128 { + b, err := hex.DecodeString(data[128 : 128+2*l]) + if err == nil { + return string(b) + } + } + } + } + // allow string properties as UTF-8 data + b, err := hex.DecodeString(data) + if err == nil { + i := bytes.Index(b, []byte{0}) + if i > 32 { + i = 32 + } + if i > 0 { + b = b[:i] + } + if utf8.Valid(b) { + return string(b) + } + } + if glog.V(1) { + glog.Warning("Cannot parse '", data, "' for contract ", contractDesc) + } + return "" +} + +// EthereumTypeGetErc20ContractInfo returns information about ERC20 contract +func (b *EnergiRPC) EthereumTypeGetErc20ContractInfo(contractDesc bchain.AddressDescriptor) (*bchain.Erc20Contract, error) { + cds := string(contractDesc) + cachedContractsMux.Lock() + contract, found := cachedContracts[cds] + cachedContractsMux.Unlock() + if !found { + address := EIP55Address(contractDesc) + data, err := b.ethCall(erc20NameSignature, address) + if err != nil { + // ignore the error from the eth_call - since geth v1.9.15 they changed the behavior + // and returning error "execution reverted" for some non contract addresses + // https://github.com/ethereum/go-ethereum/issues/21249#issuecomment-648647672 + glog.Warning(errors.Annotatef(err, "erc20NameSignature %v", address)) + return nil, nil + // return nil, errors.Annotatef(err, "erc20NameSignature %v", address) + } + name := parseErc20StringProperty(contractDesc, data) + if name != "" { + data, err = b.ethCall(erc20SymbolSignature, address) + if err != nil { + glog.Warning(errors.Annotatef(err, "erc20SymbolSignature %v", address)) + return nil, nil + // return nil, errors.Annotatef(err, "erc20SymbolSignature %v", address) + } + symbol := parseErc20StringProperty(contractDesc, data) + data, err = b.ethCall(erc20DecimalsSignature, address) + if err != nil { + glog.Warning(errors.Annotatef(err, "erc20DecimalsSignature %v", address)) + // return nil, errors.Annotatef(err, "erc20DecimalsSignature %v", address) + } + contract = &bchain.Erc20Contract{ + Contract: address, + Name: name, + Symbol: symbol, + } + d := parseErc20NumericProperty(contractDesc, data) + if d != nil { + contract.Decimals = int(uint8(d.Uint64())) + } else { + contract.Decimals = EtherAmountDecimalPoint + } + } else { + contract = nil + } + cachedContractsMux.Lock() + cachedContracts[cds] = contract + cachedContractsMux.Unlock() + } + return contract, nil +} + +// EthereumTypeGetErc20ContractBalance returns balance of ERC20 contract for given address +func (b *EnergiRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) { + addr := EIP55Address(addrDesc) + contract := EIP55Address(contractDesc) + req := erc20BalanceOf + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr)-2:] + addr[2:] + data, err := b.ethCall(req, contract) + if err != nil { + return nil, err + } + r := parseErc20NumericProperty(contractDesc, data) + if r == nil { + return nil, errors.New("Invalid balance") + } + return r, nil +} diff --git a/bchain/coins/energi/erc20_test.go b/bchain/coins/energi/erc20_test.go new file mode 100644 index 00000000..ff82b92a --- /dev/null +++ b/bchain/coins/energi/erc20_test.go @@ -0,0 +1,204 @@ +// +build unittest + +package energi + +import ( + "fmt" + "math/big" + "strings" + "testing" + + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/tests/dbtestdata" +) + +func TestErc20_erc20GetTransfersFromLog(t *testing.T) { + tests := []struct { + name string + args []*rpcLog + want []bchain.Erc20Transfer + wantErr bool + }{ + { + name: "1", + args: []*rpcLog{ + { + Address: "0x76a45e8976499ab9ae223cc584019341d5a84e96", + Topics: []string{ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000002aacf811ac1a60081ea39f7783c0d26c500871a8", + "0x000000000000000000000000e9a5216ff992cfa01594d43501a56e12769eb9d2", + }, + Data: "0x0000000000000000000000000000000000000000000000000000000000000123", + }, + }, + want: []bchain.Erc20Transfer{ + { + Contract: "0x76a45e8976499ab9ae223cc584019341d5a84e96", + From: "0x2aacf811ac1a60081ea39f7783c0d26c500871a8", + To: "0xe9a5216ff992cfa01594d43501a56e12769eb9d2", + Tokens: *big.NewInt(0x123), + }, + }, + }, + { + name: "2", + args: []*rpcLog{ + { // Transfer + Address: "0x0d0f936ee4c93e25944694d6c121de94d9760f11", + Topics: []string{ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000006f44cceb49b4a5812d54b6f494fc2febf25511ed", + "0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d", + }, + Data: "0x0000000000000000000000000000000000000000000000006a8313d60b1f606b", + }, + { // Transfer + Address: "0xc778417e063141139fce010982780140aa0cd5ab", + Topics: []string{ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d", + "0x0000000000000000000000006f44cceb49b4a5812d54b6f494fc2febf25511ed", + }, + Data: "0x000000000000000000000000000000000000000000000000000308fd0e798ac0", + }, + { // not Transfer + Address: "0x479cc461fecd078f766ecc58533d6f69580cf3ac", + Topics: []string{ + "0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3", + "0x0000000000000000000000006f44cceb49b4a5812d54b6f494fc2febf25511ed", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x5af266c0a89a07c1917deaa024414577e6c3c31c8907d079e13eb448c082594f", + }, + Data: "0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000", + }, + { // not Transfer + Address: "0x0d0f936ee4c93e25944694d6c121de94d9760f11", + Topics: []string{ + "0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3", + "0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b", + "0xb0b69dad58df6032c3b266e19b1045b19c87acd2c06fb0c598090f44b8e263aa", + }, + Data: "0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d000000000000000000000000c778417e063141139fce010982780140aa0cd5ab0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f1100000000000000000000000000000000000000000000000000031855667df7a80000000000000000000000000000000000000000000000006a8313d60b1f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }, + }, + want: []bchain.Erc20Transfer{ + { + Contract: "0x0d0f936ee4c93e25944694d6c121de94d9760f11", + From: "0x6f44cceb49b4a5812d54b6f494fc2febf25511ed", + To: "0x4bda106325c335df99eab7fe363cac8a0ba2a24d", + Tokens: *big.NewInt(0x6a8313d60b1f606b), + }, + { + Contract: "0xc778417e063141139fce010982780140aa0cd5ab", + From: "0x4bda106325c335df99eab7fe363cac8a0ba2a24d", + To: "0x6f44cceb49b4a5812d54b6f494fc2febf25511ed", + Tokens: *big.NewInt(0x308fd0e798ac0), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := erc20GetTransfersFromLog(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("erc20GetTransfersFromLog error = %v, wantErr %v", err, tt.wantErr) + return + } + // the addresses could have different case + if strings.ToLower(fmt.Sprint(got)) != strings.ToLower(fmt.Sprint(tt.want)) { + t.Errorf("erc20GetTransfersFromLog = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestErc20_parseErc20StringProperty(t *testing.T) { + tests := []struct { + name string + args string + want string + }{ + { + name: "1", + args: "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000758504c4f44444500000000000000000000000000000000000000000000000000", + want: "XPLODDE", + }, + { + name: "2", + args: "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000022426974436c617665202d20436f6e73756d657220416374697669747920546f6b656e00000000000000", + want: "BitClave - Consumer Activity Token", + }, + { + name: "short", + args: "0x44616920537461626c65636f696e2076312e3000000000000000000000000000", + want: "Dai Stablecoin v1.0", + }, + { + name: "short2", + args: "0x44616920537461626c65636f696e2076312e3020444444444444444444444444", + want: "Dai Stablecoin v1.0 DDDDDDDDDDDD", + }, + { + name: "long", + args: "0x556e6973776170205631000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + want: "Uniswap V1", + }, + { + name: "garbage", + args: "0x2234880850896048596206002535425366538144616734015984380565810000", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseErc20StringProperty(nil, tt.args) + // the addresses could have different case + if got != tt.want { + t.Errorf("parseErc20StringProperty = %v, want %v", got, tt.want) + } + }) + } +} + +func TestErc20_erc20GetTransfersFromTx(t *testing.T) { + p := NewEnergiParser(1) + b := dbtestdata.GetTestEthereumTypeBlock1(p) + bn, _ := new(big.Int).SetString("21e19e0c9bab2400000", 16) + tests := []struct { + name string + args *rpcTransaction + want []bchain.Erc20Transfer + }{ + { + name: "0", + args: (b.Txs[0].CoinSpecificData.(completeTransaction)).Tx, + want: []bchain.Erc20Transfer{}, + }, + { + name: "1", + args: (b.Txs[1].CoinSpecificData.(completeTransaction)).Tx, + want: []bchain.Erc20Transfer{ + { + Contract: "0x4af4114f73d1c1c903ac9e0361b379d1291808a2", + From: "0x20cd153de35d469ba46127a0c8f18626b59a256a", + To: "0x555ee11fbddc0e49a9bab358a8941ad95ffdb48f", + Tokens: *bn, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := erc20GetTransfersFromTx(tt.args) + if err != nil { + t.Errorf("erc20GetTransfersFromTx error = %v", err) + return + } + // the addresses could have different case + if strings.ToLower(fmt.Sprint(got)) != strings.ToLower(fmt.Sprint(tt.want)) { + t.Errorf("erc20GetTransfersFromTx = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/bchain/coins/energi/ethparser.go b/bchain/coins/energi/ethparser.go new file mode 100644 index 00000000..82528192 --- /dev/null +++ b/bchain/coins/energi/ethparser.go @@ -0,0 +1,520 @@ +package energi + +import ( + "encoding/hex" + "math/big" + "strconv" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/golang/protobuf/proto" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "golang.org/x/crypto/sha3" +) + +// EthereumTypeAddressDescriptorLen - in case of EthereumType, the AddressDescriptor has fixed length +const EthereumTypeAddressDescriptorLen = 20 + +// EtherAmountDecimalPoint defines number of decimal points in Ether amounts +const EtherAmountDecimalPoint = 18 + +// EnergiParser handle +type EnergiParser struct { + *bchain.BaseParser +} + +// NewEnergiParser returns new EnergiParser instance +func NewEnergiParser(b int) *EnergiParser { + return &EnergiParser{&bchain.BaseParser{ + BlockAddressesToKeep: b, + AmountDecimalPoint: EtherAmountDecimalPoint, + }} +} + +type rpcHeader struct { + Hash string `json:"hash"` + ParentHash string `json:"parentHash"` + Difficulty string `json:"difficulty"` + Number string `json:"number"` + Time string `json:"timestamp"` + Size string `json:"size"` + Nonce string `json:"nonce"` +} + +type rpcTransaction struct { + AccountNonce string `json:"nonce"` + GasPrice string `json:"gasPrice"` + GasLimit string `json:"gas"` + To string `json:"to"` // nil means contract creation + Value string `json:"value"` + Payload string `json:"input"` + Hash string `json:"hash"` + BlockNumber string `json:"blockNumber"` + BlockHash string `json:"blockHash,omitempty"` + From string `json:"from"` + TransactionIndex string `json:"transactionIndex"` + // Signature values - ignored + // V string `json:"v"` + // R string `json:"r"` + // S string `json:"s"` +} + +type rpcLog struct { + Address string `json:"address"` + Topics []string `json:"topics"` + Data string `json:"data"` +} + +type rpcLogWithTxHash struct { + rpcLog + Hash string `json:"transactionHash"` +} + +type rpcReceipt struct { + GasUsed string `json:"gasUsed"` + Status string `json:"status"` + Logs []*rpcLog `json:"logs"` +} + +type completeTransaction struct { + Tx *rpcTransaction `json:"tx"` + Receipt *rpcReceipt `json:"receipt,omitempty"` +} + +type rpcBlockTransactions struct { + Transactions []rpcTransaction `json:"transactions"` +} + +type rpcBlockTxids struct { + Transactions []string `json:"transactions"` +} + +func ethNumber(n string) (int64, error) { + if len(n) > 2 { + return strconv.ParseInt(n[2:], 16, 64) + } + return 0, errors.Errorf("Not a number: '%v'", n) +} + +type hash string + +type header struct { + Difficulty hexutil.Big `json:"difficulty"` + Number hexutil.Big `json:"number"` + Hash hash `json:"hash"` +} + +func (p *EnergiParser) ethTxToTx(tx *rpcTransaction, receipt *rpcReceipt, blocktime int64, confirmations uint32, fixEIP55 bool) (*bchain.Tx, error) { + txid := tx.Hash + var ( + fa, ta []string + err error + ) + if len(tx.From) > 2 { + if fixEIP55 { + tx.From = EIP55AddressFromAddress(tx.From) + } + fa = []string{tx.From} + } + if len(tx.To) > 2 { + if fixEIP55 { + tx.To = EIP55AddressFromAddress(tx.To) + } + ta = []string{tx.To} + } + if fixEIP55 && receipt != nil && receipt.Logs != nil { + for _, l := range receipt.Logs { + if len(l.Address) > 2 { + l.Address = EIP55AddressFromAddress(l.Address) + } + } + } + ct := completeTransaction{ + Tx: tx, + Receipt: receipt, + } + vs, err := hexutil.DecodeBig(tx.Value) + if err != nil { + return nil, err + } + return &bchain.Tx{ + Blocktime: blocktime, + Confirmations: confirmations, + // Hex + // LockTime + Time: blocktime, + Txid: txid, + Vin: []bchain.Vin{ + { + Addresses: fa, + // Coinbase + // ScriptSig + // Sequence + // Txid + // Vout + }, + }, + Vout: []bchain.Vout{ + { + N: 0, // there is always up to one To address + ValueSat: *vs, + ScriptPubKey: bchain.ScriptPubKey{ + // Hex + Addresses: ta, + }, + }, + }, + CoinSpecificData: ct, + }, nil +} + +// GetAddrDescFromVout returns internal address representation of given transaction output +func (p *EnergiParser) GetAddrDescFromVout(output *bchain.Vout) (bchain.AddressDescriptor, error) { + if len(output.ScriptPubKey.Addresses) != 1 { + return nil, bchain.ErrAddressMissing + } + return p.GetAddrDescFromAddress(output.ScriptPubKey.Addresses[0]) +} + +func has0xPrefix(s string) bool { + return len(s) >= 2 && s[0] == '0' && (s[1]|32) == 'x' +} + +// GetAddrDescFromAddress returns internal address representation of given address +func (p *EnergiParser) GetAddrDescFromAddress(address string) (bchain.AddressDescriptor, error) { + // github.com/ethereum/go-ethereum/common.HexToAddress does not handle address errors, using own decoding + if has0xPrefix(address) { + address = address[2:] + } + if len(address) != EthereumTypeAddressDescriptorLen*2 { + return nil, bchain.ErrAddressMissing + } + return hex.DecodeString(address) +} + +// EIP55Address returns an EIP55-compliant hex string representation of the address +func EIP55Address(addrDesc bchain.AddressDescriptor) string { + raw := hexutil.Encode(addrDesc) + if len(raw) != 42 { + return raw + } + sha := sha3.NewLegacyKeccak256() + result := []byte(raw) + sha.Write(result[2:]) + hash := sha.Sum(nil) + + for i := 2; i < len(result); i++ { + hashByte := hash[(i-2)>>1] + if i%2 == 0 { + hashByte = hashByte >> 4 + } else { + hashByte &= 0xf + } + if result[i] > '9' && hashByte > 7 { + result[i] -= 32 + } + } + return string(result) +} + +// EIP55AddressFromAddress returns an EIP55-compliant hex string representation of the address +func EIP55AddressFromAddress(address string) string { + if has0xPrefix(address) { + address = address[2:] + } + b, err := hex.DecodeString(address) + if err != nil { + return address + } + return EIP55Address(b) +} + +// GetAddressesFromAddrDesc returns addresses for given address descriptor with flag if the addresses are searchable +func (p *EnergiParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, bool, error) { + return []string{EIP55Address(addrDesc)}, true, nil +} + +// GetScriptFromAddrDesc returns output script for given address descriptor +func (p *EnergiParser) GetScriptFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]byte, error) { + return addrDesc, nil +} + +func hexDecode(s string) ([]byte, error) { + b, err := hexutil.Decode(s) + if err != nil && err != hexutil.ErrEmptyString { + return nil, err + } + return b, nil +} + +func hexDecodeBig(s string) ([]byte, error) { + b, err := hexutil.DecodeBig(s) + if err != nil { + return nil, err + } + return b.Bytes(), nil +} + +func hexEncodeBig(b []byte) string { + var i big.Int + i.SetBytes(b) + return hexutil.EncodeBig(&i) +} + +// PackTx packs transaction to byte array +func (p *EnergiParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) { + var err error + var n uint64 + r, ok := tx.CoinSpecificData.(completeTransaction) + if !ok { + return nil, errors.New("Missing CoinSpecificData") + } + pt := &ProtoCompleteTransaction{} + pt.Tx = &ProtoCompleteTransaction_TxType{} + if pt.Tx.AccountNonce, err = hexutil.DecodeUint64(r.Tx.AccountNonce); err != nil { + return nil, errors.Annotatef(err, "AccountNonce %v", r.Tx.AccountNonce) + } + // pt.BlockNumber = height + if n, err = hexutil.DecodeUint64(r.Tx.BlockNumber); err != nil { + return nil, errors.Annotatef(err, "BlockNumber %v", r.Tx.BlockNumber) + } + pt.BlockNumber = uint32(n) + pt.BlockTime = uint64(blockTime) + if pt.Tx.From, err = hexDecode(r.Tx.From); err != nil { + return nil, errors.Annotatef(err, "From %v", r.Tx.From) + } + if pt.Tx.GasLimit, err = hexutil.DecodeUint64(r.Tx.GasLimit); err != nil { + return nil, errors.Annotatef(err, "GasLimit %v", r.Tx.GasLimit) + } + if pt.Tx.Hash, err = hexDecode(r.Tx.Hash); err != nil { + return nil, errors.Annotatef(err, "Hash %v", r.Tx.Hash) + } + if pt.Tx.Payload, err = hexDecode(r.Tx.Payload); err != nil { + return nil, errors.Annotatef(err, "Payload %v", r.Tx.Payload) + } + if pt.Tx.GasPrice, err = hexDecodeBig(r.Tx.GasPrice); err != nil { + return nil, errors.Annotatef(err, "Price %v", r.Tx.GasPrice) + } + // if pt.R, err = hexDecodeBig(r.R); err != nil { + // return nil, errors.Annotatef(err, "R %v", r.R) + // } + // if pt.S, err = hexDecodeBig(r.S); err != nil { + // return nil, errors.Annotatef(err, "S %v", r.S) + // } + // if pt.V, err = hexDecodeBig(r.V); err != nil { + // return nil, errors.Annotatef(err, "V %v", r.V) + // } + if pt.Tx.To, err = hexDecode(r.Tx.To); err != nil { + return nil, errors.Annotatef(err, "To %v", r.Tx.To) + } + if n, err = hexutil.DecodeUint64(r.Tx.TransactionIndex); err != nil { + return nil, errors.Annotatef(err, "TransactionIndex %v", r.Tx.TransactionIndex) + } + pt.Tx.TransactionIndex = uint32(n) + if pt.Tx.Value, err = hexDecodeBig(r.Tx.Value); err != nil { + return nil, errors.Annotatef(err, "Value %v", r.Tx.Value) + } + if r.Receipt != nil { + pt.Receipt = &ProtoCompleteTransaction_ReceiptType{} + if pt.Receipt.GasUsed, err = hexDecodeBig(r.Receipt.GasUsed); err != nil { + return nil, errors.Annotatef(err, "GasUsed %v", r.Receipt.GasUsed) + } + if r.Receipt.Status != "" { + if pt.Receipt.Status, err = hexDecodeBig(r.Receipt.Status); err != nil { + return nil, errors.Annotatef(err, "Status %v", r.Receipt.Status) + } + } else { + // unknown status, use 'U' as status bytes + // there is a potential for conflict with value 0x55 but this is not used by any chain at this moment + pt.Receipt.Status = []byte{'U'} + } + ptLogs := make([]*ProtoCompleteTransaction_ReceiptType_LogType, len(r.Receipt.Logs)) + for i, l := range r.Receipt.Logs { + a, err := hexutil.Decode(l.Address) + if err != nil { + return nil, errors.Annotatef(err, "Address cannot be decoded %v", l) + } + d, err := hexutil.Decode(l.Data) + if err != nil { + return nil, errors.Annotatef(err, "Data cannot be decoded %v", l) + } + t := make([][]byte, len(l.Topics)) + for j, s := range l.Topics { + t[j], err = hexutil.Decode(s) + if err != nil { + return nil, errors.Annotatef(err, "Topic cannot be decoded %v", l) + } + } + ptLogs[i] = &ProtoCompleteTransaction_ReceiptType_LogType{ + Address: a, + Data: d, + Topics: t, + } + + } + pt.Receipt.Log = ptLogs + } + return proto.Marshal(pt) +} + +// UnpackTx unpacks transaction from byte array +func (p *EnergiParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { + var pt ProtoCompleteTransaction + err := proto.Unmarshal(buf, &pt) + if err != nil { + return nil, 0, err + } + rt := rpcTransaction{ + AccountNonce: hexutil.EncodeUint64(pt.Tx.AccountNonce), + BlockNumber: hexutil.EncodeUint64(uint64(pt.BlockNumber)), + From: EIP55Address(pt.Tx.From), + GasLimit: hexutil.EncodeUint64(pt.Tx.GasLimit), + Hash: hexutil.Encode(pt.Tx.Hash), + Payload: hexutil.Encode(pt.Tx.Payload), + GasPrice: hexEncodeBig(pt.Tx.GasPrice), + // R: hexEncodeBig(pt.R), + // S: hexEncodeBig(pt.S), + // V: hexEncodeBig(pt.V), + To: EIP55Address(pt.Tx.To), + TransactionIndex: hexutil.EncodeUint64(uint64(pt.Tx.TransactionIndex)), + Value: hexEncodeBig(pt.Tx.Value), + } + var rr *rpcReceipt + if pt.Receipt != nil { + logs := make([]*rpcLog, len(pt.Receipt.Log)) + for i, l := range pt.Receipt.Log { + topics := make([]string, len(l.Topics)) + for j, t := range l.Topics { + topics[j] = hexutil.Encode(t) + } + logs[i] = &rpcLog{ + Address: EIP55Address(l.Address), + Data: hexutil.Encode(l.Data), + Topics: topics, + } + } + status := "" + // handle a special value []byte{'U'} as unknown state + if len(pt.Receipt.Status) != 1 || pt.Receipt.Status[0] != 'U' { + status = hexEncodeBig(pt.Receipt.Status) + } + rr = &rpcReceipt{ + GasUsed: hexEncodeBig(pt.Receipt.GasUsed), + Status: status, + Logs: logs, + } + } + tx, err := p.ethTxToTx(&rt, rr, int64(pt.BlockTime), 0, false) + if err != nil { + return nil, 0, err + } + return tx, pt.BlockNumber, nil +} + +// PackedTxidLen returns length in bytes of packed txid +func (p *EnergiParser) PackedTxidLen() int { + return 32 +} + +// PackTxid packs txid to byte array +func (p *EnergiParser) PackTxid(txid string) ([]byte, error) { + if has0xPrefix(txid) { + txid = txid[2:] + } + return hex.DecodeString(txid) +} + +// UnpackTxid unpacks byte array to txid +func (p *EnergiParser) UnpackTxid(buf []byte) (string, error) { + return hexutil.Encode(buf), nil +} + +// PackBlockHash packs block hash to byte array +func (p *EnergiParser) PackBlockHash(hash string) ([]byte, error) { + if has0xPrefix(hash) { + hash = hash[2:] + } + return hex.DecodeString(hash) +} + +// UnpackBlockHash unpacks byte array to block hash +func (p *EnergiParser) UnpackBlockHash(buf []byte) (string, error) { + return hexutil.Encode(buf), nil +} + +// EthereumTypeGetErc20FromTx returns Erc20 data from bchain.Tx +func (p *EnergiParser) EthereumTypeGetErc20FromTx(tx *bchain.Tx) ([]bchain.Erc20Transfer, error) { + var r []bchain.Erc20Transfer + var err error + csd, ok := tx.CoinSpecificData.(completeTransaction) + if ok { + if csd.Receipt != nil { + r, err = erc20GetTransfersFromLog(csd.Receipt.Logs) + } else { + r, err = erc20GetTransfersFromTx(csd.Tx) + } + if err != nil { + return nil, err + } + } + return r, nil +} + +// TxStatus is status of transaction +type TxStatus int + +// statuses of transaction +const ( + TxStatusUnknown = TxStatus(iota - 2) + TxStatusPending + TxStatusFailure + TxStatusOK +) + +// EthereumTxData contains ethereum specific transaction data +type EthereumTxData struct { + Status TxStatus `json:"status"` // 1 OK, 0 Fail, -1 pending, -2 unknown + Nonce uint64 `json:"nonce"` + GasLimit *big.Int `json:"gaslimit"` + GasUsed *big.Int `json:"gasused"` + GasPrice *big.Int `json:"gasprice"` + Data string `json:"data"` +} + +// GetEthereumTxData returns EthereumTxData from bchain.Tx +func GetEthereumTxData(tx *bchain.Tx) *EthereumTxData { + return GetEthereumTxDataFromSpecificData(tx.CoinSpecificData) +} + +// GetEthereumTxDataFromSpecificData returns EthereumTxData from coinSpecificData +func GetEthereumTxDataFromSpecificData(coinSpecificData interface{}) *EthereumTxData { + etd := EthereumTxData{Status: TxStatusPending} + csd, ok := coinSpecificData.(completeTransaction) + if ok { + if csd.Tx != nil { + etd.Nonce, _ = hexutil.DecodeUint64(csd.Tx.AccountNonce) + etd.GasLimit, _ = hexutil.DecodeBig(csd.Tx.GasLimit) + etd.GasPrice, _ = hexutil.DecodeBig(csd.Tx.GasPrice) + etd.Data = csd.Tx.Payload + } + if csd.Receipt != nil { + switch csd.Receipt.Status { + case "0x1": + etd.Status = TxStatusOK + case "": // old transactions did not set status + etd.Status = TxStatusUnknown + default: + etd.Status = TxStatusFailure + } + etd.GasUsed, _ = hexutil.DecodeBig(csd.Receipt.GasUsed) + } + } + return &etd +} + +func toBlockNumArg(number *big.Int) string { + if number == nil { + return "latest" + } + return hexutil.EncodeBig(number) +} diff --git a/bchain/coins/energi/ethparser_test.go b/bchain/coins/energi/ethparser_test.go new file mode 100644 index 00000000..115eed77 --- /dev/null +++ b/bchain/coins/energi/ethparser_test.go @@ -0,0 +1,433 @@ +// +build unittest + +package energi + +import ( + "encoding/hex" + "fmt" + "math/big" + "reflect" + "testing" + + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/tests/dbtestdata" +) + +func TestEthParser_GetAddrDescFromAddress(t *testing.T) { + type args struct { + address string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "with 0x prefix", + args: args{address: "0x81b7e08f65bdf5648606c89998a9cc8164397647"}, + want: "81b7e08f65bdf5648606c89998a9cc8164397647", + }, + { + name: "without 0x prefix", + args: args{address: "47526228d673e9f079630d6cdaff5a2ed13e0e60"}, + want: "47526228d673e9f079630d6cdaff5a2ed13e0e60", + }, + { + name: "address of wrong length", + args: args{address: "7526228d673e9f079630d6cdaff5a2ed13e0e60"}, + want: "", + wantErr: true, + }, + { + name: "ErrAddressMissing", + args: args{address: ""}, + want: "", + wantErr: true, + }, + { + name: "error - not eth address", + args: args{address: "1JKgN43B9SyLuZH19H5ECvr4KcfrbVHzZ6"}, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := NewEnergiParser(1) + got, err := p.GetAddrDescFromAddress(tt.args.address) + if (err != nil) != tt.wantErr { + t.Errorf("EthParser.GetAddrDescFromAddress() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("EthParser.GetAddrDescFromAddress() = %v, want %v", h, tt.want) + } + }) + } +} + +var testTx1, testTx2, testTx1Failed, testTx1NoStatus bchain.Tx + +func init() { + + testTx1 = bchain.Tx{ + Blocktime: 1534858022, + Time: 1534858022, + Txid: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + Vin: []bchain.Vin{ + { + Addresses: []string{"0x3E3a3D69dc66bA10737F531ed088954a9EC89d97"}, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(1999622000000000000), + ScriptPubKey: bchain.ScriptPubKey{ + Addresses: []string{"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f"}, + }, + }, + }, + CoinSpecificData: completeTransaction{ + Tx: &rpcTransaction{ + AccountNonce: "0xb26c", + GasPrice: "0x430e23400", + GasLimit: "0x5208", + To: "0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f", + Value: "0x1bc0159d530e6000", + Payload: "0x", + Hash: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + BlockNumber: "0x41eee8", + From: "0x3E3a3D69dc66bA10737F531ed088954a9EC89d97", + TransactionIndex: "0xa", + }, + Receipt: &rpcReceipt{ + GasUsed: "0x5208", + Status: "0x1", + Logs: []*rpcLog{}, + }, + }, + } + + testTx2 = bchain.Tx{ + Blocktime: 1534858022, + Time: 1534858022, + Txid: "0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101", + Vin: []bchain.Vin{ + { + Addresses: []string{"0x20cD153de35D469BA46127A0C8F18626b59a256A"}, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(0), + ScriptPubKey: bchain.ScriptPubKey{ + Addresses: []string{"0x4af4114F73d1c1C903aC9E0361b379D1291808A2"}, + }, + }, + }, + CoinSpecificData: completeTransaction{ + Tx: &rpcTransaction{ + AccountNonce: "0xd0", + GasPrice: "0x9502f9000", + GasLimit: "0x130d5", + To: "0x4af4114F73d1c1C903aC9E0361b379D1291808A2", + Value: "0x0", + Payload: "0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000", + Hash: "0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101", + BlockNumber: "0x41eee8", + From: "0x20cD153de35D469BA46127A0C8F18626b59a256A", + TransactionIndex: "0x0"}, + Receipt: &rpcReceipt{ + GasUsed: "0xcb39", + Status: "0x1", + Logs: []*rpcLog{ + { + Address: "0x4af4114F73d1c1C903aC9E0361b379D1291808A2", + Data: "0x00000000000000000000000000000000000000000000021e19e0c9bab2400000", + Topics: []string{ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x00000000000000000000000020cd153de35d469ba46127a0c8f18626b59a256a", + "0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f", + }, + }, + }, + }, + }, + } + + testTx1Failed = bchain.Tx{ + Blocktime: 1534858022, + Time: 1534858022, + Txid: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + Vin: []bchain.Vin{ + { + Addresses: []string{"0x3E3a3D69dc66bA10737F531ed088954a9EC89d97"}, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(1999622000000000000), + ScriptPubKey: bchain.ScriptPubKey{ + Addresses: []string{"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f"}, + }, + }, + }, + CoinSpecificData: completeTransaction{ + Tx: &rpcTransaction{ + AccountNonce: "0xb26c", + GasPrice: "0x430e23400", + GasLimit: "0x5208", + To: "0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f", + Value: "0x1bc0159d530e6000", + Payload: "0x", + Hash: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + BlockNumber: "0x41eee8", + From: "0x3E3a3D69dc66bA10737F531ed088954a9EC89d97", + TransactionIndex: "0xa", + }, + Receipt: &rpcReceipt{ + GasUsed: "0x5208", + Status: "0x0", + Logs: []*rpcLog{}, + }, + }, + } + + testTx1NoStatus = bchain.Tx{ + Blocktime: 1534858022, + Time: 1534858022, + Txid: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + Vin: []bchain.Vin{ + { + Addresses: []string{"0x3E3a3D69dc66bA10737F531ed088954a9EC89d97"}, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(1999622000000000000), + ScriptPubKey: bchain.ScriptPubKey{ + Addresses: []string{"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f"}, + }, + }, + }, + CoinSpecificData: completeTransaction{ + Tx: &rpcTransaction{ + AccountNonce: "0xb26c", + GasPrice: "0x430e23400", + GasLimit: "0x5208", + To: "0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f", + Value: "0x1bc0159d530e6000", + Payload: "0x", + Hash: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + BlockNumber: "0x41eee8", + From: "0x3E3a3D69dc66bA10737F531ed088954a9EC89d97", + TransactionIndex: "0xa", + }, + Receipt: &rpcReceipt{ + GasUsed: "0x5208", + Status: "", + Logs: []*rpcLog{}, + }, + }, + } + +} + +func TestEnergiParser_PackTx(t *testing.T) { + type args struct { + tx *bchain.Tx + height uint32 + blockTime int64 + } + tests := []struct { + name string + p *EnergiParser + args args + want string + wantErr bool + }{ + { + name: "1", + args: args{ + tx: &testTx1, + height: 4321000, + blockTime: 1534858022, + }, + want: dbtestdata.EthTx1Packed, + }, + { + name: "2", + args: args{ + tx: &testTx2, + height: 4321000, + blockTime: 1534858022, + }, + want: dbtestdata.EthTx2Packed, + }, + { + name: "3", + args: args{ + tx: &testTx1Failed, + height: 4321000, + blockTime: 1534858022, + }, + want: dbtestdata.EthTx1FailedPacked, + }, + { + name: "4", + args: args{ + tx: &testTx1NoStatus, + height: 4321000, + blockTime: 1534858022, + }, + want: dbtestdata.EthTx1NoStatusPacked, + }, + } + p := NewEnergiParser(1) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := p.PackTx(tt.args.tx, tt.args.height, tt.args.blockTime) + if (err != nil) != tt.wantErr { + t.Errorf("EnergiParser.PackTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + h := hex.EncodeToString(got) + if !reflect.DeepEqual(h, tt.want) { + t.Errorf("EnergiParser.PackTx() = %v, want %v", h, tt.want) + } + }) + } +} + +func TestEnergiParser_UnpackTx(t *testing.T) { + type args struct { + hex string + } + tests := []struct { + name string + p *EnergiParser + args args + want *bchain.Tx + want1 uint32 + wantErr bool + }{ + { + name: "1", + args: args{hex: dbtestdata.EthTx1Packed}, + want: &testTx1, + want1: 4321000, + }, + { + name: "2", + args: args{hex: dbtestdata.EthTx2Packed}, + want: &testTx2, + want1: 4321000, + }, + { + name: "3", + args: args{hex: dbtestdata.EthTx1FailedPacked}, + want: &testTx1Failed, + want1: 4321000, + }, + { + name: "4", + args: args{hex: dbtestdata.EthTx1NoStatusPacked}, + want: &testTx1NoStatus, + want1: 4321000, + }, + } + p := NewEnergiParser(1) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := hex.DecodeString(tt.args.hex) + if err != nil { + panic(err) + } + got, got1, err := p.UnpackTx(b) + if (err != nil) != tt.wantErr { + t.Errorf("EnergiParser.UnpackTx() error = %v, wantErr %v", err, tt.wantErr) + return + } + // DeepEqual has problems with pointers in completeTransaction + gs := got.CoinSpecificData.(completeTransaction) + ws := tt.want.CoinSpecificData.(completeTransaction) + gc := *got + wc := *tt.want + gc.CoinSpecificData = nil + wc.CoinSpecificData = nil + if fmt.Sprint(gc) != fmt.Sprint(wc) { + // if !reflect.DeepEqual(gc, wc) { + t.Errorf("EnergiParser.UnpackTx() gc got = %+v, want %+v", gc, wc) + } + if !reflect.DeepEqual(gs.Tx, ws.Tx) { + t.Errorf("EnergiParser.UnpackTx() gs.Tx got = %+v, want %+v", gs.Tx, ws.Tx) + } + if !reflect.DeepEqual(gs.Receipt, ws.Receipt) { + t.Errorf("EnergiParser.UnpackTx() gs.Receipt got = %+v, want %+v", gs.Receipt, ws.Receipt) + } + if got1 != tt.want1 { + t.Errorf("EnergiParser.UnpackTx() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestEnergiParser_GetEthereumTxData(t *testing.T) { + tests := []struct { + name string + tx *bchain.Tx + want string + }{ + { + name: "Test empty data", + tx: &testTx1, + want: "0x", + }, + { + name: "Test non empty data", + tx: &testTx2, + want: "0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetEthereumTxData(tt.tx) + if got.Data != tt.want { + t.Errorf("EnergiParser.GetEthereumTxData() = %v, want %v", got.Data, tt.want) + } + }) + } +} + +func Test_toBlockNumArg(t *testing.T) { + tests := []struct { + name string + number *big.Int + want string + }{ + { + name: "test a valid number 1", + number: big.NewInt(1), + want: "0x1", + }, + { + name: "test a valid number 2", + number: big.NewInt(2), + want: "0x2", + }, + { + name: "test the latest block", + number: nil, + want: "latest", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toBlockNumArg(tt.number); got != tt.want { + t.Errorf("toBlockNumArg() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/bchain/coins/energi/ethrpc.go b/bchain/coins/energi/ethrpc.go new file mode 100644 index 00000000..57aaed7b --- /dev/null +++ b/bchain/coins/energi/ethrpc.go @@ -0,0 +1,790 @@ +package energi + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "strconv" + "sync" + "time" + + ethereum "github.com/ethereum/go-ethereum" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/common" +) + +// NRGNet type specifies the type of Energi network +type NRGNet uint32 + +const ( + // MainNet is production network + MainNet NRGNet = 39797 + // TestNet is Ropsten test network + TestNet NRGNet = 49797 +) + +// Configuration represents json config file +type Configuration struct { + CoinName string `json:"coin_name"` + CoinShortcut string `json:"coin_shortcut"` + RPCURL string `json:"rpc_url"` + RPCTimeout int `json:"rpc_timeout"` + BlockAddressesToKeep int `json:"block_addresses_to_keep"` + MempoolTxTimeoutHours int `json:"mempoolTxTimeoutHours"` + QueryBackendOnMempoolResync bool `json:"queryBackendOnMempoolResync"` +} + +// EnergiRPC is an interface to JSON-RPC eth service. +type EnergiRPC struct { + *bchain.BaseChain + client *ethclient.Client + rpc *rpc.Client + timeout time.Duration + Parser *EnergiParser + Mempool *bchain.MempoolEthereumType + mempoolInitialized bool + bestHeaderLock sync.Mutex + bestHeader *header + bestHeaderTime time.Time + chanNewBlock chan *header + newBlockSubscription *rpc.ClientSubscription + chanNewTx chan ethcommon.Hash + newTxSubscription *rpc.ClientSubscription + ChainConfig *Configuration +} + +// NewEnergiRPC returns new EthRPC instance. +func NewEnergiRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { + var err error + var c Configuration + err = json.Unmarshal(config, &c) + if err != nil { + return nil, errors.Annotatef(err, "Invalid configuration file") + } + // keep at least 100 mappings block->addresses to allow rollback + if c.BlockAddressesToKeep < 100 { + c.BlockAddressesToKeep = 100 + } + + rc, ec, err := openRPC(c.RPCURL) + if err != nil { + return nil, err + } + + s := &EnergiRPC{ + BaseChain: &bchain.BaseChain{}, + client: ec, + rpc: rc, + ChainConfig: &c, + } + + // always create parser + s.Parser = NewEnergiParser(c.BlockAddressesToKeep) + s.timeout = time.Duration(c.RPCTimeout) * time.Second + + // new blocks notifications handling + // the subscription is done in Initialize + s.chanNewBlock = make(chan *header) + go func() { + for { + h, ok := <-s.chanNewBlock + if !ok { + break + } + glog.V(2).Info("rpc: new block header ", h.Number) + // update best header to the new header + s.bestHeaderLock.Lock() + s.bestHeader = h + s.bestHeaderTime = time.Now() + s.bestHeaderLock.Unlock() + // notify blockbook + pushHandler(bchain.NotificationNewBlock) + } + }() + + // new mempool transaction notifications handling + // the subscription is done in Initialize + s.chanNewTx = make(chan ethcommon.Hash) + go func() { + for { + t, ok := <-s.chanNewTx + if !ok { + break + } + hex := t.Hex() + if glog.V(2) { + glog.Info("rpc: new tx ", hex) + } + s.Mempool.AddTransactionToMempool(hex) + pushHandler(bchain.NotificationNewTx) + } + }() + + return s, nil +} + +func openRPC(url string) (*rpc.Client, *ethclient.Client, error) { + rc, err := rpc.Dial(url) + if err != nil { + return nil, nil, err + } + ec := ethclient.NewClient(rc) + return rc, ec, nil +} + +// Initialize initializes ethereum rpc interface +func (b *EnergiRPC) Initialize() error { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + + id, err := b.client.NetworkID(ctx) + if err != nil { + return err + } + + // parameters for getInfo request + switch NRGNet(id.Uint64()) { + case MainNet: + b.Testnet = false + b.Network = "livenet" + break + case TestNet: + b.Testnet = true + b.Network = "testnet" + break + default: + return errors.Errorf("Unknown network id %v", id) + } + glog.Info("rpc: block chain ", b.Network) + + return nil +} + +// CreateMempool creates mempool if not already created, however does not initialize it +func (b *EnergiRPC) CreateMempool(chain bchain.BlockChain) (bchain.Mempool, error) { + if b.Mempool == nil { + b.Mempool = bchain.NewMempoolEthereumType(chain, b.ChainConfig.MempoolTxTimeoutHours, b.ChainConfig.QueryBackendOnMempoolResync) + glog.Info("mempool created, MempoolTxTimeoutHours=", b.ChainConfig.MempoolTxTimeoutHours, ", QueryBackendOnMempoolResync=", b.ChainConfig.QueryBackendOnMempoolResync) + } + return b.Mempool, nil +} + +// InitializeMempool creates subscriptions to newHeads and newPendingTransactions +func (b *EnergiRPC) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTxAddr bchain.OnNewTxAddrFunc, onNewTx bchain.OnNewTxFunc) error { + if b.Mempool == nil { + return errors.New("Mempool not created") + } + + // get initial mempool transactions + txs, err := b.GetMempoolTransactions() + if err != nil { + return err + } + for _, txid := range txs { + b.Mempool.AddTransactionToMempool(txid) + } + + b.Mempool.OnNewTxAddr = onNewTxAddr + b.Mempool.OnNewTx = onNewTx + + if err = b.subscribeEvents(); err != nil { + return err + } + + b.mempoolInitialized = true + + return nil +} + +func (b *EnergiRPC) subscribeEvents() error { + // subscriptions + if err := b.subscribe(func() (*rpc.ClientSubscription, error) { + // invalidate the previous subscription - it is either the first one or there was an error + b.newBlockSubscription = nil + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + sub, err := b.rpc.EthSubscribe(ctx, b.chanNewBlock, "newHeads") + if err != nil { + return nil, errors.Annotatef(err, "EthSubscribe newHeads") + } + b.newBlockSubscription = sub + glog.Info("Subscribed to newHeads") + return sub, nil + }); err != nil { + return err + } + + if err := b.subscribe(func() (*rpc.ClientSubscription, error) { + // invalidate the previous subscription - it is either the first one or there was an error + b.newTxSubscription = nil + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + sub, err := b.rpc.EthSubscribe(ctx, b.chanNewTx, "newPendingTransactions") + if err != nil { + return nil, errors.Annotatef(err, "EthSubscribe newPendingTransactions") + } + b.newTxSubscription = sub + glog.Info("Subscribed to newPendingTransactions") + return sub, nil + }); err != nil { + return err + } + + return nil +} + +// subscribe subscribes notification and tries to resubscribe in case of error +func (b *EnergiRPC) subscribe(f func() (*rpc.ClientSubscription, error)) error { + s, err := f() + if err != nil { + return err + } + go func() { + Loop: + for { + // wait for error in subscription + e := <-s.Err() + // nil error means sub.Unsubscribe called, exit goroutine + if e == nil { + return + } + glog.Error("Subscription error ", e) + timer := time.NewTimer(time.Second * 2) + // try in 2 second interval to resubscribe + for { + select { + case e = <-s.Err(): + if e == nil { + return + } + case <-timer.C: + ns, err := f() + if err == nil { + // subscription successful, restart wait for next error + s = ns + continue Loop + } + glog.Error("Resubscribe error ", err) + timer.Reset(time.Second * 2) + } + } + } + }() + return nil +} + +func (b *EnergiRPC) closeRPC() { + if b.newBlockSubscription != nil { + b.newBlockSubscription.Unsubscribe() + } + if b.newTxSubscription != nil { + b.newTxSubscription.Unsubscribe() + } + if b.rpc != nil { + b.rpc.Close() + } +} + +func (b *EnergiRPC) reconnectRPC() error { + glog.Info("Reconnecting RPC") + b.closeRPC() + rc, ec, err := openRPC(b.ChainConfig.RPCURL) + if err != nil { + return err + } + b.rpc = rc + b.client = ec + return b.subscribeEvents() +} + +// Shutdown cleans up rpc interface to ethereum +func (b *EnergiRPC) Shutdown(ctx context.Context) error { + b.closeRPC() + close(b.chanNewBlock) + glog.Info("rpc: shutdown") + return nil +} + +// GetCoinName returns coin name +func (b *EnergiRPC) GetCoinName() string { + return b.ChainConfig.CoinName +} + +// GetSubversion returns empty string, ethereum does not have subversion +func (b *EnergiRPC) GetSubversion() string { + return "" +} + +// GetChainInfo returns information about the connected backend +func (b *EnergiRPC) GetChainInfo() (*bchain.ChainInfo, error) { + h, err := b.getBestHeader() + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + id, err := b.client.NetworkID(ctx) + if err != nil { + return nil, err + } + var ver, protocol string + if err := b.rpc.CallContext(ctx, &ver, "web3_clientVersion"); err != nil { + return nil, err + } + if err := b.rpc.CallContext(ctx, &protocol, "eth_protocolVersion"); err != nil { + return nil, err + } + rv := &bchain.ChainInfo{ + Blocks: int(h.Number.ToInt().Int64()), + Bestblockhash: string(h.Hash), + Difficulty: h.Difficulty.String(), + Version: ver, + ProtocolVersion: protocol, + } + idi := int(id.Uint64()) + if idi == 1 { + rv.Chain = "mainnet" + } else { + rv.Chain = "testnet " + strconv.Itoa(idi) + } + return rv, nil +} + +func (b *EnergiRPC) getBestHeader() (*header, error) { + b.bestHeaderLock.Lock() + defer b.bestHeaderLock.Unlock() + // if the best header was not updated for 15 minutes, there could be a subscription problem, reconnect RPC + // do it only in case of normal operation, not initial synchronization + if b.bestHeaderTime.Add(15*time.Minute).Before(time.Now()) && !b.bestHeaderTime.IsZero() && b.mempoolInitialized { + err := b.reconnectRPC() + if err != nil { + return nil, err + } + b.bestHeader = nil + } + if b.bestHeader == nil { + var err error + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + b.bestHeader, err = b.headerByNumber(ctx, nil) + if err != nil { + b.bestHeader = nil + return nil, err + } + b.bestHeaderTime = time.Now() + } + return b.bestHeader, nil +} + +// GetBestBlockHash returns hash of the tip of the best-block-chain +func (b *EnergiRPC) GetBestBlockHash() (string, error) { + h, err := b.getBestHeader() + if err != nil { + return "", err + } + return string(h.Hash), nil +} + +// GetBestBlockHeight returns height of the tip of the best-block-chain +func (b *EnergiRPC) GetBestBlockHeight() (uint32, error) { + h, err := b.getBestHeader() + if err != nil { + return 0, err + } + return uint32(h.Number.ToInt().Uint64()), nil +} + +// GetBlockHash returns hash of block in best-block-chain at given height +func (b *EnergiRPC) GetBlockHash(height uint32) (string, error) { + var n big.Int + n.SetUint64(uint64(height)) + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + h, err := b.headerByNumber(ctx, &n) + if err != nil { + if err == ethereum.NotFound { + return "", bchain.ErrBlockNotFound + } + return "", errors.Annotatef(err, "height %v", height) + } + return string(h.Hash), nil +} + +func (b *EnergiRPC) ethHeaderToBlockHeader(h *rpcHeader) (*bchain.BlockHeader, error) { + height, err := ethNumber(h.Number) + if err != nil { + return nil, err + } + c, err := b.computeConfirmations(uint64(height)) + if err != nil { + return nil, err + } + time, err := ethNumber(h.Time) + if err != nil { + return nil, err + } + size, err := ethNumber(h.Size) + if err != nil { + return nil, err + } + return &bchain.BlockHeader{ + Hash: h.Hash, + Prev: h.ParentHash, + Height: uint32(height), + Confirmations: int(c), + Time: time, + Size: int(size), + }, nil +} + +// GetBlockHeader returns header of block with given hash +func (b *EnergiRPC) GetBlockHeader(hash string) (*bchain.BlockHeader, error) { + raw, err := b.getBlockRaw(hash, 0, false) + if err != nil { + return nil, err + } + var h rpcHeader + if err := json.Unmarshal(raw, &h); err != nil { + return nil, errors.Annotatef(err, "hash %v", hash) + } + return b.ethHeaderToBlockHeader(&h) +} + +func (b *EnergiRPC) computeConfirmations(n uint64) (uint32, error) { + bh, err := b.getBestHeader() + if err != nil { + return 0, err + } + bn := bh.Number.ToInt().Uint64() + // transaction in the best block has 1 confirmation + return uint32(bn - n + 1), nil +} + +func (b *EnergiRPC) getBlockRaw(hash string, height uint32, fullTxs bool) (json.RawMessage, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + var raw json.RawMessage + var err error + if hash != "" { + if hash == "pending" { + err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByNumber", hash, fullTxs) + } else { + err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByHash", ethcommon.HexToHash(hash), fullTxs) + } + } else { + err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByNumber", fmt.Sprintf("%#x", height), fullTxs) + } + if err != nil { + return nil, errors.Annotatef(err, "hash %v, height %v", hash, height) + } else if len(raw) == 0 { + return nil, bchain.ErrBlockNotFound + } + return raw, nil +} + +func (b *EnergiRPC) getERC20EventsForBlock(blockNumber string) (map[string][]*rpcLog, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + var logs []rpcLogWithTxHash + err := b.rpc.CallContext(ctx, &logs, "eth_getLogs", map[string]interface{}{ + "fromBlock": blockNumber, + "toBlock": blockNumber, + "topics": []string{erc20TransferEventSignature}, + }) + if err != nil { + return nil, errors.Annotatef(err, "blockNumber %v", blockNumber) + } + r := make(map[string][]*rpcLog) + for i := range logs { + l := &logs[i] + r[l.Hash] = append(r[l.Hash], &l.rpcLog) + } + return r, nil +} + +// GetBlock returns block with given hash or height, hash has precedence if both passed +func (b *EnergiRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { + raw, err := b.getBlockRaw(hash, height, true) + if err != nil { + return nil, err + } + var head rpcHeader + if err := json.Unmarshal(raw, &head); err != nil { + return nil, errors.Annotatef(err, "hash %v, height %v", hash, height) + } + var body rpcBlockTransactions + if err := json.Unmarshal(raw, &body); err != nil { + return nil, errors.Annotatef(err, "hash %v, height %v", hash, height) + } + bbh, err := b.ethHeaderToBlockHeader(&head) + if err != nil { + return nil, errors.Annotatef(err, "hash %v, height %v", hash, height) + } + // get ERC20 events + logs, err := b.getERC20EventsForBlock(head.Number) + if err != nil { + return nil, err + } + btxs := make([]bchain.Tx, len(body.Transactions)) + for i := range body.Transactions { + tx := &body.Transactions[i] + btx, err := b.Parser.ethTxToTx(tx, &rpcReceipt{Logs: logs[tx.Hash]}, bbh.Time, uint32(bbh.Confirmations), true) + if err != nil { + return nil, errors.Annotatef(err, "hash %v, height %v, txid %v", hash, height, tx.Hash) + } + btxs[i] = *btx + if b.mempoolInitialized { + b.Mempool.RemoveTransactionFromMempool(tx.Hash) + } + } + bbk := bchain.Block{ + BlockHeader: *bbh, + Txs: btxs, + } + return &bbk, nil +} + +// GetBlockInfo returns extended header (more info than in bchain.BlockHeader) with a list of txids +func (b *EnergiRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) { + raw, err := b.getBlockRaw(hash, 0, false) + if err != nil { + return nil, err + } + var head rpcHeader + var txs rpcBlockTxids + if err := json.Unmarshal(raw, &head); err != nil { + return nil, errors.Annotatef(err, "hash %v", hash) + } + if err = json.Unmarshal(raw, &txs); err != nil { + return nil, err + } + bch, err := b.ethHeaderToBlockHeader(&head) + if err != nil { + return nil, err + } + return &bchain.BlockInfo{ + BlockHeader: *bch, + Difficulty: common.JSONNumber(head.Difficulty), + Nonce: common.JSONNumber(head.Nonce), + Txids: txs.Transactions, + }, nil +} + +// GetTransactionForMempool returns a transaction by the transaction ID. +// It could be optimized for mempool, i.e. without block time and confirmations +func (b *EnergiRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) { + return b.GetTransaction(txid) +} + +// GetTransaction returns a transaction by the transaction ID. +func (b *EnergiRPC) GetTransaction(txid string) (*bchain.Tx, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + var tx *rpcTransaction + hash := ethcommon.HexToHash(txid) + err := b.rpc.CallContext(ctx, &tx, "eth_getTransactionByHash", hash) + if err != nil { + return nil, err + } else if tx == nil { + if b.mempoolInitialized { + b.Mempool.RemoveTransactionFromMempool(txid) + } + return nil, bchain.ErrTxNotFound + } + var btx *bchain.Tx + if tx.BlockNumber == "" { + // mempool tx + btx, err = b.Parser.ethTxToTx(tx, nil, 0, 0, true) + if err != nil { + return nil, errors.Annotatef(err, "txid %v", txid) + } + } else { + // non mempool tx - read the block header to get the block time + raw, err := b.getBlockRaw(tx.BlockHash, 0, false) + if err != nil { + return nil, err + } + var ht struct { + Time string `json:"timestamp"` + } + if err := json.Unmarshal(raw, &ht); err != nil { + return nil, errors.Annotatef(err, "hash %v", hash) + } + var time int64 + if time, err = ethNumber(ht.Time); err != nil { + return nil, errors.Annotatef(err, "txid %v", txid) + } + var receipt rpcReceipt + err = b.rpc.CallContext(ctx, &receipt, "eth_getTransactionReceipt", hash) + if err != nil { + return nil, errors.Annotatef(err, "txid %v", txid) + } + n, err := ethNumber(tx.BlockNumber) + if err != nil { + return nil, errors.Annotatef(err, "txid %v", txid) + } + confirmations, err := b.computeConfirmations(uint64(n)) + if err != nil { + return nil, errors.Annotatef(err, "txid %v", txid) + } + btx, err = b.Parser.ethTxToTx(tx, &receipt, time, confirmations, true) + if err != nil { + return nil, errors.Annotatef(err, "txid %v", txid) + } + // remove tx from mempool if it is there + if b.mempoolInitialized { + b.Mempool.RemoveTransactionFromMempool(txid) + } + } + return btx, nil +} + +// GetTransactionSpecific returns json as returned by backend, with all coin specific data +func (b *EnergiRPC) GetTransactionSpecific(tx *bchain.Tx) (json.RawMessage, error) { + csd, ok := tx.CoinSpecificData.(completeTransaction) + if !ok { + ntx, err := b.GetTransaction(tx.Txid) + if err != nil { + return nil, err + } + csd, ok = ntx.CoinSpecificData.(completeTransaction) + if !ok { + return nil, errors.New("Cannot get CoinSpecificData") + } + } + m, err := json.Marshal(&csd) + return json.RawMessage(m), err +} + +// GetMempoolTransactions returns transactions in mempool +func (b *EnergiRPC) GetMempoolTransactions() ([]string, error) { + raw, err := b.getBlockRaw("pending", 0, false) + if err != nil { + return nil, err + } + var body rpcBlockTxids + if len(raw) > 0 { + if err := json.Unmarshal(raw, &body); err != nil { + return nil, err + } + } + return body.Transactions, nil +} + +// EstimateFee returns fee estimation +func (b *EnergiRPC) EstimateFee(blocks int) (big.Int, error) { + return b.EstimateSmartFee(blocks, true) +} + +// EstimateSmartFee returns fee estimation +func (b *EnergiRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + var r big.Int + gp, err := b.client.SuggestGasPrice(ctx) + if err == nil && b != nil { + r = *gp + } + return r, err +} + +func getStringFromMap(p string, params map[string]interface{}) (string, bool) { + v, ok := params[p] + if ok { + s, ok := v.(string) + return s, ok + } + return "", false +} + +// EthereumTypeEstimateGas returns estimation of gas consumption for given transaction parameters +func (b *EnergiRPC) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + msg := ethereum.CallMsg{} + s, ok := getStringFromMap("from", params) + if ok && len(s) > 0 { + msg.From = ethcommon.HexToAddress(s) + } + s, ok = getStringFromMap("to", params) + if ok && len(s) > 0 { + a := ethcommon.HexToAddress(s) + msg.To = &a + } + s, ok = getStringFromMap("data", params) + if ok && len(s) > 0 { + msg.Data = ethcommon.FromHex(s) + } + s, ok = getStringFromMap("value", params) + if ok && len(s) > 0 { + msg.Value, _ = hexutil.DecodeBig(s) + } + s, ok = getStringFromMap("gas", params) + if ok && len(s) > 0 { + msg.Gas, _ = hexutil.DecodeUint64(s) + } + s, ok = getStringFromMap("gasPrice", params) + if ok && len(s) > 0 { + msg.GasPrice, _ = hexutil.DecodeBig(s) + } + return b.client.EstimateGas(ctx, msg) +} + +// SendRawTransaction sends raw transaction +func (b *EnergiRPC) SendRawTransaction(hex string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + var raw json.RawMessage + err := b.rpc.CallContext(ctx, &raw, "eth_sendRawTransaction", hex) + if err != nil { + return "", err + } else if len(raw) == 0 { + return "", errors.New("SendRawTransaction: failed") + } + var result string + if err := json.Unmarshal(raw, &result); err != nil { + return "", errors.Annotatef(err, "raw result %v", raw) + } + if result == "" { + return "", errors.New("SendRawTransaction: failed, empty result") + } + return result, nil +} + +// EthereumTypeGetBalance returns current balance of an address +func (b *EnergiRPC) EthereumTypeGetBalance(addrDesc bchain.AddressDescriptor) (*big.Int, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + return b.client.BalanceAt(ctx, ethcommon.BytesToAddress(addrDesc), nil) +} + +// EthereumTypeGetNonce returns current balance of an address +func (b *EnergiRPC) EthereumTypeGetNonce(addrDesc bchain.AddressDescriptor) (uint64, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + return b.client.NonceAt(ctx, ethcommon.BytesToAddress(addrDesc), nil) +} + +// GetChainParser returns ethereum BlockChainParser +func (b *EnergiRPC) GetChainParser() bchain.BlockChainParser { + return b.Parser +} + +// GetChainParser returns ethereum block header by number. +func (b *EnergiRPC) headerByNumber(ctx context.Context, number *big.Int) (*header, error) { + var head header + err := b.rpc.CallContext(ctx, &head, "eth_getBlockByNumber", toBlockNumArg(number), false) + if err != nil { + return nil, err + } else if len(head.Hash) == 0 { + return nil, bchain.ErrBlockNotFound + } + return &head, nil +} diff --git a/bchain/coins/energi/ethtx.pb.go b/bchain/coins/energi/ethtx.pb.go new file mode 100644 index 00000000..08789ebf --- /dev/null +++ b/bchain/coins/energi/ethtx.pb.go @@ -0,0 +1,261 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: bchain/coins/energi/ethtx.proto + +/* +Package energi is a generated protocol buffer package. + +It is generated from these files: + bchain/coins/energi/ethtx.proto + +It has these top-level messages: + ProtoCompleteTransaction +*/ +package energi + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type ProtoCompleteTransaction struct { + BlockNumber uint32 `protobuf:"varint,1,opt,name=BlockNumber" json:"BlockNumber,omitempty"` + BlockTime uint64 `protobuf:"varint,2,opt,name=BlockTime" json:"BlockTime,omitempty"` + Tx *ProtoCompleteTransaction_TxType `protobuf:"bytes,3,opt,name=Tx" json:"Tx,omitempty"` + Receipt *ProtoCompleteTransaction_ReceiptType `protobuf:"bytes,4,opt,name=Receipt" json:"Receipt,omitempty"` +} + +func (m *ProtoCompleteTransaction) Reset() { *m = ProtoCompleteTransaction{} } +func (m *ProtoCompleteTransaction) String() string { return proto.CompactTextString(m) } +func (*ProtoCompleteTransaction) ProtoMessage() {} +func (*ProtoCompleteTransaction) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *ProtoCompleteTransaction) GetBlockNumber() uint32 { + if m != nil { + return m.BlockNumber + } + return 0 +} + +func (m *ProtoCompleteTransaction) GetBlockTime() uint64 { + if m != nil { + return m.BlockTime + } + return 0 +} + +func (m *ProtoCompleteTransaction) GetTx() *ProtoCompleteTransaction_TxType { + if m != nil { + return m.Tx + } + return nil +} + +func (m *ProtoCompleteTransaction) GetReceipt() *ProtoCompleteTransaction_ReceiptType { + if m != nil { + return m.Receipt + } + return nil +} + +type ProtoCompleteTransaction_TxType struct { + AccountNonce uint64 `protobuf:"varint,1,opt,name=AccountNonce" json:"AccountNonce,omitempty"` + GasPrice []byte `protobuf:"bytes,2,opt,name=GasPrice,proto3" json:"GasPrice,omitempty"` + GasLimit uint64 `protobuf:"varint,3,opt,name=GasLimit" json:"GasLimit,omitempty"` + Value []byte `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"` + Payload []byte `protobuf:"bytes,5,opt,name=Payload,proto3" json:"Payload,omitempty"` + Hash []byte `protobuf:"bytes,6,opt,name=Hash,proto3" json:"Hash,omitempty"` + To []byte `protobuf:"bytes,7,opt,name=To,proto3" json:"To,omitempty"` + From []byte `protobuf:"bytes,8,opt,name=From,proto3" json:"From,omitempty"` + TransactionIndex uint32 `protobuf:"varint,9,opt,name=TransactionIndex" json:"TransactionIndex,omitempty"` +} + +func (m *ProtoCompleteTransaction_TxType) Reset() { *m = ProtoCompleteTransaction_TxType{} } +func (m *ProtoCompleteTransaction_TxType) String() string { return proto.CompactTextString(m) } +func (*ProtoCompleteTransaction_TxType) ProtoMessage() {} +func (*ProtoCompleteTransaction_TxType) Descriptor() ([]byte, []int) { + return fileDescriptor0, []int{0, 0} +} + +func (m *ProtoCompleteTransaction_TxType) GetAccountNonce() uint64 { + if m != nil { + return m.AccountNonce + } + return 0 +} + +func (m *ProtoCompleteTransaction_TxType) GetGasPrice() []byte { + if m != nil { + return m.GasPrice + } + return nil +} + +func (m *ProtoCompleteTransaction_TxType) GetGasLimit() uint64 { + if m != nil { + return m.GasLimit + } + return 0 +} + +func (m *ProtoCompleteTransaction_TxType) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func (m *ProtoCompleteTransaction_TxType) GetPayload() []byte { + if m != nil { + return m.Payload + } + return nil +} + +func (m *ProtoCompleteTransaction_TxType) GetHash() []byte { + if m != nil { + return m.Hash + } + return nil +} + +func (m *ProtoCompleteTransaction_TxType) GetTo() []byte { + if m != nil { + return m.To + } + return nil +} + +func (m *ProtoCompleteTransaction_TxType) GetFrom() []byte { + if m != nil { + return m.From + } + return nil +} + +func (m *ProtoCompleteTransaction_TxType) GetTransactionIndex() uint32 { + if m != nil { + return m.TransactionIndex + } + return 0 +} + +type ProtoCompleteTransaction_ReceiptType struct { + GasUsed []byte `protobuf:"bytes,1,opt,name=GasUsed,proto3" json:"GasUsed,omitempty"` + Status []byte `protobuf:"bytes,2,opt,name=Status,proto3" json:"Status,omitempty"` + Log []*ProtoCompleteTransaction_ReceiptType_LogType `protobuf:"bytes,3,rep,name=Log" json:"Log,omitempty"` +} + +func (m *ProtoCompleteTransaction_ReceiptType) Reset() { *m = ProtoCompleteTransaction_ReceiptType{} } +func (m *ProtoCompleteTransaction_ReceiptType) String() string { return proto.CompactTextString(m) } +func (*ProtoCompleteTransaction_ReceiptType) ProtoMessage() {} +func (*ProtoCompleteTransaction_ReceiptType) Descriptor() ([]byte, []int) { + return fileDescriptor0, []int{0, 1} +} + +func (m *ProtoCompleteTransaction_ReceiptType) GetGasUsed() []byte { + if m != nil { + return m.GasUsed + } + return nil +} + +func (m *ProtoCompleteTransaction_ReceiptType) GetStatus() []byte { + if m != nil { + return m.Status + } + return nil +} + +func (m *ProtoCompleteTransaction_ReceiptType) GetLog() []*ProtoCompleteTransaction_ReceiptType_LogType { + if m != nil { + return m.Log + } + return nil +} + +type ProtoCompleteTransaction_ReceiptType_LogType struct { + Address []byte `protobuf:"bytes,1,opt,name=Address,proto3" json:"Address,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"` + Topics [][]byte `protobuf:"bytes,3,rep,name=Topics,proto3" json:"Topics,omitempty"` +} + +func (m *ProtoCompleteTransaction_ReceiptType_LogType) Reset() { + *m = ProtoCompleteTransaction_ReceiptType_LogType{} +} +func (m *ProtoCompleteTransaction_ReceiptType_LogType) String() string { + return proto.CompactTextString(m) +} +func (*ProtoCompleteTransaction_ReceiptType_LogType) ProtoMessage() {} +func (*ProtoCompleteTransaction_ReceiptType_LogType) Descriptor() ([]byte, []int) { + return fileDescriptor0, []int{0, 1, 0} +} + +func (m *ProtoCompleteTransaction_ReceiptType_LogType) GetAddress() []byte { + if m != nil { + return m.Address + } + return nil +} + +func (m *ProtoCompleteTransaction_ReceiptType_LogType) GetData() []byte { + if m != nil { + return m.Data + } + return nil +} + +func (m *ProtoCompleteTransaction_ReceiptType_LogType) GetTopics() [][]byte { + if m != nil { + return m.Topics + } + return nil +} + +func init() { + proto.RegisterType((*ProtoCompleteTransaction)(nil), "eth.ProtoCompleteTransaction") + proto.RegisterType((*ProtoCompleteTransaction_TxType)(nil), "eth.ProtoCompleteTransaction.TxType") + proto.RegisterType((*ProtoCompleteTransaction_ReceiptType)(nil), "eth.ProtoCompleteTransaction.ReceiptType") + proto.RegisterType((*ProtoCompleteTransaction_ReceiptType_LogType)(nil), "eth.ProtoCompleteTransaction.ReceiptType.LogType") +} + +func init() { proto.RegisterFile("bchain/coins/energi/ethtx.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 409 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0xdf, 0x8a, 0xd4, 0x30, + 0x18, 0xc5, 0xe9, 0x9f, 0x99, 0xd9, 0xfd, 0xa6, 0x8a, 0x04, 0x91, 0x30, 0xec, 0x45, 0x59, 0xbc, + 0x18, 0xbd, 0xe8, 0xe2, 0xea, 0x0b, 0xac, 0x23, 0xae, 0xc2, 0xb0, 0x0e, 0x31, 0x7a, 0x9f, 0x49, + 0xc3, 0x36, 0x38, 0x6d, 0x4a, 0x93, 0x42, 0xf7, 0x8d, 0x7c, 0x21, 0xdf, 0xc5, 0x4b, 0xc9, 0xd7, + 0x74, 0x1d, 0x11, 0x65, 0x2f, 0x0a, 0xf9, 0x9d, 0x7e, 0xa7, 0x39, 0x27, 0x29, 0x9c, 0xed, 0x65, + 0x25, 0x74, 0x73, 0x21, 0x8d, 0x6e, 0xec, 0x85, 0x72, 0x95, 0x7f, 0xdc, 0x50, 0xb4, 0x9d, 0x71, + 0x86, 0x24, 0xca, 0x55, 0xe7, 0xdf, 0x67, 0x40, 0x77, 0x1e, 0x37, 0xa6, 0x6e, 0x0f, 0xca, 0x29, + 0xde, 0x89, 0xc6, 0x0a, 0xe9, 0xb4, 0x69, 0x48, 0x0e, 0xcb, 0xb7, 0x07, 0x23, 0xbf, 0xdd, 0xf4, + 0xf5, 0x5e, 0x75, 0x34, 0xca, 0xa3, 0xf5, 0x23, 0x76, 0x2c, 0x91, 0x33, 0x38, 0x45, 0xe4, 0xba, + 0x56, 0x34, 0xce, 0xa3, 0x75, 0xca, 0x7e, 0x0b, 0xe4, 0x0d, 0xc4, 0x7c, 0xa0, 0x49, 0x1e, 0xad, + 0x97, 0x97, 0xcf, 0x0b, 0xe5, 0xaa, 0xe2, 0x5f, 0x5b, 0x15, 0x7c, 0xe0, 0x77, 0xad, 0x62, 0x31, + 0x1f, 0xc8, 0x06, 0x16, 0x4c, 0x49, 0xa5, 0x5b, 0x47, 0x53, 0xb4, 0xbe, 0xf8, 0xbf, 0x35, 0x0c, + 0xa3, 0x7f, 0x72, 0xae, 0x7e, 0x46, 0x30, 0x1f, 0xbf, 0x49, 0xce, 0x21, 0xbb, 0x92, 0xd2, 0xf4, + 0x8d, 0xbb, 0x31, 0x8d, 0x54, 0x58, 0x23, 0x65, 0x7f, 0x68, 0x64, 0x05, 0x27, 0xd7, 0xc2, 0xee, + 0x3a, 0x2d, 0xc7, 0x1a, 0x19, 0xbb, 0xe7, 0xf0, 0x6e, 0xab, 0x6b, 0xed, 0xb0, 0x4b, 0xca, 0xee, + 0x99, 0x3c, 0x85, 0xd9, 0x57, 0x71, 0xe8, 0x15, 0x26, 0xcd, 0xd8, 0x08, 0x84, 0xc2, 0x62, 0x27, + 0xee, 0x0e, 0x46, 0x94, 0x74, 0x86, 0xfa, 0x84, 0x84, 0x40, 0xfa, 0x41, 0xd8, 0x8a, 0xce, 0x51, + 0xc6, 0x35, 0x79, 0x0c, 0x31, 0x37, 0x74, 0x81, 0x4a, 0xcc, 0x8d, 0x9f, 0x79, 0xdf, 0x99, 0x9a, + 0x9e, 0x8c, 0x33, 0x7e, 0x4d, 0x5e, 0xc2, 0x93, 0xa3, 0xca, 0x1f, 0x9b, 0x52, 0x0d, 0xf4, 0x14, + 0xaf, 0xe3, 0x2f, 0x7d, 0xf5, 0x23, 0x82, 0xe5, 0xd1, 0x99, 0xf8, 0x34, 0xd7, 0xc2, 0x7e, 0xb1, + 0xaa, 0xc4, 0xea, 0x19, 0x9b, 0x90, 0x3c, 0x83, 0xf9, 0x67, 0x27, 0x5c, 0x6f, 0x43, 0xe7, 0x40, + 0x64, 0x03, 0xc9, 0xd6, 0xdc, 0xd2, 0x24, 0x4f, 0xd6, 0xcb, 0xcb, 0x57, 0x0f, 0x3e, 0xfd, 0x62, + 0x6b, 0x6e, 0xf1, 0x16, 0xbc, 0x7b, 0xf5, 0x09, 0x16, 0x81, 0x7d, 0x82, 0xab, 0xb2, 0xec, 0x94, + 0xb5, 0x53, 0x82, 0x80, 0xbe, 0xeb, 0x3b, 0xe1, 0x44, 0xd8, 0x1f, 0xd7, 0x3e, 0x15, 0x37, 0xad, + 0x96, 0x16, 0x03, 0x64, 0x2c, 0xd0, 0x7e, 0x8e, 0xbf, 0xed, 0xeb, 0x5f, 0x01, 0x00, 0x00, 0xff, + 0xff, 0xc2, 0x69, 0x8d, 0xdf, 0xd6, 0x02, 0x00, 0x00, +} diff --git a/bchain/coins/energi/ethtx.proto b/bchain/coins/energi/ethtx.proto new file mode 100644 index 00000000..ac65da00 --- /dev/null +++ b/bchain/coins/energi/ethtx.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + package energi; + + message ProtoCompleteTransaction { + message TxType { + uint64 AccountNonce = 1; + bytes GasPrice = 2; + uint64 GasLimit = 3; + bytes Value = 4; + bytes Payload = 5; + bytes Hash = 6; + bytes To = 7; + bytes From = 8; + uint32 TransactionIndex = 9; + } + message ReceiptType { + message LogType { + bytes Address = 1; + bytes Data = 2; + repeated bytes Topics = 3; + } + bytes GasUsed = 1; + bytes Status = 2; + repeated LogType Log = 3; + } + uint32 BlockNumber = 1; + uint64 BlockTime = 2; + TxType Tx = 3; + ReceiptType Receipt = 4; + } \ No newline at end of file diff --git a/configs/coins/energi.json b/configs/coins/energi.json new file mode 100644 index 00000000..c9dc7522 --- /dev/null +++ b/configs/coins/energi.json @@ -0,0 +1,65 @@ +{ + "coin": { + "name": "Energi", + "shortcut": "NRG", + "label": "Energi", + "alias": "energi" + }, + "ports": { + "backend_rpc": 8097, + "backend_message_queue": 0, + "backend_p2p": 38397, + "backend_http": 8197, + "blockbook_internal": 9097, + "blockbook_public": 9197 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-energi", + "package_revision": "Pantani", + "system_user": "energi", + "version": "3.0.7", + "binary_url": "https://s3-us-west-2.amazonaws.com/download.energi.software/releases/energi3/3.0.7/energi3-3.0.7-linux-amd64.tgz", + "verification_type": "sha256", + "verification_source": "6f6e0fe03f9cd38fe29addffb42ae4217656a7a9ee9be3a824f104b6a4ac7237", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/energi3 --ipcdisable --syncmode full --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port 38397 --ws --wsapi eth,net,web3 --wsaddr 0.0.0.0 --wsport {{.Ports.BackendRPC}} --wsorigins \"*\" --rpc --rpcapi personal,eth,net,web3b --rpcport 8197 -rpcaddr 0.0.0.0 --rpccorsdomain \"*\" --rpcvhosts \"*\" 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-energi", + "system_user": "blockbook-energi", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "slip44": 9797, + "additional_params": { + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"energi\", \"periodSeconds\": 60}" + } + } + }, + "meta": { + "package_maintainer": "Pantani", + "package_maintainer_email": "danpantani@gmail.com" + } +} \ No newline at end of file diff --git a/docs/ports.md b/docs/ports.md index d38a9f03..ec6cdacd 100644 --- a/docs/ports.md +++ b/docs/ports.md @@ -8,7 +8,7 @@ | Dash | 9033 | 9133 | 8033 | 38333 | | Litecoin | 9034 | 9134 | 8034 | 38334 | | Bitcoin Gold | 9035 | 9135 | 8035 | 38335 | -| Ethereum | 9036 | 9136 | 8036 | 38336 p2p, 8136 http | +| Ethereum | 9036 | 9136 | 8036 | 8136 http, 38336 p2p | | Ethereum Classic | 9037 | 9137 | 8037 | | | Dogecoin | 9038 | 9138 | 8038 | 38338 | | Namecoin | 9039 | 9139 | 8039 | 38339 | @@ -45,6 +45,7 @@ | Omotenashicoin | 9094 | 9194 | 8094 | 38394 | | BitZeny | 9095 | 9195 | 8095 | 38395 | | Trezarcoin | 9096 | 9196 | 8096 | 38396 | +| Energi | 9097 | 9197 | 8097 | 38397 p2p, 8197 http | | Bitcoin Signet | 19020 | 19120 | 18020 | 48320 | | Ethereum Goerli | 19026 | 19126 | 18026 | 48326 p2p | | Bitcoin Testnet | 19030 | 19130 | 18030 | 48330 |