github.com/Finschia/finschia-sdk@v0.48.1/server/rosetta/client_online.go (about) 1 package rosetta 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "encoding/hex" 8 "errors" 9 "fmt" 10 "regexp" 11 "strconv" 12 "time" 13 14 rosettatypes "github.com/coinbase/rosetta-sdk-go/types" 15 "google.golang.org/grpc" 16 "google.golang.org/grpc/metadata" 17 18 ocrpc "github.com/Finschia/ostracon/rpc/client" 19 "github.com/Finschia/ostracon/rpc/client/http" 20 abci "github.com/tendermint/tendermint/abci/types" 21 22 crgerrs "github.com/Finschia/finschia-sdk/server/rosetta/lib/errors" 23 crgtypes "github.com/Finschia/finschia-sdk/server/rosetta/lib/types" 24 sdk "github.com/Finschia/finschia-sdk/types" 25 grpctypes "github.com/Finschia/finschia-sdk/types/grpc" 26 "github.com/Finschia/finschia-sdk/version" 27 authtx "github.com/Finschia/finschia-sdk/x/auth/tx" 28 auth "github.com/Finschia/finschia-sdk/x/auth/types" 29 bank "github.com/Finschia/finschia-sdk/x/bank/types" 30 ) 31 32 // interface assertion 33 var _ crgtypes.Client = (*Client)(nil) 34 35 const ( 36 tmWebsocketPath = "/websocket" 37 defaultNodeTimeout = 15 * time.Second 38 ) 39 40 // Client implements a single network client to interact with cosmos based chains 41 type Client struct { 42 supportedOperations []string 43 44 config *Config 45 46 auth auth.QueryClient 47 bank bank.QueryClient 48 tmRPC ocrpc.Client 49 50 version string 51 52 converter Converter 53 } 54 55 // NewClient instantiates a new online servicer 56 func NewClient(cfg *Config) (*Client, error) { 57 info := version.NewInfo() 58 59 v := info.Version 60 if v == "" { 61 v = "unknown" 62 } 63 64 txConfig := authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes) 65 66 var supportedOperations []string 67 for _, ii := range cfg.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) { 68 resolvedMsg, err := cfg.InterfaceRegistry.Resolve(ii) 69 if err != nil { 70 continue 71 } 72 73 if _, ok := resolvedMsg.(sdk.Msg); ok { 74 supportedOperations = append(supportedOperations, ii) 75 } 76 } 77 78 supportedOperations = append( 79 supportedOperations, 80 bank.EventTypeCoinSpent, 81 bank.EventTypeCoinReceived, 82 bank.EventTypeCoinBurn, 83 ) 84 85 return &Client{ 86 supportedOperations: supportedOperations, 87 config: cfg, 88 auth: nil, 89 bank: nil, 90 tmRPC: nil, 91 version: fmt.Sprintf("%s/%s", info.AppName, v), 92 converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, txConfig), 93 }, nil 94 } 95 96 // ---------- cosmos-rosetta-gateway.types.Client implementation ------------ // 97 98 // Bootstrap is gonna connect the client to the endpoints 99 func (c *Client) Bootstrap() error { 100 grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithInsecure()) 101 if err != nil { 102 return err 103 } 104 105 tmRPC, err := http.New(c.config.TendermintRPC, tmWebsocketPath) 106 if err != nil { 107 return err 108 } 109 110 authClient := auth.NewQueryClient(grpcConn) 111 bankClient := bank.NewQueryClient(grpcConn) 112 113 c.auth = authClient 114 c.bank = bankClient 115 c.tmRPC = tmRPC 116 117 return nil 118 } 119 120 // Ready performs a health check and returns an error if the client is not ready. 121 func (c *Client) Ready() error { 122 ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout) 123 defer cancel() 124 _, err := c.tmRPC.Health(ctx) 125 if err != nil { 126 return err 127 } 128 _, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{}) 129 if err != nil { 130 return err 131 } 132 return nil 133 } 134 135 func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (*SignerData, error) { 136 if height != nil { 137 strHeight := strconv.FormatInt(*height, 10) 138 ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight) 139 } 140 141 accountInfo, err := c.auth.Account(ctx, &auth.QueryAccountRequest{ 142 Address: addr, 143 }) 144 if err != nil { 145 return nil, crgerrs.FromGRPCToRosettaError(err) 146 } 147 148 signerData, err := c.converter.ToRosetta().SignerData(accountInfo.Account) 149 if err != nil { 150 return nil, err 151 } 152 return signerData, nil 153 } 154 155 func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*rosettatypes.Amount, error) { 156 if height != nil { 157 strHeight := strconv.FormatInt(*height, 10) 158 ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight) 159 } 160 161 balance, err := c.bank.AllBalances(ctx, &bank.QueryAllBalancesRequest{ 162 Address: addr, 163 }) 164 if err != nil { 165 return nil, crgerrs.FromGRPCToRosettaError(err) 166 } 167 168 availableCoins, err := c.coins(ctx) 169 if err != nil { 170 return nil, err 171 } 172 173 return c.converter.ToRosetta().Amounts(balance.Balances, availableCoins), nil 174 } 175 176 func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockResponse, error) { 177 bHash, err := hex.DecodeString(hash) 178 if err != nil { 179 return crgtypes.BlockResponse{}, fmt.Errorf("invalid block hash: %s", err) 180 } 181 182 block, err := c.tmRPC.BlockByHash(ctx, bHash) 183 if err != nil { 184 return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error()) 185 } 186 187 return c.converter.ToRosetta().BlockResponse(block), nil 188 } 189 190 func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.BlockResponse, error) { 191 height, err := c.getHeight(ctx, height) 192 if err != nil { 193 return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error()) 194 } 195 block, err := c.tmRPC.Block(ctx, height) 196 if err != nil { 197 return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error()) 198 } 199 200 return c.converter.ToRosetta().BlockResponse(block), nil 201 } 202 203 func (c *Client) BlockTransactionsByHash(ctx context.Context, hash string) (crgtypes.BlockTransactionsResponse, error) { 204 // TODO(fdymylja): use a faster path, by searching the block by hash, instead of doing a double query operation 205 blockResp, err := c.BlockByHash(ctx, hash) 206 if err != nil { 207 return crgtypes.BlockTransactionsResponse{}, err 208 } 209 210 return c.blockTxs(ctx, &blockResp.Block.Index) 211 } 212 213 func (c *Client) BlockTransactionsByHeight(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) { 214 height, err := c.getHeight(ctx, height) 215 if err != nil { 216 return crgtypes.BlockTransactionsResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error()) 217 } 218 blockTxResp, err := c.blockTxs(ctx, height) 219 if err != nil { 220 return crgtypes.BlockTransactionsResponse{}, err 221 } 222 return blockTxResp, nil 223 } 224 225 // Coins fetches the existing coins in the application 226 func (c *Client) coins(ctx context.Context) (sdk.Coins, error) { 227 supply, err := c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{}) 228 if err != nil { 229 return nil, crgerrs.FromGRPCToRosettaError(err) 230 } 231 return supply.Supply, nil 232 } 233 234 func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) { 235 switch signed { 236 case false: 237 rosTx, err := c.converter.ToRosetta().Tx(txBytes, nil) 238 if err != nil { 239 return nil, nil, err 240 } 241 return rosTx.Operations, nil, err 242 default: 243 ops, signers, err = c.converter.ToRosetta().OpsAndSigners(txBytes) 244 return 245 } 246 } 247 248 // GetTx returns a transaction given its hash. For Rosetta we make a synthetic transaction for BeginBlock 249 // and EndBlock to adhere to balance tracking rules. 250 func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) { 251 hashBytes, err := hex.DecodeString(hash) 252 if err != nil { 253 return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("bad tx hash: %s", err)) 254 } 255 256 // get tx type and hash 257 txType, hashBytes := c.converter.ToSDK().HashToTxType(hashBytes) 258 259 // construct rosetta tx 260 switch txType { 261 // handle begin block hash 262 case BeginBlockTx: 263 // get block height by hash 264 block, err := c.tmRPC.BlockByHash(ctx, hashBytes) 265 if err != nil { 266 return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) 267 } 268 269 // get block txs 270 fullBlock, err := c.blockTxs(ctx, &block.Block.Height) 271 if err != nil { 272 return nil, err 273 } 274 275 return fullBlock.Transactions[0], nil 276 // handle deliver tx hash 277 case DeliverTxTx: 278 rawTx, err := c.tmRPC.Tx(ctx, hashBytes, true) 279 if err != nil { 280 return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) 281 } 282 return c.converter.ToRosetta().Tx(rawTx.Tx, &rawTx.TxResult) 283 // handle end block hash 284 case EndBlockTx: 285 // get block height by hash 286 block, err := c.tmRPC.BlockByHash(ctx, hashBytes) 287 if err != nil { 288 return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) 289 } 290 291 // get block txs 292 fullBlock, err := c.blockTxs(ctx, &block.Block.Height) 293 if err != nil { 294 return nil, err 295 } 296 297 // get last tx 298 return fullBlock.Transactions[len(fullBlock.Transactions)-1], nil 299 // unrecognized tx 300 default: 301 return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("invalid tx hash provided: %s", hash)) 302 } 303 } 304 305 // GetUnconfirmedTx gets an unconfirmed transaction given its hash 306 func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) { 307 res, err := c.tmRPC.UnconfirmedTxs(ctx, nil) 308 if err != nil { 309 return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "unconfirmed tx not found") 310 } 311 312 hashAsBytes, err := hex.DecodeString(hash) 313 if err != nil { 314 return nil, crgerrs.WrapError(crgerrs.ErrInterpreting, "invalid hash") 315 } 316 317 // assert that correct tx length is provided 318 switch len(hashAsBytes) { 319 default: 320 return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("unrecognized tx size: %d", len(hashAsBytes))) 321 case BeginEndBlockTxSize: 322 return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "endblock and begin block txs cannot be unconfirmed") 323 case DeliverTxSize: 324 break 325 } 326 327 // iterate over unconfirmed txs to find the one with matching hash 328 for _, unconfirmedTx := range res.Txs { 329 if !bytes.Equal(unconfirmedTx.Hash(), hashAsBytes) { 330 continue 331 } 332 333 return c.converter.ToRosetta().Tx(unconfirmedTx, nil) 334 } 335 return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool: "+hash) 336 } 337 338 // Mempool returns the unconfirmed transactions in the mempool 339 func (c *Client) Mempool(ctx context.Context) ([]*rosettatypes.TransactionIdentifier, error) { 340 txs, err := c.tmRPC.UnconfirmedTxs(ctx, nil) 341 if err != nil { 342 return nil, err 343 } 344 345 return c.converter.ToRosetta().TxIdentifiers(txs.Txs), nil 346 } 347 348 // Peers gets the number of peers 349 func (c *Client) Peers(ctx context.Context) ([]*rosettatypes.Peer, error) { 350 netInfo, err := c.tmRPC.NetInfo(ctx) 351 if err != nil { 352 return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) 353 } 354 return c.converter.ToRosetta().Peers(netInfo.Peers), nil 355 } 356 357 func (c *Client) Status(ctx context.Context) (*rosettatypes.SyncStatus, error) { 358 status, err := c.tmRPC.Status(ctx) 359 if err != nil { 360 return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) 361 } 362 return c.converter.ToRosetta().SyncStatus(status), err 363 } 364 365 func (c *Client) PostTx(txBytes []byte) (*rosettatypes.TransactionIdentifier, map[string]interface{}, error) { 366 // sync ensures it will go through checkTx 367 res, err := c.tmRPC.BroadcastTxSync(context.Background(), txBytes) 368 if err != nil { 369 return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) 370 } 371 // check if tx was broadcast successfully 372 if res.Code != abci.CodeTypeOK { 373 return nil, nil, crgerrs.WrapError( 374 crgerrs.ErrUnknown, 375 fmt.Sprintf("transaction broadcast failure: (%d) %s ", res.Code, res.Log), 376 ) 377 } 378 379 return &rosettatypes.TransactionIdentifier{ 380 Hash: fmt.Sprintf("%X", res.Hash), 381 }, 382 map[string]interface{}{ 383 Log: res.Log, 384 }, nil 385 } 386 387 // construction endpoints 388 389 // ConstructionMetadataFromOptions builds the metadata given the options 390 func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options map[string]interface{}) (meta map[string]interface{}, err error) { 391 if len(options) == 0 { 392 return nil, crgerrs.ErrBadArgument 393 } 394 395 constructionOptions := new(PreprocessOperationsOptionsResponse) 396 397 err = constructionOptions.FromMetadata(options) 398 if err != nil { 399 return nil, err 400 } 401 402 signersData := make([]*SignerData, len(constructionOptions.ExpectedSigners)) 403 404 for i, signer := range constructionOptions.ExpectedSigners { 405 accountInfo, err := c.accountInfo(ctx, signer, nil) 406 if err != nil { 407 return nil, err 408 } 409 410 signersData[i] = accountInfo 411 } 412 413 status, err := c.tmRPC.Status(ctx) 414 if err != nil { 415 return nil, err 416 } 417 418 metadataResp := ConstructionMetadata{ 419 ChainID: status.NodeInfo.Network, 420 SignersData: signersData, 421 GasLimit: constructionOptions.GasLimit, 422 GasPrice: constructionOptions.GasPrice, 423 Memo: constructionOptions.Memo, 424 } 425 426 return metadataResp.ToMetadata() 427 } 428 429 func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) { 430 // get block info 431 blockInfo, err := c.tmRPC.Block(ctx, height) 432 if err != nil { 433 return crgtypes.BlockTransactionsResponse{}, err 434 } 435 // get block events 436 blockResults, err := c.tmRPC.BlockResults(ctx, height) 437 if err != nil { 438 return crgtypes.BlockTransactionsResponse{}, err 439 } 440 441 if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) { 442 // wtf? 443 panic("block results transactions do now match block transactions") 444 } 445 // process begin and end block txs 446 beginBlockTx := &rosettatypes.Transaction{ 447 TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().BeginBlockTxHash(blockInfo.BlockID.Hash)}, 448 Operations: AddOperationIndexes( 449 nil, 450 c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.BeginBlockEvents), 451 ), 452 } 453 454 endBlockTx := &rosettatypes.Transaction{ 455 TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().EndBlockTxHash(blockInfo.BlockID.Hash)}, 456 Operations: AddOperationIndexes( 457 nil, 458 c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.EndBlockEvents), 459 ), 460 } 461 462 deliverTx := make([]*rosettatypes.Transaction, len(blockInfo.Block.Txs)) 463 // process normal txs 464 for i, tx := range blockInfo.Block.Txs { 465 rosTx, err := c.converter.ToRosetta().Tx(tx, blockResults.TxsResults[i]) 466 if err != nil { 467 return crgtypes.BlockTransactionsResponse{}, err 468 } 469 deliverTx[i] = rosTx 470 } 471 472 finalTxs := make([]*rosettatypes.Transaction, 0, 2+len(deliverTx)) 473 finalTxs = append(finalTxs, beginBlockTx) 474 finalTxs = append(finalTxs, deliverTx...) 475 finalTxs = append(finalTxs, endBlockTx) 476 477 return crgtypes.BlockTransactionsResponse{ 478 BlockResponse: c.converter.ToRosetta().BlockResponse(blockInfo), 479 Transactions: finalTxs, 480 }, nil 481 } 482 483 func (c *Client) getHeight(ctx context.Context, height *int64) (realHeight *int64, err error) { 484 if height != nil && *height == -1 { 485 genesisChunk, err := c.tmRPC.GenesisChunked(ctx, 0) 486 if err != nil { 487 return nil, err 488 } 489 490 heightNum, err := extractInitialHeightFromGenesisChunk(genesisChunk.Data) 491 if err != nil { 492 return nil, err 493 } 494 495 realHeight = &heightNum 496 } else { 497 realHeight = height 498 } 499 return 500 } 501 502 func extractInitialHeightFromGenesisChunk(genesisChunk string) (int64, error) { 503 firstChunk, err := base64.StdEncoding.DecodeString(genesisChunk) 504 if err != nil { 505 return 0, err 506 } 507 508 re, err := regexp.Compile("\"initial_height\":\"(\\d+)\"") //nolint:gocritic 509 if err != nil { 510 return 0, err 511 } 512 513 matches := re.FindStringSubmatch(string(firstChunk)) 514 if len(matches) != 2 { 515 return 0, errors.New("failed to fetch initial_height") 516 } 517 518 heightStr := matches[1] 519 return strconv.ParseInt(heightStr, 10, 64) 520 }