Skip to content
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

feat: import wallet account via mnemonic #233

Merged
merged 1 commit into from Jul 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/chat/client.nim
Expand Up @@ -56,6 +56,12 @@ proc addWalletPrivateKey*(self: ChatClient, name: string, privateKey: string,
asyncSpawn addWalletPrivateKey(self.taskRunner, status, name, privateKey,
password)

proc addWalletSeed*(self: ChatClient, name, mnemonic, password,
bip39passphrase: string) {.async.} =

asyncSpawn addWalletSeed(self.taskRunner, status, name, mnemonic,
password, bip39passphrase)

proc connect*(self: ChatClient, username: string) {.async.} =
asyncSpawn startWakuChat2(self.taskRunner, status, username)

Expand Down
47 changes: 45 additions & 2 deletions examples/chat/client/tasks.nim
Expand Up @@ -138,7 +138,7 @@ proc addWalletAccount*(name: string,

let
walletAccount = walletAccountResult.get
walletName = if walletAccount.name.isNone: "" else: walletAccount.name.get
walletName = walletAccount.name.get("")
event = AddWalletAccountEvent(name: walletName,
address: walletAccount.address, timestamp: timestamp)
eventEnc = event.encode
Expand Down Expand Up @@ -181,7 +181,50 @@ proc addWalletPrivateKey*(name: string, privateKey: string, password: string)

let
walletAccount = walletAccountResult.get
walletName = if walletAccount.name.isNone: "" else: walletAccount.name.get
walletName = walletAccount.name.get("")
event = AddWalletAccountEvent(name: walletName,
address: walletAccount.address, timestamp: timestamp)
eventEnc = event.encode
task = taskArg.taskName

trace "task sent event to host", event=eventEnc, task
asyncSpawn chanSendToHost.send(eventEnc.safe)

proc addWalletSeed*(name: string, mnemonic: string, password: string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styling: consider removing superfluous string type specifiers

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this?

proc addWalletSeed*(name, mnemonic, password, bip39Passphrase: string)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I just wanted to make sure I understood you correctly. I often forget to take advantage of that shortcut, thanks for the tip!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I myself noticed it in other parts of @emizzle 's code, so all kudos are his :) Didn't think it's possible in Nim.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After trying to make this change, I remember now why I didn't do this perviously for tasks. When combining same-typed parameters in the signature of a task proc, the compiler barfs with this error:

/Users/emizzle/repos/status-im/nim-status/examples/chat/client/tasks.nim(193, 37) Error: initialization not allowed here

I really don't know why, but I'm assuming it has something to do with the task macro.

bip39Passphrase: string) {.task(kind=no_rts, stoppable=false).} =

let timestamp = getTime().toUnix

if statusState != StatusState.loggedin:
let
eventNotLoggedIn = AddWalletAccountEvent(error: "Not logged in, " &
"cannot add a new wallet account.",
timestamp: timestamp)
eventNotLoggedInEnc = eventNotLoggedIn.encode
task = taskArg.taskName

trace "task sent event to host", event=eventNotLoggedInEnc, task
asyncSpawn chanSendToHost.send(eventNotLoggedInEnc.safe)
return

let
dir = status.dataDir / "keystore"
walletAccountResult = status.addWalletSeed(Mnemonic mnemonic, name,
password, dir, bip39Passphrase)

if walletAccountResult.isErr:
let
event = AddWalletAccountEvent(error: "Error adding wallet account, " &
"error: " & walletAccountResult.error, timestamp: timestamp)
eventEnc = event.encode
task = taskArg.taskName
trace "task sent event with error to host", event=eventEnc, task
asyncSpawn chanSendToHost.send(eventEnc.safe)
return

let
walletAccount = walletAccountResult.get
walletName = walletAccount.name.get("")
event = AddWalletAccountEvent(name: walletName,
address: walletAccount.address, timestamp: timestamp)
eventEnc = event.encode
Expand Down
76 changes: 71 additions & 5 deletions examples/chat/tui/commands.nim
Expand Up @@ -66,7 +66,7 @@ proc command*(self: ChatTUI, command: AddWalletAccount) {.async, gcsafe,
except:
self.wprintFormatError(epochTime().int64, "invalid arguments.")

# AddWalletPrivateKey -----------------------------------------------------------------
# AddWalletPrivateKey ----------------------------------------------------------

proc help*(T: type AddWalletPrivateKey): HelpText =
let command = "addwalletpk"
Expand Down Expand Up @@ -114,6 +114,72 @@ proc command*(self: ChatTUI, command: AddWalletPrivateKey) {.async, gcsafe,
else:
asyncSpawn self.client.addWalletPrivateKey(command.name,
command.privateKey, command.password)

# AddWalletSeed ------------------------------------------------------------

proc help*(T: type AddWalletSeed): HelpText =
let command = "addwalletseed"
HelpText(command: command, aliases: aliased[command], parameters: @[
CommandParameter(name: "name", description: "(Optional) Display name for " &
"the new account."),
CommandParameter(name: "mnemonic", description: "The 12-word mnemonic " &
"seed phrase of the wallet account to import."),
CommandParameter(name: "password", description: "Password of the current " &
"account.")
], description: "Imports a wallet account from a mnemonic seed.")

proc new*(T: type AddWalletSeed, args: varargs[string]): T =
T(name: args[0], mnemonic: args[1], password: args[2], bip39Passphrase: args[3])

proc split*(T: type AddWalletSeed, argsRaw: string): seq[string] =
let args = argsRaw.split(" ")
var
name: string
mnemonic: string
# bip39passphrase could be supplied (ie for use in Trezor hardwallets),
# however here we are assuming it not being passed in for simplicity in
# supplying input parameters to the command. This is in parity with how
# status-desktop and status-react are doing it. status-desktop
# implementation:
# https://github.com/status-im/status-desktop/tree/master/src/status/libstatus/accounts.nim#L244
passphrase = ""
password: string

if args.len == 0:
name = ""
mnemonic = ""
password = ""
elif args.len < 13:
name = ""
mnemonic = args[0..^1].join(" ")
password = ""
elif args.len < 14:
name = ""
mnemonic = args[0..11].join(" ")
password = args[12..^1].join(" ")
else:
name = args[0]
mnemonic = args[1..12].join(" ")
password = args[13..^1].join(" ")

@[name, mnemonic, password, passphrase]

proc command*(self: ChatTUI, command: AddWalletSeed) {.async, gcsafe,
nimcall.} =

if command.mnemonic == "" and command.password == "":
self.wprintFormatError(getTime().toUnix(),
"mnemonic and password cannot be blank.")
elif command.mnemonic == "":
self.wprintFormatError(getTime().toUnix(),
"mnemonic cannot be blank.")
elif command.password == "":
self.wprintFormatError(getTime().toUnix(),
"password cannot be blank, please provide a password as the last argument.")
else:
asyncSpawn self.client.addWalletSeed(command.name,
command.mnemonic, command.password, command.bip39Passphrase)

# Connect ----------------------------------------------------------------------

proc help*(T: type Connect): HelpText =
Expand Down Expand Up @@ -180,10 +246,10 @@ proc command*(self: ChatTUI, command: Disconnect) {.async, gcsafe, nimcall.} =
proc help*(T: type ImportMnemonic): HelpText =
let command = "importmnemonic"
HelpText(command: command, aliases: aliased[command], parameters: @[
CommandParameter(name: "mnemonic", description: "The mnemonic seed " &
"phrase used to import the account."),
CommandParameter(name: "passphrase", description: "(Optional) The " &
"passphrase used for securing against seed loss/theft."),
CommandParameter(name: "mnemonic", description: "The 12-word mnemonic " &
"seed phrase of the account to import."),
CommandParameter(name: "bip39passphrase", description: "(Optional) The " &
"BIP-39 passphrase used for securing against seed loss/theft."),
CommandParameter(name: "password", description: "The password used to " &
"encrypt the keystore file.")
], description: "Imports a Status account from a mnemoic.")
Expand Down
9 changes: 9 additions & 0 deletions examples/chat/tui/common.nim
Expand Up @@ -50,6 +50,12 @@ type
password*: string
privateKey*: string

AddWalletSeed* = ref object of Command
bip39Passphrase*: string
name*: string
mnemonic*: string
password*: string

Connect* = ref object of Command

CommandParameter* = ref object of RootObj
Expand Down Expand Up @@ -111,6 +117,7 @@ const
DEFAULT_COMMAND: "SendMessage",
"addwallet": "AddWalletAccount",
"addwalletpk": "AddWalletPrivateKey",
"addwalletseed": "AddWalletSeed",
"connect": "Connect",
"createaccount": "CreateAccount",
"disconnect": "Disconnect",
Expand All @@ -130,6 +137,7 @@ const
"?": "help",
"add": "addwallet",
"addpk": "addwalletpk",
"addseed": "addwalletseed",
"create": "createaccount",
"import": "importmnemonic",
"join": "jointopic",
Expand All @@ -151,6 +159,7 @@ const
DEFAULT_COMMAND: @["send"],
"addwallet": @["add"],
"addwalletpk": @["addpk"],
"addwalletseed": @["addseed"],
"createaccount": @["create"],
"importmnemonic": @["import"],
"help": @["?"],
Expand Down
2 changes: 1 addition & 1 deletion nim_status/accounts/generator/generator.nim
Expand Up @@ -57,7 +57,7 @@ proc addAccount(self: Generator, acc: Account): AddAccountResult =
AddAccountResult.ok(uuid)

proc deriveChildAccount(self: Generator, a: Account,
path: KeyPath): DeriveChildAccountResult =
path: KeyPath): DeriveChildAccountResult {.used.} =

let
childExtKey = ?a.extendedKey.derive(path)
Expand Down
102 changes: 54 additions & 48 deletions nim_status/client.nim
Expand Up @@ -10,8 +10,8 @@ import # nim-status libs
./accounts/[accounts, public_accounts],
./accounts/generator/[generator, utils],
./accounts/generator/account as generator_account, ./alias, ./chats,
./conversions, ./database, ./extkeys/[paths, types], ./identicon, ./settings,
./settings/types as settings_types, ./util
./conversions, ./database, ./extkeys/[hdkey, mnemonic, paths, types],
./identicon, ./settings, ./settings/types as settings_types, ./util

export results

Expand Down Expand Up @@ -69,42 +69,51 @@ proc close*(self: StatusObject) =
proc initUserDb(self: StatusObject, keyUid, password: string) =
self.userDbConn = initializeDB(self.dataDir / keyUid & ".db", password)

proc storeDerivedAccount(self: StatusObject, id: UUID, keyUid: string,
path: KeyPath, name, password, dir: string,
accountType: AccountType): WalletAccountResult =

let
accountInfos = ?self.accountsGenerator.storeDerivedAccounts(id, @[path],
password, dir)
acct = accountInfos[path]

let walletPubKeyResult = SkPublicKey.fromHex(acct.publicKey)

if walletPubKeyResult.isErr:
return WalletAccountResult.err $walletPubKeyResult.error
proc storeWalletAccount(self: StatusObject, name: string, address: Address,
publicKey: SkPublicKey, accountType: AccountType,
path: KeyPath): WalletAccountResult =

var walletName = name
if walletName == "":
let pathStr {.used.} = $path
walletName = fmt"Wallet account {pathStr[pathStr.len - 1]}"
let walletAccts {.used.} = self.userDb.getWalletAccounts()
walletName = fmt"Wallet account {walletAccts.len}"

let
walletAccount = accounts.Account(
address: acct.address.parseAddress,
address: address,
wallet: false.some, # NOTE: this *should* be true, however in status-go,
# only the wallet root account is true, and there is a unique db
# constraint enforcing only one account to have wallet = true
chat: false.some,
`type`: some($accountType),
`type`: ($accountType).some,
storage: string.none,
path: path.some,
publicKey: walletPubKeyResult.get.some,
publicKey: publicKey.some,
name: walletName.some,
color: "#4360df".some # TODO: pass in colour
)
self.userDb.createAccount(walletAccount)

WalletAccountResult.ok(walletAccount)
return WalletAccountResult.ok(walletAccount)

proc storeDerivedAccount(self: StatusObject, id: UUID, path: KeyPath, name,
password, dir: string, accountType: AccountType): WalletAccountResult =

let
accountInfos = ?self.accountsGenerator.storeDerivedAccounts(id, @[path],
password, dir)
acct = accountInfos[path]

let walletPubKeyResult = SkPublicKey.fromHex(acct.publicKey)

if walletPubKeyResult.isErr:
return WalletAccountResult.err $walletPubKeyResult.error

let
address = acct.address.parseAddress
publicKey = walletPubKeyResult.get

return self.storeWalletAccount(name, address, publicKey, accountType, path)

proc storeDerivedAccounts(self: StatusObject, id: UUID, keyUid: string,
paths: seq[KeyPath], password, dir: string): PublicAccountResult =
Expand Down Expand Up @@ -196,31 +205,13 @@ proc storeImportedWalletAccount(self: StatusObject, privateKey: SkSecretKey,

discard ?self.accountsGenerator.storeKeyFile(privateKey, password, dir)

let keyPath = PATH_DEFAULT_WALLET # NOTE: this is the keypath
# given to imported wallet accounts in status-desktop
var walletName = name
if walletName == "":
let walletAccts {.used.} = self.userDb.getWalletAccounts()
walletName = fmt"Wallet account {walletAccts.len}"

let
path = PATH_DEFAULT_WALLET # NOTE: this is the keypath
# given to imported wallet accounts in status-desktop
publicKey = privateKey.toPublicKey
walletAccount = accounts.Account(
address: (PublicKey publicKey).toAddress.parseAddress,
wallet: false.some, # NOTE: this *should* be true, however in status-go,
# only the wallet root account is true, and there is a unique db
# constraint enforcing only one account to have wallet = true
chat: false.some,
`type`: ($accountType).some,
storage: string.none,
path: keyPath.some,
publicKey: publicKey.some,
name: walletName.some,
color: "#4360df".some # TODO: pass in colour
)
self.userDb.createAccount(walletAccount)

return WalletAccountResult.ok(walletAccount)
address = privateKey.toAddress
return self.storeWalletAccount(name, address, publicKey, accountType, path)

except Exception as e:
return WalletAccountResult.err e.msg

Expand All @@ -243,9 +234,8 @@ proc addWalletAccount*(self: StatusObject, name, password,
address.get.parseAddress, password, dir)
newIdx = lastDerivedPathIdx + 1
path = fmt"{PATH_WALLET_ROOT}/{newIdx}"
walletAccount = ?self.storeDerivedAccount(loadedAccount.id,
loadedAccount.keyUid, KeyPath path, name, password, dir,
AccountType.Generated)
walletAccount = ?self.storeDerivedAccount(loadedAccount.id, KeyPath path,
name, password, dir, AccountType.Generated)

self.userDb.saveSetting(SettingsCol.LatestDerivedPath, newIdx)

Expand All @@ -268,6 +258,22 @@ proc addWalletPrivateKey*(self: StatusObject, privateKeyHex: string,
except Exception as e:
return WalletAccountResult.err e.msg

proc addWalletSeed*(self: StatusObject, mnemonic: Mnemonic, name, password,
dir, bip39Passphrase: string): WalletAccountResult =

try:
if not self.validatePassword(password, dir):
return WalletAccountResult.err "Invalid password"

let imported = ?self.accountsGenerator.importMnemonic(mnemonic,
bip39Passphrase)

return self.storeDerivedAccount(imported.id, PATH_DEFAULT_WALLET, name,
password, dir, AccountType.Seed)

except Exception as e:
return WalletAccountResult.err e.msg

proc createAccount*(self: StatusObject, mnemonicPhraseLength: int,
bip39Passphrase, password: string, dir: string): PublicAccountResult =

Expand Down Expand Up @@ -342,7 +348,7 @@ proc createSettings*(self: StatusObject, settings: Settings,
proc getPublicAccounts*(self: StatusObject): seq[PublicAccount] =
self.accountsDb.getPublicAccounts()

proc toWalletAccount(account: accounts.Account): WalletAccount =
proc toWalletAccount(account: accounts.Account): WalletAccount {.used.} =
let name = if account.name.isNone: "" else: account.name.get
WalletAccount(address: account.address, name: name)

Expand Down