feat: user db accounts CRUD

Closes: #204
Closes: #205

feat: insert accounts in user db during create account
feat: insert accounts in user db during import mnemonic

1. This only handles `createAccount`. No other parts of CRUD are handled yet.
This commit is contained in:
Eric Mastro 2021-07-12 17:09:46 +10:00 committed by Michael Bradley
parent 3c2834d4d0
commit 68c11caad8
18 changed files with 375 additions and 83 deletions

View File

@ -5,9 +5,9 @@ import # chat libs
../config, ../task_runner
import # nim-status libs
../../../nim_status/accounts
../../../nim_status/accounts/public_accounts
export accounts, config, task_runner, sets, times
export config, public_accounts, sets, task_runner, times
logScope:
topics = "chat client"

View File

@ -1,6 +1,3 @@
import # nim-status libs
../../../nim_status/accounts
import # chat libs
./common

View File

@ -2,7 +2,7 @@ import # std libs
std/[os, strutils, sets, sugar]
import # nim-status libs
../../nim_status/[accounts, client, database],
../../nim_status/[client, database],
../../nim_status/extkeys/[paths, types]
import # chat libs
@ -193,7 +193,7 @@ proc leaveTopic*(topic: string) {.task(kind=no_rts, stoppable=false).} =
proc listAccounts*() {.task(kind=no_rts, stoppable=false).} =
let
accounts = status.getAccounts()
accounts = status.getPublicAccounts()
event = ListAccountsResult(accounts: accounts, timestamp: getTime().toUnix())
eventEnc = event.encode
task = taskArg.taskName
@ -207,7 +207,7 @@ proc login*(account: int, password: string) {.task(kind=no_rts, stoppable=false)
if statusState != StatusState.loggedout: return
statusState = StatusState.loggingin
let allAccounts = status.getAccounts()
let allAccounts = status.getPublicAccounts()
var
event: LoginResult

View File

@ -1,9 +1,6 @@
import # std libs
std/[strformat, strutils]
import # nim-status libs
../../../nim_status/accounts
import # chat libs
./parser

View File

@ -0,0 +1,93 @@
import # nim libs
json, options, strformat, times
import # vendor libs
chronos, json_serialization, json_serialization/[reader, writer, lexer],
secp256k1, sqlcipher, web3/conversions as web3_conversions, web3/ethtypes
import # nim-status libs
../conversions, ../database, ../extkeys/types, ../settings
type
Account* {.dbTableName("accounts").} = object
address* {.serializedFieldName("address"), dbColumnName("address").}: Address
wallet* {.serializedFieldName("wallet"), dbColumnName("wallet").}: Option[bool]
chat* {.serializedFieldName("chat"), dbColumnName("chat").}: Option[bool]
`type`* {.serializedFieldName("type"), dbColumnName("type").}: Option[string]
storage* {.serializedFieldName("storage"), dbColumnName("storage").}: Option[string]
path* {.serializedFieldName("path"), dbColumnName("path").}: Option[KeyPath]
publicKey* {.serializedFieldName("pubkey"), dbColumnName("pubkey").}: Option[SkPublicKey]
name* {.serializedFieldName("name"), dbColumnName("name").}: Option[string]
color* {.serializedFieldName("color"), dbColumnName("color").}: Option[string]
createdAt* {.serializedFieldName("created_at"), dbColumnName("created_at").}: DateTime
updatedAt* {.serializedFieldName("updated_at"), dbColumnName("updated_at").}: DateTime
AccountType* {.pure.} = enum
Generated = "generated",
Key = "key",
Seed = "seed",
Watch = "watch"
proc createAccount*(db: DbConn, account: Account) =
var tblAccounts: Account
let query = fmt"""
INSERT OR REPLACE INTO {tblAccounts.tableName} (
{tblAccounts.address.columnName},
{tblAccounts.wallet.columnName},
{tblAccounts.chat.columnName},
{tblAccounts.`type`.columnName},
{tblAccounts.storage.columnName},
{tblAccounts.path.columnName},
{tblAccounts.publicKey.columnName},
{tblAccounts.name.columnName},
{tblAccounts.color.columnName},
{tblAccounts.createdAt.columnName},
{tblAccounts.updatedAt.columnName})
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
let now = now()
db.exec(query, account.address, account.wallet, account.chat,
account.`type`, account.storage, account.path, account.publicKey,
account.name, account.color, now, now)
proc deleteAccount*(db: DbConn, address: Address) =
var tblAccounts: Account
let query = fmt"""DELETE FROM {tblAccounts.tableName}
WHERE {tblAccounts.address.columnName} = ?"""
db.exec(query, address)
proc getAccounts*(db: DbConn): seq[Account] =
var tblAccounts: Account
let query = fmt"""SELECT {tblAccounts.address.columnName},
{tblAccounts.wallet.columnName},
{tblAccounts.chat.columnName},
{tblAccounts.`type`.columnName},
{tblAccounts.storage.columnName},
{tblAccounts.path.columnName},
{tblAccounts.publicKey.columnName},
{tblAccounts.name.columnName},
{tblAccounts.color.columnName},
{tblAccounts.createdAt.columnName},
{tblAccounts.updatedAt.columnName}
FROM {tblAccounts.tableName}
ORDER BY {tblAccounts.createdAt.columnName} ASC"""
result = db.all(Account, query)
proc updateAccount*(db: DbConn, account: Account) =
var tblAccounts: Account
let query = fmt"""UPDATE {tblAccounts.tableName}
SET {tblAccounts.wallet.columnName} = ?,
{tblAccounts.chat.columnName} = ?,
{tblAccounts.`type`.columnName} = ?,
{tblAccounts.storage.columnName} = ?,
{tblAccounts.path.columnName} = ?,
{tblAccounts.publicKey.columnName} = ?,
{tblAccounts.name.columnName} = ?,
{tblAccounts.color.columnName} = ?,
{tblAccounts.updatedAt.columnName} = ?
WHERE {tblAccounts.address.columnName}= ?"""
db.exec(query, account.wallet, account.chat, account.`type`, account.storage,
account.path, account.publicKey, account.name, account.color, now(),
account.address)

View File

@ -6,7 +6,7 @@ import # vendor libs
stew/results
import # nim-status libs
./account, ../../accounts, ../../extkeys/[hdkey, mnemonic, types], ./utils
./account, ../../extkeys/[hdkey, mnemonic, types], ../public_accounts, ./utils
export utils
@ -163,7 +163,7 @@ proc loadAccount*(self: Generator, address: string, password: string,
$privateKeyResult.error
# TODO: Add ValidateKeystoreExtendedKey
# https://github.com/status-im/status-go/blob/e0eb96a992fea9d52d16ae9413b1198827360278/account/generator/generator.go#L213-L215
# https://github.com/status-im/status-go/blob/e0eb96a992fea9d52d16ae9413b1198827360278/accounts/generator/generator.go#L213-L215
let
secretKey = SkSecretKey(privateKeyResult.get)

View File

@ -12,4 +12,4 @@ proc mnemonicPhraseLengthToEntropyStrength*(length: int): EntropyStrength =
return EntropyStrength(bitsLength - checksumLength)
# TODO: Add ValidateKeystoreExtendedKey
# https://github.com/status-im/status-go/blob/287e5cdf79fc06d5cf5c9d3bd3a99a1df1e3cd10/account/generator/utils.go#L24-L34
# https://github.com/status-im/status-go/blob/287e5cdf79fc06d5cf5c9d3bd3a99a1df1e3cd10/accounts/generator/utils.go#L24-L34

View File

@ -6,7 +6,7 @@ import # vendor libs
sqlcipher
import # nim-status libs
./conversions, ./settings, ./database
../conversions, ../settings, ../database
type
PublicAccount* {.dbTableName("accounts").} = object
@ -24,7 +24,7 @@ proc deleteAccount*(db: DbConn, keyUid: string) =
db.exec(query, keyUid)
proc getAccounts*(db: DbConn): seq[PublicAccount] =
proc getPublicAccounts*(db: DbConn): seq[PublicAccount] =
var tblAccounts: PublicAccount
let query = fmt"""SELECT {tblAccounts.creationTimestamp.columnName},
{tblAccounts.name.columnName},

View File

@ -2,17 +2,20 @@ import # nim libs
std/[os, json, times]
import # vendor libs
confutils, eth/keyfile/uuid, sqlcipher
confutils, eth/keyfile/uuid, secp256k1, sqlcipher
import # nim-status libs
./account/generator/generator, ./accounts, ./alias, ./chats, ./database,
./extkeys/types, ./identicon, ./settings
./accounts/[accounts, public_accounts],
./accounts/generator/generator,
./accounts/generator/account as generator_account, ./alias, ./chats,
./conversions, ./database, ./extkeys/types, ./identicon, ./settings
type StatusObject* = ref object
accountsGenerator*: Generator
accountsDb: DbConn
dataDir*: string
userDb: DbConn
type
StatusObject* = ref object
accountsGenerator*: Generator
accountsDb: DbConn
dataDir*: string
userDb: DbConn
proc new*(T: type StatusObject, dataDir: string,
accountsDbFileName: string = "accounts.sql"): T =
@ -20,8 +23,8 @@ proc new*(T: type StatusObject, dataDir: string,
T(accountsDb: initializeDB(dataDir / accountsDbFileName),
dataDir: dataDir, accountsGenerator: Generator.new())
proc getAccounts*(self: StatusObject): seq[PublicAccount] =
self.accountsDb.getAccounts()
proc getPublicAccounts*(self: StatusObject): seq[PublicAccount] =
self.accountsDb.getPublicAccounts()
proc saveAccount*(self: StatusObject, account: PublicAccount) =
self.accountsDb.saveAccount(account)
@ -42,6 +45,61 @@ proc logout*(self: StatusObject) =
self.userDb.close()
self.userDb = nil
proc storeDerivedAccountsInDb(self: StatusObject, id: UUID, keyUid: string,
paths: seq[KeyPath], password, dir: string): PublicAccountResult =
let
accountInfos = ?self.accountsGenerator.storeDerivedAccounts(id, paths,
password, dir)
whisperAcct = accountInfos[2]
pubAccount = PublicAccount(
creationTimestamp: getTime().toUnix().int,
name: whisperAcct.publicKey.generateAlias(),
identicon: whisperAcct.publicKey.identicon(),
keycardPairing: "",
keyUid: keyUid # whisper key-uid
)
self.accountsDb.saveAccount(pubAccount)
let
defaultWalletAccountDerived = accountInfos[3]
defaultWalletPubKeyResult = SkPublicKey.fromHex(defaultWalletAccountDerived.publicKey)
whisperAccountPubKeyResult = SkPublicKey.fromHex(whisperAcct.publicKey)
if defaultWalletPubKeyResult.isErr:
return PublicAccountResult.err $defaultWalletPubKeyResult.error
if whisperAccountPubKeyResult.isErr:
return PublicAccountResult.err $whisperAccountPubKeyResult.error
let
defaultWalletAccount = accounts.Account(
address: defaultWalletAccountDerived.address.parseAddress,
wallet: true.some,
chat: false.some,
`type`: some($AccountType.Seed),
storage: string.none,
path: paths[3].some,
publicKey: defaultWalletPubKeyResult.get.some,
name: "Status account".some,
color: "#4360df".some
)
whisperAccount = accounts.Account(
address: whisperAcct.address.parseAddress,
wallet: false.some,
chat: true.some,
`type`: some($AccountType.Seed),
storage: string.none,
path: paths[2].some,
publicKey: whisperAccountPubKeyResult.get.some,
name: pubAccount.name.some,
color: "#4360df".some
)
self.userDb.createAccount(defaultWalletAccount)
self.userDb.createAccount(whisperAccount)
PublicAccountResult.ok(pubAccount)
proc createAccount*(self: StatusObject,
mnemonicPhraseLength, n: int, bip39Passphrase, password: string,
paths: seq[KeyPath], dir: string): PublicAccountResult =
@ -51,50 +109,27 @@ proc createAccount*(self: StatusObject,
mnemonicPhraseLength, n, bip39Passphrase, paths)
gndAccount = gndAccounts[0]
let
accountInfos = ?self.accountsGenerator.storeDerivedAccounts(gndAccount.id,
paths, password, dir)
whisperAcct = accountInfos[2]
account = PublicAccount(
creationTimestamp: getTime().toUnix().int,
name: whisperAcct.publicKey.generateAlias(),
identicon: whisperAcct.publicKey.identicon(),
keycardPairing: "",
keyUid: gndAccount.keyUid # whisper key-uid
)
self.accountsDb.saveAccount(account)
# create the user db on disk by initializing it then immediately closing it
self.userDb = initializeDB(self.dataDir / account.keyUid & ".db", password)
self.userDb = initializeDB(self.dataDir / gndAccount.keyUid & ".db", password)
let pubAccount = ?self.storeDerivedAccountsInDb(gndAccount.id, gndAccount.keyUid, paths,
password, dir)
self.userDb.close()
PublicAccountResult.ok(account)
PublicAccountResult.ok(pubAccount)
proc importMnemonic*(self: StatusObject, mnemonic: Mnemonic,
bip39Passphrase, password: string, paths: seq[KeyPath],
dir: string): PublicAccountResult =
let
imported = ?self.accountsGenerator.importMnemonic(mnemonic, bip39Passphrase)
accountInfos = ?self.accountsGenerator.storeDerivedAccounts(imported.id,
paths, password, dir)
whisperAcct = accountInfos[2]
account = PublicAccount(
creationTimestamp: getTime().toUnix().int,
name: whisperAcct.publicKey.generateAlias(),
identicon: whisperAcct.publicKey.identicon(),
keycardPairing: "",
keyUid: imported.keyUid # whisper key-uid
)
let imported = ?self.accountsGenerator.importMnemonic(mnemonic, bip39Passphrase)
self.accountsDb.saveAccount(account)
# create the user db by initializing it then immediately closing it
self.userDb = initializeDB(self.dataDir / account.keyUid & ".db", password)
# create the user db by initializing it then closing it
self.userDb = initializeDB(self.dataDir / imported.keyUid & ".db", password)
let pubAccount = ?self.storeDerivedAccountsInDb(imported.id, imported.keyUid, paths, password,
dir)
self.userDb.close()
PublicAccountResult.ok(account)
PublicAccountResult.ok(pubAccount)
proc loadAccount*(self: StatusObject, address: string, password: string,
dir: string = ""): LoadAccountResult =

View File

@ -1,12 +1,12 @@
import # std libs
std/[json, options, strutils]
std/[json, options, strutils, times]
import # vendor libs
json_serialization, json_serialization/std/options as json_options, sqlcipher,
web3/ethtypes, stew/byteutils
chronicles, json_serialization, json_serialization/std/options as json_options,
secp256k1, stew/byteutils, sqlcipher, web3/ethtypes
import # nim_status libs
./settings/types
./extkeys/types as key_types, ./settings/types
from ./tx_history/types as tx_history_types import TxType
@ -14,26 +14,55 @@ from ./tx_history/types as tx_history_types import TxType
# json_serialization/std/options imported
export json_options
proc parseAddress*(strAddress: string): Address =
fromHex(Address, strAddress)
const dtFormat = "yyyy-MM-dd HH:mm:ss fffffffff"
proc parseAddress*(address: string): Address =
Address.fromHex(address)
proc toDbValue*[T: Address](val: T): DbValue =
DbValue(kind: sqliteText, strVal: $val)
proc toDbValue*(val: DateTime): DbValue =
DbValue(kind: sqliteText, strVal: val.format(dtFormat))
proc toDbValue*(val: JsonNode): DbValue =
DbValue(kind: sqliteText, strVal: $val)
proc toDbValue*(val: KeyPath): DbValue =
DbValue(kind: sqliteText, strVal: val.string)
proc toDbValue*[T: seq[auto]](val: T): DbValue =
DbValue(kind: sqliteText, strVal: Json.encode(val))
proc toDbValue*(val: SkPublicKey): DbValue =
DbValue(kind: sqliteBlob, blobVal: ($val).hexToSeqByte)
proc toDbValue*(val: TxType): DbValue =
DbValue(kind: sqliteText, strVal: $val)
proc fromDbValue*(val: DbValue, T: typedesc[JsonNode]): JsonNode = val.strVal.parseJson
proc fromDbValue*(val: DbValue, T: typedesc[Address]): Address =
val.strVal.parseAddress
proc fromDbValue*(val: DbValue, T: typedesc[TxType]): TxType = parseEnum[TxType](val.strVal)
proc fromDbValue*(val: DbValue, T: typedesc[DateTime]): DateTime =
val.strVal.parse(dtFormat)
proc fromDbValue*(val: DbValue, T: typedesc[Address]): Address = val.strVal.parseAddress
proc fromDbValue*(val: DbValue, T: typedesc[JsonNode]): JsonNode =
val.strVal.parseJson
proc fromDbValue*(val: DbValue, T: typedesc[KeyPath]): KeyPath =
KeyPath val.strVal
proc fromDbValue*(val: DbValue, T: typedesc[SkPublicKey]): SkPublicKey =
let pubKeyResult = SkPublicKey.fromRaw(val.blobVal)
if pubKeyResult.isErr:
# TODO: implement chronicles in nim-status (in the tests)
echo "error converting db value to public key, error: " &
$(pubKeyResult.error)
return
pubKeyResult.get
proc fromDbValue*(val: DbValue, T: typedesc[TxType]): TxType =
parseEnum[TxType](val.strVal)
proc fromDbValue*[T: seq[auto]](val: DbValue, _: typedesc[T]): T =
Json.decode(val.strVal, T, allowUnknownFields = true)

View File

@ -3,7 +3,6 @@ import callrpc, conversions, os
import web3, json, strutils, strformat, sequtils
import json_rpc/client
import nimcrypto
import accounts
import sets
import tables
import rlocks

141
test/accounts/accounts.nim Normal file
View File

@ -0,0 +1,141 @@
import # nim libs
json, options, os, times, unittest
import # vendor libs
chronos, json_serialization, secp256k1, sqlcipher, web3/ethtypes
import # nim-status libs
../../nim_status/[database, conversions],
../../nim_status/accounts/accounts, ../../nim_status/extkeys/types,
../test_helpers
procSuite "accounts":
var account = Account(
address: "0xdeadbeefdeadbeefdeadbeefdeadbeef11111111".parseAddress,
wallet: true.some,
chat: false.some,
`type`: "type".some,
storage: "storage".some,
path: KeyPath("m/43'/60'/1581'/0'/0").some,
publicKey: some(SkPublicKey.fromHex("0x04986dee3b8afe24cb8ccb2ac23dac3f8c43d22850d14b809b26d6b8aa5a1f47784152cd2c7d9edd0ab20392a837464b5a750b2a7f3f06e6a5756b5211b6a6ed05").get),
name: "name".some,
color: "#4360df".some
)
asyncTest "createAccount":
let
password = "qwerty"
path = currentSourcePath.parentDir() & "/build/my.db"
removeFile(path)
let db = initializeDB(path, password)
db.createAccount(account)
# check that the values saved correctly
let
accountList = db.getAccounts()
accountFromDb = accountList[0]
check:
accountList.len == 1
accountFromDb.address == account.address
accountFromDb.wallet.get == account.wallet.get
accountFromDb.chat.get == account.chat.get
accountFromDb.`type`.get == account.`type`.get
accountFromDb.storage.get == account.storage.get
accountFromDb.path.get.string == account.path.get.string
accountFromDb.publicKey.get == account.publicKey.get
accountFromDb.name.get == account.name.get
accountFromDb.color.get == account.color.get
accountFromDb.createdAt == accountFromDb.updatedAt
db.close()
removeFile(path)
asyncTest "updateAccount":
let
password = "qwerty"
path = currentSourcePath.parentDir() & "/build/my.db"
removeFile(path)
let db = initializeDB(path, password)
db.createAccount(account)
# check that the values saved correctly
var
accountList = db.getAccounts()
accountFromDb = accountList[0]
# change values, then update
let
address_updated = "0xdeadbeefdeadbeefdeadbeefdeadbeef11111111".parseAddress
wallet_updated = false.some
chat_updated = true.some
type_updated = "type_changed".some
storage_updated = "storage_changed".some
path_updated = KeyPath("m/44'/0'/0'/0/0").some
publicKey_updated = some(SkPublicKey.fromHex("0x03ddb90a4f67a81adf534bc19ed06d1546a3cad16a3b2995e18e3d7af823fe5c9a").get)
name_updated = "name_updated".some
color_updated = "#1360df".some
accountFromDb.address = address_updated
accountFromDb.wallet = wallet_updated
accountFromDb.chat = chat_updated
accountFromDb.`type` = type_updated
accountFromDb.storage = storage_updated
accountFromDb.path = path_updated
accountFromDb.publicKey = publicKey_updated
accountFromDb.name = name_updated
accountFromDb.color = color_updated
db.updateAccount(accountFromDb)
accountList = db.getAccounts()
accountFromDb = accountList[0]
check:
accountList.len == 1
accountFromDb.address == address_updated
accountFromDb.wallet.get == wallet_updated.get
accountFromDb.chat.get == chat_updated.get
accountFromDb.`type`.get == type_updated.get
accountFromDb.storage.get == storage_updated.get
accountFromDb.path.get.string == path_updated.get.string
accountFromDb.publicKey.get == publicKey_updated.get
accountFromDb.name.get == name_updated.get
accountFromDb.color.get == color_updated.get
accountFromDb.createdAt != accountFromDb.updatedAt
db.close()
removeFile(path)
asyncTest "deleteAccount":
let
password = "qwerty"
path = currentSourcePath.parentDir() & "/build/my.db"
removeFile(path)
let db = initializeDB(path, password)
db.createAccount(account)
# check that the values saved correctly
var
accountList = db.getAccounts()
accountFromDb = accountList[0]
check:
accountList.len == 1
db.deleteAccount(accountFromDb.address)
accountList = db.getAccounts()
check:
accountList.len == 0
db.close()
removeFile(path)

View File

@ -5,7 +5,7 @@ import # vednor libs
chronos, eth/keys, eth/keyfile/uuid
import # nim-status libs
../../../nim_status/account/generator/generator,
../../../nim_status/accounts/generator/generator,
../../../nim_status/extkeys/types, ../../test_helpers

View File

@ -5,10 +5,10 @@ import # vendor libs
chronos, json_serialization, sqlcipher, web3/conversions as web3_conversions
import # nim-status libs
../nim_status/[accounts, database, conversions],
./test_helpers
../../nim_status/[database, conversions],
../../nim_status/accounts/public_accounts, ../test_helpers
procSuite "accounts":
procSuite "public accounts":
asyncTest "saveAccount, updateAccountTimestamp, deleteAccount":
let path = currentSourcePath.parentDir() & "/build/my.db"
removeFile(path)
@ -27,7 +27,7 @@ procSuite "accounts":
db.saveAccount(account)
# check that the values saved correctly
var accountList = db.getAccounts()
var accountList = db.getPublicAccounts()
check:
accountList[0].creationTimestamp == timestamp1
accountList[0].name == account.name
@ -44,7 +44,7 @@ procSuite "accounts":
account.keycardPairing = account.keycardPairing & "_updated"
account.loginTimestamp = timestamp2.some
db.updateAccount(account)
accountList = db.getAccounts()
accountList = db.getPublicAccounts()
check:
accountList.len == 1
@ -58,7 +58,7 @@ procSuite "accounts":
# check that we only update timestamp with `updateAccountTimestamp`
let newTimestamp = 1
db.updateAccountTimestamp(newTimestamp, account.keyUid)
accountList = db.getAccounts()
accountList = db.getPublicAccounts()
check:
accountList.len == 1
@ -71,7 +71,7 @@ procSuite "accounts":
# check that we can delete accounts
db.deleteAccount(account.keyUid)
accountList = db.getAccounts()
accountList = db.getPublicAccounts()
check:
accountList.len == 0

View File

@ -6,8 +6,8 @@ import # vendor libs
web3/conversions as web3_conversions
import # nim-status libs
../nim_status/[accounts, client, conversions, database, settings],
./test_helpers
../nim_status/[client, conversions, database, settings],
../nim_status/accounts/public_accounts, ./test_helpers
procSuite "client":
asyncTest "client":
@ -26,7 +26,7 @@ procSuite "client":
statusObj.saveAccount(account)
statusObj.updateAccountTimestamp(1, "0x1234")
let accounts = statusObj.getAccounts()
let accounts = statusObj.getPublicAccounts()
check:
statusObj.dataDir == dataDir
accounts[0].keyUid == "0x1234"

View File

@ -5,7 +5,7 @@ import # vednor libs
chronos, eth/[keys, p2p], stew/byteutils
import # nim-status libs
../nim_status/account/generator/generator, ../nim_status/extkeys/paths,
../nim_status/accounts/generator/generator, ../nim_status/extkeys/paths,
./test_helpers

View File

@ -4,8 +4,9 @@ import
# been removed from this repo
# ./account,
./account/generator/generator,
./accounts,
./accounts/generator/generator,
./accounts/accounts,
./accounts/public_accounts,
./bip32,
./callrpc,
./chats,