feat: Implement a new contract-based payment system with wallet binding, payment intents, and subscription plan management.

This commit is contained in:
2569718930@qq.com
2026-03-14 10:35:30 +08:00
parent be61c39795
commit 02e35a3a5f
10 changed files with 698 additions and 9 deletions
+63 -3
View File
@@ -125,6 +125,14 @@ type CreatedIntent = {
};
};
type IntentStatusResponse = {
intent?: {
intent_id?: string;
status?: string;
tx_hash?: string | null;
};
};
declare global {
interface Window {
ethereum?: EvmProvider;
@@ -398,7 +406,7 @@ function normalizePaymentError(error: unknown): NormalizedPaymentError {
?.trim();
const lower = String(rawMessage || "").toLowerCase();
if (lower.includes("confirm pending")) {
if (lower.includes("confirm pending") || lower.includes("payment pending timeout")) {
return {
message: "链上交易已提交,正在确认中,请稍后刷新查看状态。",
pending: true,
@@ -940,6 +948,55 @@ export function AccountCenter() {
throw new Error(`transaction confirmation timeout: ${txHash}`);
};
const pollIntentUntilConfirmed = useCallback(
async (
intentId: string,
authHeaders: Record<string, string>,
txHashHint = "",
timeoutMs = 180000,
pollMs = 5000,
) => {
const startedAt = Date.now();
const shortTx = shortAddress(txHashHint);
while (Date.now() - startedAt < timeoutMs) {
const statusRes = await fetch(`/api/payments/intents/${intentId}`, {
method: "GET",
headers: authHeaders,
cache: "no-store",
});
if (!statusRes.ok) {
if (statusRes.status >= 500 || statusRes.status === 429) {
await new Promise((resolve) => setTimeout(resolve, pollMs));
continue;
}
const raw = (await statusRes.text()).slice(0, 260);
throw new Error(`query intent failed: ${raw}`);
}
const statusJson = (await statusRes.json()) as IntentStatusResponse;
const intent = statusJson.intent || {};
const status = String(intent.status || "").toLowerCase();
const txHash = String(intent.tx_hash || txHashHint || "").toLowerCase();
if (status === "confirmed") {
setPaymentError("");
setPaymentInfo(`支付确认成功,交易: ${shortAddress(txHash)}`);
await loadSnapshot();
await loadPaymentSnapshot();
return;
}
if (status === "failed" || status === "cancelled" || status === "expired") {
throw new Error(`payment ${status}`);
}
setPaymentInfo(
`交易已提交: ${shortTx},正在链上确认(状态: ${status || "submitted"}...`,
);
await new Promise((resolve) => setTimeout(resolve, pollMs));
}
throw new Error("payment pending timeout");
},
[loadPaymentSnapshot, loadSnapshot],
);
const signBindMessage = async (
eth: EvmProvider,
address: string,
@@ -1296,8 +1353,11 @@ export function AccountCenter() {
(lowerRaw.includes("confirmations not enough") ||
lowerRaw.includes("tx indexed partially")));
if (maybePending) {
setPaymentInfo(`交易已提交: ${shortAddress(txHashNorm)},等待确认中。`);
throw new Error(`confirm pending: ${raw}`);
setPaymentInfo(
`交易已提交: ${shortAddress(txHashNorm)},等待链上确认中...`,
);
await pollIntentUntilConfirmed(intentId, authHeaders, txHashNorm);
return;
}
throw new Error(`confirm failed: ${raw}`);
}