Skip to content

Commit

Permalink
Merge pull-request #49
Browse files Browse the repository at this point in the history
  • Loading branch information
timurnkey committed Nov 3, 2023
2 parents 3f70e6f + 231e537 commit 6dd4e03
Showing 1 changed file with 350 additions and 0 deletions.
350 changes: 350 additions & 0 deletions src/cmd/turnkey/pkg/wallets.go
Original file line number Diff line number Diff line change
@@ -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 (
walletNameOrID string
walletAccountAddressFormat string
walletAccountCurve string
walletAccountPathFormat string
walletAccountPath string
)

func init() {
walletCreateCmd.Flags().StringVar(&walletNameOrID, "name", "", "name to be applied to the wallet")

walletAccountsListCmd.Flags().StringVar(&walletNameOrID, "wallet", "", "name or ID of wallet used to fetch accounts")

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.")
walletAccountCreateCmd.Flags().StringVar(&walletAccountPath, "path", "", "the derivation path for account. If unset, will predict 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 walletNameOrID == "" {
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: &walletNameOrID,
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 walletNameOrID == "" {
OutputError(eris.New("name or id for wallet must be specified"))
}

if walletAccountAddressFormat == "" {
OutputError(eris.New("address format cannot be empty"))
}
},
Run: func(cmd *cobra.Command, args []string) {
wallet, err := lookupWallet(walletNameOrID)
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 walletNameOrID == "" {
OutputError(eris.New("name or ID for wallet must be specified"))
}
},
Run: func(cmd *cobra.Command, args []string) {
wallet, err := lookupWallet(walletNameOrID)
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 lookupWallet(nameOrID 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 == nameOrID || *wallet.WalletID == nameOrID {
return wallet, nil
}
}

return nil, eris.Errorf("wallet %q not found in list of wallets", nameOrID)
}

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
}

0 comments on commit 6dd4e03

Please sign in to comment.