@faremeter/payment-solana 0.15.0 → 0.17.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.
@@ -0,0 +1,12 @@
1
+ import type { Signature } from "@solana/kit";
2
+ export declare class TransactionStore {
3
+ private maxAge;
4
+ private sigToBlock;
5
+ private highestBlock;
6
+ constructor(maxAge?: number);
7
+ add(signature: Signature, blockHeight: number): void;
8
+ has(signature: Signature): boolean;
9
+ private prune;
10
+ get size(): number;
11
+ }
12
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../src/exact/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,YAAY,CAAS;gBAEjB,MAAM,SAAM;IAMxB,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IASpD,GAAG,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO;IAIlC,OAAO,CAAC,KAAK;IASb,IAAI,IAAI,WAEP;CACF"}
@@ -0,0 +1,31 @@
1
+ export class TransactionStore {
2
+ maxAge;
3
+ sigToBlock;
4
+ highestBlock;
5
+ constructor(maxAge = 150) {
6
+ this.maxAge = maxAge;
7
+ this.sigToBlock = new Map();
8
+ this.highestBlock = 0;
9
+ }
10
+ add(signature, blockHeight) {
11
+ if (blockHeight > this.highestBlock) {
12
+ this.highestBlock = blockHeight;
13
+ this.prune();
14
+ }
15
+ this.sigToBlock.set(signature, blockHeight);
16
+ }
17
+ has(signature) {
18
+ return this.sigToBlock.has(signature);
19
+ }
20
+ prune() {
21
+ const cutoff = this.highestBlock - this.maxAge;
22
+ for (const [sig, height] of this.sigToBlock) {
23
+ if (height < cutoff) {
24
+ this.sigToBlock.delete(sig);
25
+ }
26
+ }
27
+ }
28
+ get size() {
29
+ return this.sigToBlock.size;
30
+ }
31
+ }
@@ -1,9 +1,11 @@
1
1
  import type { PaymentHandler } from "@faremeter/types/client";
2
+ import type { SolanaCAIP2Network } from "@faremeter/info/solana";
2
3
  import { Connection, PublicKey, TransactionInstruction, VersionedTransaction } from "@solana/web3.js";
3
4
  export type Wallet = {
4
- network: string;
5
+ network: string | SolanaCAIP2Network;
5
6
  publicKey: PublicKey;
6
7
  buildTransaction?: (instructions: TransactionInstruction[], recentBlockHash: string) => Promise<VersionedTransaction>;
8
+ partiallySignTransaction?: (tx: VersionedTransaction) => Promise<VersionedTransaction>;
7
9
  updateTransaction?: (tx: VersionedTransaction) => Promise<VersionedTransaction>;
8
10
  sendTransaction?: (tx: VersionedTransaction) => Promise<string>;
9
11
  };
@@ -14,10 +16,23 @@ interface GetAssociatedTokenAddressSyncOptions {
14
16
  }
15
17
  interface CreatePaymentHandlerOptions {
16
18
  token?: GetAssociatedTokenAddressSyncOptions;
19
+ settlementRentDestination?: string;
17
20
  features?: {
18
21
  enableSettlementAccounts?: boolean;
19
22
  };
20
23
  }
24
+ /**
25
+ * Creates a payment handler for the Solana exact payment scheme.
26
+ *
27
+ * The handler builds SPL token transfer transactions that can be signed
28
+ * and submitted by the client to fulfill x402 payment requirements.
29
+ *
30
+ * @param wallet - Wallet providing signing capabilities
31
+ * @param mint - SPL token mint public key
32
+ * @param connection - Optional Solana connection for fetching blockhash and mint info
33
+ * @param options - Optional configuration for token address and features
34
+ * @returns A PaymentHandler function for use with the x402 client
35
+ */
21
36
  export declare function createPaymentHandler(wallet: Wallet, mint: PublicKey, connection?: Connection, options?: CreatePaymentHandlerOptions): PaymentHandler;
22
37
  export {};
23
38
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/exact/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,cAAc,EAEf,MAAM,yBAAyB,CAAC;AAYjC,OAAO,EAEL,UAAU,EACV,SAAS,EACT,sBAAsB,EAEtB,oBAAoB,EAErB,MAAM,iBAAiB,CAAC;AAIzB,MAAM,MAAM,MAAM,GAAG;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,gBAAgB,CAAC,EAAE,CACjB,YAAY,EAAE,sBAAsB,EAAE,EACtC,eAAe,EAAE,MAAM,KACpB,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,iBAAiB,CAAC,EAAE,CAClB,EAAE,EAAE,oBAAoB,KACrB,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,oBAAoB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACjE,CAAC;AAEF,UAAU,oCAAoC;IAC5C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,wBAAwB,CAAC,EAAE,SAAS,CAAC;CACtC;AAiFD,UAAU,2BAA2B;IACnC,KAAK,CAAC,EAAE,oCAAoC,CAAC;IAC7C,QAAQ,CAAC,EAAE;QACT,wBAAwB,CAAC,EAAE,OAAO,CAAC;KACpC,CAAC;CACH;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,EACf,UAAU,CAAC,EAAE,UAAU,EACvB,OAAO,CAAC,EAAE,2BAA2B,GACpC,cAAc,CAwKhB"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/exact/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,cAAc,EAEf,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAYjE,OAAO,EAEL,UAAU,EACV,SAAS,EACT,sBAAsB,EAEtB,oBAAoB,EAErB,MAAM,iBAAiB,CAAC;AAKzB,MAAM,MAAM,MAAM,GAAG;IACnB,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAAC;IACrC,SAAS,EAAE,SAAS,CAAC;IACrB,gBAAgB,CAAC,EAAE,CACjB,YAAY,EAAE,sBAAsB,EAAE,EACtC,eAAe,EAAE,MAAM,KACpB,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,wBAAwB,CAAC,EAAE,CACzB,EAAE,EAAE,oBAAoB,KACrB,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,iBAAiB,CAAC,EAAE,CAClB,EAAE,EAAE,oBAAoB,KACrB,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,oBAAoB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACjE,CAAC;AAEF,UAAU,oCAAoC;IAC5C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,wBAAwB,CAAC,EAAE,SAAS,CAAC;CACtC;AAiFD,UAAU,2BAA2B;IACnC,KAAK,CAAC,EAAE,oCAAoC,CAAC;IAC7C,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,EAAE;QACT,wBAAwB,CAAC,EAAE,OAAO,CAAC;KACpC,CAAC;CACH;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,EACf,UAAU,CAAC,EAAE,UAAU,EACvB,OAAO,CAAC,EAAE,2BAA2B,GACpC,cAAc,CA2LhB"}
@@ -4,6 +4,7 @@ import { getBase64EncodedWireTransaction, } from "@solana/transactions";
4
4
  import { ComputeBudgetProgram, Connection, PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction, Keypair, } from "@solana/web3.js";
5
5
  import { PaymentRequirementsExtra } from "./facilitator.js";
6
6
  import { generateMatcher } from "./common.js";
7
+ import { logger } from "./logger.js";
7
8
  function generateGetAssociatedTokenAddressSyncRest(tokenConfig) {
8
9
  const { allowOwnerOffCurve, programId, associatedTokenProgramId } = tokenConfig;
9
10
  // NOTE: These map to the trailing default args of
@@ -45,7 +46,7 @@ async function extractMetadata(args) {
45
46
  }
46
47
  const payerKey = new PublicKey(extra.feePayer);
47
48
  const payTo = new PublicKey(requirements.payTo);
48
- const amount = Number(requirements.maxAmountRequired);
49
+ const amount = Number(requirements.amount);
49
50
  let paymentMode = PaymentMode.ToSpec;
50
51
  if (options?.features?.enableSettlementAccounts &&
51
52
  extra.features?.xSettlementAccountSupported &&
@@ -61,11 +62,38 @@ async function extractMetadata(args) {
61
62
  paymentMode,
62
63
  };
63
64
  }
65
+ /**
66
+ * Creates a payment handler for the Solana exact payment scheme.
67
+ *
68
+ * The handler builds SPL token transfer transactions that can be signed
69
+ * and submitted by the client to fulfill x402 payment requirements.
70
+ *
71
+ * @param wallet - Wallet providing signing capabilities
72
+ * @param mint - SPL token mint public key
73
+ * @param connection - Optional Solana connection for fetching blockhash and mint info
74
+ * @param options - Optional configuration for token address and features
75
+ * @returns A PaymentHandler function for use with the x402 client
76
+ */
64
77
  export function createPaymentHandler(wallet, mint, connection, options) {
65
78
  const getAssociatedTokenAddressSyncRest = generateGetAssociatedTokenAddressSyncRest(options?.token ?? {});
79
+ let hasWarnedAboutDeprecation = false;
80
+ const signTransaction = async (tx) => {
81
+ if (wallet.partiallySignTransaction) {
82
+ return wallet.partiallySignTransaction(tx);
83
+ }
84
+ if (wallet.updateTransaction) {
85
+ if (!hasWarnedAboutDeprecation) {
86
+ logger.warning("wallet.partiallySignTransaction is not available, falling back to updateTransaction");
87
+ hasWarnedAboutDeprecation = true;
88
+ }
89
+ return wallet.updateTransaction(tx);
90
+ }
91
+ return tx;
92
+ };
66
93
  const { isMatchingRequirement } = generateMatcher(wallet.network, mint ? mint.toBase58() : "sol");
67
94
  return async (_context, accepts) => {
68
- const res = accepts.filter(isMatchingRequirement).map((requirements) => {
95
+ const compatibleRequirements = accepts.filter(isMatchingRequirement);
96
+ const res = compatibleRequirements.map((requirements) => {
69
97
  const exec = async () => {
70
98
  const { recentBlockhash, decimals, payTo, amount, payerKey, paymentMode, } = await extractMetadata({
71
99
  connection,
@@ -99,9 +127,7 @@ export function createPaymentHandler(wallet, mint, connection, options) {
99
127
  }).compileToV0Message();
100
128
  tx = new VersionedTransaction(message);
101
129
  }
102
- if (wallet.updateTransaction) {
103
- tx = await wallet.updateTransaction(tx);
104
- }
130
+ tx = await signTransaction(tx);
105
131
  const base64EncodedWireTransaction = getBase64EncodedWireTransaction({
106
132
  messageBytes: tx.message.serialize(),
107
133
  signatures: tx.signatures,
@@ -127,17 +153,17 @@ export function createPaymentHandler(wallet, mint, connection, options) {
127
153
  }).compileToV0Message();
128
154
  tx = new VersionedTransaction(message);
129
155
  }
130
- if (wallet.updateTransaction) {
131
- tx = await wallet.updateTransaction(tx);
132
- }
156
+ tx = await signTransaction(tx);
133
157
  if (!wallet.sendTransaction) {
134
158
  throw new Error("wallet must support sending transactions to use settlement accounts with exact");
135
159
  }
136
160
  const transactionSignature = await wallet.sendTransaction(tx);
137
161
  const settleSecretKey = Buffer.from(settleKeypair.secretKey).toString("base64");
162
+ const settlementRentDestination = options?.settlementRentDestination ?? wallet.publicKey.toBase58();
138
163
  const payload = {
139
164
  settleSecretKey,
140
165
  transactionSignature,
166
+ settlementRentDestination,
141
167
  };
142
168
  return { payload };
143
169
  }
@@ -1,5 +1,6 @@
1
+ import { type SolanaCAIP2Network } from "@faremeter/info/solana";
1
2
  export declare const x402Scheme = "exact";
2
- export declare function generateMatcher(network: string, asset: string): {
3
+ export declare function generateMatcher(network: string | SolanaCAIP2Network, asset: string): {
3
4
  matchTuple: import("arktype/internal/methods/object.ts").ObjectType<{
4
5
  scheme: (In: string) => import("arktype/internal/attributes.ts").To<Lowercase<string>>;
5
6
  network: (In: string) => import("arktype/internal/attributes.ts").To<Lowercase<string>>;
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/exact/common.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;;;;;;;;;;;EAI7D"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/exact/common.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,GAAG,kBAAkB,EACpC,KAAK,EAAE,MAAM;;;;;;;;;;;EASd"}
@@ -1,8 +1,7 @@
1
1
  import { generateRequirementsMatcher } from "@faremeter/types/x402";
2
- import { lookupX402Network } from "@faremeter/info/solana";
2
+ import { lookupX402Network, } from "@faremeter/info/solana";
3
3
  export const x402Scheme = "exact";
4
4
  export function generateMatcher(network, asset) {
5
- return generateRequirementsMatcher([x402Scheme], lookupX402Network(network), [
6
- asset,
7
- ]);
5
+ const solanaNetwork = lookupX402Network(network);
6
+ return generateRequirementsMatcher([x402Scheme], [solanaNetwork.caip2], [asset]);
8
7
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env pnpm tsx
2
2
  import t from "tap";
3
3
  import { isValidationError as iVE } from "@faremeter/types";
4
- import { lookupKnownSPLToken } from "@faremeter/info/solana";
4
+ import { lookupKnownSPLToken, clusterToCAIP2 } from "@faremeter/info/solana";
5
5
  import { generateMatcher } from "./common.js";
6
6
  await t.test("testBasicMatching", async (t) => {
7
7
  {
@@ -11,13 +11,21 @@ await t.test("testBasicMatching", async (t) => {
11
11
  return;
12
12
  }
13
13
  const { matchTuple } = generateMatcher("mainnet-beta", tokenInfo.address);
14
+ const network = clusterToCAIP2("mainnet-beta");
14
15
  const req = {
15
- network: "solana-mainnet-beta",
16
+ network: network.caip2,
16
17
  scheme: "exact",
17
18
  asset: tokenInfo.address,
18
19
  };
20
+ // CAIP-2 network identifier should match
19
21
  t.ok(!iVE(matchTuple(req)));
20
- t.ok(!iVE(matchTuple({
22
+ // Legacy network names should not match (normalization happens in
23
+ // the routes layer before dispatch, not in the handler matcher)
24
+ t.ok(iVE(matchTuple({
25
+ ...req,
26
+ network: "solana-mainnet-beta",
27
+ })));
28
+ t.ok(iVE(matchTuple({
21
29
  ...req,
22
30
  network: "solana",
23
31
  })));
@@ -1,7 +1,29 @@
1
+ import { type x402PaymentRequirements, type x402PaymentPayload, type x402SettleResponse, type x402VerifyResponse } from "@faremeter/types/x402v2";
1
2
  import type { FacilitatorHandler } from "@faremeter/types/facilitator";
3
+ import { type SolanaCAIP2Network } from "@faremeter/info/solana";
4
+ import { fetchMint } from "@solana-program/token";
2
5
  import { type Rpc, type SolanaRpcApi } from "@solana/kit";
3
6
  import type { TransactionError } from "@solana/rpc-types";
4
7
  import { Keypair, PublicKey } from "@solana/web3.js";
8
+ import { logger } from "./logger.js";
9
+ export interface HookBaseArgs {
10
+ network: string | SolanaCAIP2Network;
11
+ rpc: Rpc<SolanaRpcApi>;
12
+ feePayerKeypair: Keypair;
13
+ mint: PublicKey;
14
+ mintInfo: Awaited<ReturnType<typeof fetchMint>>;
15
+ requirements: x402PaymentRequirements;
16
+ payment: x402PaymentPayload;
17
+ logger: typeof logger;
18
+ }
19
+ export type HookResponseArgs<Response> = HookBaseArgs & {
20
+ response: Response;
21
+ };
22
+ export type HookResponseFuncs<Response> = (args: HookResponseArgs<Response>) => Promise<Response> | Promise<void>;
23
+ export interface FacilitatorHooks {
24
+ afterVerify?: HookResponseFuncs<x402VerifyResponse>;
25
+ afterSettle?: HookResponseFuncs<x402SettleResponse>;
26
+ }
5
27
  export declare const PaymentRequirementsExtraFeatures: import("arktype/internal/methods/object.ts").ObjectType<{
6
28
  xSettlementAccountSupported?: boolean;
7
29
  }, {}>;
@@ -18,9 +40,12 @@ interface FacilitatorOptions {
18
40
  maxRetries?: number;
19
41
  retryDelayMs?: number;
20
42
  maxPriorityFee?: number;
43
+ maxTransactionAge?: number;
21
44
  features?: {
22
45
  enableSettlementAccounts?: boolean;
46
+ enableDuplicateCheck?: boolean;
23
47
  };
48
+ hooks?: readonly FacilitatorHooks[];
24
49
  }
25
50
  export declare const PaymentPayloadTransaction: import("arktype/internal/methods/object.ts").ObjectType<{
26
51
  transaction: (In: string) => import("arktype").Out<Readonly<{
@@ -32,9 +57,23 @@ export type PaymentPayloadTransaction = typeof PaymentPayloadTransaction.infer;
32
57
  export declare const PaymentPayloadSettlementAccount: import("arktype/internal/methods/object.ts").ObjectType<{
33
58
  transactionSignature: string;
34
59
  settleSecretKey: (In: string) => import("arktype").Out<Uint8Array<ArrayBuffer>>;
60
+ settlementRentDestination?: string;
35
61
  }, {}>;
36
62
  export type PaymentPayloadSettlementAccount = typeof PaymentPayloadSettlementAccount.infer;
37
63
  export declare function transactionErrorToString(t: TransactionError): string;
38
- export declare const createFacilitatorHandler: (network: string, rpc: Rpc<SolanaRpcApi>, feePayerKeypair: Keypair, mint: PublicKey, config?: FacilitatorOptions) => Promise<FacilitatorHandler>;
64
+ /**
65
+ * Creates a facilitator handler for the Solana exact payment scheme.
66
+ *
67
+ * The handler validates incoming payment transactions, signs them with the
68
+ * fee payer keypair, and submits them to the Solana network.
69
+ *
70
+ * @param network - Solana network identifier (cluster name, CAIP-2 string, or SolanaCAIP2Network object)
71
+ * @param rpc - Solana RPC client
72
+ * @param feePayerKeypair - Keypair for paying transaction fees
73
+ * @param mint - SPL token mint public key
74
+ * @param config - Optional configuration for retries, fees, and hooks
75
+ * @returns A FacilitatorHandler for processing Solana exact payments
76
+ */
77
+ export declare const createFacilitatorHandler: (network: string | SolanaCAIP2Network, rpc: Rpc<SolanaRpcApi>, feePayerKeypair: Keypair, mint: PublicKey, config?: FacilitatorOptions) => Promise<FacilitatorHandler>;
39
78
  export {};
40
79
  //# sourceMappingURL=facilitator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"facilitator.d.ts","sourceRoot":"","sources":["../../../src/exact/facilitator.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,OAAO,EAYL,KAAK,GAAG,EACR,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAMrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAerD,eAAO,MAAM,gCAAgC;;MAE3C,CAAC;AAEH,MAAM,MAAM,gCAAgC,GAC1C,OAAO,gCAAgC,CAAC,KAAK,CAAC;AAEhD,eAAO,MAAM,wBAAwB;;;;;;;MAKnC,CAAC;AAEH,UAAU,kBAAkB;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE;QACT,wBAAwB,CAAC,EAAE,OAAO,CAAC;KACpC,CAAC;CACH;AASD,eAAO,MAAM,yBAAyB;;;;;MAEpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,OAAO,yBAAyB,CAAC,KAAK,CAAC;AAE/E,eAAO,MAAM,+BAA+B;;;MAK1C,CAAC;AACH,MAAM,MAAM,+BAA+B,GACzC,OAAO,+BAA+B,CAAC,KAAK,CAAC;AAE/C,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,gBAAgB,UAY3D;AAiDD,eAAO,MAAM,wBAAwB,GACnC,SAAS,MAAM,EACf,KAAK,GAAG,CAAC,YAAY,CAAC,EACtB,iBAAiB,OAAO,EACxB,MAAM,SAAS,EACf,SAAS,kBAAkB,KAC1B,OAAO,CAAC,kBAAkB,CA+T5B,CAAC"}
1
+ {"version":3,"file":"facilitator.d.ts","sourceRoot":"","sources":["../../../src/exact/facilitator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EAExB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAML,KAAK,kBAAkB,EACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAYL,KAAK,GAAG,EACR,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAOrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAalC,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAAC;IACrC,GAAG,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC;IAChD,YAAY,EAAE,uBAAuB,CAAC;IACtC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,MAAM,EAAE,OAAO,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,gBAAgB,CAAC,QAAQ,IAAI,YAAY,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,iBAAiB,CAAC,QAAQ,IAAI,CACxC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,CAAC,KAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvC,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,iBAAiB,CAAC,kBAAkB,CAAC,CAAC;IACpD,WAAW,CAAC,EAAE,iBAAiB,CAAC,kBAAkB,CAAC,CAAC;CACrD;AAED,eAAO,MAAM,gCAAgC;;MAE3C,CAAC;AAEH,MAAM,MAAM,gCAAgC,GAC1C,OAAO,gCAAgC,CAAC,KAAK,CAAC;AAEhD,eAAO,MAAM,wBAAwB;;;;;;;MAKnC,CAAC;AAEH,UAAU,kBAAkB;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE;QACT,wBAAwB,CAAC,EAAE,OAAO,CAAC;QACnC,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC;IACF,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACrC;AASD,eAAO,MAAM,yBAAyB;;;;;MAEpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,OAAO,yBAAyB,CAAC,KAAK,CAAC;AAE/E,eAAO,MAAM,+BAA+B;;;;MAM1C,CAAC;AACH,MAAM,MAAM,+BAA+B,GACzC,OAAO,+BAA+B,CAAC,KAAK,CAAC;AAE/C,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,gBAAgB,UAY3D;AAiDD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,GACnC,SAAS,MAAM,GAAG,kBAAkB,EACpC,KAAK,GAAG,CAAC,YAAY,CAAC,EACtB,iBAAiB,OAAO,EACxB,MAAM,SAAS,EACf,SAAS,kBAAkB,KAC1B,OAAO,CAAC,kBAAkB,CAub5B,CAAC"}
@@ -1,9 +1,9 @@
1
- import { x402PaymentRequirements, } from "@faremeter/types/x402";
1
+ import {} from "@faremeter/types/x402v2";
2
2
  import { isValidationError } from "@faremeter/types";
3
- import { lookupX402Network } from "@faremeter/info/solana";
3
+ import { clusterToCAIP2, isKnownCluster, caip2ToCluster, isSolanaCAIP2Network, } from "@faremeter/info/solana";
4
4
  import { fetchMint } from "@solana-program/token";
5
5
  import { address, createKeyPairSignerFromBytes, decompileTransactionMessage, getBase64Encoder, getCompiledTransactionMessageDecoder, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, pipe, } from "@solana/kit";
6
- import { getBase64EncodedWireTransaction, getTransactionDecoder, partiallySignTransaction, } from "@solana/transactions";
6
+ import { getBase64EncodedWireTransaction, getSignatureFromTransaction, getTransactionDecoder, partiallySignTransaction, } from "@solana/transactions";
7
7
  import { Keypair, PublicKey } from "@solana/web3.js";
8
8
  import { type } from "arktype";
9
9
  import { isValidTransaction } from "./verify.js";
@@ -11,6 +11,7 @@ import { logger } from "./logger.js";
11
11
  import { x402Scheme, generateMatcher } from "./common.js";
12
12
  import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, getTransferCheckedInstruction, getCloseAccountInstruction, } from "@solana-program/token";
13
13
  import { getAddMemoInstruction } from "@solana-program/memo";
14
+ import { TransactionStore } from "./cache.js";
14
15
  export const PaymentRequirementsExtraFeatures = type({
15
16
  xSettlementAccountSupported: "boolean?",
16
17
  });
@@ -32,6 +33,7 @@ export const PaymentPayloadTransaction = type({
32
33
  export const PaymentPayloadSettlementAccount = type({
33
34
  transactionSignature: "string",
34
35
  settleSecretKey: type("string.base64").pipe.try((s) => Uint8Array.from(Buffer.from(s, "base64"))),
36
+ "settlementRentDestination?": "string",
35
37
  });
36
38
  export function transactionErrorToString(t) {
37
39
  if (typeof t == "string") {
@@ -50,7 +52,7 @@ const sendTransaction = async (rpc, signedTransaction, maxRetries, retryDelayMs)
50
52
  })
51
53
  .send();
52
54
  if (simResult.value.err) {
53
- logger.error("transaction simulation failed: {*}", simResult.value);
55
+ logger.error("transaction simulation failed", simResult.value);
54
56
  return { success: false, error: "Transaction simulation failed" };
55
57
  }
56
58
  const signature = await rpc
@@ -74,14 +76,36 @@ const sendTransaction = async (rpc, signedTransaction, maxRetries, retryDelayMs)
74
76
  }
75
77
  return { success: false, error: "Transaction confirmation timeout" };
76
78
  };
79
+ /**
80
+ * Creates a facilitator handler for the Solana exact payment scheme.
81
+ *
82
+ * The handler validates incoming payment transactions, signs them with the
83
+ * fee payer keypair, and submits them to the Solana network.
84
+ *
85
+ * @param network - Solana network identifier (cluster name, CAIP-2 string, or SolanaCAIP2Network object)
86
+ * @param rpc - Solana RPC client
87
+ * @param feePayerKeypair - Keypair for paying transaction fees
88
+ * @param mint - SPL token mint public key
89
+ * @param config - Optional configuration for retries, fees, and hooks
90
+ * @returns A FacilitatorHandler for processing Solana exact payments
91
+ */
77
92
  export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mint, config) => {
78
93
  const { isMatchingRequirement } = generateMatcher(network, mint.toBase58());
79
- const { maxRetries = 30, retryDelayMs = 1000, maxPriorityFee = 100_000, } = config ?? {};
94
+ const { maxRetries = 30, retryDelayMs = 1000, maxPriorityFee = 100_000, maxTransactionAge = 150, } = config ?? {};
80
95
  const mintInfo = await fetchMint(rpc, address(mint.toBase58()));
96
+ const hookArgs = {
97
+ network,
98
+ rpc,
99
+ feePayerKeypair,
100
+ mint,
101
+ mintInfo,
102
+ logger,
103
+ };
81
104
  const features = {};
82
105
  if (config?.features?.enableSettlementAccounts) {
83
106
  features.xSettlementAccountSupported = true;
84
107
  }
108
+ const seenTxs = new TransactionStore(maxTransactionAge);
85
109
  const processSettlementAccount = async (requirements, paymentPayload) => {
86
110
  const errorResponse = (error) => ({ error });
87
111
  // XXX - It would be nicer to do this check in the arktype
@@ -101,12 +125,12 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
101
125
  const { value: accountBalance } = await rpc
102
126
  .getTokenAccountBalance(settleATA, { commitment: "confirmed" })
103
127
  .send();
104
- logger.debug("settlement account info: {*}", {
128
+ logger.debug("settlement account info", {
105
129
  settleOwner,
106
130
  settleATA,
107
131
  accountBalance,
108
132
  });
109
- if (BigInt(accountBalance.amount) !== BigInt(requirements.maxAmountRequired)) {
133
+ if (BigInt(accountBalance.amount) !== BigInt(requirements.amount)) {
110
134
  return errorResponse("settlement account balance didn't match payment requirements");
111
135
  }
112
136
  const settle = async () => {
@@ -116,6 +140,9 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
116
140
  owner: address(requirements.payTo),
117
141
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
118
142
  });
143
+ const closeDestination = paymentPayload.settlementRentDestination
144
+ ? address(paymentPayload.settlementRentDestination)
145
+ : feePayerSigner.address;
119
146
  const instructions = [
120
147
  getAddMemoInstruction({ memo: crypto.randomUUID() }),
121
148
  getTransferCheckedInstruction({
@@ -123,12 +150,12 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
123
150
  mint: address(mint.toBase58()),
124
151
  destination: payToATA,
125
152
  authority: settleSigner,
126
- amount: BigInt(requirements.maxAmountRequired),
153
+ amount: BigInt(requirements.amount),
127
154
  decimals: mintInfo.data.decimals,
128
155
  }),
129
156
  getCloseAccountInstruction({
130
157
  account: settleATA,
131
- destination: feePayerSigner.address,
158
+ destination: closeDestination,
132
159
  owner: settleSigner,
133
160
  }),
134
161
  ];
@@ -141,22 +168,32 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
141
168
  };
142
169
  };
143
170
  return {
171
+ payer: settleOwner,
144
172
  settle,
145
173
  };
146
174
  };
147
175
  const processTransaction = async (requirements, paymentPayload) => {
148
176
  const errorResponse = (error) => ({ error });
149
- let transactionMessage, transaction;
177
+ let transactionMessage, transaction, blockHeight;
150
178
  try {
151
179
  transaction = paymentPayload.transaction;
152
180
  const compiledTransactionMessage = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes);
153
181
  transactionMessage = decompileTransactionMessage(compiledTransactionMessage);
182
+ const lifetimeConstraint = transactionMessage.lifetimeConstraint;
183
+ if ("blockhash" in lifetimeConstraint) {
184
+ blockHeight = Number(lifetimeConstraint.lastValidBlockHeight) - 150;
185
+ }
186
+ else {
187
+ return errorResponse("Transaction cannot include a nonce account");
188
+ }
154
189
  }
155
190
  catch (cause) {
156
191
  throw new Error("Failed to get compiled transaction message", { cause });
157
192
  }
193
+ let validResult;
158
194
  try {
159
- if (!(await isValidTransaction(transactionMessage, requirements, feePayerKeypair.publicKey, maxPriorityFee))) {
195
+ validResult = await isValidTransaction(transactionMessage, requirements, feePayerKeypair.publicKey.toBase58(), maxPriorityFee);
196
+ if (!validResult) {
160
197
  logger.error("Invalid transaction");
161
198
  return errorResponse("Invalid transaction");
162
199
  }
@@ -164,7 +201,10 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
164
201
  catch (cause) {
165
202
  throw new Error("Failed to validate transaction", { cause });
166
203
  }
204
+ const { payer } = validResult;
167
205
  return {
206
+ payer,
207
+ blockHeight,
168
208
  settle: async () => {
169
209
  let signedTransaction;
170
210
  try {
@@ -174,9 +214,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
174
214
  catch (cause) {
175
215
  throw new Error("Failed to partially sign transaction", { cause });
176
216
  }
177
- return {
178
- signedTransaction,
179
- };
217
+ return { signedTransaction };
180
218
  },
181
219
  };
182
220
  };
@@ -201,18 +239,37 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
201
239
  return processTransaction(requirements, paymentPayload);
202
240
  }
203
241
  };
242
+ const resolveCluster = () => {
243
+ if (isSolanaCAIP2Network(network)) {
244
+ const resolved = caip2ToCluster(network.caip2);
245
+ if (resolved) {
246
+ return resolved;
247
+ }
248
+ throw new Error(`Unknown Solana network: ${network.caip2}`);
249
+ }
250
+ if (isKnownCluster(network)) {
251
+ return network;
252
+ }
253
+ const resolved = caip2ToCluster(network);
254
+ if (resolved) {
255
+ return resolved;
256
+ }
257
+ throw new Error(`Unknown Solana network: ${network}`);
258
+ };
204
259
  const getSupported = () => {
205
- return lookupX402Network(network).map((network) => Promise.resolve({
206
- x402Version: 1,
207
- scheme: x402Scheme,
208
- network,
209
- extra: {
210
- feePayer: feePayerKeypair.publicKey.toString(),
211
- features,
212
- },
213
- }));
260
+ return [
261
+ Promise.resolve({
262
+ x402Version: 2,
263
+ scheme: x402Scheme,
264
+ network: clusterToCAIP2(resolveCluster()).caip2,
265
+ extra: {
266
+ feePayer: feePayerKeypair.publicKey.toString(),
267
+ features,
268
+ },
269
+ }),
270
+ ];
214
271
  };
215
- const getRequirements = async (req) => {
272
+ const getRequirements = async ({ accepts: req, }) => {
216
273
  const recentBlockhash = (await rpc.getLatestBlockhash().send()).value
217
274
  .blockhash;
218
275
  return req.filter(isMatchingRequirement).map((x) => {
@@ -244,7 +301,32 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
244
301
  if ("error" in verifyResult) {
245
302
  return errorResponse(verifyResult.error);
246
303
  }
247
- return { isValid: true };
304
+ let response = {
305
+ isValid: true,
306
+ payer: verifyResult.payer,
307
+ };
308
+ const hooks = config?.hooks;
309
+ if (hooks !== undefined) {
310
+ const args = {
311
+ ...hookArgs,
312
+ requirements,
313
+ payment,
314
+ response,
315
+ };
316
+ for (const hook of hooks) {
317
+ if (hook.afterVerify === undefined) {
318
+ continue;
319
+ }
320
+ const res = await hook.afterVerify({
321
+ ...args,
322
+ response,
323
+ });
324
+ if (res !== undefined) {
325
+ response = res;
326
+ }
327
+ }
328
+ }
329
+ return response;
248
330
  };
249
331
  const handleSettle = async (requirements, payment) => {
250
332
  if (!isMatchingRequirement(requirements)) {
@@ -254,9 +336,9 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
254
336
  logger.error(msg);
255
337
  return {
256
338
  success: false,
257
- error: msg,
258
- txHash: null,
259
- networkId: null,
339
+ errorReason: msg,
340
+ transaction: "",
341
+ network: requirements.network,
260
342
  };
261
343
  };
262
344
  const processor = determinePaymentPayload(requirements, payment.payload);
@@ -267,7 +349,18 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
267
349
  if ("error" in verifyResult) {
268
350
  return errorResponse(verifyResult.error);
269
351
  }
352
+ const { payer } = verifyResult;
270
353
  const { signedTransaction } = await verifyResult.settle();
354
+ if (config?.features?.enableDuplicateCheck &&
355
+ "blockHeight" in verifyResult) {
356
+ const signature = getSignatureFromTransaction(signedTransaction);
357
+ const { blockHeight } = verifyResult;
358
+ if (seenTxs.has(signature)) {
359
+ logger.warning("Duplicate transaction rejected", { signature });
360
+ return errorResponse("Duplicate transaction");
361
+ }
362
+ seenTxs.add(signature, blockHeight);
363
+ }
271
364
  let result;
272
365
  try {
273
366
  result = await sendTransaction(rpc, signedTransaction, maxRetries, retryDelayMs);
@@ -278,12 +371,34 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
278
371
  if (!result.success) {
279
372
  return errorResponse(result.error);
280
373
  }
281
- return {
374
+ let response = {
282
375
  success: true,
283
- error: null,
284
- txHash: result.signature,
285
- networkId: payment.network,
376
+ transaction: result.signature,
377
+ network: payment.accepted.network,
378
+ payer,
286
379
  };
380
+ const hooks = config?.hooks;
381
+ if (hooks !== undefined) {
382
+ const args = {
383
+ ...hookArgs,
384
+ requirements,
385
+ payment,
386
+ response,
387
+ };
388
+ for (const hook of hooks) {
389
+ if (hook.afterSettle === undefined) {
390
+ continue;
391
+ }
392
+ const res = await hook.afterSettle({
393
+ ...args,
394
+ response,
395
+ });
396
+ if (res !== undefined) {
397
+ response = res;
398
+ }
399
+ }
400
+ }
401
+ return response;
287
402
  };
288
403
  return {
289
404
  getSupported,