@faremeter/payment-solana 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @faremeter/payment-solana
2
+
3
+ Solana payment scheme implementations for the x402 protocol, supporting SPL token transfers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm install @faremeter/payment-solana
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - SPL token payments (USDC, etc.)
14
+ - Devnet, testnet, and mainnet support
15
+ - Automatic fee payer handling
16
+ - Transaction verification
17
+ - Works with any SPL token
18
+
19
+ ## API Reference
20
+
21
+ <!-- TSDOC_START -->
22
+
23
+ <!-- TSDOC_END -->
24
+
25
+ ## Related Packages
26
+
27
+ - [@faremeter/wallet-solana](https://www.npmjs.com/package/@faremeter/wallet-solana) - Solana wallet adapter
28
+ - [@faremeter/fetch](https://www.npmjs.com/package/@faremeter/fetch) - Client fetch wrapper
29
+ - [@faremeter/facilitator](https://www.npmjs.com/package/@faremeter/facilitator) - Payment facilitator
30
+
31
+ ## License
32
+
33
+ LGPL-3.0-only
@@ -14,6 +14,9 @@ interface GetAssociatedTokenAddressSyncOptions {
14
14
  }
15
15
  interface CreatePaymentHandlerOptions {
16
16
  token?: GetAssociatedTokenAddressSyncOptions;
17
+ features?: {
18
+ enableSettlementAccounts?: boolean;
19
+ };
17
20
  }
18
21
  export declare function createPaymentHandler(wallet: Wallet, mint: PublicKey, connection?: Connection, options?: CreatePaymentHandlerOptions): PaymentHandler;
19
22
  export {};
@@ -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;AAWjC,OAAO,EAEL,UAAU,EACV,SAAS,EACT,sBAAsB,EAEtB,oBAAoB,EACrB,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;AAgBD,UAAU,2BAA2B;IACnC,KAAK,CAAC,EAAE,oCAAoC,CAAC;CAC9C;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,EACf,UAAU,CAAC,EAAE,UAAU,EACvB,OAAO,CAAC,EAAE,2BAA2B,GACpC,cAAc,CAuHhB"}
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,7 +1,7 @@
1
1
  import { isValidationError, throwValidationError } from "@faremeter/types";
2
- import { createTransferCheckedInstruction, getAssociatedTokenAddressSync, getMint, } from "@solana/spl-token";
2
+ import { createAssociatedTokenAccountIdempotentInstruction, createTransferCheckedInstruction, getAssociatedTokenAddressSync, getMint, } from "@solana/spl-token";
3
3
  import { getBase64EncodedWireTransaction, } from "@solana/transactions";
4
- import { ComputeBudgetProgram, Connection, PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js";
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
7
  function generateGetAssociatedTokenAddressSyncRest(tokenConfig) {
@@ -12,44 +12,68 @@ function generateGetAssociatedTokenAddressSyncRest(tokenConfig) {
12
12
  // implementation.
13
13
  return [allowOwnerOffCurve, programId, associatedTokenProgramId];
14
14
  }
15
+ const PaymentMode = {
16
+ ToSpec: "toSpec",
17
+ SettlementAccount: "settlementAccount",
18
+ };
19
+ async function extractMetadata(args) {
20
+ const { connection, mint, requirements, wallet, options } = args;
21
+ const extra = PaymentRequirementsExtra(requirements.extra);
22
+ if (isValidationError(extra)) {
23
+ throwValidationError("couldn't validate requirements extra field", extra);
24
+ }
25
+ let recentBlockhash;
26
+ if (extra.recentBlockhash !== undefined) {
27
+ recentBlockhash = extra.recentBlockhash;
28
+ }
29
+ else if (connection !== undefined) {
30
+ recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
31
+ }
32
+ else {
33
+ throw new Error("couldn't get the latest Solana network block hash");
34
+ }
35
+ let decimals;
36
+ if (extra.decimals !== undefined) {
37
+ decimals = extra.decimals;
38
+ }
39
+ else if (connection !== undefined) {
40
+ const mintInfo = await getMint(connection, mint);
41
+ decimals = mintInfo.decimals;
42
+ }
43
+ else {
44
+ throw new Error("couldn't get the decimal information for the mint");
45
+ }
46
+ const payerKey = new PublicKey(extra.feePayer);
47
+ const payTo = new PublicKey(requirements.payTo);
48
+ const amount = Number(requirements.maxAmountRequired);
49
+ let paymentMode = PaymentMode.ToSpec;
50
+ if (options?.features?.enableSettlementAccounts &&
51
+ extra.features?.xSettlementAccountSupported &&
52
+ wallet.sendTransaction) {
53
+ paymentMode = PaymentMode.SettlementAccount;
54
+ }
55
+ return {
56
+ recentBlockhash,
57
+ decimals,
58
+ payTo,
59
+ amount,
60
+ payerKey,
61
+ paymentMode,
62
+ };
63
+ }
15
64
  export function createPaymentHandler(wallet, mint, connection, options) {
16
65
  const getAssociatedTokenAddressSyncRest = generateGetAssociatedTokenAddressSyncRest(options?.token ?? {});
17
66
  const { isMatchingRequirement } = generateMatcher(wallet.network, mint ? mint.toBase58() : "sol");
18
67
  return async (_context, accepts) => {
19
68
  const res = accepts.filter(isMatchingRequirement).map((requirements) => {
20
- const extra = PaymentRequirementsExtra(requirements.extra);
21
- if (isValidationError(extra)) {
22
- throwValidationError("couldn't validate requirements extra field", extra);
23
- }
24
69
  const exec = async () => {
25
- let recentBlockhash;
26
- if (extra.recentBlockhash !== undefined) {
27
- recentBlockhash = extra.recentBlockhash;
28
- }
29
- else if (connection !== undefined) {
30
- recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
31
- }
32
- else {
33
- throw new Error("couldn't get the latest Solana network block hash");
34
- }
35
- let decimals;
36
- if (extra.decimals !== undefined) {
37
- decimals = extra.decimals;
38
- }
39
- else if (connection !== undefined) {
40
- const mintInfo = await getMint(connection, mint);
41
- decimals = mintInfo.decimals;
42
- }
43
- else {
44
- throw new Error("couldn't get the decimal information for the mint");
45
- }
46
- const paymentRequirements = {
47
- ...extra,
48
- amount: Number(requirements.maxAmountRequired),
49
- receiver: new PublicKey(requirements.payTo),
50
- };
51
- const sourceAccount = getAssociatedTokenAddressSync(mint, wallet.publicKey, ...getAssociatedTokenAddressSyncRest);
52
- const receiverAccount = getAssociatedTokenAddressSync(mint, paymentRequirements.receiver, ...getAssociatedTokenAddressSyncRest);
70
+ const { recentBlockhash, decimals, payTo, amount, payerKey, paymentMode, } = await extractMetadata({
71
+ connection,
72
+ mint,
73
+ requirements,
74
+ wallet,
75
+ options,
76
+ });
53
77
  const instructions = [
54
78
  ComputeBudgetProgram.setComputeUnitLimit({
55
79
  units: 50_000,
@@ -57,31 +81,67 @@ export function createPaymentHandler(wallet, mint, connection, options) {
57
81
  ComputeBudgetProgram.setComputeUnitPrice({
58
82
  microLamports: 1,
59
83
  }),
60
- createTransferCheckedInstruction(sourceAccount, mint, receiverAccount, wallet.publicKey, paymentRequirements.amount, decimals),
61
84
  ];
62
- let tx;
63
- if (wallet.buildTransaction) {
64
- tx = await wallet.buildTransaction(instructions, recentBlockhash);
65
- }
66
- else {
67
- const message = new TransactionMessage({
68
- instructions,
69
- payerKey: new PublicKey(paymentRequirements.feePayer),
70
- recentBlockhash,
71
- }).compileToV0Message();
72
- tx = new VersionedTransaction(message);
73
- }
74
- if (wallet.updateTransaction) {
75
- tx = await wallet.updateTransaction(tx);
85
+ const sourceAccount = getAssociatedTokenAddressSync(mint, wallet.publicKey, ...getAssociatedTokenAddressSyncRest);
86
+ switch (paymentMode) {
87
+ case PaymentMode.ToSpec: {
88
+ const receiverAccount = getAssociatedTokenAddressSync(mint, payTo, ...getAssociatedTokenAddressSyncRest);
89
+ instructions.push(createTransferCheckedInstruction(sourceAccount, mint, receiverAccount, wallet.publicKey, amount, decimals));
90
+ let tx;
91
+ if (wallet.buildTransaction) {
92
+ tx = await wallet.buildTransaction(instructions, recentBlockhash);
93
+ }
94
+ else {
95
+ const message = new TransactionMessage({
96
+ instructions,
97
+ payerKey,
98
+ recentBlockhash,
99
+ }).compileToV0Message();
100
+ tx = new VersionedTransaction(message);
101
+ }
102
+ if (wallet.updateTransaction) {
103
+ tx = await wallet.updateTransaction(tx);
104
+ }
105
+ const base64EncodedWireTransaction = getBase64EncodedWireTransaction({
106
+ messageBytes: tx.message.serialize(),
107
+ signatures: tx.signatures,
108
+ });
109
+ const payload = {
110
+ transaction: base64EncodedWireTransaction,
111
+ };
112
+ return { payload };
113
+ }
114
+ case PaymentMode.SettlementAccount: {
115
+ const settleKeypair = Keypair.generate();
116
+ const settleATA = getAssociatedTokenAddressSync(mint, settleKeypair.publicKey, ...getAssociatedTokenAddressSyncRest);
117
+ instructions.push(createAssociatedTokenAccountIdempotentInstruction(wallet.publicKey, settleATA, settleKeypair.publicKey, mint), createTransferCheckedInstruction(sourceAccount, mint, settleATA, wallet.publicKey, amount, decimals));
118
+ let tx;
119
+ if (wallet.buildTransaction) {
120
+ tx = await wallet.buildTransaction(instructions, recentBlockhash);
121
+ }
122
+ else {
123
+ const message = new TransactionMessage({
124
+ instructions,
125
+ payerKey,
126
+ recentBlockhash,
127
+ }).compileToV0Message();
128
+ tx = new VersionedTransaction(message);
129
+ }
130
+ if (wallet.updateTransaction) {
131
+ tx = await wallet.updateTransaction(tx);
132
+ }
133
+ if (!wallet.sendTransaction) {
134
+ throw new Error("wallet must support sending transactions to use settlement accounts with exact");
135
+ }
136
+ const transactionSignature = await wallet.sendTransaction(tx);
137
+ const settleSecretKey = Buffer.from(settleKeypair.secretKey).toString("base64");
138
+ const payload = {
139
+ settleSecretKey,
140
+ transactionSignature,
141
+ };
142
+ return { payload };
143
+ }
76
144
  }
77
- const base64EncodedWireTransaction = getBase64EncodedWireTransaction({
78
- messageBytes: tx.message.serialize(),
79
- signatures: tx.signatures,
80
- });
81
- const payload = {
82
- transaction: base64EncodedWireTransaction,
83
- };
84
- return { payload };
85
145
  };
86
146
  return {
87
147
  exec,
@@ -1,24 +1,40 @@
1
1
  import type { FacilitatorHandler } from "@faremeter/types/facilitator";
2
2
  import { type Rpc, type SolanaRpcApi } from "@solana/kit";
3
3
  import type { TransactionError } from "@solana/rpc-types";
4
- import { Keypair, type PublicKey } from "@solana/web3.js";
4
+ import { Keypair, PublicKey } from "@solana/web3.js";
5
+ export declare const PaymentRequirementsExtraFeatures: import("arktype/internal/methods/object.ts").ObjectType<{
6
+ xSettlementAccountSupported?: boolean;
7
+ }, {}>;
8
+ export type PaymentRequirementsExtraFeatures = typeof PaymentRequirementsExtraFeatures.infer;
5
9
  export declare const PaymentRequirementsExtra: import("arktype/internal/methods/object.ts").ObjectType<{
6
10
  feePayer: string;
7
11
  decimals?: number;
8
12
  recentBlockhash?: string;
13
+ features?: {
14
+ xSettlementAccountSupported?: boolean;
15
+ };
9
16
  }, {}>;
10
17
  interface FacilitatorOptions {
11
18
  maxRetries?: number;
12
19
  retryDelayMs?: number;
13
20
  maxPriorityFee?: number;
21
+ features?: {
22
+ enableSettlementAccounts?: boolean;
23
+ };
14
24
  }
15
- export declare const PaymentPayload: import("arktype/internal/methods/object.ts").ObjectType<{
25
+ export declare const PaymentPayloadTransaction: import("arktype/internal/methods/object.ts").ObjectType<{
16
26
  transaction: (In: string) => import("arktype").Out<Readonly<{
17
27
  messageBytes: import("@solana/transactions").TransactionMessageBytes;
18
28
  signatures: import("@solana/transactions").SignaturesMap;
19
29
  }>>;
20
30
  }, {}>;
31
+ export type PaymentPayloadTransaction = typeof PaymentPayloadTransaction.infer;
32
+ export declare const PaymentPayloadSettlementAccount: import("arktype/internal/methods/object.ts").ObjectType<{
33
+ transactionSignature: string;
34
+ settleSecretKey: (In: string) => import("arktype").Out<Uint8Array<ArrayBuffer>>;
35
+ }, {}>;
36
+ export type PaymentPayloadSettlementAccount = typeof PaymentPayloadSettlementAccount.infer;
21
37
  export declare function transactionErrorToString(t: TransactionError): string;
22
- export declare const createFacilitatorHandler: (network: string, rpc: Rpc<SolanaRpcApi>, feePayerKeypair: Keypair, mint: PublicKey, config?: FacilitatorOptions) => FacilitatorHandler;
38
+ export declare const createFacilitatorHandler: (network: string, rpc: Rpc<SolanaRpcApi>, feePayerKeypair: Keypair, mint: PublicKey, config?: FacilitatorOptions) => Promise<FacilitatorHandler>;
23
39
  export {};
24
40
  //# 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,EAML,KAAK,GAAG,EACR,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AAOrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAM1D,eAAO,MAAM,wBAAwB;;;;MAInC,CAAC;AAEH,UAAU,kBAAkB;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAmBD,eAAO,MAAM,cAAc;;;;;MAEzB,CAAC;AAEH,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,kBA0HF,CAAC"}
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,36 +1,38 @@
1
+ import { x402PaymentRequirements, } from "@faremeter/types/x402";
1
2
  import { isValidationError } from "@faremeter/types";
2
3
  import { lookupX402Network } from "@faremeter/info/solana";
3
4
  import { fetchMint } from "@solana-program/token";
4
- import { address, createKeyPairSignerFromBytes, decompileTransactionMessage, getBase64Encoder, getCompiledTransactionMessageDecoder, } from "@solana/kit";
5
+ import { address, createKeyPairSignerFromBytes, decompileTransactionMessage, getBase64Encoder, getCompiledTransactionMessageDecoder, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, pipe, } from "@solana/kit";
5
6
  import { getBase64EncodedWireTransaction, getTransactionDecoder, partiallySignTransaction, } from "@solana/transactions";
6
- import { Keypair } from "@solana/web3.js";
7
+ import { Keypair, PublicKey } from "@solana/web3.js";
7
8
  import { type } from "arktype";
8
9
  import { isValidTransaction } from "./verify.js";
9
10
  import { logger } from "./logger.js";
10
11
  import { x402Scheme, generateMatcher } from "./common.js";
12
+ import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, getTransferCheckedInstruction, getCloseAccountInstruction, } from "@solana-program/token";
13
+ import { getAddMemoInstruction } from "@solana-program/memo";
14
+ export const PaymentRequirementsExtraFeatures = type({
15
+ xSettlementAccountSupported: "boolean?",
16
+ });
11
17
  export const PaymentRequirementsExtra = type({
12
18
  feePayer: "string",
13
19
  decimals: "number?",
14
20
  recentBlockhash: "string?",
21
+ features: PaymentRequirementsExtraFeatures.optional(),
15
22
  });
16
- function errorResponse(msg) {
17
- logger.error(msg);
18
- return {
19
- success: false,
20
- error: msg,
21
- txHash: null,
22
- networkId: null,
23
- };
24
- }
25
23
  const TransactionString = type("string").pipe.try((tx) => {
26
24
  const decoder = getTransactionDecoder();
27
25
  const base64Encoder = getBase64Encoder();
28
26
  const transactionBytes = base64Encoder.encode(tx);
29
27
  return decoder.decode(transactionBytes);
30
28
  });
31
- export const PaymentPayload = type({
29
+ export const PaymentPayloadTransaction = type({
32
30
  transaction: TransactionString,
33
31
  });
32
+ export const PaymentPayloadSettlementAccount = type({
33
+ transactionSignature: "string",
34
+ settleSecretKey: type("string.base64").pipe.try((s) => Uint8Array.from(Buffer.from(s, "base64"))),
35
+ });
34
36
  export function transactionErrorToString(t) {
35
37
  if (typeof t == "string") {
36
38
  return t;
@@ -72,9 +74,133 @@ const sendTransaction = async (rpc, signedTransaction, maxRetries, retryDelayMs)
72
74
  }
73
75
  return { success: false, error: "Transaction confirmation timeout" };
74
76
  };
75
- export const createFacilitatorHandler = (network, rpc, feePayerKeypair, mint, config) => {
77
+ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mint, config) => {
76
78
  const { isMatchingRequirement } = generateMatcher(network, mint.toBase58());
77
79
  const { maxRetries = 30, retryDelayMs = 1000, maxPriorityFee = 100_000, } = config ?? {};
80
+ const mintInfo = await fetchMint(rpc, address(mint.toBase58()));
81
+ const features = {};
82
+ if (config?.features?.enableSettlementAccounts) {
83
+ features.xSettlementAccountSupported = true;
84
+ }
85
+ const processSettlementAccount = async (requirements, paymentPayload) => {
86
+ const errorResponse = (error) => ({ error });
87
+ // XXX - It would be nicer to do this check in the arktype
88
+ // validation. Unfortunately getting match to generate things
89
+ // properly turned out to create excessive TypeScript types
90
+ // that caused tsc to error out.
91
+ if (!config?.features?.enableSettlementAccounts) {
92
+ return errorResponse("settlement accounts are not accepted");
93
+ }
94
+ const settleSigner = await createKeyPairSignerFromBytes(paymentPayload.settleSecretKey);
95
+ const settleOwner = settleSigner.address;
96
+ const [settleATA] = await findAssociatedTokenPda({
97
+ mint: address(mint.toBase58()),
98
+ owner: settleOwner,
99
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
100
+ });
101
+ const { value: accountBalance } = await rpc
102
+ .getTokenAccountBalance(settleATA, { commitment: "confirmed" })
103
+ .send();
104
+ logger.debug("settlement account info: {*}", {
105
+ settleOwner,
106
+ settleATA,
107
+ accountBalance,
108
+ });
109
+ if (BigInt(accountBalance.amount) !== BigInt(requirements.maxAmountRequired)) {
110
+ return errorResponse("settlement account balance didn't match payment requirements");
111
+ }
112
+ const settle = async () => {
113
+ const feePayerSigner = await createKeyPairSignerFromBytes(feePayerKeypair.secretKey);
114
+ const [payToATA] = await findAssociatedTokenPda({
115
+ mint: address(mint.toBase58()),
116
+ owner: address(requirements.payTo),
117
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
118
+ });
119
+ const instructions = [
120
+ getAddMemoInstruction({ memo: crypto.randomUUID() }),
121
+ getTransferCheckedInstruction({
122
+ source: settleATA,
123
+ mint: address(mint.toBase58()),
124
+ destination: payToATA,
125
+ authority: settleSigner,
126
+ amount: BigInt(requirements.maxAmountRequired),
127
+ decimals: mintInfo.data.decimals,
128
+ }),
129
+ getCloseAccountInstruction({
130
+ account: settleATA,
131
+ destination: feePayerSigner.address,
132
+ owner: settleSigner,
133
+ }),
134
+ ];
135
+ const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
136
+ // Build the transaction message
137
+ const transactionMessage = pipe(createTransactionMessage({ version: 0 }), (msg) => setTransactionMessageFeePayerSigner(feePayerSigner, msg), (msg) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, msg), (msg) => appendTransactionMessageInstructions(instructions, msg));
138
+ const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
139
+ return {
140
+ signedTransaction,
141
+ };
142
+ };
143
+ return {
144
+ settle,
145
+ };
146
+ };
147
+ const processTransaction = async (requirements, paymentPayload) => {
148
+ const errorResponse = (error) => ({ error });
149
+ let transactionMessage, transaction;
150
+ try {
151
+ transaction = paymentPayload.transaction;
152
+ const compiledTransactionMessage = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes);
153
+ transactionMessage = decompileTransactionMessage(compiledTransactionMessage);
154
+ }
155
+ catch (cause) {
156
+ throw new Error("Failed to get compiled transaction message", { cause });
157
+ }
158
+ try {
159
+ if (!(await isValidTransaction(transactionMessage, requirements, feePayerKeypair.publicKey, maxPriorityFee))) {
160
+ logger.error("Invalid transaction");
161
+ return errorResponse("Invalid transaction");
162
+ }
163
+ }
164
+ catch (cause) {
165
+ throw new Error("Failed to validate transaction", { cause });
166
+ }
167
+ return {
168
+ settle: async () => {
169
+ let signedTransaction;
170
+ try {
171
+ const kitKeypair = await createKeyPairSignerFromBytes(feePayerKeypair.secretKey);
172
+ signedTransaction = await partiallySignTransaction([kitKeypair.keyPair], transaction);
173
+ }
174
+ catch (cause) {
175
+ throw new Error("Failed to partially sign transaction", { cause });
176
+ }
177
+ return {
178
+ signedTransaction,
179
+ };
180
+ },
181
+ };
182
+ };
183
+ const determinePaymentPayload = function (requirements, possiblePayload) {
184
+ // XXX - It would be great to do this automatically using arktype,
185
+ // but because of the overlapping input types and the morphs, this
186
+ // ends up being more annoying than you'd think. So instead, use
187
+ // hints to do the correct validation.
188
+ if (config?.features?.enableSettlementAccounts &&
189
+ "settleSecretKey" in possiblePayload) {
190
+ const paymentPayload = PaymentPayloadSettlementAccount(possiblePayload);
191
+ if (isValidationError(paymentPayload)) {
192
+ return paymentPayload;
193
+ }
194
+ return processSettlementAccount(requirements, paymentPayload);
195
+ }
196
+ else {
197
+ const paymentPayload = PaymentPayloadTransaction(possiblePayload);
198
+ if (isValidationError(paymentPayload)) {
199
+ return paymentPayload;
200
+ }
201
+ return processTransaction(requirements, paymentPayload);
202
+ }
203
+ };
78
204
  const getSupported = () => {
79
205
  return lookupX402Network(network).map((network) => Promise.resolve({
80
206
  x402Version: 1,
@@ -82,13 +208,13 @@ export const createFacilitatorHandler = (network, rpc, feePayerKeypair, mint, co
82
208
  network,
83
209
  extra: {
84
210
  feePayer: feePayerKeypair.publicKey.toString(),
211
+ features,
85
212
  },
86
213
  }));
87
214
  };
88
215
  const getRequirements = async (req) => {
89
216
  const recentBlockhash = (await rpc.getLatestBlockhash().send()).value
90
217
  .blockhash;
91
- const mintInfo = await fetchMint(rpc, address(mint.toBase58()));
92
218
  return req.filter(isMatchingRequirement).map((x) => {
93
219
  return {
94
220
  ...x,
@@ -97,44 +223,51 @@ export const createFacilitatorHandler = (network, rpc, feePayerKeypair, mint, co
97
223
  feePayer: feePayerKeypair.publicKey.toString(),
98
224
  decimals: mintInfo.data.decimals,
99
225
  recentBlockhash,
226
+ features,
100
227
  },
101
228
  };
102
229
  });
103
230
  };
104
- const handleSettle = async (requirements, payment) => {
231
+ const handleVerify = async (requirements, payment) => {
105
232
  if (!isMatchingRequirement(requirements)) {
106
233
  return null;
107
234
  }
108
- const paymentPayload = PaymentPayload(payment.payload);
109
- if (isValidationError(paymentPayload)) {
110
- return errorResponse(paymentPayload.summary);
111
- }
112
- let transactionMessage, transaction;
113
- try {
114
- transaction = paymentPayload.transaction;
115
- const compiledTransactionMessage = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes);
116
- transactionMessage = decompileTransactionMessage(compiledTransactionMessage);
117
- }
118
- catch (cause) {
119
- throw new Error("Failed to get compiled transaction message", { cause });
235
+ const errorResponse = (invalidReason) => ({
236
+ isValid: false,
237
+ invalidReason,
238
+ });
239
+ const processor = determinePaymentPayload(requirements, payment.payload);
240
+ if (isValidationError(processor)) {
241
+ return errorResponse(processor.summary);
120
242
  }
121
- try {
122
- if (!(await isValidTransaction(transactionMessage, requirements, feePayerKeypair.publicKey, maxPriorityFee))) {
123
- logger.error("Invalid transaction");
124
- return errorResponse("Invalid transaction");
125
- }
243
+ const verifyResult = await processor;
244
+ if ("error" in verifyResult) {
245
+ return errorResponse(verifyResult.error);
126
246
  }
127
- catch (cause) {
128
- throw new Error("Failed to validate transaction", { cause });
247
+ return { isValid: true };
248
+ };
249
+ const handleSettle = async (requirements, payment) => {
250
+ if (!isMatchingRequirement(requirements)) {
251
+ return null;
129
252
  }
130
- let signedTransaction;
131
- try {
132
- const kitKeypair = await createKeyPairSignerFromBytes(feePayerKeypair.secretKey);
133
- signedTransaction = await partiallySignTransaction([kitKeypair.keyPair], transaction);
253
+ const errorResponse = (msg) => {
254
+ logger.error(msg);
255
+ return {
256
+ success: false,
257
+ error: msg,
258
+ txHash: null,
259
+ networkId: null,
260
+ };
261
+ };
262
+ const processor = determinePaymentPayload(requirements, payment.payload);
263
+ if (isValidationError(processor)) {
264
+ return errorResponse(processor.summary);
134
265
  }
135
- catch (cause) {
136
- throw new Error("Failed to partially sign transaction", { cause });
266
+ const verifyResult = await processor;
267
+ if ("error" in verifyResult) {
268
+ return errorResponse(verifyResult.error);
137
269
  }
270
+ const { signedTransaction } = await verifyResult.settle();
138
271
  let result;
139
272
  try {
140
273
  result = await sendTransaction(rpc, signedTransaction, maxRetries, retryDelayMs);
@@ -155,6 +288,7 @@ export const createFacilitatorHandler = (network, rpc, feePayerKeypair, mint, co
155
288
  return {
156
289
  getSupported,
157
290
  getRequirements,
291
+ handleVerify,
158
292
  handleSettle,
159
293
  };
160
294
  };