Skip to content

Commit d871f6b

Browse files
committed
Merge branch 'main' into release
2 parents 0ac32d6 + d5a065d commit d871f6b

File tree

90 files changed

+2299
-3763
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+2299
-3763
lines changed

.github/workflows/setup/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ runs:
77

88
steps:
99
- name: Setup NodeJS
10-
uses: actions/setup-node@v4
10+
uses: actions/setup-node@v6
1111
with: { node-version-file: .nvmrc }
1212

1313
- name: Install PNPM

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,7 @@ build
4848
.cache
4949

5050
# macOS
51-
.DS_Store
51+
.DS_Store
52+
53+
# React Router
54+
.react-router/

api/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,24 @@
1414
"license": "ISC",
1515
"description": "",
1616
"dependencies": {
17-
"@hono/zod-validator": "^0.7.3",
17+
"@hono/zod-validator": "^0.7.5",
1818
"@interledger/open-payments": "^7.1.3",
1919
"@noble/ed25519": "^3.0.0",
20-
"@paralleldrive/cuid2": "^2.2.2",
20+
"@paralleldrive/cuid2": "^2.3.1",
2121
"@shared/config-storage-service": "workspace:^",
2222
"@shared/probabilistic-revenue-share": "workspace:^",
2323
"@shared/utils": "workspace:^",
2424
"hono": "^4.9.8",
2525
"http-message-signatures": "^1.0.4",
2626
"httpbis-digest-headers": "^1.0.0",
27-
"zod": "^3.25.76"
27+
"zod": "^4.1.13"
2828
},
2929
"types": "./src/types.ts",
3030
"devDependencies": {
3131
"@cloudflare/workers-types": "^4.20251011.0",
3232
"@shared/defines": "workspace:^",
3333
"@shared/types": "workspace:^",
34-
"typescript": "5.9.2",
34+
"typescript": "5.9.3",
3535
"wrangler": "^4.40.0"
3636
}
3737
}

api/src/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { Hono } from 'hono'
22
import { cors } from 'hono/cors'
33
import { HTTPException } from 'hono/http-exception'
44
import { ZodError } from 'zod'
5-
import { serializeError } from './utils/utils.js'
65
import type { KVNamespace } from '@cloudflare/workers-types'
6+
import { serializeError } from './utils/utils.js'
77

88
export type Env = {
99
AWS_ACCESS_KEY_ID: string
@@ -46,7 +46,7 @@ app.onError((error, c) => {
4646
message: 'Validation failed',
4747
code: 'VALIDATION_ERROR',
4848
details: {
49-
issues: error.errors.map((err) => ({
49+
issues: error.issues.map((err) => ({
5050
path: err.path.join('.'),
5151
message: err.message,
5252
code: err.code

api/src/routes/get-config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { zValidator } from '@hono/zod-validator'
21
import { HTTPException } from 'hono/http-exception'
3-
import { z } from 'zod'
2+
import z from 'zod'
3+
import { zValidator } from '@hono/zod-validator'
44
import { ConfigStorageService } from '@shared/config-storage-service'
55
import { AWS_PREFIX } from '@shared/defines'
66
import { PRESET_IDS, TOOLS } from '@shared/types'
@@ -26,7 +26,7 @@ app.get(
2626
zValidator(
2727
'query',
2828
z.object({
29-
wa: z.string().url(),
29+
wa: z.url(),
3030
preset: z.enum(PRESET_IDS)
3131
})
3232
),

api/src/routes/payment.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { zValidator } from '@hono/zod-validator'
22
import { APP_URL } from '@shared/defines'
3-
import { createHTTPException, waitWithAbort } from '../utils/utils'
4-
import { OpenPaymentsService } from '../utils/open-payments.js'
3+
import { KV_PAYMENTS_PREFIX } from '@shared/types'
4+
import { app } from '../app.js'
55
import {
66
PaymentQuoteSchema,
77
PaymentGrantSchema,
88
PaymentFinalizeSchema,
99
PaymentStatusParamSchema
1010
} from '../schemas/payment.js'
11-
import { KV_PAYMENTS_PREFIX } from '@shared/types'
1211
import type { PaymentStatus } from '../types'
13-
import { app } from '../app.js'
12+
import { OpenPaymentsService } from '../utils/open-payments.js'
13+
import { createHTTPException, waitWithAbort } from '../utils/utils'
1414

1515
app.post(
1616
'/payment/quote',

api/src/routes/probabilistic-revshare.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { HTTPException } from 'hono/http-exception'
2-
import { zValidator } from '@hono/zod-validator'
3-
import { z } from 'zod'
42
import type { ContentfulStatusCode } from 'hono/utils/http-status'
3+
import z from 'zod'
4+
import { zValidator } from '@hono/zod-validator'
55
import type { WalletAddress } from '@interledger/open-payments'
66
import { decode, pickWeightedRandom } from '@shared/probabilistic-revenue-share'
77
import { isWalletAddress, validateWalletAddressOrPointer } from '@shared/utils'
8-
import { createHTTPException } from '../utils/utils'
9-
108
import { app } from '../app.js'
9+
import { createHTTPException } from '../utils/utils'
1110

1211
app.get(
1312
'/revshare/:payload',
1413
zValidator(
1514
'param',
1615
z.object({
17-
payload: z.string().base64url().max(50_000).min(20)
16+
payload: z.base64url().max(50_000).min(20)
1817
})
1918
),
2019
async ({ req, json }) => {

api/src/schemas/payment.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import * as z from 'zod/v4'
1+
import z from 'zod'
22

33
export const PaymentQuoteSchema = z.object({
4-
senderWalletAddress: z.string().url('Invalid sender wallet address'),
5-
receiverWalletAddress: z.string().url('Invalid receiver wallet address'),
4+
senderWalletAddress: z.url('Invalid sender wallet address'),
5+
receiverWalletAddress: z.url('Invalid receiver wallet address'),
66
amount: z.number().positive('Amount must be positive'),
77
note: z.string().optional()
88
})
@@ -35,11 +35,11 @@ export const PaymentFinalizeSchema = z.object({
3535
walletAddress: WalletAddressSchema,
3636
pendingGrant: z.object({
3737
interact: z.object({
38-
redirect: z.string().url(),
38+
redirect: z.url(),
3939
finish: z.string()
4040
}),
4141
continue: z.object({
42-
uri: z.string().url(),
42+
uri: z.url(),
4343
access_token: z.object({
4444
value: z.string()
4545
}),
@@ -48,18 +48,18 @@ export const PaymentFinalizeSchema = z.object({
4848
}),
4949
quote: z.object({
5050
id: z.string(),
51-
walletAddress: z.string().url('Invalid wallet address'),
52-
receiver: z.string().url(),
51+
walletAddress: z.url('Invalid wallet address'),
52+
receiver: z.url(),
5353
receiveAmount: AmountSchema,
5454
debitAmount: AmountSchema,
5555
method: z.literal('ilp'),
56-
createdAt: z.string().datetime(),
57-
expiresAt: z.string().datetime().optional()
56+
createdAt: z.iso.datetime(),
57+
expiresAt: z.iso.datetime().optional()
5858
}),
5959
incomingPaymentGrant: z.object({
6060
access_token: z.object({
6161
value: z.string(),
62-
manage: z.string().url(),
62+
manage: z.url(),
6363
expires_in: z.number().int(),
6464
access: z.array(
6565
z.object({
@@ -73,7 +73,7 @@ export const PaymentFinalizeSchema = z.object({
7373
access_token: z.object({
7474
value: z.string()
7575
}),
76-
uri: z.string().url(),
76+
uri: z.url(),
7777
wait: z.number().int().optional()
7878
})
7979
}),

api/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import type z from 'zod'
12
import type {
23
PaymentFinalizeSchema,
34
PaymentGrantSchema,
45
PaymentQuoteSchema,
56
WalletAddressParamSchema
67
} from './schemas/payment.js'
7-
import type { z } from 'zod/v4'
88

99
export type PaymentStatusSuccess = {
1010
paymentId: string

api/src/utils/open-payments.ts

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import {
99
isPendingGrant,
1010
createAuthenticatedClient
1111
} from '@interledger/open-payments'
12+
import { createId } from '@paralleldrive/cuid2'
1213
import { getWalletAddress } from '@shared/utils'
1314
import {
1415
createHeaders,
1516
sleep,
1617
createHTTPException,
1718
urlWithParams
1819
} from './utils.js'
19-
import { createId } from '@paralleldrive/cuid2'
2020
import type { Env } from '../app.js'
2121

2222
export interface Amount {
@@ -52,6 +52,18 @@ type CreateOutgoingPaymentParams = {
5252
paymentId: string
5353
}
5454

55+
const OUTGOING_PAYMENT_POLLING_INITIAL_DELAY = 3000
56+
const OUTGOING_PAYMENT_POLLING_INTERVAL = 1500
57+
const OUTGOING_PAYMENT_POLLING_MAX_ATTEMPTS = 3
58+
59+
function hasCancellationReason(outgoingPayment: OutgoingPayment): boolean {
60+
return (
61+
outgoingPayment.failed &&
62+
typeof outgoingPayment.metadata === 'object' &&
63+
'cancellationReason' in outgoingPayment.metadata
64+
)
65+
}
66+
5567
export class OpenPaymentsService {
5668
private client: AuthenticatedClient | null = null
5769
private static _instance: OpenPaymentsService
@@ -248,11 +260,11 @@ export class OpenPaymentsService {
248260
throw new Error('Could not create outgoing payment.')
249261
})
250262

251-
return await this.checkOutgoingPayment(
252-
outgoingPayment.id,
253-
continuation.access_token.value,
263+
return await this.completePaymentProcess(
264+
quote.receiver,
254265
incomingPaymentGrant,
255-
quote.receiver
266+
outgoingPayment.id,
267+
continuation.access_token.value
256268
)
257269
}
258270

@@ -417,28 +429,46 @@ export class OpenPaymentsService {
417429
}
418430
}
419431

420-
private async checkOutgoingPayment(
421-
outgoingPaymentId: OutgoingPayment['id'],
422-
continuationAccessToken: string,
432+
private async completePaymentProcess(
433+
incomingPaymentId: string,
423434
incomingPaymentGrant: Grant,
424-
incomingPaymentId: string
435+
outgoingPaymentId: OutgoingPayment['id'],
436+
continuationAccessToken: string
425437
): Promise<CheckPaymentResult> {
426-
await sleep(3000)
438+
let attempts = 0
439+
await sleep(OUTGOING_PAYMENT_POLLING_INITIAL_DELAY)
440+
while (++attempts <= OUTGOING_PAYMENT_POLLING_MAX_ATTEMPTS) {
441+
const outgoingPayment = await this.client!.outgoingPayment.get({
442+
url: outgoingPaymentId,
443+
accessToken: continuationAccessToken
444+
})
427445

428-
const outgoingPayment = await this.client!.outgoingPayment.get({
429-
url: outgoingPaymentId,
430-
accessToken: continuationAccessToken
431-
})
446+
if (hasCancellationReason(outgoingPayment)) {
447+
return {
448+
success: false,
449+
error: {
450+
code: 'CANCELLATION_REASON',
451+
message: `Payment aborted due to: ${outgoingPayment.metadata?.cancellationReason}`
452+
}
453+
}
454+
}
455+
456+
if (
457+
outgoingPayment.debitAmount.value === outgoingPayment.sentAmount.value
458+
) {
459+
break
460+
}
432461

433-
// get outgoing payment, to check if there was enough balance
434-
if (!(Number(outgoingPayment.sentAmount.value) > 0)) {
435-
return {
436-
success: false,
437-
error: {
438-
code: 'INSUFFICIENT_BALANCE',
439-
message: 'Insufficient funds. Check your balance and try again.'
462+
if (attempts === OUTGOING_PAYMENT_POLLING_MAX_ATTEMPTS) {
463+
return {
464+
success: false,
465+
error: {
466+
code: 'OUTGOING_PAYMENT_INCOMPLETE',
467+
message: 'The payment did not complete within the expected time.'
468+
}
440469
}
441470
}
471+
await sleep(OUTGOING_PAYMENT_POLLING_INTERVAL)
442472
}
443473

444474
try {
@@ -458,7 +488,7 @@ export class OpenPaymentsService {
458488
(error) => {
459489
throw createHTTPException(
460490
500,
461-
'Could not revoke incoming payment grant. ',
491+
'Could not revoke incoming payment grant.',
462492
error
463493
)
464494
}

0 commit comments

Comments
 (0)