diff --git a/README.md b/README.md index 7d6eaad..305e976 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # INTERX +For test INTERX is an interchain engine, proxy, load balancer & security gateway service for communication between backend and frontend. It will connect to the node using the GRPC endpoint as well as the RPC endpoint ([`Tendermint RPC`](https://docs.tendermint.com/master/rpc/)). @@ -509,4 +510,4 @@ Remember this settings when you set/update manually from `config.json`. ### How to update caching configurations All caching configurations are set in `config.json` file. -`config.json` file includes `rpc_methods` field and there you can set/update caching config of each endpoint. \ No newline at end of file +`config.json` file includes `rpc_methods` field and there you can set/update caching config of each endpoint. diff --git a/RELEASE.md b/RELEASE.md index 2dbb00a..d7fa990 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,4 @@ Features: -* Add webhook for sekin repo -* Bump sekai and interx version -* Fix pub_p2p_list endpoint -* Del bin - +* Implement KIP-87 : broadcast metamask coin send transaction to sekai +* Implement KIP-87 : broadcast eip-712 signed transaction to sekai diff --git a/common/api.go b/common/api.go index 86775b5..c772d0f 100644 --- a/common/api.go +++ b/common/api.go @@ -145,10 +145,10 @@ func GetAccountNumberSequence(gwCosmosmux *runtime.ServeMux, r *http.Request, be type QueryAccountResponse struct { Account struct { - Address string `json:"addresss"` - PubKey string `json:"pubKey"` - AccountNumber string `json:"accountNumber"` - Sequence string `json:"sequence"` + Address string `json:"addresss"` + PubKey interface{} `json:"pubKey"` + AccountNumber string `json:"accountNumber"` + Sequence string `json:"sequence"` } `json:"account"` } result := QueryAccountResponse{} @@ -331,7 +331,6 @@ func GetTokenAliases(gwCosmosmux *runtime.ServeMux, r *http.Request) ([]types.To } result := TokenAliasesResponse{} - err := json.NewDecoder(resp.Body).Decode(&result) if err != nil { GetLogger().Error("[grpc-call] Unable to decode response: ", err) diff --git a/common/gateway.go b/common/gateway.go index 69c7155..0d7be12 100644 --- a/common/gateway.go +++ b/common/gateway.go @@ -3,6 +3,7 @@ package common import ( "encoding/base64" "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -344,3 +345,9 @@ func RosettaBuildError(code int, message string, description string, retriable b func RosettaServeError(code int, data string, message string, statusCode int) (interface{}, interface{}, int) { return nil, RosettaBuildError(code, message, data, true, nil), statusCode } + +// BuildTxSearchEndpoint creates a tx_search endpoint. +func BuildTxSearchEndpoint(rpcAddr string, query string, page int, limit int, orderBy string) string { + return fmt.Sprintf("%s/tx_search?query=\"%s\"&page=%d&per_page=%d&order_by=\"%s\"", + rpcAddr, query, page, limit, orderBy) +} diff --git a/config/constants.go b/config/constants.go index 96d5828..cf84c2f 100755 --- a/config/constants.go +++ b/config/constants.go @@ -1,10 +1,19 @@ package config const ( - InterxVersion = "v0.4.48" - SekaiVersion = "v0.3.42" + + InterxVersion = "v0.4.49" + SekaiVersion = "v0.3.45" CosmosVersion = "v0.47.6" + DefaultChainID = 8789 + DefaultKiraAddrPrefix = "kira" + DefaultKiraValAddrPrefix = "kiravaloper" + DefaultKiraDenom = "ukex" + + MetamaskEndpoint = "/api/kira/evm" + RegisterAddrEndpoint = "/api/kira/evm/register_address/{eth_addr}{cosmos_addr}" + QueryDashboard = "/api/dashboard" QueryAccounts = "/api/kira/accounts/{address}" diff --git a/database/transactions.go b/database/transactions.go index 5b0dbf0..071e0dd 100644 --- a/database/transactions.go +++ b/database/transactions.go @@ -13,9 +13,18 @@ import ( // GetTransactions is a function to get user transactions from cache func GetTransactions(address string, isWithdraw bool) (*tmTypes.ResultTxSearch, error) { - filePath := fmt.Sprintf("%s/transactions/%s", config.GetDbCacheDir(), address) - if !isWithdraw { - filePath = filePath + "-inbound" + var filePath string + + basePath := fmt.Sprintf("%s/transactions", config.GetDbCacheDir()) + suffix := "-inbound" + if isWithdraw { + suffix = "" + } + + if address == "" { + filePath = fmt.Sprintf("%s/all-transactions%s", basePath, suffix) + } else { + filePath = fmt.Sprintf("%s/%s%s", basePath, address, suffix) } data := tmTypes.ResultTxSearch{} @@ -55,10 +64,12 @@ func GetLastBlockFetched(address string, isWithdraw bool) int64 { func SaveTransactions(address string, txsData tmTypes.ResultTxSearch, isWithdraw bool) error { cachedData, _ := GetTransactions(address, isWithdraw) - // Append new txs to the cached txs array - if cachedData.TotalCount > 0 { - txsData.Txs = append(txsData.Txs, cachedData.Txs...) - txsData.TotalCount = txsData.TotalCount + cachedData.TotalCount + if address != "" { + // Append new txs to the cached txs array + if cachedData.TotalCount > 0 { + txsData.Txs = append(txsData.Txs, cachedData.Txs...) + txsData.TotalCount = txsData.TotalCount + cachedData.TotalCount + } } data, err := json.Marshal(txsData) @@ -67,10 +78,8 @@ func SaveTransactions(address string, txsData tmTypes.ResultTxSearch, isWithdraw } folderPath := fmt.Sprintf("%s/transactions", config.GetDbCacheDir()) - filePath := fmt.Sprintf("%s/%s", folderPath, address) - if !isWithdraw { - filePath = filePath + "-inbound" - } + fileName := resolveFileName(address, isWithdraw) + filePath := fmt.Sprintf("%s/%s", folderPath, fileName) global.Mutex.Lock() err = os.MkdirAll(folderPath, os.ModePerm) @@ -85,8 +94,21 @@ func SaveTransactions(address string, txsData tmTypes.ResultTxSearch, isWithdraw global.Mutex.Unlock() if err != nil { - fmt.Println("[cache] Unable to save response: ", filePath) + fmt.Println("[SaveTransactions][cache] Unable to save response: ", filePath) } return err } + +// Helper function to determine the file name +func resolveFileName(address string, isWithdraw bool) string { + if address == "" { + address = "all-transactions" + } + + if !isWithdraw { + return fmt.Sprintf("%s-inbound", address) + } + + return address +} diff --git a/gateway/interx/interx.tx.go b/gateway/interx/interx.tx.go index 7d72a49..5d41296 100644 --- a/gateway/interx/interx.tx.go +++ b/gateway/interx/interx.tx.go @@ -26,11 +26,6 @@ import ( "golang.org/x/exp/slices" ) -type TxsResponse struct { - Transactions []types.TransactionResponse `json:"transactions"` - TotalCount int `json:"total_count"` -} - // RegisterInterxTxRoutes registers tx query routers. func RegisterInterxTxRoutes(r *mux.Router, gwCosmosmux *runtime.ServeMux, rpcAddr string) { r.HandleFunc(config.QueryUnconfirmedTxs, QueryUnconfirmedTxs(rpcAddr)).Methods("GET") @@ -47,10 +42,6 @@ func GetTransactionsWithSync(rpcAddr string, address string, isOutbound bool) (* var limit = 100 var limitPages = 100 - if address == "" { - return &tmTypes.ResultTxSearch{}, nil - } - lastBlock := database.GetLastBlockFetched(address, isOutbound) totalResult := tmTypes.ResultTxSearch{ Txs: []*tmTypes.ResultTx{}, @@ -59,15 +50,23 @@ func GetTransactionsWithSync(rpcAddr string, address string, isOutbound bool) (* for page < limitPages { var events = make([]string, 0, 5) - if isOutbound { - events = append(events, fmt.Sprintf("message.sender='%s'", address)) + if address != "" { + if isOutbound { + events = append(events, fmt.Sprintf("message.sender='%s'", address)) + } else { + events = append(events, fmt.Sprintf("transfer.recipient='%s'", address)) + } + events = append(events, fmt.Sprintf("tx.height>%d", lastBlock)) + } + + var query string + if address == "" { + query = "tx.height>0" } else { - events = append(events, fmt.Sprintf("transfer.recipient='%s'", address)) + query = strings.Join(events, "%20AND%20") } - events = append(events, fmt.Sprintf("tx.height>%d", lastBlock)) - // search transactions - endpoint := fmt.Sprintf("%s/tx_search?query=\"%s\"&page=%d&per_page=%d&order_by=\"desc\"", rpcAddr, strings.Join(events, "%20AND%20"), page, limit) + endpoint := common.BuildTxSearchEndpoint(rpcAddr, query, page, limit, "desc") common.GetLogger().Info("[query-transaction] Entering transaction search: ", endpoint) resp, err := http.Get(endpoint) @@ -131,31 +130,23 @@ func GetFilteredTransactions(rpcAddr string, address string, txtypes []string, d Txs: []*tmTypes.ResultTx{}, TotalCount: 0, } + if len(directions) == 0 { directions = []string{"inbound", "outbound"} } - if slices.Contains(directions, "inbound") { - cachedTxs1, err := GetTransactionsWithSync(rpcAddr, address, false) - for _, cachedTx := range cachedTxs1.Txs { - hashToDirectionMap[cachedTx.Hash.String()] = append(hashToDirectionMap[cachedTx.Hash.String()], "inbound") - } - if err != nil { - return nil, err - } - cachedTxs.TotalCount += cachedTxs1.TotalCount - cachedTxs.Txs = append(cachedTxs.Txs, cachedTxs1.Txs...) - } - if slices.Contains(directions, "outbound") { - cachedTxs2, err := GetTransactionsWithSync(rpcAddr, address, true) - for _, cachedTx := range cachedTxs2.Txs { - hashToDirectionMap[cachedTx.Hash.String()] = append(hashToDirectionMap[cachedTx.Hash.String()], "outbound") - } - if err != nil { - return nil, err + if address != "" { + for _, direction := range directions { + switch direction { + case "inbound": + cachedTxs, _ = processDirection(rpcAddr, address, false, "inbound", hashToDirectionMap, &cachedTxs) + + case "outbound": + cachedTxs, _ = processDirection(rpcAddr, address, true, "outbound", hashToDirectionMap, &cachedTxs) + } } - cachedTxs.TotalCount += cachedTxs2.TotalCount - cachedTxs.Txs = append(cachedTxs.Txs, cachedTxs2.Txs...) + } else { + cachedTxs, _ = processDirection(rpcAddr, address, true, "outbound", hashToDirectionMap, &cachedTxs) } var res []types.TransactionResponse @@ -224,6 +215,7 @@ func GetFilteredTransactions(rpcAddr string, address string, txtypes []string, d Status: hashStatus, Direction: hashToDirectionMap[cachedTx.Hash.String()][0], Hash: fmt.Sprintf("0x%X", cachedTx.Hash), + Height: cachedTx.Height, Txs: txResponses, } if len(hashToDirectionMap[cachedTx.Hash.String()]) > 1 { @@ -306,39 +298,6 @@ func SearchTxHashHandle(rpcAddr string, sender string, recipient string, txType return result, nil } -// Get block height for tx hash from cache or tendermint -func getBlockHeight(rpcAddr string, hash string) (int64, error) { - endpoint := fmt.Sprintf("%s/tx?hash=%s", rpcAddr, hash) - common.GetLogger().Info("[query-block] Entering block query: ", endpoint) - - resp, err := http.Get(endpoint) - if err != nil { - common.GetLogger().Error("[query-block] Unable to connect to ", endpoint) - return 0, err - } - defer resp.Body.Close() - - respBody, _ := ioutil.ReadAll(resp.Body) - response := new(tmJsonRPCTypes.RPCResponse) - - if err := json.Unmarshal(respBody, response); err != nil { - common.GetLogger().Error("[query-block] Unable to decode response: ", err) - return 0, err - } - if response.Error != nil { - common.GetLogger().Error("[query-block] Error response:", response.Error.Message) - return 0, errors.New(response.Error.Message) - } - - result := new(tmTypes.ResultTx) - if err := tmjson.Unmarshal(response.Result, result); err != nil { - common.GetLogger().Error("[query-block] Failed to unmarshal result:", err) - return 0, fmt.Errorf("error unmarshalling result: %w", err) - } - - return result.Height, nil -} - func QueryBlockTransactionsHandler(rpcAddr string, r *http.Request) (interface{}, interface{}, int) { err := r.ParseForm() if err != nil { @@ -371,10 +330,6 @@ func QueryBlockTransactionsHandler(rpcAddr string, r *http.Request) (interface{} //------------ Address ------------ account = r.FormValue("address") - if account == "" { - common.GetLogger().Error("[query-transactions] 'address' is not set") - return common.ServeError(0, "'address' is not set", "", http.StatusBadRequest) - } //------------ Direction ------------ directionsParam := r.FormValue("direction") @@ -509,7 +464,7 @@ func QueryBlockTransactionsHandler(rpcAddr string, r *http.Request) (interface{} } txResults = txResults[offset:int(math.Min(float64(offset+limit), float64(len(txResults))))] - res := TxsResponse{ + res := types.TxsResponse{ TotalCount: totalCount, Transactions: txResults, } @@ -655,3 +610,24 @@ func QueryUnconfirmedTxs(rpcAddr string) http.HandlerFunc { common.WrapResponse(w, request, *response, statusCode, false) } } + +func processDirection( + rpcAddr, address string, + isOutbound bool, + direction string, + hashToDirectionMap map[string][]string, + cachedTxs *tmTypes.ResultTxSearch, +) (tmTypes.ResultTxSearch, error) { + transactions, err := GetTransactionsWithSync(rpcAddr, address, isOutbound) + if err != nil { + return tmTypes.ResultTxSearch{}, err + } + + for _, tx := range transactions.Txs { + hashToDirectionMap[tx.Hash.String()] = append(hashToDirectionMap[tx.Hash.String()], direction) + } + + cachedTxs.TotalCount += transactions.TotalCount + cachedTxs.Txs = append(cachedTxs.Txs, transactions.Txs...) + return *cachedTxs, nil +} diff --git a/gateway/interx/interx.tx_test.go b/gateway/interx/interx.tx_test.go index b4f32ce..b9cf0b4 100644 --- a/gateway/interx/interx.tx_test.go +++ b/gateway/interx/interx.tx_test.go @@ -2,12 +2,16 @@ package interx import ( "encoding/json" + "errors" + "fmt" + "io/ioutil" "net/http" "net/http/httptest" "os" "testing" "time" + "github.com/KiraCore/interx/common" "github.com/KiraCore/interx/config" "github.com/KiraCore/interx/database" "github.com/KiraCore/interx/test" @@ -96,7 +100,7 @@ func (suite *InterxTxTestSuite) TestBlockTransactionsHandler() { suite.Assert() } - resultTxSearch := TxsResponse{} + resultTxSearch := types.TxsResponse{} err = json.Unmarshal(suite.blockTransactionsQueryResponse.Result, &resultTxSearch) suite.Require().NoError(err) suite.Require().EqualValues(result.TotalCount, resultTxSearch.TotalCount) @@ -140,7 +144,7 @@ func TestInterxTxTestSuite(t *testing.T) { txMsg := make(map[string]interface{}) txMsg["type"] = "send" - resBytes, err = json.Marshal(TxsResponse{ + resBytes, err = json.Marshal(types.TxsResponse{ TotalCount: 1, Transactions: []types.TransactionResponse{ { @@ -229,3 +233,36 @@ func TestInterxTxTestSuite(t *testing.T) { tendermintServer.Close() } + +// Get block height for tx hash from cache or tendermint +func getBlockHeight(rpcAddr string, hash string) (int64, error) { + endpoint := fmt.Sprintf("%s/tx?hash=%s", rpcAddr, hash) + common.GetLogger().Info("[query-block] Entering block query: ", endpoint) + + resp, err := http.Get(endpoint) + if err != nil { + common.GetLogger().Error("[query-block] Unable to connect to ", endpoint) + return 0, err + } + defer resp.Body.Close() + + respBody, _ := ioutil.ReadAll(resp.Body) + response := new(tmJsonRPCTypes.RPCResponse) + + if err := json.Unmarshal(respBody, response); err != nil { + common.GetLogger().Error("[query-block] Unable to decode response: ", err) + return 0, err + } + if response.Error != nil { + common.GetLogger().Error("[query-block] Error response:", response.Error.Message) + return 0, errors.New(response.Error.Message) + } + + result := new(tmRPCTypes.ResultTx) + if err := tmjson.Unmarshal(response.Result, result); err != nil { + common.GetLogger().Error("[query-block] Failed to unmarshal result:", err) + return 0, fmt.Errorf("error unmarshalling result: %w", err) + } + + return result.Height, nil +} diff --git a/gateway/kira/gov_test.go b/gateway/kira/gov_test.go index 2b5471d..bb222a1 100644 --- a/gateway/kira/gov_test.go +++ b/gateway/kira/gov_test.go @@ -12,6 +12,7 @@ import ( "os" "testing" + "cosmossdk.io/math" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/KiraCore/interx/config" @@ -138,7 +139,7 @@ func (s *govServer) NetworkProperties(ctx context.Context, in *govTypes.NetworkP Properties: &govTypes.NetworkProperties{ MinTxFee: 777, MaxTxFee: 888, - VoteQuorum: 999, + VoteQuorum: math.LegacyNewDecWithPrec(33, 2), }, }, nil } diff --git a/gateway/kira/main.go b/gateway/kira/main.go index 4992465..ee4c5bf 100644 --- a/gateway/kira/main.go +++ b/gateway/kira/main.go @@ -1,6 +1,7 @@ package kira import ( + "github.com/KiraCore/interx/gateway/kira/metamask" "github.com/gorilla/mux" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" ) @@ -18,4 +19,5 @@ func RegisterRequest(router *mux.Router, gwCosmosmux *runtime.ServeMux, rpcAddr RegisterKiraSpendingRoutes(router, gwCosmosmux, rpcAddr) RegisterKiraUbiRoutes(router, gwCosmosmux, rpcAddr) RegisterKiraMultiStakingRoutes(router, gwCosmosmux, rpcAddr) + metamask.RegisterKiraMetamaskRoutes(router, gwCosmosmux, rpcAddr) } diff --git a/gateway/kira/metamask/cosmosaccountinfo.go b/gateway/kira/metamask/cosmosaccountinfo.go new file mode 100644 index 0000000..351fdce --- /dev/null +++ b/gateway/kira/metamask/cosmosaccountinfo.go @@ -0,0 +1,31 @@ +package metamask + +import ( + "net/http" + + "github.com/KiraCore/interx/common" + interxtypes "github.com/KiraCore/interx/types" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" +) + +func GetAccountInfo(params EthGetTransactionCount, gwCosmosmux *runtime.ServeMux, r *http.Request) (uint64, uint64, error) { + bech32Addr, err := hex2bech32(params.From, TypeKiraAddr) + if err != nil { + return 0, 0, err + } + + accountNumber, sequence := common.GetAccountNumberSequence(gwCosmosmux, r, bech32Addr) + + return accountNumber, sequence, nil +} + +func GetBalance(params EthGetBalanceParams, gwCosmosmux *runtime.ServeMux, r *http.Request) []interxtypes.Coin { + bech32Addr, err := hex2bech32(params.From, TypeKiraAddr) + if err != nil { + return nil + } + + balances := common.GetAccountBalances(gwCosmosmux, r.Clone(r.Context()), bech32Addr) + + return balances +} diff --git a/gateway/kira/metamask/cosmosblockinfo.go b/gateway/kira/metamask/cosmosblockinfo.go new file mode 100644 index 0000000..0fc2471 --- /dev/null +++ b/gateway/kira/metamask/cosmosblockinfo.go @@ -0,0 +1,162 @@ +package metamask + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/KiraCore/interx/common" + types "github.com/cometbft/cometbft/proto/tendermint/types" +) + +const ( + BlockQueryByNumber = iota + BLockQueryByHash +) + +type PartSetHeader struct { + Total uint32 `json:"total,omitempty"` + Hash []byte `json:"hash,omitempty"` +} + +type BlockID struct { + Hash string `json:"hash,omitempty"` + PartSetHeader PartSetHeader `json:"part_set_header"` +} + +type Consensus struct { + Block uint64 `json:"block,string,omitempty"` + App uint64 `json:"app,string,omitempty"` +} + +type Header struct { + // basic block info + Version Consensus `json:"version"` + ChainID string `json:"chain_id,omitempty"` + Height int64 `json:"height,string,omitempty"` + Time time.Time `json:"time"` + // prev block info + LastBlockId types.BlockID `json:"last_block_id"` + // hashes of block data + LastCommitHash string `json:"last_commit_hash,omitempty"` + DataHash string `json:"data_hash,omitempty"` + // hashes from the app output from the prev block + ValidatorsHash string `json:"validators_hash,omitempty"` + NextValidatorsHash string `json:"next_validators_hash,omitempty"` + ConsensusHash string `json:"consensus_hash,omitempty"` + AppHash string `json:"app_hash,omitempty"` + LastResultsHash string `json:"last_results_hash,omitempty"` + // consensus info + EvidenceHash string `json:"evidence_hash,omitempty"` + // proposer_address is the original block proposer address, formatted as a Bech32 string. + // In Tendermint, this type is `bytes`, but in the SDK, we convert it to a Bech32 string + // for better UX. + ProposerAddress string `json:"proposer_address,omitempty"` +} + +type Commit struct { + Height int64 `json:"height,string,omitempty"` + Round int32 `json:"round,omitempty"` + BlockID BlockID `json:"block_id"` + Signatures []types.CommitSig `json:"signatures"` +} + +type Data struct { + // Txs that will be applied by state @ block.Height+1. + // NOTE: not all txs here are valid. We're just agreeing on the order first. + // This means that block.AppHash does not include these txs. + Txs []string `protobuf:"bytes,1,rep,name=txs,proto3" json:"txs,omitempty"` +} + +type Block struct { + Header Header `json:"header"` + Data Data `json:"data"` + Evidence types.EvidenceList `json:"evidence"` + LastCommit Commit `json:"last_commit,omitempty"` +} + +type CosmosBlockInfo struct { + BlockId BlockID `json:"block_id,omitempty"` + // Since: cosmos-sdk 0.47 + SdkBlock Block `json:"block,omitempty"` +} + +func GetBlockNumber(rpcAddr string) (int, error) { + sentryStatus := common.GetKiraStatus((rpcAddr)) + currentHeight, err := strconv.Atoi(sentryStatus.SyncInfo.LatestBlockHeight) + return currentHeight, err +} + +func GetBlockByNumberOrHash(blockParam string, rpcAddr string, queryType int) (CosmosBlockInfo, []string, interface{}) { + var responseData, blockErr interface{} + var statusCode int + if queryType == BlockQueryByNumber { + var blockNum int64 + var err error + if strings.Contains(blockParam, "0x") { + blockNum, err = hex2int64(blockParam) + } else { + blockNum, err = strconv.ParseInt(blockParam, 10, 64) + } + if err != nil { + return CosmosBlockInfo{}, nil, err + } + + responseData, blockErr, statusCode = queryBlockByHeight(rpcAddr, strconv.Itoa(int(blockNum))) + } else if queryType == BLockQueryByHash { + if !strings.Contains(blockParam, "0x") { + blockParam = "0x" + blockParam + } + responseData, blockErr, statusCode = queryBlockByHash(rpcAddr, blockParam) + } + if blockErr != nil { + return CosmosBlockInfo{}, nil, blockErr + } + + if statusCode != 200 { + return CosmosBlockInfo{}, nil, fmt.Errorf("request faield, status code - %d", statusCode) + } + + jsonData, err := json.Marshal(responseData) + if err != nil { + return CosmosBlockInfo{}, nil, err + } + + response := CosmosBlockInfo{} + err = json.Unmarshal(jsonData, &response) + if err != nil { + return CosmosBlockInfo{}, nil, err + } + + txhashes := []string{} + txs := response.SdkBlock.Data.Txs + for _, txStr := range txs { + txBz, err := base64.StdEncoding.DecodeString(txStr) + if err != nil { + return CosmosBlockInfo{}, nil, err + } + converted := []byte(txBz) + hasher := sha256.New() + hasher.Write(converted) + txhashes = append(txhashes, "0x"+hex.EncodeToString(hasher.Sum(nil))) + } + + return response, txhashes, nil +} + +func queryBlockByHeight(rpcAddr string, height string) (interface{}, interface{}, int) { + success, err, statusCode := common.MakeTendermintRPCRequest(rpcAddr, "/block", fmt.Sprintf("height=%s", height)) + + return success, err, statusCode +} + +func queryBlockByHash(rpcAddr string, height string) (interface{}, interface{}, int) { + success, err, statusCode := common.MakeTendermintRPCRequest(rpcAddr, "/block_by_hash", fmt.Sprintf("hash=%s", height)) + + return success, err, statusCode +} diff --git a/gateway/kira/metamask/cosmostxinfo.go b/gateway/kira/metamask/cosmostxinfo.go new file mode 100644 index 0000000..82869a8 --- /dev/null +++ b/gateway/kira/metamask/cosmostxinfo.go @@ -0,0 +1,120 @@ +package metamask + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/KiraCore/interx/common" +) + +// Attribute is a struct that represents an attribute in the JSON data +type Attribute struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// Event is a struct that represents an event in the JSON data +type Event struct { + Type string `json:"type"` + Attributes []Attribute `json:"attributes"` +} + +// Msg is a struct that represents a message in the JSON data +type LogInfo struct { + MsgIndex int `json:"msg_index"` + Events []Event `json:"events"` +} + +// map[msgIndex][event_type][attribute_key] +type LogInfoForMap struct { + LogInfo map[int]map[string]map[string]string +} + +type Log struct { + logForMap map[int]LogInfoForMap + logForString string +} + +type TxInfo struct { + Hash string `json:"hash"` + Height string `json:"height"` + Index int `json:"index"` + TxResult struct { + Code int `json:"code"` + Data string `json:"data"` + Log string `json:"log"` + Info string `json:"info"` + GasWanted string `json:"gas_wanted"` + GasUsed string `json:"gas_used"` + Events []struct { + Type string `json:"type"` + Attributes []struct { + Key string `json:"key"` + Value string `json:"value"` + Index bool `json:"index"` + } `json:"attributes"` + } `json:"events"` + Codespace string `json:"codespace"` + } `json:"tx_result"` + Tx string `json:"tx"` +} + +func GetTxInfo(txHash string, rpcAddr string) (TxInfo, Log, interface{}) { + responseData, err, statusCode := queryTxByHash(rpcAddr, txHash) + logResult := Log{} + + if err != nil { + return TxInfo{}, logResult, err + } + + if statusCode != 200 { + return TxInfo{}, logResult, fmt.Errorf("request faield, status code - %d", statusCode) + } + + jsonData, err := json.Marshal(responseData) + if err != nil { + return TxInfo{}, logResult, err + } + + response := TxInfo{} + err = json.Unmarshal(jsonData, &response) + if err != nil { + return TxInfo{}, logResult, err + } + + var logInfos []LogInfo + + // Unmarshal the JSON data to the msgs slice + err = json.Unmarshal([]byte(response.TxResult.Log), &logInfos) + logResult.logForString = response.TxResult.Log + if err != nil { + return response, logResult, nil + } + + logInfosForMap := map[int]LogInfoForMap{} + for i, logInfo := range logInfos { + logInfosForMap[i] = LogInfoForMap{LogInfo: make(map[int]map[string]map[string]string)} + for _, event := range logInfo.Events { + if logInfosForMap[i].LogInfo[logInfo.MsgIndex] == nil { + logInfosForMap[i].LogInfo[logInfo.MsgIndex] = make(map[string]map[string]string) + } + if logInfosForMap[i].LogInfo[logInfo.MsgIndex][event.Type] == nil { + logInfosForMap[i].LogInfo[logInfo.MsgIndex][event.Type] = make(map[string]string) + } + for _, attribute := range event.Attributes { + logInfosForMap[i].LogInfo[logInfo.MsgIndex][event.Type][attribute.Key] = attribute.Value + } + } + } + + logResult.logForMap = logInfosForMap + return response, logResult, nil +} + +func queryTxByHash(rpcAddr string, hash string) (interface{}, interface{}, int) { + if !strings.HasPrefix(hash, "0x") { + hash = "0x" + hash + } + return common.MakeTendermintRPCRequest(rpcAddr, "/tx", fmt.Sprintf("hash=%s", hash)) +} diff --git a/gateway/kira/metamask/cosmostxsender.go b/gateway/kira/metamask/cosmostxsender.go new file mode 100644 index 0000000..a35d49d --- /dev/null +++ b/gateway/kira/metamask/cosmostxsender.go @@ -0,0 +1,589 @@ +package metamask + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/KiraCore/interx/config" + tx "github.com/KiraCore/interx/proto-gen/cosmos/tx/v1beta1" + custodytypes "github.com/KiraCore/sekai/x/custody/types" + evidencetypes "github.com/KiraCore/sekai/x/evidence/types" + customgovtypes "github.com/KiraCore/sekai/x/gov/types" + multistakingtypes "github.com/KiraCore/sekai/x/multistaking/types" + customslashingtypes "github.com/KiraCore/sekai/x/slashing/types" + spendingtypes "github.com/KiraCore/sekai/x/spending/types" + customstakingtypes "github.com/KiraCore/sekai/x/staking/types" + tokenstypes "github.com/KiraCore/sekai/x/tokens/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" +) + +const ( + MsgBankSend = iota + // customgov + MsgSubmitEvidence + MsgSubmitProposal + MsgVoteProposal + MsgRegisterIdentityRecords + MsgDeleteIdentityRecords + MsgRequestIdentityRecordsVerify + MsgHandleIdentityRecordsVerifyRequest + MsgCancelIdentityRecordsVerifyRequest + MsgSetNetworkProperties + MsgSetExecutionFee + MsgClaimCouncilor + MsgWhitelistPermissions + MsgBlacklistPermissions + MsgCreateRole + MsgAssignRole + MsgUnassignRole + MsgWhitelistRolePermission + MsgBlacklistRolePermission + MsgRemoveWhitelistRolePermission + MsgRemoveBlacklistRolePermission + // staking + MsgClaimValidator + // tokens + MsgUpsertTokenAlias + MsgUpsertTokenRate + // slashing + MsgActivate + MsgPause + MsgUnpause + // spending + MsgCreateSpendingPool + MsgDepositSpendingPool + MsgRegisterSpendingPoolBeneficiary + MsgClaimSpendingPool + // multistaking + MsgUpsertStakingPool + MsgDelegate + MsgUndelegate + MsgClaimRewards + MsgClaimUndelegation + MsgSetCompoundInfo + MsgRegisterDelegator + // custody module + MsgCreateCustody + MsgAddToCustodyWhiteList + MsgAddToCustodyCustodians + MsgRemoveFromCustodyCustodians + MsgDropCustodyCustodians + MsgRemoveFromCustodyWhiteList + MsgDropCustodyWhiteList + MsgApproveCustodyTx + MsgDeclineCustodyTx +) + +var grpcConn *grpc.ClientConn + +type ListRoleParam struct { + RoleIdentifier string `json:"role_identifier"` + Permission uint32 `json:"permission"` +} + +type PermissionsParam struct { + Permission uint32 `json:"permission"` + ControlledAddr string `json:"controlled_addr"` +} + +type RoleParam struct { + RoleId uint32 `json:"role_id"` + Controller string `json:"controller"` +} + +type DelegateParam struct { + Amounts string `json:"amounts"` + To string `json:"to"` +} + +type CustodianParam struct { + NewAddrs []string `json:"new_addrs"` + OldKey string `json:"old_key"` + NewKey string `json:"new_key"` + Next string `json:"next"` + Target string `json:"target"` +} + +type SingleCustodianParam struct { + NewAddr string `json:"new_addr"` + OldKey string `json:"old_key"` + NewKey string `json:"new_key"` + Next string `json:"next"` + Target string `json:"target"` +} + +type CustodyParam struct { + To string `json:"to"` + Hash string `json:"hash"` +} + +type SpendingPoolParam struct { + Name string `json:"name"` + Amounts string `json:"amounts"` +} + +// decode 256bit param like bool, uint, hex-typed address etc +func Decode256Bit(data *[]byte, params *[][]byte) error { + if len(*data) < 32 { + return errors.New("decoding 256bit failed, not enough length") + } + *params = append(*params, (*data)[:32]) + *data = (*data)[32:] + + return nil +} + +// decode string-typed param +// structure: +// * offset - offset of the string in the data : 32byte +// * length - length of the string : 32byte +// * content - content of the string : (length/32+1)*32byte +func DecodeString(data *[]byte, params *[][]byte) error { + // offset := data[:32] // string value offset + *data = (*data)[32:] + + length, err := bytes2uint64((*data)[:32]) + if err != nil { + return err + } + *data = (*data)[32:] + + *params = append(*params, (*data)[:length]) + *data = (*data)[(length/32+1)*32:] + return nil +} + +func DecodeParam(data []byte, txType int) ([][]byte, error) { + if txType == MsgBankSend { + return nil, nil + } + + var params [][]byte + + // decode data field v, r, s, sender + for i := 0; i < 4; i++ { + err := Decode256Bit(&data, ¶ms) + if err != nil { + return nil, err + } + } + + // decode param string + err := DecodeString(&data, ¶ms) + + return params, err +} + +func sendTx(txRawData string, gwCosmosmux *runtime.ServeMux, r *http.Request) (string, error) { + byteData, err := hex.DecodeString(txRawData[2:]) + if err != nil { + return "", err + } + + ethTxData, err := GetEthTxInfo(byteData) + if err != nil { + return "", err + } + + txBytes, err := SignTx(ethTxData, byteData, gwCosmosmux, r) + if err != nil { + return "", err + } + + txHash, err := sendCosmosTx(r.Context(), txBytes) + if err != nil { + return "", err + } + + return txHash, nil +} + +func getTxType(txData []byte) (int, error) { + submitEvidencePrefix, _ := hex.DecodeString("85db2453") + submitProposalPrefix, _ := hex.DecodeString("00000000") + voteProposalPrefix, _ := hex.DecodeString("7f1f06dc") + registerIdentityRecordsPrefix, _ := hex.DecodeString("bc05f106") + deleteIdentityRecordsPrefix, _ := hex.DecodeString("2dbad8e8") + requestIdentityRecordsVerifyPrefix, _ := hex.DecodeString("9765358e") + handleIdentityRecordsVerifyRequestPrefix, _ := hex.DecodeString("4335ed0c") + cancelIdentityRecordsVerifyRequestPrefix, _ := hex.DecodeString("eeaa0488") + setNetworkPropertiesPrefix, _ := hex.DecodeString("f8060fb5") + setExecutionFeePrefix, _ := hex.DecodeString("c7586de8") + claimCouncilorPrefix, _ := hex.DecodeString("b7b8ff46") + whitelistPermissionsPrefix, _ := hex.DecodeString("2f313ab8") + blacklistPermissionsPrefix, _ := hex.DecodeString("3864f845") + createRolePrefix, _ := hex.DecodeString("2d8abfdf") + assignRolePrefix, _ := hex.DecodeString("fcc121b5") + unassignRolePrefix, _ := hex.DecodeString("c79ca19d") + whitelistRolePermissionPrefix, _ := hex.DecodeString("59472362") + blacklistRolePermissionPrefix, _ := hex.DecodeString("99c557da") + removeWhitelistRolePermissionPrefix, _ := hex.DecodeString("2a11d702") + removeBlacklistRolePermissionPrefix, _ := hex.DecodeString("f5f865e4") + claimValidatorPrefix, _ := hex.DecodeString("00000000") + upsertTokenAliasPrefix, _ := hex.DecodeString("f69a4787") + upsertTokenRatePrefix, _ := hex.DecodeString("3b30a97a") + activatePrefix, _ := hex.DecodeString("a1374dc2") + pausePrefix, _ := hex.DecodeString("1371cf19") + unpausePrefix, _ := hex.DecodeString("b9179894") + createSpendingPoolPrefix, _ := hex.DecodeString("4ed8a0a2") + depositSpendingPoolPrefix, _ := hex.DecodeString("e10c925c") + registerSpendingPoolBeneficiaryPrefix, _ := hex.DecodeString("7ab7eecf") + claimSpendingPoolPrefix, _ := hex.DecodeString("efeed4a0") + upsertStakingPoolPrefix, _ := hex.DecodeString("fb24f5cc") + delegatePrefix, _ := hex.DecodeString("4b193c09") + undelegatePrefix, _ := hex.DecodeString("94574f0c") + claimRewardsPrefix, _ := hex.DecodeString("9838bc2f") + claimUndelegationPrefix, _ := hex.DecodeString("2f608d76") + setCompoundInfoPrefix, _ := hex.DecodeString("e2d6a093") + registerDelegatorPrefix, _ := hex.DecodeString("99db185d") + createCustodyPrefix, _ := hex.DecodeString("bebde6d1") + addToCustodyWhiteListPrefix, _ := hex.DecodeString("25a1d834") + addToCustodyCustodiansPrefix, _ := hex.DecodeString("8c7fdb91") + removeFromCustodyCustodiansPrefix, _ := hex.DecodeString("90be51cf") + dropCustodyCustodiansPrefix, _ := hex.DecodeString("0ca697b4") + removeFromCustodyWhiteListPrefix, _ := hex.DecodeString("fa431c3e") + dropCustodyWhiteListPrefix, _ := hex.DecodeString("bc65010a") + approveCustodyTxPrefix, _ := hex.DecodeString("5da292d4") + declineCustodyTxPrefix, _ := hex.DecodeString("dce4399a") + + var msgType int + switch { + case txData == nil: + msgType = MsgBankSend + case len(txData) == 0: + msgType = MsgBankSend + case bytes.Equal(txData, delegatePrefix): + msgType = MsgDelegate + case bytes.Equal(txData, undelegatePrefix): + msgType = MsgUndelegate + case bytes.Equal(txData, submitEvidencePrefix): + msgType = MsgSubmitEvidence + case bytes.Equal(txData, submitProposalPrefix): + msgType = MsgSubmitProposal + case bytes.Equal(txData, voteProposalPrefix): + msgType = MsgVoteProposal + case bytes.Equal(txData, registerIdentityRecordsPrefix): + msgType = MsgRegisterIdentityRecords + case bytes.Equal(txData, deleteIdentityRecordsPrefix): + msgType = MsgDeleteIdentityRecords + case bytes.Equal(txData, requestIdentityRecordsVerifyPrefix): + msgType = MsgRequestIdentityRecordsVerify + case bytes.Equal(txData, handleIdentityRecordsVerifyRequestPrefix): + msgType = MsgHandleIdentityRecordsVerifyRequest + case bytes.Equal(txData, cancelIdentityRecordsVerifyRequestPrefix): + msgType = MsgCancelIdentityRecordsVerifyRequest + case bytes.Equal(txData, setNetworkPropertiesPrefix): + msgType = MsgSetNetworkProperties + case bytes.Equal(txData, setExecutionFeePrefix): + msgType = MsgSetExecutionFee + case bytes.Equal(txData, claimCouncilorPrefix): + msgType = MsgClaimCouncilor + case bytes.Equal(txData, whitelistPermissionsPrefix): + msgType = MsgWhitelistPermissions + case bytes.Equal(txData, blacklistPermissionsPrefix): + msgType = MsgBlacklistPermissions + case bytes.Equal(txData, createRolePrefix): + msgType = MsgCreateRole + case bytes.Equal(txData, assignRolePrefix): + msgType = MsgAssignRole + case bytes.Equal(txData, unassignRolePrefix): + msgType = MsgUnassignRole + case bytes.Equal(txData, whitelistRolePermissionPrefix): + msgType = MsgWhitelistRolePermission + case bytes.Equal(txData, blacklistRolePermissionPrefix): + msgType = MsgBlacklistRolePermission + case bytes.Equal(txData, removeWhitelistRolePermissionPrefix): + msgType = MsgRemoveWhitelistRolePermission + case bytes.Equal(txData, removeBlacklistRolePermissionPrefix): + msgType = MsgBlacklistRolePermission + case bytes.Equal(txData, claimValidatorPrefix): + msgType = MsgClaimValidator + case bytes.Equal(txData, upsertTokenAliasPrefix): + msgType = MsgUpsertTokenAlias + case bytes.Equal(txData, upsertTokenRatePrefix): + msgType = MsgUpsertTokenRate + case bytes.Equal(txData, activatePrefix): + msgType = MsgActivate + case bytes.Equal(txData, pausePrefix): + msgType = MsgPause + case bytes.Equal(txData, unpausePrefix): + msgType = MsgUnpause + case bytes.Equal(txData, createSpendingPoolPrefix): + msgType = MsgCreateSpendingPool + case bytes.Equal(txData, depositSpendingPoolPrefix): + msgType = MsgDepositSpendingPool + case bytes.Equal(txData, registerSpendingPoolBeneficiaryPrefix): + msgType = MsgRegisterSpendingPoolBeneficiary + case bytes.Equal(txData, claimSpendingPoolPrefix): + msgType = MsgClaimSpendingPool + case bytes.Equal(txData, upsertStakingPoolPrefix): + msgType = MsgUpsertStakingPool + case bytes.Equal(txData, claimRewardsPrefix): + msgType = MsgClaimRewards + case bytes.Equal(txData, claimUndelegationPrefix): + msgType = MsgClaimUndelegation + case bytes.Equal(txData, setCompoundInfoPrefix): + msgType = MsgSetCompoundInfo + case bytes.Equal(txData, registerDelegatorPrefix): + msgType = MsgRegisterDelegator + case bytes.Equal(txData, createCustodyPrefix): + msgType = MsgCreateCustody + case bytes.Equal(txData, addToCustodyWhiteListPrefix): + msgType = MsgAddToCustodyWhiteList + case bytes.Equal(txData, addToCustodyCustodiansPrefix): + msgType = MsgAddToCustodyCustodians + case bytes.Equal(txData, removeFromCustodyCustodiansPrefix): + msgType = MsgRemoveFromCustodyCustodians + case bytes.Equal(txData, dropCustodyCustodiansPrefix): + msgType = MsgDropCustodyCustodians + case bytes.Equal(txData, removeFromCustodyWhiteListPrefix): + msgType = MsgRemoveFromCustodyWhiteList + case bytes.Equal(txData, dropCustodyWhiteListPrefix): + msgType = MsgDropCustodyWhiteList + case bytes.Equal(txData, approveCustodyTxPrefix): + msgType = MsgApproveCustodyTx + case bytes.Equal(txData, declineCustodyTxPrefix): + msgType = MsgDeclineCustodyTx + default: + return 0, errors.New("no such functions") + } + return msgType, nil +} + +func getInstanceOfTx(txType int) cosmostypes.Msg { + switch txType { + case MsgSubmitEvidence: + return &evidencetypes.MsgSubmitEvidence{} + case MsgSubmitProposal: + return &customgovtypes.MsgSubmitProposal{} + case MsgVoteProposal: + return &customgovtypes.MsgVoteProposal{} + case MsgRegisterIdentityRecords: + return &customgovtypes.MsgRegisterIdentityRecords{} + case MsgDeleteIdentityRecords: + return &customgovtypes.MsgDeleteIdentityRecords{} + case MsgRequestIdentityRecordsVerify: + return &customgovtypes.MsgRequestIdentityRecordsVerify{} + case MsgHandleIdentityRecordsVerifyRequest: + return &customgovtypes.MsgHandleIdentityRecordsVerifyRequest{} + case MsgCancelIdentityRecordsVerifyRequest: + return &customgovtypes.MsgCancelIdentityRecordsVerifyRequest{} + case MsgSetNetworkProperties: + return &customgovtypes.MsgSetNetworkProperties{} + case MsgSetExecutionFee: + return &customgovtypes.MsgSetExecutionFee{} + case MsgClaimCouncilor: + return &customgovtypes.MsgClaimCouncilor{} + case MsgWhitelistPermissions: + return &customgovtypes.MsgWhitelistPermissions{} + case MsgBlacklistPermissions: + return &customgovtypes.MsgBlacklistPermissions{} + case MsgCreateRole: + return &customgovtypes.MsgCreateRole{} + case MsgAssignRole: + return &customgovtypes.MsgAssignRole{} + case MsgUnassignRole: + return &customgovtypes.MsgUnassignRole{} + case MsgWhitelistRolePermission: + return &customgovtypes.MsgWhitelistRolePermission{} + case MsgBlacklistRolePermission: + return &customgovtypes.MsgBlacklistRolePermission{} + case MsgRemoveWhitelistRolePermission: + return &customgovtypes.MsgRemoveWhitelistRolePermission{} + case MsgRemoveBlacklistRolePermission: + return &customgovtypes.MsgRemoveBlacklistRolePermission{} + case MsgClaimValidator: + return &customstakingtypes.MsgClaimValidator{} + case MsgUpsertTokenAlias: + return &tokenstypes.MsgUpsertTokenAlias{} + case MsgUpsertTokenRate: + return &tokenstypes.MsgUpsertTokenRate{} + case MsgActivate: + return &customslashingtypes.MsgActivate{} + case MsgPause: + return &customslashingtypes.MsgPause{} + case MsgUnpause: + return &customslashingtypes.MsgUnpause{} + case MsgCreateSpendingPool: + return &spendingtypes.MsgCreateSpendingPool{} + case MsgDepositSpendingPool: + return &spendingtypes.MsgDepositSpendingPool{} + case MsgRegisterSpendingPoolBeneficiary: + return &spendingtypes.MsgRegisterSpendingPoolBeneficiary{} + case MsgClaimSpendingPool: + return &spendingtypes.MsgClaimSpendingPool{} + case MsgUpsertStakingPool: + return &multistakingtypes.MsgUpsertStakingPool{} + case MsgDelegate: + return &multistakingtypes.MsgDelegate{} + case MsgUndelegate: + return &multistakingtypes.MsgUndelegate{} + case MsgClaimRewards: + return &multistakingtypes.MsgClaimRewards{} + case MsgClaimUndelegation: + return &multistakingtypes.MsgClaimUndelegation{} + case MsgSetCompoundInfo: + return &multistakingtypes.MsgSetCompoundInfo{} + case MsgRegisterDelegator: + return &multistakingtypes.MsgRegisterDelegator{} + case MsgCreateCustody: + return &custodytypes.MsgCreateCustodyRecord{} + case MsgAddToCustodyWhiteList: + return &custodytypes.MsgAddToCustodyWhiteList{} + case MsgAddToCustodyCustodians: + return &custodytypes.MsgAddToCustodyCustodians{} + case MsgRemoveFromCustodyCustodians: + return &custodytypes.MsgRemoveFromCustodyCustodians{} + case MsgDropCustodyCustodians: + return &custodytypes.MsgDropCustodyCustodians{} + case MsgRemoveFromCustodyWhiteList: + return &custodytypes.MsgRemoveFromCustodyWhiteList{} + case MsgDropCustodyWhiteList: + return &custodytypes.MsgDropCustodyWhiteList{} + case MsgApproveCustodyTx: + return &custodytypes.MsgApproveCustodyTransaction{} + case MsgDeclineCustodyTx: + return &custodytypes.MsgDeclineCustodyTransaction{} + default: + return nil + } +} + +func SignTx(ethTxData EthTxData, ethTxBytes []byte, gwCosmosmux *runtime.ServeMux, r *http.Request) ([]byte, error) { + // Create a new TxBuilder. + txBuilder := config.EncodingCg.TxConfig.NewTxBuilder() + + addr1, err := hex2bech32(ethTxData.From, TypeKiraAddr) + if err != nil { + return nil, err + } + + var msg cosmostypes.Msg = &tokenstypes.MsgEthereumTx{ + TxType: "NativeSend", + Sender: addr1, + Hash: ethTxData.Hash, + Data: ethTxBytes, + } + + var signature []byte + if len(ethTxData.Data) >= 4 { + txType, err := getTxType(ethTxData.Data[:4]) + if err != nil { + return nil, err + } + params, err := DecodeParam(ethTxData.Data[4:], txType) + if err != nil { + return nil, err + } + if len(params) < 5 { + return nil, errors.New("insufficient number of params") + } + + v, r, s := params[0][len(params[0])-1:], params[1], params[2] + signature = getSignatureV2(r, s, v) + + msg = getInstanceOfTx(txType) + if msg == nil { + return nil, fmt.Errorf("unrecognized transaction type: %d", txType) + } + err = json.Unmarshal(params[4], &msg) + if err != nil { + return nil, err + } + } + + // fmt.Println(msg) + err = txBuilder.SetMsgs(msg) + if err != nil { + return nil, err + } + txBuilder.SetGasLimit(ethTxData.GasLimit) + // TODO: set fee amount - how can I get the fee amount from eth tx? or fix this? + txBuilder.SetFeeAmount(cosmostypes.NewCoins(cosmostypes.NewInt64Coin(config.DefaultKiraDenom, 200))) + // txBuilder.SetMemo() + // txBuilder.SetTimeoutHeight() + // txHash, err := sendTx(context.Background(), byteData) + + privs := []cryptotypes.PrivKey{config.Config.PrivKey} + accSeqs := []uint64{ethTxData.Nonce} // The accounts' sequence numbers + + // First round: gather all the signer infos + var sigsV2 []signing.SignatureV2 + for i, priv := range privs { + sigV2 := signing.SignatureV2{ + PubKey: priv.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: config.EncodingCg.TxConfig.SignModeHandler().DefaultMode(), + Signature: signature, + }, + Sequence: accSeqs[i], + } + + sigsV2 = append(sigsV2, sigV2) + } + + err = txBuilder.SetSignatures(sigsV2...) + if err != nil { + return nil, err + } + + txBytes, err := config.EncodingCg.TxConfig.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return nil, err + } + + return txBytes, err +} + +func sendCosmosTx(ctx context.Context, txBytes []byte) (string, error) { + // --snip-- + + // Create a connection to the gRPC server. + if grpcConn == nil { + var err error + + grpcConn, err = grpc.Dial( + "127.0.0.1:9090", // Or your gRPC server address. + grpc.WithInsecure(), // The Cosmos SDK doesn't support any transport security mechanism. + ) + + if err != nil { + return "", err + } + + // defer grpcConn.Close() + } + + // Broadcast the tx via gRPC. We create a new client for the Protobuf Tx + // service. + txClient := tx.NewServiceClient(grpcConn) + // We then call the BroadcastTx method on this client. + grpcRes, err := txClient.BroadcastTx( + ctx, + &tx.BroadcastTxRequest{ + Mode: tx.BroadcastMode_BROADCAST_MODE_SYNC, + TxBytes: txBytes, // Proto-binary of the signed tx, see previous step. + }, + ) + + if err != nil { + return "", err + } + + // fmt.Println(grpcRes.TxResponse) + + if grpcRes.TxResponse.Code != 0 { + return "", errors.New(fmt.Sprintln("send tx failed - result code: ", grpcRes.TxResponse.Code, grpcRes.TxResponse.RawLog)) + } + + return grpcRes.TxResponse.TxHash, nil +} diff --git a/gateway/kira/metamask/ethtxgenerator.go b/gateway/kira/metamask/ethtxgenerator.go new file mode 100644 index 0000000..1204d7d --- /dev/null +++ b/gateway/kira/metamask/ethtxgenerator.go @@ -0,0 +1,112 @@ +package metamask + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +// LogsBloom is a struct that represents a logsBloom field +type LogsBloom struct { + m int // size of the bit array + bits []bool // bit array +} + +// NewLogsBloom creates a new logsBloom field with the given size +func NewLogsBloom(m int) *LogsBloom { + return &LogsBloom{ + m: m, + bits: make([]bool, m), + } +} + +// hash is a helper function that hashes a byte slice using keccak256 algorithm +func hash(b []byte) []byte { + return crypto.Keccak256(b) +} + +// Add adds an element to the logsBloom field +func (lb *LogsBloom) Add(b []byte) { + h := hash(b) + for i := 0; i < 3; i++ { + // extract 11-bit segments from the first, third, and fifth 16-bit words of the hash + // https://github.com/ethereum/wiki/wiki/Design-Rationale#bloom-filter + seg := new(big.Int).SetBytes(h[i*2 : i*2+2]) + seg.Rsh(seg, uint(4-i)) + seg.And(seg, big.NewInt(2047)) + lb.bits[seg.Int64()] = true + } +} + +// Contains checks if an element is in the logsBloom field +func (lb *LogsBloom) Contains(b []byte) bool { + h := hash(b) + for i := 0; i < 3; i++ { + seg := new(big.Int).SetBytes(h[i*2 : i*2+2]) + seg.Rsh(seg, uint(4-i)) + seg.And(seg, big.NewInt(2047)) + if !lb.bits[seg.Int64()] { + return false + } + } + return true +} + +// Or performs a bitwise OR operation with another logsBloom field +func (lb *LogsBloom) Or(other *LogsBloom) { + for i := 0; i < lb.m; i++ { + lb.bits[i] = lb.bits[i] || other.bits[i] + } +} + +// CreateLogsBloomFromLogs creates a logsBloom field from a slice of ethereum tx logs +func CreateLogsBloomFromLogs(logs []*types.Log, m int) *LogsBloom { + lb := NewLogsBloom(m) + for _, log := range logs { + // add the topics and data to the logsBloom field + for _, topic := range log.Topics { + lb.Add(topic.Bytes()) + } + lb.Add(log.Data) + } + return lb +} + +func boolsToBytes(bools []bool) []byte { + bi := new(big.Int) // create a new big.Int + for i, b := range bools { + if b { + bi.SetBit(bi, i, 1) // set the ith bit to 1 + } + } + return bi.Bytes() // return the byte representation +} + +// func convertCosmosLog2EvmLog(logInfos []LogInfo) []types.Log { + +// var logs []types.Log + +// types.LegacyTx + +// for _, logInfo := range logInfos { +// log := &types.Log{ +// Address +// Topics: []common.Hash{ +// common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), +// common.HexToHash("0x000000000000000000000000" + bech322hex(logInfo.Events[0].Attributes[1])), +// common.HexToHash("0x000000000000000000000000"), +// }, +// Data: common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000000"), +// } + +// logs = append(logs, *log) +// } +// } + +func GetLogsBloom(logs []*types.Log) []byte { + // create a logsBloom field from the log with 2048 bits + lb := CreateLogsBloomFromLogs(logs, 2048) + + return boolsToBytes(lb.bits) +} diff --git a/gateway/kira/metamask/ethtxinfo.go b/gateway/kira/metamask/ethtxinfo.go new file mode 100644 index 0000000..fdecb00 --- /dev/null +++ b/gateway/kira/metamask/ethtxinfo.go @@ -0,0 +1,90 @@ +package metamask + +import ( + "errors" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" +) + +type EthTxData struct { + Nonce uint64 `json:"nonce"` + GasPrice big.Int `json:"gas_price"` + GasLimit uint64 `json:"gas_limit"` + From string `json:"from"` + To string `json:"to"` + Value big.Int `json:"value"` + Data []byte `json:"data"` + Hash string `json:"hash"` + V big.Int `json:"signature_v"` + R big.Int `json:"signature_r"` + S big.Int `json:"signature_s"` +} + +func GetSenderAddrFromRawTxBytes(rawTxBytes []byte) (common.Address, error) { + var rawTx ethtypes.Transaction + if err := rlp.DecodeBytes(rawTxBytes, &rawTx); err != nil { + return common.Address{}, err + } + + signer := ethtypes.NewEIP155Signer(rawTx.ChainId()) + sender, err := signer.Sender(&rawTx) + if err != nil { + return common.Address{}, err + } + return sender, nil +} + +func validateTx(rawTxBytes []byte, sender common.Address) bool { + senderFromTx, err := GetSenderAddrFromRawTxBytes(rawTxBytes) + if err != nil { + return false + } + + if senderFromTx.Hex() == sender.Hex() { + return true + } + return false +} + +// SendRawTransaction send a raw Ethereum transaction. +func GetEthTxInfo(data hexutil.Bytes) (EthTxData, error) { + tx := new(ethtypes.Transaction) + rlp.DecodeBytes(data, &tx) + + msg, err := tx.AsMessage(ethtypes.NewEIP155Signer(tx.ChainId()), big.NewInt(1)) + if err != nil { + log.Fatal(err) + return EthTxData{}, errors.New("decoding transaction data is failed") + } + + v, r, s := tx.RawSignatureValues() + + // check the local node config in case unprotected txs are disabled + if !tx.Protected() { + // Ensure only eip155 signed transactions are submitted if EIP155Required is set. + return EthTxData{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") + } + + if !validateTx(data, msg.From()) { + return EthTxData{}, errors.New("validation is failed") + } + + return EthTxData{ + Nonce: tx.Nonce(), + GasPrice: *tx.GasPrice(), + GasLimit: tx.Gas(), + From: msg.From().String(), + To: msg.To().String(), + Value: *tx.Value(), + Data: tx.Data(), + Hash: tx.Hash().Hex(), + V: *v, + R: *r, + S: *s, + }, nil +} diff --git a/gateway/kira/metamask/metamask.go b/gateway/kira/metamask/metamask.go new file mode 100644 index 0000000..41b9ad8 --- /dev/null +++ b/gateway/kira/metamask/metamask.go @@ -0,0 +1,334 @@ +package metamask + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math" + "math/big" + "net/http" + + "github.com/KiraCore/interx/common" + "github.com/KiraCore/interx/config" + + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" +) + +type EthMetamaskRequestBody struct { + ID interface{} `json:"id"` + Method string `json:"method"` + JsonRPC string `json:"jsonrpc"` + Params []interface{} `json:"params"` + // Add other fields from the request body if needed +} + +type EthMetamaskResponseBody struct { + ID interface{} `json:"id"` + JsonRPC string `json:"jsonrpc"` + Result interface{} `json:"result"` + // Add other fields from the request body if needed +} + +type EthEstimateGas struct { + From string `json:"from"` + Value string `json:"value"` + GasPrice string `json:"gasPrice"` + Data string `json:"data"` + To string `json:"to"` +} + +// balance of the account of given address +type EthGetBalanceParams struct { + From string + // integer block number, or the string "latest", "earliest" or "pending" + + // defaultBlock parameter: + // HEX String - an integer block number + // String "earliest" for the earliest/genesis block + // String "latest" - for the latest mined block + // String "safe" - for the latest safe head block + // String "finalized" - for the latest finalized block + // String "pending" - for the pending state/transactions + BlockNum string +} + +type EthGetBlockNumber struct { + BlockNum string + // If true it returns the full transaction objects, if false only the hashes of the transactions + Filter bool +} + +// number of transactions sent from an address. +type EthGetTransactionCount struct { + From string + // integer block number, or the string "latest", "earliest" or "pending" + BlockNum string +} + +func RegisterKiraMetamaskRoutes(r *mux.Router, gwCosmosmux *runtime.ServeMux, rpcAddr string) { + r.HandleFunc(config.MetamaskEndpoint, MetamaskRequestHandler(gwCosmosmux, rpcAddr)).Methods("POST") + // r.HandleFunc(config.RegisterAddrEndpoint, AddressRegisterHandler(gwCosmosmux, rpcAddr)).Methods("Get") + + common.AddRPCMethod("POST", config.MetamaskEndpoint, "This is an API to interact with metamask.", true) + // common.AddRPCMethod("POST", config.MetamaskEndpoint, "This is an API to interact with metamask.", true) +} + +func MetamaskRequestHandler(gwCosmosmux *runtime.ServeMux, rpcAddr string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + byteData, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Unmarshal the request body into a struct + var requestBody EthMetamaskRequestBody + err = json.Unmarshal(byteData, &requestBody) + if err != nil { + fmt.Println("unmarshal err:", err, byteData) + http.Error(w, "Failed to parse request body", http.StatusBadRequest) + return + } + + fmt.Println("eth method:", requestBody.Method) + // fmt.Println("eth params:", requestBody.Params) + + response := map[string]interface{}{ + "id": requestBody.ID, + "jsonrpc": requestBody.JsonRPC, + } + + var result interface{} + + switch requestBody.Method { + case "eth_chainId": + result = fmt.Sprintf("0x%x", config.DefaultChainID) + case "eth_getBalance": + params := EthGetBalanceParams{ + From: requestBody.Params[0].(string), + BlockNum: requestBody.Params[1].(string), + } + + balances := GetBalance(params, gwCosmosmux, r) + + result = "0x0" + if len(balances) > 0 { + for _, coin := range balances { + if coin.Denom == config.DefaultKiraDenom { + balance := new(big.Int) + balance.SetString(coin.Amount, 10) + balance = balance.Mul(balance, big.NewInt(int64(math.Pow10(12)))) + result = fmt.Sprintf("0x%x", balance) + } + } + } + case "eth_blockNumber": + currentHeight, err := GetBlockNumber(rpcAddr) + + if err != nil { + fmt.Println("eth_getBlockByNumber err:", err) + return + } + + result = fmt.Sprintf("0x%x", currentHeight) + case "net_version": + result = config.DefaultChainID + case "eth_getBlockByNumber": + blockNum := requestBody.Params[0].(string) + response, txs, err := GetBlockByNumberOrHash(blockNum, rpcAddr, BlockQueryByNumber) + if err != nil { + fmt.Println("eth_getBlockByNumber err:", err) + return + } + + result = map[string]interface{}{ + "extraData": "0x737061726b706f6f6c2d636e2d6e6f64652d3132", + "gasLimit": "0x1c9c380", + "gasUsed": "0x79ccd3", + "hash": "0x" + response.BlockId.Hash, + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner": "0x" + response.SdkBlock.Header.ProposerAddress, + // "mixHash": "0x3d1fdd16f15aeab72e7db1013b9f034ee33641d92f71c0736beab4e67d34c7a7", + // "nonce": "0x4db7a1c01d8a8072", + "number": "0x" + fmt.Sprintf("%x", response.SdkBlock.Header.Height), + "parentHash": "0x" + hex.EncodeToString(response.SdkBlock.Header.LastBlockId.Hash), + "receiptsRoot": "0x" + response.SdkBlock.Header.DataHash, + "size": "0x41c7", + "stateRoot": "0xddc8b0234c2e0cad087c8b389aa7ef01f7d79b2570bccb77ce48648aa61c904d", + "timestamp": fmt.Sprintf("0x%x", response.SdkBlock.Header.Time.UnixMilli()), + "totalDifficulty": "0x12ac11391a2f3872fcd", + "transactions": txs, + "transactionsRoot": "0x" + response.SdkBlock.Header.DataHash, + "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncles": []string{}, + } + case "eth_getBlockByHash": + blockHash := requestBody.Params[0].(string) + + response, txs, err := GetBlockByNumberOrHash(blockHash, rpcAddr, BLockQueryByHash) + if err != nil { + fmt.Println("eth_getBlockByNumber err:", err) + return + } + + result = map[string]interface{}{ + "extraData": "0x737061726b706f6f6c2d636e2d6e6f64652d3132", + "gasLimit": "0x1c9c380", + "gasUsed": "0x79ccd3", + "hash": "0x" + response.BlockId.Hash, + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner": "0x" + response.SdkBlock.Header.ProposerAddress, + // "mixHash": "0x3d1fdd16f15aeab72e7db1013b9f034ee33641d92f71c0736beab4e67d34c7a7", + // "nonce": "0x4db7a1c01d8a8072", + "number": "0x" + fmt.Sprintf("%x", response.SdkBlock.Header.Height), + "parentHash": "0x" + hex.EncodeToString(response.SdkBlock.Header.LastBlockId.Hash), + "receiptsRoot": "0x" + response.SdkBlock.Header.DataHash, + "size": "0x41c7", + "stateRoot": "0xddc8b0234c2e0cad087c8b389aa7ef01f7d79b2570bccb77ce48648aa61c904d", + "timestamp": fmt.Sprintf("0x%x", response.SdkBlock.Header.Time.UnixMilli()), + "totalDifficulty": "0x12ac11391a2f3872fcd", + "transactions": txs, + "transactionsRoot": "0x" + response.SdkBlock.Header.DataHash, + "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncles": []string{}, + } + + file, _ := json.MarshalIndent(result, "", " ") + fmt.Println("getblockbyhash:", string(file)) + case "eth_estimateGas": + var params EthEstimateGas + byteData, err := json.Marshal(requestBody.Params[0]) + if err != nil { + fmt.Println("Error:", err) + return + } + err = json.Unmarshal(byteData, ¶ms) + if err != nil { + fmt.Println("Error:", err) + return + } + result = "0x5cec" + case "eth_gasPrice": + result = "0x64" + case "eth_getCode": + result = "" + case "eth_getTransactionCount": + params := EthGetTransactionCount{ + From: requestBody.Params[0].(string), + BlockNum: requestBody.Params[1].(string), + } + + _, sequence, err := GetAccountInfo(params, gwCosmosmux, r) + if err != nil { + fmt.Println("eth_getTransactionCount err:", err) + return + } + + result = fmt.Sprintf("0x%x", sequence) + case "eth_sendTransaction": + // var params EthSendTransaction + // byteData, err := json.Marshal(requestBody.Params[0]) + // if err != nil { + // fmt.Println("Error:", err) + // return + // } + // err = json.Unmarshal(byteData, ¶ms) + // if err != nil { + // fmt.Println("Error:", err) + // return + // } + // fmt.Println("eth_sendTransaction params:", params) + case "eth_sendRawTransaction": + fmt.Println("params:", requestBody.Params) + strData, ok := requestBody.Params[0].(string) + if !ok { + fmt.Println("eth_sendRawTransaction err: convert from interface{} to string failed") + return + } + + txHash, err := sendTx(strData, gwCosmosmux, r) + if err != nil { + fmt.Println("eth_sendRawTransaction err:", err) + return + } + + result = "0x" + txHash + + fmt.Println("sendrawtx:", result) + case "eth_getTransactionReceipt": + txHash := requestBody.Params[0].(string) + fmt.Println("eth_getTransactionReceipt", txHash) + txInfo, logInfos, err := GetTxInfo(txHash, rpcAddr) + var txStatus, txIndex int + blockInfo := CosmosBlockInfo{} + var fromAddr, toAddr string + + if err != nil { + txStatus = 0 + } else { + var txhashes []string + blockInfo, txhashes, err = GetBlockByNumberOrHash(txInfo.Height, rpcAddr, BlockQueryByNumber) + if err != nil { + fmt.Println("eth_getTransactionReceipt err:", err) + return + } + + if logInfos.logForString != "" { + fromAddr, err = bech322hex(logInfos.logForMap[0].LogInfo[0]["transfer"]["sender"]) + if err != nil { + fmt.Println("eth_getTransactionReceipt err:", err) + return + } + toAddr, err = bech322hex(logInfos.logForMap[0].LogInfo[0]["transfer"]["recipient"]) + if err != nil { + fmt.Println("eth_getTransactionReceipt err:", err) + return + } + } + + txIndex = 0 + for i, blockTxHash := range txhashes { + if blockTxHash == txHash { + txIndex = i + } + } + + txStatus = 1 + } + + result = map[string]interface{}{ + "blockHash": "0x" + blockInfo.BlockId.Hash, + "blockNumber": fmt.Sprintf("0x%x", blockInfo.SdkBlock.Header.Height), + "contractAddress": nil, // string of the address if it was created + "cumulativeGasUsed": "0x79ccd3", + "effectiveGasPrice": "0x64", + "from": "0x" + fromAddr, + "gasUsed": "0x5208", + "logs": []string{}, + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "status": fmt.Sprintf("0x%x", txStatus), + "to": "0x" + toAddr, + "transactionHash": "0x" + txInfo.Hash, + "transactionIndex": fmt.Sprintf("0x%x", txIndex), + "type": "0x2", + } + + file, _ := json.MarshalIndent(result, "", " ") + fmt.Println("gettxreceipt:", string(file)) + default: + fmt.Println("unknown method:", requestBody.Method) + } + + response["result"] = result + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + } +} diff --git a/gateway/kira/metamask/utils.go b/gateway/kira/metamask/utils.go new file mode 100644 index 0000000..abffbb3 --- /dev/null +++ b/gateway/kira/metamask/utils.go @@ -0,0 +1,222 @@ +package metamask + +import ( + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/KiraCore/interx/config" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/bech32" + "github.com/ethereum/go-ethereum/accounts/abi" +) + +const ( + TypeKiraAddr = iota + TypeKiraValAddr +) + +func hex2bech32(hexAddr string, addrType int) (string, error) { + hexAddress := hexAddr + if strings.Contains(hexAddr, "0x") { + hexAddress = hexAddr[2:] + } + byteArray, err := hex.DecodeString(hexAddress) + if err != nil { + return "", err + } + + var bech32Addr string + switch addrType { + case TypeKiraAddr: + bech32Addr, err = bech32.ConvertAndEncode(config.DefaultKiraAddrPrefix, byteArray) + case TypeKiraValAddr: + bech32Addr, err = bech32.ConvertAndEncode(config.DefaultKiraValAddrPrefix, byteArray) + default: + err = errors.New(fmt.Sprintln("invalid addrType: ", addrType)) + } + if err != nil { + return "", err + } + return bech32Addr, nil +} + +func bech322hex(bech32Addr string) (string, error) { + _, data, err := bech32.DecodeAndConvert(bech32Addr) + if err != nil { + return "", nil + } + + // Encode the byte slice as a hex string + hexStr := hex.EncodeToString(data) + return hexStr, nil +} + +func hex2int64(hexStr string) (int64, error) { + hexString := hexStr + if strings.Contains(hexStr, "0x") { + hexString = hexStr[2:] + } + integer, err := strconv.ParseInt(hexString, 16, 64) + if err != nil { + return 0, err + } + return integer, nil +} + +func hex2int32(hexStr string) (int32, error) { + hexString := hexStr + if strings.Contains(hexStr, "0x") { + hexString = hexStr[2:] + } + integer, err := strconv.ParseInt(hexString, 16, 32) + if err != nil { + return 0, err + } + return int32(integer), nil +} + +func hex2uint64(hexStr string) (uint64, error) { + hexString := hexStr + if strings.Contains(hexStr, "0x") { + hexString = hexStr[2:] + } + integer, err := strconv.ParseUint(hexString, 16, 64) + if err != nil { + return 0, err + } + return integer, nil +} + +func hex2uint32(hexStr string) (uint32, error) { + hexString := hexStr + if strings.Contains(hexStr, "0x") { + hexString = hexStr[2:] + } + integer, err := strconv.ParseUint(hexString, 16, 32) + if err != nil { + return 0, err + } + return uint32(integer), nil +} + +func bytes2uint64(param []byte) (uint64, error) { + integer, err := strconv.ParseUint(hex.EncodeToString(param), 16, 64) + if err != nil { + return 0, err + } + return integer, nil +} + +func bytes2uint32(param []byte) (uint32, error) { + integer, err := strconv.ParseUint(hex.EncodeToString(param), 16, 32) + if err != nil { + return 0, err + } + return uint32(integer), nil +} + +func bytes2int64(param []byte) (int64, error) { + integer, err := strconv.ParseInt(hex.EncodeToString(param), 16, 64) + if err != nil { + return 0, err + } + return integer, nil +} + +func bytes2int32(param []byte) (int32, error) { + integer, err := strconv.ParseUint(hex.EncodeToString(param), 16, 32) + if err != nil { + return 0, err + } + return int32(integer), nil +} + +func uint32To32Bytes(val uint32) []byte { + byteArr := make([]byte, 4) + binary.BigEndian.PutUint32(byteArr, val) + + paddedByteArr := addPaddingTo32Bytes(byteArr) + + return paddedByteArr +} + +func addPaddingTo32Bytes(byteArr []byte) []byte { + // Pad the byte array with leading zeros to get the desired length + paddedByteArr := make([]byte, 32) + copy(paddedByteArr[32-len(byteArr):], byteArr) + + return paddedByteArr +} + +func bytes2cosmosAddr(param []byte) (cosmostypes.AccAddress, error) { + addrBech32, err := hex2bech32(hex.EncodeToString(param), TypeKiraAddr) + if err != nil { + return nil, err + } + + addr, err := cosmostypes.AccAddressFromBech32(addrBech32) + if err != nil { + return nil, err + } + + return addr, nil +} + +func string2cosmosAddr(param string) (cosmostypes.AccAddress, error) { + addr, err := cosmostypes.AccAddressFromBech32(param) + + return addr, err +} + +func bytes2cosmosValAddr(param []byte) (cosmostypes.ValAddress, error) { + addr, err := cosmostypes.ValAddressFromHex("0x" + hex.EncodeToString(param)) + if err != nil { + return nil, err + } + + return addr, nil +} + +func bytes2bool(param []byte) bool { + return (param[len(param)-1] == 0x01) +} + +func PackABIParams(abi abi.ABI, name string, args ...interface{}) ([]byte, error) { + method, exist := abi.Methods[name] + if !exist { + return nil, fmt.Errorf("method '%s' not found", name) + } + arguments, err := method.Inputs.Pack(args...) + if err != nil { + return nil, err + } + // Pack up the method ID too if not a constructor and return + return arguments, nil +} + +func convertByteArr2Bytes32(val []byte) [32]byte { + var returnVal [32]byte + copy(returnVal[:], val) + return returnVal +} + +func getSignature(r, s, v []byte) []byte { + sig := make([]byte, 65) + copy(sig[:32], r) + copy(sig[32:64], s) + copy(sig[64:], v) + sig[64] = sig[64] - 27 + return sig +} + +func getSignatureV2(r, s, v []byte) []byte { + sig := make([]byte, 65) + copy(sig[:32], r) + copy(sig[32:64], s) + copy(sig[64:], v) + return sig +} diff --git a/go.mod b/go.mod index 3664ca9..c7bf699 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module github.com/KiraCore/interx go 1.19 require ( + cosmossdk.io/math v1.2.0 github.com/KeisukeYamashita/go-jsonrpc v1.0.1 - github.com/KiraCore/sekai v0.3.38 + github.com/KiraCore/sekai v0.3.46-0.20240514123010-1b8573dfe96c github.com/btcsuite/btcd v0.22.1 github.com/cometbft/cometbft v0.37.2 github.com/cosmos/cosmos-proto v1.0.0-beta.2 @@ -35,7 +36,6 @@ require ( cosmossdk.io/depinject v1.0.0-alpha.4 // indirect cosmossdk.io/errors v1.0.0 // indirect cosmossdk.io/log v1.2.1 // indirect - cosmossdk.io/math v1.2.0 // indirect cosmossdk.io/tools/rosetta v0.2.1 // indirect filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect diff --git a/go.sum b/go.sum index 7fef56c..bb25c90 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1: github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/KeisukeYamashita/go-jsonrpc v1.0.1 h1:mkg8th2I7C1y3tnAsxroQbmQcXEZ22AkQ5pazRd/KDM= github.com/KeisukeYamashita/go-jsonrpc v1.0.1/go.mod h1:NyiYd1oDwaSsIflCju5dKRvLdG5rZ/I4E07ygRYYmgc= -github.com/KiraCore/sekai v0.3.38 h1:Z0DLYxu4nY9sWnTfQs10bYLq7ZQDhoZmwkUOWSQEWfo= -github.com/KiraCore/sekai v0.3.38/go.mod h1:4lJS3LLWUl+bZwD9Mdl/vsYerMKOJMITx8ftepF80Ug= +github.com/KiraCore/sekai v0.3.46-0.20240514123010-1b8573dfe96c h1:dYb6PmGqa6Ry/xtGCmrLh5XDcPtRlN129CBmbAQQdc4= +github.com/KiraCore/sekai v0.3.46-0.20240514123010-1b8573dfe96c/go.mod h1:SlwwQqlw20qPR35Iuyd3gzv2vOQIawk6RZUrEU/Y8W4= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= diff --git a/scripts/test-local/Proposal/query-proposal-by-id.sh b/scripts/test-local/Proposal/query-proposal-by-id.sh index 7a91295..ab0d511 100755 --- a/scripts/test-local/Proposal/query-proposal-by-id.sh +++ b/scripts/test-local/Proposal/query-proposal-by-id.sh @@ -15,14 +15,15 @@ sleep 5 INTERX_GATEWAY="127.0.0.1:11000" PROPOSAL_ID_INTERX=$(curl --fail "$INTERX_GATEWAY/api/kira/gov/proposals" | jq '.proposals[0].proposal_id' | tr -d '"' || exit 1) RESULT_INTERX=$(curl --fail $INTERX_GATEWAY/api/kira/gov/proposals/$PROPOSAL_ID_INTERX || exit 1) +echo $RESULT_INTERX DESCRIPTION_INTERX=$(echo $RESULT_INTERX | jq '.proposal.content.messages[0]') QUORUM_INTERX=$(echo $RESULT_INTERX | jq '.proposal.quorum' | tr -d '"') -QUORUM_INTERX=$(printf "%.0f" "$(echo "$QUORUM_INTERX * 100" | bc)") +QUORUM_INTERX=$(printf "%.12f000000" "$(echo "$QUORUM_INTERX" | bc)") DESCRIPTION_CLI=$(showProposal $PROPOSAL_ID_INTERX | jq '.content.messages[0]') QUORUM_CLI=$(showNetworkProperties | jq '.properties.vote_quorum' | tr -d '"' || exit 1) [[ $DESCRIPTION_INTERX != $DESCRIPTION_CLI ]] && echoErr "ERROR: Expected proposal description to be '$DESCRIPTION_CLI', but got '$DESCRIPTION_INTERX'" && exit 1 -[[ $QUORUM_INTERX != $QUORUM_CLI ]] && echoErr "ERROR: Expected proposal description to be '$QUORUM_CLI', but got '$QUORUM_INTERX'" && exit 1 +[[ $QUORUM_INTERX != $QUORUM_CLI ]] && echoErr "ERROR: Expected proposal quorum to be '$QUORUM_CLI', but got '$QUORUM_INTERX'" && exit 1 echoInfo "INFO: $TEST_NAME - Integration Test - END, elapsed: $(prettyTime $(timerSpan $TEST_NAME))" \ No newline at end of file diff --git a/tasks/proposals.go b/tasks/proposals.go index eb81a5d..57d49fe 100644 --- a/tasks/proposals.go +++ b/tasks/proposals.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "cosmossdk.io/math" "github.com/KiraCore/interx/common" "github.com/KiraCore/interx/config" "github.com/KiraCore/interx/database" @@ -219,12 +220,12 @@ func GetCachedProposals(gwCosmosmux *runtime.ServeMux, gatewayAddr string, rpcAd } if result["properties"] != nil { - quorum, err := strconv.Atoi(result["properties"]["voteQuorum"].(string)) - if err != nil { - return nil, err + str := result["properties"]["voteQuorum"].(string) + quorumInt, ok := math.NewIntFromString(str) + if !ok { + return nil, fmt.Errorf("Not able to parse: %s", str) } - - quorumStr = fmt.Sprintf("%.2f", float64(quorum)/100) + quorumStr = math.LegacyNewDecFromIntWithPrec(quorumInt, 18).String() } } diff --git a/types/main.go b/types/main.go index a7490fc..5656f54 100644 --- a/types/main.go +++ b/types/main.go @@ -76,6 +76,7 @@ type ResponseSign struct { type TransactionResponse struct { Time int64 `json:"time"` Hash string `json:"hash"` + Height int64 `json:"height"` Status string `json:"status"` Direction string `json:"direction"` Memo string `json:"memo"` @@ -337,3 +338,8 @@ const ( // POST is a constant to refer POST HTTP Method POST string = "POST" ) + +type TxsResponse struct { + Transactions []TransactionResponse `json:"transactions"` + TotalCount int `json:"total_count"` +}