Skip to content

Commit dd0ff31

Browse files
committed
Merge branch 'main' into release
2 parents e117e77 + 8e96f68 commit dd0ff31

File tree

24 files changed

+402
-164
lines changed

24 files changed

+402
-164
lines changed

api/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "1.0.0",
44
"scripts": {
55
"predev": "node -e \"require('fs').copyFileSync('../.dev.vars', '.dev.vars');\"",
6-
"dev": "wrangler dev",
6+
"dev": "wrangler dev --persist-to ../.wrangler",
77
"build": "wrangler deploy --dry-run --outdir dist",
88
"preview": "wrangler dev",
99
"typecheck": "tsc --noEmit",
@@ -28,8 +28,9 @@
2828
},
2929
"types": "./src/types.ts",
3030
"devDependencies": {
31-
"@shared/types": "workspace:^",
31+
"@cloudflare/workers-types": "^4.20251011.0",
3232
"@shared/defines": "workspace:^",
33+
"@shared/types": "workspace:^",
3334
"typescript": "5.9.2",
3435
"wrangler": "^4.40.0"
3536
}

api/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { cors } from 'hono/cors'
33
import { HTTPException } from 'hono/http-exception'
44
import { ZodError } from 'zod'
55
import { serializeError } from './utils/utils.js'
6+
import type { KVNamespace } from '@cloudflare/workers-types'
67

78
export type Env = {
89
AWS_ACCESS_KEY_ID: string
@@ -11,6 +12,7 @@ export type Env = {
1112
OP_WALLET_ADDRESS: string
1213
OP_PRIVATE_KEY: string
1314
OP_KEY_ID: string
15+
PUBLISHER_TOOLS_KV: KVNamespace
1416
}
1517

1618
export const app = new Hono<{ Bindings: Env }>()

api/src/routes/payment.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { zValidator } from '@hono/zod-validator'
2-
32
import { APP_URL } from '@shared/defines'
4-
import { createHTTPException } from '../utils/utils'
3+
import { createHTTPException, waitWithAbort } from '../utils/utils'
54
import { OpenPaymentsService } from '../utils/open-payments.js'
65
import {
76
PaymentQuoteSchema,
87
PaymentGrantSchema,
9-
PaymentFinalizeSchema
8+
PaymentFinalizeSchema,
9+
PaymentStatusParamSchema
1010
} from '../schemas/payment.js'
11-
11+
import { KV_PAYMENTS_PREFIX } from '@shared/types'
12+
import type { PaymentStatus } from '../types'
1213
import { app } from '../app.js'
1314

1415
app.post(
@@ -91,6 +92,44 @@ app.post(
9192
}
9293
)
9394

95+
app.get(
96+
'/payment/status/:paymentId',
97+
zValidator('param', PaymentStatusParamSchema),
98+
async ({ req, json, env }) => {
99+
const { paymentId } = req.param()
100+
101+
const POLLING_MAX_DURATION = 25000
102+
const POLLING_INTERVAL = 1500
103+
const signal = AbortSignal.timeout(POLLING_MAX_DURATION)
104+
105+
try {
106+
while (!signal.aborted) {
107+
await waitWithAbort(POLLING_INTERVAL, signal)
108+
109+
const status = await env.PUBLISHER_TOOLS_KV.get<PaymentStatus>(
110+
KV_PAYMENTS_PREFIX + paymentId,
111+
'json'
112+
)
113+
114+
if (status) {
115+
return json({
116+
type: 'GRANT_INTERACTION',
117+
...status
118+
})
119+
}
120+
}
121+
122+
throw new Error('AbortError')
123+
} catch (error) {
124+
if (error instanceof Error && error.message === 'TimeoutError') {
125+
throw createHTTPException(408, 'Payment status polling timeout', {})
126+
}
127+
128+
throw createHTTPException(404, 'Failed to retrieve data', error)
129+
}
130+
}
131+
)
132+
94133
function isAllowedRedirectUrl(redirectUrl: string) {
95134
const redirectUrlOrigin = new URL(redirectUrl).origin
96135
const ALLOWED_ORIGINS = Object.values(APP_URL)

api/src/schemas/payment.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ export const PaymentFinalizeSchema = z.object({
8181
note: z.string().optional().default('Tools payment')
8282
})
8383

84+
export const PaymentStatusParamSchema = z.object({
85+
paymentId: z
86+
.string()
87+
.min(1, 'Payment ID is required')
88+
.max(100, 'Payment ID invalid')
89+
})
90+
8491
export const WalletAddressParamSchema = z.object({
8592
wa: z.string().min(1, 'Wallet address is required'),
8693
version: z.string().optional().default('default')

api/src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ import type {
66
} from './schemas/payment.js'
77
import type { z } from 'zod/v4'
88

9+
export type PaymentStatusSuccess = {
10+
paymentId: string
11+
hash: string
12+
interact_ref: string
13+
}
14+
15+
export type PaymentStatusRejected = {
16+
paymentId: string
17+
result: 'grant_rejected'
18+
}
19+
20+
export type PaymentStatus = PaymentStatusSuccess | PaymentStatusRejected
21+
922
export type PaymentQuoteInput = z.infer<typeof PaymentQuoteSchema>
1023
export type PaymentGrantInput = z.infer<typeof PaymentGrantSchema>
1124
export type PaymentFinalizeInput = z.infer<typeof PaymentFinalizeSchema>

api/src/utils/open-payments.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
type PendingGrant,
33
type WalletAddress,
44
type AuthenticatedClient,
5+
type OutgoingPayment,
56
type Quote,
67
type Grant,
78
isFinalizedGrant,
@@ -11,7 +12,7 @@ import {
1112
import { getWalletAddress } from '@shared/utils'
1213
import {
1314
createHeaders,
14-
timeout,
15+
sleep,
1516
createHTTPException,
1617
urlWithParams
1718
} from './utils.js'
@@ -193,7 +194,7 @@ export class OpenPaymentsService {
193194
debitAmount: Amount
194195
receiveAmount: Amount
195196
redirectUrl: string
196-
}): Promise<PendingGrant> {
197+
}): Promise<{ grant: PendingGrant; paymentId: string }> {
197198
const clientNonce = crypto.randomUUID()
198199
const paymentId = createId()
199200

@@ -206,7 +207,7 @@ export class OpenPaymentsService {
206207
redirectUrl: args.redirectUrl
207208
})
208209

209-
return outgoingPaymentGrant
210+
return { grant: outgoingPaymentGrant, paymentId }
210211
}
211212

212213
async finishPaymentProcess(
@@ -419,22 +420,20 @@ export class OpenPaymentsService {
419420
}
420421

421422
private async checkOutgoingPayment(
422-
finishPaymentUrl: string,
423+
outgoingPaymentId: OutgoingPayment['id'],
423424
continuationAccessToken: string,
424425
incomingPaymentGrant: Grant,
425426
incomingPaymentId: string
426427
): Promise<CheckPaymentResult> {
427-
await timeout(3000)
428+
await sleep(3000)
428429

429-
// get outgoing payment, to check if there was enough balance
430-
const checkOutgoingPaymentResponse = await this.client!.outgoingPayment.get(
431-
{
432-
url: finishPaymentUrl,
433-
accessToken: continuationAccessToken
434-
}
435-
)
430+
const outgoingPayment = await this.client!.outgoingPayment.get({
431+
url: outgoingPaymentId,
432+
accessToken: continuationAccessToken
433+
})
436434

437-
if (!(Number(checkOutgoingPaymentResponse.sentAmount.value) > 0)) {
435+
// get outgoing payment, to check if there was enough balance
436+
if (!(Number(outgoingPayment.sentAmount.value) > 0)) {
438437
return {
439438
success: false,
440439
error: {

api/src/utils/utils.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,26 @@ interface SignOptions {
2828
keyId: string
2929
}
3030

31-
export function timeout(delay: number): Promise<void> {
32-
return new Promise((resolve) => setTimeout(resolve, delay))
31+
export function sleep(delay: number): Promise<void> {
32+
return new Promise((r) => setTimeout(r, delay))
33+
}
34+
35+
export function waitWithAbort(ms: number, signal: AbortSignal): Promise<void> {
36+
return new Promise((resolve, reject) => {
37+
if (signal.aborted) {
38+
reject(new Error('TimeoutError'))
39+
return
40+
}
41+
42+
const timer = setTimeout(resolve, ms)
43+
44+
const onAbort = () => {
45+
clearTimeout(timer)
46+
reject(new Error('TimeoutError'))
47+
}
48+
49+
signal.addEventListener('abort', onAbort, { once: true })
50+
})
3351
}
3452

3553
export async function createHeaders({

api/wrangler.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@ main = "src/index.ts"
33
compatibility_date = "2024-09-23"
44
compatibility_flags = ["nodejs_compat"]
55

6+
kv_namespaces = [
7+
{ binding = "PUBLISHER_TOOLS_KV", id = "b00031e45d5945c78e1eb3df6336e54a", preview_id = "preview_publisher_tools_kv" }
8+
]
9+
610
[dev]
711
port = 8787

components/src/widget/controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface WidgetState {
1515
incomingPaymentGrant: Grant
1616
quote: Quote
1717
outgoingPaymentGrant: PendingGrant
18+
paymentId: string
1819
debitAmount: string
1920
receiveAmount: string
2021
receiverPublicName?: string

components/src/widget/views/confirmation/confirmation.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,15 @@ export class PaymentConfirmation extends LitElement {
220220
private async handlePaymentConfirmed() {
221221
try {
222222
const { walletAddress, quote } = this.configController.state
223-
const outgoingPaymentGrant = await this.requestOutgoingGrant({
223+
const { grant, paymentId } = await this.requestOutgoingGrant({
224224
walletAddress,
225225
debitAmount: quote.debitAmount,
226226
receiveAmount: quote.receiveAmount
227227
})
228228

229229
this.configController.updateState({
230-
outgoingPaymentGrant,
230+
outgoingPaymentGrant: grant,
231+
paymentId,
231232
note: this.note
232233
})
233234

@@ -255,7 +256,7 @@ export class PaymentConfirmation extends LitElement {
255256
walletAddress: WalletAddress
256257
debitAmount: Amount
257258
receiveAmount: Amount
258-
}): Promise<PendingGrant> {
259+
}): Promise<{ grant: PendingGrant; paymentId: string }> {
259260
const { apiUrl, frontendUrl } = this.configController.config
260261
const url = new URL('/payment/grant', apiUrl).href
261262
const redirectUrl = new URL('payment-confirmation', frontendUrl).href

0 commit comments

Comments
 (0)