diff --git a/Connector/bch/apirpc.py b/Connector/bch/apirpc.py index 1a8b7a4e..ae7336fe 100644 --- a/Connector/bch/apirpc.py +++ b/Connector/bch/apirpc.py @@ -278,14 +278,46 @@ def getTransaction(id, params): if err is not None: raise rpcerrorhandler.BadRequestError(err.message) - transactionRaw = RPCConnector.request(RPC_ELECTRUM_CASH_ENDPOINT, id, GET_TRANSACTION_METHOD, [params[TX_HASH]]) - transaction = RPCConnector.request(RPC_CORE_ENDPOINT, id, DECODE_RAW_TRANSACTION_METHOD, [transactionRaw]) + try: + # Parameters: TransactionId, include_watchonly, verbose + transaction = RPCConnector.request(RPC_CORE_ENDPOINT, id, GET_TRANSACTION_METHOD, [params[TX_HASH], True, True]) - err = rpcutils.validateJSONRPCSchema(transaction, responseSchema) + vinAddressBalances = {} + transactionAmount = 0 + + if "generated" not in transaction: + + for vin in transaction["decoded"][VIN]: + inputTransaction = RPCConnector.request(RPC_CORE_ENDPOINT, id, GET_TRANSACTION_METHOD, [vin[TX_ID], True, True]) + + transactionAmount += inputTransaction["decoded"][VOUT][vin[VOUT]][VALUE] + address = inputTransaction["decoded"][VOUT][vin[VOUT]][SCRIPT_PUB_KEY][ADDRESSES][0] + value = inputTransaction["decoded"][VOUT][vin[VOUT]][VALUE] + vinAddressBalances[address] = value + + response = { + "transaction": { + BLOCK_HASH: transaction["blockhash"] if transaction[CONFIRMATIONS] >= 1 else None, + "fee": -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"] + } + } + + except rpcerrorhandler.BadRequestError as err: + logger.printError(f"Transaction {params[TX_HASH]} could not be retrieve: {err}") + return {"transaction": None} + + err = rpcutils.validateJSONRPCSchema(response, responseSchema) if err is not None: raise rpcerrorhandler.BadRequestError(err.message) - return transaction + return response @httputils.postMethod diff --git a/Connector/bch/constants.py b/Connector/bch/constants.py index e5b452ec..eb7ae06d 100644 --- a/Connector/bch/constants.py +++ b/Connector/bch/constants.py @@ -12,6 +12,8 @@ GET_BLOCKCHAIN_INFO = "getblockchaininfo" SYNCING = "syncing" +BTC_CASH_PRECISION = 8 + VERBOSITY_LESS_MODE = 0 VERBOSITY_DEFAULT_MODE = 1 VERBOSITY_MORE_MODE = 2 diff --git a/Connector/bch/rpcschemas/gettransaction_response.json b/Connector/bch/rpcschemas/gettransaction_response.json index c085b925..9db383c0 100644 --- a/Connector/bch/rpcschemas/gettransaction_response.json +++ b/Connector/bch/rpcschemas/gettransaction_response.json @@ -4,97 +4,138 @@ "description": "", "type": "object", "properties": { - "txid": { - "type": "string" - }, - "hash": { - "type": "string" - }, - "version": { - "type": "integer" - }, - "size": { - "type": "integer" - }, - "vsize": { - "type": "integer" - }, - "weight": { - "type": "integer" - }, - "locktime": { - "type": "integer" - }, - "hex": { - "type": "string" - }, - "vin": { - "type": "array", - "items": { - "type": "object", - "properties": { - "coinbase": { - "type": "string" - }, - "sequence": { - "type": "integer" - }, - "txid": { - "type": "string" - }, - "vout": { - "type": "integer" - }, - "scriptSig": { - "type": "object", + "transaction": { + "type": [ + "object", + "null" + ], + "properties": { + "blockHash": { + "type": [ + "string", + "null" + ] + }, + "fee": { + "type": "number" + }, + "transfers": { + "type": "array", + "items": { "properties": { - "asm": { + "from": { "type": "string" }, - "hex": { + "to": { "type": "string" + }, + "fee": { + "type": "number" + }, + "amount": { + "type": "number" } } - }, - "txinwitness": { - "type": "array", - "items": { - "type": "string" - } } - } - } - }, - "vout": { - "type": "array", - "items": { - "type": "object", - "properties": { - "value": { - "type": "number" - }, - "n": { - "type": "integer" - }, - "scriptPubKey": { - "type": "object", - "properties": { - "asm": { - "type": "string" - }, - "hex": { - "type": "string" - }, - "reqSigs": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "addresses": { - "type": "array", - "items": { - "type": "string" + }, + "data": { + "type": "object", + "properties": { + "txid": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "version": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "vsize": { + "type": "integer" + }, + "weight": { + "type": "integer" + }, + "locktime": { + "type": "integer" + }, + "hex": { + "type": "string" + }, + "vin": { + "type": "array", + "items": { + "type": "object", + "properties": { + "coinbase": { + "type": "string" + }, + "sequence": { + "type": "integer" + }, + "txid": { + "type": "string" + }, + "vout": { + "type": "integer" + }, + "scriptSig": { + "type": "object", + "properties": { + "asm": { + "type": "string" + }, + "hex": { + "type": "string" + } + } + }, + "txinwitness": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "vout": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "number" + }, + "n": { + "type": "integer" + }, + "scriptPubKey": { + "type": "object", + "properties": { + "asm": { + "type": "string" + }, + "hex": { + "type": "string" + }, + "reqSigs": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "addresses": { + "type": "array", + "items": { + "type": "string" + } + } + } + } } } } diff --git a/Connector/bch/utils.py b/Connector/bch/utils.py index cfb34151..264082da 100644 --- a/Connector/bch/utils.py +++ b/Connector/bch/utils.py @@ -17,3 +17,48 @@ def getRequestMethodSchema(name): def getResponseMethodSchema(name): return RPC_JSON_SCHEMA_FOLDER + name + SCHEMA_CHAR_SEPARATOR + RESPONSE + SCHEMA_EXTENSION + + +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: vinAmount, + "fee": round(vinAmount * fee / amount, BTC_CASH_PRECISION) + } + del vin[address] + else: + transfer = { + "from": address, + "to": utxo[ADDRESS], + AMOUNT: voutAmount, + "fee": round(voutAmount * fee / amount, BTC_CASH_PRECISION) + } + + diff = diff + voutAmount - vinAmount + transfers.append(transfer) + + if utxo["category"] in ["generate", "immature", "orphan"]: + transfers.append( + { + "to": utxo[ADDRESS], + "fee": 0, + AMOUNT: utxo[AMOUNT] + } + ) + + return transfers diff --git a/Connector/btc/apirpc.py b/Connector/btc/apirpc.py index e0aac26f..638a4dcf 100644 --- a/Connector/btc/apirpc.py +++ b/Connector/btc/apirpc.py @@ -286,14 +286,46 @@ def getTransaction(id, params): if err is not None: raise rpcerrorhandler.BadRequestError(err.message) - transactionRaw = RPCConnector.request(RPC_ELECTRUM_ENDPOINT, id, GET_TRANSACTION_METHOD, [params[TX_HASH]]) - transaction = RPCConnector.request(RPC_CORE_ENDPOINT, id, DECODE_RAW_TRANSACTION_METHOD, [transactionRaw]) + try: + # Parameters: TransactionId, include_watchonly, verbose + transaction = RPCConnector.request(RPC_CORE_ENDPOINT, id, GET_TRANSACTION_METHOD, [params[TX_HASH], True, True]) - err = rpcutils.validateJSONRPCSchema(transaction, responseSchema) + vinAddressBalances = {} + transactionAmount = 0 + + if "generated" not in transaction: + + for vin in transaction["decoded"][VIN]: + inputTransaction = RPCConnector.request(RPC_CORE_ENDPOINT, id, GET_TRANSACTION_METHOD, [vin[TX_ID], True, True]) + + transactionAmount += inputTransaction["decoded"][VOUT][vin[VOUT]][VALUE] + address = inputTransaction["decoded"][VOUT][vin[VOUT]][SCRIPT_PUB_KEY][ADDRESSES][0] + value = inputTransaction["decoded"][VOUT][vin[VOUT]][VALUE] + vinAddressBalances[address] = value + + response = { + "transaction": { + BLOCK_HASH: transaction["blockhash"] if transaction[CONFIRMATIONS] >= 1 else None, + "fee": -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"] + } + } + + except rpcerrorhandler.BadRequestError as err: + logger.printError(f"Transaction {params[TX_HASH]} could not be retrieve: {err}") + return {"transaction": None} + + err = rpcutils.validateJSONRPCSchema(response, responseSchema) if err is not None: raise rpcerrorhandler.BadRequestError(err.message) - return transaction + return response @httputils.postMethod diff --git a/Connector/btc/constants.py b/Connector/btc/constants.py index 82783eeb..d9884536 100644 --- a/Connector/btc/constants.py +++ b/Connector/btc/constants.py @@ -13,6 +13,8 @@ GET_BLOCKCHAIN_INFO = "getblockchaininfo" SYNCING = "syncing" +BTC_PRECISION = 8 + VERBOSITY_LESS_MODE = 0 VERBOSITY_DEFAULT_MODE = 1 VERBOSITY_MORE_MODE = 2 diff --git a/Connector/btc/rpcschemas/gettransaction_response.json b/Connector/btc/rpcschemas/gettransaction_response.json index c085b925..9db383c0 100644 --- a/Connector/btc/rpcschemas/gettransaction_response.json +++ b/Connector/btc/rpcschemas/gettransaction_response.json @@ -4,97 +4,138 @@ "description": "", "type": "object", "properties": { - "txid": { - "type": "string" - }, - "hash": { - "type": "string" - }, - "version": { - "type": "integer" - }, - "size": { - "type": "integer" - }, - "vsize": { - "type": "integer" - }, - "weight": { - "type": "integer" - }, - "locktime": { - "type": "integer" - }, - "hex": { - "type": "string" - }, - "vin": { - "type": "array", - "items": { - "type": "object", - "properties": { - "coinbase": { - "type": "string" - }, - "sequence": { - "type": "integer" - }, - "txid": { - "type": "string" - }, - "vout": { - "type": "integer" - }, - "scriptSig": { - "type": "object", + "transaction": { + "type": [ + "object", + "null" + ], + "properties": { + "blockHash": { + "type": [ + "string", + "null" + ] + }, + "fee": { + "type": "number" + }, + "transfers": { + "type": "array", + "items": { "properties": { - "asm": { + "from": { "type": "string" }, - "hex": { + "to": { "type": "string" + }, + "fee": { + "type": "number" + }, + "amount": { + "type": "number" } } - }, - "txinwitness": { - "type": "array", - "items": { - "type": "string" - } } - } - } - }, - "vout": { - "type": "array", - "items": { - "type": "object", - "properties": { - "value": { - "type": "number" - }, - "n": { - "type": "integer" - }, - "scriptPubKey": { - "type": "object", - "properties": { - "asm": { - "type": "string" - }, - "hex": { - "type": "string" - }, - "reqSigs": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "addresses": { - "type": "array", - "items": { - "type": "string" + }, + "data": { + "type": "object", + "properties": { + "txid": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "version": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "vsize": { + "type": "integer" + }, + "weight": { + "type": "integer" + }, + "locktime": { + "type": "integer" + }, + "hex": { + "type": "string" + }, + "vin": { + "type": "array", + "items": { + "type": "object", + "properties": { + "coinbase": { + "type": "string" + }, + "sequence": { + "type": "integer" + }, + "txid": { + "type": "string" + }, + "vout": { + "type": "integer" + }, + "scriptSig": { + "type": "object", + "properties": { + "asm": { + "type": "string" + }, + "hex": { + "type": "string" + } + } + }, + "txinwitness": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "vout": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "number" + }, + "n": { + "type": "integer" + }, + "scriptPubKey": { + "type": "object", + "properties": { + "asm": { + "type": "string" + }, + "hex": { + "type": "string" + }, + "reqSigs": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "addresses": { + "type": "array", + "items": { + "type": "string" + } + } + } + } } } } diff --git a/Connector/btc/utils.py b/Connector/btc/utils.py index badb65e6..9bffd4ee 100644 --- a/Connector/btc/utils.py +++ b/Connector/btc/utils.py @@ -58,3 +58,48 @@ def closeAddrBalanceTopic(topicName): if not response[SUCCESS]: logger.printError(f"Can not unsubscribe {topicName} to node") 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: vinAmount, + "fee": round(vinAmount * fee / amount, BTC_PRECISION) + } + del vin[address] + else: + transfer = { + "from": address, + "to": utxo[ADDRESS], + AMOUNT: voutAmount, + "fee": 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: utxo[AMOUNT] + } + ) + + return transfers diff --git a/Connector/eth/apirpc.py b/Connector/eth/apirpc.py index 097d1c40..6ed89207 100644 --- a/Connector/eth/apirpc.py +++ b/Connector/eth/apirpc.py @@ -157,15 +157,23 @@ def getTransaction(id, params): if transaction is None: logger.printWarning("Could not get transaction from node") - raise rpcerrorhandler.BadRequestError("Could not get transaction from node") + return {TRANSACTION: None} - inputs = [] - outputs = [] - - inputs.append({ADDRESS: transaction[FROM], AMOUNT: str(int(transaction[VALUE], 16))}) - outputs.append({ADDRESS: transaction[TO], AMOUNT: str(int(transaction[VALUE], 16))}) - - response = {TRANSACTION: transaction, INPUTS: inputs, OUTPUTS: outputs} + response = { + TRANSACTION: { + "fee": utils.toHex(utils.toWei(transaction[GAS_PRICE]) * utils.toWei(transaction["gas"])), + BLOCK_HASH: transaction[BLOCK_HASH], + "data": transaction, + "transfers": [ + { + FROM: transaction[FROM], + TO: transaction[TO], + AMOUNT: transaction[VALUE], + "fee": utils.toHex(utils.toWei(transaction[GAS_PRICE]) * utils.toWei(transaction["gas"])) + } + ] + } + } err = rpcutils.validateJSONRPCSchema(response, responseSchema) if err is not None: diff --git a/Connector/eth/rpcschemas/gettransaction_response.json b/Connector/eth/rpcschemas/gettransaction_response.json index 31eedb1b..0f6a6cdb 100644 --- a/Connector/eth/rpcschemas/gettransaction_response.json +++ b/Connector/eth/rpcschemas/gettransaction_response.json @@ -8,87 +8,85 @@ "type": "object", "properties": { "blockHash": { - "type": "string" - }, - "blockNumber": { - "type": "string" - }, - "from": { - "type": "string" - }, - "gas": { - "type": "string" - }, - "gasPrice": { - "type": "string" - }, - "hash": { - "type": "string" - }, - "input": { - "type": "string" - }, - "nonce": { - "type": "string" - }, - "r": { - "type": "string" - }, - "s": { - "type": "string" - }, - "to": { "type": [ "string", "null" ] }, - "transactionIndex": { - "type": "string" - }, - "v": { + "fee": { "type": "string" }, - "value": { - "type": "string" - } - } - }, - "inputs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { - "type": "string" - }, - "amount": { - "type": "string" + "transfers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "to": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "fee": { + "type": "string" + } + } } - } - } - }, - "outputs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { - "type": [ - "string", - "null" - ] - }, - "amount": { - "type": "string" + }, + "data": { + "type": "object", + "properties": { + "blockHash": { + "type": "string" + }, + "blockNumber": { + "type": "string" + }, + "from": { + "type": "string" + }, + "gas": { + "type": "string" + }, + "gasPrice": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "input": { + "type": "string" + }, + "nonce": { + "type": "string" + }, + "r": { + "type": "string" + }, + "s": { + "type": "string" + }, + "to": { + "type": [ + "string", + "null" + ] + }, + "transactionIndex": { + "type": "string" + }, + "v": { + "type": "string" + }, + "value": { + "type": "string" + } } } } } - }, - "required": [ - "transaction", - "inputs", - "outputs" - ] + } } \ No newline at end of file diff --git a/Connector/eth/utils.py b/Connector/eth/utils.py index 344ba8b9..8645afd5 100644 --- a/Connector/eth/utils.py +++ b/Connector/eth/utils.py @@ -54,3 +54,7 @@ def closingAddrBalanceTopic(topicName): def toWei(amount): return int(amount, 16) + + +def toHex(amount): + return hex(amount) diff --git a/Connector/rpcutils/rpcutils.py b/Connector/rpcutils/rpcutils.py index d816b4c6..106164aa 100644 --- a/Connector/rpcutils/rpcutils.py +++ b/Connector/rpcutils/rpcutils.py @@ -93,3 +93,7 @@ def generateRPCErrorResponse(id, err): MESSAGE: err[MESSAGE] } } + + +def isRPCErrorResponse(response): + return CODE in response or MESSAGE in response diff --git a/Connector/tests/btc/test_btc.py b/Connector/tests/btc/test_btc.py index 4cd5d120..d012a1c3 100644 --- a/Connector/tests/btc/test_btc.py +++ b/Connector/tests/btc/test_btc.py @@ -5,7 +5,7 @@ import time from btc.connector import RPC_CORE_ENDPOINT, RPC_ELECTRUM_ENDPOINT from btc.constants import * -from btc.utils import convertToSatoshi +from btc.utils import convertToSatoshi, parseBalancesToTransfers from logger import logger from rpcutils.rpcconnector import RPCConnector from rpcutils.rpcutils import RPCMethods @@ -42,6 +42,7 @@ def mineBlocksToAddress(address, numBlocks=1): if wallet1Name not in makeBitcoinCoreRequest("listwallets", []): makeBitcoinCoreRequest("createwallet", [wallet1Name]) + address1 = makeBitcoinCoreRequest("getnewaddress", []) privateKey1 = makeBitcoinCoreRequest("dumpprivkey", [address1]) address2 = makeBitcoinCoreRequest("getnewaddress", []) @@ -210,7 +211,7 @@ def testBroadcastTransaction(): logger.printError("broadcastTransaction not loaded in RPCMethods") assert False - signedRawTransaction, ok = createSignedRawTransaction(address1, address2, 115) + signedRawTransaction, ok = createSignedRawTransaction(address1, address2, 0.5) if not ok: logger.printError("Can not create transaction to broadcasts") @@ -372,17 +373,40 @@ def testGetTransaction(): logger.printError("getTransaction not loaded in RPCMethods") assert False - addressHistory = makeElectrumRequest(GET_ADDRESS_HISTORY_METHOD, [address1]) - txHash = addressHistory[0][TX_HASH_SNAKE_CASE] + txHash, _ = sendTransaction(address1, address2, 0.25) + mineBlocksToAddress(minerAddress, 1) - rawTransaction = makeElectrumRequest(GET_TRANSACTION_METHOD, [txHash]) - expected = makeBitcoinCoreRequest(DECODE_RAW_TRANSACTION_METHOD, [rawTransaction]) + expected = makeBitcoinCoreRequest(GET_TRANSACTION_METHOD, [txHash, True, True]) got = RPCMethods["getTransaction"](0, { TX_HASH: txHash }) - assert json.dumps(expected, sort_keys=True) == json.dumps(got, sort_keys=True) + vins = {} + transactionAmount = 0 + if "generated" not in expected: + for vin in expected["decoded"][VIN]: + + inputTransaction = makeBitcoinCoreRequest(GET_TRANSACTION_METHOD, [vin[TX_ID], True, True]) + value = inputTransaction["decoded"][VOUT][vin[VOUT]][VALUE] + address = inputTransaction["decoded"][VOUT][vin[VOUT]][SCRIPT_PUB_KEY][ADDRESSES][0] + transactionAmount += value + vins[address] = value + + assert json.dumps( + { + "transaction": { + "data": expected["decoded"], + "fee": -expected["fee"], + BLOCK_HASH: expected["blockhash"], + "transfers": parseBalancesToTransfers( + vins, + expected["details"], + -expected["fee"], + transactionAmount + ) + } + }, sort_keys=True) == json.dumps(got, sort_keys=True) def testSubscribeToAddressBalance(): diff --git a/Connector/tests/eth/test_eth.py b/Connector/tests/eth/test_eth.py index c7977718..214663b6 100644 --- a/Connector/tests/eth/test_eth.py +++ b/Connector/tests/eth/test_eth.py @@ -5,6 +5,7 @@ from web3 import Web3 from eth.connector import RPC_ENDPOINT from eth.constants import * +from eth import utils from logger import logger from rpcutils.rpcutils import RPCMethods from rpcutils.rpcconnector import RPCConnector @@ -140,24 +141,15 @@ def testGetTransaction(): expected = makeEtherumgoRequest(GET_TRANSACTION_BY_HASH_METHOD, [txHash.hex()]) - for key in expected: - if key not in got[TRANSACTION]: - logger.printError(f"{key} not found in Connector response") - assert False - if got[TRANSACTION][key] != expected[key]: - logger.printError("Transaction data not correct") - assert False - for input in got[INPUTS]: - if input[ADDRESS] != expected[FROM] or input[AMOUNT] != str(int(expected[VALUE], 16)): - logger.printError(f"Transaction input not correct. Output address: {input[ADDRESS]} Expected: {expected[FROM]} Output ampount: {input[AMOUNT]} Expected: {expected[VALUE]}") - assert False - - for output in got[OUTPUTS]: - if output[ADDRESS] != expected[TO] or output[AMOUNT] != str(int(expected[VALUE], 16)): - logger.printError(f"Transaction output not correct. Output address: {output[ADDRESS]} Expected: {expected[TO]} Output ampount: {output[AMOUNT]} Expected: {expected[VALUE]}") - assert False + assert json.dumps(got[TRANSACTION]["data"], sort_keys=True) == json.dumps(expected, sort_keys=True) + assert got[TRANSACTION][BLOCK_HASH] == expected[BLOCK_HASH] + assert got[TRANSACTION]["fee"] == utils.toHex(utils.toWei(expected["gas"]) * utils.toWei(expected["gasPrice"])) - assert True + for transfer in got[TRANSACTION]["transfers"]: + assert transfer[TO] == expected[TO] + assert transfer[FROM] == expected[FROM] + assert transfer[AMOUNT] == expected[VALUE] + assert transfer["fee"] == utils.toHex(utils.toWei(expected["gas"]) * utils.toWei(expected["gasPrice"])) def testEstimateGas():