diff --git a/jmclient/jmclient/wallet-rpc-api.yaml b/jmclient/jmclient/wallet-rpc-api.yaml index 00e0373..6e77e31 100644 --- a/jmclient/jmclient/wallet-rpc-api.yaml +++ b/jmclient/jmclient/wallet-rpc-api.yaml @@ -399,6 +399,31 @@ paths: $ref: "#/components/responses/401-Unauthorized" '409': $ref: '#/components/responses/409-NoConfig' + /wallet/{walletname}/freeze: + post: + security: + - bearerAuth: [] + summary: freeze or unfreeze an individual utxo for spending + operationId: freeze + description: freeze or unfreeze an individual utxo for spending + parameters: + - name: walletname + in: path + description: name of wallet including .jmdat + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FreezeRequest' + description: utxo string and freeze toggle as boolean + responses: + '200': + $ref: "#/components/responses/Freeze-200-OK" + '400': + $ref: '#/components/responses/400-BadRequest' /wallet/{walletname}/getseed: get: security: @@ -432,6 +457,16 @@ components: scheme: bearer bearerFormat: JWT schemas: + FreezeRequest: + type: object + required: + - utxo-string + - freeze + properties: + utxo-string: + type: string + freeze: + type: boolean ConfigSetRequest: type: object required: @@ -464,6 +499,8 @@ components: type: string ConfigSetResponse: type: object + FreezeResponse: + type: object DoCoinjoinRequest: type: object required: @@ -830,6 +867,12 @@ components: application/json: schema: $ref: "#/components/schemas/GetSeedResponse" + Freeze-200-OK: + description: "freeze or unfreeze utxo action completed successfully" + content: + application/json: + schema: + $ref: "#/components/schemas/FreezeResponse" 202-Accepted: description: The request has been submitted successfully for processing, but the processing has not been completed. 204-NoResultFound: diff --git a/jmclient/jmclient/wallet_rpc.py b/jmclient/jmclient/wallet_rpc.py index 5d82f4d..ce0c090 100644 --- a/jmclient/jmclient/wallet_rpc.py +++ b/jmclient/jmclient/wallet_rpc.py @@ -22,7 +22,7 @@ from jmclient import Taker, jm_single, \ StorageError, StoragePasswordError, JmwalletdWebSocketServerFactory, \ JmwalletdWebSocketServerProtocol, RetryableStorageError, \ SegwitWalletFidelityBonds, wallet_gettimelockaddress -from jmbase.support import get_log +from jmbase.support import get_log, utxostr_to_utxo jlog = get_log() @@ -791,6 +791,35 @@ class JMWalletDaemon(Service): # null return indicates success in updating: return make_jmwalletd_response(request) + @app.route('/wallet//freeze', methods=["POST"]) + def freeze(self, request, walletname): + """ Freeze (true) or unfreeze (false), for spending a specified utxo + in this wallet. Note that this is persisted in the wallet file, + so the status survives across sessions. Note that re-application of + the same state is allowed and does not alter the 200 OK return. + """ + self.check_cookie(request) + if not self.wallet_name == walletname: + raise InvalidRequestFormat() + freeze_json = self.get_POST_body(request, ["utxo-string", "freeze"]) + if not freeze_json: + raise InvalidRequestFormat() + to_disable = freeze_json["freeze"] + valid, txidindex = utxostr_to_utxo(freeze_json["utxo-string"]) + if not valid: + raise InvalidRequestFormat() + txid, index = txidindex + try: + # note: this does not raise or fail if the applied + # disable state (true/false) is the same as the current + # one; that is accepted and not an error. + self.wallet_service.disable_utxo(txid, index, to_disable) + except AssertionError: + # should be impossible because format checked by + # utxostr_to_utxo: + raise InvalidRequestFormat() + return make_jmwalletd_response(request) + def get_listutxos_response(self, utxos): res = [] for k, v in utxos.items(): diff --git a/jmclient/test/test_wallet_rpc.py b/jmclient/test/test_wallet_rpc.py index 6e81ae0..5291012 100644 --- a/jmclient/test/test_wallet_rpc.py +++ b/jmclient/test/test_wallet_rpc.py @@ -381,16 +381,31 @@ class TrialTestWRPC_DisplayWallet(WalletRPCTestBase, unittest.TestCase): assert validate_address(json_body["address"])[0] @defer.inlineCallbacks - def test_listutxos(self): + def test_listutxos_and_freeze(self): self.daemon.auth_disabled = True agent = get_nontor_agent() - addr = self.get_route_root() - addr += "/wallet/" - addr += self.daemon.wallet_name - addr += "/utxos" + pre_addr = self.get_route_root() + pre_addr += "/wallet/" + pre_addr += self.daemon.wallet_name + addr = pre_addr + "/utxos" addr = addr.encode() yield self.do_request(agent, b"GET", addr, None, self.process_listutxos_response) + # Test of freezing is currently very primitive: we only + # check that the action was accepted; a full test would + # involve checking that spending the coin works or doesn't + # work, as expected. + addr = pre_addr + "/freeze" + addr = addr.encode() + utxostr = self.mixdepth1_utxos[0]["utxo"] + body = BytesProducer(json.dumps({"utxo-string": utxostr, + "freeze": True}).encode()) + yield self.do_request(agent, b"POST", addr, body, + self.process_utxo_freeze) + body = BytesProducer(json.dumps({"utxo-string": utxostr, + "freeze": False}).encode()) + yield self.do_request(agent, b"POST", addr, body, + self.process_utxo_freeze) def process_listutxos_response(self, response, code): assert code == 200 @@ -399,11 +414,15 @@ class TrialTestWRPC_DisplayWallet(WalletRPCTestBase, unittest.TestCase): # have depend on what other tests occurred. # For now, we at least check that we have 3 utxos in mixdepth # 1 because none of the other tests spend them: - mixdepth1_utxos = 0 + mixdepth1_utxos = [] for d in json_body["utxos"]: if d["mixdepth"] == 1: - mixdepth1_utxos += 1 - assert mixdepth1_utxos == 3 + mixdepth1_utxos.append(d) + assert len(mixdepth1_utxos) == 3 + self.mixdepth1_utxos = mixdepth1_utxos + + def process_utxo_freeze(self, response, code): + assert code == 200 @defer.inlineCallbacks def test_session(self):