From e71ea97300757c8a64cdd72b19a24da567b1ebc6 Mon Sep 17 00:00:00 2001 From: GG Date: Wed, 20 Jul 2022 17:07:14 +0800 Subject: [PATCH] feat: support red packet. --- core/eth/account.go | 16 ++++ core/eth/chain.go | 50 ++++++++++- core/eth/erc20token.go | 70 +++++++++++++++- core/eth/red_packet.go | 162 ++++++++++++++++++++++++++++++++++++ core/eth/red_packet_test.go | 68 +++++++++++++++ core/eth/rpc_config_test.go | 4 +- core/eth/token.go | 34 +------- core/eth/types.go | 27 ++++++ core/eth/utils.go | 22 +++-- 9 files changed, 409 insertions(+), 44 deletions(-) create mode 100644 core/eth/red_packet.go create mode 100644 core/eth/red_packet_test.go diff --git a/core/eth/account.go b/core/eth/account.go index 80fe757..a1b16cb 100644 --- a/core/eth/account.go +++ b/core/eth/account.go @@ -3,6 +3,7 @@ package eth import ( "crypto/ecdsa" "fmt" + "strings" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" @@ -57,6 +58,21 @@ func NewAccountWithMnemonic(mnemonic string) (*Account, error) { }, nil } +// We cannot use name `NewAccountWithPrivateKey`, because android not support. +func EthAccountWithPrivateKey(privateKey string) (*Account, error) { + privateKey = strings.TrimPrefix(privateKey, "0x") + privateKeyECDSA, err := crypto.HexToECDSA(privateKey) + if err != nil { + return nil, err + } + address := crypto.PubkeyToAddress(privateKeyECDSA.PublicKey).Hex() + return &Account{ + Util: NewUtil(), + privateKeyECDSA: privateKeyECDSA, + address: address, + }, nil +} + // MARK - Implement the protocol wallet.Account // @return privateKey data diff --git a/core/eth/chain.go b/core/eth/chain.go index 1d55498..6d32c1e 100644 --- a/core/eth/chain.go +++ b/core/eth/chain.go @@ -1,6 +1,12 @@ package eth -import "github.com/coming-chat/wallet-SDK/core/base" +import ( + "crypto/ecdsa" + "strings" + + "github.com/coming-chat/wallet-SDK/core/base" + "github.com/ethereum/go-ethereum/crypto" +) type Chain struct { RpcUrl string @@ -99,3 +105,45 @@ func (c *Chain) BatchFetchTransactionStatus(hashListString string) string { } return chain.SdkBatchTransactionStatus(hashListString) } + +func (c *Chain) BuildTransferTx(privateKey string, transaction *Transaction) (*base.OptionalString, error) { + privateKey = strings.TrimPrefix(privateKey, "0x") + privateKeyECDSA, err := crypto.HexToECDSA(privateKey) + if err != nil { + return nil, err + } + return c.buildTransfer(privateKeyECDSA, transaction) +} + +func (c *Chain) BuildTransferTxWithAccount(account *Account, transaction *Transaction) (*base.OptionalString, error) { + return c.buildTransfer(account.privateKeyECDSA, transaction) +} + +func (c *Chain) buildTransfer(privateKey *ecdsa.PrivateKey, transaction *Transaction) (*base.OptionalString, error) { + chain, err := GetConnection(c.RpcUrl) + if err != nil { + return nil, err + } + + if transaction.Nonce == "" || transaction.Nonce == "0" { + address := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + nonce, err := chain.Nonce(address) + if err != nil { + nonce = "0" + err = nil + } + transaction.Nonce = nonce + } + + rawTx, err := transaction.GetRawTx() + if err != nil { + return nil, err + } + + txResult, err := chain.buildTxWithTransaction(rawTx, privateKey) + if err != nil { + return nil, err + } + + return &base.OptionalString{Value: txResult.TxHex}, nil +} diff --git a/core/eth/erc20token.go b/core/eth/erc20token.go index 78d9661..8c5c213 100644 --- a/core/eth/erc20token.go +++ b/core/eth/erc20token.go @@ -3,9 +3,11 @@ package eth import ( "crypto/ecdsa" "errors" + "math/big" "strings" "github.com/coming-chat/wallet-SDK/core/base" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/shopspring/decimal" ) @@ -27,9 +29,11 @@ type Erc20Token struct { ContractAddress string } -// Warning: initial unavailable, You must create based on Chain.Erc20Token() -func NewErc20Token() (*Erc20Token, error) { - return nil, errors.New("Token initial unavailable, You must create based on Chain.MainToken()") +func NewErc20Token(chain *Chain, contractAddress string) *Erc20Token { + return &Erc20Token{ + Token: &Token{chain: chain}, + ContractAddress: contractAddress, + } } // MARK - Implement the protocol Token, Override @@ -74,6 +78,14 @@ func (t *Erc20Token) TokenInfo() (*base.TokenInfo, error) { return info, nil } +func (t *Erc20Token) Decimal() (int16, error) { + chain, err := GetConnection(t.chain.RpcUrl) + if err != nil { + return 18, err + } + return chain.TokenDecimal(t.ContractAddress) +} + func (t *Erc20Token) BalanceOfAddress(address string) (*base.Balance, error) { b := base.EmptyBalance() chain, err := GetConnection(t.chain.RpcUrl) @@ -141,5 +153,55 @@ func (t *Erc20Token) buildTransfer(privateKey *ecdsa.PrivateKey, transaction *Tr if err != nil { return nil, err } - return t.Token.buildTransfer(privateKey, transaction) + return t.chain.buildTransfer(privateKey, transaction) +} + +func (t *Erc20Token) Allowance(owner, spender string) (*big.Int, error) { + chain, err := GetConnection(t.chain.RpcUrl) + if err != nil { + return nil, err + } + res := big.NewInt(0) + ownerAddress := common.HexToAddress(owner) + spenderAddress := common.HexToAddress(spender) + err = chain.CallContractConstant(&res, t.ContractAddress, Erc20AbiStr, "allowance", nil, ownerAddress, spenderAddress) + if err != nil { + return nil, err + } + return res, nil +} + +func (t *Erc20Token) Approve(account *Account, spender string, amount *big.Int) (string, error) { + err := errors.New("Approve failed") + + approveData, err := EncodeErc20Approve(spender, amount) + if err != nil { + return "", err + } + + gasPrice, err := t.chain.SuggestGasPrice() + if err != nil { + return "", err + } + msg := NewCallMsg() + msg.SetFrom(account.Address()) + msg.SetTo(t.ContractAddress) + msg.SetGasPrice(gasPrice.Value) + msg.SetData(approveData) + msg.SetValue("0") + + gasLimit, err := t.chain.EstimateGasLimit(msg) + if err != nil { + gasLimit = &base.OptionalString{Value: "100000"} + err = nil + } + msg.SetGasLimit(gasLimit.Value) + + transaction := msg.TransferToTransaction() + rawTx, err := t.chain.buildTransfer(account.privateKeyECDSA, transaction) + if err != nil { + return "", err + } + + return t.chain.SendRawTransaction(rawTx.Value) } diff --git a/core/eth/red_packet.go b/core/eth/red_packet.go new file mode 100644 index 0000000..cfaeec9 --- /dev/null +++ b/core/eth/red_packet.go @@ -0,0 +1,162 @@ +package eth + +import ( + "fmt" + "math/big" + + "github.com/coming-chat/wallet-SDK/core/base" + "github.com/ethereum/go-ethereum/common" +) + +const RedPacketABI = `[{"inputs":[{"internalType":"address","name":"_admin","type":"address"},{"internalType":"address","name":"_beneficiary","type":"address"},{"internalType":"uint256","name":"_base_fee","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_old","type":"address"},{"indexed":false,"internalType":"address","name":"_new","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_old","type":"address"},{"indexed":false,"internalType":"address","name":"_new","type":"address"}],"name":"BeneficiaryChanged","type":"event"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"address","name":"maybe_creator","type":"address"}],"name":"close","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"token","type":"address"},{"internalType":"uint256","name":"count","type":"uint256"},{"internalType":"uint256","name":"total_balance","type":"uint256"}],"name":"create","outputs":[],"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"_fee","type":"uint256"}],"name":"NewBasePrepaidFee","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"_id","type":"uint256"},{"indexed":false,"internalType":"contract IERC20","name":"_token","type":"address"},{"indexed":false,"internalType":"uint256","name":"_count","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"_balance","type":"uint256"}],"name":"NewRedEnvelop","type":"event"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"address[]","name":"luck_accounts","type":"address[]"},{"internalType":"uint256[]","name":"balances","type":"uint256[]"}],"name":"open","outputs":[],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"new_admin","type":"address"}],"name":"set_admin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"new_beneficiary","type":"address"}],"name":"set_beneficiary","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"new_fee","type":"uint256"}],"name":"set_prepaid_fee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"_id","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"_remain_count","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"_remain_balance","type":"uint256"}],"name":"UpdateRedEnvelop","type":"event"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"base_prepaid_fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"beneficiary","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"count","type":"uint256"}],"name":"calc_prepaid_fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"is_valid","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"max_count","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"next_id","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"red_envelop_infos","outputs":[{"internalType":"contract IERC20","name":"token","type":"address"},{"internalType":"uint256","name":"remain_count","type":"uint256"},{"internalType":"uint256","name":"remain_balance","type":"uint256"}],"stateMutability":"view","type":"function"}]` + +const ( + RPAMethodCreate = "create" + RPAMethodOpen = "open" + RPAMethodClose = "close" +) + +type RedPacketAction struct { + Method string + Params []interface{} +} + +// 用户发红包 的操作 +func NewRedPacketActionCreate(erc20TokenAddress string, count int, amount string) (*RedPacketAction, error) { + addr := common.HexToAddress(erc20TokenAddress) + c := big.NewInt(int64(count)) + a, ok := big.NewInt(0).SetString(amount, 10) + if !ok { + return nil, fmt.Errorf("Invalid red packet amount %v", amount) + } + return &RedPacketAction{ + Method: RPAMethodCreate, + Params: []interface{}{addr, c, a}, + }, nil +} + +// 批量打开红包 的操作 +func NewRedPacketActionOpen(packetId int64, addresses []string, amounts []string) (*RedPacketAction, error) { + id := big.NewInt(packetId) + if len(addresses) != len(amounts) { + return nil, fmt.Errorf("The number of opened addresses is not the same as the amount") + } + addrs := make([]common.Address, len(addresses)) + for index, address := range addresses { + addrs[index] = common.HexToAddress(address) + } + amountInts := make([]*big.Int, len(amounts)) + for index, amount := range amounts { + aInt, ok := big.NewInt(0).SetString(amount, 10) + if !ok { + return nil, fmt.Errorf("Invalid red packet amount %v", amount) + } + amountInts[index] = aInt + } + return &RedPacketAction{ + Method: RPAMethodOpen, + Params: []interface{}{id, addrs, amountInts}, + }, nil +} + +// 结束红包领取 的操作 +func NewRedPacketActionClose(packetId int64, creator string) (*RedPacketAction, error) { + id := big.NewInt(packetId) + addr := common.HexToAddress(creator) + return &RedPacketAction{ + Method: RPAMethodClose, + Params: []interface{}{id, addr}, + }, nil +} + +func (rpa *RedPacketAction) EstimateAmount() string { + switch rpa.Method { + case RPAMethodCreate: + count := rpa.Params[1].(*big.Int).Int64() + rate := 200.0 + switch { + case count <= 10: + rate = 4 + case count <= 100: + rate = 16 + case count <= 1000: + rate = 200 + } + feeFloat := big.NewFloat(0.025 * rate) + feeFloat.Mul(feeFloat, big.NewFloat(1e18)) + feeInt, _ := feeFloat.Int(big.NewInt(0)) + return feeInt.String() + default: + return "0" + } +} + +// 保证用户发 erc20 的红包时,红包合约可以有权限操作用户的资产 +// @param account 要发红包的用户的账号,也许需要用到私钥来发起授权交易 +// @param chain evm 链 +// @param erc20Contract 要用作发红包的币种 +// @param coins 如果需要发起新授权,指定要授权的币个数 default 10^6 +// @return 如果授权成功,不会返回错误,如果有新授权,会返回授权交易的 hash +func (rpa *RedPacketAction) EnsureApprovedTokens(account *Account, chain *Chain, spender string, coins int) (string, error) { + // only red packet **create** need approve + if rpa.Method != RPAMethodCreate { + return "", nil + } + + erc20Token := rpa.Params[0].(common.Address).String() + amount := rpa.Params[2].(*big.Int) + + token := NewErc20Token(chain, erc20Token) + approved, err := token.Allowance(account.Address(), spender) + if err != nil { + return "", err + } + if approved.Cmp(amount) >= 0 { + return "", nil + } + + decimal, err := token.Decimal() + if err != nil { + decimal = 18 + err = nil + } + if coins <= 0 { + coins = 1e6 // default 1 million coins + } + oneCoin := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(decimal)), nil) + approveValue := big.NewInt(0).Mul(oneCoin, big.NewInt(int64(coins))) + approveValue = base.MaxBigInt(approveValue, amount) + + return token.Approve(account, spender, approveValue) +} + +// @param fromAddress 要调用红包业务的操作者 +// @param contractAddress 红包合约地址 +// @param chain 要发红包的链 +func (rpa *RedPacketAction) TransactionFrom(fromAddress, contractAddress string, chain *Chain) (*Transaction, error) { + data, err := EncodeAbiData(RedPacketABI, rpa.Method, rpa.Params...) + if err != nil { + return nil, err + } + gasPrice, err := chain.SuggestGasPrice() + if err != nil { + return nil, err + } + + value := rpa.EstimateAmount() + msg := NewCallMsg() + msg.SetFrom(fromAddress) + msg.SetTo(contractAddress) + msg.SetGasPrice(gasPrice.Value) + msg.SetData(data) + msg.SetValue(value) + + gasLimit, err := chain.EstimateGasLimit(msg) + if err != nil { + gasLimit = &base.OptionalString{Value: "200000"} + err = nil + } + msg.SetGasLimit(gasLimit.Value) + + return msg.TransferToTransaction(), nil +} diff --git a/core/eth/red_packet_test.go b/core/eth/red_packet_test.go new file mode 100644 index 0000000..6da32d9 --- /dev/null +++ b/core/eth/red_packet_test.go @@ -0,0 +1,68 @@ +package eth + +import ( + "testing" + + "github.com/coming-chat/wallet-SDK/core/testcase" +) + +func TestRedPacketProcess(t *testing.T) { + chain := rpcs.sherpaxProd.Chain() + account, _ := NewAccountWithMnemonic(testcase.M1) + // account, _ := EthAccountWithPrivateKey("0x......") + contractAddress := "0x4777abDEc6D52C25b4bc55a361da495011ccDBC3" + + balance, err := chain.BalanceOfAddress(account.Address()) + t.Logf("the account's balance %v %v", balance, err) + + // 1.1 Create red packet + erc20Token := "0xa10AF02fD7eD3B5FF107B57bB1068a3f54BcAE92" // erc20 PCX + count := 3 + amount := "100000000" + action, err := NewRedPacketActionCreate(erc20Token, count, amount) + + // 1.2 Open red packet + // packetId := int64(0) + // addresses := []string{"0x99e5f4759fC07ee8F4f1B5a017ba100EFFC0C9C0"} + // amounts := []string{"50000000"} + // action, err := NewRedPacketActionOpen(packetId, addresses, amounts) + + // 1.3 Close red packet + // packetId := int64(0) + // creator := "0x6334d64D5167F726d8A44f3fbCA66613708E59E7" + // action, err := NewRedPacketActionClose(packetId, creator) + + if err != nil { + t.Fatal(err) + } + + // 2. ensure erc20 coin approved + // 如果是发红包,那这一步是必须的 + txhash, err := action.EnsureApprovedTokens(account, chain, contractAddress, 0) + if err != nil { + t.Fatal(err) + } + t.Logf("Are there new approve? %v", txhash) + + // 3. make transaction + transaction, err := action.TransactionFrom(account.Address(), contractAddress, chain) + if err != nil { + t.Fatal(err) + } + t.Logf("Service Fee = %v", transaction.TotalAmount()) + + // 4.1 signing raw tx with privatekey + // rawTx, err := chain.BuildTransferTx(account.PrivateKeyHex(), transaction) + // 4.2 or signing raw tx with account + rawTx, err := chain.BuildTransferTxWithAccount(account, transaction) + if err != nil { + t.Fatal(err) + } + + // 5. send transaction + txHash, err := chain.SendRawTransaction(rawTx.Value) + if err != nil { + t.Fatal(err) + } + t.Logf("Red packet process success! txHash = %v", txHash) +} diff --git a/core/eth/rpc_config_test.go b/core/eth/rpc_config_test.go index b08b972..0449725 100644 --- a/core/eth/rpc_config_test.go +++ b/core/eth/rpc_config_test.go @@ -95,7 +95,9 @@ var rpcs = rpcConfig{ arbitrumProd: rpcInfo{ "https://arb1.arbitrum.io/rpc", "https://arbiscan.io", - nil, + &contracts{ + USDT: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + }, }, arbitrumTest: rpcInfo{ "https://rinkeby.arbitrum.io/rpc", diff --git a/core/eth/token.go b/core/eth/token.go index e467d2d..4588f7b 100644 --- a/core/eth/token.go +++ b/core/eth/token.go @@ -1,7 +1,6 @@ package eth import ( - "crypto/ecdsa" "encoding/json" "errors" "math/big" @@ -75,40 +74,11 @@ func (t *Token) BuildTransferTx(privateKey string, transaction *Transaction) (*b if err != nil { return nil, err } - return t.buildTransfer(privateKeyECDSA, transaction) + return t.chain.buildTransfer(privateKeyECDSA, transaction) } func (t *Token) BuildTransferTxWithAccount(account *Account, transaction *Transaction) (*base.OptionalString, error) { - return t.buildTransfer(account.privateKeyECDSA, transaction) -} - -func (t *Token) buildTransfer(privateKey *ecdsa.PrivateKey, transaction *Transaction) (*base.OptionalString, error) { - chain, err := GetConnection(t.chain.RpcUrl) - if err != nil { - return nil, err - } - - if transaction.Nonce == "" || transaction.Nonce == "0" { - address := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() - nonce, err := chain.Nonce(address) - if err != nil { - nonce = "0" - err = nil - } - transaction.Nonce = nonce - } - - rawTx, err := transaction.GetRawTx() - if err != nil { - return nil, err - } - - txResult, err := chain.buildTxWithTransaction(rawTx, privateKey) - if err != nil { - return nil, err - } - - return &base.OptionalString{Value: txResult.TxHex}, nil + return t.chain.buildTransfer(account.privateKeyECDSA, transaction) } type OptimismLayer2Gas struct { diff --git a/core/eth/types.go b/core/eth/types.go index 116105f..83da796 100644 --- a/core/eth/types.go +++ b/core/eth/types.go @@ -113,6 +113,16 @@ func (msg *CallMsg) SetTo(address string) { } } +func (msg *CallMsg) TransferToTransaction() *Transaction { + return &Transaction{ + GasPrice: msg.GetGasPrice(), + GasLimit: msg.GetGasLimit(), + To: msg.GetTo(), + Value: msg.GetValue(), + Data: msg.GetDataHex(), + } +} + type Transaction struct { Nonce string // nonce of sender account GasPrice string // wei per gas @@ -247,3 +257,20 @@ func (tx *Transaction) TransformToErc20Transaction(contractAddress string) error tx.Data = HexType.HexEncodeToString(data) return nil } + +// @return gasPrice * gasLimit + value +func (tx *Transaction) TotalAmount() string { + priceInt, ok := big.NewInt(0).SetString(tx.GasPrice, 10) + if !ok { + return "0" + } + limitInt, ok := big.NewInt(0).SetString(tx.GasLimit, 10) + if !ok { + return "0" + } + amount, ok := big.NewInt(0).SetString(tx.Value, 10) + if !ok { + return "0" + } + return amount.Add(amount, priceInt.Mul(priceInt, limitInt)).String() +} diff --git a/core/eth/utils.go b/core/eth/utils.go index fe40660..62ddfa6 100644 --- a/core/eth/utils.go +++ b/core/eth/utils.go @@ -40,11 +40,6 @@ func PrivateKeyToAddress(privateKey string) (string, error) { // Encode erc20 transfer data func EncodeErc20Transfer(toAddress, amount string) ([]byte, error) { - parsedAbi, err := abi.JSON(strings.NewReader(Erc20AbiStr)) - if err != nil { - return nil, err - } - if !common.IsHexAddress(toAddress) { return nil, errors.New("Invalid receiver address") } @@ -52,5 +47,20 @@ func EncodeErc20Transfer(toAddress, amount string) ([]byte, error) { if !valid { return nil, errors.New("Invalid transfer amount") } - return parsedAbi.Pack(ERC20_METHOD_TRANSFER, common.HexToAddress(toAddress), amountInt) + return EncodeAbiData(Erc20AbiStr, ERC20_METHOD_TRANSFER, common.HexToAddress(toAddress), amountInt) +} + +func EncodeErc20Approve(spender string, amount *big.Int) ([]byte, error) { + if !common.IsHexAddress(spender) { + return nil, errors.New("Invalid receiver address") + } + return EncodeAbiData(Erc20AbiStr, ERC20_METHOD_APPROVE, common.HexToAddress(spender), amount) +} + +func EncodeAbiData(abiString, method string, params ...interface{}) ([]byte, error) { + parsedAbi, err := abi.JSON(strings.NewReader(abiString)) + if err != nil { + return nil, err + } + return parsedAbi.Pack(method, params...) }