import { describe, it, expect } from "vitest"; import { verifyPaymentProof } from "./paymentVerification.js"; import { MemoType, AcpJobPhase, type AcpMemoData } from "./types.js"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** A well-formed EVM tx hash: 0x + 64 lowercase hex chars. */ const VALID_TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; function makeMemo(overrides: Partial = {}): AcpMemoData { return { id: 1, memoType: MemoType.MESSAGE, content: "test", nextPhase: AcpJobPhase.TRANSACTION, ...overrides, }; } // --------------------------------------------------------------------------- // Happy path — valid tx hashes // --------------------------------------------------------------------------- describe("verifyPaymentProof", () => { it("accepts PAYABLE_TRANSFER memo with valid tx hash", () => { const memos = [ makeMemo({ memoType: MemoType.PAYABLE_TRANSFER, content: VALID_TX_HASH }), ]; const result = verifyPaymentProof(memos); expect(result.verified).toBe(true); expect(result.memo?.memoType).toBe(MemoType.PAYABLE_TRANSFER); }); it("accepts TXHASH memo with valid tx hash", () => { const memos = [ makeMemo({ memoType: MemoType.TXHASH, content: VALID_TX_HASH }), ]; const result = verifyPaymentProof(memos); expect(result.verified).toBe(true); expect(result.memo?.memoType).toBe(MemoType.TXHASH); }); it("accepts uppercase hex chars in tx hash", () => { const memos = [ makeMemo({ memoType: MemoType.TXHASH, content: "0x" + "A".repeat(64) }), ]; expect(verifyPaymentProof(memos).verified).toBe(true); }); it("accepts mixed-case hex chars in tx hash", () => { const mixed = "0xAbCdEf1234567890AbCdEf1234567890AbCdEf1234567890AbCdEf1234567890"; expect( verifyPaymentProof([ makeMemo({ memoType: MemoType.TXHASH, content: mixed }), ]).verified, ).toBe(true); }); // --------------------------------------------------------------------------- // No payment memo // --------------------------------------------------------------------------- it("rejects when no payment memo exists", () => { const memos = [ makeMemo({ memoType: MemoType.MESSAGE, content: "hello" }), makeMemo({ memoType: MemoType.CONTEXT_URL, content: "https://example.com", }), ]; const result = verifyPaymentProof(memos); expect(result.verified).toBe(false); expect(result.reason).toContain("No payment proof memo found"); }); it("rejects when memos array is empty", () => { const result = verifyPaymentProof([]); expect(result.verified).toBe(false); expect(result.reason).toContain("No payment proof memo found"); }); // --------------------------------------------------------------------------- // Empty / whitespace content // --------------------------------------------------------------------------- it("rejects PAYABLE_TRANSFER memo with empty content", () => { const memos = [ makeMemo({ memoType: MemoType.PAYABLE_TRANSFER, content: "" }), ]; const result = verifyPaymentProof(memos); expect(result.verified).toBe(false); expect(result.reason).toContain("empty content"); }); it("rejects PAYABLE_TRANSFER memo with whitespace-only content", () => { const memos = [ makeMemo({ memoType: MemoType.PAYABLE_TRANSFER, content: " " }), ]; const result = verifyPaymentProof(memos); expect(result.verified).toBe(false); expect(result.reason).toContain("empty content"); }); // --------------------------------------------------------------------------- // Invalid tx hash formats (security: #783) // --------------------------------------------------------------------------- it("rejects an arbitrary non-hex string (e.g. 'fake-tx-hash')", () => { const result = verifyPaymentProof([ makeMemo({ memoType: MemoType.TXHASH, content: "fake-tx-hash" }), ]); expect(result.verified).toBe(false); expect(result.reason).toMatch(/not a valid transaction hash/i); }); it("rejects a hash without the 0x prefix", () => { const noPrefix = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; const result = verifyPaymentProof([ makeMemo({ memoType: MemoType.TXHASH, content: noPrefix }), ]); expect(result.verified).toBe(false); expect(result.reason).toMatch(/not a valid transaction hash/i); }); it("rejects a hash that is too short (63 hex chars after 0x)", () => { const short = "0x" + "a".repeat(63); const result = verifyPaymentProof([ makeMemo({ memoType: MemoType.TXHASH, content: short }), ]); expect(result.verified).toBe(false); expect(result.reason).toMatch(/not a valid transaction hash/i); }); it("rejects a hash that is too long (65 hex chars after 0x)", () => { const long = "0x" + "a".repeat(65); const result = verifyPaymentProof([ makeMemo({ memoType: MemoType.TXHASH, content: long }), ]); expect(result.verified).toBe(false); expect(result.reason).toMatch(/not a valid transaction hash/i); }); it("rejects a hash with non-hex characters", () => { const badHex = "0x" + "g".repeat(64); const result = verifyPaymentProof([ makeMemo({ memoType: MemoType.TXHASH, content: badHex }), ]); expect(result.verified).toBe(false); expect(result.reason).toMatch(/not a valid transaction hash/i); }); it("rejects '0x' alone (no hex digits)", () => { const result = verifyPaymentProof([ makeMemo({ memoType: MemoType.TXHASH, content: "0x" }), ]); expect(result.verified).toBe(false); expect(result.reason).toMatch(/not a valid transaction hash/i); }); it("includes the offending value in the error reason for debuggability", () => { const bad = "clearly-not-a-hash"; const result = verifyPaymentProof([ makeMemo({ memoType: MemoType.TXHASH, content: bad }), ]); expect(result.verified).toBe(false); expect(result.reason).toContain(bad); }); // --------------------------------------------------------------------------- // Memo ordering // --------------------------------------------------------------------------- it("finds payment memo among other memos", () => { const memos = [ makeMemo({ id: 1, memoType: MemoType.MESSAGE, content: "negotiation" }), makeMemo({ id: 2, memoType: MemoType.PAYABLE_REQUEST, content: "request", }), makeMemo({ id: 3, memoType: MemoType.TXHASH, content: VALID_TX_HASH }), ]; const result = verifyPaymentProof(memos); expect(result.verified).toBe(true); expect(result.memo?.id).toBe(3); expect(result.memo?.content).toBe(VALID_TX_HASH); }); it("prefers first matching payment memo", () => { const memos = [ makeMemo({ id: 1, memoType: MemoType.PAYABLE_TRANSFER, content: VALID_TX_HASH, }), makeMemo({ id: 2, memoType: MemoType.TXHASH, content: VALID_TX_HASH }), ]; const result = verifyPaymentProof(memos); expect(result.verified).toBe(true); expect(result.memo?.id).toBe(1); }); });