@oobe-protocol-labs/synapse-sap-sdk 0.6.2 → 0.7.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.
Files changed (140) hide show
  1. package/dist/cjs/constants/seeds.js +7 -0
  2. package/dist/cjs/constants/seeds.js.map +1 -1
  3. package/dist/cjs/core/client.js +42 -0
  4. package/dist/cjs/core/client.js.map +1 -1
  5. package/dist/cjs/events/geyser.js +295 -0
  6. package/dist/cjs/events/geyser.js.map +1 -0
  7. package/dist/cjs/idl/synapse_agent_sap.json +7545 -3501
  8. package/dist/cjs/index.js +28 -3
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/modules/escrow-v2.js +241 -0
  11. package/dist/cjs/modules/escrow-v2.js.map +1 -0
  12. package/dist/cjs/modules/escrow.js +4 -0
  13. package/dist/cjs/modules/escrow.js.map +1 -1
  14. package/dist/cjs/modules/index.js +7 -1
  15. package/dist/cjs/modules/index.js.map +1 -1
  16. package/dist/cjs/modules/staking.js +94 -0
  17. package/dist/cjs/modules/staking.js.map +1 -0
  18. package/dist/cjs/modules/subscription.js +96 -0
  19. package/dist/cjs/modules/subscription.js.map +1 -0
  20. package/dist/cjs/pda/index.js +143 -1
  21. package/dist/cjs/pda/index.js.map +1 -1
  22. package/dist/cjs/postgres/sync.js +72 -4
  23. package/dist/cjs/postgres/sync.js.map +1 -1
  24. package/dist/cjs/registries/x402.js +88 -51
  25. package/dist/cjs/registries/x402.js.map +1 -1
  26. package/dist/cjs/types/enums.js +51 -1
  27. package/dist/cjs/types/enums.js.map +1 -1
  28. package/dist/cjs/types/index.js +4 -1
  29. package/dist/cjs/types/index.js.map +1 -1
  30. package/dist/cjs/types/instructions.js.map +1 -1
  31. package/dist/cjs/utils/escrow-validation.js +219 -0
  32. package/dist/cjs/utils/escrow-validation.js.map +1 -0
  33. package/dist/cjs/utils/index.js +12 -1
  34. package/dist/cjs/utils/index.js.map +1 -1
  35. package/dist/cjs/utils/merchant-validator.js +246 -0
  36. package/dist/cjs/utils/merchant-validator.js.map +1 -0
  37. package/dist/cjs/utils/x402-direct.js +231 -0
  38. package/dist/cjs/utils/x402-direct.js.map +1 -0
  39. package/dist/esm/constants/seeds.js +7 -0
  40. package/dist/esm/constants/seeds.js.map +1 -1
  41. package/dist/esm/core/client.js +42 -0
  42. package/dist/esm/core/client.js.map +1 -1
  43. package/dist/esm/events/geyser.js +258 -0
  44. package/dist/esm/events/geyser.js.map +1 -0
  45. package/dist/esm/idl/synapse_agent_sap.json +7545 -3501
  46. package/dist/esm/index.js +7 -3
  47. package/dist/esm/index.js.map +1 -1
  48. package/dist/esm/modules/escrow-v2.js +237 -0
  49. package/dist/esm/modules/escrow-v2.js.map +1 -0
  50. package/dist/esm/modules/escrow.js +4 -0
  51. package/dist/esm/modules/escrow.js.map +1 -1
  52. package/dist/esm/modules/index.js +3 -0
  53. package/dist/esm/modules/index.js.map +1 -1
  54. package/dist/esm/modules/staking.js +90 -0
  55. package/dist/esm/modules/staking.js.map +1 -0
  56. package/dist/esm/modules/subscription.js +92 -0
  57. package/dist/esm/modules/subscription.js.map +1 -0
  58. package/dist/esm/pda/index.js +135 -0
  59. package/dist/esm/pda/index.js.map +1 -1
  60. package/dist/esm/postgres/sync.js +72 -4
  61. package/dist/esm/postgres/sync.js.map +1 -1
  62. package/dist/esm/registries/x402.js +89 -52
  63. package/dist/esm/registries/x402.js.map +1 -1
  64. package/dist/esm/types/enums.js +50 -0
  65. package/dist/esm/types/enums.js.map +1 -1
  66. package/dist/esm/types/index.js +1 -1
  67. package/dist/esm/types/index.js.map +1 -1
  68. package/dist/esm/types/instructions.js.map +1 -1
  69. package/dist/esm/utils/escrow-validation.js +212 -0
  70. package/dist/esm/utils/escrow-validation.js.map +1 -0
  71. package/dist/esm/utils/index.js +4 -0
  72. package/dist/esm/utils/index.js.map +1 -1
  73. package/dist/esm/utils/merchant-validator.js +241 -0
  74. package/dist/esm/utils/merchant-validator.js.map +1 -0
  75. package/dist/esm/utils/x402-direct.js +228 -0
  76. package/dist/esm/utils/x402-direct.js.map +1 -0
  77. package/dist/types/constants/seeds.d.ts +7 -0
  78. package/dist/types/constants/seeds.d.ts.map +1 -1
  79. package/dist/types/core/client.d.ts +33 -0
  80. package/dist/types/core/client.d.ts.map +1 -1
  81. package/dist/types/events/geyser.d.ts +150 -0
  82. package/dist/types/events/geyser.d.ts.map +1 -0
  83. package/dist/types/index.d.ts +8 -4
  84. package/dist/types/index.d.ts.map +1 -1
  85. package/dist/types/modules/escrow-v2.d.ts +51 -0
  86. package/dist/types/modules/escrow-v2.d.ts.map +1 -0
  87. package/dist/types/modules/escrow.d.ts +4 -0
  88. package/dist/types/modules/escrow.d.ts.map +1 -1
  89. package/dist/types/modules/index.d.ts +3 -0
  90. package/dist/types/modules/index.d.ts.map +1 -1
  91. package/dist/types/modules/staking.d.ts +32 -0
  92. package/dist/types/modules/staking.d.ts.map +1 -0
  93. package/dist/types/modules/subscription.d.ts +33 -0
  94. package/dist/types/modules/subscription.d.ts.map +1 -0
  95. package/dist/types/pda/index.d.ts +99 -0
  96. package/dist/types/pda/index.d.ts.map +1 -1
  97. package/dist/types/plugin/schemas.d.ts +2 -2
  98. package/dist/types/postgres/sync.d.ts +26 -2
  99. package/dist/types/postgres/sync.d.ts.map +1 -1
  100. package/dist/types/registries/x402.d.ts +14 -12
  101. package/dist/types/registries/x402.d.ts.map +1 -1
  102. package/dist/types/types/accounts.d.ts +157 -1
  103. package/dist/types/types/accounts.d.ts.map +1 -1
  104. package/dist/types/types/enums.d.ts +64 -0
  105. package/dist/types/types/enums.d.ts.map +1 -1
  106. package/dist/types/types/index.d.ts +4 -4
  107. package/dist/types/types/index.d.ts.map +1 -1
  108. package/dist/types/types/instructions.d.ts +34 -0
  109. package/dist/types/types/instructions.d.ts.map +1 -1
  110. package/dist/types/utils/escrow-validation.d.ts +145 -0
  111. package/dist/types/utils/escrow-validation.d.ts.map +1 -0
  112. package/dist/types/utils/index.d.ts +6 -0
  113. package/dist/types/utils/index.d.ts.map +1 -1
  114. package/dist/types/utils/merchant-validator.d.ts +176 -0
  115. package/dist/types/utils/merchant-validator.d.ts.map +1 -0
  116. package/dist/types/utils/x402-direct.d.ts +114 -0
  117. package/dist/types/utils/x402-direct.d.ts.map +1 -0
  118. package/package.json +5 -1
  119. package/src/constants/seeds.ts +7 -0
  120. package/src/core/client.ts +45 -0
  121. package/src/events/geyser.ts +384 -0
  122. package/src/events/yellowstone.d.ts +7 -0
  123. package/src/idl/synapse_agent_sap.json +7545 -3501
  124. package/src/index.ts +51 -0
  125. package/src/modules/escrow-v2.ts +396 -0
  126. package/src/modules/escrow.ts +4 -0
  127. package/src/modules/index.ts +3 -0
  128. package/src/modules/staking.ts +122 -0
  129. package/src/modules/subscription.ts +147 -0
  130. package/src/pda/index.ts +196 -0
  131. package/src/postgres/sync.ts +90 -4
  132. package/src/registries/x402.ts +108 -69
  133. package/src/types/accounts.ts +192 -1
  134. package/src/types/enums.ts +65 -0
  135. package/src/types/index.ts +15 -0
  136. package/src/types/instructions.ts +40 -0
  137. package/src/utils/escrow-validation.ts +301 -0
  138. package/src/utils/index.ts +28 -0
  139. package/src/utils/merchant-validator.ts +359 -0
  140. package/src/utils/x402-direct.ts +370 -0
@@ -0,0 +1,301 @@
1
+ /**
2
+ * @module utils/escrow-validation
3
+ * @description Server-side escrow validation pipeline.
4
+ *
5
+ * Provides typed helpers to validate escrow state before settlement
6
+ * and to build the correct SPL `AccountMeta[]` for token escrows.
7
+ *
8
+ * @category Utils
9
+ * @since v0.6.4
10
+ */
11
+
12
+ import {
13
+ PublicKey,
14
+ type Connection,
15
+ type AccountMeta,
16
+ } from "@solana/web3.js";
17
+ import { BN } from "@coral-xyz/anchor";
18
+ import { findATA } from "./rpc-strategy";
19
+ import { deriveAgent, deriveEscrow } from "../pda";
20
+ import { SapError } from "../errors";
21
+ import type { EscrowAccountData } from "../types";
22
+
23
+ // ═══════════════════════════════════════════════════════════════════
24
+ // Types
25
+ // ═══════════════════════════════════════════════════════════════════
26
+
27
+ /**
28
+ * @interface SplAccountMeta
29
+ * @description Typed SPL account metadata for escrow operations.
30
+ * @category Utils
31
+ * @since v0.6.4
32
+ */
33
+ export interface SplAccountMeta {
34
+ /** Account role in the escrow pipeline. */
35
+ readonly kind: "escrowAta" | "depositorAta" | "tokenMint" | "tokenProgram";
36
+ /** Account public key. */
37
+ readonly pubkey: PublicKey;
38
+ /** Whether this account is writable. */
39
+ readonly writable: boolean;
40
+ }
41
+
42
+ /**
43
+ * @interface EscrowValidationResult
44
+ * @description Result of server-side escrow state validation.
45
+ * @category Utils
46
+ * @since v0.6.4
47
+ */
48
+ export interface EscrowValidationResult {
49
+ /** Whether the escrow is valid for settlement. */
50
+ readonly valid: boolean;
51
+ /** Escrow account data (if found). */
52
+ readonly escrow: EscrowAccountData | null;
53
+ /** Escrow PDA address. */
54
+ readonly escrowPda: PublicKey;
55
+ /** Agent PDA address. */
56
+ readonly agentPda: PublicKey;
57
+ /** Whether this is an SPL token escrow (vs SOL). */
58
+ readonly isSplEscrow: boolean;
59
+ /** Generated SPL account metas (empty for SOL escrows). */
60
+ readonly splAccounts: SplAccountMeta[];
61
+ /** Validation errors (empty when valid). */
62
+ readonly errors: string[];
63
+ }
64
+
65
+ // ═══════════════════════════════════════════════════════════════════
66
+ // Error
67
+ // ═══════════════════════════════════════════════════════════════════
68
+
69
+ /**
70
+ * @name MissingEscrowAtaError
71
+ * @description Thrown when an SPL escrow operation is missing required
72
+ * Associated Token Accounts.
73
+ * @category Errors
74
+ * @since v0.6.4
75
+ */
76
+ export class MissingEscrowAtaError extends SapError {
77
+ /** The ATA address that is missing. */
78
+ readonly ataAddress: string;
79
+ /** Which side is missing: depositor or escrow. */
80
+ readonly side: "depositor" | "escrow";
81
+
82
+ constructor(ataAddress: string, side: "depositor" | "escrow") {
83
+ super(
84
+ `Missing ${side} ATA: ${ataAddress}. ` +
85
+ `Settlement mode is Escrow/SPL but the Associated Token Account does not exist. ` +
86
+ `The ${side} must create the ATA before escrow operations.`,
87
+ "SAP_MISSING_ESCROW_ATA",
88
+ );
89
+ this.name = "MissingEscrowAtaError";
90
+ this.ataAddress = ataAddress;
91
+ this.side = side;
92
+ }
93
+ }
94
+
95
+ // ═══════════════════════════════════════════════════════════════════
96
+ // Validation
97
+ // ═══════════════════════════════════════════════════════════════════
98
+
99
+ /** Standard SPL Token program ID. */
100
+ const TOKEN_PROGRAM_ID_STR = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
101
+
102
+ /**
103
+ * @name validateEscrowState
104
+ * @description Validates that an escrow is in a correct state for settlement.
105
+ *
106
+ * Checks:
107
+ * - Escrow PDA exists on-chain
108
+ * - If SPL: depositor ATA exists, escrow ATA exists, token mint matches
109
+ * - Balance >= requested settlement amount
110
+ * - Escrow is not expired
111
+ * - Max calls not exceeded
112
+ *
113
+ * @param connection - Solana RPC connection.
114
+ * @param agentWallet - The agent's wallet public key.
115
+ * @param depositorWallet - The depositor's wallet public key.
116
+ * @param fetchEscrow - Callback to fetch escrow data (avoids coupling to SapProgram).
117
+ * @param opts
118
+ * @param opts.callsToSettle - Number of calls to validate affordability for.
119
+ *
120
+ * @returns A detailed {@link EscrowValidationResult}.
121
+ *
122
+ * @category Utils
123
+ * @since v0.6.4
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const result = await validateEscrowState(
128
+ * connection,
129
+ * agentWallet,
130
+ * depositorWallet,
131
+ * (pda) => client.escrow.fetchByPda(pda),
132
+ * { callsToSettle: 5 },
133
+ * );
134
+ *
135
+ * if (!result.valid) {
136
+ * console.error("Escrow validation failed:", result.errors);
137
+ * }
138
+ * ```
139
+ */
140
+ export async function validateEscrowState(
141
+ connection: Connection,
142
+ agentWallet: PublicKey,
143
+ depositorWallet: PublicKey,
144
+ fetchEscrow: (escrowPda: PublicKey) => Promise<EscrowAccountData | null>,
145
+ opts?: { callsToSettle?: number },
146
+ ): Promise<EscrowValidationResult> {
147
+ const [agentPda] = deriveAgent(agentWallet);
148
+ const [escrowPda] = deriveEscrow(agentPda, depositorWallet);
149
+ const errors: string[] = [];
150
+
151
+ // 1. Fetch escrow
152
+ const escrow = await fetchEscrow(escrowPda);
153
+ if (!escrow) {
154
+ return {
155
+ valid: false,
156
+ escrow: null,
157
+ escrowPda,
158
+ agentPda,
159
+ isSplEscrow: false,
160
+ splAccounts: [],
161
+ errors: [`Escrow not found at ${escrowPda.toBase58()}`],
162
+ };
163
+ }
164
+
165
+ // 2. Check expiry
166
+ if (escrow.expiresAt.gt(new BN(0))) {
167
+ const now = Math.floor(Date.now() / 1000);
168
+ if (escrow.expiresAt.lt(new BN(now))) {
169
+ errors.push(`Escrow expired at ${escrow.expiresAt.toString()}`);
170
+ }
171
+ }
172
+
173
+ // 3. Check balance / calls
174
+ const callsToSettle = opts?.callsToSettle ?? 1;
175
+ const costForCalls = escrow.pricePerCall.mul(new BN(callsToSettle));
176
+ if (escrow.balance.lt(costForCalls)) {
177
+ errors.push(
178
+ `Insufficient balance: ${escrow.balance.toString()} < ${costForCalls.toString()} (${callsToSettle} calls × ${escrow.pricePerCall.toString()})`,
179
+ );
180
+ }
181
+
182
+ // 4. Check max calls
183
+ if (escrow.maxCalls.gt(new BN(0))) {
184
+ const remaining = escrow.maxCalls.sub(escrow.totalCallsSettled);
185
+ if (remaining.lt(new BN(callsToSettle))) {
186
+ errors.push(
187
+ `Max calls exceeded: ${remaining.toString()} remaining but needs ${callsToSettle}`,
188
+ );
189
+ }
190
+ }
191
+
192
+ // 5. Determine if SPL
193
+ const isSplEscrow =
194
+ escrow.tokenMint !== null &&
195
+ escrow.tokenMint.toBase58() !== "11111111111111111111111111111111";
196
+
197
+ // 6. Build SPL accounts (if SPL escrow)
198
+ const splAccounts: SplAccountMeta[] = [];
199
+ if (isSplEscrow) {
200
+ const mint = escrow.tokenMint!;
201
+ const depositorAta = findATA(depositorWallet, mint);
202
+ const escrowAta = findATA(escrowPda, mint);
203
+
204
+ // Verify depositor ATA exists
205
+ const depositorAtaInfo = await connection.getAccountInfo(depositorAta);
206
+ if (!depositorAtaInfo) {
207
+ errors.push(`Depositor ATA does not exist: ${depositorAta.toBase58()}`);
208
+ }
209
+
210
+ // Verify escrow ATA exists
211
+ const escrowAtaInfo = await connection.getAccountInfo(escrowAta);
212
+ if (!escrowAtaInfo) {
213
+ errors.push(`Escrow ATA does not exist: ${escrowAta.toBase58()}`);
214
+ }
215
+
216
+ // Verify mint matches
217
+ if (depositorAtaInfo) {
218
+ // SPL token account data: bytes 0-32 = mint
219
+ const ataMint = depositorAtaInfo.data.subarray(0, 32);
220
+ if (Buffer.from(ataMint).toString("hex") !== mint.toBuffer().toString("hex")) {
221
+ errors.push(`Depositor ATA mint mismatch: expected ${mint.toBase58()}`);
222
+ }
223
+ }
224
+
225
+ splAccounts.push(
226
+ { kind: "depositorAta", pubkey: depositorAta, writable: true },
227
+ { kind: "escrowAta", pubkey: escrowAta, writable: true },
228
+ { kind: "tokenMint", pubkey: mint, writable: false },
229
+ { kind: "tokenProgram", pubkey: new PublicKey(TOKEN_PROGRAM_ID_STR), writable: false },
230
+ );
231
+ }
232
+
233
+ return {
234
+ valid: errors.length === 0,
235
+ escrow,
236
+ escrowPda,
237
+ agentPda,
238
+ isSplEscrow,
239
+ splAccounts,
240
+ errors,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * @name attachSplAccounts
246
+ * @description Build the typed `SplAccountMeta[]` for an SPL token escrow operation.
247
+ * Does NOT validate existence — use {@link validateEscrowState} for full validation.
248
+ *
249
+ * @param escrowPda - The escrow PDA address.
250
+ * @param depositorWallet - The depositor's wallet public key.
251
+ * @param tokenMint - The SPL token mint.
252
+ * @returns An array of typed {@link SplAccountMeta} for SPL escrow operations.
253
+ *
254
+ * @category Utils
255
+ * @since v0.6.4
256
+ *
257
+ * @example
258
+ * ```ts
259
+ * const splMetas = attachSplAccounts(escrowPda, depositorWallet, usdcMint);
260
+ *
261
+ * // Convert to Anchor-compatible AccountMeta[]
262
+ * const accountMetas = splMetas.map(m => ({
263
+ * pubkey: m.pubkey,
264
+ * isWritable: m.writable,
265
+ * isSigner: false,
266
+ * }));
267
+ *
268
+ * await client.escrow.settle(depositor, calls, hash, accountMetas);
269
+ * ```
270
+ */
271
+ export function attachSplAccounts(
272
+ escrowPda: PublicKey,
273
+ depositorWallet: PublicKey,
274
+ tokenMint: PublicKey,
275
+ ): SplAccountMeta[] {
276
+ return [
277
+ { kind: "depositorAta", pubkey: findATA(depositorWallet, tokenMint), writable: true },
278
+ { kind: "escrowAta", pubkey: findATA(escrowPda, tokenMint), writable: true },
279
+ { kind: "tokenMint", pubkey: tokenMint, writable: false },
280
+ { kind: "tokenProgram", pubkey: new PublicKey(TOKEN_PROGRAM_ID_STR), writable: false },
281
+ ];
282
+ }
283
+
284
+ /**
285
+ * @name toAccountMetas
286
+ * @description Convert typed {@link SplAccountMeta} to Anchor-compatible
287
+ * `AccountMeta[]` for use with `.remainingAccounts()`.
288
+ *
289
+ * @param splMetas - Array of typed SPL account metas.
290
+ * @returns `AccountMeta[]` compatible with Anchor's `remainingAccounts`.
291
+ *
292
+ * @category Utils
293
+ * @since v0.6.4
294
+ */
295
+ export function toAccountMetas(splMetas: SplAccountMeta[]): AccountMeta[] {
296
+ return splMetas.map((m) => ({
297
+ pubkey: m.pubkey,
298
+ isWritable: m.writable,
299
+ isSigner: false,
300
+ }));
301
+ }
@@ -71,3 +71,31 @@ export type {
71
71
  PriorityFeeConfig,
72
72
  SettleOptions,
73
73
  } from "./priority-fee";
74
+
75
+ // ── v0.6.4 Escrow Validation & Merchant Middleware ──
76
+ export {
77
+ validateEscrowState,
78
+ attachSplAccounts,
79
+ toAccountMetas,
80
+ MissingEscrowAtaError,
81
+ } from "./escrow-validation";
82
+ export type {
83
+ SplAccountMeta,
84
+ EscrowValidationResult,
85
+ } from "./escrow-validation";
86
+
87
+ export {
88
+ SapMerchantValidator,
89
+ parseX402Headers,
90
+ } from "./merchant-validator";
91
+ export type {
92
+ ParsedX402Headers,
93
+ MerchantValidationResult,
94
+ } from "./merchant-validator";
95
+
96
+ export { getX402DirectPayments } from "./x402-direct";
97
+ export type {
98
+ X402DirectPayment,
99
+ SettlementPayload,
100
+ GetX402DirectOptions,
101
+ } from "./x402-direct";
@@ -0,0 +1,359 @@
1
+ /**
2
+ * @module utils/merchant-validator
3
+ * @description Standard Synapse merchant middleware for x402 settlement.
4
+ *
5
+ * Reads `X-Payment-*` headers from incoming HTTP requests, validates
6
+ * the escrow on-chain, auto-generates the correct `AccountMeta[]`,
7
+ * and throws explicit errors (e.g. {@link MissingEscrowAtaError})
8
+ * instead of letting the program return a generic crash.
9
+ *
10
+ * Designed for agents like Syra/Invoica that receive x402 payments.
11
+ *
12
+ * @category Utils
13
+ * @since v0.6.4
14
+ */
15
+
16
+ import {
17
+ type PublicKey,
18
+ type Connection,
19
+ type AccountMeta,
20
+ } from "@solana/web3.js";
21
+ import { PublicKey as PK } from "@solana/web3.js";
22
+ import { BN } from "@coral-xyz/anchor";
23
+ import {
24
+ validateEscrowState,
25
+ toAccountMetas,
26
+ MissingEscrowAtaError,
27
+ } from "./escrow-validation";
28
+ import type {
29
+ EscrowValidationResult,
30
+ } from "./escrow-validation";
31
+ import { SapValidationError } from "../errors";
32
+ import type { EscrowAccountData } from "../types";
33
+
34
+ // ═══════════════════════════════════════════════════════════════════
35
+ // Types
36
+ // ═══════════════════════════════════════════════════════════════════
37
+
38
+ /**
39
+ * @interface ParsedX402Headers
40
+ * @description Parsed and typed x402 payment headers from incoming HTTP request.
41
+ * @category Utils
42
+ * @since v0.6.4
43
+ */
44
+ export interface ParsedX402Headers {
45
+ /** x402 protocol identifier — must be "SAP-x402". */
46
+ readonly protocol: string;
47
+ /** Escrow PDA address. */
48
+ readonly escrowPda: PublicKey;
49
+ /** Agent PDA address. */
50
+ readonly agentPda: PublicKey;
51
+ /** Depositor wallet address. */
52
+ readonly depositorWallet: PublicKey;
53
+ /** Max calls allowed. */
54
+ readonly maxCalls: BN;
55
+ /** Price per call. */
56
+ readonly pricePerCall: BN;
57
+ /** SAP program ID. */
58
+ readonly programId: PublicKey;
59
+ /** Network identifier. */
60
+ readonly network: string;
61
+ }
62
+
63
+ /**
64
+ * @interface MerchantValidationResult
65
+ * @description Complete validation result for merchant-side x402 processing.
66
+ * @category Utils
67
+ * @since v0.6.4
68
+ */
69
+ export interface MerchantValidationResult {
70
+ /** Whether the escrow is valid and ready for settlement. */
71
+ readonly valid: boolean;
72
+ /** Parsed x402 headers. */
73
+ readonly headers: ParsedX402Headers;
74
+ /** Full escrow validation result. */
75
+ readonly escrowValidation: EscrowValidationResult;
76
+ /** Pre-built AccountMeta[] for settlement TX (empty for SOL escrows). */
77
+ readonly accountMetas: AccountMeta[];
78
+ /** All validation errors. */
79
+ readonly errors: string[];
80
+ }
81
+
82
+ // ═══════════════════════════════════════════════════════════════════
83
+ // Header parsing
84
+ // ═══════════════════════════════════════════════════════════════════
85
+
86
+ /** Required x402 headers. */
87
+ const REQUIRED_HEADERS = [
88
+ "X-Payment-Protocol",
89
+ "X-Payment-Escrow",
90
+ "X-Payment-Agent",
91
+ "X-Payment-Depositor",
92
+ "X-Payment-MaxCalls",
93
+ "X-Payment-PricePerCall",
94
+ "X-Payment-Program",
95
+ "X-Payment-Network",
96
+ ] as const;
97
+
98
+ /**
99
+ * @name parseX402Headers
100
+ * @description Parse and validate x402 headers from an HTTP request.
101
+ *
102
+ * @param headers - HTTP headers object (case-insensitive key lookup).
103
+ * @returns Parsed x402 headers.
104
+ * @throws {SapValidationError} If required headers are missing or malformed.
105
+ *
106
+ * @category Utils
107
+ * @since v0.6.4
108
+ */
109
+ export function parseX402Headers(
110
+ headers: Record<string, string | string[] | undefined>,
111
+ ): ParsedX402Headers {
112
+ // Normalize to case-insensitive
113
+ const normalized = new Map<string, string>();
114
+ for (const [key, value] of Object.entries(headers)) {
115
+ const val = Array.isArray(value) ? value[0] : value;
116
+ if (val !== undefined) {
117
+ normalized.set(key.toLowerCase(), val);
118
+ }
119
+ }
120
+
121
+ // Validate required headers present
122
+ const missing: string[] = [];
123
+ for (const h of REQUIRED_HEADERS) {
124
+ if (!normalized.has(h.toLowerCase())) {
125
+ missing.push(h);
126
+ }
127
+ }
128
+ if (missing.length > 0) {
129
+ throw new SapValidationError(
130
+ `Missing required x402 headers: ${missing.join(", ")}`,
131
+ "x402-headers",
132
+ );
133
+ }
134
+
135
+ const get = (key: string): string => normalized.get(key.toLowerCase())!;
136
+
137
+ // Validate protocol
138
+ const protocol = get("X-Payment-Protocol");
139
+ if (protocol !== "SAP-x402") {
140
+ throw new SapValidationError(
141
+ `Invalid X-Payment-Protocol: "${protocol}" (expected "SAP-x402")`,
142
+ "X-Payment-Protocol",
143
+ );
144
+ }
145
+
146
+ // Parse PublicKeys
147
+ let escrowPda: PublicKey;
148
+ let agentPda: PublicKey;
149
+ let depositorWallet: PublicKey;
150
+ let programId: PublicKey;
151
+ try {
152
+ escrowPda = new PK(get("X-Payment-Escrow"));
153
+ agentPda = new PK(get("X-Payment-Agent"));
154
+ depositorWallet = new PK(get("X-Payment-Depositor"));
155
+ programId = new PK(get("X-Payment-Program"));
156
+ } catch {
157
+ throw new SapValidationError(
158
+ "Malformed public key in x402 headers",
159
+ "x402-headers",
160
+ );
161
+ }
162
+
163
+ // Parse numeric values
164
+ const maxCallsStr = get("X-Payment-MaxCalls");
165
+ const pricePerCallStr = get("X-Payment-PricePerCall");
166
+ let maxCalls: BN;
167
+ let pricePerCall: BN;
168
+ try {
169
+ maxCalls = new BN(maxCallsStr);
170
+ pricePerCall = new BN(pricePerCallStr);
171
+ } catch {
172
+ throw new SapValidationError(
173
+ "Invalid numeric value in X-Payment-MaxCalls or X-Payment-PricePerCall",
174
+ "x402-headers",
175
+ );
176
+ }
177
+
178
+ return {
179
+ protocol,
180
+ escrowPda,
181
+ agentPda,
182
+ depositorWallet,
183
+ maxCalls,
184
+ pricePerCall,
185
+ programId,
186
+ network: get("X-Payment-Network"),
187
+ };
188
+ }
189
+
190
+ // ═══════════════════════════════════════════════════════════════════
191
+ // Merchant Validator
192
+ // ═══════════════════════════════════════════════════════════════════
193
+
194
+ /**
195
+ * @name SapMerchantValidator
196
+ * @description Standard Synapse merchant middleware for x402 payment validation.
197
+ *
198
+ * Reads `X-Payment-*` headers, validates escrow state on-chain, generates
199
+ * correct `AccountMeta[]` for SPL token escrows, and throws explicit errors
200
+ * (e.g. {@link MissingEscrowAtaError}) when ATA accounts are missing.
201
+ *
202
+ * @category Utils
203
+ * @since v0.6.4
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * const validator = new SapMerchantValidator(connection, fetchEscrow);
208
+ *
209
+ * // Express.js integration
210
+ * app.post("/api/v1/chat", async (req, res) => {
211
+ * try {
212
+ * const validation = await validator.validateRequest(req.headers, {
213
+ * callsToSettle: 1,
214
+ * });
215
+ *
216
+ * if (!validation.valid) {
217
+ * return res.status(402).json({ errors: validation.errors });
218
+ * }
219
+ *
220
+ * // Process request...
221
+ *
222
+ * // Settle payment using pre-built account metas
223
+ * await client.escrow.settle(
224
+ * validation.headers.depositorWallet,
225
+ * 1,
226
+ * serviceHash,
227
+ * validation.accountMetas,
228
+ * );
229
+ *
230
+ * res.json({ result: "..." });
231
+ * } catch (err) {
232
+ * if (err instanceof MissingEscrowAtaError) {
233
+ * return res.status(402).json({
234
+ * error: err.message,
235
+ * side: err.side,
236
+ * ata: err.ataAddress,
237
+ * });
238
+ * }
239
+ * throw err;
240
+ * }
241
+ * });
242
+ * ```
243
+ */
244
+ export class SapMerchantValidator {
245
+ private readonly connection: Connection;
246
+ private readonly fetchEscrow: (
247
+ escrowPda: PublicKey,
248
+ ) => Promise<EscrowAccountData | null>;
249
+
250
+ /**
251
+ * @param connection - Solana RPC connection.
252
+ * @param fetchEscrow - Callback to fetch escrow account data by PDA.
253
+ * Typically `(pda) => client.escrow.fetchByPda(pda).catch(() => null)`.
254
+ */
255
+ constructor(
256
+ connection: Connection,
257
+ fetchEscrow: (
258
+ escrowPda: PublicKey,
259
+ ) => Promise<EscrowAccountData | null>,
260
+ ) {
261
+ this.connection = connection;
262
+ this.fetchEscrow = fetchEscrow;
263
+ }
264
+
265
+ /**
266
+ * @name validateRequest
267
+ * @description Full validation pipeline for an incoming x402 request.
268
+ *
269
+ * Steps:
270
+ * 1. Parse `X-Payment-*` headers
271
+ * 2. Fetch escrow on-chain
272
+ * 3. Validate escrow state (balance, expiry, max calls)
273
+ * 4. If SPL escrow: validate ATAs exist and mint matches
274
+ * 5. Build `AccountMeta[]` for settlement TX
275
+ *
276
+ * @param headers - HTTP headers from the incoming request.
277
+ * @param opts
278
+ * @param opts.callsToSettle - Number of calls to validate affordability for (default: 1).
279
+ * @param opts.throwOnMissingAta - Throw {@link MissingEscrowAtaError} instead of returning errors (default: true).
280
+ *
281
+ * @returns A complete {@link MerchantValidationResult}.
282
+ *
283
+ * @throws {SapValidationError} If headers are missing or malformed.
284
+ * @throws {MissingEscrowAtaError} If SPL ATAs are missing and `throwOnMissingAta` is true.
285
+ *
286
+ * @category Utils
287
+ * @since v0.6.4
288
+ */
289
+ async validateRequest(
290
+ headers: Record<string, string | string[] | undefined>,
291
+ opts?: {
292
+ callsToSettle?: number;
293
+ throwOnMissingAta?: boolean;
294
+ },
295
+ ): Promise<MerchantValidationResult> {
296
+ // 1. Parse headers
297
+ const parsed = parseX402Headers(headers);
298
+
299
+ // 2. Validate escrow state
300
+ const escrowValidation = await validateEscrowState(
301
+ this.connection,
302
+ parsed.agentPda, // agentWallet derived inside validate
303
+ parsed.depositorWallet,
304
+ this.fetchEscrow,
305
+ { callsToSettle: opts?.callsToSettle ?? 1 },
306
+ );
307
+
308
+ // 3. Check for ATA errors and optionally throw
309
+ const throwOnMissingAta = opts?.throwOnMissingAta !== false;
310
+ if (throwOnMissingAta && escrowValidation.isSplEscrow) {
311
+ for (const error of escrowValidation.errors) {
312
+ if (error.includes("Depositor ATA does not exist")) {
313
+ const ataAddr = error.split(": ")[1] ?? "unknown";
314
+ throw new MissingEscrowAtaError(ataAddr, "depositor");
315
+ }
316
+ if (error.includes("Escrow ATA does not exist")) {
317
+ const ataAddr = error.split(": ")[1] ?? "unknown";
318
+ throw new MissingEscrowAtaError(ataAddr, "escrow");
319
+ }
320
+ }
321
+ }
322
+
323
+ // 4. Build account metas
324
+ const accountMetas = toAccountMetas(escrowValidation.splAccounts);
325
+
326
+ return {
327
+ valid: escrowValidation.valid,
328
+ headers: parsed,
329
+ escrowValidation,
330
+ accountMetas,
331
+ errors: escrowValidation.errors,
332
+ };
333
+ }
334
+
335
+ /**
336
+ * @name validateEscrow
337
+ * @description Validate escrow from pre-parsed headers (convenience method).
338
+ * Call this when you've already parsed the headers yourself.
339
+ *
340
+ * @param headers - Pre-parsed x402 headers.
341
+ * @param opts
342
+ * @returns The escrow validation result with pre-built account metas.
343
+ *
344
+ * @category Utils
345
+ * @since v0.6.4
346
+ */
347
+ async validateEscrow(
348
+ headers: ParsedX402Headers,
349
+ opts?: { callsToSettle?: number },
350
+ ): Promise<EscrowValidationResult> {
351
+ return validateEscrowState(
352
+ this.connection,
353
+ headers.agentPda,
354
+ headers.depositorWallet,
355
+ this.fetchEscrow,
356
+ opts,
357
+ );
358
+ }
359
+ }