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