github.com/cerberus-wallet/blockbook@v0.3.2/bchain/coins/eth/erc20.go (about)

     1  package eth
     2  
     3  import (
     4  	"blockbook/bchain"
     5  	"bytes"
     6  	"context"
     7  	"encoding/hex"
     8  	"math/big"
     9  	"strings"
    10  	"sync"
    11  	"unicode/utf8"
    12  
    13  	ethcommon "github.com/ethereum/go-ethereum/common"
    14  	"github.com/golang/glog"
    15  	"github.com/juju/errors"
    16  )
    17  
    18  var erc20abi = `[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x06fdde03"},
    19  {"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x95d89b41"},
    20  {"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function","signature":"0x313ce567"},
    21  {"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function","signature":"0x18160ddd"},
    22  {"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function","signature":"0x70a08231"},
    23  {"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0xa9059cbb"},
    24  {"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0x23b872dd"},
    25  {"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0x095ea7b3"},
    26  {"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function","signature":"0xdd62ed3e"},
    27  {"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","signature":"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"},
    28  {"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","signature":"0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"},
    29  {"inputs":[{"name":"_initialAmount","type":"uint256"},{"name":"_tokenName","type":"string"},{"name":"_decimalUnits","type":"uint8"},{"name":"_tokenSymbol","type":"string"}],"payable":false,"type":"constructor"},
    30  {"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_extraData","type":"bytes"}],"name":"approveAndCall","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0xcae9ca51"},
    31  {"constant":true,"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x54fd4d50"}]`
    32  
    33  // doing the parsing/processing without using go-ethereum/accounts/abi library, it is simple to get data from Transfer event
    34  const erc20TransferMethodSignature = "0xa9059cbb"
    35  const erc20TransferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
    36  const erc20NameSignature = "0x06fdde03"
    37  const erc20SymbolSignature = "0x95d89b41"
    38  const erc20DecimalsSignature = "0x313ce567"
    39  const erc20BalanceOf = "0x70a08231"
    40  
    41  var cachedContracts = make(map[string]*bchain.Erc20Contract)
    42  var cachedContractsMux sync.Mutex
    43  
    44  func addressFromPaddedHex(s string) (string, error) {
    45  	var t big.Int
    46  	var ok bool
    47  	if has0xPrefix(s) {
    48  		_, ok = t.SetString(s[2:], 16)
    49  	} else {
    50  		_, ok = t.SetString(s, 16)
    51  	}
    52  	if !ok {
    53  		return "", errors.New("Data is not a number")
    54  	}
    55  	a := ethcommon.BigToAddress(&t)
    56  	return a.String(), nil
    57  }
    58  
    59  func erc20GetTransfersFromLog(logs []*rpcLog) ([]bchain.Erc20Transfer, error) {
    60  	var r []bchain.Erc20Transfer
    61  	for _, l := range logs {
    62  		if len(l.Topics) == 3 && l.Topics[0] == erc20TransferEventSignature {
    63  			var t big.Int
    64  			_, ok := t.SetString(l.Data, 0)
    65  			if !ok {
    66  				return nil, errors.New("Data is not a number")
    67  			}
    68  			from, err := addressFromPaddedHex(l.Topics[1])
    69  			if err != nil {
    70  				return nil, err
    71  			}
    72  			to, err := addressFromPaddedHex(l.Topics[2])
    73  			if err != nil {
    74  				return nil, err
    75  			}
    76  			r = append(r, bchain.Erc20Transfer{
    77  				Contract: EIP55AddressFromAddress(l.Address),
    78  				From:     EIP55AddressFromAddress(from),
    79  				To:       EIP55AddressFromAddress(to),
    80  				Tokens:   t,
    81  			})
    82  		}
    83  	}
    84  	return r, nil
    85  }
    86  
    87  func erc20GetTransfersFromTx(tx *rpcTransaction) ([]bchain.Erc20Transfer, error) {
    88  	var r []bchain.Erc20Transfer
    89  	if len(tx.Payload) == 128+len(erc20TransferMethodSignature) && strings.HasPrefix(tx.Payload, erc20TransferMethodSignature) {
    90  		to, err := addressFromPaddedHex(tx.Payload[len(erc20TransferMethodSignature) : 64+len(erc20TransferMethodSignature)])
    91  		if err != nil {
    92  			return nil, err
    93  		}
    94  		var t big.Int
    95  		_, ok := t.SetString(tx.Payload[len(erc20TransferMethodSignature)+64:], 16)
    96  		if !ok {
    97  			return nil, errors.New("Data is not a number")
    98  		}
    99  		r = append(r, bchain.Erc20Transfer{
   100  			Contract: EIP55AddressFromAddress(tx.To),
   101  			From:     EIP55AddressFromAddress(tx.From),
   102  			To:       EIP55AddressFromAddress(to),
   103  			Tokens:   t,
   104  		})
   105  	}
   106  	return r, nil
   107  }
   108  
   109  func (b *EthereumRPC) ethCall(data, to string) (string, error) {
   110  	ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
   111  	defer cancel()
   112  	var r string
   113  	err := b.rpc.CallContext(ctx, &r, "eth_call", map[string]interface{}{
   114  		"data": data,
   115  		"to":   to,
   116  	}, "latest")
   117  	if err != nil {
   118  		return "", err
   119  	}
   120  	return r, nil
   121  }
   122  
   123  func parseErc20NumericProperty(contractDesc bchain.AddressDescriptor, data string) *big.Int {
   124  	if has0xPrefix(data) {
   125  		data = data[2:]
   126  	}
   127  	if len(data) == 64 {
   128  		var n big.Int
   129  		_, ok := n.SetString(data, 16)
   130  		if ok {
   131  			return &n
   132  		}
   133  	}
   134  	if glog.V(1) {
   135  		glog.Warning("Cannot parse '", data, "' for contract ", contractDesc)
   136  	}
   137  	return nil
   138  }
   139  
   140  func parseErc20StringProperty(contractDesc bchain.AddressDescriptor, data string) string {
   141  	if has0xPrefix(data) {
   142  		data = data[2:]
   143  	}
   144  	if len(data) > 128 {
   145  		n := parseErc20NumericProperty(contractDesc, data[64:128])
   146  		if n != nil {
   147  			l := n.Uint64()
   148  			if 2*int(l) <= len(data)-128 {
   149  				b, err := hex.DecodeString(data[128 : 128+2*l])
   150  				if err == nil {
   151  					return string(b)
   152  				}
   153  			}
   154  		}
   155  	} else if len(data) == 64 {
   156  		// allow string properties as 32 bytes of UTF-8 data
   157  		b, err := hex.DecodeString(data)
   158  		if err == nil {
   159  			i := bytes.Index(b, []byte{0})
   160  			if i > 0 {
   161  				b = b[:i]
   162  			}
   163  			if utf8.Valid(b) {
   164  				return string(b)
   165  			}
   166  		}
   167  	}
   168  	if glog.V(1) {
   169  		glog.Warning("Cannot parse '", data, "' for contract ", contractDesc)
   170  	}
   171  	return ""
   172  }
   173  
   174  // EthereumTypeGetErc20ContractInfo returns information about ERC20 contract
   175  func (b *EthereumRPC) EthereumTypeGetErc20ContractInfo(contractDesc bchain.AddressDescriptor) (*bchain.Erc20Contract, error) {
   176  	cds := string(contractDesc)
   177  	cachedContractsMux.Lock()
   178  	contract, found := cachedContracts[cds]
   179  	cachedContractsMux.Unlock()
   180  	if !found {
   181  		address := EIP55Address(contractDesc)
   182  		data, err := b.ethCall(erc20NameSignature, address)
   183  		if err != nil {
   184  			return nil, err
   185  		}
   186  		name := parseErc20StringProperty(contractDesc, data)
   187  		if name != "" {
   188  			data, err = b.ethCall(erc20SymbolSignature, address)
   189  			if err != nil {
   190  				return nil, err
   191  			}
   192  			symbol := parseErc20StringProperty(contractDesc, data)
   193  			data, err = b.ethCall(erc20DecimalsSignature, address)
   194  			if err != nil {
   195  				return nil, err
   196  			}
   197  			contract = &bchain.Erc20Contract{
   198  				Contract: address,
   199  				Name:     name,
   200  				Symbol:   symbol,
   201  			}
   202  			d := parseErc20NumericProperty(contractDesc, data)
   203  			if d != nil {
   204  				contract.Decimals = int(uint8(d.Uint64()))
   205  			} else {
   206  				contract.Decimals = EtherAmountDecimalPoint
   207  			}
   208  		} else {
   209  			contract = nil
   210  		}
   211  		cachedContractsMux.Lock()
   212  		cachedContracts[cds] = contract
   213  		cachedContractsMux.Unlock()
   214  	}
   215  	return contract, nil
   216  }
   217  
   218  // EthereumTypeGetErc20ContractBalance returns balance of ERC20 contract for given address
   219  func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) {
   220  	addr := EIP55Address(addrDesc)
   221  	contract := EIP55Address(contractDesc)
   222  	req := erc20BalanceOf + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr)-2:] + addr[2:]
   223  	data, err := b.ethCall(req, contract)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	r := parseErc20NumericProperty(contractDesc, data)
   228  	if r == nil {
   229  		return nil, errors.New("Invalid balance")
   230  	}
   231  	return r, nil
   232  }