243 lines
9.3 KiB
Go
243 lines
9.3 KiB
Go
package eth
|
|
|
|
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"
|
|
"spacecruft.org/spacecruft/blockbook/bchain"
|
|
)
|
|
|
|
var erc20abi = `[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x06fdde03"},
|
|
{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x95d89b41"},
|
|
{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function","signature":"0x313ce567"},
|
|
{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function","signature":"0x18160ddd"},
|
|
{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function","signature":"0x70a08231"},
|
|
{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0xa9059cbb"},
|
|
{"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"},
|
|
{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0x095ea7b3"},
|
|
{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function","signature":"0xdd62ed3e"},
|
|
{"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"},
|
|
{"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"},
|
|
{"inputs":[{"name":"_initialAmount","type":"uint256"},{"name":"_tokenName","type":"string"},{"name":"_decimalUnits","type":"uint8"},{"name":"_tokenSymbol","type":"string"}],"payable":false,"type":"constructor"},
|
|
{"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"},
|
|
{"constant":true,"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x54fd4d50"}]`
|
|
|
|
// 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 *EthereumRPC) 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 *EthereumRPC) 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 *EthereumRPC) 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
|
|
}
|