Initial import
This commit is contained in:
parent
53d2c323a0
commit
0b24d7a341
|
@ -0,0 +1,3 @@
|
|||
# nim-status-lib
|
||||
|
||||
WIP refactor to extract business logic from status-desktop into a reusable library
|
|
@ -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"))
|
|
@ -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"
|
|
@ -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
|
|
@ -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)
|
|
@ -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 don’t 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]
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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())
|
|
@ -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)
|
|
@ -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"
|
|
@ -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
|
||||
|
|
@ -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)})
|
|
@ -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
|
|
@ -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)
|
|
@ -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"]
|
|
@ -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)
|
|
@ -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
|
|
@ -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])
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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")
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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])
|
|
@ -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])
|
|
@ -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)
|
||||
|
|
@ -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]()
|
|
@ -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)
|
|
@ -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 = ""
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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))
|
|
@ -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()])
|
|
@ -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)
|
|
@ -0,0 +1,3 @@
|
|||
type
|
||||
MailServer* = ref object
|
||||
name*, endpoint*: string
|
|
@ -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: @[]
|
||||
)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -0,0 +1,7 @@
|
|||
{.used.}
|
||||
|
||||
type Bookmark* = ref object
|
||||
name*: string
|
||||
url*: string
|
||||
imageUrl*: string
|
||||
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,6 @@
|
|||
{.used.}
|
||||
|
||||
type CommunityCategory* = object
|
||||
id*: string
|
||||
name*: string
|
||||
position*: int
|
|
@ -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,
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
{.used.}
|
||||
|
||||
type DerivedAccount* = object
|
||||
publicKey*: string
|
||||
address*: string
|
||||
derivationPath*: string
|
|
@ -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]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{.used.}
|
||||
|
||||
type GasPricePrediction* = object
|
||||
safeLow*: float
|
||||
standard*: float
|
||||
fast*: float
|
||||
fastest*: float
|
||||
currentBaseFee*: float
|
||||
recommendedBaseFee*: float
|
|
@ -0,0 +1,6 @@
|
|||
{.used.}
|
||||
|
||||
type
|
||||
IdentityImage* = ref object
|
||||
thumbnail*: string
|
||||
large*: string
|
|
@ -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
|
|
@ -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
|
|
@ -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})"
|
|
@ -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
|
||||
)
|
|
@ -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)
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{.used.}
|
||||
|
||||
type PendingTransactionType* {.pure.} = enum
|
||||
RegisterENS = "RegisterENS",
|
||||
SetPubKey = "SetPubKey",
|
||||
ReleaseENS = "ReleaseENS",
|
||||
BuyStickerPack = "BuyStickerPack"
|
||||
WalletTransfer = "WalletTransfer"
|
|
@ -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
|
|
@ -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,
|
||||
)
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
||||
)
|
|
@ -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]")
|
|
@ -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)
|
|
@ -0,0 +1,8 @@
|
|||
{.used.}
|
||||
|
||||
import json_serialization
|
||||
|
||||
type
|
||||
UpstreamConfig* = ref object
|
||||
enabled* {.serializedFieldName("Enabled").}: bool
|
||||
url* {.serializedFieldName("URL").}: string
|
|
@ -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
|
|
@ -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))
|
|
@ -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
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue