Skip to content

Commit

Permalink
feat: [Pay] hooks and utils (#1338)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xAlec authored Oct 1, 2024
1 parent 70e5a0b commit 98f971d
Show file tree
Hide file tree
Showing 17 changed files with 945 additions and 58 deletions.
66 changes: 34 additions & 32 deletions src/api/buildPayTransaction.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
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 {
CDP_CREATE_PRODUCT_CHARGE,
CDP_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_CREATE_PRODUCT_CHARGE_SUCCESS_RESPONSE,
MOCK_HYDRATE_CHARGE_INVALID_CHARGE_ERROR_RESPONSE,
MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE,
MOCK_INVALID_CHARGE_ID,
MOCK_VALID_CHARGE_ID,
MOCK_VALID_PAYER_ADDRESS,
MOCK_VALID_PRODUCT_ID,
} from './mocks';
import type {
BuildPayTransactionParams,
CreateProductChargeParams,
HydrateChargeAPIParams,
} from './types';

Expand All @@ -31,66 +35,66 @@ describe('buildPayTransaction', () => {
vi.clearAllMocks();
});

it('should return a Pay Transaction', async () => {
it('should return a Pay Transaction with chargeId', 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, [
expect(sendRequest).toHaveBeenCalledWith(CDP_HYDRATE_CHARGE, [
mockAPIParams,
]);
});

it('should return an error for chains other than Base', async () => {
it('should return a Pay Transaction with productId', async () => {
const mockParams: BuildPayTransactionParams = {
address: MOCK_VALID_PAYER_ADDRESS,
chainId: mainnet.id,
chargeId: MOCK_VALID_CHARGE_ID,
productId: MOCK_VALID_PRODUCT_ID,
};
const mockAPIParams: CreateProductChargeParams = {
sender: MOCK_VALID_PAYER_ADDRESS,
productId: MOCK_VALID_PRODUCT_ID,
};
(sendRequest as Mock).mockResolvedValue(
MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE,
MOCK_CREATE_PRODUCT_CHARGE_SUCCESS_RESPONSE,
);
const payTransaction = await buildPayTransaction(mockParams);
expect(payTransaction).toEqual(
MOCK_CREATE_PRODUCT_CHARGE_SUCCESS_RESPONSE.result,
);
expect(sendRequest).toHaveBeenCalledTimes(1);
expect(sendRequest).toHaveBeenCalledWith(CDP_CREATE_PRODUCT_CHARGE, [
mockAPIParams,
]);
});

it('should return an error if neither chargeId nor productId is provided', async () => {
const mockParams: BuildPayTransactionParams = {
address: MOCK_VALID_PAYER_ADDRESS,
};
const error = await buildPayTransaction(mockParams);
expect(error).toEqual({
code: 'AmBPTa01',
error: 'Pay Transactions must be on Base',
message: PAY_UNSUPPORTED_CHAIN_ERROR_MESSAGE,
error: 'No chargeId or productId provided',
message: UNCAUGHT_PAY_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',
);
Expand All @@ -103,16 +107,14 @@ describe('buildPayTransaction', () => {
});
});

it('should return an error object from buildPayTransaction', async () => {
it('should return an error object when hydrating an invalid charge', 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,
Expand All @@ -124,7 +126,7 @@ describe('buildPayTransaction', () => {
message: PAY_INVALID_CHARGE_ERROR_MESSAGE,
});
expect(sendRequest).toHaveBeenCalledTimes(1);
expect(sendRequest).toHaveBeenCalledWith(PAY_HYDRATE_CHARGE, [
expect(sendRequest).toHaveBeenCalledWith(CDP_HYDRATE_CHARGE, [
mockAPIParams,
]);
});
Expand Down
57 changes: 35 additions & 22 deletions src/api/buildPayTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,37 +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 {
CDP_CREATE_PRODUCT_CHARGE,
CDP_HYDRATE_CHARGE,
} from '../network/definitions/pay';
import { type JSONRPCResult, sendRequest } from '../network/request';
import type {
BuildPayTransactionParams,
BuildPayTransactionResponse,
CreateProductChargeParams,
HydrateChargeAPIParams,
} from './types';
import { getPayErrorMessage } from './utils/getPayErrorMessage';

export async function buildPayTransaction({
address,
chainId,
chargeId,
productId,
}: BuildPayTransactionParams): Promise<BuildPayTransactionResponse> {
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,
},
]);
let res: JSONRPCResult<BuildPayTransactionResponse>;
if (chargeId) {
res = await sendRequest<
HydrateChargeAPIParams,
BuildPayTransactionResponse
>(CDP_HYDRATE_CHARGE, [
{
sender: address,
chargeId,
},
]);
} else if (productId) {
res = await sendRequest<
CreateProductChargeParams,
BuildPayTransactionResponse
>(CDP_CREATE_PRODUCT_CHARGE, [
{
sender: address,
productId,
},
]);
} else {
return {
code: 'AmBPTa01', // Api Module Build Pay Transaction Error 01
error: 'No chargeId or productId provided',
message: getPayErrorMessage(),
};
}
if (res.error) {
return {
code: 'AmBPTa02', // Api Module Build Pay Transaction Error 02
Expand Down
28 changes: 28 additions & 0 deletions src/api/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,34 @@ export const MOCK_HYDRATE_CHARGE_SUCCESS_RESPONSE = {
},
},
};
export const MOCK_VALID_PRODUCT_ID = '1b03e80d-4e87-46fd-9772-422a1b693fb7';
export const MOCK_CREATE_PRODUCT_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',
Expand Down
10 changes: 7 additions & 3 deletions src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export type APIError = {
*/
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
chargeId?: string; // The ID of the Commerce Charge to be paid
productId?: string; // The ID of the product being paid for
};

/**
Expand Down Expand Up @@ -50,6 +50,11 @@ export type BuildSwapTransactionParams = GetSwapQuoteParams & {
*/
export type BuildSwapTransactionResponse = BuildSwapTransaction | APIError;

export type CreateProductChargeParams = {
sender: Address; // The address of the wallet paying
productId: string; // The ID of the product being paid for
};

export type GetAPIParamsForToken =
| GetSwapQuoteParams
| BuildSwapTransactionParams;
Expand Down Expand Up @@ -101,7 +106,6 @@ export type GetTokensResponse = Token[] | APIError;

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
};

Expand Down
3 changes: 2 additions & 1 deletion src/network/definitions/pay.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const PAY_HYDRATE_CHARGE = 'pay_hydrateCharge';
export const CDP_HYDRATE_CHARGE = 'cdp_hydrateCharge';
export const CDP_CREATE_PRODUCT_CHARGE = 'cdp_createProductCharge';
92 changes: 92 additions & 0 deletions src/pay/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,99 @@ 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_INSUFFICIENT_BALANCE_ERROR = 'User has insufficient balance';
export const PAY_INSUFFICIENT_BALANCE_ERROR_MESSAGE =
"You don't have enough USDC. Add funds and try again.";
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';

export enum PayErrorCode {
INSUFFICIENT_BALANCE = 'insufficient_balance',
GENERIC_ERROR = 'generic_error',
UNEXPECTED_ERROR = 'unexpected_error',
}

export interface PayErrorType {
code: PayErrorCode;
error: string;
message: string;
}

export type PayErrors = {
[K in PayErrorCode]: PayErrorType;
};

export enum PAY_LIFECYCLESTATUS {
INIT = 'init',
PENDING = 'paymentPending',
SUCCESS = 'success',
ERROR = 'error',
}

export const USDC_ADDRESS_BASE = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';

export enum CONTRACT_METHODS {
APPROVE = 'approve',
BALANCE_OF = 'balanceOf',
TRANSFER_TOKEN_PRE_APPROVED = 'transferTokenPreApproved',
}

export const COMMERCE_ABI = [
{
type: 'function',
name: 'transferTokenPreApproved',
inputs: [
{
name: '_intent',
type: 'tuple',
components: [
{
name: 'recipientAmount',
type: 'uint256',
},
{
name: 'deadline',
type: 'uint256',
},
{
name: 'recipient',
type: 'address',
},
{
name: 'recipientCurrency',
type: 'address',
},
{
name: 'refundDestination',
type: 'address',
},
{
name: 'feeAmount',
type: 'uint256',
},
{
name: 'id',
type: 'bytes16',
},
{
name: 'operator',
type: 'address',
},
{
name: 'signature',
type: 'bytes',
},
{
name: 'prefix',
type: 'bytes',
},
],
},
],
outputs: [],
stateMutability: 'nonpayable',
},
] as const;
Loading

0 comments on commit 98f971d

Please sign in to comment.