@faremeter/payment-solana 0.19.0 → 0.20.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.
Files changed (44) hide show
  1. package/README.md +70 -0
  2. package/dist/src/charge/client.d.ts +18 -0
  3. package/dist/src/charge/client.d.ts.map +1 -0
  4. package/dist/src/charge/client.js +205 -0
  5. package/dist/src/charge/common.d.ts +44 -0
  6. package/dist/src/charge/common.d.ts.map +1 -0
  7. package/dist/src/charge/common.js +29 -0
  8. package/dist/src/charge/index.d.ts +7 -0
  9. package/dist/src/charge/index.d.ts.map +1 -0
  10. package/dist/src/charge/index.js +3 -0
  11. package/dist/src/charge/logger.d.ts +2 -0
  12. package/dist/src/charge/logger.d.ts.map +1 -0
  13. package/dist/src/charge/logger.js +2 -0
  14. package/dist/src/charge/replay.d.ts +12 -0
  15. package/dist/src/charge/replay.d.ts.map +1 -0
  16. package/dist/src/charge/replay.js +24 -0
  17. package/dist/src/charge/server.d.ts +31 -0
  18. package/dist/src/charge/server.d.ts.map +1 -0
  19. package/dist/src/charge/server.js +395 -0
  20. package/dist/src/charge/verify.d.ts +40 -0
  21. package/dist/src/charge/verify.d.ts.map +1 -0
  22. package/dist/src/charge/verify.js +185 -0
  23. package/dist/src/common.d.ts +12 -0
  24. package/dist/src/common.d.ts.map +1 -0
  25. package/dist/src/common.js +1 -0
  26. package/dist/src/exact/client.d.ts.map +1 -1
  27. package/dist/src/exact/client.js +5 -2
  28. package/dist/src/exact/facilitator.d.ts +1 -0
  29. package/dist/src/exact/facilitator.d.ts.map +1 -1
  30. package/dist/src/exact/facilitator.js +6 -0
  31. package/dist/src/exact/index.d.ts +1 -1
  32. package/dist/src/exact/index.d.ts.map +1 -1
  33. package/dist/src/exact/memo.d.ts +4 -0
  34. package/dist/src/exact/memo.d.ts.map +1 -0
  35. package/dist/src/exact/memo.js +16 -0
  36. package/dist/src/exact/verify.d.ts +2 -1
  37. package/dist/src/exact/verify.d.ts.map +1 -1
  38. package/dist/src/exact/verify.js +48 -4
  39. package/dist/src/exact/verify.test.js +155 -16
  40. package/dist/src/index.d.ts +6 -0
  41. package/dist/src/index.d.ts.map +1 -1
  42. package/dist/src/index.js +6 -0
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +21 -15
@@ -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 { createMemoInstruction, generateMemoNonce } from "./memo.js";
7
8
  import { logger } from "./logger.js";
8
9
  function generateGetAssociatedTokenAddressSyncRest(tokenConfig) {
9
10
  const { allowOwnerOffCurve, programId, associatedTokenProgramId } = tokenConfig;
@@ -56,6 +57,7 @@ async function extractMetadata(args) {
56
57
  const tokenProgramId = extra.tokenProgram
57
58
  ? new PublicKey(extra.tokenProgram)
58
59
  : (options?.token?.programId ?? TOKEN_PROGRAM_ID);
60
+ const memo = extra.memo;
59
61
  return {
60
62
  recentBlockhash,
61
63
  decimals,
@@ -64,6 +66,7 @@ async function extractMetadata(args) {
64
66
  payerKey,
65
67
  paymentMode,
66
68
  tokenProgramId,
69
+ memo,
67
70
  };
68
71
  }
69
72
  /**
@@ -99,7 +102,7 @@ export function createPaymentHandler(wallet, mint, connection, options) {
99
102
  const compatibleRequirements = accepts.filter(isMatchingRequirement);
100
103
  const res = compatibleRequirements.map((requirements) => {
101
104
  const exec = async () => {
102
- const { recentBlockhash, decimals, payTo, amount, payerKey, paymentMode, tokenProgramId, } = await extractMetadata({
105
+ const { recentBlockhash, decimals, payTo, amount, payerKey, paymentMode, tokenProgramId, memo, } = await extractMetadata({
103
106
  connection,
104
107
  mint,
105
108
  requirements,
@@ -122,7 +125,7 @@ export function createPaymentHandler(wallet, mint, connection, options) {
122
125
  switch (paymentMode) {
123
126
  case PaymentMode.ToSpec: {
124
127
  const receiverAccount = getAssociatedTokenAddressSync(mint, payTo, ...getAssociatedTokenAddressSyncRest);
125
- instructions.push(createTransferCheckedInstruction(sourceAccount, mint, receiverAccount, wallet.publicKey, amount, decimals, undefined, tokenProgramId));
128
+ instructions.push(createTransferCheckedInstruction(sourceAccount, mint, receiverAccount, wallet.publicKey, amount, decimals, undefined, tokenProgramId), createMemoInstruction(memo ?? generateMemoNonce()));
126
129
  let tx;
127
130
  if (wallet.buildTransaction) {
128
131
  tx = await wallet.buildTransaction(instructions, recentBlockhash);
@@ -32,6 +32,7 @@ export declare const PaymentRequirementsExtra: import("arktype/internal/methods/
32
32
  feePayer: string;
33
33
  decimals?: number;
34
34
  recentBlockhash?: string;
35
+ memo?: string;
35
36
  tokenProgram?: string;
36
37
  features?: {
37
38
  xSettlementAccountSupported?: boolean;
@@ -1 +1 @@
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,EACL,SAAS,EAIV,MAAM,uBAAuB,CAAC;AAC/B,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;AAKlC,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;;;;;;;;MAMnC,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,CAmc5B,CAAC"}
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,EACL,SAAS,EAIV,MAAM,uBAAuB,CAAC;AAC/B,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;AAKlC,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;;;;;;;;;MAOnC,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,CA0c5B,CAAC"}
@@ -18,6 +18,7 @@ export const PaymentRequirementsExtra = type({
18
18
  feePayer: "string",
19
19
  decimals: "number?",
20
20
  recentBlockhash: "string?",
21
+ "memo?": "string",
21
22
  "tokenProgram?": "string",
22
23
  features: PaymentRequirementsExtraFeatures.optional(),
23
24
  });
@@ -276,6 +277,10 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
276
277
  const recentBlockhash = (await rpc.getLatestBlockhash().send()).value
277
278
  .blockhash;
278
279
  return req.filter(isMatchingRequirement).map((x) => {
280
+ const incomingExtra = PaymentRequirementsExtra(x.extra);
281
+ const memo = !isValidationError(incomingExtra) && incomingExtra.memo !== undefined
282
+ ? { memo: incomingExtra.memo }
283
+ : {};
279
284
  return {
280
285
  ...x,
281
286
  asset: mint.toBase58(),
@@ -283,6 +288,7 @@ export const createFacilitatorHandler = async (network, rpc, feePayerKeypair, mi
283
288
  feePayer: feePayerKeypair.publicKey.toString(),
284
289
  decimals: mintInfo.data.decimals,
285
290
  recentBlockhash,
291
+ ...memo,
286
292
  tokenProgram,
287
293
  features,
288
294
  },
@@ -1,3 +1,3 @@
1
- export { createPaymentHandler } from "./client.js";
1
+ export { createPaymentHandler, type Wallet } from "./client.js";
2
2
  export { createFacilitatorHandler } from "./facilitator.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/exact/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/exact/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { TransactionInstruction } from "@solana/web3.js";
2
+ export declare function generateMemoNonce(): string;
3
+ export declare function createMemoInstruction(memo: string): TransactionInstruction;
4
+ //# sourceMappingURL=memo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memo.d.ts","sourceRoot":"","sources":["../../../src/exact/memo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAGpE,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,sBAAsB,CAM1E"}
@@ -0,0 +1,16 @@
1
+ import { PublicKey, TransactionInstruction } from "@solana/web3.js";
2
+ import { MEMO_PROGRAM_ADDRESS } from "@solana-program/memo";
3
+ export function generateMemoNonce() {
4
+ const bytes = new Uint8Array(16);
5
+ crypto.getRandomValues(bytes);
6
+ return Array.from(bytes)
7
+ .map((b) => b.toString(16).padStart(2, "0"))
8
+ .join("");
9
+ }
10
+ export function createMemoInstruction(memo) {
11
+ return new TransactionInstruction({
12
+ programId: new PublicKey(MEMO_PROGRAM_ADDRESS),
13
+ keys: [],
14
+ data: Buffer.from(memo, "utf-8"),
15
+ });
16
+ }
@@ -1,5 +1,6 @@
1
1
  import type { x402PaymentRequirements } from "@faremeter/types/x402v2";
2
- import { type Address, type CompilableTransactionMessage } from "@solana/kit";
2
+ import { type Address } from "@solana/kit";
3
+ import type { CompilableTransactionMessage } from "../common.js";
3
4
  export declare function isValidTransaction(transactionMessage: CompilableTransactionMessage, paymentRequirements: x402PaymentRequirements, facilitatorAddress: string, tokenProgram: Address, maxPriorityFee?: number): Promise<{
4
5
  payer: string;
5
6
  } | false>;
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/exact/verify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAUvE,OAAO,EAEL,KAAK,OAAO,EACZ,KAAK,4BAA4B,EAElC,MAAM,aAAa,CAAC;AAwGrB,wBAAsB,kBAAkB,CACtC,kBAAkB,EAAE,4BAA4B,EAChD,mBAAmB,EAAE,uBAAuB,EAC5C,kBAAkB,EAAE,MAAM,EAC1B,YAAY,EAAE,OAAO,EACrB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAC,CAmEpC"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/exact/verify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAUvE,OAAO,EAAW,KAAK,OAAO,EAAoB,MAAM,aAAa,CAAC;AAGtE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,WAAW,CAAC;AA0H9D,wBAAsB,kBAAkB,CACtC,kBAAkB,EAAE,4BAA4B,EAChD,mBAAmB,EAAE,uBAAuB,EAC5C,kBAAkB,EAAE,MAAM,EAC1B,YAAY,EAAE,OAAO,EACrB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAAC,CAkGpC"}
@@ -1,13 +1,26 @@
1
1
  import { isValidationError } from "@faremeter/types";
2
2
  import { parseSetComputeUnitLimitInstruction, parseSetComputeUnitPriceInstruction, } from "@solana-program/compute-budget";
3
3
  import { findAssociatedTokenPda, parseTransferCheckedInstruction, } from "@solana-program/token";
4
- import { address, } from "@solana/kit";
4
+ import { address } from "@solana/kit";
5
+ import { MEMO_PROGRAM_ADDRESS } from "@solana-program/memo";
5
6
  import { PaymentRequirementsExtra } from "./facilitator.js";
6
7
  import { logger } from "./logger.js";
7
8
  const LIGHTHOUSE_PROGRAM_ADDRESS = address("L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95");
8
9
  function isLighthouseInstruction(instruction) {
9
10
  return instruction.programAddress === LIGHTHOUSE_PROGRAM_ADDRESS;
10
11
  }
12
+ function isMemoInstruction(instruction) {
13
+ return instruction.programAddress === MEMO_PROGRAM_ADDRESS;
14
+ }
15
+ function isAllowedTrailingInstruction(instruction) {
16
+ return isLighthouseInstruction(instruction) || isMemoInstruction(instruction);
17
+ }
18
+ function getMemoData(instruction) {
19
+ if (!isMemoInstruction(instruction) || !instruction.data) {
20
+ return undefined;
21
+ }
22
+ return new TextDecoder().decode(new Uint8Array(instruction.data));
23
+ }
11
24
  function verifyComputeUnitLimitInstruction(instruction) {
12
25
  if (!instruction.data) {
13
26
  return { valid: false };
@@ -38,6 +51,10 @@ function verifyComputeUnitPriceInstruction(instruction) {
38
51
  return { valid: false };
39
52
  }
40
53
  }
54
+ // The upstream spec caps the per-CU price (<=5 lamports/CU), but that still
55
+ // allows an attacker to inflate the CU limit to the Solana maximum and drain
56
+ // the facilitator's SOL. A total-fee cap closes that vector because the
57
+ // facilitator is the one paying the priority fee.
41
58
  function calculatePriorityFee(units, microLamports) {
42
59
  return (units * Number(microLamports)) / 1_000_000;
43
60
  }
@@ -90,7 +107,7 @@ export async function isValidTransaction(transactionMessage, paymentRequirements
90
107
  tokenProgram,
91
108
  });
92
109
  const instructions = transactionMessage.instructions;
93
- if (instructions.length < 3 || instructions.length > 5) {
110
+ if (instructions.length < 3 || instructions.length > 6) {
94
111
  return false;
95
112
  }
96
113
  const [ix0, ix1, ix2, ...rest] = instructions;
@@ -111,10 +128,37 @@ export async function isValidTransaction(transactionMessage, paymentRequirements
111
128
  return false;
112
129
  }
113
130
  }
114
- if (!rest.every(isLighthouseInstruction)) {
115
- logger.error("Dropping transaction with non-Lighthouse trailing instructions");
131
+ if (!rest.every(isAllowedTrailingInstruction)) {
132
+ logger.error("Dropping transaction with unexpected trailing instructions");
116
133
  return false;
117
134
  }
135
+ const facilitator = address(facilitatorAddress);
136
+ for (const ix of instructions) {
137
+ if (!ix.accounts)
138
+ continue;
139
+ for (const account of ix.accounts) {
140
+ if (account.address === facilitator) {
141
+ logger.error("Dropping transaction where the facilitator appears in instruction accounts");
142
+ return false;
143
+ }
144
+ }
145
+ }
146
+ const memoInstructions = rest.filter(isMemoInstruction);
147
+ if (memoInstructions.length !== 1) {
148
+ logger.error("Expected exactly one Memo instruction");
149
+ return false;
150
+ }
151
+ if (extra.memo !== undefined) {
152
+ const memoIx = memoInstructions[0];
153
+ if (!memoIx) {
154
+ return false;
155
+ }
156
+ const memoData = getMemoData(memoIx);
157
+ if (memoData !== extra.memo) {
158
+ logger.error("Memo instruction data does not match extra.memo");
159
+ return false;
160
+ }
161
+ }
118
162
  const payer = await verifyTransferInstruction(ix2, paymentRequirements, destination, facilitatorAddress, tokenProgram);
119
163
  if (!payer)
120
164
  return false;
@@ -3,6 +3,7 @@ import t from "tap";
3
3
  import { isValidTransaction } from "./verify.js";
4
4
  import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from "@solana-program/compute-budget";
5
5
  import { findAssociatedTokenPda, getTransferCheckedInstruction, TOKEN_PROGRAM_ADDRESS, } from "@solana-program/token";
6
+ import { MEMO_PROGRAM_ADDRESS } from "@solana-program/memo";
6
7
  import { TOKEN_2022_PROGRAM_ADDRESS } from "../splToken.js";
7
8
  import { address, appendTransactionMessageInstructions, createTransactionMessage, generateKeyPairSigner, pipe, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, } from "@solana/kit";
8
9
  function createRequirements(overrides) {
@@ -85,24 +86,36 @@ function makeLighthouseIx(data) {
85
86
  data: new Uint8Array(data ?? [0]),
86
87
  };
87
88
  }
89
+ function makeMemoIx(memo) {
90
+ return {
91
+ programAddress: MEMO_PROGRAM_ADDRESS,
92
+ data: new TextEncoder().encode(memo),
93
+ };
94
+ }
88
95
  await t.test("isValidTransaction", async (t) => {
89
- await t.test("accepts valid 3-instruction transaction", async (t) => {
96
+ await t.test("accepts valid 4-instruction transaction", async (t) => {
90
97
  const f = await createFixtures();
91
- const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx], f.facilitator);
98
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx, makeMemoIx("nonce")], f.facilitator);
92
99
  const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram);
93
100
  t.ok(result);
94
101
  t.equal(result && result.payer, f.sender.address);
95
102
  t.end();
96
103
  });
97
- await t.test("accepts valid 4-instruction transaction with one lighthouse ix", async (t) => {
104
+ await t.test("accepts valid 5-instruction transaction with one lighthouse ix", async (t) => {
98
105
  const f = await createFixtures();
99
- const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx, makeLighthouseIx()], f.facilitator);
106
+ const txMsg = buildTxMessage([
107
+ f.computeLimitIx,
108
+ f.computePriceIx,
109
+ f.transferIx,
110
+ makeLighthouseIx(),
111
+ makeMemoIx("nonce"),
112
+ ], f.facilitator);
100
113
  const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram);
101
114
  t.ok(result);
102
115
  t.equal(result && result.payer, f.sender.address);
103
116
  t.end();
104
117
  });
105
- await t.test("accepts valid 5-instruction transaction with two lighthouse ixs", async (t) => {
118
+ await t.test("accepts valid 6-instruction transaction with two lighthouse ixs", async (t) => {
106
119
  const f = await createFixtures();
107
120
  const txMsg = buildTxMessage([
108
121
  f.computeLimitIx,
@@ -110,6 +123,7 @@ await t.test("isValidTransaction", async (t) => {
110
123
  f.transferIx,
111
124
  makeLighthouseIx([1]),
112
125
  makeLighthouseIx([2]),
126
+ makeMemoIx("nonce"),
113
127
  ], f.facilitator);
114
128
  const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram);
115
129
  t.ok(result);
@@ -122,13 +136,19 @@ await t.test("isValidTransaction", async (t) => {
122
136
  t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
123
137
  t.end();
124
138
  });
125
- await t.test("rejects transaction with more than 5 instructions", async (t) => {
139
+ await t.test("rejects transaction with more than 6 instructions", async (t) => {
126
140
  const f = await createFixtures();
127
- const extras = Array.from({ length: 3 }, (_, i) => makeLighthouseIx([i]));
141
+ const extras = Array.from({ length: 4 }, (_, i) => makeLighthouseIx([i]));
128
142
  const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx, ...extras], f.facilitator);
129
143
  t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
130
144
  t.end();
131
145
  });
146
+ await t.test("rejects transaction without memo instruction", async (t) => {
147
+ const f = await createFixtures();
148
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx], f.facilitator);
149
+ t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
150
+ t.end();
151
+ });
132
152
  await t.test("rejects transaction with wrong fee payer", async (t) => {
133
153
  const f = await createFixtures();
134
154
  const wrongPayer = await generateKeyPairSigner();
@@ -154,7 +174,7 @@ await t.test("isValidTransaction", async (t) => {
154
174
  });
155
175
  await t.test("accepts transaction within priority fee limit", async (t) => {
156
176
  const f = await createFixtures();
157
- const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx], f.facilitator);
177
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx, makeMemoIx("nonce")], f.facilitator);
158
178
  const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, 100_000);
159
179
  t.ok(result);
160
180
  t.equal(result && result.payer, f.sender.address);
@@ -168,7 +188,7 @@ await t.test("isValidTransaction", async (t) => {
168
188
  const highPriceIx = getSetComputeUnitPriceInstruction({
169
189
  microLamports: 10000000n,
170
190
  });
171
- const txMsg = buildTxMessage([highLimitIx, highPriceIx, f.transferIx], f.facilitator);
191
+ const txMsg = buildTxMessage([highLimitIx, highPriceIx, f.transferIx, makeMemoIx("nonce")], f.facilitator);
172
192
  t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, 100), false);
173
193
  t.end();
174
194
  });
@@ -180,7 +200,13 @@ await t.test("isValidTransaction", async (t) => {
180
200
  const highPriceIx = getSetComputeUnitPriceInstruction({
181
201
  microLamports: 10000000n,
182
202
  });
183
- const txMsg = buildTxMessage([highLimitIx, highPriceIx, f.transferIx, makeLighthouseIx()], f.facilitator);
203
+ const txMsg = buildTxMessage([
204
+ highLimitIx,
205
+ highPriceIx,
206
+ f.transferIx,
207
+ makeLighthouseIx(),
208
+ makeMemoIx("nonce"),
209
+ ], f.facilitator);
184
210
  t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram, 100), false);
185
211
  t.end();
186
212
  });
@@ -222,7 +248,7 @@ await t.test("isValidTransaction", async (t) => {
222
248
  amount: f.amount + 1n,
223
249
  decimals: f.decimals,
224
250
  });
225
- const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, wrongAmountIx], f.facilitator);
251
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, wrongAmountIx, makeMemoIx("nonce")], f.facilitator);
226
252
  t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
227
253
  t.end();
228
254
  });
@@ -247,7 +273,7 @@ await t.test("isValidTransaction", async (t) => {
247
273
  amount: f.amount,
248
274
  decimals: f.decimals,
249
275
  });
250
- const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, wrongMintIx], f.facilitator);
276
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, wrongMintIx, makeMemoIx("nonce")], f.facilitator);
251
277
  t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
252
278
  t.end();
253
279
  });
@@ -267,7 +293,7 @@ await t.test("isValidTransaction", async (t) => {
267
293
  amount: f.amount,
268
294
  decimals: f.decimals,
269
295
  });
270
- const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, wrongDestIx], f.facilitator);
296
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, wrongDestIx, makeMemoIx("nonce")], f.facilitator);
271
297
  t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
272
298
  t.end();
273
299
  });
@@ -281,7 +307,7 @@ await t.test("isValidTransaction", async (t) => {
281
307
  amount: f.amount,
282
308
  decimals: f.decimals,
283
309
  });
284
- const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, badIx], f.facilitator);
310
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, badIx, makeMemoIx("nonce")], f.facilitator);
285
311
  t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
286
312
  t.end();
287
313
  });
@@ -295,7 +321,7 @@ await t.test("isValidTransaction", async (t) => {
295
321
  amount: f.amount,
296
322
  decimals: f.decimals,
297
323
  });
298
- const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, badIx], f.facilitator);
324
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, badIx, makeMemoIx("nonce")], f.facilitator);
299
325
  t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
300
326
  t.end();
301
327
  });
@@ -303,11 +329,124 @@ await t.test("isValidTransaction", async (t) => {
303
329
  const f = await createFixtures({
304
330
  tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
305
331
  });
306
- const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx], f.facilitator);
332
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx, makeMemoIx("nonce")], f.facilitator);
333
+ const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram);
334
+ t.ok(result);
335
+ t.equal(result && result.payer, f.sender.address);
336
+ t.end();
337
+ });
338
+ await t.test("accepts transaction with memo instruction", async (t) => {
339
+ const f = await createFixtures();
340
+ const txMsg = buildTxMessage([
341
+ f.computeLimitIx,
342
+ f.computePriceIx,
343
+ f.transferIx,
344
+ makeMemoIx("some-random-nonce"),
345
+ ], f.facilitator);
346
+ const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram);
347
+ t.ok(result);
348
+ t.equal(result && result.payer, f.sender.address);
349
+ t.end();
350
+ });
351
+ await t.test("accepts transaction with lighthouse and memo instructions", async (t) => {
352
+ const f = await createFixtures();
353
+ const txMsg = buildTxMessage([
354
+ f.computeLimitIx,
355
+ f.computePriceIx,
356
+ f.transferIx,
357
+ makeLighthouseIx(),
358
+ makeMemoIx("nonce-123"),
359
+ ], f.facilitator);
307
360
  const result = await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram);
308
361
  t.ok(result);
309
362
  t.equal(result && result.payer, f.sender.address);
310
363
  t.end();
311
364
  });
365
+ await t.test("validates memo matches extra.memo when set", async (t) => {
366
+ const f = await createFixtures();
367
+ const requirements = {
368
+ ...f.requirements,
369
+ extra: { ...f.requirements.extra, memo: "invoice-456" },
370
+ };
371
+ const txMsg = buildTxMessage([
372
+ f.computeLimitIx,
373
+ f.computePriceIx,
374
+ f.transferIx,
375
+ makeMemoIx("invoice-456"),
376
+ ], f.facilitator);
377
+ const result = await isValidTransaction(txMsg, requirements, f.facilitator.address, f.tokenProgram);
378
+ t.ok(result);
379
+ t.equal(result && result.payer, f.sender.address);
380
+ t.end();
381
+ });
382
+ await t.test("rejects memo mismatch when extra.memo is set", async (t) => {
383
+ const f = await createFixtures();
384
+ const requirements = {
385
+ ...f.requirements,
386
+ extra: { ...f.requirements.extra, memo: "invoice-456" },
387
+ };
388
+ const txMsg = buildTxMessage([
389
+ f.computeLimitIx,
390
+ f.computePriceIx,
391
+ f.transferIx,
392
+ makeMemoIx("wrong-memo"),
393
+ ], f.facilitator);
394
+ t.equal(await isValidTransaction(txMsg, requirements, f.facilitator.address, f.tokenProgram), false);
395
+ t.end();
396
+ });
397
+ await t.test("rejects transaction where facilitator appears in trailing instruction accounts", async (t) => {
398
+ const f = await createFixtures();
399
+ const badMemoIx = {
400
+ programAddress: MEMO_PROGRAM_ADDRESS,
401
+ data: new TextEncoder().encode("nonce"),
402
+ accounts: [
403
+ {
404
+ address: f.facilitator.address,
405
+ role: 0,
406
+ },
407
+ ],
408
+ };
409
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx, badMemoIx], f.facilitator);
410
+ t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
411
+ t.end();
412
+ });
413
+ await t.test("rejects transaction with multiple memo instructions", async (t) => {
414
+ const f = await createFixtures();
415
+ const txMsg = buildTxMessage([
416
+ f.computeLimitIx,
417
+ f.computePriceIx,
418
+ f.transferIx,
419
+ makeMemoIx("nonce-1"),
420
+ makeMemoIx("nonce-2"),
421
+ ], f.facilitator);
422
+ t.equal(await isValidTransaction(txMsg, f.requirements, f.facilitator.address, f.tokenProgram), false);
423
+ t.end();
424
+ });
425
+ await t.test("rejects missing memo when extra.memo is set", async (t) => {
426
+ const f = await createFixtures();
427
+ const requirements = {
428
+ ...f.requirements,
429
+ extra: { ...f.requirements.extra, memo: "invoice-456" },
430
+ };
431
+ const txMsg = buildTxMessage([f.computeLimitIx, f.computePriceIx, f.transferIx], f.facilitator);
432
+ t.equal(await isValidTransaction(txMsg, requirements, f.facilitator.address, f.tokenProgram), false);
433
+ t.end();
434
+ });
435
+ await t.test("rejects multiple memo instructions when extra.memo is set", async (t) => {
436
+ const f = await createFixtures();
437
+ const requirements = {
438
+ ...f.requirements,
439
+ extra: { ...f.requirements.extra, memo: "invoice-456" },
440
+ };
441
+ const txMsg = buildTxMessage([
442
+ f.computeLimitIx,
443
+ f.computePriceIx,
444
+ f.transferIx,
445
+ makeMemoIx("invoice-456"),
446
+ makeMemoIx("invoice-456"),
447
+ ], f.facilitator);
448
+ t.equal(await isValidTransaction(txMsg, requirements, f.facilitator.address, f.tokenProgram), false);
449
+ t.end();
450
+ });
312
451
  t.end();
313
452
  });
@@ -10,6 +10,12 @@
10
10
  * @description SPL token transfer payment handlers for Solana
11
11
  */
12
12
  export * as exact from "./exact/index.js";
13
+ /**
14
+ * @title Solana Charge Payment Scheme
15
+ * @sidebarTitle Payment Solana / Charge
16
+ * @description MPP charge-based payment handlers for Solana
17
+ */
18
+ export * as charge from "./charge/index.js";
13
19
  /**
14
20
  * @title SPL Token Utilities
15
21
  * @sidebarTitle Payment Solana / SPL Token
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH;;;;GAIG;AACH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AACjC;;;;GAIG;AACH,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH;;;;GAIG;AACH,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AACjC;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC;;;;GAIG;AACH,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAC"}
package/dist/src/index.js CHANGED
@@ -10,6 +10,12 @@
10
10
  * @description SPL token transfer payment handlers for Solana
11
11
  */
12
12
  export * as exact from "./exact/index.js";
13
+ /**
14
+ * @title Solana Charge Payment Scheme
15
+ * @sidebarTitle Payment Solana / Charge
16
+ * @description MPP charge-based payment handlers for Solana
17
+ */
18
+ export * as charge from "./charge/index.js";
13
19
  /**
14
20
  * @title SPL Token Utilities
15
21
  * @sidebarTitle Payment Solana / SPL Token