修复支付代币匹配问题:direct 模式遍历所有支持代币查 Transfer 事件,未知代币不再伪装 USDC
- _extract_direct_transfer_event: 遍历 supported_tokens 所有合约而不仅是 intent.token_address - _default_token_meta: 未知代币显示地址缩写而非 USDC - _token_symbol_for: 未知代币同样不伪装 USDC
This commit is contained in:
@@ -257,7 +257,9 @@ class PaymentContractCheckoutService:
|
|||||||
self.supabase_service_role_key = str(
|
self.supabase_service_role_key = str(
|
||||||
os.getenv("SUPABASE_SERVICE_ROLE_KEY") or ""
|
os.getenv("SUPABASE_SERVICE_ROLE_KEY") or ""
|
||||||
).strip()
|
).strip()
|
||||||
self.chain_id = _env_int("POLYWEATHER_PAYMENT_CHAIN_ID", DEFAULT_POLYGON_CHAIN_ID)
|
self.chain_id = _env_int(
|
||||||
|
"POLYWEATHER_PAYMENT_CHAIN_ID", DEFAULT_POLYGON_CHAIN_ID
|
||||||
|
)
|
||||||
self.token_decimals = _env_int("POLYWEATHER_PAYMENT_TOKEN_DECIMALS", 6)
|
self.token_decimals = _env_int("POLYWEATHER_PAYMENT_TOKEN_DECIMALS", 6)
|
||||||
self.rpc_url = str(os.getenv("POLYWEATHER_PAYMENT_RPC_URL") or "").strip()
|
self.rpc_url = str(os.getenv("POLYWEATHER_PAYMENT_RPC_URL") or "").strip()
|
||||||
self.rpc_urls = self._load_rpc_urls(
|
self.rpc_urls = self._load_rpc_urls(
|
||||||
@@ -267,7 +269,8 @@ class PaymentContractCheckoutService:
|
|||||||
os.getenv("POLYWEATHER_PAYMENT_RECEIVER_CONTRACT") or ""
|
os.getenv("POLYWEATHER_PAYMENT_RECEIVER_CONTRACT") or ""
|
||||||
)
|
)
|
||||||
legacy_token_address = (
|
legacy_token_address = (
|
||||||
os.getenv("POLYWEATHER_PAYMENT_TOKEN_ADDRESS") or DEFAULT_NATIVE_USDC_ADDRESS
|
os.getenv("POLYWEATHER_PAYMENT_TOKEN_ADDRESS")
|
||||||
|
or DEFAULT_NATIVE_USDC_ADDRESS
|
||||||
)
|
)
|
||||||
self.supported_tokens = self._load_supported_tokens(
|
self.supported_tokens = self._load_supported_tokens(
|
||||||
os.getenv("POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON") or "",
|
os.getenv("POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON") or "",
|
||||||
@@ -287,21 +290,25 @@ class PaymentContractCheckoutService:
|
|||||||
self.default_token_address = next(iter(self.supported_tokens.keys()))
|
self.default_token_address = next(iter(self.supported_tokens.keys()))
|
||||||
default_token = self.supported_tokens.get(self.default_token_address)
|
default_token = self.supported_tokens.get(self.default_token_address)
|
||||||
self.token_address = default_token.address if default_token else ""
|
self.token_address = default_token.address if default_token else ""
|
||||||
self.receiver_contract = default_token.receiver_contract if default_token else ""
|
self.receiver_contract = (
|
||||||
|
default_token.receiver_contract if default_token else ""
|
||||||
|
)
|
||||||
self.direct_receiver_address = (
|
self.direct_receiver_address = (
|
||||||
_normalize_address(os.getenv("POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS") or "")
|
_normalize_address(
|
||||||
|
os.getenv("POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS") or ""
|
||||||
|
)
|
||||||
or self.receiver_contract
|
or self.receiver_contract
|
||||||
)
|
)
|
||||||
self.token_decimals = (
|
self.token_decimals = (
|
||||||
int(default_token.decimals) if default_token else int(self.token_decimals)
|
int(default_token.decimals) if default_token else int(self.token_decimals)
|
||||||
)
|
)
|
||||||
self.intent_ttl_sec = max(300, _env_int("POLYWEATHER_PAYMENT_INTENT_TTL_SEC", 1800))
|
self.intent_ttl_sec = max(
|
||||||
|
300, _env_int("POLYWEATHER_PAYMENT_INTENT_TTL_SEC", 1800)
|
||||||
|
)
|
||||||
self.challenge_ttl_sec = max(
|
self.challenge_ttl_sec = max(
|
||||||
60, _env_int("POLYWEATHER_PAYMENT_WALLET_CHALLENGE_TTL_SEC", 600)
|
60, _env_int("POLYWEATHER_PAYMENT_WALLET_CHALLENGE_TTL_SEC", 600)
|
||||||
)
|
)
|
||||||
self.confirmations = max(
|
self.confirmations = max(1, _env_int("POLYWEATHER_PAYMENT_CONFIRMATIONS", 2))
|
||||||
1, _env_int("POLYWEATHER_PAYMENT_CONFIRMATIONS", 2)
|
|
||||||
)
|
|
||||||
self.timeout_sec = max(5, _env_int("POLYWEATHER_PAYMENT_HTTP_TIMEOUT_SEC", 10))
|
self.timeout_sec = max(5, _env_int("POLYWEATHER_PAYMENT_HTTP_TIMEOUT_SEC", 10))
|
||||||
self.poll_interval_sec = max(
|
self.poll_interval_sec = max(
|
||||||
2, _env_int("POLYWEATHER_PAYMENT_POLL_INTERVAL_SEC", 4)
|
2, _env_int("POLYWEATHER_PAYMENT_POLL_INTERVAL_SEC", 4)
|
||||||
@@ -329,7 +336,9 @@ class PaymentContractCheckoutService:
|
|||||||
"POLYWEATHER_PAYMENT_TELEGRAM_NOTIFY_ENABLED", True
|
"POLYWEATHER_PAYMENT_TELEGRAM_NOTIFY_ENABLED", True
|
||||||
)
|
)
|
||||||
self.points_enabled = _env_bool("POLYWEATHER_PAYMENT_POINTS_ENABLED", True)
|
self.points_enabled = _env_bool("POLYWEATHER_PAYMENT_POINTS_ENABLED", True)
|
||||||
self.points_per_usdc = max(1, _env_int("POLYWEATHER_PAYMENT_POINTS_PER_USDC", 500))
|
self.points_per_usdc = max(
|
||||||
|
1, _env_int("POLYWEATHER_PAYMENT_POINTS_PER_USDC", 500)
|
||||||
|
)
|
||||||
self.points_max_discount_usdc = max(
|
self.points_max_discount_usdc = max(
|
||||||
0, _env_int("POLYWEATHER_PAYMENT_POINTS_MAX_DISCOUNT_USDC", 3)
|
0, _env_int("POLYWEATHER_PAYMENT_POINTS_MAX_DISCOUNT_USDC", 3)
|
||||||
)
|
)
|
||||||
@@ -387,7 +396,8 @@ class PaymentContractCheckoutService:
|
|||||||
return {"code": "pusd", "symbol": "pUSD", "name": "Polymarket pUSD"}
|
return {"code": "pusd", "symbol": "pUSD", "name": "Polymarket pUSD"}
|
||||||
if normalized == _normalize_address(DEFAULT_USDC_E_ADDRESS):
|
if normalized == _normalize_address(DEFAULT_USDC_E_ADDRESS):
|
||||||
return {"code": "usdc_e", "symbol": "USDC.e", "name": "USDC.e (PoS)"}
|
return {"code": "usdc_e", "symbol": "USDC.e", "name": "USDC.e (PoS)"}
|
||||||
return {"code": "usdc_token", "symbol": "USDC", "name": "USDC"}
|
short = f"{normalized[:6]}...{normalized[-4:]}"
|
||||||
|
return {"code": f"token_{short}", "symbol": short, "name": short}
|
||||||
|
|
||||||
def _to_token_config(
|
def _to_token_config(
|
||||||
self,
|
self,
|
||||||
@@ -460,7 +470,9 @@ class PaymentContractCheckoutService:
|
|||||||
elif isinstance(parsed, dict):
|
elif isinstance(parsed, dict):
|
||||||
if isinstance(parsed.get("tokens"), list):
|
if isinstance(parsed.get("tokens"), list):
|
||||||
parsed_rows = [
|
parsed_rows = [
|
||||||
row for row in parsed.get("tokens") or [] if isinstance(row, dict)
|
row
|
||||||
|
for row in parsed.get("tokens") or []
|
||||||
|
if isinstance(row, dict)
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
for key, value in parsed.items():
|
for key, value in parsed.items():
|
||||||
@@ -508,7 +520,8 @@ class PaymentContractCheckoutService:
|
|||||||
if token:
|
if token:
|
||||||
return token
|
return token
|
||||||
available = ", ".join(
|
available = ", ".join(
|
||||||
f"{item.symbol}:{item.address}" for item in self.supported_tokens.values()
|
f"{item.symbol}:{item.address}"
|
||||||
|
for item in self.supported_tokens.values()
|
||||||
)
|
)
|
||||||
raise PaymentCheckoutError(
|
raise PaymentCheckoutError(
|
||||||
400,
|
400,
|
||||||
@@ -529,7 +542,10 @@ class PaymentContractCheckoutService:
|
|||||||
token = self.supported_tokens.get(_normalize_address(token_address))
|
token = self.supported_tokens.get(_normalize_address(token_address))
|
||||||
if token and token.symbol:
|
if token and token.symbol:
|
||||||
return str(token.symbol)
|
return str(token.symbol)
|
||||||
return "USDC"
|
normalized = _normalize_address(token_address)
|
||||||
|
if normalized:
|
||||||
|
return f"{normalized[:6]}...{normalized[-4:]}"
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
def _service_headers(self, prefer: Optional[str] = None) -> Dict[str, str]:
|
def _service_headers(self, prefer: Optional[str] = None) -> Dict[str, str]:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -606,7 +622,9 @@ class PaymentContractCheckoutService:
|
|||||||
timeout=self.timeout_sec,
|
timeout=self.timeout_sec,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise PaymentCheckoutError(503, f"supabase auth request failed: {exc}") from exc
|
raise PaymentCheckoutError(
|
||||||
|
503, f"supabase auth request failed: {exc}"
|
||||||
|
) from exc
|
||||||
if response.status_code not in status_ok:
|
if response.status_code not in status_ok:
|
||||||
detail = response.text[:350] if response.text else response.reason
|
detail = response.text[:350] if response.text else response.reason
|
||||||
raise PaymentCheckoutError(
|
raise PaymentCheckoutError(
|
||||||
@@ -629,7 +647,9 @@ class PaymentContractCheckoutService:
|
|||||||
if isinstance(user_payload.get("user_metadata"), dict):
|
if isinstance(user_payload.get("user_metadata"), dict):
|
||||||
return dict(user_payload.get("user_metadata") or {})
|
return dict(user_payload.get("user_metadata") or {})
|
||||||
user_obj = user_payload.get("user")
|
user_obj = user_payload.get("user")
|
||||||
if isinstance(user_obj, dict) and isinstance(user_obj.get("user_metadata"), dict):
|
if isinstance(user_obj, dict) and isinstance(
|
||||||
|
user_obj.get("user_metadata"), dict
|
||||||
|
):
|
||||||
return dict(user_obj.get("user_metadata") or {})
|
return dict(user_obj.get("user_metadata") or {})
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -809,10 +829,14 @@ class PaymentContractCheckoutService:
|
|||||||
if points_before <= 0:
|
if points_before <= 0:
|
||||||
return result
|
return result
|
||||||
redeemable = min(points_before, planned_points)
|
redeemable = min(points_before, planned_points)
|
||||||
redeemable = (redeemable // int(self.points_per_usdc)) * int(self.points_per_usdc)
|
redeemable = (redeemable // int(self.points_per_usdc)) * int(
|
||||||
|
self.points_per_usdc
|
||||||
|
)
|
||||||
if redeemable <= 0:
|
if redeemable <= 0:
|
||||||
return result
|
return result
|
||||||
spend_result = self._db.spend_points_by_supabase_user_id(user_id, redeemable)
|
spend_result = self._db.spend_points_by_supabase_user_id(
|
||||||
|
user_id, redeemable
|
||||||
|
)
|
||||||
if not bool(spend_result.get("ok")):
|
if not bool(spend_result.get("ok")):
|
||||||
return result
|
return result
|
||||||
points_after = int(spend_result.get("balance") or 0)
|
points_after = int(spend_result.get("balance") or 0)
|
||||||
@@ -831,7 +855,9 @@ class PaymentContractCheckoutService:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
redeemable = min(points_before, planned_points)
|
redeemable = min(points_before, planned_points)
|
||||||
redeemable = (redeemable // int(self.points_per_usdc)) * int(self.points_per_usdc)
|
redeemable = (redeemable // int(self.points_per_usdc)) * int(
|
||||||
|
self.points_per_usdc
|
||||||
|
)
|
||||||
if redeemable <= 0:
|
if redeemable <= 0:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -898,7 +924,9 @@ class PaymentContractCheckoutService:
|
|||||||
|
|
||||||
def _get_contract(self, receiver_address: Optional[str] = None):
|
def _get_contract(self, receiver_address: Optional[str] = None):
|
||||||
w3 = self._get_web3()
|
w3 = self._get_web3()
|
||||||
contract_address = _normalize_address(receiver_address or self.receiver_contract)
|
contract_address = _normalize_address(
|
||||||
|
receiver_address or self.receiver_contract
|
||||||
|
)
|
||||||
if not contract_address:
|
if not contract_address:
|
||||||
contract_address = self.receiver_contract
|
contract_address = self.receiver_contract
|
||||||
return w3.eth.contract(
|
return w3.eth.contract(
|
||||||
@@ -915,9 +943,13 @@ class PaymentContractCheckoutService:
|
|||||||
"address": token.address,
|
"address": token.address,
|
||||||
"decimals": int(token.decimals),
|
"decimals": int(token.decimals),
|
||||||
"receiver_contract": token.receiver_contract,
|
"receiver_contract": token.receiver_contract,
|
||||||
"is_default": bool(token.is_default or token.address == self.default_token_address),
|
"is_default": bool(
|
||||||
|
token.is_default or token.address == self.default_token_address
|
||||||
|
),
|
||||||
}
|
}
|
||||||
for token in sorted(self.supported_tokens.values(), key=lambda row: row.code)
|
for token in sorted(
|
||||||
|
self.supported_tokens.values(), key=lambda row: row.code
|
||||||
|
)
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
"enabled": self.enabled,
|
"enabled": self.enabled,
|
||||||
@@ -950,7 +982,9 @@ class PaymentContractCheckoutService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _serialize_intent(self, row: Dict[str, Any]) -> PaymentIntentRecord:
|
def _serialize_intent(self, row: Dict[str, Any]) -> PaymentIntentRecord:
|
||||||
token_address = _normalize_address(row.get("token_address") or self.token_address)
|
token_address = _normalize_address(
|
||||||
|
row.get("token_address") or self.token_address
|
||||||
|
)
|
||||||
token_decimals = self._token_decimals_for(token_address)
|
token_decimals = self._token_decimals_for(token_address)
|
||||||
amount_units = int(_parse_decimal(row.get("amount_units"), Decimal("0")))
|
amount_units = int(_parse_decimal(row.get("amount_units"), Decimal("0")))
|
||||||
amount_display = _units_to_decimal(amount_units, token_decimals)
|
amount_display = _units_to_decimal(amount_units, token_decimals)
|
||||||
@@ -973,7 +1007,9 @@ class PaymentContractCheckoutService:
|
|||||||
allowed_wallet=_normalize_address(row.get("allowed_wallet") or "") or None,
|
allowed_wallet=_normalize_address(row.get("allowed_wallet") or "") or None,
|
||||||
expires_at=str(row.get("expires_at")),
|
expires_at=str(row.get("expires_at")),
|
||||||
tx_hash=str(row.get("tx_hash") or "") or None,
|
tx_hash=str(row.get("tx_hash") or "") or None,
|
||||||
metadata=dict(row.get("metadata") or {}) if isinstance(row.get("metadata"), dict) else {},
|
metadata=dict(row.get("metadata") or {})
|
||||||
|
if isinstance(row.get("metadata"), dict)
|
||||||
|
else {},
|
||||||
)
|
)
|
||||||
|
|
||||||
def list_wallets(self, user_id: str) -> List[WalletBindingRecord]:
|
def list_wallets(self, user_id: str) -> List[WalletBindingRecord]:
|
||||||
@@ -1101,7 +1137,9 @@ class PaymentContractCheckoutService:
|
|||||||
allowed_status=[200],
|
allowed_status=[200],
|
||||||
)
|
)
|
||||||
if not isinstance(challenge_rows, list) or not challenge_rows:
|
if not isinstance(challenge_rows, list) or not challenge_rows:
|
||||||
raise PaymentCheckoutError(400, "wallet challenge not found or already used")
|
raise PaymentCheckoutError(
|
||||||
|
400, "wallet challenge not found or already used"
|
||||||
|
)
|
||||||
|
|
||||||
challenge = challenge_rows[0]
|
challenge = challenge_rows[0]
|
||||||
try:
|
try:
|
||||||
@@ -1137,8 +1175,14 @@ class PaymentContractCheckoutService:
|
|||||||
)
|
)
|
||||||
if isinstance(existing, list) and existing:
|
if isinstance(existing, list) and existing:
|
||||||
owner_id = str(existing[0].get("user_id") or "")
|
owner_id = str(existing[0].get("user_id") or "")
|
||||||
if owner_id and owner_id != user_id and str(existing[0].get("status")) == "active":
|
if (
|
||||||
raise PaymentCheckoutError(409, "wallet already bound by another account")
|
owner_id
|
||||||
|
and owner_id != user_id
|
||||||
|
and str(existing[0].get("status")) == "active"
|
||||||
|
):
|
||||||
|
raise PaymentCheckoutError(
|
||||||
|
409, "wallet already bound by another account"
|
||||||
|
)
|
||||||
|
|
||||||
has_primary = self._rest(
|
has_primary = self._rest(
|
||||||
"GET",
|
"GET",
|
||||||
@@ -1231,7 +1275,9 @@ class PaymentContractCheckoutService:
|
|||||||
|
|
||||||
new_primary = ""
|
new_primary = ""
|
||||||
if isinstance(active_primary_rows, list) and active_primary_rows:
|
if isinstance(active_primary_rows, list) and active_primary_rows:
|
||||||
new_primary = _normalize_address(active_primary_rows[0].get("address") or "")
|
new_primary = _normalize_address(
|
||||||
|
active_primary_rows[0].get("address") or ""
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
active_wallet_rows = self._rest(
|
active_wallet_rows = self._rest(
|
||||||
"GET",
|
"GET",
|
||||||
@@ -1305,7 +1351,9 @@ class PaymentContractCheckoutService:
|
|||||||
except Exception:
|
except Exception:
|
||||||
telegram_id = None
|
telegram_id = None
|
||||||
price_payload = pricing.resolve_price_for_telegram_id(telegram_id)
|
price_payload = pricing.resolve_price_for_telegram_id(telegram_id)
|
||||||
amount_dec = _parse_decimal(price_payload.get("amount_usdc"), out["amount_usdc_decimal"])
|
amount_dec = _parse_decimal(
|
||||||
|
price_payload.get("amount_usdc"), out["amount_usdc_decimal"]
|
||||||
|
)
|
||||||
if amount_dec <= 0:
|
if amount_dec <= 0:
|
||||||
return out
|
return out
|
||||||
out["amount_usdc"] = _format_decimal(amount_dec)
|
out["amount_usdc"] = _format_decimal(amount_dec)
|
||||||
@@ -1358,7 +1406,9 @@ class PaymentContractCheckoutService:
|
|||||||
if mode == "manual":
|
if mode == "manual":
|
||||||
mode = "direct"
|
mode = "direct"
|
||||||
if mode not in {"strict", "flex", "direct"}:
|
if mode not in {"strict", "flex", "direct"}:
|
||||||
raise PaymentCheckoutError(400, "payment_mode must be strict, flex, or direct")
|
raise PaymentCheckoutError(
|
||||||
|
400, "payment_mode must be strict, flex, or direct"
|
||||||
|
)
|
||||||
bound_wallets = [] if mode == "direct" else self.list_wallets(user_id)
|
bound_wallets = [] if mode == "direct" else self.list_wallets(user_id)
|
||||||
if mode != "direct" and not bound_wallets:
|
if mode != "direct" and not bound_wallets:
|
||||||
raise PaymentCheckoutError(403, "bind wallet first")
|
raise PaymentCheckoutError(403, "bind wallet first")
|
||||||
@@ -1384,7 +1434,9 @@ class PaymentContractCheckoutService:
|
|||||||
requested_points_to_consume=points_to_consume,
|
requested_points_to_consume=points_to_consume,
|
||||||
)
|
)
|
||||||
final_amount_usdc = redemption["pay_amount_usdc"]
|
final_amount_usdc = redemption["pay_amount_usdc"]
|
||||||
amount_units = _decimal_to_units(final_amount_usdc, int(selected_token.decimals))
|
amount_units = _decimal_to_units(
|
||||||
|
final_amount_usdc, int(selected_token.decimals)
|
||||||
|
)
|
||||||
if amount_units <= 0:
|
if amount_units <= 0:
|
||||||
raise PaymentCheckoutError(400, "invalid final payment amount")
|
raise PaymentCheckoutError(400, "invalid final payment amount")
|
||||||
combined_metadata = dict(metadata or {})
|
combined_metadata = dict(metadata or {})
|
||||||
@@ -1397,15 +1449,27 @@ class PaymentContractCheckoutService:
|
|||||||
if mode == "direct"
|
if mode == "direct"
|
||||||
else selected_token.receiver_contract
|
else selected_token.receiver_contract
|
||||||
)
|
)
|
||||||
combined_metadata["amount_before_discount_usdc"] = _format_decimal(plan_amount_usdc)
|
combined_metadata["amount_before_discount_usdc"] = _format_decimal(
|
||||||
combined_metadata["amount_after_discount_usdc"] = _format_decimal(final_amount_usdc)
|
plan_amount_usdc
|
||||||
|
)
|
||||||
|
combined_metadata["amount_after_discount_usdc"] = _format_decimal(
|
||||||
|
final_amount_usdc
|
||||||
|
)
|
||||||
combined_metadata["points_redemption"] = {
|
combined_metadata["points_redemption"] = {
|
||||||
"enabled": bool(redemption.get("enabled")),
|
"enabled": bool(redemption.get("enabled")),
|
||||||
"applied": bool(redemption.get("applied")),
|
"applied": bool(redemption.get("applied")),
|
||||||
"points_per_usdc": int(redemption.get("points_per_usdc") or self.points_per_usdc),
|
"points_per_usdc": int(
|
||||||
"max_discount_usdc": int(redemption.get("max_discount_usdc") or self.points_max_discount_usdc),
|
redemption.get("points_per_usdc") or self.points_per_usdc
|
||||||
"points_source": str(redemption.get("points_source") or "supabase_metadata"),
|
),
|
||||||
"points_balance_snapshot": int(redemption.get("points_balance_snapshot") or 0),
|
"max_discount_usdc": int(
|
||||||
|
redemption.get("max_discount_usdc") or self.points_max_discount_usdc
|
||||||
|
),
|
||||||
|
"points_source": str(
|
||||||
|
redemption.get("points_source") or "supabase_metadata"
|
||||||
|
),
|
||||||
|
"points_balance_snapshot": int(
|
||||||
|
redemption.get("points_balance_snapshot") or 0
|
||||||
|
),
|
||||||
"points_to_consume": int(redemption.get("points_to_consume") or 0),
|
"points_to_consume": int(redemption.get("points_to_consume") or 0),
|
||||||
"discount_usdc": str(redemption.get("discount_usdc") or "0"),
|
"discount_usdc": str(redemption.get("discount_usdc") or "0"),
|
||||||
}
|
}
|
||||||
@@ -1457,10 +1521,14 @@ class PaymentContractCheckoutService:
|
|||||||
},
|
},
|
||||||
"points_redemption": {
|
"points_redemption": {
|
||||||
"applied": bool(redemption.get("applied")),
|
"applied": bool(redemption.get("applied")),
|
||||||
"points_source": str(redemption.get("points_source") or "supabase_metadata"),
|
"points_source": str(
|
||||||
|
redemption.get("points_source") or "supabase_metadata"
|
||||||
|
),
|
||||||
"points_to_consume": int(redemption.get("points_to_consume") or 0),
|
"points_to_consume": int(redemption.get("points_to_consume") or 0),
|
||||||
"discount_usdc": str(redemption.get("discount_usdc") or "0"),
|
"discount_usdc": str(redemption.get("discount_usdc") or "0"),
|
||||||
"points_balance_snapshot": int(redemption.get("points_balance_snapshot") or 0),
|
"points_balance_snapshot": int(
|
||||||
|
redemption.get("points_balance_snapshot") or 0
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if mode == "direct":
|
if mode == "direct":
|
||||||
@@ -1477,6 +1545,7 @@ class PaymentContractCheckoutService:
|
|||||||
"expires_at": intent.expires_at,
|
"expires_at": intent.expires_at,
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_intent(self, user_id: str, intent_id: str) -> PaymentIntentRecord:
|
def get_intent(self, user_id: str, intent_id: str) -> PaymentIntentRecord:
|
||||||
self._ensure_enabled()
|
self._ensure_enabled()
|
||||||
rows = self._rest(
|
rows = self._rest(
|
||||||
@@ -1623,7 +1692,9 @@ class PaymentContractCheckoutService:
|
|||||||
continue
|
continue
|
||||||
existing_intent = str(row.get("intent_id") or "").strip()
|
existing_intent = str(row.get("intent_id") or "").strip()
|
||||||
if existing_intent and existing_intent != str(intent_id):
|
if existing_intent and existing_intent != str(intent_id):
|
||||||
raise PaymentCheckoutError(409, "tx_hash already used by another payment intent")
|
raise PaymentCheckoutError(
|
||||||
|
409, "tx_hash already used by another payment intent"
|
||||||
|
)
|
||||||
|
|
||||||
def _record_duplicate_transaction(
|
def _record_duplicate_transaction(
|
||||||
self,
|
self,
|
||||||
@@ -1649,8 +1720,11 @@ class PaymentContractCheckoutService:
|
|||||||
"chain_id": self.chain_id,
|
"chain_id": self.chain_id,
|
||||||
"tx_hash": tx_hash_text,
|
"tx_hash": tx_hash_text,
|
||||||
"from_address": _normalize_address(from_address) or None,
|
"from_address": _normalize_address(from_address) or None,
|
||||||
"to_address": _normalize_address(to_address) or intent.receiver_address,
|
"to_address": _normalize_address(to_address)
|
||||||
"payment_method": "direct" if intent.payment_mode == "direct" else "wallet",
|
or intent.receiver_address,
|
||||||
|
"payment_method": "direct"
|
||||||
|
if intent.payment_mode == "direct"
|
||||||
|
else "wallet",
|
||||||
"status": status,
|
"status": status,
|
||||||
"raw_receipt": {},
|
"raw_receipt": {},
|
||||||
"raw_tx": {
|
"raw_tx": {
|
||||||
@@ -1744,7 +1818,7 @@ class PaymentContractCheckoutService:
|
|||||||
"valid": False,
|
"valid": False,
|
||||||
"reason": "direct_transfer_not_found",
|
"reason": "direct_transfer_not_found",
|
||||||
"detail": "ERC20 Transfer event not found on token contract. "
|
"detail": "ERC20 Transfer event not found on token contract. "
|
||||||
"Ensure you transferred the correct token to the receiver address.",
|
"Ensure you transferred the correct token to the receiver address.",
|
||||||
"checks": checks,
|
"checks": checks,
|
||||||
}
|
}
|
||||||
event_from = _normalize_address(event_match.get("from"))
|
event_from = _normalize_address(event_match.get("from"))
|
||||||
@@ -1786,7 +1860,7 @@ class PaymentContractCheckoutService:
|
|||||||
"valid": False,
|
"valid": False,
|
||||||
"reason": "order_paid_event_not_found",
|
"reason": "order_paid_event_not_found",
|
||||||
"detail": "OrderPaid event not found. "
|
"detail": "OrderPaid event not found. "
|
||||||
"Ensure the tx was sent to the correct receiver contract.",
|
"Ensure the tx was sent to the correct receiver contract.",
|
||||||
"checks": checks,
|
"checks": checks,
|
||||||
}
|
}
|
||||||
event_payer = _normalize_address(event_match.get("payer"))
|
event_payer = _normalize_address(event_match.get("payer"))
|
||||||
@@ -1812,13 +1886,21 @@ class PaymentContractCheckoutService:
|
|||||||
if not all([order_match, plan_match, token_match, amount_match]):
|
if not all([order_match, plan_match, token_match, amount_match]):
|
||||||
failures = []
|
failures = []
|
||||||
if not order_match:
|
if not order_match:
|
||||||
failures.append(f"order_id mismatch: got {event_order_id}, expected {intent.order_id_hex.lower()}")
|
failures.append(
|
||||||
|
f"order_id mismatch: got {event_order_id}, expected {intent.order_id_hex.lower()}"
|
||||||
|
)
|
||||||
if not plan_match:
|
if not plan_match:
|
||||||
failures.append(f"plan_id mismatch: got {event_plan_id}, expected {intent.plan_id}")
|
failures.append(
|
||||||
|
f"plan_id mismatch: got {event_plan_id}, expected {intent.plan_id}"
|
||||||
|
)
|
||||||
if not token_match:
|
if not token_match:
|
||||||
failures.append(f"token mismatch: got {event_token}, expected {intent.token_address}")
|
failures.append(
|
||||||
|
f"token mismatch: got {event_token}, expected {intent.token_address}"
|
||||||
|
)
|
||||||
if not amount_match:
|
if not amount_match:
|
||||||
failures.append(f"amount mismatch: got {event_amount}, expected {intent.amount_units}")
|
failures.append(
|
||||||
|
f"amount mismatch: got {event_amount}, expected {intent.amount_units}"
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"valid": False,
|
"valid": False,
|
||||||
"reason": "event_mismatch",
|
"reason": "event_mismatch",
|
||||||
@@ -1839,7 +1921,10 @@ class PaymentContractCheckoutService:
|
|||||||
intent = self.get_intent(user_id, intent_id)
|
intent = self.get_intent(user_id, intent_id)
|
||||||
tx_hash_text = str(tx_hash or "").strip().lower()
|
tx_hash_text = str(tx_hash or "").strip().lower()
|
||||||
if intent.status == "confirmed":
|
if intent.status == "confirmed":
|
||||||
if tx_hash_text and tx_hash_text != str(intent.tx_hash or "").strip().lower():
|
if (
|
||||||
|
tx_hash_text
|
||||||
|
and tx_hash_text != str(intent.tx_hash or "").strip().lower()
|
||||||
|
):
|
||||||
self._record_duplicate_transaction(
|
self._record_duplicate_transaction(
|
||||||
intent=intent,
|
intent=intent,
|
||||||
tx_hash=tx_hash_text,
|
tx_hash=tx_hash_text,
|
||||||
@@ -1852,7 +1937,9 @@ class PaymentContractCheckoutService:
|
|||||||
"该订单已支付,请勿重复付款;如已重复转账请联系客服处理退款",
|
"该订单已支付,请勿重复付款;如已重复转账请联系客服处理退款",
|
||||||
)
|
)
|
||||||
if intent.status not in {"created", "submitted"}:
|
if intent.status not in {"created", "submitted"}:
|
||||||
raise PaymentCheckoutError(409, f"intent status is {intent.status}, cannot submit")
|
raise PaymentCheckoutError(
|
||||||
|
409, f"intent status is {intent.status}, cannot submit"
|
||||||
|
)
|
||||||
|
|
||||||
from_addr = _normalize_address(from_address)
|
from_addr = _normalize_address(from_address)
|
||||||
if not (tx_hash_text.startswith("0x") and len(tx_hash_text) == 66):
|
if not (tx_hash_text.startswith("0x") and len(tx_hash_text) == 66):
|
||||||
@@ -1911,7 +1998,9 @@ class PaymentContractCheckoutService:
|
|||||||
"tx_hash": tx_hash_text,
|
"tx_hash": tx_hash_text,
|
||||||
"from_address": from_addr,
|
"from_address": from_addr,
|
||||||
"to_address": intent.receiver_address,
|
"to_address": intent.receiver_address,
|
||||||
"payment_method": "direct" if intent.payment_mode == "direct" else "wallet",
|
"payment_method": "direct"
|
||||||
|
if intent.payment_mode == "direct"
|
||||||
|
else "wallet",
|
||||||
"status": "submitted",
|
"status": "submitted",
|
||||||
"updated_at": now_iso,
|
"updated_at": now_iso,
|
||||||
},
|
},
|
||||||
@@ -1923,7 +2012,9 @@ class PaymentContractCheckoutService:
|
|||||||
"status": "submitted",
|
"status": "submitted",
|
||||||
"tx_hash": tx_hash_text,
|
"tx_hash": tx_hash_text,
|
||||||
"from_address": from_addr,
|
"from_address": from_addr,
|
||||||
"transaction": tx_rows[0] if isinstance(tx_rows, list) and tx_rows else None,
|
"transaction": tx_rows[0]
|
||||||
|
if isinstance(tx_rows, list) and tx_rows
|
||||||
|
else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _wait_receipt(self, tx_hash: str) -> Any:
|
def _wait_receipt(self, tx_hash: str) -> Any:
|
||||||
@@ -1992,33 +2083,54 @@ class PaymentContractCheckoutService:
|
|||||||
def _extract_direct_transfer_event(
|
def _extract_direct_transfer_event(
|
||||||
self, receipt: Any, intent: PaymentIntentRecord
|
self, receipt: Any, intent: PaymentIntentRecord
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
token_contract = self._get_web3().eth.contract(
|
|
||||||
address=Web3.to_checksum_address(intent.token_address),
|
|
||||||
abi=[ERC20_TRANSFER_EVENT_ABI],
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
events = token_contract.events.Transfer().process_receipt(receipt)
|
|
||||||
except Exception:
|
|
||||||
events = []
|
|
||||||
if not events:
|
|
||||||
return None
|
|
||||||
|
|
||||||
expected_to = intent.receiver_address
|
expected_to = intent.receiver_address
|
||||||
expected_amount = int(intent.amount_units)
|
expected_amount = int(intent.amount_units)
|
||||||
for ev in events:
|
|
||||||
args = ev.get("args") if isinstance(ev, dict) else getattr(ev, "args", None)
|
# Collect all token contracts to check: intent's token first,
|
||||||
if not args:
|
# then all other supported tokens (in case user transferred a
|
||||||
|
# different token than selected in the UI).
|
||||||
|
token_addresses: List[str] = []
|
||||||
|
if intent.token_address:
|
||||||
|
token_addresses.append(_normalize_address(intent.token_address))
|
||||||
|
for addr in self.supported_tokens:
|
||||||
|
normalized = _normalize_address(addr)
|
||||||
|
if normalized and normalized not in token_addresses:
|
||||||
|
token_addresses.append(normalized)
|
||||||
|
|
||||||
|
for token_addr in token_addresses:
|
||||||
|
try:
|
||||||
|
token_contract = self._get_web3().eth.contract(
|
||||||
|
address=Web3.to_checksum_address(token_addr),
|
||||||
|
abi=[ERC20_TRANSFER_EVENT_ABI],
|
||||||
|
)
|
||||||
|
events = token_contract.events.Transfer().process_receipt(receipt)
|
||||||
|
except Exception:
|
||||||
continue
|
continue
|
||||||
payer = _normalize_address(args.get("from"))
|
|
||||||
receiver = _normalize_address(args.get("to"))
|
for ev in events:
|
||||||
amount = int(args.get("value") or 0)
|
args = (
|
||||||
if receiver == expected_to and amount >= expected_amount:
|
ev.get("args")
|
||||||
return {
|
if isinstance(ev, dict)
|
||||||
"from": payer,
|
else getattr(ev, "args", None)
|
||||||
"to": receiver,
|
)
|
||||||
"token_address": intent.token_address,
|
if not args:
|
||||||
"amount_units": amount,
|
continue
|
||||||
}
|
payer = _normalize_address(args.get("from"))
|
||||||
|
receiver = _normalize_address(args.get("to"))
|
||||||
|
amount = int(args.get("value") or 0)
|
||||||
|
if receiver == expected_to and amount >= expected_amount:
|
||||||
|
token_meta = self._token_symbol_for(token_addr)
|
||||||
|
return {
|
||||||
|
"from": payer,
|
||||||
|
"to": receiver,
|
||||||
|
"token_address": token_addr,
|
||||||
|
"amount_units": amount,
|
||||||
|
"token_mismatch": (
|
||||||
|
token_addr != _normalize_address(intent.token_address)
|
||||||
|
),
|
||||||
|
"token_symbol": token_meta,
|
||||||
|
}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _insert_payment_record(
|
def _insert_payment_record(
|
||||||
@@ -2093,7 +2205,9 @@ class PaymentContractCheckoutService:
|
|||||||
if isinstance(current_subscription, dict):
|
if isinstance(current_subscription, dict):
|
||||||
try:
|
try:
|
||||||
latest_exp = datetime.fromisoformat(
|
latest_exp = datetime.fromisoformat(
|
||||||
str(current_subscription.get("expires_at") or "").replace("Z", "+00:00")
|
str(current_subscription.get("expires_at") or "").replace(
|
||||||
|
"Z", "+00:00"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if latest_exp.tzinfo is None:
|
if latest_exp.tzinfo is None:
|
||||||
latest_exp = latest_exp.replace(tzinfo=timezone.utc)
|
latest_exp = latest_exp.replace(tzinfo=timezone.utc)
|
||||||
@@ -2147,9 +2261,9 @@ class PaymentContractCheckoutService:
|
|||||||
user_id,
|
user_id,
|
||||||
respect_requirement=False,
|
respect_requirement=False,
|
||||||
)
|
)
|
||||||
if isinstance(latest_subscription, dict) and not self._subscription_row_is_trial(
|
if isinstance(
|
||||||
latest_subscription
|
latest_subscription, dict
|
||||||
):
|
) and not self._subscription_row_is_trial(latest_subscription):
|
||||||
return latest_subscription
|
return latest_subscription
|
||||||
|
|
||||||
plan = self._select_plan(intent.plan_code)
|
plan = self._select_plan(intent.plan_code)
|
||||||
@@ -2293,7 +2407,9 @@ class PaymentContractCheckoutService:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def _notify_telegram(self, user_id: str, plan_code: str, amount_usdc: str, tx_hash: str) -> None:
|
def _notify_telegram(
|
||||||
|
self, user_id: str, plan_code: str, amount_usdc: str, tx_hash: str
|
||||||
|
) -> None:
|
||||||
if not self.notify_telegram:
|
if not self.notify_telegram:
|
||||||
return
|
return
|
||||||
token = str(os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
|
token = str(os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
|
||||||
@@ -2305,7 +2421,9 @@ class PaymentContractCheckoutService:
|
|||||||
telegram_id = int(user.get("telegram_id") or 0)
|
telegram_id = int(user.get("telegram_id") or 0)
|
||||||
if telegram_id <= 0:
|
if telegram_id <= 0:
|
||||||
return
|
return
|
||||||
short_hash = tx_hash[:10] + "..." + tx_hash[-8:] if len(tx_hash) > 20 else tx_hash
|
short_hash = (
|
||||||
|
tx_hash[:10] + "..." + tx_hash[-8:] if len(tx_hash) > 20 else tx_hash
|
||||||
|
)
|
||||||
text = (
|
text = (
|
||||||
"✅ PolyWeather 支付确认\n"
|
"✅ PolyWeather 支付确认\n"
|
||||||
f"用户: {user_id}\n"
|
f"用户: {user_id}\n"
|
||||||
@@ -2348,7 +2466,9 @@ class PaymentContractCheckoutService:
|
|||||||
raise PaymentCheckoutError(409, f"intent status is {intent.status}")
|
raise PaymentCheckoutError(409, f"intent status is {intent.status}")
|
||||||
tx_hash_text = str(tx_hash or intent.tx_hash or "").strip().lower()
|
tx_hash_text = str(tx_hash or intent.tx_hash or "").strip().lower()
|
||||||
if intent.status == "failed" and not tx_hash_text:
|
if intent.status == "failed" and not tx_hash_text:
|
||||||
raise PaymentCheckoutError(409, "intent status is failed and tx_hash is missing")
|
raise PaymentCheckoutError(
|
||||||
|
409, "intent status is failed and tx_hash is missing"
|
||||||
|
)
|
||||||
if not tx_hash_text:
|
if not tx_hash_text:
|
||||||
raise PaymentCheckoutError(400, "tx_hash required")
|
raise PaymentCheckoutError(400, "tx_hash required")
|
||||||
if not (tx_hash_text.startswith("0x") and len(tx_hash_text) == 66):
|
if not (tx_hash_text.startswith("0x") and len(tx_hash_text) == 66):
|
||||||
@@ -2378,12 +2498,8 @@ class PaymentContractCheckoutService:
|
|||||||
tx_get = getattr(tx, "get", None)
|
tx_get = getattr(tx, "get", None)
|
||||||
tx_to_raw = tx_get("to") if callable(tx_get) else None
|
tx_to_raw = tx_get("to") if callable(tx_get) else None
|
||||||
tx_from_raw = tx_get("from") if callable(tx_get) else None
|
tx_from_raw = tx_get("from") if callable(tx_get) else None
|
||||||
tx_to = _normalize_address(
|
tx_to = _normalize_address(tx_to_raw or receipt.get("to"))
|
||||||
tx_to_raw or receipt.get("to")
|
tx_from = _normalize_address(tx_from_raw or receipt.get("from"))
|
||||||
)
|
|
||||||
tx_from = _normalize_address(
|
|
||||||
tx_from_raw or receipt.get("from")
|
|
||||||
)
|
|
||||||
if not tx_to or not tx_from:
|
if not tx_to or not tx_from:
|
||||||
raise PaymentCheckoutError(409, "tx indexed partially; retry confirm")
|
raise PaymentCheckoutError(409, "tx indexed partially; retry confirm")
|
||||||
block_number = int(receipt.get("blockNumber") or 0)
|
block_number = int(receipt.get("blockNumber") or 0)
|
||||||
@@ -2396,12 +2512,16 @@ class PaymentContractCheckoutService:
|
|||||||
is_direct = intent.payment_mode == "direct"
|
is_direct = intent.payment_mode == "direct"
|
||||||
if is_direct:
|
if is_direct:
|
||||||
event_match = self._extract_direct_transfer_event(receipt, intent)
|
event_match = self._extract_direct_transfer_event(receipt, intent)
|
||||||
event_payer = _normalize_address(event_match.get("from")) if event_match else None
|
event_payer = (
|
||||||
|
_normalize_address(event_match.get("from")) if event_match else None
|
||||||
|
)
|
||||||
effective_payer = event_payer or tx_from
|
effective_payer = event_payer or tx_from
|
||||||
routed_via_delegate = False
|
routed_via_delegate = False
|
||||||
else:
|
else:
|
||||||
event_match = self._extract_matching_event(receipt, intent)
|
event_match = self._extract_matching_event(receipt, intent)
|
||||||
event_payer = _normalize_address(event_match.get("payer")) if event_match else None
|
event_payer = (
|
||||||
|
_normalize_address(event_match.get("payer")) if event_match else None
|
||||||
|
)
|
||||||
effective_payer = event_payer or tx_from
|
effective_payer = event_payer or tx_from
|
||||||
routed_via_delegate = bool(
|
routed_via_delegate = bool(
|
||||||
event_match and tx_to and tx_to != intent.receiver_address
|
event_match and tx_to and tx_to != intent.receiver_address
|
||||||
@@ -2468,7 +2588,9 @@ class PaymentContractCheckoutService:
|
|||||||
redemption_meta = confirmed_metadata.get("points_redemption")
|
redemption_meta = confirmed_metadata.get("points_redemption")
|
||||||
if isinstance(redemption_meta, dict):
|
if isinstance(redemption_meta, dict):
|
||||||
redemption_meta["consumed"] = bool(points_result.get("points_redeemed"))
|
redemption_meta["consumed"] = bool(points_result.get("points_redeemed"))
|
||||||
redemption_meta["consumed_points"] = int(points_result.get("points_redeemed") or 0)
|
redemption_meta["consumed_points"] = int(
|
||||||
|
points_result.get("points_redeemed") or 0
|
||||||
|
)
|
||||||
redemption_meta["points_after"] = points_result.get("points_after")
|
redemption_meta["points_after"] = points_result.get("points_after")
|
||||||
redemption_meta["consumed_at"] = now_iso
|
redemption_meta["consumed_at"] = now_iso
|
||||||
confirmed_metadata["points_redemption"] = redemption_meta
|
confirmed_metadata["points_redemption"] = redemption_meta
|
||||||
@@ -2522,7 +2644,9 @@ class PaymentContractCheckoutService:
|
|||||||
"payment": repaired.get("payment"),
|
"payment": repaired.get("payment"),
|
||||||
"subscription": repaired.get("subscription"),
|
"subscription": repaired.get("subscription"),
|
||||||
}
|
}
|
||||||
raise PaymentCheckoutError(409, f"intent status is {refreshed.status}, cannot confirm")
|
raise PaymentCheckoutError(
|
||||||
|
409, f"intent status is {refreshed.status}, cannot confirm"
|
||||||
|
)
|
||||||
tx_rows = self._rest(
|
tx_rows = self._rest(
|
||||||
"POST",
|
"POST",
|
||||||
"payment_transactions",
|
"payment_transactions",
|
||||||
@@ -2589,7 +2713,9 @@ class PaymentContractCheckoutService:
|
|||||||
refreshed = self.get_intent(user_id, intent.intent_id)
|
refreshed = self.get_intent(user_id, intent.intent_id)
|
||||||
return {
|
return {
|
||||||
"intent": refreshed.__dict__,
|
"intent": refreshed.__dict__,
|
||||||
"transaction": tx_rows[0] if isinstance(tx_rows, list) and tx_rows else None,
|
"transaction": tx_rows[0]
|
||||||
|
if isinstance(tx_rows, list) and tx_rows
|
||||||
|
else None,
|
||||||
"payment": payment_row,
|
"payment": payment_row,
|
||||||
"subscription": subscription_row,
|
"subscription": subscription_row,
|
||||||
"points_redemption": points_result,
|
"points_redemption": points_result,
|
||||||
@@ -2625,7 +2751,9 @@ class PaymentContractCheckoutService:
|
|||||||
tx_hash_text = str(intent.tx_hash or "").strip().lower()
|
tx_hash_text = str(intent.tx_hash or "").strip().lower()
|
||||||
try:
|
try:
|
||||||
if status in {"submitted", "failed"} and tx_hash_text:
|
if status in {"submitted", "failed"} and tx_hash_text:
|
||||||
result = self.confirm_intent_tx(user_id, intent.intent_id, tx_hash_text)
|
result = self.confirm_intent_tx(
|
||||||
|
user_id, intent.intent_id, tx_hash_text
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"action": "confirmed_submitted_intent"
|
"action": "confirmed_submitted_intent"
|
||||||
@@ -2634,7 +2762,9 @@ class PaymentContractCheckoutService:
|
|||||||
**result,
|
**result,
|
||||||
}
|
}
|
||||||
if status == "confirmed":
|
if status == "confirmed":
|
||||||
repaired = self._ensure_confirm_side_effects(user_id, intent, tx_hash_text)
|
repaired = self._ensure_confirm_side_effects(
|
||||||
|
user_id, intent, tx_hash_text
|
||||||
|
)
|
||||||
refreshed = self.get_intent(user_id, intent.intent_id)
|
refreshed = self.get_intent(user_id, intent.intent_id)
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -2705,5 +2835,6 @@ class PaymentContractCheckoutService:
|
|||||||
"processed_users": len(seen_users),
|
"processed_users": len(seen_users),
|
||||||
"repaired_users": repaired_users,
|
"repaired_users": repaired_users,
|
||||||
}
|
}
|
||||||
PAYMENT_CHECKOUT = PaymentContractCheckoutService()
|
|
||||||
|
|
||||||
|
|
||||||
|
PAYMENT_CHECKOUT = PaymentContractCheckoutService()
|
||||||
|
|||||||
Reference in New Issue
Block a user