From 11e24c67acb6f38f4880dd956fd688dd9461c630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= Date: Mon, 25 Sep 2023 17:45:30 +0200 Subject: [PATCH] cmd: Add reserved addresses, migrate ResolveAddress --- cmd/account/withdraw.go | 2 +- cmd/common/selector.go | 14 +---- cmd/common/wallet.go | 128 ++++++++++++++++++++++++++++++++++++-- cmd/common/wallet_test.go | 76 ++++++++++++++++++++++ cmd/network/show.go | 3 +- docs/account.md | 20 ++++++ 6 files changed, 223 insertions(+), 20 deletions(-) create mode 100644 cmd/common/wallet_test.go diff --git a/cmd/account/withdraw.go b/cmd/account/withdraw.go index a4b90d72..e4af99bd 100644 --- a/cmd/account/withdraw.go +++ b/cmd/account/withdraw.go @@ -61,7 +61,7 @@ var withdrawCmd = &cobra.Command{ } } else { // Destination address is implicit, but obtain it for safety check below nonetheless. - addr, _, err := helpers.ResolveAddress(npa.Network, npa.Account.Address) + addr, _, err := common.ResolveAddress(npa.Network, npa.Account.Address) cobra.CheckErr(err) addrToCheck = addr.String() } diff --git a/cmd/common/selector.go b/cmd/common/selector.go index 19d680f6..d1d0e328 100644 --- a/cmd/common/selector.go +++ b/cmd/common/selector.go @@ -7,7 +7,6 @@ import ( flag "github.com/spf13/pflag" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" - "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" cliConfig "github.com/oasisprotocol/cli/config" ) @@ -80,16 +79,9 @@ func GetNPASelection(cfg *cliConfig.Config) *NPASelection { s.AccountName = selectedAccount } if s.AccountName != "" { - if testName := helpers.ParseTestAccountAddress(s.AccountName); testName != "" { - testAcc, err := LoadTestAccountConfig(testName) - cobra.CheckErr(err) - s.Account = testAcc - } else { - s.Account = cfg.Wallet.All[s.AccountName] - if s.Account == nil { - cobra.CheckErr(fmt.Errorf("account '%s' does not exist in the wallet", s.AccountName)) - } - } + accCfg, err := LoadAccountConfig(cfg, s.AccountName) + cobra.CheckErr(err) + s.Account = accCfg } return &s diff --git a/cmd/common/wallet.go b/cmd/common/wallet.go index ed6c3056..618c76b5 100644 --- a/cmd/common/wallet.go +++ b/cmd/common/wallet.go @@ -2,9 +2,12 @@ package common import ( "fmt" + "strings" "github.com/AlecAivazis/survey/v2" ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/accounts" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/consensusaccounts" "github.com/spf13/cobra" staking "github.com/oasisprotocol/oasis-core/go/staking/api" @@ -19,10 +22,31 @@ import ( "github.com/oasisprotocol/cli/wallet/test" ) +const ( + addressExplicitSeparator = ":" + addressExplicitParaTime = "paratime" + addressExplicitConsensus = "consensus" + addressExplicitPool = "pool" + addressExplicitTest = "test" + + // Shared address literals: + poolCommon = "common" + poolFeeAccumulator = "fee-accumulator" + + // Consensus address literals: + poolGovernanceDeposits = "governance-deposits" + poolBurn = "burn" + + // ParaTime address literals: + poolRewards = "rewards" + poolPendingWithdrawal = "pending-withdrawal" + poolPendingDelegation = "pending-delegation" +) + // LoadAccount loads the given named account. func LoadAccount(cfg *config.Config, name string) wallet.Account { // Check if the specified account is a test account. - if testName := helpers.ParseTestAccountAddress(name); testName != "" { + if testName := ParseTestAccountAddress(name); testName != "" { acc, err := LoadTestAccount(testName) cobra.CheckErr(err) return acc @@ -49,9 +73,22 @@ func LoadAccount(cfg *config.Config, name string) wallet.Account { return acc } +// ParseTestAccountAddress extracts test account name from "test:some_test_account" format or +// returns an empty string, if the format doesn't match. +func ParseTestAccountAddress(name string) string { + if strings.Contains(name, addressExplicitSeparator) { + subs := strings.SplitN(name, addressExplicitSeparator, 2) + if subs[0] == addressExplicitTest { + return subs[1] + } + } + + return "" +} + // LoadAccountConfig loads the config instance of the given named account. func LoadAccountConfig(cfg *config.Config, name string) (*config.Account, error) { - if testName := helpers.ParseTestAccountAddress(name); testName != "" { + if testName := ParseTestAccountAddress(name); testName != "" { return LoadTestAccountConfig(testName) } @@ -113,7 +150,78 @@ func ResolveLocalAccountOrAddress(net *configSdk.Network, address string) (*type return &addr, entry.GetEthAddress(), nil } - return helpers.ResolveAddress(net, address) + return ResolveAddress(net, address) +} + +// ResolveAddress resolves a string address into the corresponding account address. +func ResolveAddress(net *configSdk.Network, address string) (*types.Address, *ethCommon.Address, error) { + if addr, ethAddr, _ := helpers.ResolveEthOrOasisAddress(address); addr != nil { + return addr, ethAddr, nil + } + + if !strings.Contains(address, addressExplicitSeparator) { + return nil, nil, fmt.Errorf("unsupported address format") + } + + subs := strings.SplitN(address, addressExplicitSeparator, 3) + switch kind, data := subs[0], subs[1]; kind { + case addressExplicitParaTime: + // paratime:sapphire, paratime:emerald, paratime:cipher + pt := net.ParaTimes.All[data] + if pt == nil { + return nil, nil, fmt.Errorf("paratime '%s' does not exist", data) + } + + addr := types.NewAddressFromConsensus(staking.NewRuntimeAddress(pt.Namespace())) + return &addr, nil, nil + case addressExplicitPool: + // pool:paratime:pending-withdrawal, pool:paratime:fee-accumulator, pool:consensus:fee-accumulator + poolKind, poolName := data, "" + if len(subs) > 2 { + poolName = subs[2] + } + if poolKind == addressExplicitParaTime { + switch poolName { + case poolRewards: + return &rewards.RewardPoolAddress, nil, nil + case poolPendingWithdrawal: + return &consensusaccounts.PendingWithdrawalAddress, nil, nil + case poolPendingDelegation: + return &consensusaccounts.PendingDelegationAddress, nil, nil + case poolCommon: + return &accounts.CommonPoolAddress, nil, nil + case poolFeeAccumulator: + return &accounts.FeeAccumulatorAddress, nil, nil + default: + return nil, nil, fmt.Errorf("unsupported ParaTime pool: %s", poolName) + } + } else if poolKind == addressExplicitConsensus { + var addr types.Address + switch poolName { + case poolCommon: + addr = types.NewAddressFromConsensus(staking.CommonPoolAddress) + case poolFeeAccumulator: + addr = types.NewAddressFromConsensus(staking.FeeAccumulatorAddress) + case poolGovernanceDeposits: + addr = types.NewAddressFromConsensus(staking.GovernanceDepositsAddress) + case poolBurn: + addr = types.NewAddressFromConsensus(staking.BurnAddress) + default: + return nil, nil, fmt.Errorf("unsupported consensus pool: %s", poolName) + } + return &addr, nil, nil + } + return nil, nil, fmt.Errorf("unsupported pool kind: %s. Please use pool::, for example pool:paratime:pending-withdrawal", poolKind) + case addressExplicitTest: + // test:alice, test:dave + if testKey, ok := testing.TestAccounts[data]; ok { + return &testKey.Address, testKey.EthAddress, nil + } + return nil, nil, fmt.Errorf("unsupported test account: %s", data) + default: + // Unsupported kind. + return nil, nil, fmt.Errorf("unsupported explicit address kind: %s", kind) + } } // CheckAddressIsConsensusCapable checks whether the given address is derived from any known @@ -150,15 +258,23 @@ func CheckAddressIsConsensusCapable(cfg *config.Config, address string) error { // fee accumulator or the native ParaTime addresses. func CheckAddressNotReserved(cfg *config.Config, address string) error { if address == rewards.RewardPoolAddress.String() { - return fmt.Errorf("address '%s' is rewards pool address", address) + return fmt.Errorf("address '%s' is ParaTime rewards pool address", address) + } + + if address == accounts.CommonPoolAddress.String() { + return fmt.Errorf("address '%s' is ParaTime common pool address", address) + } + + if address == accounts.FeeAccumulatorAddress.String() { + return fmt.Errorf("address '%s' is ParaTime fee accumulator address", address) } if address == staking.CommonPoolAddress.String() { - return fmt.Errorf("address '%s' is common pool address", address) + return fmt.Errorf("address '%s' is consensus common pool address", address) } if address == staking.FeeAccumulatorAddress.String() { - return fmt.Errorf("address '%s' is fee accumulator address", address) + return fmt.Errorf("address '%s' is consensus fee accumulator address", address) } if address == staking.GovernanceDepositsAddress.String() { diff --git a/cmd/common/wallet_test.go b/cmd/common/wallet_test.go new file mode 100644 index 00000000..918134f9 --- /dev/null +++ b/cmd/common/wallet_test.go @@ -0,0 +1,76 @@ +package common + +import ( + "testing" + + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + "github.com/stretchr/testify/require" +) + +func TestResolveAddress(t *testing.T) { + require := require.New(t) + + net := config.Network{ + ParaTimes: config.ParaTimes{ + All: map[string]*config.ParaTime{ + "pt1": { + ID: "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + } + + for _, tc := range []struct { + address string + expectedAddr string + expectedEthAddr string + }{ + {"", "", ""}, + {"oasis1", "", ""}, + {"oasis1blah", "", ""}, + {"oasis1qqzh32kr72v7x55cjnjp2me0pdn579u6as38kacz", "oasis1qqzh32kr72v7x55cjnjp2me0pdn579u6as38kacz", ""}, + {"0x", "", ""}, + {"0xblah", "", ""}, + {"0x60a6321eA71d37102Dbf923AAe2E08d005C4e403", "oasis1qpaqumrpewltmh9mr73hteycfzveus2rvvn8w5sp", "0x60a6321eA71d37102Dbf923AAe2E08d005C4e403"}, + {"paratime:", "", ""}, + {"paratime:invalid", "", ""}, + {"paratime:pt1", "oasis1qqdn25n5a2jtet2s5amc7gmchsqqgs4j0qcg5k0t", ""}, + {"pool:", "", ""}, + {"pool:invalid", "", ""}, + {"pool:rewards", "oasis1qp7x0q9qahahhjas0xde8w0v04ctp4pqzu5mhjav", ""}, + {"test:alice", "oasis1qrec770vrek0a9a5lcrv0zvt22504k68svq7kzve", ""}, + {"test:dave", "oasis1qrk58a6j2qn065m6p06jgjyt032f7qucy5wqeqpt", "0xDce075E1C39b1ae0b75D554558b6451A226ffe00"}, + {"test:frank", "oasis1qqnf0s9p8z79zfutszt0hwlh7w7jjrfqnq997mlw", ""}, + {"test:invalid", "", ""}, + {"invalid:", "", ""}, + } { + addr, ethAddr, err := ResolveAddress(&net, tc.address) + if len(tc.expectedAddr) > 0 { + require.NoError(err, tc.address) + require.EqualValues(tc.expectedAddr, addr.String(), tc.address) + if len(tc.expectedEthAddr) > 0 { + require.EqualValues(tc.expectedEthAddr, ethAddr.String()) + } + } else { + require.Error(err, tc.address) + } + } +} + +func TestParseTestAccountAddress(t *testing.T) { + require := require.New(t) + + for _, tc := range []struct { + address string + expected string + }{ + {"test:abc", "abc"}, + {"testabc", ""}, + {"testing:abc", ""}, + {"oasis1qqzh32kr72v7x55cjnjp2me0pdn579u6as38kacz", ""}, + {"", ""}, + } { + testName := ParseTestAccountAddress(tc.address) + require.EqualValues(tc.expected, testName, tc.address) + } +} diff --git a/cmd/network/show.go b/cmd/network/show.go index bf098eec..3f13ad16 100644 --- a/cmd/network/show.go +++ b/cmd/network/show.go @@ -17,7 +17,6 @@ import ( staking "github.com/oasisprotocol/oasis-core/go/staking/api" "github.com/oasisprotocol/oasis-core/go/staking/api/token" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" - "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" "github.com/oasisprotocol/cli/cmd/common" @@ -213,7 +212,7 @@ func parseIdentifier( return sel, nil } - addr, _, err := helpers.ResolveAddress(npa.Network, s) + addr, _, err := common.ResolveAddress(npa.Network, s) if err == nil { return addr, nil } diff --git a/docs/account.md b/docs/account.md index cc6dee7d..3ccd362b 100644 --- a/docs/account.md +++ b/docs/account.md @@ -648,3 +648,23 @@ assets anymore, but will also permanently remove the tokens from circulation. command. ::: + +### Pools and Reserved Addresses {#reserved-addresses} + +The following literals are used in the Oasis CLI to denote special reserved +addresses which cannot be directly used in the ledger: + +#### Consensus layer + +- `pool:consensus:burn`: The token burn address. +- `pool:consensus:common`: The common pool address. +- `pool:consensus:fee-accumulator`: The per-block fee accumulator address. +- `pool:consensus:governance-deposits`: The governance deposits address. + +#### ParaTime layer + +- `pool:paratime:common`: The common pool address. +- `pool:paratime:fee-accumulator`: The per-block fee accumulator address. +- `pool:paratime:pending-withdrawal`: The internal pending withdrawal address. +- `pool:paratime:pending-delegation`: The internal pending delegation address. +- `pool:paratime:rewards`: The reward pool address.