Skip to content

Commit 086d2a9

Browse files
committed
Merge branch 'main' of github.com:efdevcon/monorepo
2 parents 316a934 + 140384a commit 086d2a9

5 files changed

Lines changed: 375 additions & 49 deletions

File tree

devcon/src/config/ticketing.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ const ENV_CONFIG = {
2121
recipientAddress: '0xA163a78C0b811A984fFe1B98b4b1b95BAb24aAcD',
2222
cryptoDiscountPercent: 3,
2323
},
24+
tax: {
25+
vatPercent: 18,
26+
label: 'GST',
27+
},
2428
self: {
2529
scope: 'devcon-india-local-discount',
2630
staging: true,
@@ -62,6 +66,10 @@ const ENV_CONFIG = {
6266
// recipientAddress: '0xFc488aE9cB395B150574Aa5ce8a321c9100b1ee3',
6367
cryptoDiscountPercent: 3,
6468
},
69+
tax: {
70+
vatPercent: 18,
71+
label: 'GST',
72+
},
6573
self: {
6674
scope: 'devcon-india-local-discount',
6775
// TODO: replace with production staging

devcon/src/pages/api/x402/tickets/verify.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* - Ticket details
1515
*/
1616
import type { NextApiRequest, NextApiResponse } from 'next'
17-
import { verifyPayment, verifyPaymentDirect, verifyPaymentNativeEth, getPaymentRecipient, usdToUsdcAmount, encodeSettlementResponseHeader } from 'services/x402'
17+
import { verifyPayment, verifyPaymentDirect, verifyPaymentNativeEth, getPaymentRecipient, usdToUsdcAmount, encodeSettlementResponseHeader, getPublicClientForChainId } from 'services/x402'
1818
import { getUsdcConfig } from 'services/relayer'
1919
import type { SettleResponse } from 'types/x402'
2020
import { createOrder, confirmOrderPayment } from 'services/pretix'
@@ -52,6 +52,22 @@ interface VerifyRequest {
5252
symbol?: string
5353
/** Token contract address. Required for multi-token chains (e.g. USDC vs USDT0 on Arbitrum). Falls back to USDC if omitted. */
5454
tokenAddress?: string
55+
/**
56+
* EIP-191 signature proving the caller owns the `payer` wallet.
57+
* REQUIRED when `symbol === 'ETH'`. Prevents cross-order tx reuse attacks
58+
* on the native ETH path (USDC/USDT0 are already bound via EIP-3009).
59+
*/
60+
ethPayerSignature?: string
61+
}
62+
63+
/** Build the ETH-only message that the payer signs before their /verify call. */
64+
function buildEthPayerMessage(paymentReference: string, payer: string, chainId: number): string {
65+
return (
66+
'Devcon ticket payment (ETH)\n' +
67+
`Payment reference: ${paymentReference}\n` +
68+
`Payer: ${payer}\n` +
69+
`Chain: ${chainId}`
70+
)
5571
}
5672

5773
interface VerifySuccessResponse {
@@ -197,6 +213,55 @@ export default async function handler(
197213
})
198214
}
199215

216+
// For native ETH, require a signature over paymentReference+payer+chainId to
217+
// cryptographically prove the caller owns the `payer` wallet. USDC/USDT0 are
218+
// already bound via EIP-3009 at prepare-authorization time so no extra
219+
// signature is needed for those paths.
220+
if (body.symbol === 'ETH') {
221+
if (!body.ethPayerSignature || typeof body.ethPayerSignature !== 'string') {
222+
return res.status(400).json({
223+
success: false,
224+
error: 'ethPayerSignature is required for native ETH payments',
225+
})
226+
}
227+
if (body.chainId == null) {
228+
return res.status(400).json({
229+
success: false,
230+
error: 'chainId is required for native ETH payments',
231+
})
232+
}
233+
const message = buildEthPayerMessage(body.paymentReference, body.payer, body.chainId)
234+
// Use verifyMessage which handles BOTH EOA (ECDSA recovery) and smart
235+
// wallet (ERC-1271 isValidSignature) signatures. For contracts it calls
236+
// isValidSignature on the payer address and checks the 0x1626ba7e magic value.
237+
const client = getPublicClientForChainId(body.chainId)
238+
if (!client) {
239+
return res.status(400).json({
240+
success: false,
241+
error: `Unsupported chainId for signature verification: ${body.chainId}`,
242+
})
243+
}
244+
let sigValid = false
245+
try {
246+
sigValid = await client.verifyMessage({
247+
address: body.payer as `0x${string}`,
248+
message,
249+
signature: body.ethPayerSignature as `0x${string}`,
250+
})
251+
} catch (e) {
252+
return res.status(400).json({
253+
success: false,
254+
error: `ethPayerSignature verification failed: ${(e as Error).message}`,
255+
})
256+
}
257+
if (!sigValid) {
258+
return res.status(403).json({
259+
success: false,
260+
error: 'ethPayerSignature does not match the payer address',
261+
})
262+
}
263+
}
264+
200265
// Verify payment on-chain
201266
const expectedAmount = usdToUsdcAmount(pendingOrder.totalUsd)
202267
const paymentProof: X402PaymentProof = {
@@ -211,12 +276,16 @@ export default async function handler(
211276
console.log('[Verify] Attempting primary verification via x402 service', body.chainId != null ? `(chain ${body.chainId})` : '')
212277
let verification = await verifyPayment(paymentProof)
213278

214-
// Only try native ETH when client indicates ETH payment; otherwise we'd wrongly run it for USDC and fail (from/to mismatch)
279+
// Only try native ETH when client indicates ETH payment; otherwise we'd wrongly run it for USDC and fail (from/to mismatch).
280+
// Error prefix match (not exact) because verifyPayment may append diagnostic detail to the message.
215281
if (
216282
!verification.verified &&
217283
body.symbol === 'ETH' &&
218284
body.chainId != null &&
219-
(verification.error === 'No matching transfer found in transaction' || verification.error === 'Invalid payment reference')
285+
(
286+
verification.error?.startsWith('No matching transfer found in transaction') ||
287+
verification.error === 'Invalid payment reference'
288+
)
220289
) {
221290
const expectedAmountWei = pendingOrder.expectedEthAmountWeiByChain?.[String(body.chainId)]
222291
if (!expectedAmountWei) {

devcon/src/pages/tickets/store/checkout.module.scss

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,12 +1442,21 @@ div.wallet-identicon {
14421442

14431443
.mobile-order-bar-total {
14441444
display: flex;
1445-
align-items: baseline;
1445+
flex-direction: column;
1446+
align-items: flex-end;
1447+
gap: 4px;
14461448
font-size: 16px;
14471449
font-weight: 700;
14481450
color: #1a0d33;
14491451
}
14501452

1453+
.mobile-order-bar-tax {
1454+
font-size: 12px;
1455+
font-weight: 400;
1456+
line-height: 16px;
1457+
color: #594d73;
1458+
}
1459+
14511460
.mobile-order-expanded {
14521461
background: #f2f1f4;
14531462
border-bottom: 1px solid $checkout-border;
@@ -1509,7 +1518,9 @@ div.wallet-identicon {
15091518

15101519
.mobile-inline-summary-total {
15111520
display: flex;
1512-
align-items: baseline;
1521+
flex-direction: column;
1522+
align-items: flex-end;
1523+
gap: 4px;
15131524
font-size: 16px;
15141525
font-weight: 700;
15151526
color: #160b2b;
@@ -1625,6 +1636,11 @@ div.wallet-identicon {
16251636
color: #594d73;
16261637
}
16271638

1639+
.panel-item-tax {
1640+
font-weight: 400;
1641+
color: #160b2b;
1642+
}
1643+
16281644
/* ─── Discount Row ──────────────────────────────────────── */
16291645

16301646
.discount-section {

0 commit comments

Comments
 (0)