import ens, wallet, permissions, utils import ../eventemitter import ./types/[setting] import utils import statusgo_backend/accounts import statusgo_backend/core import statusgo_backend/settings as status_settings import json, json_serialization, sets, strutils import chronicles import stew/byteutils from stew/base32 import nil from stew/base58 import nil const HTTPS_SCHEME* = "https" const IPFS_GATEWAY* = ".infura.status.im" const SWARM_GATEWAY* = "swarm-gateways.net" 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 wallet*: WalletModel proc newProviderModel*(events: EventEmitter, permissions: PermissionsModel, wallet: WalletModel): ProviderModel = result = ProviderModel() result.events = events result.permissions = permissions result.wallet = wallet 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 selectedTipLimit = request{"selectedTipLimit"}.getStr() let selectedOverallLimit = request{"selectedOverallLimit"}.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 let eip1559Enabled = self.wallet.isEIP1559Enabled() try: validateTransactionInput(fromAddress, to, "", value, selectedGasLimit, selectedGasPrice, txData, eip1559Enabled, selectedTipLimit, selectedOverallLimit, "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, eip1559Enabled, selectedTipLimit, selectedOverallLimit, 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 base58bytes = base58.decode(base58.BTCBase58, decodedHash[1]) base32Hash = base32.encode(base32.Base32Lower, base58bytes) 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