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)
2120 lines
56 KiB
Go
2120 lines
56 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package xvm
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
|
|
"github.com/go-json-experiment/json"
|
|
"github.com/luxfi/consensus/core/choices"
|
|
"github.com/luxfi/crypto/secp256k1"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/formatting"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/math/set"
|
|
apitypes "github.com/luxfi/api/types"
|
|
lux "github.com/luxfi/utxo"
|
|
"github.com/luxfi/node/vms/components/verify"
|
|
"github.com/luxfi/node/vms/xvm/txs"
|
|
"github.com/luxfi/utils"
|
|
"github.com/luxfi/utxo/nftfx"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
|
|
safemath "github.com/luxfi/math"
|
|
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 items allowed in a page
|
|
maxPageSize uint64 = 1024
|
|
)
|
|
|
|
// JSONSpendHeader includes common fields for spend transactions
|
|
type JSONSpendHeader struct {
|
|
apitypes.UserPass
|
|
JSONFromAddrs
|
|
JSONChangeAddr
|
|
}
|
|
|
|
// JSONFromAddrs contains the addresses to spend from
|
|
type JSONFromAddrs struct {
|
|
From []string `json:"from"`
|
|
}
|
|
|
|
// JSONChangeAddr contains the change address
|
|
type JSONChangeAddr struct {
|
|
ChangeAddr string `json:"changeAddr"`
|
|
}
|
|
|
|
// JSONTxIDChangeAddr contains a transaction ID and a change address
|
|
type JSONTxIDChangeAddr struct {
|
|
apitypes.JSONTxID
|
|
JSONChangeAddr
|
|
}
|
|
|
|
// publicKeyToAddress converts a secp256k1 public key to an ids.ShortID address
|
|
func publicKeyToAddress(pk *secp256k1.PublicKey) (ids.ShortID, error) {
|
|
pkBytes := pk.Bytes()
|
|
addressBytes := secp256k1.PubkeyBytesToAddress(pkBytes)
|
|
return ids.ToShortID(addressBytes)
|
|
}
|
|
|
|
var (
|
|
errServiceNotReady = errors.New("xvm service not ready: VM not initialized")
|
|
errTxNotCreateAsset = errors.New("transaction doesn't create an asset")
|
|
errNoMinters = errors.New("no minters provided")
|
|
errNoHoldersOrMinters = errors.New("no minters or initialHolders provided")
|
|
errZeroAmount = errors.New("amount must be positive")
|
|
errNoOutputs = errors.New("no outputs to send")
|
|
errInvalidMintAmount = errors.New("amount minted must be positive")
|
|
errNilTxID = errors.New("nil transaction ID")
|
|
errNoAddresses = errors.New("no addresses provided")
|
|
errNoKeys = errors.New("from addresses have no keys or funds")
|
|
errMissingPrivateKey = errors.New("argument 'privateKey' not given")
|
|
errNotLinearized = errors.New("chain is not linearized")
|
|
)
|
|
|
|
// FormattedAssetID defines a JSON formatted struct containing an assetID as a string
|
|
type FormattedAssetID struct {
|
|
AssetID ids.ID `json:"assetID"`
|
|
}
|
|
|
|
// Service defines the base service for the asset vm
|
|
type Service struct{ vm *VM }
|
|
|
|
// ready returns an error if the VM is not fully initialized.
|
|
// Protects against nil pointer dereference panics when HTTP handlers
|
|
// are invoked before the VM is ready (e.g. during bootstrap).
|
|
func (s *Service) ready() error {
|
|
if s == nil || s.vm == nil {
|
|
return errServiceNotReady
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetBlock returns the requested block.
|
|
func (s *Service) GetBlock(_ *http.Request, args *apitypes.GetBlockArgs, reply *apitypes.GetBlockResponse) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getBlock"),
|
|
log.Stringer("blkID", args.BlockID),
|
|
log.Stringer("encoding", args.Encoding),
|
|
)
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
if s.vm.chainManager == nil {
|
|
return errNotLinearized
|
|
}
|
|
block, err := s.vm.chainManager.GetStatelessBlock(args.BlockID)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't get block with id %s: %w", args.BlockID, err)
|
|
}
|
|
reply.Encoding = args.Encoding
|
|
|
|
var result any
|
|
if args.Encoding == formatting.JSON {
|
|
// InitRuntime is no longer needed with new consensus
|
|
for _, tx := range block.Txs() {
|
|
err := tx.Unsigned.Visit(&txInit{
|
|
tx: tx,
|
|
typeToFxIndex: s.vm.typeToFxIndex,
|
|
fxs: s.vm.fxs,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
result = block
|
|
} else {
|
|
result, err = formatting.Encode(args.Encoding, block.Bytes())
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode block %s as string: %w", args.BlockID, err)
|
|
}
|
|
}
|
|
|
|
reply.Block, err = json.Marshal(result)
|
|
return err
|
|
}
|
|
|
|
// GetBlockByHeight returns the block at the given height.
|
|
func (s *Service) GetBlockByHeight(_ *http.Request, args *apitypes.GetBlockByHeightArgs, reply *apitypes.GetBlockResponse) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getBlockByHeight"),
|
|
log.Uint64("height", uint64(args.Height)),
|
|
)
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
if s.vm.chainManager == nil {
|
|
return errNotLinearized
|
|
}
|
|
reply.Encoding = args.Encoding
|
|
|
|
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.chainManager.GetStatelessBlock(blockID)
|
|
if err != nil {
|
|
s.vm.log.Error("couldn't get accepted block",
|
|
log.Stringer("blkID", blockID),
|
|
log.String("error", err.Error()),
|
|
)
|
|
return fmt.Errorf("couldn't get block with id %s: %w", blockID, err)
|
|
}
|
|
|
|
var result any
|
|
if args.Encoding == formatting.JSON {
|
|
// InitRuntime is no longer needed with new consensus
|
|
for _, tx := range block.Txs() {
|
|
err := tx.Unsigned.Visit(&txInit{
|
|
tx: tx,
|
|
typeToFxIndex: s.vm.typeToFxIndex,
|
|
fxs: s.vm.fxs,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
result = block
|
|
} else {
|
|
result, err = formatting.Encode(args.Encoding, block.Bytes())
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode block %s as string: %w", blockID, err)
|
|
}
|
|
}
|
|
|
|
reply.Block, err = json.Marshal(result)
|
|
return err
|
|
}
|
|
|
|
// GetHeight returns the height of the last accepted block.
|
|
func (s *Service) GetHeight(_ *http.Request, _ *struct{}, reply *apitypes.GetHeightResponse) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getHeight"),
|
|
)
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
if s.vm.chainManager == nil {
|
|
return errNotLinearized
|
|
}
|
|
|
|
blockID := s.vm.state.GetLastAccepted()
|
|
block, err := s.vm.chainManager.GetStatelessBlock(blockID)
|
|
if err != nil {
|
|
s.vm.log.Error("couldn't get last accepted block",
|
|
log.Stringer("blkID", blockID),
|
|
log.String("error", err.Error()),
|
|
)
|
|
return fmt.Errorf("couldn't get block with id %s: %w", blockID, err)
|
|
}
|
|
|
|
reply.Height = apitypes.Uint64(block.Height())
|
|
return nil
|
|
}
|
|
|
|
// IssueTx attempts to issue a transaction into consensus
|
|
func (s *Service) IssueTx(_ *http.Request, args *apitypes.FormattedTx, reply *apitypes.JSONTxID) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "issueTx"),
|
|
log.String("tx", args.Tx),
|
|
)
|
|
|
|
txBytes, err := formatting.Decode(args.Encoding, args.Tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem decoding transaction: %w", err)
|
|
}
|
|
|
|
tx, err := s.vm.parser.ParseTx(txBytes)
|
|
if err != nil {
|
|
s.vm.log.Debug("failed to parse tx",
|
|
log.String("error", err.Error()),
|
|
)
|
|
return err
|
|
}
|
|
|
|
reply.TxID, err = s.vm.issueTxFromRPC(tx)
|
|
return err
|
|
}
|
|
|
|
// GetTxStatusReply defines the GetTxStatus replies returned from the API
|
|
type GetTxStatusReply struct {
|
|
Status choices.Status `json:"status"`
|
|
}
|
|
|
|
type GetAddressTxsArgs struct {
|
|
apitypes.JSONAddress
|
|
// Cursor used as a page index / offset
|
|
Cursor avajson.Uint64 `json:"cursor"`
|
|
// PageSize num of items per page
|
|
PageSize avajson.Uint64 `json:"pageSize"`
|
|
// AssetID defaulted to LUX if omitted or left blank
|
|
AssetID string `json:"assetID"`
|
|
}
|
|
|
|
type GetAddressTxsReply struct {
|
|
TxIDs []ids.ID `json:"txIDs"`
|
|
// Cursor used as a page index / offset
|
|
Cursor avajson.Uint64 `json:"cursor"`
|
|
}
|
|
|
|
// GetAddressTxs returns list of transactions for a given address
|
|
func (s *Service) GetAddressTxs(_ *http.Request, args *GetAddressTxsArgs, reply *GetAddressTxsReply) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
cursor := uint64(args.Cursor)
|
|
pageSize := uint64(args.PageSize)
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getAddressTxs"),
|
|
log.String("address", args.Address),
|
|
log.String("assetID", args.AssetID),
|
|
log.Uint64("cursor", cursor),
|
|
log.Uint64("pageSize", pageSize),
|
|
)
|
|
if pageSize > maxPageSize {
|
|
return fmt.Errorf("pageSize > maximum allowed (%d)", maxPageSize)
|
|
} else if pageSize == 0 {
|
|
pageSize = maxPageSize
|
|
}
|
|
|
|
// Parse to address
|
|
address, err := lux.ParseServiceAddress(s.vm, args.Address)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse argument 'address' to address: %w", err)
|
|
}
|
|
|
|
// Lookup assetID
|
|
assetID, err := s.vm.lookupAssetID(args.AssetID)
|
|
if err != nil {
|
|
return fmt.Errorf("specified `assetID` is invalid: %w", err)
|
|
}
|
|
|
|
s.vm.log.Debug("fetching transactions",
|
|
log.String("address", args.Address),
|
|
log.String("assetID", args.AssetID),
|
|
log.Uint64("cursor", cursor),
|
|
log.Uint64("pageSize", pageSize),
|
|
)
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
// Read transactions from the indexer
|
|
reply.TxIDs, err = s.vm.addressTxsIndexer.Read(address[:], assetID, cursor, pageSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("fetched transactions",
|
|
log.String("address", args.Address),
|
|
log.String("assetID", args.AssetID),
|
|
log.Int("numTxs", len(reply.TxIDs)),
|
|
)
|
|
|
|
// To get the next set of tx IDs, the user should provide this cursor.
|
|
// e.g. if they provided cursor 5, and read 6 tx IDs, they should start
|
|
// next time from index (cursor) 11.
|
|
reply.Cursor = avajson.Uint64(cursor + uint64(len(reply.TxIDs)))
|
|
return nil
|
|
}
|
|
|
|
// GetTxStatus returns the status of the specified transaction
|
|
//
|
|
// Deprecated: GetTxStatus only returns Accepted or Unknown, GetTx should be
|
|
// used instead to determine if the tx was accepted.
|
|
func (s *Service) GetTxStatus(_ *http.Request, args *apitypes.JSONTxID, reply *GetTxStatusReply) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getTxStatus"),
|
|
log.Stringer("txID", args.TxID),
|
|
)
|
|
|
|
if args.TxID == ids.Empty {
|
|
return errNilTxID
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
_, err := s.vm.state.GetTx(args.TxID)
|
|
switch err {
|
|
case nil:
|
|
reply.Status = choices.Accepted
|
|
case database.ErrNotFound:
|
|
reply.Status = choices.Unknown
|
|
default:
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetTx returns the specified transaction
|
|
func (s *Service) GetTx(_ *http.Request, args *apitypes.GetTxArgs, reply *apitypes.GetTxReply) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getTx"),
|
|
log.Stringer("txID", args.TxID),
|
|
)
|
|
|
|
if args.TxID == ids.Empty {
|
|
return errNilTxID
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
tx, err := s.vm.state.GetTx(args.TxID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Encoding = args.Encoding
|
|
|
|
var result any
|
|
if args.Encoding == formatting.JSON {
|
|
err = tx.Unsigned.Visit(&txInit{
|
|
tx: tx,
|
|
typeToFxIndex: s.vm.typeToFxIndex,
|
|
fxs: s.vm.fxs,
|
|
})
|
|
result = tx
|
|
} else {
|
|
result, err = formatting.Encode(args.Encoding, tx.Bytes())
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reply.Tx, err = json.Marshal(result)
|
|
return err
|
|
}
|
|
|
|
// GetUTXOs gets all utxos for passed in addresses
|
|
func (s *Service) GetUTXOs(_ *http.Request, args *apitypes.GetUTXOsArgs, reply *apitypes.GetUTXOsReply) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getUTXOs"),
|
|
log.Strings("addresses", args.Addresses),
|
|
)
|
|
|
|
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.consensusRuntime.ChainID
|
|
} else {
|
|
chainID, err := s.vm.bcLookup.Lookup(args.SourceChain)
|
|
if err != nil {
|
|
return fmt.Errorf("problem parsing source chainID %q: %w", args.SourceChain, err)
|
|
}
|
|
sourceChain = chainID
|
|
}
|
|
|
|
addrSet, err := lux.ParseServiceAddresses(s.vm, 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.vm, 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 || int(maxPageSize) < limit {
|
|
limit = int(maxPageSize)
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
if sourceChain == s.vm.consensusRuntime.ChainID {
|
|
utxos, endAddr, endUTXOID, err = lux.GetPaginatedUTXOs(
|
|
s.vm.state,
|
|
addrSet,
|
|
startAddr,
|
|
startUTXO,
|
|
limit,
|
|
)
|
|
} else {
|
|
// Create a wrapper to convert interface type
|
|
// This is a workaround for the type mismatch between interfaces.SharedMemory and atomic.SharedMemory
|
|
utxos, endAddr, endUTXOID, err = lux.GetAtomicUTXOs(
|
|
nil, // Temporarily pass nil - will need proper fix
|
|
sourceChain,
|
|
addrSet,
|
|
startAddr,
|
|
startUTXO,
|
|
limit,
|
|
)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("problem retrieving UTXOs: %w", err)
|
|
}
|
|
|
|
reply.UTXOs = make([]string, len(utxos))
|
|
for i, utxo := range utxos {
|
|
b, err := utxo.WireBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("problem marshalling UTXO: %w", err)
|
|
}
|
|
reply.UTXOs[i], err = formatting.Encode(args.Encoding, b)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't encode UTXO %s as string: %w", utxo.InputID(), err)
|
|
}
|
|
}
|
|
|
|
endAddress, err := s.vm.FormatLocalAddress(endAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("problem formatting address: %w", err)
|
|
}
|
|
|
|
reply.EndIndex.Address = endAddress
|
|
reply.EndIndex.UTXO = endUTXOID.String()
|
|
reply.NumFetched = apitypes.Uint64(len(utxos))
|
|
reply.Encoding = args.Encoding
|
|
return nil
|
|
}
|
|
|
|
// GetAssetDescriptionArgs are arguments for passing into GetAssetDescription requests
|
|
type GetAssetDescriptionArgs struct {
|
|
AssetID string `json:"assetID"`
|
|
}
|
|
|
|
// GetAssetDescriptionReply defines the GetAssetDescription replies returned from the API
|
|
type GetAssetDescriptionReply struct {
|
|
FormattedAssetID
|
|
Name string `json:"name"`
|
|
Symbol string `json:"symbol"`
|
|
Denomination avajson.Uint8 `json:"denomination"`
|
|
}
|
|
|
|
// GetAssetDescription creates an empty account with the name passed in
|
|
func (s *Service) GetAssetDescription(_ *http.Request, args *GetAssetDescriptionArgs, reply *GetAssetDescriptionReply) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getAssetDescription"),
|
|
log.String("assetID", args.AssetID),
|
|
)
|
|
|
|
assetID, err := s.vm.lookupAssetID(args.AssetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
tx, err := s.vm.state.GetTx(assetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
createAssetTx, ok := tx.Unsigned.(*txs.CreateAssetTx)
|
|
if !ok {
|
|
return errTxNotCreateAsset
|
|
}
|
|
|
|
reply.FormattedAssetID.AssetID = assetID
|
|
reply.Name = createAssetTx.Name
|
|
reply.Symbol = createAssetTx.Symbol
|
|
reply.Denomination = avajson.Uint8(createAssetTx.Denomination)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBalanceArgs are arguments for passing into GetBalance requests
|
|
type GetBalanceArgs struct {
|
|
Address string `json:"address"`
|
|
AssetID string `json:"assetID"`
|
|
IncludePartial bool `json:"includePartial"`
|
|
}
|
|
|
|
// GetBalanceReply defines the GetBalance replies returned from the API
|
|
type GetBalanceReply struct {
|
|
Balance avajson.Uint64 `json:"balance"`
|
|
UTXOIDs []lux.UTXOID `json:"utxoIDs"`
|
|
}
|
|
|
|
// GetBalance returns the balance of an asset held by an address.
|
|
// If ![args.IncludePartial], returns only the balance held solely
|
|
// (1 out of 1 multisig) by the address and with a locktime in the past.
|
|
// Otherwise, returned balance includes assets held only partially by the
|
|
// address, and includes balances with locktime in the future.
|
|
func (s *Service) GetBalance(_ *http.Request, args *GetBalanceArgs, reply *GetBalanceReply) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getBalance"),
|
|
log.String("address", args.Address),
|
|
log.String("assetID", args.AssetID),
|
|
)
|
|
|
|
addr, err := lux.ParseServiceAddress(s.vm, args.Address)
|
|
if err != nil {
|
|
return fmt.Errorf("problem parsing address '%s': %w", args.Address, err)
|
|
}
|
|
|
|
assetID, err := s.vm.lookupAssetID(args.AssetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addrSet := set.Of(addr)
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
utxos, err := lux.GetAllUTXOs(s.vm.state, addrSet)
|
|
if err != nil {
|
|
return fmt.Errorf("problem retrieving UTXOs: %w", err)
|
|
}
|
|
|
|
now := s.vm.clock.Unix()
|
|
reply.UTXOIDs = make([]lux.UTXOID, 0, len(utxos))
|
|
for _, utxo := range utxos {
|
|
if utxo.AssetID() != assetID {
|
|
continue
|
|
}
|
|
// Only secp256k1fx.TransferOutput is supported; other output types are skipped.
|
|
transferable, ok := utxo.Out.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
continue
|
|
}
|
|
owners := transferable.OutputOwners
|
|
if !args.IncludePartial && (len(owners.Addrs) != 1 || owners.Locktime > now) {
|
|
continue
|
|
}
|
|
amt, err := safemath.Add64(transferable.Amount(), uint64(reply.Balance))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Balance = avajson.Uint64(amt)
|
|
reply.UTXOIDs = append(reply.UTXOIDs, utxo.UTXOID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Balance struct {
|
|
AssetID string `json:"asset"`
|
|
Balance avajson.Uint64 `json:"balance"`
|
|
}
|
|
|
|
type GetAllBalancesArgs struct {
|
|
apitypes.JSONAddress
|
|
IncludePartial bool `json:"includePartial"`
|
|
}
|
|
|
|
// GetAllBalancesReply is the response from a call to GetAllBalances
|
|
type GetAllBalancesReply struct {
|
|
Balances []Balance `json:"balances"`
|
|
}
|
|
|
|
// GetAllBalances returns a map where:
|
|
//
|
|
// Key: ID of an asset such that [args.Address] has a non-zero balance of the asset
|
|
// Value: The balance of the asset held by the address
|
|
//
|
|
// If ![args.IncludePartial], returns only unlocked balance/UTXOs with a 1-out-of-1 multisig.
|
|
// Otherwise, returned balance/UTXOs includes assets held only partially by the
|
|
// address, and includes balances with locktime in the future.
|
|
func (s *Service) GetAllBalances(_ *http.Request, args *GetAllBalancesArgs, reply *GetAllBalancesReply) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Debug("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "getAllBalances"),
|
|
log.String("address", args.Address),
|
|
)
|
|
|
|
address, err := lux.ParseServiceAddress(s.vm, args.Address)
|
|
if err != nil {
|
|
return fmt.Errorf("problem parsing address '%s': %w", args.Address, err)
|
|
}
|
|
addrSet := set.Of(address)
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
utxos, err := lux.GetAllUTXOs(s.vm.state, addrSet)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't get address's UTXOs: %w", err)
|
|
}
|
|
|
|
now := s.vm.clock.Unix()
|
|
assetIDs := make(set.Set[ids.ID]) // IDs of assets the address has a non-zero balance of
|
|
balances := make(map[ids.ID]uint64) // key: ID (as bytes). value: balance of that asset
|
|
for _, utxo := range utxos {
|
|
// Only secp256k1fx.TransferOutput is supported; other output types are skipped.
|
|
transferable, ok := utxo.Out.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
continue
|
|
}
|
|
owners := transferable.OutputOwners
|
|
if !args.IncludePartial && (len(owners.Addrs) != 1 || owners.Locktime > now) {
|
|
continue
|
|
}
|
|
assetID := utxo.AssetID()
|
|
assetIDs.Add(assetID)
|
|
balance := balances[assetID] // 0 if key doesn't exist
|
|
balance, err := safemath.Add64(transferable.Amount(), balance)
|
|
if err != nil {
|
|
balances[assetID] = math.MaxUint64
|
|
} else {
|
|
balances[assetID] = balance
|
|
}
|
|
}
|
|
|
|
reply.Balances = make([]Balance, assetIDs.Len())
|
|
i := 0
|
|
for assetID := range assetIDs {
|
|
alias := s.vm.PrimaryAliasOrDefault(assetID)
|
|
reply.Balances[i] = Balance{
|
|
AssetID: alias,
|
|
Balance: avajson.Uint64(balances[assetID]),
|
|
}
|
|
i++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Holder describes how much an address owns of an asset
|
|
type Holder struct {
|
|
Amount avajson.Uint64 `json:"amount"`
|
|
Address string `json:"address"`
|
|
}
|
|
|
|
// Owners describes who can perform an action
|
|
type Owners struct {
|
|
Threshold avajson.Uint32 `json:"threshold"`
|
|
Minters []string `json:"minters"`
|
|
}
|
|
|
|
// CreateAssetArgs are arguments for passing into CreateAsset
|
|
type CreateAssetArgs struct {
|
|
JSONSpendHeader // User, password, from addrs, change addr
|
|
Name string `json:"name"`
|
|
Symbol string `json:"symbol"`
|
|
Denomination byte `json:"denomination"`
|
|
InitialHolders []*Holder `json:"initialHolders"`
|
|
MinterSets []Owners `json:"minterSets"`
|
|
}
|
|
|
|
// AssetIDChangeAddr is an asset ID and a change address
|
|
type AssetIDChangeAddr struct {
|
|
FormattedAssetID
|
|
JSONChangeAddr
|
|
}
|
|
|
|
// CreateAsset returns ID of the newly created asset
|
|
func (s *Service) CreateAsset(_ *http.Request, args *CreateAssetArgs, reply *AssetIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "createAsset"),
|
|
log.String("name", args.Name),
|
|
log.String("symbol", args.Symbol),
|
|
log.Int("numInitialHolders", len(args.InitialHolders)),
|
|
log.Int("numMinters", len(args.MinterSets)),
|
|
)
|
|
|
|
tx, changeAddr, err := s.buildCreateAssetTx(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
assetID, err := s.vm.issueTxFromRPC(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem issuing transaction: %w", err)
|
|
}
|
|
|
|
reply.AssetID = assetID
|
|
reply.ChangeAddr, err = s.vm.FormatLocalAddress(changeAddr)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) buildCreateAssetTx(args *CreateAssetArgs) (*txs.Tx, ids.ShortID, error) {
|
|
if err := s.ready(); err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
if len(args.InitialHolders) == 0 && len(args.MinterSets) == 0 {
|
|
return nil, ids.ShortEmpty, errNoHoldersOrMinters
|
|
}
|
|
|
|
// Parse the from addresses
|
|
fromAddrs, err := lux.ParseServiceAddresses(s.vm, args.From)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
// Get the UTXOs/keys for the from addresses
|
|
utxos, kc, err := s.vm.LoadUser(args.Username, args.Password, fromAddrs)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Parse the change address.
|
|
if len(kc.Keys) == 0 {
|
|
return nil, ids.ShortEmpty, errNoKeys
|
|
}
|
|
defaultAddr, err := publicKeyToAddress(kc.Keys[0].PublicKey())
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
changeAddr, err := s.vm.selectChangeAddr(defaultAddr, args.ChangeAddr)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
amountsSpent, ins, keys, err := s.vm.Spend(
|
|
utxos,
|
|
kc,
|
|
map[ids.ID]uint64{
|
|
s.vm.feeAssetID: s.vm.CreateAssetTxFee,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
outs := []*lux.TransferableOutput{}
|
|
if amountSpent := amountsSpent[s.vm.feeAssetID]; amountSpent > s.vm.CreateAssetTxFee {
|
|
outs = append(outs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: s.vm.feeAssetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amountSpent - s.vm.CreateAssetTxFee,
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{changeAddr},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
initialState := &txs.InitialState{
|
|
FxIndex: 0, // Implementation note
|
|
Outs: make([]verify.State, 0, len(args.InitialHolders)+len(args.MinterSets)),
|
|
}
|
|
for _, holder := range args.InitialHolders {
|
|
addr, err := lux.ParseServiceAddress(s.vm, holder.Address)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
initialState.Outs = append(initialState.Outs, &secp256k1fx.TransferOutput{
|
|
Amt: uint64(holder.Amount),
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{addr},
|
|
},
|
|
})
|
|
}
|
|
for _, owner := range args.MinterSets {
|
|
minter := &secp256k1fx.MintOutput{
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Threshold: uint32(owner.Threshold),
|
|
Addrs: make([]ids.ShortID, 0, len(owner.Minters)),
|
|
},
|
|
}
|
|
minterAddrsSet, err := lux.ParseServiceAddresses(s.vm, owner.Minters)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
minter.Addrs = minterAddrsSet.List()
|
|
utils.Sort(minter.Addrs)
|
|
initialState.Outs = append(initialState.Outs, minter)
|
|
}
|
|
|
|
codec := s.vm.parser.Codec()
|
|
initialState.Sort(codec)
|
|
|
|
tx := &txs.Tx{Unsigned: &txs.CreateAssetTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: s.vm.consensusRuntime.NetworkID,
|
|
BlockchainID: s.vm.consensusRuntime.ChainID,
|
|
Outs: outs,
|
|
Ins: ins,
|
|
}},
|
|
Name: args.Name,
|
|
Symbol: args.Symbol,
|
|
Denomination: args.Denomination,
|
|
States: []*txs.InitialState{initialState},
|
|
}}
|
|
return tx, changeAddr, tx.SignSECP256K1Fx(codec, keys)
|
|
}
|
|
|
|
// CreateFixedCapAsset returns ID of the newly created asset
|
|
func (s *Service) CreateFixedCapAsset(r *http.Request, args *CreateAssetArgs, reply *AssetIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "createFixedCapAsset"),
|
|
log.String("name", args.Name),
|
|
log.String("symbol", args.Symbol),
|
|
log.Int("numInitialHolders", len(args.InitialHolders)),
|
|
)
|
|
|
|
return s.CreateAsset(r, args, reply)
|
|
}
|
|
|
|
// CreateVariableCapAsset returns ID of the newly created asset
|
|
func (s *Service) CreateVariableCapAsset(r *http.Request, args *CreateAssetArgs, reply *AssetIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "createVariableCapAsset"),
|
|
log.String("name", args.Name),
|
|
log.String("symbol", args.Symbol),
|
|
log.Int("numMinters", len(args.MinterSets)),
|
|
)
|
|
|
|
return s.CreateAsset(r, args, reply)
|
|
}
|
|
|
|
// CreateNFTAssetArgs are arguments for passing into CreateNFTAsset requests
|
|
type CreateNFTAssetArgs struct {
|
|
JSONSpendHeader // User, password, from addrs, change addr
|
|
Name string `json:"name"`
|
|
Symbol string `json:"symbol"`
|
|
MinterSets []Owners `json:"minterSets"`
|
|
}
|
|
|
|
// CreateNFTAsset returns ID of the newly created asset
|
|
func (s *Service) CreateNFTAsset(_ *http.Request, args *CreateNFTAssetArgs, reply *AssetIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "createNFTAsset"),
|
|
log.String("name", args.Name),
|
|
log.String("symbol", args.Symbol),
|
|
log.Int("numMinters", len(args.MinterSets)),
|
|
)
|
|
|
|
tx, changeAddr, err := s.buildCreateNFTAsset(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
assetID, err := s.vm.issueTxFromRPC(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem issuing transaction: %w", err)
|
|
}
|
|
|
|
reply.AssetID = assetID
|
|
reply.ChangeAddr, err = s.vm.FormatLocalAddress(changeAddr)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) buildCreateNFTAsset(args *CreateNFTAssetArgs) (*txs.Tx, ids.ShortID, error) {
|
|
if err := s.ready(); err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
if len(args.MinterSets) == 0 {
|
|
return nil, ids.ShortEmpty, errNoMinters
|
|
}
|
|
|
|
// Parse the from addresses
|
|
fromAddrs, err := lux.ParseServiceAddresses(s.vm, args.From)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
// Get the UTXOs/keys for the from addresses
|
|
utxos, kc, err := s.vm.LoadUser(args.Username, args.Password, fromAddrs)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Parse the change address.
|
|
if len(kc.Keys) == 0 {
|
|
return nil, ids.ShortEmpty, errNoKeys
|
|
}
|
|
defaultAddr, err := publicKeyToAddress(kc.Keys[0].PublicKey())
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
changeAddr, err := s.vm.selectChangeAddr(defaultAddr, args.ChangeAddr)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
amountsSpent, ins, keys, err := s.vm.Spend(
|
|
utxos,
|
|
kc,
|
|
map[ids.ID]uint64{
|
|
s.vm.feeAssetID: s.vm.CreateAssetTxFee,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
outs := []*lux.TransferableOutput{}
|
|
if amountSpent := amountsSpent[s.vm.feeAssetID]; amountSpent > s.vm.CreateAssetTxFee {
|
|
outs = append(outs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: s.vm.feeAssetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amountSpent - s.vm.CreateAssetTxFee,
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{changeAddr},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
initialState := &txs.InitialState{
|
|
FxIndex: 1, // Implementation note
|
|
Outs: make([]verify.State, 0, len(args.MinterSets)),
|
|
}
|
|
for i, owner := range args.MinterSets {
|
|
minter := &nftfx.MintOutput{
|
|
GroupID: uint32(i),
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Threshold: uint32(owner.Threshold),
|
|
},
|
|
}
|
|
minterAddrsSet, err := lux.ParseServiceAddresses(s.vm, owner.Minters)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
minter.Addrs = minterAddrsSet.List()
|
|
utils.Sort(minter.Addrs)
|
|
initialState.Outs = append(initialState.Outs, minter)
|
|
}
|
|
|
|
codec := s.vm.parser.Codec()
|
|
initialState.Sort(codec)
|
|
|
|
tx := &txs.Tx{Unsigned: &txs.CreateAssetTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: s.vm.consensusRuntime.NetworkID,
|
|
BlockchainID: s.vm.consensusRuntime.ChainID,
|
|
Outs: outs,
|
|
Ins: ins,
|
|
}},
|
|
Name: args.Name,
|
|
Symbol: args.Symbol,
|
|
Denomination: 0, // NFTs are non-fungible
|
|
States: []*txs.InitialState{initialState},
|
|
}}
|
|
return tx, changeAddr, tx.SignSECP256K1Fx(codec, keys)
|
|
}
|
|
|
|
// CreateAddress creates an address for the user [args.Username]
|
|
func (s *Service) CreateAddress(_ *http.Request, args *apitypes.UserPass, reply *apitypes.JSONAddress) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "createAddress"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
sk, err := secp256k1.NewPrivateKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addr, err := publicKeyToAddress(sk.PublicKey())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Address, err = s.vm.FormatLocalAddress(addr)
|
|
return err
|
|
}
|
|
|
|
// ListAddresses returns all of the addresses controlled by user [args.Username]
|
|
func (s *Service) ListAddresses(_ *http.Request, args *apitypes.UserPass, response *apitypes.JSONAddresses) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "listAddresses"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
response.Addresses = []string{}
|
|
return nil
|
|
}
|
|
|
|
// ExportKeyArgs are arguments for ExportKey
|
|
type ExportKeyArgs struct {
|
|
apitypes.UserPass
|
|
Address string `json:"address"`
|
|
}
|
|
|
|
// ExportKeyReply is the response for ExportKey
|
|
type ExportKeyReply struct {
|
|
// The decrypted PrivateKey for the Address provided in the arguments
|
|
PrivateKey *secp256k1.PrivateKey `json:"privateKey"`
|
|
}
|
|
|
|
// ExportKey returns a private key from the provided user
|
|
func (s *Service) ExportKey(_ *http.Request, args *ExportKeyArgs, reply *ExportKeyReply) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "exportKey"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
_, err := lux.ParseServiceAddress(s.vm, args.Address)
|
|
if err != nil {
|
|
return fmt.Errorf("problem parsing address %q: %w", args.Address, err)
|
|
}
|
|
|
|
return fmt.Errorf("key export not available without keystore")
|
|
}
|
|
|
|
// ImportKeyArgs are arguments for ImportKey
|
|
type ImportKeyArgs struct {
|
|
apitypes.UserPass
|
|
PrivateKey *secp256k1.PrivateKey `json:"privateKey"`
|
|
}
|
|
|
|
// ImportKeyReply is the response for ImportKey
|
|
type ImportKeyReply struct {
|
|
// The address controlled by the PrivateKey provided in the arguments
|
|
Address string `json:"address"`
|
|
}
|
|
|
|
// ImportKey adds a private key to the provided user
|
|
func (s *Service) ImportKey(_ *http.Request, args *ImportKeyArgs, reply *apitypes.JSONAddress) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "importKey"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
if args.PrivateKey == nil {
|
|
return errMissingPrivateKey
|
|
}
|
|
|
|
addr, err := publicKeyToAddress(args.PrivateKey.PublicKey())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Address, err = s.vm.FormatLocalAddress(addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SendOutput specifies that [Amount] of asset [AssetID] be sent to [To]
|
|
type SendOutput struct {
|
|
// The amount of funds to send
|
|
Amount avajson.Uint64 `json:"amount"`
|
|
|
|
// ID of the asset being sent
|
|
AssetID string `json:"assetID"`
|
|
|
|
// Address of the recipient
|
|
To string `json:"to"`
|
|
}
|
|
|
|
// SendArgs are arguments for passing into Send requests
|
|
type SendArgs struct {
|
|
// User, password, from addrs, change addr
|
|
JSONSpendHeader
|
|
|
|
// The amount, assetID, and destination to send funds to
|
|
SendOutput
|
|
|
|
// Memo field
|
|
Memo string `json:"memo"`
|
|
}
|
|
|
|
// SendMultipleArgs are arguments for passing into SendMultiple requests
|
|
type SendMultipleArgs struct {
|
|
// User, password, from addrs, change addr
|
|
JSONSpendHeader
|
|
|
|
// The outputs of the transaction
|
|
Outputs []SendOutput `json:"outputs"`
|
|
|
|
// Memo field
|
|
Memo string `json:"memo"`
|
|
}
|
|
|
|
// Send returns the ID of the newly created transaction
|
|
func (s *Service) Send(r *http.Request, args *SendArgs, reply *JSONTxIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
return s.SendMultiple(r, &SendMultipleArgs{
|
|
JSONSpendHeader: args.JSONSpendHeader,
|
|
Outputs: []SendOutput{args.SendOutput},
|
|
Memo: args.Memo,
|
|
}, reply)
|
|
}
|
|
|
|
// SendMultiple sends a transaction with multiple outputs.
|
|
func (s *Service) SendMultiple(_ *http.Request, args *SendMultipleArgs, reply *JSONTxIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "sendMultiple"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
tx, changeAddr, err := s.buildSendMultiple(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txID, err := s.vm.issueTxFromRPC(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem issuing transaction: %w", err)
|
|
}
|
|
|
|
reply.TxID = txID
|
|
reply.ChangeAddr, err = s.vm.FormatLocalAddress(changeAddr)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) buildSendMultiple(args *SendMultipleArgs) (*txs.Tx, ids.ShortID, error) {
|
|
if err := s.ready(); err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
// Validate the memo field
|
|
memoBytes := []byte(args.Memo)
|
|
if l := len(memoBytes); l > lux.MaxMemoSize {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("max memo length is %d but provided memo field is length %d", lux.MaxMemoSize, l)
|
|
} else if len(args.Outputs) == 0 {
|
|
return nil, ids.ShortEmpty, errNoOutputs
|
|
}
|
|
|
|
// Parse the from addresses
|
|
fromAddrs, err := lux.ParseServiceAddresses(s.vm, args.From)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
// Load user's UTXOs/keys
|
|
utxos, kc, err := s.vm.LoadUser(args.Username, args.Password, fromAddrs)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Parse the change address.
|
|
if len(kc.Keys) == 0 {
|
|
return nil, ids.ShortEmpty, errNoKeys
|
|
}
|
|
defaultAddr, err := publicKeyToAddress(kc.Keys[0].PublicKey())
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
changeAddr, err := s.vm.selectChangeAddr(defaultAddr, args.ChangeAddr)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Calculate required input amounts and create the desired outputs
|
|
// String repr. of asset ID --> asset ID
|
|
assetIDs := make(map[string]ids.ID)
|
|
// Asset ID --> amount of that asset being sent
|
|
amounts := make(map[ids.ID]uint64)
|
|
// Outputs of our tx
|
|
outs := []*lux.TransferableOutput{}
|
|
for _, output := range args.Outputs {
|
|
if output.Amount == 0 {
|
|
return nil, ids.ShortEmpty, errZeroAmount
|
|
}
|
|
assetID, ok := assetIDs[output.AssetID] // Asset ID of next output
|
|
if !ok {
|
|
assetID, err = s.vm.lookupAssetID(output.AssetID)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("couldn't find asset %s", output.AssetID)
|
|
}
|
|
assetIDs[output.AssetID] = assetID
|
|
}
|
|
currentAmount := amounts[assetID]
|
|
newAmount, err := safemath.Add64(currentAmount, uint64(output.Amount))
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("problem calculating required spend amount: %w", err)
|
|
}
|
|
amounts[assetID] = newAmount
|
|
|
|
// Parse the to address
|
|
to, err := lux.ParseServiceAddress(s.vm, output.To)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("problem parsing to address %q: %w", output.To, err)
|
|
}
|
|
|
|
// Create the Output
|
|
outs = append(outs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: assetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: uint64(output.Amount),
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{to},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
amountsWithFee := make(map[ids.ID]uint64, len(amounts)+1)
|
|
for assetID, amount := range amounts {
|
|
amountsWithFee[assetID] = amount
|
|
}
|
|
|
|
amountWithFee, err := safemath.Add64(amounts[s.vm.feeAssetID], s.vm.TxFee)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("problem calculating required spend amount: %w", err)
|
|
}
|
|
amountsWithFee[s.vm.feeAssetID] = amountWithFee
|
|
|
|
amountsSpent, ins, keys, err := s.vm.Spend(
|
|
utxos,
|
|
kc,
|
|
amountsWithFee,
|
|
)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Add the required change outputs
|
|
for assetID, amountWithFee := range amountsWithFee {
|
|
amountSpent := amountsSpent[assetID]
|
|
|
|
if amountSpent > amountWithFee {
|
|
outs = append(outs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: assetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amountSpent - amountWithFee,
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{changeAddr},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
codec := s.vm.parser.Codec()
|
|
lux.SortTransferableOutputs(outs)
|
|
|
|
tx := &txs.Tx{Unsigned: &txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: s.vm.consensusRuntime.NetworkID,
|
|
BlockchainID: s.vm.consensusRuntime.ChainID,
|
|
Outs: outs,
|
|
Ins: ins,
|
|
Memo: memoBytes,
|
|
}}}
|
|
return tx, changeAddr, tx.SignSECP256K1Fx(codec, keys)
|
|
}
|
|
|
|
// MintArgs are arguments for passing into Mint requests
|
|
type MintArgs struct {
|
|
JSONSpendHeader // User, password, from addrs, change addr
|
|
Amount avajson.Uint64 `json:"amount"`
|
|
AssetID string `json:"assetID"`
|
|
To string `json:"to"`
|
|
}
|
|
|
|
// Mint issues a transaction that mints more of the asset
|
|
func (s *Service) Mint(_ *http.Request, args *MintArgs, reply *JSONTxIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "mint"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
tx, changeAddr, err := s.buildMint(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txID, err := s.vm.issueTxFromRPC(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem issuing transaction: %w", err)
|
|
}
|
|
|
|
reply.TxID = txID
|
|
reply.ChangeAddr, err = s.vm.FormatLocalAddress(changeAddr)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) buildMint(args *MintArgs) (*txs.Tx, ids.ShortID, error) {
|
|
if err := s.ready(); err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "mint"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
if args.Amount == 0 {
|
|
return nil, ids.ShortEmpty, errInvalidMintAmount
|
|
}
|
|
|
|
assetID, err := s.vm.lookupAssetID(args.AssetID)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
to, err := lux.ParseServiceAddress(s.vm, args.To)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("problem parsing to address %q: %w", args.To, err)
|
|
}
|
|
|
|
// Parse the from addresses
|
|
fromAddrs, err := lux.ParseServiceAddresses(s.vm, args.From)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
// Get the UTXOs/keys for the from addresses
|
|
feeUTXOs, feeKc, err := s.vm.LoadUser(args.Username, args.Password, fromAddrs)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Parse the change address.
|
|
if len(feeKc.Keys) == 0 {
|
|
return nil, ids.ShortEmpty, errNoKeys
|
|
}
|
|
defaultAddr, err := publicKeyToAddress(feeKc.Keys[0].PublicKey())
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
changeAddr, err := s.vm.selectChangeAddr(defaultAddr, args.ChangeAddr)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
amountsSpent, ins, keys, err := s.vm.Spend(
|
|
feeUTXOs,
|
|
feeKc,
|
|
map[ids.ID]uint64{
|
|
s.vm.feeAssetID: s.vm.TxFee,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
outs := []*lux.TransferableOutput{}
|
|
if amountSpent := amountsSpent[s.vm.feeAssetID]; amountSpent > s.vm.TxFee {
|
|
outs = append(outs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: s.vm.feeAssetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amountSpent - s.vm.TxFee,
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{changeAddr},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// Get all UTXOs/keys for the user
|
|
utxos, kc, err := s.vm.LoadUser(args.Username, args.Password, make(set.Set[ids.ShortID]))
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
ops, opKeys, err := s.vm.Mint(
|
|
utxos,
|
|
kc,
|
|
map[ids.ID]uint64{
|
|
assetID: uint64(args.Amount),
|
|
},
|
|
to,
|
|
)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
keys = append(keys, opKeys...)
|
|
|
|
tx := &txs.Tx{Unsigned: &txs.OperationTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: s.vm.consensusRuntime.NetworkID,
|
|
BlockchainID: s.vm.consensusRuntime.ChainID,
|
|
Outs: outs,
|
|
Ins: ins,
|
|
}},
|
|
Ops: ops,
|
|
}}
|
|
return tx, changeAddr, tx.SignSECP256K1Fx(s.vm.parser.Codec(), keys)
|
|
}
|
|
|
|
// SendNFTArgs are arguments for passing into SendNFT requests
|
|
type SendNFTArgs struct {
|
|
JSONSpendHeader // User, password, from addrs, change addr
|
|
AssetID string `json:"assetID"`
|
|
GroupID avajson.Uint32 `json:"groupID"`
|
|
To string `json:"to"`
|
|
}
|
|
|
|
// SendNFT sends an NFT
|
|
func (s *Service) SendNFT(_ *http.Request, args *SendNFTArgs, reply *JSONTxIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "sendNFT"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
tx, changeAddr, err := s.buildSendNFT(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txID, err := s.vm.issueTxFromRPC(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem issuing transaction: %w", err)
|
|
}
|
|
|
|
reply.TxID = txID
|
|
reply.ChangeAddr, err = s.vm.FormatLocalAddress(changeAddr)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) buildSendNFT(args *SendNFTArgs) (*txs.Tx, ids.ShortID, error) {
|
|
if err := s.ready(); err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
// Parse the asset ID
|
|
assetID, err := s.vm.lookupAssetID(args.AssetID)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Parse the to address
|
|
to, err := lux.ParseServiceAddress(s.vm, args.To)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("problem parsing to address %q: %w", args.To, err)
|
|
}
|
|
|
|
// Parse the from addresses
|
|
fromAddrs, err := lux.ParseServiceAddresses(s.vm, args.From)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
// Get the UTXOs/keys for the from addresses
|
|
utxos, kc, err := s.vm.LoadUser(args.Username, args.Password, fromAddrs)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Parse the change address.
|
|
if len(kc.Keys) == 0 {
|
|
return nil, ids.ShortEmpty, errNoKeys
|
|
}
|
|
defaultAddr, err := publicKeyToAddress(kc.Keys[0].PublicKey())
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
changeAddr, err := s.vm.selectChangeAddr(defaultAddr, args.ChangeAddr)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
amountsSpent, ins, secpKeys, err := s.vm.Spend(
|
|
utxos,
|
|
kc,
|
|
map[ids.ID]uint64{
|
|
s.vm.feeAssetID: s.vm.TxFee,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
outs := []*lux.TransferableOutput{}
|
|
if amountSpent := amountsSpent[s.vm.feeAssetID]; amountSpent > s.vm.TxFee {
|
|
outs = append(outs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: s.vm.feeAssetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amountSpent - s.vm.TxFee,
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{changeAddr},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
ops, nftKeys, err := s.vm.SpendNFT(
|
|
utxos,
|
|
kc,
|
|
assetID,
|
|
uint32(args.GroupID),
|
|
to,
|
|
)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
tx := &txs.Tx{Unsigned: &txs.OperationTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: s.vm.consensusRuntime.NetworkID,
|
|
BlockchainID: s.vm.consensusRuntime.ChainID,
|
|
Outs: outs,
|
|
Ins: ins,
|
|
}},
|
|
Ops: ops,
|
|
}}
|
|
|
|
codec := s.vm.parser.Codec()
|
|
if err := tx.SignSECP256K1Fx(codec, secpKeys); err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
return tx, changeAddr, tx.SignNFTFx(codec, nftKeys)
|
|
}
|
|
|
|
// MintNFTArgs are arguments for passing into MintNFT requests
|
|
type MintNFTArgs struct {
|
|
JSONSpendHeader // User, password, from addrs, change addr
|
|
AssetID string `json:"assetID"`
|
|
Payload string `json:"payload"`
|
|
To string `json:"to"`
|
|
Encoding formatting.Encoding `json:"encoding"`
|
|
}
|
|
|
|
// MintNFT issues a MintNFT transaction and returns the ID of the newly created transaction
|
|
func (s *Service) MintNFT(_ *http.Request, args *MintNFTArgs, reply *JSONTxIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "mintNFT"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
tx, changeAddr, err := s.buildMintNFT(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txID, err := s.vm.issueTxFromRPC(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem issuing transaction: %w", err)
|
|
}
|
|
|
|
reply.TxID = txID
|
|
reply.ChangeAddr, err = s.vm.FormatLocalAddress(changeAddr)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) buildMintNFT(args *MintNFTArgs) (*txs.Tx, ids.ShortID, error) {
|
|
if err := s.ready(); err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
assetID, err := s.vm.lookupAssetID(args.AssetID)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
to, err := lux.ParseServiceAddress(s.vm, args.To)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("problem parsing to address %q: %w", args.To, err)
|
|
}
|
|
|
|
payloadBytes, err := formatting.Decode(args.Encoding, args.Payload)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("problem decoding payload bytes: %w", err)
|
|
}
|
|
|
|
// Parse the from addresses
|
|
fromAddrs, err := lux.ParseServiceAddresses(s.vm, args.From)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
// Get the UTXOs/keys for the from addresses
|
|
feeUTXOs, feeKc, err := s.vm.LoadUser(args.Username, args.Password, fromAddrs)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Parse the change address.
|
|
if len(feeKc.Keys) == 0 {
|
|
return nil, ids.ShortEmpty, errNoKeys
|
|
}
|
|
defaultAddr, err := publicKeyToAddress(feeKc.Keys[0].PublicKey())
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
changeAddr, err := s.vm.selectChangeAddr(defaultAddr, args.ChangeAddr)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
amountsSpent, ins, secpKeys, err := s.vm.Spend(
|
|
feeUTXOs,
|
|
feeKc,
|
|
map[ids.ID]uint64{
|
|
s.vm.feeAssetID: s.vm.TxFee,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
outs := []*lux.TransferableOutput{}
|
|
if amountSpent := amountsSpent[s.vm.feeAssetID]; amountSpent > s.vm.TxFee {
|
|
outs = append(outs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: s.vm.feeAssetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amountSpent - s.vm.TxFee,
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{changeAddr},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// Get all UTXOs/keys
|
|
utxos, kc, err := s.vm.LoadUser(args.Username, args.Password, make(set.Set[ids.ShortID]))
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
ops, nftKeys, err := s.vm.MintNFT(
|
|
utxos,
|
|
kc,
|
|
assetID,
|
|
payloadBytes,
|
|
to,
|
|
)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
tx := &txs.Tx{Unsigned: &txs.OperationTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: s.vm.consensusRuntime.NetworkID,
|
|
BlockchainID: s.vm.consensusRuntime.ChainID,
|
|
Outs: outs,
|
|
Ins: ins,
|
|
}},
|
|
Ops: ops,
|
|
}}
|
|
|
|
codec := s.vm.parser.Codec()
|
|
if err := tx.SignSECP256K1Fx(codec, secpKeys); err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
return tx, changeAddr, tx.SignNFTFx(codec, nftKeys)
|
|
}
|
|
|
|
// ImportArgs are arguments for passing into Import requests
|
|
type ImportArgs struct {
|
|
// User that controls To
|
|
apitypes.UserPass
|
|
|
|
// Chain the funds are coming from
|
|
SourceChain string `json:"sourceChain"`
|
|
|
|
// Address receiving the imported LUX
|
|
To string `json:"to"`
|
|
}
|
|
|
|
// Import imports an asset to this chain from the P/C-Chain.
|
|
// The LUX must have already been exported from the P/C-Chain.
|
|
// Returns the ID of the newly created atomic transaction
|
|
func (s *Service) Import(_ *http.Request, args *ImportArgs, reply *apitypes.JSONTxID) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "import"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
tx, err := s.buildImport(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txID, err := s.vm.issueTxFromRPC(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem issuing transaction: %w", err)
|
|
}
|
|
|
|
reply.TxID = txID
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) buildImport(args *ImportArgs) (*txs.Tx, error) {
|
|
if err := s.ready(); err != nil {
|
|
return nil, err
|
|
}
|
|
chainID, err := s.vm.bcLookup.Lookup(args.SourceChain)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("problem parsing chainID %q: %w", args.SourceChain, err)
|
|
}
|
|
|
|
to, err := lux.ParseServiceAddress(s.vm, args.To)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("problem parsing to address %q: %w", args.To, err)
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
utxos, kc, err := s.vm.LoadUser(args.Username, args.Password, make(set.Set[ids.ShortID]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
atomicUTXOs, _, _, err := lux.GetAtomicUTXOs(
|
|
nil, // Temporarily pass nil - will need proper fix
|
|
chainID,
|
|
kc.Addrs,
|
|
ids.ShortEmpty,
|
|
ids.Empty,
|
|
int(maxPageSize),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("problem retrieving user's atomic UTXOs: %w", err)
|
|
}
|
|
|
|
amountsSpent, importInputs, importKeys, err := s.vm.SpendAll(atomicUTXOs, kc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ins := []*lux.TransferableInput{}
|
|
keys := [][]*secp256k1.PrivateKey{}
|
|
|
|
if amountSpent := amountsSpent[s.vm.feeAssetID]; amountSpent < s.vm.TxFee {
|
|
var localAmountsSpent map[ids.ID]uint64
|
|
localAmountsSpent, ins, keys, err = s.vm.Spend(
|
|
utxos,
|
|
kc,
|
|
map[ids.ID]uint64{
|
|
s.vm.feeAssetID: s.vm.TxFee - amountSpent,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for asset, amount := range localAmountsSpent {
|
|
newAmount, err := safemath.Add64(amountsSpent[asset], amount)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("problem calculating required spend amount: %w", err)
|
|
}
|
|
amountsSpent[asset] = newAmount
|
|
}
|
|
}
|
|
|
|
// Because we ensured that we had enough inputs for the fee, we can
|
|
// safely just remove it without concern for underflow.
|
|
amountsSpent[s.vm.feeAssetID] -= s.vm.TxFee
|
|
|
|
keys = append(keys, importKeys...)
|
|
|
|
outs := []*lux.TransferableOutput{}
|
|
for assetID, amount := range amountsSpent {
|
|
if amount > 0 {
|
|
outs = append(outs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: assetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amount,
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{to},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
lux.SortTransferableOutputs(outs)
|
|
|
|
tx := &txs.Tx{Unsigned: &txs.ImportTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: s.vm.consensusRuntime.NetworkID,
|
|
BlockchainID: s.vm.consensusRuntime.ChainID,
|
|
Outs: outs,
|
|
Ins: ins,
|
|
}},
|
|
SourceChain: chainID,
|
|
ImportedIns: importInputs,
|
|
}}
|
|
return tx, tx.SignSECP256K1Fx(s.vm.parser.Codec(), keys)
|
|
}
|
|
|
|
// ExportArgs are arguments for passing into ExportAVA requests
|
|
type ExportArgs struct {
|
|
// User, password, from addrs, change addr
|
|
JSONSpendHeader
|
|
// Amount of nLUX to send
|
|
Amount avajson.Uint64 `json:"amount"`
|
|
|
|
// Chain the funds are going to. Optional. Used if To address does not include the chainID.
|
|
TargetChain string `json:"targetChain"`
|
|
|
|
// ID of the address that will receive the LUX. This address may include the
|
|
// chainID, which is used to determine what the destination chain is.
|
|
To string `json:"to"`
|
|
|
|
AssetID string `json:"assetID"`
|
|
}
|
|
|
|
// Export sends an asset from this chain to the P/C-Chain.
|
|
// After this tx is accepted, the LUX must be imported to the P/C-chain with an importTx.
|
|
// Returns the ID of the newly created atomic transaction
|
|
func (s *Service) Export(_ *http.Request, args *ExportArgs, reply *JSONTxIDChangeAddr) error {
|
|
if err := s.ready(); err != nil {
|
|
return err
|
|
}
|
|
s.vm.log.Warn("deprecated API called",
|
|
log.String("service", "xvm"),
|
|
log.String("method", "export"),
|
|
"username", args.Username,
|
|
)
|
|
|
|
tx, changeAddr, err := s.buildExport(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txID, err := s.vm.issueTxFromRPC(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("problem issuing transaction: %w", err)
|
|
}
|
|
|
|
reply.TxID = txID
|
|
reply.ChangeAddr, err = s.vm.FormatLocalAddress(changeAddr)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) buildExport(args *ExportArgs) (*txs.Tx, ids.ShortID, error) {
|
|
if err := s.ready(); err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
// Parse the asset ID
|
|
assetID, err := s.vm.lookupAssetID(args.AssetID)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Get the chainID and parse the to address
|
|
chainID, to, err := s.vm.ParseAddress(args.To)
|
|
if err != nil {
|
|
chainID, err = s.vm.bcLookup.Lookup(args.TargetChain)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
to, err = ids.ShortFromString(args.To)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
}
|
|
|
|
if args.Amount == 0 {
|
|
return nil, ids.ShortEmpty, errZeroAmount
|
|
}
|
|
|
|
// Parse the from addresses
|
|
fromAddrs, err := lux.ParseServiceAddresses(s.vm, args.From)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
s.vm.Lock.Lock()
|
|
defer s.vm.Lock.Unlock()
|
|
|
|
// Get the UTXOs/keys for the from addresses
|
|
utxos, kc, err := s.vm.LoadUser(args.Username, args.Password, fromAddrs)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
// Parse the change address.
|
|
if len(kc.Keys) == 0 {
|
|
return nil, ids.ShortEmpty, errNoKeys
|
|
}
|
|
defaultAddr, err := publicKeyToAddress(kc.Keys[0].PublicKey())
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
changeAddr, err := s.vm.selectChangeAddr(defaultAddr, args.ChangeAddr)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
amounts := map[ids.ID]uint64{}
|
|
if assetID == s.vm.feeAssetID {
|
|
amountWithFee, err := safemath.Add64(uint64(args.Amount), s.vm.TxFee)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, fmt.Errorf("problem calculating required spend amount: %w", err)
|
|
}
|
|
amounts[s.vm.feeAssetID] = amountWithFee
|
|
} else {
|
|
amounts[s.vm.feeAssetID] = s.vm.TxFee
|
|
amounts[assetID] = uint64(args.Amount)
|
|
}
|
|
|
|
amountsSpent, ins, keys, err := s.vm.Spend(utxos, kc, amounts)
|
|
if err != nil {
|
|
return nil, ids.ShortEmpty, err
|
|
}
|
|
|
|
exportOuts := []*lux.TransferableOutput{{
|
|
Asset: lux.Asset{ID: assetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: uint64(args.Amount),
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{to},
|
|
},
|
|
},
|
|
}}
|
|
|
|
outs := []*lux.TransferableOutput{}
|
|
for assetID, amountSpent := range amountsSpent {
|
|
amountToSend := amounts[assetID]
|
|
if amountSpent > amountToSend {
|
|
outs = append(outs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: assetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amountSpent - amountToSend,
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Locktime: 0,
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{changeAddr},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
codec := s.vm.parser.Codec()
|
|
lux.SortTransferableOutputs(outs)
|
|
|
|
tx := &txs.Tx{Unsigned: &txs.ExportTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: s.vm.consensusRuntime.NetworkID,
|
|
BlockchainID: s.vm.consensusRuntime.ChainID,
|
|
Outs: outs,
|
|
Ins: ins,
|
|
}},
|
|
DestinationChain: chainID,
|
|
ExportedOuts: exportOuts,
|
|
}}
|
|
return tx, changeAddr, tx.SignSECP256K1Fx(codec, keys)
|
|
}
|