@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.
- 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 +20 -3
- package/dist/src/exact/facilitator.d.ts.map +1 -1
- package/dist/src/exact/facilitator.js +92 -29
- package/dist/src/exact/facilitator.test.js +35 -0
- 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,12 +1,13 @@
|
|
|
1
|
-
import { x402PaymentRequirements, type x402PaymentPayload, type x402SettleResponse, type x402VerifyResponse } from "@faremeter/types/
|
|
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
|
-
|
|
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,
|
|
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") {
|
|
@@ -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.
|
|
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.
|
|
153
|
+
amount: BigInt(requirements.amount),
|
|
135
154
|
decimals: mintInfo.data.decimals,
|
|
136
155
|
}),
|
|
137
156
|
getCloseAccountInstruction({
|
|
138
157
|
account: settleATA,
|
|
139
|
-
destination:
|
|
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
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 = {
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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/
|
|
1
|
+
import type { x402PaymentRequirements } from "@faremeter/types/x402v2";
|
|
2
2
|
import { type CompilableTransactionMessage } from "@solana/kit";
|
|
3
|
-
|
|
4
|
-
|
|
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,
|
|
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"}
|