Simplify account payment flow and harden direct transfers

This commit is contained in:
2569718930@qq.com
2026-06-14 06:58:45 +08:00
parent 715d79c9b5
commit 0f497ab822
15 changed files with 310 additions and 167 deletions
+194
View File
@@ -523,6 +523,111 @@ def test_submit_rejects_mined_direct_tx_when_receiver_mismatches(monkeypatch, tm
raise AssertionError("expected receiver mismatch rejection")
def test_validate_direct_transfer_rejects_receiver_self_transfer(monkeypatch, tmp_path):
_setup_env(monkeypatch, tmp_path)
service = PaymentContractCheckoutService()
tx_hash = "0x" + "9" * 64
receiver = "0xed2f13aa5ff033c58fb436e178451cd07f693f32"
intent = PaymentIntentRecord(
intent_id="intent-direct-self",
order_id_hex="0x" + "1" * 64,
plan_code="pro_monthly",
plan_id=101,
chain_id=137,
amount_units=5000000,
amount_usdc="5",
token_address="0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
token_decimals=6,
token_symbol="USDC.e",
receiver_address=receiver,
status="created",
payment_mode="direct",
allowed_wallet=None,
expires_at="2099-01-01T00:00:00+00:00",
tx_hash=None,
metadata={},
)
class FakeEth:
def get_transaction_receipt(self, _tx_hash):
return {"status": 1, "to": intent.token_address, "blockNumber": 123}
class FakeWeb3:
eth = FakeEth()
monkeypatch.setattr(service, "_get_web3", lambda *args, **kwargs: FakeWeb3())
monkeypatch.setattr(
service,
"_extract_direct_transfer_event",
lambda *args, **kwargs: {
"from": receiver,
"to": receiver,
"amount_units": 5000000,
"token_address": intent.token_address,
},
)
result = service._validate_loaded_intent_tx(intent, tx_hash)
assert result["valid"] is False
assert result["reason"] == "direct_self_transfer"
def test_submit_rejects_direct_receiver_self_transfer(monkeypatch, tmp_path):
_setup_env(monkeypatch, tmp_path)
service = PaymentContractCheckoutService()
tx_hash = "0x" + "9" * 64
monkeypatch.setattr(
service,
"get_intent",
lambda user_id, intent_id: service._serialize_intent(
{
"id": intent_id,
"plan_code": "pro_monthly",
"plan_id": 101,
"chain_id": 137,
"token_address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
"receiver_address": "0xeD2f13Aa5fF033c58FB436E178451Cd07f693f32",
"amount_units": "5000000",
"payment_mode": "direct",
"allowed_wallet": None,
"order_id_hex": "0x" + "1" * 64,
"status": "created",
"expires_at": "2099-01-01T00:00:00+00:00",
"tx_hash": None,
"metadata": {},
}
),
)
monkeypatch.setattr(
service,
"_validate_loaded_intent_tx",
lambda *args, **kwargs: {
"valid": False,
"reason": "direct_self_transfer",
"detail": "Transfer sender and receiver are both the payment receiver address.",
},
)
def fake_rest(method, table, **kwargs):
if method == "GET" and table == "payment_transactions":
return []
if method == "GET" and table == "payment_intents":
return []
raise AssertionError(f"self-transfer must not mutate {table}")
monkeypatch.setattr(service, "_rest", fake_rest)
try:
service.submit_intent_tx("user-1", "intent-direct-self", tx_hash, "")
except Exception as exc:
assert getattr(exc, "status_code", None) == 400
assert "direct_self_transfer" in getattr(exc, "detail", "")
else:
raise AssertionError("expected direct receiver self-transfer rejection")
def test_confirm_direct_transfer_uses_erc20_transfer_without_wallet_binding(monkeypatch, tmp_path):
_setup_env(monkeypatch, tmp_path)
@@ -625,6 +730,95 @@ def test_confirm_direct_transfer_uses_erc20_transfer_without_wallet_binding(monk
)
assert transaction_write[2]["prefer"] == "resolution=merge-duplicates,return=minimal"
def test_confirm_direct_transfer_rejects_receiver_self_transfer(monkeypatch, tmp_path):
_setup_env(monkeypatch, tmp_path)
service = PaymentContractCheckoutService()
tx_hash = "0x" + "4" * 64
receiver = "0xed2f13aa5ff033c58fb436e178451cd07f693f32"
intent = PaymentIntentRecord(
intent_id="intent-direct-self-confirm",
order_id_hex="0x" + "1" * 64,
plan_code="pro_monthly",
plan_id=101,
chain_id=137,
amount_units=5000000,
amount_usdc="5",
token_address="0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
token_decimals=6,
token_symbol="USDC.e",
receiver_address=receiver,
status="submitted",
payment_mode="direct",
allowed_wallet=None,
expires_at="2099-01-01T00:00:00+00:00",
tx_hash=tx_hash,
metadata={},
)
failed = {}
monkeypatch.setattr(service, "get_intent", lambda user_id, intent_id: intent)
class _Eth:
chain_id = 137
block_number = 20
@staticmethod
def get_transaction(_tx_hash):
return {
"to": intent.token_address,
"from": receiver,
}
class _Web3:
eth = _Eth()
@staticmethod
def is_connected():
return True
monkeypatch.setattr(service, "_get_web3", lambda *args, **kwargs: _Web3())
monkeypatch.setattr(
service,
"_wait_receipt",
lambda _tx_hash, *args, **kwargs: {
"status": 1,
"to": intent.token_address,
"from": receiver,
"blockNumber": 10,
},
)
monkeypatch.setattr(
service,
"_extract_direct_transfer_event",
lambda receipt, local_intent: {
"from": receiver,
"to": receiver,
"token_address": local_intent.token_address,
"amount_units": local_intent.amount_units,
},
)
monkeypatch.setattr(
service,
"_mark_intent_failed",
lambda **kwargs: failed.update(kwargs),
)
def fake_rest(method, table, **kwargs):
if method == "GET" and table in {"payment_transactions", "payment_intents"}:
return []
raise AssertionError((method, table, kwargs))
monkeypatch.setattr(service, "_rest", fake_rest)
with pytest.raises(PaymentCheckoutError) as exc:
service.confirm_intent_tx("user-1", intent.intent_id, tx_hash)
assert exc.value.status_code == 400
assert "direct_self_transfer" in exc.value.detail
assert failed["reason"] == "direct_self_transfer"
def test_submit_rejects_tx_hash_used_by_another_intent(monkeypatch, tmp_path):
_setup_env(monkeypatch, tmp_path)
service = PaymentContractCheckoutService()