mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
External (HTTP / JSON-RPC) is the only place JSON is legitimate. Every
existing encoding/json import in node/ moves to github.com/go-json-experiment/json
(v2 root, not the v1 sub-package). NewEncoder/NewDecoder rewrite to
MarshalWrite/UnmarshalRead. MarshalIndent rewrites to Marshal with
jsontext.WithIndent. json.RawMessage rewrites to jsontext.Value.
*json.SyntaxError rewrites to *jsontext.SyntacticError.
81 files migrated. LLM.md captures the rule + v1->v2 delta table.
Known v2 semantic deltas surfaced by existing tests (followups, not regressions):
- [N]byte fields with no MarshalJSON now marshal as base64 string (v1 marshalled
as JSON array of byte numbers). Affects vms/platformvm/txs/*_test.go fixtures
with embedded BLS proofOfPossession.
- time.Duration has no v2 default representation; configs that wire-format
Duration as nanoseconds (vms/{xvm,platformvm}/config, config/spec) need to
switch to string-form Duration or carry an explicit option. v2 root does not
re-export FormatDurationAsNano.
- v2 enforces strict UTF-8 (vms/chainadapter/messaging fixture has non-UTF-8).
- json.MarshalWrite does not append a trailing '\n' (v1 NewEncoder.Encode did);
service/auth/auth_test.go expectation updated.
- nil []byte round-trips to empty (not nil); config_test deep-equal fixtures
surface this.
All affected sites are at the API boundary; ZAP wire envelope already covers
the internal data paths (state, P2P, consensus, MPC, threshold). Internal
JSON sites that should move to ZAP next (separate work):
- vms/da/store.go (DA blob/cert storage as JSON)
- vms/platformvm/airdrop (airdrop claims as JSON in db)
- vms/chainadapter/appchain (SQLite materializer schema/data blobs)
- vms/chainadapter/messaging (conversation codec)
- staking/kms.go (KMS HTTP client — external technically, leave)
- utils/{bimap,ips} (small marshaler shims — low priority)
2223 lines
63 KiB
Go
2223 lines
63 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package platformvm
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"net/http"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/go-json-experiment/json"
|
|
"github.com/luxfi/log"
|
|
|
|
validators "github.com/luxfi/validators"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/formatting"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/math/set"
|
|
apitypes "github.com/luxfi/api/types"
|
|
"github.com/luxfi/node/cache/lru"
|
|
"github.com/luxfi/node/vms/components/gas"
|
|
lux "github.com/luxfi/utxo"
|
|
"github.com/luxfi/node/vms/platformvm/reward"
|
|
"github.com/luxfi/node/vms/platformvm/stakeable"
|
|
"github.com/luxfi/node/vms/platformvm/state"
|
|
"github.com/luxfi/node/vms/platformvm/status"
|
|
"github.com/luxfi/node/vms/platformvm/txs"
|
|
"github.com/luxfi/node/vms/platformvm/validators/fee"
|
|
"github.com/luxfi/node/vms/platformvm/warp/message"
|
|
safemath "github.com/luxfi/math"
|
|
"github.com/luxfi/node/vms/platformvm/fx"
|
|
"github.com/luxfi/node/vms/platformvm/signer"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
"github.com/luxfi/vm/types"
|
|
|
|
platformapitypes "github.com/luxfi/node/vms/platformvm/api"
|
|
avajson "github.com/luxfi/node/utils/json"
|
|
)
|
|
|
|
const (
|
|
// Max number of addresses that can be passed in as argument to GetUTXOs
|
|
maxGetUTXOsAddrs = 1024
|
|
|
|
// Max number of addresses that can be passed in as argument to GetStake
|
|
maxGetStakeAddrs = 256
|
|
|
|
// Max number of items allowed in a page
|
|
maxPageSize = 1024
|
|
|
|
// Note: Staker attributes cache should be large enough so that no evictions
|
|
// happen when the API loops through all stakers.
|
|
stakerAttributesCacheSize = 100_000
|
|
)
|
|
|
|
var (
|
|
errMissingDecisionBlock = errors.New("should have a decision block within the past two blocks")
|
|
errPrimaryNetworkIsNotANet = errors.New("the primary network isn't a net")
|
|
errNoAddresses = errors.New("no addresses provided")
|
|
errMissingBlockchainID = errors.New("argument 'blockchainID' not given")
|
|
)
|
|
|
|
// Service defines the API calls that can be made to the platform chain
|
|
type Service struct {
|
|
vm *VM
|
|
addrManager lux.AddressManager
|
|
stakerAttributesCache *lru.Cache[ids.ID, *stakerAttributes]
|
|
}
|
|
|
|
// All attributes are optional and may not be filled for each stakerTx.
|
|
type stakerAttributes struct {
|
|
shares uint32
|
|
rewardsOwner fx.Owner
|
|
validationRewardsOwner fx.Owner
|
|
delegationRewardsOwner fx.Owner
|
|
proofOfPossession *signer.ProofOfPossession
|
|
}
|
|
|
|
// GetHeight returns the height of the last accepted block
|
|
func (s *Service) GetHeight(r *http.Request, _ *struct{}, response *apitypes.GetHeightResponse) error {
|
|
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getHeight",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
ctx := r.Context()
|
|
height, err := s.vm.GetCurrentHeight(ctx)
|
|
response.Height = apitypes.Uint64(height)
|
|
return err
|
|
}
|
|
|
|
// GetProposedHeight returns the current ProposerVM height
|
|
func (s *Service) GetProposedHeight(r *http.Request, _ *struct{}, reply *apitypes.GetHeightResponse) error {
|
|
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "platform"),
|
|
log.String("method", "getProposedHeight"),
|
|
)
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
lastAcceptedID := s.vm.state.GetLastAccepted()
|
|
lastAcceptedBlock, err := s.vm.manager.GetStatelessBlock(lastAcceptedID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Height = apitypes.Uint64(lastAcceptedBlock.Height())
|
|
return nil
|
|
}
|
|
|
|
type GetBalanceRequest struct {
|
|
Addresses []string `json:"addresses"`
|
|
}
|
|
|
|
// Note: We explicitly duplicate LUX out of the maps to ensure backwards
|
|
// compatibility.
|
|
type GetBalanceResponse struct {
|
|
// Balance, in nLUX, of the address
|
|
Balance avajson.Uint64 `json:"balance"`
|
|
Unlocked avajson.Uint64 `json:"unlocked"`
|
|
LockedStakeable avajson.Uint64 `json:"lockedStakeable"`
|
|
LockedNotStakeable avajson.Uint64 `json:"lockedNotStakeable"`
|
|
Balances map[ids.ID]avajson.Uint64 `json:"balances"`
|
|
Unlockeds map[ids.ID]avajson.Uint64 `json:"unlockeds"`
|
|
LockedStakeables map[ids.ID]avajson.Uint64 `json:"lockedStakeables"`
|
|
LockedNotStakeables map[ids.ID]avajson.Uint64 `json:"lockedNotStakeables"`
|
|
UTXOIDs []*lux.UTXOID `json:"utxoIDs"`
|
|
}
|
|
|
|
// GetBalance gets the balance of an address
|
|
func (s *Service) GetBalance(_ *http.Request, args *GetBalanceRequest, response *GetBalanceResponse) error {
|
|
|
|
s.vm.log.Debug("deprecated API called",
|
|
"service", "platform",
|
|
"method", "getBalance",
|
|
"addresses", args.Addresses,
|
|
)
|
|
|
|
addrs, err := lux.ParseServiceAddresses(s.addrManager, args.Addresses)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
utxos, err := lux.GetAllUTXOs(s.vm.state, addrs)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't get UTXO set of %v: %w", args.Addresses, err)
|
|
}
|
|
|
|
currentTime := s.vm.nodeClock.Unix()
|
|
|
|
unlockeds := map[ids.ID]uint64{}
|
|
lockedStakeables := map[ids.ID]uint64{}
|
|
lockedNotStakeables := map[ids.ID]uint64{}
|
|
|
|
utxoFor:
|
|
for _, utxo := range utxos {
|
|
assetID := utxo.AssetID()
|
|
switch out := utxo.Out.(type) {
|
|
case *secp256k1fx.TransferOutput:
|
|
if out.Locktime <= currentTime {
|
|
newBalance, err := safemath.Add(unlockeds[assetID], out.Amount())
|
|
if err != nil {
|
|
unlockeds[assetID] = math.MaxUint64
|
|
} else {
|
|
unlockeds[assetID] = newBalance
|
|
}
|
|
} else {
|
|
newBalance, err := safemath.Add(lockedNotStakeables[assetID], out.Amount())
|
|
if err != nil {
|
|
lockedNotStakeables[assetID] = math.MaxUint64
|
|
} else {
|
|
lockedNotStakeables[assetID] = newBalance
|
|
}
|
|
}
|
|
case *stakeable.LockOut:
|
|
innerOut, ok := out.TransferableOut.(*secp256k1fx.TransferOutput)
|
|
switch {
|
|
case !ok:
|
|
s.vm.log.Warn("unexpected output type in UTXO",
|
|
"type", fmt.Sprintf("%T", out.TransferableOut),
|
|
)
|
|
continue utxoFor
|
|
case innerOut.Locktime > currentTime:
|
|
newBalance, err := safemath.Add(lockedNotStakeables[assetID], out.Amount())
|
|
if err != nil {
|
|
lockedNotStakeables[assetID] = math.MaxUint64
|
|
} else {
|
|
lockedNotStakeables[assetID] = newBalance
|
|
}
|
|
case out.Locktime <= currentTime:
|
|
newBalance, err := safemath.Add(unlockeds[assetID], out.Amount())
|
|
if err != nil {
|
|
unlockeds[assetID] = math.MaxUint64
|
|
} else {
|
|
unlockeds[assetID] = newBalance
|
|
}
|
|
default:
|
|
newBalance, err := safemath.Add(lockedStakeables[assetID], out.Amount())
|
|
if err != nil {
|
|
lockedStakeables[assetID] = math.MaxUint64
|
|
} else {
|
|
lockedStakeables[assetID] = newBalance
|
|
}
|
|
}
|
|
default:
|
|
continue utxoFor
|
|
}
|
|
|
|
response.UTXOIDs = append(response.UTXOIDs, &utxo.UTXOID)
|
|
}
|
|
|
|
balances := maps.Clone(lockedStakeables)
|
|
for assetID, amount := range lockedNotStakeables {
|
|
newBalance, err := safemath.Add(balances[assetID], amount)
|
|
if err != nil {
|
|
balances[assetID] = math.MaxUint64
|
|
} else {
|
|
balances[assetID] = newBalance
|
|
}
|
|
}
|
|
for assetID, amount := range unlockeds {
|
|
newBalance, err := safemath.Add(balances[assetID], amount)
|
|
if err != nil {
|
|
balances[assetID] = math.MaxUint64
|
|
} else {
|
|
balances[assetID] = newBalance
|
|
}
|
|
}
|
|
|
|
response.Balances = newJSONBalanceMap(balances)
|
|
response.Unlockeds = newJSONBalanceMap(unlockeds)
|
|
response.LockedStakeables = newJSONBalanceMap(lockedStakeables)
|
|
response.LockedNotStakeables = newJSONBalanceMap(lockedNotStakeables)
|
|
response.Balance = response.Balances[s.vm.utxoAssetID]
|
|
response.Unlocked = response.Unlockeds[s.vm.utxoAssetID]
|
|
response.LockedStakeable = response.LockedStakeables[s.vm.utxoAssetID]
|
|
response.LockedNotStakeable = response.LockedNotStakeables[s.vm.utxoAssetID]
|
|
return nil
|
|
}
|
|
|
|
func newJSONBalanceMap(balanceMap map[ids.ID]uint64) map[ids.ID]avajson.Uint64 {
|
|
jsonBalanceMap := make(map[ids.ID]avajson.Uint64, len(balanceMap))
|
|
for assetID, amount := range balanceMap {
|
|
jsonBalanceMap[assetID] = avajson.Uint64(amount)
|
|
}
|
|
return jsonBalanceMap
|
|
}
|
|
|
|
// Index is an address and an associated UTXO.
|
|
// Marks a starting or stopping point when fetching UTXOs. Used for pagination.
|
|
type Index struct {
|
|
Address string `json:"address"` // The address as a string
|
|
UTXO string `json:"utxo"` // The UTXO ID as a string
|
|
}
|
|
|
|
// GetUTXOs returns the UTXOs controlled by the given addresses
|
|
func (s *Service) GetUTXOs(_ *http.Request, args *apitypes.GetUTXOsArgs, response *apitypes.GetUTXOsReply) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getUTXOs",
|
|
)
|
|
|
|
if len(args.Addresses) == 0 {
|
|
return errNoAddresses
|
|
}
|
|
if len(args.Addresses) > maxGetUTXOsAddrs {
|
|
return fmt.Errorf("number of addresses given, %d, exceeds maximum, %d", len(args.Addresses), maxGetUTXOsAddrs)
|
|
}
|
|
|
|
var sourceChain ids.ID
|
|
if args.SourceChain == "" {
|
|
sourceChain = s.vm.chainID
|
|
} else {
|
|
// Try to parse as ID first
|
|
chainID, err := ids.FromString(args.SourceChain)
|
|
if err != nil {
|
|
// If not a valid ID, try as an alias
|
|
// Note: bcLookup doesn't have Lookup method, would need reverse lookup
|
|
// For now, just return error
|
|
return fmt.Errorf("problem parsing source chainID %q: %w", args.SourceChain, err)
|
|
}
|
|
sourceChain = chainID
|
|
}
|
|
|
|
addrSet, err := lux.ParseServiceAddresses(s.addrManager, args.Addresses)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
startAddr := ids.ShortEmpty
|
|
startUTXO := ids.Empty
|
|
if args.StartIndex.Address != "" || args.StartIndex.UTXO != "" {
|
|
startAddr, err = lux.ParseServiceAddress(s.addrManager, args.StartIndex.Address)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse start index address %q: %w", args.StartIndex.Address, err)
|
|
}
|
|
startUTXO, err = ids.FromString(args.StartIndex.UTXO)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse start index utxo: %w", err)
|
|
}
|
|
}
|
|
|
|
var (
|
|
utxos []*lux.UTXO
|
|
endAddr ids.ShortID
|
|
endUTXOID ids.ID
|
|
)
|
|
limit := int(args.Limit)
|
|
if limit <= 0 || maxPageSize < limit {
|
|
limit = maxPageSize
|
|
}
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
if sourceChain == s.vm.chainID {
|
|
utxos, endAddr, endUTXOID, err = lux.GetPaginatedUTXOs(
|
|
s.vm.state,
|
|
addrSet,
|
|
startAddr,
|
|
startUTXO,
|
|
limit,
|
|
)
|
|
} else {
|
|
// For now, return empty results when shared memory is used
|
|
utxos = []*lux.UTXO{}
|
|
endAddr = ids.ShortEmpty
|
|
endUTXOID = ids.Empty
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("problem retrieving UTXOs: %w", err)
|
|
}
|
|
|
|
response.UTXOs = make([]string, len(utxos))
|
|
for i, utxo := range utxos {
|
|
bytes, err := utxo.WireBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't serialize UTXO %q: %w", utxo.InputID(), err)
|
|
}
|
|
response.UTXOs[i], err = formatting.Encode(args.Encoding, bytes)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode UTXO %s as %s: %w", utxo.InputID(), args.Encoding, err)
|
|
}
|
|
}
|
|
|
|
endAddress, err := s.addrManager.FormatLocalAddress(endAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("problem formatting address: %w", err)
|
|
}
|
|
|
|
response.EndIndex.Address = endAddress
|
|
response.EndIndex.UTXO = endUTXOID.String()
|
|
response.NumFetched = apitypes.Uint64(len(utxos))
|
|
response.Encoding = args.Encoding
|
|
return nil
|
|
}
|
|
|
|
// GetNetArgs are the arguments to GetNet
|
|
type GetNetArgs struct {
|
|
// ID of the net to retrieve information about
|
|
ChainID ids.ID `json:"netID"`
|
|
}
|
|
|
|
// GetNetResponse is the response from calling GetNet
|
|
type GetNetResponse struct {
|
|
// whether it is permissioned or not
|
|
IsPermissioned bool `json:"isPermissioned"`
|
|
// net auth information for a permissioned net
|
|
ControlKeys []string `json:"controlKeys"`
|
|
Threshold avajson.Uint32 `json:"threshold"`
|
|
Locktime avajson.Uint64 `json:"locktime"`
|
|
// net transformation tx ID for an elastic net
|
|
NetTransformationTxID ids.ID `json:"netTransformationTxID"`
|
|
// net conversion information for an L1
|
|
ConversionID ids.ID `json:"conversionID"`
|
|
ManagerChainID ids.ID `json:"managerChainID"`
|
|
ManagerAddress types.JSONByteSlice `json:"managerAddress"`
|
|
}
|
|
|
|
func (s *Service) GetNet(_ *http.Request, args *GetNetArgs, response *GetNetResponse) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getNet",
|
|
"netID", args.ChainID,
|
|
)
|
|
|
|
if args.ChainID == constants.PrimaryNetworkID {
|
|
return errPrimaryNetworkIsNotANet
|
|
}
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
netOwner, err := s.vm.state.GetNetOwner(args.ChainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
owner, ok := netOwner.(*secp256k1fx.OutputOwners)
|
|
if !ok {
|
|
return fmt.Errorf("expected *secp256k1fx.OutputOwners but got %T", netOwner)
|
|
}
|
|
controlAddrs := make([]string, len(owner.Addrs))
|
|
for i, controlKeyID := range owner.Addrs {
|
|
addr, err := s.addrManager.FormatLocalAddress(controlKeyID)
|
|
if err != nil {
|
|
return fmt.Errorf("problem formatting address: %w", err)
|
|
}
|
|
controlAddrs[i] = addr
|
|
}
|
|
|
|
response.ControlKeys = controlAddrs
|
|
response.Threshold = avajson.Uint32(owner.Threshold)
|
|
response.Locktime = avajson.Uint64(owner.Locktime)
|
|
|
|
switch netTransformationTx, err := s.vm.state.GetNetTransformation(args.ChainID); err {
|
|
case nil:
|
|
response.IsPermissioned = false
|
|
response.NetTransformationTxID = netTransformationTx.ID()
|
|
case database.ErrNotFound:
|
|
response.IsPermissioned = true
|
|
response.NetTransformationTxID = ids.Empty
|
|
default:
|
|
return err
|
|
}
|
|
|
|
switch c, err := s.vm.state.GetNetToL1Conversion(args.ChainID); err {
|
|
case nil:
|
|
response.IsPermissioned = false
|
|
response.ConversionID = c.ConversionID
|
|
response.ManagerChainID = c.ChainID
|
|
response.ManagerAddress = c.Addr
|
|
case database.ErrNotFound:
|
|
response.ConversionID = ids.Empty
|
|
response.ManagerChainID = ids.Empty
|
|
response.ManagerAddress = []byte(nil)
|
|
default:
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// APINet is a representation of a net used in API calls
|
|
type APINet struct {
|
|
// ID of the net
|
|
ID ids.ID `json:"id"`
|
|
|
|
// Each element of [ControlKeys] the address of a public key.
|
|
// A transaction to add a validator to this net requires
|
|
// signatures from [Threshold] of these keys to be valid.
|
|
ControlKeys []string `json:"controlKeys"`
|
|
Threshold avajson.Uint32 `json:"threshold"`
|
|
}
|
|
|
|
// GetNetsArgs are the arguments to GetNets
|
|
type GetNetsArgs struct {
|
|
// IDs of the nets to retrieve information about
|
|
// If omitted, gets all nets
|
|
IDs []ids.ID `json:"ids"`
|
|
}
|
|
|
|
// GetNetsResponse is the response from calling GetNets
|
|
type GetNetsResponse struct {
|
|
// Each element is a net that exists
|
|
// Null if there are no nets other than the primary network
|
|
Nets []APINet `json:"nets"`
|
|
}
|
|
|
|
// GetNets returns the nets whose ID are in [args.IDs]
|
|
// The response will include the primary network
|
|
func (s *Service) GetNets(_ *http.Request, args *GetNetsArgs, response *GetNetsResponse) error {
|
|
s.vm.log.Debug("deprecated API called",
|
|
"service", "platform",
|
|
"method", "getNets",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
getAll := len(args.IDs) == 0
|
|
if getAll {
|
|
netIDs, err := s.vm.state.GetChainIDs() // all nets
|
|
if err != nil {
|
|
return fmt.Errorf("error getting nets from database: %w", err)
|
|
}
|
|
|
|
response.Nets = make([]APINet, len(netIDs)+1)
|
|
for i, netID := range netIDs {
|
|
if _, err := s.vm.state.GetNetTransformation(netID); err == nil {
|
|
response.Nets[i] = APINet{
|
|
ID: netID,
|
|
ControlKeys: []string{},
|
|
Threshold: avajson.Uint32(0),
|
|
}
|
|
continue
|
|
}
|
|
|
|
netOwner, err := s.vm.state.GetNetOwner(netID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
owner, ok := netOwner.(*secp256k1fx.OutputOwners)
|
|
if !ok {
|
|
return fmt.Errorf("expected *secp256k1fx.OutputOwners but got %T", netOwner)
|
|
}
|
|
|
|
controlAddrs := make([]string, len(owner.Addrs))
|
|
for i, controlKeyID := range owner.Addrs {
|
|
addr, err := s.addrManager.FormatLocalAddress(controlKeyID)
|
|
if err != nil {
|
|
return fmt.Errorf("problem formatting address: %w", err)
|
|
}
|
|
controlAddrs[i] = addr
|
|
}
|
|
response.Nets[i] = APINet{
|
|
ID: netID,
|
|
ControlKeys: controlAddrs,
|
|
Threshold: avajson.Uint32(owner.Threshold),
|
|
}
|
|
}
|
|
// Include primary network
|
|
response.Nets[len(netIDs)] = APINet{
|
|
ID: constants.PrimaryNetworkID,
|
|
ControlKeys: []string{},
|
|
Threshold: avajson.Uint32(0),
|
|
}
|
|
return nil
|
|
}
|
|
|
|
netSet := set.NewSet[ids.ID](len(args.IDs))
|
|
for _, netID := range args.IDs {
|
|
if netSet.Contains(netID) {
|
|
continue
|
|
}
|
|
netSet.Add(netID)
|
|
|
|
if netID == constants.PrimaryNetworkID {
|
|
response.Nets = append(response.Nets,
|
|
APINet{
|
|
ID: constants.PrimaryNetworkID,
|
|
ControlKeys: []string{},
|
|
Threshold: avajson.Uint32(0),
|
|
},
|
|
)
|
|
continue
|
|
}
|
|
|
|
if _, err := s.vm.state.GetNetTransformation(netID); err == nil {
|
|
response.Nets = append(response.Nets, APINet{
|
|
ID: netID,
|
|
ControlKeys: []string{},
|
|
Threshold: avajson.Uint32(0),
|
|
})
|
|
continue
|
|
}
|
|
|
|
netOwner, err := s.vm.state.GetNetOwner(netID)
|
|
if err == database.ErrNotFound {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
owner, ok := netOwner.(*secp256k1fx.OutputOwners)
|
|
if !ok {
|
|
return fmt.Errorf("expected *secp256k1fx.OutputOwners but got %T", netOwner)
|
|
}
|
|
|
|
controlAddrs := make([]string, len(owner.Addrs))
|
|
for i, controlKeyID := range owner.Addrs {
|
|
addr, err := s.addrManager.FormatLocalAddress(controlKeyID)
|
|
if err != nil {
|
|
return fmt.Errorf("problem formatting address: %w", err)
|
|
}
|
|
controlAddrs[i] = addr
|
|
}
|
|
|
|
response.Nets = append(response.Nets, APINet{
|
|
ID: netID,
|
|
ControlKeys: controlAddrs,
|
|
Threshold: avajson.Uint32(owner.Threshold),
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// APIChain is the canonical wire-shape for a chain registered on the
|
|
// platform via CreateNetworkTx. Replaces APINet — same fields, named
|
|
// after the user-facing concept ("chain") rather than the internal
|
|
// canonical naming. The wire encoding is byte-identical so a
|
|
// deserializer that targets APIChain reads APINet responses and vice
|
|
// versa; no proxies need to translate.
|
|
type APIChain struct {
|
|
ID ids.ID `json:"id"`
|
|
ControlKeys []string `json:"controlKeys"`
|
|
Threshold avajson.Uint32 `json:"threshold"`
|
|
}
|
|
|
|
// GetChainsArgs are the arguments to GetChains. IDs is the optional
|
|
// allowlist filter — empty list returns every chain (including the
|
|
// primary network). Same shape as GetNetsArgs.
|
|
type GetChainsArgs struct {
|
|
IDs []ids.ID `json:"ids"`
|
|
}
|
|
|
|
// GetChainsResponse is the response from GetChains. Same shape as
|
|
// GetNetsResponse — `Chains` instead of `Nets`. Field names are
|
|
// canonical going forward; downstream parsers should target this
|
|
// struct.
|
|
type GetChainsResponse struct {
|
|
Chains []APIChain `json:"chains"`
|
|
}
|
|
|
|
// GetChains is the canonical RPC for listing chains registered on the
|
|
// platform. Behaviorally identical to GetNets (which it supersedes):
|
|
// returns every chain when IDs is empty, otherwise filters to the
|
|
// listed IDs, including the primary network sentinel
|
|
// (constants.PrimaryNetworkID) regardless of filter — callers
|
|
// deciding whether a chain is the primary network should compare the
|
|
// returned ID rather than relying on omission.
|
|
//
|
|
// Use this in new code; GetNets stays available indefinitely for
|
|
// wire-protocol compatibility but logs every call as deprecated.
|
|
func (s *Service) GetChains(r *http.Request, args *GetChainsArgs, response *GetChainsResponse) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getChains",
|
|
)
|
|
// Delegate to the existing implementation by translating shapes —
|
|
// avoids re-implementing the state walk and keeps the two methods
|
|
// in lock-step. APIChain is structurally identical to APINet (same
|
|
// JSON tags), so the cast is safe and free at runtime.
|
|
netsResp := &GetNetsResponse{}
|
|
if err := s.GetNets(r, &GetNetsArgs{IDs: args.IDs}, netsResp); err != nil {
|
|
return err
|
|
}
|
|
response.Chains = make([]APIChain, len(netsResp.Nets))
|
|
for i, n := range netsResp.Nets {
|
|
response.Chains[i] = APIChain(n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetStakingAssetIDArgs are the arguments to GetStakingAssetID
|
|
type GetStakingAssetIDArgs struct {
|
|
ChainID ids.ID `json:"netID"`
|
|
}
|
|
|
|
// GetStakingAssetIDResponse is the response from calling GetStakingAssetID
|
|
type GetStakingAssetIDResponse struct {
|
|
AssetID ids.ID `json:"assetID"`
|
|
}
|
|
|
|
// GetStakingAssetID returns the assetID of the token used to stake on the
|
|
// provided net
|
|
func (s *Service) GetStakingAssetID(_ *http.Request, args *GetStakingAssetIDArgs, response *GetStakingAssetIDResponse) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getStakingAssetID",
|
|
)
|
|
|
|
if args.ChainID == constants.PrimaryNetworkID {
|
|
response.AssetID = s.vm.utxoAssetID
|
|
return nil
|
|
}
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
transformNetIntf, err := s.vm.state.GetNetTransformation(args.ChainID)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"failed fetching net transformation for %s: %w",
|
|
args.ChainID,
|
|
err,
|
|
)
|
|
}
|
|
transformNet, ok := transformNetIntf.Unsigned.(*txs.TransformChainTx)
|
|
if !ok {
|
|
return fmt.Errorf(
|
|
"unexpected net transformation tx type fetched %T",
|
|
transformNetIntf.Unsigned,
|
|
)
|
|
}
|
|
|
|
response.AssetID = transformNet.AssetID
|
|
return nil
|
|
}
|
|
|
|
// GetCurrentValidatorsArgs are the arguments for calling GetCurrentValidators
|
|
type GetCurrentValidatorsArgs struct {
|
|
// Net we're listing the validators of
|
|
// If omitted, defaults to primary network
|
|
ChainID ids.ID `json:"netID"`
|
|
// NodeIDs of validators to request. If [NodeIDs]
|
|
// is empty, it fetches all current validators. If
|
|
// some nodeIDs are not currently validators, they
|
|
// will be omitted from the response.
|
|
NodeIDs []ids.NodeID `json:"nodeIDs"`
|
|
}
|
|
|
|
// GetCurrentValidatorsReply are the results from calling GetCurrentValidators.
|
|
// Each validator contains a list of delegators to itself.
|
|
type GetCurrentValidatorsReply struct {
|
|
Validators []any `json:"validators"`
|
|
}
|
|
|
|
func (s *Service) loadStakerTxAttributes(txID ids.ID) (*stakerAttributes, error) {
|
|
// Lookup tx from the cache first.
|
|
attr, found := s.stakerAttributesCache.Get(txID)
|
|
if found {
|
|
return attr, nil
|
|
}
|
|
|
|
// Tx not available in cache; pull it from disk and populate the cache.
|
|
tx, _, err := s.vm.state.GetTx(txID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch stakerTx := tx.Unsigned.(type) {
|
|
case txs.ValidatorTx:
|
|
var pop *signer.ProofOfPossession
|
|
if staker, ok := stakerTx.(*txs.AddPermissionlessValidatorTx); ok {
|
|
if s, ok := staker.Signer.(*signer.ProofOfPossession); ok {
|
|
pop = s
|
|
}
|
|
}
|
|
|
|
attr = &stakerAttributes{
|
|
shares: stakerTx.Shares(),
|
|
validationRewardsOwner: stakerTx.ValidationRewardsOwner(),
|
|
delegationRewardsOwner: stakerTx.DelegationRewardsOwner(),
|
|
proofOfPossession: pop,
|
|
}
|
|
|
|
case txs.DelegatorTx:
|
|
attr = &stakerAttributes{
|
|
rewardsOwner: stakerTx.RewardsOwner(),
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unexpected staker tx type %T", tx.Unsigned)
|
|
}
|
|
|
|
s.stakerAttributesCache.Put(txID, attr)
|
|
return attr, nil
|
|
}
|
|
|
|
// GetCurrentValidators returns the current validators. If a single nodeID
|
|
// is provided, full delegators information is also returned. Otherwise only
|
|
// delegators' number and total weight is returned.
|
|
func (s *Service) GetCurrentValidators(request *http.Request, args *GetCurrentValidatorsArgs, reply *GetCurrentValidatorsReply) error {
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "platform"),
|
|
log.String("method", "getCurrentValidators"),
|
|
)
|
|
|
|
// Create set of nodeIDs
|
|
nodeIDs := set.Of(args.NodeIDs...)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
// Check if net is L1
|
|
_, err := s.vm.state.GetNetToL1Conversion(args.ChainID)
|
|
if errors.Is(err, database.ErrNotFound) {
|
|
// Net is not L1, get validators for the net
|
|
reply.Validators, err = s.getPrimaryOrNetValidators(
|
|
args.ChainID,
|
|
nodeIDs,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get primary or net validators: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get net to L1 conversion: %w", err)
|
|
}
|
|
|
|
// Net is L1, get validators for L1
|
|
reply.Validators, err = s.getL1Validators(
|
|
request.Context(),
|
|
args.ChainID,
|
|
nodeIDs,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get L1 validators: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) getL1Validators(
|
|
ctx context.Context,
|
|
netID ids.ID,
|
|
nodeIDs set.Set[ids.NodeID],
|
|
) ([]any, error) {
|
|
validators := []any{}
|
|
baseStakers, l1Validators, _, err := s.vm.state.GetCurrentValidators(ctx, netID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get current validators: %w", err)
|
|
}
|
|
|
|
fetchAll := nodeIDs.Len() == 0
|
|
|
|
for _, staker := range baseStakers {
|
|
if !fetchAll && !nodeIDs.Contains(staker.NodeID) {
|
|
continue
|
|
}
|
|
|
|
apiStaker := toPlatformStaker(staker)
|
|
validators = append(validators, apiStaker)
|
|
}
|
|
|
|
for _, l1Validator := range l1Validators {
|
|
if !fetchAll && !nodeIDs.Contains(l1Validator.NodeID) {
|
|
continue
|
|
}
|
|
|
|
apiL1Vdr, err := s.convertL1ValidatorToAPI(l1Validator)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting L1 validator to API format: %w", err)
|
|
}
|
|
|
|
validators = append(validators, apiL1Vdr)
|
|
}
|
|
|
|
return validators, nil
|
|
}
|
|
|
|
func (s *Service) getPrimaryOrNetValidators(netID ids.ID, nodeIDs set.Set[ids.NodeID]) ([]any, error) {
|
|
numNodeIDs := nodeIDs.Len()
|
|
|
|
targetStakers := make([]*state.Staker, 0, numNodeIDs)
|
|
|
|
// Validator's node ID as string --> Delegators to them
|
|
vdrToDelegators := map[ids.NodeID][]platformapitypes.PrimaryDelegator{}
|
|
|
|
validators := []any{}
|
|
|
|
if numNodeIDs == 0 { // Include all nodes
|
|
currentStakerIterator, err := s.vm.state.GetCurrentStakerIterator()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for currentStakerIterator.Next() {
|
|
staker := currentStakerIterator.Value()
|
|
if netID != staker.ChainID {
|
|
continue
|
|
}
|
|
targetStakers = append(targetStakers, staker)
|
|
}
|
|
currentStakerIterator.Release()
|
|
} else {
|
|
for nodeID := range nodeIDs {
|
|
staker, err := s.vm.state.GetCurrentValidator(netID, nodeID)
|
|
switch err {
|
|
case nil:
|
|
case database.ErrNotFound:
|
|
// nothing to do, continue
|
|
continue
|
|
default:
|
|
return nil, err
|
|
}
|
|
targetStakers = append(targetStakers, staker)
|
|
|
|
// Delegator iteration happens per-nodeID; acceptable for small numNodeIDs.
|
|
delegatorsIt, err := s.vm.state.GetCurrentDelegatorIterator(netID, nodeID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for delegatorsIt.Next() {
|
|
staker := delegatorsIt.Value()
|
|
targetStakers = append(targetStakers, staker)
|
|
}
|
|
delegatorsIt.Release()
|
|
}
|
|
}
|
|
|
|
for _, currentStaker := range targetStakers {
|
|
apiStaker := toPlatformStaker(currentStaker)
|
|
potentialReward := avajson.Uint64(currentStaker.PotentialReward)
|
|
|
|
delegateeReward, err := s.vm.state.GetDelegateeReward(currentStaker.ChainID, currentStaker.NodeID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
jsonDelegateeReward := avajson.Uint64(delegateeReward)
|
|
|
|
switch currentStaker.Priority {
|
|
case txs.PrimaryNetworkValidatorCurrentPriority, txs.ChainPermissionlessValidatorCurrentPriority:
|
|
attr, err := s.loadStakerTxAttributes(currentStaker.TxID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
shares := attr.shares
|
|
delegationFee := avajson.Float32(100 * float32(shares) / float32(reward.PercentDenominator))
|
|
var (
|
|
uptime *avajson.Float32
|
|
connected *bool
|
|
)
|
|
if netID == constants.PrimaryNetworkID {
|
|
rawUptime, err := s.vm.uptimeManager.CalculateUptimePercentFrom(currentStaker.NodeID, netID, currentStaker.StartTime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Transform this to a percentage (0-100) to make it consistent
|
|
// with observedUptime in info.peers API
|
|
currentUptime := avajson.Float32(rawUptime * 100)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// connected field left nil - IsConnected method no longer exists
|
|
uptime = ¤tUptime
|
|
}
|
|
|
|
var (
|
|
validationRewardOwner *platformapitypes.Owner
|
|
delegationRewardOwner *platformapitypes.Owner
|
|
)
|
|
validationOwner, ok := attr.validationRewardsOwner.(*secp256k1fx.OutputOwners)
|
|
if ok {
|
|
validationRewardOwner, err = s.getAPIOwner(validationOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
delegationOwner, ok := attr.delegationRewardsOwner.(*secp256k1fx.OutputOwners)
|
|
if ok {
|
|
delegationRewardOwner, err = s.getAPIOwner(delegationOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
vdr := platformapitypes.PermissionlessValidator{
|
|
Staker: apiStaker,
|
|
Uptime: uptime,
|
|
Connected: connected,
|
|
PotentialReward: &potentialReward,
|
|
AccruedDelegateeReward: &jsonDelegateeReward,
|
|
ValidationRewardOwner: validationRewardOwner,
|
|
DelegationRewardOwner: delegationRewardOwner,
|
|
DelegationFee: delegationFee,
|
|
Signer: attr.proofOfPossession,
|
|
}
|
|
validators = append(validators, vdr)
|
|
|
|
case txs.PrimaryNetworkDelegatorCurrentPriority, txs.ChainPermissionlessDelegatorCurrentPriority:
|
|
var rewardOwner *platformapitypes.Owner
|
|
// If we are handling multiple nodeIDs, we don't return the
|
|
// delegator information.
|
|
if numNodeIDs == 1 {
|
|
attr, err := s.loadStakerTxAttributes(currentStaker.TxID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
owner, ok := attr.rewardsOwner.(*secp256k1fx.OutputOwners)
|
|
if ok {
|
|
rewardOwner, err = s.getAPIOwner(owner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
delegator := platformapitypes.PrimaryDelegator{
|
|
Staker: apiStaker,
|
|
RewardOwner: rewardOwner,
|
|
PotentialReward: &potentialReward,
|
|
}
|
|
vdrToDelegators[delegator.NodeID] = append(vdrToDelegators[delegator.NodeID], delegator)
|
|
|
|
case txs.ChainPermissionedValidatorCurrentPriority:
|
|
validators = append(validators, apiStaker)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unexpected staker priority %d", currentStaker.Priority)
|
|
}
|
|
}
|
|
|
|
// handle delegators' information
|
|
for i, vdrIntf := range validators {
|
|
vdr, ok := vdrIntf.(platformapitypes.PermissionlessValidator)
|
|
if !ok {
|
|
continue
|
|
}
|
|
delegators, ok := vdrToDelegators[vdr.NodeID]
|
|
if !ok {
|
|
// If we are expected to populate the delegators field, we should
|
|
// always return a non-nil value.
|
|
delegators = []platformapitypes.PrimaryDelegator{}
|
|
}
|
|
delegatorCount := avajson.Uint64(len(delegators))
|
|
delegatorWeight := avajson.Uint64(0)
|
|
for _, d := range delegators {
|
|
delegatorWeight += d.Weight
|
|
}
|
|
|
|
vdr.DelegatorCount = &delegatorCount
|
|
vdr.DelegatorWeight = &delegatorWeight
|
|
|
|
if numNodeIDs == 1 {
|
|
// queried a specific validator, load all of its delegators
|
|
vdr.Delegators = &delegators
|
|
}
|
|
validators[i] = vdr
|
|
}
|
|
|
|
return validators, nil
|
|
}
|
|
|
|
type GetL1ValidatorArgs struct {
|
|
ValidationID ids.ID `json:"validationID"`
|
|
}
|
|
|
|
type GetL1ValidatorReply struct {
|
|
platformapitypes.APIL1Validator
|
|
ChainID ids.ID `json:"netID"`
|
|
// Height is the height of the last accepted block
|
|
Height avajson.Uint64 `json:"height"`
|
|
}
|
|
|
|
// GetL1Validator returns the L1 validator if it exists
|
|
func (s *Service) GetL1Validator(r *http.Request, args *GetL1ValidatorArgs, reply *GetL1ValidatorReply) error {
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "platform"),
|
|
log.String("method", "getL1Validator"),
|
|
log.Stringer("validationID", args.ValidationID),
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
l1Validator, err := s.vm.state.GetL1Validator(args.ValidationID)
|
|
if err != nil {
|
|
return fmt.Errorf("fetching L1 validator %q failed: %w", args.ValidationID, err)
|
|
}
|
|
|
|
ctx := r.Context()
|
|
height, err := s.vm.GetCurrentHeight(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get the current height: %w", err)
|
|
}
|
|
apiVdr, err := s.convertL1ValidatorToAPI(l1Validator)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert L1 validator to API format: %w", err)
|
|
}
|
|
|
|
reply.APIL1Validator = apiVdr
|
|
reply.ChainID = l1Validator.ChainID
|
|
reply.Height = avajson.Uint64(height)
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) convertL1ValidatorToAPI(vdr state.L1Validator) (platformapitypes.APIL1Validator, error) {
|
|
var remainingBalanceOwner message.PChainOwner
|
|
if _, err := txs.Codec.Unmarshal(vdr.RemainingBalanceOwner, &remainingBalanceOwner); err != nil {
|
|
return platformapitypes.APIL1Validator{}, fmt.Errorf("failed unmarshalling remaining balance owner: %w", err)
|
|
}
|
|
remainingBalanceAPIOwner, err := s.getAPIOwner(&secp256k1fx.OutputOwners{
|
|
Threshold: remainingBalanceOwner.Threshold,
|
|
Addrs: remainingBalanceOwner.Addresses,
|
|
})
|
|
if err != nil {
|
|
return platformapitypes.APIL1Validator{}, fmt.Errorf("failed formatting remaining balance owner: %w", err)
|
|
}
|
|
|
|
var deactivationOwner message.PChainOwner
|
|
if _, err := txs.Codec.Unmarshal(vdr.DeactivationOwner, &deactivationOwner); err != nil {
|
|
return platformapitypes.APIL1Validator{}, fmt.Errorf("failed unmarshalling deactivation owner: %w", err)
|
|
}
|
|
deactivationAPIOwner, err := s.getAPIOwner(&secp256k1fx.OutputOwners{
|
|
Threshold: deactivationOwner.Threshold,
|
|
Addrs: deactivationOwner.Addresses,
|
|
})
|
|
if err != nil {
|
|
return platformapitypes.APIL1Validator{}, fmt.Errorf("failed formatting deactivation owner: %w", err)
|
|
}
|
|
|
|
pubKey := types.JSONByteSlice(bls.PublicKeyToCompressedBytes(
|
|
bls.PublicKeyFromValidUncompressedBytes(vdr.PublicKey),
|
|
))
|
|
minNonce := avajson.Uint64(vdr.MinNonce)
|
|
|
|
apiVdr := platformapitypes.APIL1Validator{
|
|
NodeID: vdr.NodeID,
|
|
StartTime: avajson.Uint64(vdr.StartTime),
|
|
Weight: avajson.Uint64(vdr.Weight),
|
|
BaseL1Validator: platformapitypes.BaseL1Validator{
|
|
ValidationID: &vdr.ValidationID,
|
|
PublicKey: &pubKey,
|
|
RemainingBalanceOwner: remainingBalanceAPIOwner,
|
|
DeactivationOwner: deactivationAPIOwner,
|
|
MinNonce: &minNonce,
|
|
},
|
|
}
|
|
zero := avajson.Uint64(0)
|
|
apiVdr.Balance = &zero
|
|
if vdr.EndAccumulatedFee != 0 {
|
|
accruedFees := s.vm.state.GetAccruedFees()
|
|
balance := avajson.Uint64(vdr.EndAccumulatedFee - accruedFees)
|
|
apiVdr.Balance = &balance
|
|
}
|
|
return apiVdr, nil
|
|
}
|
|
|
|
// GetCurrentSupplyArgs are the arguments for calling GetCurrentSupply
|
|
type GetCurrentSupplyArgs struct {
|
|
ChainID ids.ID `json:"netID"`
|
|
}
|
|
|
|
// GetCurrentSupplyReply are the results from calling GetCurrentSupply
|
|
type GetCurrentSupplyReply struct {
|
|
Supply avajson.Uint64 `json:"supply"`
|
|
Height avajson.Uint64 `json:"height"`
|
|
}
|
|
|
|
// GetCurrentSupply returns an upper bound on the supply of LUX in the system
|
|
func (s *Service) GetCurrentSupply(r *http.Request, args *GetCurrentSupplyArgs, reply *GetCurrentSupplyReply) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getCurrentSupply",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
supply, err := s.vm.state.GetCurrentSupply(args.ChainID)
|
|
if err != nil {
|
|
return fmt.Errorf("fetching current supply failed: %w", err)
|
|
}
|
|
reply.Supply = avajson.Uint64(supply)
|
|
|
|
ctx := r.Context()
|
|
height, err := s.vm.GetCurrentHeight(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("fetching current height failed: %w", err)
|
|
}
|
|
reply.Height = avajson.Uint64(height)
|
|
|
|
return nil
|
|
}
|
|
|
|
// SampleValidatorsArgs are the arguments for calling SampleValidators
|
|
type SampleValidatorsArgs struct {
|
|
// Number of validators in the sample
|
|
Size avajson.Uint16 `json:"size"`
|
|
|
|
// ID of net to sample validators from
|
|
// If omitted, defaults to the primary network
|
|
ChainID ids.ID `json:"netID"`
|
|
}
|
|
|
|
// SampleValidatorsReply are the results from calling Sample
|
|
type SampleValidatorsReply struct {
|
|
Validators []ids.NodeID `json:"validators"`
|
|
}
|
|
|
|
// SampleValidators returns a sampling of the list of current validators
|
|
func (s *Service) SampleValidators(_ *http.Request, args *SampleValidatorsArgs, reply *SampleValidatorsReply) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "sampleValidators",
|
|
"size", uint16(args.Size),
|
|
)
|
|
|
|
// Sampling is not supported by validators.Manager; return an error.
|
|
return fmt.Errorf("validator sampling is not supported")
|
|
}
|
|
|
|
// GetBlockchainStatusArgs is the arguments for calling GetBlockchainStatus
|
|
// [BlockchainID] is the ID of or an alias of the blockchain to get the status of.
|
|
type GetBlockchainStatusArgs struct {
|
|
BlockchainID string `json:"blockchainID"`
|
|
}
|
|
|
|
// GetBlockchainStatusReply is the reply from calling GetBlockchainStatus
|
|
// [Status] is the blockchain's status.
|
|
type GetBlockchainStatusReply struct {
|
|
Status status.BlockchainStatus `json:"status"`
|
|
}
|
|
|
|
// GetBlockchainStatus gets the status of a blockchain with the ID [args.BlockchainID].
|
|
func (s *Service) GetBlockchainStatus(r *http.Request, args *GetBlockchainStatusArgs, reply *GetBlockchainStatusReply) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getBlockchainStatus",
|
|
)
|
|
|
|
if args.BlockchainID == "" {
|
|
return errMissingBlockchainID
|
|
}
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
// if its aliased then vm created this chain.
|
|
if aliasedID, err := s.vm.Chains.Lookup(args.BlockchainID); err == nil {
|
|
if s.nodeValidates(aliasedID) {
|
|
reply.Status = status.Validating
|
|
return nil
|
|
}
|
|
|
|
reply.Status = status.Syncing
|
|
return nil
|
|
}
|
|
|
|
blockchainID, err := ids.FromString(args.BlockchainID)
|
|
if err != nil {
|
|
return fmt.Errorf("problem parsing blockchainID %q: %w", args.BlockchainID, err)
|
|
}
|
|
|
|
ctx := r.Context()
|
|
lastAcceptedID, err := s.vm.LastAccepted(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem loading last accepted ID: %w", err)
|
|
}
|
|
|
|
exists, err := s.chainExists(ctx, lastAcceptedID, blockchainID)
|
|
if err != nil {
|
|
return fmt.Errorf("problem looking up blockchain: %w", err)
|
|
}
|
|
if exists {
|
|
reply.Status = status.Created
|
|
return nil
|
|
}
|
|
|
|
preferredBlkID := s.vm.manager.Preferred()
|
|
preferred, err := s.chainExists(ctx, preferredBlkID, blockchainID)
|
|
if err != nil {
|
|
return fmt.Errorf("problem looking up blockchain: %w", err)
|
|
}
|
|
if preferred {
|
|
reply.Status = status.Preferred
|
|
} else {
|
|
reply.Status = status.UnknownChain
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) nodeValidates(blockchainID ids.ID) bool {
|
|
chainTx, _, err := s.vm.state.GetTx(blockchainID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
chain, ok := chainTx.Unsigned.(*txs.CreateChainTx)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
_, isValidator := s.vm.Validators.GetValidator(chain.ChainID, s.vm.nodeID)
|
|
return isValidator
|
|
}
|
|
|
|
func (s *Service) chainExists(ctx context.Context, blockID ids.ID, chainID ids.ID) (bool, error) {
|
|
state, ok := s.vm.manager.GetState(blockID)
|
|
if !ok {
|
|
block, err := s.vm.GetBlock(ctx, blockID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
state, ok = s.vm.manager.GetState(block.Parent())
|
|
if !ok {
|
|
return false, errMissingDecisionBlock
|
|
}
|
|
}
|
|
|
|
tx, _, err := state.GetTx(chainID)
|
|
if err == database.ErrNotFound {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
_, ok = tx.Unsigned.(*txs.CreateChainTx)
|
|
return ok, nil
|
|
}
|
|
|
|
// ValidatedByArgs is the arguments for calling ValidatedBy
|
|
type ValidatedByArgs struct {
|
|
// ValidatedBy returns the ID of the Net validating the blockchain with this ID
|
|
BlockchainID ids.ID `json:"blockchainID"`
|
|
}
|
|
|
|
// ValidatedByResponse is the reply from calling ValidatedBy
|
|
type ValidatedByResponse struct {
|
|
// ID of the Net validating the specified blockchain
|
|
ChainID ids.ID `json:"netID"`
|
|
}
|
|
|
|
// ValidatedBy returns the ID of the Net that validates [args.BlockchainID]
|
|
func (s *Service) ValidatedBy(r *http.Request, args *ValidatedByArgs, response *ValidatedByResponse) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "validatedBy",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
// GetChainID is not available in the current validators.Manager interface
|
|
// Return primary network for now
|
|
response.ChainID = constants.PrimaryNetworkID
|
|
return nil
|
|
}
|
|
|
|
// ValidatesArgs are the arguments to Validates
|
|
type ValidatesArgs struct {
|
|
ChainID ids.ID `json:"netID"`
|
|
}
|
|
|
|
// ValidatesResponse is the response from calling Validates
|
|
type ValidatesResponse struct {
|
|
BlockchainIDs []ids.ID `json:"blockchainIDs"`
|
|
}
|
|
|
|
// Validates returns the IDs of the blockchains validated by [args.ChainID]
|
|
func (s *Service) Validates(_ *http.Request, args *ValidatesArgs, response *ValidatesResponse) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "validates",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
if args.ChainID != constants.PrimaryNetworkID {
|
|
netTx, _, err := s.vm.state.GetTx(args.ChainID)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"problem retrieving net %q: %w",
|
|
args.ChainID,
|
|
err,
|
|
)
|
|
}
|
|
_, ok := netTx.Unsigned.(*txs.CreateChainTx)
|
|
if !ok {
|
|
return fmt.Errorf("%q is not a net", args.ChainID)
|
|
}
|
|
}
|
|
|
|
// Get the chains that exist
|
|
chains, err := s.vm.state.GetChains(args.ChainID)
|
|
if err != nil {
|
|
return fmt.Errorf("problem retrieving chains for net %q: %w", args.ChainID, err)
|
|
}
|
|
|
|
response.BlockchainIDs = make([]ids.ID, len(chains))
|
|
for i, chain := range chains {
|
|
response.BlockchainIDs[i] = chain.ID()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// APIBlockchain is the representation of a blockchain used in API calls
|
|
type APIBlockchain struct {
|
|
// Blockchain's ID
|
|
ID ids.ID `json:"id"`
|
|
|
|
// Blockchain's (non-unique) human-readable name
|
|
Name string `json:"name"`
|
|
|
|
// Net that validates the blockchain
|
|
ChainID ids.ID `json:"netID"`
|
|
|
|
// Virtual Machine the blockchain runs
|
|
VMID ids.ID `json:"vmID"`
|
|
}
|
|
|
|
// GetBlockchainsResponse is the response from a call to GetBlockchains
|
|
type GetBlockchainsResponse struct {
|
|
// blockchains that exist
|
|
Blockchains []APIBlockchain `json:"blockchains"`
|
|
}
|
|
|
|
// GetBlockchains returns all of the blockchains that exist
|
|
func (s *Service) GetBlockchains(_ *http.Request, _ *struct{}, response *GetBlockchainsResponse) error {
|
|
s.vm.log.Debug("deprecated API called",
|
|
"service", "platform",
|
|
"method", "getBlockchains",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
netIDs, err := s.vm.state.GetChainIDs()
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't retrieve nets: %w", err)
|
|
}
|
|
|
|
response.Blockchains = []APIBlockchain{}
|
|
for _, netID := range netIDs {
|
|
chains, err := s.vm.state.GetChains(netID)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"couldn't retrieve chains for net %q: %w",
|
|
netID,
|
|
err,
|
|
)
|
|
}
|
|
|
|
for _, chainTx := range chains {
|
|
chainID := chainTx.ID()
|
|
chain, ok := chainTx.Unsigned.(*txs.CreateChainTx)
|
|
if !ok {
|
|
return fmt.Errorf("expected tx type *txs.CreateChainTx but got %T", chainTx.Unsigned)
|
|
}
|
|
response.Blockchains = append(response.Blockchains, APIBlockchain{
|
|
ID: chainID,
|
|
Name: chain.BlockchainName,
|
|
ChainID: netID,
|
|
VMID: chain.VMID,
|
|
})
|
|
}
|
|
}
|
|
|
|
chains, err := s.vm.state.GetChains(constants.PrimaryNetworkID)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't retrieve nets: %w", err)
|
|
}
|
|
for _, chainTx := range chains {
|
|
chainID := chainTx.ID()
|
|
chain, ok := chainTx.Unsigned.(*txs.CreateChainTx)
|
|
if !ok {
|
|
return fmt.Errorf("expected tx type *txs.CreateChainTx but got %T", chainTx.Unsigned)
|
|
}
|
|
response.Blockchains = append(response.Blockchains, APIBlockchain{
|
|
ID: chainID,
|
|
Name: chain.BlockchainName,
|
|
ChainID: constants.PrimaryNetworkID,
|
|
VMID: chain.VMID,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) IssueTx(_ *http.Request, args *apitypes.FormattedTx, response *apitypes.JSONTxID) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "issueTx",
|
|
)
|
|
|
|
txBytes, err := formatting.Decode(args.Encoding, args.Tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem decoding transaction: %w", err)
|
|
}
|
|
tx, err := txs.Parse(txs.Codec, txBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse tx: %w", err)
|
|
}
|
|
|
|
if err := s.vm.issueTxFromRPC(tx); err != nil {
|
|
return fmt.Errorf("couldn't issue tx: %w", err)
|
|
}
|
|
|
|
response.TxID = tx.ID()
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) GetTx(_ *http.Request, args *apitypes.GetTxArgs, response *apitypes.GetTxReply) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getTx",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
tx, _, err := s.vm.state.GetTx(args.TxID)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't get tx: %w", err)
|
|
}
|
|
response.Encoding = args.Encoding
|
|
|
|
var result any
|
|
if args.Encoding == formatting.JSON {
|
|
tx.Unsigned.InitRuntime(s.vm.rt)
|
|
result = tx
|
|
} else {
|
|
result, err = formatting.Encode(args.Encoding, tx.Bytes())
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode tx as %s: %w", args.Encoding, err)
|
|
}
|
|
}
|
|
|
|
response.Tx, err = json.Marshal(result)
|
|
return err
|
|
}
|
|
|
|
type GetTxStatusArgs struct {
|
|
TxID ids.ID `json:"txID"`
|
|
}
|
|
|
|
type GetTxStatusResponse struct {
|
|
Status status.Status `json:"status"`
|
|
// Reason this tx was dropped.
|
|
// Only non-empty if Status is dropped
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// GetTxStatus gets a tx's status
|
|
func (s *Service) GetTxStatus(_ *http.Request, args *GetTxStatusArgs, response *GetTxStatusResponse) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getTxStatus",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
_, txStatus, err := s.vm.state.GetTx(args.TxID)
|
|
if err == nil { // Found the status. Report it.
|
|
response.Status = txStatus
|
|
return nil
|
|
}
|
|
if err != database.ErrNotFound {
|
|
return err
|
|
}
|
|
|
|
// The status of this transaction is not in the database - check if the tx
|
|
// is in the preferred block's db. If so, return that it's processing.
|
|
preferredID := s.vm.manager.Preferred()
|
|
onAccept, ok := s.vm.manager.GetState(preferredID)
|
|
if !ok {
|
|
// Preferred state may not be cached after recent block acceptance.
|
|
// Fall back to last accepted state.
|
|
lastAccepted := s.vm.manager.LastAccepted()
|
|
onAccept, ok = s.vm.manager.GetState(lastAccepted)
|
|
if !ok {
|
|
return fmt.Errorf("could not retrieve state for block %s", preferredID)
|
|
}
|
|
}
|
|
|
|
_, _, err = onAccept.GetTx(args.TxID)
|
|
if err == nil {
|
|
// Found the status in the preferred block's db. Report tx is processing.
|
|
response.Status = status.Processing
|
|
return nil
|
|
}
|
|
if err != database.ErrNotFound {
|
|
return err
|
|
}
|
|
|
|
if _, ok := s.vm.Builder.Get(args.TxID); ok {
|
|
// Found the tx in the mempool. Report tx is processing.
|
|
response.Status = status.Processing
|
|
return nil
|
|
}
|
|
|
|
// Note: we check if tx is dropped only after having looked for it
|
|
// in the database and the mempool, because dropped txs may be re-issued.
|
|
reason := s.vm.Builder.GetDropReason(args.TxID)
|
|
if reason == nil {
|
|
// The tx isn't being tracked by the node.
|
|
response.Status = status.Unknown
|
|
return nil
|
|
}
|
|
|
|
// The tx was recently dropped because it was invalid.
|
|
response.Status = status.Dropped
|
|
response.Reason = reason.Error()
|
|
return nil
|
|
}
|
|
|
|
type GetStakeArgs struct {
|
|
apitypes.JSONAddresses
|
|
ValidatorsOnly bool `json:"validatorsOnly"`
|
|
Encoding formatting.Encoding `json:"encoding"`
|
|
}
|
|
|
|
// GetStakeReply is the response from calling GetStake.
|
|
type GetStakeReply struct {
|
|
Staked avajson.Uint64 `json:"staked"`
|
|
Stakeds map[ids.ID]avajson.Uint64 `json:"stakeds"`
|
|
// String representation of staked outputs
|
|
// Each is of type lux.TransferableOutput
|
|
Outputs []string `json:"stakedOutputs"`
|
|
// Encoding of [Outputs]
|
|
Encoding formatting.Encoding `json:"encoding"`
|
|
}
|
|
|
|
// GetStake returns the amount of nLUX that [args.Addresses] have cumulatively
|
|
// staked on the Primary Network.
|
|
//
|
|
// This method assumes that each stake output has only owner
|
|
// This method assumes only LUX can be staked
|
|
// This method only concerns itself with the Primary Network, not nets
|
|
// in a data structure rather than re-calculating it by iterating over stakers
|
|
func (s *Service) GetStake(_ *http.Request, args *GetStakeArgs, response *GetStakeReply) error {
|
|
s.vm.log.Debug("deprecated API called",
|
|
"service", "platform",
|
|
"method", "getStake",
|
|
)
|
|
|
|
if len(args.Addresses) > maxGetStakeAddrs {
|
|
return fmt.Errorf("%d addresses provided but this method can take at most %d", len(args.Addresses), maxGetStakeAddrs)
|
|
}
|
|
|
|
addrs, err := lux.ParseServiceAddresses(s.addrManager, args.Addresses)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
currentStakerIterator, err := s.vm.state.GetCurrentStakerIterator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer currentStakerIterator.Release()
|
|
|
|
var (
|
|
totalAmountStaked = make(map[ids.ID]uint64)
|
|
stakedOuts []lux.TransferableOutput
|
|
)
|
|
for currentStakerIterator.Next() { // Iterates over current stakers
|
|
staker := currentStakerIterator.Value()
|
|
|
|
if args.ValidatorsOnly && !staker.Priority.IsValidator() {
|
|
continue
|
|
}
|
|
|
|
tx, _, err := s.vm.state.GetTx(staker.TxID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stakedOuts = append(stakedOuts, getStakeHelper(tx, addrs, totalAmountStaked)...)
|
|
}
|
|
|
|
pendingStakerIterator, err := s.vm.state.GetPendingStakerIterator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer pendingStakerIterator.Release()
|
|
|
|
for pendingStakerIterator.Next() { // Iterates over pending stakers
|
|
staker := pendingStakerIterator.Value()
|
|
|
|
if args.ValidatorsOnly && !staker.Priority.IsValidator() {
|
|
continue
|
|
}
|
|
|
|
tx, _, err := s.vm.state.GetTx(staker.TxID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stakedOuts = append(stakedOuts, getStakeHelper(tx, addrs, totalAmountStaked)...)
|
|
}
|
|
|
|
response.Stakeds = newJSONBalanceMap(totalAmountStaked)
|
|
response.Staked = response.Stakeds[s.vm.utxoAssetID]
|
|
response.Outputs = make([]string, len(stakedOuts))
|
|
for i, output := range stakedOuts {
|
|
bytes, err := txs.Codec.Marshal(txs.CodecVersion, output)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't serialize output %s: %w", output.ID, err)
|
|
}
|
|
response.Outputs[i], err = formatting.Encode(args.Encoding, bytes)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode output %s as %s: %w", output.ID, args.Encoding, err)
|
|
}
|
|
}
|
|
response.Encoding = args.Encoding
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetMinStakeArgs are the arguments for calling GetMinStake.
|
|
type GetMinStakeArgs struct {
|
|
ChainID ids.ID `json:"netID"`
|
|
}
|
|
|
|
// GetMinStakeReply is the response from calling GetMinStake.
|
|
type GetMinStakeReply struct {
|
|
// The minimum amount of tokens one must bond to be a validator
|
|
MinValidatorStake avajson.Uint64 `json:"minValidatorStake"`
|
|
// Minimum stake, in nLUX, that can be delegated on the primary network
|
|
MinDelegatorStake avajson.Uint64 `json:"minDelegatorStake"`
|
|
}
|
|
|
|
// GetMinStake returns the minimum staking amount in nLUX.
|
|
func (s *Service) GetMinStake(_ *http.Request, args *GetMinStakeArgs, reply *GetMinStakeReply) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getMinStake",
|
|
)
|
|
|
|
if args.ChainID == constants.PrimaryNetworkID {
|
|
reply.MinValidatorStake = avajson.Uint64(s.vm.MinValidatorStake)
|
|
reply.MinDelegatorStake = avajson.Uint64(s.vm.MinDelegatorStake)
|
|
return nil
|
|
}
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
transformNetIntf, err := s.vm.state.GetNetTransformation(args.ChainID)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"failed fetching net transformation for %s: %w",
|
|
args.ChainID,
|
|
err,
|
|
)
|
|
}
|
|
transformNet, ok := transformNetIntf.Unsigned.(*txs.TransformChainTx)
|
|
if !ok {
|
|
return fmt.Errorf(
|
|
"unexpected net transformation tx type fetched %T",
|
|
transformNetIntf.Unsigned,
|
|
)
|
|
}
|
|
|
|
reply.MinValidatorStake = avajson.Uint64(transformNet.MinValidatorStake)
|
|
reply.MinDelegatorStake = avajson.Uint64(transformNet.MinDelegatorStake)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTotalStakeArgs are the arguments for calling GetTotalStake
|
|
type GetTotalStakeArgs struct {
|
|
// Net we're getting the total stake
|
|
// If omitted returns Primary network weight
|
|
ChainID ids.ID `json:"netID"`
|
|
}
|
|
|
|
// GetTotalStakeReply is the response from calling GetTotalStake.
|
|
type GetTotalStakeReply struct {
|
|
// Deprecated: Use Weight instead.
|
|
Stake avajson.Uint64 `json:"stake"`
|
|
|
|
Weight avajson.Uint64 `json:"weight"`
|
|
}
|
|
|
|
// GetTotalStake returns the total amount staked on the Primary Network
|
|
func (s *Service) GetTotalStake(_ *http.Request, args *GetTotalStakeArgs, reply *GetTotalStakeReply) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getTotalStake",
|
|
)
|
|
|
|
totalWeight, err := s.vm.Validators.TotalWeight(args.ChainID)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't get total weight: %w", err)
|
|
}
|
|
weight := avajson.Uint64(totalWeight)
|
|
reply.Weight = weight
|
|
reply.Stake = weight
|
|
return nil
|
|
}
|
|
|
|
// GetRewardUTXOsReply defines the GetRewardUTXOs replies returned from the API
|
|
type GetRewardUTXOsReply struct {
|
|
// Number of UTXOs returned
|
|
NumFetched avajson.Uint64 `json:"numFetched"`
|
|
// The UTXOs
|
|
UTXOs []string `json:"utxos"`
|
|
// Encoding specifies the encoding format the UTXOs are returned in
|
|
Encoding formatting.Encoding `json:"encoding"`
|
|
}
|
|
|
|
// GetRewardUTXOs returns the UTXOs that were rewarded after the provided
|
|
// transaction's staking period ended.
|
|
func (s *Service) GetRewardUTXOs(_ *http.Request, args *apitypes.GetTxArgs, reply *GetRewardUTXOsReply) error {
|
|
s.vm.log.Debug("deprecated API called",
|
|
"service", "platform",
|
|
"method", "getRewardUTXOs",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
utxos, err := s.vm.state.GetRewardUTXOs(args.TxID)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't get reward UTXOs: %w", err)
|
|
}
|
|
|
|
reply.NumFetched = avajson.Uint64(len(utxos))
|
|
reply.UTXOs = make([]string, len(utxos))
|
|
for i, utxo := range utxos {
|
|
utxoBytes, err := utxo.WireBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode UTXO to bytes: %w", err)
|
|
}
|
|
|
|
utxoStr, err := formatting.Encode(args.Encoding, utxoBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode utxo as %s: %w", args.Encoding, err)
|
|
}
|
|
reply.UTXOs[i] = utxoStr
|
|
}
|
|
reply.Encoding = args.Encoding
|
|
return nil
|
|
}
|
|
|
|
// GetTimestampReply is the response from GetTimestamp
|
|
type GetTimestampReply struct {
|
|
// Current timestamp
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// GetTimestamp returns the current timestamp on chain.
|
|
func (s *Service) GetTimestamp(_ *http.Request, _ *struct{}, reply *GetTimestampReply) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getTimestamp",
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
reply.Timestamp = s.vm.state.GetTimestamp()
|
|
return nil
|
|
}
|
|
|
|
// GetValidatorsAtArgs is the response from GetValidatorsAt
|
|
type GetValidatorsAtArgs struct {
|
|
Height platformapitypes.Height `json:"height"`
|
|
ChainID ids.ID `json:"netID"`
|
|
}
|
|
|
|
type jsonGetValidatorOutput struct {
|
|
PublicKey *string `json:"publicKey"`
|
|
Weight avajson.Uint64 `json:"weight"`
|
|
}
|
|
|
|
func (v *GetValidatorsAtReply) MarshalJSON() ([]byte, error) {
|
|
m := make(map[ids.NodeID]*jsonGetValidatorOutput, len(v.Validators))
|
|
for _, vdr := range v.Validators {
|
|
vdrJSON := &jsonGetValidatorOutput{
|
|
Weight: avajson.Uint64(vdr.Weight),
|
|
}
|
|
|
|
if vdr.PublicKey != nil {
|
|
pk, err := formatting.Encode(formatting.HexNC, vdr.PublicKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vdrJSON.PublicKey = &pk
|
|
}
|
|
|
|
m[vdr.NodeID] = vdrJSON
|
|
}
|
|
return json.Marshal(m)
|
|
}
|
|
|
|
func (v *GetValidatorsAtReply) UnmarshalJSON(b []byte) error {
|
|
var m map[ids.NodeID]*jsonGetValidatorOutput
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
return err
|
|
}
|
|
|
|
if m == nil {
|
|
v.Validators = nil
|
|
return nil
|
|
}
|
|
|
|
v.Validators = make(map[ids.NodeID]*validators.GetValidatorOutput, len(m))
|
|
for nodeID, vdrJSON := range m {
|
|
vdr := &validators.GetValidatorOutput{
|
|
NodeID: nodeID,
|
|
Weight: uint64(vdrJSON.Weight),
|
|
}
|
|
|
|
if vdrJSON.PublicKey != nil {
|
|
pkBytes, err := formatting.Decode(formatting.HexNC, *vdrJSON.PublicKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
vdr.PublicKey = pkBytes
|
|
}
|
|
|
|
v.Validators[nodeID] = vdr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetValidatorsAtReply is the response from GetValidatorsAt
|
|
type GetValidatorsAtReply struct {
|
|
Validators map[ids.NodeID]*validators.GetValidatorOutput
|
|
}
|
|
|
|
// GetValidatorsAt returns the weights of the validator set of a provided net
|
|
// at the specified height.
|
|
func (s *Service) GetValidatorsAt(r *http.Request, args *GetValidatorsAtArgs, reply *GetValidatorsAtReply) error {
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "platform"),
|
|
log.String("method", "getValidatorsAt"),
|
|
log.Uint64("height", uint64(args.Height)),
|
|
log.Bool("isProposed", args.Height.IsProposed()),
|
|
log.Stringer("netID", args.ChainID),
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
ctx := r.Context()
|
|
var err error
|
|
height := uint64(args.Height)
|
|
if args.Height.IsProposed() {
|
|
// Get the proposed height from the last accepted block
|
|
lastAcceptedID := s.vm.state.GetLastAccepted()
|
|
lastAcceptedBlock, err := s.vm.manager.GetStatelessBlock(lastAcceptedID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get last accepted block: %w", err)
|
|
}
|
|
height = lastAcceptedBlock.Height()
|
|
}
|
|
|
|
reply.Validators, err = s.vm.GetValidatorSet(ctx, height, args.ChainID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get validator set: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAllValidatorsAtArgs are the arguments to GetAllValidatorsAt
|
|
type GetAllValidatorsAtArgs struct {
|
|
Height platformapitypes.Height `json:"height"`
|
|
}
|
|
|
|
// GetAllValidatorsAtReply is the response from GetAllValidatorsAt
|
|
type GetAllValidatorsAtReply struct {
|
|
// Map of ChainID -> ValidatorSet
|
|
ValidatorSets map[ids.ID]map[ids.NodeID]*validators.GetValidatorOutput `json:"validatorSets"`
|
|
}
|
|
|
|
// GetAllValidatorsAt returns the validator sets of all nets (including primary network)
|
|
// at the specified height.
|
|
func (s *Service) GetAllValidatorsAt(r *http.Request, args *GetAllValidatorsAtArgs, reply *GetAllValidatorsAtReply) error {
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "platform"),
|
|
log.String("method", "getAllValidatorsAt"),
|
|
log.Uint64("height", uint64(args.Height)),
|
|
log.Bool("isProposed", args.Height.IsProposed()),
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
ctx := r.Context()
|
|
height := uint64(args.Height)
|
|
if args.Height.IsProposed() {
|
|
// Get the proposed height from the last accepted block
|
|
lastAcceptedID := s.vm.state.GetLastAccepted()
|
|
lastAcceptedBlock, err := s.vm.manager.GetStatelessBlock(lastAcceptedID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get last accepted block: %w", err)
|
|
}
|
|
height = lastAcceptedBlock.Height()
|
|
}
|
|
|
|
// Get all net IDs
|
|
netIDs, err := s.vm.state.GetChainIDs()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get net IDs: %w", err)
|
|
}
|
|
|
|
// Initialize the result map
|
|
reply.ValidatorSets = make(map[ids.ID]map[ids.NodeID]*validators.GetValidatorOutput)
|
|
|
|
// Add primary network first
|
|
primaryValidators, err := s.vm.GetValidatorSet(ctx, height, constants.PrimaryNetworkID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get primary network validator set: %w", err)
|
|
}
|
|
reply.ValidatorSets[constants.PrimaryNetworkID] = primaryValidators
|
|
|
|
// Add all nets
|
|
for _, netID := range netIDs {
|
|
netValidators, err := s.vm.GetValidatorSet(ctx, height, netID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get validator set for net %s: %w", netID, err)
|
|
}
|
|
reply.ValidatorSets[netID] = netValidators
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) GetBlock(_ *http.Request, args *apitypes.GetBlockArgs, response *apitypes.GetBlockResponse) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getBlock",
|
|
"blkID", args.BlockID,
|
|
"encoding", args.Encoding,
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
block, err := s.vm.manager.GetStatelessBlock(args.BlockID)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't get block with id %s: %w", args.BlockID, err)
|
|
}
|
|
response.Encoding = args.Encoding
|
|
|
|
var result any
|
|
if args.Encoding == formatting.JSON {
|
|
// block.InitRuntime(s.vm.rt)
|
|
result = block
|
|
} else {
|
|
result, err = formatting.Encode(args.Encoding, block.Bytes())
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode block %s as %s: %w", args.BlockID, args.Encoding, err)
|
|
}
|
|
}
|
|
|
|
response.Block, err = json.Marshal(result)
|
|
return err
|
|
}
|
|
|
|
// GetBlockByHeight returns the block at the given height.
|
|
func (s *Service) GetBlockByHeight(_ *http.Request, args *apitypes.GetBlockByHeightArgs, response *apitypes.GetBlockResponse) error {
|
|
s.vm.log.Debug("API called",
|
|
"service", "platform",
|
|
"method", "getBlockByHeight",
|
|
"height", uint64(args.Height),
|
|
"encoding", args.Encoding,
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
blockID, err := s.vm.state.GetBlockIDAtHeight(uint64(args.Height))
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't get block at height %d: %w", args.Height, err)
|
|
}
|
|
|
|
block, err := s.vm.manager.GetStatelessBlock(blockID)
|
|
if err != nil {
|
|
s.vm.log.Error("couldn't get accepted block",
|
|
"blkID", blockID,
|
|
"error", err,
|
|
)
|
|
return fmt.Errorf("couldn't get block with id %s: %w", blockID, err)
|
|
}
|
|
response.Encoding = args.Encoding
|
|
|
|
var result any
|
|
if args.Encoding == formatting.JSON {
|
|
// block.InitRuntime(s.vm.rt)
|
|
result = block
|
|
} else {
|
|
result, err = formatting.Encode(args.Encoding, block.Bytes())
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode block %s as %s: %w", blockID, args.Encoding, err)
|
|
}
|
|
}
|
|
|
|
response.Block, err = json.Marshal(result)
|
|
return err
|
|
}
|
|
|
|
// GetFeeConfig returns the dynamic fee config of the chain.
|
|
func (s *Service) GetFeeConfig(_ *http.Request, _ *struct{}, reply *gas.Config) error {
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "platform"),
|
|
log.String("method", "getFeeConfig"),
|
|
)
|
|
|
|
*reply = s.vm.DynamicFeeConfig
|
|
return nil
|
|
}
|
|
|
|
type GetFeeStateReply struct {
|
|
gas.State
|
|
Price gas.Price `json:"price"`
|
|
Time time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// GetFeeState returns the current fee state of the chain.
|
|
func (s *Service) GetFeeState(_ *http.Request, _ *struct{}, reply *GetFeeStateReply) error {
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "platform"),
|
|
log.String("method", "getFeeState"),
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
reply.State = s.vm.state.GetFeeState()
|
|
reply.Price = gas.CalculatePrice(
|
|
s.vm.DynamicFeeConfig.MinPrice,
|
|
reply.State.Excess,
|
|
s.vm.DynamicFeeConfig.ExcessConversionConstant,
|
|
)
|
|
reply.Time = s.vm.state.GetTimestamp()
|
|
return nil
|
|
}
|
|
|
|
// GetValidatorFeeConfig returns the validator fee config of the chain.
|
|
func (s *Service) GetValidatorFeeConfig(_ *http.Request, _ *struct{}, reply *fee.Config) error {
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "platform"),
|
|
log.String("method", "getValidatorFeeConfig"),
|
|
)
|
|
|
|
*reply = s.vm.ValidatorFeeConfig
|
|
return nil
|
|
}
|
|
|
|
type GetValidatorFeeStateReply struct {
|
|
Excess gas.Gas `json:"excess"`
|
|
Price gas.Price `json:"price"`
|
|
Time time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// GetValidatorFeeState returns the current validator fee state of the chain.
|
|
func (s *Service) GetValidatorFeeState(_ *http.Request, _ *struct{}, reply *GetValidatorFeeStateReply) error {
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "platform"),
|
|
log.String("method", "getValidatorFeeState"),
|
|
)
|
|
|
|
s.vm.lock.Lock()
|
|
defer s.vm.lock.Unlock()
|
|
|
|
reply.Excess = s.vm.state.GetL1ValidatorExcess()
|
|
reply.Price = gas.CalculatePrice(
|
|
s.vm.ValidatorFeeConfig.MinPrice,
|
|
reply.Excess,
|
|
s.vm.ValidatorFeeConfig.ExcessConversionConstant,
|
|
)
|
|
reply.Time = s.vm.state.GetTimestamp()
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) getAPIOwner(owner *secp256k1fx.OutputOwners) (*platformapitypes.Owner, error) {
|
|
apiOwner := &platformapitypes.Owner{
|
|
Locktime: avajson.Uint64(owner.Locktime),
|
|
Threshold: avajson.Uint32(owner.Threshold),
|
|
Addresses: make([]string, 0, len(owner.Addrs)),
|
|
}
|
|
for _, addr := range owner.Addrs {
|
|
addrStr, err := s.addrManager.FormatLocalAddress(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
apiOwner.Addresses = append(apiOwner.Addresses, addrStr)
|
|
}
|
|
return apiOwner, nil
|
|
}
|
|
|
|
// Takes in a staker and a set of addresses
|
|
// Returns:
|
|
// 1) The total amount staked by addresses in [addrs]
|
|
// 2) The staked outputs
|
|
func getStakeHelper(tx *txs.Tx, addrs set.Set[ids.ShortID], totalAmountStaked map[ids.ID]uint64) []lux.TransferableOutput {
|
|
staker, ok := tx.Unsigned.(txs.PermissionlessStaker)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
stake := staker.Stake()
|
|
stakedOuts := make([]lux.TransferableOutput, 0, len(stake))
|
|
// Go through all of the staked outputs
|
|
for _, output := range stake {
|
|
out := output.Out
|
|
if lockedOut, ok := out.(*stakeable.LockOut); ok {
|
|
// This output can only be used for staking until [stakeOnlyUntil]
|
|
out = lockedOut.TransferableOut
|
|
}
|
|
secpOut, ok := out.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Check whether this output is owned by one of the given addresses
|
|
contains := slices.ContainsFunc(secpOut.Addrs, addrs.Contains)
|
|
if !contains {
|
|
// This output isn't owned by one of the given addresses. Ignore.
|
|
continue
|
|
}
|
|
|
|
assetID := output.AssetID()
|
|
newAmount, err := safemath.Add(totalAmountStaked[assetID], secpOut.Amt)
|
|
if err != nil {
|
|
newAmount = math.MaxUint64
|
|
}
|
|
totalAmountStaked[assetID] = newAmount
|
|
|
|
stakedOuts = append(
|
|
stakedOuts,
|
|
*output,
|
|
)
|
|
}
|
|
return stakedOuts
|
|
}
|
|
|
|
func toPlatformStaker(staker *state.Staker) platformapitypes.Staker {
|
|
return platformapitypes.Staker{
|
|
TxID: staker.TxID,
|
|
StartTime: avajson.Uint64(staker.StartTime.Unix()),
|
|
EndTime: avajson.Uint64(staker.EndTime.Unix()),
|
|
Weight: avajson.Uint64(staker.Weight),
|
|
NodeID: staker.NodeID,
|
|
}
|
|
}
|