diff --git a/core/eth/chain_nft.go b/core/eth/chain_nft.go new file mode 100644 index 0000000..964eb15 --- /dev/null +++ b/core/eth/chain_nft.go @@ -0,0 +1,45 @@ +package eth + +import ( + "errors" + "strings" + + "github.com/coming-chat/wallet-SDK/core/base" +) + +func (c *Chain) TransferNFT(sender, receiver string, nft *base.NFT) (*Transaction, error) { + return c.TransferNFTParams(sender, receiver, nft.Id, nft.ContractAddress, nft.Standard) +} + +// TransferNFTParams +// - param nftStandard: only support erc-721 now, else throw error unsupported nft type. +func (c *Chain) TransferNFTParams(sender, receiver, nftId, nftContractAddress, nftStandard string) (txn *Transaction, err error) { + defer base.CatchPanicAndMapToBasicError(&err) + + if strings.ToLower(nftStandard) != "erc-721" { + return nil, errors.New("unsupported nft type") + } + data, err := EncodeErc721TransferFrom(sender, receiver, nftId) + if err != nil { + return nil, err + } + gasPrice, err := c.SuggestGasPrice() + if err != nil { + return nil, err + } + + msg := NewCallMsg() + msg.SetFrom(sender) + msg.SetTo(nftContractAddress) + msg.SetValue("0") + msg.SetGasPrice(gasPrice.Value) + msg.SetData(data) + + gasLimit, err := c.EstimateGasLimit(msg) + if err != nil { + return nil, err + } + msg.SetGasLimit(gasLimit.Value) + + return msg.TransferToTransaction(), nil +} diff --git a/core/eth/chain_nft_test.go b/core/eth/chain_nft_test.go new file mode 100644 index 0000000..d943e9e --- /dev/null +++ b/core/eth/chain_nft_test.go @@ -0,0 +1,35 @@ +package eth + +import ( + "testing" + + "github.com/coming-chat/wallet-SDK/core/testcase" + "github.com/stretchr/testify/require" +) + +func TestChain_TransferNFT_Erc721(t *testing.T) { + mn := testcase.M1 + sender, err := NewAccountWithMnemonic(mn) + require.Nil(t, err) + + receiver := sender.Address() + nftId := "0" + nftContract := "0x199Dcb0132a66b05723882259832e240fF735810" + nftStandard := "erc-721" + + chain := NewChainWithRpc("https://canary-testnet.bevm.io/") + + txn, err := chain.TransferNFTParams(sender.Address(), receiver, + nftId, nftContract, nftStandard) + require.Nil(t, err) + + signedTx, err := chain.BuildTransferTxWithAccount(sender, txn) + require.Nil(t, err) + + run := false + if run { + txHash, err := chain.SendRawTransaction(signedTx.Value) + require.Nil(t, err) + t.Log(txHash) + } +} diff --git a/core/eth/ethchain_erc20.go b/core/eth/ethchain_erc20.go index 42659e3..0da7dc6 100644 --- a/core/eth/ethchain_erc20.go +++ b/core/eth/ethchain_erc20.go @@ -17,6 +17,8 @@ import ( const ( // 合约 ABI json文件,查询ERC20 相关代币信息需要使用 ABI 文件 Erc20AbiStr = `[{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"address","name":"pauser","type":"address"},{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"},{"internalType":"uint8","name":"decimal","type":"uint8"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"new_operator","type":"address"},{"internalType":"address","name":"new_pauser","type":"address"}],"name":"changeUser","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"}]` + // erc721 的 ABI 文件, 只支持 transferFrom 方法 + Erc721Abi_TransferOnly = `[{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"}]` ) // Deprecated: SdkBatchTokenBalance is deprecated. Please Use Chain.BatchFetchErc20TokenBalance() instead. diff --git a/core/eth/types.go b/core/eth/types.go index 14429d0..67f2869 100644 --- a/core/eth/types.go +++ b/core/eth/types.go @@ -140,6 +140,28 @@ func NewTransaction(nonce, gasPrice, gasLimit, to, value, data string) *Transact return &Transaction{nonce, gasPrice, gasLimit, to, value, data, ""} } +func NewTransactionNftTransfer(sender, receiver, gasPrice, gasLimit string, nft *base.NFT) *Transaction { + return NewTransactionNftTransferParams(sender, receiver, gasPrice, gasLimit, nft.Id, nft.ContractAddress, nft.Standard) +} + +func NewTransactionNftTransferParams(sender, receiver, gasPrice, gasLimit, nftId, nftContractAddress, nftStandard string) *Transaction { + if strings.ToLower(nftStandard) != "erc-721" { + return nil + } + data, err := EncodeErc721TransferFrom(sender, receiver, nftId) + if err != nil { + return nil + } + return &Transaction{ + Nonce: "", + GasPrice: gasPrice, + GasLimit: gasLimit, + To: nftContractAddress, + Value: "0", + Data: common.Bytes2Hex(data), + } +} + func NewTransactionFromHex(hexData string) (*Transaction, error) { rawBytes, err := hex.DecodeString(hexData) if err != nil { diff --git a/core/eth/types_test.go b/core/eth/types_test.go index 43ddb08..a30d77b 100644 --- a/core/eth/types_test.go +++ b/core/eth/types_test.go @@ -1,10 +1,26 @@ package eth import ( + "encoding/hex" "reflect" "testing" + + "github.com/stretchr/testify/require" ) +func TestUtils_EncodeErc721TransferFrom(t *testing.T) { + sender := "0x151e446ca01b57e495a31d53bc622ac33bd7a0be" + receiver := "0x2c32bd5f7d3eab4bc9d968c90c82debb1bdcced9" + nftId := "1" + + // Reference: https://scan-canary-testnet.bevm.io/tx/0x2c763fc26b1021340edc5614c7411a8f4d2220d22fecd805557fef4536268ef8 + data, err := EncodeErc721TransferFrom(sender, receiver, nftId) + require.Nil(t, err) + dataHex := hex.EncodeToString(data) + require.Equal(t, dataHex, + "23b872dd000000000000000000000000151e446ca01b57e495a31d53bc622ac33bd7a0be0000000000000000000000002c32bd5f7d3eab4bc9d968c90c82debb1bdcced90000000000000000000000000000000000000000000000000000000000000001") +} + func TestNewTransactionFromHex(t *testing.T) { type args struct { hexData string diff --git a/core/eth/utils.go b/core/eth/utils.go index 58e8391..916df53 100644 --- a/core/eth/utils.go +++ b/core/eth/utils.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/coming-chat/wallet-SDK/core/base" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -57,6 +58,20 @@ func EncodeErc20Approve(spender string, amount *big.Int) ([]byte, error) { return EncodeContractData(Erc20AbiStr, ERC20_METHOD_APPROVE, common.HexToAddress(spender), amount) } +func EncodeErc721TransferFrom(sender, receiver, nftId string) ([]byte, error) { + if !common.IsHexAddress(sender) || !common.IsHexAddress(receiver) { + return nil, base.ErrInvalidAddress + } + amtInt, ok := big.NewInt(0).SetString(nftId, 10) + if !ok { + return nil, base.ErrInvalidAmount + } + return EncodeContractData(Erc721Abi_TransferOnly, "transferFrom", + common.HexToAddress(sender), + common.HexToAddress(receiver), + amtInt) +} + func EncodeContractData(abiString, method string, params ...interface{}) ([]byte, error) { parsedAbi, err := abi.JSON(strings.NewReader(abiString)) if err != nil {