@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.
- package/dist/src/exact/cache.d.ts +12 -0
- package/dist/src/exact/cache.d.ts.map +1 -0
- package/dist/src/exact/cache.js +31 -0
- package/dist/src/exact/client.d.ts +16 -1
- package/dist/src/exact/client.d.ts.map +1 -1
- package/dist/src/exact/client.js +34 -8
- package/dist/src/exact/common.d.ts +2 -1
- package/dist/src/exact/common.d.ts.map +1 -1
- package/dist/src/exact/common.js +3 -4
- package/dist/src/exact/common.test.js +11 -3
- package/dist/src/exact/facilitator.d.ts +40 -1
- package/dist/src/exact/facilitator.d.ts.map +1 -1
- package/dist/src/exact/facilitator.js +147 -32
- package/dist/src/exact/facilitator.test.js +35 -0
- package/dist/src/exact/logger.d.ts +1 -1
- package/dist/src/exact/logger.d.ts.map +1 -1
- package/dist/src/exact/logger.js +2 -2
- package/dist/src/exact/verify.d.ts +4 -3
- package/dist/src/exact/verify.d.ts.map +1 -1
- package/dist/src/exact/verify.js +34 -64
- package/dist/src/exact/verify.test.d.ts +3 -0
- package/dist/src/exact/verify.test.d.ts.map +1 -0
- package/dist/src/exact/verify.test.js +301 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +16 -0
- package/dist/src/splToken.d.ts +26 -0
- package/dist/src/splToken.d.ts.map +1 -1
- package/dist/src/splToken.js +20 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -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;
|
|
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"}
|
package/dist/src/exact/client.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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"}
|
package/dist/src/exact/common.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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 {
|
|
1
|
+
import {} from "@faremeter/types/x402v2";
|
|
2
2
|
import { isValidationError } from "@faremeter/types";
|
|
3
|
-
import {
|
|
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
|
|
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.
|
|
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.
|
|
153
|
+
amount: BigInt(requirements.amount),
|
|
127
154
|
decimals: mintInfo.data.decimals,
|
|
128
155
|
}),
|
|
129
156
|
getCloseAccountInstruction({
|
|
130
157
|
account: settleATA,
|
|
131
|
-
destination:
|
|
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
|
-
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
374
|
+
let response = {
|
|
282
375
|
success: true,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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,
|