import { test, expect, describe } from 'vitest'; import { PayPro } from '../../src/protocols/paypro.js'; import { Address, Satoshis } from '@xocash/primitives'; const BCH_ADDRESS = 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted'; const CASH_ADDRESS = 'bitcoincash:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyquzsnp0a'; const TEST_TOKEN_CATEGORY_ID = 'f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63'; const BASE_RAW = { address: Address.fromCashAddr(CASH_ADDRESS), satoshis: Satoshis.fromSats(546), }; const SUCCESS_CASES = [ // Basic URI { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted', raw: BASE_RAW, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546`, }, // Basic URI with amount { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?amount=0.00000546', raw: BASE_RAW, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546`, }, // Basic URI with Sats and Amount { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?s=546&amount=0.00000546', raw: BASE_RAW, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546`, }, // URI with Tokens and Fungible Tokens { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?c=f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63&f=10000', raw: { ...BASE_RAW, satoshis: Satoshis.fromSats(657), tokenCategoryId: TEST_TOKEN_CATEGORY_ID, fungibleTokenAmount: 10000, }, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=657&c=f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63&f=10000`, }, // URI with Tokens and Non-Fungible Tokens { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?c=f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63&n=01', raw: { ...BASE_RAW, satoshis: Satoshis.fromSats(654), tokenCategoryId: TEST_TOKEN_CATEGORY_ID, nftCommitment: '01', }, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=654&c=f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63&n=01`, }, // URI with Sats, Token Category and Fungible Tokens { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?s=546&c=f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63&f=10000', raw: { ...BASE_RAW, tokenCategoryId: TEST_TOKEN_CATEGORY_ID, fungibleTokenAmount: 10000, }, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546&c=f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63&f=10000`, }, // URI with Sats, Token Category and Non-Fungible Tokens { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?s=546&c=f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63&n=01', raw: { ...BASE_RAW, tokenCategoryId: TEST_TOKEN_CATEGORY_ID, nftCommitment: '01', }, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546&c=f6677f3d3805d70949b375d36e094ff0ec9ece2a2cb1fde6d8b0e90b368f1f63&n=01`, }, // URI with Expiry { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?e=1715769600', raw: { ...BASE_RAW, expiry: 1715769600, }, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546&e=1715769600`, }, // URI with Message { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?m=Hello', raw: { ...BASE_RAW, message: 'Hello', }, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546&m=Hello`, }, // URI with Message and Expiry { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?m=Hello&e=1715769600', raw: { ...BASE_RAW, message: 'Hello', expiry: 1715769600, }, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546&m=Hello&e=1715769600`, }, // URI with optional params { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?x-custom=value', raw: BASE_RAW, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546`, }, // URI with multiple optional params { url: 'bch:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gym58kzted?x-custom=value&x-custom2=value2', raw: BASE_RAW, result: `bch:zrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyu75gvdx7?s=546`, }, ]; const FAILURE_CASES = [ // Duplicate amount `${BCH_ADDRESS}?amount=1&amount=2`, // Duplicate sats `${BCH_ADDRESS}?s=1&s=2`, // Duplicate token category `${BCH_ADDRESS}?c=1&c=2`, // Category without tokens `${BCH_ADDRESS}?c=1`, // Fungible tokens without category `${BCH_ADDRESS}?f=1`, // Non-fungible tokens without category `${BCH_ADDRESS}?n=1`, // Amount and sats are different `${BCH_ADDRESS}?s=1&amount=2`, // requestUrl is populated `${BCH_ADDRESS}?r=https://example.com`, // Invalid key `${BCH_ADDRESS}?invalid=1`, // Invalid amount `${BCH_ADDRESS}?amount=invalid`, // Invalid sats `${BCH_ADDRESS}?s=invalid`, // Invalid expiry `${BCH_ADDRESS}?e=invalid`, // Not an address `bch:invalid`, // Bip21 `bitcoincash:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyquzsnp0a`, // Bip21 with amount `bitcoincash:qrlt8kd0gy0z4kcpvr9lmqesjupp0jg4gyquzsnp0a?amount=0.00000546`, ]; describe('PayProPaymentUrl', () => { test.each(SUCCESS_CASES)('should be able to parse a valid url', (url) => { const paymentUrl = PayPro.from(url.url); expect(paymentUrl.toRaw()).toEqual(url.raw); if (url.result) { expect(paymentUrl.toUrl()).toEqual(url.result); } else { expect(paymentUrl.toUrl()).toEqual(url.url); } }); test.each(FAILURE_CASES)( 'should throw an error for an invalid url', (url) => { expect(() => PayPro.from(url)).toThrow(); }, ); });