diff --git a/src/api/buildPayTransaction.test.ts b/src/api/buildPayTransaction.test.ts new file mode 100644 index 0000000000..60e0b14863 --- /dev/null +++ b/src/api/buildPayTransaction.test.ts @@ -0,0 +1,131 @@ +import { base, mainnet } from 'viem/chains'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { Mock } from 'vitest'; +import { PAY_HYDRATE_CHARGE } from '../network/definitions/pay'; +import { sendRequest } from '../network/request'; +import { + PAY_INVALID_CHARGE_ERROR_MESSAGE, + PAY_UNSUPPORTED_CHAIN_ERROR_MESSAGE, + UNCAUGHT_PAY_ERROR_MESSAGE, +} from '../pay/constants'; +/** + * @vitest-environment node + */ +import { buildPayTransaction } from './buildPayTransaction'; +import { + MOCK_HYDRATE_CHARGE_INVALID_CHARGE_ERROR_RESPONSE, + MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE, + MOCK_INVALID_CHARGE_ID, + MOCK_VALID_CHARGE_ID, + MOCK_VALID_PAYER_ADDRESS, +} from './mocks'; +import type { + BuildPayTransactionParams, + HydrateChargeAPIParams, +} from './types'; + +vi.mock('../network/request'); + +describe('buildPayTransaction', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return a Pay Transaction', async () => { + const mockParams: BuildPayTransactionParams = { + address: MOCK_VALID_PAYER_ADDRESS, + chainId: base.id, + chargeId: MOCK_VALID_CHARGE_ID, + }; + const mockAPIParams: HydrateChargeAPIParams = { + sender: MOCK_VALID_PAYER_ADDRESS, + chargeId: MOCK_VALID_CHARGE_ID, + chainId: base.id, + }; + (sendRequest as Mock).mockResolvedValue( + MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE, + ); + const payTransaction = await buildPayTransaction(mockParams); + expect(payTransaction).toEqual(MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE.result); + expect(sendRequest).toHaveBeenCalledTimes(1); + expect(sendRequest).toHaveBeenCalledWith(PAY_HYDRATE_CHARGE, [ + mockAPIParams, + ]); + }); + + it('should return an error for chains other than Base', async () => { + const mockParams: BuildPayTransactionParams = { + address: MOCK_VALID_PAYER_ADDRESS, + chainId: mainnet.id, + chargeId: MOCK_VALID_CHARGE_ID, + }; + (sendRequest as Mock).mockResolvedValue( + MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE, + ); + const error = await buildPayTransaction(mockParams); + expect(error).toEqual({ + code: 'AmBPTa01', + error: 'Pay Transactions must be on Base', + message: PAY_UNSUPPORTED_CHAIN_ERROR_MESSAGE, + }); + expect(sendRequest).not.toHaveBeenCalled(); + }); + + it('should return an error if sendRequest fails', async () => { + const mockParams: BuildPayTransactionParams = { + address: MOCK_VALID_PAYER_ADDRESS, + chainId: base.id, + chargeId: MOCK_VALID_CHARGE_ID, + }; + const mockAPIParams: HydrateChargeAPIParams = { + sender: MOCK_VALID_PAYER_ADDRESS, + chargeId: MOCK_VALID_CHARGE_ID, + chainId: base.id, + }; + (sendRequest as Mock).mockResolvedValue( + MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE, + ); + const hydratedCharge = await buildPayTransaction(mockParams); + expect(hydratedCharge).toEqual(MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE.result); + expect(sendRequest).toHaveBeenCalledTimes(1); + expect(sendRequest).toHaveBeenCalledWith(PAY_HYDRATE_CHARGE, [ + mockAPIParams, + ]); + const mockError = new Error( + 'buildPayTransaction: Error: Failed to send request', + ); + (sendRequest as Mock).mockRejectedValue(mockError); + const error = await buildPayTransaction(mockParams); + expect(error).toEqual({ + code: 'AmBPTa03', + error: 'Something went wrong', + message: UNCAUGHT_PAY_ERROR_MESSAGE, + }); + }); + + it('should return an error object from buildPayTransaction', async () => { + const mockParams: BuildPayTransactionParams = { + address: MOCK_VALID_PAYER_ADDRESS, + chainId: base.id, + chargeId: MOCK_INVALID_CHARGE_ID, + }; + const mockAPIParams: HydrateChargeAPIParams = { + sender: MOCK_VALID_PAYER_ADDRESS, + chargeId: MOCK_INVALID_CHARGE_ID, + chainId: base.id, + }; + (sendRequest as Mock).mockResolvedValue( + MOCK_HYDRATE_CHARGE_INVALID_CHARGE_ERROR_RESPONSE, + ); + const error = await buildPayTransaction(mockParams); + expect(error).toEqual({ + code: 'AmBPTa02', + error: 'method not found - Not found', + message: PAY_INVALID_CHARGE_ERROR_MESSAGE, + }); + expect(sendRequest).toHaveBeenCalledTimes(1); + expect(sendRequest).toHaveBeenCalledWith(PAY_HYDRATE_CHARGE, [ + mockAPIParams, + ]); + }); +}); diff --git a/src/api/buildPayTransaction.ts b/src/api/buildPayTransaction.ts new file mode 100644 index 0000000000..8febe87f7b --- /dev/null +++ b/src/api/buildPayTransaction.ts @@ -0,0 +1,50 @@ +import { base } from 'viem/chains'; +import { PAY_HYDRATE_CHARGE } from '../network/definitions/pay'; +import { sendRequest } from '../network/request'; +import { PAY_UNSUPPORTED_CHAIN_ERROR_MESSAGE } from '../pay/constants'; +import type { + BuildPayTransactionParams, + BuildPayTransactionResponse, + HydrateChargeAPIParams, +} from './types'; +import { getPayErrorMessage } from './utils/getPayErrorMessage'; + +export async function buildPayTransaction({ + address, + chainId, + chargeId, +}: BuildPayTransactionParams): Promise { + if (chainId !== base.id) { + return { + code: 'AmBPTa01', // Api Module Build Pay Transaction Error 01 + error: 'Pay Transactions must be on Base', + message: PAY_UNSUPPORTED_CHAIN_ERROR_MESSAGE, + }; + } + try { + const res = await sendRequest< + HydrateChargeAPIParams, + BuildPayTransactionResponse + >(PAY_HYDRATE_CHARGE, [ + { + sender: address, + chainId: chainId, + chargeId, + }, + ]); + if (res.error) { + return { + code: 'AmBPTa02', // Api Module Build Pay Transaction Error 02 + error: res.error.message, + message: getPayErrorMessage(res.error?.code), + }; + } + return res.result; + } catch (_error) { + return { + code: 'AmBPTa03', // Api Module Build Pay Transaction Error 03 + error: 'Something went wrong', + message: getPayErrorMessage(), + }; + } +} diff --git a/src/api/index.ts b/src/api/index.ts index b532852214..0f88f9e12d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,9 +1,12 @@ // 🌲☀️🌲 export { buildSwapTransaction } from './buildSwapTransaction'; +export { buildPayTransaction } from './buildPayTransaction'; export { getSwapQuote } from './getSwapQuote'; export { getTokens } from './getTokens'; export type { APIError, + BuildPayTransactionParams, + BuildPayTransactionResponse, BuildSwapTransactionParams, GetSwapQuoteParams, GetSwapQuoteResponse, diff --git a/src/api/mocks.ts b/src/api/mocks.ts new file mode 100644 index 0000000000..7457c43cad --- /dev/null +++ b/src/api/mocks.ts @@ -0,0 +1,38 @@ +export const MOCK_VALID_CHARGE_ID = '1b03e80d-4e87-46fd-9772-422a1b693fb7'; +export const MOCK_INVALID_CHARGE_ID = '00000000-0000-0000-0000-000000000000'; +export const MOCK_VALID_PAYER_ADDRESS = + '0x98fAbEA34A3A377916EBF7793f37E11EE98D29Eb'; +export const MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE = { + id: 1, + jsonrpc: '2.0', + result: { + id: MOCK_VALID_CHARGE_ID, + callData: { + deadline: '2024-08-29T23:00:38Z', + feeAmount: '10000', + id: '0xd2e57fb373f246768a193cadd4a5ce1e', + operator: '0xd1db362f9d23a029834375afa2b37d91d2e67a95', + prefix: '0x4b3220496e666f726d6174696f6e616c204d6573736167653a20333220', + recipient: '0xb724dcF5f1156dd8E2AB217921b5Bd46a9e5cAa5', + recipientAmount: '990000', + recipientCurrency: '0xF175520C52418dfE19C8098071a252da48Cd1C19', + refundDestination: MOCK_VALID_PAYER_ADDRESS, + signature: + '0xb49a08026bdfdc55e3b1b797a9481fbdb7a9246c73f5f77fece76d5f24e979561f2168862aed0dd72980a4f9930cf23836084f3326b98b5546b280c4f0d57aae1b', + }, + metaData: { + chainId: 8453, + contractAddress: '0x131642c019AF815Ae5F9926272A70C84AE5C37ab', + sender: MOCK_VALID_PAYER_ADDRESS, + settlementCurrencyAddress: '0xF175520C52418dfE19C8098071a252da48Cd1C19', + }, + }, +}; +export const MOCK_HYDRATE_CHARGE_INVALID_CHARGE_ERROR_RESPONSE = { + id: 1, + jsonrpc: '2.0', + error: { + code: -32601, + message: 'method not found - Not found', + }, +}; diff --git a/src/api/types.ts b/src/api/types.ts index 121224540a..14a5adf245 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -79,3 +79,47 @@ export type RawTransactionData = { }; export type SwapAPIParams = GetQuoteAPIParams | GetSwapAPIParams; + +/** + * Note: exported as public Type + */ +export type BuildPayTransactionParams = { + address: Address; // The address of the wallet paying + chainId: number; // The Chain ID of the payment Network (only Base is supported) + chargeId: string; // The ID of the Commerce Charge to be paid +}; + +export type HydrateChargeAPIParams = { + sender: Address; // The address of the wallet paying + chainId: number; // The Chain ID of the payment Network (only Base is supported) + chargeId: string; // The ID of the Commerce Charge to be paid +}; + +export type PayTransaction = { + id: string; // The id of the Commerce Charge to be paid + callData: { + // Collection of fields used to make the contract call to the Payment contract + deadline: string; // Timestamp of when the payment will expire and be unpayable + feeAmount: string; // The amount of the processing fee in the recipient currency + id: string; // The id of the prepared transaction + operator: Address; // The address of the operator of the Payment contract + prefix: Address; // The prefix of the signature generated by Commerce + recipient: Address; // The address funds will settle in + recipientAmount: string; // The amount the recipient will get in the recipient currency + recipientCurrency: Address; // The address of the currency being paid (always USDC) + refundDestination: Address; // The wallet address of the payer + signature: Address; // The signature generated by the Payment contract operator, encoding the payment details + }; + metaData: { + // Collection of metadata needed to make the contract call to the Payment Contract + chainId: number; // The chain this prepared transaction can be paid on + contractAddress: Address; // The address of the Payment contract + sender: Address; // The wallet address of the payer + settlementCurrencyAddress: Address; // The address of the currency being paid (always USDC) + }; +}; + +/** + * Note: exported as public Type + */ +export type BuildPayTransactionResponse = PayTransaction | APIError; diff --git a/src/api/utils/getPayErrorMessage.test.ts b/src/api/utils/getPayErrorMessage.test.ts new file mode 100644 index 0000000000..1faaec0c45 --- /dev/null +++ b/src/api/utils/getPayErrorMessage.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { + GENERAL_PAY_ERROR_MESSAGE, + PAY_INVALID_CHARGE_ERROR_MESSAGE, + PAY_INVALID_PARAMETER_ERROR_MESSAGE, + PAY_TOO_MANY_REQUESTS_ERROR_MESSAGE, + UNCAUGHT_PAY_ERROR_MESSAGE, +} from '../../pay/constants'; +import { getPayErrorMessage } from './getPayErrorMessage'; + +describe('getPayErrorMessage', () => { + it('should return TOO_MANY_REQUESTS_ERROR_MESSAGE for errorCode -32001', () => { + const result = getPayErrorMessage(-32001); + expect(result).toBe(PAY_TOO_MANY_REQUESTS_ERROR_MESSAGE); + }); + + it('should return PAY_INVALID_CHARGE_ERROR_MESSAGE for errorCode -32601', () => { + const result = getPayErrorMessage(-32601); + expect(result).toBe(PAY_INVALID_CHARGE_ERROR_MESSAGE); + }); + + it('should return PAY_INVALID_PARAMETER_ERROR_MESSAGE for errorCode -32602', () => { + const result = getPayErrorMessage(-32602); + expect(result).toBe(PAY_INVALID_PARAMETER_ERROR_MESSAGE); + }); + + it('should return GENERAL_PAY_ERROR_MESSAGE for misc errorCode', () => { + const result = getPayErrorMessage(-32603); + expect(result).toBe(GENERAL_PAY_ERROR_MESSAGE); + }); + + it('should return UNCAUGHT_PAY_ERROR_MESSAGE for no errorCode', () => { + const result = getPayErrorMessage(); + expect(result).toBe(UNCAUGHT_PAY_ERROR_MESSAGE); + }); +}); diff --git a/src/api/utils/getPayErrorMessage.ts b/src/api/utils/getPayErrorMessage.ts new file mode 100644 index 0000000000..c269d82111 --- /dev/null +++ b/src/api/utils/getPayErrorMessage.ts @@ -0,0 +1,27 @@ +import { + GENERAL_PAY_ERROR_MESSAGE, + PAY_INVALID_CHARGE_ERROR_MESSAGE, + PAY_INVALID_PARAMETER_ERROR_MESSAGE, + PAY_TOO_MANY_REQUESTS_ERROR_MESSAGE, + UNCAUGHT_PAY_ERROR_MESSAGE, +} from '../../pay/constants'; + +export function getPayErrorMessage(errorCode?: number) { + if (!errorCode) { + return UNCAUGHT_PAY_ERROR_MESSAGE; + } + + if (errorCode === -32001) { + return PAY_TOO_MANY_REQUESTS_ERROR_MESSAGE; + } + + if (errorCode === -32601) { + return PAY_INVALID_CHARGE_ERROR_MESSAGE; + } + + if (errorCode === -32602) { + return PAY_INVALID_PARAMETER_ERROR_MESSAGE; + } + + return GENERAL_PAY_ERROR_MESSAGE; +} diff --git a/src/network/definitions/pay.ts b/src/network/definitions/pay.ts new file mode 100644 index 0000000000..022ce66ab4 --- /dev/null +++ b/src/network/definitions/pay.ts @@ -0,0 +1 @@ +export const PAY_HYDRATE_CHARGE = 'pay_hydrateCharge'; diff --git a/src/pay/constants.ts b/src/pay/constants.ts new file mode 100644 index 0000000000..0a820e71d6 --- /dev/null +++ b/src/pay/constants.ts @@ -0,0 +1,8 @@ +export const GENERAL_PAY_ERROR_MESSAGE = 'PAY_ERROR'; +export const PAY_UNSUPPORTED_CHAIN_ERROR_MESSAGE = 'UNSUPPORTED_CHAIN'; +export const PAY_TOO_MANY_REQUESTS_ERROR_MESSAGE = + 'PAY_TOO_MANY_REQUESTS_ERROR'; +export const PAY_INVALID_CHARGE_ERROR_MESSAGE = 'PAY_INVALID_CHARGE_ERROR'; +export const PAY_INVALID_PARAMETER_ERROR_MESSAGE = + 'PAY_INVALID_PARAMETER_ERROR'; +export const UNCAUGHT_PAY_ERROR_MESSAGE = 'UNCAUGHT_PAY_ERROR';