Initial import

This commit is contained in:
Richard Ramos 2021-09-08 14:05:39 -04:00
parent 53d2c323a0
commit 0b24d7a341
No known key found for this signature in database
GPG Key ID: 80D4B01265FDFE8F
107 changed files with 9374 additions and 0 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# nim-status-lib
WIP refactor to extract business logic from status-desktop into a reusable library

52
eventemitter.nim Normal file
View File

@ -0,0 +1,52 @@
import # system libs
tables
import # deps
uuids
type
Args* = ref object of RootObj # ...args
Handler* = proc (args: Args) {.closure.} # callback function type
EventEmitter* = ref object
events: Table[string, Table[UUID, Handler]]
proc createEventEmitter*(): EventEmitter =
result.new
result.events = initTable[string, Table[UUID, Handler]]()
proc on(this: EventEmitter, name: string, handlerId: UUID, handler: Handler): void =
if this.events.hasKey(name):
this.events[name].add handlerId, handler
return
this.events[name] = [(handlerId, handler)].toTable
proc on*(this: EventEmitter, name: string, handler: Handler): void =
var uuid: UUID
this.on(name, uuid, handler)
proc once*(this:EventEmitter, name:string, handler:Handler): void =
var handlerId = genUUID()
this.on(name, handlerId) do(a: Args):
handler(a)
this.events[name].del handlerId
proc emit*(this:EventEmitter, name:string, args:Args): void =
if this.events.hasKey(name):
for (id, handler) in this.events[name].pairs:
handler(args)
when isMainModule:
block:
type ReadyArgs = ref object of Args
text: string
var evts = createEventEmitter()
evts.on("ready") do(a: Args):
var args = ReadyArgs(a)
echo args.text, ": from [1st] handler"
evts.once("ready") do(a: Args):
var args = ReadyArgs(a)
echo args.text, ": from [2nd] handler"
evts.emit("ready", ReadyArgs(text:"Hello, World"))
evts.emit("ready", ReadyArgs(text:"Hello, World"))

9
nim_status_lib.nimble Normal file
View File

@ -0,0 +1,9 @@
mode = ScriptMode.Verbose
version = "0.1.0"
author = "Status Research & Development GmbH"
description = "WIP refactor to extract business logic from status-desktop into a reusable library"
license = "MIT"
skipDirs = @["test"]
requires "nim >= 1.2.0"

86
status/accounts.nim Normal file
View File

@ -0,0 +1,86 @@
import options, chronicles, json, json_serialization, sequtils, sugar
import libstatus/accounts as status_accounts
import libstatus/settings as status_settings
import ./types/[account, fleet, sticker, setting]
import utils
import ../eventemitter
const DEFAULT_NETWORK_NAME* = "mainnet_rpc"
type
AccountModel* = ref object
generatedAddresses*: seq[GeneratedAccount]
nodeAccounts*: seq[NodeAccount]
events: EventEmitter
proc newAccountModel*(events: EventEmitter): AccountModel =
result = AccountModel()
result.events = events
proc generateAddresses*(self: AccountModel): seq[GeneratedAccount] =
var accounts = status_accounts.generateAddresses()
for account in accounts.mitems:
account.name = status_accounts.generateAlias(account.derived.whisper.publicKey)
account.identicon = status_accounts.generateIdenticon(account.derived.whisper.publicKey)
self.generatedAddresses.add(account)
result = self.generatedAddresses
proc openAccounts*(self: AccountModel): seq[NodeAccount] =
result = status_accounts.openAccounts()
proc login*(self: AccountModel, selectedAccountIndex: int, password: string): NodeAccount =
let currentNodeAccount = self.nodeAccounts[selectedAccountIndex]
result = status_accounts.login(currentNodeAccount, password)
proc storeAccountAndLogin*(self: AccountModel, fleetConfig: FleetConfig, selectedAccountIndex: int, password: string): Account =
let generatedAccount: GeneratedAccount = self.generatedAddresses[selectedAccountIndex]
result = status_accounts.setupAccount(fleetConfig, generatedAccount, password)
proc storeDerivedAndLogin*(self: AccountModel, fleetConfig: FleetConfig, importedAccount: GeneratedAccount, password: string): Account =
result = status_accounts.setupAccount(fleetConfig, importedAccount, password)
proc importMnemonic*(self: AccountModel, mnemonic: string): GeneratedAccount =
let importedAccount = status_accounts.multiAccountImportMnemonic(mnemonic)
importedAccount.derived = status_accounts.deriveAccounts(importedAccount.id)
importedAccount.name = status_accounts.generateAlias(importedAccount.derived.whisper.publicKey)
importedAccount.identicon = status_accounts.generateIdenticon(importedAccount.derived.whisper.publicKey)
result = importedAccount
proc reset*(self: AccountModel) =
self.nodeAccounts = @[]
self.generatedAddresses = @[]
proc generateAlias*(publicKey: string): string =
result = status_accounts.generateAlias(publicKey)
proc generateIdenticon*(publicKey: string): string =
result = status_accounts.generateIdenticon(publicKey)
proc generateAlias*(self: AccountModel, publicKey: string): string =
result = generateAlias(publicKey)
proc generateIdenticon*(self: AccountModel, publicKey: string): string =
result = generateIdenticon(publicKey)
proc changeNetwork*(self: AccountModel, fleetConfig: FleetConfig, network: string) =
var statusGoResult = status_settings.setNetwork(network)
if statusGoResult.error != "":
error "Error saving updated node config", msg=statusGoResult.error
# remove all installed sticker packs (pack ids do not match across networks)
statusGoResult = status_settings.saveSetting(Setting.Stickers_PacksInstalled, %* {})
if statusGoResult.error != "":
error "Error removing all installed sticker packs", msg=statusGoResult.error
# remove all recent stickers (pack ids do not match across networks)
statusGoResult = status_settings.saveSetting(Setting.Stickers_Recent, %* {})
if statusGoResult.error != "":
error "Error removing all recent stickers", msg=statusGoResult.error
proc changePassword*(self: AccountModel, keyUID: string, password: string, newPassword: string): bool =
try:
if not status_accounts.changeDatabasePassword(keyUID, password, newPassword):
return false
except:
return false
return true

24
status/browser.nim Normal file
View File

@ -0,0 +1,24 @@
import libstatus/browser as status_browser
import ../eventemitter
import ./types/[bookmark]
type
BrowserModel* = ref object
events*: EventEmitter
proc newBrowserModel*(events: EventEmitter): BrowserModel =
result = BrowserModel()
result.events = events
proc storeBookmark*(self: BrowserModel, url: string, name: string): Bookmark =
result = status_browser.storeBookmark(url, name)
proc updateBookmark*(self: BrowserModel, ogUrl: string, url: string, name: string) =
status_browser.updateBookmark(ogUrl, url, name)
proc getBookmarks*(self: BrowserModel): string =
result = status_browser.getBookmarks()
proc deleteBookmark*(self: BrowserModel, url: string) =
status_browser.deleteBookmark(url)

763
status/chat.nim Normal file
View File

@ -0,0 +1,763 @@
import json, strutils, sequtils, tables, chronicles, times, sugar
import libstatus/chat as status_chat
import libstatus/chatCommands as status_chat_commands
import types/[message, status_update, activity_center_notification,
sticker, removed_message]
import utils as status_utils
import stickers
import ../eventemitter
import profile/profile
import contacts
import chat/[chat, utils]
import ens, accounts
include utils/json_utils
logScope:
topics = "chat-model"
const backToFirstChat* = "__goBackToFirstChat"
const ZERO_ADDRESS* = "0x0000000000000000000000000000000000000000"
type
ChatUpdateArgs* = ref object of Args
chats*: seq[Chat]
messages*: seq[Message]
pinnedMessages*: seq[Message]
contacts*: seq[Profile]
emojiReactions*: seq[Reaction]
communities*: seq[Community]
communityMembershipRequests*: seq[CommunityMembershipRequest]
activityCenterNotifications*: seq[ActivityCenterNotification]
statusUpdates*: seq[StatusUpdate]
deletedMessages*: seq[RemovedMessage]
ChatIdArg* = ref object of Args
chatId*: string
ChannelArgs* = ref object of Args
chat*: Chat
ChatArgs* = ref object of Args
chats*: seq[Chat]
CommunityActiveChangedArgs* = ref object of Args
active*: bool
MsgsLoadedArgs* = ref object of Args
chatId*: string
messages*: seq[Message]
statusUpdates*: seq[StatusUpdate]
ActivityCenterNotificationsArgs* = ref object of Args
activityCenterNotifications*: seq[ActivityCenterNotification]
ReactionsLoadedArgs* = ref object of Args
reactions*: seq[Reaction]
MessageArgs* = ref object of Args
id*: string
channel*: string
MessageSendingSuccess* = ref object of Args
chat*: Chat
message*: Message
MarkAsReadNotificationProperties* = ref object of Args
communityId*: string
channelId*: string
notificationTypes*: seq[ActivityCenterNotificationType]
type ChatModel* = ref object
publicKey*: string
events*: EventEmitter
communitiesToFetch*: seq[string]
mailserverReady*: bool
contacts*: Table[string, Profile]
channels*: Table[string, Chat]
msgCursor: Table[string, string]
pinnedMsgCursor: Table[string, string]
activityCenterCursor*: string
emojiCursor: Table[string, string]
lastMessageTimestamps*: Table[string, int64]
proc newChatModel*(events: EventEmitter): ChatModel =
result = ChatModel()
result.events = events
result.mailserverReady = false
result.communitiesToFetch = @[]
result.contacts = initTable[string, Profile]()
result.channels = initTable[string, Chat]()
result.msgCursor = initTable[string, string]()
result.pinnedMsgCursor = initTable[string, string]()
result.emojiCursor = initTable[string, string]()
result.lastMessageTimestamps = initTable[string, int64]()
proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message], emojiReactions: seq[Reaction], communities: seq[Community], communityMembershipRequests: seq[CommunityMembershipRequest], pinnedMessages: seq[Message], activityCenterNotifications: seq[ActivityCenterNotification], statusUpdates: seq[StatusUpdate], deletedMessages: seq[RemovedMessage]) =
for chat in chats:
self.channels[chat.id] = chat
for message in messages:
let chatId = message.chatId
let ts = times.convert(Milliseconds, Seconds, message.whisperTimestamp.parseInt())
if not self.lastMessageTimestamps.hasKey(chatId):
self.lastMessageTimestamps[chatId] = ts
else:
if self.lastMessageTimestamps[chatId] > ts:
self.lastMessageTimestamps[chatId] = ts
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages,chats: chats, contacts: @[], emojiReactions: emojiReactions, communities: communities, communityMembershipRequests: communityMembershipRequests, pinnedMessages: pinnedMessages, activityCenterNotifications: activityCenterNotifications, statusUpdates: statusUpdates, deletedMessages: deletedMessages))
proc parseChatResponse(self: ChatModel, response: string): (seq[Chat], seq[Message]) =
var parsedResponse = parseJson(response)
var chats: seq[Chat] = @[]
var messages: seq[Message] = @[]
if parsedResponse{"result"}{"messages"} != nil:
for jsonMsg in parsedResponse["result"]["messages"]:
messages.add(jsonMsg.toMessage())
if parsedResponse{"result"}{"chats"} != nil:
for jsonChat in parsedResponse["result"]["chats"]:
let chat = jsonChat.toChat
self.channels[chat.id] = chat
chats.add(chat)
result = (chats, messages)
proc emitUpdate(self: ChatModel, response: string) =
var (chats, messages) = self.parseChatResponse(response)
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
proc removeFiltersByChatId(self: ChatModel, chatId: string, filters: JsonNode)
proc removeChatFilters(self: ChatModel, chatId: string) =
# TODO: this code should be handled by status-go / stimbus instead of the client
# Clients should not have to care about filters. For more info about filters:
# https://github.com/status-im/specs/blob/master/docs/stable/3-whisper-usage.md#keys-management
let filters = parseJson(status_chat.loadFilters(@[]))["result"]
case self.channels[chatId].chatType
of ChatType.Public:
for filter in filters:
if filter["chatId"].getStr == chatId:
status_chat.removeFilters(chatId, filter["filterId"].getStr)
of ChatType.OneToOne, ChatType.Profile:
# Check if user does not belong to any active chat group
var inGroup = false
for channel in self.channels.values:
if channel.isActive and channel.id != chatId and channel.chatType == ChatType.PrivateGroupChat:
inGroup = true
break
if not inGroup: self.removeFiltersByChatId(chatId, filters)
of ChatType.PrivateGroupChat:
for member in self.channels[chatId].members:
# Check that any of the members are not in other active group chats, or that you dont have a one-to-one open.
var hasConversation = false
for channel in self.channels.values:
if (channel.isActive and channel.chatType == ChatType.OneToOne and channel.id == member.id) or
(channel.isActive and channel.id != chatId and channel.chatType == ChatType.PrivateGroupChat and channel.isMember(member.id)):
hasConversation = true
break
if not hasConversation: self.removeFiltersByChatId(member.id, filters)
else:
error "Unknown chat type removed", chatId
proc removeFiltersByChatId(self: ChatModel, chatId: string, filters: JsonNode) =
var partitionedTopic = ""
for filter in filters:
# Contact code filter should be removed
if filter["identity"].getStr == chatId and filter["chatId"].getStr.endsWith("-contact-code"):
status_chat.removeFilters(chatId, filter["filterId"].getStr)
# Remove partitioned topic if no other user in an active group chat or one-to-one is from the
# same partitioned topic
if filter["identity"].getStr == chatId and filter["chatId"].getStr.startsWith("contact-discovery-"):
partitionedTopic = filter["topic"].getStr
var samePartitionedTopic = false
for f in filters.filterIt(it["topic"].getStr == partitionedTopic and it["filterId"].getStr != filter["filterId"].getStr):
let fIdentity = f["identity"].getStr;
if self.channels.hasKey(fIdentity) and self.channels[fIdentity].isActive:
samePartitionedTopic = true
break
if not samePartitionedTopic:
status_chat.removeFilters(chatId, filter["filterId"].getStr)
proc hasChannel*(self: ChatModel, chatId: string): bool =
self.channels.hasKey(chatId)
proc getActiveChannel*(self: ChatModel): string =
if (self.channels.len == 0): "" else: toSeq(self.channels.values)[self.channels.len - 1].id
proc emitTopicAndJoin(self: ChatModel, chat: Chat) =
let filterResult = status_chat.loadFilters(@[status_chat.buildFilter(chat)])
self.events.emit("channelJoined", ChannelArgs(chat: chat))
proc join*(self: ChatModel, chatId: string, chatType: ChatType, ensName: string = "", pubKey: string = "") =
if self.hasChannel(chatId): return
var chat = newChat(chatId, ChatType(chatType))
self.channels[chat.id] = chat
status_chat.saveChat(chatId, chatType, color=chat.color, ensName=ensName, profile=pubKey)
self.emitTopicAndJoin(chat)
proc createOneToOneChat*(self: ChatModel, publicKey: string, ensName: string = "") =
if self.hasChannel(publicKey):
self.emitTopicAndJoin(self.channels[publicKey])
return
var chat = newChat(publicKey, ChatType.OneToOne)
if ensName != "":
chat.name = ensName
chat.ensName = ensName
self.channels[chat.id] = chat
discard status_chat.createOneToOneChat(publicKey)
self.emitTopicAndJoin(chat)
proc createPublicChat*(self: ChatModel, chatId: string) =
if self.hasChannel(chatId): return
var chat = newChat(chatId, ChatType.Public)
self.channels[chat.id] = chat
discard status_chat.createPublicChat(chatId)
self.emitTopicAndJoin(chat)
proc updateContacts*(self: ChatModel, contacts: seq[Profile]) =
for c in contacts:
self.contacts[c.id] = c
self.events.emit("chatUpdate", ChatUpdateArgs(contacts: contacts))
proc requestMissingCommunityInfos*(self: ChatModel) =
if (self.communitiesToFetch.len == 0):
return
for communityId in self.communitiesToFetch:
status_chat.requestCommunityInfo(communityId)
proc init*(self: ChatModel, pubKey: string) =
self.publicKey = pubKey
var contacts = getAddedContacts()
var chatList = status_chat.loadChats()
let profileUpdatesChatIds = chatList.filter(c => c.chatType == ChatType.Profile).map(c => c.id)
if chatList.filter(c => c.chatType == ChatType.Timeline).len == 0:
var timelineChannel = newChat(status_utils.getTimelineChatId(), ChatType.Timeline)
self.join(timelineChannel.id, timelineChannel.chatType)
chatList.add(timelineChannel)
let timelineChatId = status_utils.getTimelineChatId(pubKey)
if not profileUpdatesChatIds.contains(timelineChatId):
var profileUpdateChannel = newChat(timelineChatId, ChatType.Profile)
status_chat.saveChat(profileUpdateChannel.id, profileUpdateChannel.chatType, profile=pubKey)
chatList.add(profileUpdateChannel)
# For profile updates and timeline, we have to make sure that for
# each added contact, a chat has been saved for the currently logged-in
# user. Users that will use a version of Status with timeline support for the
# first time, won't have any of those otherwise.
if profileUpdatesChatIds.filter(id => id != timelineChatId).len != contacts.len:
for contact in contacts:
if not profileUpdatesChatIds.contains(status_utils.getTimelineChatId(contact.address)):
let profileUpdatesChannel = newChat(status_utils.getTimelineChatId(contact.address), ChatType.Profile)
status_chat.saveChat(profileUpdatesChannel.id, profileUpdatesChannel.chatType, ensName=contact.ensName, profile=contact.address)
chatList.add(profileUpdatesChannel)
var filters:seq[JsonNode] = @[]
for chat in chatList:
if self.hasChannel(chat.id):
continue
filters.add status_chat.buildFilter(chat)
self.channels[chat.id] = chat
self.events.emit("channelLoaded", ChannelArgs(chat: chat))
if filters.len == 0: return
let filterResult = status_chat.loadFilters(filters)
self.events.emit("chatsLoaded", ChatArgs(chats: chatList))
self.events.once("mailserverAvailable") do(a: Args):
self.mailserverReady = true
self.requestMissingCommunityInfos()
self.events.on("contactUpdate") do(a: Args):
var evArgs = ContactUpdateArgs(a)
self.updateContacts(evArgs.contacts)
proc statusUpdates*(self: ChatModel) =
let statusUpdates = status_chat.statusUpdates()
self.events.emit("messagesLoaded", MsgsLoadedArgs(statusUpdates: statusUpdates))
proc leave*(self: ChatModel, chatId: string) =
self.removeChatFilters(chatId)
if self.channels[chatId].chatType == ChatType.PrivateGroupChat:
let leaveGroupResponse = status_chat.leaveGroupChat(chatId)
self.emitUpdate(leaveGroupResponse)
discard status_chat.deactivateChat(self.channels[chatId])
self.channels.del(chatId)
discard status_chat.clearChatHistory(chatId)
self.events.emit("channelLeft", ChatIdArg(chatId: chatId))
proc clearHistory*(self: ChatModel, chatId: string) =
discard status_chat.clearChatHistory(chatId)
let chat = self.channels[chatId]
self.events.emit("chatHistoryCleared", ChannelArgs(chat: chat))
proc setActiveChannel*(self: ChatModel, chatId: string) =
self.events.emit("activeChannelChanged", ChatIdArg(chatId: chatId))
proc processMessageUpdateAfterSend(self: ChatModel, response: string): (seq[Chat], seq[Message]) =
result = self.parseChatResponse(response)
var (chats, messages) = result
if chats.len == 0 and messages.len == 0:
self.events.emit("messageSendingFailed", Args())
return
self.events.emit("messageSendingSuccess", MessageSendingSuccess(message: messages[0], chat: chats[0]))
proc sendMessage*(self: ChatModel, chatId: string, msg: string, replyTo: string = "", contentType: int = ContentType.Message.int, communityId: string = "") =
var response = status_chat.sendChatMessage(chatId, msg, replyTo, contentType, communityId)
discard self.processMessageUpdateAfterSend(response)
proc editMessage*(self: ChatModel, messageId: string, msg: string) =
var response = status_chat.editMessage(messageId, msg)
discard self.processMessageUpdateAfterSend(response)
proc sendImage*(self: ChatModel, chatId: string, image: string) =
var response = status_chat.sendImageMessage(chatId, image)
discard self.processMessageUpdateAfterSend(response)
proc sendImages*(self: ChatModel, chatId: string, images: var seq[string]) =
var response = status_chat.sendImageMessages(chatId, images)
discard self.processMessageUpdateAfterSend(response)
proc deleteMessageAndSend*(self: ChatModel, messageId: string) =
var response = status_chat.deleteMessageAndSend(messageId)
discard self.processMessageUpdateAfterSend(response)
proc sendSticker*(self: ChatModel, chatId: string, replyTo: string, sticker: Sticker) =
var response = status_chat.sendStickerMessage(chatId, replyTo, sticker)
self.events.emit("stickerSent", StickerArgs(sticker: sticker, save: true))
var (chats, messages) = self.parseChatResponse(response)
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
self.events.emit("sendingMessage", MessageArgs(id: messages[0].id, channel: messages[0].chatId))
proc addEmojiReaction*(self: ChatModel, chatId: string, messageId: string, emojiId: int) =
let reactions = status_chat.addEmojiReaction(chatId, messageId, emojiId)
self.events.emit("reactionsLoaded", ReactionsLoadedArgs(reactions: reactions))
proc removeEmojiReaction*(self: ChatModel, emojiReactionId: string) =
let reactions = status_chat.removeEmojiReaction(emojiReactionId)
self.events.emit("reactionsLoaded", ReactionsLoadedArgs(reactions: reactions))
proc onMarkMessagesRead(self: ChatModel, response: string, chatId: string): JsonNode =
result = parseJson(response)
if self.channels.hasKey(chatId):
self.channels[chatId].unviewedMessagesCount = 0
self.channels[chatId].mentionsCount = 0
self.events.emit("channelUpdate", ChatUpdateArgs(messages: @[], chats: @[self.channels[chatId]], contacts: @[]))
proc onAsyncMarkMessagesRead*(self: ChatModel, response: string) =
let parsedResponse = parseJson(response)
discard self.onMarkMessagesRead(parsedResponse{"response"}.getStr, parsedResponse{"chatId"}.getStr)
proc markAllChannelMessagesRead*(self: ChatModel, chatId: string): JsonNode =
var response = status_chat.markAllRead(chatId)
return self.onMarkMessagesRead(response, chatId)
proc markMessagesSeen*(self: ChatModel, chatId: string, messageIds: seq[string]): JsonNode =
var response = status_chat.markMessagesSeen(chatId, messageIds)
return self.onMarkMessagesRead(response, chatId)
proc confirmJoiningGroup*(self: ChatModel, chatId: string) =
var response = status_chat.confirmJoiningGroup(chatId)
self.emitUpdate(response)
proc renameGroup*(self: ChatModel, chatId: string, newName: string) =
var response = status_chat.renameGroup(chatId, newName)
self.emitUpdate(response)
proc getUserName*(self: ChatModel, id: string, defaultUserName: string):string =
if(self.contacts.hasKey(id)):
return userNameOrAlias(self.contacts[id])
else:
return defaultUserName
proc processGroupChatCreation*(self: ChatModel, result: string) =
var response = parseJson(result)
var (chats, messages) = formatChatUpdate(response)
let chat = chats[0]
self.channels[chat.id] = chat
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[]))
self.events.emit("activeChannelChanged", ChatIdArg(chatId: chat.id))
proc createGroup*(self: ChatModel, groupName: string, pubKeys: seq[string]) =
var result = status_chat.createGroup(groupName, pubKeys)
self.processGroupChatCreation(result)
proc createGroupChatFromInvitation*(self: ChatModel, groupName: string, chatID: string, adminPK: string) =
var result = status_chat.createGroupChatFromInvitation(groupName, chatID, adminPK)
self.processGroupChatCreation(result)
proc addGroupMembers*(self: ChatModel, chatId: string, pubKeys: seq[string]) =
var response = status_chat.addGroupMembers(chatId, pubKeys)
self.emitUpdate(response)
proc kickGroupMember*(self: ChatModel, chatId: string, pubKey: string) =
var response = status_chat.kickGroupMember(chatId, pubKey)
self.emitUpdate(response)
proc makeAdmin*(self: ChatModel, chatId: string, pubKey: string) =
var response = status_chat.makeAdmin(chatId, pubKey)
self.emitUpdate(response)
proc resendMessage*(self: ChatModel, messageId: string) =
discard status_chat.reSendChatMessage(messageId)
proc muteChat*(self: ChatModel, chat: Chat) =
discard status_chat.muteChat(chat.id)
self.events.emit("chatUpdate", ChatUpdateArgs(messages: @[], chats: @[chat], contacts: @[]))
proc unmuteChat*(self: ChatModel, chat: Chat) =
discard status_chat.unmuteChat(chat.id)
self.events.emit("chatUpdate", ChatUpdateArgs(messages: @[], chats: @[chat], contacts: @[]))
proc processUpdateForTransaction*(self: ChatModel, messageId: string, response: string) =
var (chats, messages) = self.processMessageUpdateAfterSend(response)
self.events.emit("messageDeleted", MessageArgs(id: messageId, channel: chats[0].id))
proc acceptRequestAddressForTransaction*(self: ChatModel, messageId: string, address: string) =
let response = status_chat_commands.acceptRequestAddressForTransaction(messageId, address)
self.processUpdateForTransaction(messageId, response)
proc declineRequestAddressForTransaction*(self: ChatModel, messageId: string) =
let response = status_chat_commands.declineRequestAddressForTransaction(messageId)
self.processUpdateForTransaction(messageId, response)
proc declineRequestTransaction*(self: ChatModel, messageId: string) =
let response = status_chat_commands.declineRequestTransaction(messageId)
self.processUpdateForTransaction(messageId, response)
proc requestAddressForTransaction*(self: ChatModel, chatId: string, fromAddress: string, amount: string, tokenAddress: string) =
let address = if (tokenAddress == ZERO_ADDRESS): "" else: tokenAddress
let response = status_chat_commands.requestAddressForTransaction(chatId, fromAddress, amount, address)
discard self.processMessageUpdateAfterSend(response)
proc acceptRequestTransaction*(self: ChatModel, transactionHash: string, messageId: string, signature: string) =
let response = status_chat_commands.acceptRequestTransaction(transactionHash, messageId, signature)
discard self.processMessageUpdateAfterSend(response)
proc requestTransaction*(self: ChatModel, chatId: string, fromAddress: string, amount: string, tokenAddress: string) =
let address = if (tokenAddress == ZERO_ADDRESS): "" else: tokenAddress
let response = status_chat_commands.requestTransaction(chatId, fromAddress, amount, address)
discard self.processMessageUpdateAfterSend(response)
proc getAllComunities*(self: ChatModel): seq[Community] =
result = status_chat.getAllComunities()
proc getJoinedComunities*(self: ChatModel): seq[Community] =
result = status_chat.getJoinedComunities()
proc createCommunity*(self: ChatModel, name: string, description: string, access: int, ensOnly: bool, color: string, imageUrl: string, aX: int, aY: int, bX: int, bY: int): Community =
result = status_chat.createCommunity(name, description, access, ensOnly, color, imageUrl, aX, aY, bX, bY)
proc editCommunity*(self: ChatModel, id: string, name: string, description: string, access: int, ensOnly: bool, color: string, imageUrl: string, aX: int, aY: int, bX: int, bY: int): Community =
result = status_chat.editCommunity(id, name, description, access, ensOnly, color, imageUrl, aX, aY, bX, bY)
proc createCommunityChannel*(self: ChatModel, communityId: string, name: string, description: string): Chat =
result = status_chat.createCommunityChannel(communityId, name, description)
proc editCommunityChannel*(self: ChatModel, communityId: string, channelId: string, name: string, description: string, categoryId: string): Chat =
result = status_chat.editCommunityChannel(communityId, channelId, name, description, categoryId)
proc deleteCommunityChat*(self: ChatModel, communityId: string, channelId: string) =
status_chat.deleteCommunityChat(communityId, channelId)
proc reorderCommunityCategories*(self: ChatModel, communityId: string, categoryId: string, position: int) =
status_chat.reorderCommunityCategories(communityId, categoryId, position)
proc createCommunityCategory*(self: ChatModel, communityId: string, name: string, channels: seq[string]): CommunityCategory =
result = status_chat.createCommunityCategory(communityId, name, channels)
proc editCommunityCategory*(self: ChatModel, communityId: string, categoryId: string, name: string, channels: seq[string]) =
status_chat.editCommunityCategory(communityId, categoryId, name, channels)
proc deleteCommunityCategory*(self: ChatModel, communityId: string, categoryId: string) =
status_chat.deleteCommunityCategory(communityId, categoryId)
proc reorderCommunityChannel*(self: ChatModel, communityId: string, categoryId: string, chatId: string, position: int) =
status_chat.reorderCommunityChat(communityId, categoryId, chatId, position)
proc joinCommunity*(self: ChatModel, communityId: string) =
status_chat.joinCommunity(communityId)
proc requestCommunityInfo*(self: ChatModel, communityId: string) =
if (not self.mailserverReady):
self.communitiesToFetch.add(communityId)
self.communitiesToFetch = self.communitiesToFetch.deduplicate()
return
status_chat.requestCommunityInfo(communityId)
proc leaveCommunity*(self: ChatModel, communityId: string) =
status_chat.leaveCommunity(communityId)
proc inviteUserToCommunity*(self: ChatModel, communityId: string, pubKey: string) =
status_chat.inviteUsersToCommunity(communityId, @[pubKey])
proc inviteUsersToCommunity*(self: ChatModel, communityId: string, pubKeys: seq[string]) =
status_chat.inviteUsersToCommunity(communityId, pubKeys)
proc removeUserFromCommunity*(self: ChatModel, communityId: string, pubKey: string) =
status_chat.removeUserFromCommunity(communityId, pubKey)
proc banUserFromCommunity*(self: ChatModel, pubKey: string, communityId: string): string =
return status_chat.banUserFromCommunity(pubKey, communityId)
proc exportCommunity*(self: ChatModel, communityId: string): string =
result = status_chat.exportCommunity(communityId)
proc importCommunity*(self: ChatModel, communityKey: string): string =
result = status_chat.importCommunity(communityKey)
proc requestToJoinCommunity*(self: ChatModel, communityKey: string, ensName: string): seq[CommunityMembershipRequest] =
status_chat.requestToJoinCommunity(communityKey, ensName)
proc acceptRequestToJoinCommunity*(self: ChatModel, requestId: string) =
status_chat.acceptRequestToJoinCommunity(requestId)
proc declineRequestToJoinCommunity*(self: ChatModel, requestId: string) =
status_chat.declineRequestToJoinCommunity(requestId)
proc pendingRequestsToJoinForCommunity*(self: ChatModel, communityKey: string): seq[CommunityMembershipRequest] =
result = status_chat.pendingRequestsToJoinForCommunity(communityKey)
proc setCommunityMuted*(self: ChatModel, communityId: string, muted: bool) =
status_chat.setCommunityMuted(communityId, muted)
proc myPendingRequestsToJoin*(self: ChatModel): seq[CommunityMembershipRequest] =
result = status_chat.myPendingRequestsToJoin()
proc setPinMessage*(self: ChatModel, messageId: string, chatId: string, pinned: bool) =
status_chat.setPinMessage(messageId, chatId, pinned)
proc activityCenterNotifications*(self: ChatModel, initialLoad: bool = true) =
# Notifications were already loaded, since cursor will
# be nil/empty if there are no more notifs
if(not initialLoad and self.activityCenterCursor == ""): return
let activityCenterNotificationsTuple = status_chat.activityCenterNotification(self.activityCenterCursor)
self.activityCenterCursor = activityCenterNotificationsTuple[0];
self.events.emit("activityCenterNotificationsLoaded", ActivityCenterNotificationsArgs(activityCenterNotifications: activityCenterNotificationsTuple[1]))
proc activityCenterNotifications*(self: ChatModel, cursor: string = "", activityCenterNotifications: seq[ActivityCenterNotification]) =
self.activityCenterCursor = cursor
self.events.emit("activityCenterNotificationsLoaded", ActivityCenterNotificationsArgs(activityCenterNotifications: activityCenterNotifications))
proc markAllActivityCenterNotificationsRead*(self: ChatModel): string =
try:
status_chat.markAllActivityCenterNotificationsRead()
except Exception as e:
error "Error marking all as read", msg = e.msg
result = e.msg
# This proc should accept ActivityCenterNotificationType in order to clear all notifications
# per type, that's why we have this part here. If we add all types to notificationsType that
# means that we need to clear all notifications for all types.
var types : seq[ActivityCenterNotificationType]
for t in ActivityCenterNotificationType:
types.add(t)
self.events.emit("markNotificationsAsRead", MarkAsReadNotificationProperties(notificationTypes: types))
proc markActivityCenterNotificationRead*(self: ChatModel, notificationId: string,
markAsReadProps: MarkAsReadNotificationProperties): string =
try:
status_chat.markActivityCenterNotificationsRead(@[notificationId])
except Exception as e:
error "Error marking as read", msg = e.msg
result = e.msg
self.events.emit("markNotificationsAsRead", markAsReadProps)
proc acceptActivityCenterNotifications*(self: ChatModel, ids: seq[string]): string =
try:
let response = status_chat.acceptActivityCenterNotifications(ids)
let (chats, messages) = self.parseChatResponse(response)
self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats))
except Exception as e:
error "Error marking as accepted", msg = e.msg
result = e.msg
proc dismissActivityCenterNotifications*(self: ChatModel, ids: seq[string]): string =
try:
discard status_chat.dismissActivityCenterNotifications(ids)
except Exception as e:
error "Error marking as dismissed", msg = e.msg
result = e.msg
proc unreadActivityCenterNotificationsCount*(self: ChatModel): int =
status_chat.unreadActivityCenterNotificationsCount()
proc getLinkPreviewData*(link: string, success: var bool): JsonNode =
result = status_chat.getLinkPreviewData(link, success)
proc getCommunityIdForChat*(self: ChatModel, chatId: string): string =
if (not self.hasChannel(chatId)):
return ""
return self.channels[chatId].communityId
proc onAsyncSearchMessages*(self: ChatModel, response: string) =
let responseObj = response.parseJson
if (responseObj.kind != JObject):
info "search messages response is not an json object"
return
var chatId: string
discard responseObj.getProp("chatId", chatId)
var messagesObj: JsonNode
if (not responseObj.getProp("messages", messagesObj)):
info "search messages response doesn't contain messages property"
return
var messagesArray: JsonNode
if (not messagesObj.getProp("messages", messagesArray)):
info "search messages response doesn't contain messages array"
return
var messages: seq[Message] = @[]
if (messagesArray.kind == JArray):
for jsonMsg in messagesArray:
messages.add(jsonMsg.toMessage())
self.events.emit("searchMessagesLoaded", MsgsLoadedArgs(chatId: chatId, messages: messages))
proc onLoadMoreMessagesForChannel*(self: ChatModel, response: string) =
let responseObj = response.parseJson
if (responseObj.kind != JObject):
info "load more messages response is not an json object"
# notify view
self.events.emit("messagesLoaded", MsgsLoadedArgs())
self.events.emit("reactionsLoaded", ReactionsLoadedArgs())
self.events.emit("pinnedMessagesLoaded", MsgsLoadedArgs())
return
var chatId: string
discard responseObj.getProp("chatId", chatId)
# handling chat messages
var chatMessagesObj: JsonNode
var chatCursor: string
discard responseObj.getProp("messages", chatMessagesObj)
discard responseObj.getProp("messagesCursor", chatCursor)
self.msgCursor[chatId] = chatCursor
var messages: seq[Message] = @[]
if (chatMessagesObj.kind == JArray):
for jsonMsg in chatMessagesObj:
messages.add(jsonMsg.toMessage())
if messages.len > 0:
let lastMsgIndex = messages.len - 1
let ts = times.convert(Milliseconds, Seconds, messages[lastMsgIndex].whisperTimestamp.parseInt())
self.lastMessageTimestamps[chatId] = ts
# handling reactions
var reactionsObj: JsonNode
var reactionsCursor: string
discard responseObj.getProp("reactions", reactionsObj)
discard responseObj.getProp("reactionsCursor", reactionsCursor)
self.emojiCursor[chatId] = reactionsCursor;
var reactions: seq[Reaction] = @[]
if (reactionsObj.kind == JArray):
for jsonMsg in reactionsObj:
reactions.add(jsonMsg.toReaction)
# handling pinned messages
var pinnedMsgObj: JsonNode
var pinnedMsgCursor: string
discard responseObj.getProp("pinnedMessages", pinnedMsgObj)
discard responseObj.getProp("pinnedMessagesCursor", pinnedMsgCursor)
self.pinnedMsgCursor[chatId] = pinnedMsgCursor
var pinnedMessages: seq[Message] = @[]
if (pinnedMsgObj.kind == JArray):
for jsonMsg in pinnedMsgObj:
var msgObj: JsonNode
if(jsonMsg.getProp("message", msgObj)):
var msg: Message
msg = msgObj.toMessage()
discard jsonMsg.getProp("pinnedBy", msg.pinnedBy)
pinnedMessages.add(msg)
# notify view
self.events.emit("messagesLoaded", MsgsLoadedArgs(chatId: chatId, messages: messages))
self.events.emit("reactionsLoaded", ReactionsLoadedArgs(reactions: reactions))
self.events.emit("pinnedMessagesLoaded", MsgsLoadedArgs(chatId: chatId, messages: pinnedMessages))
proc userNameOrAlias*(self: ChatModel, pubKey: string,
prettyForm: bool = false): string =
## Returns ens name or alias, in case if prettyForm is true and ens name
## ends with ".stateofus.eth" that part will be removed.
var alias: string
if self.contacts.hasKey(pubKey):
alias = ens.userNameOrAlias(self.contacts[pubKey])
else:
alias = generateAlias(pubKey)
if (prettyForm and alias.endsWith(".stateofus.eth")):
alias = alias[0 .. ^15]
return alias
proc chatName*(self: ChatModel, chatItem: Chat): string =
if (not chatItem.chatType.isOneToOne):
return chatItem.name
if (self.contacts.hasKey(chatItem.id) and
self.contacts[chatItem.id].hasNickname()):
return self.contacts[chatItem.id].localNickname
if chatItem.ensName != "":
return "@" & userName(chatItem.ensName).userName(true)
return self.userNameOrAlias(chatItem.id)
proc isMessageCursorSet*(self: ChatModel, channelId: string): bool =
self.msgCursor.hasKey(channelId)
proc getCurrentMessageCursor*(self: ChatModel, channelId: string): string =
if(not self.msgCursor.hasKey(channelId)):
self.msgCursor[channelId] = ""
return self.msgCursor[channelId]
proc isEmojiCursorSet*(self: ChatModel, channelId: string): bool =
self.emojiCursor.hasKey(channelId)
proc getCurrentEmojiCursor*(self: ChatModel, channelId: string): string =
if(not self.emojiCursor.hasKey(channelId)):
self.emojiCursor[channelId] = ""
return self.emojiCursor[channelId]
proc isPinnedMessageCursorSet*(self: ChatModel, channelId: string): bool =
self.pinnedMsgCursor.hasKey(channelId)
proc getCurrentPinnedMessageCursor*(self: ChatModel, channelId: string): string =
if(not self.pinnedMsgCursor.hasKey(channelId)):
self.pinnedMsgCursor[channelId] = ""
return self.pinnedMsgCursor[channelId]

76
status/chat/chat.nim Normal file
View File

@ -0,0 +1,76 @@
import ../types/[chat, community]
export chat, community
proc findIndexById*(self: seq[Chat], id: string): int =
result = -1
var idx = -1
for item in self:
inc idx
if(item.id == id):
result = idx
break
proc findIndexById*(self: seq[Community], id: string): int =
result = -1
var idx = -1
for item in self:
inc idx
if(item.id == id):
result = idx
break
proc findIndexById*(self: seq[CommunityMembershipRequest], id: string): int =
result = -1
var idx = -1
for item in self:
inc idx
if(item.id == id):
result = idx
break
proc findIndexById*(self: seq[CommunityCategory], id: string): int =
result = -1
var idx = -1
for item in self:
inc idx
if(item.id == id):
result = idx
break
proc isMember*(self: Chat, pubKey: string): bool =
for member in self.members:
if member.id == pubKey:
return member.joined
return false
proc isMemberButNotJoined*(self: Chat, pubKey: string): bool =
for member in self.members:
if member.id == pubKey:
return not member.joined
return false
proc contains*(self: Chat, pubKey: string): bool =
for member in self.members:
if member.id == pubKey: return true
return false
proc isAdmin*(self: Chat, pubKey: string): bool =
for member in self.members:
if member.id == pubKey:
return member.joined and member.admin
return false
proc recalculateUnviewedMessages*(community: var Community) =
var total = 0
for chat in community.chats:
total += chat.unviewedMessagesCount
community.unviewedMessagesCount = total
proc recalculateMentions*(community: var Community) =
var total = 0
for chat in community.chats:
total += chat.unviewedMentionsCount
community.unviewedMentionsCount = total

9
status/chat/stickers.nim Normal file
View File

@ -0,0 +1,9 @@
import chronicles
import ../stickers as status_stickers
logScope:
topics = "sticker-decoding"
# TODO: this is for testing purposes, the correct function should decode the hash
proc decodeContentHash*(value: string): string =
status_stickers.decodeContentHash(value)

13
status/chat/utils.nim Normal file
View File

@ -0,0 +1,13 @@
import json
import ../types/[message, chat]
proc formatChatUpdate*(response: JsonNode): (seq[Chat], seq[Message]) =
var chats: seq[Chat] = @[]
var messages: seq[Message] = @[]
if response["result"]{"messages"} != nil:
for jsonMsg in response["result"]["messages"]:
messages.add(jsonMsg.toMessage())
if response["result"]{"chats"} != nil:
for jsonChat in response["result"]["chats"]:
chats.add(jsonChat.toChat)
result = (chats, messages)

10
status/constants.nim Normal file
View File

@ -0,0 +1,10 @@
import libstatus/accounts/constants
export DATADIR
export STATUSGODIR
export KEYSTOREDIR
export TMPDIR
export LOGDIR
const APP_UPDATES_ENS* = "desktop.status.eth"
const CHECK_VERSION_TIMEOUT_MS* = 5000

151
status/contacts.nim Normal file
View File

@ -0,0 +1,151 @@
import json, sequtils, sugar, chronicles
import libstatus/contacts as status_contacts
import libstatus/accounts as status_accounts
import libstatus/chat as status_chat
import profile/profile
import ../eventemitter
const DELETE_CONTACT* = "__deleteThisContact__"
type
ContactModel* = ref object
events*: EventEmitter
type
ContactUpdateArgs* = ref object of Args
contacts*: seq[Profile]
proc newContactModel*(events: EventEmitter): ContactModel =
result = ContactModel()
result.events = events
proc getContactByID*(self: ContactModel, id: string): Profile =
let response = status_contacts.getContactByID(id)
# TODO: change to options
let responseResult = parseJSON($response)["result"]
if responseResult == nil or responseResult.kind == JNull:
result = nil
else:
result = toProfileModel(parseJSON($response)["result"])
proc blockContact*(self: ContactModel, id: string): string =
var contact = self.getContactByID(id)
contact.systemTags.add(contactBlocked)
discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.alias, contact.identicon, contact.identityImage.thumbnail, contact.systemTags, contact.localNickname)
self.events.emit("contactBlocked", Args())
proc unblockContact*(self: ContactModel, id: string): string =
var contact = self.getContactByID(id)
contact.systemTags.delete(contact.systemTags.find(contactBlocked))
discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.alias, contact.identicon, contact.identityImage.thumbnail, contact.systemTags, contact.localNickname)
self.events.emit("contactUnblocked", Args())
proc getAllContacts*(): seq[Profile] =
result = map(status_contacts.getContacts().getElems(), proc(x: JsonNode): Profile = x.toProfileModel())
proc getAddedContacts*(): seq[Profile] =
result = getAllContacts().filter(c => c.systemTags.contains(contactAdded))
proc getContacts*(self: ContactModel): seq[Profile] =
result = getAllContacts()
self.events.emit("contactUpdate", ContactUpdateArgs(contacts: result))
proc getOrCreateContact*(self: ContactModel, id: string): Profile =
result = self.getContactByID(id)
if result == nil:
let alias = status_accounts.generateAlias(id)
result = Profile(
id: id,
username: alias,
localNickname: "",
identicon: status_accounts.generateIdenticon(id),
alias: alias,
ensName: "",
ensVerified: false,
appearance: 0,
systemTags: @[]
)
proc setNickName*(self: ContactModel, id: string, localNickname: string): string =
var contact = self.getOrCreateContact(id)
let nickname =
if (localNickname == ""):
contact.localNickname
elif (localNickname == DELETE_CONTACT):
""
else:
localNickname
var thumbnail = ""
if contact.identityImage != nil:
thumbnail = contact.identityImage.thumbnail
result = status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.alias, contact.identicon, thumbnail, contact.systemTags, nickname)
self.events.emit("contactAdded", Args())
discard requestContactUpdate(contact.id)
proc addContact*(self: ContactModel, id: string): string =
var contact = self.getOrCreateContact(id)
let updating = contact.systemTags.contains(contactAdded)
if not updating:
contact.systemTags.add(contactAdded)
discard status_chat.createProfileChat(contact.id)
else:
let index = contact.systemTags.find(contactBlocked)
if (index > -1):
contact.systemTags.delete(index)
var thumbnail = ""
if contact.identityImage != nil:
thumbnail = contact.identityImage.thumbnail
result = status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.alias, contact.identicon, thumbnail, contact.systemTags, contact.localNickname)
self.events.emit("contactAdded", Args())
discard requestContactUpdate(contact.id)
if updating:
let profile = Profile(
id: contact.id,
username: contact.alias,
identicon: contact.identicon,
alias: contact.alias,
ensName: contact.ensName,
ensVerified: contact.ensVerified,
appearance: 0,
systemTags: contact.systemTags,
localNickname: contact.localNickname
)
self.events.emit("contactUpdate", ContactUpdateArgs(contacts: @[profile]))
proc removeContact*(self: ContactModel, id: string) =
let contact = self.getContactByID(id)
contact.systemTags.delete(contact.systemTags.find(contactAdded))
var thumbnail = ""
if contact.identityImage != nil:
thumbnail = contact.identityImage.thumbnail
discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.alias, contact.identicon, thumbnail, contact.systemTags, contact.localNickname)
self.events.emit("contactRemoved", Args())
proc isAdded*(self: ContactModel, id: string): bool =
var contact = self.getContactByID(id)
if contact.isNil: return false
contact.systemTags.contains(contactAdded)
proc contactRequestReceived*(self: ContactModel, id: string): bool =
var contact = self.getContactByID(id)
if contact.isNil: return false
contact.systemTags.contains(contactRequest)
proc rejectContactRequest*(self: ContactModel, id: string) =
let contact = self.getContactByID(id)
contact.systemTags.delete(contact.systemTags.find(contactRequest))
var thumbnail = ""
if contact.identityImage != nil:
thumbnail = contact.identityImage.thumbnail
discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.alias, contact.identicon, thumbnail, contact.systemTags, contact.localNickname)
self.events.emit("contactRemoved", Args())

43
status/devices.nim Normal file
View File

@ -0,0 +1,43 @@
import system
import libstatus/settings
import types/[setting, installation]
import libstatus/installations
import json
proc setDeviceName*(name: string) =
discard setInstallationMetadata(getSetting[string](Setting.InstallationId, "", true), name, hostOs)
proc isDeviceSetup*():bool =
let installationId = getSetting[string](Setting.InstallationId, "", true)
let responseResult = getOurInstallations()
if responseResult.kind == JNull:
return false
for installation in responseResult:
if installation["id"].getStr == installationId:
return installation["metadata"].kind != JNull
result = false
proc syncAllDevices*() =
let preferredUsername = getSetting[string](Setting.PreferredUsername, "")
discard syncDevices(preferredUsername)
proc advertise*() =
discard sendPairInstallation()
proc getAllDevices*():seq[Installation] =
let responseResult = getOurInstallations()
let installationId = getSetting[string](Setting.InstallationId, "", true)
result = @[]
if responseResult.kind != JNull:
for inst in responseResult:
var device = inst.toInstallation
if device.installationId == installationId:
device.isUserDevice = true
result.add(device)
proc enable*(installationId: string) =
# TODO handle errors
discard enableInstallation(installationId)
proc disable*(installationId: string) =
discard disableInstallation(installationId)

387
status/ens.nim Normal file
View File

@ -0,0 +1,387 @@
import sequtils
import strutils
import profile/profile
import nimcrypto
import json
import json_serialization
import tables
import strformat
import libstatus/core
import ./types/[transaction, setting, rpc_response]
import utils
import libstatus/wallet
import stew/byteutils
import unicode
import transactions
import algorithm
import web3/[ethtypes, conversions], stew/byteutils, stint
import libstatus/eth/contracts
import libstatus/eth/transactions as eth_transactions
import chronicles, libp2p/[multihash, multicodec, cid]
import ./settings as status_settings
import ./wallet as status_wallet
const domain* = ".stateofus.eth"
proc userName*(ensName: string, removeSuffix: bool = false): string =
if ensName != "" and ensName.endsWith(domain):
if removeSuffix:
result = ensName.split(".")[0]
else:
result = ensName
else:
if ensName.endsWith(".eth") and removeSuffix:
return ensName.split(".")[0]
result = ensName
proc addDomain*(username: string): string =
if username.endsWith(".eth"):
return username
else:
return username & domain
proc hasNickname*(contact: Profile): bool = contact.localNickname != ""
proc userNameOrAlias*(contact: Profile, removeSuffix: bool = false): string =
if(contact.ensName != "" and contact.ensVerified):
result = "@" & userName(contact.ensName, removeSuffix)
elif(contact.localNickname != ""):
result = contact.localNickname
else:
result = contact.alias
proc label*(username:string): string =
let name = username.toLower()
var node:array[32, byte] = keccak_256.digest(username).data
result = "0x" & node.toHex()
proc namehash*(ensName:string): string =
let name = ensName.toLower()
var node:array[32, byte]
node.fill(0)
var parts = name.split(".")
for i in countdown(parts.len - 1,0):
let elem = keccak_256.digest(parts[i]).data
var concatArrays: array[64, byte]
concatArrays[0..31] = node
concatArrays[32..63] = elem
node = keccak_256.digest(concatArrays).data
result = "0x" & node.toHex()
const registry* = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
const resolver_signature = "0x0178b8bf"
proc resolver*(usernameHash: string): string =
let payload = %* [{
"to": registry,
"from": "0x0000000000000000000000000000000000000000",
"data": fmt"{resolver_signature}{userNameHash}"
}, "latest"]
let response = callPrivateRPC("eth_call", payload)
# TODO: error handling
var resolverAddr = response.parseJson["result"].getStr
resolverAddr.removePrefix("0x000000000000000000000000")
result = "0x" & resolverAddr
const owner_signature = "0x02571be3" # owner(bytes32 node)
proc owner*(username: string): string =
var userNameHash = namehash(addDomain(username))
userNameHash.removePrefix("0x")
let payload = %* [{
"to": registry,
"from": "0x0000000000000000000000000000000000000000",
"data": fmt"{owner_signature}{userNameHash}"
}, "latest"]
let response = callPrivateRPC("eth_call", payload)
# TODO: error handling
let ownerAddr = response.parseJson["result"].getStr;
if ownerAddr == "0x0000000000000000000000000000000000000000000000000000000000000000":
return ""
result = "0x" & ownerAddr.substr(26)
const pubkey_signature = "0xc8690233" # pubkey(bytes32 node)
proc pubkey*(username: string): string =
var userNameHash = namehash(addDomain(username))
userNameHash.removePrefix("0x")
let ensResolver = resolver(userNameHash)
let payload = %* [{
"to": ensResolver,
"from": "0x0000000000000000000000000000000000000000",
"data": fmt"{pubkey_signature}{userNameHash}"
}, "latest"]
let response = callPrivateRPC("eth_call", payload)
# TODO: error handling
var pubkey = response.parseJson["result"].getStr
if pubkey == "0x" or pubkey == "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000":
result = ""
else:
pubkey.removePrefix("0x")
result = "0x04" & pubkey
const address_signature = "0x3b3b57de" # addr(bytes32 node)
proc address*(username: string): string =
var userNameHash = namehash(addDomain(username))
userNameHash.removePrefix("0x")
let ensResolver = resolver(userNameHash)
let payload = %* [{
"to": ensResolver,
"from": "0x0000000000000000000000000000000000000000",
"data": fmt"{address_signature}{userNameHash}"
}, "latest"]
let response = callPrivateRPC("eth_call", payload)
# TODO: error handling
let address = response.parseJson["result"].getStr;
if address == "0x0000000000000000000000000000000000000000000000000000000000000000":
return ""
result = "0x" & address.substr(26)
const contenthash_signature = "0xbc1c58d1" # contenthash(bytes32)
proc contenthash*(ensAddr: string): string =
var ensHash = namehash(ensAddr)
ensHash.removePrefix("0x")
let ensResolver = resolver(ensHash)
let payload = %* [{
"to": ensResolver,
"from": "0x0000000000000000000000000000000000000000",
"data": fmt"{contenthash_signature}{ensHash}"
}, "latest"]
let response = callPrivateRPC("eth_call", payload)
let bytesResponse = response.parseJson["result"].getStr;
if bytesResponse == "0x":
return ""
let size = fromHex(Stuint[256], bytesResponse[66..129]).truncate(int)
result = bytesResponse[130..129+size*2]
proc getPrice*(): Stuint[256] =
let
contract = contracts.getContract("ens-usernames")
payload = %* [{
"to": $contract.address,
"data": contract.methods["getPrice"].encodeAbi()
}, "latest"]
let responseStr = callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if not response.error.isNil:
raise newException(RpcException, "Error getting ens username price: " & response.error.message)
if response.result == "0x":
raise newException(RpcException, "Error getting ens username price: 0x")
result = fromHex(Stuint[256], response.result)
proc releaseEstimateGas*(username: string, address: string, success: var bool): int =
let
label = fromHex(FixedBytes[32], label(username))
ensUsernamesContract = contracts.getContract("ens-usernames")
release = Release(label: label)
var tx = transactions.buildTokenTransaction(parseAddress(address), ensUsernamesContract.address, "", "")
try:
let response = ensUsernamesContract.methods["release"].estimateGas(tx, release, success)
if success:
result = fromHex[int](response)
except RpcException as e:
error "Could not estimate gas for ens release", err=e.msg
proc release*(username: string, address: string, gas, gasPrice, password: string, success: var bool): string =
let
label = fromHex(FixedBytes[32], label(username))
ensUsernamesContract = contracts.getContract("ens-usernames")
release = Release(label: label)
var tx = transactions.buildTokenTransaction(parseAddress(address), ensUsernamesContract.address, "", "")
try:
result = ensUsernamesContract.methods["release"].send(tx, release, password, success)
if success:
trackPendingTransaction(result, address, $ensUsernamesContract.address, PendingTransactionType.ReleaseENS, username)
except RpcException as e:
error "Could not estimate gas for ens release", err=e.msg
proc getExpirationTime*(username: string, success: var bool): int =
let
label = fromHex(FixedBytes[32], label(username))
expTime = ExpirationTime(label: label)
ensUsernamesContract = contracts.getContract("ens-usernames")
var tx = transactions.buildTransaction(parseAddress("0x0000000000000000000000000000000000000000"), 0.u256)
tx.to = ensUsernamesContract.address.some
tx.data = ensUsernamesContract.methods["getExpirationTime"].encodeAbi(expTime)
var response = ""
try:
response = eth_transactions.call(tx).result
success = true
except RpcException as e:
success = false
error "Error obtaining expiration time", err=e.msg
if success:
result = fromHex[int](response)
proc extractCoordinates*(pubkey: string):tuple[x: string, y:string] =
result = ("0x" & pubkey[4..67], "0x" & pubkey[68..131])
proc registerUsernameEstimateGas*(username: string, address: string, pubKey: string, success: var bool): int =
let
label = fromHex(FixedBytes[32], label(username))
coordinates = extractCoordinates(pubkey)
x = fromHex(FixedBytes[32], coordinates.x)
y = fromHex(FixedBytes[32], coordinates.y)
ensUsernamesContract = contracts.getContract("ens-usernames")
sntContract = contracts.getSntContract()
price = getPrice()
let
register = Register(label: label, account: parseAddress(address), x: x, y: y)
registerAbiEncoded = ensUsernamesContract.methods["register"].encodeAbi(register)
approveAndCallObj = ApproveAndCall[132](to: ensUsernamesContract.address, value: price, data: DynamicBytes[132].fromHex(registerAbiEncoded))
approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi(approveAndCallObj)
var tx = transactions.buildTokenTransaction(parseAddress(address), sntContract.address, "", "")
let response = sntContract.methods["approveAndCall"].estimateGas(tx, approveAndCallObj, success)
if success:
result = fromHex[int](response)
proc registerUsername*(username, pubKey, address, gas, gasPrice, password: string, success: var bool): string =
let
label = fromHex(FixedBytes[32], label(username))
coordinates = extractCoordinates(pubkey)
x = fromHex(FixedBytes[32], coordinates.x)
y = fromHex(FixedBytes[32], coordinates.y)
ensUsernamesContract = contracts.getContract("ens-usernames")
sntContract = contracts.getSntContract()
price = getPrice()
let
register = Register(label: label, account: parseAddress(address), x: x, y: y)
registerAbiEncoded = ensUsernamesContract.methods["register"].encodeAbi(register)
approveAndCallObj = ApproveAndCall[132](to: ensUsernamesContract.address, value: price, data: DynamicBytes[132].fromHex(registerAbiEncoded))
var tx = transactions.buildTokenTransaction(parseAddress(address), sntContract.address, gas, gasPrice)
result = sntContract.methods["approveAndCall"].send(tx, approveAndCallObj, password, success)
if success:
trackPendingTransaction(result, address, $sntContract.address, PendingTransactionType.RegisterENS, username & domain)
proc setPubKeyEstimateGas*(username: string, address: string, pubKey: string, success: var bool): int =
var hash = namehash(username)
hash.removePrefix("0x")
let
label = fromHex(FixedBytes[32], "0x" & hash)
x = fromHex(FixedBytes[32], "0x" & pubkey[4..67])
y = fromHex(FixedBytes[32], "0x" & pubkey[68..131])
resolverContract = contracts.getContract("ens-resolver")
setPubkey = SetPubkey(label: label, x: x, y: y)
resolverAddress = resolver(hash)
var tx = transactions.buildTokenTransaction(parseAddress(address), parseAddress(resolverAddress), "", "")
try:
let response = resolverContract.methods["setPubkey"].estimateGas(tx, setPubkey, success)
if success:
result = fromHex[int](response)
except RpcException as e:
raise
proc setPubKey*(username, pubKey, address, gas, gasPrice, password: string, success: var bool): string =
var hash = namehash(username)
hash.removePrefix("0x")
let
label = fromHex(FixedBytes[32], "0x" & hash)
x = fromHex(FixedBytes[32], "0x" & pubkey[4..67])
y = fromHex(FixedBytes[32], "0x" & pubkey[68..131])
resolverContract = contracts.getContract("ens-resolver")
setPubkey = SetPubkey(label: label, x: x, y: y)
resolverAddress = resolver(hash)
var tx = transactions.buildTokenTransaction(parseAddress(address), parseAddress(resolverAddress), gas, gasPrice)
try:
result = resolverContract.methods["setPubkey"].send(tx, setPubkey, password, success)
if success:
trackPendingTransaction(result, $address, resolverAddress, PendingTransactionType.SetPubKey, username)
except RpcException as e:
raise
proc statusRegistrarAddress*():string =
result = $contracts.getContract("ens-usernames").address
type
ENSType* {.pure.} = enum
IPFS,
SWARM,
IPNS,
UNKNOWN
proc decodeENSContentHash*(value: string): tuple[ensType: ENSType, output: string] =
if value == "":
return (ENSType.UNKNOWN, "")
if value[0..5] == "e40101":
return (ENSType.SWARM, value.split("1b20")[1])
if value[0..7] == "e3010170":
try:
let defaultCodec = parseHexInt("70") #dag-pb
var codec = defaultCodec # no codec specified
var codecStartIdx = 2 # idx of where codec would start if it was specified
# handle the case when starts with 0xe30170 instead of 0xe3010170
if value[2..5] == "0101":
codecStartIdx = 6
codec = parseHexInt(value[6..7])
elif value[2..3] == "01" and value[4..5] != "12":
codecStartIdx = 4
codec = parseHexInt(value[4..5])
# strip the info we no longer need
var multiHashStr = value[codecStartIdx + 2..<value.len]
# The rest of the hash identifies the multihash algo, length, and digest
# More info: https://multiformats.io/multihash/
# 12 = identifies sha2-256 hash
# 20 = multihash length = 32
# ...rest = multihash digest
let
multiHash = MultiHash.init(nimcrypto.fromHex(multiHashStr)).get()
decoded = Cid.init(CIDv0, MultiCodec.codec(codec), multiHash).get()
return (ENSType.IPFS, $decoded)
except Exception as e:
error "Error decoding ENS contenthash", hash=value, exception=e.msg
raise
if value[0..8] == "e50101700":
return (ENSType.IPNS, parseHexStr(value[12..value.len-1]))
return (ENSType.UNKNOWN, "")
proc validateEnsName*(ens: string, isStatus: bool, usernames: seq[string]): string =
var username = ens & (if(isStatus): domain else: "")
result = ""
if usernames.filter(proc(x: string):bool = x == username).len > 0:
result = "already-connected"
else:
let ownerAddr = owner(username)
if ownerAddr == "" and isStatus:
result = "available"
else:
let userPubKey = status_settings.getSetting2[string](Setting.PublicKey, "0x0")
let userWallet = status_wallet.getWalletAccounts()[0].address
let ens_pubkey = pubkey(ens)
if ownerAddr != "":
if ens_pubkey == "" and ownerAddr == userWallet:
result = "owned" # "Continuing will connect this username with your chat key."
elif ens_pubkey == userPubkey:
result = "connected"
elif ownerAddr == userWallet:
result = "connected-different-key" # "Continuing will require a transaction to connect the username with your current chat key.",
else:
result = "taken"
else:
result = "taken"

15
status/fleet.nim Normal file
View File

@ -0,0 +1,15 @@
import ./types/[fleet]
export fleet
type
FleetModel* = ref object
config*: FleetConfig
proc newFleetModel*(fleetConfigJson: string): FleetModel =
result = FleetModel()
result.config = fleetConfigJson.toFleetConfig()
proc delete*(self: FleetModel) =
discard

151
status/gif.nim Normal file
View File

@ -0,0 +1,151 @@
import httpclient
import json
import strformat
import os
import sequtils
from libstatus/gif import getRecentGifs, getFavoriteGifs, setFavoriteGifs, setRecentGifs
const MAX_RECENT = 50
# set via `nim c` param `-d:TENOR_API_KEY:[api_key]`; should be set in CI/release builds
const TENOR_API_KEY {.strdefine.} = ""
let TENOR_API_KEY_ENV = $getEnv("TENOR_API_KEY")
let TENOR_API_KEY_RESOLVED =
if TENOR_API_KEY_ENV != "":
TENOR_API_KEY_ENV
else:
TENOR_API_KEY
const baseUrl = "https://g.tenor.com/v1/"
let defaultParams = fmt("&media_filter=minimal&limit=50&key={TENOR_API_KEY_RESOLVED}")
type
GifItem* = object
id*: string
title*: string
url*: string
tinyUrl*: string
height*: int
proc tenorToGifItem(jsonMsg: JsonNode): GifItem =
return GifItem(
id: jsonMsg{"id"}.getStr,
title: jsonMsg{"title"}.getStr,
url: jsonMsg{"media"}[0]["gif"]["url"].getStr,
tinyUrl: jsonMsg{"media"}[0]["tinygif"]["url"].getStr,
height: jsonMsg{"media"}[0]["gif"]["dims"][1].getInt
)
proc settingToGifItem(jsonMsg: JsonNode): GifItem =
return GifItem(
id: jsonMsg{"id"}.getStr,
title: jsonMsg{"title"}.getStr,
url: jsonMsg{"url"}.getStr,
tinyUrl: jsonMsg{"tinyUrl"}.getStr,
height: jsonMsg{"height"}.getInt
)
proc toJsonNode*(self: GifItem): JsonNode =
result = %* {
"id": self.id,
"title": self.title,
"url": self.url,
"tinyUrl": self.tinyUrl,
"height": self.height
}
proc `$`*(self: GifItem): string =
return fmt"GifItem(id:{self.id}, title:{self.title}, url:{self.url}, tinyUrl:{self.tinyUrl}, height:{self.height})"
type
GifClient* = ref object
client: HttpClient
favorites: seq[GifItem]
recents: seq[GifItem]
favoritesLoaded: bool
recentsLoaded: bool
proc newGifClient*(): GifClient =
result = GifClient()
result.client = newHttpClient()
result.favorites = @[]
result.recents = @[]
proc tenorQuery(self: GifClient, path: string): seq[GifItem] =
try:
let content = self.client.getContent(fmt("{baseUrl}{path}{defaultParams}"))
let doc = content.parseJson()
var items: seq[GifItem] = @[]
for json in doc["results"]:
items.add(tenorToGifItem(json))
return items
except:
echo getCurrentExceptionMsg()
return @[]
proc search*(self: GifClient, query: string): seq[GifItem] =
return self.tenorQuery(fmt("search?q={query}"))
proc getTrendings*(self: GifClient): seq[GifItem] =
return self.tenorQuery("trending?")
proc getFavorites*(self: GifClient): seq[GifItem] =
if not self.favoritesLoaded:
self.favoritesLoaded = true
self.favorites = map(getFavoriteGifs(){"items"}.getElems(), settingToGifItem)
return self.favorites
proc getRecents*(self: GifClient): seq[GifItem] =
if not self.recentsLoaded:
self.recentsLoaded = true
self.recents = map(getRecentGifs(){"items"}.getElems(), settingToGifItem)
return self.recents
proc isFavorite*(self: GifClient, gifItem: GifItem): bool =
for favorite in self.getFavorites():
if favorite.id == gifItem.id:
return true
return false
proc toggleFavorite*(self: GifClient, gifItem: GifItem) =
var newFavorites: seq[GifItem] = @[]
var found = false
for favoriteGif in self.getFavorites():
if favoriteGif.id == gifItem.id:
found = true
continue
newFavorites.add(favoriteGif)
if not found:
newFavorites.add(gifItem)
self.favorites = newFavorites
setFavoriteGifs(%*{"items": map(newFavorites, toJsonNode)})
proc addToRecents*(self: GifClient, gifItem: GifItem) =
let recents = self.getRecents()
var newRecents: seq[GifItem] = @[gifItem]
var idx = 0
while idx < MAX_RECENT - 1:
if idx >= recents.len:
break
if recents[idx].id == gifItem.id:
idx += 1
continue
newRecents.add(recents[idx])
idx += 1
self.recents = newRecents
setRecentGifs(%*{"items": map(newRecents, toJsonNode)})

View File

@ -0,0 +1,393 @@
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
import ../types/[account, fleet, rpc_response]
import ../signals/[base]
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["ClusterConfig"]["WakuNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Waku)
result["ClusterConfig"]["WakuStoreNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Waku)
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)
# 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 getDefaultAccount*(): string =
var response = callPrivateRPC("eth_accounts")
result = parseJson(response)["result"][0].getStr()
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 generateAlias*(publicKey: string): string =
result = $status_go.generateAlias(publicKey)
proc generateIdenticon*(publicKey: string): string =
result = $status_go.identicon(publicKey)
proc initNode*() =
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*(): 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): 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

View File

@ -0,0 +1,235 @@
import # std libs
json, os, sequtils, strutils
import # vendor libs
confutils
const GENERATED* = "generated"
const SEED* = "seed"
const KEY* = "key"
const WATCH* = "watch"
const ZERO_ADDRESS* = "0x0000000000000000000000000000000000000000"
const PATH_WALLET_ROOT* = "m/44'/60'/0'/0"
# EIP1581 Root Key, the extended key from which any whisper key/encryption key can be derived
const PATH_EIP_1581* = "m/43'/60'/1581'"
# BIP44-0 Wallet key, the default wallet key
const PATH_DEFAULT_WALLET* = PATH_WALLET_ROOT & "/0"
# EIP1581 Chat Key 0, the default whisper key
const PATH_WHISPER* = PATH_EIP_1581 & "/0'/0"
# set via `nim c` param `-d:INFURA_TOKEN:[token]`; should be set in CI/release builds
const INFURA_TOKEN {.strdefine.} = ""
# allow runtime override via environment variable; core contributors can set a
# release token in this way for local development
let INFURA_TOKEN_ENV = $getEnv("INFURA_TOKEN")
let INFURA_TOKEN_RESOLVED =
if INFURA_TOKEN_ENV != "":
INFURA_TOKEN_ENV
else:
INFURA_TOKEN
let DEFAULT_NETWORKS* = %* [
{
"id": "testnet_rpc",
"etherscan-link": "https://ropsten.etherscan.io/address/",
"name": "Ropsten with upstream RPC",
"config": {
"NetworkId": 3,
"DataDir": "/ethereum/testnet_rpc",
"UpstreamConfig": {
"Enabled": true,
"URL": "https://ropsten.infura.io/v3/" & INFURA_TOKEN_RESOLVED
}
}
},
{
"id": "rinkeby_rpc",
"etherscan-link": "https://rinkeby.etherscan.io/address/",
"name": "Rinkeby with upstream RPC",
"config": {
"NetworkId": 4,
"DataDir": "/ethereum/rinkeby_rpc",
"UpstreamConfig": {
"Enabled": true,
"URL": "https://rinkeby.infura.io/v3/" & INFURA_TOKEN_RESOLVED
}
}
},
{
"id": "goerli_rpc",
"etherscan-link": "https://goerli.etherscan.io/address/",
"name": "Goerli with upstream RPC",
"config": {
"NetworkId": 5,
"DataDir": "/ethereum/goerli_rpc",
"UpstreamConfig": {
"Enabled": true,
"URL": "https://goerli.blockscout.com/"
}
}
},
{
"id": "mainnet_rpc",
"etherscan-link": "https://etherscan.io/address/",
"name": "Mainnet with upstream RPC",
"config": {
"NetworkId": 1,
"DataDir": "/ethereum/mainnet_rpc",
"UpstreamConfig": {
"Enabled": true,
"URL": "https://mainnet.infura.io/v3/" & INFURA_TOKEN_RESOLVED
}
}
},
{
"id": "xdai_rpc",
"name": "xDai Chain",
"config": {
"NetworkId": 100,
"DataDir": "/ethereum/xdai_rpc",
"UpstreamConfig": {
"Enabled": true,
"URL": "https://dai.poa.network"
}
}
},
{
"id": "poa_rpc",
"name": "POA Network",
"config": {
"NetworkId": 99,
"DataDir": "/ethereum/poa_rpc",
"UpstreamConfig": {
"Enabled": true,
"URL": "https://core.poa.network"
}
}
}
]
var NODE_CONFIG* = %* {
"BrowsersConfig": {
"Enabled": true
},
"ClusterConfig": {
"Enabled": true
},
"DataDir": "./ethereum/mainnet",
"EnableNTPSync": true,
"KeyStoreDir": "./keystore",
# 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
#"ListenAddr": ":30304",
"LogEnabled": true,
"LogFile": "geth.log",
"LogLevel": "INFO",
"MailserversConfig": {
"Enabled": true
},
"Name": "StatusDesktop",
"NetworkId": 1,
"NoDiscovery": false,
"PermissionsConfig": {
"Enabled": true
},
"Rendezvous": true,
"RegisterTopics": @["whispermail"],
"RequireTopics": {
"whisper": {
"Max": 2,
"Min": 2
}
},
"ShhextConfig": {
"BackupDisabledDataDir": "./",
"DataSyncEnabled": true,
"InstallationID": "aef27732-8d86-5039-a32e-bdbe094d8791",
"MailServerConfirmations": true,
"MaxMessageDeliveryAttempts": 6,
"PFSEnabled": true,
"VerifyENSContractAddress": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
"VerifyENSURL": "https://mainnet.infura.io/v3/" & INFURA_TOKEN_RESOLVED,
"VerifyTransactionChainID": 1,
"VerifyTransactionURL": "https://mainnet.infura.io/v3/" & INFURA_TOKEN_RESOLVED
},
"StatusAccountsConfig": {
"Enabled": true
},
"UpstreamConfig": {
"Enabled": true,
"URL": "https://mainnet.infura.io/v3/" & INFURA_TOKEN_RESOLVED
},
"WakuConfig": {
"BloomFilterMode": true,
"Enabled": true,
"LightClient": true,
"MinimumPoW": 0.001
},
"WakuV2Config": {
"Enabled": false,
"Host": "0.0.0.0",
"Port": 0
},
"WalletConfig": {
"Enabled": true
}
}
const DEFAULT_NETWORK_NAME* = "mainnet_rpc"
const sep = when defined(windows): "\\" else: "/"
proc defaultDataDir(): string =
let homeDir = getHomeDir()
let parentDir =
if defined(development):
parentDir(getAppDir())
elif homeDir == "":
getCurrentDir()
elif defined(macosx):
joinPath(homeDir, "Library", "Application Support")
elif defined(windows):
let targetDir = getEnv("LOCALAPPDATA").string
if targetDir == "":
joinPath(homeDir, "AppData", "Local")
else:
targetDir
else:
let targetDir = getEnv("XDG_CONFIG_HOME").string
if targetDir == "":
joinPath(homeDir, ".config")
else:
targetDir
absolutePath(joinPath(parentDir, "Status"))
type StatusDesktopConfig = object
dataDir* {.
defaultValue: defaultDataDir()
desc: "Status Desktop data directory"
abbr: "d" .}: string
# On macOS the first time when a user gets the "App downloaded from the
# internet" warning, and clicks the Open button, the OS passes a unique process
# serial number (PSN) as -psn_... command-line argument, which we remove before
# processing the arguments with nim-confutils.
# Credit: https://github.com/bitcoin/bitcoin/blame/b6e34afe9735faf97d6be7a90fafd33ec18c0cbb/src/util/system.cpp#L383-L389
var cliParams = commandLineParams()
if defined(macosx):
cliParams.keepIf(proc(p: string): bool = not p.startsWith("-psn_"))
let desktopConfig = StatusDesktopConfig.load(cliParams)
let
baseDir = absolutePath(expandTilde(desktopConfig.dataDir))
DATADIR* = baseDir & sep
STATUSGODIR* = joinPath(baseDir, "data") & sep
KEYSTOREDIR* = joinPath(baseDir, "data", "keystore") & sep
TMPDIR* = joinPath(baseDir, "tmp") & sep
LOGDIR* = joinPath(baseDir, "logs") & sep
createDir(DATADIR)
createDir(TMPDIR)
createDir(LOGDIR)

View File

@ -0,0 +1,623 @@
const phrases*: seq[string] = @[
"acid",
"alto",
"apse",
"arch",
"area",
"army",
"atom",
"aunt",
"babe",
"baby",
"back",
"bail",
"bait",
"bake",
"ball",
"band",
"bank",
"barn",
"base",
"bass",
"bath",
"bead",
"beak",
"beam",
"bean",
"bear",
"beat",
"beef",
"beer",
"beet",
"bell",
"belt",
"bend",
"bike",
"bill",
"bird",
"bite",
"blow",
"blue",
"boar",
"boat",
"body",
"bolt",
"bomb",
"bone",
"book",
"boot",
"bore",
"boss",
"bowl",
"brow",
"bulb",
"bull",
"burn",
"bush",
"bust",
"cafe",
"cake",
"calf",
"call",
"calm",
"camp",
"cane",
"cape",
"card",
"care",
"carp",
"cart",
"case",
"cash",
"cast",
"cave",
"cell",
"cent",
"chap",
"chef",
"chin",
"chip",
"chop",
"chub",
"chug",
"city",
"clam",
"clef",
"clip",
"club",
"clue",
"coal",
"coat",
"code",
"coil",
"coin",
"coke",
"cold",
"colt",
"comb",
"cone",
"cook",
"cope",
"copy",
"cord",
"cork",
"corn",
"cost",
"crab",
"craw",
"crew",
"crib",
"crop",
"crow",
"curl",
"cyst",
"dame",
"dare",
"dark",
"dart",
"dash",
"data",
"date",
"dead",
"deal",
"dear",
"debt",
"deck",
"deep",
"deer",
"desk",
"dhow",
"diet",
"dill",
"dime",
"dirt",
"dish",
"disk",
"dock",
"doll",
"door",
"dory",
"drag",
"draw",
"drop",
"drug",
"drum",
"duck",
"dump",
"dust",
"duty",
"ease",
"east",
"eave",
"eddy",
"edge",
"envy",
"epee",
"exam",
"exit",
"face",
"fact",
"fail",
"fall",
"fame",
"fang",
"farm",
"fawn",
"fear",
"feed",
"feel",
"feet",
"file",
"fill",
"film",
"find",
"fine",
"fire",
"fish",
"flag",
"flat",
"flax",
"flow",
"foam",
"fold",
"font",
"food",
"foot",
"fork",
"form",
"fort",
"fowl",
"frog",
"fuel",
"full",
"gain",
"gale",
"galn",
"game",
"garb",
"gate",
"gear",
"gene",
"gift",
"girl",
"give",
"glad",
"glen",
"glue",
"glut",
"goal",
"goat",
"gold",
"golf",
"gong",
"good",
"gown",
"grab",
"gram",
"gray",
"grey",
"grip",
"grit",
"gyro",
"hail",
"hair",
"half",
"hall",
"hand",
"hang",
"harm",
"harp",
"hate",
"hawk",
"head",
"heat",
"heel",
"hell",
"helo",
"help",
"hemp",
"herb",
"hide",
"high",
"hill",
"hire",
"hive",
"hold",
"hole",
"home",
"hood",
"hoof",
"hook",
"hope",
"hops",
"horn",
"hose",
"host",
"hour",
"hunt",
"hurt",
"icon",
"idea",
"inch",
"iris",
"iron",
"item",
"jail",
"jeep",
"jeff",
"joey",
"join",
"joke",
"judo",
"jump",
"junk",
"jury",
"jute",
"kale",
"keep",
"kick",
"kill",
"kilt",
"kind",
"king",
"kiss",
"kite",
"knee",
"knot",
"lace",
"lack",
"lady",
"lake",
"lamb",
"lamp",
"land",
"lark",
"lava",
"lawn",
"lead",
"leaf",
"leek",
"lier",
"life",
"lift",
"lily",
"limo",
"line",
"link",
"lion",
"lisa",
"list",
"load",
"loaf",
"loan",
"lock",
"loft",
"long",
"look",
"loss",
"lout",
"love",
"luck",
"lung",
"lute",
"lynx",
"lyre",
"maid",
"mail",
"main",
"make",
"male",
"mall",
"manx",
"many",
"mare",
"mark",
"mask",
"mass",
"mate",
"math",
"meal",
"meat",
"meet",
"menu",
"mess",
"mice",
"midi",
"mile",
"milk",
"mime",
"mind",
"mine",
"mini",
"mint",
"miss",
"mist",
"moat",
"mode",
"mole",
"mood",
"moon",
"most",
"moth",
"move",
"mule",
"mutt",
"nail",
"name",
"neat",
"neck",
"need",
"neon",
"nest",
"news",
"node",
"nose",
"note",
"oboe",
"okra",
"open",
"oval",
"oven",
"oxen",
"pace",
"pack",
"page",
"pail",
"pain",
"pair",
"palm",
"pard",
"park",
"part",
"pass",
"past",
"path",
"peak",
"pear",
"peen",
"peer",
"pelt",
"perp",
"pest",
"pick",
"pier",
"pike",
"pile",
"pimp",
"pine",
"ping",
"pink",
"pint",
"pipe",
"piss",
"pith",
"plan",
"play",
"plot",
"plow",
"poem",
"poet",
"pole",
"polo",
"pond",
"pony",
"poof",
"pool",
"port",
"post",
"prow",
"pull",
"puma",
"pump",
"pupa",
"push",
"quit",
"race",
"rack",
"raft",
"rage",
"rail",
"rain",
"rake",
"rank",
"rate",
"read",
"rear",
"reef",
"rent",
"rest",
"rice",
"rich",
"ride",
"ring",
"rise",
"risk",
"road",
"robe",
"rock",
"role",
"roll",
"roof",
"room",
"root",
"rope",
"rose",
"ruin",
"rule",
"rush",
"ruth",
"sack",
"safe",
"sage",
"sail",
"sale",
"salt",
"sand",
"sari",
"sash",
"save",
"scow",
"seal",
"seat",
"seed",
"self",
"sell",
"shed",
"shin",
"ship",
"shoe",
"shop",
"shot",
"show",
"sick",
"side",
"sign",
"silk",
"sill",
"silo",
"sing",
"sink",
"site",
"size",
"skin",
"sled",
"slip",
"smog",
"snob",
"snow",
"soap",
"sock",
"soda",
"sofa",
"soft",
"soil",
"song",
"soot",
"sort",
"soup",
"spot",
"spur",
"stag",
"star",
"stay",
"stem",
"step",
"stew",
"stop",
"stud",
"suck",
"suit",
"swan",
"swim",
"tail",
"tale",
"talk",
"tank",
"tard",
"task",
"taxi",
"team",
"tear",
"teen",
"tell",
"temp",
"tent",
"term",
"test",
"text",
"thaw",
"tile",
"till",
"time",
"tire",
"toad",
"toga",
"togs",
"tone",
"tool",
"toot",
"tote",
"tour",
"town",
"tram",
"tray",
"tree",
"trim",
"trip",
"tuba",
"tube",
"tuna",
"tune",
"turn",
"tutu",
"twig",
"type",
"unit",
"user",
"vane",
"vase",
"vast",
"veal",
"veil",
"vein",
"vest",
"vibe",
"view",
"vise",
"wait",
"wake",
"walk",
"wall",
"wash",
"wasp",
"wave",
"wear",
"weed",
"week",
"well",
"west",
"whip",
"wife",
"will",
"wind",
"wine",
"wing",
"wire",
"wish",
"wolf",
"wood",
"wool",
"word",
"work",
"worm",
"wrap",
"wren",
"yard",
"yarn",
"yawl",
"year",
"yoga",
"yoke",
"yurt",
"zinc",
"zone"]

View File

@ -0,0 +1,27 @@
import core, ../types/[bookmark], json, chronicles
proc storeBookmark*(url: string, name: string): Bookmark =
let payload = %* [{"url": url, "name": name}]
result = Bookmark(name: name, url: url)
try:
let resp = callPrivateRPC("browsers_storeBookmark", payload).parseJson["result"]
result.imageUrl = resp["imageUrl"].getStr
except Exception as e:
error "Error updating bookmark", msg = e.msg
discard
proc updateBookmark*(ogUrl: string, url: string, name: string) =
let payload = %* [ogUrl, {"url": url, "name": name}]
try:
discard callPrivateRPC("browsers_updateBookmark", payload)
except Exception as e:
error "Error updating bookmark", msg = e.msg
discard
proc getBookmarks*(): string =
let payload = %* []
result = callPrivateRPC("browsers_getBookmarks", payload)
proc deleteBookmark*(url: string) =
let payload = %* [url]
discard callPrivateRPC("browsers_deleteBookmark", payload)

592
status/libstatus/chat.nim Normal file
View File

@ -0,0 +1,592 @@
import json, times, strutils, sequtils, chronicles, json_serialization, algorithm, strformat, sugar
import core, ../utils
import ../types/[chat, message, community, activity_center_notification,
status_update, rpc_response, setting, sticker]
import ./settings as status_settings
proc buildFilter*(chat: Chat):JsonNode =
if chat.chatType == ChatType.PrivateGroupChat:
return newJNull()
result = %* { "ChatID": chat.id, "OneToOne": chat.chatType == ChatType.OneToOne }
proc loadFilters*(filters: seq[JsonNode]): string =
result = callPrivateRPC("loadFilters".prefix, %* [filter(filters, proc(x:JsonNode):bool = x.kind != JNull)])
proc removeFilters*(chatId: string, filterId: string) =
discard callPrivateRPC("removeFilters".prefix, %* [
[{ "ChatID": chatId, "FilterID": filterId }]
])
proc saveChat*(chatId: string, chatType: ChatType, active: bool = true, color: string = "#000000", ensName: string = "", profile: string = "", joined: int64 = 0) =
# TODO: ideally status-go/stimbus should handle some of these fields instead of having the client
# send them: lastMessage, unviewedMEssagesCount, timestamp, lastClockValue, name?
discard callPrivateRPC("saveChat".prefix, %* [
{
"lastClockValue": 0, # TODO:
"color": color,
"name": (if ensName != "": ensName else: chatId),
"lastMessage": nil, # TODO:
"active": active,
"profile": profile,
"id": chatId,
"unviewedMessagesCount": 0, # TODO:
"chatType": chatType.int,
"timestamp": 1588940692659, # TODO:
"joined": joined
}
])
proc createPublicChat*(chatId: string):string =
callPrivateRPC("createPublicChat".prefix, %* [{"ID": chatId}])
proc createOneToOneChat*(chatId: string):string =
callPrivateRPC("createOneToOneChat".prefix, %* [{"ID": chatId}])
proc deactivateChat*(chat: Chat):string =
chat.isActive = false
callPrivateRPC("deactivateChat".prefix, %* [{ "ID": chat.id }])
proc createProfileChat*(pubKey: string):string =
callPrivateRPC("createProfileChat".prefix, %* [{ "ID": pubKey }])
proc sortChats(x, y: Chat): int =
var t1 = x.lastMessage.whisperTimestamp
var t2 = y.lastMessage.whisperTimestamp
if t1 <= $x.joined:
t1 = $x.joined
if t2 <= $y.joined:
t2 = $y.joined
if t1 > t2: 1
elif t1 == t2: 0
else: -1
proc loadChats*(): seq[Chat] =
result = @[]
let jsonResponse = parseJson($callPrivateRPC("chats".prefix))
if jsonResponse["result"].kind != JNull:
for jsonChat in jsonResponse{"result"}:
let chat = jsonChat.toChat
if chat.isActive and chat.chatType != ChatType.Unknown:
result.add(chat)
result.sort(sortChats)
proc parseActivityCenterNotifications*(rpcResult: JsonNode): (string, seq[ActivityCenterNotification]) =
var notifs: seq[ActivityCenterNotification] = @[]
var msg: Message
if rpcResult{"notifications"}.kind != JNull:
for jsonMsg in rpcResult["notifications"]:
notifs.add(jsonMsg.toActivityCenterNotification())
return (rpcResult{"cursor"}.getStr, notifs)
proc statusUpdates*(): seq[StatusUpdate] =
let rpcResult = callPrivateRPC("statusUpdates".prefix, %* []).parseJson()["result"]
if rpcResult != nil and rpcResult{"statusUpdates"} != nil and rpcResult["statusUpdates"].len != 0:
for jsonStatusUpdate in rpcResult["statusUpdates"]:
result.add(jsonStatusUpdate.toStatusUpdate)
proc fetchChatMessages*(chatId: string, cursorVal: string, limit: int, success: var bool): string =
success = true
try:
result = callPrivateRPC("chatMessages".prefix, %* [chatId, cursorVal, limit])
except RpcException as e:
success = false
result = e.msg
proc editMessage*(messageId: string, msg: string): string =
callPrivateRPC("editMessage".prefix, %* [
{
"id": messageId,
"text": msg
}
])
proc deleteMessageAndSend*(messageId: string): string =
callPrivateRPC("deleteMessageAndSend".prefix, %* [messageId])
proc rpcReactions*(chatId: string, cursorVal: string, limit: int, success: var bool): string =
success = true
try:
result = callPrivateRPC("emojiReactionsByChatID".prefix, %* [chatId, cursorVal, limit])
except RpcException as e:
success = false
result = e.msg
proc addEmojiReaction*(chatId: string, messageId: string, emojiId: int): seq[Reaction] =
let rpcResult = parseJson(callPrivateRPC("sendEmojiReaction".prefix, %* [chatId, messageId, emojiId]))["result"]
var reactions: seq[Reaction] = @[]
if rpcResult != nil and rpcResult["emojiReactions"] != nil and rpcResult["emojiReactions"].len != 0:
for jsonMsg in rpcResult["emojiReactions"]:
reactions.add(jsonMsg.toReaction)
result = reactions
proc removeEmojiReaction*(emojiReactionId: string): seq[Reaction] =
let rpcResult = parseJson(callPrivateRPC("sendEmojiReactionRetraction".prefix, %* [emojiReactionId]))["result"]
var reactions: seq[Reaction] = @[]
if rpcResult != nil and rpcResult["emojiReactions"] != nil and rpcResult["emojiReactions"].len != 0:
for jsonMsg in rpcResult["emojiReactions"]:
reactions.add(jsonMsg.toReaction)
result = reactions
# TODO this probably belongs in another file
proc generateSymKeyFromPassword*(): string =
result = ($parseJson(callPrivateRPC("waku_generateSymKeyFromPassword", %* [
# TODO unhardcode this for non-status mailservers
"status-offline-inbox"
]))["result"]).strip(chars = {'"'})
proc sendChatMessage*(chatId: string, msg: string, replyTo: string, contentType: int, communityId: string = ""): string =
let preferredUsername = getSetting[string](Setting.PreferredUsername, "")
callPrivateRPC("sendChatMessage".prefix, %* [
{
"chatId": chatId,
"text": msg,
"responseTo": replyTo,
"ensName": preferredUsername,
"sticker": nil,
"contentType": contentType,
"communityId": communityId
}
])
proc sendImageMessage*(chatId: string, image: string): string =
let preferredUsername = getSetting[string](Setting.PreferredUsername, "")
callPrivateRPC("sendChatMessage".prefix, %* [
{
"chatId": chatId,
"contentType": ContentType.Image.int,
"imagePath": image,
"ensName": preferredUsername,
"text": "Update to latest version to see a nice image here!"
}
])
proc sendImageMessages*(chatId: string, images: var seq[string]): string =
let
preferredUsername = getSetting[string](Setting.PreferredUsername, "")
let imagesJson = %* images.map(image => %*
{
"chatId": chatId,
"contentType": ContentType.Image.int,
"imagePath": image,
"ensName": preferredUsername,
"text": "Update to latest version to see a nice image here!"
}
)
callPrivateRPC("sendChatMessages".prefix, %* [imagesJson])
proc sendStickerMessage*(chatId: string, replyTo: string, sticker: Sticker): string =
let preferredUsername = getSetting[string](Setting.PreferredUsername, "")
callPrivateRPC("sendChatMessage".prefix, %* [
{
"chatId": chatId,
"text": "Update to latest version to see a nice sticker here!",
"responseTo": replyTo,
"ensName": preferredUsername,
"sticker": {
"hash": sticker.hash,
"pack": sticker.packId
},
"contentType": ContentType.Sticker.int
}
])
proc markAllRead*(chatId: string): string =
callPrivateRPC("markAllRead".prefix, %* [chatId])
proc markMessagesSeen*(chatId: string, messageIds: seq[string]): string =
callPrivateRPC("markMessagesSeen".prefix, %* [chatId, messageIds])
proc confirmJoiningGroup*(chatId: string): string =
callPrivateRPC("confirmJoiningGroup".prefix, %* [chatId])
proc leaveGroupChat*(chatId: string): string =
callPrivateRPC("leaveGroupChat".prefix, %* [nil, chatId, true])
proc clearChatHistory*(chatId: string): string =
callPrivateRPC("deleteMessagesByChatID".prefix, %* [chatId])
proc deleteMessage*(messageId: string): string =
callPrivateRPC("deleteMessage".prefix, %* [messageId])
proc renameGroup*(chatId: string, newName: string): string =
callPrivateRPC("changeGroupChatName".prefix, %* [nil, chatId, newName])
proc createGroup*(groupName: string, pubKeys: seq[string]): string =
callPrivateRPC("createGroupChatWithMembers".prefix, %* [nil, groupName, pubKeys])
proc createGroupChatFromInvitation*(groupName: string, chatID: string, adminPK: string): string =
callPrivateRPC("createGroupChatFromInvitation".prefix, %* [groupName, chatID, adminPK])
proc addGroupMembers*(chatId: string, pubKeys: seq[string]): string =
callPrivateRPC("addMembersToGroupChat".prefix, %* [nil, chatId, pubKeys])
proc kickGroupMember*(chatId: string, pubKey: string): string =
callPrivateRPC("removeMemberFromGroupChat".prefix, %* [nil, chatId, pubKey])
proc makeAdmin*(chatId: string, pubKey: string): string =
callPrivateRPC("addAdminsToGroupChat".prefix, %* [nil, chatId, [pubKey]])
proc updateOutgoingMessageStatus*(messageId: string, status: string): string =
result = callPrivateRPC("updateMessageOutgoingStatus".prefix, %* [messageId, status])
# TODO: handle errors
proc reSendChatMessage*(messageId: string): string =
result = callPrivateRPC("reSendChatMessage".prefix, %*[messageId])
proc muteChat*(chatId: string): string =
result = callPrivateRPC("muteChat".prefix, %*[chatId])
proc unmuteChat*(chatId: string): string =
result = callPrivateRPC("unmuteChat".prefix, %*[chatId])
proc getLinkPreviewData*(link: string, success: var bool): JsonNode =
let
responseStr = callPrivateRPC("getLinkPreviewData".prefix, %*[link])
response = Json.decode(responseStr, RpcResponseTyped[JsonNode], allowUnknownFields = false)
if not response.error.isNil:
success = false
return %* { "error": fmt"""Error getting link preview data for '{link}': {response.error.message}""" }
success = true
response.result
proc getAllComunities*(): seq[Community] =
var communities: seq[Community] = @[]
let rpcResult = callPrivateRPC("communities".prefix).parseJSON()
if rpcResult{"result"}.kind != JNull:
for jsonCommunity in rpcResult["result"]:
var community = jsonCommunity.toCommunity()
communities.add(community)
return communities
proc getJoinedComunities*(): seq[Community] =
var communities: seq[Community] = @[]
let rpcResult = callPrivateRPC("joinedCommunities".prefix).parseJSON()
if rpcResult{"result"}.kind != JNull:
for jsonCommunity in rpcResult["result"]:
var community = jsonCommunity.toCommunity()
communities.add(community)
return communities
proc createCommunity*(name: string, description: string, access: int, ensOnly: bool, color: string, imageUrl: string, aX: int, aY: int, bX: int, bY: int): Community =
let rpcResult = callPrivateRPC("createCommunity".prefix, %*[{
# TODO this will need to be renamed membership (small m)
"Membership": access,
"name": name,
"description": description,
"ensOnly": ensOnly,
"color": color,
"image": imageUrl,
"imageAx": aX,
"imageAy": aY,
"imageBx": bX,
"imageBy": bY
}]).parseJSON()
if rpcResult{"error"} != nil:
let error = Json.decode($rpcResult{"error"}, RpcError)
raise newException(RpcException, "Error creating community: " & error.message)
if rpcResult{"result"} != nil and rpcResult{"result"}.kind != JNull:
result = rpcResult["result"]["communities"][0].toCommunity()
proc editCommunity*(communityId: string, name: string, description: string, access: int, ensOnly: bool, color: string, imageUrl: string, aX: int, aY: int, bX: int, bY: int): Community =
let rpcResult = callPrivateRPC("editCommunity".prefix, %*[{
# TODO this will need to be renamed membership (small m)
"CommunityID": communityId,
"Membership": access,
"name": name,
"description": description,
"ensOnly": ensOnly,
"color": color,
"image": imageUrl,
"imageAx": aX,
"imageAy": aY,
"imageBx": bX,
"imageBy": bY
}]).parseJSON()
if rpcResult{"error"} != nil:
let error = Json.decode($rpcResult{"error"}, RpcError)
raise newException(RpcException, "Error editing community: " & error.message)
if rpcResult{"result"} != nil and rpcResult{"result"}.kind != JNull:
result = rpcResult["result"]["communities"][0].toCommunity()
proc createCommunityChannel*(communityId: string, name: string, description: string): Chat =
let rpcResult = callPrivateRPC("createCommunityChat".prefix, %*[
communityId,
{
"permissions": {
"access": 1 # TODO get this from user selected privacy setting
},
"identity": {
"display_name": name,
"description": description#,
# "color": color#,
# TODO add images once it is supported by Status-Go
# "images": [
# {
# "payload": image,
# # TODO get that from an enum
# "image_type": 1 # 1 is a raw payload
# }
# ]
}
}]).parseJSON()
if rpcResult{"error"} != nil:
let error = Json.decode($rpcResult{"error"}, RpcError)
raise newException(RpcException, "Error creating community channel: " & error.message)
if rpcResult{"result"} != nil and rpcResult{"result"}.kind != JNull:
result = rpcResult["result"]["chats"][0].toChat()
proc editCommunityChannel*(communityId: string, channelId: string, name: string, description: string, categoryId: string): Chat =
let rpcResult = callPrivateRPC("editCommunityChat".prefix, %*[
communityId,
channelId.replace(communityId, ""),
{
"permissions": {
"access": 1 # TODO get this from user selected privacy setting
},
"identity": {
"display_name": name,
"description": description#,
# "color": color#,
# TODO add images once it is supported by Status-Go
# "images": [
# {
# "payload": image,
# # TODO get that from an enum
# "image_type": 1 # 1 is a raw payload
# }
# ]
},
"category_id": categoryId
}]).parseJSON()
if rpcResult{"error"} != nil:
let error = Json.decode($rpcResult{"error"}, RpcError)
raise newException(RpcException, "Error editing community channel: " & error.message)
if rpcResult{"result"} != nil and rpcResult{"result"}.kind != JNull:
result = rpcResult["result"]["chats"][0].toChat()
proc deleteCommunityChat*(communityId: string, chatId: string) =
discard callPrivateRPC("deleteCommunityChat".prefix, %*[communityId, chatId])
proc createCommunityCategory*(communityId: string, name: string, channels: seq[string]): CommunityCategory =
let rpcResult = callPrivateRPC("createCommunityCategory".prefix, %*[
{
"communityId": communityId,
"categoryName": name,
"chatIds": channels
}]).parseJSON()
if rpcResult.contains("error"):
raise newException(StatusGoException, rpcResult["error"]["message"].getStr())
else:
for k, v in rpcResult["result"]["communityChanges"].getElems()[0]["categoriesAdded"].pairs():
result.id = v["category_id"].getStr()
result.name = v["name"].getStr()
result.position = v{"position"}.getInt()
proc editCommunityCategory*(communityId: string, categoryId: string, name: string, channels: seq[string]) =
let rpcResult = callPrivateRPC("editCommunityCategory".prefix, %*[
{
"communityId": communityId,
"categoryId": categoryId,
"categoryName": name,
"chatIds": channels
}]).parseJSON()
if rpcResult.contains("error"):
raise newException(StatusGoException, rpcResult["error"]["message"].getStr())
proc reorderCommunityChat*(communityId: string, categoryId: string, chatId: string, position: int) =
let rpcResult = callPrivateRPC("reorderCommunityChat".prefix, %*[
{
"communityId": communityId,
"categoryId": categoryId,
"chatId": chatId,
"position": position
}]).parseJSON()
if rpcResult.contains("error"):
raise newException(StatusGoException, rpcResult["error"]["message"].getStr())
proc reorderCommunityCategories*(communityId: string, categoryId: string, position: int) =
let rpcResult = callPrivateRPC("reorderCommunityCategories".prefix, %*[
{
"communityId": communityId,
"categoryId": categoryId,
"position": position
}]).parseJSON()
if rpcResult.contains("error"):
raise newException(StatusGoException, rpcResult["error"]["message"].getStr())
proc deleteCommunityCategory*(communityId: string, categoryId: string) =
let rpcResult = callPrivateRPC("deleteCommunityCategory".prefix, %*[
{
"communityId": communityId,
"categoryId": categoryId
}]).parseJSON()
if rpcResult.contains("error"):
raise newException(StatusGoException, rpcResult["error"]["message"].getStr())
proc requestCommunityInfo*(communityId: string) =
discard callPrivateRPC("requestCommunityInfoFromMailserver".prefix, %*[communityId])
proc joinCommunity*(communityId: string) =
discard callPrivateRPC("joinCommunity".prefix, %*[communityId])
proc leaveCommunity*(communityId: string) =
discard callPrivateRPC("leaveCommunity".prefix, %*[communityId])
proc inviteUsersToCommunity*(communityId: string, pubKeys: seq[string]) =
discard callPrivateRPC("inviteUsersToCommunity".prefix, %*[{
"communityId": communityId,
"users": pubKeys
}])
proc exportCommunity*(communityId: string):string =
result = callPrivateRPC("exportCommunity".prefix, %*[communityId]).parseJson()["result"].getStr
proc importCommunity*(communityKey: string): string =
return callPrivateRPC("importCommunity".prefix, %*[communityKey])
proc removeUserFromCommunity*(communityId: string, pubKey: string) =
discard callPrivateRPC("removeUserFromCommunity".prefix, %*[communityId, pubKey])
proc requestToJoinCommunity*(communityId: string, ensName: string): seq[CommunityMembershipRequest] =
let rpcResult = callPrivateRPC("requestToJoinCommunity".prefix, %*[{
"communityId": communityId,
"ensName": ensName
}]).parseJSON()
var communityRequests: seq[CommunityMembershipRequest] = @[]
if rpcResult{"result"}{"requestsToJoinCommunity"} != nil and rpcResult{"result"}{"requestsToJoinCommunity"}.kind != JNull:
for jsonCommunityReqest in rpcResult["result"]["requestsToJoinCommunity"]:
communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest())
return communityRequests
proc acceptRequestToJoinCommunity*(requestId: string) =
discard callPrivateRPC("acceptRequestToJoinCommunity".prefix, %*[{
"id": requestId
}])
proc declineRequestToJoinCommunity*(requestId: string) =
discard callPrivateRPC("declineRequestToJoinCommunity".prefix, %*[{
"id": requestId
}])
proc pendingRequestsToJoinForCommunity*(communityId: string): seq[CommunityMembershipRequest] =
let rpcResult = callPrivateRPC("pendingRequestsToJoinForCommunity".prefix, %*[communityId]).parseJSON()
var communityRequests: seq[CommunityMembershipRequest] = @[]
if rpcResult{"result"}.kind != JNull:
for jsonCommunityReqest in rpcResult["result"]:
communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest())
return communityRequests
proc myPendingRequestsToJoin*(): seq[CommunityMembershipRequest] =
let rpcResult = callPrivateRPC("myPendingRequestsToJoin".prefix).parseJSON()
var communityRequests: seq[CommunityMembershipRequest] = @[]
if rpcResult.hasKey("result") and rpcResult{"result"}.kind != JNull:
for jsonCommunityReqest in rpcResult["result"]:
communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest())
return communityRequests
proc banUserFromCommunity*(pubKey: string, communityId: string): string =
return callPrivateRPC("banUserFromCommunity".prefix, %*[{
"communityId": communityId,
"user": pubKey
}])
proc setCommunityMuted*(communityId: string, muted: bool) =
discard callPrivateRPC("setCommunityMuted".prefix, %*[communityId, muted])
proc rpcPinnedChatMessages*(chatId: string, cursorVal: string, limit: int, success: var bool): string =
success = true
try:
result = callPrivateRPC("chatPinnedMessages".prefix, %* [chatId, cursorVal, limit])
except RpcException as e:
success = false
result = e.msg
proc setPinMessage*(messageId: string, chatId: string, pinned: bool) =
discard callPrivateRPC("sendPinMessage".prefix, %*[{
"message_id": messageId,
"pinned": pinned,
"chat_id": chatId
}])
proc rpcActivityCenterNotifications*(cursorVal: JsonNode, limit: int, success: var bool): string =
success = true
try:
result = callPrivateRPC("activityCenterNotifications".prefix, %* [cursorVal, limit])
except RpcException as e:
success = false
result = e.msg
proc activityCenterNotification*(cursor: string = ""): (string, seq[ActivityCenterNotification]) =
var cursorVal: JsonNode
if cursor == "":
cursorVal = newJNull()
else:
cursorVal = newJString(cursor)
var success: bool
let callResult = rpcActivityCenterNotifications(cursorVal, 20, success)
if success:
result = parseActivityCenterNotifications(callResult.parseJson()["result"])
proc markAllActivityCenterNotificationsRead*() =
discard callPrivateRPC("markAllActivityCenterNotificationsRead".prefix, %*[])
proc markActivityCenterNotificationsRead*(ids: seq[string]) =
discard callPrivateRPC("markActivityCenterNotificationsRead".prefix, %*[ids])
proc acceptActivityCenterNotifications*(ids: seq[string]): string =
result = callPrivateRPC("acceptActivityCenterNotifications".prefix, %*[ids])
proc dismissActivityCenterNotifications*(ids: seq[string]): string =
result = callPrivateRPC("dismissActivityCenterNotifications".prefix, %*[ids])
proc unreadActivityCenterNotificationsCount*(): int =
let rpcResult = callPrivateRPC("unreadActivityCenterNotificationsCount".prefix, %*[]).parseJson
if rpcResult{"result"}.kind != JNull:
return rpcResult["result"].getInt
proc asyncSearchMessages*(chatId: string, searchTerm: string, caseSensitive: bool, success: var bool): string =
success = true
try:
result = callPrivateRPC("allMessagesFromChatWhichMatchTerm".prefix, %* [chatId, searchTerm, caseSensitive])
except RpcException as e:
success = false
result = e.msg
proc asyncSearchMessages*(communityIds: seq[string], chatIds: seq[string], searchTerm: string, caseSensitive: bool, success: var bool): string =
success = true
try:
result = callPrivateRPC("allMessagesFromChatsAndCommunitiesWhichMatchTerm".prefix, %* [communityIds, chatIds, searchTerm, caseSensitive])
except RpcException as e:
success = false
result = e.msg

View File

@ -0,0 +1,20 @@
import json, chronicles
import core, ../utils
proc acceptRequestAddressForTransaction*(messageId: string, address: string): string =
result = callPrivateRPC("acceptRequestAddressForTransaction".prefix, %* [messageId, address])
proc declineRequestAddressForTransaction*(messageId: string): string =
result = callPrivateRPC("declineRequestAddressForTransaction".prefix, %* [messageId])
proc declineRequestTransaction*(messageId: string): string =
result = callPrivateRPC("declineRequestTransaction".prefix, %* [messageId])
proc requestAddressForTransaction*(chatId: string, fromAddress: string, amount: string, tokenAddress: string): string =
result = callPrivateRPC("requestAddressForTransaction".prefix, %* [chatId, fromAddress, amount, tokenAddress])
proc requestTransaction*(chatId: string, fromAddress: string, amount: string, tokenAddress: string): string =
result = callPrivateRPC("requestTransaction".prefix, %* [chatId, amount, tokenAddress, fromAddress])
proc acceptRequestTransaction*(transactionHash: string, messageId: string, signature: string): string =
result = callPrivateRPC("acceptRequestTransaction".prefix, %* [transactionHash, messageId, signature])

View File

@ -0,0 +1,77 @@
import macros
import web3/[encoding, ethtypes], stint
type
GetPackData* = object
packId*: Stuint[256]
PackData* = object
category*: DynamicBytes[32] # bytes4[]
owner*: Address # address
mintable*: bool # bool
timestamp*: Stuint[256] # uint256
price*: Stuint[256] # uint256
contentHash*: DynamicBytes[64] # bytes
BuyToken* = object
packId*: Stuint[256]
address*: Address
price*: Stuint[256]
Register* = object
label*: FixedBytes[32]
account*: Address
x*: FixedBytes[32]
y*: FixedBytes[32]
SetPubkey* = object
label*: FixedBytes[32]
x*: FixedBytes[32]
y*: FixedBytes[32]
ExpirationTime* = object
label*: FixedBytes[32]
Release* = object
label*: FixedBytes[32]
ApproveAndCall*[N: static[int]] = object
to*: Address
value*: Stuint[256]
data*: DynamicBytes[N]
Transfer* = object
to*: Address
value*: Stuint[256]
BalanceOf* = object
address*: Address
TokenOfOwnerByIndex* = object
address*: Address
index*: Stuint[256]
TokenPackId* = object
tokenId*: Stuint[256]
TokenUri* = object
tokenId*: Stuint[256]
# TODO: Figure out a way to parse a bool as a Bool instead of bool, as it is
# done in nim-web3
func decode*(input: string, offset: int, to: var bool): int {.inline.} =
let val = input[offset..offset+63].parse(Int256)
to = val.truncate(int) == 1
64
# TODO: This is taken directly from nim-web3 in order to be able to decode
# booleans. I could not get the type Bool, as used in nim-web3, to be decoded
# properly, and instead resorted to a standard bool type.
func decodeHere*(input: string, offset: int, obj: var object): int =
var offset = offset
for field in fields(obj):
offset += decode(input, offset, field)
func decodeContractResponse*[T](input: string): T =
result = T()
discard decodeHere(input.strip0xPrefix, 0, result)

View File

@ -0,0 +1,45 @@
import json, strmisc, atomics
import core, ../utils
var
contacts {.threadvar.}: JsonNode
contactsInited {.threadvar.}: bool
dirty: Atomic[bool]
proc getContactByID*(id: string): string =
result = callPrivateRPC("getContactByID".prefix, %* [id])
dirty.store(true)
proc getContacts*(): JsonNode =
let cacheIsDirty = (not contactsInited) or dirty.load
if not cacheIsDirty:
result = contacts
else:
let payload = %* []
let response = callPrivateRPC("contacts".prefix, payload).parseJson
if response["result"].kind == JNull:
result = %* []
else:
result = response["result"]
dirty.store(false)
contacts = result
contactsInited = true
proc saveContact*(id: string, ensVerified: bool, ensName: string, alias: string, identicon: string, thumbnail: string, systemTags: seq[string], localNickname: string): string =
let payload = %* [{
"id": id,
"name": ensName,
"ensVerified": ensVerified,
"alias": alias,
"identicon": identicon,
"images": {"thumbnail": {"Payload": thumbnail.partition(",")[2]}},
"systemTags": systemTags,
"localNickname": localNickname
}]
# TODO: StatusGoError handling
result = callPrivateRPC("saveContact".prefix, payload)
dirty.store(true)
proc requestContactUpdate*(publicKey: string): string =
result = callPrivateRPC("sendContactUpdate".prefix, %* [publicKey, "", ""])
dirty.store(true)

View File

@ -0,0 +1,29 @@
import
json, options, strutils
import
web3/[conversions, ethtypes], stint
# TODO: make this public in nim-web3 lib
template stripLeadingZeros*(value: string): string =
var cidx = 0
# ignore the last character so we retain '0' on zero value
while cidx < value.len - 1 and value[cidx] == '0':
cidx.inc
value[cidx .. ^1]
# TODO: update this in nim-web3
proc `%`*(x: EthSend): JsonNode =
result = newJobject()
result["from"] = %x.source
if x.to.isSome:
result["to"] = %x.to.unsafeGet
if x.gas.isSome:
result["gas"] = %x.gas.unsafeGet
if x.gasPrice.isSome:
result["gasPrice"] = %("0x" & x.gasPrice.unsafeGet.toHex.stripLeadingZeros)
if x.value.isSome:
result["value"] = %("0x" & x.value.unsafeGet.toHex)
result["data"] = %x.data
if x.nonce.isSome:
result["nonce"] = %x.nonce.unsafeGet

59
status/libstatus/core.nim Normal file
View File

@ -0,0 +1,59 @@
import json, nimcrypto, chronicles
import status_go, ../utils
logScope:
topics = "rpc"
proc callRPC*(inputJSON: string): string =
return $status_go.callRPC(inputJSON)
proc callPrivateRPCRaw*(inputJSON: string): string =
return $status_go.callPrivateRPC(inputJSON)
proc callPrivateRPC*(methodName: string, payload = %* []): string =
try:
let inputJSON = %* {
"jsonrpc": "2.0",
"method": methodName,
"params": %payload
}
debug "callPrivateRPC", rpc_method=methodName
let response = status_go.callPrivateRPC($inputJSON)
result = $response
if parseJSON(result).hasKey("error"):
writeStackTrace()
error "rpc response error", result, payload, methodName
except Exception as e:
error "error doing rpc request", methodName = methodName, exception=e.msg
proc sendTransaction*(inputJSON: string, password: string): string =
var hashed_password = "0x" & $keccak_256.digest(password)
return $status_go.sendTransaction(inputJSON, hashed_password)
proc startMessenger*() =
discard callPrivateRPC("startMessenger".prefix)
proc addPeer*(peer: string) =
discard callPrivateRPC("admin_addPeer", %* [peer])
proc removePeer*(peer: string) =
discard callPrivateRPC("admin_removePeer", %* [peer])
proc markTrustedPeer*(peer: string) =
discard callPrivateRPC("markTrustedPeer".prefix(false), %* [peer])
proc getBlockByNumber*(blockNumber: string): string =
result = callPrivateRPC("eth_getBlockByNumber", %* [blockNumber, false])
proc getTransfersByAddress*(address: string, toBlock: string, limit: string, fetchMore: bool = false): string =
let toBlockParsed = if not fetchMore: newJNull() else: %toBlock
result = callPrivateRPC("wallet_getTransfersByAddress", %* [address, toBlockParsed, limit, fetchMore])
proc signMessage*(rpcParams: string): string =
return $status_go.signMessage(rpcParams)
proc signTypedData*(data: string, address: string, password: string): string =
return $status_go.signTypedData(data, address, password)
proc getBloomFilter*(): string =
return $callPrivateRPC("bloomFilter".prefix, %* []).parseJSON()["result"].getStr

View File

@ -0,0 +1,83 @@
import typetraits
import edn, chronicles
import ../types/[sticker] # FIXME: there should be no type deps
# forward declaration:
proc parseNode[T](node: EdnNode, searchName: string): T
proc parseMap[T](map: HMap, searchName: string,): T
proc getValueFromNode[T](node: EdnNode): T =
if node.kind == EdnSymbol:
when T is string:
result = node.symbol.name
elif node.kind == EdnKeyword:
when T is string:
result = node.keyword.name
elif node.kind == EdnString:
when T is string:
result = node.str
elif node.kind == EdnCharacter:
when T is string:
result = node.character
elif node.kind == EdnBool:
when T is bool:
result = node.boolVal
elif node.kind == EdnInt:
when T is int:
try:
result = cast[int](node.num)
except:
warn "Returned 0 value for node, when value should have been ", val = $node.num
result = 0
else:
raise newException(ValueError, "couldn't get '" & T.type.name & "'value from node: " & repr(node))
proc parseVector[T: seq[Sticker]](node: EdnNode, searchName: string): seq[Sticker] =
# TODO: make this generic to accept any seq[T]. Issue is that instantiating result
# like `result = T()` is not allowed when T is `seq[Sticker]`
# because seq[Sticker] isn't an object, whereas it works when T is
# an object type (like Sticker). IOW, Sticker is an object type, but seq[Sticker]
# is not
result = newSeq[Sticker]()
for i in 0..<node.vec.len:
var sticker: Sticker = Sticker()
let child = node.vec[i]
if child.kind == EdnMap:
for k, v in sticker.fieldPairs:
v = parseMap[v.type](child.map, k)
result.add(sticker)
proc parseMap[T](map: HMap, searchName: string): T =
for iBucket in 0..<map.buckets.len:
let bucket = map.buckets[iBucket]
if bucket.len > 0:
for iChild in 0..<bucket.len:
let child = bucket[iChild]
let isRoot = child.key.kind == EdnSymbol and child.key.symbol.name == "meta"
if child.key.kind != EdnKeyword and not isRoot:
continue
if isRoot or child.key.keyword.name == searchName:
if child.value.kind == EdnMap:
result = parseMap[T](child.value.map, searchName)
break
elif child.value.kind == EdnVector:
when T is seq[Sticker]:
result = parseVector[T](child.value, searchName)
break
result = getValueFromNode[T](child.value)
break
proc parseNode[T](node: EdnNode, searchName: string): T =
if node.kind == EdnMap:
result = parseMap[T](node.map, searchName)
else:
result = getValueFromNode[T](node)
proc decode*[T](node: EdnNode): T =
result = T()
for k, v in result.fieldPairs:
v = parseNode[v.type](node, k)
proc decode*[T](edn: string): T =
decode[T](read(edn))

View File

@ -0,0 +1,319 @@
import
sequtils, sugar, macros, tables, strutils
import
web3/ethtypes, stew/byteutils, nimcrypto, json_serialization, chronicles
import
../../types/[network], ../settings, ../coder, transactions, methods, ../../utils
export
GetPackData, PackData, BuyToken, ApproveAndCall, Transfer, BalanceOf, Register, SetPubkey,
TokenOfOwnerByIndex, TokenPackId, TokenUri, FixedBytes, DynamicBytes, toHex, fromHex,
decodeContractResponse, encodeAbi, estimateGas, send, call, ExpirationTime, Release
logScope:
topics = "contracts"
const ERC20_METHODS = @[
("name", Method(signature: "name()")),
("symbol", Method(signature: "symbol()")),
("decimals", Method(signature: "decimals()")),
("totalSupply", Method(signature: "totalSupply()")),
("balanceOf", Method(signature: "balanceOf(address)")),
("transfer", Method(signature: "transfer(address,uint256)")),
("allowance", Method(signature: "allowance(address,address)")),
("approve", Method(signature: "approve(address,uint256)")),
("transferFrom", Method(signature: "approve(address,address,uint256)")),
("increaseAllowance", Method(signature: "increaseAllowance(address,uint256)")),
("decreaseAllowance", Method(signature: "decreaseAllowance(address,uint256)")),
("approveAndCall", Method(signature: "approveAndCall(address,uint256,bytes)"))
]
const ERC721_ENUMERABLE_METHODS = @[
("balanceOf", Method(signature: "balanceOf(address)")),
("ownerOf", Method(signature: "ownerOf(uint256)")),
("name", Method(signature: "name()")),
("symbol", Method(signature: "symbol()")),
("tokenURI", Method(signature: "tokenURI(uint256)")),
("baseURI", Method(signature: "baseURI()")),
("tokenOfOwnerByIndex", Method(signature: "tokenOfOwnerByIndex(address,uint256)")),
("totalSupply", Method(signature: "totalSupply()")),
("tokenByIndex", Method(signature: "tokenByIndex(uint256)")),
("approve", Method(signature: "approve(address,uint256)")),
("getApproved", Method(signature: "getApproved(uint256)")),
("setApprovalForAll", Method(signature: "setApprovalForAll(address,bool)")),
("isApprovedForAll", Method(signature: "isApprovedForAll(address,address)")),
("transferFrom", Method(signature: "transferFrom(address,address,uint256)")),
("safeTransferFrom", Method(signature: "safeTransferFrom(address,address,uint256)")),
("safeTransferFromWithData", Method(signature: "safeTransferFrom(address,address,uint256,bytes)"))
]
type
Contract* = ref object of RootObj
name*: string
network*: Network
address*: Address
methods* {.dontSerialize.}: Table[string, Method]
Erc20Contract* = ref object of Contract
symbol*: string
decimals*: int
hasIcon* {.dontSerialize.}: bool
color*: string
Erc721Contract* = ref object of Contract
symbol*: string
hasIcon*: bool
proc newErc20Contract*(name: string, network: Network, address: Address, symbol: string, decimals: int, hasIcon: bool): Erc20Contract =
Erc20Contract(name: name, network: network, address: address, methods: ERC20_METHODS.toTable, symbol: symbol, decimals: decimals, hasIcon: hasIcon)
proc newErc20Contract*(network: Network, address: Address): Erc20Contract =
Erc20Contract(name: "", network: network, address: address, methods: ERC20_METHODS.toTable, symbol: "", decimals: 0, hasIcon: false)
proc newErc721Contract(name: string, network: Network, address: Address, symbol: string, hasIcon: bool, addlMethods: seq[tuple[name: string, meth: Method]] = @[]): Erc721Contract =
Erc721Contract(name: name, network: network, address: address, symbol: symbol, hasIcon: hasIcon, methods: ERC721_ENUMERABLE_METHODS.concat(addlMethods).toTable)
var
contracts {.threadvar.}: seq[Contract]
contractsInited {.threadvar.}: bool
proc allContracts(): seq[Contract] =
if contractsInited:
result = contracts
else:
contracts = @[
# Mainnet contracts
newErc20Contract("Status Network Token", Network.Mainnet, parseAddress("0x744d70fdbe2ba4cf95131626614a1763df805b9e"), "SNT", 18, true),
newErc20Contract("Dai Stablecoin", Network.Mainnet, parseAddress("0x6b175474e89094c44da98b954eedeac495271d0f"), "DAI", 18, true),
newErc20Contract("Sai Stablecoin v1.0", Network.Mainnet, parseAddress("0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359"), "SAI", 18, true),
newErc20Contract("MKR", Network.Mainnet, parseAddress("0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2"), "MKR", 18, true),
newErc20Contract("EOS", Network.Mainnet, parseAddress("0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0"), "EOS", 18, true),
newErc20Contract("OMGToken", Network.Mainnet, parseAddress("0xd26114cd6ee289accf82350c8d8487fedb8a0c07"), "OMG", 18, true),
newErc20Contract("Populous Platform", Network.Mainnet, parseAddress("0xd4fa1460f537bb9085d22c7bccb5dd450ef28e3a"), "PPT", 8, true),
newErc20Contract("Reputation", Network.Mainnet, parseAddress("0x1985365e9f78359a9b6ad760e32412f4a445e862"), "REP", 18, true),
newErc20Contract("PowerLedger", Network.Mainnet, parseAddress("0x595832f8fc6bf59c85c527fec3740a1b7a361269"), "POWR", 6, true),
newErc20Contract("TenX Pay Token", Network.Mainnet, parseAddress("0xb97048628db6b661d4c2aa833e95dbe1a905b280"), "PAY", 18, true),
newErc20Contract("Veros", Network.Mainnet, parseAddress("0x92e78dae1315067a8819efd6dca432de9dcde2e9"), "VRS", 6, false),
newErc20Contract("Golem Network Token", Network.Mainnet, parseAddress("0xa74476443119a942de498590fe1f2454d7d4ac0d"), "GNT", 18, true),
newErc20Contract("Salt", Network.Mainnet, parseAddress("0x4156d3342d5c385a87d264f90653733592000581"), "SALT", 8, true),
newErc20Contract("BNB", Network.Mainnet, parseAddress("0xb8c77482e45f1f44de1745f52c74426c631bdd52"), "BNB", 18, true),
newErc20Contract("Basic Attention Token", Network.Mainnet, parseAddress("0x0d8775f648430679a709e98d2b0cb6250d2887ef"), "BAT", 18, true),
newErc20Contract("Kyber Network Crystal", Network.Mainnet, parseAddress("0xdd974d5c2e2928dea5f71b9825b8b646686bd200"), "KNC", 18, true),
newErc20Contract("BTU Protocol", Network.Mainnet, parseAddress("0xb683D83a532e2Cb7DFa5275eED3698436371cc9f"), "BTU", 18, true),
newErc20Contract("Digix DAO", Network.Mainnet, parseAddress("0xe0b7927c4af23765cb51314a0e0521a9645f0e2a"), "DGD", 9, true),
newErc20Contract("Aeternity", Network.Mainnet, parseAddress("0x5ca9a71b1d01849c0a95490cc00559717fcf0d1d"), "AE", 18, true),
newErc20Contract("Tronix", Network.Mainnet, parseAddress("0xf230b790e05390fc8295f4d3f60332c93bed42e2"), "TRX", 6, true),
newErc20Contract("Ethos", Network.Mainnet, parseAddress("0x5af2be193a6abca9c8817001f45744777db30756"), "ETHOS", 8, true),
newErc20Contract("Raiden Token", Network.Mainnet, parseAddress("0x255aa6df07540cb5d3d297f0d0d4d84cb52bc8e6"), "RDN", 18, true),
newErc20Contract("SingularDTV", Network.Mainnet, parseAddress("0xaec2e87e0a235266d9c5adc9deb4b2e29b54d009"), "SNGLS", 0, true),
newErc20Contract("Gnosis Token", Network.Mainnet, parseAddress("0x6810e776880c02933d47db1b9fc05908e5386b96"), "GNO", 18, true),
newErc20Contract("StorjToken", Network.Mainnet, parseAddress("0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac"), "STORJ", 8, true),
newErc20Contract("AdEx", Network.Mainnet, parseAddress("0x4470bb87d77b963a013db939be332f927f2b992e"), "ADX", 4, false),
newErc20Contract("FunFair", Network.Mainnet, parseAddress("0x419d0d8bdd9af5e606ae2232ed285aff190e711b"), "FUN", 8, true),
newErc20Contract("Civic", Network.Mainnet, parseAddress("0x41e5560054824ea6b0732e656e3ad64e20e94e45"), "CVC", 8, true),
newErc20Contract("ICONOMI", Network.Mainnet, parseAddress("0x888666ca69e0f178ded6d75b5726cee99a87d698"), "ICN", 18, true),
newErc20Contract("Walton Token", Network.Mainnet, parseAddress("0xb7cb1c96db6b22b0d3d9536e0108d062bd488f74"), "WTC", 18, true),
newErc20Contract("Bytom", Network.Mainnet, parseAddress("0xcb97e65f07da24d46bcdd078ebebd7c6e6e3d750"), "BTM", 8, true),
newErc20Contract("0x Protocol Token", Network.Mainnet, parseAddress("0xe41d2489571d322189246dafa5ebde1f4699f498"), "ZRX", 18, true),
newErc20Contract("Bancor Network Token", Network.Mainnet, parseAddress("0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c"), "BNT", 18, true),
newErc20Contract("Metal", Network.Mainnet, parseAddress("0xf433089366899d83a9f26a773d59ec7ecf30355e"), "MTL", 8, false),
newErc20Contract("PayPie", Network.Mainnet, parseAddress("0xc42209accc14029c1012fb5680d95fbd6036e2a0"), "PPP", 18, true),
newErc20Contract("ChainLink Token", Network.Mainnet, parseAddress("0x514910771af9ca656af840dff83e8264ecf986ca"), "LINK", 18, true),
newErc20Contract("Kin", Network.Mainnet, parseAddress("0x818fc6c2ec5986bc6e2cbf00939d90556ab12ce5"), "KIN", 18, true),
newErc20Contract("Aragon Network Token", Network.Mainnet, parseAddress("0x960b236a07cf122663c4303350609a66a7b288c0"), "ANT", 18, true),
newErc20Contract("MobileGo Token", Network.Mainnet, parseAddress("0x40395044ac3c0c57051906da938b54bd6557f212"), "MGO", 8, true),
newErc20Contract("Monaco", Network.Mainnet, parseAddress("0xb63b606ac810a52cca15e44bb630fd42d8d1d83d"), "MCO", 8, true),
newErc20Contract("loopring", Network.Mainnet, parseAddress("0xef68e7c694f40c8202821edf525de3782458639f"), "LRC", 18, true),
newErc20Contract("Zeus Shield Coin", Network.Mainnet, parseAddress("0x7a41e0517a5eca4fdbc7fbeba4d4c47b9ff6dc63"), "ZSC", 18, true),
newErc20Contract("Streamr DATAcoin", Network.Mainnet, parseAddress("0x0cf0ee63788a0849fe5297f3407f701e122cc023"), "DATA", 18, true),
newErc20Contract("Ripio Credit Network Token", Network.Mainnet, parseAddress("0xf970b8e36e23f7fc3fd752eea86f8be8d83375a6"), "RCN", 18, true),
newErc20Contract("WINGS", Network.Mainnet, parseAddress("0x667088b212ce3d06a1b553a7221e1fd19000d9af"), "WINGS", 18, true),
newErc20Contract("Edgeless", Network.Mainnet, parseAddress("0x08711d3b02c8758f2fb3ab4e80228418a7f8e39c"), "EDG", 0, true),
newErc20Contract("Melon Token", Network.Mainnet, parseAddress("0xbeb9ef514a379b997e0798fdcc901ee474b6d9a1"), "MLN", 18, true),
newErc20Contract("Moeda Loyalty Points", Network.Mainnet, parseAddress("0x51db5ad35c671a87207d88fc11d593ac0c8415bd"), "MDA", 18, true),
newErc20Contract("PILLAR", Network.Mainnet, parseAddress("0xe3818504c1b32bf1557b16c238b2e01fd3149c17"), "PLR", 18, true),
newErc20Contract("QRL", Network.Mainnet, parseAddress("0x697beac28b09e122c4332d163985e8a73121b97f"), "QRL", 8, true),
newErc20Contract("Modum Token", Network.Mainnet, parseAddress("0x957c30ab0426e0c93cd8241e2c60392d08c6ac8e"), "MOD", 0, true),
newErc20Contract("Token-as-a-Service", Network.Mainnet, parseAddress("0xe7775a6e9bcf904eb39da2b68c5efb4f9360e08c"), "TAAS", 6, true),
newErc20Contract("GRID Token", Network.Mainnet, parseAddress("0x12b19d3e2ccc14da04fae33e63652ce469b3f2fd"), "GRID", 12, true),
newErc20Contract("SANtiment network token", Network.Mainnet, parseAddress("0x7c5a0ce9267ed19b22f8cae653f198e3e8daf098"), "SAN", 18, true),
newErc20Contract("SONM Token", Network.Mainnet, parseAddress("0x983f6d60db79ea8ca4eb9968c6aff8cfa04b3c63"), "SNM", 18, true),
newErc20Contract("Request Token", Network.Mainnet, parseAddress("0x8f8221afbb33998d8584a2b05749ba73c37a938a"), "REQ", 18, true),
newErc20Contract("Substratum", Network.Mainnet, parseAddress("0x12480e24eb5bec1a9d4369cab6a80cad3c0a377a"), "SUB", 2, true),
newErc20Contract("Decentraland MANA", Network.Mainnet, parseAddress("0x0f5d2fb29fb7d3cfee444a200298f468908cc942"), "MANA", 18, true),
newErc20Contract("AirSwap Token", Network.Mainnet, parseAddress("0x27054b13b1b798b345b591a4d22e6562d47ea75a"), "AST", 4, true),
newErc20Contract("R token", Network.Mainnet, parseAddress("0x48f775efbe4f5ece6e0df2f7b5932df56823b990"), "R", 0, true),
newErc20Contract("FirstBlood Token", Network.Mainnet, parseAddress("0xaf30d2a7e90d7dc361c8c4585e9bb7d2f6f15bc7"), "1ST", 18, true),
newErc20Contract("Cofoundit", Network.Mainnet, parseAddress("0x12fef5e57bf45873cd9b62e9dbd7bfb99e32d73e"), "CFI", 18, true),
newErc20Contract("Enigma", Network.Mainnet, parseAddress("0xf0ee6b27b759c9893ce4f094b49ad28fd15a23e4"), "ENG", 8, true),
newErc20Contract("Amber Token", Network.Mainnet, parseAddress("0x4dc3643dbc642b72c158e7f3d2ff232df61cb6ce"), "AMB", 18, true),
newErc20Contract("XPlay Token", Network.Mainnet, parseAddress("0x90528aeb3a2b736b780fd1b6c478bb7e1d643170"), "XPA", 18, true),
newErc20Contract("Open Trading Network", Network.Mainnet, parseAddress("0x881ef48211982d01e2cb7092c915e647cd40d85c"), "OTN", 18, true),
newErc20Contract("Trustcoin", Network.Mainnet, parseAddress("0xcb94be6f13a1182e4a4b6140cb7bf2025d28e41b"), "TRST", 6, true),
newErc20Contract("Monolith TKN", Network.Mainnet, parseAddress("0xaaaf91d9b90df800df4f55c205fd6989c977e73a"), "TKN", 8, true),
newErc20Contract("RHOC", Network.Mainnet, parseAddress("0x168296bb09e24a88805cb9c33356536b980d3fc5"), "RHOC", 8, true),
newErc20Contract("Target Coin", Network.Mainnet, parseAddress("0xac3da587eac229c9896d919abc235ca4fd7f72c1"), "TGT", 1, false),
newErc20Contract("Everex", Network.Mainnet, parseAddress("0xf3db5fa2c66b7af3eb0c0b782510816cbe4813b8"), "EVX", 4, true),
newErc20Contract("ICOS", Network.Mainnet, parseAddress("0x014b50466590340d41307cc54dcee990c8d58aa8"), "ICOS", 6, true),
newErc20Contract("district0x Network Token", Network.Mainnet, parseAddress("0x0abdace70d3790235af448c88547603b945604ea"), "DNT", 18, true),
newErc20Contract("Dentacoin", Network.Mainnet, parseAddress("0x08d32b0da63e2c3bcf8019c9c5d849d7a9d791e6"), "٨", 0, false),
newErc20Contract("Eidoo Token", Network.Mainnet, parseAddress("0xced4e93198734ddaff8492d525bd258d49eb388e"), "EDO", 18, true),
newErc20Contract("BitDice", Network.Mainnet, parseAddress("0x29d75277ac7f0335b2165d0895e8725cbf658d73"), "CSNO", 8, false),
newErc20Contract("Cobinhood Token", Network.Mainnet, parseAddress("0xb2f7eb1f2c37645be61d73953035360e768d81e6"), "COB", 18, true),
newErc20Contract("Enjin Coin", Network.Mainnet, parseAddress("0xf629cbd94d3791c9250152bd8dfbdf380e2a3b9c"), "ENJ", 18, false),
newErc20Contract("AVENTUS", Network.Mainnet, parseAddress("0x0d88ed6e74bbfd96b831231638b66c05571e824f"), "AVT", 18, false),
newErc20Contract("Chronobank TIME", Network.Mainnet, parseAddress("0x6531f133e6deebe7f2dce5a0441aa7ef330b4e53"), "TIME", 8, false),
newErc20Contract("Cindicator Token", Network.Mainnet, parseAddress("0xd4c435f5b09f855c3317c8524cb1f586e42795fa"), "CND", 18, true),
newErc20Contract("Stox", Network.Mainnet, parseAddress("0x006bea43baa3f7a6f765f14f10a1a1b08334ef45"), "STX", 18, true),
newErc20Contract("Xaurum", Network.Mainnet, parseAddress("0x4df812f6064def1e5e029f1ca858777cc98d2d81"), "XAUR", 8, true),
newErc20Contract("Vibe", Network.Mainnet, parseAddress("0x2c974b2d0ba1716e644c1fc59982a89ddd2ff724"), "VIB", 18, true),
newErc20Contract("PRG", Network.Mainnet, parseAddress("0x7728dfef5abd468669eb7f9b48a7f70a501ed29d"), "PRG", 6, false),
newErc20Contract("Delphy Token", Network.Mainnet, parseAddress("0x6c2adc2073994fb2ccc5032cc2906fa221e9b391"), "DPY", 18, true),
newErc20Contract("CoinDash Token", Network.Mainnet, parseAddress("0x2fe6ab85ebbf7776fee46d191ee4cea322cecf51"), "CDT", 18, true),
newErc20Contract("Tierion Network Token", Network.Mainnet, parseAddress("0x08f5a9235b08173b7569f83645d2c7fb55e8ccd8"), "TNT", 8, true),
newErc20Contract("DomRaiderToken", Network.Mainnet, parseAddress("0x9af4f26941677c706cfecf6d3379ff01bb85d5ab"), "DRT", 8, true),
newErc20Contract("SPANK", Network.Mainnet, parseAddress("0x42d6622dece394b54999fbd73d108123806f6a18"), "SPANK", 18, true),
newErc20Contract("Berlin Coin", Network.Mainnet, parseAddress("0x80046305aaab08f6033b56a360c184391165dc2d"), "BRLN", 18, true),
newErc20Contract("USD//C", Network.Mainnet, parseAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), "USDC", 6, true),
newErc20Contract("Livepeer Token", Network.Mainnet, parseAddress("0x58b6a8a3302369daec383334672404ee733ab239"), "LPT", 18, true),
newErc20Contract("Simple Token", Network.Mainnet, parseAddress("0x2c4e8f2d746113d0696ce89b35f0d8bf88e0aeca"), "ST", 18, true),
newErc20Contract("Wrapped BTC", Network.Mainnet, parseAddress("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599"), "WBTC", 8, true),
newErc20Contract("Bloom Token", Network.Mainnet, parseAddress("0x107c4504cd79c5d2696ea0030a8dd4e92601b82e"), "BLT", 18, true),
newErc20Contract("Unisocks", Network.Mainnet, parseAddress("0x23b608675a2b2fb1890d3abbd85c5775c51691d5"), "SOCKS", 18, true),
newErc20Contract("Hermez Network Token", Network.Mainnet, parseAddress("0xEEF9f339514298C6A857EfCfC1A762aF84438dEE"), "HEZ", 18, true),
Contract(name: "stickers", network: Network.Mainnet, address: parseAddress("0x0577215622f43a39f4bc9640806dfea9b10d2a36"),
methods: [
("packCount", Method(signature: "packCount()")),
("getPackData", Method(signature: "getPackData(uint256)"))
].toTable
),
Contract(name: "sticker-market", network: Network.Mainnet, address: parseAddress("0x12824271339304d3a9f7e096e62a2a7e73b4a7e7"),
methods: [
("buyToken", Method(signature: "buyToken(uint256,address,uint256)"))
].toTable
),
newErc721Contract("sticker-pack", Network.Mainnet, parseAddress("0x110101156e8F0743948B2A61aFcf3994A8Fb172e"), "PACK", false, @[("tokenPackId", Method(signature: "tokenPackId(uint256)"))]),
# Strikers seems dead. Their website doesn't work anymore
newErc721Contract("strikers", Network.Mainnet, parseAddress("0xdcaad9fd9a74144d226dbf94ce6162ca9f09ed7e"), "STRK", true),
newErc721Contract("ethermon", Network.Mainnet, parseAddress("0xb2c0782ae4a299f7358758b2d15da9bf29e1dd99"), "EMONA", true),
newErc721Contract("kudos", Network.Mainnet, parseAddress("0x2aea4add166ebf38b63d09a75de1a7b94aa24163"), "KDO", true),
newErc721Contract("crypto-kitties", Network.Mainnet, parseAddress("0x06012c8cf97bead5deae237070f9587f8e7a266d"), "CK", true),
Contract(name: "ens-usernames", network: Network.Mainnet, address: parseAddress("0xDB5ac1a559b02E12F29fC0eC0e37Be8E046DEF49"),
methods: [
("register", Method(signature: "register(bytes32,address,bytes32,bytes32)")),
("getPrice", Method(signature: "getPrice()")),
("getExpirationTime", Method(signature: "getExpirationTime(bytes32)")),
("release", Method(signature: "release(bytes32)"))
].toTable
),
Contract(name: "ens-resolver", network: Network.Mainnet, address: parseAddress("0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41"),
methods: [
("setPubkey", Method(signature: "setPubkey(bytes32,bytes32,bytes32)"))
].toTable
),
# Testnet (Ropsten) contracts
newErc20Contract("Status Test Token", Network.Testnet, parseAddress("0xc55cf4b03948d7ebc8b9e8bad92643703811d162"), "STT", 18, true),
newErc20Contract("Handy Test Token", Network.Testnet, parseAddress("0xdee43a267e8726efd60c2e7d5b81552dcd4fa35c"), "HND", 0, false),
newErc20Contract("Lucky Test Token", Network.Testnet, parseAddress("0x703d7dc0bc8e314d65436adf985dda51e09ad43b"), "LXS", 2, false),
newErc20Contract("Adi Test Token", Network.Testnet, parseAddress("0xe639e24346d646e927f323558e6e0031bfc93581"), "ADI", 7, false),
newErc20Contract("Wagner Test Token", Network.Testnet, parseAddress("0x2e7cd05f437eb256f363417fd8f920e2efa77540"), "WGN", 10, false),
newErc20Contract("Modest Test Token", Network.Testnet, parseAddress("0x57cc9b83730e6d22b224e9dc3e370967b44a2de0"), "MDS", 18, false),
Contract(name: "tribute-to-talk", network: Network.Testnet, address: parseAddress("0xC61aa0287247a0398589a66fCD6146EC0F295432")),
Contract(name: "stickers", network: Network.Testnet, address: parseAddress("0x8cc272396be7583c65bee82cd7b743c69a87287d"),
methods: [
("packCount", Method(signature: "packCount()")),
("getPackData", Method(signature: "getPackData(uint256)"))
].toTable
),
Contract(name: "sticker-market", network: Network.Testnet, address: parseAddress("0x6CC7274aF9cE9572d22DFD8545Fb8c9C9Bcb48AD"),
methods: [
("buyToken", Method(signature: "buyToken(uint256,address,uint256)"))
].toTable
),
newErc721Contract("sticker-pack", Network.Testnet, parseAddress("0xf852198d0385c4b871e0b91804ecd47c6ba97351"), "PACK", false, @[("tokenPackId", Method(signature: "tokenPackId(uint256)"))]),
newErc721Contract("kudos", Network.Testnet, parseAddress("0xcd520707fc68d153283d518b29ada466f9091ea8"), "KDO", true),
Contract(name: "ens-usernames", network: Network.Testnet, address: parseAddress("0xdaae165beb8c06e0b7613168138ebba774aff071"),
methods: [
("register", Method(signature: "register(bytes32,address,bytes32,bytes32)")),
("getPrice", Method(signature: "getPrice()")),
("getExpirationTime", Method(signature: "getExpirationTime(bytes32)")),
("release", Method(signature: "release(bytes32)"))
].toTable
),
Contract(name: "ens-resolver", network: Network.Testnet, address: parseAddress("0x42D63ae25990889E35F215bC95884039Ba354115"),
methods: [
("setPubkey", Method(signature: "setPubkey(bytes32,bytes32,bytes32)"))
].toTable
),
# Rinkeby contracts
newErc20Contract("Moksha Coin", Network.Rinkeby, parseAddress("0x6ba7dc8dd10880ab83041e60c4ede52bb607864b"), "MOKSHA", 18, false),
newErc20Contract("WIBB", Network.Rinkeby, parseAddress("0x7d4ccf6af2f0fdad48ee7958bcc28bdef7b732c7"), "WIBB", 18, false),
newErc20Contract("Status Test Token", Network.Rinkeby, parseAddress("0x43d5adc3b49130a575ae6e4b00dfa4bc55c71621"), "STT", 18, false),
# xDai contracts
newErc20Contract("buffiDai", Network.XDai, parseAddress("0x3e50bf6703fc132a94e4baff068db2055655f11b"), "BUFF", 18, false),
newErc20Contract("Uniswap", Network.Mainnet, parseAddress("0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"), "UNI", 18, true),
newErc20Contract("Compound", Network.Mainnet, parseAddress("0xc00e94cb662c3520282e6f5717214004a7f26888"), "COMP", 18, true),
newErc20Contract("Balancer", Network.Mainnet, parseAddress("0xba100000625a3754423978a60c9317c58a424e3d"), "BAL", 18, true),
newErc20Contract("Akropolis", Network.Mainnet, parseAddress("0x8ab7404063ec4dbcfd4598215992dc3f8ec853d7"), "AKRO", 18, true),
newErc20Contract("Orchid", Network.Mainnet, parseAddress("0x4575f41308EC1483f3d399aa9a2826d74Da13Deb"), "OXT", 18, false),
]
contractsInited = true
result = contracts
proc getContract(network: Network, name: string): Contract =
let found = allContracts().filter(contract => contract.name == name and contract.network == network)
result = if found.len > 0: found[0] else: nil
proc getContract*(name: string): Contract =
let network = settings.getCurrentNetwork()
getContract(network, name)
proc getErc20ContractBySymbol*(contracts: seq[Erc20Contract], symbol: string): Erc20Contract =
let found = contracts.filter(contract => contract.symbol.toLower == symbol.toLower)
result = if found.len > 0: found[0] else: nil
proc getErc20ContractByAddress*(contracts: seq[Erc20Contract], address: Address): Erc20Contract =
let found = contracts.filter(contract => contract.address == address)
result = if found.len > 0: found[0] else: nil
proc getErc20Contract*(symbol: string): Erc20Contract =
let network = settings.getCurrentNetwork()
result = allContracts().filter(contract => contract.network == network and contract of Erc20Contract).map(contract => Erc20Contract(contract)).getErc20ContractBySymbol(symbol)
proc getErc20Contract*(address: Address): Erc20Contract =
let network = settings.getCurrentNetwork()
result = allContracts().filter(contract => contract.network == network and contract of Erc20Contract).map(contract => Erc20Contract(contract)).getErc20ContractByAddress(address)
proc getErc20Contracts*(): seq[Erc20Contract] =
let network = settings.getCurrentNetwork()
result = allContracts().filter(contract => contract of Erc20Contract and contract.network == network).map(contract => Erc20Contract(contract))
proc getErc721Contract(network: Network, name: string): Erc721Contract =
let found = allContracts().filter(contract => contract of Erc721Contract and Erc721Contract(contract).name.toLower == name.toLower and contract.network == network)
result = if found.len > 0: Erc721Contract(found[0]) else: nil
proc getErc721Contract*(name: string): Erc721Contract =
let network = settings.getCurrentNetwork()
getErc721Contract(network, name)
proc getErc721Contracts*(): seq[Erc721Contract] =
let network = settings.getCurrentNetwork()
result = allContracts().filter(contract => contract of Erc721Contract and contract.network == network).map(contract => Erc721Contract(contract))
proc getSntContract*(): Erc20Contract =
if settings.getCurrentNetwork() == Network.Mainnet:
result = getErc20Contract("snt")
else:
result = getErc20Contract("stt")
if result == nil:
# TODO: xDai network does not have an SNT contract listed. We will need to handle
# having no SNT contract in other places in the code (ie anywhere that
# getSntContract() is called)
raise newException(ValueError, "A status contract could not be found for the current network")

View File

@ -0,0 +1,23 @@
import
web3/ethtypes
import
transactions, ../../types/[rpc_response]
proc sendTransaction*(tx: var EthSend, password: string, success: var bool): string =
success = true
try:
let response = transactions.sendTransaction(tx, password)
result = response.result
except RpcException as e:
success = false
result = e.msg
proc estimateGas*(tx: var EthSend, success: var bool): string =
success = true
try:
let response = transactions.estimateGas(tx)
result = response.result
except RpcException as e:
success = false
result = e.msg

View File

@ -0,0 +1,64 @@
import
strutils, options
import
nimcrypto, web3/[encoding, ethtypes]
import
../../types/[rpc_response], ../coder, eth, transactions
export sendTransaction
type Method* = object
name*: string
signature*: string
proc encodeMethod(self: Method): string =
($nimcrypto.keccak256.digest(self.signature))[0..<8].toLower
proc encodeAbi*(self: Method, obj: object = RootObj()): string =
result = "0x" & self.encodeMethod()
# .fields is an iterator, and there's no way to get a count of an iterator
# in nim, so we have to loop and increment a counter
var fieldCount = 0
for i in obj.fields:
fieldCount += 1
var
offset = 32*fieldCount
data = ""
for field in obj.fields:
let encoded = encode(field)
if encoded.dynamic:
result &= offset.toHex(64).toLower
data &= encoded.data
offset += encoded.data.len
else:
result &= encoded.data
result &= data
proc estimateGas*(self: Method, tx: var EthSend, methodDescriptor: object, success: var bool): string =
success = true
tx.data = self.encodeAbi(methodDescriptor)
try:
let response = transactions.estimateGas(tx)
result = response.result # gas estimate in hex
except RpcException as e:
success = false
result = e.msg
proc send*(self: Method, tx: var EthSend, methodDescriptor: object, password: string, success: var bool): string =
tx.data = self.encodeAbi(methodDescriptor)
result = eth.sendTransaction(tx, password, success)
proc call*[T](self: Method, tx: var EthSend, methodDescriptor: object, success: var bool): T =
success = true
tx.data = self.encodeAbi(methodDescriptor)
let response: RpcResponse
try:
response = transactions.call(tx)
except RpcException as e:
success = false
result = e.msg
result = coder.decodeContractResponse[T](response.result)

View File

@ -0,0 +1,30 @@
import
json
import
json_serialization, chronicles, web3/ethtypes
import
../core, ../../types/[rpc_response], ../conversions
proc estimateGas*(tx: EthSend): RpcResponse =
let response = core.callPrivateRPC("eth_estimateGas", %*[%tx])
result = Json.decode(response, RpcResponse)
if not result.error.isNil:
raise newException(RpcException, "Error getting gas estimate: " & result.error.message)
trace "Gas estimated succesfully", estimate=result.result
proc sendTransaction*(tx: EthSend, password: string): RpcResponse =
let responseStr = core.sendTransaction($(%tx), password)
result = Json.decode(responseStr, RpcResponse)
if not result.error.isNil:
raise newException(RpcException, "Error sending transaction: " & result.error.message)
trace "Transaction sent succesfully", hash=result.result
proc call*(tx: EthSend): RpcResponse =
let responseStr = core.callPrivateRPC("eth_call", %*[%tx, "latest"])
result = Json.decode(responseStr, RpcResponse)
if not result.error.isNil:
raise newException(RpcException, "Error calling method: " & result.error.message)

16
status/libstatus/gif.nim Normal file
View File

@ -0,0 +1,16 @@
import json
import ./settings
import ../types/[setting]
proc getRecentGifs*(): JsonNode =
return settings.getSetting[JsonNode](Setting.Gifs_Recent, %*{})
proc getFavoriteGifs*(): JsonNode =
return settings.getSetting[JsonNode](Setting.Gifs_Favorite, %*{})
proc setFavoriteGifs*(items: JsonNode) =
discard settings.saveSetting(Setting.Gifs_Favorite, items)
proc setRecentGifs*(items: JsonNode) =
discard settings.saveSetting(Setting.Gifs_Recent, items)

View File

@ -0,0 +1,29 @@
import json, core, ../utils, system
var installations: JsonNode = %*{}
var dirty: bool = true
proc setInstallationMetadata*(installationId: string, deviceName: string, deviceType: string): string =
result = callPrivateRPC("setInstallationMetadata".prefix, %* [installationId, {"name": deviceName, "deviceType": deviceType}])
# TODO: handle errors
proc getOurInstallations*(useCached: bool = true): JsonNode =
if useCached and not dirty:
return installations
installations = callPrivateRPC("getOurInstallations".prefix, %* []).parseJSON()["result"]
dirty = false
result = installations
proc syncDevices*(preferredName: string): string =
# TODO change this to identicon when status-go is updated
let photoPath = ""
result = callPrivateRPC("syncDevices".prefix, %* [preferredName, photoPath])
proc sendPairInstallation*(): string =
result = callPrivateRPC("sendPairInstallation".prefix)
proc enableInstallation*(installationId: string): string =
result = callPrivateRPC("enableInstallation".prefix, %* [installationId])
proc disableInstallation*(installationId: string): string =
result = callPrivateRPC("disableInstallation".prefix, %* [installationId])

View File

@ -0,0 +1,49 @@
import json, times
import core, ../utils
proc ping*(mailservers: seq[string], timeoutMs: int): string =
var addresses: seq[string] = @[]
for mailserver in mailservers:
addresses.add(mailserver)
result = callPrivateRPC("mailservers_ping", %* [
{ "addresses": addresses, "timeoutMs": timeoutMs }
])
proc update*(peer: string) =
discard callPrivateRPC("updateMailservers".prefix, %* [[peer]])
proc setMailserver*(peer: string): string =
return callPrivateRPC("setMailserver".prefix, %* [peer])
proc delete*(peer: string) =
discard callPrivateRPC("mailservers_deleteMailserver", %* [peer])
proc requestAllHistoricMessages*(): string =
return callPrivateRPC("requestAllHistoricMessages".prefix, %*[])
proc requestStoreMessages*(topics: seq[string], symKeyID: string, peer: string, numberOfMessages: int, fromTimestamp: int64 = 0, toTimestamp: int64 = 0, force: bool = false) =
var toValue = times.toUnix(times.getTime())
var fromValue = toValue - 86400
if fromTimestamp != 0:
fromValue = fromTimestamp
if toTimestamp != 0:
toValue = toTimestamp
echo callPrivateRPC("requestMessages".prefix, %* [
{
"topics": topics,
"mailServerPeer": "16Uiu2HAmVVi6Q4j7MAKVibquW8aA27UNrA4Q8Wkz9EetGViu8ZF1",
"timeout": 30,
"limit": numberOfMessages,
"cursor": nil,
"from": fromValue,
"to": toValue,
"force": force
}
])
proc syncChatFromSyncedFrom*(chatId: string): string =
return callPrivateRPC("syncChatFromSyncedFrom".prefix, %*[chatId])
proc fillGaps*(chatId: string, messageIds: seq[string]): string =
return callPrivateRPC("fillGaps".prefix, %*[chatId, messageIds])

View File

@ -0,0 +1,215 @@
import
json, tables, sugar, sequtils, strutils, atomics, os
import
json_serialization, chronicles, uuids
import
./core, ./accounts/constants, ../utils
import ../types/[setting, network, fleet]
import ../signals/[base]
from status_go import nil
var
settings {.threadvar.}: JsonNode
settingsInited {.threadvar.}: bool
dirty: Atomic[bool]
dirty.store(true)
settings = %* {}
proc saveSetting*(key: Setting, value: string | JsonNode | bool): StatusGoError =
try:
let response = callPrivateRPC("settings_saveSetting", %* [key, value])
let responseResult = $(response.parseJSON(){"result"})
if responseResult == "null":
result.error = ""
else: result = Json.decode(response, StatusGoError)
dirty.store(true)
except Exception as e:
error "Error saving setting", key=key, value=value, msg=e.msg
proc getWeb3ClientVersion*(): string =
parseJson(callPrivateRPC("web3_clientVersion"))["result"].getStr
proc getSettings*(useCached: bool = true, keepSensitiveData: bool = false): JsonNode =
let cacheIsDirty = (not settingsInited) or dirty.load
if useCached and (not cacheIsDirty) and (not keepSensitiveData):
result = settings
else:
var
allSettings = callPrivateRPC("settings_getSettings").parseJSON()["result"]
var
noSensitiveData = allSettings.deepCopy
noSensitiveData.delete("mnemonic")
if not keepSensitiveData:
result = noSensitiveData
else:
result = allSettings
dirty.store(false)
settings = noSensitiveData # never include sensitive data in cache
settingsInited = true
proc getSetting*[T](name: Setting, defaultValue: T, useCached: bool = true): T =
let settings: JsonNode = getSettings(useCached, $name == "mnemonic")
if not settings.contains($name) or settings{$name}.isEmpty():
return defaultValue
let value = $settings{$name}
try:
result = Json.decode(value, T)
except Exception as e:
error "Error decoding setting", name=name, value=value, msg=e.msg
return defaultValue
proc getSetting*[T](name: Setting, useCached: bool = true): T =
result = getSetting(name, default(type(T)), useCached)
proc getCurrentNetwork*(): Network =
case getSetting[string](Setting.Networks_CurrentNetwork, constants.DEFAULT_NETWORK_NAME):
of "mainnet_rpc":
result = Network.Mainnet
of "testnet_rpc":
result = Network.Testnet
of "rinkeby_rpc":
result = Network.Rinkeby
of "goerli_rpc":
result = Network.Goerli
of "xdai_rpc":
result = Network.XDai
of "poa_rpc":
result = Network.Poa
else:
result = Network.Other
proc getCurrentNetworkDetails*(): NetworkDetails =
let currNetwork = getSetting[string](Setting.Networks_CurrentNetwork, constants.DEFAULT_NETWORK_NAME)
let networks = getSetting[seq[NetworkDetails]](Setting.Networks_Networks)
networks.find((network: NetworkDetails) => network.id == currNetwork)
proc getLinkPreviewWhitelist*(): JsonNode =
result = callPrivateRPC("getLinkPreviewWhitelist".prefix, %* []).parseJSON()["result"]
proc getFleet*(): Fleet =
let fleet = getSetting[string](Setting.Fleet, $Fleet.PROD)
result = parseEnum[Fleet](fleet)
proc getPinnedMailserver*(): string =
let pinnedMailservers = getSetting[JsonNode](Setting.PinnedMailservers, %*{})
let fleet = getSetting[string](Setting.Fleet, $Fleet.PROD)
return pinnedMailservers{fleet}.getStr()
proc pinMailserver*(enode: string = "") =
let pinnedMailservers = getSetting[JsonNode](Setting.PinnedMailservers, %*{})
let fleet = getSetting[string](Setting.Fleet, $Fleet.PROD)
pinnedMailservers[fleet] = newJString(enode)
discard saveSetting(Setting.PinnedMailservers, pinnedMailservers)
proc saveMailserver*(name, enode: string) =
let fleet = getSetting[string](Setting.Fleet, $Fleet.PROD)
let result = callPrivateRPC("mailservers_addMailserver", %* [
%*{
"id": $genUUID(),
"name": name,
"address": enode,
"fleet": $fleet
}
]).parseJSON()["result"]
proc getMailservers*():JsonNode =
let fleet = getSetting[string](Setting.Fleet, $Fleet.PROD)
result = callPrivateRPC("mailservers_getMailservers").parseJSON()["result"]
proc getNodeConfig*():JsonNode =
result = status_go.getNodeConfig().parseJSON()
# setting correct values in json
let currNetwork = getSetting[string](Setting.Networks_CurrentNetwork, constants.DEFAULT_NETWORK_NAME)
let networks = getSetting[JsonNode](Setting.Networks_Networks)
let networkConfig = networks.getElems().find((n:JsonNode) => n["id"].getStr() == currNetwork)
var newDataDir = networkConfig["config"]["DataDir"].getStr
newDataDir.removeSuffix("_rpc")
result["DataDir"] = newDataDir.newJString()
result["KeyStoreDir"] = newJString("./keystore")
result["LogFile"] = newJString("./geth.log")
result["ShhextConfig"]["BackupDisabledDataDir"] = newJString("./")
proc getWakuVersion*():int =
let nodeConfig = getNodeConfig()
if nodeConfig["WakuConfig"]["Enabled"].getBool():
return 1
if nodeConfig["WakuV2Config"]["Enabled"].getBool():
return 2
return 0
proc setWakuVersion*(newVersion: int) =
let nodeConfig = getNodeConfig()
nodeConfig["RegisterTopics"] = %* @["whispermail"]
if newVersion == 1:
nodeConfig["WakuConfig"]["Enabled"] = newJBool(true)
nodeConfig["WakuV2Config"]["Enabled"] = newJBool(false)
nodeConfig["NoDiscovery"] = newJBool(false)
nodeConfig["Rendezvous"] = newJBool(true)
else:
nodeConfig["WakuConfig"]["Enabled"] = newJBool(false)
nodeConfig["WakuV2Config"]["Enabled"] = newJBool(true)
nodeConfig["NoDiscovery"] = newJBool(true)
nodeConfig["Rendezvous"] = newJBool(false)
discard saveSetting(Setting.NodeConfig, nodeConfig)
proc setNetwork*(network: string): StatusGoError =
let statusGoResult = saveSetting(Setting.Networks_CurrentNetwork, network)
if statusGoResult.error != "":
return statusGoResult
let networks = getSetting[JsonNode](Setting.Networks_Networks)
let networkConfig = networks.getElems().find((n:JsonNode) => n["id"].getStr() == network)
var nodeConfig = getNodeConfig()
let upstreamUrl = networkConfig["config"]["UpstreamConfig"]["URL"]
var newDataDir = networkConfig["config"]["DataDir"].getStr
newDataDir.removeSuffix("_rpc")
nodeConfig["NetworkId"] = networkConfig["config"]["NetworkId"]
nodeConfig["DataDir"] = newDataDir.newJString()
nodeConfig["UpstreamConfig"]["Enabled"] = networkConfig["config"]["UpstreamConfig"]["Enabled"]
nodeConfig["UpstreamConfig"]["URL"] = upstreamUrl
return saveSetting(Setting.NodeConfig, nodeConfig)
proc setBloomFilterMode*(bloomFilterMode: bool): StatusGoError =
let statusGoResult = saveSetting(Setting.WakuBloomFilterMode, bloomFilterMode)
if statusGoResult.error != "":
return statusGoResult
var nodeConfig = getNodeConfig()
nodeConfig["WakuConfig"]["BloomFilterMode"] = newJBool(bloomFilterMode)
return saveSetting(Setting.NodeConfig, nodeConfig)
proc setBloomLevel*(bloomFilterMode: bool, fullNode: bool): StatusGoError =
let statusGoResult = saveSetting(Setting.WakuBloomFilterMode, bloomFilterMode)
if statusGoResult.error != "":
return statusGoResult
var nodeConfig = getNodeConfig()
nodeConfig["WakuConfig"]["BloomFilterMode"] = newJBool(bloomFilterMode)
nodeConfig["WakuConfig"]["FullNode"] = newJBool(fullNode)
nodeConfig["WakuConfig"]["LightClient"] = newJBool(not fullNode)
return saveSetting(Setting.NodeConfig, nodeConfig)
proc setFleet*(fleetConfig: FleetConfig, fleet: Fleet): StatusGoError =
let statusGoResult = saveSetting(Setting.Fleet, $fleet)
if statusGoResult.error != "":
return statusGoResult
var nodeConfig = getNodeConfig()
nodeConfig["ClusterConfig"]["Fleet"] = newJString($fleet)
nodeConfig["ClusterConfig"]["BootNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Bootnodes)
nodeConfig["ClusterConfig"]["TrustedMailServers"] = %* fleetConfig.getNodes(fleet, FleetNodes.Mailservers)
nodeConfig["ClusterConfig"]["StaticNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Whisper)
nodeConfig["ClusterConfig"]["RendezvousNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Rendezvous)
nodeConfig["ClusterConfig"]["WakuNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Waku)
nodeConfig["ClusterConfig"]["WakuStoreNodes"] = %* fleetConfig.getNodes(fleet, FleetNodes.Waku)
return saveSetting(Setting.NodeConfig, nodeConfig)

View File

@ -0,0 +1,234 @@
import # std libs
atomics, json, tables, sequtils, httpclient, net
from strutils import parseHexInt, parseInt
import # vendor libs
json_serialization, chronicles, libp2p/[multihash, multicodec, cid], stint,
web3/[ethtypes, conversions]
from nimcrypto import fromHex
import # status-desktop libs
./core as status, ../types/[sticker, setting, rpc_response],
./eth/contracts, ./settings, ./edn_helpers
proc decodeContentHash*(value: string): string =
if value == "":
return ""
# eg encoded sticker multihash cid:
# e30101701220eab9a8ef4eac6c3e5836a3768d8e04935c10c67d9a700436a0e53199e9b64d29
# e3017012205c531b83da9dd91529a4cf8ecd01cb62c399139e6f767e397d2f038b820c139f (testnet)
# e3011220c04c617170b1f5725070428c01280b4c19ae9083b7e6d71b7a0d2a1b5ae3ce30 (testnet)
#
# The first 4 bytes (in hex) represent:
# e3 = codec identifier "ipfs-ns" for content-hash
# 01 = unused - sometimes this is NOT included (ie ropsten)
# 01 = CID version (effectively unused, as we will decode with CIDv0 regardless)
# 70 = codec identifier "dag-pb"
# ipfs-ns
if value[0..1] != "e3":
warn "Could not decode sticker. It may still be valid, but requires a different codec to be used", hash=value
return ""
try:
# dag-pb
let defaultCodec = parseHexInt("70") #dag-pb
var codec = defaultCodec # no codec specified
var codecStartIdx = 2 # idx of where codec would start if it was specified
# handle the case when starts with 0xe30170 instead of 0xe3010170
if value[2..5] == "0101":
codecStartIdx = 6
codec = parseHexInt(value[6..7])
elif value[2..3] == "01" and value[4..5] != "12":
codecStartIdx = 4
codec = parseHexInt(value[4..5])
# strip the info we no longer need
var multiHashStr = value[codecStartIdx + 2..<value.len]
# The rest of the hash identifies the multihash algo, length, and digest
# More info: https://multiformats.io/multihash/
# 12 = identifies sha2-256 hash
# 20 = multihash length = 32
# ...rest = multihash digest
let multiHash = MultiHash.init(nimcrypto.fromHex(multiHashStr)).get()
let resultTyped = Cid.init(CIDv0, MultiCodec.codec(codec), multiHash).get()
result = $resultTyped
trace "Decoded sticker hash", cid=result
except Exception as e:
error "Error decoding sticker", hash=value, exception=e.msg
raise
# Retrieves number of sticker packs owned by user
# See https://notes.status.im/Q-sQmQbpTOOWCQcYiXtf5g#Read-Sticker-Packs-owned-by-a-user
# for more details
proc getBalance*(address: Address): int =
let contract = contracts.getContract("sticker-pack")
if contract == nil: return 0
let
balanceOf = BalanceOf(address: address)
payload = %* [{
"to": $contract.address,
"data": contract.methods["balanceOf"].encodeAbi(balanceOf)
}, "latest"]
let responseStr = status.callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if not response.error.isNil:
raise newException(RpcException, "Error getting stickers balance: " & response.error.message)
if response.result == "0x":
return 0
result = parseHexInt(response.result)
# Gets number of sticker packs
proc getPackCount*(): int =
let contract = contracts.getContract("stickers")
if contract == nil: return 0
let payload = %* [{
"to": $contract.address,
"data": contract.methods["packCount"].encodeAbi()
}, "latest"]
let responseStr = status.callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if not response.error.isNil:
raise newException(RpcException, "Error getting stickers balance: " & response.error.message)
if response.result == "0x":
return 0
result = parseHexInt(response.result)
# Gets sticker pack data
proc getPackData*(id: Stuint[256], running: var Atomic[bool]): StickerPack =
let secureSSLContext = newContext()
let client = newHttpClient(sslContext = secureSSLContext)
try:
let
contract = contracts.getContract("stickers")
contractMethod = contract.methods["getPackData"]
getPackData = GetPackData(packId: id)
payload = %* [{
"to": $contract.address,
"data": contractMethod.encodeAbi(getPackData)
}, "latest"]
let responseStr = status.callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if not response.error.isNil:
raise newException(RpcException, "Error getting sticker pack data: " & response.error.message)
let packData = contracts.decodeContractResponse[PackData](response.result)
if not running.load():
trace "Sticker pack task interrupted, exiting sticker pack loading"
return
# contract response includes a contenthash, which needs to be decoded to reveal
# an IPFS identifier. Once decoded, download the content from IPFS. This content
# is in EDN format, ie https://ipfs.infura.io/ipfs/QmWVVLwVKCwkVNjYJrRzQWREVvEk917PhbHYAUhA1gECTM
# and it also needs to be decoded in to a nim type
let contentHash = contracts.toHex(packData.contentHash)
let url = "https://ipfs.infura.io/ipfs/" & decodeContentHash(contentHash)
var ednMeta = client.getContent(url)
# decode the EDN content in to a StickerPack
result = edn_helpers.decode[StickerPack](ednMeta)
# EDN doesn't include a packId for each sticker, so add it here
result.stickers.apply(proc(sticker: var Sticker) =
sticker.packId = truncate(id, int))
result.id = truncate(id, int)
result.price = packData.price
except Exception as e:
raise newException(RpcException, "Error getting sticker pack data: " & e.msg)
finally:
client.close()
proc tokenOfOwnerByIndex*(address: Address, idx: Stuint[256]): int =
let
contract = contracts.getContract("sticker-pack")
tokenOfOwnerByIndex = TokenOfOwnerByIndex(address: address, index: idx)
payload = %* [{
"to": $contract.address,
"data": contract.methods["tokenOfOwnerByIndex"].encodeAbi(tokenOfOwnerByIndex)
}, "latest"]
let responseStr = status.callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if not response.error.isNil:
raise newException(RpcException, "Error getting owned tokens: " & response.error.message)
if response.result == "0x":
return 0
result = parseHexInt(response.result)
proc getPackIdFromTokenId*(tokenId: Stuint[256]): int =
let
contract = contracts.getContract("sticker-pack")
tokenPackId = TokenPackId(tokenId: tokenId)
payload = %* [{
"to": $contract.address,
"data": contract.methods["tokenPackId"].encodeAbi(tokenPackId)
}, "latest"]
let responseStr = status.callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if not response.error.isNil:
raise newException(RpcException, "Error getting pack id from token id: " & response.error.message)
if response.result == "0x":
return 0
result = parseHexInt(response.result)
proc saveInstalledStickerPacks*(installedStickerPacks: Table[int, StickerPack]) =
let json = %* {}
for packId, pack in installedStickerPacks.pairs:
json[$packId] = %(pack)
discard settings.saveSetting(Setting.Stickers_PacksInstalled, $json)
proc saveRecentStickers*(stickers: seq[Sticker]) =
discard settings.saveSetting(Setting.Stickers_Recent, %(stickers.mapIt($it.hash)))
proc getInstalledStickerPacks*(): Table[int, StickerPack] =
let setting = settings.getSetting[string](Setting.Stickers_PacksInstalled, "{}").parseJson
result = initTable[int, StickerPack]()
for i in setting.keys:
let packId = parseInt(i)
result[packId] = Json.decode($(setting[i]), StickerPack)
result[packId].stickers.apply(proc(sticker: var Sticker) =
sticker.packId = packId)
proc getPackIdForSticker*(packs: Table[int, StickerPack], hash: string): int =
for packId, pack in packs.pairs:
if pack.stickers.any(proc(sticker: Sticker): bool = return sticker.hash == hash):
return packId
return 0
proc getRecentStickers*(): seq[Sticker] =
# TODO: this should be a custom `readValue` implementation of nim-json-serialization
let settings = settings.getSetting[seq[string]](Setting.Stickers_Recent, @[])
let installedStickers = getInstalledStickerPacks()
result = newSeq[Sticker]()
for hash in settings:
# pack id is not returned from status-go settings, populate here
let packId = getPackIdForSticker(installedStickers, $hash)
# .insert instead of .add to effectively reverse the order stickers because
# stickers are re-reversed when added to the view due to the nature of
# inserting recent stickers at the front of the list
result.insert(Sticker(hash: $hash, packId: packId), 0)
proc getAvailableStickerPacks*(running: var Atomic[bool]): Table[int, StickerPack] =
var availableStickerPacks = initTable[int, StickerPack]()
try:
let numPacks = getPackCount()
for i in 0..<numPacks:
if not running.load():
trace "Sticker pack task interrupted, exiting sticker pack loading"
break
try:
let stickerPack = getPackData(i.u256, running)
availableStickerPacks[stickerPack.id] = stickerPack
except:
continue
result = availableStickerPacks
except RpcException:
error "Error in getAvailableStickerPacks", message = getCurrentExceptionMsg()
result = initTable[int, StickerPack]()

172
status/libstatus/tokens.nim Normal file
View File

@ -0,0 +1,172 @@
import
json, chronicles, strformat, stint, strutils, sequtils, tables, atomics
import
web3/[ethtypes, conversions], json_serialization
import
./settings, ./core, ./wallet, ./eth/contracts,
../types/[setting, network, rpc_response]
from ../utils import parseAddress
logScope:
topics = "wallet"
var
customTokens {.threadvar.}: seq[Erc20Contract]
customTokensInited {.threadvar.}: bool
dirty: Atomic[bool]
dirty.store(true)
proc getCustomTokens*(useCached: bool = true): seq[Erc20Contract] =
let cacheIsDirty = not customTokensInited or dirty.load
if useCached and not cacheIsDirty:
result = customTokens
else:
let payload = %* []
let responseStr = callPrivateRPC("wallet_getCustomTokens", payload)
# TODO: this should be handled in the deserialisation of RpcResponse,
# question has been posed: https://discordapp.com/channels/613988663034118151/616299964242460682/762828178624217109
let response = RpcResponse(result: $(responseStr.parseJSON()["result"]))
if not response.error.isNil:
raise newException(RpcException, "Error getting custom tokens: " & response.error.message)
result = if response.result == "null": @[] else: Json.decode(response.result, seq[Erc20Contract])
dirty.store(false)
customTokens = result
customTokensInited = true
proc visibleTokensSNTDefault(): JsonNode =
let currentNetwork = getCurrentNetwork()
let SNT = if currentNetwork == Network.Mainnet: "SNT" else: "STT"
let response = getSetting[string](Setting.VisibleTokens, "{}").parseJSON
if not response.hasKey($currentNetwork):
# Set STT/SNT visible by default
response[$currentNetwork] = %* [SNT]
return response
proc convertStringSeqToERC20ContractSeq*(stringSeq: seq[string]): seq[Erc20Contract] =
result = @[]
for v in stringSeq:
let t = getErc20Contract(v)
if t != nil: result.add t
let ct = customTokens.getErc20ContractBySymbol(v)
if ct != nil: result.add ct
proc toggleAsset*(symbol: string): seq[Erc20Contract] =
let currentNetwork = getCurrentNetwork()
let visibleTokens = visibleTokensSNTDefault()
var visibleTokenList = visibleTokens[$currentNetwork].to(seq[string])
let symbolIdx = visibleTokenList.find(symbol)
if symbolIdx > -1:
visibleTokenList.del(symbolIdx)
else:
visibleTokenList.add symbol
visibleTokens[$currentNetwork] = newJArray()
visibleTokens[$currentNetwork] = %* visibleTokenList
let saved = saveSetting(Setting.VisibleTokens, $visibleTokens)
convertStringSeqToERC20ContractSeq(visibleTokenList)
proc hideAsset*(symbol: string) =
let currentNetwork = getCurrentNetwork()
let visibleTokens = visibleTokensSNTDefault()
var visibleTokenList = visibleTokens[$currentNetwork].to(seq[string])
var symbolIdx = visibleTokenList.find(symbol)
if symbolIdx > -1:
visibleTokenList.del(symbolIdx)
visibleTokens[$currentNetwork] = newJArray()
visibleTokens[$currentNetwork] = %* visibleTokenList
discard saveSetting(Setting.VisibleTokens, $visibleTokens)
proc getVisibleTokens*(): seq[Erc20Contract] =
let currentNetwork = getCurrentNetwork()
let visibleTokens = visibleTokensSNTDefault()
var visibleTokenList = visibleTokens[$currentNetwork].to(seq[string])
let customTokens = getCustomTokens()
result = convertStringSeqToERC20ContractSeq(visibleTokenList)
proc addCustomToken*(address: string, name: string, symbol: string, decimals: int, color: string) =
let payload = %* [{"address": address, "name": name, "symbol": symbol, "decimals": decimals, "color": color}]
discard callPrivateRPC("wallet_addCustomToken", payload)
dirty.store(true)
proc removeCustomToken*(address: string) =
let payload = %* [address]
echo callPrivateRPC("wallet_deleteCustomToken", payload)
dirty.store(true)
proc getTokensBalances*(accounts: openArray[string], tokens: openArray[string]): JsonNode =
let payload = %* [accounts, tokens]
let response = callPrivateRPC("wallet_getTokensBalances", payload).parseJson
if response["result"].kind == JNull:
return %* {}
response["result"]
proc getToken*(tokenAddress: string): Erc20Contract =
getErc20Contracts().concat(getCustomTokens()).getErc20ContractByAddress(tokenAddress.parseAddress)
proc getTokenBalance*(tokenAddress: string, account: string): string =
var postfixedAccount: string = account
postfixedAccount.removePrefix("0x")
let payload = %* [{
"to": tokenAddress, "from": account, "data": fmt"0x70a08231000000000000000000000000{postfixedAccount}"
}, "latest"]
let response = callPrivateRPC("eth_call", payload)
let balance = response.parseJson["result"].getStr
var decimals = 18
let address = parseAddress(tokenAddress)
let t = getErc20Contract(address)
let ct = getCustomTokens().getErc20ContractByAddress(address)
if t != nil:
decimals = t.decimals
elif ct != nil:
decimals = ct.decimals
result = $hex2Token(balance, decimals)
proc getSNTAddress*(): string =
let snt = contracts.getSntContract()
result = $snt.address
proc getSNTBalance*(account: string): string =
let snt = contracts.getSntContract()
result = getTokenBalance($snt.address, account)
proc getTokenString*(contract: Contract, methodName: string): string =
let payload = %* [{
"to": $contract.address,
"data": contract.methods[methodName].encodeAbi()
}, "latest"]
let responseStr = callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if not response.error.isNil:
raise newException(RpcException, "Error getting token string - " & methodName & ": " & response.error.message)
if response.result == "0x":
return ""
let size = fromHex(Stuint[256], response.result[66..129]).truncate(int)
result = response.result[130..129+size*2].parseHexStr
proc tokenName*(contract: Contract): string = getTokenString(contract, "name")
proc tokenSymbol*(contract: Contract): string = getTokenString(contract, "symbol")
proc tokenDecimals*(contract: Contract): int =
let payload = %* [{
"to": $contract.address,
"data": contract.methods["decimals"].encodeAbi()
}, "latest"]
let responseStr = callPrivateRPC("eth_call", payload)
let response = Json.decode(responseStr, RpcResponse)
if not response.error.isNil:
raise newException(RpcException, "Error getting token decimals: " & response.error.message)
if response.result == "0x":
return 0
result = parseHexInt(response.result)

147
status/libstatus/wallet.nim Normal file
View File

@ -0,0 +1,147 @@
import json, json, options, json_serialization, stint, chronicles
import core, conversions, ../types/[transaction, rpc_response], ../utils, strutils, strformat
from status_go import validateMnemonic#, startWallet
import ../wallet/account
import web3/ethtypes
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 getTransactionReceipt*(transactionHash: string): string =
result = callPrivateRPC("eth_getTransactionReceipt", %* [transactionHash])
proc getTransfersByAddress*(address: string, toBlock: Uint256, limit: int, loadMore: bool = false): seq[Transaction] =
try:
let
toBlockParsed = "0x" & stint.toHex(toBlock)
limitParsed = "0x" & limit.toHex.stripLeadingZeros
transactionsResponse = getTransfersByAddress(address, toBlockParsed, limitParsed, loadMore)
transactions = parseJson(transactionsResponse)["result"]
var accountTransactions: seq[Transaction] = @[]
for transaction in transactions:
accountTransactions.add(Transaction(
id: transaction["id"].getStr,
typeValue: transaction["type"].getStr,
address: transaction["address"].getStr,
contract: transaction["contract"].getStr,
blockNumber: transaction["blockNumber"].getStr,
blockHash: transaction["blockhash"].getStr,
timestamp: $hex2LocalDateTime(transaction["timestamp"].getStr()),
gasPrice: transaction["gasPrice"].getStr,
gasLimit: transaction["gasLimit"].getStr,
gasUsed: transaction["gasUsed"].getStr,
nonce: transaction["nonce"].getStr,
txStatus: transaction["txStatus"].getStr,
value: transaction["value"].getStr,
fromAddress: transaction["from"].getStr,
to: transaction["to"].getStr
))
return accountTransactions
except:
let msg = getCurrentExceptionMsg()
error "Failed getting wallet account transactions", msg
proc getBalance*(address: string): string =
let payload = %* [address, "latest"]
let response = parseJson(callPrivateRPC("eth_getBalance", payload))
if response.hasKey("error"):
raise newException(RpcException, "Error getting balance: " & $response["error"])
else:
result = response["result"].str
proc hex2Eth*(input: string): string =
var value = fromHex(Stuint[256], input)
result = utils.wei2Eth(value)
proc validateMnemonic*(mnemonic: string): string =
result = $status_go.validateMnemonic(mnemonic)
proc startWallet*(watchNewBlocks: bool) =
# this will be fixed in a later PR
discard
proc hex2Token*(input: string, decimals: int): string =
var value = fromHex(Stuint[256], input)
if decimals == 0:
return fmt"{value}"
var p = u256(10).pow(decimals)
var i = value.div(p)
var r = value.mod(p)
var leading_zeros = "0".repeat(decimals - ($r).len)
var d = fmt"{leading_zeros}{$r}"
result = $i
if(r > 0): result = fmt"{result}.{d}"
proc trackPendingTransaction*(hash: string, fromAddress: string, toAddress: string, trxType: PendingTransactionType, data: string) =
let payload = %* [{"hash": hash, "from": fromAddress, "to": toAddress, "type": $trxType, "additionalData": data, "data": "", "value": 0, "timestamp": 0, "gasPrice": 0, "gasLimit": 0}]
discard callPrivateRPC("wallet_storePendingTransaction", payload)
proc getPendingTransactions*(): string =
let payload = %* []
try:
result = callPrivateRPC("wallet_getPendingTransactions", payload)
except Exception as e:
error "Error getting pending transactions (possible dev Infura key)", msg = e.msg
result = ""
proc getPendingOutboundTransactionsByAddress*(address: string): string =
let payload = %* [address]
result = callPrivateRPC("wallet_getPendingOutboundTransactionsByAddress", payload)
proc deletePendingTransaction*(transactionHash: string) =
let payload = %* [transactionHash]
discard callPrivateRPC("wallet_deletePendingTransaction", payload)
proc setInitialBlocksRange*(): string =
let payload = %* []
result = callPrivateRPC("wallet_setInitialBlocksRange", payload)
proc watchTransaction*(transactionHash: string): string =
let payload = %* [transactionHash]
result = callPrivateRPC("wallet_watchTransaction", payload)
proc checkRecentHistory*(addresses: seq[string]): string =
let payload = %* [addresses]
result = callPrivateRPC("wallet_checkRecentHistory", payload)
proc getOpenseaCollections*(address: string): string =
let payload = %* [address]
result = callPrivateRPC("wallet_getOpenseaCollectionsByOwner", payload)
proc getOpenseaAssets*(address: string, collectionSlug: string, limit: int): string =
let payload = %* [address, collectionSlug, limit]
result = callPrivateRPC("wallet_getOpenseaAssetsByOwnerAndCollection", payload)
proc fetchCryptoServices*(success: var bool): string =
success = true
try:
result = callPrivateRPC("wallet_getCryptoOnRamps")
except Exception as e:
success = false
error "Error getting crypto services: ", msg = e.msg
result = ""

21
status/mailservers.nim Normal file
View File

@ -0,0 +1,21 @@
import json
import libstatus/mailservers as status_mailservers
import ../eventemitter
type
MailserversModel* = ref object
events*: EventEmitter
proc newMailserversModel*(events: EventEmitter): MailserversModel =
result = MailserversModel()
result.events = events
proc fillGaps*(self: MailserversModel, chatId: string, messageIds: seq[string]): string =
result = status_mailservers.fillGaps(chatId, messageIds)
proc setMailserver*(self: MailserversModel, peer: string): string =
result = status_mailservers.setMailserver(peer)
proc requestAllHistoricMessages*(self: MailserversModel): string =
result = status_mailservers.requestAllHistoricMessages()

51
status/messages.nim Normal file
View File

@ -0,0 +1,51 @@
import tables, sets
import libstatus/chat
import ../eventemitter
type
MessageDetails* = object
status*: string
chatId*: string
MessagesModel* = ref object
events*: EventEmitter
messages*: Table[string, MessageDetails]
confirmations*: HashSet[string]
MessageSentArgs* = ref object of Args
messageId*: string
chatId*: string
proc newMessagesModel*(events: EventEmitter): MessagesModel =
result = MessagesModel()
result.events = events
result.messages = initTable[string, MessageDetails]()
result.confirmations = initHashSet[string]()
proc delete*(self: MessagesModel) =
discard
# For each message sent we call trackMessage to register the message id,
# and wait until an EnvelopeSent signals is emitted for that message. However
# due to communication being async, it's possible that the signal arrives
# first, hence why we check if there's a confirmation (an envelope.sent)
# inside trackMessage to emit the "messageSent" event
proc trackMessage*(self: MessagesModel, messageId: string, chatId: string) =
if self.messages.hasKey(messageId): return
self.messages[messageId] = MessageDetails(status: "sending", chatId: chatId)
if self.confirmations.contains(messageId):
self.confirmations.excl(messageId)
self.messages[messageId].status = "sent"
discard updateOutgoingMessageStatus(messageId, "sent")
self.events.emit("messageSent", MessageSentArgs(messageId: messageId, chatId: chatId))
proc updateStatus*(self: MessagesModel, messageIds: seq[string]) =
for messageId in messageIds:
if self.messages.hasKey(messageId):
self.messages[messageId].status = "sent"
discard updateOutgoingMessageStatus(messageId, "sent")
self.events.emit("messageSent", MessageSentArgs(messageId: messageId, chatId: self.messages[messageId].chatId))
else:
self.confirmations.incl(messageId)

54
status/network.nim Normal file
View File

@ -0,0 +1,54 @@
import chronicles
import ../eventemitter
import libstatus/settings
import json
import uuids
import json_serialization
import ./types/[setting]
logScope:
topics = "network-model"
type
NetworkModel* = ref object
peers*: seq[string]
events*: EventEmitter
connected*: bool
proc newNetworkModel*(events: EventEmitter): NetworkModel =
result = NetworkModel()
result.events = events
result.peers = @[]
result.connected = false
proc peerSummaryChange*(self: NetworkModel, peers: seq[string]) =
if peers.len == 0 and self.connected:
self.connected = false
self.events.emit("network:disconnected", Args())
if peers.len > 0 and not self.connected:
self.connected = true
self.events.emit("network:connected", Args())
self.peers = peers
proc peerCount*(self: NetworkModel): int = self.peers.len
proc isConnected*(self: NetworkModel): bool = self.connected
proc addNetwork*(self: NetworkModel, name: string, endpoint: string, networkId: int, networkType: string) =
var networks = getSetting[JsonNode](Setting.Networks_Networks)
let id = genUUID()
networks.elems.add(%*{
"id": $genUUID(),
"name": name,
"config": {
"NetworkId": networkId,
"DataDir": "/ethereum/" & networkType,
"UpstreamConfig": {
"Enabled": true,
"URL": endpoint
}
}
})
discard saveSetting(Setting.Networks_Networks, networks)

16
status/node.nim Normal file
View File

@ -0,0 +1,16 @@
import libstatus/core as status
import ../eventemitter
type NodeModel* = ref object
events*: EventEmitter
proc newNodeModel*(): NodeModel =
result = NodeModel()
result.events = createEventEmitter()
proc delete*(self: NodeModel) =
discard
proc sendRPCMessageRaw*(self: NodeModel, msg: string): string =
echo "sending RPC message"
status.callPrivateRPCRaw(msg)

View File

@ -0,0 +1,20 @@
import json
import ../types/[os_notification]
import ../../eventemitter
type OsNotifications* = ref object
events: EventEmitter
proc delete*(self: OsNotifications) =
discard
proc newOsNotifications*(events: EventEmitter): OsNotifications =
result = OsNotifications()
result.events = events
proc onNotificationClicked*(self: OsNotifications, identifier: string) =
## This slot is called once user clicks a notificaiton bubble, "identifier"
## contains data which uniquely define that notification.
let details = toOsNotificationDetails(parseJson(identifier))
self.events.emit("osNotificationClicked", OsNotificationsArgs(details: details))

105
status/permissions.nim Normal file
View File

@ -0,0 +1,105 @@
import json
import strutils
import sets
import libstatus/core
import chronicles
import ../eventemitter
import sequtils
type
Permission* {.pure.} = enum
Web3 = "web3",
ContactCode = "contact-code"
Unknown = "unknown"
logScope:
topics = "permissions-model"
type
PermissionsModel* = ref object
events*: EventEmitter
proc toPermission*(value: string): Permission =
result = Permission.Unknown
try:
result = parseEnum[Permission](value)
except:
warn "Unknown permission requested", value
proc newPermissionsModel*(events: EventEmitter): PermissionsModel =
result = PermissionsModel()
result.events = events
proc init*(self: PermissionsModel) =
discard
type Dapp* = object
name*: string
permissions*: HashSet[Permission]
proc getDapps*(self: PermissionsModel): seq[Dapp] =
let response = callPrivateRPC("permissions_getDappPermissions")
result = @[]
for dapps in response.parseJson["result"].getElems():
var dapp = Dapp(
name: dapps["dapp"].getStr(),
permissions: initHashSet[Permission]()
)
for permission in dapps["permissions"].getElems():
dapp.permissions.incl(permission.getStr().toPermission())
result.add(dapp)
proc getPermissions*(self: PermissionsModel, dapp: string): HashSet[Permission] =
let response = callPrivateRPC("permissions_getDappPermissions")
result = initHashSet[Permission]()
for dappPermission in response.parseJson["result"].getElems():
if dappPermission["dapp"].getStr() == dapp:
if not dappPermission.hasKey("permissions"): return
for permission in dappPermission["permissions"].getElems():
result.incl(permission.getStr().toPermission())
proc revoke*(self: PermissionsModel, permission: Permission) =
let response = callPrivateRPC("permissions_getDappPermissions")
var permissions = initHashSet[Permission]()
for dapps in response.parseJson["result"].getElems():
for currPerm in dapps["permissions"].getElems():
let p = currPerm.getStr().toPermission()
if p != permission:
permissions.incl(p)
discard callPrivateRPC("permissions_addDappPermissions", %*[{
"dapp": dapps["dapp"].getStr(),
"permissions": permissions.toSeq()
}])
proc hasPermission*(self: PermissionsModel, dapp: string, permission: Permission): bool =
result = self.getPermissions(dapp).contains(permission)
proc addPermission*(self: PermissionsModel, dapp: string, permission: Permission) =
var permissions = self.getPermissions(dapp)
permissions.incl(permission)
discard callPrivateRPC("permissions_addDappPermissions", %*[{
"dapp": dapp,
"permissions": permissions.toSeq()
}])
proc revokePermission*(self: PermissionsModel, dapp: string, permission: Permission) =
var permissions = self.getPermissions(dapp)
permissions.excl(permission)
if permissions.len == 0:
discard callPrivateRPC("permissions_deleteDappPermissions", %*[dapp])
else:
discard callPrivateRPC("permissions_addDappPermissions", %*[{
"dapp": dapp,
"permissions": permissions.toSeq()
}])
proc clearPermissions*(self: PermissionsModel, dapp: string) =
discard callPrivateRPC("permissions_deleteDappPermissions", %*[dapp])
proc clearPermissions*(self: PermissionsModel) =
let response = callPrivateRPC("permissions_getDappPermissions")
for dapps in response.parseJson["result"].getElems():
discard callPrivateRPC("permissions_deleteDappPermissions", %*[dapps["dapp"].getStr()])

28
status/profile.nim Normal file
View File

@ -0,0 +1,28 @@
import json
import ./types/[identity_image]
import profile/profile
import libstatus/core as libstatus_core
import libstatus/accounts as status_accounts
import libstatus/settings as status_settings
import ../eventemitter
type
ProfileModel* = ref object
proc newProfileModel*(): ProfileModel =
result = ProfileModel()
proc logout*(self: ProfileModel) =
discard status_accounts.logout()
proc getLinkPreviewWhitelist*(self: ProfileModel): JsonNode =
result = status_settings.getLinkPreviewWhitelist()
proc storeIdentityImage*(self: ProfileModel, keyUID: string, imagePath: string, aX, aY, bX, bY: int): IdentityImage =
result = status_accounts.storeIdentityImage(keyUID, imagePath, aX, aY, bX, bY)
proc getIdentityImage*(self: ProfileModel, keyUID: string): IdentityImage =
result = status_accounts.getIdentityImage(keyUID)
proc deleteIdentityImage*(self: ProfileModel, keyUID: string): string =
result = status_accounts.deleteIdentityImage(keyUID)

View File

@ -0,0 +1,3 @@
type
MailServer* = ref object
name*, endpoint*: string

View File

@ -0,0 +1,29 @@
import json
import ../types/[profile, account]
export profile
const contactAdded* = ":contact/added"
const contactBlocked* = ":contact/blocked"
const contactRequest* = ":contact/request-received"
proc isContact*(self: Profile): bool =
result = self.systemTags.contains(contactAdded)
proc isBlocked*(self: Profile): bool =
result = self.systemTags.contains(contactBlocked)
proc requestReceived*(self: Profile): bool =
result = self.systemTags.contains(contactRequest)
proc toProfileModel*(account: Account): Profile =
result = Profile(
id: "",
username: account.name,
identicon: account.identicon,
alias: account.name,
ensName: "",
ensVerified: false,
appearance: 0,
systemTags: @[]
)

276
status/provider.nim Normal file
View File

@ -0,0 +1,276 @@
import ens, wallet, permissions, utils
import ../eventemitter
import ./types/[setting]
import utils
import libstatus/accounts
import libstatus/core
import libstatus/settings as status_settings
import json, json_serialization, sets, strutils
import chronicles
import nbaser
import stew/byteutils
from base32 import nil
const HTTPS_SCHEME* = "https"
const IPFS_GATEWAY* = ".infura.status.im"
const SWARM_GATEWAY* = "swarm-gateways.net"
const base58* = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
logScope:
topics = "provider-model"
type
RequestTypes {.pure.} = enum
Web3SendAsyncReadOnly = "web3-send-async-read-only",
HistoryStateChanged = "history-state-changed",
APIRequest = "api-request"
Unknown = "unknown"
ResponseTypes {.pure.} = enum
Web3SendAsyncCallback = "web3-send-async-callback",
APIResponse = "api-response",
Web3ResponseError = "web3-response-error"
type
Payload = ref object
id: JsonNode
rpcMethod: string
Web3SendAsyncReadOnly = ref object
messageId: JsonNode
payload: Payload
request: string
hostname: string
APIRequest = ref object
isAllowed: bool
messageId: JsonNode
permission: Permission
hostname: string
const AUTH_METHODS = toHashSet(["eth_accounts", "eth_coinbase", "eth_sendTransaction", "eth_sign", "keycard_signTypedData", "eth_signTypedData", "eth_signTypedData_v3", "personal_sign", "personal_ecRecover"])
const SIGN_METHODS = toHashSet(["eth_sign", "personal_sign", "eth_signTypedData", "eth_signTypedData_v3"])
const ACC_METHODS = toHashSet(["eth_accounts", "eth_coinbase"])
type ProviderModel* = ref object
events*: EventEmitter
permissions*: PermissionsModel
proc newProviderModel*(events: EventEmitter, permissions: PermissionsModel): ProviderModel =
result = ProviderModel()
result.events = events
result.permissions = permissions
proc requestType(message: string): RequestTypes =
let data = message.parseJson
result = RequestTypes.Unknown
try:
result = parseEnum[RequestTypes](data["type"].getStr())
except:
warn "Unknown request type received", value=data["permission"].getStr()
proc toWeb3SendAsyncReadOnly(message: string): Web3SendAsyncReadOnly =
let data = message.parseJson
result = Web3SendAsyncReadOnly(
messageId: data["messageId"],
request: $data["payload"],
hostname: data{"hostname"}.getStr(),
payload: Payload(
id: data["payload"]{"id"},
rpcMethod: data["payload"]["method"].getStr()
)
)
proc toAPIRequest(message: string): APIRequest =
let data = message.parseJson
result = APIRequest(
messageId: data["messageId"],
isAllowed: data{"isAllowed"}.getBool(),
permission: data["permission"].getStr().toPermission(),
hostname: data{"hostname"}.getStr()
)
proc process(self: ProviderModel, data: Web3SendAsyncReadOnly): string =
if AUTH_METHODS.contains(data.payload.rpcMethod) and not self.permissions.hasPermission(data.hostname, Permission.Web3):
return $ %* {
"type": ResponseTypes.Web3SendAsyncCallback,
"messageId": data.messageId,
"error": {
"code": 4100
}
}
if data.payload.rpcMethod == "eth_sendTransaction":
try:
let request = data.request.parseJson
let fromAddress = request["params"][0]["from"].getStr()
let to = request["params"][0]{"to"}.getStr()
let value = if (request["params"][0]["value"] != nil):
request["params"][0]["value"].getStr()
else:
"0"
let password = request["password"].getStr()
let selectedGasLimit = request["selectedGasLimit"].getStr()
let selectedGasPrice = request["selectedGasPrice"].getStr()
let txData = if (request["params"][0].hasKey("data") and request["params"][0]["data"].kind != JNull):
request["params"][0]["data"].getStr()
else:
""
var success: bool
var errorMessage = ""
var response = ""
var validInput: bool = true
try:
validateTransactionInput(fromAddress, to, "", value, selectedGasLimit, selectedGasPrice, txData, "dummy")
except Exception as e:
validInput = false
success = false
errorMessage = e.msg
if validInput:
# TODO make this async
response = wallet.sendTransaction(fromAddress, to, value, selectedGasLimit, selectedGasPrice, password, success, txData)
errorMessage = if not success:
if response == "":
"web3-response-error"
else:
response
else:
""
return $ %* {
"type": ResponseTypes.Web3SendAsyncCallback,
"messageId": data.messageId,
"error": errorMessage,
"result": {
"jsonrpc": "2.0",
"id": data.payload.id,
"result": if (success): response else: ""
}
}
except Exception as e:
error "Error sending the transaction", msg = e.msg
return $ %* {
"type": ResponseTypes.Web3SendAsyncCallback,
"messageId": data.messageId,
"error": {
"code": 4100,
"message": e.msg
}
}
if SIGN_METHODS.contains(data.payload.rpcMethod):
try:
let request = data.request.parseJson
var params = request["params"]
let password = hashPassword(request["password"].getStr())
let dappAddress = status_settings.getSetting[string](Setting.DappsAddress)
var rpcResult = "{}"
case data.payload.rpcMethod:
of "eth_signTypedData", "eth_signTypedData_v3":
rpcResult = signTypedData(params[1].getStr(), dappAddress, password)
else:
rpcResult = signMessage($ %* {
"data": params[0].getStr(),
"password": password,
"account": dappAddress
})
let jsonRpcResult = rpcResult.parseJson
let success: bool = not jsonRpcResult.hasKey("error")
let errorMessage = if success: "" else: jsonRpcResult["error"]{"message"}.getStr()
let response = if success: jsonRpcResult["result"].getStr() else: ""
return $ %* {
"type": ResponseTypes.Web3SendAsyncCallback,
"messageId": data.messageId,
"error": errorMessage,
"result": {
"jsonrpc": "2.0",
"id": if data.payload.id == nil: newJNull() else: data.payload.id,
"result": if (success): response else: ""
}
}
except Exception as e:
error "Error signing message", msg = e.msg
return $ %* {
"type": ResponseTypes.Web3SendAsyncCallback,
"messageId": data.messageId,
"error": {
"code": 4100,
"message": e.msg
}
}
if ACC_METHODS.contains(data.payload.rpcMethod):
let dappAddress = status_settings.getSetting[string](Setting.DappsAddress)
return $ %* {
"type": ResponseTypes.Web3SendAsyncCallback,
"messageId": data.messageId,
"result": {
"jsonrpc": "2.0",
"id": data.payload.id,
"result": if data.payload.rpcMethod == "eth_coinbase": newJString(dappAddress) else: %*[dappAddress]
}
}
let rpcResult = callRPC(data.request)
return $ %* {
"type": ResponseTypes.Web3SendAsyncCallback,
"messageId": data.messageId,
"error": (if rpcResult == "": newJString("web3-response-error") else: newJNull()),
"result": rpcResult.parseJson
}
proc process*(self: ProviderModel, data: APIRequest): string =
var value:JsonNode = case data.permission
of Permission.Web3: %* [status_settings.getSetting[string](Setting.DappsAddress, "0x0000000000000000000000000000000000000000")]
of Permission.ContactCode: %* status_settings.getSetting[string](Setting.PublicKey, "0x0")
of Permission.Unknown: newJNull()
let isAllowed = data.isAllowed and data.permission != Permission.Unknown
info "API request received", host=data.hostname, value=data.permission, isAllowed
if isAllowed: self.permissions.addPermission(data.hostname, data.permission)
return $ %* {
"type": ResponseTypes.APIResponse,
"isAllowed": isAllowed,
"permission": data.permission,
"messageId": data.messageId,
"data": value
}
proc postMessage*(self: ProviderModel, message: string): string =
case message.requestType():
of RequestTypes.Web3SendAsyncReadOnly: self.process(message.toWeb3SendAsyncReadOnly())
of RequestTypes.HistoryStateChanged: """{"type":"TODO-IMPLEMENT-THIS"}""" ############# TODO:
of RequestTypes.APIRequest: self.process(message.toAPIRequest())
else: """{"type":"TODO-IMPLEMENT-THIS"}""" ##################### TODO:
proc ensResourceURL*(self: ProviderModel, ens: string, url: string): (string, string, string, string, bool) =
let contentHash = contenthash(ens)
if contentHash == "": # ENS does not have a content hash
return (url, url, HTTPS_SCHEME, "", false)
let decodedHash = contentHash.decodeENSContentHash()
case decodedHash[0]:
of ENSType.IPFS:
let base32Hash = base32.encode(string.fromBytes(base58.decode(decodedHash[1]))).toLowerAscii().replace("=", "")
result = (url, base32Hash & IPFS_GATEWAY, HTTPS_SCHEME, "", true)
of ENSType.SWARM:
result = (url, SWARM_GATEWAY, HTTPS_SCHEME, "/bzz:/" & decodedHash[1] & "/", true)
of ENSType.IPNS:
result = (url, decodedHash[1], HTTPS_SCHEME, "", true)
else:
warn "Unknown content for", ens, contentHash

72
status/settings.nim Normal file
View File

@ -0,0 +1,72 @@
import json, json_serialization
import
sugar, sequtils, strutils, atomics
import libstatus/settings as libstatus_settings
import ../eventemitter
import ./types/[fleet, network, setting]
import ./signals/[base]
type
SettingsModel* = ref object
events*: EventEmitter
proc newSettingsModel*(events: EventEmitter): SettingsModel =
result = SettingsModel()
result.events = events
proc saveSetting*(self: SettingsModel, key: Setting, value: string | JsonNode | bool): StatusGoError =
result = libstatus_settings.saveSetting(key, value)
proc getSetting*[T](self: SettingsModel, name: Setting, defaultValue: T, useCached: bool = true): T =
result = libstatus_settings.getSetting(name, defaultValue, useCached)
proc getSetting*[T](self: SettingsModel, name: Setting, useCached: bool = true): T =
result = libstatus_settings.getSetting[T](name, useCached)
# TODO: name with a 2 due to namespace conflicts that need to be addressed in subsquent PRs
proc getSetting2*[T](name: Setting, defaultValue: T, useCached: bool = true): T =
result = libstatus_settings.getSetting(name, defaultValue, useCached)
proc getSetting2*[T](name: Setting, useCached: bool = true): T =
result = libstatus_settings.getSetting[T](name, useCached)
proc getCurrentNetworkDetails*(self: SettingsModel): NetworkDetails =
result = libstatus_settings.getCurrentNetworkDetails()
proc getMailservers*(self: SettingsModel):JsonNode =
result = libstatus_settings.getMailservers()
proc getPinnedMailserver*(self: SettingsModel): string =
result = libstatus_settings.getPinnedMailserver()
proc pinMailserver*(self: SettingsModel, enode: string = "") =
libstatus_settings.pinMailserver(enode)
proc saveMailserver*(self: SettingsModel, name, enode: string) =
libstatus_settings.saveMailserver(name, enode)
proc getFleet*(self: SettingsModel): Fleet =
result = libstatus_settings.getFleet()
proc getCurrentNetwork*(): Network =
result = libstatus_settings.getCurrentNetwork()
proc getCurrentNetwork*(self: SettingsModel): Network =
result = getCurrentNetwork()
proc setWakuVersion*(self: SettingsModel, newVersion: int) =
libstatus_settings.setWakuVersion(newVersion)
proc setBloomFilterMode*(self: SettingsModel, bloomFilterMode: bool): StatusGoError =
libstatus_settings.setBloomFilterMode(bloomFilterMode)
proc setFleet*(self: SettingsModel, fleetConfig: FleetConfig, fleet: Fleet): StatusGoError =
libstatus_settings.setFleet(fleetConfig, fleet)
proc getNodeConfig*(self: SettingsModel): JsonNode =
libstatus_settings.getNodeConfig()
proc setBloomLevel*(self: SettingsModel, bloomFilterMode: bool, fullNode: bool): StatusGoError =
libstatus_settings.setBloomLevel(bloomFilterMode, fullNode)

15
status/signals/base.nim Normal file
View File

@ -0,0 +1,15 @@
import json_serialization
import signal_type
import ../../eventemitter
export signal_type
type Signal* = ref object of Args
signalType* {.serializedFieldName("type").}: SignalType
type StatusGoError* = object
error*: string
type NodeSignal* = ref object of Signal
event*: StatusGoError

View File

@ -0,0 +1,13 @@
import json
import base
import status/types/community
type CommunitySignal* = ref object of Signal
community*: Community
proc fromEvent*(event: JsonNode): Signal =
var signal: CommunitySignal = CommunitySignal()
signal.community = event["event"].toCommunity()
result = signal

View File

@ -0,0 +1,13 @@
import json
import base
type DiscoverySummarySignal* = ref object of Signal
enodes*: seq[string]
proc fromEvent*(jsonSignal: JsonNode): Signal =
var signal:DiscoverySummarySignal = DiscoverySummarySignal()
if jsonSignal["event"].kind != JNull:
for discoveryItem in jsonSignal["event"]:
signal.enodes.add(discoveryItem["enode"].getStr)
result = signal

View File

@ -0,0 +1,14 @@
import json
import base
type EnvelopeSentSignal* = ref object of Signal
messageIds*: seq[string]
proc fromEvent*(jsonSignal: JsonNode): Signal =
var signal:EnvelopeSentSignal = EnvelopeSentSignal()
if jsonSignal["event"].kind != JNull and jsonSignal["event"].hasKey("ids") and jsonSignal["event"]["ids"].kind != JNull:
for messageId in jsonSignal["event"]["ids"]:
signal.messageIds.add(messageId.getStr)
result = signal

View File

@ -0,0 +1,14 @@
import json
import base
type EnvelopeExpiredSignal* = ref object of Signal
messageIds*: seq[string]
proc fromEvent*(jsonSignal: JsonNode): Signal =
var signal:EnvelopeExpiredSignal = EnvelopeExpiredSignal()
if jsonSignal["event"].kind != JNull and jsonSignal["event"].hasKey("ids") and jsonSignal["event"]["ids"].kind != JNull:
for messageId in jsonSignal["event"]["ids"]:
signal.messageIds.add(messageId.getStr)
result = signal

View File

@ -0,0 +1,29 @@
import json
import base
type MailserverRequestCompletedSignal* = ref object of Signal
requestID*: string
lastEnvelopeHash*: string
cursor*: string
errorMessage*: string
error*: bool
type MailserverRequestExpiredSignal* = ref object of Signal
# TODO
proc fromCompletedEvent*(jsonSignal: JsonNode): Signal =
var signal:MailserverRequestCompletedSignal = MailserverRequestCompletedSignal()
if jsonSignal["event"].kind != JNull:
signal.requestID = jsonSignal["event"]{"requestID"}.getStr()
signal.lastEnvelopeHash = jsonSignal["event"]{"lastEnvelopeHash"}.getStr()
signal.cursor = jsonSignal["event"]{"cursor"}.getStr()
signal.errorMessage = jsonSignal["event"]{"errorMessage"}.getStr()
signal.error = signal.errorMessage != ""
result = signal
proc fromExpiredEvent*(jsonSignal: JsonNode): Signal =
var signal:MailserverRequestExpiredSignal = MailserverRequestExpiredSignal()
# TODO: parse signal
result = signal

View File

@ -0,0 +1,95 @@
import json, chronicles
import base
import status/types/[message, chat, community, profile, installation,
activity_center_notification, status_update, removed_message]
type MessageSignal* = ref object of Signal
messages*: seq[Message]
pinnedMessages*: seq[Message]
chats*: seq[Chat]
contacts*: seq[Profile]
installations*: seq[Installation]
emojiReactions*: seq[Reaction]
communities*: seq[Community]
membershipRequests*: seq[CommunityMembershipRequest]
activityCenterNotification*: seq[ActivityCenterNotification]
statusUpdates*: seq[StatusUpdate]
deletedMessages*: seq[RemovedMessage]
proc fromEvent*(event: JsonNode): Signal =
var signal:MessageSignal = MessageSignal()
signal.messages = @[]
signal.contacts = @[]
if event["event"]{"contacts"} != nil:
for jsonContact in event["event"]["contacts"]:
signal.contacts.add(jsonContact.toProfileModel())
var chatsWithMentions: seq[string] = @[]
if event["event"]{"messages"} != nil:
for jsonMsg in event["event"]["messages"]:
var message = jsonMsg.toMessage()
if message.hasMention:
chatsWithMentions.add(message.chatId)
signal.messages.add(message)
if event["event"]{"chats"} != nil:
for jsonChat in event["event"]["chats"]:
var chat = jsonChat.toChat
if chatsWithMentions.contains(chat.id):
chat.mentionsCount.inc
signal.chats.add(chat)
if event["event"]{"statusUpdates"} != nil:
for jsonStatusUpdate in event["event"]["statusUpdates"]:
var statusUpdate = jsonStatusUpdate.toStatusUpdate
signal.statusUpdates.add(statusUpdate)
if event["event"]{"installations"} != nil:
for jsonInstallation in event["event"]["installations"]:
signal.installations.add(jsonInstallation.toInstallation)
if event["event"]{"emojiReactions"} != nil:
for jsonReaction in event["event"]["emojiReactions"]:
signal.emojiReactions.add(jsonReaction.toReaction)
if event["event"]{"communities"} != nil:
for jsonCommunity in event["event"]["communities"]:
signal.communities.add(jsonCommunity.toCommunity)
if event["event"]{"requestsToJoinCommunity"} != nil:
for jsonCommunity in event["event"]["requestsToJoinCommunity"]:
signal.membershipRequests.add(jsonCommunity.toCommunityMembershipRequest)
if event["event"]{"removedMessages"} != nil:
for jsonRemovedMessage in event["event"]["removedMessages"]:
signal.deletedMessages.add(jsonRemovedMessage.toRemovedMessage)
if event["event"]{"activityCenterNotifications"} != nil:
for jsonNotification in event["event"]["activityCenterNotifications"]:
signal.activityCenterNotification.add(jsonNotification.toActivityCenterNotification())
if event["event"]{"pinMessages"} != nil:
for jsonPinnedMessage in event["event"]["pinMessages"]:
var contentType: ContentType
try:
contentType = ContentType(jsonPinnedMessage{"contentType"}.getInt)
except:
warn "Unknown content type received", type = jsonPinnedMessage{"contentType"}.getInt
contentType = ContentType.Message
signal.pinnedMessages.add(Message(
id: jsonPinnedMessage{"message_id"}.getStr,
chatId: jsonPinnedMessage{"chat_id"}.getStr,
localChatId: jsonPinnedMessage{"localChatId"}.getStr,
pinnedBy: jsonPinnedMessage{"from"}.getStr,
identicon: jsonPinnedMessage{"identicon"}.getStr,
alias: jsonPinnedMessage{"alias"}.getStr,
clock: jsonPinnedMessage{"clock"}.getInt,
isPinned: jsonPinnedMessage{"pinned"}.getBool,
contentType: contentType
))
result = signal

View File

@ -0,0 +1,26 @@
{.used.}
type SignalType* {.pure.} = enum
Message = "messages.new"
Wallet = "wallet"
NodeReady = "node.ready"
NodeCrashed = "node.crashed"
NodeStarted = "node.started"
NodeStopped = "node.stopped"
NodeLogin = "node.login"
EnvelopeSent = "envelope.sent"
EnvelopeExpired = "envelope.expired"
MailserverRequestCompleted = "mailserver.request.completed"
MailserverRequestExpired = "mailserver.request.expired"
DiscoveryStarted = "discovery.started"
DiscoveryStopped = "discovery.stopped"
DiscoverySummary = "discovery.summary"
SubscriptionsData = "subscriptions.data"
SubscriptionsError = "subscriptions.error"
WhisperFilterAdded = "whisper.filter.added"
CommunityFound = "community.found"
Stats = "stats"
Unknown
proc event*(self:SignalType):string =
result = "signal:" & $self

21
status/signals/stats.nim Normal file
View File

@ -0,0 +1,21 @@
import json
import base
type Stats* = object
uploadRate*: uint64
downloadRate*: uint64
type StatsSignal* = ref object of Signal
stats*: Stats
proc toStats(jsonMsg: JsonNode): Stats =
result = Stats(
uploadRate: uint64(jsonMsg{"uploadRate"}.getBiggestInt()),
downloadRate: uint64(jsonMsg{"downloadRate"}.getBiggestInt())
)
proc fromEvent*(event: JsonNode): Signal =
var signal:StatsSignal = StatsSignal()
signal.stats = event["event"].toStats
result = signal

25
status/signals/wallet.nim Normal file
View File

@ -0,0 +1,25 @@
import json
import base
type WalletSignal* = ref object of Signal
content*: string
eventType*: string
blockNumber*: int
accounts*: seq[string]
# newTransactions*: ???
erc20*: bool
proc fromEvent*(jsonSignal: JsonNode): Signal =
var signal:WalletSignal = WalletSignal()
signal.content = $jsonSignal
if jsonSignal["event"].kind != JNull:
signal.eventType = jsonSignal["event"]["type"].getStr
signal.blockNumber = jsonSignal["event"]{"blockNumber"}.getInt
signal.erc20 = jsonSignal["event"]{"erc20"}.getBool
signal.accounts = @[]
if jsonSignal["event"]["accounts"].kind != JNull:
for account in jsonSignal["event"]["accounts"]:
signal.accounts.add(account.getStr)
result = signal

View File

@ -0,0 +1,33 @@
import json
import base
type Filter* = object
chatId*: string
symKeyId*: string
listen*: bool
filterId*: string
identity*: string
topic*: string
type WhisperFilterSignal* = ref object of Signal
filters*: seq[Filter]
proc toFilter(jsonMsg: JsonNode): Filter =
result = Filter(
chatId: jsonMsg{"chatId"}.getStr,
symKeyId: jsonMsg{"symKeyId"}.getStr,
listen: jsonMsg{"listen"}.getBool,
filterId: jsonMsg{"filterId"}.getStr,
identity: jsonMsg{"identity"}.getStr,
topic: jsonMsg{"topic"}.getStr,
)
proc fromEvent*(event: JsonNode): Signal =
var signal:WhisperFilterSignal = WhisperFilterSignal()
if event["event"]{"filters"} != nil:
for jsonMsg in event["event"]["filters"]:
signal.filters.add(jsonMsg.toFilter)
result = signal

89
status/status.nim Normal file
View File

@ -0,0 +1,89 @@
import libstatus/accounts as libstatus_accounts
import libstatus/core as libstatus_core
import libstatus/settings as libstatus_settings
import chat, accounts, wallet, wallet2, node, network, messages, contacts, profile, stickers, permissions, fleet, settings, mailservers, browser, tokens, provider
import notifications/os_notifications
import ../eventemitter
import bitops, stew/byteutils, chronicles
import ./types/[setting]
export chat, accounts, node, messages, contacts, profile, network, permissions, fleet, eventemitter
type Status* = ref object
events*: EventEmitter
fleet*: FleetModel
chat*: ChatModel
messages*: MessagesModel
accounts*: AccountModel
wallet*: WalletModel
wallet2*: StatusWalletController
node*: NodeModel
profile*: ProfileModel
contacts*: ContactModel
network*: NetworkModel
stickers*: StickersModel
permissions*: PermissionsModel
settings*: SettingsModel
mailservers*: MailserversModel
browser*: BrowserModel
tokens*: TokensModel
provider*: ProviderModel
osnotifications*: OsNotifications
proc newStatusInstance*(fleetConfig: string): Status =
result = Status()
result.events = createEventEmitter()
result.fleet = fleet.newFleetModel(fleetConfig)
result.chat = chat.newChatModel(result.events)
result.accounts = accounts.newAccountModel(result.events)
result.wallet = wallet.newWalletModel(result.events)
result.wallet.initEvents()
result.wallet2 = wallet2.newStatusWalletController(result.events)
result.node = node.newNodeModel()
result.messages = messages.newMessagesModel(result.events)
result.profile = profile.newProfileModel()
result.contacts = contacts.newContactModel(result.events)
result.network = network.newNetworkModel(result.events)
result.stickers = stickers.newStickersModel(result.events)
result.permissions = permissions.newPermissionsModel(result.events)
result.settings = settings.newSettingsModel(result.events)
result.mailservers = mailservers.newMailserversModel(result.events)
result.browser = browser.newBrowserModel(result.events)
result.tokens = tokens.newTokensModel(result.events)
result.provider = provider.newProviderModel(result.events, result.permissions)
result.osnotifications = newOsNotifications(result.events)
proc initNode*(self: Status) =
libstatus_accounts.initNode()
proc startMessenger*(self: Status) =
libstatus_core.startMessenger()
proc reset*(self: Status) =
# TODO: remove this once accounts are not tracked in the AccountsModel
self.accounts.reset()
# NOT NEEDED self.chat.reset()
# NOT NEEDED self.wallet.reset()
# NOT NEEDED self.node.reset()
# NOT NEEDED self.mailservers.reset()
# NOT NEEDED self.profile.reset()
# TODO: add all resets here
proc getNodeVersion*(self: Status): string =
libstatus_settings.getWeb3ClientVersion()
# TODO: duplicated??
proc saveSetting*(self: Status, setting: Setting, value: string | bool) =
discard libstatus_settings.saveSetting(setting, value)
proc getBloomFilter*(self: Status): string =
result = libstatus_core.getBloomFilter()
proc getBloomFilterBitsSet*(self: Status): int =
let bloomFilter = libstatus_core.getBloomFilter()
var bitCount = 0;
for b in hexToSeqByte(bloomFilter):
bitCount += countSetBits(b)
return bitCount

146
status/stickers.nim Normal file
View File

@ -0,0 +1,146 @@
import # global deps
atomics, sequtils, strutils, tables
import # project deps
chronicles, web3/[ethtypes, conversions], stint
import # local deps
libstatus/eth/contracts as status_contracts,
libstatus/stickers as status_stickers, transactions,
libstatus/wallet, ../eventemitter
import ./types/[sticker, transaction, rpc_response]
from utils as libstatus_utils import eth2Wei, gwei2Wei, toUInt64, parseAddress
logScope:
topics = "stickers-model"
type
StickersModel* = ref object
events*: EventEmitter
recentStickers*: seq[Sticker]
availableStickerPacks*: Table[int, StickerPack]
installedStickerPacks*: Table[int, StickerPack]
purchasedStickerPacks*: seq[int]
StickerArgs* = ref object of Args
sticker*: Sticker
save*: bool
# forward declaration
proc addStickerToRecent*(self: StickersModel, sticker: Sticker, save: bool = false)
proc newStickersModel*(events: EventEmitter): StickersModel =
result = StickersModel()
result.events = events
result.recentStickers = @[]
result.availableStickerPacks = initTable[int, StickerPack]()
result.installedStickerPacks = initTable[int, StickerPack]()
result.purchasedStickerPacks = @[]
proc init*(self: StickersModel) =
self.events.on("stickerSent") do(e: Args):
var evArgs = StickerArgs(e)
self.addStickerToRecent(evArgs.sticker, evArgs.save)
proc buildTransaction(packId: Uint256, address: Address, price: Uint256, approveAndCall: var ApproveAndCall[100], sntContract: var Erc20Contract, gas = "", gasPrice = ""): EthSend =
sntContract = status_contracts.getSntContract()
let
stickerMktContract = status_contracts.getContract("sticker-market")
buyToken = BuyToken(packId: packId, address: address, price: price)
buyTxAbiEncoded = stickerMktContract.methods["buyToken"].encodeAbi(buyToken)
approveAndCall = ApproveAndCall[100](to: stickerMktContract.address, value: price, data: DynamicBytes[100].fromHex(buyTxAbiEncoded))
transactions.buildTokenTransaction(address, sntContract.address, gas, gasPrice)
proc estimateGas*(packId: int, address: string, price: string, success: var bool): int =
var
approveAndCall: ApproveAndCall[100]
sntContract = status_contracts.getSntContract()
tx = buildTransaction(
packId.u256,
parseAddress(address),
eth2Wei(parseFloat(price), sntContract.decimals),
approveAndCall,
sntContract
)
let response = sntContract.methods["approveAndCall"].estimateGas(tx, approveAndCall, success)
if success:
result = fromHex[int](response)
proc buyPack*(self: StickersModel, packId: int, address, price, gas, gasPrice, password: string, success: var bool): string =
var
sntContract: Erc20Contract
approveAndCall: ApproveAndCall[100]
tx = buildTransaction(
packId.u256,
parseAddress(address),
eth2Wei(parseFloat(price), 18), # SNT
approveAndCall,
sntContract,
gas,
gasPrice
)
result = sntContract.methods["approveAndCall"].send(tx, approveAndCall, password, success)
if success:
trackPendingTransaction(result, address, $sntContract.address, PendingTransactionType.BuyStickerPack, $packId)
proc getStickerMarketAddress*(self: StickersModel): Address =
result = status_contracts.getContract("sticker-market").address
proc getPurchasedStickerPacks*(self: StickersModel, address: Address): seq[int] =
try:
let
balance = status_stickers.getBalance(address)
tokenIds = toSeq[0..<balance].mapIt(status_stickers.tokenOfOwnerByIndex(address, it.u256))
purchasedPackIds = tokenIds.mapIt(status_stickers.getPackIdFromTokenId(it.u256))
self.purchasedStickerPacks = self.purchasedStickerPacks.concat(purchasedPackIds)
result = self.purchasedStickerPacks
except RpcException:
error "Error getting purchased sticker packs", message = getCurrentExceptionMsg()
result = @[]
proc getInstalledStickerPacks*(self: StickersModel): Table[int, StickerPack] =
if self.installedStickerPacks != initTable[int, StickerPack]():
return self.installedStickerPacks
self.installedStickerPacks = status_stickers.getInstalledStickerPacks()
result = self.installedStickerPacks
proc getAvailableStickerPacks*(running: var Atomic[bool]): Table[int, StickerPack] = status_stickers.getAvailableStickerPacks(running)
proc getRecentStickers*(self: StickersModel): seq[Sticker] =
result = status_stickers.getRecentStickers()
proc installStickerPack*(self: StickersModel, packId: int) =
if not self.availableStickerPacks.hasKey(packId):
return
let pack = self.availableStickerPacks[packId]
self.installedStickerPacks[packId] = pack
status_stickers.saveInstalledStickerPacks(self.installedStickerPacks)
proc removeRecentStickers*(self: StickersModel, packId: int) =
self.recentStickers.keepItIf(it.packId != packId)
status_stickers.saveRecentStickers(self.recentStickers)
proc uninstallStickerPack*(self: StickersModel, packId: int) =
if not self.installedStickerPacks.hasKey(packId):
return
let pack = self.availableStickerPacks[packId]
self.installedStickerPacks.del(packId)
status_stickers.saveInstalledStickerPacks(self.installedStickerPacks)
proc addStickerToRecent*(self: StickersModel, sticker: Sticker, save: bool = false) =
self.recentStickers.insert(sticker, 0)
self.recentStickers = self.recentStickers.deduplicate()
if self.recentStickers.len > 24:
self.recentStickers = self.recentStickers[0..23] # take top 24 most recent
if save:
status_stickers.saveRecentStickers(self.recentStickers)
proc decodeContentHash*(value: string): string =
result = status_stickers.decodeContentHash(value)
proc getPackIdFromTokenId*(tokenId: Stuint[256]): int =
result = status_stickers.getPackIdFromTokenId(tokenId)

43
status/tokens.nim Normal file
View File

@ -0,0 +1,43 @@
import libstatus/tokens as status_tokens
import libstatus/eth/contracts
import ../eventemitter
type
TokensModel* = ref object
events*: EventEmitter
proc newTokensModel*(events: EventEmitter): TokensModel =
result = TokensModel()
result.events = events
proc getSNTAddress*(): string =
result = status_tokens.getSNTAddress()
proc getCustomTokens*(self: TokensModel, useCached: bool = true): seq[Erc20Contract] =
result = status_tokens.getCustomTokens(useCached)
proc removeCustomToken*(self: TokensModel, address: string) =
status_tokens.removeCustomToken(address)
proc getSNTBalance*(account: string): string =
result = status_tokens.getSNTBalance(account)
proc tokenDecimals*(contract: Contract): int =
result = status_tokens.tokenDecimals(contract)
proc tokenName*(contract: Contract): string =
result = status_tokens.tokenName(contract)
proc tokensymbol*(contract: Contract): string =
result = status_tokens.tokensymbol(contract)
proc getTokenBalance*(tokenAddress: string, account: string): string =
result = status_tokens.getTokenBalance(tokenAddress, account)
proc getToken*(self: TokensModel, tokenAddress: string): Erc20Contract =
result = status_tokens.getToken(tokenAddress)
export newErc20Contract
export getErc20Contracts
export Erc20Contract
export getErc20ContractByAddress

20
status/transactions.nim Normal file
View File

@ -0,0 +1,20 @@
import
options, strutils
import
stint, web3/ethtypes
from utils as status_utils import toUInt64, gwei2Wei, parseAddress
proc buildTransaction*(source: Address, value: Uint256, gas = "", gasPrice = "", data = ""): EthSend =
result = EthSend(
source: source,
value: value.some,
gas: (if gas.isEmptyOrWhitespace: Quantity.none else: Quantity(cast[uint64](parseFloat(gas).toUInt64)).some),
gasPrice: (if gasPrice.isEmptyOrWhitespace: int.none else: gwei2Wei(parseFloat(gasPrice)).truncate(int).some),
data: data
)
proc buildTokenTransaction*(source, contractAddress: Address, gas = "", gasPrice = ""): EthSend =
result = buildTransaction(source, 0.u256, gas, gasPrice)
result.to = contractAddress.some

46
status/types/account.nim Normal file
View File

@ -0,0 +1,46 @@
{.used.}
import json_serialization
import ../../eventemitter
import identity_image
include multi_accounts
export identity_image
type
Account* = ref object of RootObj
name*: string
keyUid* {.serializedFieldName("key-uid").}: string
identityImage*: IdentityImage
identicon*: string
type
NodeAccount* = ref object of Account
timestamp*: int
keycardPairing* {.serializedFieldName("keycard-pairing").}: string
type
GeneratedAccount* = ref object
publicKey*: string
address*: string
id*: string
mnemonic*: string
derived*: MultiAccounts
# FIXME: should inherit from Account but multiAccountGenerateAndDeriveAddresses
# response has a camel-cased properties like "publicKey" and "keyUid", so the
# serializedFieldName pragma would need to be different
name*: string
keyUid*: string
identicon*: string
identityImage*: IdentityImage
proc toAccount*(account: GeneratedAccount): Account =
result = Account(name: account.name, identityImage: account.identityImage, identicon: account.identicon, keyUid: account.keyUid)
proc toAccount*(account: NodeAccount): Account =
result = Account(name: account.name, identityImage: account.identityImage, identicon: account.identicon, keyUid: account.keyUid)
type AccountArgs* = ref object of Args
account*: Account

View File

@ -0,0 +1,49 @@
{.used.}
import json, chronicles
import message
export message
type ActivityCenterNotificationType* {.pure.}= enum
Unknown = 0,
NewOneToOne = 1,
NewPrivateGroupChat = 2,
Mention = 3
Reply = 4
type ActivityCenterNotification* = ref object of RootObj
id*: string # ID is the id of the chat, for public chats it is the name e.g. status, for one-to-one is the hex encoded public key and for group chats is a random uuid appended with the hex encoded pk of the creator of the chat
chatId*: string
name*: string
author*: string
notificationType*: ActivityCenterNotificationType
message*: Message
timestamp*: int64
read*: bool
dismissed*: bool
accepted*: bool
proc toActivityCenterNotification*(jsonNotification: JsonNode): ActivityCenterNotification =
var activityCenterNotificationType: ActivityCenterNotificationType
try:
activityCenterNotificationType = ActivityCenterNotificationType(jsonNotification{"type"}.getInt)
except:
warn "Unknown notification type received", type = jsonNotification{"type"}.getInt
activityCenterNotificationType = ActivityCenterNotificationType.Unknown
result = ActivityCenterNotification(
id: jsonNotification{"id"}.getStr,
chatId: jsonNotification{"chatId"}.getStr,
name: jsonNotification{"name"}.getStr,
author: jsonNotification{"author"}.getStr,
notificationType: activityCenterNotificationType,
timestamp: jsonNotification{"timestamp"}.getInt,
read: jsonNotification{"read"}.getBool,
dismissed: jsonNotification{"dismissed"}.getBool,
accepted: jsonNotification{"accepted"}.getBool
)
if jsonNotification.contains("message") and jsonNotification{"message"}.kind != JNull:
result.message = jsonNotification{"message"}.toMessage()
elif activityCenterNotificationType == ActivityCenterNotificationType.NewOneToOne and jsonNotification.contains("lastMessage") and jsonNotification{"lastMessage"}.kind != JNull:
result.message = jsonNotification{"lastMessage"}.toMessage()

View File

@ -0,0 +1,7 @@
{.used.}
type Bookmark* = ref object
name*: string
url*: string
imageUrl*: string

170
status/types/chat.nim Normal file
View File

@ -0,0 +1,170 @@
{.used.}
import strutils, random, strformat, json
import ../libstatus/accounts as status_accounts
import message
include chat_member
include chat_membership_event
type ChatType* {.pure.}= enum
Unknown = 0,
OneToOne = 1,
Public = 2,
PrivateGroupChat = 3,
Profile = 4,
Timeline = 5
CommunityChat = 6
proc isOneToOne*(self: ChatType): bool = self == ChatType.OneToOne
proc isTimeline*(self: ChatType): bool = self == ChatType.Timeline
type Chat* = ref object
id*: string # ID is the id of the chat, for public chats it is the name e.g. status, for one-to-one is the hex encoded public key and for group chats is a random uuid appended with the hex encoded pk of the creator of the chat
communityId*: string
private*: bool
categoryId*: string
name*: string
description*: string
color*: string
identicon*: string
isActive*: bool # indicates whether the chat has been soft deleted
chatType*: ChatType
timestamp*: int64 # indicates the last time this chat has received/sent a message
joined*: int64 # indicates when the user joined the chat last time
lastClockValue*: int64 # indicates the last clock value to be used when sending messages
deletedAtClockValue*: int64 # indicates the clock value at time of deletion, messages with lower clock value of this should be discarded
unviewedMessagesCount*: int
unviewedMentionsCount*: int
lastMessage*: Message
members*: seq[ChatMember]
membershipUpdateEvents*: seq[ChatMembershipEvent]
mentionsCount*: int # Using this is not a good approach, we should instead use unviewedMentionsCount and refer to it always.
muted*: bool
canPost*: bool
ensName*: string
position*: int
proc `$`*(self: Chat): string =
result = fmt"Chat(id:{self.id}, name:{self.name}, active:{self.isActive}, type:{self.chatType})"
proc toJsonNode*(self: Chat): JsonNode =
result = %* {
"active": self.isActive,
"chatType": self.chatType.int,
"color": self.color,
"deletedAtClockValue": self.deletedAtClockValue,
"id": self.id,
"lastClockValue": self.lastClockValue,
"lastMessage": nil,
"members": self.members.toJsonNode,
"membershipUpdateEvents": self.membershipUpdateEvents.toJsonNode,
"name": (if self.ensName != "": self.ensName else: self.name),
"timestamp": self.timestamp,
"unviewedMessagesCount": self.unviewedMessagesCount,
"joined": self.joined,
"position": self.position
}
proc toChatMember*(jsonMember: JsonNode): ChatMember =
let pubkey = jsonMember["id"].getStr
result = ChatMember(
admin: jsonMember["admin"].getBool,
id: pubkey,
joined: jsonMember["joined"].getBool,
identicon: generateIdenticon(pubkey),
userName: generateAlias(pubkey)
)
proc toChatMembershipEvent*(jsonMembership: JsonNode): ChatMembershipEvent =
result = ChatMembershipEvent(
chatId: jsonMembership["chatId"].getStr,
clockValue: jsonMembership["clockValue"].getBiggestInt,
fromKey: jsonMembership["from"].getStr,
rawPayload: jsonMembership["rawPayload"].getStr,
signature: jsonMembership["signature"].getStr,
eventType: jsonMembership["type"].getInt,
name: jsonMembership{"name"}.getStr,
members: @[]
)
if jsonMembership{"members"} != nil:
for member in jsonMembership["members"]:
result.members.add(member.getStr)
proc toChat*(jsonChat: JsonNode): Chat =
let chatTypeInt = jsonChat{"chatType"}.getInt
let chatType: ChatType = if chatTypeInt >= ord(low(ChatType)) or chatTypeInt <= ord(high(ChatType)): ChatType(chatTypeInt) else: ChatType.Unknown
result = Chat(
id: jsonChat{"id"}.getStr,
communityId: jsonChat{"communityId"}.getStr,
name: jsonChat{"name"}.getStr,
description: jsonChat{"description"}.getStr,
identicon: "",
color: jsonChat{"color"}.getStr,
isActive: jsonChat{"active"}.getBool,
chatType: chatType,
timestamp: jsonChat{"timestamp"}.getBiggestInt,
lastClockValue: jsonChat{"lastClockValue"}.getBiggestInt,
deletedAtClockValue: jsonChat{"deletedAtClockValue"}.getBiggestInt,
unviewedMessagesCount: jsonChat{"unviewedMessagesCount"}.getInt,
unviewedMentionsCount: jsonChat{"unviewedMentionsCount"}.getInt,
mentionsCount: 0,
muted: false,
ensName: "",
joined: 0,
private: jsonChat{"private"}.getBool
)
if jsonChat.hasKey("muted") and jsonChat["muted"].kind != JNull:
result.muted = jsonChat["muted"].getBool
if jsonChat["lastMessage"].kind != JNull:
result.lastMessage = jsonChat{"lastMessage"}.toMessage()
if jsonChat.hasKey("joined") and jsonChat["joined"].kind != JNull:
result.joined = jsonChat{"joined"}.getInt
if result.chatType == ChatType.OneToOne:
result.identicon = generateIdenticon(result.id)
if result.name.endsWith(".eth"):
result.ensName = result.name
if result.name == "":
result.name = generateAlias(result.id)
if jsonChat["members"].kind != JNull:
result.members = @[]
for jsonMember in jsonChat["members"]:
result.members.add(jsonMember.toChatMember)
if jsonChat["membershipUpdateEvents"].kind != JNull:
result.membershipUpdateEvents = @[]
for jsonMember in jsonChat["membershipUpdateEvents"]:
result.membershipUpdateEvents.add(jsonMember.toChatMembershipEvent)
const channelColors* = ["#fa6565", "#7cda00", "#887af9", "#51d0f0", "#FE8F59", "#d37ef4"]
proc newChat*(id: string, chatType: ChatType): Chat =
randomize()
result = Chat(
id: id,
color: channelColors[rand(channelColors.len - 1)],
isActive: true,
chatType: chatType,
timestamp: 0,
lastClockValue: 0,
deletedAtClockValue: 0,
unviewedMessagesCount: 0,
mentionsCount: 0,
members: @[]
)
if chatType == ChatType.OneToOne:
result.identicon = generateIdenticon(id)
result.name = generateAlias(id)
else:
result.name = id

View File

@ -0,0 +1,21 @@
{.used.}
import strformat, json, sequtils
type ChatMember* = object
admin*: bool
id*: string
joined*: bool
identicon*: string
userName*: string
localNickname*: string
proc toJsonNode*(self: ChatMember): JsonNode =
result = %* {
"id": self.id,
"admin": self.admin,
"joined": self.joined
}
proc toJsonNode*(self: seq[ChatMember]): seq[JsonNode] =
result = map(self, proc(x: ChatMember): JsonNode = x.toJsonNode)

View File

@ -0,0 +1,28 @@
{.used.}
import strformat, json, sequtils
type ChatMembershipEvent* = object
chatId*: string
clockValue*: int64
fromKey*: string
name*: string
members*: seq[string]
rawPayload*: string
signature*: string
eventType*: int
proc toJsonNode*(self: ChatMembershipEvent): JsonNode =
result = %* {
"chatId": self.chatId,
"name": self.name,
"clockValue": self.clockValue,
"from": self.fromKey,
"members": self.members,
"rawPayload": self.rawPayload,
"signature": self.signature,
"type": self.eventType
}
proc toJsonNode*(self: seq[ChatMembershipEvent]): seq[JsonNode] =
result = map(self, proc(x: ChatMembershipEvent): JsonNode = x.toJsonNode)

View File

@ -0,0 +1,91 @@
{.used.}
import json, strformat, tables
import chat, status_update, identity_image
include community_category
include community_membership_request
type Community* = object
id*: string
name*: string
lastChannelSeen*: string
description*: string
chats*: seq[Chat]
categories*: seq[CommunityCategory]
members*: seq[string]
access*: int
unviewedMessagesCount*: int
unviewedMentionsCount*: int
admin*: bool
joined*: bool
verified*: bool
ensOnly*: bool
canRequestAccess*: bool
canManageUsers*: bool
canJoin*: bool
isMember*: bool
muted*: bool
communityImage*: IdentityImage
membershipRequests*: seq[CommunityMembershipRequest]
communityColor*: string
memberStatus*: OrderedTable[string, StatusUpdate]
proc `$`*(self: Community): string =
result = fmt"Community(id:{self.id}, name:{self.name}, description:{self.description}"
proc toCommunity*(jsonCommunity: JsonNode): Community =
result = Community(
id: jsonCommunity{"id"}.getStr,
name: jsonCommunity{"name"}.getStr,
description: jsonCommunity{"description"}.getStr,
access: jsonCommunity{"permissions"}{"access"}.getInt,
admin: jsonCommunity{"admin"}.getBool,
joined: jsonCommunity{"joined"}.getBool,
verified: jsonCommunity{"verified"}.getBool,
ensOnly: jsonCommunity{"permissions"}{"ens_only"}.getBool,
canRequestAccess: jsonCommunity{"canRequestAccess"}.getBool,
canManageUsers: jsonCommunity{"canManageUsers"}.getBool,
canJoin: jsonCommunity{"canJoin"}.getBool,
isMember: jsonCommunity{"isMember"}.getBool,
muted: jsonCommunity{"muted"}.getBool,
chats: newSeq[Chat](),
members: newSeq[string](),
communityColor: jsonCommunity{"color"}.getStr,
communityImage: IdentityImage()
)
result.memberStatus = initOrderedTable[string, StatusUpdate]()
if jsonCommunity.hasKey("images") and jsonCommunity["images"].kind != JNull:
if jsonCommunity["images"].hasKey("thumbnail"):
result.communityImage.thumbnail = jsonCommunity["images"]["thumbnail"]["uri"].str
if jsonCommunity["images"].hasKey("large"):
result.communityImage.large = jsonCommunity["images"]["large"]["uri"].str
if jsonCommunity.hasKey("chats") and jsonCommunity["chats"].kind != JNull:
for chatId, chat in jsonCommunity{"chats"}:
result.chats.add(Chat(
id: result.id & chatId,
categoryId: chat{"categoryID"}.getStr(),
communityId: result.id,
name: chat{"name"}.getStr,
description: chat{"description"}.getStr,
canPost: chat{"canPost"}.getBool,
chatType: ChatType.CommunityChat,
private: chat{"permissions"}{"private"}.getBool,
position: chat{"position"}.getInt
))
if jsonCommunity.hasKey("categories") and jsonCommunity["categories"].kind != JNull:
for catId, cat in jsonCommunity{"categories"}:
result.categories.add(CommunityCategory(
id: catId,
name: cat{"name"}.getStr,
position: cat{"position"}.getInt
))
if jsonCommunity.hasKey("members") and jsonCommunity["members"].kind != JNull:
# memberInfo is empty for now
for memberPubKey, memeberInfo in jsonCommunity{"members"}:
result.members.add(memberPubKey)

View File

@ -0,0 +1,6 @@
{.used.}
type CommunityCategory* = object
id*: string
name*: string
position*: int

View File

@ -0,0 +1,21 @@
{.used.}
import json
type CommunityMembershipRequest* = object
id*: string
publicKey*: string
chatId*: string
communityId*: string
state*: int
our*: string
proc toCommunityMembershipRequest*(jsonCommunityMembershipRequest: JsonNode): CommunityMembershipRequest =
result = CommunityMembershipRequest(
id: jsonCommunityMembershipRequest{"id"}.getStr,
publicKey: jsonCommunityMembershipRequest{"publicKey"}.getStr,
chatId: jsonCommunityMembershipRequest{"chatId"}.getStr,
state: jsonCommunityMembershipRequest{"state"}.getInt,
communityId: jsonCommunityMembershipRequest{"communityId"}.getStr,
our: jsonCommunityMembershipRequest{"our"}.getStr,
)

View File

@ -0,0 +1,6 @@
{.used.}
type DerivedAccount* = object
publicKey*: string
address*: string
derivationPath*: string

52
status/types/fleet.nim Normal file
View File

@ -0,0 +1,52 @@
{.used.}
import json, typetraits, tables, sequtils
type
Fleet* {.pure.} = enum
Prod = "eth.prod",
Staging = "eth.staging",
Test = "eth.test",
WakuV2Prod = "wakuv2.prod"
WakuV2Test = "wakuv2.test"
FleetNodes* {.pure.} = enum
Bootnodes = "boot",
Mailservers = "mail",
Rendezvous = "rendezvous",
Whisper = "whisper",
Waku = "waku"
FleetMeta* = object
hostname*: string
timestamp*: uint64
FleetConfig* = object
fleet*: Table[string, Table[string, Table[string, string]]]
meta*: FleetMeta
proc toFleetConfig*(jsonString: string): FleetConfig =
let fleetJson = jsonString.parseJSON
result.meta.hostname = fleetJson["meta"]["hostname"].getStr
result.meta.timestamp = fleetJson["meta"]["timestamp"].getBiggestInt.uint64
result.fleet = initTable[string, Table[string, Table[string, string]]]()
for fleet in fleetJson["fleets"].keys():
result.fleet[fleet] = initTable[string, Table[string, string]]()
for nodes in fleetJson["fleets"][fleet].keys():
result.fleet[fleet][nodes] = initTable[string, string]()
for server in fleetJson["fleets"][fleet][nodes].keys():
result.fleet[fleet][nodes][server] = fleetJson["fleets"][fleet][nodes][server].getStr
proc getNodes*(self: FleetConfig, fleet: Fleet, nodeType: FleetNodes = FleetNodes.Bootnodes): seq[string] =
if not self.fleet[$fleet].hasKey($nodeType): return
result = toSeq(self.fleet[$fleet][$nodeType].values)
proc getMailservers*(self: FleetConfig, fleet: Fleet): Table[string, string] =
if not self.fleet[$fleet].hasKey($FleetNodes.Mailservers):
result = initTable[string,string]()
return
result = self.fleet[$fleet][$FleetNodes.Mailservers]

View File

@ -0,0 +1,9 @@
{.used.}
type GasPricePrediction* = object
safeLow*: float
standard*: float
fast*: float
fastest*: float
currentBaseFee*: float
recommendedBaseFee*: float

View File

@ -0,0 +1,6 @@
{.used.}
type
IdentityImage* = ref object
thumbnail*: string
large*: string

View File

@ -0,0 +1,14 @@
import json
type Installation* = ref object
installationId*: string
name*: string
deviceType*: string
enabled*: bool
isUserDevice*: bool
proc toInstallation*(jsonInstallation: JsonNode): Installation =
result = Installation(installationid: jsonInstallation{"id"}.getStr, enabled: jsonInstallation{"enabled"}.getBool, name: "", deviceType: "", isUserDevice: false)
if jsonInstallation["metadata"].kind != JNull:
result.name = jsonInstallation["metadata"]["name"].getStr
result.deviceType = jsonInstallation["metadata"]["deviceType"].getStr

192
status/types/message.nim Normal file
View File

@ -0,0 +1,192 @@
{.used.}
import json, strutils, sequtils, sugar, chronicles
import json_serialization
import ../utils
import ../wallet/account
import ../libstatus/accounts/constants as constants
import ../libstatus/wallet as status_wallet
import ../libstatus/settings as status_settings
import ../libstatus/tokens as status_tokens
import ../libstatus/eth/contracts as status_contracts
import web3/conversions
from ../utils import parseAddress, wei2Eth
import setting, network
include message_command_parameters
include message_reaction
include message_text_item
type ContentType* {.pure.} = enum
FetchMoreMessagesButton = -2
ChatIdentifier = -1,
Unknown = 0,
Message = 1,
Sticker = 2,
Status = 3,
Emoji = 4,
Transaction = 5,
Group = 6,
Image = 7,
Audio = 8
Community = 9
Gap = 10
Edit = 11
type Message* = object
alias*: string
userName*: string
localName*: string
chatId*: string
clock*: int
gapFrom*: int
gapTo*: int
commandParameters*: CommandParameters
contentType*: ContentType
ensName*: string
fromAuthor*: string
id*: string
identicon*: string
lineCount*: int
localChatId*: string
messageType*: string # ???
parsedText*: seq[TextItem]
# quotedMessage: # ???
replace*: string
responseTo*: string
rtl*: bool # ???
seen*: bool # ???
sticker*: string
stickerPackId*: int
text*: string
timestamp*: string
editedAt*: string
whisperTimestamp*: string
isCurrentUser*: bool
stickerHash*: string
outgoingStatus*: string
linkUrls*: string
image*: string
audio*: string
communityId*: string
audioDurationMs*: int
hasMention*: bool
isPinned*: bool
pinnedBy*: string
deleted*: bool
proc `$`*(self: Message): string =
result = fmt"Message(id:{self.id}, chatId:{self.chatId}, clock:{self.clock}, from:{self.fromAuthor}, contentType:{self.contentType})"
proc currentUserWalletContainsAddress(address: string): bool =
if (address.len == 0):
return false
let accounts = status_wallet.getWalletAccounts()
for acc in accounts:
if (acc.address == address):
return true
return false
proc toMessage*(jsonMsg: JsonNode): Message =
let publicChatKey = status_settings.getSetting[string](Setting.PublicKey, "0x0")
var contentType: ContentType
try:
contentType = ContentType(jsonMsg{"contentType"}.getInt)
except:
warn "Unknown content type received", type = jsonMsg{"contentType"}.getInt
contentType = ContentType.Message
var message = Message(
alias: jsonMsg{"alias"}.getStr,
userName: "",
localName: "",
chatId: jsonMsg{"localChatId"}.getStr,
clock: jsonMsg{"clock"}.getInt,
contentType: contentType,
ensName: jsonMsg{"ensName"}.getStr,
fromAuthor: jsonMsg{"from"}.getStr,
id: jsonMsg{"id"}.getStr,
identicon: jsonMsg{"identicon"}.getStr,
lineCount: jsonMsg{"lineCount"}.getInt,
localChatId: jsonMsg{"localChatId"}.getStr,
messageType: jsonMsg{"messageType"}.getStr,
replace: jsonMsg{"replace"}.getStr,
editedAt: $jsonMsg{"editedAt"}.getInt,
responseTo: jsonMsg{"responseTo"}.getStr,
rtl: jsonMsg{"rtl"}.getBool,
seen: jsonMsg{"seen"}.getBool,
text: jsonMsg{"text"}.getStr,
timestamp: $jsonMsg{"timestamp"}.getInt,
whisperTimestamp: $jsonMsg{"whisperTimestamp"}.getInt,
outgoingStatus: $jsonMsg{"outgoingStatus"}.getStr,
isCurrentUser: publicChatKey == jsonMsg{"from"}.getStr,
stickerHash: "",
stickerPackId: -1,
parsedText: @[],
linkUrls: "",
image: $jsonMsg{"image"}.getStr,
audio: $jsonMsg{"audio"}.getStr,
communityId: $jsonMsg{"communityId"}.getStr,
audioDurationMs: jsonMsg{"audioDurationMs"}.getInt,
deleted: jsonMsg{"deleted"}.getBool,
hasMention: false
)
if contentType == ContentType.Gap:
message.gapFrom = jsonMsg["gapParameters"]["from"].getInt
message.gapTo = jsonMsg["gapParameters"]["to"].getInt
if jsonMsg.contains("parsedText") and jsonMsg{"parsedText"}.kind != JNull:
for text in jsonMsg{"parsedText"}:
message.parsedText.add(text.toTextItem)
message.linkUrls = concat(message.parsedText.map(t => t.children.filter(c => c.textType == "link")))
.filter(t => t.destination.startsWith("http") or t.destination.startsWith("statusim://"))
.map(t => t.destination)
.join(" ")
if message.contentType == ContentType.Sticker:
message.stickerHash = jsonMsg["sticker"]["hash"].getStr
message.stickerPackId = jsonMsg["sticker"]["pack"].getInt
if message.contentType == ContentType.Transaction:
let
allContracts = getErc20Contracts().concat(getCustomTokens())
ethereum = newErc20Contract("Ethereum", Network.Mainnet, parseAddress(constants.ZERO_ADDRESS), "ETH", 18, true)
tokenAddress = jsonMsg["commandParameters"]["contract"].getStr
tokenContract = if tokenAddress == "": ethereum else: allContracts.getErc20ContractByAddress(parseAddress(tokenAddress))
tokenContractStr = if tokenContract == nil: "{}" else: $(Json.encode(tokenContract))
var weiStr = if tokenContract == nil: "0" else: wei2Eth(jsonMsg["commandParameters"]["value"].getStr, tokenContract.decimals)
weiStr.trimZeros()
# TODO find a way to use json_seralization for this. When I try, I get an error
message.commandParameters = CommandParameters(
id: jsonMsg["commandParameters"]["id"].getStr,
fromAddress: jsonMsg["commandParameters"]["from"].getStr,
address: jsonMsg["commandParameters"]["address"].getStr,
contract: tokenContractStr,
value: weiStr,
transactionHash: jsonMsg["commandParameters"]["transactionHash"].getStr,
commandState: jsonMsg["commandParameters"]["commandState"].getInt,
signature: jsonMsg["commandParameters"]["signature"].getStr
)
# This is kind of a workaround in case we're processing a transaction message. The reason for
# that is a message where a recipient accepted to share his address with sender. In that message
# a recipient's public key is set as a "from" property of a "Message" object and we cannot
# determine which of two users has initiated transaction actually.
#
# To overcome this we're checking if the "from" address from the "commandParameters" object of
# the "Message" is contained as an address in the wallet of logged in user. If yes, means that
# currently logged in user has initiated a transaction (he is a sender), otherwise currently
# logged in user is a recipient.
message.isCurrentUser = currentUserWalletContainsAddress(message.commandParameters.fromAddress)
message.hasMention = concat(message.parsedText.map(
t => t.children.filter(
c => c.textType == "mention" and c.literal == publicChatKey))).len > 0
result = message

View File

@ -0,0 +1,16 @@
{.used.}
import strformat
type CommandParameters* = object
id*: string
fromAddress*: string
address*: string
contract*: string
value*: string
transactionHash*: string
commandState*: int
signature*: string
proc `$`*(self: CommandParameters): string =
result = fmt"CommandParameters(id:{self.id}, fromAddr:{self.fromAddress}, addr:{self.address}, contract:{self.contract}, value:{self.value}, transactionHash:{self.transactionHash}, commandState:{self.commandState}, signature:{self.signature})"

View File

@ -0,0 +1,21 @@
{.used.}
import json
type Reaction* = object
id*: string
chatId*: string
fromAccount*: string
messageId*: string
emojiId*: int
retracted*: bool
proc toReaction*(jsonReaction: JsonNode): Reaction =
result = Reaction(
id: jsonReaction{"id"}.getStr,
chatId: jsonReaction{"chatId"}.getStr,
fromAccount: jsonReaction{"from"}.getStr,
messageId: jsonReaction{"messageId"}.getStr,
emojiId: jsonReaction{"emojiId"}.getInt,
retracted: jsonReaction{"retracted"}.getBool
)

View File

@ -0,0 +1,25 @@
{.used.}
import json, strutils
type TextItem* = object
textType*: string
children*: seq[TextItem]
literal*: string
destination*: string
proc toTextItem*(jsonText: JsonNode): TextItem =
result = TextItem(
literal: jsonText{"literal"}.getStr,
textType: jsonText{"type"}.getStr,
destination: jsonText{"destination"}.getStr,
children: @[]
)
if (result.literal.startsWith("statusim://")):
result.textType = "link"
# TODO isolate the link only
result.destination = result.literal
if jsonText.hasKey("children") and jsonText["children"].kind != JNull:
for child in jsonText["children"]:
result.children.add(child.toTextItem)

View File

@ -0,0 +1,12 @@
{.used.}
import json_serialization
import ../libstatus/accounts/constants
include derived_account
type MultiAccounts* = object
whisper* {.serializedFieldName(PATH_WHISPER).}: DerivedAccount
walletRoot* {.serializedFieldName(PATH_WALLET_ROOT).}: DerivedAccount
defaultWallet* {.serializedFieldName(PATH_DEFAULT_WALLET).}: DerivedAccount
eip1581* {.serializedFieldName(PATH_EIP_1581).}: DerivedAccount

15
status/types/network.nim Normal file
View File

@ -0,0 +1,15 @@
{.used.}
include node_config
include network_details
include upstream_config
type
Network* {.pure.} = enum
Mainnet = "mainnet_rpc",
Testnet = "testnet_rpc",
Rinkeby = "rinkeby_rpc",
Goerli = "goerli_rpc",
XDai = "xdai_rpc",
Poa = "poa_rpc",
Other = "other"

View File

@ -0,0 +1,12 @@
{.used.}
import json_serialization
import node_config
type
NetworkDetails* = ref object
id*: string
name*: string
etherscanLink* {.serializedFieldName("etherscan-link").}: string
config*: NodeConfig

View File

@ -0,0 +1,11 @@
{.used.}
import json_serialization
import upstream_config
type
NodeConfig* = ref object
networkId* {.serializedFieldName("NetworkId").}: int
dataDir* {.serializedFieldName("DataDir").}: string
upstreamConfig* {.serializedFieldName("UpstreamConfig").}: UpstreamConfig

View File

@ -0,0 +1,46 @@
{.used.}
import json
import ../../eventemitter
type
OsNotificationType* {.pure.} = enum
NewContactRequest = 1,
AcceptedContactRequest,
JoinCommunityRequest,
AcceptedIntoCommunity,
RejectedByCommunity,
NewMessage
OsNotificationDetails* = object
notificationType*: OsNotificationType
communityId*: string
channelId*: string
messageId*: string
type
OsNotificationsArgs* = ref object of Args
details*: OsNotificationDetails
proc toOsNotificationDetails*(json: JsonNode): OsNotificationDetails =
if (not (json.contains("notificationType") and
json.contains("communityId") and
json.contains("channelId") and
json.contains("messageId"))):
return OsNotificationDetails()
return OsNotificationDetails(
notificationType: json{"notificationType"}.getInt.OsNotificationType,
communityId: json{"communityId"}.getStr,
channelId: json{"channelId"}.getStr,
messageId: json{"messageId"}.getStr
)
proc toJsonNode*(self: OsNotificationDetails): JsonNode =
result = %* {
"notificationType": self.notificationType.int,
"communityId": self.communityId,
"channelId": self.channelId,
"messageId": self.messageId
}

View File

@ -0,0 +1,8 @@
{.used.}
type PendingTransactionType* {.pure.} = enum
RegisterENS = "RegisterENS",
SetPubKey = "SetPubKey",
ReleaseENS = "ReleaseENS",
BuyStickerPack = "BuyStickerPack"
WalletTransfer = "WalletTransfer"

49
status/types/profile.nim Normal file
View File

@ -0,0 +1,49 @@
{.used.}
import json, strformat
import identity_image
export identity_image
type Profile* = ref object
id*, alias*, username*, identicon*, address*, ensName*, localNickname*: string
ensVerified*: bool
messagesFromContactsOnly*: bool
sendUserStatus*: bool
currentUserStatus*: int
identityImage*: IdentityImage
appearance*: int
systemTags*: seq[string]
proc `$`*(self: Profile): string =
return fmt"Profile(id:{self.id}, username:{self.username})"
proc toProfileModel*(profile: JsonNode): Profile =
var systemTags: seq[string] = @[]
if profile["systemTags"].kind != JNull:
systemTags = profile["systemTags"].to(seq[string])
result = Profile(
id: profile["id"].str,
username: profile["alias"].str,
identicon: profile["identicon"].str,
identityImage: IdentityImage(),
address: profile["id"].str,
alias: profile["alias"].str,
ensName: "",
ensVerified: profile["ensVerified"].getBool,
appearance: 0,
systemTags: systemTags
)
if profile.hasKey("name"):
result.ensName = profile["name"].str
if profile.hasKey("localNickname"):
result.localNickname = profile["localNickname"].str
if profile.hasKey("images") and profile["images"].kind != JNull:
if profile["images"].hasKey("thumbnail"):
result.identityImage.thumbnail = profile["images"]["thumbnail"]["uri"].str
if profile["images"].hasKey("large"):
result.identityImage.large = profile["images"]["large"]["uri"].str

View File

@ -0,0 +1,13 @@
{.used.}
import json
type RemovedMessage* = object
chatId*: string
messageId*: string
proc toRemovedMessage*(jsonRemovedMessage: JsonNode): RemovedMessage =
result = RemovedMessage(
chatId: jsonRemovedMessage{"chatId"}.getStr,
messageId: jsonRemovedMessage{"messageId"}.getStr,
)

View File

@ -0,0 +1,29 @@
{.used.}
type
RpcError* = ref object
code*: int
message*: string
type
RpcResponse* = ref object
jsonrpc*: string
result*: string
id*: int
error*: RpcError
# TODO: replace all RpcResponse and RpcResponseTyped occurances with a generic
# form of RpcReponse. IOW, rename RpceResponseTyped*[T] to RpcResponse*[T] and
# remove RpcResponse.
type
RpcResponseTyped*[T] = object
jsonrpc*: string
result*: T
id*: int
error*: RpcError
type
StatusGoException* = object of CatchableError
type
RpcException* = object of CatchableError

31
status/types/setting.nim Normal file
View File

@ -0,0 +1,31 @@
{.used.}
type
Setting* {.pure.} = enum
Appearance = "appearance",
Bookmarks = "bookmarks",
Currency = "currency"
EtherscanLink = "etherscan-link"
InstallationId = "installation-id"
MessagesFromContactsOnly = "messages-from-contacts-only"
Mnemonic = "mnemonic"
Networks_Networks = "networks/networks"
Networks_CurrentNetwork = "networks/current-network"
NodeConfig = "node-config"
PublicKey = "public-key"
DappsAddress = "dapps-address"
Stickers_PacksInstalled = "stickers/packs-installed"
Stickers_Recent = "stickers/recent-stickers"
Gifs_Recent = "gifs/recent-gifs"
Gifs_Favorite = "gifs/favorite-gifs"
WalletRootAddress = "wallet-root-address"
LatestDerivedPath = "latest-derived-path"
PreferredUsername = "preferred-name"
Usernames = "usernames"
SigningPhrase = "signing-phrase"
Fleet = "fleet"
VisibleTokens = "wallet/visible-tokens"
PinnedMailservers = "pinned-mailservers"
WakuBloomFilterMode = "waku-bloom-filter-mode"
SendUserStatus = "send-status-updates?"
CurrentUserStatus = "current-user-status"

View File

@ -0,0 +1,24 @@
{.used.}
import json
type StatusUpdateType* {.pure.}= enum
Unknown = 0,
Online = 1,
DoNotDisturb = 2
type StatusUpdate* = object
publicKey*: string
statusType*: StatusUpdateType
clock*: uint64
text*: string
proc toStatusUpdate*(jsonStatusUpdate: JsonNode): StatusUpdate =
let statusTypeInt = jsonStatusUpdate{"statusType"}.getInt
let statusType: StatusUpdateType = if statusTypeInt >= ord(low(StatusUpdateType)) or statusTypeInt <= ord(high(StatusUpdateType)): StatusUpdateType(statusTypeInt) else: StatusUpdateType.Unknown
result = StatusUpdate(
publicKey: jsonStatusUpdate{"publicKey"}.getStr,
statusType: statusType,
clock: uint64(jsonStatusUpdate{"clock"}.getBiggestInt),
text: jsonStatusUpdate{"text"}.getStr
)

32
status/types/sticker.nim Normal file
View File

@ -0,0 +1,32 @@
{.used.}
import json, options, typetraits
import web3/ethtypes, json_serialization, stint
type Sticker* = object
hash*: string
packId*: int
type StickerPack* = object
author*: string
id*: int
name*: string
price*: Stuint[256]
preview*: string
stickers*: seq[Sticker]
thumbnail*: string
proc `%`*(stuint256: Stuint[256]): JsonNode =
newJString($stuint256)
proc readValue*(reader: var JsonReader, value: var Stuint[256])
{.raises: [IOError, SerializationError, Defect].} =
try:
let strVal = reader.readValue(string)
value = strVal.parse(Stuint[256])
except:
try:
let intVal = reader.readValue(int)
value = intVal.stuint(256)
except:
raise newException(SerializationError, "Expected string or int representation of Stuint[256]")

View File

@ -0,0 +1,30 @@
{.used.}
import strutils
include pending_transaction_type
type
Transaction* = ref object
id*: string
typeValue*: string
address*: string
blockNumber*: string
blockHash*: string
contract*: string
timestamp*: string
gasPrice*: string
gasLimit*: string
gasUsed*: string
nonce*: string
txStatus*: string
value*: string
fromAddress*: string
to*: string
proc cmpTransactions*(x, y: Transaction): int =
# Sort proc to compare transactions from a single account.
# Compares first by block number, then by nonce
result = cmp(x.blockNumber.parseHexInt, y.blockNumber.parseHexInt)
if result == 0:
result = cmp(x.nonce, y.nonce)

View File

@ -0,0 +1,8 @@
{.used.}
import json_serialization
type
UpstreamConfig* = ref object
enabled* {.serializedFieldName("Enabled").}: bool
url* {.serializedFieldName("URL").}: string

44
status/updates.nim Normal file
View File

@ -0,0 +1,44 @@
import ens, provider
import stew/byteutils
from base32 import nil
import chronicles, httpclient, net
import nbaser, strutils
import semver
import constants
type
VersionInfo* = object
version*: string
url*: string
proc getLatestVersion*(): VersionInfo =
let contentHash = contenthash(APP_UPDATES_ENS)
if contentHash == "":
raise newException(ValueError, "ENS does not have a content hash")
var url: string = ""
let decodedHash = contentHash.decodeENSContentHash()
case decodedHash[0]:
of ENSType.IPFS:
let base32Hash = base32.encode(string.fromBytes(base58.decode(decodedHash[1]))).toLowerAscii().replace("=", "")
url = "https://" & base32Hash & IPFS_GATEWAY
of ENSType.SWARM:
url = "https://" & SWARM_GATEWAY & "/bzz:/" & decodedHash[1]
of ENSType.IPNS:
url = "https://" & decodedHash[1]
else:
warn "Unknown content for", contentHash
raise newException(ValueError, "Unknown content for " & contentHash)
# Read version from folder
let secureSSLContext = newContext()
let client = newHttpClient(sslContext = secureSSLContext, timeout = CHECK_VERSION_TIMEOUT_MS)
result.version = client.getContent(url & "/VERSION").strip()
result.url = url
proc isNewer*(currentVersion, versionToCheck: string): bool =
let lastVersion = parseVersion(versionToCheck)
let currVersion = parseVersion(currentVersion)
result = lastVersion > currVersion

178
status/utils.nim Normal file
View File

@ -0,0 +1,178 @@
import json, random, strutils, strformat, tables, chronicles, unicode, times
from sugar import `=>`, `->`
import stint
from times import getTime, toUnix, nanosecond
import libstatus/accounts/signing_phrases
from web3 import Address, fromHex
import web3/ethhexstrings
proc getTimelineChatId*(pubKey: string = ""): string =
if pubKey == "":
return "@timeline70bd746ddcc12beb96b2c9d572d0784ab137ffc774f5383e50585a932080b57cca0484b259e61cecbaa33a4c98a300a"
else:
return "@" & pubKey
proc isWakuEnabled(): bool =
true # TODO:
proc prefix*(methodName: string, isExt:bool = true): string =
result = if isWakuEnabled(): "waku" else: "shh"
result = result & (if isExt: "ext_" else: "_")
result = result & methodName
proc isOneToOneChat*(chatId: string): bool =
result = chatId.startsWith("0x") # There is probably a better way to do this
proc keys*(obj: JsonNode): seq[string] =
result = newSeq[string]()
for k, _ in obj:
result.add k
proc generateSigningPhrase*(count: int): string =
let now = getTime()
var rng = initRand(now.toUnix * 1000000000 + now.nanosecond)
var phrases: seq[string] = @[]
for i in 1..count:
phrases.add(rng.sample(signing_phrases.phrases))
result = phrases.join(" ")
proc handleRPCErrors*(response: string) =
let parsedReponse = parseJson(response)
if (parsedReponse.hasKey("error")):
raise newException(ValueError, parsedReponse["error"]["message"].str)
proc toStUInt*[bits: static[int]](flt: float, T: typedesc[StUint[bits]]): T =
var stringValue = fmt"{flt:<.0f}"
stringValue.removeSuffix('.')
if (flt >= 0):
result = parse($stringValue, StUint[bits])
else:
result = parse("0", StUint[bits])
proc toUInt256*(flt: float): UInt256 =
toStUInt(flt, StUInt[256])
proc toUInt64*(flt: float): StUInt[64] =
toStUInt(flt, StUInt[64])
proc eth2Wei*(eth: float, decimals: int = 18): UInt256 =
let weiValue = eth * parseFloat(alignLeft("1", decimals + 1, '0'))
weiValue.toUInt256
proc gwei2Wei*(gwei: float): UInt256 =
eth2Wei(gwei, 9)
proc wei2Eth*(input: Stuint[256], decimals: int = 18): string =
var one_eth = u256(10).pow(decimals) # fromHex(Stuint[256], "DE0B6B3A7640000")
var (eth, remainder) = divmod(input, one_eth)
let leading_zeros = "0".repeat(($one_eth).len - ($remainder).len - 1)
fmt"{eth}.{leading_zeros}{remainder}"
proc wei2Eth*(input: string, decimals: int): string =
try:
var input256: Stuint[256]
if input.contains("e+"): # we have a js string BN, ie 1e+21
let
inputSplit = input.split("e+")
whole = inputSplit[0].u256
remainder = u256(10).pow(inputSplit[1].parseInt)
input256 = whole * remainder
else:
input256 = input.u256
result = wei2Eth(input256, decimals)
except Exception as e:
error "Error parsing this wei value", input, msg=e.msg
result = "0"
proc first*(jArray: JsonNode, fieldName, id: string): JsonNode =
if jArray == nil:
return nil
if jArray.kind != JArray:
raise newException(ValueError, "Parameter 'jArray' is a " & $jArray.kind & ", but must be a JArray")
for child in jArray.getElems:
if child{fieldName}.getStr.toLower == id.toLower:
return child
proc any*(jArray: JsonNode, fieldName, id: string): bool =
if jArray == nil:
return false
result = false
for child in jArray.getElems:
if child{fieldName}.getStr.toLower == id.toLower:
return true
proc isEmpty*(a: JsonNode): bool =
case a.kind:
of JObject: return a.fields.len == 0
of JArray: return a.elems.len == 0
of JString: return a.str == ""
of JNull: return true
else:
return false
proc find*[T](s: seq[T], pred: proc(x: T): bool {.closure.}): T {.inline.} =
let results = s.filter(pred)
if results.len == 0:
return default(type(T))
result = results[0]
proc find*[T](s: seq[T], pred: proc(x: T): bool {.closure.}, found: var bool): T {.inline.} =
let results = s.filter(pred)
if results.len == 0:
found = false
return default(type(T))
result = results[0]
found = true
proc parseAddress*(strAddress: string): Address =
fromHex(Address, strAddress)
proc isAddress*(strAddress: string): bool =
try:
discard parseAddress(strAddress)
except:
return false
return true
proc validateTransactionInput*(from_addr, to_addr, assetAddress, value, gas, gasPrice, data, uuid: string) =
if not isAddress(from_addr): raise newException(ValueError, "from_addr is not a valid ETH address")
if not isAddress(to_addr): raise newException(ValueError, "to_addr is not a valid ETH address")
if parseFloat(value) < 0: raise newException(ValueError, "value should be a number >= 0")
if parseInt(gas) <= 0: raise newException(ValueError, "gas should be a number > 0")
if parseFloat(gasPrice) <= 0: raise newException(ValueError, "gasPrice should be a number > 0")
if uuid.isEmptyOrWhitespace(): raise newException(ValueError, "uuid is required")
if assetAddress != "": # If a token is being used
if not isAddress(assetAddress): raise newException(ValueError, "assetAddress is not a valid ETH address")
if assetAddress == "0x0000000000000000000000000000000000000000": raise newException(ValueError, "assetAddress requires a valid token address")
if data != "": # If data is being used
if not validate(HexDataStr(data)): raise newException(ValueError, "data should contain a valid hex string")
proc hex2Time*(hex: string): Time =
# represents the time since 1970-01-01T00:00:00Z
fromUnix(fromHex[int64](hex))
proc hex2LocalDateTime*(hex: string): DateTime =
# Convert hex time (since 1970-01-01T00:00:00Z) into a DateTime using the
# local timezone.
hex.hex2Time.local
proc isUnique*[T](key: T, existingKeys: var seq[T]): bool =
# If the key doesn't exist in the existingKeys seq, add it and return true.
# Otherwise, the key already existed, so return false.
# Can be used to deduplicate sequences with `deduplicate[T]`.
if not existingKeys.contains(key):
existingKeys.add key
return true
return false
proc deduplicate*[T](txs: var seq[T], key: (T) -> string) =
var existingKeys: seq[string] = @[]
txs.keepIf(tx => tx.key().isUnique(existingKeys))

17
status/utils/cache.nim Normal file
View File

@ -0,0 +1,17 @@
import tables, times
type ValueTime* = ref object
value*: string
timestamp*: DateTime
type CachedValues* = Table[string, ValueTime]
proc newCachedValues*(): CachedValues = initTable[string, ValueTime]()
proc isCached*(self: CachedValues, cacheKey: string, duration=initDuration(minutes = 5)): bool =
self.hasKey(cacheKey) and ((self[cacheKey].timestamp + duration) >= now())
proc cacheValue*(self: var CachedValues, cacheKey: string, value: string) =
self[cacheKey] = ValueTime(value: value, timestamp: now())
proc get*(self: var CachedValues, cacheKey: string): string = self[cacheKey].value

View File

@ -0,0 +1,33 @@
import json
template getProp(obj: JsonNode, prop: string, value: var typedesc[int]): bool =
var success = false
if (obj.kind == JObject and obj.contains(prop)):
value = obj[prop].getInt
success = true
success
template getProp(obj: JsonNode, prop: string, value: var typedesc[string]): bool =
var success = false
if (obj.kind == JObject and obj.contains(prop)):
value = obj[prop].getStr
success = true
success
template getProp(obj: JsonNode, prop: string, value: var typedesc[float]): bool =
var success = false
if (obj.kind == JObject and obj.contains(prop)):
value = obj[prop].getFloat
success = true
success
template getProp(obj: JsonNode, prop: string, value: var typedesc[JsonNode]): bool =
var success = false
if (obj.kind == JObject and obj.contains(prop)):
value = obj[prop]
success = true
success

408
status/wallet.nim Normal file
View File

@ -0,0 +1,408 @@
import json, strformat, strutils, chronicles, sequtils, sugar, httpclient, tables, net
import json_serialization, stint
from web3/ethtypes import Address, EthSend, Quantity
from web3/conversions import `$`
from libstatus/core import getBlockByNumber
import libstatus/accounts as status_accounts
import libstatus/tokens as status_tokens
import libstatus/settings as status_settings
import libstatus/wallet as status_wallet
import libstatus/accounts/constants as constants
import libstatus/eth/[eth, contracts]
from libstatus/core import getBlockByNumber
from utils as libstatus_utils import eth2Wei, gwei2Wei, first, toUInt64, parseAddress
import wallet/[balance_manager, collectibles]
import wallet/account as wallet_account
import transactions
import ../eventemitter
import options
import ./types/[account, transaction, network, setting, gas_prediction, rpc_response]
export wallet_account, collectibles
export Transaction
logScope:
topics = "wallet-model"
proc confirmed*(self:PendingTransactionType):string =
result = "transaction:" & $self
type TransactionMinedArgs* = ref object of Args
data*: string
transactionHash*: string
success*: bool
revertReason*: string # TODO: possible to get revert reason in here?
type WalletModel* = ref object
events*: EventEmitter
accounts*: seq[WalletAccount]
defaultCurrency*: string
tokens*: seq[Erc20Contract]
totalBalance*: float
proc getDefaultCurrency*(self: WalletModel): string
proc calculateTotalFiatBalance*(self: WalletModel)
proc newWalletModel*(events: EventEmitter): WalletModel =
result = WalletModel()
result.accounts = @[]
result.tokens = @[]
result.events = events
result.defaultCurrency = ""
result.totalBalance = 0.0
proc initEvents*(self: WalletModel) =
self.events.on("currencyChanged") do(e: Args):
self.defaultCurrency = self.getDefaultCurrency()
for account in self.accounts:
updateBalance(account, self.getDefaultCurrency())
self.calculateTotalFiatBalance()
self.events.emit("accountsUpdated", Args())
self.events.on("newAccountAdded") do(e: Args):
self.calculateTotalFiatBalance()
proc delete*(self: WalletModel) =
discard
proc buildTokenTransaction(source, to, assetAddress: Address, value: float, transfer: var Transfer, contract: var Erc20Contract, gas = "", gasPrice = ""): EthSend =
contract = getErc20Contract(assetAddress)
if contract == nil:
raise newException(ValueError, fmt"Could not find ERC-20 contract with address '{assetAddress}' for the current network")
transfer = Transfer(to: to, value: eth2Wei(value, contract.decimals))
transactions.buildTokenTransaction(source, assetAddress, gas, gasPrice)
proc getKnownTokenContract*(self: WalletModel, address: Address): Erc20Contract =
getErc20Contracts().concat(getCustomTokens()).getErc20ContractByAddress(address)
proc estimateGas*(self: WalletModel, source, to, value, data: string, success: var bool): string =
var tx = transactions.buildTransaction(
parseAddress(source),
eth2Wei(parseFloat(value), 18),
data = data
)
tx.to = parseAddress(to).some
result = eth.estimateGas(tx, success)
proc getTransactionReceipt*(self: WalletModel, transactionHash: string): JsonNode =
result = status_wallet.getTransactionReceipt(transactionHash).parseJSON()["result"]
proc confirmTransactionStatus(self: WalletModel, pendingTransactions: JsonNode, blockNumber: int) =
for trx in pendingTransactions.getElems():
let transactionReceipt = self.getTransactionReceipt(trx["hash"].getStr)
if transactionReceipt.kind != JNull:
status_wallet.deletePendingTransaction(trx["hash"].getStr)
let ev = TransactionMinedArgs(
data: trx["additionalData"].getStr,
transactionHash: trx["hash"].getStr,
success: transactionReceipt{"status"}.getStr == "0x1",
revertReason: ""
)
self.events.emit(parseEnum[PendingTransactionType](trx["type"].getStr).confirmed, ev)
proc getLatestBlockNumber*(self: WalletModel): int =
let response = getBlockByNumber("latest").parseJson()
if not response.hasKey("result"):
return -1
return parseInt($fromHex(Stuint[256], response["result"]["number"].getStr))
proc checkPendingTransactions*(self: WalletModel) =
let latestBlockNumber = self.getLatestBlockNumber()
if latestBlockNumber == -1:
return
let pendingTransactions = status_wallet.getPendingTransactions()
if (pendingTransactions != ""):
self.confirmTransactionStatus(pendingTransactions.parseJson{"result"}, latestBlockNumber)
proc checkPendingTransactions*(self: WalletModel, address: string, blockNumber: int) =
self.confirmTransactionStatus(status_wallet.getPendingOutboundTransactionsByAddress(address).parseJson["result"], blockNumber)
proc estimateTokenGas*(self: WalletModel, source, to, assetAddress, value: string, success: var bool): string =
var
transfer: Transfer
contract: Erc20Contract
tx = buildTokenTransaction(
parseAddress(source),
parseAddress(to),
parseAddress(assetAddress),
parseFloat(value),
transfer,
contract
)
result = contract.methods["transfer"].estimateGas(tx, transfer, success)
proc sendTransaction*(source, to, value, gas, gasPrice, password: string, success: var bool, data = ""): string =
var tx = transactions.buildTransaction(
parseAddress(source),
eth2Wei(parseFloat(value), 18), gas, gasPrice, data
)
if to != "":
tx.to = parseAddress(to).some
result = eth.sendTransaction(tx, password, success)
if success:
trackPendingTransaction(result, $source, $to, PendingTransactionType.WalletTransfer, "")
proc sendTokenTransaction*(source, to, assetAddress, value, gas, gasPrice, password: string, success: var bool): string =
var
transfer: Transfer
contract: Erc20Contract
tx = buildTokenTransaction(
parseAddress(source),
parseAddress(to),
parseAddress(assetAddress),
parseFloat(value),
transfer,
contract,
gas,
gasPrice
)
result = contract.methods["transfer"].send(tx, transfer, password, success)
if success:
trackPendingTransaction(result, $source, $to, PendingTransactionType.WalletTransfer, "")
proc getDefaultCurrency*(self: WalletModel): string =
# TODO: this should come from a model? It is going to be used too in the
# profile section and ideally we should not call the settings more than once
status_settings.getSetting[string](Setting.Currency, "usd")
# TODO: This needs to be removed or refactored so that test tokens are shown
# when on testnet https://github.com/status-im/nim-status-client/issues/613.
proc getStatusToken*(self: WalletModel): string =
var
token = Asset()
erc20Contract = getSntContract()
token.name = erc20Contract.name
token.symbol = erc20Contract.symbol
token.address = $erc20Contract.address
result = $(%token)
proc setDefaultCurrency*(self: WalletModel, currency: string) =
discard status_settings.saveSetting(Setting.Currency, currency)
self.events.emit("currencyChanged", CurrencyArgs(currency: currency))
proc generateAccountConfiguredAssets*(self: WalletModel, accountAddress: string): seq[Asset] =
var assets: seq[Asset] = @[]
var asset = Asset(name:"Ethereum", symbol: "ETH", value: "0.0", fiatBalanceDisplay: "0.0", accountAddress: accountAddress)
assets.add(asset)
for token in self.tokens:
var symbol = token.symbol
var existingToken = Asset(name: token.name, symbol: symbol, value: fmt"0.0", fiatBalanceDisplay: "$0.0", accountAddress: accountAddress, address: $token.address)
assets.add(existingToken)
assets
proc populateAccount*(self: WalletModel, walletAccount: var WalletAccount, balance: string, refreshCache: bool = false) =
var assets: seq[Asset] = self.generateAccountConfiguredAssets(walletAccount.address)
walletAccount.balance = none[string]()
walletAccount.assetList = assets
walletAccount.realFiatBalance = none[float]()
proc update*(self: WalletModel, address: string, ethBalance: string, tokens: JsonNode) =
for account in self.accounts:
if account.address != address: continue
storeBalances(account, ethBalance, tokens)
updateBalance(account, self.getDefaultCurrency(), false)
proc getEthBalance*(address: string): string =
var balance = getBalance(address)
result = hex2token(balance, 18)
proc newAccount*(self: WalletModel, walletType: string, derivationPath: string, name: string, address: string, iconColor: string, balance: string, publicKey: string): WalletAccount =
var assets: seq[Asset] = self.generateAccountConfiguredAssets(address)
var account = WalletAccount(name: name, path: derivationPath, walletType: walletType, address: address, iconColor: iconColor, balance: none[string](), assetList: assets, realFiatBalance: none[float](), publicKey: publicKey)
updateBalance(account, self.getDefaultCurrency())
account
proc initAccounts*(self: WalletModel) =
self.tokens = status_tokens.getVisibleTokens()
let accounts = status_wallet.getWalletAccounts()
for account in accounts:
var acc = WalletAccount(account)
self.populateAccount(acc, "")
updateBalance(acc, self.getDefaultCurrency(), true)
self.accounts.add(acc)
proc updateAccount*(self: WalletModel, address: string) =
for acc in self.accounts.mitems:
if acc.address == address:
self.populateAccount(acc, "", true)
updateBalance(acc, self.getDefaultCurrency(), true)
self.events.emit("accountsUpdated", Args())
proc getTotalFiatBalance*(self: WalletModel): string =
self.calculateTotalFiatBalance()
fmt"{self.totalBalance:.2f}"
proc convertValue*(self: WalletModel, balance: string, fromCurrency: string, toCurrency: string): float =
result = convertValue(balance, fromCurrency, toCurrency)
proc calculateTotalFiatBalance*(self: WalletModel) =
self.totalBalance = 0.0
for account in self.accounts:
if account.realFiatBalance.isSome:
self.totalBalance += account.realFiatBalance.get()
proc addNewGeneratedAccount(self: WalletModel, generatedAccount: GeneratedAccount, password: string, accountName: string, color: string, accountType: string, isADerivedAccount = true, walletIndex: int = 0) =
try:
generatedAccount.name = accountName
var derivedAccount: DerivedAccount = status_accounts.saveAccount(generatedAccount, password, color, accountType, isADerivedAccount, walletIndex)
var account = self.newAccount(accountType, derivedAccount.derivationPath, accountName, derivedAccount.address, color, fmt"0.00 {self.defaultCurrency}", derivedAccount.publicKey)
self.accounts.add(account)
# wallet_checkRecentHistory is required to be called when a new account is
# added before wallet_getTransfersByAddress can be called. This is because
# wallet_checkRecentHistory populates the status-go db that
# wallet_getTransfersByAddress reads from
discard status_wallet.checkRecentHistory(self.accounts.map(account => account.address))
self.events.emit("newAccountAdded", wallet_account.AccountArgs(account: account))
except Exception as e:
raise newException(StatusGoException, fmt"Error adding new account: {e.msg}")
proc generateNewAccount*(self: WalletModel, password: string, accountName: string, color: string) =
let
walletRootAddress = status_settings.getSetting[string](Setting.WalletRootAddress, "")
walletIndex = status_settings.getSetting[int](Setting.LatestDerivedPath) + 1
loadedAccount = status_accounts.loadAccount(walletRootAddress, password)
derivedAccount = status_accounts.deriveWallet(loadedAccount.id, walletIndex)
generatedAccount = GeneratedAccount(
id: loadedAccount.id,
publicKey: derivedAccount.publicKey,
address: derivedAccount.address
)
# if we've gotten here, the password is ok (loadAccount requires a valid password)
# so no need to check for a valid password
self.addNewGeneratedAccount(generatedAccount, password, accountName, color, constants.GENERATED, true, walletIndex)
let statusGoResult = status_settings.saveSetting(Setting.LatestDerivedPath, $walletIndex)
if statusGoResult.error != "":
error "Error storing the latest wallet index", msg=statusGoResult.error
proc addAccountsFromSeed*(self: WalletModel, seed: string, password: string, accountName: string, color: string) =
let mnemonic = replace(seed, ',', ' ')
var generatedAccount = status_accounts.multiAccountImportMnemonic(mnemonic)
generatedAccount.derived = status_accounts.deriveAccounts(generatedAccount.id)
let
defaultAccount = status_accounts.getDefaultAccount()
isPasswordOk = status_accounts.verifyAccountPassword(defaultAccount, password)
if not isPasswordOk:
raise newException(StatusGoException, "Error generating new account: invalid password")
self.addNewGeneratedAccount(generatedAccount, password, accountName, color, constants.SEED)
proc addAccountsFromPrivateKey*(self: WalletModel, privateKey: string, password: string, accountName: string, color: string) =
let
generatedAccount = status_accounts.MultiAccountImportPrivateKey(privateKey)
defaultAccount = status_accounts.getDefaultAccount()
isPasswordOk = status_accounts.verifyAccountPassword(defaultAccount, password)
if not isPasswordOk:
raise newException(StatusGoException, "Error generating new account: invalid password")
self.addNewGeneratedAccount(generatedAccount, password, accountName, color, constants.KEY, false)
proc addWatchOnlyAccount*(self: WalletModel, address: string, accountName: string, color: string) =
let account = GeneratedAccount(address: address)
self.addNewGeneratedAccount(account, "", accountName, color, constants.WATCH, false)
proc hasAsset*(self: WalletModel, symbol: string): bool =
self.tokens.anyIt(it.symbol == symbol)
proc changeAccountSettings*(self: WalletModel, address: string, accountName: string, color: string): string =
var selectedAccount: WalletAccount
for account in self.accounts:
if (account.address == address):
selectedAccount = account
break
if (isNil(selectedAccount)):
result = "No account found with that address"
error "No account found with that address", address
selectedAccount.name = accountName
selectedAccount.iconColor = color
result = status_accounts.changeAccount(selectedAccount.name, selectedAccount.address,
selectedAccount.publicKey, selectedAccount.walletType, selectedAccount.iconColor)
proc deleteAccount*(self: WalletModel, address: string): string =
result = status_accounts.deleteAccount(address)
self.accounts = self.accounts.filter(acc => acc.address.toLowerAscii != address.toLowerAscii)
proc toggleAsset*(self: WalletModel, symbol: string) =
self.tokens = status_tokens.toggleAsset(symbol)
for account in self.accounts:
account.assetList = self.generateAccountConfiguredAssets(account.address)
updateBalance(account, self.getDefaultCurrency())
self.events.emit("assetChanged", Args())
proc hideAsset*(self: WalletModel, symbol: string) =
status_tokens.hideAsset(symbol)
self.tokens = status_tokens.getVisibleTokens()
for account in self.accounts:
account.assetList = self.generateAccountConfiguredAssets(account.address)
updateBalance(account, self.getDefaultCurrency())
self.events.emit("assetChanged", Args())
proc addCustomToken*(self: WalletModel, symbol: string, enable: bool, address: string, name: string, decimals: int, color: string) =
addCustomToken(address, name, symbol, decimals, color)
proc getTransfersByAddress*(self: WalletModel, address: string, toBlock: Uint256, limit: int, loadMore: bool): seq[Transaction] =
result = status_wallet.getTransfersByAddress(address, toBlock, limit, loadMore)
proc validateMnemonic*(self: WalletModel, mnemonic: string): string =
result = status_wallet.validateMnemonic(mnemonic).parseJSON()["error"].getStr
proc getGasPricePredictions*(): GasPricePrediction =
if status_settings.getCurrentNetwork() != Network.Mainnet:
# TODO: what about other chains like xdai?
return GasPricePrediction(safeLow: 1.0, standard: 2.0, fast: 3.0, fastest: 4.0)
let secureSSLContext = newContext()
let client = newHttpClient(sslContext = secureSSLContext)
try:
let url: string = fmt"https://etherchain.org/api/gasPriceOracle"
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let response = client.request(url)
result = Json.decode(response.body, GasPricePrediction)
except Exception as e:
echo "error getting gas price predictions"
echo e.msg
finally:
client.close()
proc checkRecentHistory*(self: WalletModel, addresses: seq[string]): string =
result = status_wallet.checkRecentHistory(addresses)
proc setInitialBlocksRange*(self: WalletModel): string =
result = status_wallet.setInitialBlocksRange()
proc getWalletAccounts*(self: WalletModel): seq[WalletAccount] =
result = status_wallet.getWalletAccounts()
proc getWalletAccounts*(): seq[WalletAccount] =
result = status_wallet.getWalletAccounts()
proc watchTransaction*(self: WalletModel, transactionHash: string): string =
result = status_wallet.watchTransaction(transactionHash)
proc getPendingTransactions*(self: WalletModel): string =
result = status_wallet.getPendingTransactions()
# proc getTransfersByAddress*(address: string): seq[types.Transaction] =
# result = status_wallet.getTransfersByAddress(address)
proc getTransfersByAddress*(address: string, toBlock: Uint256, limit: int, loadMore: bool): seq[Transaction] =
result = status_wallet.getTransfersByAddress(address, toBlock, limit, loadMore)
proc watchTransaction*(transactionHash: string): string =
result = status_wallet.watchTransaction(transactionHash)
proc hex2Token*(self: WalletModel, input: string, decimals: int): string =
result = status_wallet.hex2Token(input, decimals)
proc getOpenseaCollections*(address: string): string =
result = status_wallet.getOpenseaCollections(address)
proc getOpenseaAssets*(address: string, collectionSlug: string, limit: int): string =
result = status_wallet.getOpenseaAssets(address, collectionSlug, limit)

Some files were not shown because too many files have changed in this diff Show More