github.com/klaytn/klaytn@v1.10.2/datasync/chaindatafetcher/kas/contract_caller.go (about)

     1  // Copyright 2020 The klaytn Authors
     2  // This file is part of the klaytn library.
     3  //
     4  // The klaytn library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The klaytn library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the klaytn library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package kas
    18  
    19  import (
    20  	"context"
    21  	"math/big"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/klaytn/klaytn/blockchain"
    26  	"github.com/klaytn/klaytn/blockchain/vm"
    27  
    28  	"github.com/klaytn/klaytn"
    29  	"github.com/klaytn/klaytn/accounts/abi/bind"
    30  	"github.com/klaytn/klaytn/api"
    31  	"github.com/klaytn/klaytn/common"
    32  	"github.com/klaytn/klaytn/common/hexutil"
    33  	"github.com/klaytn/klaytn/contracts/kip13"
    34  	"github.com/klaytn/klaytn/networks/rpc"
    35  )
    36  
    37  // TODO-ChainDataFetcher extract the call timeout c as a configuration
    38  const callTimeout = 300 * time.Millisecond
    39  
    40  var (
    41  	// KIP 13: Interface Query Standard - https://kips.klaytn.com/KIPs/kip-13
    42  	IKIP13Id  = [4]byte{0x01, 0xff, 0xc9, 0xa7}
    43  	InvalidId = [4]byte{0xff, 0xff, 0xff, 0xff}
    44  
    45  	// KIP 7: Fungible Token Standard - https://kips.klaytn.com/KIPs/kip-7
    46  	IKIP7Id         = [4]byte{0x65, 0x78, 0x73, 0x71}
    47  	IKIP7MetadataId = [4]byte{0xa2, 0x19, 0xa0, 0x25}
    48  
    49  	// KIP 17: Non-fungible Token Standard - https://kips.klaytn.com/KIPs/kip-17
    50  	IKIP17Id         = [4]byte{0x80, 0xac, 0x58, 0xcd}
    51  	IKIP17MetadataId = [4]byte{0x5b, 0x5e, 0x13, 0x9f}
    52  
    53  	errMsgEmptyOutput = "abi: unmarshalling empty output"
    54  )
    55  
    56  //go:generate mockgen -destination=./mocks/blockchain_api_mock.go -package=mocks github.com/klaytn/klaytn/datasync/chaindatafetcher/kas BlockchainAPI
    57  // BlockchainAPI interface is for testing purpose.
    58  type BlockchainAPI interface {
    59  	GetCode(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error)
    60  	Call(ctx context.Context, args api.CallArgs, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error)
    61  }
    62  
    63  // contractCaller performs kip13 method `supportsInterface` to detect the deployed contracts are KIP7 or KIP17.
    64  type contractCaller struct {
    65  	blockchainAPI BlockchainAPI
    66  	callTimeout   time.Duration
    67  }
    68  
    69  func newContractCaller(api BlockchainAPI) *contractCaller {
    70  	return &contractCaller{blockchainAPI: api, callTimeout: callTimeout}
    71  }
    72  
    73  func (f *contractCaller) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) {
    74  	num := rpc.LatestBlockNumber
    75  	if blockNumber != nil {
    76  		num = rpc.BlockNumber(blockNumber.Int64())
    77  	}
    78  	return f.blockchainAPI.GetCode(ctx, contract, rpc.NewBlockNumberOrHashWithNumber(num))
    79  }
    80  
    81  func (f *contractCaller) CallContract(ctx context.Context, call klaytn.CallMsg, blockNumber *big.Int) ([]byte, error) {
    82  	num := rpc.LatestBlockNumber
    83  	if blockNumber != nil {
    84  		num = rpc.BlockNumber(blockNumber.Int64())
    85  	}
    86  	callArgs := api.CallArgs{
    87  		From: call.From,
    88  		To:   call.To,
    89  		Data: hexutil.Bytes(call.Data),
    90  	}
    91  	return f.blockchainAPI.Call(ctx, callArgs, rpc.NewBlockNumberOrHashWithNumber(num))
    92  }
    93  
    94  func getCallOpts(blockNumber *big.Int, timeout time.Duration) (*bind.CallOpts, context.CancelFunc) {
    95  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
    96  	return &bind.CallOpts{Context: ctx, BlockNumber: blockNumber}, cancel
    97  }
    98  
    99  // supportsInterface returns true if the given interfaceID is supported, otherwise returns false.
   100  func (f *contractCaller) supportsInterface(contract common.Address, opts *bind.CallOpts, interfaceID [4]byte) (bool, error) {
   101  	caller, err := kip13.NewInterfaceIdentifierCaller(contract, f)
   102  	if err != nil {
   103  		logger.Error("NewInterfaceIdentifierCaller is failed", "contract", contract.String(), "interfaceID", hexutil.Encode(interfaceID[:]))
   104  		return false, err
   105  	}
   106  	isSupported, err := caller.SupportsInterface(opts, interfaceID)
   107  	if err != nil {
   108  		// removed handle error case for SupportsInterface contract call.
   109  		// 1. The error cases are too many to be handled.
   110  		// 2. There is no case to return an error (e.g. network error, ...) other than internal errors.
   111  		if !strings.Contains(err.Error(), errMsgEmptyOutput) && err != vm.ErrExecutionReverted && err != bind.ErrNoCode && err != blockchain.ErrVMDefault {
   112  			logger.Warn("supports interface returns an abnormal error", "err", err, "contract", contract.String(), "interfaceID", hexutil.Encode(interfaceID[:]))
   113  		}
   114  		return false, nil
   115  	}
   116  	return isSupported, nil
   117  }
   118  
   119  // isKIP13 checks if the given contract implements KIP13 interface or not at the given block.
   120  func (f *contractCaller) isKIP13(contract common.Address, blockNumber *big.Int) (bool, error) {
   121  	var (
   122  		opts   *bind.CallOpts
   123  		cancel context.CancelFunc
   124  	)
   125  	opts, cancel = getCallOpts(blockNumber, f.callTimeout)
   126  	defer cancel()
   127  	if isKIP13, err := f.supportsInterface(contract, opts, IKIP13Id); err != nil {
   128  		logger.Error("supportsInterface is failed", "contract", contract.String(), "blockNumber", blockNumber, "interfaceID", hexutil.Encode(IKIP13Id[:]))
   129  		return false, err
   130  	} else if !isKIP13 {
   131  		return false, nil
   132  	}
   133  
   134  	opts, cancel = getCallOpts(blockNumber, f.callTimeout)
   135  	defer cancel()
   136  	if isInvalid, err := f.supportsInterface(contract, opts, InvalidId); err != nil {
   137  		logger.Error("supportsInterface is failed", "contract", contract.String(), "blockNumber", blockNumber, "interfaceID", hexutil.Encode(InvalidId[:]))
   138  		return false, err
   139  	} else if isInvalid {
   140  		return false, nil
   141  	}
   142  
   143  	return true, nil
   144  }
   145  
   146  // isKIP7 checks if the given contract implements IKIP7 and IKIP7Metadata interface or not at the given block.
   147  func (f *contractCaller) isKIP7(contract common.Address, blockNumber *big.Int) (bool, error) {
   148  	var (
   149  		opts   *bind.CallOpts
   150  		cancel context.CancelFunc
   151  	)
   152  	opts, cancel = getCallOpts(blockNumber, f.callTimeout)
   153  	defer cancel()
   154  	if isIKIP7, err := f.supportsInterface(contract, opts, IKIP7Id); err != nil {
   155  		logger.Error("supportsInterface is failed", "contract", contract.String(), "blockNumber", blockNumber, "interfaceID", hexutil.Encode(IKIP7Id[:]))
   156  		return false, err
   157  	} else if !isIKIP7 {
   158  		return false, nil
   159  	}
   160  
   161  	opts, cancel = getCallOpts(blockNumber, f.callTimeout)
   162  	defer cancel()
   163  	if isIKIP7Metadata, err := f.supportsInterface(contract, opts, IKIP7MetadataId); err != nil {
   164  		logger.Error("supportsInterface is failed", "contract", contract.String(), "blockNumber", blockNumber, "interfaceID", hexutil.Encode(IKIP7MetadataId[:]))
   165  		return false, err
   166  	} else if !isIKIP7Metadata {
   167  		return false, nil
   168  	}
   169  
   170  	return true, nil
   171  }
   172  
   173  // isKIP17 checks if the given contract implements IKIP17 and IKIP17Metadata interface or not at the given block.
   174  func (f *contractCaller) isKIP17(contract common.Address, blockNumber *big.Int) (bool, error) {
   175  	var (
   176  		opts   *bind.CallOpts
   177  		cancel context.CancelFunc
   178  	)
   179  	opts, cancel = getCallOpts(blockNumber, f.callTimeout)
   180  	defer cancel()
   181  	if isIKIP17, err := f.supportsInterface(contract, opts, IKIP17Id); err != nil {
   182  		logger.Error("supportsInterface is failed", "contract", contract.String(), "blockNumber", blockNumber, "interfaceID", hexutil.Encode(IKIP17Id[:]))
   183  		return false, err
   184  	} else if !isIKIP17 {
   185  		return false, nil
   186  	}
   187  
   188  	opts, cancel = getCallOpts(blockNumber, f.callTimeout)
   189  	defer cancel()
   190  	if isIKIP17Metadata, err := f.supportsInterface(contract, opts, IKIP17MetadataId); err != nil {
   191  		logger.Error("supportsInterface is failed", "contract", contract.String(), "blockNumber", blockNumber, "interfaceID", hexutil.Encode(IKIP17MetadataId[:]))
   192  		return false, err
   193  	} else if !isIKIP17Metadata {
   194  		return false, nil
   195  	}
   196  
   197  	return true, nil
   198  }