Skip to content

Commit

Permalink
WT-1758 Handle expired and insufficient balance errors from orderbook (
Browse files Browse the repository at this point in the history
  • Loading branch information
imx-mikhala authored Oct 24, 2023
1 parent 1384b21 commit fb19863
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 63 deletions.
1 change: 1 addition & 0 deletions packages/checkout/sdk/src/errors/checkoutError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export enum CheckoutErrorType {
ORDER_FEE_ERROR = 'ORDER_FEE_ERROR',
ITEM_REQUIREMENTS_ERROR = 'ITEM_REQUIREMENTS_ERROR',
API_ERROR = 'API_ERROR',
ORDER_EXPIRED_ERROR = 'ORDER_EXPIRED_ERROR',
}

/**
Expand Down
199 changes: 139 additions & 60 deletions packages/checkout/sdk/src/smartCheckout/buy/buy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,60 +497,65 @@ describe('buy', () => {
},
);

it('should call smart checkout with item requirements and gas limit', async () => {
(getUnsignedERC20ApprovalTransactions as jest.Mock).mockResolvedValue([{ from: '0xAPPROVAL' }]);
(getUnsignedFulfillmentTransactions as jest.Mock).mockResolvedValue([]);
(smartCheckout as jest.Mock).mockResolvedValue({});
(createOrderbookInstance as jest.Mock).mockReturnValue({
getListing: jest.fn().mockResolvedValue({
result: {
buy: [
{
type: 'NATIVE',
amount: '1000000000000000000',
},
],
fees: [
{
amount: '1000000000000000000',
},
],
},
}),
config: jest.fn().mockReturnValue({
seaportContractAddress,
}),
fulfillOrder: jest.fn().mockRejectedValue({}),
});
it(
'should call smart checkout with item requirements and gas limit if fulfillOrder errors with balance error',
async () => {
(getUnsignedERC20ApprovalTransactions as jest.Mock).mockResolvedValue([{ from: '0xAPPROVAL' }]);
(getUnsignedFulfillmentTransactions as jest.Mock).mockResolvedValue([]);
(smartCheckout as jest.Mock).mockResolvedValue({});
(createOrderbookInstance as jest.Mock).mockReturnValue({
getListing: jest.fn().mockResolvedValue({
result: {
buy: [
{
type: 'NATIVE',
amount: '1000000000000000000',
},
],
fees: [
{
amount: '1000000000000000000',
},
],
},
}),
config: jest.fn().mockReturnValue({
seaportContractAddress,
}),
fulfillOrder: jest.fn().mockRejectedValue(
new Error('The fulfiller does not have the balances needed to fulfill.'),
),
});

const order:BuyOrder = {
id: '1',
takerFees: [{ amount: { percentageDecimal: 0.01 }, recipient: '0xFEERECIPIENT' }],
};
const itemRequirements = [
{
type: ItemType.NATIVE,
amount: BigNumber.from('2000000000000000000'),
},
];
const gasAmount: GasAmount = {
type: TransactionOrGasType.GAS,
gasToken: {
type: GasTokenType.NATIVE,
limit: BigNumber.from(gasLimit),
},
};
const order:BuyOrder = {
id: '1',
takerFees: [{ amount: { percentageDecimal: 0.01 }, recipient: '0xFEERECIPIENT' }],
};
const itemRequirements = [
{
type: ItemType.NATIVE,
amount: BigNumber.from('2000000000000000000'),
},
];
const gasAmount: GasAmount = {
type: TransactionOrGasType.GAS,
gasToken: {
type: GasTokenType.NATIVE,
limit: BigNumber.from(gasLimit),
},
};

await buy(config, mockProvider, [order]);
expect(smartCheckout).toBeCalledWith(
config,
mockProvider,
itemRequirements,
gasAmount,
);
});
await buy(config, mockProvider, [order]);
expect(smartCheckout).toBeCalledWith(
config,
mockProvider,
itemRequirements,
gasAmount,
);
},
);

it('should call smart checkout with an erc20 requirement', async () => {
it('should call smart checkout with an erc20 requirement if fulfillOrder errors with balance error', async () => {
(getUnsignedERC20ApprovalTransactions as jest.Mock).mockResolvedValue([{ from: '0xAPPROVAL' }]);
(getUnsignedFulfillmentTransactions as jest.Mock).mockResolvedValue([]);
(createOrderbookInstance as jest.Mock).mockReturnValue({
Expand All @@ -573,7 +578,9 @@ describe('buy', () => {
config: jest.fn().mockReturnValue({
seaportContractAddress,
}),
fulfillOrder: jest.fn().mockRejectedValue({}),
fulfillOrder: jest.fn().mockRejectedValue(
new Error('The fulfiller does not have the balances needed to fulfill.'),
),
});
const smartCheckoutResult = {
sufficient: true,
Expand Down Expand Up @@ -1006,7 +1013,9 @@ describe('buy', () => {
config: jest.fn().mockReturnValue({
seaportContractAddress,
}),
fulfillOrder: jest.fn().mockRejectedValue({}),
fulfillOrder: jest.fn().mockReturnValue({
actions: [],
}),
});

const order = {
Expand Down Expand Up @@ -1049,7 +1058,9 @@ describe('buy', () => {
config: jest.fn().mockReturnValue({
seaportContractAddress,
}),
fulfillOrder: jest.fn().mockRejectedValue({}),
fulfillOrder: jest.fn().mockReturnValue({
actions: [],
}),
});

const order = {
Expand All @@ -1072,12 +1083,31 @@ describe('buy', () => {
expect(data).toEqual({ orderId: '1' });
});

it('should throw error if orderbook returns error', async () => {
it('should throw expired error if orderbook fulfillOrder returns expired error', async () => {
(createOrderbookInstance as jest.Mock).mockReturnValue({
getListing: jest.fn().mockRejectedValue(new Error('error from orderbook')),
getListing: jest.fn().mockResolvedValue({
result: {
buy: [
{
type: 'NATIVE',
amount: '1',
},
],
fees: [
{
amount: '1',
},
],
},
}),
config: jest.fn().mockReturnValue({
seaportContractAddress,
}),
fulfillOrder: jest.fn().mockRejectedValue(new Error(
'Unable to prepare fulfillment date: order is not active: 1, actual status EXPIRED',
)),
});

const provider = {} as any;
const order = {
id: '1',
takerFees: [],
Expand All @@ -1088,15 +1118,64 @@ describe('buy', () => {
let data;

try {
await buy(config, provider, [order]);
await buy(config, mockProvider, [order]);
} catch (err: any) {
message = err.message;
type = err.type;
data = err.data;
}

expect(message).toEqual('Order is expired');
expect(type).toEqual(CheckoutErrorType.ORDER_EXPIRED_ERROR);
expect(data).toEqual({
orderId: '1',
});
});

it('should throw error if orderbook fulfillOrder returns error other than expired or balances', async () => {
(createOrderbookInstance as jest.Mock).mockReturnValue({
getListing: jest.fn().mockResolvedValue({
result: {
buy: [
{
type: 'NATIVE',
amount: '1',
},
],
fees: [
{
amount: '1',
},
],
},
}),
config: jest.fn().mockReturnValue({
seaportContractAddress,
}),
fulfillOrder: jest.fn().mockRejectedValue(new Error(
'error from orderbook',
)),
});

const order = {
id: '1',
takerFees: [],
};

let message;
let type;
let data;

try {
await buy(config, mockProvider, [order]);
} catch (err: any) {
message = err.message;
type = err.type;
data = err.data;
}

expect(message).toEqual('An error occurred while getting the order listing');
expect(type).toEqual(CheckoutErrorType.GET_ORDER_LISTING_ERROR);
expect(message).toEqual('Error occurred while trying to fulfill the order');
expect(type).toEqual(CheckoutErrorType.FULFILL_ORDER_LISTING_ERROR);
expect(data).toEqual({
orderId: '1',
message: 'error from orderbook',
Expand Down
23 changes: 20 additions & 3 deletions packages/checkout/sdk/src/smartCheckout/buy/buy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
FeeValue,
Action,
FulfillOrderResponse,
OrderStatusName,
} from '@imtbl/orderbook';
import * as instance from '../../instance';
import { CheckoutConfiguration } from '../../config';
Expand Down Expand Up @@ -155,7 +156,7 @@ export const buy = async (
let unsignedFulfillmentTransactions: TransactionRequest[] = [];
let orderActions: Action[] = [];

const fulfillOrderStartTime = new Date().getTime();
const fulfillOrderStartTime = performance.now();
try {
const fulfillerAddress = await measureAsyncExecution<string>(
config,
Expand All @@ -174,9 +175,25 @@ export const buy = async (
getUnsignedERC20ApprovalTransactions(actions),
);
} catch (err: any) {
// Silently ignore error as this is usually thrown if user does not have enough balance
const elapsedTimeInSeconds = (new Date().getTime() - fulfillOrderStartTime) / 1000;
const elapsedTimeInSeconds = (performance.now() - fulfillOrderStartTime) / 1000;
debugLogger(config, 'Time to call fulfillOrder from the orderbook', elapsedTimeInSeconds);

if (err.message.includes(OrderStatusName.EXPIRED)) {
throw new CheckoutError('Order is expired', CheckoutErrorType.ORDER_EXPIRED_ERROR, { orderId: id });
}

// The balances error will be handled by bulk order fulfillment but for now we
// need to assert on this string to check that the error is not a balances error
if (!err.message.includes('The fulfiller does not have the balances needed to fulfill')) {
throw new CheckoutError(
'Error occurred while trying to fulfill the order',
CheckoutErrorType.FULFILL_ORDER_LISTING_ERROR,
{
orderId: id,
message: err.message,
},
);
}
}

try {
Expand Down

0 comments on commit fb19863

Please sign in to comment.