@faremeter/payment-solana 0.16.0 → 0.17.1

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,12 +1,13 @@
1
- import { x402PaymentRequirements, type x402PaymentPayload, type x402SettleResponse, type x402VerifyResponse } from "@faremeter/types/x402";
1
+ import { type x402PaymentRequirements, type x402PaymentPayload, type x402SettleResponse, type x402VerifyResponse } from "@faremeter/types/x402v2";
2
2
  import type { FacilitatorHandler } from "@faremeter/types/facilitator";
3
+ import { type SolanaCAIP2Network } from "@faremeter/info/solana";
3
4
  import { fetchMint } from "@solana-program/token";
4
5
  import { type Rpc, type SolanaRpcApi } from "@solana/kit";
5
6
  import type { TransactionError } from "@solana/rpc-types";
6
7
  import { Keypair, PublicKey } from "@solana/web3.js";
7
8
  import { logger } from "./logger.js";
8
9
  export interface HookBaseArgs {
9
- network: string;
10
+ network: string | SolanaCAIP2Network;
10
11
  rpc: Rpc<SolanaRpcApi>;
11
12
  feePayerKeypair: Keypair;
12
13
  mint: PublicKey;
@@ -39,8 +40,10 @@ interface FacilitatorOptions {
39
40
  maxRetries?: number;
40
41
  retryDelayMs?: number;
41
42
  maxPriorityFee?: number;
43
+ maxTransactionAge?: number;
42
44
  features?: {
43
45
  enableSettlementAccounts?: boolean;
46
+ enableDuplicateCheck?: boolean;
44
47
  };
45
48
  hooks?: readonly FacilitatorHooks[];
46
49
  }
@@ -54,9 +57,23 @@ export type PaymentPayloadTransaction = typeof PaymentPayloadTransaction.infer;
54
57
  export declare const PaymentPayloadSettlementAccount: import("arktype/internal/methods/object.ts").ObjectType<{
55
58
  transactionSignature: string;
56
59
  settleSecretKey: (In: string) => import("arktype").Out<Uint8Array<ArrayBuffer>>;
60
+ settlementRentDestination?: string;
57
61
  }, {}>;
58
62
  export type PaymentPayloadSettlementAccount = typeof PaymentPayloadSettlementAccount.infer;
59
63
  export declare function transactionErrorToString(t: TransactionError): string;
60
- 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>;
61
78
  export {};
62
79
  //# sourceMappingURL=facilitator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"facilitator.d.ts","sourceRoot":"","sources":["../../../src/exact/facilitator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EAExB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEvE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,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;AAGrD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAYlC,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,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,QAAQ,CAAC,EAAE;QACT,wBAAwB,CAAC,EAAE,OAAO,CAAC;KACpC,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;;;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+X5B,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") {
@@ -74,9 +76,22 @@ 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()));
81
96
  const hookArgs = {
82
97
  network,
@@ -90,6 +105,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
90
105
  if (config?.features?.enableSettlementAccounts) {
91
106
  features.xSettlementAccountSupported = true;
92
107
  }
108
+ const seenTxs = new TransactionStore(maxTransactionAge);
93
109
  const processSettlementAccount = async (requirements, paymentPayload) => {
94
110
  const errorResponse = (error) => ({ error });
95
111
  // XXX - It would be nicer to do this check in the arktype
@@ -114,7 +130,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
114
130
  settleATA,
115
131
  accountBalance,
116
132
  });
117
- if (BigInt(accountBalance.amount) !== BigInt(requirements.maxAmountRequired)) {
133
+ if (BigInt(accountBalance.amount) !== BigInt(requirements.amount)) {
118
134
  return errorResponse("settlement account balance didn't match payment requirements");
119
135
  }
120
136
  const settle = async () => {
@@ -124,6 +140,9 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
124
140
  owner: address(requirements.payTo),
125
141
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
126
142
  });
143
+ const closeDestination = paymentPayload.settlementRentDestination
144
+ ? address(paymentPayload.settlementRentDestination)
145
+ : feePayerSigner.address;
127
146
  const instructions = [
128
147
  getAddMemoInstruction({ memo: crypto.randomUUID() }),
129
148
  getTransferCheckedInstruction({
@@ -131,12 +150,12 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
131
150
  mint: address(mint.toBase58()),
132
151
  destination: payToATA,
133
152
  authority: settleSigner,
134
- amount: BigInt(requirements.maxAmountRequired),
153
+ amount: BigInt(requirements.amount),
135
154
  decimals: mintInfo.data.decimals,
136
155
  }),
137
156
  getCloseAccountInstruction({
138
157
  account: settleATA,
139
- destination: feePayerSigner.address,
158
+ destination: closeDestination,
140
159
  owner: settleSigner,
141
160
  }),
142
161
  ];
@@ -149,22 +168,32 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
149
168
  };
150
169
  };
151
170
  return {
171
+ payer: settleOwner,
152
172
  settle,
153
173
  };
154
174
  };
155
175
  const processTransaction = async (requirements, paymentPayload) => {
156
176
  const errorResponse = (error) => ({ error });
157
- let transactionMessage, transaction;
177
+ let transactionMessage, transaction, blockHeight;
158
178
  try {
159
179
  transaction = paymentPayload.transaction;
160
180
  const compiledTransactionMessage = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes);
161
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
+ }
162
189
  }
163
190
  catch (cause) {
164
191
  throw new Error("Failed to get compiled transaction message", { cause });
165
192
  }
193
+ let validResult;
166
194
  try {
167
- if (!(await isValidTransaction(transactionMessage, requirements, feePayerKeypair.publicKey, maxPriorityFee))) {
195
+ validResult = await isValidTransaction(transactionMessage, requirements, feePayerKeypair.publicKey.toBase58(), maxPriorityFee);
196
+ if (!validResult) {
168
197
  logger.error("Invalid transaction");
169
198
  return errorResponse("Invalid transaction");
170
199
  }
@@ -172,7 +201,10 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
172
201
  catch (cause) {
173
202
  throw new Error("Failed to validate transaction", { cause });
174
203
  }
204
+ const { payer } = validResult;
175
205
  return {
206
+ payer,
207
+ blockHeight,
176
208
  settle: async () => {
177
209
  let signedTransaction;
178
210
  try {
@@ -182,9 +214,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
182
214
  catch (cause) {
183
215
  throw new Error("Failed to partially sign transaction", { cause });
184
216
  }
185
- return {
186
- signedTransaction,
187
- };
217
+ return { signedTransaction };
188
218
  },
189
219
  };
190
220
  };
@@ -209,18 +239,37 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
209
239
  return processTransaction(requirements, paymentPayload);
210
240
  }
211
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
+ };
212
259
  const getSupported = () => {
213
- return lookupX402Network(network).map((network) => Promise.resolve({
214
- x402Version: 1,
215
- scheme: x402Scheme,
216
- network,
217
- extra: {
218
- feePayer: feePayerKeypair.publicKey.toString(),
219
- features,
220
- },
221
- }));
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
+ ];
222
271
  };
223
- const getRequirements = async (req) => {
272
+ const getRequirements = async ({ accepts: req, }) => {
224
273
  const recentBlockhash = (await rpc.getLatestBlockhash().send()).value
225
274
  .blockhash;
226
275
  return req.filter(isMatchingRequirement).map((x) => {
@@ -252,7 +301,10 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
252
301
  if ("error" in verifyResult) {
253
302
  return errorResponse(verifyResult.error);
254
303
  }
255
- let response = { isValid: true };
304
+ let response = {
305
+ isValid: true,
306
+ payer: verifyResult.payer,
307
+ };
256
308
  const hooks = config?.hooks;
257
309
  if (hooks !== undefined) {
258
310
  const args = {
@@ -284,9 +336,9 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
284
336
  logger.error(msg);
285
337
  return {
286
338
  success: false,
287
- error: msg,
288
- txHash: null,
289
- networkId: null,
339
+ errorReason: msg,
340
+ transaction: "",
341
+ network: requirements.network,
290
342
  };
291
343
  };
292
344
  const processor = determinePaymentPayload(requirements, payment.payload);
@@ -297,7 +349,18 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
297
349
  if ("error" in verifyResult) {
298
350
  return errorResponse(verifyResult.error);
299
351
  }
352
+ const { payer } = verifyResult;
300
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
+ }
301
364
  let result;
302
365
  try {
303
366
  result = await sendTransaction(rpc, signedTransaction, maxRetries, retryDelayMs);
@@ -310,9 +373,9 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
310
373
  }
311
374
  let response = {
312
375
  success: true,
313
- error: null,
314
- txHash: result.signature,
315
- networkId: payment.network,
376
+ transaction: result.signature,
377
+ network: payment.accepted.network,
378
+ payer,
316
379
  };
317
380
  const hooks = config?.hooks;
318
381
  if (hooks !== undefined) {
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env pnpm tsx
2
2
  import t from "tap";
3
3
  import { transactionErrorToString } from "./facilitator.js";
4
+ import { getV1NetworkIds, clusterToCAIP2 } from "@faremeter/info/solana";
4
5
  class MyBigCrazyError {
5
6
  someBigInt = 32n;
6
7
  someString = "a string";
@@ -37,3 +38,37 @@ await t.test("transactionErrorToString", async (t) => {
37
38
  }
38
39
  t.end();
39
40
  });
41
+ await t.test("getV1NetworkIds returns legacy network identifiers", async (t) => {
42
+ await t.test("mainnet-beta returns two legacy network IDs", (t) => {
43
+ const networkIds = getV1NetworkIds("mainnet-beta");
44
+ t.equal(networkIds.length, 2);
45
+ t.ok(networkIds.includes("solana-mainnet-beta"));
46
+ t.ok(networkIds.includes("solana"));
47
+ t.end();
48
+ });
49
+ await t.test("devnet returns one legacy network ID", (t) => {
50
+ const networkIds = getV1NetworkIds("devnet");
51
+ t.equal(networkIds.length, 1);
52
+ t.ok(networkIds.includes("solana-devnet"));
53
+ t.end();
54
+ });
55
+ t.end();
56
+ });
57
+ await t.test("clusterToCAIP2 returns CAIP-2 network identifiers", async (t) => {
58
+ await t.test("mainnet-beta maps to solana genesis hash", (t) => {
59
+ const network = clusterToCAIP2("mainnet-beta");
60
+ t.equal(network.caip2, "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
61
+ t.end();
62
+ });
63
+ await t.test("devnet maps to solana devnet genesis hash", (t) => {
64
+ const network = clusterToCAIP2("devnet");
65
+ t.equal(network.caip2, "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1");
66
+ t.end();
67
+ });
68
+ await t.test("testnet maps to solana testnet genesis hash", (t) => {
69
+ const network = clusterToCAIP2("testnet");
70
+ t.equal(network.caip2, "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z");
71
+ t.end();
72
+ });
73
+ t.end();
74
+ });
@@ -1,5 +1,6 @@
1
- import type { x402PaymentRequirements } from "@faremeter/types/x402";
1
+ import type { x402PaymentRequirements } from "@faremeter/types/x402v2";
2
2
  import { type CompilableTransactionMessage } from "@solana/kit";
3
- import type { PublicKey } from "@solana/web3.js";
4
- export declare function isValidTransaction(transactionMessage: CompilableTransactionMessage, paymentRequirements: x402PaymentRequirements, facilitatorAddress: PublicKey, maxPriorityFee?: number): Promise<boolean>;
3
+ export declare function isValidTransaction(transactionMessage: CompilableTransactionMessage, paymentRequirements: x402PaymentRequirements, facilitatorAddress: string, maxPriorityFee?: number): Promise<{
4
+ payer: string;
5
+ } | false>;
5
6
  //# 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,uBAAuB,CAAC;AAYrE,OAAO,EAEL,KAAK,4BAA4B,EAElC,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAqHjD,wBAAsB,kBAAkB,CACtC,kBAAkB,EAAE,4BAA4B,EAChD,mBAAmB,EAAE,uBAAuB,EAC5C,kBAAkB,EAAE,SAAS,EAC7B,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,OAAO,CAAC,CAkGlB"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/exact/verify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAWvE,OAAO,EAEL,KAAK,4BAA4B,EAElC,MAAM,aAAa,CAAC;AAuGrB,wBAAsB,kBAAkB,CACtC,kBAAkB,EAAE,4BAA4B,EAChD,mBAAmB,EAAE,uBAAuB,EAC5C,kBAAkB,EAAE,MAAM,EAC1B,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAC,CAkEpC"}