diff --git a/src/schemas/sep24.ts b/src/schemas/sep24.ts new file mode 100644 index 0000000..9cc5360 --- /dev/null +++ b/src/schemas/sep24.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +const memoType = z.enum(["text", "id", "hash"]); + +/** Shared SEP-24 parameter contract for deposit and withdrawal starts. */ +export const sep24InteractiveSchema = z + .object({ + assetCode: z.string().trim().min(1).max(12), + anchorName: z.string().trim().min(1).max(120).optional(), + account: z.string().regex(/^G[A-Z2-7]{55}$/).optional(), + memo: z.string().trim().min(1).max(64).optional(), + memoType: memoType.optional(), + walletName: z.string().trim().min(1).max(120).optional(), + }) + .superRefine((value, ctx) => { + if (value.memo && !value.memoType) { + ctx.addIssue({ code: "custom", path: ["memoType"], message: "memoType is required when memo is supplied" }); + } + if (value.memoType && !value.memo) { + ctx.addIssue({ code: "custom", path: ["memo"], message: "memo is required when memoType is supplied" }); + } + }); diff --git a/tests/routes/sep24.test.ts b/tests/routes/sep24.test.ts index 8313ef4..b6ad5c9 100644 --- a/tests/routes/sep24.test.ts +++ b/tests/routes/sep24.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import jwt from "jsonwebtoken"; +import { sep24InteractiveSchema } from "../../src/schemas/sep24"; const h = vi.hoisted(() => { const prisma: any = { @@ -344,3 +345,30 @@ describe("POST /api/sep24/callback — state updates", () => { expect(res.statusCode).toBe(200); }); }); + +describe("SEP-24 interactive parameters", () => { + it("accepts a valid asset and memo pair", () => { + const result = sep24InteractiveSchema.safeParse({ + assetCode: "USDC", + account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + memo: "invoice-42", + memoType: "text", + }); + expect(result.success).toBe(true); + }); + + it("rejects a memo without a memo type", () => { + const result = sep24InteractiveSchema.safeParse({ assetCode: "XLM", memo: "42" }); + expect(result.success).toBe(false); + }); + + it("rejects an invalid account and unsupported memo type", () => { + const result = sep24InteractiveSchema.safeParse({ + assetCode: "XLM", + account: "not-a-stellar-account", + memo: "42", + memoType: "binary", + }); + expect(result.success).toBe(false); + }); +});