-
Notifications
You must be signed in to change notification settings - Fork 2
refactor: account management #191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…d account management This commit updates the wallet refactoring based on ADR decisions: - PrimaryOperations interface for P/X/C chain operations - Adds Primary() method to wallet interface that returns PrimaryOperations - Refactors BuildTx/SignTx/SendTx to use account names instead of account objects - Adds SubmitTx convenience method that combines build, sign, and send - Implements account management with named accounts and active account tracking - Adds AccountInfo for public account data and AccountSpec for import/export - Creates helper functions for wallet refresh and account operations - Moves commit retry logic to pchain.SendTx with sendWithRetry helper - Splits wallet operations into separate files (accounts.go, primary_operations.go, evm.go) - Updates example to use new API with environment variables - Changes from embedded fields to explicit members (LocalWallet.wallet, LocalAccount.softKey) - Removes unused wallet/builder.go, wallet/sender.go, wallet/signer.go files - Adds contract method wrapper and functional options pattern - Implements WalletRefreshTimeout context for account switching
5928fa8 to
a1538aa
Compare
|
This PR can definitely be broken down into smaller PRs (e.g. one with only evm interface, another with moving submit tx, etc) |
Updates the wallet interface to match wallet-local-evm branch: - Replace NewContractMethod with Method function - Add ParseResult helper for typed result extraction - Update ReadContract signature to remove outputParams parameter - Revert GetCChainAddress to not take network parameter - Improve error handling in P/X chain address methods - Update all interface examples to use consistent naming (w instead of wallet)
Remove network parameter from GetCChainAddress call in accounts.go to match the updated interface signature.
|
|
||
| // AccountInfo contains public information about an account | ||
| // Does NOT expose private key material | ||
| type AccountInfo struct { | ||
| Name string // Account name | ||
| PAddress string // P-Chain address | ||
| XAddress string // X-Chain address | ||
| CAddress string // C-Chain address | ||
| EVMAddress string // EVM address (0x format) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think we need this. We should keep account only to interface getter functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already agreed upon on ADR 5.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think it is, adr 5 discussion was all around evm and p/x/c placements in wallet interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the discussions by chat and meeting over ADR 4, a consensus was obtained on the account management interface. I wrote ADR 5 to have record of the decisions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Account naming features was discussed on adr 5, but not specific implementation, IIRC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anyways, IMO this reduces simplicity of account interface, but open to second opinions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems awkwardly placed here since none of the Account interface methods return this struct. The interface should have GetAccountInfo(). I'm still not fully convinced we need this struct to begin with. I will defer my review of this struct after some other comments of mine are resolved.
| // AccountSpec contains account creation/import specifications | ||
| type AccountSpec struct { | ||
| PrivateKey string // Hex-encoded private key (for local accounts) | ||
| DerivationPath string // For Ledger accounts | ||
| RemoteKeyID string // For remote signing services | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, seems that each field is specific to each account type like local accountn, server wallet, etc. They can be localized to each account type. like private key can be private getter function in local account
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already agreed upon on ADR 5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think it is, adr 5 discussion was all around evm and p/x/c placements in wallet interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same
examples/create_chain.go
Outdated
|
|
||
| // Get required environment variables | ||
| privateKey := os.Getenv("PRIVATE_KEY") | ||
| if privateKey == "" { | ||
| return fmt.Errorf("PRIVATE_KEY environment variable is required") | ||
| } | ||
|
|
||
| subnetAuthKey := os.Getenv("SUBNET_AUTH_KEY") | ||
| if subnetAuthKey == "" { | ||
| return fmt.Errorf("SUBNET_AUTH_KEY environment variable is required") | ||
| } | ||
|
|
||
| subnetID := os.Getenv("SUBNET_ID") | ||
| if subnetID == "" { | ||
| return fmt.Errorf("SUBNET_ID environment variable is required") | ||
| } | ||
|
|
||
| evmAddress := os.Getenv("EVM_ADDRESS") | ||
| if evmAddress == "" { | ||
| return fmt.Errorf("EVM_ADDRESS environment variable is required") | ||
| } | ||
|
|
||
| blockchainName := os.Getenv("BLOCKCHAIN_NAME") | ||
| if blockchainName == "" { | ||
| blockchainName = "MyBlockchain" | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe its important to not use Get ENV but instead use the var format i have in the other exmaple file so users know what thse variables should ook like. For example, subnet auth key, how will users know it should be P-xxx? o r Node iD-> NodeID-xxxxx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same goes for all example files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it is best to use get env so the file is executable and reusable in different settings. We can also have:
- add a comment on the expected format, both as code comment above Getenv() call and error returned to the used when variable is not set (eg "EVM_ADDRESS environment variable of format "..." (eg "...") is required").
- also in certain cases have a default value is no one is given
|
|
||
| createChainParams := &pchainTxs.CreateChainTxParams{ | ||
| SubnetAuthKeys: []string{"P-fujixxxxx"}, | ||
| SubnetID: subnetID, | ||
| VMID: vmID.String(), | ||
| ChainName: blockchainName, | ||
| Genesis: evmGenesisBytes, | ||
| } | ||
| buildTxParams := types.BuildTxParams{ | ||
| Account: *existingAccount, | ||
| Network: network, | ||
| BuildTxInput: createChainParams, | ||
| } | ||
| buildTxResult, err := localWallet.BuildTx(ctx, buildTxParams) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to BuildTx: %w", err) | ||
| } | ||
|
|
||
| signTxParams := types.SignTxParams{ | ||
| Account: *existingAccount, | ||
| Network: network, | ||
| BuildTxResult: &buildTxResult, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These examples should be kept so users know they can also use build/sign/send in addition to submit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same goes for all 3 example files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we want to propose SubmitTx as the operation to use in examples. Maybe we can just add
one example that shows the intermediate parts but not duplicate for everything. Eg I can add just an
example create_sign_send_subnet_tx.go
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ofc this example (and the other 3) were not removed but:
- moved into examples/ so we stop having directories examples/ and example/
- refactored to use SudmitTx
| type BuildTxParams struct { | ||
| Account account.Account | ||
| Network network.Network | ||
| AccountNames []string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be account
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much cleaner, it can remain []string
wallet/types/submit.go
Outdated
| @@ -0,0 +1,37 @@ | |||
| // Copyright (C) 2025, Ava Labs, Inc. All rights reserved. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just submit.go
wallet/wallet.go
Outdated
|
|
||
| // Primary returns the interface for P/X/C chain operations | ||
| // Example: w.Primary().BuildTx(...) | ||
| Primary() PrimaryOperations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think Primary is confusing to external devs. I would go for Avalanche, but open to opinions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no preference here. So it can be Avalanche. cc @jasonatran @artemanosov
wallet/result.go
Outdated
| // result, err := w.ReadContract(contractAddr, method) | ||
| // addr, err := wallet.ParseResult[common.Address](result, 0) | ||
| // amount, err := wallet.ParseResult[*big.Int](result, 1) | ||
| func ParseResult[T any](result []interface{}, index ...int) (T, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should jsut place all this into helper.go
wallet/chains/pchain/sender.go
Outdated
| ) | ||
|
|
||
| // sendWithRetry submits a transaction to the network with retry logic | ||
| func sendWithRetry(wallet *primary.Wallet, tx *txs.Tx) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this commit? should remain commit as function name
wallet/chains/pchain/sender.go
Outdated
| func SendTx(wallet *primary.Wallet, params types.SendTxParams) (types.SendTxResult, error) { | ||
| // Get the P-Chain transaction from the SignTxResult | ||
| pChainTx, ok := params.SignTxResult.GetTx().(*avagoTxs.Tx) | ||
| if !ok { | ||
| return types.SendTxResult{}, fmt.Errorf("expected P-Chain transaction, got %T", params.SignTxResult.GetTx()) | ||
| // Validate transaction is defined | ||
| if params.SignTxResult.Undefined() { | ||
| return types.SendTxResult{}, multisig.ErrUndefinedTx | ||
| } | ||
|
|
||
| // Submit the signed transaction to the network | ||
| if err := wallet.P().IssueTx(pChainTx); err != nil { | ||
| // Validate transaction is ready to commit | ||
| isReady, err := params.SignTxResult.IsReadyToCommit() | ||
| if err != nil { | ||
| return types.SendTxResult{}, err | ||
| } | ||
| if !isReady { | ||
| return types.SendTxResult{}, errors.New("tx is not fully signed so can't be committed") | ||
| } | ||
|
|
||
| // Extract the P-Chain transaction | ||
| tx, err := params.SignTxResult.GetWrappedPChainTx() | ||
| if err != nil { | ||
| return types.SendTxResult{}, err | ||
| } | ||
|
|
||
| // Submit the signed transaction to the network with retry logic | ||
| if err := sendWithRetry(wallet, tx); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this function even being changed?
wallet/local/primary_operations.go
Outdated
|
|
||
| // Determine which wallet to use | ||
| var walletToUse *primary.Wallet | ||
| if len(params.AccountNames) > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
break this up into the orignial sign/send/build files that i had
| // GetPChainAddress returns the P-Chain address for the given network | ||
| GetPChainAddress(network network.Network) (string, error) | ||
|
|
||
| // GetXChainAddress returns the X-Chain address for the given network |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should document what an account is here succinctly. In this case, it is a set of X/P/EVM addresses. This is equivalent to an avalanchego secp256k1fx.Keychain with just a single address of all types (avax type or EVM type). X and P can be empty or will be auto-filled if you create an Account with a constructor. I have no preference between these two.
|
I really think this PR needs to be broken down into separate PRs. This is way too large for us to adequately review this. I have only commented on the Account interface and local_account implementation. |
| // LocalAccount represents a local account implementation | ||
| type LocalAccount struct { | ||
| *key.SoftKey | ||
| softKey *key.SoftKey |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should drop this softkey abstraction. The implementation of LocalAccount should have a clear association with the Account interface. using this SoftKey abstraction adds another layer of indirection before you can tease out where the X P C EVM addresses are stored in the struct.
| ) | ||
|
|
||
| // generateAccountName generates an automatic account name based on the current count | ||
| func (w *LocalWallet) generateAccountName() string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the account name be part of the Account interface and implementation?
| } | ||
|
|
||
| // Accounts returns all accounts managed by this wallet with their info | ||
| func (w *LocalWallet) Accounts() map[string]account.AccountInfo { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be consistent, these getter functions should return a type account.Account. All functions that return something related to an Account should be returning account.Account not account.AccountInfo.
| } | ||
|
|
||
| // ImportAccount imports an account with a name | ||
| func (w *LocalWallet) ImportAccount(name string, spec account.AccountSpec) (account.AccountInfo, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of account.AccountSpec may seem more simple and more convenient to import accounts quickly, but I think the additional struct of account.AccountSpec adds some confusion/bloat. To import an account, I believe we should have this sequence of steps which seems natural.
account := NewAccount("acct_name") // we may have other constructors like NewAccountFromPrivKey(privKey)
wallet.ImportAccount(account)
This simplicity keeps all Account constructors, getters, and setters functions located with the Account implementation (/account/local_account.go)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In CDP, they only have private key as params https://docs.cdp.coinbase.com/api-reference/v2/rest-api/evm-accounts/import-an-evm-account
…actor-primary-operations # Conflicts: # account/local_account.go # example/convert_subnet.go # example/create_chain.go # example/create_subnet.go # examples/wallet_example.go # wallet/builder.go # wallet/local/accounts.go # wallet/local/wallet.go # wallet/wallet.go
Why this should be merged
Implements the wallet interface refactor agreed upon in ADRs [#4-a, #4-b, #5]. This provides:
How this works
Walletinterface with accounts, network, evm and primary operation methodslocal/accounts.go: Account management operationslocal/evm.go: EVM-specific operationslocal/primary_operations.go: P/X/C chain operations