Skip to content

Commit d474715

Browse files
committed
Stablecoin payments
1 parent 6eb39b3 commit d474715

12 files changed

Lines changed: 1677 additions & 8 deletions

File tree

.kiro/specs/workwork-ledger-mvp/tasks.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,17 +241,17 @@
241241
- **属性 16: 多链地址独立性**
242242
- **验证: 需求 7.1**
243243

244-
- [ ] 15. 稳定币支付流程
245-
- [ ] 15.1 实现稳定币支付选项 UI
244+
- [x] 15. 稳定币支付流程
245+
- [x] 15.1 实现稳定币支付选项 UI
246246
- 链选择、地址显示、QR 码生成
247247
- _需求: 6.3_
248-
- [ ] 15.2 配置 Alchemy Webhooks
248+
- [x] 15.2 配置 Alchemy Webhooks
249249
- 监听 USDC/USDT 转账事件
250250
- _需求: 6.5_
251-
- [ ] 15.3 实现链上交易验证
251+
- [x] 15.3 实现链上交易验证
252252
- 验证金额、确认数
253253
- _需求: 6.5, 7.4_
254-
- [ ] 15.4 实现稳定币支付成功处理
254+
- [x] 15.4 实现稳定币支付成功处理
255255
- 更新发票状态
256256
- 创建包含链信息的账本条目
257257
- _需求: 6.5, 7.2_

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"otpauth": "^9.4.1",
2929
"prisma": "^6.19.1",
3030
"qrcode": "^1.5.4",
31+
"qrcode.react": "^4.2.0",
3132
"react": "^18",
3233
"react-dom": "^18",
3334
"resend": "^6.6.0",
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Crypto Payment Configuration API
3+
* Returns wallet addresses and supported chains/assets for crypto payments
4+
* _需求: 6.3_
5+
*/
6+
7+
import { NextRequest, NextResponse } from 'next/server';
8+
import { prisma } from '@/lib/prisma';
9+
import { validatePaymentToken } from '@/server/payment/payment-link';
10+
import type { Chain, StablecoinAsset } from '@/types/domain';
11+
12+
interface CryptoSettings {
13+
enabled: boolean;
14+
supportedChains: Chain[];
15+
supportedAssets: StablecoinAsset[];
16+
}
17+
18+
/**
19+
* GET /api/pay/[token]/crypto-config
20+
* Returns crypto payment configuration for an invoice
21+
*/
22+
export async function GET(
23+
request: NextRequest,
24+
{ params }: { params: Promise<{ token: string }> }
25+
) {
26+
try {
27+
const { token } = await params;
28+
29+
if (!token) {
30+
return NextResponse.json(
31+
{ error: 'Invalid payment token' },
32+
{ status: 400 }
33+
);
34+
}
35+
36+
// Validate the payment token
37+
const result = await validatePaymentToken(token);
38+
39+
if (!result.valid) {
40+
const errorMessages: Record<string, string> = {
41+
not_found: 'Invoice not found',
42+
expired: 'Payment link has expired',
43+
already_paid: 'Invoice has already been paid',
44+
cancelled: 'Invoice has been cancelled',
45+
};
46+
return NextResponse.json(
47+
{ error: errorMessages[result.error] || 'Invalid payment link' },
48+
{ status: 400 }
49+
);
50+
}
51+
52+
const { invoice } = result;
53+
54+
// Check if crypto payment is allowed
55+
if (!invoice.allowCryptoPayment) {
56+
return NextResponse.json(
57+
{ error: 'Crypto payment is not enabled for this invoice' },
58+
{ status: 400 }
59+
);
60+
}
61+
62+
// Get the invoice owner's user ID and crypto settings
63+
const invoiceRecord = await prisma.invoice.findFirst({
64+
where: { paymentToken: token },
65+
select: {
66+
userId: true,
67+
user: {
68+
select: {
69+
settings: {
70+
select: {
71+
cryptoSettings: true,
72+
},
73+
},
74+
},
75+
},
76+
},
77+
});
78+
79+
if (!invoiceRecord) {
80+
return NextResponse.json(
81+
{ error: 'Invoice not found' },
82+
{ status: 404 }
83+
);
84+
}
85+
86+
// Parse crypto settings
87+
const cryptoSettings = invoiceRecord.user.settings?.cryptoSettings as CryptoSettings | null;
88+
89+
if (!cryptoSettings?.enabled) {
90+
return NextResponse.json(
91+
{ error: 'Crypto payment is not configured for this merchant' },
92+
{ status: 400 }
93+
);
94+
}
95+
96+
const supportedChains = cryptoSettings.supportedChains || [];
97+
const supportedAssets = cryptoSettings.supportedAssets || [];
98+
99+
if (supportedChains.length === 0 || supportedAssets.length === 0) {
100+
return NextResponse.json(
101+
{ error: 'No crypto payment options configured' },
102+
{ status: 400 }
103+
);
104+
}
105+
106+
// Get wallet addresses for the user
107+
const walletAddresses = await prisma.walletAddress.findMany({
108+
where: {
109+
userId: invoiceRecord.userId,
110+
chain: { in: supportedChains },
111+
asset: { in: supportedAssets },
112+
},
113+
select: {
114+
address: true,
115+
chain: true,
116+
asset: true,
117+
},
118+
});
119+
120+
return NextResponse.json({
121+
addresses: walletAddresses.map((addr) => ({
122+
address: addr.address,
123+
chain: addr.chain as Chain,
124+
asset: addr.asset as StablecoinAsset,
125+
})),
126+
supportedChains,
127+
supportedAssets,
128+
});
129+
} catch (error) {
130+
console.error('Crypto config error:', error);
131+
return NextResponse.json(
132+
{ error: 'Failed to load crypto payment configuration' },
133+
{ status: 500 }
134+
);
135+
}
136+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Crypto Payment Status API
3+
* Returns the current status of a crypto payment for an invoice
4+
* _需求: 6.5, 7.4_
5+
*/
6+
7+
import { NextRequest, NextResponse } from 'next/server';
8+
import { prisma } from '@/lib/prisma';
9+
import { validatePaymentToken } from '@/server/payment/payment-link';
10+
import { getVerificationService } from '@/server/payment/crypto-verification.service';
11+
import type { Chain } from '@/types/domain';
12+
13+
/**
14+
* GET /api/pay/[token]/crypto-status
15+
* Returns the current status of a crypto payment
16+
*/
17+
export async function GET(
18+
request: NextRequest,
19+
{ params }: { params: Promise<{ token: string }> }
20+
) {
21+
try {
22+
const { token } = await params;
23+
24+
if (!token) {
25+
return NextResponse.json(
26+
{ error: 'Invalid payment token' },
27+
{ status: 400 }
28+
);
29+
}
30+
31+
// Validate the payment token
32+
const result = await validatePaymentToken(token);
33+
34+
// If invoice is already paid, return success status
35+
if (!result.valid && result.error === 'already_paid') {
36+
return NextResponse.json({
37+
status: 'paid',
38+
message: 'Invoice has been paid',
39+
});
40+
}
41+
42+
if (!result.valid) {
43+
const errorMessages: Record<string, string> = {
44+
not_found: 'Invoice not found',
45+
expired: 'Payment link has expired',
46+
cancelled: 'Invoice has been cancelled',
47+
};
48+
return NextResponse.json(
49+
{ error: errorMessages[result.error] || 'Invalid payment link' },
50+
{ status: 400 }
51+
);
52+
}
53+
54+
const { invoice } = result;
55+
56+
// Get the payment record with crypto details
57+
const payment = await prisma.payment.findFirst({
58+
where: {
59+
invoice: {
60+
paymentToken: token,
61+
},
62+
type: 'crypto',
63+
},
64+
include: {
65+
cryptoPayment: true,
66+
},
67+
});
68+
69+
// No crypto payment initiated yet
70+
if (!payment || !payment.cryptoPayment) {
71+
return NextResponse.json({
72+
status: 'waiting',
73+
message: 'Waiting for payment',
74+
});
75+
}
76+
77+
const cryptoPayment = payment.cryptoPayment;
78+
79+
// If payment is already succeeded
80+
if (payment.status === 'succeeded') {
81+
return NextResponse.json({
82+
status: 'paid',
83+
message: 'Payment confirmed',
84+
details: {
85+
chain: cryptoPayment.chain,
86+
asset: cryptoPayment.asset,
87+
txHash: cryptoPayment.txHash,
88+
confirmations: cryptoPayment.confirmations,
89+
},
90+
});
91+
}
92+
93+
// If we have a transaction hash, check its status
94+
if (cryptoPayment.txHash) {
95+
try {
96+
const verificationService = getVerificationService();
97+
const confirmationStatus = await verificationService.getConfirmationStatus(
98+
cryptoPayment.txHash,
99+
cryptoPayment.chain as Chain
100+
);
101+
102+
// Update confirmations in database
103+
await prisma.cryptoPayment.update({
104+
where: { id: cryptoPayment.id },
105+
data: { confirmations: confirmationStatus.confirmations },
106+
});
107+
108+
if (confirmationStatus.isConfirmed) {
109+
return NextResponse.json({
110+
status: 'confirmed',
111+
message: 'Payment confirmed',
112+
details: {
113+
chain: cryptoPayment.chain,
114+
asset: cryptoPayment.asset,
115+
txHash: cryptoPayment.txHash,
116+
confirmations: confirmationStatus.confirmations,
117+
required: confirmationStatus.required,
118+
},
119+
});
120+
}
121+
122+
return NextResponse.json({
123+
status: 'confirming',
124+
message: `Waiting for confirmations (${confirmationStatus.confirmations}/${confirmationStatus.required})`,
125+
details: {
126+
chain: cryptoPayment.chain,
127+
asset: cryptoPayment.asset,
128+
txHash: cryptoPayment.txHash,
129+
confirmations: confirmationStatus.confirmations,
130+
required: confirmationStatus.required,
131+
},
132+
});
133+
} catch (error) {
134+
console.error('Error checking transaction status:', error);
135+
// Return pending status if we can't verify
136+
return NextResponse.json({
137+
status: 'pending',
138+
message: 'Verifying transaction...',
139+
details: {
140+
chain: cryptoPayment.chain,
141+
asset: cryptoPayment.asset,
142+
txHash: cryptoPayment.txHash,
143+
},
144+
});
145+
}
146+
}
147+
148+
// Payment record exists but no transaction yet
149+
return NextResponse.json({
150+
status: 'waiting',
151+
message: 'Waiting for payment',
152+
});
153+
} catch (error) {
154+
console.error('Crypto status error:', error);
155+
return NextResponse.json(
156+
{ error: 'Failed to check payment status' },
157+
{ status: 500 }
158+
);
159+
}
160+
}

0 commit comments

Comments
 (0)