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 }