From 35d5d63247f29e566c275e644bec6b775cee4b92 Mon Sep 17 00:00:00 2001 From: Tim Hawbaker Date: Thu, 2 Nov 2023 15:55:09 -0700 Subject: [PATCH 1/3] add wallet commands --- src/cmd/turnkey/pkg/wallets.go | 350 +++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 src/cmd/turnkey/pkg/wallets.go diff --git a/src/cmd/turnkey/pkg/wallets.go b/src/cmd/turnkey/pkg/wallets.go new file mode 100644 index 0000000..e4c4e7c --- /dev/null +++ b/src/cmd/turnkey/pkg/wallets.go @@ -0,0 +1,350 @@ +package pkg + +import ( + "fmt" + "github.com/rotisserie/eris" + "github.com/spf13/cobra" + + "github.com/tkhq/go-sdk/pkg/api/client/wallets" + "github.com/tkhq/go-sdk/pkg/api/models" + "github.com/tkhq/go-sdk/pkg/util" +) + +var ( + walletName string + walletAccountAddressFormat string + walletAccountCurve string + walletAccountPathFormat string + walletAccountPath string +) + +func init() { + walletCreateCmd.Flags().StringVar(&walletName, "name", "", "name to be applied to the wallet") + + walletAccountsListCmd.Flags().StringVar(&walletName, "name", "", "name of wallet used to fetch accounts") + + walletAccountCreateCmd.Flags().StringVar(&walletName, "name", "", "name of wallet used for account creation") + walletAccountCreateCmd.Flags().StringVar(&walletAccountAddressFormat, "address-format", "", "address format for account. For a list of formats, use 'turnkey address-formats list'.") + walletAccountCreateCmd.Flags().StringVar(&walletAccountCurve, "curve", "", "curve for account. For a list of curves, use 'turnkey curves list'. If unset, will predicate based on address format.") + walletAccountCreateCmd.Flags().StringVar(&walletAccountPathFormat, "path-format", string(models.PathFormatBip32), "the derivation path format for account.") + walletAccountCreateCmd.Flags().StringVar(&walletAccountPath, "path", "", "the derivation path for account. If unset, will predicate next path.") + + walletAccountsCmd.AddCommand(walletAccountsListCmd) + walletAccountsCmd.AddCommand(walletAccountCreateCmd) + walletsCmd.AddCommand(walletCreateCmd) + walletsCmd.AddCommand(walletsListCmd) + walletsCmd.AddCommand(walletAccountsCmd) + + rootCmd.AddCommand(walletsCmd) +} + +var walletsCmd = &cobra.Command{ + Use: "wallets", + Short: "Interact with wallets", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + basicSetup(cmd) + LoadKeypair("") + LoadClient() + }, + Aliases: []string{}, +} + +var walletCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new wallet", + PreRun: func(cmd *cobra.Command, args []string) { + if walletName == "" { + OutputError(eris.New("name for wallet must be specified")) + } + }, + Run: func(cmd *cobra.Command, args []string) { + activity := string(models.ActivityTypeCreateWallet) + + params := wallets.NewCreateWalletParams() + params.SetBody(&models.CreateWalletRequest{ + OrganizationID: &Organization, + Parameters: &models.CreateWalletIntent{ + WalletName: &walletName, + Accounts: []*models.WalletAccountParams{}, + }, + TimestampMs: util.RequestTimestamp(), + Type: &activity, + }) + + if err := params.Body.Validate(nil); err != nil { + OutputError(eris.Wrap(err, "request validation failed")) + } + + resp, err := APIClient.V0().Wallets.CreateWallet(params, APIClient.Authenticator) + if err != nil { + OutputError(eris.Wrap(err, "request failed")) + } + + if !resp.IsSuccess() { + OutputError(eris.Errorf("failed to create wallet: %s", resp.Error())) + } + + Output(resp.Payload) + }, +} + +var walletsListCmd = &cobra.Command{ + Use: "list", + Short: "Return wallets for the organization", + Run: func(cmd *cobra.Command, args []string) { + params := wallets.NewGetWalletsParams() + + params.SetBody(&models.GetWalletsRequest{ + OrganizationID: &Organization, + }) + + if err := params.Body.Validate(nil); err != nil { + OutputError(eris.Wrap(err, "request validation failed")) + } + + resp, err := APIClient.V0().Wallets.GetWallets(params, APIClient.Authenticator) + if err != nil { + OutputError(eris.Wrap(err, "request failed")) + } + + if !resp.IsSuccess() { + OutputError(eris.Errorf("failed to list wallets: %s", resp.Error())) + } + + Output(resp.Payload) + }, +} + +var walletAccountsCmd = &cobra.Command{ + Use: "accounts", + Short: "Interact with wallet accounts", + Aliases: []string{"acc"}, +} + +var walletAccountCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new account for a wallet", + PreRun: func(cmd *cobra.Command, args []string) { + if walletName == "" { + OutputError(eris.New("name for wallet must be specified")) + } + + if walletAccountAddressFormat == "" { + OutputError(eris.New("address format cannot be empty")) + } + }, + Run: func(cmd *cobra.Command, args []string) { + wallet, err := lookupWalletByName(walletName) + if err != nil { + OutputError(eris.Wrap(err, "failed to lookup wallet")) + } + + addressFormat := models.AddressFormat(walletAccountAddressFormat) + curve := models.Curve(walletAccountCurve) + pathFormat := models.PathFormat(walletAccountPathFormat) + path := walletAccountPath + + // set standard curve, if we can, if no override + if curve == "" { + if standardCurve := getCurveForAddressFormat(addressFormat); standardCurve != nil { + curve = *standardCurve + } + } + + // set standard path, if we can, if no override + if path == "" { + accounts, err := listAccountsForWallet(wallet.WalletID) + if err != nil { + OutputError(eris.Wrap(err, "failed to lookup wallet accounts")) + } + + // build path map to avoid conflicts + paths := make(map[string]struct{}) + for _, account := range accounts { + // we only need to care about accounts w/ this same address format + if *account.AddressFormat != addressFormat { + continue + } + + paths[*account.Path] = struct{}{} + } + + // find the next unused standard path + for i := 0; i < len(paths)+1; i++ { + if standardPath := getStandardPath(pathFormat, addressFormat, i); standardPath != nil { + // we've found an unused path! + if _, ok := paths[*standardPath]; !ok { + path = *standardPath + break + } + } + } + } + + activity := string(models.ActivityTypeCreateWalletAccounts) + + params := wallets.NewCreateWalletAccountsParams() + params.SetBody(&models.CreateWalletAccountsRequest{ + OrganizationID: &Organization, + Parameters: &models.CreateWalletAccountsIntent{ + WalletID: wallet.WalletID, + Accounts: []*models.WalletAccountParams{ + { + AddressFormat: &addressFormat, + Curve: &curve, + PathFormat: &pathFormat, + Path: &path, + }, + }, + }, + TimestampMs: util.RequestTimestamp(), + Type: &activity, + }) + + if err := params.Body.Validate(nil); err != nil { + OutputError(eris.Wrap(err, "request validation failed")) + } + + resp, err := APIClient.V0().Wallets.CreateWalletAccounts(params, APIClient.Authenticator) + if err != nil { + OutputError(eris.Wrap(err, "request failed")) + } + + if !resp.IsSuccess() { + OutputError(eris.Errorf("failed to create wallet account: %s", resp.Error())) + } + + Output(resp.Payload) + }, +} + +var walletAccountsListCmd = &cobra.Command{ + Use: "list", + Short: "Return accounts for the wallet", + PreRun: func(cmd *cobra.Command, args []string) { + if walletName == "" { + OutputError(eris.New("name for wallet must be specified")) + } + }, + Run: func(cmd *cobra.Command, args []string) { + wallet, err := lookupWalletByName(walletName) + if err != nil { + OutputError(eris.Wrap(err, "failed to lookup wallet")) + } + + params := wallets.NewGetWalletAccountsParams() + + params.SetBody(&models.GetWalletAccountsRequest{ + OrganizationID: &Organization, + WalletID: wallet.WalletID, + }) + + if err := params.Body.Validate(nil); err != nil { + OutputError(eris.Wrap(err, "request validation failed")) + } + + resp, err := APIClient.V0().Wallets.GetWalletAccounts(params, APIClient.Authenticator) + if err != nil { + OutputError(eris.Wrap(err, "request failed")) + } + + if !resp.IsSuccess() { + OutputError(eris.Errorf("failed to list wallets: %s", resp.Error())) + } + + Output(resp.Payload) + }, +} + +func lookupWalletByName(name string) (*models.Wallet, error) { + params := wallets.NewGetWalletsParams() + + params.SetBody(&models.GetWalletsRequest{ + OrganizationID: &Organization, + }) + + if err := params.Body.Validate(nil); err != nil { + OutputError(eris.Wrap(err, "request validation failed")) + } + + resp, err := APIClient.V0().Wallets.GetWallets(params, APIClient.Authenticator) + if err != nil { + OutputError(eris.Wrap(err, "request failed")) + } + + if !resp.IsSuccess() { + OutputError(eris.Errorf("failed to list wallets: %s", resp.Error())) + } + + for _, wallet := range resp.Payload.Wallets { + if *wallet.WalletName == name { + return wallet, nil + } + } + + return nil, eris.Errorf("wallet name %q not found in list of wallets", name) +} + +func listAccountsForWallet(walletID *string) ([]*models.WalletAccount, error) { + params := wallets.NewGetWalletAccountsParams() + + params.SetBody(&models.GetWalletAccountsRequest{ + OrganizationID: &Organization, + WalletID: walletID, + }) + + if err := params.Body.Validate(nil); err != nil { + OutputError(eris.Wrap(err, "request validation failed")) + } + + resp, err := APIClient.V0().Wallets.GetWalletAccounts(params, APIClient.Authenticator) + if err != nil { + OutputError(eris.Wrap(err, "request failed")) + } + + if !resp.IsSuccess() { + OutputError(eris.Errorf("failed to list wallets: %s", resp.Error())) + } + + return resp.Payload.Accounts, nil +} + +func getCurveForAddressFormat(addressFormat models.AddressFormat) *models.Curve { + switch addressFormat { + case models.AddressFormatEthereum, models.AddressFormatCosmos, models.AddressFormatUncompressed: + return models.NewCurve(models.CurveSecp256k1) + case models.AddressFormatSolana: + return models.NewCurve(models.CurveEd25519) + default: + // we're here because either we haven't updated this switch statement after adding new + // address formats OR we've hit an address format that supports multiple curves so we'll + // make no assumptions on the expected curve + return nil + } +} + +func getStandardPath(pathFormat models.PathFormat, addressFormat models.AddressFormat, accountIndex int) *string { + // we currently only support BIP-32 so we'll make no assumptions about the path if given a different path format + if pathFormat != models.PathFormatBip32 { + return nil + } + + var path string + + switch addressFormat { + case models.AddressFormatEthereum: + path = fmt.Sprintf(`m/44'/60'/%d'/0/0`, accountIndex) + case models.AddressFormatCosmos: + path = fmt.Sprintf(`m/44'/118'/%d'/0/0`, accountIndex) + case models.AddressFormatSolana: + path = fmt.Sprintf(`m/44'/501'/%d'/0'`, accountIndex) + case models.AddressFormatUncompressed, models.AddressFormatCompressed: + path = fmt.Sprintf(`m/%d'`, accountIndex) + default: + // we're here because we haven't updated this switch statement after adding new + // address formats so we'll make no assumptions on the expected path + return nil + } + + return &path +} From f593420b937421bfe44a6cc7341230f588e4c1a0 Mon Sep 17 00:00:00 2001 From: Tim Hawbaker Date: Thu, 2 Nov 2023 16:11:17 -0700 Subject: [PATCH 2/3] fix typo --- src/cmd/turnkey/pkg/wallets.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmd/turnkey/pkg/wallets.go b/src/cmd/turnkey/pkg/wallets.go index e4c4e7c..c2e4dbc 100644 --- a/src/cmd/turnkey/pkg/wallets.go +++ b/src/cmd/turnkey/pkg/wallets.go @@ -25,9 +25,9 @@ func init() { walletAccountCreateCmd.Flags().StringVar(&walletName, "name", "", "name of wallet used for account creation") walletAccountCreateCmd.Flags().StringVar(&walletAccountAddressFormat, "address-format", "", "address format for account. For a list of formats, use 'turnkey address-formats list'.") - walletAccountCreateCmd.Flags().StringVar(&walletAccountCurve, "curve", "", "curve for account. For a list of curves, use 'turnkey curves list'. If unset, will predicate based on address format.") + walletAccountCreateCmd.Flags().StringVar(&walletAccountCurve, "curve", "", "curve for account. For a list of curves, use 'turnkey curves list'. If unset, will predict based on address format.") walletAccountCreateCmd.Flags().StringVar(&walletAccountPathFormat, "path-format", string(models.PathFormatBip32), "the derivation path format for account.") - walletAccountCreateCmd.Flags().StringVar(&walletAccountPath, "path", "", "the derivation path for account. If unset, will predicate next path.") + walletAccountCreateCmd.Flags().StringVar(&walletAccountPath, "path", "", "the derivation path for account. If unset, will predict next path.") walletAccountsCmd.AddCommand(walletAccountsListCmd) walletAccountsCmd.AddCommand(walletAccountCreateCmd) From 231e53783808fd9cdcc4250f9aaddb54f2c5b101 Mon Sep 17 00:00:00 2001 From: Tim Hawbaker Date: Fri, 3 Nov 2023 09:16:11 -0700 Subject: [PATCH 3/3] rename --name to --wallet, allow wallet name or ID --- src/cmd/turnkey/pkg/wallets.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/cmd/turnkey/pkg/wallets.go b/src/cmd/turnkey/pkg/wallets.go index c2e4dbc..0a8e0cc 100644 --- a/src/cmd/turnkey/pkg/wallets.go +++ b/src/cmd/turnkey/pkg/wallets.go @@ -11,7 +11,7 @@ import ( ) var ( - walletName string + walletNameOrID string walletAccountAddressFormat string walletAccountCurve string walletAccountPathFormat string @@ -19,11 +19,11 @@ var ( ) func init() { - walletCreateCmd.Flags().StringVar(&walletName, "name", "", "name to be applied to the wallet") + walletCreateCmd.Flags().StringVar(&walletNameOrID, "name", "", "name to be applied to the wallet") - walletAccountsListCmd.Flags().StringVar(&walletName, "name", "", "name of wallet used to fetch accounts") + walletAccountsListCmd.Flags().StringVar(&walletNameOrID, "wallet", "", "name or ID of wallet used to fetch accounts") - walletAccountCreateCmd.Flags().StringVar(&walletName, "name", "", "name of wallet used for account creation") + walletAccountCreateCmd.Flags().StringVar(&walletNameOrID, "wallet", "", "name or ID of wallet used for account creation") walletAccountCreateCmd.Flags().StringVar(&walletAccountAddressFormat, "address-format", "", "address format for account. For a list of formats, use 'turnkey address-formats list'.") walletAccountCreateCmd.Flags().StringVar(&walletAccountCurve, "curve", "", "curve for account. For a list of curves, use 'turnkey curves list'. If unset, will predict based on address format.") walletAccountCreateCmd.Flags().StringVar(&walletAccountPathFormat, "path-format", string(models.PathFormatBip32), "the derivation path format for account.") @@ -53,7 +53,7 @@ var walletCreateCmd = &cobra.Command{ Use: "create", Short: "Create a new wallet", PreRun: func(cmd *cobra.Command, args []string) { - if walletName == "" { + if walletNameOrID == "" { OutputError(eris.New("name for wallet must be specified")) } }, @@ -64,7 +64,7 @@ var walletCreateCmd = &cobra.Command{ params.SetBody(&models.CreateWalletRequest{ OrganizationID: &Organization, Parameters: &models.CreateWalletIntent{ - WalletName: &walletName, + WalletName: &walletNameOrID, Accounts: []*models.WalletAccountParams{}, }, TimestampMs: util.RequestTimestamp(), @@ -125,8 +125,8 @@ var walletAccountCreateCmd = &cobra.Command{ Use: "create", Short: "Create a new account for a wallet", PreRun: func(cmd *cobra.Command, args []string) { - if walletName == "" { - OutputError(eris.New("name for wallet must be specified")) + if walletNameOrID == "" { + OutputError(eris.New("name or id for wallet must be specified")) } if walletAccountAddressFormat == "" { @@ -134,7 +134,7 @@ var walletAccountCreateCmd = &cobra.Command{ } }, Run: func(cmd *cobra.Command, args []string) { - wallet, err := lookupWalletByName(walletName) + wallet, err := lookupWallet(walletNameOrID) if err != nil { OutputError(eris.Wrap(err, "failed to lookup wallet")) } @@ -222,12 +222,12 @@ var walletAccountsListCmd = &cobra.Command{ Use: "list", Short: "Return accounts for the wallet", PreRun: func(cmd *cobra.Command, args []string) { - if walletName == "" { - OutputError(eris.New("name for wallet must be specified")) + if walletNameOrID == "" { + OutputError(eris.New("name or ID for wallet must be specified")) } }, Run: func(cmd *cobra.Command, args []string) { - wallet, err := lookupWalletByName(walletName) + wallet, err := lookupWallet(walletNameOrID) if err != nil { OutputError(eris.Wrap(err, "failed to lookup wallet")) } @@ -256,7 +256,7 @@ var walletAccountsListCmd = &cobra.Command{ }, } -func lookupWalletByName(name string) (*models.Wallet, error) { +func lookupWallet(nameOrID string) (*models.Wallet, error) { params := wallets.NewGetWalletsParams() params.SetBody(&models.GetWalletsRequest{ @@ -277,12 +277,12 @@ func lookupWalletByName(name string) (*models.Wallet, error) { } for _, wallet := range resp.Payload.Wallets { - if *wallet.WalletName == name { + if *wallet.WalletName == nameOrID || *wallet.WalletID == nameOrID { return wallet, nil } } - return nil, eris.Errorf("wallet name %q not found in list of wallets", name) + return nil, eris.Errorf("wallet %q not found in list of wallets", nameOrID) } func listAccountsForWallet(walletID *string) ([]*models.WalletAccount, error) {