Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions node/rustchain_v2_integrated_v2.2.1_rip200.py
Original file line number Diff line number Diff line change
Expand Up @@ -2631,20 +2631,35 @@ def request_withdrawal():
"""Request RTC withdrawal"""
withdrawal_requests.inc()

data = request.get_json()
data = request.get_json(silent=True)
if not isinstance(data, dict):
withdrawal_failed.inc()
return jsonify({"error": "Invalid JSON body"}), 400

# Extract client IP (handle nginx proxy)
client_ip = client_ip_from_request(request)
miner_pk = data.get('miner_pk')
amount = float(data.get('amount', 0))
amount_raw = data.get('amount', 0)
destination = data.get('destination')
signature = data.get('signature')
nonce = data.get('nonce')

if not all([miner_pk, destination, signature, nonce]):
withdrawal_failed.inc()
return jsonify({"error": "Missing required fields"}), 400

try:
amount = float(amount_raw)
except (TypeError, ValueError):
withdrawal_failed.inc()
return jsonify({"error": "Amount must be a number"}), 400

if not math.isfinite(amount) or amount <= 0:
withdrawal_failed.inc()
return jsonify({"error": "Amount must be a finite positive number"}), 400

if amount < MIN_WITHDRAWAL:
withdrawal_failed.inc()
return jsonify({"error": f"Minimum withdrawal is {MIN_WITHDRAWAL} RTC"}), 400

with sqlite3.connect(DB_PATH) as c:
Expand Down
82 changes: 82 additions & 0 deletions node/tests/test_withdraw_amount_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import importlib.util
import os
import sys
import tempfile
import unittest


NODE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
MODULE_PATH = os.path.join(NODE_DIR, "rustchain_v2_integrated_v2.2.1_rip200.py")


class TestWithdrawAmountValidation(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._tmp = tempfile.TemporaryDirectory()
cls._prev_db_path = os.environ.get("RUSTCHAIN_DB_PATH")
cls._prev_admin_key = os.environ.get("RC_ADMIN_KEY")
os.environ["RUSTCHAIN_DB_PATH"] = os.path.join(cls._tmp.name, "import.db")
os.environ["RC_ADMIN_KEY"] = "0123456789abcdef0123456789abcdef"

if NODE_DIR not in sys.path:
sys.path.insert(0, NODE_DIR)

spec = importlib.util.spec_from_file_location("rustchain_integrated_withdraw_test", MODULE_PATH)
cls.mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cls.mod)
cls.client = cls.mod.app.test_client()

@classmethod
def tearDownClass(cls):
if cls._prev_db_path is None:
os.environ.pop("RUSTCHAIN_DB_PATH", None)
else:
os.environ["RUSTCHAIN_DB_PATH"] = cls._prev_db_path
if cls._prev_admin_key is None:
os.environ.pop("RC_ADMIN_KEY", None)
else:
os.environ["RC_ADMIN_KEY"] = cls._prev_admin_key
cls._tmp.cleanup()

def _payload(self, amount):
return {
"miner_pk": "miner-test",
"amount": amount,
"destination": "rtc-destination",
"signature": "00",
"nonce": "nonce-1",
}

def test_invalid_json_body_rejected(self):
resp = self.client.post(
"/withdraw/request",
data="{not-json",
content_type="application/json",
)
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.get_json().get("error"), "Invalid JSON body")

def test_non_numeric_amount_rejected(self):
resp = self.client.post("/withdraw/request", json=self._payload("abc"))
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.get_json().get("error"), "Amount must be a number")

def test_nan_amount_rejected(self):
resp = self.client.post("/withdraw/request", json=self._payload("NaN"))
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.get_json().get("error"), "Amount must be a finite positive number")

def test_infinite_amount_rejected(self):
resp = self.client.post("/withdraw/request", json=self._payload("inf"))
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.get_json().get("error"), "Amount must be a finite positive number")

def test_minimum_withdrawal_check_still_applies(self):
amount = max(0.000001, float(self.mod.MIN_WITHDRAWAL) / 2.0)
resp = self.client.post("/withdraw/request", json=self._payload(amount))
self.assertEqual(resp.status_code, 400)
self.assertIn("Minimum withdrawal", resp.get_json().get("error", ""))


if __name__ == "__main__":
unittest.main()
Loading