diff --git a/api/camino.go b/api/camino.go new file mode 100644 index 000000000000..887e48ddb6e5 --- /dev/null +++ b/api/camino.go @@ -0,0 +1,10 @@ +// Copyright (C) 2022-2024, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package api + +import "github.com/ava-labs/avalanchego/utils/formatting" + +type Encoding struct { + Encoding formatting.Encoding `json:"encoding"` +} diff --git a/vms/platformvm/camino_client.go b/vms/platformvm/camino_client.go index df3b30d11ff0..16e0e24ec975 100644 --- a/vms/platformvm/camino_client.go +++ b/vms/platformvm/camino_client.go @@ -5,9 +5,17 @@ package platformvm import ( "context" + "fmt" "github.com/ava-labs/avalanchego/api" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/json" "github.com/ava-labs/avalanchego/utils/rpc" + platformapi "github.com/ava-labs/avalanchego/vms/platformvm/api" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) type CaminoClient interface { @@ -18,6 +26,11 @@ type CaminoClient interface { GetMultisigAlias(ctx context.Context, multisigAddress string, options ...rpc.Option) (*GetMultisigAliasReply, error) GetAllDepositOffers(ctx context.Context, getAllDepositOffersArgs *GetAllDepositOffersArgs, options ...rpc.Option) (*GetAllDepositOffersReply, error) + + GetRegisteredShortIDLink(ctx context.Context, addrStr ids.ShortID, options ...rpc.Option) (string, error) + GetLastAcceptedBlock(ctx context.Context, encoding formatting.Encoding, options ...rpc.Option) (any, error) + GetBlockAtHeight(ctx context.Context, height uint32, encoding formatting.Encoding, options ...rpc.Option) (any, error) + GetClaimables(ctx context.Context, owners []*secp256k1fx.OutputOwners, options ...rpc.Option) ([]*state.Claimable, error) } func (c *client) GetConfiguration(ctx context.Context, options ...rpc.Option) (*GetConfigurationReply, error) { @@ -39,3 +52,115 @@ func (c *client) GetAllDepositOffers(ctx context.Context, getAllDepositOffersArg err := c.requester.SendRequest(ctx, "platform.getAllDepositOffers", &getAllDepositOffersArgs, res, options...) return res, err } + +func (c *client) GetRegisteredShortIDLink(ctx context.Context, addrStr ids.ShortID, options ...rpc.Option) (string, error) { + res := &api.JSONAddress{} + err := c.requester.SendRequest(ctx, "platform.getMultisigAlias", &api.JSONAddress{ + Address: addrStr.String(), + }, res, options...) + return res.Address, err +} + +func (c *client) GetLastAcceptedBlock(ctx context.Context, encoding formatting.Encoding, options ...rpc.Option) (any, error) { + res := &api.GetBlockResponse{} + err := c.requester.SendRequest(ctx, "platform.getLastAcceptedBlock", &api.Encoding{ + Encoding: encoding, + }, res, options...) + return res.Block, err +} + +func (c *client) GetBlockAtHeight(ctx context.Context, height uint32, encoding formatting.Encoding, options ...rpc.Option) (any, error) { + res := &api.GetBlockResponse{} + err := c.requester.SendRequest(ctx, "platform.getBlockAtHeight", &GetBlockAtHeightArgs{ + Height: height, + Encoding: encoding, + }, res, options...) + return res.Block, err +} + +func (c *client) GetClaimables(ctx context.Context, owners []*secp256k1fx.OutputOwners, options ...rpc.Option) ([]*state.Claimable, error) { + res := &GetClaimablesReply{} + if err := c.requester.SendRequest(ctx, "platform.getClaimables", &GetClaimablesArgs{ + Owners: apiOwnersFromSECP(owners), + }, res, options...); err != nil { + return nil, err + } + return claimablesFromAPI(res.Claimables) +} + +func claimablesFromAPI(apiClaimables []APIClaimable) ([]*state.Claimable, error) { + claimables := make([]*state.Claimable, len(apiClaimables)) + for i := range claimables { + claimable, err := claimableFromAPI(&apiClaimables[i]) + if err != nil { + return nil, err + } + claimables[i] = &claimable + } + return claimables, nil +} + +func claimableFromAPI(apiClaimable *APIClaimable) (state.Claimable, error) { + claimableOwner, err := secpOwnerFromAPI(&apiClaimable.RewardOwner) + if err != nil { + return state.Claimable{}, err + } + return state.Claimable{ + Owner: claimableOwner, + ValidatorReward: uint64(apiClaimable.ValidatorRewards), + ExpiredDepositReward: uint64(apiClaimable.ExpiredDepositRewards), + }, nil +} + +func apiOwnersFromSECP(secpOwners []*secp256k1fx.OutputOwners) []platformapi.Owner { + owners := make([]platformapi.Owner, len(secpOwners)) + for i := range owners { + owners[i] = *apiOwnerFromSECP(secpOwners[i]) + } + return owners +} + +func apiOwnerFromSECP(secpOwner *secp256k1fx.OutputOwners) *platformapi.Owner { + apiOwner := &platformapi.Owner{ + Locktime: json.Uint64(secpOwner.Locktime), + Threshold: json.Uint32(secpOwner.Threshold), + Addresses: make([]string, len(secpOwner.Addrs)), + } + for i := range secpOwner.Addrs { + apiOwner.Addresses[i] = secpOwner.Addrs[i].String() + } + return apiOwner +} + +func secpOwnerFromAPI(apiOwner *platformapi.Owner) (*secp256k1fx.OutputOwners, error) { + if len(apiOwner.Addresses) > 0 { + secpOwner := &secp256k1fx.OutputOwners{ + Locktime: uint64(apiOwner.Locktime), + Threshold: uint32(apiOwner.Threshold), + Addrs: make([]ids.ShortID, len(apiOwner.Addresses)), + } + for i := range apiOwner.Addresses { + addr, err := parseAddr(apiOwner.Addresses[i]) + if err != nil { + return nil, err + } + secpOwner.Addrs[i] = addr + } + secpOwner.Sort() + return secpOwner, nil + } + return nil, nil +} + +func parseAddr(addrStr string) (ids.ShortID, error) { + addr, err1 := address.ParseToID(addrStr) + if err1 == nil { + return addr, nil + } + addr, err2 := ids.ShortFromString(addrStr) + if err2 != nil { + return ids.ShortEmpty, fmt.Errorf("failed to parse addr both as shortID (%s) and as bech32 (%s)", + err2, err1) + } + return addr, nil +} diff --git a/vms/platformvm/camino_service.go b/vms/platformvm/camino_service.go index 128a3c88a4db..aca1d25e6f44 100644 --- a/vms/platformvm/camino_service.go +++ b/vms/platformvm/camino_service.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/keystore" as "github.com/ava-labs/avalanchego/vms/platformvm/addrstate" + "github.com/ava-labs/avalanchego/vms/platformvm/blocks" "github.com/ava-labs/avalanchego/vms/platformvm/deposit" "github.com/ava-labs/avalanchego/vms/platformvm/locked" "github.com/ava-labs/avalanchego/vms/platformvm/state" @@ -769,7 +770,7 @@ func (s *CaminoService) GetDeposits(_ *http.Request, args *GetDepositsArgs, repl } // GetLastAcceptedBlock returns the last accepted block -func (s *CaminoService) GetLastAcceptedBlock(r *http.Request, _ *struct{}, reply *api.GetBlockResponse) error { +func (s *CaminoService) GetLastAcceptedBlock(r *http.Request, args *api.Encoding, reply *api.GetBlockResponse) error { s.vm.ctx.Log.Debug("Platform: GetLastAcceptedBlock called") ctx := r.Context() @@ -782,18 +783,29 @@ func (s *CaminoService) GetLastAcceptedBlock(r *http.Request, _ *struct{}, reply return fmt.Errorf("couldn't get block with id %s: %w", lastAcceptedID, err) } - block.InitCtx(s.vm.ctx) - reply.Encoding = formatting.JSON - reply.Block = block + reply.Encoding = args.Encoding + + if args.Encoding == formatting.JSON { + block.InitCtx(s.vm.ctx) + reply.Block = block + return nil + } + + reply.Block, err = formatting.Encode(args.Encoding, block.Bytes()) + if err != nil { + return fmt.Errorf("couldn't encode block %s as string: %w", block.ID(), err) + } + return nil } -type GetBlockAtHeight struct { - Height uint32 `json:"height"` +type GetBlockAtHeightArgs struct { + Encoding formatting.Encoding `json:"encoding"` + Height uint32 `json:"height"` } // GetBlockAtHeight returns block at given height -func (s *CaminoService) GetBlockAtHeight(r *http.Request, args *GetBlockAtHeight, reply *api.GetBlockResponse) error { +func (s *CaminoService) GetBlockAtHeight(r *http.Request, args *GetBlockAtHeightArgs, reply *api.GetBlockResponse) error { s.vm.ctx.Log.Debug("Platform: GetBlockAtHeight called") ctx := r.Context() @@ -802,20 +814,37 @@ func (s *CaminoService) GetBlockAtHeight(r *http.Request, args *GetBlockAtHeight return fmt.Errorf("couldn't get last accepted block ID: %w", err) } + var desiredBlock blocks.Block + for { block, err := s.vm.manager.GetStatelessBlock(blockID) if err != nil { return fmt.Errorf("couldn't get block with id %s: %w", blockID, err) } if block.Height() == uint64(args.Height) { - block.InitCtx(s.vm.ctx) - reply.Encoding = formatting.JSON - reply.Block = block + desiredBlock = block break } blockID = block.Parent() } + if desiredBlock == nil { + return fmt.Errorf("couldn't find block at height %d", args.Height) + } + + reply.Encoding = args.Encoding + + if args.Encoding == formatting.JSON { + desiredBlock.InitCtx(s.vm.ctx) + reply.Block = desiredBlock + return nil + } + + reply.Block, err = formatting.Encode(args.Encoding, desiredBlock.Bytes()) + if err != nil { + return fmt.Errorf("couldn't encode block %s as string: %w", desiredBlock.ID(), err) + } + return nil }