@brninpay/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/LICENSE +203 -0
- package/README.md +7 -0
- package/dist/decision-resolver.d.ts +5 -0
- package/dist/decision-resolver.d.ts.map +1 -0
- package/dist/decision-resolver.js +20 -0
- package/dist/decision-resolver.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/policies/allowlist.d.ts +3 -0
- package/dist/policies/allowlist.d.ts.map +1 -0
- package/dist/policies/allowlist.js +21 -0
- package/dist/policies/allowlist.js.map +1 -0
- package/dist/policies/budget-check.d.ts +3 -0
- package/dist/policies/budget-check.d.ts.map +1 -0
- package/dist/policies/budget-check.js +22 -0
- package/dist/policies/budget-check.js.map +1 -0
- package/dist/policies/index.d.ts +5 -0
- package/dist/policies/index.d.ts.map +1 -0
- package/dist/policies/index.js +5 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/max-per-call.d.ts +3 -0
- package/dist/policies/max-per-call.d.ts.map +1 -0
- package/dist/policies/max-per-call.js +20 -0
- package/dist/policies/max-per-call.js.map +1 -0
- package/dist/policies/rate-limit.d.ts +3 -0
- package/dist/policies/rate-limit.d.ts.map +1 -0
- package/dist/policies/rate-limit.js +33 -0
- package/dist/policies/rate-limit.js.map +1 -0
- package/dist/policy-engine.d.ts +8 -0
- package/dist/policy-engine.d.ts.map +1 -0
- package/dist/policy-engine.js +39 -0
- package/dist/policy-engine.js.map +1 -0
- package/dist/types/errors.d.ts +80 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +150 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/payment.d.ts +55 -0
- package/dist/types/payment.d.ts.map +1 -0
- package/dist/types/payment.js +188 -0
- package/dist/types/payment.js.map +1 -0
- package/dist/types/policy.d.ts +58 -0
- package/dist/types/policy.d.ts.map +1 -0
- package/dist/types/policy.js +194 -0
- package/dist/types/policy.js.map +1 -0
- package/dist/types/wallet.d.ts +66 -0
- package/dist/types/wallet.d.ts.map +1 -0
- package/dist/types/wallet.js +13 -0
- package/dist/types/wallet.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/crypto.d.ts +25 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/dist/utils/crypto.js +119 -0
- package/dist/utils/crypto.js.map +1 -0
- package/dist/validation/schemas.d.ts +598 -0
- package/dist/validation/schemas.d.ts.map +1 -0
- package/dist/validation/schemas.js +178 -0
- package/dist/validation/schemas.js.map +1 -0
- package/package.json +30 -0
- package/src/decision-resolver.ts +16 -0
- package/src/errors.ts +20 -0
- package/src/index.ts +139 -0
- package/src/policies/allowlist.ts +26 -0
- package/src/policies/budget-check.ts +26 -0
- package/src/policies/index.ts +4 -0
- package/src/policies/max-per-call.ts +25 -0
- package/src/policies/rate-limit.ts +40 -0
- package/src/policy-engine.ts +65 -0
- package/src/types/errors.ts +215 -0
- package/src/types/payment.ts +297 -0
- package/src/types/policy.ts +289 -0
- package/src/types/wallet.ts +84 -0
- package/src/types.ts +193 -0
- package/src/utils/crypto.ts +182 -0
- package/src/validation/schemas.ts +208 -0
- package/test/architecture/architecture-invariants.test.ts +95 -0
- package/test/authorization-pipeline.test.ts +117 -0
- package/test/crypto.test.ts +56 -0
- package/test/decision-resolver.test.ts +52 -0
- package/test/errors.test.ts +97 -0
- package/test/payment-types.test.ts +49 -0
- package/test/policies.test.ts +162 -0
- package/test/policy-engine.test.ts +96 -0
- package/test/policy-types.test.ts +93 -0
- package/test/property/auth-invariants.test.ts +110 -0
- package/test/property/policy-invariants.test.ts +241 -0
- package/test/schemas.test.ts +58 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export enum ErrorCode {
|
|
2
|
+
VALIDATION_ERROR = 'validation_error',
|
|
3
|
+
AUTHORIZATION_ERROR = 'authorization_error',
|
|
4
|
+
BUDGET_EXHAUSTED = 'budget_exhausted',
|
|
5
|
+
ALLOWLIST_DENIED = 'allowlist_denied',
|
|
6
|
+
RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded',
|
|
7
|
+
AUTHORIZATION_EXPIRED = 'authorization_expired',
|
|
8
|
+
EXECUTION_ERROR = 'execution_error',
|
|
9
|
+
NETWORK_ERROR = 'network_error',
|
|
10
|
+
GAS_ESTIMATION_ERROR = 'gas_estimation_error',
|
|
11
|
+
BROADCAST_ERROR = 'broadcast_error',
|
|
12
|
+
REORG_DETECTED = 'reorg_detected',
|
|
13
|
+
HMAC_VERIFICATION_FAILED = 'hmac_verification_failed',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ErrorCodeMessage {
|
|
17
|
+
code: string
|
|
18
|
+
message: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SerializedError {
|
|
22
|
+
name: string
|
|
23
|
+
code: string
|
|
24
|
+
message: string
|
|
25
|
+
statusCode: number
|
|
26
|
+
details?: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class RuntimeeError extends Error {
|
|
30
|
+
public readonly code: string
|
|
31
|
+
public readonly statusCode: number
|
|
32
|
+
public readonly details?: unknown
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
code: string,
|
|
36
|
+
message: string,
|
|
37
|
+
statusCode = 500,
|
|
38
|
+
details?: unknown,
|
|
39
|
+
options?: ErrorOptions
|
|
40
|
+
) {
|
|
41
|
+
super(message, options)
|
|
42
|
+
this.name = new.target.name
|
|
43
|
+
this.code = code
|
|
44
|
+
this.statusCode = statusCode
|
|
45
|
+
this.details = details
|
|
46
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toJSON(): SerializedError {
|
|
50
|
+
return serializeError(this)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class ValidationError extends RuntimeeError {
|
|
55
|
+
constructor(message: string, details?: unknown, options?: ErrorOptions) {
|
|
56
|
+
super(ErrorCode.VALIDATION_ERROR, message, 400, details, options)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class AuthorizationError extends RuntimeeError {
|
|
61
|
+
constructor(code: string, message: string, details?: unknown, options?: ErrorOptions) {
|
|
62
|
+
super(code, message, 403, details, options)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class BudgetExhaustedError extends AuthorizationError {
|
|
67
|
+
public readonly policyId: string
|
|
68
|
+
public readonly budgetUsed: bigint
|
|
69
|
+
public readonly budgetLimit: bigint
|
|
70
|
+
|
|
71
|
+
constructor(policyId: string, used: bigint, limit: bigint, options?: ErrorOptions) {
|
|
72
|
+
super(
|
|
73
|
+
ErrorCode.BUDGET_EXHAUSTED,
|
|
74
|
+
`Budget exhausted: ${used} used of ${limit} limit`,
|
|
75
|
+
{
|
|
76
|
+
policyId,
|
|
77
|
+
budgetUsed: used.toString(),
|
|
78
|
+
budgetLimit: limit.toString(),
|
|
79
|
+
},
|
|
80
|
+
options
|
|
81
|
+
)
|
|
82
|
+
this.policyId = policyId
|
|
83
|
+
this.budgetUsed = used
|
|
84
|
+
this.budgetLimit = limit
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class AllowlistDeniedError extends AuthorizationError {
|
|
89
|
+
public readonly policyId: string
|
|
90
|
+
public readonly target: string
|
|
91
|
+
|
|
92
|
+
constructor(policyId: string, target: string, options?: ErrorOptions) {
|
|
93
|
+
super(
|
|
94
|
+
ErrorCode.ALLOWLIST_DENIED,
|
|
95
|
+
`Target "${target}" not in allowlist`,
|
|
96
|
+
{
|
|
97
|
+
policyId,
|
|
98
|
+
target,
|
|
99
|
+
},
|
|
100
|
+
options
|
|
101
|
+
)
|
|
102
|
+
this.policyId = policyId
|
|
103
|
+
this.target = target
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class RateLimitExceededError extends AuthorizationError {
|
|
108
|
+
public readonly policyId: string
|
|
109
|
+
public readonly windowMs: number
|
|
110
|
+
public readonly maxCalls: number
|
|
111
|
+
|
|
112
|
+
constructor(policyId: string, windowMs: number, maxCalls: number, options?: ErrorOptions) {
|
|
113
|
+
super(
|
|
114
|
+
ErrorCode.RATE_LIMIT_EXCEEDED,
|
|
115
|
+
`Rate limit exceeded: max ${maxCalls} calls per ${windowMs}ms window`,
|
|
116
|
+
{
|
|
117
|
+
policyId,
|
|
118
|
+
windowMs,
|
|
119
|
+
maxCalls,
|
|
120
|
+
},
|
|
121
|
+
options
|
|
122
|
+
)
|
|
123
|
+
this.policyId = policyId
|
|
124
|
+
this.windowMs = windowMs
|
|
125
|
+
this.maxCalls = maxCalls
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class AuthorizationExpiredError extends AuthorizationError {
|
|
130
|
+
constructor(options?: ErrorOptions) {
|
|
131
|
+
super(
|
|
132
|
+
ErrorCode.AUTHORIZATION_EXPIRED,
|
|
133
|
+
'Authorization preview has expired. Re-simulate.',
|
|
134
|
+
undefined,
|
|
135
|
+
options
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class ExecutionError extends RuntimeeError {
|
|
141
|
+
constructor(code: string, message: string, details?: unknown, options?: ErrorOptions) {
|
|
142
|
+
super(code, message, 502, details, options)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class NetworkError extends ExecutionError {
|
|
147
|
+
constructor(message: string, details?: unknown, options?: ErrorOptions) {
|
|
148
|
+
super(ErrorCode.NETWORK_ERROR, message, details, options)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class GasEstimationError extends ExecutionError {
|
|
153
|
+
constructor(message: string, details?: unknown, options?: ErrorOptions) {
|
|
154
|
+
super(ErrorCode.GAS_ESTIMATION_ERROR, message, details, options)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class BroadcastError extends ExecutionError {
|
|
159
|
+
constructor(message: string, details?: unknown, options?: ErrorOptions) {
|
|
160
|
+
super(ErrorCode.BROADCAST_ERROR, message, details, options)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class ReorgDetectedError extends ExecutionError {
|
|
165
|
+
public readonly depth: number
|
|
166
|
+
|
|
167
|
+
constructor(depth: number, options?: ErrorOptions) {
|
|
168
|
+
super(
|
|
169
|
+
ErrorCode.REORG_DETECTED,
|
|
170
|
+
`Chain reorg detected at depth ${depth}`,
|
|
171
|
+
{ depth },
|
|
172
|
+
options
|
|
173
|
+
)
|
|
174
|
+
this.depth = depth
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export class HmacVerificationError extends RuntimeeError {
|
|
179
|
+
constructor(message: string, details?: unknown, options?: ErrorOptions) {
|
|
180
|
+
super(ErrorCode.HMAC_VERIFICATION_FAILED, message, 401, details, options)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function isRuntimeeError(error: unknown): error is RuntimeeError {
|
|
185
|
+
return error instanceof RuntimeeError
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function serializeError(error: unknown): SerializedError {
|
|
189
|
+
if (error instanceof RuntimeeError) {
|
|
190
|
+
return {
|
|
191
|
+
name: error.name,
|
|
192
|
+
code: error.code,
|
|
193
|
+
message: error.message,
|
|
194
|
+
statusCode: error.statusCode,
|
|
195
|
+
details: error.details,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (error instanceof Error) {
|
|
200
|
+
return {
|
|
201
|
+
name: error.name,
|
|
202
|
+
code: ErrorCode.EXECUTION_ERROR,
|
|
203
|
+
message: error.message,
|
|
204
|
+
statusCode: 500,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
name: 'UnknownError',
|
|
210
|
+
code: ErrorCode.EXECUTION_ERROR,
|
|
211
|
+
message: 'Unknown error',
|
|
212
|
+
statusCode: 500,
|
|
213
|
+
details: error,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { ValidationError } from './errors.js'
|
|
2
|
+
import type { Address } from './wallet.js'
|
|
3
|
+
|
|
4
|
+
export const PAYMENT_INTENT_STATUSES = [
|
|
5
|
+
'draft',
|
|
6
|
+
'pending_policy',
|
|
7
|
+
'pending_approval',
|
|
8
|
+
'approved',
|
|
9
|
+
'rejected',
|
|
10
|
+
'pending_submission',
|
|
11
|
+
'submitted',
|
|
12
|
+
'confirming',
|
|
13
|
+
'succeeded',
|
|
14
|
+
'failed',
|
|
15
|
+
'cancelled',
|
|
16
|
+
'expired',
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
export type PaymentIntentStatus = (typeof PAYMENT_INTENT_STATUSES)[number]
|
|
20
|
+
|
|
21
|
+
export interface PaymentAmount {
|
|
22
|
+
currency: 'USDC'
|
|
23
|
+
value: bigint
|
|
24
|
+
decimals: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PaymentRecipient {
|
|
28
|
+
address: Address
|
|
29
|
+
chainId: number
|
|
30
|
+
ensName?: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PaymentIntentFailure {
|
|
34
|
+
code: string
|
|
35
|
+
message: string
|
|
36
|
+
retryable: boolean
|
|
37
|
+
detail?: Record<string, unknown>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type PaymentMetadataValue =
|
|
41
|
+
| string
|
|
42
|
+
| number
|
|
43
|
+
| boolean
|
|
44
|
+
| null
|
|
45
|
+
| PaymentMetadataValue[]
|
|
46
|
+
| { [key: string]: PaymentMetadataValue }
|
|
47
|
+
|
|
48
|
+
export interface PaymentIntent {
|
|
49
|
+
id: string
|
|
50
|
+
workspaceId: string
|
|
51
|
+
actorId: string
|
|
52
|
+
status: PaymentIntentStatus
|
|
53
|
+
amount: PaymentAmount
|
|
54
|
+
recipient: PaymentRecipient
|
|
55
|
+
createdAt: string
|
|
56
|
+
updatedAt: string
|
|
57
|
+
expiresAt?: string | null
|
|
58
|
+
idempotencyKey?: string | null
|
|
59
|
+
description?: string | null
|
|
60
|
+
reference?: string | null
|
|
61
|
+
transactionHash?: `0x${string}` | null
|
|
62
|
+
failure?: PaymentIntentFailure | null
|
|
63
|
+
metadata?: Record<string, PaymentMetadataValue>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SerializedBigInt {
|
|
67
|
+
__type: 'bigint'
|
|
68
|
+
value: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const PAYMENT_INTENT_TERMINAL_STATUSES = new Set<PaymentIntentStatus>([
|
|
72
|
+
'rejected',
|
|
73
|
+
'succeeded',
|
|
74
|
+
'failed',
|
|
75
|
+
'cancelled',
|
|
76
|
+
'expired',
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
export const PAYMENT_INTENT_TRANSITIONS: Record<
|
|
80
|
+
PaymentIntentStatus,
|
|
81
|
+
readonly PaymentIntentStatus[]
|
|
82
|
+
> = {
|
|
83
|
+
draft: ['pending_policy', 'cancelled', 'expired'],
|
|
84
|
+
pending_policy: ['pending_approval', 'approved', 'rejected', 'cancelled', 'expired'],
|
|
85
|
+
pending_approval: ['approved', 'rejected', 'cancelled', 'expired'],
|
|
86
|
+
approved: ['pending_submission', 'submitted', 'cancelled', 'expired'],
|
|
87
|
+
rejected: [],
|
|
88
|
+
pending_submission: ['submitted', 'failed', 'cancelled', 'expired'],
|
|
89
|
+
submitted: ['confirming', 'succeeded', 'failed'],
|
|
90
|
+
confirming: ['succeeded', 'failed'],
|
|
91
|
+
succeeded: [],
|
|
92
|
+
failed: [],
|
|
93
|
+
cancelled: [],
|
|
94
|
+
expired: [],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
98
|
+
return typeof value === 'object' && value !== null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function assertIsoTimestamp(value: string, fieldName: string): void {
|
|
102
|
+
if (Number.isNaN(Date.parse(value))) {
|
|
103
|
+
throw new ValidationError(`${fieldName} must be a valid ISO-8601 timestamp`, {
|
|
104
|
+
field: fieldName,
|
|
105
|
+
value,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function validatePaymentAmount(amount: PaymentAmount): void {
|
|
111
|
+
if (amount.currency !== 'USDC') {
|
|
112
|
+
throw new ValidationError('Payment amount currency must be USDC', {
|
|
113
|
+
field: 'amount.currency',
|
|
114
|
+
value: amount.currency,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (amount.value <= 0n) {
|
|
119
|
+
throw new ValidationError('Payment amount must be greater than zero', {
|
|
120
|
+
field: 'amount.value',
|
|
121
|
+
value: amount.value.toString(),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!Number.isInteger(amount.decimals) || amount.decimals < 0 || amount.decimals > 18) {
|
|
126
|
+
throw new ValidationError('Payment amount decimals must be an integer between 0 and 18', {
|
|
127
|
+
field: 'amount.decimals',
|
|
128
|
+
value: amount.decimals,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isPaymentIntentTerminalStatus(status: PaymentIntentStatus): boolean {
|
|
134
|
+
return PAYMENT_INTENT_TERMINAL_STATUSES.has(status)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function canTransitionPaymentIntentStatus(
|
|
138
|
+
currentStatus: PaymentIntentStatus,
|
|
139
|
+
nextStatus: PaymentIntentStatus
|
|
140
|
+
): boolean {
|
|
141
|
+
return PAYMENT_INTENT_TRANSITIONS[currentStatus].includes(nextStatus)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function assertValidPaymentIntentTransition(
|
|
145
|
+
currentStatus: PaymentIntentStatus,
|
|
146
|
+
nextStatus: PaymentIntentStatus
|
|
147
|
+
): void {
|
|
148
|
+
if (!canTransitionPaymentIntentStatus(currentStatus, nextStatus)) {
|
|
149
|
+
throw new ValidationError(
|
|
150
|
+
`Invalid payment intent transition from ${currentStatus} to ${nextStatus}`,
|
|
151
|
+
{
|
|
152
|
+
currentStatus,
|
|
153
|
+
nextStatus,
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function assertPaymentIntent(value: unknown): asserts value is PaymentIntent {
|
|
160
|
+
if (!isObject(value)) {
|
|
161
|
+
throw new ValidationError('Payment intent payload must be an object')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof value.id !== 'string' || !value.id.trim()) {
|
|
165
|
+
throw new ValidationError('Payment intent id is required', { field: 'id' })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof value.workspaceId !== 'string' || !value.workspaceId.trim()) {
|
|
169
|
+
throw new ValidationError('Payment intent workspaceId is required', {
|
|
170
|
+
field: 'workspaceId',
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof value.actorId !== 'string' || !value.actorId.trim()) {
|
|
175
|
+
throw new ValidationError('Payment intent actorId is required', {
|
|
176
|
+
field: 'actorId',
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
typeof value.status !== 'string' ||
|
|
182
|
+
!PAYMENT_INTENT_STATUSES.includes(value.status as PaymentIntentStatus)
|
|
183
|
+
) {
|
|
184
|
+
throw new ValidationError('Payment intent status is invalid', {
|
|
185
|
+
field: 'status',
|
|
186
|
+
value: value.status,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!isObject(value.amount)) {
|
|
191
|
+
throw new ValidationError('Payment intent amount is required', { field: 'amount' })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!isObject(value.recipient)) {
|
|
195
|
+
throw new ValidationError('Payment intent recipient is required', {
|
|
196
|
+
field: 'recipient',
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof value.recipient.address !== 'string' || !value.recipient.address.startsWith('0x')) {
|
|
201
|
+
throw new ValidationError('Payment intent recipient address is invalid', {
|
|
202
|
+
field: 'recipient.address',
|
|
203
|
+
value: value.recipient.address,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
typeof value.recipient.chainId !== 'number' ||
|
|
209
|
+
!Number.isInteger(value.recipient.chainId) ||
|
|
210
|
+
value.recipient.chainId <= 0
|
|
211
|
+
) {
|
|
212
|
+
throw new ValidationError('Payment intent recipient chainId is invalid', {
|
|
213
|
+
field: 'recipient.chainId',
|
|
214
|
+
value: value.recipient.chainId,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
validatePaymentAmount(value.amount as unknown as PaymentAmount)
|
|
219
|
+
|
|
220
|
+
if (typeof value.createdAt !== 'string') {
|
|
221
|
+
throw new ValidationError('Payment intent createdAt is required', {
|
|
222
|
+
field: 'createdAt',
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (typeof value.updatedAt !== 'string') {
|
|
227
|
+
throw new ValidationError('Payment intent updatedAt is required', {
|
|
228
|
+
field: 'updatedAt',
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
assertIsoTimestamp(value.createdAt, 'createdAt')
|
|
233
|
+
assertIsoTimestamp(value.updatedAt, 'updatedAt')
|
|
234
|
+
|
|
235
|
+
if (value.expiresAt !== undefined && value.expiresAt !== null && typeof value.expiresAt !== 'string') {
|
|
236
|
+
throw new ValidationError('Payment intent expiresAt must be a timestamp string', {
|
|
237
|
+
field: 'expiresAt',
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (value.expiresAt) {
|
|
242
|
+
assertIsoTimestamp(value.expiresAt, 'expiresAt')
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function serializeBigInts(value: unknown): unknown {
|
|
247
|
+
if (typeof value === 'bigint') {
|
|
248
|
+
return {
|
|
249
|
+
__type: 'bigint',
|
|
250
|
+
value: value.toString(),
|
|
251
|
+
} satisfies SerializedBigInt
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (Array.isArray(value)) {
|
|
255
|
+
return value.map((entry) => serializeBigInts(entry))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (isObject(value)) {
|
|
259
|
+
return Object.fromEntries(
|
|
260
|
+
Object.entries(value).map(([key, entry]) => [key, serializeBigInts(entry)])
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return value
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function deserializeBigInts<T>(value: unknown): T {
|
|
268
|
+
if (Array.isArray(value)) {
|
|
269
|
+
return value.map((entry) => deserializeBigInts(entry)) as T
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (isObject(value)) {
|
|
273
|
+
if (value.__type === 'bigint' && typeof value.value === 'string') {
|
|
274
|
+
return BigInt(value.value) as T
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return Object.fromEntries(
|
|
278
|
+
Object.entries(value).map(([key, entry]) => [key, deserializeBigInts(entry)])
|
|
279
|
+
) as T
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return value as T
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function serializePaymentIntent(paymentIntent: PaymentIntent): string {
|
|
286
|
+
assertPaymentIntent(paymentIntent)
|
|
287
|
+
|
|
288
|
+
// Preserve bigint fidelity over JSON transport without leaking decimal precision.
|
|
289
|
+
return JSON.stringify(serializeBigInts(paymentIntent))
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function deserializePaymentIntent(payload: string | unknown): PaymentIntent {
|
|
293
|
+
const candidate = typeof payload === 'string' ? JSON.parse(payload) : payload
|
|
294
|
+
const paymentIntent = deserializeBigInts<unknown>(candidate)
|
|
295
|
+
assertPaymentIntent(paymentIntent)
|
|
296
|
+
return paymentIntent
|
|
297
|
+
}
|