package aptos import ( "context" "fmt" "net/http" "strconv" "time" "github.com/go-resty/resty/v2" "github.com/wormhole-foundation/wormhole-explorer/contract-watcher/internal/metrics" "go.uber.org/ratelimit" ) var ErrTooManyRequests = fmt.Errorf("too many requests") const clientName = "aptos" // AptosSDK is a client for the Aptos API. type AptosSDK struct { client *resty.Client rl ratelimit.Limiter metrics metrics.Metrics } type GetLatestBlock struct { BlockHeight string `json:"block_height"` } type Payload struct { Function string `json:"function"` TypeArguments []string `json:"type_arguments"` Arguments []any `json:"arguments"` Type string `json:"type"` } type Transaction struct { Version string `json:"version"` Hash string `json:"hash"` Payload Payload `json:"payload,omitempty"` } type GetBlockResult struct { BlockHeight string `json:"block_height"` BlockHash string `json:"block_hash"` BlockTimestamp string `json:"block_timestamp"` Transactions []Transaction `json:"transactions"` } type GetTransactionResult struct { Version string `json:"version"` Hash string `json:"hash"` StateChangeHash string `json:"state_change_hash"` EventRootHash string `json:"event_root_hash"` StateCheckpointHash any `json:"state_checkpoint_hash"` GasUsed string `json:"gas_used"` Success bool `json:"success"` VMStatus string `json:"vm_status"` } func (r *GetBlockResult) GetBlockTime() (*time.Time, error) { t, err := strconv.ParseUint(r.BlockTimestamp, 10, 64) if err != nil { return nil, err } tm := time.UnixMicro(int64(t)) return &tm, nil } // NewAptosSDK creates a new AptosSDK. func NewAptosSDK(url string, rl ratelimit.Limiter, metrics metrics.Metrics) *AptosSDK { return &AptosSDK{ rl: rl, client: resty.New().SetBaseURL(url), metrics: metrics, } } func (s *AptosSDK) GetLatestBlock(ctx context.Context) (uint64, error) { s.rl.Take() resp, err := s.client.R(). SetContext(ctx). SetResult(&GetLatestBlock{}). Get("v1") if err != nil { return 0, err } s.metrics.IncRpcRequest(clientName, "get-latest-block", resp.StatusCode()) if resp.IsError() { return 0, fmt.Errorf("status code: %s. %s", resp.Status(), string(resp.Body())) } result := resp.Result().(*GetLatestBlock) if result == nil { return 0, fmt.Errorf("empty response") } if result.BlockHeight == "" { return 0, fmt.Errorf("empty block height") } return strconv.ParseUint(result.BlockHeight, 10, 64) } func (s *AptosSDK) GetBlock(ctx context.Context, block uint64) (*GetBlockResult, error) { s.rl.Take() resp, err := s.client.R(). SetContext(ctx). SetResult(&GetBlockResult{}). SetQueryParam("with_transactions", "true"). Get(fmt.Sprintf("v1/blocks/by_height/%d", block)) if err != nil { return nil, err } s.metrics.IncRpcRequest(clientName, "get-block", resp.StatusCode()) if resp.IsError() { if resp.StatusCode() == http.StatusTooManyRequests { return nil, ErrTooManyRequests } return nil, fmt.Errorf("status code: %s. %s", resp.Status(), string(resp.Body())) } return resp.Result().(*GetBlockResult), nil } func (s *AptosSDK) GetTransaction(ctx context.Context, version string) (*GetTransactionResult, error) { s.rl.Take() resp, err := s.client.R(). SetContext(ctx). SetResult(&GetTransactionResult{}). SetQueryParam("with_transactions", "true"). Get(fmt.Sprintf("v1/transactions/by_version/%s", version)) if err != nil { return nil, err } s.metrics.IncRpcRequest(clientName, "get-transaction", resp.StatusCode()) if resp.IsError() { if resp.StatusCode() == http.StatusTooManyRequests { return nil, ErrTooManyRequests } return nil, fmt.Errorf("status code: %s. %s", resp.Status(), string(resp.Body())) } return resp.Result().(*GetTransactionResult), nil }