Skip to content

Commit

Permalink
Node API - getbalance part1
Browse files Browse the repository at this point in the history
- Updated online documentation to reflect that 'account' and 'include_watchonly` parameters are expected
to be null as they are not supported.
  • Loading branch information
AustEcon authored and rt121212121 committed Mar 16, 2023
1 parent 6ce42ff commit 20d1adb
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 7 deletions.
8 changes: 2 additions & 6 deletions docs/standalone/building-on-electrumsv/node-wallet-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -749,16 +749,12 @@ call processing are described above.
- :Code: -32700 ``RPC_PARSE_ERROR``
:Message: | ``JSON value is not a null as expected``
| This is an intentional incompatibility as we do not support this parameter. The
type of the entries in the ``account`` parameter are expected to be strings and
one or more were interpreted as another type.
| This is an intentional incompatibility as we do not support this parameter.
:Message: | ``JSON value is not an integer as expected``
| The type of the ``minconf`` parameters are expected to be integers
and one or more were interpreted as another type.
:Message: | ``JSON value is not a null as expected``
| This is an intentional incompatibility as we do not support this parameter. The
type of the entries in the ``include_watchonly`` parameter are expected to be
strings and one or more were interpreted as another type.
| This is an intentional incompatibility as we do not support this parameter.
getnewaddress
~~~~~~~~~~~~~
Expand Down
60 changes: 60 additions & 0 deletions electrumsv/nodeapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,8 @@ async def execute_jsonrpc_call_async(request: web.Request, object_data: Any) \
# able to just read the code and understand it without layers of abstraction.
if method_name == "createrawtransaction":
return request_id, await jsonrpc_createrawtransaction_async(request, request_id, params)
elif method_name == "getbalance":
return request_id, await jsonrpc_getbalance_async(request, request_id, params)
elif method_name == "getnewaddress":
return request_id, await jsonrpc_getnewaddress_async(request, request_id, params)
elif method_name == "listunspent":
Expand Down Expand Up @@ -703,6 +705,64 @@ async def jsonrpc_createrawtransaction_async(request: web.Request, request_id: R

return Tx(1, transaction_inputs, transaction_outputs, locktime).to_hex()


async def jsonrpc_getbalance_async(request: web.Request, request_id: RequestIdType,
parameters: RequestParametersType) -> Any:
"""
Get the balance of the default account in the loaded wallet filtered for the desired number
of confirmations.
Raises `HTTPInternalServerError` for related errors to return to the API using application.
"""
# Ensure the user is accessing either an explicit or implicit wallet.
wallet = get_wallet_from_request(request, request_id)
assert wallet is not None

# Similarly the user must only have one account (and we will ignore any
# automatically created petty cash accounts which we do not use yet).
accounts = wallet.get_visible_accounts()
if len(accounts) != 1:
raise web.HTTPInternalServerError(headers={ "Content-Type": "application/json" },
text=json.dumps(ResponseDict(id=request_id, result=None,
error=ErrorDict(code=RPCError.WALLET_ERROR,
message=f"Ambiguous account (found {len(accounts)}, expected 1)"))))
account = accounts[0]

# Compatibility: Raises RPC_INVALID_PARAMETER if we were given unlisted named parameters.
parameter_values = transform_parameters(request_id, [ "account", "minconf",
"include_watchonly" ], parameters)

# INCOMPATIBILITY: Raises RPC_INVALID_PARAMETER to indicate current lack of support for the
# "account" parameter - it should always be null.
if len(parameter_values) > 0 and parameter_values[0] is not None:
raise web.HTTPInternalServerError(headers={"Content-Type": "application/json"},
text=json.dumps(ResponseDict(id=request_id, result=None,
error=ErrorDict(code=RPCError.PARSE_ERROR,
message="JSON value is not a null as expected"))))

# TODO - Currently minconf parameter is not actually used. Handling for this will be
# implemented next.
minconf = 1
if len(parameter_values) > 1 and parameter_values[1] is not None:
# INCOMPATIBILITY: It is not necessary to do a `node_RPCTypeCheckArgument` as the node does.
minconf = get_integer_parameter(request_id, parameter_values[1])

# INCOMPATIBILITY: Raises RPC_INVALID_PARAMETER to indicate current lack of support for the
# "include_watchonly" parameter - it should always be null.
if len(parameter_values) > 2 and parameter_values[2] is not None:
raise web.HTTPInternalServerError(headers={"Content-Type": "application/json"},
text=json.dumps(ResponseDict(id=request_id, result=None,
error=ErrorDict(code=RPCError.PARSE_ERROR,
message="JSON value is not a null as expected"))))

# Compatibility: Unmatured coins should be excluded from the final balance
# see GetLegacyBalance: `https://github.com/bitcoin-sv/bitcoin-sv/
# blob/b489c32ef55d428c5c3825d5526de018031a20af/src/wallet/wallet.cpp#L2238`
balance = account.get_balance()
total_balance = balance.confirmed
return total_balance/COIN


async def jsonrpc_getnewaddress_async(request: web.Request, request_id: RequestIdType,
parameters: RequestParametersType) -> Any:
"""
Expand Down
54 changes: 53 additions & 1 deletion electrumsv/tests/test_nodeapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from electrumsv.wallet import StandardAccount, Wallet
from electrumsv.wallet_database.types import AccountTransactionOutputSpendableRowExtended, \
PaymentRequestOutputRow, PaymentRequestRow, TransactionLinkState, \
TransactionOutputSpendableProtocol
TransactionOutputSpendableProtocol, WalletBalance

from .util import _create_mock_app_state2, MockStorage, TEST_DATA_PATH

Expand Down Expand Up @@ -785,6 +785,58 @@ def get_tip_filter_server_state() -> None:
assert object["result"] == resulting_hex
assert object["error"] is None

@unittest.mock.patch('electrumsv.nodeapi.app_state')
async def test_call_getbalance_success_async(
app_state_nodeapi: AppStateProxy, server_tester: TestClient) -> None:
assert server_tester.app is not None
mock_server = server_tester.app["server"]
# Ensure the server does not require authorization to make a call.
mock_server._password = ""

wallets: dict[str, Wallet] = {}
irrelevant_path = os.urandom(32).hex()
wallet = unittest.mock.Mock()
wallets[irrelevant_path] = wallet
app_state_nodeapi.daemon.wallets = wallets

account = unittest.mock.Mock(spec=StandardAccount)
def get_visible_accounts() -> list[StandardAccount]:
nonlocal account
return [ account ]
wallet.get_visible_accounts.side_effect = get_visible_accounts

account.get_balance.side_effect = lambda *args, **kwargs: WalletBalance(0, 0, 0, 0)

# Params as an empty list
call_object = {
"id": 343,
"method": "getbalance",
"params": [],
}
response = await server_tester.request(path="/", method="POST", json=call_object)
assert response.status == HTTPStatus.OK
object = await response.json()
assert len(object) == 3
assert object["id"] == 343
assert object["result"] == 0
assert isinstance(object["result"], float)
assert object["error"] is None

# Params as jsonrpc v2.0 dictionary
call_object = {
"id": 343,
"method": "getbalance",
"params": {"account": None, "minconf": 1, "include_watchonly": None},
"jsonrpc": 2.0
}
response = await server_tester.request(path="/", method="POST", json=call_object)
assert response.status == HTTPStatus.OK
object = await response.json()
assert len(object) == 3
assert object["id"] == 343
assert object["result"] == 0
assert object["error"] is None

@unittest.mock.patch('electrumsv.nodeapi.app_state')
async def test_call_getnewaddress_no_connected_blockchain_server_async(
app_state_nodeapi: AppStateProxy, server_tester: TestClient) -> None:
Expand Down

0 comments on commit 20d1adb

Please sign in to comment.