feat: integrate nim-waku within the nim-status library

Previous usage of nim-waku code in the example client was implemented in the
example client itself. Move that code and logic inside the library so that
nim-waku is an integral part of nim-status.

Closes #286.
Closes #272.
Closes #258.
Closes #241.
This commit is contained in:
Michael Bradley, Jr 2021-08-02 16:22:06 -05:00 committed by Michael Bradley
parent 18d4c35f85
commit 0850ab20ef
42 changed files with 1950 additions and 1413 deletions

View File

@ -1,9 +1,12 @@
## client.nim is an example program demonstrating usage of nim-status,
## nim-waku, nim-task-runner, and nim-ncurses
## nim-task-runner, and nim-ncurses
when not(compileOption("threads")):
{.fatal: "Please compile this program with the --threads:on option!".}
import # std libs
std/random
import # client modules
./client/tui
@ -22,6 +25,10 @@ proc main() {.async.} =
notice "program exited"
when isMainModule:
# initialize default random number generator, only needs to be called once:
# https://nim-lang.org/docs/random.html#randomize
randomize()
# client program will handle all control characters with ncurses in raw mode
proc nop() {.noconv.} = discard
setControlCHook(nop)

View File

@ -1,13 +1,14 @@
import # std libs
std/sugar
import # vendor libs
eth/common as eth_common
import # client modules
./client/tasks
./client/[common, events, tasks]
import # vendor libs
eth/common
export # modules
events
export tasks
export # symbols
Client, clientEvents
logScope:
topics = "client"
@ -18,19 +19,21 @@ logScope:
# `type Client` is defined in ./common to avoid circular dependency
const status = "status"
proc new*(T: type Client, clientConfig: ClientConfig): T =
let statusArg = StatusArg(clientConfig: clientConfig)
var taskRunner = TaskRunner.new()
var
statusArg = StatusArg(clientConfig: clientConfig)
taskRunner = TaskRunner.new()
taskRunner.createWorker(thread, status, statusContext, statusArg)
statusArg.chanSendToHost =
taskRunner.workers[status].worker.chanRecvFromWorker
var topics: OrderedSet[string]
let topicsStr = clientConfig.contentTopics.strip()
if topicsStr != "":
topics = topicsStr.split(" ").map(handleTopic).filter(t => t != "")
.toOrderedSet()
T(clientConfig: clientConfig, events: newEventChannel(), running: false,
taskRunner: taskRunner)
T(clientConfig: clientConfig, events: newEventChannel(), loggedin: false,
online: false, running: false, taskRunner: taskRunner, topics: topics)
proc getTopics(self: Client): Future[seq[ContentTopic]] {.async, gcsafe.}
proc start*(self: Client) {.async.} =
debug "client starting"
@ -44,16 +47,29 @@ proc start*(self: Client) {.async.} =
debug "client started"
asyncSpawn self.listen()
self.topics = toOrderedSet(await self.getTopics())
if self.topics.len > 0: self.currentTopic = self.topics.toSeq[^1]
proc stopContext(self: Client): Future[void] {.async, gcsafe.}
proc stop*(self: Client) {.async.} =
debug "client stopping"
self.running = false
await self.stopContext()
await self.taskRunner.stop()
self.events.close()
debug "client stopped"
# task invocation procs --------------------------------------------------------
proc addCustomToken*(self: Client, address: Address, name, symbol,
color: string, decimals: uint) {.async.} =
asyncSpawn addCustomToken(self.taskRunner, status, address, name, symbol,
color, decimals)
proc addWalletAccount*(self: Client, name, password: string) {.async.} =
asyncSpawn addWalletAccount(self.taskRunner, status, name, password)
@ -72,8 +88,17 @@ proc addWalletSeed*(self: Client, name, mnemonic, password,
proc addWalletWatchOnly*(self: Client, address, name: string) {.async.} =
asyncSpawn addWalletWatchOnly(self.taskRunner, status, address, name)
proc connect*(self: Client, username: string) {.async.} =
asyncSpawn startWakuChat(self.taskRunner, status, username)
proc callRpc*(self: Client, rpcMethod: string, params: JsonNode) {.async.} =
asyncSpawn callRpc(self.taskRunner, status, rpcMethod, params)
proc connect*(self: Client) {.async.} =
asyncSpawn connect(self.taskRunner, status)
proc createAccount*(self: Client, password: string) {.async.} =
asyncSpawn createAccount(self.taskRunner, status, password)
proc deleteCustomToken*(self: Client, index: int) {.async.} =
asyncSpawn deleteCustomToken(self.taskRunner, status, index)
proc deleteWalletAccount*(self: Client, index: int,
password: string) {.async.} =
@ -81,10 +106,19 @@ proc deleteWalletAccount*(self: Client, index: int,
asyncSpawn deleteWalletAccount(self.taskRunner, status, index, password)
proc disconnect*(self: Client) {.async.} =
asyncSpawn stopWakuChat(self.taskRunner, status)
asyncSpawn disconnect(self.taskRunner, status)
proc createAccount*(self: Client, password: string) {.async.} =
asyncSpawn createAccount(self.taskRunner, status, password)
proc getAssets*(self: Client, owner: Address) {.async.} =
asyncSpawn getAssets(self.taskRunner, status, owner)
proc getCustomTokens*(self: Client) {.async.} =
asyncSpawn getCustomTokens(self.taskRunner, status)
proc getTopics(self: Client): Future[seq[ContentTopic]] {.async, gcsafe.} =
return await getTopics(self.taskRunner, status)
proc getPrice*(self: Client, tokenSymbol, fiatCurrency: string) {.async.} =
asyncSpawn getPrice(self.taskRunner, status, tokenSymbol, fiatCurrency)
proc importMnemonic*(self: Client, mnemonic: string, passphrase: string,
password: string) {.async.} =
@ -92,10 +126,10 @@ proc importMnemonic*(self: Client, mnemonic: string, passphrase: string,
asyncSpawn importMnemonic(self.taskRunner, status, mnemonic, passphrase,
password)
proc joinTopic*(self: Client, topic: string) {.async.} =
proc joinTopic*(self: Client, topic: ContentTopic) {.async.} =
asyncSpawn joinTopic(self.taskRunner, status, topic)
proc leaveTopic*(self: Client, topic: string) {.async.} =
proc leaveTopic*(self: Client, topic: ContentTopic) {.async.} =
asyncSpawn leaveTopic(self.taskRunner, status, topic)
proc listAccounts*(self: Client) {.async.} =
@ -110,29 +144,19 @@ proc login*(self: Client, account: int, password: string) {.async.} =
proc logout*(self: Client) {.async.} =
asyncSpawn logout(self.taskRunner, status)
proc sendMessage*(self: Client, message: string) {.async.} =
asyncSpawn publishWakuChat(self.taskRunner, status, message)
proc sendMessage*(self: Client, message: string, topic: ContentTopic)
{.async.} =
proc getAssets*(self: Client, owner: Address) {.async.} =
asyncSpawn getAssets(self.taskRunner, status, owner)
asyncSpawn sendMessage(self.taskRunner, status, message, topic)
proc getCustomTokens*(self: Client) {.async.} =
asyncSpawn getCustomTokens(self.taskRunner, status)
proc sendTransaction*(self: Client, fromAddress: EthAddress,
transaction: Transaction, password: string) {.async.} =
proc addCustomToken*(self: Client, address: Address, name, symbol, color: string, decimals: uint) {.async.} =
asyncSpawn addCustomToken(self.taskRunner, status, address, name, symbol, color, decimals)
proc deleteCustomToken*(self: Client, index: int) {.async.} =
asyncSpawn deleteCustomToken(self.taskRunner, status, index)
proc callRpc*(self: Client, rpcMethod: string, params: JsonNode) {.async.} =
asyncSpawn callRpc(self.taskRunner, status, rpcMethod, params)
proc sendTransaction*(self: Client, fromAddress: EthAddress, transaction: Transaction, password: string) {.async.} =
asyncSpawn sendTransaction(self.taskRunner, status, fromAddress, transaction, password)
proc getPrice*(self: Client, tokenSymbol, fiatCurrency: string) {.async.} =
asyncSpawn getPrice(self.taskRunner, status, tokenSymbol, fiatCurrency)
asyncSpawn sendTransaction(self.taskRunner, status, fromAddress, transaction,
password)
proc setPriceTimeout*(self: Client, timeout: int) {.async.} =
asyncSpawn setPriceTimeout(self.taskRunner, status, timeout)
proc stopContext(self: Client) {.async, gcsafe.} =
await stopContext(self.taskRunner, status)

View File

@ -1,59 +1,16 @@
import # std libs
std/[sets, strformat, strutils, sugar, times]
import # vendor libs
task_runner
import # status lib
status/api/accounts
import # client modules
../config
../common
export accounts, config, sets, strformat, strutils, task_runner, times
export common
logScope:
topics = "client"
type
Event* = ref object of RootObj
EventChannel* = AsyncChannel[ThreadSafeString]
# TODO: alphabetise Client above HelpText -- didn't want to interfere with
# ongoing work
Client* = ref object
account*: PublicAccount
clientConfig*: ClientConfig
currentTopic*: ContentTopic
events*: EventChannel
loggedin*: bool
online*: bool
running*: bool
taskRunner*: TaskRunner
topics*: OrderedSet[string]
const
hashCharSet* = {'#'}
status* = "status"
proc handleTopic*(topic: string): string =
var t = topic
let topicSplit = topic.split('/')
# if event.topic is a properly formatted waku v2 content topic then the
# whole string will be passed to joinTopic
if topicSplit.len != 5 or topicSplit[0] != "":
# otherwise convert it to a properly formatted content topic
t = topic.strip(true, false, hashCharSet)
# should end with `/rlp` for real encoding and decoding
if t != "":
# formatted topic should use hex encoded first four bytes of sha256
# digest, but will need to e.g. return a tuple and come up with some
# structure/s to keep track of hashed and human-friendly names
# t = "0x" & ($sha256.digest(t))[0..7].toLowerAscii
t = fmt"/waku/1/{t}/proto"
return t
proc newEventChannel*(): EventChannel =
newAsyncChannel[ThreadSafeString](-1)
topics*: OrderedSet[ContentTopic]

View File

@ -1,14 +1,9 @@
import # vendor libs
web3/ethtypes
import # status lib
status/api/[opensea, tokens, wallet]
status/api/[accounts, opensea, tokens, wallet]
import # client modules
./common
export common
logScope:
topics = "client"
@ -17,79 +12,65 @@ type
AddCustomTokenEvent* = ref object of ClientEvent
address*: string
name*: string
symbol*: string
color*: string
decimals*: uint
error*: string
timestamp*: int64
name*: string
symbol*: string
AddWalletAccountEvent* = ref object of ClientEvent
name*: string
address*: Address
name*: string
error*: string
timestamp*: int64
CreateAccountEvent* = ref object of ClientEvent
account*: PublicAccount
error*: string
timestamp*: int64
DeleteCustomTokenEvent* = ref object of ClientEvent
address*: string
error*: string
timestamp*: int64
CallRpcEvent* = ref object of ClientEvent
response*: string
error*: string
timestamp*: int64
response*: string
GetAssetsEvent* = ref object of ClientEvent
assets*: seq[Asset]
error*: string
timestamp*: int64
GetCustomTokensEvent* = ref object of ClientEvent
tokens*: seq[Token]
error*: string
timestamp*: int64
tokens*: seq[Token]
DeleteWalletAccountEvent* = ref object of ClientEvent
name*: string
address*: string
error*: string
timestamp*: int64
name*: string
GetPriceEvent* = ref object of ClientEvent
symbol*: string
currency*: string
price*: float
error*: string
timestamp*: int64
price*: float
symbol*: string
ImportMnemonicEvent* = ref object of ClientEvent
error*: string
account*: PublicAccount
timestamp*: int64
error*: string
JoinTopicEvent* = ref object of ClientEvent
timestamp*: int64
topic*: string
topic*: ContentTopic
LeaveTopicEvent* = ref object of ClientEvent
timestamp*: int64
topic*: string
topic*: ContentTopic
ListAccountsEvent* = ref object of ClientEvent
accounts*: seq[PublicAccount]
error*: string
timestamp*: int64
ListWalletAccountsEvent* = ref object of ClientEvent
accounts*: seq[WalletAccount]
error*: string
timestamp*: int64
LoginEvent* = ref object of ClientEvent
account*: PublicAccount
@ -100,50 +81,56 @@ type
error*: string
loggedin*: bool
NetworkStatusEvent* = ref object of ClientEvent
WakuConnectionEvent* = ref object of ClientEvent
error*: string
online*: bool
SendTransactionEvent* = ref object of ClientEvent
response*: string
SendMessageEvent* = ref object of ClientEvent
error*: string
timestamp*: int64
sent*: bool
SendTransactionEvent* = ref object of ClientEvent
error*: string
response*: string
SetPriceTimeoutEvent* = ref object of ClientEvent
timeout*: int
error*: string
timestamp*: int64
timeout*: int
UserMessageEvent* = ref object of ClientEvent
message*: string
timestamp*: int64
topic*: string
topic*: ContentTopic
username*: string
const clientEvents* = [
"AddCustomTokenEvent",
"AddWalletAccountEvent",
"CallRpcEvent",
"CreateAccountEvent",
"DeleteCustomTokenEvent",
"DeleteWalletAccountEvent",
"GetCustomTokensEvent",
"GetAssetsEvent",
"GetPriceEvent",
"ImportMnemonicEvent",
"JoinTopicEvent",
"LeaveTopicEvent",
"ListAccountsEvent",
"ListWalletAccountsEvent",
"LoginEvent",
"LogoutEvent",
"NetworkStatusEvent",
"SendTransactionEvent",
"SetPriceTimeoutEvent",
"UserMessageEvent"
]
const
clientEvents* = [
"AddCustomTokenEvent",
"AddWalletAccountEvent",
"CallRpcEvent",
"CreateAccountEvent",
"DeleteCustomTokenEvent",
"DeleteWalletAccountEvent",
"GetCustomTokensEvent",
"GetAssetsEvent",
"GetPriceEvent",
"ImportMnemonicEvent",
"JoinTopicEvent",
"LeaveTopicEvent",
"ListAccountsEvent",
"ListWalletAccountsEvent",
"LoginEvent",
"LogoutEvent",
"SendMessageEvent",
"SendTransactionEvent",
"SetPriceTimeoutEvent",
"UserMessageEvent",
"WakuConnectionEvent"
]
status = "status"
proc listenToStatus(self: Client) {.async.} =
let worker = self.taskRunner.workers["status"].worker
let worker = self.taskRunner.workers[status].worker
while self.running and self.taskRunner.running.load():
let event = await worker.chanRecvFromWorker.recv()
asyncSpawn self.events.send(event)

View File

@ -5,4 +5,4 @@ proc writeValue*(writer: var JsonWriter, value: ChainId) =
writeValue(writer, uint64 value)
proc readValue*(reader: var JsonReader, value: var ChainId) =
value = ChainId reader.readValue(uint64)
value = ChainId reader.readValue(uint64)

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +0,0 @@
import # std libs
std/[json, options, random, sequtils, strutils, tables, times, uri]
import # vendor libs
bearssl, chronicles, chronos, chronos/apps/http/httpclient, eth/keys,
libp2p/[crypto/crypto, crypto/secp, multiaddress, muxers/muxer, peerid,
peerinfo, protobuf/minprotobuf, protocols/protocol, stream/connection,
switch],
nimcrypto/utils,
stew/[byteutils, endians2, results],
waku/common/utils/nat,
waku/v2/node/[waku_payload, wakunode2],
waku/v2/protocol/[waku_filter/waku_filter, waku_lightpush/waku_lightpush,
waku_message, waku_store/waku_store],
waku/v2/utils/peers
export
byteutils, crypto, keys, minprotobuf, nat, peers, results, secp, utils,
waku_filter, waku_lightpush, waku_message, waku_store, wakunode2
logScope:
topics = "client"
type
Chat2Message* = object
nick*: string
payload*: seq[byte]
timestamp*: int64
PrivateKey* = crypto.PrivateKey
Topic* = wakunode2.Topic
const DefaultTopic* = "/waku/2/default-waku/proto"
# Initialize the default random number generator, only needs to be called once:
# https://nim-lang.org/docs/random.html#randomize
randomize()
proc encode*(message: Chat2Message): ProtoBuffer =
result = initProtoBuffer()
result.write(1, uint64(message.timestamp))
result.write(2, message.nick)
result.write(3, message.payload)
proc init*(T: type Chat2Message, buffer: seq[byte]): ProtoResult[T] =
var msg = Chat2Message()
let pb = initProtoBuffer(buffer)
var timestamp: uint64
discard ? pb.getField(1, timestamp)
msg.timestamp = int64(timestamp)
discard ? pb.getField(2, msg.nick)
discard ? pb.getField(3, msg.payload)
ok(msg)
proc init*(T: type Chat2Message, nick: string, message: string): T =
let
payload = message.toBytes()
timestamp = getTime().toUnix()
T(nick: nick, payload: payload, timestamp: timestamp)
proc selectRandomNode*(fleetStr: string): Future[string] {.async.} =
let
url = "https://fleets.status.im"
response = await fetch(HttpSessionRef.new(), parseUri(url))
fleet = string.fromBytes(response.data)
nodes = toSeq(
fleet.parseJson(){"fleets", "wakuv2." & fleetStr, "waku"}.pairs())
return nodes[rand(nodes.len - 1)].val.getStr()

View File

@ -0,0 +1,28 @@
import # std libs
std/[sets, strformat, strutils]
# std libs
from times import getTime, toUnix
import # vendor libs
chronicles, chronos, task_runner
import # status lib
status/api/common
import # client modules
./config
export # modules
chronicles, chronos, common, config, sets, strformat, strutils, task_runner
export # symbols
getTime, toUnix
type
Event* = ref object of RootObj
timestamp*: int64
EventChannel* = AsyncChannel[ThreadSafeString]
proc newEventChannel*(): EventChannel = newAsyncChannel[ThreadSafeString](-1)

View File

@ -4,10 +4,14 @@ import # std libs
import # vendor libs
chronicles, confutils, confutils/std/net
import # client modules
./client/waku_chat2
import # status lib
status/api/waku
export confutils, net.ValidIpAddress, net.init
export # modules
confutils
export # symbols
net.ValidIpAddress, net.init, waku.WakuFleet
logScope:
topics = "client config"
@ -19,8 +23,6 @@ type
VIP* = distinct string
WakuFleet* = enum none, prod, test
const
ERROR = LogLevel.ERROR
NONE = LogLevel.NONE
@ -181,7 +183,7 @@ macro config(): untyped =
topics* {.
defaultValue: "/waku/2/default-waku/proto"
desc: "Default topics to subscribe to (space separated list)"
desc: "PubSub topics to subscribe to (space separated list)"
name: "waku-topics"
.}: string
@ -302,17 +304,12 @@ macro config(): untyped =
name: "waku-fleet"
.}: WakuFleet
# in the help text for --waku-content-topics the formatting-indicator
# should end with /rlp for real decoding; and when topic sha256 hashing
# is implemented for /waku/1 chats should change {topic} to
# {topic-digest}
contentTopics* {.
defaultValue: "#test"
desc: "Default content topics for chat messages " &
"(space separated list). Topic names that do not conform to " &
"23/WAKU2-TOPICS will formatted as /waku/1/{topic}/proto " &
"with leading \"#\" removed",
desc: "Content topics to subscribe to for chat messages (space " &
"separated list). Topic names that do not conform to " &
"23/WAKU2-TOPICS will be formatted as " &
"/waku/1/{topic-digest}/rfc26 with leading \"#\" removed",
name: "waku-content-topics"
.}: string
@ -347,10 +344,9 @@ proc completeCmdArg*(T: type LLevel, val: TaintedString): seq[string] =
proc parseCmdArg*(T: type PK, p: TaintedString): T =
try:
discard waku_chat2.crypto.PrivateKey(scheme: Secp256k1,
skkey: SkPrivateKey.init(waku_chat2.utils.fromHex(p)).tryGet())
result = PK(p)
except CatchableError:
discard Nodekey.fromHex(p).tryGet()
return PK(p)
except CatchableError as e:
raise newException(ConfigurationError, "Invalid private key")
proc completeCmdArg*(T: type PK, val: TaintedString): seq[string] =

View File

@ -1,7 +1,7 @@
import # client modules
./tui/events
./tui/[common, events, screen, tasks]
export events
export common
logScope:
topics = "tui"
@ -11,11 +11,11 @@ logScope:
# This module's purpose is to start the client and initiate listening for
# events coming from the client and user
const input = "input"
# `type Tui` and `proc stop(self: Tui)` are defined in ./common to avoid
# circular dependency
const input = "input"
proc new*(T: type Tui, clientConfig: ClientConfig): T =
let
(locale, mainWin, mouse) = initScreen()

View File

@ -1,16 +1,11 @@
import # std libs
std/[strformat, strutils, tables]
std/tables
import # vendor libs
web3/conversions
import # status lib
status/api/opensea
import # client modules
./parser
export parser
./commands, ./common, ./macros, ./parser, ./screen
logScope:
topics = "tui"
@ -343,28 +338,66 @@ proc action*(self: Tui, event: ImportMnemonicEvent) {.async, gcsafe, nimcall.} =
proc action*(self: Tui, event: JoinTopicEvent) {.async, gcsafe, nimcall.} =
let
timestamp = event.timestamp
topic = event.topic
topic = if event.topic.shortName != "": event.topic.shortName
else: $event.topic
if not self.client.topics.contains(topic):
self.client.topics.incl(topic)
self.printResult(fmt"Joined topic: {topic}", timestamp)
if not self.client.topics.contains(event.topic):
self.client.topics.incl(event.topic)
if self.outputReady:
self.printResult(fmt"Joined topic: {topic}", timestamp)
else:
self.printResult(fmt"Topic already joined: {topic}", timestamp)
if self.outputReady:
self.printResult(fmt"Topic already joined: {topic}", timestamp)
if self.client.currentTopic != event.topic:
self.client.currentTopic = event.topic
if self.outputReady:
self.printResult(fmt"Switched current topic: {topic}", timestamp)
# LeaveTopicEvent -------------------------------------------------------------
proc action*(self: Tui, event: LeaveTopicEvent) {.async, gcsafe,
nimcall.} =
let
timestamp = event.timestamp
topic = event.topic
let timestamp = event.timestamp
var topic = if event.topic.shortName != "": event.topic.shortName
else: $event.topic
if self.client.topics.contains(topic):
self.client.topics.excl(topic)
self.printResult(fmt"Left topic: {topic}", timestamp)
if self.client.topics.contains(event.topic):
self.client.topics.excl(event.topic)
if self.outputReady:
self.printResult(fmt"Left topic: {topic}", timestamp)
else:
self.printResult(fmt"Topic not joined, no need to leave: {topic}",
timestamp)
if self.outputReady:
self.printResult(fmt"Topic not joined, no need to leave: {topic}",
timestamp)
if self.client.topics.len > 0:
if self.client.currentTopic == event.topic or
self.client.currentTopic == noTopic:
let currentTopic = self.client.topics.toSeq[^1]
topic = if currentTopic.shortName != "": currentTopic.shortName
else: $currentTopic
self.client.currentTopic = currentTopic
if self.outputReady:
self.printResult(fmt"Switched current topic: {topic}", timestamp)
else:
let currentTopic = self.client.currentTopic
topic = if currentTopic.shortName != "": currentTopic.shortName
else: $currentTopic
if self.outputReady:
self.printResult(fmt"Current topic: {topic}", timestamp)
else:
self.client.currentTopic = noTopic
if self.outputReady:
self.printResult("No current topic set because no topics are joined",
timestamp)
# ListAccountsEvent -----------------------------------------------------------
@ -391,6 +424,7 @@ proc action*(self: Tui, event: ListAccountsEvent) {.async, gcsafe,
self.printResult(fmt"{2.indent()}{i}. {name} ({abbrev})",
timestamp)
i = i + 1
else:
self.printResult(
"No accounts. Create an account using `/create <password>`.",
@ -432,46 +466,58 @@ proc action*(self: Tui, event: ListWalletAccountsEvent) {.async, gcsafe,
# LoginEvent ------------------------------------------------------------------
proc action*(self: Tui, event: LoginEvent) {.async, gcsafe, nimcall.} =
let
error = event.error
loggedin = event.loggedin
# if TUI is not ready for output then ignore it
if self.outputReady:
let
error = event.error
timestamp = event.timestamp
if error != "":
self.wprintFormatError(getTime().toUnix(), fmt"{error}")
else:
self.client.account = event.account
self.printResult("Login successful.", getTime().toUnix())
if not self.client.online:
asyncSpawn self.client.connect(self.client.account.name)
self.client.loggedin = loggedin
trace "TUI updated client state", loggedin
if error != "":
self.wprintFormatError(timestamp, fmt"{error}")
else:
self.printResult("Login successful.", timestamp)
# LogoutEvent -----------------------------------------------------------------
proc action*(self: Tui, event: LogoutEvent) {.async, gcsafe, nimcall.} =
let
error = event.error
loggedin = event.loggedin
# if TUI is not ready for output then ignore it
if self.outputReady:
let
error = event.error
timestamp = event.timestamp
if error != "":
self.wprintFormatError(getTime().toUnix(), fmt"{error}")
else:
self.client.account = PublicAccount()
self.printResult("Logout successful.", getTime().toUnix())
if self.client.online:
asyncSpawn self.client.disconnect()
if error != "":
self.wprintFormatError(timestamp, fmt"{error}")
else:
self.printResult("Logout successful.", timestamp)
self.client.loggedin = loggedin
trace "TUI updated client state", loggedin
# WakuConnectionEvent ----------------------------------------------------------
# NetworkStatusEvent -----------------------------------------------------------
proc action*(self: Tui, event: WakuConnectionEvent) {.async, gcsafe, nimcall.} =
# if TUI is not ready for output then ignore it
if self.outputReady:
let
error = event.error
online = event.online
timestamp = event.timestamp
proc action*(self: Tui, event: NetworkStatusEvent) {.async, gcsafe, nimcall.} =
let online = event.online
if error != "":
self.wprintFormatError(timestamp, fmt"{error}")
elif online:
self.printResult("Connected to network.", timestamp)
else:
self.printResult("Disconnected from network.", timestamp)
self.client.online = online
trace "TUI updated client state", online
# SendMessageEvent -------------------------------------------------------------
proc action*(self: Tui, event: SendMessageEvent) {.async, gcsafe, nimcall.} =
# if TUI is not ready for output then ignore it
if self.outputReady:
let
error = event.error
timestamp = event.timestamp
if error != "": self.wprintFormatError(timestamp, fmt"{error}")
# UserMessageEvent -------------------------------------------------------------
@ -481,25 +527,23 @@ proc action*(self: Tui, event: UserMessageEvent) {.async, gcsafe, nimcall.} =
let
message = event.message
timestamp = event.timestamp
topic =
if event.topic.shortName != "":
event.topic.shortName
else:
if isChat2(event.topic):
"chat2: #" & event.topic.topicName
else:
$event.topic
username = event.username
var topic = event.topic
let topicSplit = topic.split('/')
# if event.topic is not a properly formatted waku v2 content topic then the
# whole string will be passed to printMessage
if topicSplit.len == 5 and topicSplit[0] == "":
# for "/toy-chat/2/example/proto", topic would be "example"
topic = topicSplit[3]
debug "TUI received user message", message, timestamp, username
self.printMessage(message, timestamp, username, topic)
# CallRpcEvent -----------------------------------------------------------------
proc action*(self: Tui, event: CallRpcEvent) {.async, gcsafe,
nimcall.} =
proc action*(self: Tui, event: CallRpcEvent) {.async, gcsafe, nimcall.} =
# if TUI is not ready for output then ignore it
if self.outputReady:
if event.error != "":

View File

@ -1,14 +1,11 @@
import # std libs
std/[json, sequtils, strformat, strutils, sugar]
import # client modules
./common, ./macros, ./screen, ./tasks
std/sugar
import # vendor libs
eth/common as eth_common, stew/byteutils
export common, screen, strutils, tasks
import # client modules
./common, ./macros, ./screen
logScope:
topics = "tui"
@ -61,7 +58,8 @@ proc new*(T: type AddCustomToken, args: varargs[string]): T =
color = args[3]
decimals = args[4]
T(address: address, name: name, symbol: symbol, color: color, decimals: decimals)
T(address: address, name: name, symbol: symbol, color: color,
decimals: decimals)
proc split*(T: type AddCustomToken, argsRaw: string): seq[string] =
argsRaw.split(" ")
@ -309,11 +307,7 @@ proc split*(T: type Connect, argsRaw: string): seq[string] =
@[]
proc command*(self: Tui, command: Connect) {.async, gcsafe, nimcall.} =
if self.client.loggedin:
asyncSpawn self.client.connect(self.client.account.name)
else:
self.wprintFormatError(getTime().toUnix,
"client is not logged in, cannot connect.")
asyncSpawn self.client.connect()
# CreateAccount ----------------------------------------------------------------
@ -436,10 +430,7 @@ proc split*(T: type Disconnect, argsRaw: string): seq[string] =
@[]
proc command*(self: Tui, command: Disconnect) {.async, gcsafe, nimcall.} =
if self.client.online:
asyncSpawn self.client.disconnect()
else:
self.wprintFormatError(getTime().toUnix, "client is not online.")
asyncSpawn self.client.disconnect()
# GetAssets -----------------------------------------------------------------
@ -620,15 +611,21 @@ proc split*(T: type JoinTopic, argsRaw: string): seq[string] =
@[argsRaw.strip().split(" ")[0]]
proc command*(self: Tui, command: JoinTopic) {.async, gcsafe, nimcall.} =
var topic = handleTopic(command.topic)
let timestamp = getTime().toUnix
var topic = command.topic
if topic == "":
self.wprintFormatError(getTime().toUnix,
self.wprintFormatError(timestamp,
"topic cannot be blank, please provide a topic as the first argument.")
elif self.client.topics.contains(topic):
self.printResult(fmt"Topic already joined: {topic}", getTime().toUnix)
else:
asyncSpawn self.client.joinTopic(topic)
let topicResult = ContentTopic.init(topic)
if topicResult.isErr:
self.wprintFormatError(timestamp, $topicResult.error)
else:
asyncSpawn self.client.joinTopic(topicResult.get)
# LeaveTopic -------------------------------------------------------------------
@ -646,16 +643,21 @@ proc split*(T: type LeaveTopic, argsRaw: string): seq[string] =
@[argsRaw.strip().split(" ")[0]]
proc command*(self: Tui, command: LeaveTopic) {.async, gcsafe, nimcall.} =
let topic = handleTopic(command.topic)
let timestamp = getTime().toUnix
var topic = command.topic
if topic == "":
self.wprintFormatError(getTime().toUnix,
self.wprintFormatError(timestamp,
"topic cannot be blank, please provide a topic as the first argument.")
elif not self.client.topics.contains(topic):
self.printResult(fmt"Topic not joined, no need to leave: {topic}",
getTime().toUnix)
else:
asyncSpawn self.client.leaveTopic(topic)
let topicResult = ContentTopic.init(topic)
if topicResult.isErr:
self.wprintFormatError(timestamp, $topicResult.error)
else:
asyncSpawn self.client.leaveTopic(topicResult.get)
# ListAccounts -----------------------------------------------------------------
@ -673,7 +675,7 @@ proc split*(T: type ListAccounts, argsRaw: string): seq[string] =
proc command*(self: Tui, command: ListAccounts) {.async, gcsafe, nimcall.} =
asyncSpawn self.client.listAccounts()
# ListTopics -----------------------------------------------------------------
# ListTopics -------------------------------------------------------------------
proc help*(T: type ListTopics): HelpText =
let command = "listtopics"
@ -694,15 +696,34 @@ proc command*(self: Tui, command: ListTopics) {.async, gcsafe, nimcall.} =
if topics.len > 0:
var i = 1
self.printResult("Joined topics:", timestamp)
for topic in topics:
self.printResult(fmt"{2.indent()}{i}. {topic}", timestamp)
for topic in topics.items:
let t =
if topic.shortName != "":
fmt("{topic.shortName} ({$topic})")
else:
$topic
self.printResult(fmt"{2.indent()}{i}. {t}", timestamp)
i = i + 1
let currentTopic = self.client.currentTopic
if currentTopic != noTopic:
let topic = if currentTopic.shortName != "": currentTopic.shortName
else: $currentTopic
self.printResult(fmt"Current topic: {topic}", timestamp)
else:
# there shouldn't be a situation where:
# `topics.len > 0 and currentTopic == noTopic`
# but hand-written state machines are tricky, so just in case...
self.printResult("No current topic set", timestamp)
else:
self.printResult("No topics joined. Join a topic using `/join <topic>`.",
timestamp)
# ListWalletAccounts -----------------------------------------------------------------
# ListWalletAccounts -----------------------------------------------------------
proc help*(T: type ListWalletAccounts): HelpText =
let command = "listwalletaccounts"
@ -789,7 +810,7 @@ proc split*(T: type Quit, argsRaw: string): seq[string] =
@[]
proc command*(self: Tui, command: Quit) {.async, gcsafe, nimcall.} =
await self.stop()
waitFor self.stop()
# SendMessage ------------------------------------------------------------------
@ -810,11 +831,14 @@ proc split*(T: type SendMessage, argsRaw: string): seq[string] =
@[argsRaw]
proc command*(self: Tui, command: SendMessage) {.async, gcsafe, nimcall.} =
if not self.client.online:
self.wprintFormatError(getTime().toUnix,
"client is not online, cannot send message.")
let timestamp = getTime().toUnix
if self.client.currentTopic == noTopic:
self.wprintFormatError(timestamp,
"current topic is not set, cannot send message.")
else:
asyncSpawn self.client.sendMessage(command.message)
asyncSpawn self.client.sendMessage(command.message,
self.client.currentTopic)
# CallRpc ----------------------------------------------------------------------
@ -980,6 +1004,46 @@ proc command*(self: Tui, cmd: SendTransaction) {.async, gcsafe,
except:
self.wprintFormatError(getTime().toUnix, "invalid arguments.")
# Switchtopic ------------------------------------------------------------------
proc help*(T: type SwitchTopic): HelpText =
let command = "switchtopic"
HelpText(command: command, aliases: aliased.getOrDefault(command), parameters: @[
CommandParameter(name: "topic",
description: "Name of the topic to make the current topic.")
], description: "Sets the current topic to which messages will be sent.")
proc new*(T: type Switchtopic, args: varargs[string]): T =
T(topic: args[0])
proc split*(T: type Switchtopic, argsRaw: string): seq[string] =
@[argsRaw.strip().split(" ")[0]]
proc command*(self: Tui, command: Switchtopic) {.async, gcsafe, nimcall.} =
let timestamp = getTime().toUnix
var topic = command.topic
if topic == "":
self.wprintFormatError(timestamp,
"topic cannot be blank, please provide a topic as the first argument.")
else:
let topicResult = ContentTopic.init(topic)
if topicResult.isErr:
self.wprintFormatError(timestamp, $topicResult.error)
else:
let contentTopic = topicResult.get
topic = if contentTopic.shortName != "": contentTopic.shortName
else: $contentTopic
if self.client.topics.contains(contentTopic):
self.client.currentTopic = contentTopic
self.printResult(fmt"Switched current topic: {topic}", timestamp)
else:
self.wprintFormatError(timestamp,
fmt"Cannot set current topic to an unjoined topic: {topic}")
# Help -------------------------------------------------------------------------
# Note: although "Help" is not alphabetically last, we need to keep this below
# all other `help()` definitions so that they are available to the

View File

@ -1,10 +1,8 @@
import # client modules
../client, ./ncurses_helpers
../client, ../common,
./ncurses_helpers
import # vendor libs
eth/common
export client, ncurses_helpers
export client, common, ncurses_helpers
logScope:
topics = "tui"
@ -46,10 +44,10 @@ type
AddCustomToken* = ref object of Command
address*: string
name*: string
symbol*: string
color*: string
decimals*: string
name*: string
symbol*: string
AddWalletAccount* = ref object of Command
name*: string
@ -62,23 +60,23 @@ type
AddWalletSeed* = ref object of Command
bip39Passphrase*: string
name*: string
mnemonic*: string
name*: string
password*: string
AddWalletWatchOnly* = ref object of Command
name*: string
address*: string
name*: string
CallRpc* = ref object of Command
rpcMethod*: string
params*: string
rpcMethod*: string
Connect* = ref object of Command
CommandParameter* = ref object of RootObj
name*: string
description*: string
name*: string
CreateAccount* = ref object of Command
password*: string
@ -98,17 +96,17 @@ type
GetCustomTokens* = ref object of Command
GetPrice* = ref object of Command
tokenSymbol*: string
fiatCurrency*: string
tokenSymbol*: string
Help* = ref object of Command
command*: string
HelpText* = ref object of RootObj
command*: string
parameters*: seq[CommandParameter]
aliases*: seq[string]
command*: string
description*: string
parameters*: seq[CommandParameter]
ImportMnemonic* = ref object of Command
mnemonic*: string
@ -140,19 +138,25 @@ type
SendTransaction* = ref object of Command
fromAddress*: string
toAddress*: string
value*: string
maxPriorityFee*: string
maxFee*: string
gasLimit*: string
payload*: string
maxFee*: string
maxPriorityFee*: string
nonce*: string
password*: string
payload*: string
toAddress*: string
value*: string
SetPriceTimeout* = ref object of Command
timeout*: string
SwitchTopic* = ref object of Command
topic*: string
const
ESCAPE* = "ESCAPE"
RETURN* = "RETURN"
TuiEvents* = [
"InputKey",
"InputReady",
@ -186,9 +190,11 @@ const
"listtopics": "ListTopics",
"login": "Login",
"logout": "Logout",
"quit": "Quit",
"sendtransaction": "SendTransaction",
"setpricetimeout": "SetPriceTimeout",
"quit": "Quit"
"quit": "Quit",
"switchtopic": "SwitchTopic"
}.toTable
aliases* = {
@ -205,7 +211,11 @@ const
"gettokens": "getcustomtokens",
"import": "importmnemonic",
"join": "jointopic",
"joinpublic": "jointopic",
"joinpublicchat": "jointopic",
"leave": "leavetopic",
"leavepublic": "leavetopic",
"leavepublicchat": "leavetopic",
"list": "listaccounts",
"listwallets": "listwalletaccounts",
"part": "leavetopic",
@ -213,6 +223,7 @@ const
"send": DEFAULT_COMMAND,
"sub": "jointopic",
"subscribe": "jointopic",
"switch": "switchtopic",
"topics": "listtopics",
"trx": "sendtransaction",
"unjoin": "leavetopic",
@ -236,12 +247,14 @@ const
"getcustomtokens": @["gettokens"],
"importmnemonic": @["import"],
"help": @["?"],
"jointopic": @["join", "sub", "subscribe"],
"leavetopic": @["leave", "part", "unjoin", "unsub", "unsubscribe"],
"jointopic": @["join", "joinpublic", "joinpublicchat", "sub", "subscribe"],
"leavetopic": @["leave", "leavepublic", "leavepublicchat", "part", "unjoin",
"unsub", "unsubscribe"],
"listaccounts": @["list"],
"listtopics": @["topics"],
"listwalletaccounts": @["listwallets", "wallets"],
"sendtransaction": @["trx"]
"sendtransaction": @["trx"],
"switchtopic": @["switch"]
}.toTable
proc stop*(self: Tui) {.async.} =

View File

@ -1,7 +1,5 @@
import # client modules
./actions
export actions
./actions, ./common, ./macros
logScope:
topics = "tui"

View File

@ -4,8 +4,6 @@ import # std libs
import # client modules
./common
export common
# Events -----------------------------------------------------------------------
macro `&`[T; A, B: static int](a: array[A, T], b: array[B, T]): untyped =

View File

@ -2,13 +2,10 @@ import # std libs
std/bitops
import # vendor libs
chronicles, ncurses
ncurses
export ncurses
logScope:
topics = "tui"
# NOTE: depending on OS, terminal emulator, font, and related software, there
# can be problems re: how ncurses displays some emojis and other characters,
# e.g. those that make use of ZWJ or ZWNJ (more generally "extended grapheme

View File

@ -1,7 +1,5 @@
import # client modules
./commands as cmd, ./macros
export cmd, macros
./commands as cmd, ./common, ./macros
logScope:
topics = "tui"

View File

@ -1,11 +1,9 @@
import # std libs
std/[strformat, strutils]
# std libs
from times import fromUnix, inZone, local
import # client modules
./common
export common
logScope:
topics = "tui"

View File

@ -4,15 +4,9 @@ import # vendor libs
import # client modules
./common
export common
logScope:
topics = "tui"
const
ESCAPE* = "ESCAPE"
RETURN* = "RETURN"
type ByteArray = array[0..3, byte]
proc readInput*() {.task(kind=no_rts, stoppable=false).} =

View File

@ -1,4 +1,5 @@
import # status modules
./api/[accounts, auth, common, opensea, provider, settings, tokens, wallet]
./api/[accounts, auth, common, opensea, provider, settings, tokens, waku,
wallet]
export accounts, auth, common, opensea, provider, settings, tokens, wallet
export accounts, auth, common, opensea, provider, settings, tokens, waku, wallet

View File

@ -13,11 +13,11 @@ import # status modules
../private/extkeys/[paths, types],
./common
export
common, public_accounts
# TODO: are these exports needed?
# accounts, alias, conversions, generator, identicon, paths,
# public_accounts, secp256k1, settings, types, uuid
export common except setLoginState, setNetworkState
export public_accounts
# TODO: are these exports needed?
# accounts, alias, conversions, generator, identicon, paths,
# public_accounts, secp256k1, settings, types, uuid
type
AccountsError* = enum
@ -113,10 +113,11 @@ proc storeDerivedAccounts(self: StatusObject, id: UUID, keyUid: string,
# First, record if we are currently logged in, and then init the user db
# if not. After we know the db has been inited, create the needed accounts.
# Once finished, close the db if we were originally logged out.
let wasLoggedIn = self.isLoggedIn
let wasLoggedIn = self.loginState == LoginState.loggedin
if not wasLoggedIn:
?self.initUserDb(keyUid, password).mapErrTo(
{DbError.KeyError: InvalidPassword}.toTable, InitUserDbError)
self.setLoginState(LoginState.loggedin)
let userDb = ?self.userDb.mapErrTo(UserDbError)
?userDb.createAccount(defaultWalletAccount).mapErrTo(CreateAcctError)
@ -124,15 +125,15 @@ proc storeDerivedAccounts(self: StatusObject, id: UUID, keyUid: string,
if not wasLoggedIn:
?self.closeUserDb.mapErrTo(CloseDbError)
self.setLoginState(LoginState.loggedout)
ok pubAccount
proc createAccount*(self: StatusObject, mnemonicPhraseLength: int,
bip39Passphrase, password: string, dir: string):
AccountsResult[PublicAccount] =
if self.isLoggedIn:
if self.loginState != LoginState.loggedout:
return err MustBeLoggedOut
let
@ -145,6 +146,8 @@ proc createAccount*(self: StatusObject, mnemonicPhraseLength: int,
?self.initUserDb(account.keyUid, password).mapErrTo(
{DbError.KeyError: InvalidPassword}.toTable, InitUserDbError)
self.setLoginState(LoginState.loggedin)
let
pubAccount = ?self.storeDerivedAccounts(account.id, account.keyUid, paths,
password, dir, AccountType.Generated).mapErrTo(StoreDerivedAcctsError)
@ -195,6 +198,7 @@ proc createAccount*(self: StatusObject, mnemonicPhraseLength: int,
let userDb = ?self.userDb.mapErrTo(UserDbError)
?userDb.createSettings(settings, nodeConfig).mapErrTo(CreateSettingsError)
?self.closeUserDb.mapErrTo(CloseDbError)
self.setLoginState(LoginState.loggedout)
ok pubAccount
@ -202,6 +206,7 @@ proc getChatAccount*(self: StatusObject): AccountsResult[accounts.Account] =
let
userDb = ?self.userDb.mapErrTo(UserDbError)
acct = ?userDb.getChatAccount.mapErrTo(GetChatAcctError)
ok acct
proc getPublicAccounts*(self: StatusObject):
@ -210,10 +215,8 @@ proc getPublicAccounts*(self: StatusObject):
let accts = ?self.accountsDb.getPublicAccounts().mapErrTo(GetPublicAcctsError)
ok accts
proc importMnemonic*(self: StatusObject, mnemonic: Mnemonic,
bip39Passphrase, password: string, dir: string):
AccountsResult[PublicAccount] =
bip39Passphrase, password, dir: string): AccountsResult[PublicAccount] =
let
imported = ?self.accountsGenerator.importMnemonic(mnemonic,
@ -222,6 +225,7 @@ proc importMnemonic*(self: StatusObject, mnemonic: Mnemonic,
PATH_DEFAULT_WALLET]
pubAccount = ?self.storeDerivedAccounts(imported.id, imported.keyUid,
paths, password, dir, AccountType.Seed).mapErrTo(StoreDerivedAcctsError)
ok pubAccount
proc saveAccount*(self: StatusObject, account: PublicAccount):

View File

@ -7,8 +7,7 @@ import # status modules
../private/[accounts/public_accounts, conversions, settings, util],
./common
export
common
export common except setLoginState, setNetworkState
# TODO: do we still need these exports?
# conversions, public_accounts, settings
@ -21,6 +20,8 @@ type
"keyUid"
InitUserDbError = "auth: error initialising user db"
InvalidPassword = "auth: invalid password"
MustBeLoggedIn = "auth: operation not permitted, must be logged " &
"in"
MustBeLoggedOut = "auth: operation not permitted, must be logged " &
"out"
ParseAddressError = "auth: failed to parse address"
@ -34,22 +35,42 @@ type
proc login*(self: StatusObject, keyUid, password: string):
AuthResult[PublicAccount] =
if self.isLoggedIn:
if self.loginState != LoginState.loggedout:
return err MustBeLoggedOut
let account = ?self.accountsDb.getPublicAccount(keyUid).mapErrTo(
GetAccountError)
self.setLoginState(LoginState.loggingin)
if account.isNone:
let account = self.accountsDb.getPublicAccount(keyUid)
if account.isErr:
self.setLoginState(LoginState.loggedout)
return err GetAccountError
if account.get.isNone:
self.setLoginState(LoginState.loggedout)
return err InvalidKeyUid
?self.initUserDb(keyUid, password).mapErrTo(
[(DbError.KeyError, InvalidPassword)].toTable, InitUserDbError)
let init = self.initUserDb(keyUid, password).mapErrTo(
{DbError.KeyError: InvalidPassword}.toTable, InitUserDbError)
ok account.get
if init.isErr:
self.setLoginState(LoginState.loggedout)
return err init.error
proc logout*(self: StatusObject): AuthResult[void] {.raises: [].} =
?self.closeUserDb().mapErrTo(CloseDbError)
self.setLoginState(LoginState.loggedin)
ok account.get.get
proc logout*(self: StatusObject): AuthResult[void] =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
self.setLoginState(LoginState.loggingout)
let close = self.closeUserDb().mapErrTo(CloseDbError)
if close.isErr:
self.setLoginState(LoginState.loggedin)
return close
self.setLoginState(LoginState.loggedout)
ok()
proc validatePassword*(self: StatusObject, password, dir: string):
@ -59,10 +80,13 @@ proc validatePassword*(self: StatusObject, password, dir: string):
userDb = ?self.userDb.mapErrTo(UserDbError)
address = ?userDb.getSetting(string,
SettingsCol.WalletRootAddress).mapErrTo(WalletRootAddressError)
if address.isNone:
return ok false
let
addressParsed = ?address.get.parseAddress.mapErrTo(ParseAddressError)
loadAcctResult = self.accountsGenerator.loadAccount(addressParsed, password,
dir)
return ok loadAcctResult.isOk

View File

@ -1,7 +1,7 @@
{.push raises: [Defect].}
import # std libs
std/[os, tables, typetraits]
std/[os, sets, strformat, strutils, tables, typetraits]
import # vendor libs
sqlcipher, web3, web3/ethtypes
@ -10,43 +10,66 @@ from web3/conversions as web3_conversions import `$`
import # status modules
../private/common,
../private/[accounts/generator/generator, callrpc, database, settings,
token_prices, util]
../private/[accounts/generator/generator, callrpc, database, events, settings,
token_prices, util, waku]
from ../private/conversions import parseAddress, readValue, writeValue
from ../private/extkeys/types import Mnemonic
from ../private/opensea import Asset, AssetContract, Collection
export
`$`, common, database, ethtypes, generator, Mnemonic, parseAddress, readValue,
sqlcipher, util, writeValue
export # modules
common, database, ethtypes, events, generator, sqlcipher, util
export # symbols
`$`, Asset, AssetContract, Collection, Mnemonic, parseAddress, readValue,
writeValue
type
StatusObject* = ref object
accountsGenerator*: Generator
accountsDbConn: DbConn
dataDir*: string
userDbConn: DbConn
web3Conn: Table[string, Web3]
priceMap*: PriceMap
LoginState* = enum loggedout, loggingin, loggedin, loggingout
NetworkState* = enum offline, connecting, online, disconnecting
StatusObject* = ref object
accountsDbConn: DbConn
accountsGenerator*: Generator
dataDir*: string
loginState: LoginState
networkState: NetworkState
priceMap*: PriceMap
signalHandler*: StatusSignalHandler
topics*: OrderedSet[common.ContentTopic]
userDbConn: DbConn
wakuFilter*: bool
wakuFilterHandler*: ContentFilterHandler
wakuFilternode*: string
wakuHistoryHandler*: QueryHandlerFunc
wakuLightpush*: bool
wakuLightpushnode*: string
wakuNode*: WakuNode
wakuPubSubTopics*: seq[string]
wakuRlnRelay*: bool
wakuStore*: bool
wakuStorenode*: string
web3Conn: Table[string, Web3]
proc new*(T: type StatusObject, dataDir: string,
accountsDbFileName: string = "accounts.sql"): DbResult[T] =
accountsDbFileName = "accounts.sql",
signalHandler = defaultStatusSignalHandler):
DbResult[T] =
let
accountsDb = ?initDb(dataDir / accountsDbFileName).mapErrTo(
InitFailure)
accountsDb = ?initDb(dataDir / accountsDbFileName).mapErrTo(InitFailure)
generator = Generator.new()
ok T(accountsDbConn: accountsDb, dataDir: dataDir, accountsGenerator: generator,
web3Conn: initTable[string, Web3](), priceMap: newTable[string, ToPriceMap]())
ok T(accountsDbConn: accountsDb, accountsGenerator: generator,
dataDir: dataDir, loginState: loggedout, networkState: offline,
priceMap: newTable[string, ToPriceMap](), signalHandler: signalHandler,
wakuPubSubTopics: @[waku.DefaultTopic], wakuStore: true,
web3Conn: initTable[string, Web3]())
proc accountsDb*(self: StatusObject): DbConn {.raises: [].} =
self.accountsDbConn
proc isLoggedIn*(self: StatusObject): bool {.raises: [].} =
not distinctBase(self.userDbConn).isNil and self.userDbConn.isOpen
proc userDb*(self: StatusObject): DbResult[DbConn] {.raises: [].} =
if distinctBase(self.userDbConn).isNil:
return err NotInitialized
@ -60,15 +83,6 @@ proc closeUserDb*(self: StatusObject): DbResult[void] {.raises: [].} =
self.userDbConn = nil
ok()
proc close*(self: StatusObject): DbResult[void] {.raises: [].} =
if self.isLoggedIn:
?self.closeUserDb()
try:
self.accountsDb.close()
ok()
except SqliteError, Exception:
err CloseFailure
proc initUserDb*(self: StatusObject, keyUid, password: string): DbResult[void] =
self.userDbConn = ?initDb(self.dataDir / keyUid & ".db", password)
ok()
@ -104,3 +118,27 @@ proc web3*(self: StatusObject): Web3Result[Web3] =
.mapErrTo(Web3Error(kind: web3Internal, internalError: NotFound))
ok web3Conn
proc loginState*(self: StatusObject): LoginState =
self.loginState
proc setLoginState*(self: StatusObject, state: LoginState) {.raises: [].} =
self.loginState = state
proc networkState*(self: StatusObject): NetworkState =
self.networkState
proc setNetworkState*(self: StatusObject, state: NetworkState) {.raises: [].} =
self.networkState = state
# this and logic around login/out and dis/connect needs to be reconsidered
proc close*(self: StatusObject): DbResult[void] {.raises: [].} =
if self.loginState == LoginState.loggedin:
?self.closeUserDb()
self.setLoginState(LoginState.loggedout)
try:
self.accountsDb.close()
ok()
except SqliteError, Exception:
err CloseFailure

View File

@ -47,7 +47,7 @@ type
proc callRpc*(self: StatusObject, rpcMethod: string, params: JsonNode):
Future[ProviderResult[JsonNode]] {.async.} =
if not self.isLoggedIn:
if self.loginState != LoginState.loggedin:
return err ProviderError(kind: pApi, apiError: MustBeLoggedIn)
let web3Result = self.web3
@ -72,7 +72,7 @@ proc sendTransaction*(self: StatusObject, fromAddress: EthAddress,
transaction: Transaction, password: string, dir: string):
Future[ProviderResult[JsonNode]] {.async.} =
if not self.isLoggedIn:
if self.loginState != LoginState.loggedin:
return err ProviderError(kind: pApi, apiError: MustBeLoggedIn)
let db = self.userDb()
@ -129,5 +129,7 @@ proc sendTransaction*(self: StatusObject, fromAddress: EthAddress,
let
ogRpcError = error.rpcError
rpcError = RpcError(code: ogRpcError.code, message: ogRpcError.message)
return err ProviderError(kind: pRpc, rpcError: rpcError)
return ok respResult.get

View File

@ -4,8 +4,7 @@ import # status modules
../private/[settings, util],
./common
export
common
export common except setLoginState, setNetworkState
type
SettingsError* = enum
@ -16,10 +15,11 @@ type
SettingsResult*[T] = Result[T, SettingsError]
proc getSettings*(self: StatusObject): SettingsResult[Settings] =
if not self.isLoggedIn:
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
let
userDb = ?self.userDb.mapErrTo(UserDbError)
settings = ?userDb.getSettings.mapErrTo(GetSettingsError)
ok settings
ok settings

View File

@ -10,8 +10,8 @@ import # status modules
../private/[util, token_prices, tokens],
./common
export
common, tokens
export common except setLoginState, setNetworkState
export tokens
type
CustomTokenError* = enum
@ -29,7 +29,7 @@ type
proc addCustomToken*(self: StatusObject, address: Address, name, symbol,
color: string, decimals: uint): CustomTokenResult[Token] =
if not self.isLoggedIn:
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
let token = Token(address: address, name: name, symbol: symbol, color: color,
@ -43,7 +43,7 @@ proc addCustomToken*(self: StatusObject, address: Address, name, symbol,
proc deleteCustomToken*(self: StatusObject, address: Address):
CustomTokenResult[Address] =
if not self.isLoggedIn:
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
let userDb = ?self.userDb.mapErrTo(UserDbError)
@ -52,16 +52,20 @@ proc deleteCustomToken*(self: StatusObject, address: Address):
ok address
proc getCustomTokens*(self: StatusObject): CustomTokenResult[seq[Token]] =
if not self.isLoggedIn:
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
let
userDb = ?self.userDb.mapErrTo(UserDbError)
tokens = ?userDb.getCustomTokens().mapErrTo(GetFailure)
ok tokens
proc getPrice*(self: StatusObject, tokenSymbol: string, fiatCurrency: string):
CustomTokenResult[float] {.raises: [ref KeyError].} =
CustomTokenResult[float] {.raises: [Defect, ref KeyError].} =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
if not contains(self.priceMap, tokenSymbol) or
not contains(self.priceMap[tokenSymbol], fiatCurrency):
@ -72,6 +76,9 @@ proc getPrice*(self: StatusObject, tokenSymbol: string, fiatCurrency: string):
proc updatePrices*(self: StatusObject): Future[CustomTokenResult[void]]
{.async.} =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
let tokensResult = self.getCustomTokens()
if tokensResult.isErr: return err tokensResult.error
let tokens = tokensResult.get

524
status/api/waku.nim Normal file
View File

@ -0,0 +1,524 @@
{.push raises: [Defect].}
import # std libs
std/[options, sequtils, sets, strutils, sugar]
from times import getTime, toUnix
import # vendor libs
chronicles, chronos, eth/keys as eth_keys, nimcrypto/pbkdf2, stew/byteutils
# status libs
import ../private/waku except ContentTopic
import
../private/[alias, protocol, util],
./accounts, ./common
export common except setLoginState, setNetworkState
export waku except ContentTopic
logScope:
topics = "status_api"
type
Nodekey* = waku.crypto.PrivateKey
WakuError* = enum
InvalidKey = "waku: invalid private key"
MustBeLoggedIn = "waku: operation not permitted, must be logged in"
MustBeOffline = "waku: operation not permitted, must be offline"
MustBeOnline = "waku: operation not permitted, must be online"
NoChatAccount = "waku: could not retrieve chat account"
NoChatAccountName = "waku: could not retrieve name of chat account"
NoSendUnsuspportedApp = "waku: cannot send message to unsupported " &
"content topic"
SendNotSupportedWaku1 = "waku: sending messages to public chats is not " &
"currently supported"
WakuResult*[T] = Result[T, WakuError]
proc init*(T: type Nodekey): T =
Nodekey.random(Secp256k1, waku.keys.newRng()[]).expect(
"random key generation should never fail")
proc fromHex*(T: type Nodekey, key: string): Result[T, WakuError] =
if not (key.len == 64 or key.len == 66): return err InvalidKey
var hex: string
if key[0..1] == "0x":
hex = key
else:
hex = "0x" & key
if hex.len == 66 and isHexString(hex).get(false):
let skkey = ?SkPrivateKey.init(
waku.utils.fromHex(hex[2..^1])).mapErrTo(InvalidKey)
ok Nodekey(scheme: Secp256k1, skkey: skkey)
else:
err InvalidKey
proc handleChat2Message(self: StatusObject, message: WakuMessage,
contentTopic: ContentTopic, pubSubTopic: waku.Topic) {.async.} =
let
chat2MessageResult = Chat2Message.decode(message.payload)
timestamp = getTime().toUnix
if chat2MessageResult.isOk:
let
chat2Message = chat2MessageResult.get
event = Chat2MessageEvent(data: chat2Message, timestamp: timestamp,
topic: contentTopic)
asyncSpawn self.signalHandler((event: event,
kind: StatusEventKind.chat2Message))
else:
let
chat2MessageError = chat2MessageResult.error
event = Chat2MessageErrorEvent(error: chat2MessageError,
timestamp: timestamp, topic: contentTopic)
asyncSpawn self.signalHandler((event: event,
kind: StatusEventKind.chat2MessageError))
proc handleWaku1Message(self: StatusObject, message: WakuMessage,
contentTopic: ContentTopic, pubSubTopic: waku.Topic) {.async.} =
# currently we only support public chat messages
var
ctx: HMAC[sha256]
salt: seq[byte] = @[]
shortName = contentTopic.shortName
symKey: SymKey
let timestamp = getTime().toUnix
if shortName.startsWith('#'): shortName = shortName[1..^1]
if pbkdf2(ctx, shortName.toBytes(), salt, 65356, symKey) != sizeof(SymKey):
let
publicChatMessageError = PublicChatMessageError.BadKey
event = PublicChatMessageErrorEvent(error: publicChatMessageError,
timestamp: timestamp, topic: contentTopic)
asyncSpawn self.signalHandler((event: event,
kind: StatusEventKind.publicChatMessageError))
else:
let decryptedMessageOption = decode(message.payload,
none[eth_keys.PrivateKey](), some(symKey))
if decryptedMessageOption.isNone:
let
publicChatMessageError = PublicChatMessageError.DecryptFailed
event = PublicChatMessageErrorEvent(error: publicChatMessageError,
timestamp: timestamp, topic: contentTopic)
asyncSpawn self.signalHandler((event: event,
kind: StatusEventKind.publicChatMessageError))
else:
let decryptedMessage = decryptedMessageOption.get
try:
let
protoMessage = protocol.ProtocolMessage.decode(
decryptedMessage.payload)
appMetaMessage = protocol.ApplicationMetadataMessage.decode(
protoMessage.public_message)
chatMessage = protocol.ChatMessage.decode(
appMetaMessage.payload)
pubkeyOption = decryptedMessage.src
if pubkeyOption.isNone:
let
publicChatMessageError = PublicChatMessageError.NoPublicKey
event = PublicChatMessageErrorEvent(error: publicChatMessageError,
timestamp: timestamp, topic: contentTopic)
asyncSpawn self.signalHandler((event: event,
kind: StatusEventKind.publicChatMessageError))
else:
let
pubkey = pubkeyOption.get
aliasResult = generateAlias(
"0x04" & byteutils.toHex(pubkey.toRaw()))
if aliasResult.isErr:
let
publicChatMessageError = PublicChatMessageError.NoAlias
event = PublicChatMessageErrorEvent(error: publicChatMessageError,
timestamp: timestamp, topic: contentTopic)
asyncSpawn self.signalHandler((event: event,
kind: StatusEventKind.publicChatMessageError))
else:
let
alias = aliasResult.get
timestamp = chatMessage.timestamp.int64 div 1000.int64
publicChatMessage = PublicChatMessage(alias: alias,
message: chatMessage, pubkey: pubkey, timestamp: timestamp)
event = PublicChatMessageEvent(data: publicChatMessage,
timestamp: timestamp, topic: contentTopic)
asyncSpawn self.signalHandler((event: event,
kind: StatusEventKind.publicChatMessage))
except ProtobufReadError as e:
let
publicChatMessageError = PublicChatMessageError.DecodeFailed
event = PublicChatMessageErrorEvent(error: publicChatMessageError,
timestamp: timestamp, topic: contentTopic)
asyncSpawn self.signalHandler((event: event,
kind: StatusEventKind.publicChatMessageError))
proc handleAppMessage(self: StatusObject, message: WakuMessage,
contentTopic: ContentTopic, pubSubTopic: waku.Topic) {.async.} =
# we know how to handle messages for only some content topics:
# * `/toy-chat/2/{topic}/proto`
# * `/waku/1/{topic-digest}/rfc26`
if contentTopic.isChat2:
asyncSpawn self.handleChat2Message(message, contentTopic, pubSubTopic)
elif contentTopic.isWaku1:
asyncSpawn self.handleWaku1Message(message, contentTopic, pubSubTopic)
else:
trace "ignored message for unsupported app", contentTopic, pubSubTopic
proc handleWakuMessage(self: StatusObject, pubSubTopic: waku.Topic,
message: WakuMessage) {.async.} =
let contentTopicResult = ContentTopic.init(message.contentTopic)
if contentTopicResult.isErr:
error "received WakuMessage with invalid content topic",
contentTopic=message.contentTopic, pubSubTopic
return
var contentTopic = contentTopicResult.get
# use our util.includes as a workaround for what seems to be a bug in
# Nim's std/sets.contains
if self.topics.includes(contentTopic):
# is there a better way to do it? `[]` proc doesn't exist for OrderedSet
let h = contentTopic.hash
for t in self.topics:
if t.hash == h: contentTopic.shortName = t.shortName
asyncSpawn self.handleAppMessage(message, contentTopic, pubSubTopic)
else:
trace "ignored message for unjoined topic", contentTopic,
joined=self.topics, pubSubTopic
proc getTopics*(self: StatusObject): seq[ContentTopic] =
self.topics.toSeq
proc joinTopic*(self: StatusObject, topic: ContentTopic) =
self.topics.incl(topic)
proc leaveTopic*(self: StatusObject, topic: ContentTopic) =
self.topics.excl(topic)
proc lightpushHandler(response: PushResponse) {.gcsafe.} =
trace "received lightpush response", response
proc sendChat2Message(self: StatusObject, message: string, topic: ContentTopic):
Future[WakuResult[void]] {.async.} =
var nick: string
let chatAccountResult = self.getChatAccount()
if chatAccountResult.isOk:
let chatAccount = chatAccountResult.get
if chatAccount.name.isSome:
nick = chatAccount.name.get
else:
return err NoChatAccountName
else:
return err NoChatAccount
let
chat2protobuf = Chat2Message.init(nick, message).encode()
payload = chat2protobuf.buffer
wakuMessage = WakuMessage(payload: payload, contentTopic: $topic,
version: 0)
if not self.wakuNode.wakuLightPush.isNil():
for pTopic in self.wakuPubSubTopics:
asyncSpawn self.wakuNode.lightpush(pTopic, wakuMessage, lightpushHandler)
else:
for pTopic in self.wakuPubSubTopics:
asyncSpawn self.wakuNode.publish(pTopic, wakuMessage, self.wakuRlnRelay)
return ok()
proc sendWaku1Message(self: StatusObject, message: string, topic: ContentTopic):
Future[WakuResult[void]] {.async.} =
return err SendNotSupportedWaku1
proc sendMessage*(self: StatusObject, message: string, topic: ContentTopic):
Future[WakuResult[void]] {.async.} =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
if self.networkState != NetworkState.online:
return err MustBeOnline
# we know how to send messages for only some content topics:
# * `/toy-chat/2/{topic}/proto`
# * `/waku/1/{topic-digest}/rfc26`
if topic.isChat2:
return await self.sendChat2Message(message, topic)
elif topic.isWaku1:
return await self.sendWaku1Message(message, topic)
else:
return err NoSendUnsuspportedApp
proc addFiltersImpl(self: StatusObject, topics: seq[ContentTopic]):
Future[WakuResult[void]] {.async.} =
if not self.wakuNode.wakuFilter.isNil():
let contentFilters = collect(newSeq):
for topic in topics:
ContentFilter(contentTopic: $topic)
for pTopic in self.wakuPubSubTopics:
await self.wakuNode.subscribe(FilterRequest(
contentFilters: contentFilters, pubSubTopic: pTopic, subscribe: true),
self.wakuFilterHandler)
else:
warn "cannot subscribe to filter requests when node's filter is nil"
return ok()
proc addFilters*(self: StatusObject, topics: seq[ContentTopic]):
Future[WakuResult[void]] {.async.} =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
if self.networkState != NetworkState.online:
return err MustBeOnline
return await self.addFiltersImpl(topics)
proc removeFiltersImpl(self: StatusObject, topics: seq[ContentTopic]):
Future[WakuResult[void]] {.async.} =
if not self.wakuNode.wakuFilter.isNil():
let contentFilters = collect(newSeq):
for topic in topics:
ContentFilter(contentTopic: $topic)
for pTopic in self.wakuPubSubTopics:
await self.wakuNode.unsubscribe(FilterRequest(
contentFilters: contentFilters, pubSubTopic: pTopic, subscribe: false))
else:
warn "cannot unsubscribe from filter requests when node's filter is nil"
return ok()
proc removeFilters*(self: StatusObject, topics: seq[ContentTopic]):
Future[WakuResult[void]] {.async.} =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
if self.networkState != NetworkState.online:
return err MustBeOnline
return await self.removeFiltersImpl(topics)
proc queryHistory*(self: StatusObject, topics: seq[ContentTopic]):
Future[WakuResult[void]] {.async.} =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
if self.networkState != NetworkState.online:
return err MustBeOnline
let contentFilters = collect(newSeq):
for topic in topics:
HistoryContentFilter(contentTopic: $topic)
await self.wakuNode.query(HistoryQuery(contentFilters: contentFilters),
self.wakuHistoryHandler)
return ok()
proc connect*(self: StatusObject, nodekey = Nodekey.init(),
extIp = none[ValidIpAddress](), extTcpPort = none[Port](),
extUdpPort = none[Port](), bindIp = ValidIpAddress.init("0.0.0.0"),
bindTcpPort = Port(60000), bindUdpPort = Port(60000), portsShift = 0.uint16,
pubSubTopics = @[waku.DefaultTopic], rlnRelay = false, relay = true,
fleet = WakuFleet.prod, staticnodes: seq[string] = @[], swapProtocol = true,
filternode = "", lightpushnode = "", store = true, storenode = "",
keepalive = false):
Future[WakuResult[void]] {.async.} =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
if self.networkState != NetworkState.offline:
return err MustBeOffline
self.setNetworkState(NetworkState.connecting)
let
filter = filternode != ""
lightpush = lightpushnode != ""
wakuNode = WakuNode.new(nodekey, bindIp,
Port(bindTcpPort.uint16 + portsShift), extIp, extTcpPort)
self.wakuFilter = filter
self.wakuFilternode = filternode
self.wakuLightpush = lightpush
self.wakuLightpushnode = lightpushnode
self.wakuNode = wakuNode
self.wakuPubSubTopics = pubSubTopics
self.wakuRlnRelay = rlnRelay
self.wakuStore = store
self.wakuStorenode = storenode
await wakuNode.start()
wakuNode.mountRelay(pubSubTopics, rlnRelayEnabled = rlnRelay,
relayMessages = relay)
wakuNode.mountLibp2pPing()
if staticnodes.len > 0:
info "connecting to static peers", nodes=staticnodes
await wakuNode.connectToNodes(staticnodes)
elif fleet != WakuFleet.none:
info "static peers not configured, choosing one at random", fleet
let node = await selectRandomNode($fleet)
info "connecting to peer", node
await wakuNode.connectToNodes(@[node])
if swapProtocol: wakuNode.mountSwap()
if filter:
proc filterHandler(message: WakuMessage) {.gcsafe, closure.} =
try:
discard self.handleWakuMessage(waku.DefaultTopic, message)
except CatchableError as e:
error "waku filter handler encountered an unknown error", error=e.msg
self.wakuFilterHandler = filterHandler
wakuNode.mountFilter()
wakuNode.wakuFilter.setPeer(parsePeerInfo(filternode))
(await self.addFiltersImpl(self.getTopics)).expect(
"addFilters is not expected to fail in this context")
if lightpush:
wakuNode.mountLightPush()
wakuNode.wakuLightPush.setPeer(parsePeerInfo(lightpushnode))
if store or storenode != "":
proc historyHandler(response: HistoryResponse) {.gcsafe, closure.} =
for message in response.messages:
try:
discard self.handleWakuMessage(waku.DefaultTopic, message)
except CatchableError as e:
error "waku history handler encountered an unknown error", error=e.msg
self.wakuHistoryHandler = historyHandler
wakuNode.mountStore(persistMessages = false)
var snode: Option[string]
if storenode != "":
snode = some(storenode)
elif fleet != WakuFleet.none:
info "store nodes not configured, choosing one at random", fleet
snode = some(await selectRandomNode($fleet))
if snode.isNone:
warn "unable to determine a storenode, no connection made"
else:
info "connecting to storenode", storenode=snode
wakuNode.wakuStore.setPeer(parsePeerInfo(snode.get()))
if relay:
proc relayHandler(pubSubTopic: waku.Topic, data: seq[byte])
{.async, gcsafe.} =
let decoded = WakuMessage.init(data)
if decoded.isOk():
let message = decoded.get()
asyncSpawn self.handleWakuMessage(pubSubTopic, message)
else:
error "received invalid WakuMessage", error=decoded.error, pubSubTopic
for pTopic in pubSubTopics:
wakuNode.subscribe(pTopic, relayHandler)
if keepAlive: wakuNode.startKeepalive()
self.setNetworkState(NetworkState.online)
return ok()
proc disconnect*(self: StatusObject): Future[WakuResult[void]] {.async.} =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
if self.networkState != NetworkState.online:
return err MustBeOnline
self.setNetworkState(NetworkState.disconnecting)
if self.wakuFilter:
(await self.removeFiltersImpl(self.getTopics)).expect(
"removeFilters is not expected to fail in this context")
for pTopic in self.wakuPubSubTopics:
self.wakuNode.unsubscribeAll(pTopic)
await self.wakuNode.stop()
self.wakuFilter = false
self.wakuFilternode = ""
self.wakuLightpush = false
self.wakuLightpushnode = ""
self.wakuNode = nil
self.wakuPubSubTopics = @[waku.DefaultTopic]
self.wakuRlnRelay = false
self.wakuStore = true
self.wakuStorenode = ""
self.setNetworkState(NetworkState.offline)
return ok()

View File

@ -12,8 +12,8 @@ import # status modules
../private/extkeys/[paths, types],
./auth, ./common
export
accounts, common
export accounts
export common except setLoginState, setNetworkState
# TODO: do we still need these exports?
# auth, conversions, paths, secp256k1, settings, types, uuid
@ -104,10 +104,10 @@ proc storeDerivedAccount(self: StatusObject, id: UUID, path: KeyPath, name,
return self.storeWalletAccount(name, address, publicKey.some, accountType,
path)
proc addWalletAccount*(self: StatusObject, name, password,
dir: string): WalletResult[accounts.Account] =
proc addWalletAccount*(self: StatusObject, name, password, dir: string):
WalletResult[accounts.Account] =
if not self.isLoggedIn:
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
let
@ -159,6 +159,9 @@ proc storeImportedWalletAccount(self: StatusObject, privateKey: SkSecretKey,
proc addWalletPrivateKey*(self: StatusObject, privateKeyHex: string,
name, password, dir: string): WalletResult[accounts.Account] =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
var privateKeyStripped = privateKeyHex
privateKeyStripped.removePrefix("0x")
@ -171,6 +174,9 @@ proc addWalletPrivateKey*(self: StatusObject, privateKeyHex: string,
proc addWalletSeed*(self: StatusObject, mnemonic: Mnemonic, name, password,
dir, bip39Passphrase: string): WalletResult[accounts.Account] =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
let isPasswordValid = ?self.validatePassword(password, dir).mapErrTo(
PasswordValidationError)
if not isPasswordValid:
@ -182,8 +188,11 @@ proc addWalletSeed*(self: StatusObject, mnemonic: Mnemonic, name, password,
return self.storeDerivedAccount(imported.id, PATH_DEFAULT_WALLET, name,
password, dir, AccountType.Seed)
proc addWalletWatchOnly*(self: StatusObject, address: Address,
name: string): WalletResult[accounts.Account] =
proc addWalletWatchOnly*(self: StatusObject, address: Address, name: string):
WalletResult[accounts.Account] =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
return self.storeWalletAccount(name, address, SkPublicKey.none,
AccountType.Watch, PATH_DEFAULT_WALLET)
@ -191,6 +200,9 @@ proc addWalletWatchOnly*(self: StatusObject, address: Address,
proc deleteWalletAccount*(self: StatusObject, address: Address,
password, dir: string): WalletResult[accounts.Account] =
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
let isPasswordValid = ?self.validatePassword(password, dir).mapErrTo(
PasswordValidationError)
if not isPasswordValid:
@ -207,18 +219,17 @@ proc deleteWalletAccount*(self: StatusObject, address: Address,
ok deleted.get
proc toWalletAccount(account: accounts.Account): WalletAccount {.used.} =
let name = if account.name.isNone: "" else: account.name.get
WalletAccount(address: account.address, name: name)
proc getWalletAccounts*(self: StatusObject): WalletResult[seq[WalletAccount]] =
if not self.isLoggedIn:
if self.loginState != LoginState.loggedin:
return err MustBeLoggedIn
let
userDb = ?self.userDb.mapErrTo(UserDbError)
walletAccts = ?userDb.getWalletAccounts.mapErrTo(GetWalletError)
accounts = walletAccts.map(a => a.toWalletAccount)
ok accounts

View File

@ -112,6 +112,9 @@ proc deleteWalletAccount*(db: DbConn, address: Address):
var tblAccounts: Account
let account = ?db.getWalletAccount(address)
if account.isSome:
if account.get.wallet.isSome and
account.get.wallet.get: return ok none(Account)
let query = fmt"""DELETE FROM {tblAccounts.tableName}
WHERE {tblAccounts.address.columnName} = ?
AND {tblAccounts.wallet.columnName} = FALSE"""

View File

@ -1,6 +1,12 @@
import
import # std libs
std/[hashes, strutils]
import # vendor libs
stew/results
import # status modules
./util
export results
type
@ -8,6 +14,16 @@ type
StatusError* = object of CatchableError
ContentTopic* = object
appName*: string
appVersion*: string
encoding*: string
shortName*: string
topicName*: string
ContentTopicError* = enum
invalid = "invalid content topic"
DbError* = enum
CloseFailure = "db: failed to close database"
DataAndTypeMismatch = "db: failed to deserialise data to supplied type"
@ -64,3 +80,63 @@ type
rpcError*: RpcError
Web3Result*[T] = Result[T, Web3Error]
const noTopic* = ContentTopic()
proc `$`*(t: ContentTopic): string =
"/" & t.appName & "/" & t.appVersion & "/" & t.topicName & "/" & t.encoding
proc hash*(t: ContentTopic): Hash =
hash $t
proc init*(T: type ContentTopic, t: string, s: string = ""):
Result[ContentTopic, ContentTopicError] =
var
appName: string
appVersion: string
encoding: string
shortName = s
topicName: string
topic = t.strip()
if topic == "" or topic == "#":
return err ContentTopicError.invalid
else:
let topicSplit = topic.split('/')
if topic.startsWith('/') and topicSplit.len == 5:
appName = topicSplit[1]
appVersion = topicSplit[2]
topicName = topicSplit[3]
encoding = topicSplit[4]
else:
if topic.startsWith('#'): topic = topic[1..^1]
appName = "waku"
appVersion = "1"
topicName = "0x" & ($keccak256.digest(topic))[0..7].toLowerAscii
encoding = "rfc26"
if shortName == "":
shortName = "#" & topic
elif not shortName.startsWith('#'):
shortName = "#" & shortName
ok(T(appName: appName, appVersion: appVersion, encoding: encoding,
shortName: shortName, topicName: topicName))
proc isChat2*(t: ContentTopic): bool =
t.appName == "toy-chat" and
t.appVersion == "2" and
t.encoding == "proto"
proc isWaku1*(t: ContentTopic): bool =
t.appName == "waku" and
t.appVersion == "1" and
isHexString(t.topicName).get(false) and
t.topicName.toLowerAscii == t.topicName and
t.encoding == "rfc26"

73
status/private/events.nim Normal file
View File

@ -0,0 +1,73 @@
{.push raises: [Defect].}
import # vendor libs
chronicles, json_serialization
import # status modules
./common, ./protocol
logScope:
topics = "status_private"
type
StatusEvent* = ref object of RootObj
timestamp*: int64
StatusDataEvent*[T] = ref object of StatusEvent
data*: T
StatusErrorEvent*[T] = ref object of StatusEvent
error*: T
StatusEventKind* = enum
chat2Message
chat2MessageError
publicChatMessage
publicChatMessageError
StatusSignal* = tuple[event: StatusEvent, kind: StatusEventKind]
StatusSignalHandler* = proc(signal: StatusSignal):
Future[void] {.gcsafe, nimcall.}
StatusMessageEvent*[T] = ref object of StatusDataEvent[T]
topic*: ContentTopic
StatusMessageErrorEvent*[T] = ref object of StatusErrorEvent[T]
topic*: ContentTopic
Chat2MessageEvent* = StatusMessageEvent[Chat2Message]
Chat2MessageErrorEvent* = StatusMessageErrorEvent[Chat2MessageError]
PublicChatMessageEvent* = StatusMessageEvent[PublicChatMessage]
PublicChatMessageErrorEvent* = StatusMessageErrorEvent[PublicChatMessageError]
proc encode[T](arg: T): string {.raises: [Defect, IOError].} =
arg.toJson(typeAnnotations = true)
const defaultStatusSignalHandler*: StatusSignalHandler =
proc(signal: StatusSignal) {.async, gcsafe, nimcall.} =
let kind = signal.kind
try:
case kind:
of chat2Message:
let data = cast[Chat2MessageEvent](signal.event).data
trace "received data signal", kind, data
of chat2MessageError:
let error = cast[Chat2MessageErrorEvent](signal.event).error
trace "received error signal", kind, error
of publicChatMessage:
let data = cast[PublicChatMessageEvent](signal.event).data
trace "received data signal", kind, data
of publicChatMessageError:
let error = cast[PublicChatMessageErrorEvent](signal.event).error
trace "received error signal", kind, error
except IOError as e:
error "failed to encode signal.event for log", kind, error=e.msg

View File

@ -1,5 +1,6 @@
import # status modules
./protocol/[application_metadata_message, chat_message,
./protocol/[application_metadata_message, chat_message, chat2_message,
encryption/protocol_message]
export application_metadata_message, chat_message, protocol_message
export application_metadata_message, chat_message, chat2_message,
protocol_message

View File

@ -1,3 +1,5 @@
{.push raises: [Defect].}
import # vendor libs
protobuf_serialization
@ -7,7 +9,9 @@ export protobuf_serialization
# automatically exports `type ApplicationMetadataMessage`
import_proto3 "protobuf/application_metadata_message.proto"
proc decode*(T: type ApplicationMetadataMessage, input: seq[byte]): T =
proc decode*(T: type ApplicationMetadataMessage, input: seq[byte]):
T {.raises: [Defect, ProtobufReadError].} =
Protobuf.decode(input, T)
proc encode*(input: ApplicationMetadataMessage): seq[byte] =

View File

@ -0,0 +1,53 @@
{.push raises: [Defect].}
import # std libs
std/times
import # vendor libs
libp2p/protobuf/minprotobuf
import # status modules
../util
export ProtoBuffer, ProtoResult
type
Chat2Message* = object
nick*: string
payload*: seq[byte]
timestamp*: int64
Chat2MessageError* = enum
GetFieldError = "chat2: error getting field from protobuffer"
proc decode*(T: type Chat2Message, input: seq[byte]):
Result[Chat2Message, Chat2MessageError] =
var
msg = Chat2Message()
timestamp: uint64
let pb = input.initProtoBuffer
discard ? pb.getField(1, timestamp).mapErrTo(GetFieldError)
msg.timestamp = int64(timestamp)
discard ? pb.getField(2, msg.nick).mapErrTo(GetFieldError)
discard ? pb.getField(3, msg.payload).mapErrTo(GetFieldError)
ok(msg)
proc encode*(message: Chat2Message): ProtoBuffer =
var pb = initProtoBuffer()
pb.write(1, uint64(message.timestamp))
pb.write(2, message.nick)
pb.write(3, message.payload)
return pb
proc init*(T: type Chat2Message, nick: string, message: string): T =
let
payload = message.toBytes
timestamp = getTime().toUnix
T(nick: nick, payload: payload, timestamp: timestamp)

View File

@ -1,5 +1,7 @@
{.push raises: [Defect].}
import # vendor libs
protobuf_serialization
eth/keys, protobuf_serialization
export protobuf_serialization
@ -7,7 +9,25 @@ export protobuf_serialization
# automatically exports `type ChatMessage`
import_proto3 "protobuf/chat_message.proto"
proc decode*(T: type ChatMessage, input: seq[byte]): T =
type
PublicChatMessage* = object
alias*: string
message*: ChatMessage
pubkey*: PublicKey
timestamp*: int64
PublicChatMessageError* = enum
BadKey = "pubchat: symmetric key derived from content topic had " &
"incorrect length"
DecodeFailed = "pubchat: failed to decode message protobuf"
DecryptFailed = "pubchat: failed to decrypt message payload"
NoAlias = "pubchat: failed to generate alias from public key in " &
"message"
NoPublicKey = "pubchat: failed to get public key from message"
proc decode*(T: type ChatMessage, input: seq[byte]):
T {.raises: [Defect, ProtobufReadError].} =
Protobuf.decode(input, T)
proc encode*(input: ChatMessage): seq[byte] =

View File

@ -1,3 +1,5 @@
{.push raises: [Defect].}
import # vendor libs
protobuf_serialization
@ -7,7 +9,9 @@ export protobuf_serialization
# automatically exports `type ProtocolMessage`
import_proto3 "protobuf/protocol_message.proto"
proc decode*(T: type ProtocolMessage, input: seq[byte]): T =
proc decode*(T: type ProtocolMessage, input: seq[byte]):
T {.raises: [Defect, ProtobufReadError].} =
Protobuf.decode(input, T)
proc encode*(input: ProtocolMessage): seq[byte] =

View File

@ -1,7 +1,7 @@
{.push raises: [Defect].}
import # std libs
std/[re, strutils, tables, unicode]
std/[re, sets, strutils, tables, unicode]
import # vendor libs
nimcrypto, stew/results, web3/ethtypes
@ -88,3 +88,13 @@ proc mapErrTo*[T, E1, E2](r: Result[T, E1], t: Table[E1, E2], default: E2):
return t[e]
except KeyError:
return default)
proc includes*[T](s: OrderedSet[T], key: T): bool =
# There seems to be a bug in Nim's std/sets.contains where on a non-main
# thread matching hashes do not result in a return value of true. Create a
# substitute proc here as a workaround until the bug can be identified and
# fixed.
let k = key.hash
for i in s:
if i.hash == k: return true
return false

View File

@ -1,28 +1,43 @@
# NOTE: Including a top-level {.push raises: [Defect].} here interferes with
# nim-confutils. The compiler will force nim-confutils to annotate its procs
# with the needed `{.raises: [,,,].}` pragmas.
{.push raises: [Defect].}
# imports and exports in this module need to be checked, some don't seem to be
# necessary even though the compiler doesn't warn about unused imports
import # std libs
std/[options, os]
std/[json, options, random, sequtils, strutils, tables, uri]
import # vendor libs
confutils, chronicles, chronos,
libp2p/crypto/[crypto, secp],
eth/keys,
json_rpc/[rpcclient, rpcserver],
stew/shims/net as stewNet,
waku/v2/node/[config, wakunode2],
waku/common/utils/nat
bearssl, chronos, chronos/apps/http/httpclient, eth/keys,
libp2p/[crypto/crypto, crypto/secp, multiaddress, muxers/muxer, peerid,
peerinfo, protocols/protocol, stream/connection, switch],
nimcrypto/utils,
stew/[byteutils, endians2, results, shims/net],
waku/common/utils/nat,
waku/v2/node/wakunode2,
waku/v2/protocol/[waku_filter/waku_filter, waku_lightpush/waku_lightpush,
waku_message, waku_store/waku_store],
waku/v2/utils/peers,
waku/whisper/whisper_types
# The initial implementation of initNode is by intention a minimum viable usage
# of nim-waku v2 from within nim-status
export # modules
byteutils, crypto, keys, nat, net, peers, results, secp, wakunode2,
whisper_types
proc initNode*(config: WakuNodeConf = WakuNodeConf.load()): WakuNode =
type
PrivateKey* = crypto.PrivateKey
Topic* = wakunode2.Topic
WakuFleet* = enum none, prod, test
const DefaultTopic* = "/waku/2/default-waku/proto"
proc selectRandomNode*(fleetStr: string): Future[string] {.async.} =
let
(extIp, extTcpPort, extUdpPort) = setupNat(config.nat, clientId,
Port(uint16(config.tcpPort) + config.portsShift),
Port(uint16(config.udpPort) + config.portsShift))
url = "https://fleets.status.im"
response = await fetch(HttpSessionRef.new(), parseUri(url))
fleet = string.fromBytes(response.data)
nodes = toSeq(
fleet.parseJson(){"fleets", "wakuv2." & fleetStr, "waku"}.pairs())
result = WakuNode.new(config.nodeKey, config.listenAddress,
Port(uint16(config.tcpPort) + config.portsShift), extIp, extTcpPort)
return nodes[rand(nodes.len - 1)].val.getStr()

View File

@ -1,109 +0,0 @@
{.push raises: [Defect].}
import # vendor libs
chronos, confutils,
eth/[keys, p2p]
stew/results,
waku/v1/[protocol/waku_protocol, node/waku_helpers],
waku/common/utils/nat, stew/byteutils, stew/shims/net as stewNet
var connThread: Thread[void]
proc initWakuV1*() {.thread.} =
# Test waku
const clientId = "NimStatusWaku"
let nodeKey = KeyPair.random(keys.newRng()[])
let rng = keys.newRng()
let (ipExt, tcpPortExt, udpPortExt) = setupNat("any", clientId, Port(30307), Port(30307))
let address = if ipExt.isNone(): Address(ip: parseIpAddress("0.0.0.0"), tcpPort: Port(30307),udpPort: Port(30307)) else: Address(ip: ipExt.get(), tcpPort: Port(30307), udpPort: Port(30307))
# Create Ethereum Node
var node = newEthereumNode(nodekey, # Node identifier
address, # Address reachable for incoming requests
1, # Network Id, only applicable for ETH protocol
nil, # Database, not required for Waku
clientId, # Client id string
addAllCapabilities = false, # Disable default all RLPx capabilities
rng = rng)
node.addCapability Waku # Enable only the Waku protocol.
# Set up the Waku configuration.
let wakuConfig = WakuConfig(powRequirement: 0.002,
bloom: some(fullBloom()), # Full bloom filter
isLightNode: false, # Full node
maxMsgSize: waku_protocol.defaultMaxMsgSize,
topics: none(seq[waku_protocol.Topic]) # empty topic interest
)
node.configureWaku(wakuConfig)
let staticNodes = @[
"enode://6e6554fb3034b211398fcd0f0082cbb6bd13619e1a7e76ba66e1809aaa0c5f1ac53c9ae79cf2fd4a7bacb10d12010899b370c75fed19b991d9c0cdd02891abad@47.75.99.169:443",
"enode://436cc6f674928fdc9a9f7990f2944002b685d1c37f025c1be425185b5b1f0900feaf1ccc2a6130268f9901be4a7d252f37302c8335a2c1a62736e9232691cc3a@178.128.138.128:443",
"enode://32ff6d88760b0947a3dee54ceff4d8d7f0b4c023c6dad34568615fcae89e26cc2753f28f12485a4116c977be937a72665116596265aa0736b53d46b27446296a@34.70.75.208:443",
"enode://23d0740b11919358625d79d4cac7d50a34d79e9c69e16831c5c70573757a1f5d7d884510bc595d7ee4da3c1508adf87bbc9e9260d804ef03f8c1e37f2fb2fc69@47.52.106.107:443",
"enode://5395aab7833f1ecb671b59bf0521cf20224fe8162fc3d2675de4ee4d5636a75ec32d13268fc184df8d1ddfa803943906882da62a4df42d4fccf6d17808156a87@178.128.140.188:443",
"enode://5405c509df683c962e7c9470b251bb679dd6978f82d5b469f1f6c64d11d50fbd5dd9f7801c6ad51f3b20a5f6c7ffe248cc9ab223f8bcbaeaf14bb1c0ef295fd0@35.223.215.156:443",
"enode://b957e51f41e4abab8382e1ea7229e88c6e18f34672694c6eae389eac22dab8655622bbd4a08192c321416b9becffaab11c8e2b7a5d0813b922aa128b82990dab@47.75.222.178:443",
"enode://66ba15600cda86009689354c3a77bdf1a97f4f4fb3ab50ffe34dbc904fac561040496828397be18d9744c75881ffc6ac53729ddbd2cdbdadc5f45c400e2622f7@178.128.141.87:443",
"enode://182ed5d658d1a1a4382c9e9f7c9e5d8d9fec9db4c71ae346b9e23e1a589116aeffb3342299bdd00e0ab98dbf804f7b2d8ae564ed18da9f45650b444aed79d509@34.68.132.118:443",
"enode://8bebe73ddf7cf09e77602c7d04c93a73f455b51f24ae0d572917a4792f1dec0bb4c562759b8830cc3615a658d38c1a4a38597a1d7ae3ba35111479fc42d65dec@47.75.85.212:443",
"enode://4ea35352702027984a13274f241a56a47854a7fd4b3ba674a596cff917d3c825506431cf149f9f2312a293bb7c2b1cca55db742027090916d01529fe0729643b@134.209.136.79:443",
"enode://fbeddac99d396b91d59f2c63a3cb5fc7e0f8a9f7ce6fe5f2eed5e787a0154161b7173a6a73124a4275ef338b8966dc70a611e9ae2192f0f2340395661fad81c0@34.67.230.193:443",
"enode://ac3948b2c0786ada7d17b80cf869cf59b1909ea3accd45944aae35bf864cc069126da8b82dfef4ddf23f1d6d6b44b1565c4cf81c8b98022253c6aea1a89d3ce2@47.75.88.12:443",
"enode://ce559a37a9c344d7109bd4907802dd690008381d51f658c43056ec36ac043338bd92f1ac6043e645b64953b06f27202d679756a9c7cf62fdefa01b2e6ac5098e@134.209.136.123:443",
"enode://c07aa0deea3b7056c5d45a85bca42f0d8d3b1404eeb9577610f386e0a4744a0e7b2845ae328efc4aa4b28075af838b59b5b3985bffddeec0090b3b7669abc1f3@35.226.92.155:443",
"enode://385579fc5b14e04d5b04af7eee835d426d3d40ccf11f99dbd95340405f37cf3bbbf830b3eb8f70924be0c2909790120682c9c3e791646e2d5413e7801545d353@47.244.221.249:443",
"enode://4e0a8db9b73403c9339a2077e911851750fc955db1fc1e09f81a4a56725946884dd5e4d11258eac961f9078a393c45bcab78dd0e3bc74e37ce773b3471d2e29c@134.209.136.101:443",
"enode://0624b4a90063923c5cc27d12624b6a49a86dfb3623fcb106801217fdbab95f7617b83fa2468b9ae3de593ff6c1cf556ccf9bc705bfae9cb4625999765127b423@35.222.158.246:443",
"enode://b77bffc29e2592f30180311dd81204ab845e5f78953b5ba0587c6631be9c0862963dea5eb64c90617cf0efd75308e22a42e30bc4eb3cd1bbddbd1da38ff6483e@47.75.10.177:443",
"enode://a8bddfa24e1e92a82609b390766faa56cf7a5eef85b22a2b51e79b333c8aaeec84f7b4267e432edd1cf45b63a3ad0fc7d6c3a16f046aa6bc07ebe50e80b63b8c@178.128.141.249:443",
"enode://a5fe9c82ad1ffb16ae60cb5d4ffe746b9de4c5fbf20911992b7dd651b1c08ba17dd2c0b27ee6b03162c52d92f219961cc3eb14286aca8a90b75cf425826c3bd8@104.154.230.58:443",
"enode://cf5f7a7e64e3b306d1bc16073fba45be3344cb6695b0b616ccc2da66ea35b9f35b3b231c6cf335fdfaba523519659a440752fc2e061d1e5bc4ef33864aac2f19@47.75.221.196:443",
"enode://887cbd92d95afc2c5f1e227356314a53d3d18855880ac0509e0c0870362aee03939d4074e6ad31365915af41d34320b5094bfcc12a67c381788cd7298d06c875@178.128.141.0:443",
"enode://282e009967f9f132a5c2dd366a76319f0d22d60d0c51f7e99795a1e40f213c2705a2c10e4cc6f3890319f59da1a535b8835ed9b9c4b57c3aad342bf312fd7379@35.223.240.17:443",
"enode://13d63a1f85ccdcbd2fb6861b9bd9d03f94bdba973608951f7c36e5df5114c91de2b8194d71288f24bfd17908c48468e89dd8f0fb8ccc2b2dedae84acdf65f62a@47.244.210.80:443",
"enode://2b01955d7e11e29dce07343b456e4e96c081760022d1652b1c4b641eaf320e3747871870fa682e9e9cfb85b819ce94ed2fee1ac458904d54fd0b97d33ba2c4a4@134.209.136.112:443",
"enode://b706a60572634760f18a27dd407b2b3582f7e065110dae10e3998498f1ae3f29ba04db198460d83ed6d2bfb254bb06b29aab3c91415d75d3b869cd0037f3853c@35.239.5.162:443",
"enode://32915c8841faaef21a6b75ab6ed7c2b6f0790eb177ad0f4ea6d731bacc19b938624d220d937ebd95e0f6596b7232bbb672905ee12601747a12ee71a15bfdf31c@47.75.59.11:443",
"enode://0d9d65fcd5592df33ed4507ce862b9c748b6dbd1ea3a1deb94e3750052760b4850aa527265bbaf357021d64d5cc53c02b410458e732fafc5b53f257944247760@178.128.141.42:443",
"enode://e87f1d8093d304c3a9d6f1165b85d6b374f1c0cc907d39c0879eb67f0a39d779be7a85cbd52920b6f53a94da43099c58837034afa6a7be4b099bfcd79ad13999@35.238.106.101:443"
]
connectToNodes(node, staticNodes)
let connectedFut = node.connectToNetwork(@[],
true, # Enable listening
false # Disable discovery (only discovery v4 is currently supported)
)
connectedFut.callback = proc(data: pointer) {.gcsafe.} =
{.gcsafe.}:
if connectedFut.failed:
fatal "connectToNetwork failed", msg = connectedFut.readError.msg
quit(1)
# Code to be executed on receival of a message on filter.
proc handler(msg: ReceivedMessage) =
echo "MSG RECEIVED!"
if msg.decoded.src.isSome():
echo "Received message from ", $msg.decoded.src.get(), ": ",
string.fromBytes(msg.decoded.payload)
let
symKey: SymKey = hexToByteArray[32]("0xa82a520aff70f7a989098376e48ec128f25f767085e84d7fb995a9815eebff0a")
topic = hexToByteArray[4]("0x9c22ff5f") # test
filter = initFilter(symKey = some(symKey), topics = @[topic])
discard node.subscribeFilter(filter, handler)
proc initAsyncThread() =
initWakuV1()
runForever()
proc startWakuV1*() =
connThread.createThread(initAsyncThread)
debug "Async thread created"

View File

@ -19,7 +19,7 @@ procSuite "api":
check statusObjResult.isOk
let statusObj = statusObjResult.get
check:
statusObj.isLoggedIn == false
statusObj.loginState == LoginState.loggedout
statusObj.accountsGenerator != nil
statusObj.dataDir == dataDir
@ -69,7 +69,7 @@ procSuite "api":
check:
# should not be able to log out when not logged in
logoutResult.isErr
statusObj.isLoggedIn == false
statusObj.loginState == LoginState.loggedout
# var getSettingResult =
# statusObj.getSetting(int, SettingsCol.LatestDerivedPath, 0)
@ -108,7 +108,7 @@ procSuite "api":
logoutResult = statusObj.logout()
check:
logoutResult.isOk
statusObj.isLoggedIn == false
statusObj.loginState == LoginState.loggedout
check statusObj.close.isOk

View File

@ -17,5 +17,5 @@ import # test modules
./permissions,
./settings,
./tokens,
./tx_history,
./waku_smoke
./tx_history
# ./waku_smoke