status-lib/status/statusgo_backend/accounts.nim

431 lines
17 KiB
Nim

import json, os, nimcrypto, uuids, json_serialization, chronicles, strutils
from status_go import multiAccountGenerateAndDeriveAddresses, generateAlias, identicon, saveAccountAndLogin, login, openAccounts, getNodeConfig
import core
import ../utils as utils
from ../wallet/account as walletAccount import WalletAccount
import ../types/[account, fleet, rpc_response]
import accounts/constants
proc getNetworkConfig(currentNetwork: string): JsonNode =
result = constants.DEFAULT_NETWORKS.first("id", currentNetwork)
proc getDefaultNodeConfig*(fleetConfig: FleetConfig, installationId: string): JsonNode =
let networkConfig = getNetworkConfig(constants.DEFAULT_NETWORK_NAME)
let upstreamUrl = networkConfig["config"]["UpstreamConfig"]["URL"]
let fleet = Fleet.PROD
var newDataDir = networkConfig["config"]["DataDir"].getStr
newDataDir.removeSuffix("_rpc")
result = constants.NODE_CONFIG.copy()
result["ClusterConfig"]["Fleet"] = newJString($fleet)
result["ClusterConfig"]["BootNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Bootnodes)
result["ClusterConfig"]["TrustedMailServers"] = %* fleetConfig.getNodes(fleet, FleetNodes.Mailservers)
result["ClusterConfig"]["StaticNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Whisper)
result["ClusterConfig"]["RendezvousNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Rendezvous)
result["NetworkId"] = networkConfig["config"]["NetworkId"]
result["DataDir"] = newDataDir.newJString()
result["UpstreamConfig"]["Enabled"] = networkConfig["config"]["UpstreamConfig"]["Enabled"]
result["UpstreamConfig"]["URL"] = upstreamUrl
result["ShhextConfig"]["InstallationID"] = newJString(installationId)
case fleet:
of Fleet.WakuV2Prod:
result["ClusterConfig"]["RelayNodes"] = %* @["enrtree://ANTL4SLG2COUILKAPE7EF2BYNL2SHSHVCHLRD5J7ZJLN5R3PRJD2Y@prod.waku.nodes.status.im"]
result["ClusterConfig"]["StoreNodes"] = %* @["enrtree://ANTL4SLG2COUILKAPE7EF2BYNL2SHSHVCHLRD5J7ZJLN5R3PRJD2Y@prod.waku.nodes.status.im"]
result["ClusterConfig"]["FilterNodes"] = %* @["enrtree://ANTL4SLG2COUILKAPE7EF2BYNL2SHSHVCHLRD5J7ZJLN5R3PRJD2Y@prod.waku.nodes.status.im"]
result["ClusterConfig"]["LightpushNodes"] = %* @["enrtree://ANTL4SLG2COUILKAPE7EF2BYNL2SHSHVCHLRD5J7ZJLN5R3PRJD2Y@prod.waku.nodes.status.im"]
of Fleet.WakuV2Test:
result["ClusterConfig"]["RelayNodes"] = %* @["enrtree://AOFTICU2XWDULNLZGRMQS4RIZPAZEHYMV4FYHAPW563HNRAOERP7C@test.waku.nodes.status.im"]
result["ClusterConfig"]["StoreNodes"] = %* @["enrtree://AOFTICU2XWDULNLZGRMQS4RIZPAZEHYMV4FYHAPW563HNRAOERP7C@test.waku.nodes.status.im"]
result["ClusterConfig"]["FilterNodes"] = %* @["enrtree://AOFTICU2XWDULNLZGRMQS4RIZPAZEHYMV4FYHAPW563HNRAOERP7C@test.waku.nodes.status.im"]
result["ClusterConfig"]["LightpushNodes"] = %* @["enrtree://AOFTICU2XWDULNLZGRMQS4RIZPAZEHYMV4FYHAPW563HNRAOERP7C@test.waku.nodes.status.im"]
else:
discard
#TODO: in the meantime we're using the go-waku test fleet for rendezvous.
# once we have a prod fleet this code needs to be updated
result["ClusterConfig"]["WakuRendezvousNodes"] = %* fleetConfig.getNodes(Fleet.GoWakuTest, FleetNodes.LibP2P)
# TODO: commented since it's not necessary (we do the connections thru C bindings). Enable it thru an option once status-nodes are able to be configured in desktop
# result["ListenAddr"] = if existsEnv("STATUS_PORT"): newJString("0.0.0.0:" & $getEnv("STATUS_PORT")) else: newJString("0.0.0.0:30305")
proc hashPassword*(password: string): string =
result = "0x" & $keccak_256.digest(password)
proc generateAddresses*(n = 5): seq[GeneratedAccount] =
let multiAccountConfig = %* {
"n": n,
"mnemonicPhraseLength": 12,
"bip39Passphrase": "",
"paths": [PATH_WALLET_ROOT, PATH_EIP_1581, PATH_WHISPER, PATH_DEFAULT_WALLET]
}
let generatedAccounts = $status_go.multiAccountGenerateAndDeriveAddresses($multiAccountConfig)
result = Json.decode(generatedAccounts, seq[GeneratedAccount])
proc getWalletAccounts*(): seq[WalletAccount] =
try:
var response = callPrivateRPC("accounts_getAccounts")
let accounts = parseJson(response)["result"]
var walletAccounts:seq[WalletAccount] = @[]
for account in accounts:
if (account["chat"].to(bool) == false): # Might need a better condition
walletAccounts.add(WalletAccount(
address: $account["address"].getStr,
path: $account["path"].getStr,
walletType: if (account.hasKey("type")): $account["type"].getStr else: "",
# Watch accoutns don't have a public key
publicKey: if (account.hasKey("public-key")): $account["public-key"].getStr else: "",
name: $account["name"].getStr,
iconColor: $account["color"].getStr,
wallet: account["wallet"].getBool,
chat: account["chat"].getBool,
))
result = walletAccounts
except:
let msg = getCurrentExceptionMsg()
error "Failed getting wallet accounts", msg
proc generateAlias*(publicKey: string): string =
result = $status_go.generateAlias(publicKey)
proc generateIdenticon*(publicKey: string): string =
result = $status_go.identicon(publicKey)
proc initNode*(statusGoDir, keystoreDir: string) =
createDir(statusGoDir)
createDir(keystoreDir)
discard $status_go.initKeystore(keystoreDir)
proc parseIdentityImage*(images: JsonNode): IdentityImage =
result = IdentityImage()
if (images.kind != JNull):
for image in images:
if (image["type"].getStr == "thumbnail"):
# TODO check if this can be url or if it's always uri
result.thumbnail = image["uri"].getStr
elif (image["type"].getStr == "large"):
result.large = image["uri"].getStr
proc openAccounts*(STATUSGODIR: string): seq[NodeAccount] =
let strNodeAccounts = status_go.openAccounts(STATUSGODIR).parseJson
# FIXME fix serialization
result = @[]
if (strNodeAccounts.kind != JNull):
for account in strNodeAccounts:
let nodeAccount = NodeAccount(
name: account["name"].getStr,
timestamp: account["timestamp"].getInt,
keyUid: account["key-uid"].getStr,
identicon: account["identicon"].getStr,
keycardPairing: account["keycard-pairing"].getStr
)
if (account{"images"}.kind != JNull):
nodeAccount.identityImage = parseIdentityImage(account["images"])
result.add(nodeAccount)
proc saveAccountAndLogin*(
account: GeneratedAccount,
accountData: string,
password: string,
configJSON: string,
settingsJSON: string): Account =
let hashedPassword = hashPassword(password)
let subaccountData = %* [
{
"public-key": account.derived.defaultWallet.publicKey,
"address": account.derived.defaultWallet.address,
"color": "#4360df",
"wallet": true,
"path": constants.PATH_DEFAULT_WALLET,
"name": "Status account"
},
{
"public-key": account.derived.whisper.publicKey,
"address": account.derived.whisper.address,
"name": account.name,
"identicon": account.identicon,
"path": constants.PATH_WHISPER,
"chat": true
}
]
var savedResult = $status_go.saveAccountAndLogin(accountData, hashedPassword, settingsJSON, configJSON, $subaccountData)
let parsedSavedResult = savedResult.parseJson
let error = parsedSavedResult["error"].getStr
if error == "":
debug "Account saved succesfully"
result = account.toAccount
return
raise newException(StatusGoException, "Error saving account and logging in: " & error)
proc storeDerivedAccounts*(account: GeneratedAccount, password: string, paths: seq[string] = @[PATH_WALLET_ROOT, PATH_EIP_1581, PATH_WHISPER, PATH_DEFAULT_WALLET]): MultiAccounts =
let hashedPassword = hashPassword(password)
let multiAccount = %* {
"accountID": account.id,
"paths": paths,
"password": hashedPassword
}
let response = $status_go.multiAccountStoreDerivedAccounts($multiAccount);
try:
result = Json.decode($response, MultiAccounts)
except:
let err = Json.decode($response, StatusGoError)
raise newException(StatusGoException, "Error storing multiaccount derived accounts: " & err.error)
proc getAccountData*(account: GeneratedAccount): JsonNode =
result = %* {
"name": account.name,
"address": account.address,
"identicon": account.identicon,
"key-uid": account.keyUid,
"keycard-pairing": nil
}
proc getAccountSettings*(account: GeneratedAccount, defaultNetworks: JsonNode, installationId: string): JsonNode =
result = %* {
"key-uid": account.keyUid,
"mnemonic": account.mnemonic,
"public-key": account.derived.whisper.publicKey,
"name": account.name,
"address": account.address,
"eip1581-address": account.derived.eip1581.address,
"dapps-address": account.derived.defaultWallet.address,
"wallet-root-address": account.derived.walletRoot.address,
"preview-privacy?": true,
"signing-phrase": generateSigningPhrase(3),
"log-level": "INFO",
"latest-derived-path": 0,
"networks/networks": defaultNetworks,
"currency": "usd",
"identicon": account.identicon,
"waku-enabled": true,
"wallet/visible-tokens": {
"mainnet": ["SNT"]
},
"appearance": 0,
"networks/current-network": constants.DEFAULT_NETWORK_NAME,
"installation-id": installationId
}
proc setupAccount*(fleetConfig: FleetConfig, account: GeneratedAccount, password: string): Account =
try:
let storeDerivedResult = storeDerivedAccounts(account, password)
let accountData = getAccountData(account)
let installationId = $genUUID()
var settingsJSON = getAccountSettings(account, constants.DEFAULT_NETWORKS, installationId)
var nodeConfig = getDefaultNodeConfig(fleetConfig, installationId)
result = saveAccountAndLogin(account, $accountData, password, $nodeConfig, $settingsJSON)
except StatusGoException as e:
raise newException(StatusGoException, "Error setting up account: " & e.msg)
finally:
# TODO this is needed for now for the retrieving of past messages. We'll either move or remove it later
let peer = "enode://44160e22e8b42bd32a06c1532165fa9e096eebedd7fa6d6e5f8bbef0440bc4a4591fe3651be68193a7ec029021cdb496cfe1d7f9f1dc69eb99226e6f39a7a5d4@35.225.221.245:443"
discard status_go.addPeer(peer)
proc login*(nodeAccount: NodeAccount, password: string): NodeAccount =
let hashedPassword = hashPassword(password)
let account = nodeAccount.toAccount
let loginResult = $status_go.login($toJson(account), hashedPassword)
let error = parseJson(loginResult)["error"].getStr
if error == "":
debug "Login requested", user=nodeAccount.name
result = nodeAccount
return
raise newException(StatusGoException, "Error logging in: " & error)
proc loadAccount*(address: string, password: string): GeneratedAccount =
let hashedPassword = hashPassword(password)
let inputJson = %* {
"address": address,
"password": hashedPassword
}
let loadResult = $status_go.multiAccountLoadAccount($inputJson)
let parsedLoadResult = loadResult.parseJson
let error = parsedLoadResult{"error"}.getStr
if error == "":
debug "Account loaded succesfully"
result = Json.decode(loadResult, GeneratedAccount)
return
raise newException(StatusGoException, "Error loading wallet account: " & error)
proc verifyAccountPassword*(address: string, password: string, keystoreDir: string): bool =
let hashedPassword = hashPassword(password)
let verifyResult = $status_go.verifyAccountPassword(keystoreDir, address, hashedPassword)
let error = parseJson(verifyResult)["error"].getStr
if error == "":
return true
return false
proc changeDatabasePassword*(keyUID: string, password: string, newPassword: string): bool =
let hashedPassword = hashPassword(password)
let hashedNewPassword = hashPassword(newPassword)
let changeResult = $status_go.changeDatabasePassword(keyUID, hashedPassword, hashedNewPassword)
let error = parseJson(changeResult)["error"].getStr
return error == ""
proc multiAccountImportMnemonic*(mnemonic: string): GeneratedAccount =
let mnemonicJson = %* {
"mnemonicPhrase": mnemonic,
"Bip39Passphrase": ""
}
# status_go.multiAccountImportMnemonic never results in an error given ANY input
let importResult = $status_go.multiAccountImportMnemonic($mnemonicJson)
result = Json.decode(importResult, GeneratedAccount)
proc MultiAccountImportPrivateKey*(privateKey: string): GeneratedAccount =
let privateKeyJson = %* {
"privateKey": privateKey
}
# status_go.MultiAccountImportPrivateKey never results in an error given ANY input
try:
let importResult = $status_go.multiAccountImportPrivateKey($privateKeyJson)
result = Json.decode(importResult, GeneratedAccount)
except Exception as e:
error "Error getting account from private key", msg=e.msg
proc storeDerivedWallet*(account: GeneratedAccount, password: string, walletIndex: int, accountType: string): string =
let hashedPassword = hashPassword(password)
let derivationPath = (if accountType == constants.GENERATED: "m/" else: "m/44'/60'/0'/0/") & $walletIndex
let multiAccount = %* {
"accountID": account.id,
"paths": [derivationPath],
"password": hashedPassword
}
let response = parseJson($status_go.multiAccountStoreDerivedAccounts($multiAccount));
let error = response{"error"}.getStr
if error == "":
debug "Wallet stored succesfully"
return "m/44'/60'/0'/0/" & $walletIndex
raise newException(StatusGoException, error)
proc storePrivateKeyAccount*(account: GeneratedAccount, password: string) =
let hashedPassword = hashPassword(password)
let response = parseJson($status_go.multiAccountStoreAccount($(%*{"accountID": account.id, "password": hashedPassword})));
let error = response{"error"}.getStr
if error == "":
debug "Wallet stored succesfully"
return
raise newException(StatusGoException, error)
proc saveAccount*(account: GeneratedAccount, password: string, color: string, accountType: string, isADerivedAccount = true, walletIndex: int = 0 ): DerivedAccount =
try:
var derivationPath = "m/44'/60'/0'/0/0"
if (isADerivedAccount):
# Only store derived accounts. Private key accounts are not multiaccounts
derivationPath = storeDerivedWallet(account, password, walletIndex, accountType)
elif accountType == constants.KEY:
storePrivateKeyAccount(account, password)
var address = account.derived.defaultWallet.address
var publicKey = account.derived.defaultWallet.publicKey
if (address == ""):
address = account.address
publicKey = account.publicKey
echo callPrivateRPC("accounts_saveAccounts", %* [
[{
"color": color,
"name": account.name,
"address": address,
"public-key": publicKey,
"type": accountType,
"path": derivationPath
}]
])
result = DerivedAccount(address: address, publicKey: publicKey, derivationPath: derivationPath)
except:
error "Error storing the new account. Bad password?"
raise
proc changeAccount*(name, address, publicKey, walletType, iconColor: string): string =
try:
let response = callPrivateRPC("accounts_saveAccounts", %* [
[{
"color": iconColor,
"name": name,
"address": address,
"public-key": publicKey,
"type": walletType,
"path": "m/44'/60'/0'/0/1" # <--- TODO: fix this. Derivation path is not supposed to change
}]
])
utils.handleRPCErrors(response)
return ""
except Exception as e:
error "Error saving the account", msg=e.msg
result = e.msg
proc deleteAccount*(address: string): string =
try:
let response = callPrivateRPC("accounts_deleteAccount", %* [address])
utils.handleRPCErrors(response)
return ""
except Exception as e:
error "Error removing the account", msg=e.msg
result = e.msg
proc deriveWallet*(accountId: string, walletIndex: int): DerivedAccount =
let path = "m/" & $walletIndex
let deriveJson = %* {
"accountID": accountId,
"paths": [path]
}
let deriveResult = parseJson($status_go.multiAccountDeriveAddresses($deriveJson))
result = DerivedAccount(
address: deriveResult[path]["address"].getStr,
publicKey: deriveResult[path]["publicKey"].getStr)
proc deriveAccounts*(accountId: string): MultiAccounts =
let deriveJson = %* {
"accountID": accountId,
"paths": [PATH_WALLET_ROOT, PATH_EIP_1581, PATH_WHISPER, PATH_DEFAULT_WALLET]
}
let deriveResult = $status_go.multiAccountDeriveAddresses($deriveJson)
result = Json.decode(deriveResult, MultiAccounts)
proc logout*(): StatusGoError =
result = Json.decode($status_go.logout(), StatusGoError)
proc storeIdentityImage*(keyUID: string, imagePath: string, aX, aY, bX, bY: int): IdentityImage =
let response = callPrivateRPC("multiaccounts_storeIdentityImage", %* [keyUID, imagePath, aX, aY, bX, bY]).parseJson
result = parseIdentityImage(response{"result"})
proc getIdentityImage*(keyUID: string): IdentityImage =
try:
let response = callPrivateRPC("multiaccounts_getIdentityImages", %* [keyUID]).parseJson
result = parseIdentityImage(response{"result"})
except Exception as e:
error "Error getting identity image", msg=e.msg
proc deleteIdentityImage*(keyUID: string): string =
try:
let response = callPrivateRPC("multiaccounts_deleteIdentityImage", %* [keyUID]).parseJson
result = ""
except Exception as e:
error "Error getting identity image", msg=e.msg
result = e.msg