@faremeter/payment-solana 0.10.1 → 0.10.2

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,11 @@
1
+ import type { PaymentHandler } from "@faremeter/types/client";
2
+ import { Connection, PublicKey, TransactionInstruction, VersionedTransaction } from "@solana/web3.js";
3
+ export type Wallet = {
4
+ network: string;
5
+ publicKey: PublicKey;
6
+ buildTransaction?: (instructions: TransactionInstruction[], recentBlockHash: string) => Promise<VersionedTransaction>;
7
+ updateTransaction?: (tx: VersionedTransaction) => Promise<VersionedTransaction>;
8
+ sendTransaction?: (tx: VersionedTransaction) => Promise<string>;
9
+ };
10
+ export declare function createPaymentHandler(wallet: Wallet, mint: PublicKey, connection?: Connection): PaymentHandler;
11
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +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,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,EACf,UAAU,CAAC,EAAE,UAAU,GACtB,cAAc,CAwHhB"}
@@ -0,0 +1,86 @@
1
+ import { isValidationError, throwValidationError } from "@faremeter/types";
2
+ import { createTransferCheckedInstruction, getAssociatedTokenAddressSync, getMint, } from "@solana/spl-token";
3
+ import { getBase64EncodedWireTransaction, } from "@solana/transactions";
4
+ import { ComputeBudgetProgram, Connection, PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js";
5
+ import { PaymentRequirementsExtra } from "./facilitator.js";
6
+ import { generateMatcher } from "./common.js";
7
+ export function createPaymentHandler(wallet, mint, connection) {
8
+ const { matchTupleAndAsset } = generateMatcher(wallet.network, mint ? mint.toBase58() : "sol");
9
+ return async (context, accepts) => {
10
+ const res = accepts
11
+ .filter((r) => !isValidationError(matchTupleAndAsset(r)))
12
+ .map((requirements) => {
13
+ const extra = PaymentRequirementsExtra(requirements.extra);
14
+ if (isValidationError(extra)) {
15
+ throwValidationError("couldn't validate requirements extra field", extra);
16
+ }
17
+ const exec = async () => {
18
+ let recentBlockhash;
19
+ if (extra.recentBlockhash !== undefined) {
20
+ recentBlockhash = extra.recentBlockhash;
21
+ }
22
+ else if (connection !== undefined) {
23
+ recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
24
+ }
25
+ else {
26
+ throw new Error("couldn't get the latest Solana network block hash");
27
+ }
28
+ let decimals;
29
+ if (extra.decimals !== undefined) {
30
+ decimals = extra.decimals;
31
+ }
32
+ else if (connection !== undefined) {
33
+ const mintInfo = await getMint(connection, mint);
34
+ decimals = mintInfo.decimals;
35
+ }
36
+ else {
37
+ throw new Error("couldn't get the decimal information for the mint");
38
+ }
39
+ const paymentRequirements = {
40
+ ...extra,
41
+ amount: Number(requirements.maxAmountRequired),
42
+ receiver: new PublicKey(requirements.payTo),
43
+ };
44
+ const sourceAccount = getAssociatedTokenAddressSync(mint, wallet.publicKey);
45
+ const receiverAccount = getAssociatedTokenAddressSync(mint, paymentRequirements.receiver);
46
+ const instructions = [
47
+ ComputeBudgetProgram.setComputeUnitLimit({
48
+ units: 50_000,
49
+ }),
50
+ ComputeBudgetProgram.setComputeUnitPrice({
51
+ microLamports: 1,
52
+ }),
53
+ createTransferCheckedInstruction(sourceAccount, mint, receiverAccount, wallet.publicKey, paymentRequirements.amount, decimals),
54
+ ];
55
+ let tx;
56
+ if (wallet.buildTransaction) {
57
+ tx = await wallet.buildTransaction(instructions, recentBlockhash);
58
+ }
59
+ else {
60
+ const message = new TransactionMessage({
61
+ instructions,
62
+ payerKey: new PublicKey(paymentRequirements.feePayer),
63
+ recentBlockhash,
64
+ }).compileToV0Message();
65
+ tx = new VersionedTransaction(message);
66
+ }
67
+ if (wallet.updateTransaction) {
68
+ tx = await wallet.updateTransaction(tx);
69
+ }
70
+ const base64EncodedWireTransaction = getBase64EncodedWireTransaction({
71
+ messageBytes: tx.message.serialize(),
72
+ signatures: tx.signatures,
73
+ });
74
+ const payload = {
75
+ transaction: base64EncodedWireTransaction,
76
+ };
77
+ return { payload };
78
+ };
79
+ return {
80
+ exec,
81
+ requirements,
82
+ };
83
+ });
84
+ return res;
85
+ };
86
+ }
@@ -0,0 +1,13 @@
1
+ export declare const x402Scheme = "exact";
2
+ export declare function generateMatcher(network: string, asset: string): {
3
+ matchTuple: import("arktype/internal/methods/object.ts").ObjectType<{
4
+ scheme: (In: string) => import("arktype/internal/attributes.ts").To<"exact">;
5
+ network: (In: string) => import("arktype/internal/attributes.ts").To<Lowercase<string>>;
6
+ }, {}>;
7
+ matchTupleAndAsset: import("arktype/internal/methods/object.ts").ObjectType<{
8
+ scheme: (In: string) => import("arktype/internal/attributes.ts").To<"exact">;
9
+ network: (In: string) => import("arktype/internal/attributes.ts").To<Lowercase<string>>;
10
+ asset: (In: string) => import("arktype/internal/attributes.ts").To<Lowercase<string>>;
11
+ }, {}>;
12
+ };
13
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/exact/common.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;;;;;;;;;;EAa7D"}
@@ -0,0 +1,17 @@
1
+ import { type } from "arktype";
2
+ import { caseInsensitiveLiteral } from "@faremeter/types";
3
+ import { lookupX402Network } from "@faremeter/info/solana";
4
+ export const x402Scheme = "exact";
5
+ export function generateMatcher(network, asset) {
6
+ const matchTuple = type({
7
+ scheme: caseInsensitiveLiteral(x402Scheme),
8
+ network: caseInsensitiveLiteral(...lookupX402Network(network)),
9
+ });
10
+ const matchTupleAndAsset = matchTuple.and({
11
+ asset: caseInsensitiveLiteral(asset),
12
+ });
13
+ return {
14
+ matchTuple,
15
+ matchTupleAndAsset,
16
+ };
17
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env pnpm tsx
2
+ export {};
3
+ //# sourceMappingURL=common.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.test.d.ts","sourceRoot":"","sources":["../../../src/exact/common.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env pnpm tsx
2
+ import t from "tap";
3
+ import { isValidationError as iVE } from "@faremeter/types";
4
+ import { lookupKnownSPLToken } from "@faremeter/info/solana";
5
+ import { generateMatcher } from "./common.js";
6
+ await t.test("testBasicMatching", async (t) => {
7
+ {
8
+ const tokenInfo = lookupKnownSPLToken("mainnet-beta", "USDC");
9
+ if (tokenInfo === undefined) {
10
+ t.bailout("couldn't find SPL token");
11
+ return;
12
+ }
13
+ const { matchTuple, matchTupleAndAsset } = generateMatcher("mainnet-beta", tokenInfo.address);
14
+ const req = {
15
+ network: "solana-mainnet-beta",
16
+ scheme: "exact",
17
+ asset: tokenInfo.address,
18
+ };
19
+ t.ok(!iVE(matchTuple(req)));
20
+ t.ok(!iVE(matchTupleAndAsset(req)));
21
+ t.ok(!iVE(matchTupleAndAsset({
22
+ ...req,
23
+ network: "solana",
24
+ })));
25
+ t.ok(iVE(matchTuple({
26
+ ...req,
27
+ network: "foobar",
28
+ })));
29
+ t.ok(iVE(matchTuple({
30
+ ...req,
31
+ scheme: "fner",
32
+ })));
33
+ }
34
+ t.end();
35
+ });
@@ -0,0 +1,24 @@
1
+ import type { FacilitatorHandler } from "@faremeter/types/facilitator";
2
+ import { type Rpc, type SolanaRpcApi } from "@solana/kit";
3
+ import type { TransactionError } from "@solana/rpc-types";
4
+ import { Keypair, type PublicKey } from "@solana/web3.js";
5
+ export declare const PaymentRequirementsExtra: import("arktype/internal/methods/object.ts").ObjectType<{
6
+ feePayer: string;
7
+ decimals?: number;
8
+ recentBlockhash?: string;
9
+ }, {}>;
10
+ interface FacilitatorOptions {
11
+ maxRetries?: number;
12
+ retryDelayMs?: number;
13
+ maxPriorityFee?: number;
14
+ }
15
+ export declare const PaymentPayload: import("arktype/internal/methods/object.ts").ObjectType<{
16
+ transaction: (In: string) => import("arktype").Out<Readonly<{
17
+ messageBytes: import("@solana/transactions").TransactionMessageBytes;
18
+ signatures: import("@solana/transactions").SignaturesMap;
19
+ }>>;
20
+ }, {}>;
21
+ export declare function transactionErrorToString(t: TransactionError): string;
22
+ export declare const createFacilitatorHandler: (network: string, rpc: Rpc<SolanaRpcApi>, feePayerKeypair: Keypair, mint: PublicKey, config?: FacilitatorOptions) => FacilitatorHandler;
23
+ export {};
24
+ //# sourceMappingURL=facilitator.d.ts.map
@@ -0,0 +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,kBA+HF,CAAC"}
@@ -0,0 +1,162 @@
1
+ import { isValidationError } from "@faremeter/types";
2
+ import { lookupX402Network } from "@faremeter/info/solana";
3
+ import { fetchMint } from "@solana-program/token";
4
+ import { address, createKeyPairSignerFromBytes, decompileTransactionMessage, getBase64Encoder, getCompiledTransactionMessageDecoder, } from "@solana/kit";
5
+ import { getBase64EncodedWireTransaction, getTransactionDecoder, partiallySignTransaction, } from "@solana/transactions";
6
+ import { Keypair } from "@solana/web3.js";
7
+ import { type } from "arktype";
8
+ import { isValidTransaction } from "./verify.js";
9
+ import { logger } from "./logger.js";
10
+ import { x402Scheme, generateMatcher } from "./common.js";
11
+ export const PaymentRequirementsExtra = type({
12
+ feePayer: "string",
13
+ decimals: "number?",
14
+ recentBlockhash: "string?",
15
+ });
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
+ const TransactionString = type("string").pipe.try((tx) => {
26
+ const decoder = getTransactionDecoder();
27
+ const base64Encoder = getBase64Encoder();
28
+ const transactionBytes = base64Encoder.encode(tx);
29
+ return decoder.decode(transactionBytes);
30
+ });
31
+ export const PaymentPayload = type({
32
+ transaction: TransactionString,
33
+ });
34
+ export function transactionErrorToString(t) {
35
+ if (typeof t == "string") {
36
+ return t;
37
+ }
38
+ if (typeof t == "object") {
39
+ return JSON.stringify(t, (_, v) => typeof v === "bigint" ? v.toString() : v);
40
+ }
41
+ return String(t);
42
+ }
43
+ const sendTransaction = async (rpc, signedTransaction, maxRetries, retryDelayMs) => {
44
+ const base64EncodedTransaction = getBase64EncodedWireTransaction(signedTransaction);
45
+ const simResult = await rpc
46
+ .simulateTransaction(base64EncodedTransaction, {
47
+ encoding: "base64",
48
+ })
49
+ .send();
50
+ if (simResult.value.err) {
51
+ logger.error("transaction simulation failed: {*}", simResult.value);
52
+ return { success: false, error: "Transaction simulation failed" };
53
+ }
54
+ const signature = await rpc
55
+ .sendTransaction(base64EncodedTransaction, {
56
+ encoding: "base64",
57
+ })
58
+ .send();
59
+ for (let i = 0; i < maxRetries; i++) {
60
+ const status = await rpc.getSignatureStatuses([signature]).send();
61
+ if (status.value[0]?.err) {
62
+ return {
63
+ success: false,
64
+ error: `Transaction failed: ${transactionErrorToString(status.value[0].err)}`,
65
+ };
66
+ }
67
+ if (status.value[0]?.confirmationStatus === "confirmed" ||
68
+ status.value[0]?.confirmationStatus === "finalized") {
69
+ return { success: true, signature };
70
+ }
71
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
72
+ }
73
+ return { success: false, error: "Transaction confirmation timeout" };
74
+ };
75
+ export const createFacilitatorHandler = (network, rpc, feePayerKeypair, mint, config) => {
76
+ const { matchTuple, matchTupleAndAsset } = generateMatcher(network, mint.toBase58());
77
+ const { maxRetries = 30, retryDelayMs = 1000, maxPriorityFee = 100_000, } = config ?? {};
78
+ const getSupported = () => {
79
+ return lookupX402Network(network).map((network) => Promise.resolve({
80
+ x402Version: 1,
81
+ scheme: x402Scheme,
82
+ network,
83
+ extra: {
84
+ feePayer: feePayerKeypair.publicKey.toString(),
85
+ },
86
+ }));
87
+ };
88
+ const getRequirements = async (req) => {
89
+ const recentBlockhash = (await rpc.getLatestBlockhash().send()).value
90
+ .blockhash;
91
+ const mintInfo = await fetchMint(rpc, address(mint.toBase58()));
92
+ return req
93
+ .filter((x) => !isValidationError(matchTupleAndAsset(x)))
94
+ .map((x) => {
95
+ return {
96
+ ...x,
97
+ asset: mint.toBase58(),
98
+ extra: {
99
+ feePayer: feePayerKeypair.publicKey.toString(),
100
+ decimals: mintInfo.data.decimals,
101
+ recentBlockhash,
102
+ },
103
+ };
104
+ });
105
+ };
106
+ const handleSettle = async (requirements, payment) => {
107
+ if (isValidationError(matchTuple(payment))) {
108
+ return null;
109
+ }
110
+ const paymentPayload = PaymentPayload(payment.payload);
111
+ if (isValidationError(paymentPayload)) {
112
+ return errorResponse(paymentPayload.summary);
113
+ }
114
+ let transactionMessage, transaction;
115
+ try {
116
+ transaction = paymentPayload.transaction;
117
+ const compiledTransactionMessage = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes);
118
+ transactionMessage = decompileTransactionMessage(compiledTransactionMessage);
119
+ }
120
+ catch (cause) {
121
+ throw new Error("Failed to get compiled transaction message", { cause });
122
+ }
123
+ try {
124
+ if (!(await isValidTransaction(transactionMessage, requirements, feePayerKeypair.publicKey, maxPriorityFee))) {
125
+ logger.error("Invalid transaction");
126
+ return errorResponse("Invalid transaction");
127
+ }
128
+ }
129
+ catch (cause) {
130
+ throw new Error("Failed to validate transaction", { cause });
131
+ }
132
+ let signedTransaction;
133
+ try {
134
+ const kitKeypair = await createKeyPairSignerFromBytes(feePayerKeypair.secretKey);
135
+ signedTransaction = await partiallySignTransaction([kitKeypair.keyPair], transaction);
136
+ }
137
+ catch (cause) {
138
+ throw new Error("Failed to partially sign transaction", { cause });
139
+ }
140
+ let result;
141
+ try {
142
+ result = await sendTransaction(rpc, signedTransaction, maxRetries, retryDelayMs);
143
+ }
144
+ catch (cause) {
145
+ throw new Error("Failed to send transaction", { cause });
146
+ }
147
+ if (!result.success) {
148
+ return errorResponse(result.error);
149
+ }
150
+ return {
151
+ success: true,
152
+ error: null,
153
+ txHash: result.signature,
154
+ networkId: payment.network,
155
+ };
156
+ };
157
+ return {
158
+ getSupported,
159
+ getRequirements,
160
+ handleSettle,
161
+ };
162
+ };
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env pnpm tsx
2
+ export {};
3
+ //# sourceMappingURL=facilitator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facilitator.test.d.ts","sourceRoot":"","sources":["../../../src/exact/facilitator.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env pnpm tsx
2
+ import t from "tap";
3
+ import { transactionErrorToString } from "./facilitator.js";
4
+ class MyBigCrazyError {
5
+ someBigInt = 32n;
6
+ someString = "a string";
7
+ DuplicateInstruction = 42;
8
+ }
9
+ await t.test("transactionErrorToString", async (t) => {
10
+ {
11
+ const err = "AccountBorrowOutstanding";
12
+ t.matchOnly(transactionErrorToString(err), "AccountBorrowOutstanding");
13
+ }
14
+ {
15
+ const err = { DuplicateInstruction: 42 };
16
+ t.matchOnly(transactionErrorToString(err), '{"DuplicateInstruction":42}');
17
+ }
18
+ {
19
+ const err = {
20
+ InstructionError: [32, "AccountBorrowFailed"],
21
+ };
22
+ t.matchOnly(transactionErrorToString(err), '{"InstructionError":[32,"AccountBorrowFailed"]}');
23
+ }
24
+ {
25
+ const err = {
26
+ SomeResultThatShouldNeverHappen: 1337n,
27
+ };
28
+ t.matchOnly(transactionErrorToString(err), '{"SomeResultThatShouldNeverHappen":"1337"}');
29
+ }
30
+ {
31
+ const err = 42;
32
+ t.matchOnly(transactionErrorToString(err), "42");
33
+ }
34
+ {
35
+ const err = new MyBigCrazyError();
36
+ t.matchOnly(transactionErrorToString(err), '{"someBigInt":"32","someString":"a string","DuplicateInstruction":42}');
37
+ }
38
+ t.end();
39
+ });
@@ -0,0 +1,3 @@
1
+ export { createPaymentHandler } from "./client.js";
2
+ export { createFacilitatorHandler } from "./facilitator.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/exact/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { createPaymentHandler } from "./client.js";
2
+ export { createFacilitatorHandler } from "./facilitator.js";
@@ -0,0 +1,2 @@
1
+ export declare const logger: import("@logtape/logtape").Logger;
2
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/exact/logger.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM,mCAAmD,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { getLogger } from "@logtape/logtape";
2
+ export const logger = getLogger(["faremeter", "payment-solana-exact"]);
@@ -0,0 +1,5 @@
1
+ import type { x402PaymentRequirements } from "@faremeter/types/x402";
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>;
5
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +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;AA2GjD,wBAAsB,kBAAkB,CACtC,kBAAkB,EAAE,4BAA4B,EAChD,mBAAmB,EAAE,uBAAuB,EAC5C,kBAAkB,EAAE,SAAS,EAC7B,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,OAAO,CAAC,CAiGlB"}
@@ -0,0 +1,147 @@
1
+ import { isValidationError } from "@faremeter/types";
2
+ import { parseSetComputeUnitLimitInstruction, parseSetComputeUnitPriceInstruction, } from "@solana-program/compute-budget";
3
+ import { findAssociatedTokenPda, parseCreateAssociatedTokenInstruction, parseTransferCheckedInstruction, TOKEN_PROGRAM_ADDRESS, } from "@solana-program/token";
4
+ import { address, } from "@solana/kit";
5
+ import { PaymentRequirementsExtra } from "./facilitator.js";
6
+ import { logger } from "./logger.js";
7
+ function verifyComputeUnitLimitInstruction(instruction) {
8
+ if (!instruction.data) {
9
+ return { valid: false };
10
+ }
11
+ try {
12
+ const parsed = parseSetComputeUnitLimitInstruction({
13
+ programAddress: instruction.programAddress,
14
+ data: new Uint8Array(instruction.data),
15
+ });
16
+ return { valid: true, units: parsed.data.units };
17
+ }
18
+ catch {
19
+ return { valid: false };
20
+ }
21
+ }
22
+ function verifyComputeUnitPriceInstruction(instruction) {
23
+ if (!instruction.data) {
24
+ return { valid: false };
25
+ }
26
+ try {
27
+ const parsed = parseSetComputeUnitPriceInstruction({
28
+ programAddress: instruction.programAddress,
29
+ data: new Uint8Array(instruction.data),
30
+ });
31
+ return { valid: true, microLamports: parsed.data.microLamports };
32
+ }
33
+ catch {
34
+ return { valid: false };
35
+ }
36
+ }
37
+ function calculatePriorityFee(units, microLamports) {
38
+ return (units * Number(microLamports)) / 1_000_000;
39
+ }
40
+ async function verifyTransferInstruction(instruction, paymentRequirements, destination, facilitatorAddress) {
41
+ if (!instruction.data || !instruction.accounts) {
42
+ return false;
43
+ }
44
+ let transfer;
45
+ try {
46
+ transfer = parseTransferCheckedInstruction({
47
+ accounts: instruction.accounts,
48
+ programAddress: instruction.programAddress,
49
+ data: new Uint8Array(instruction.data),
50
+ });
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ if (transfer.accounts.authority.address === facilitatorAddress) {
56
+ logger.error("Dropping transfer where the transfer authority is the facilitator");
57
+ return false;
58
+ }
59
+ const [facilitatorATA] = await findAssociatedTokenPda({
60
+ mint: address(paymentRequirements.asset),
61
+ owner: address(facilitatorAddress),
62
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
63
+ });
64
+ if (transfer.accounts.source.address === facilitatorATA) {
65
+ logger.error("Dropping transfer where the source is the facilitator");
66
+ return false;
67
+ }
68
+ return (transfer.data.amount === BigInt(paymentRequirements.maxAmountRequired) &&
69
+ transfer.accounts.mint.address === paymentRequirements.asset &&
70
+ transfer.accounts.destination.address === destination);
71
+ }
72
+ function verifyCreateATAInstruction(instruction) {
73
+ if (!instruction.data || !instruction.accounts) {
74
+ return false;
75
+ }
76
+ try {
77
+ parseCreateAssociatedTokenInstruction({
78
+ accounts: instruction.accounts,
79
+ programAddress: instruction.programAddress,
80
+ data: new Uint8Array(instruction.data),
81
+ });
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ export async function isValidTransaction(transactionMessage, paymentRequirements, facilitatorAddress, maxPriorityFee) {
89
+ const extra = PaymentRequirementsExtra(paymentRequirements.extra);
90
+ if (isValidationError(extra)) {
91
+ throw new Error("feePayer is required");
92
+ }
93
+ if (transactionMessage.feePayer.address !== extra.feePayer) {
94
+ return false;
95
+ }
96
+ const [destination] = await findAssociatedTokenPda({
97
+ mint: address(paymentRequirements.asset),
98
+ owner: address(paymentRequirements.payTo),
99
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
100
+ });
101
+ const instructions = transactionMessage.instructions;
102
+ if (instructions.length === 3) {
103
+ // Make typescript happy...
104
+ const [ix0, ix1, ix2] = instructions;
105
+ if (!ix0 || !ix1 || !ix2) {
106
+ return false;
107
+ }
108
+ const limitResult = verifyComputeUnitLimitInstruction(ix0);
109
+ const priceResult = verifyComputeUnitPriceInstruction(ix1);
110
+ if (!limitResult.valid || !priceResult.valid) {
111
+ return false;
112
+ }
113
+ if (maxPriorityFee !== undefined &&
114
+ limitResult.units !== undefined &&
115
+ priceResult.microLamports !== undefined) {
116
+ const priorityFee = calculatePriorityFee(limitResult.units, priceResult.microLamports);
117
+ if (priorityFee > maxPriorityFee) {
118
+ logger.error(`Priority fee ${priorityFee} exceeds maximum ${maxPriorityFee}`);
119
+ return false;
120
+ }
121
+ }
122
+ return await verifyTransferInstruction(ix2, paymentRequirements, destination, facilitatorAddress.toBase58());
123
+ }
124
+ else if (instructions.length === 4) {
125
+ const [ix0, ix1, ix2, ix3] = instructions;
126
+ if (!ix0 || !ix1 || !ix2 || !ix3) {
127
+ return false;
128
+ }
129
+ const limitResult = verifyComputeUnitLimitInstruction(ix0);
130
+ const priceResult = verifyComputeUnitPriceInstruction(ix1);
131
+ if (!limitResult.valid || !priceResult.valid) {
132
+ return false;
133
+ }
134
+ if (maxPriorityFee !== undefined &&
135
+ limitResult.units !== undefined &&
136
+ priceResult.microLamports !== undefined) {
137
+ const priorityFee = calculatePriorityFee(limitResult.units, priceResult.microLamports);
138
+ if (priorityFee > maxPriorityFee) {
139
+ logger.error(`Priority fee ${priorityFee} exceeds maximum ${maxPriorityFee}`);
140
+ return false;
141
+ }
142
+ }
143
+ return (verifyCreateATAInstruction(ix2) &&
144
+ (await verifyTransferInstruction(ix3, paymentRequirements, destination, facilitatorAddress.toBase58())));
145
+ }
146
+ return false;
147
+ }
@@ -0,0 +1,2 @@
1
+ export * as exact from "./exact/index.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC"}
@@ -0,0 +1 @@
1
+ export * as exact from "./exact/index.js";