mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
The SDK's primary wallet no longer builds a C-Chain wallet, and every caller here was carrying scaffolding for the one it used to build: an emptyEVMKeychain that answers nothing, passed as the EVMKeychain of a wallet that never consults it, and in one case a nil. rpc transfer went further and reached around the wallet, building C-Chain atomic import and export transactions by hand against lux.issueTx and polling lux.getAtomicTxStatus. Value on the C-Chain moves by an ordinary EVM transaction against /v1/bc/C/rpc; an atomic export from an EVM chain is a different act with a different settlement path, and this command was not the place for it. transfer now moves value between P and X, names an unsupported chain in its error, and reports both the export and the import transaction rather than one. Co-authored-by: Hanzo Dev <[email protected]>
797 lines
24 KiB
Go
797 lines
24 KiB
Go
// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package chain provides chain deployment and management utilities.
|
|
package chain
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/luxfi/proto/p/signer"
|
|
"github.com/luxfi/proto/p/txs"
|
|
"github.com/luxfi/sdk/platformvm"
|
|
lux "github.com/luxfi/utxo"
|
|
"github.com/luxfi/vm/components/verify"
|
|
|
|
"github.com/luxfi/address"
|
|
"github.com/luxfi/cli/pkg/application"
|
|
keychainwrapper "github.com/luxfi/cli/pkg/keychain"
|
|
climodels "github.com/luxfi/cli/pkg/models"
|
|
"github.com/luxfi/cli/pkg/txutils"
|
|
"github.com/luxfi/cli/pkg/ux"
|
|
"github.com/luxfi/constants"
|
|
ethcommon "github.com/luxfi/geth/common"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/keychain"
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/netrunner/utils"
|
|
"github.com/luxfi/sdk/models"
|
|
"github.com/luxfi/sdk/wallet/primary"
|
|
"github.com/luxfi/sdk/wallet/primary/common"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
)
|
|
|
|
// ErrNoChainAuthKeysInWallet indicates the wallet doesn't contain required chain auth keys.
|
|
var ErrNoChainAuthKeysInWallet = errors.New("auth wallet does not contain chain auth keys")
|
|
|
|
// PublicDeployer handles chain deployment to public networks.
|
|
type PublicDeployer struct {
|
|
LocalDeployer
|
|
usingLedger bool
|
|
kc keychain.Keychain
|
|
network models.Network
|
|
app *application.Lux
|
|
}
|
|
|
|
// NewPublicDeployer creates a new PublicDeployer instance.
|
|
func NewPublicDeployer(app *application.Lux, usingLedger bool, kc keychain.Keychain, network models.Network) *PublicDeployer {
|
|
return &PublicDeployer{
|
|
LocalDeployer: *NewLocalDeployer(app, "", ""),
|
|
app: app,
|
|
usingLedger: usingLedger,
|
|
kc: kc,
|
|
network: network,
|
|
}
|
|
}
|
|
|
|
// AddValidator registers a validator on the primary network by
|
|
// AddPermissionlessValidatorTx. Under the "validators validate networks,
|
|
// chains live on networks" model a sovereign L1 IS a primary network at its
|
|
// own networkID, so this one path serves mainnet, testnet, devnet and any L1.
|
|
//
|
|
// pop is the node's BLS proof of possession; the primary network refuses a
|
|
// registration without one. end is when the validator leaves the set — the
|
|
// chain reads the start from its own timestamp, so there is nothing to
|
|
// schedule. rewardsOwner names where stake and delegation rewards land; nil
|
|
// falls back to the first wallet address. delegationShares is the fraction
|
|
// out of 1,000,000 the validator keeps from delegation rewards.
|
|
func (d *PublicDeployer) AddValidator(
|
|
nodeID ids.NodeID,
|
|
pop *signer.ProofOfPossession,
|
|
stake uint64,
|
|
end time.Time,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
delegationShares uint32,
|
|
) (ids.ID, error) {
|
|
wallet, err := d.loadWallet()
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
if rewardsOwner == nil {
|
|
walletAddrs := d.kc.Addresses().List()
|
|
if len(walletAddrs) == 0 {
|
|
return ids.Empty, errors.New("wallet has no address to own the rewards")
|
|
}
|
|
rewardsOwner = &secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{walletAddrs[0]},
|
|
}
|
|
}
|
|
if d.usingLedger {
|
|
ux.Logger.PrintToUser("*** Please sign the AddValidator transaction on the ledger device *** ")
|
|
}
|
|
tx, err := wallet.P().IssueAddPermissionlessValidatorTx(
|
|
&txs.ChainValidator{
|
|
Validator: txs.Validator{
|
|
NodeID: nodeID,
|
|
End: uint64(end.Unix()), //nolint:gosec // G115: Unix time is positive
|
|
Wght: stake,
|
|
},
|
|
Chain: constants.PrimaryNetworkID,
|
|
},
|
|
pop,
|
|
wallet.P().Builder().Context().UTXOAssetID,
|
|
rewardsOwner,
|
|
rewardsOwner,
|
|
delegationShares,
|
|
)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
return tx.ID(), nil
|
|
}
|
|
|
|
// CreateAssetTx creates a new asset on the X-Chain.
|
|
func (d *PublicDeployer) CreateAssetTx(
|
|
chainID ids.ID,
|
|
tokenName string,
|
|
tokenSymbol string,
|
|
denomination byte,
|
|
initialState map[uint32][]verify.State,
|
|
) (ids.ID, error) {
|
|
wallet, err := d.loadWallet(chainID)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
|
|
if d.usingLedger {
|
|
ux.Logger.PrintToUser("*** Please sign Create Asset Transaction hash on the ledger device *** ")
|
|
}
|
|
|
|
tx, err := wallet.X().IssueCreateAssetTx(tokenName, tokenSymbol, denomination, initialState)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
ux.Logger.PrintToUser("Create Asset Transaction successful, transaction ID: %s", tx.ID())
|
|
ux.Logger.PrintToUser("Now exporting asset to P-Chain ...")
|
|
return tx.ID(), err
|
|
}
|
|
|
|
// ExportToPChainTx exports assets from X-Chain to P-Chain.
|
|
func (d *PublicDeployer) ExportToPChainTx(
|
|
chainID ids.ID,
|
|
chainAssetID ids.ID,
|
|
owner *secp256k1fx.OutputOwners,
|
|
assetAmount uint64,
|
|
) (ids.ID, error) {
|
|
wallet, err := d.loadWallet(chainID)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
|
|
if d.usingLedger {
|
|
ux.Logger.PrintToUser("*** Please sign X -> P Chain Export Transaction hash on the ledger device *** ")
|
|
}
|
|
|
|
tx, err := wallet.X().IssueExportTx(ids.Empty,
|
|
[]*lux.TransferableOutput{
|
|
{
|
|
Asset: lux.Asset{
|
|
ID: chainAssetID,
|
|
},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: assetAmount,
|
|
OutputOwners: *owner,
|
|
},
|
|
},
|
|
})
|
|
if err == nil {
|
|
ux.Logger.PrintToUser("Export to P-Chain Transaction successful, transaction ID: %s", tx.ID())
|
|
ux.Logger.PrintToUser("Now importing asset from X-Chain ...")
|
|
}
|
|
return tx.ID(), err
|
|
}
|
|
|
|
// ImportFromXChain imports assets from X-Chain to P-Chain.
|
|
func (d *PublicDeployer) ImportFromXChain(
|
|
chainID ids.ID,
|
|
owner *secp256k1fx.OutputOwners,
|
|
) (ids.ID, error) {
|
|
wallet, err := d.loadWallet(chainID)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
if d.usingLedger {
|
|
ux.Logger.PrintToUser("*** Please sign X -> P Chain Import Transaction hash on the ledger device *** ")
|
|
}
|
|
xChainID := ids.FromStringOrPanic("2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM") // X-Chain ID
|
|
|
|
tx, err := wallet.P().IssueImportTx(xChainID, owner)
|
|
if err == nil {
|
|
ux.Logger.PrintToUser("Import from X Chain Transaction successful, transaction ID: %s", tx.ID())
|
|
ux.Logger.PrintToUser("Now transforming net into elastic net ...")
|
|
}
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
return tx.ID(), err
|
|
}
|
|
|
|
// TransformChainTx transforms a chain to a permissionless elastic chain.
|
|
func (d *PublicDeployer) TransformChainTx(
|
|
controlKeys []string,
|
|
chainAuthKeysStrs []string,
|
|
elasticChainConfig climodels.ElasticChainConfig,
|
|
chainID ids.ID,
|
|
chainAssetID ids.ID,
|
|
) (bool, ids.ID, *txs.Tx, []string, error) {
|
|
wallet, err := d.loadWallet(chainID)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
chainAuthKeys, err := address.ParseToIDs(chainAuthKeysStrs)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, fmt.Errorf("failure parsing chain auth keys: %w", err)
|
|
}
|
|
|
|
if d.usingLedger {
|
|
ux.Logger.PrintToUser("*** Please sign Transform Net hash on the ledger device *** ")
|
|
}
|
|
|
|
tx, err := d.createTransformChainTX(chainAuthKeys, elasticChainConfig, wallet, chainAssetID)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
_, remainingChainAuthKeys, err := txutils.GetRemainingSigners(tx, controlKeys)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
isFullySigned := len(remainingChainAuthKeys) == 0
|
|
|
|
if isFullySigned {
|
|
txID, err := d.Commit(tx)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
ux.Logger.PrintToUser("Transaction successful, transaction ID: %s", txID)
|
|
return true, txID, nil, nil, nil
|
|
}
|
|
|
|
ux.Logger.PrintToUser("Partial tx created")
|
|
return false, ids.Empty, tx, remainingChainAuthKeys, nil
|
|
}
|
|
|
|
// RemoveValidator removes a validator from the network identified by chainID
|
|
// (still expressed as a chainID here because RemoveChainValidatorTx is kept
|
|
// for one release cycle while the sovereign-L1 model phases in).
|
|
// It verifies that the wallet is one of the chain auth keys (so as to sign the tx).
|
|
// If operation is multisig (len(chainAuthKeysStrs) > 1), it creates a remove-
|
|
// validator tx and sets the change output owner to be a wallet address.
|
|
func (d *PublicDeployer) RemoveValidator(
|
|
controlKeys []string,
|
|
chainAuthKeysStrs []string,
|
|
chainID ids.ID,
|
|
nodeID ids.NodeID,
|
|
) (bool, *txs.Tx, []string, error) {
|
|
wallet, err := d.loadWallet(chainID)
|
|
if err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
chainAuthKeys, err := address.ParseToIDs(chainAuthKeysStrs)
|
|
if err != nil {
|
|
return false, nil, nil, fmt.Errorf("failure parsing chain auth keys: %w", err)
|
|
}
|
|
|
|
if d.usingLedger {
|
|
ux.Logger.PrintToUser("*** Please sign tx hash on the ledger device *** ")
|
|
}
|
|
|
|
tx, err := d.createRemoveValidatorTX(chainAuthKeys, nodeID, chainID, wallet)
|
|
if err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
|
|
_, remainingChainAuthKeys, err := txutils.GetRemainingSigners(tx, controlKeys)
|
|
if err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
isFullySigned := len(remainingChainAuthKeys) == 0
|
|
|
|
if isFullySigned {
|
|
id, err := d.Commit(tx)
|
|
if err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
ux.Logger.PrintToUser("Transaction successful, transaction ID: %s", id)
|
|
return true, nil, nil, nil
|
|
}
|
|
|
|
ux.Logger.PrintToUser("Partial tx created")
|
|
return false, tx, remainingChainAuthKeys, nil
|
|
}
|
|
|
|
// DeployChain creates a chain using the given control keys and threshold.
|
|
func (d *PublicDeployer) DeployChain(
|
|
controlKeys []string,
|
|
threshold uint32,
|
|
) (ids.ID, error) {
|
|
ux.Logger.PrintToUser("DeployNet: starting...")
|
|
wallet, err := d.loadWallet()
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
ux.Logger.PrintToUser("DeployNet: calling createNetTx...")
|
|
chainID, err := d.createNetworkTx(controlKeys, threshold, wallet)
|
|
if err != nil {
|
|
ux.Logger.PrintToUser("DeployNet: createNetTx error: %v", err)
|
|
return ids.Empty, err
|
|
}
|
|
ux.Logger.PrintToUser("Net has been created with ID: %s", chainID.String())
|
|
time.Sleep(2 * time.Second)
|
|
return chainID, nil
|
|
}
|
|
|
|
// DeployBlockchain creates a blockchain for the given chain.
|
|
// It creates a create blockchain tx and sets the change output owner
|
|
// to be a wallet address (if not, it may go to any other chain auth address).
|
|
// chain is the display name stored in the CreateChainTx.
|
|
// vmName, if non-empty, overrides the string used for VMID derivation; when
|
|
// empty it falls back to chain (preserving legacy behaviour for custom VMs).
|
|
func (d *PublicDeployer) DeployBlockchain(
|
|
controlKeys []string,
|
|
chainAuthKeysStrs []string,
|
|
chainID ids.ID,
|
|
chain string,
|
|
genesis []byte,
|
|
vmName ...string, // optional: VM name for VMID; defaults to chain
|
|
) (bool, ids.ID, *txs.Tx, []string, error) {
|
|
ux.Logger.PrintToUser("Now creating blockchain...")
|
|
|
|
wallet, err := d.loadWallet(chainID)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
|
|
// Use explicit vmName when provided (EVM chains: "Lux EVM" ≠ chain name).
|
|
resolvedVMName := chain
|
|
if len(vmName) > 0 && vmName[0] != "" {
|
|
resolvedVMName = vmName[0]
|
|
}
|
|
vmID, err := utils.VMID(resolvedVMName)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, fmt.Errorf("failed to create VM ID from %s: %w", resolvedVMName, err)
|
|
}
|
|
|
|
chainAuthKeys, err := address.ParseToIDs(chainAuthKeysStrs)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, fmt.Errorf("failure parsing chain auth keys: %w", err)
|
|
}
|
|
|
|
if d.usingLedger {
|
|
ux.Logger.PrintToUser("*** Please sign CreateNetwork transaction on the ledger device *** ")
|
|
}
|
|
|
|
tx, err := d.createBlockchainTx(chainAuthKeys, chain, vmID, chainID, genesis, wallet)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
|
|
_, remainingChainAuthKeys, err := txutils.GetRemainingSigners(tx, controlKeys)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
isFullySigned := len(remainingChainAuthKeys) == 0
|
|
|
|
id := ids.Empty
|
|
if isFullySigned {
|
|
id, err = d.Commit(tx)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
}
|
|
|
|
return isFullySigned, id, tx, remainingChainAuthKeys, nil
|
|
}
|
|
|
|
// Commit issues a fully signed transaction to the network.
|
|
func (d *PublicDeployer) Commit(
|
|
tx *txs.Tx,
|
|
) (ids.ID, error) {
|
|
wallet, err := d.loadWallet()
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
err = wallet.P().IssueTx(tx)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
return tx.ID(), nil
|
|
}
|
|
|
|
// Sign signs a transaction with the wallet's keys.
|
|
func (d *PublicDeployer) Sign(
|
|
tx *txs.Tx,
|
|
chainAuthKeysStrs []string,
|
|
chain ids.ID,
|
|
) error {
|
|
wallet, err := d.loadWallet(chain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chainAuthKeys, err := address.ParseToIDs(chainAuthKeysStrs)
|
|
if err != nil {
|
|
return fmt.Errorf("failure parsing chain auth keys: %w", err)
|
|
}
|
|
if ok := d.checkWalletHasChainAuthAddresses(chainAuthKeys); !ok {
|
|
return ErrNoChainAuthKeysInWallet
|
|
}
|
|
if d.usingLedger {
|
|
txName := txutils.GetLedgerDisplayName(tx)
|
|
if len(txName) == 0 {
|
|
ux.Logger.PrintToUser("*** Please sign tx hash on the ledger device *** ")
|
|
} else {
|
|
ux.Logger.PrintToUser("*** Please sign %s transaction on the ledger device *** ", txName)
|
|
}
|
|
}
|
|
if err := d.signTx(tx, wallet); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *PublicDeployer) loadWallet(preloadTxs ...ids.ID) (primary.Wallet, error) {
|
|
ctx := context.Background()
|
|
ux.Logger.PrintToUser("loadWallet: starting...")
|
|
|
|
api := os.Getenv("NODE_ENDPOINT")
|
|
if api == "" {
|
|
switch d.network {
|
|
case models.Testnet:
|
|
api = constants.TestnetAPIEndpoint
|
|
case models.Mainnet:
|
|
api = constants.MainnetAPIEndpoint
|
|
case models.Devnet:
|
|
api = constants.DevnetAPIEndpoint
|
|
case models.Local:
|
|
// used for E2E testing of public related paths
|
|
api = constants.LocalAPIEndpoint
|
|
default:
|
|
return nil, fmt.Errorf("unsupported public network")
|
|
}
|
|
}
|
|
ux.Logger.PrintToUser("loadWallet: using API endpoint %s", api)
|
|
|
|
// Build the set of P-Chain transactions to fetch (e.g., chain creation txs)
|
|
// This is needed so the wallet knows about chain owners when creating blockchain txs
|
|
pChainTxsToFetch := set.Set[ids.ID]{}
|
|
for _, txID := range preloadTxs {
|
|
pChainTxsToFetch.Add(txID)
|
|
}
|
|
|
|
// Set short dial timeouts on the default HTTP transport to handle DNS
|
|
// entries with mixed live/dead IPs. Without this, the Go HTTP client
|
|
// waits the full timeout on each dead IP before trying the next one.
|
|
if t, ok := http.DefaultTransport.(*http.Transport); ok {
|
|
t.DialContext = (&net.Dialer{Timeout: 5 * time.Second}).DialContext
|
|
t.TLSHandshakeTimeout = 10 * time.Second
|
|
t.TLSClientConfig = &tls.Config{InsecureSkipVerify: false} // Keep TLS verification
|
|
}
|
|
ux.Logger.PrintToUser("loadWallet: creating P-Chain wallet...")
|
|
// Use P-Chain only wallet since our X-Chain uses exchangevm which doesn't
|
|
// support standard XVM API methods.
|
|
wallet, err := primary.MakePChainWallet(ctx, &primary.WalletConfig{
|
|
URI: api,
|
|
LUXKeychain: keychainwrapper.WrapCryptoKeychain(d.kc),
|
|
PChainTxsToFetch: pChainTxsToFetch,
|
|
})
|
|
if err != nil {
|
|
ux.Logger.PrintToUser("loadWallet: error creating wallet: %v", err)
|
|
return nil, err
|
|
}
|
|
ux.Logger.PrintToUser("loadWallet: wallet created successfully")
|
|
return wallet, nil
|
|
}
|
|
|
|
func (d *PublicDeployer) getMultisigTxOptions(chainAuthKeys []ids.ShortID) []common.Option {
|
|
options := []common.Option{}
|
|
walletAddr := d.kc.Addresses().List()[0]
|
|
// addrs to use for signing
|
|
customAddrsSet := set.Set[ids.ShortID]{}
|
|
customAddrsSet.Add(walletAddr)
|
|
customAddrsSet.Add(chainAuthKeys...)
|
|
options = append(options, common.WithCustomAddresses(customAddrsSet))
|
|
// set change to go to wallet addr (instead of any other chain auth key)
|
|
changeOwner := &secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{walletAddr},
|
|
}
|
|
options = append(options, common.WithChangeOwner(changeOwner))
|
|
return options
|
|
}
|
|
|
|
func (d *PublicDeployer) createBlockchainTx(
|
|
chainAuthKeys []ids.ShortID,
|
|
chainName string,
|
|
vmID,
|
|
chainID ids.ID,
|
|
genesis []byte,
|
|
wallet primary.Wallet,
|
|
) (*txs.Tx, error) {
|
|
fxIDs := make([]ids.ID, 0)
|
|
options := d.getMultisigTxOptions(chainAuthKeys)
|
|
// create tx
|
|
unsignedTx, err := wallet.P().Builder().NewCreateChainTx(
|
|
chainID,
|
|
genesis,
|
|
vmID,
|
|
fxIDs,
|
|
chainName,
|
|
options...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx := txs.Tx{Unsigned: unsignedTx}
|
|
// sign with current wallet
|
|
if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil {
|
|
return nil, err
|
|
}
|
|
return &tx, nil
|
|
}
|
|
|
|
func (d *PublicDeployer) createRemoveValidatorTX(
|
|
chainAuthKeys []ids.ShortID,
|
|
nodeID ids.NodeID,
|
|
chainID ids.ID,
|
|
wallet primary.Wallet,
|
|
) (*txs.Tx, error) {
|
|
options := d.getMultisigTxOptions(chainAuthKeys)
|
|
// create tx
|
|
unsignedTx, err := wallet.P().Builder().NewRemoveChainValidatorTx(nodeID, chainID, options...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx := txs.Tx{Unsigned: unsignedTx}
|
|
// sign with current wallet
|
|
if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil {
|
|
return nil, err
|
|
}
|
|
return &tx, nil
|
|
}
|
|
|
|
func (d *PublicDeployer) createTransformChainTX(
|
|
chainAuthKeys []ids.ShortID,
|
|
elasticChainConfig climodels.ElasticChainConfig,
|
|
wallet primary.Wallet,
|
|
assetID ids.ID,
|
|
) (*txs.Tx, error) {
|
|
options := d.getMultisigTxOptions(chainAuthKeys)
|
|
// create tx
|
|
unsignedTx, err := wallet.P().Builder().NewTransformChainTx(elasticChainConfig.ChainID, assetID,
|
|
elasticChainConfig.InitialSupply, elasticChainConfig.MaxSupply, elasticChainConfig.MinConsumptionRate,
|
|
elasticChainConfig.MaxConsumptionRate, elasticChainConfig.MinValidatorStake, elasticChainConfig.MaxValidatorStake,
|
|
elasticChainConfig.MinStakeDuration, elasticChainConfig.MaxStakeDuration, elasticChainConfig.MinDelegationFee,
|
|
elasticChainConfig.MinDelegatorStake, elasticChainConfig.MaxValidatorWeightFactor, elasticChainConfig.UptimeRequirement, options...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx := txs.Tx{Unsigned: unsignedTx}
|
|
// sign with current wallet
|
|
if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil {
|
|
return nil, err
|
|
}
|
|
return &tx, nil
|
|
}
|
|
|
|
// ConvertL1 converts a chain to an L1 (LP99)
|
|
func (d *PublicDeployer) ConvertL1(
|
|
controlKeys []string,
|
|
chainAuthKeysStrs []string,
|
|
chainID ids.ID,
|
|
blockchainID ids.ID,
|
|
managerAddress ethcommon.Address,
|
|
validators []interface{}, // []*txs.ConvertNetworkToL1Validator
|
|
) (bool, ids.ID, *txs.Tx, []string, error) {
|
|
ux.Logger.PrintToUser("Now calling ConvertNetworkToL1Tx...")
|
|
|
|
// Get wallet
|
|
wallet, err := d.loadWallet(chainID)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
|
|
chainAuthKeys, err := address.ParseToIDs(chainAuthKeysStrs)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, fmt.Errorf("failure parsing auth keys: %w", err)
|
|
}
|
|
|
|
// Convert []interface{} to []*txs.ConvertNetworkToL1Validator
|
|
convertValidators := make([]*txs.ConvertNetworkToL1Validator, 0, len(validators))
|
|
for _, v := range validators {
|
|
validator, ok := v.(*txs.ConvertNetworkToL1Validator)
|
|
if !ok {
|
|
return false, ids.Empty, nil, nil, fmt.Errorf("invalid validator type: expected *txs.ConvertNetworkToL1Validator, got %T", v)
|
|
}
|
|
convertValidators = append(convertValidators, validator)
|
|
}
|
|
|
|
// Build ConvertNetworkToL1Tx using the wallet builder
|
|
options := d.getMultisigTxOptions(chainAuthKeys)
|
|
|
|
unsignedTx, err := wallet.P().Builder().NewConvertNetworkToL1Tx(
|
|
chainID,
|
|
blockchainID,
|
|
managerAddress.Bytes(),
|
|
convertValidators,
|
|
options...,
|
|
)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, fmt.Errorf("error building ConvertNetworkToL1Tx: %w", err)
|
|
}
|
|
|
|
tx := txs.Tx{Unsigned: unsignedTx}
|
|
ctx, cancel := context.WithTimeout(context.Background(), constants.RequestTimeout)
|
|
defer cancel()
|
|
if err := wallet.P().Signer().Sign(ctx, &tx); err != nil {
|
|
return false, ids.Empty, nil, nil, fmt.Errorf("error signing tx: %w", err)
|
|
}
|
|
|
|
_, remainingChainAuthKeys, err := txutils.GetRemainingSigners(&tx, controlKeys)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
|
|
if len(remainingChainAuthKeys) == 0 {
|
|
// Commit the transaction
|
|
txID, err := d.Commit(&tx)
|
|
if err != nil {
|
|
return false, ids.Empty, nil, nil, err
|
|
}
|
|
return true, txID, &tx, remainingChainAuthKeys, nil
|
|
}
|
|
return false, ids.Empty, &tx, remainingChainAuthKeys, nil
|
|
}
|
|
|
|
func (*PublicDeployer) signTx(
|
|
tx *txs.Tx,
|
|
wallet primary.Wallet,
|
|
) error {
|
|
if err := wallet.P().Signer().Sign(context.Background(), tx); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *PublicDeployer) createNetworkTx(controlKeys []string, threshold uint32, wallet primary.Wallet) (ids.ID, error) {
|
|
ux.Logger.PrintToUser("createNetworkTx: starting with control keys: %v", controlKeys)
|
|
addrs, err := address.ParseToIDs(controlKeys)
|
|
if err != nil {
|
|
return ids.Empty, fmt.Errorf("failure parsing control keys: %w", err)
|
|
}
|
|
ux.Logger.PrintToUser("createNetworkTx: parsed addresses: %v", addrs)
|
|
owners := &secp256k1fx.OutputOwners{
|
|
Addrs: addrs,
|
|
Threshold: threshold,
|
|
Locktime: 0,
|
|
}
|
|
opts := []common.Option{}
|
|
if d.usingLedger {
|
|
ux.Logger.PrintToUser("*** Please sign CreateNetwork transaction on the ledger device *** ")
|
|
}
|
|
ux.Logger.PrintToUser("createNetworkTx: calling IssueCreateNetworkTx...")
|
|
tx, err := wallet.P().IssueCreateNetworkTx(owners, opts...)
|
|
if err != nil {
|
|
ux.Logger.PrintToUser("createNetworkTx: IssueCreateNetworkTx error: %v", err)
|
|
return ids.Empty, err
|
|
}
|
|
ux.Logger.PrintToUser("createNetworkTx: tx issued successfully with ID: %s", tx.ID().String())
|
|
return tx.ID(), nil
|
|
}
|
|
|
|
func (d *PublicDeployer) getChainAuthAddressesInWallet(chainAuth []ids.ShortID) []ids.ShortID {
|
|
walletAddrs := d.kc.Addresses().List()
|
|
chainAuthInWallet := []ids.ShortID{}
|
|
for _, walletAddr := range walletAddrs {
|
|
for _, addr := range chainAuth {
|
|
if addr == walletAddr {
|
|
chainAuthInWallet = append(chainAuthInWallet, addr)
|
|
}
|
|
}
|
|
}
|
|
return chainAuthInWallet
|
|
}
|
|
|
|
// check that the wallet at least contain one chain auth address
|
|
func (d *PublicDeployer) checkWalletHasChainAuthAddresses(chainAuth []ids.ShortID) bool {
|
|
addrs := d.getChainAuthAddressesInWallet(chainAuth)
|
|
return len(addrs) != 0
|
|
}
|
|
|
|
// IsChainValidator checks if a node is a validator for the given chain.
|
|
func IsChainValidator(chainID ids.ID, nodeID ids.NodeID, network models.Network) (bool, error) {
|
|
var apiURL string
|
|
switch network {
|
|
case models.Mainnet:
|
|
apiURL = constants.MainnetAPIEndpoint
|
|
case models.Testnet:
|
|
apiURL = constants.TestnetAPIEndpoint
|
|
default:
|
|
return false, fmt.Errorf("invalid network: %s", network)
|
|
}
|
|
pClient := platformvm.NewClient(apiURL)
|
|
ctx, cancel := context.WithTimeout(context.Background(), constants.E2ERequestTimeout)
|
|
defer cancel()
|
|
|
|
vals, err := pClient.GetCurrentValidators(ctx, chainID, []ids.NodeID{nodeID})
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to get current validators")
|
|
}
|
|
|
|
return len(vals) != 0, nil
|
|
}
|
|
|
|
// GetPublicChainValidators returns the validators for a chain on a public network.
|
|
func GetPublicChainValidators(chainID ids.ID, network models.Network) ([]platformvm.ClientPermissionlessValidator, error) {
|
|
var apiURL string
|
|
switch network {
|
|
case models.Mainnet:
|
|
apiURL = constants.MainnetAPIEndpoint
|
|
case models.Testnet:
|
|
apiURL = constants.TestnetAPIEndpoint
|
|
default:
|
|
return nil, fmt.Errorf("invalid network: %s", network)
|
|
}
|
|
pClient := platformvm.NewClient(apiURL)
|
|
ctx, cancel := context.WithTimeout(context.Background(), constants.E2ERequestTimeout)
|
|
defer cancel()
|
|
|
|
vals, err := pClient.GetCurrentValidators(ctx, chainID, []ids.NodeID{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get current validators")
|
|
}
|
|
|
|
return vals, nil
|
|
}
|
|
|
|
// ValidateChainNameAndGetChains validates a chain name and returns chain information
|
|
func ValidateChainNameAndGetChains(chainName string) error {
|
|
// Basic validation - can be expanded later
|
|
if chainName == "" {
|
|
return fmt.Errorf("chain name cannot be empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IncreaseValidatorPChainBalance increases a validator's balance on P-chain.
|
|
func (d *PublicDeployer) IncreaseValidatorPChainBalance(
|
|
_ ids.ID, // validationID reserved for future use
|
|
balance uint64,
|
|
) error {
|
|
wallet, err := d.loadWallet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a base transaction to transfer funds to increase validator balance
|
|
// Use the network's native asset ID (LUX)
|
|
luxAssetID := ids.Empty
|
|
if d.network.ID() == constants.MainnetID || d.network.ID() == constants.TestnetID {
|
|
luxAssetID = ids.Empty // Native asset on mainnet/testnet
|
|
}
|
|
|
|
outputs := []*lux.TransferableOutput{
|
|
{
|
|
Asset: lux.Asset{ID: luxAssetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: balance,
|
|
OutputOwners: secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
tx, err := wallet.P().IssueBaseTx(outputs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create balance increase transaction: %w", err)
|
|
}
|
|
|
|
ux.Logger.PrintToUser("Increased validator balance by %d nLUX", balance)
|
|
ux.Logger.PrintToUser("Transaction ID: %s", tx.ID())
|
|
return nil
|
|
}
|
|
|
|
// GetDefaultChainAirdropKeyInfo returns the default airdrop key information for a chain.
|
|
func GetDefaultChainAirdropKeyInfo(_ *application.Lux, _ string) (string, string, string, error) {
|
|
// Return empty values for now - this would typically read from sidecar
|
|
return "", "", "", nil
|
|
}
|