From 8a822fe260e6866179856c868019f75abe9ac5cb Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Thu, 9 Oct 2025 17:41:33 -0400 Subject: [PATCH 01/13] convert --- example/convert_subnet.go | 120 ++++++++++++++++++++++ wallet/chains/pchain/builder.go | 63 +++++++++++- wallet/txs/p-chain/convertSubnetToL1Tx.go | 25 ++++- 3 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 example/convert_subnet.go diff --git a/example/convert_subnet.go b/example/convert_subnet.go new file mode 100644 index 0000000..28884b6 --- /dev/null +++ b/example/convert_subnet.go @@ -0,0 +1,120 @@ +//go:build convert_subnet +// +build convert_subnet + +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package main + +import ( + "fmt" + "os" + "time" + + "github.com/ava-labs/avalanche-tooling-sdk-go/network" + "github.com/ava-labs/avalanche-tooling-sdk-go/utils" + "github.com/ava-labs/avalanche-tooling-sdk-go/wallet/local" + "github.com/ava-labs/avalanche-tooling-sdk-go/wallet/types" + + pchainTxs "github.com/ava-labs/avalanche-tooling-sdk-go/wallet/txs/p-chain" + avagoTxs "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +func ConvertSubnet(subnetID, chainID string) error { + ctx, cancel := utils.GetTimedContext(120 * time.Second) + defer cancel() + network := network.FujiNetwork() + + localWallet, err := local.NewLocalWallet() + if err != nil { + return fmt.Errorf("failed to create wallet: %w", err) + } + + existingAccount, err := localWallet.ImportAccount("/Users/raymondsukanto/.avalanche-cli/key/newTestKey.pk") + if err != nil { + return fmt.Errorf("failed to ImportAccount: %w", err) + } + + // Validator information + nodeIDStr := "" + BLSPublicKey := "0x..." // Replace with actual BLS public key + BLSProofOfPossession := "0x..." // Replace with actual BLS proof + ChangeOwnerAddr := "P-fujixxx" // Address to receive remaining balance + Weight := 100 // Validator weight + Balance := 1000000000 // Validator balance in nAVAX + validatorManagerAddr := "0x0FEEDC0DE0000000000000000000000000000000" // Replace with actual contract address + + bootstrapValidators := []*pchainTxs.ConvertSubnetToL1Validator{} + bootstrapValidator := &pchainTxs.ConvertSubnetToL1Validator{ + NodeID: nodeIDStr, + Weight: uint64(Weight), + Balance: uint64(Balance), + BLSPublicKey: BLSPublicKey, + BLSProofOfPossession: BLSProofOfPossession, + RemainingBalanceOwner: ChangeOwnerAddr, + } + bootstrapValidators = append(bootstrapValidators, bootstrapValidator) + convertSubnetParams := &pchainTxs.ConvertSubnetToL1TxParams{ + SubnetAuthKeys: []string{"P-fuji1377nx80rx3pzneup5qywgdgdsmzntql7trcqlg"}, + SubnetID: subnetID, + // ChainID is Blockchain ID of the L1 where the validator manager contract is deployed. + ChainID: chainID, + // Address is address of the validator manager contract. + Address: validatorManagerAddr, + // Validators are the initial set of L1 validators after the conversion. + Validators: bootstrapValidators, + } + buildTxParams := types.BuildTxParams{ + BaseParams: types.BaseParams{ + Account: *existingAccount, + Network: network, + }, + BuildTxInput: convertSubnetParams, + } + buildTxResult, err := localWallet.BuildTx(ctx, buildTxParams) + if err != nil { + return fmt.Errorf("failed to BuildTx: %w", err) + } + + signTxParams := types.SignTxParams{ + BaseParams: types.BaseParams{ + Account: *existingAccount, + Network: network, + }, + BuildTxResult: &buildTxResult, + } + signTxResult, err := localWallet.SignTx(ctx, signTxParams) + if err != nil { + return fmt.Errorf("failed to signTx: %w", err) + } + + sendTxParams := types.SendTxParams{ + BaseParams: types.BaseParams{ + Account: *existingAccount, + Network: network, + }, + SignTxResult: &signTxResult, + } + sendTxResult, err := localWallet.SendTx(ctx, sendTxParams) + if err != nil { + return fmt.Errorf("failed to sendTx: %w", err) + } + if tx := sendTxResult.GetTx(); tx != nil { + if pChainTx, ok := tx.(*avagoTxs.Tx); ok { + fmt.Printf("sendTxResult %s \n", pChainTx.ID()) + } else { + fmt.Printf("sendTxResult %s transaction \n", sendTxResult.GetChainType()) + } + } + return nil +} + +func main() { + // Use a hardcoded subnet ID for this example + // In a real scenario, you would get this from creating a subnet first + subnetID := "2ZmvHHXEmdAJT9YX6KK58B6nGtxx4JA1T53S6Go1aAHjYjJmmp" + chainID := "2ZmvHHXEmdAJT9YX6KK58B6nGtxx4JA1T53S6Go1aAHjYjJmmp" + if err := ConvertSubnet(subnetID, chainID); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/wallet/chains/pchain/builder.go b/wallet/chains/pchain/builder.go index 6f074a8..62ae698 100644 --- a/wallet/chains/pchain/builder.go +++ b/wallet/chains/pchain/builder.go @@ -3,11 +3,14 @@ package pchain import ( + "encoding/json" "fmt" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" @@ -105,12 +108,16 @@ func buildConvertSubnetToL1Tx(wallet *primary.Wallet, account account.Account, p if err != nil { return types.BuildTxResult{}, fmt.Errorf("failed to convert address to bytes: %w", err) } + avagoValidators, err := convertSubnetValidatorsToAvagoValidators(params.Validators) + if err != nil { + return types.BuildTxResult{}, fmt.Errorf("failed to convert address to bytes: %w", err) + } options := getMultisigTxOptions(account, subnetAuthKeys) unsignedTx, err := wallet.P().Builder().NewConvertSubnetToL1Tx( subnetID, chainID, addressBytes, - params.Validators, + avagoValidators, options..., ) if err != nil { @@ -160,3 +167,57 @@ func convertAddressToBytes(addressStr string) ([]byte, error) { addressBytes := []byte(addressStr) return addressBytes, nil } + +func convertSubnetValidatorsToAvagoValidators(validators []*pchainTxs.ConvertSubnetToL1Validator) ([]*avagoTxs.ConvertSubnetToL1Validator, error) { + bootstrapValidators := []*avagoTxs.ConvertSubnetToL1Validator{} + for _, validator := range validators { + nodeID, err := ids.NodeIDFromString(validator.NodeID) + if err != nil { + return nil, fmt.Errorf("failed to parse node ID: %w", err) + } + + blsInfo, err := convertToBLSProofOfPossession(validator.BLSPublicKey, validator.BLSProofOfPossession) + if err != nil { + return nil, fmt.Errorf("failure parsing BLS info: %w", err) + } + + addrs, err := address.ParseToIDs([]string{validator.RemainingBalanceOwner}) + if err != nil { + return nil, fmt.Errorf("failure parsing change owner address: %w", err) + } + bootstrapValidator := &avagoTxs.ConvertSubnetToL1Validator{ + NodeID: nodeID[:], + Weight: validator.Weight, + Balance: validator.Balance, + Signer: blsInfo, + RemainingBalanceOwner: message.PChainOwner{ + Threshold: 1, + Addresses: addrs, + }, + } + bootstrapValidators = append(bootstrapValidators, bootstrapValidator) + } + + return bootstrapValidators, nil +} + +func convertToBLSProofOfPossession(publicKey, proofOfPossesion string) (signer.ProofOfPossession, error) { + type jsonProofOfPossession struct { + PublicKey string + ProofOfPossession string + } + jsonPop := jsonProofOfPossession{ + PublicKey: publicKey, + ProofOfPossession: proofOfPossesion, + } + popBytes, err := json.Marshal(jsonPop) + if err != nil { + return signer.ProofOfPossession{}, err + } + pop := &signer.ProofOfPossession{} + err = pop.UnmarshalJSON(popBytes) + if err != nil { + return signer.ProofOfPossession{}, err + } + return *pop, nil +} diff --git a/wallet/txs/p-chain/convertSubnetToL1Tx.go b/wallet/txs/p-chain/convertSubnetToL1Tx.go index 7600be1..01d0d07 100644 --- a/wallet/txs/p-chain/convertSubnetToL1Tx.go +++ b/wallet/txs/p-chain/convertSubnetToL1Tx.go @@ -6,10 +6,29 @@ import ( "fmt" "github.com/ava-labs/avalanche-tooling-sdk-go/constants" - - avagoTxs "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) +// ConvertSubnetToL1TxParams contains all parameters needed to create a ConvertSubnetToL1Tx +type ConvertSubnetToL1Validator struct { + // NodeID of this validator + NodeID string `serialize:"true" json:"nodeID"` + // Weight of this validator used when sampling + Weight uint64 `serialize:"true" json:"weight"` + // Initial balance for this validator + Balance uint64 `serialize:"true" json:"balance"` + // [Signer] is the BLS key for this validator. + // Note: We do not enforce that the BLS key is unique across all validators. + // This means that validators can share a key if they so choose. + // However, a NodeID + Subnet does uniquely map to a BLS key + BLSPublicKey string `serialize:"true" json:"signer"` + BLSProofOfPossession string + // Leftover $AVAX from the [Balance] will be issued to this owner once it is + // removed from the validator set. + RemainingBalanceOwner string `serialize:"true" json:"remainingBalanceOwner"` + // This owner has the authority to manually deactivate this validator. + DeactivationOwner string `serialize:"true" json:"deactivationOwner"` +} + // ConvertSubnetToL1TxParams contains all parameters needed to create a ConvertSubnetToL1Tx type ConvertSubnetToL1TxParams struct { // SubnetAuthKeys are the keys used to sign `ConvertSubnetToL1Tx` @@ -21,7 +40,7 @@ type ConvertSubnetToL1TxParams struct { // Address is address of the validator manager contract. Address string // Validators are the initial set of L1 validators after the conversion. - Validators []*avagoTxs.ConvertSubnetToL1Validator + Validators []*ConvertSubnetToL1Validator } // GetTxType returns the transaction type identifier From 9fd50ac9afbde349fb645875ddd6d674dce12bbd Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Thu, 9 Oct 2025 17:42:18 -0400 Subject: [PATCH 02/13] convert --- wallet/local/wallet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/local/wallet.go b/wallet/local/wallet.go index 174218c..eb55eed 100644 --- a/wallet/local/wallet.go +++ b/wallet/local/wallet.go @@ -129,7 +129,7 @@ func extractSubnetIDFromTx(tx interface{}) (ids.ID, error) { // For CreateChainTx, the subnet ID field is SubnetID return unsignedTx.SubnetID, nil case *avagoTxs.ConvertSubnetToL1Tx: - // For CreateChainTx, the subnet ID field is SubnetID + // For ConvertSubnetToL1Tx, the subnet ID field is Subnet return unsignedTx.Subnet, nil } } From 56dd05af1da37d5149014e644b264e683380d193 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Fri, 10 Oct 2025 11:47:30 -0400 Subject: [PATCH 03/13] convert subnet --- example/convert_subnet.go | 21 ++++++++++----------- example/create_chain.go | 2 +- wallet/chains/pchain/builder.go | 12 +----------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/example/convert_subnet.go b/example/convert_subnet.go index 28884b6..4e245a4 100644 --- a/example/convert_subnet.go +++ b/example/convert_subnet.go @@ -35,13 +35,13 @@ func ConvertSubnet(subnetID, chainID string) error { } // Validator information - nodeIDStr := "" - BLSPublicKey := "0x..." // Replace with actual BLS public key - BLSProofOfPossession := "0x..." // Replace with actual BLS proof - ChangeOwnerAddr := "P-fujixxx" // Address to receive remaining balance - Weight := 100 // Validator weight - Balance := 1000000000 // Validator balance in nAVAX - validatorManagerAddr := "0x0FEEDC0DE0000000000000000000000000000000" // Replace with actual contract address + nodeIDStr := "NodeID-7iXf6xZpXF8tFmYr1gGrEnPgK1sKqitAn" + BLSPublicKey := "0xa569075008a3ce67ab6a1d7f9d3e9312c734cb99d144a169d98ac11d0177c9b3298a33d91e6b970745b802126edc5c5b" // Replace with actual BLS public key + BLSProofOfPossession := "0xaf1045946fd8668e492b7748e87bc20d5de232959b9fd7ea15f4f92c96f341ebeb1202c39b18148a5460d044655d25670a77e4079b255582153c3a372ab0b806e5f526bb1ff561f8f92eabe6e2b52adc491d1d36869818bb51be2c5b5329bcbb" // Replace with actual BLS proof + ChangeOwnerAddr := "P-fuji1377nx80rx3pzneup5qywgdgdsmzntql7trcqlg" + ValidatorManagerAddress := "0x0FEEDC0DE0000000000000000000000000000000" + Weight := 100 // Validator weight + Balance := 1000000000 // Validator balance in nAVAX bootstrapValidators := []*pchainTxs.ConvertSubnetToL1Validator{} bootstrapValidator := &pchainTxs.ConvertSubnetToL1Validator{ @@ -58,10 +58,9 @@ func ConvertSubnet(subnetID, chainID string) error { SubnetID: subnetID, // ChainID is Blockchain ID of the L1 where the validator manager contract is deployed. ChainID: chainID, - // Address is address of the validator manager contract. - Address: validatorManagerAddr, // Validators are the initial set of L1 validators after the conversion. Validators: bootstrapValidators, + Address: ValidatorManagerAddress, } buildTxParams := types.BuildTxParams{ BaseParams: types.BaseParams{ @@ -111,8 +110,8 @@ func ConvertSubnet(subnetID, chainID string) error { func main() { // Use a hardcoded subnet ID for this example // In a real scenario, you would get this from creating a subnet first - subnetID := "2ZmvHHXEmdAJT9YX6KK58B6nGtxx4JA1T53S6Go1aAHjYjJmmp" - chainID := "2ZmvHHXEmdAJT9YX6KK58B6nGtxx4JA1T53S6Go1aAHjYjJmmp" + subnetID := "2FmiyhpCzWpdiytmRJoazkxAfNowquCfSisHGB8rys13wZDRQz" + chainID := "vzozrkRR95pvepywbV22oQWh7SqPb8FpegAKD71HfJSBwr3bM" if err := ConvertSubnet(subnetID, chainID); err != nil { fmt.Println(err) os.Exit(1) diff --git a/example/create_chain.go b/example/create_chain.go index f6c9c85..337e72b 100644 --- a/example/create_chain.go +++ b/example/create_chain.go @@ -96,7 +96,7 @@ func CreateChain(subnetID string) error { func main() { // Use a hardcoded subnet ID for this example // In a real scenario, you would get this from creating a subnet first - subnetID := "2ZmvHHXEmdAJT9YX6KK58B6nGtxx4JA1T53S6Go1aAHjYjJmmp" + subnetID := "2FmiyhpCzWpdiytmRJoazkxAfNowquCfSisHGB8rys13wZDRQz" if err := CreateChain(subnetID); err != nil { fmt.Println(err) os.Exit(1) diff --git a/wallet/chains/pchain/builder.go b/wallet/chains/pchain/builder.go index 62ae698..51d244e 100644 --- a/wallet/chains/pchain/builder.go +++ b/wallet/chains/pchain/builder.go @@ -104,10 +104,7 @@ func buildConvertSubnetToL1Tx(wallet *primary.Wallet, account account.Account, p if err != nil { return types.BuildTxResult{}, fmt.Errorf("failed to parse chain ID: %w", err) } - addressBytes, err := convertAddressToBytes(params.Address) - if err != nil { - return types.BuildTxResult{}, fmt.Errorf("failed to convert address to bytes: %w", err) - } + addressBytes := []byte(params.Address) avagoValidators, err := convertSubnetValidatorsToAvagoValidators(params.Validators) if err != nil { return types.BuildTxResult{}, fmt.Errorf("failed to convert address to bytes: %w", err) @@ -161,13 +158,6 @@ func convertSubnetAuthKeys(subnetAuthKeys []string) ([]ids.ShortID, error) { return subnetAuthKeyIDs, nil } -// convertAddressToBytes converts an address string to []byte -func convertAddressToBytes(addressStr string) ([]byte, error) { - // Convert the address string to bytes - addressBytes := []byte(addressStr) - return addressBytes, nil -} - func convertSubnetValidatorsToAvagoValidators(validators []*pchainTxs.ConvertSubnetToL1Validator) ([]*avagoTxs.ConvertSubnetToL1Validator, error) { bootstrapValidators := []*avagoTxs.ConvertSubnetToL1Validator{} for _, validator := range validators { From 385bc794b374ba3507c9b1cf6fa6bb2c3d9c8bcc Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Mon, 13 Oct 2025 11:43:01 -0400 Subject: [PATCH 04/13] register validator --- constants/constants.go | 1 + example/register_validator.go | 116 ++++++++++++++++++++ wallet/chains/pchain/builder.go | 43 +++++++- wallet/txs/p-chain/registerL1ValidatorTx.go | 44 ++++++++ 4 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 example/register_validator.go create mode 100644 wallet/txs/p-chain/registerL1ValidatorTx.go diff --git a/constants/constants.go b/constants/constants.go index a7ab68d..27153fd 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -23,6 +23,7 @@ const ( const ( PChainCreateSubnetTx = "CreateSubnetTx" PChainConvertSubnetToL1Tx = "ConvertSubnetToL1Tx" + PChainRegisterL1ValidatorTx = "RegisterL1ValidatorTx" PChainAddSubnetValidatorTx = "AddSubnetValidatorTx" PChainRemoveSubnetValidatorTx = "RemoveSubnetValidatorTx" PChainCreateChainTx = "CreateChainTx" diff --git a/example/register_validator.go b/example/register_validator.go new file mode 100644 index 0000000..d38bc24 --- /dev/null +++ b/example/register_validator.go @@ -0,0 +1,116 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package main + +import ( + "fmt" + "os" + "time" + + "github.com/ava-labs/avalanche-tooling-sdk-go/network" + "github.com/ava-labs/avalanche-tooling-sdk-go/utils" + "github.com/ava-labs/avalanche-tooling-sdk-go/wallet/local" + "github.com/ava-labs/avalanche-tooling-sdk-go/wallet/types" + + pchainTxs "github.com/ava-labs/avalanche-tooling-sdk-go/wallet/txs/p-chain" + avagoTxs "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +func RegisterValidator(subnetID, chainID string) error { + ctx, cancel := utils.GetTimedContext(120 * time.Second) + defer cancel() + network := network.FujiNetwork() + + localWallet, err := local.NewLocalWallet() + if err != nil { + return fmt.Errorf("failed to create wallet: %w", err) + } + + existingAccount, err := localWallet.ImportAccount("/Users/raymondsukanto/.avalanche-cli/key/newTestKey.pk") + if err != nil { + return fmt.Errorf("failed to ImportAccount: %w", err) + } + + // Validator information + nodeIDStr := "NodeID-7iXf6xZpXF8tFmYr1gGrEnPgK1sKqitAn" + BLSPublicKey := "0xa569075008a3ce67ab6a1d7f9d3e9312c734cb99d144a169d98ac11d0177c9b3298a33d91e6b970745b802126edc5c5b" // Replace with actual BLS public key + BLSProofOfPossession := "0xaf1045946fd8668e492b7748e87bc20d5de232959b9fd7ea15f4f92c96f341ebeb1202c39b18148a5460d044655d25670a77e4079b255582153c3a372ab0b806e5f526bb1ff561f8f92eabe6e2b52adc491d1d36869818bb51be2c5b5329bcbb" // Replace with actual BLS proof + ChangeOwnerAddr := "P-fuji1377nx80rx3pzneup5qywgdgdsmzntql7trcqlg" + ValidatorManagerAddress := "0x0FEEDC0DE0000000000000000000000000000000" + Weight := 100 // Validator weight + Balance := 1000000000 // Validator balance in nAVAX + + bootstrapValidators := []*pchainTxs.ConvertSubnetToL1Validator{} + bootstrapValidator := &pchainTxs.ConvertSubnetToL1Validator{ + NodeID: nodeIDStr, + Weight: uint64(Weight), + Balance: uint64(Balance), + BLSPublicKey: BLSPublicKey, + BLSProofOfPossession: BLSProofOfPossession, + RemainingBalanceOwner: ChangeOwnerAddr, + } + bootstrapValidators = append(bootstrapValidators, bootstrapValidator) + convertSubnetParams := &pchainTxs.ConvertSubnetToL1TxParams{ + SubnetAuthKeys: []string{"P-fuji1377nx80rx3pzneup5qywgdgdsmzntql7trcqlg"}, + SubnetID: subnetID, + // ChainID is Blockchain ID of the L1 where the validator manager contract is deployed. + ChainID: chainID, + // Validators are the initial set of L1 validators after the conversion. + Validators: bootstrapValidators, + Address: ValidatorManagerAddress, + } + buildTxParams := types.BuildTxParams{ + BaseParams: types.BaseParams{ + Account: *existingAccount, + Network: network, + }, + BuildTxInput: convertSubnetParams, + } + buildTxResult, err := localWallet.BuildTx(ctx, buildTxParams) + if err != nil { + return fmt.Errorf("failed to BuildTx: %w", err) + } + + signTxParams := types.SignTxParams{ + BaseParams: types.BaseParams{ + Account: *existingAccount, + Network: network, + }, + BuildTxResult: &buildTxResult, + } + signTxResult, err := localWallet.SignTx(ctx, signTxParams) + if err != nil { + return fmt.Errorf("failed to signTx: %w", err) + } + + sendTxParams := types.SendTxParams{ + BaseParams: types.BaseParams{ + Account: *existingAccount, + Network: network, + }, + SignTxResult: &signTxResult, + } + sendTxResult, err := localWallet.SendTx(ctx, sendTxParams) + if err != nil { + return fmt.Errorf("failed to sendTx: %w", err) + } + if tx := sendTxResult.GetTx(); tx != nil { + if pChainTx, ok := tx.(*avagoTxs.Tx); ok { + fmt.Printf("sendTxResult %s \n", pChainTx.ID()) + } else { + fmt.Printf("sendTxResult %s transaction \n", sendTxResult.GetChainType()) + } + } + return nil +} + +func main() { + // Use a hardcoded subnet ID for this example + // In a real scenario, you would get this from creating a subnet first + subnetID := "2FmiyhpCzWpdiytmRJoazkxAfNowquCfSisHGB8rys13wZDRQz" + chainID := "vzozrkRR95pvepywbV22oQWh7SqPb8FpegAKD71HfJSBwr3bM" + if err := RegisterValidator(subnetID, chainID); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/wallet/chains/pchain/builder.go b/wallet/chains/pchain/builder.go index 51d244e..f15e6e4 100644 --- a/wallet/chains/pchain/builder.go +++ b/wallet/chains/pchain/builder.go @@ -3,13 +3,14 @@ package pchain import ( + "encoding/hex" "encoding/json" "fmt" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary" @@ -31,6 +32,8 @@ func BuildTx(wallet *primary.Wallet, account account.Account, params types.Build return buildCreateChainTx(wallet, account, txType) case *pchainTxs.ConvertSubnetToL1TxParams: return buildConvertSubnetToL1Tx(wallet, account, txType) + case *pchainTxs.RegisterL1ValidatorParams: + return buildRegisterL1ValidatorTx(wallet, txType) default: return types.BuildTxResult{}, fmt.Errorf("unsupported P-Chain transaction type: %T", params.BuildTxInput) } @@ -125,6 +128,44 @@ func buildConvertSubnetToL1Tx(wallet *primary.Wallet, account account.Account, p return types.BuildTxResult{BuildTxOutput: pChainResult}, nil } +// buildConvertSubnetToL1Tx provides a default implementation that can be used by any wallet +func buildRegisterL1ValidatorTx(wallet *primary.Wallet, params *pchainTxs.RegisterL1ValidatorParams) (types.BuildTxResult, error) { + blsInfo, err := convertToBLSProofOfPossession(params.BLSPublicKey, params.BLSProofOfPossession) + if err != nil { + return types.BuildTxResult{}, fmt.Errorf("failure parsing BLS info: %w", err) + } + warpMessage, err := convertSignedMessageToBytes(params.Message) + if err != nil { + return types.BuildTxResult{}, fmt.Errorf("failure parsing BLS info: %w", err) + } + unsignedTx, err := wallet.P().Builder().NewRegisterL1ValidatorTx( + params.Balance, + blsInfo.ProofOfPossession, + warpMessage.Bytes(), + ) + if err != nil { + return types.BuildTxResult{}, fmt.Errorf("error building tx: %w", err) + } + builtTx := avagoTxs.Tx{Unsigned: unsignedTx} + pChainResult := types.NewPChainBuildTxResult(&builtTx) + return types.BuildTxResult{BuildTxOutput: pChainResult}, nil +} + +func convertSignedMessageToBytes(signedMessageStr string) (*warp.Message, error) { + // Decode the hex string + signedMessageBytes, err := hex.DecodeString(signedMessageStr) + if err != nil { + return nil, fmt.Errorf("unable to convert signed message from string to bytes") + } + + // Parse the signed message + signedMessage, err := warp.ParseMessage(signedMessageBytes) + if err != nil { + return nil, fmt.Errorf("unable to convert signed message from bytes to warp message") + } + return signedMessage, nil +} + // getMultisigTxOptions is a helper function that can be shared func getMultisigTxOptions(account account.Account, subnetAuthKeys []ids.ShortID) []common.Option { options := []common.Option{} diff --git a/wallet/txs/p-chain/registerL1ValidatorTx.go b/wallet/txs/p-chain/registerL1ValidatorTx.go new file mode 100644 index 0000000..e13a8a5 --- /dev/null +++ b/wallet/txs/p-chain/registerL1ValidatorTx.go @@ -0,0 +1,44 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package txs + +import ( + "fmt" + + "github.com/ava-labs/avalanche-tooling-sdk-go/constants" +) + +// RegisterL1ValidatorParams contains all parameters needed to create a ConvertSubnetToL1Tx +type RegisterL1ValidatorParams struct { + Balance uint64 + BLSPublicKey string + BLSProofOfPossession string + Message string +} + +// GetTxType returns the transaction type identifier +func (p RegisterL1ValidatorParams) GetTxType() string { + return constants.PChainRegisterL1ValidatorTx +} + +// Validate validates the parameters +func (p RegisterL1ValidatorParams) Validate() error { + if p.Balance == 0 { + return fmt.Errorf("subnet auth keys cannot be empty") + } + if p.BLSPublicKey == "" { + return fmt.Errorf("subnet ID cannot be empty") + } + if p.BLSProofOfPossession == "" { + return fmt.Errorf("chain ID cannot be empty") + } + if p.BLSProofOfPossession == "" { + return fmt.Errorf("chain ID cannot be empty") + } + return nil +} + +// GetChainType returns which chain this transaction is for +func (p RegisterL1ValidatorParams) GetChainType() string { + return constants.ChainTypePChain +} From 2f134222cdbeeb0b736a11f1d07e4de6c3a18e98 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Mon, 13 Oct 2025 12:22:15 -0400 Subject: [PATCH 05/13] lint --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 06b4360..0a2a244 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,4 @@ blockchain/KEY_PATH # ignore GoLand metafiles directory -.idea/ - -# Data files -data/accounts.json +.idea/ \ No newline at end of file From 4d79244e7be4ddc5247260212544a47ad2545064 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Tue, 14 Oct 2025 18:07:34 -0400 Subject: [PATCH 06/13] merge chain tx --- example/convert_subnet.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/example/convert_subnet.go b/example/convert_subnet.go index 4e245a4..f48470a 100644 --- a/example/convert_subnet.go +++ b/example/convert_subnet.go @@ -29,19 +29,19 @@ func ConvertSubnet(subnetID, chainID string) error { return fmt.Errorf("failed to create wallet: %w", err) } - existingAccount, err := localWallet.ImportAccount("/Users/raymondsukanto/.avalanche-cli/key/newTestKey.pk") + existingAccount, err := localWallet.ImportAccount("EXISTING_KEY_PATH") if err != nil { return fmt.Errorf("failed to ImportAccount: %w", err) } // Validator information - nodeIDStr := "NodeID-7iXf6xZpXF8tFmYr1gGrEnPgK1sKqitAn" - BLSPublicKey := "0xa569075008a3ce67ab6a1d7f9d3e9312c734cb99d144a169d98ac11d0177c9b3298a33d91e6b970745b802126edc5c5b" // Replace with actual BLS public key - BLSProofOfPossession := "0xaf1045946fd8668e492b7748e87bc20d5de232959b9fd7ea15f4f92c96f341ebeb1202c39b18148a5460d044655d25670a77e4079b255582153c3a372ab0b806e5f526bb1ff561f8f92eabe6e2b52adc491d1d36869818bb51be2c5b5329bcbb" // Replace with actual BLS proof - ChangeOwnerAddr := "P-fuji1377nx80rx3pzneup5qywgdgdsmzntql7trcqlg" - ValidatorManagerAddress := "0x0FEEDC0DE0000000000000000000000000000000" - Weight := 100 // Validator weight - Balance := 1000000000 // Validator balance in nAVAX + nodeIDStr := "NodeID-xxxxx" + BLSPublicKey := "0x....,." // Replace with actual BLS public key + BLSProofOfPossession := "0x....." // Replace with actual BLS proof of possession + ChangeOwnerAddr := "P-fujixxxx" + ValidatorManagerAddress := "0x0FEEDC0DE0000000000000000000000000000000" // default validator manager address + Weight := 100 // Validator weight + Balance := 1000000000 // Validator balance in nAVAX bootstrapValidators := []*pchainTxs.ConvertSubnetToL1Validator{} bootstrapValidator := &pchainTxs.ConvertSubnetToL1Validator{ @@ -53,8 +53,9 @@ func ConvertSubnet(subnetID, chainID string) error { RemainingBalanceOwner: ChangeOwnerAddr, } bootstrapValidators = append(bootstrapValidators, bootstrapValidator) + convertSubnetParams := &pchainTxs.ConvertSubnetToL1TxParams{ - SubnetAuthKeys: []string{"P-fuji1377nx80rx3pzneup5qywgdgdsmzntql7trcqlg"}, + SubnetAuthKeys: []string{"P-fujixxxxxx"}, SubnetID: subnetID, // ChainID is Blockchain ID of the L1 where the validator manager contract is deployed. ChainID: chainID, @@ -108,10 +109,10 @@ func ConvertSubnet(subnetID, chainID string) error { } func main() { - // Use a hardcoded subnet ID for this example - // In a real scenario, you would get this from creating a subnet first - subnetID := "2FmiyhpCzWpdiytmRJoazkxAfNowquCfSisHGB8rys13wZDRQz" - chainID := "vzozrkRR95pvepywbV22oQWh7SqPb8FpegAKD71HfJSBwr3bM" + // Use a hardcoded subnet ID & chain ID for this example + // In a real scenario, you would get this from creating a subnet & creating a blockchain first + subnetID := "SUBNET_ID" + chainID := "CHAIN_ID" if err := ConvertSubnet(subnetID, chainID); err != nil { fmt.Println(err) os.Exit(1) From 4addd509a2115a15329f4e51981284a50492f851 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Tue, 14 Oct 2025 18:08:00 -0400 Subject: [PATCH 07/13] merge chain tx --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0a2a244..8f1a503 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ blockchain/KEY_PATH # ignore GoLand metafiles directory -.idea/ \ No newline at end of file +.idea/ From e3b52b22d548ec69c3db4cb0dc49223bdc395d36 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Tue, 14 Oct 2025 18:08:50 -0400 Subject: [PATCH 08/13] merge chain tx --- example/register_validator.go | 116 ---------------------------------- 1 file changed, 116 deletions(-) delete mode 100644 example/register_validator.go diff --git a/example/register_validator.go b/example/register_validator.go deleted file mode 100644 index d38bc24..0000000 --- a/example/register_validator.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. -package main - -import ( - "fmt" - "os" - "time" - - "github.com/ava-labs/avalanche-tooling-sdk-go/network" - "github.com/ava-labs/avalanche-tooling-sdk-go/utils" - "github.com/ava-labs/avalanche-tooling-sdk-go/wallet/local" - "github.com/ava-labs/avalanche-tooling-sdk-go/wallet/types" - - pchainTxs "github.com/ava-labs/avalanche-tooling-sdk-go/wallet/txs/p-chain" - avagoTxs "github.com/ava-labs/avalanchego/vms/platformvm/txs" -) - -func RegisterValidator(subnetID, chainID string) error { - ctx, cancel := utils.GetTimedContext(120 * time.Second) - defer cancel() - network := network.FujiNetwork() - - localWallet, err := local.NewLocalWallet() - if err != nil { - return fmt.Errorf("failed to create wallet: %w", err) - } - - existingAccount, err := localWallet.ImportAccount("/Users/raymondsukanto/.avalanche-cli/key/newTestKey.pk") - if err != nil { - return fmt.Errorf("failed to ImportAccount: %w", err) - } - - // Validator information - nodeIDStr := "NodeID-7iXf6xZpXF8tFmYr1gGrEnPgK1sKqitAn" - BLSPublicKey := "0xa569075008a3ce67ab6a1d7f9d3e9312c734cb99d144a169d98ac11d0177c9b3298a33d91e6b970745b802126edc5c5b" // Replace with actual BLS public key - BLSProofOfPossession := "0xaf1045946fd8668e492b7748e87bc20d5de232959b9fd7ea15f4f92c96f341ebeb1202c39b18148a5460d044655d25670a77e4079b255582153c3a372ab0b806e5f526bb1ff561f8f92eabe6e2b52adc491d1d36869818bb51be2c5b5329bcbb" // Replace with actual BLS proof - ChangeOwnerAddr := "P-fuji1377nx80rx3pzneup5qywgdgdsmzntql7trcqlg" - ValidatorManagerAddress := "0x0FEEDC0DE0000000000000000000000000000000" - Weight := 100 // Validator weight - Balance := 1000000000 // Validator balance in nAVAX - - bootstrapValidators := []*pchainTxs.ConvertSubnetToL1Validator{} - bootstrapValidator := &pchainTxs.ConvertSubnetToL1Validator{ - NodeID: nodeIDStr, - Weight: uint64(Weight), - Balance: uint64(Balance), - BLSPublicKey: BLSPublicKey, - BLSProofOfPossession: BLSProofOfPossession, - RemainingBalanceOwner: ChangeOwnerAddr, - } - bootstrapValidators = append(bootstrapValidators, bootstrapValidator) - convertSubnetParams := &pchainTxs.ConvertSubnetToL1TxParams{ - SubnetAuthKeys: []string{"P-fuji1377nx80rx3pzneup5qywgdgdsmzntql7trcqlg"}, - SubnetID: subnetID, - // ChainID is Blockchain ID of the L1 where the validator manager contract is deployed. - ChainID: chainID, - // Validators are the initial set of L1 validators after the conversion. - Validators: bootstrapValidators, - Address: ValidatorManagerAddress, - } - buildTxParams := types.BuildTxParams{ - BaseParams: types.BaseParams{ - Account: *existingAccount, - Network: network, - }, - BuildTxInput: convertSubnetParams, - } - buildTxResult, err := localWallet.BuildTx(ctx, buildTxParams) - if err != nil { - return fmt.Errorf("failed to BuildTx: %w", err) - } - - signTxParams := types.SignTxParams{ - BaseParams: types.BaseParams{ - Account: *existingAccount, - Network: network, - }, - BuildTxResult: &buildTxResult, - } - signTxResult, err := localWallet.SignTx(ctx, signTxParams) - if err != nil { - return fmt.Errorf("failed to signTx: %w", err) - } - - sendTxParams := types.SendTxParams{ - BaseParams: types.BaseParams{ - Account: *existingAccount, - Network: network, - }, - SignTxResult: &signTxResult, - } - sendTxResult, err := localWallet.SendTx(ctx, sendTxParams) - if err != nil { - return fmt.Errorf("failed to sendTx: %w", err) - } - if tx := sendTxResult.GetTx(); tx != nil { - if pChainTx, ok := tx.(*avagoTxs.Tx); ok { - fmt.Printf("sendTxResult %s \n", pChainTx.ID()) - } else { - fmt.Printf("sendTxResult %s transaction \n", sendTxResult.GetChainType()) - } - } - return nil -} - -func main() { - // Use a hardcoded subnet ID for this example - // In a real scenario, you would get this from creating a subnet first - subnetID := "2FmiyhpCzWpdiytmRJoazkxAfNowquCfSisHGB8rys13wZDRQz" - chainID := "vzozrkRR95pvepywbV22oQWh7SqPb8FpegAKD71HfJSBwr3bM" - if err := RegisterValidator(subnetID, chainID); err != nil { - fmt.Println(err) - os.Exit(1) - } -} From 34d91b78c7e554512a101f5cb05bf5a19ff62cb5 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Tue, 14 Oct 2025 18:09:41 -0400 Subject: [PATCH 09/13] merge chain tx --- wallet/txs/p-chain/registerL1ValidatorTx.go | 44 --------------------- 1 file changed, 44 deletions(-) delete mode 100644 wallet/txs/p-chain/registerL1ValidatorTx.go diff --git a/wallet/txs/p-chain/registerL1ValidatorTx.go b/wallet/txs/p-chain/registerL1ValidatorTx.go deleted file mode 100644 index e13a8a5..0000000 --- a/wallet/txs/p-chain/registerL1ValidatorTx.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. -package txs - -import ( - "fmt" - - "github.com/ava-labs/avalanche-tooling-sdk-go/constants" -) - -// RegisterL1ValidatorParams contains all parameters needed to create a ConvertSubnetToL1Tx -type RegisterL1ValidatorParams struct { - Balance uint64 - BLSPublicKey string - BLSProofOfPossession string - Message string -} - -// GetTxType returns the transaction type identifier -func (p RegisterL1ValidatorParams) GetTxType() string { - return constants.PChainRegisterL1ValidatorTx -} - -// Validate validates the parameters -func (p RegisterL1ValidatorParams) Validate() error { - if p.Balance == 0 { - return fmt.Errorf("subnet auth keys cannot be empty") - } - if p.BLSPublicKey == "" { - return fmt.Errorf("subnet ID cannot be empty") - } - if p.BLSProofOfPossession == "" { - return fmt.Errorf("chain ID cannot be empty") - } - if p.BLSProofOfPossession == "" { - return fmt.Errorf("chain ID cannot be empty") - } - return nil -} - -// GetChainType returns which chain this transaction is for -func (p RegisterL1ValidatorParams) GetChainType() string { - return constants.ChainTypePChain -} From 1b4381d449f20b2192565d19859deae349bd236a Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Tue, 14 Oct 2025 18:10:29 -0400 Subject: [PATCH 10/13] lint --- wallet/chains/pchain/builder.go | 1 + 1 file changed, 1 insertion(+) diff --git a/wallet/chains/pchain/builder.go b/wallet/chains/pchain/builder.go index f15e6e4..3457626 100644 --- a/wallet/chains/pchain/builder.go +++ b/wallet/chains/pchain/builder.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/set" From c1b083e2e4d1e77900e8f0c64a8f2b575521e853 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Tue, 14 Oct 2025 18:13:04 -0400 Subject: [PATCH 11/13] lint --- wallet/chains/pchain/builder.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/wallet/chains/pchain/builder.go b/wallet/chains/pchain/builder.go index 3457626..3d07110 100644 --- a/wallet/chains/pchain/builder.go +++ b/wallet/chains/pchain/builder.go @@ -33,8 +33,6 @@ func BuildTx(wallet *primary.Wallet, account account.Account, params types.Build return buildCreateChainTx(wallet, account, txType) case *pchainTxs.ConvertSubnetToL1TxParams: return buildConvertSubnetToL1Tx(wallet, account, txType) - case *pchainTxs.RegisterL1ValidatorParams: - return buildRegisterL1ValidatorTx(wallet, txType) default: return types.BuildTxResult{}, fmt.Errorf("unsupported P-Chain transaction type: %T", params.BuildTxInput) } @@ -129,29 +127,6 @@ func buildConvertSubnetToL1Tx(wallet *primary.Wallet, account account.Account, p return types.BuildTxResult{BuildTxOutput: pChainResult}, nil } -// buildConvertSubnetToL1Tx provides a default implementation that can be used by any wallet -func buildRegisterL1ValidatorTx(wallet *primary.Wallet, params *pchainTxs.RegisterL1ValidatorParams) (types.BuildTxResult, error) { - blsInfo, err := convertToBLSProofOfPossession(params.BLSPublicKey, params.BLSProofOfPossession) - if err != nil { - return types.BuildTxResult{}, fmt.Errorf("failure parsing BLS info: %w", err) - } - warpMessage, err := convertSignedMessageToBytes(params.Message) - if err != nil { - return types.BuildTxResult{}, fmt.Errorf("failure parsing BLS info: %w", err) - } - unsignedTx, err := wallet.P().Builder().NewRegisterL1ValidatorTx( - params.Balance, - blsInfo.ProofOfPossession, - warpMessage.Bytes(), - ) - if err != nil { - return types.BuildTxResult{}, fmt.Errorf("error building tx: %w", err) - } - builtTx := avagoTxs.Tx{Unsigned: unsignedTx} - pChainResult := types.NewPChainBuildTxResult(&builtTx) - return types.BuildTxResult{BuildTxOutput: pChainResult}, nil -} - func convertSignedMessageToBytes(signedMessageStr string) (*warp.Message, error) { // Decode the hex string signedMessageBytes, err := hex.DecodeString(signedMessageStr) From 5e9270f4f650d7846535c3f77698ad9718319e97 Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Tue, 14 Oct 2025 18:16:20 -0400 Subject: [PATCH 12/13] lint --- wallet/chains/pchain/builder.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/wallet/chains/pchain/builder.go b/wallet/chains/pchain/builder.go index 3d07110..51d244e 100644 --- a/wallet/chains/pchain/builder.go +++ b/wallet/chains/pchain/builder.go @@ -3,7 +3,6 @@ package pchain import ( - "encoding/hex" "encoding/json" "fmt" @@ -11,7 +10,6 @@ import ( "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/platformvm/signer" - "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary" @@ -127,21 +125,6 @@ func buildConvertSubnetToL1Tx(wallet *primary.Wallet, account account.Account, p return types.BuildTxResult{BuildTxOutput: pChainResult}, nil } -func convertSignedMessageToBytes(signedMessageStr string) (*warp.Message, error) { - // Decode the hex string - signedMessageBytes, err := hex.DecodeString(signedMessageStr) - if err != nil { - return nil, fmt.Errorf("unable to convert signed message from string to bytes") - } - - // Parse the signed message - signedMessage, err := warp.ParseMessage(signedMessageBytes) - if err != nil { - return nil, fmt.Errorf("unable to convert signed message from bytes to warp message") - } - return signedMessage, nil -} - // getMultisigTxOptions is a helper function that can be shared func getMultisigTxOptions(account account.Account, subnetAuthKeys []ids.ShortID) []common.Option { options := []common.Option{} From 029eef2ad5fd209445fbb67dd2d9668735fb0d3f Mon Sep 17 00:00:00 2001 From: Raymond Sukanto Date: Mon, 20 Oct 2025 18:01:00 -0400 Subject: [PATCH 13/13] remove base params --- example/convert_subnet.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/example/convert_subnet.go b/example/convert_subnet.go index f48470a..ecd0a28 100644 --- a/example/convert_subnet.go +++ b/example/convert_subnet.go @@ -64,10 +64,8 @@ func ConvertSubnet(subnetID, chainID string) error { Address: ValidatorManagerAddress, } buildTxParams := types.BuildTxParams{ - BaseParams: types.BaseParams{ - Account: *existingAccount, - Network: network, - }, + Account: *existingAccount, + Network: network, BuildTxInput: convertSubnetParams, } buildTxResult, err := localWallet.BuildTx(ctx, buildTxParams) @@ -76,10 +74,8 @@ func ConvertSubnet(subnetID, chainID string) error { } signTxParams := types.SignTxParams{ - BaseParams: types.BaseParams{ - Account: *existingAccount, - Network: network, - }, + Account: *existingAccount, + Network: network, BuildTxResult: &buildTxResult, } signTxResult, err := localWallet.SignTx(ctx, signTxParams) @@ -88,10 +84,8 @@ func ConvertSubnet(subnetID, chainID string) error { } sendTxParams := types.SendTxParams{ - BaseParams: types.BaseParams{ - Account: *existingAccount, - Network: network, - }, + Account: *existingAccount, + Network: network, SignTxResult: &signTxResult, } sendTxResult, err := localWallet.SendTx(ctx, sendTxParams)