diff --git a/Connector/btc/apirpc.py b/Connector/btc/apirpc.py index 40600557..d1c18bc9 100644 --- a/Connector/btc/apirpc.py +++ b/Connector/btc/apirpc.py @@ -4,6 +4,7 @@ from logger import logger from rpcutils import error from rpcutils.rpcconnector import RPCConnector +from rpcutils.rpcsocketconnector import RPCSocketConnector from . import utils from .constants import * @@ -367,7 +368,7 @@ def getTransactionHex(id, params, config): rawTransaction = RPCConnector.request( endpoint=config.bitcoincoreRpcEndpoint, id=id, - method=GET_TRANSACTION_METHOD, + method=GET_RAW_TRANSACTION_METHOD, params=[params["txHash"]] ) @@ -383,7 +384,6 @@ def getTransactionHex(id, params, config): @rpcmethod.rpcMethod(coin=COIN_SYMBOL) @httpmethod.postHttpMethod(coin=COIN_SYMBOL) def getTransaction(id, params, config): - logger.printInfo(f"Executing RPC method getTransaction with id {id} and params {params}") requestSchema, responseSchema = utils.getMethodSchemas(GET_TRANSACTION) @@ -393,53 +393,47 @@ def getTransaction(id, params, config): raise error.RpcBadRequestError(err.message) try: - # Parameters: TransactionId, include_watchonly, verbose transaction = RPCConnector.request( endpoint=config.bitcoincoreRpcEndpoint, id=id, - method=GET_TRANSACTION_METHOD, + method=GET_RAW_TRANSACTION_METHOD, params=[ params["txHash"], - True, True ] ) - vinAddressBalances = {} - transactionAmount = 0 + # Check if transaction is confirmed, and obtain block number + if "blockhash" in transaction: + transactionBlock = RPCConnector.request( + endpoint=config.bitcoincoreRpcEndpoint, + id=id, + method=GET_BLOCK, + params=[transaction["blockhash"], 1] + ) + blockNumber = transactionBlock["height"] + else: + blockNumber = None - if "generated" not in transaction: - for vin in transaction["decoded"]["vin"]: - inputTransaction = RPCConnector.request( - endpoint=config.bitcoincoreRpcEndpoint, - id=id, - method=GET_TRANSACTION_METHOD, - params=[ - vin["txid"], - True, - True - ] - ) + transactionDetails = utils.decodeTransactionDetails(transaction, config.bitcoincoreRpcEndpoint) - transactionAmount += inputTransaction["decoded"]["vout"][vin["vout"]]["value"] - address = inputTransaction["decoded"]["vout"][vin["vout"]]["scriptPubKey"]["addresses"][0] - value = inputTransaction["decoded"]["vout"][vin["vout"]]["value"] - vinAddressBalances[address] = value + # Converting all transaction details to str + transactionDetails["fee"] = str(transactionDetails["fee"]) + for input in transactionDetails["inputs"]: + input["amount"] = str(input["amount"]) + for output in transactionDetails["outputs"]: + output["amount"] = str(output["amount"]) response = { "transaction": { - "txHash": params["txHash"], - "blockhash": transaction["blockhash"] if transaction["confirmations"] >= 1 else None, - "blockNumber": str(transaction["blockheight"]) if transaction["confirmations"] >= 1 else None, - "fee": str(utils.convertToSatoshi(-transaction["fee"])) if "generated" not in transaction else "0", - "transfers": utils.parseBalancesToTransfers( - vinAddressBalances, - transaction["details"], - -transaction["fee"] if "generated" not in transaction else 0, - transactionAmount - ), - "data": transaction["decoded"] + "txId": transaction["txid"], + "txHash": transaction["hash"], + "blockNumber": str(blockNumber) if blockNumber is not None else blockNumber, + "fee": transactionDetails["fee"], + "inputs": transactionDetails["inputs"], + "outputs": transactionDetails["outputs"], + "data": transaction } } diff --git a/Connector/btc/config.py b/Connector/btc/config.py index 1e130249..de8b1510 100644 --- a/Connector/btc/config.py +++ b/Connector/btc/config.py @@ -24,6 +24,8 @@ def __init__(self, coin, networkName): self._electrumPassword = "" self._bitcoincoreCallbackProtocol = "" self._bitcoincoreCallbackHost = "" + self._electrumxHost = "" + self._electrumxPort = 0 def loadConfig(self, config): @@ -57,6 +59,8 @@ def loadConfig(self, config): else defaultConfig["bitcoincoreCallbackProtocol"] self.bitcoincoreCallbackHost = config["bitcoincoreCallbackHost"] if "bitcoincoreCallbackHost" in config \ else defaultConfig["bitcoincoreCallbackHost"] + self.electrumxHost = config["electrumxHost"] if "electrumxHost" in config else defaultConfig["electrumxHost"] + self.electrumxPort = config["electrumxPort"] if "electrumxPort" in config else defaultConfig["electrumxPort"] return True, None @@ -184,6 +188,22 @@ def bitcoincoreCallbackHost(self): def bitcoincoreCallbackHost(self, value): self._bitcoincoreCallbackHost = value + @property + def electrumxHost(self): + return self._electrumxHost + + @electrumxHost.setter + def electrumxHost(self, value): + self._electrumxHost = value + + @property + def electrumxPort(self): + return self._electrumxPort + + @electrumxPort.setter + def electrumxPort(self, value): + self._electrumxPort = value + @property def bitcoincoreRpcEndpoint(self): return f"{self.bitcoincoreProtocol}://" \ @@ -226,5 +246,7 @@ def encode(self, o): "electrumHost": o.electrumHost, "electrumPort": o.electrumPort, "electrumUser": o.electrumUser, - "electrumPassword": o.electrumPassword + "electrumPassword": o.electrumPassword, + "electrumxHost": o.electrumxHost, + "electrumxPort": o.electrumxPort } diff --git a/Connector/btc/constants.py b/Connector/btc/constants.py index 6a459e48..68d2db3f 100644 --- a/Connector/btc/constants.py +++ b/Connector/btc/constants.py @@ -8,7 +8,7 @@ GET_BLOCK_HASH_METHOD = "getblockhash" GET_BLOCK_COUNT_METHOD = "getblockcount" ESTIMATE_SMART_FEE_METHOD = "estimatesmartfee" -GET_TRANSACTION_METHOD = "gettransaction" +GET_RAW_TRANSACTION_METHOD = "getrawtransaction" DECODE_RAW_TRANSACTION_METHOD = "decoderawtransaction" SEND_RAW_TRANSACTION_METHOD = "sendrawtransaction" NOTIFY_METHOD = "notify" @@ -100,6 +100,7 @@ NOTIFY = "notify" NEW_HASH_BLOCK_ZMQ_TOPIC = "hashblock" NEW_RAW_BLOCK_ZMQ_TOPIC = "rawblock" +GET_BLOCK = "getblock" SUBSCRIBE_ADDRESS_BALANCE = "subscribetoaddressbalance" UNSUBSCRIBE_ADDRESS_BALANCE = "unsubscribefromaddressbalance" diff --git a/Connector/btc/defaultConf.json b/Connector/btc/defaultConf.json index 25641a0b..858d947f 100644 --- a/Connector/btc/defaultConf.json +++ b/Connector/btc/defaultConf.json @@ -12,5 +12,7 @@ "electrumUser": "swapper", "electrumPassword": "swapper", "bitcoincoreCallbackProtocol": "http", - "bitcoincoreCallbackHost": "connector" + "bitcoincoreCallbackHost": "connector", + "electrumxHost": "electrs", + "electrumxPort": 60001 } \ No newline at end of file diff --git a/Connector/btc/rpcschemas/gettransaction_response.json b/Connector/btc/rpcschemas/gettransaction_response.json index 25f862b0..f74999f8 100644 --- a/Connector/btc/rpcschemas/gettransaction_response.json +++ b/Connector/btc/rpcschemas/gettransaction_response.json @@ -28,18 +28,31 @@ "fee": { "type": "string" }, - "transfers": { + "inputs": { "type": "array", "items": { "properties": { - "from": { - "type": "string" - }, - "to": { - "type": "string" + "address": { + "type": [ + "string", + "null" + ] }, - "fee": { + "amount": { "type": "string" + } + } + } + }, + "outputs": { + "type": "array", + "items": { + "properties": { + "address": { + "type": [ + "string", + "null" + ] }, "amount": { "type": "string" diff --git a/Connector/btc/utils.py b/Connector/btc/utils.py index 0d52780e..87eea8ca 100644 --- a/Connector/btc/utils.py +++ b/Connector/btc/utils.py @@ -1,10 +1,15 @@ #!/usr/bin/python3 +import binascii +import hashlib +import math from decimal import Decimal import random import sys from logger import logger from rpcutils import error as rpcerrorhandler from wsutils import topics +from rpcutils.rpcconnector import RPCConnector +from rpcutils.rpcsocketconnector import RPCSocketConnector from .constants import * from . import apirpc @@ -64,49 +69,53 @@ def closeAddrBalanceTopic(topicName): raise rpcerrorhandler.BadRequestError(f"Can not unsubscribe {topicName} to node") -def parseBalancesToTransfers(vin, vout, fee, amount): - - transfers = [] - diff = 0 - - for utxo in vout: - - if utxo["category"] == "send": - - for address in list(vin.keys()): - - voutAmount = -utxo["amount"] - vinAmount = vin[address] - - if vinAmount <= (voutAmount + diff): - transfer = { - "from": address, - "to": utxo["address"], - "amount": str(convertToSatoshi(vinAmount)), - "fee": str(convertToSatoshi(round(vinAmount * fee / amount, BTC_PRECISION))) - } - del vin[address] - else: - transfer = { - "from": address, - "to": utxo["address"], - "amount": str(convertToSatoshi(voutAmount)), - "fee": str(convertToSatoshi(round(voutAmount * fee / amount, BTC_PRECISION))) - } - - diff = diff + voutAmount - vinAmount - transfers.append(transfer) - - if utxo["category"] in ["generate", "immature", "orphan"]: - transfers.append( - { - "to": utxo["address"], - "fee": "0", - "amount": str(convertToSatoshi(utxo["maount"])) - } - ) - - return transfers +def decodeTransactionDetails(txDecoded, bitcoincoreRpcEndpoint): + outputs = [] + for output in txDecoded["vout"]: + if "addresses" in output["scriptPubKey"] and len(output["scriptPubKey"]["addresses"]) == 1: + outputs.append( + {"amount": math.trunc(output["value"] * 100000000), "address": output["scriptPubKey"]["addresses"][0]}) + else: + outputs.append({"amount": math.trunc(output["value"] * 100000000), "address": None}) + + sumOutputs = 0 + for output in outputs: + sumOutputs += output["amount"] + + inputs = [] + for txInput in txDecoded["vin"]: + + if "coinbase" in txInput: # This is a coinbase transaction and thus it have one only input of 'sumOutputs' + inputs.append({"amount": sumOutputs, "address": None}) + break + + transaction = RPCConnector.request( + endpoint=bitcoincoreRpcEndpoint, + id=0, + method="getrawtransaction", + params=[ + txInput["txid"], + True + ] + ) + + for txOutput in transaction["vout"]: + if txOutput["n"] == txInput["vout"] and "addresses" in txOutput["scriptPubKey"] and len( + txOutput["scriptPubKey"]["addresses"]) == 1: + inputs.append({"amount": math.trunc(txOutput["value"] * 100000000), + "address": txOutput["scriptPubKey"]["addresses"][0]}) + elif "addresses" not in txOutput["scriptPubKey"] or len(txOutput["scriptPubKey"]["addresses"]) != 1: + inputs.append({"amount": math.trunc(txOutput["value"] * 100000000), "address": None}) + + sumInputs = 0 + for txInput in inputs: + sumInputs += txInput["amount"] + + fee = sumInputs - sumOutputs + + transactionsDetails = {"fee": fee, "inputs": inputs, "outputs": outputs} + + return transactionsDetails def sortUnspentOutputs(outputs): diff --git a/Connector/eth/apirpc.py b/Connector/eth/apirpc.py index 089fb994..ecf12e11 100644 --- a/Connector/eth/apirpc.py +++ b/Connector/eth/apirpc.py @@ -222,12 +222,16 @@ def getTransaction(id, params, config): "blockHash": transaction["blockHash"], "blockNumber": str(int(transaction["blockNumber"], 16)) if transaction["blockNumber"] is not None else None, "data": transaction, - "transfers": [ + "inputs": [ { - "from": transaction["from"], - "to": transaction["to"], - "amount": str(utils.toWei(transaction["value"])), - "fee": str(utils.toWei(transaction["gasPrice"]) * utils.toWei(transaction["gas"])) + "address": transaction["from"], + "amount": str(utils.toWei(transaction["value"])) + } + ], + "outputs": [ + { + "address": transaction["to"], + "amount": str(utils.toWei(transaction["value"])) } ] } diff --git a/Connector/eth/rpcschemas/gettransaction_response.json b/Connector/eth/rpcschemas/gettransaction_response.json index f75f3b57..e369333b 100644 --- a/Connector/eth/rpcschemas/gettransaction_response.json +++ b/Connector/eth/rpcschemas/gettransaction_response.json @@ -25,21 +25,31 @@ "fee": { "type": "string" }, - "transfers": { + "inputs": { "type": "array", "items": { - "type": "object", "properties": { - "from": { - "type": "string" - }, - "to": { - "type": "string" + "address": { + "type": [ + "string" + ] }, "amount": { "type": "string" + } + } + } + }, + "outputs": { + "type": "array", + "items": { + "properties": { + "address": { + "type": [ + "string" + ] }, - "fee": { + "amount": { "type": "string" } } diff --git a/Connector/rpcutils/rpcsocketconnector.py b/Connector/rpcutils/rpcsocketconnector.py new file mode 100644 index 00000000..a1b4b817 --- /dev/null +++ b/Connector/rpcutils/rpcsocketconnector.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +import json +import socket +from logger import logger +from . import error +from .constants import * + + +class RPCSocketConnector(): + + @staticmethod + def request(hostname, port, id, method, params): + + try: + + payload = { + ID: id, + METHOD: method, + PARAMS: params, + JSON_RPC: JSON_RPC_VERSION + } + + logger.printInfo(f"Making RPC socket request to {hostname}:{port}. Payload: {payload}") + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((hostname, port)) + + jsonText = {"jsonrpc": JSON_RPC_VERSION, "method": method, "params": params, "id": id} + + s.send(json.dumps(jsonText).encode()) + s.send('\n'.encode()) + + chunks = [] + while True: + chunk = s.recv(2048) + chunks.append(chunk) + if chunk[-1:] == b'\n': + break + + s.close() + + responseStr = b''.join(chunks).decode('ascii') + + except Exception as e: + logger.printError(f"Request to client could not be completed: {str(e)}") + raise error.RpcBadRequestError( + id=id, + message=f"Request to client could not be completed: {str(e)}" + ) + + try: + response = json.loads(responseStr) + except Exception as e: + logger.printError(f"Json in client response is not supported: {str(e)}") + raise error.RpcInternalServerError( + id=id, + message=f"Json in client response is not supported: {str(e)}" + ) + + logger.printInfo(f"Response received from {hostname}:{port}: {response}") + + if ERROR in response and response[ERROR] is not None: + logger.printError(f"Exception occured in server: {response[ERROR]}") + raise error.RpcBadRequestError( + id=id, + message=f"Exception occured in server: {response[ERROR]}" + ) + + return response[RESULT]