@faremeter/payment-solana 0.20.1 → 0.21.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 (62) hide show
  1. package/dist/src/charge/client.d.ts +10 -6
  2. package/dist/src/charge/client.d.ts.map +1 -1
  3. package/dist/src/charge/client.js +117 -102
  4. package/dist/src/charge/common.d.ts +3 -3
  5. package/dist/src/charge/server.d.ts +18 -7
  6. package/dist/src/charge/server.d.ts.map +1 -1
  7. package/dist/src/charge/server.js +22 -24
  8. package/dist/src/compat.d.ts +38 -0
  9. package/dist/src/compat.d.ts.map +1 -0
  10. package/dist/src/compat.js +86 -0
  11. package/dist/src/compat.test.d.ts +3 -0
  12. package/dist/src/compat.test.d.ts.map +1 -0
  13. package/dist/src/compat.test.js +70 -0
  14. package/dist/src/exact/client.d.ts +18 -15
  15. package/dist/src/exact/client.d.ts.map +1 -1
  16. package/dist/src/exact/client.js +124 -96
  17. package/dist/src/exact/common.d.ts +1 -1
  18. package/dist/src/exact/facilitator.d.ts +19 -12
  19. package/dist/src/exact/facilitator.d.ts.map +1 -1
  20. package/dist/src/exact/facilitator.js +19 -18
  21. package/dist/src/exact/memo.d.ts +0 -2
  22. package/dist/src/exact/memo.d.ts.map +1 -1
  23. package/dist/src/exact/memo.js +0 -9
  24. package/dist/src/exact/verify.d.ts +5 -1
  25. package/dist/src/exact/verify.d.ts.map +1 -1
  26. package/dist/src/exact/verify.js +8 -2
  27. package/dist/src/exact/verify.test.js +80 -3
  28. package/dist/src/flex/client/handler.d.ts +31 -0
  29. package/dist/src/flex/client/handler.d.ts.map +1 -0
  30. package/dist/src/flex/client/handler.js +104 -0
  31. package/dist/src/flex/client/index.d.ts +3 -0
  32. package/dist/src/flex/client/index.d.ts.map +1 -0
  33. package/dist/src/flex/client/index.js +1 -0
  34. package/dist/src/flex/common.d.ts +15 -0
  35. package/dist/src/flex/common.d.ts.map +1 -0
  36. package/dist/src/flex/common.js +7 -0
  37. package/dist/src/flex/facilitator/handler.d.ts +48 -0
  38. package/dist/src/flex/facilitator/handler.d.ts.map +1 -0
  39. package/dist/src/flex/facilitator/handler.js +705 -0
  40. package/dist/src/flex/facilitator/index.d.ts +5 -0
  41. package/dist/src/flex/facilitator/index.d.ts.map +1 -0
  42. package/dist/src/flex/facilitator/index.js +2 -0
  43. package/dist/src/flex/hono/index.d.ts +3 -0
  44. package/dist/src/flex/hono/index.d.ts.map +1 -0
  45. package/dist/src/flex/hono/index.js +1 -0
  46. package/dist/src/flex/hono/upto-handler.d.ts +20 -0
  47. package/dist/src/flex/hono/upto-handler.d.ts.map +1 -0
  48. package/dist/src/flex/hono/upto-handler.js +72 -0
  49. package/dist/src/flex/hono/upto-handler.test.d.ts +3 -0
  50. package/dist/src/flex/hono/upto-handler.test.d.ts.map +1 -0
  51. package/dist/src/flex/hono/upto-handler.test.js +381 -0
  52. package/dist/src/flex/index.d.ts +4 -0
  53. package/dist/src/flex/index.d.ts.map +1 -0
  54. package/dist/src/flex/index.js +3 -0
  55. package/dist/src/flex/logger.d.ts +2 -0
  56. package/dist/src/flex/logger.d.ts.map +1 -0
  57. package/dist/src/flex/logger.js +2 -0
  58. package/dist/src/index.d.ts +1 -0
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/index.js +1 -0
  61. package/dist/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +23 -7
@@ -4,9 +4,9 @@ import { clusterToCAIP2, isKnownCluster, caip2ToCluster, isSolanaCAIP2Network, }
4
4
  import { fetchMint, findAssociatedTokenPda, getTransferCheckedInstruction, getCloseAccountInstruction, } from "@solana-program/token";
5
5
  import { address, createKeyPairSignerFromBytes, decompileTransactionMessage, getBase64Encoder, getCompiledTransactionMessageDecoder, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, pipe, } from "@solana/kit";
6
6
  import { getBase64EncodedWireTransaction, getSignatureFromTransaction, getTransactionDecoder, partiallySignTransaction, } from "@solana/transactions";
7
- import { Keypair, PublicKey } from "@solana/web3.js";
8
7
  import { type } from "arktype";
9
8
  import { isValidTransaction } from "./verify.js";
9
+ import { toAddress, toKeyPairSigner, toRpc } from "../compat.js";
10
10
  import { logger } from "./logger.js";
11
11
  import { x402Scheme, generateMatcher } from "./common.js";
12
12
  import { getAddMemoInstruction } from "@solana-program/memo";
@@ -84,21 +84,24 @@ const sendTransaction = async (rpc, signedTransaction, maxRetries, retryDelayMs)
84
84
  * fee payer keypair, and submits them to the Solana network.
85
85
  *
86
86
  * @param network - Solana network identifier (cluster name, CAIP-2 string, or SolanaCAIP2Network object)
87
- * @param rpc - Solana RPC client
88
- * @param feePayerKeypair - Keypair for paying transaction fees
89
- * @param mint - SPL token mint public key
87
+ * @param rpcInput - Solana RPC client
88
+ * @param feePayerSignerInput - Keypair or signer for paying transaction fees
89
+ * @param mintInput - SPL token mint public key
90
90
  * @param config - Optional configuration for retries, fees, and hooks
91
91
  * @returns A FacilitatorHandler for processing Solana exact payments
92
92
  */
93
- export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mint, config) => {
94
- const { isMatchingRequirement } = generateMatcher(network, mint.toBase58());
93
+ export const createFacilitatorHandler = async (network, rpcInput, feePayerSignerInput, mintInput, config) => {
94
+ const rpc = toRpc(rpcInput);
95
+ const mint = toAddress(mintInput);
96
+ const feePayerSigner = await toKeyPairSigner(feePayerSignerInput);
97
+ const { isMatchingRequirement } = generateMatcher(network, mint);
95
98
  const { maxRetries = 30, retryDelayMs = 1000, maxPriorityFee = 100_000, maxTransactionAge = 150, } = config ?? {};
96
- const mintInfo = await fetchMint(rpc, address(mint.toBase58()));
99
+ const mintInfo = await fetchMint(rpc, mint);
97
100
  const tokenProgram = mintInfo.programAddress;
98
101
  const hookArgs = {
99
102
  network,
100
103
  rpc,
101
- feePayerKeypair,
104
+ feePayerSigner,
102
105
  mint,
103
106
  mintInfo,
104
107
  logger,
@@ -120,7 +123,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
120
123
  const settleSigner = await createKeyPairSignerFromBytes(paymentPayload.settleSecretKey);
121
124
  const settleOwner = settleSigner.address;
122
125
  const [settleATA] = await findAssociatedTokenPda({
123
- mint: address(mint.toBase58()),
126
+ mint,
124
127
  owner: settleOwner,
125
128
  tokenProgram,
126
129
  });
@@ -136,9 +139,8 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
136
139
  return errorResponse("settlement account balance didn't match payment requirements");
137
140
  }
138
141
  const settle = async () => {
139
- const feePayerSigner = await createKeyPairSignerFromBytes(feePayerKeypair.secretKey);
140
142
  const [payToATA] = await findAssociatedTokenPda({
141
- mint: address(mint.toBase58()),
143
+ mint,
142
144
  owner: address(requirements.payTo),
143
145
  tokenProgram,
144
146
  });
@@ -150,7 +152,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
150
152
  getAddMemoInstruction({ memo: crypto.randomUUID() }),
151
153
  getTransferCheckedInstruction({
152
154
  source: settleATA,
153
- mint: address(mint.toBase58()),
155
+ mint,
154
156
  destination: payToATA,
155
157
  authority: settleSigner,
156
158
  amount: BigInt(requirements.amount),
@@ -195,7 +197,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
195
197
  }
196
198
  let validResult;
197
199
  try {
198
- validResult = await isValidTransaction(transactionMessage, requirements, feePayerKeypair.publicKey.toBase58(), tokenProgram, maxPriorityFee);
200
+ validResult = await isValidTransaction(transactionMessage, requirements, feePayerSigner.address, tokenProgram, { maxPriorityFee, requireMemo: config?.requireMemo ?? true });
199
201
  if (!validResult) {
200
202
  logger.error("Invalid transaction");
201
203
  return errorResponse("Invalid transaction");
@@ -211,8 +213,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
211
213
  settle: async () => {
212
214
  let signedTransaction;
213
215
  try {
214
- const kitKeypair = await createKeyPairSignerFromBytes(feePayerKeypair.secretKey);
215
- signedTransaction = await partiallySignTransaction([kitKeypair.keyPair], transaction);
216
+ signedTransaction = await partiallySignTransaction([feePayerSigner.keyPair], transaction);
216
217
  }
217
218
  catch (cause) {
218
219
  throw new Error("Failed to partially sign transaction", { cause });
@@ -266,7 +267,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
266
267
  scheme: x402Scheme,
267
268
  network: clusterToCAIP2(resolveCluster()).caip2,
268
269
  extra: {
269
- feePayer: feePayerKeypair.publicKey.toString(),
270
+ feePayer: feePayerSigner.address,
270
271
  tokenProgram,
271
272
  features,
272
273
  },
@@ -283,9 +284,9 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
283
284
  : {};
284
285
  return {
285
286
  ...x,
286
- asset: mint.toBase58(),
287
+ asset: mint,
287
288
  extra: {
288
- feePayer: feePayerKeypair.publicKey.toString(),
289
+ feePayer: feePayerSigner.address,
289
290
  decimals: mintInfo.data.decimals,
290
291
  recentBlockhash,
291
292
  ...memo,
@@ -1,4 +1,2 @@
1
- import { TransactionInstruction } from "@solana/web3.js";
2
1
  export declare function generateMemoNonce(): string;
3
- export declare function createMemoInstruction(memo: string): TransactionInstruction;
4
2
  //# sourceMappingURL=memo.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memo.d.ts","sourceRoot":"","sources":["../../../src/exact/memo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAGpE,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,sBAAsB,CAM1E"}
1
+ {"version":3,"file":"memo.d.ts","sourceRoot":"","sources":["../../../src/exact/memo.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C"}
@@ -1,5 +1,3 @@
1
- import { PublicKey, TransactionInstruction } from "@solana/web3.js";
2
- import { MEMO_PROGRAM_ADDRESS } from "@solana-program/memo";
3
1
  export function generateMemoNonce() {
4
2
  const bytes = new Uint8Array(16);
5
3
  crypto.getRandomValues(bytes);
@@ -7,10 +5,3 @@ export function generateMemoNonce() {
7
5
  .map((b) => b.toString(16).padStart(2, "0"))
8
6
  .join("");
9
7
  }
10
- export function createMemoInstruction(memo) {
11
- return new TransactionInstruction({
12
- programId: new PublicKey(MEMO_PROGRAM_ADDRESS),
13
- keys: [],
14
- data: Buffer.from(memo, "utf-8"),
15
- });
16
- }
@@ -1,7 +1,11 @@
1
1
  import type { x402PaymentRequirements } from "@faremeter/types/x402v2";
2
2
  import { type Address } from "@solana/kit";
3
3
  import type { CompilableTransactionMessage } from "../common.js";
4
- export declare function isValidTransaction(transactionMessage: CompilableTransactionMessage, paymentRequirements: x402PaymentRequirements, facilitatorAddress: string, tokenProgram: Address, maxPriorityFee?: number): Promise<{
4
+ export interface TransactionValidationOptions {
5
+ maxPriorityFee?: number;
6
+ requireMemo?: boolean;
7
+ }
8
+ export declare function isValidTransaction(transactionMessage: CompilableTransactionMessage, paymentRequirements: x402PaymentRequirements, facilitatorAddress: string, tokenProgram: Address, options?: TransactionValidationOptions): Promise<{
5
9
  payer: string;
6
10
  } | false>;
7
11
  //# sourceMappingURL=verify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/exact/verify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAUvE,OAAO,EAAW,KAAK,OAAO,EAAoB,MAAM,aAAa,CAAC;AAGtE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,WAAW,CAAC;AA0H9D,wBAAsB,kBAAkB,CACtC,kBAAkB,EAAE,4BAA4B,EAChD,mBAAmB,EAAE,uBAAuB,EAC5C,kBAAkB,EAAE,MAAM,EAC1B,YAAY,EAAE,OAAO,EACrB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAC,CAkGpC"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/exact/verify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAUvE,OAAO,EAAW,KAAK,OAAO,EAAoB,MAAM,aAAa,CAAC;AAGtE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,WAAW,CAAC;AA0H9D,MAAM,WAAW,4BAA4B;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,kBAAkB,CACtC,kBAAkB,EAAE,4BAA4B,EAChD,mBAAmB,EAAE,uBAAuB,EAC5C,kBAAkB,EAAE,MAAM,EAC1B,YAAY,EAAE,OAAO,EACrB,OAAO,CAAC,EAAE,4BAA4B,GACrC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAC,CA0GpC"}
@@ -93,7 +93,7 @@ async function verifyTransferInstruction(instruction, paymentRequirements, desti
93
93
  }
94
94
  return false;
95
95
  }
96
- export async function isValidTransaction(transactionMessage, paymentRequirements, facilitatorAddress, tokenProgram, maxPriorityFee) {
96
+ export async function isValidTransaction(transactionMessage, paymentRequirements, facilitatorAddress, tokenProgram, options) {
97
97
  const extra = PaymentRequirementsExtra(paymentRequirements.extra);
98
98
  if (isValidationError(extra)) {
99
99
  throw new Error("feePayer is required");
@@ -119,6 +119,7 @@ export async function isValidTransaction(transactionMessage, paymentRequirements
119
119
  if (!limitResult.valid || !priceResult.valid) {
120
120
  return false;
121
121
  }
122
+ const { maxPriorityFee, requireMemo = true } = options ?? {};
122
123
  if (maxPriorityFee !== undefined &&
123
124
  limitResult.units !== undefined &&
124
125
  priceResult.microLamports !== undefined) {
@@ -144,13 +145,18 @@ export async function isValidTransaction(transactionMessage, paymentRequirements
144
145
  }
145
146
  }
146
147
  const memoInstructions = rest.filter(isMemoInstruction);
147
- if (memoInstructions.length !== 1) {
148
+ if (memoInstructions.length > 1) {
149
+ logger.error("Expected at most one Memo instruction");
150
+ return false;
151
+ }
152
+ if (requireMemo && memoInstructions.length !== 1) {
148
153
  logger.error("Expected exactly one Memo instruction");
149
154
  return false;
150
155
  }
151
156
  if (extra.memo !== undefined) {
152
157
  const memoIx = memoInstructions[0];
153
158
  if (!memoIx) {
159
+ logger.error("extra.memo is set but no Memo instruction found");
154
160
  return false;
155
161
  }
156
162
  const memoData = getMemoData(memoIx);
@@ -175,7 +175,7 @@ await t.test("isValidTransaction", async (t) => {
175
175
  await t.test("accepts transaction within priority fee limit", async (t) => {
176
176
  const f = await createFixtures();
177
177
  const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx, makeMemoIx("nonce")], f.facilitator);
178
- const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, 100_000);
178
+ const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, { maxPriorityFee: 100_000 });
179
179
  t.ok(result);
180
180
  t.equal(result && result.payer, f.sender.address);
181
181
  t.end();
@@ -189,7 +189,7 @@ await t.test("isValidTransaction", async (t) => {
189
189
  microLamports: 10000000n,
190
190
  });
191
191
  const txMsg = buildTxMessage([highLimitIx, highPriceIx, f.transferIx, makeMemoIx("nonce")], f.facilitator);
192
- t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, 100), false);
192
+ t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, { maxPriorityFee: 100 }), false);
193
193
  t.end();
194
194
  });
195
195
  await t.test("rejects transaction exceeding priority fee limit with lighthouse ixs", async (t) => {
@@ -207,7 +207,7 @@ await t.test("isValidTransaction", async (t) => {
207
207
  makeLighthouseIx(),
208
208
  makeMemoIx("nonce"),
209
209
  ], f.facilitator);
210
- t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, 100), false);
210
+ t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, { maxPriorityFee: 100 }), false);
211
211
  t.end();
212
212
  });
213
213
  await t.test("rejects trailing non-lighthouse instruction", async (t) => {
@@ -448,5 +448,82 @@ await t.test("isValidTransaction", async (t) => {
448
448
  t.equal(await isValidTransaction(txMsg, requirements, f.facilitator.address, f.tokenProgram), false);
449
449
  t.end();
450
450
  });
451
+ await t.test("rejects transaction without memo when requireMemo defaults to true", async (t) => {
452
+ const f = await createFixtures();
453
+ // Older clients build exactly 3 instructions:
454
+ // 1. SetComputeUnitLimit
455
+ // 2. SetComputeUnitPrice
456
+ // 3. TransferChecked
457
+ // No memo instruction is included. With requireMemo defaulting to
458
+ // true, the facilitator rejects these transactions.
459
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx], f.facilitator);
460
+ t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
461
+ t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, { requireMemo: true }), false);
462
+ t.end();
463
+ });
464
+ await t.test("accepts transaction without memo when requireMemo is false", async (t) => {
465
+ const f = await createFixtures();
466
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx], f.facilitator);
467
+ const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, { requireMemo: false });
468
+ t.ok(result);
469
+ t.equal(result && result.payer, f.sender.address);
470
+ t.end();
471
+ });
472
+ await t.test("accepts transaction with lighthouse but no memo when requireMemo is false", async (t) => {
473
+ const f = await createFixtures();
474
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx, makeLighthouseIx()], f.facilitator);
475
+ const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, { requireMemo: false });
476
+ t.ok(result);
477
+ t.equal(result && result.payer, f.sender.address);
478
+ t.end();
479
+ });
480
+ await t.test("accepts transaction with memo when requireMemo is false", async (t) => {
481
+ const f = await createFixtures();
482
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx, makeMemoIx("nonce")], f.facilitator);
483
+ const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, { requireMemo: false });
484
+ t.ok(result);
485
+ t.equal(result && result.payer, f.sender.address);
486
+ t.end();
487
+ });
488
+ await t.test("rejects multiple memos even when requireMemo is false", async (t) => {
489
+ const f = await createFixtures();
490
+ const txMsg = buildTxMessage([
491
+ f.computeLimitIx,
492
+ f.computePriceIx,
493
+ f.transferIx,
494
+ makeMemoIx("a"),
495
+ makeMemoIx("b"),
496
+ ], f.facilitator);
497
+ t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, { requireMemo: false }), false);
498
+ t.end();
499
+ });
500
+ await t.test("rejects memo mismatch when requireMemo is false and extra.memo is set", async (t) => {
501
+ const f = await createFixtures();
502
+ const requirements = {
503
+ ...f.requirements,
504
+ extra: { ...f.requirements.extra, memo: "expected-value" },
505
+ };
506
+ const txMsg = buildTxMessage([
507
+ f.computeLimitIx,
508
+ f.computePriceIx,
509
+ f.transferIx,
510
+ makeMemoIx("wrong-value"),
511
+ ], f.facilitator);
512
+ t.equal(await isValidTransaction(txMsg, requirements, f.facilitator.address, f.tokenProgram, { requireMemo: false }), false);
513
+ t.end();
514
+ });
515
+ await t.test("rejects missing memo when requireMemo is false but extra.memo is set", async (t) => {
516
+ const f = await createFixtures();
517
+ const requirements = {
518
+ ...f.requirements,
519
+ extra: { ...f.requirements.extra, memo: "invoice-789" },
520
+ };
521
+ // An older client cannot supply a memo instruction, so a seller
522
+ // that requires a specific memo value should still reject the
523
+ // transaction regardless of the requireMemo setting.
524
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx], f.facilitator);
525
+ t.equal(await isValidTransaction(txMsg, requirements, f.facilitator.address, f.tokenProgram, { requireMemo: false }), false);
526
+ t.end();
527
+ });
451
528
  t.end();
452
529
  });
@@ -0,0 +1,31 @@
1
+ import { type Address } from "@solana/kit";
2
+ import type { PaymentHandler } from "@faremeter/types/client";
3
+ import { fetchEscrowAccount } from "@faremeter/flex-solana";
4
+ import type { webcrypto } from "node:crypto";
5
+ type CryptoKeyPair = webcrypto.CryptoKeyPair;
6
+ type Rpc = Parameters<typeof fetchEscrowAccount>[0];
7
+ type SlotProvider = {
8
+ getSlot(): {
9
+ send(): Promise<bigint>;
10
+ };
11
+ };
12
+ /** Configuration for `createPaymentHandler`. */
13
+ export type CreateFlexPaymentHandlerOpts = {
14
+ network: string;
15
+ escrow: Address;
16
+ mint: Address;
17
+ sessionKeyPair: CryptoKeyPair;
18
+ sessionKeyAddress: Address;
19
+ rpc: Rpc & SlotProvider;
20
+ programAddress?: Address;
21
+ };
22
+ /**
23
+ * Creates a client-side `PaymentHandler` that signs Flex payment
24
+ * authorizations against compatible x402 requirements.
25
+ *
26
+ * @param opts - Escrow, session key, and RPC configuration
27
+ * @returns A handler that produces signed payment payloads
28
+ */
29
+ export declare function createPaymentHandler(opts: CreateFlexPaymentHandlerOpts): PaymentHandler;
30
+ export {};
31
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../src/flex/client/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAW,MAAM,aAAa,CAAC;AAEpD,OAAO,KAAK,EAEV,cAAc,EAEf,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAGL,kBAAkB,EAGnB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,KAAK,aAAa,GAAG,SAAS,CAAC,aAAa,CAAC;AAE7C,KAAK,GAAG,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEpD,KAAK,YAAY,GAAG;IAAE,OAAO,IAAI;QAAE,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;CAAE,CAAC;AAE/D,gDAAgD;AAChD,MAAM,MAAM,4BAA4B,GAAG;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,aAAa,CAAC;IAC9B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,GAAG,EAAE,GAAG,GAAG,YAAY,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAKF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,4BAA4B,GACjC,cAAc,CAsHhB"}
@@ -0,0 +1,104 @@
1
+ import { address } from "@solana/kit";
2
+ import { isValidationError } from "@faremeter/types";
3
+ import { serializePaymentAuthorization, signPaymentAuthorization, fetchEscrowAccount, FlexPaymentRequirementsExtra, FLEX_PROGRAM_ADDRESS, } from "@faremeter/flex-solana";
4
+ import { generateMatcher } from "../common.js";
5
+ import { logger } from "../logger.js";
6
+ const MS_PER_SLOT = 400;
7
+ const EXPIRY_BUFFER_SLOTS = 20n;
8
+ /**
9
+ * Creates a client-side `PaymentHandler` that signs Flex payment
10
+ * authorizations against compatible x402 requirements.
11
+ *
12
+ * @param opts - Escrow, session key, and RPC configuration
13
+ * @returns A handler that produces signed payment payloads
14
+ */
15
+ export function createPaymentHandler(opts) {
16
+ const { escrow, mint, sessionKeyPair, sessionKeyAddress, rpc, programAddress = FLEX_PROGRAM_ADDRESS, } = opts;
17
+ const { isMatchingRequirement } = generateMatcher(opts.network, mint);
18
+ let cachedRefundTimeoutSlots = null;
19
+ let refundTimeoutPromise = null;
20
+ async function getRefundTimeoutSlots() {
21
+ if (cachedRefundTimeoutSlots !== null)
22
+ return cachedRefundTimeoutSlots;
23
+ refundTimeoutPromise ??= fetchEscrowAccount(rpc, escrow)
24
+ .then((account) => {
25
+ if (!account) {
26
+ throw new Error("Escrow account not found on-chain");
27
+ }
28
+ cachedRefundTimeoutSlots = account.refundTimeoutSlots;
29
+ return account.refundTimeoutSlots;
30
+ })
31
+ .catch((cause) => {
32
+ refundTimeoutPromise = null;
33
+ throw cause;
34
+ });
35
+ return refundTimeoutPromise;
36
+ }
37
+ let lastKnownSlot = 0n;
38
+ let lastSlotFetchedAtMs = 0;
39
+ async function getCurrentSlot() {
40
+ const elapsed = Date.now() - lastSlotFetchedAtMs;
41
+ if (lastKnownSlot > 0n && elapsed < 5_000) {
42
+ return lastKnownSlot + BigInt(Math.floor(elapsed / MS_PER_SLOT));
43
+ }
44
+ lastKnownSlot = await rpc.getSlot().send();
45
+ lastSlotFetchedAtMs = Date.now();
46
+ return lastKnownSlot;
47
+ }
48
+ function randomU64() {
49
+ const buf = new Uint8Array(8);
50
+ crypto.getRandomValues(buf);
51
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
52
+ return view.getBigUint64(0, true);
53
+ }
54
+ return async (_context, accepts) => {
55
+ const compatible = accepts.filter(isMatchingRequirement);
56
+ return compatible.map((requirements) => {
57
+ const exec = async () => {
58
+ const extraResult = FlexPaymentRequirementsExtra(requirements.extra);
59
+ if (isValidationError(extraResult)) {
60
+ throw new Error(`Invalid flex requirements extra: ${extraResult.summary}`);
61
+ }
62
+ const refundTimeoutSlots = await getRefundTimeoutSlots();
63
+ const currentSlot = await getCurrentSlot();
64
+ const authorizationId = randomU64();
65
+ const expiresAtSlot = currentSlot + refundTimeoutSlots - EXPIRY_BUFFER_SLOTS;
66
+ const maxAmount = BigInt(requirements.amount);
67
+ const message = serializePaymentAuthorization({
68
+ programId: programAddress,
69
+ escrow,
70
+ mint,
71
+ maxAmount,
72
+ authorizationId,
73
+ expiresAtSlot,
74
+ splits: extraResult.splits.map((s) => ({
75
+ recipient: address(s.recipient),
76
+ bps: s.bps,
77
+ })),
78
+ });
79
+ const signature = await signPaymentAuthorization({
80
+ message,
81
+ keyPair: sessionKeyPair,
82
+ });
83
+ const signatureBase64 = btoa(String.fromCharCode(...signature));
84
+ logger.debug("signed payment authorization", {
85
+ escrow,
86
+ authorizationId: authorizationId.toString(),
87
+ maxAmount: maxAmount.toString(),
88
+ });
89
+ const payload = {
90
+ escrow,
91
+ mint,
92
+ maxAmount: maxAmount.toString(),
93
+ authorizationId: authorizationId.toString(),
94
+ expiresAtSlot: expiresAtSlot.toString(),
95
+ splits: extraResult.splits,
96
+ sessionKey: sessionKeyAddress,
97
+ signature: signatureBase64,
98
+ };
99
+ return { payload };
100
+ };
101
+ return { exec, requirements };
102
+ });
103
+ };
104
+ }
@@ -0,0 +1,3 @@
1
+ export { createPaymentHandler } from "./handler.js";
2
+ export type { CreateFlexPaymentHandlerOpts } from "./handler.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/flex/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACjD,YAAY,EAAE,4BAA4B,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1 @@
1
+ export { createPaymentHandler } from "./handler.js";
@@ -0,0 +1,15 @@
1
+ import { type SolanaCAIP2Network } from "@faremeter/info/solana";
2
+ export declare const FLEX_SCHEME = "flex";
3
+ export declare function generateMatcher(network: string | SolanaCAIP2Network, asset: string): {
4
+ matchTuple: import("arktype/internal/variants/object.ts").ObjectType<{
5
+ scheme: (In: string) => import("arktype/internal/attributes.ts").To<Lowercase<string>>;
6
+ network: (In: string) => import("arktype/internal/attributes.ts").To<Lowercase<string>>;
7
+ asset: (In: string) => import("arktype/internal/attributes.ts").To<Lowercase<string>>;
8
+ }, {}>;
9
+ isMatchingRequirement: (req: {
10
+ scheme: string;
11
+ network: string;
12
+ asset: string;
13
+ }) => boolean;
14
+ };
15
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/flex/common.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,WAAW,SAAS,CAAC;AAElC,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,GAAG,kBAAkB,EACpC,KAAK,EAAE,MAAM;;;;;;;;;;;EASd"}
@@ -0,0 +1,7 @@
1
+ import { generateRequirementsMatcher } from "@faremeter/types/x402";
2
+ import { lookupX402Network, } from "@faremeter/info/solana";
3
+ export const FLEX_SCHEME = "flex";
4
+ export function generateMatcher(network, asset) {
5
+ const solanaNetwork = lookupX402Network(network);
6
+ return generateRequirementsMatcher([FLEX_SCHEME], [solanaNetwork.caip2], [asset]);
7
+ }
@@ -0,0 +1,48 @@
1
+ import { type Address, type TransactionSigner, type Rpc, type SolanaRpcApi } from "@solana/kit";
2
+ import type { FacilitatorHandler } from "@faremeter/types/facilitator";
3
+ import { type HoldManager } from "@faremeter/flex-solana/facilitator";
4
+ type FlexFacilitatorConfig = {
5
+ supportedMints: Address[];
6
+ defaultSplits: {
7
+ recipient: string;
8
+ bps: number;
9
+ }[];
10
+ maxSubmitRetries?: number;
11
+ submitRetryDelayMs?: number;
12
+ minGracePeriodSlots?: bigint;
13
+ confirmationBufferSlots?: bigint;
14
+ snapshotMaxAgeMs?: number;
15
+ flushIntervalMs?: number;
16
+ };
17
+ /** Outcome of submitting a single hold to the on-chain program. */
18
+ export type FlushResult = {
19
+ authorizationId: bigint;
20
+ success: boolean;
21
+ transaction?: string;
22
+ error?: string;
23
+ };
24
+ /**
25
+ * Extended `FacilitatorHandler` with Flex-specific lifecycle
26
+ * methods for flushing holds to chain and inspecting the hold manager.
27
+ */
28
+ export type FlexFacilitator = FacilitatorHandler & {
29
+ flush(): Promise<FlushResult[]>;
30
+ getHoldManager(): HoldManager;
31
+ stop(): void;
32
+ };
33
+ /**
34
+ * Creates a facilitator handler that verifies Flex payment authorizations,
35
+ * manages in-memory holds, and submits/finalizes settlements on-chain.
36
+ *
37
+ * Starts a background interval that periodically flushes settled holds
38
+ * and finalizes confirmed transactions. Call `stop()` to clear it.
39
+ *
40
+ * @param network - Solana cluster name (e.g. "mainnet", "devnet")
41
+ * @param rpc - Solana RPC client
42
+ * @param facilitatorSigner - Transaction signer for the facilitator
43
+ * @param config - Supported mints, splits, and timing configuration
44
+ * @returns A `FlexFacilitator` with verify/settle/flush/stop methods
45
+ */
46
+ export declare const createFacilitatorHandler: (network: string, rpc: Rpc<SolanaRpcApi>, facilitatorSigner: TransactionSigner, config: FlexFacilitatorConfig) => Promise<FlexFacilitator>;
47
+ export {};
48
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../src/flex/facilitator/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAaZ,KAAK,iBAAiB,EACtB,KAAK,GAAG,EACR,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AASrB,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,8BAA8B,CAAC;AAiBtC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,oCAAoC,CAAC;AAiC5C,KAAK,qBAAqB,GAAG;IAC3B,cAAc,EAAE,OAAO,EAAE,CAAC;IAC1B,aAAa,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAcF,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAUF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,kBAAkB,GAAG;IACjD,KAAK,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAChC,cAAc,IAAI,WAAW,CAAC;IAC9B,IAAI,IAAI,IAAI,CAAC;CACd,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,GACnC,SAAS,MAAM,EACf,KAAK,GAAG,CAAC,YAAY,CAAC,EACtB,mBAAmB,iBAAiB,EACpC,QAAQ,qBAAqB,KAC5B,OAAO,CAAC,eAAe,CA83BzB,CAAC"}