@yoroi/swap
Version:
The Swap package of Yoroi SDK
413 lines (412 loc) • 14.4 kB
JavaScript
"use strict";
import { fetchData } from '@yoroi/common';
import { Api, Chain } from '@yoroi/types';
import { muesliswapApiMaker, parseMuesliError } from './api-maker';
import { api } from './api.mocks';
jest.mock('@yoroi/common', () => ({
fetchData: jest.fn(),
isLeft: jest.requireActual('@yoroi/common').isLeft,
difference: jest.requireActual('@yoroi/common').difference
}));
describe('muesliswapApiMaker', () => {
const mockFetchData = fetchData;
const config = {
addressHex: 'someAddressHex',
address: 'someAddress',
primaryTokenInfo: {},
isPrimaryToken: () => false,
stakingKey: 'someStakingKey',
network: Chain.Network.Mainnet,
partner: 'somePartnerId'
};
afterEach(() => {
mockFetchData.mockReset();
});
it('should return an object with the Swap.Api interface', () => {
const muesliApi = muesliswapApiMaker(config);
expect(muesliApi).toHaveProperty('tokens');
expect(muesliApi).toHaveProperty('orders');
expect(muesliApi).toHaveProperty('estimate');
expect(muesliApi).toHaveProperty('create');
expect(muesliApi).toHaveProperty('cancel');
});
it('should return error if network is not Mainnet', async () => {
const testConfig = {
...config,
network: Chain.Network.Preprod
};
const muesliApi = muesliswapApiMaker(testConfig);
const result = await muesliApi.tokens();
if (result.tag !== 'left') fail();
expect(result.tag).toBe('left');
expect(result.error.message).toMatch(/works on mainnet/);
});
describe('tokens()', () => {
it('returns a successful response', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: api.responses.tokens
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.tokens();
expect(mockFetchData).toHaveBeenCalledWith({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
method: 'get',
url: 'https://aggregator-v2.muesliswap.com/tokens'
});
expect(result.tag).toBe('right');
});
it('returns an error (isLeft)', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'left',
error: {
status: 500,
message: 'Server error',
responseData: {
detail: 'something went wrong'
}
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.tokens();
if (result.tag !== 'left') fail();
expect(result.tag).toBe('left');
expect(result.error.message).toContain('something went wrong');
});
});
describe('orders()', () => {
it('should return a successful transformed response', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: {
...api.responses.orders,
orders: [...api.responses.orders.orders, {
dex: 'sundaeswap-v1',
aggregator: null,
fromToken: '.',
toToken: '4cb48d60d1f7823d1307c61b9ecf472ff78cf22d1ccc5786d59461f8.4144414d4f4f4e',
fromAmount: '0.000036',
toAmount: '10',
paidAmount: '0.000036',
receivedAmount: '11',
batcherFee: '2.500000',
attachedValues: [{
amount: 4500036,
token: '.'
}],
sender: 'addr1q9r502tqdksvqmhs3lwlxx5f5cz0c92cftqqludl3r0urtk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwql6sl74',
beneficiary: 'addr1q9r502tqdksvqmhs3lwlxx5f5cz0c92cftqqludl3r0urtk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwql6sl74',
txHash: '29f51a2a9e46ced05f03abc9b419ae57164dc056534121f041d69e307b9722f8',
outputIdx: 0,
deposit: '2.000000',
status: 'matched',
placedAt: undefined,
finalizedAt: undefined,
finalizedTxHash: '8d3b20bafb8378366f819f506da327a43e94d6948c002bac00a9b1de401bc571',
providerSpecifics: {
poolId: '1701',
swapDirection: 0
}
}, {
dex: 'minswap-v2',
aggregator: null,
fromToken: '.',
toToken: '49e423161ef818adc475c783571cb479d5f15ad52a01a240eacc0d3b.434f434b',
fromAmount: '0.008137',
toAmount: '1',
paidAmount: '0.000000',
receivedAmount: '0',
batcherFee: '2.000000',
attachedValues: [{
amount: 4008137,
token: '.'
}],
sender: 'addr1q9r502tqdksvqmhs3lwlxx5f5cz0c92cftqqludl3r0urtk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwql6sl74',
beneficiary: 'addr1q9r502tqdksvqmhs3lwlxx5f5cz0c92cftqqludl3r0urtk0ppwv8x4ylafdu84xqmh9sx4vrk4czekksv884xmvanwql6sl74',
txHash: '475ffb1f1820eee1790729d86ced473e9f7724ddcd7bf59b477e3293415f16bf',
outputIdx: 0,
deposit: '2.000000',
status: 'canceled',
placedAt: undefined,
finalizedAt: undefined,
finalizedTxHash: null,
providerSpecifics: null
}]
}
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.orders();
expect(mockFetchData).toHaveBeenCalledWith(expect.objectContaining({
url: 'https://aggregator-v2.muesliswap.com/order_history',
method: 'get'
}), {
params: {
user_address: 'someAddress',
numbers_have_decimals: true
}
});
expect(result.tag).toBe('right');
});
it('should return error (isLeft) when the response is left', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'left',
error: {
status: 500,
message: 'Some error',
responseData: {
detail: 'orderHistory error'
}
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.orders();
if (result.tag !== 'left') fail();
expect(result.tag).toBe('left');
expect(result.error.message).toContain('orderHistory error');
});
it('should return left if transformer throws an error', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: {}
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.orders();
if (result.tag !== 'left') fail();
expect(result.tag).toBe('left');
expect(result.error.message).toBe('Failed to transform orderHistory');
});
});
describe('estimate()', () => {
it('calls /quote if wantedPrice is undefined', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: api.responses.quote
}
});
const muesliApi = muesliswapApiMaker(config);
// has not wantedPrice
const result = await muesliApi.estimate(api.inputs.quote);
expect(mockFetchData).toHaveBeenCalledWith(expect.objectContaining({
url: 'https://aggregator-v2.muesliswap.com/quote',
method: 'post',
data: expect.any(Object)
}));
expect(result.tag).toBe('right');
});
it('calls /limit_order_quote if wantedPrice is provided', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: api.responses.quote
}
});
const muesliApi = muesliswapApiMaker(config);
// has wantedPrice
const result = await muesliApi.estimate({
slippage: 0.01,
tokenIn: '.',
tokenOut: 'af2e27f580f7f08e93190a81f72462f153026d06450924726645891b.44524950',
protocol: 'minswap-v1',
wantedPrice: 1,
amountOut: undefined,
amountIn: 1,
multiples: 1
});
expect(mockFetchData).toHaveBeenCalledWith(expect.objectContaining({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
url: 'https://aggregator-v2.muesliswap.com/limit_order_quote',
method: 'post',
data: expect.any(Object)
}));
expect(result.tag).toBe('right');
});
it('should return error (isLeft) when the response is left', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'left',
error: {
status: 500,
message: 'Some error',
responseData: {
detail: 'random error'
}
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.estimate(api.inputs.quote);
if (result.tag !== 'left') fail();
expect(result.tag).toBe('left');
expect(result.error.message).toContain('random error');
});
it('should handle transformer error in estimate', async () => {
// Mock successful API response but transformer will throw
mockFetchData.mockResolvedValueOnce({
tag: 'right',
value: {
status: 200,
data: 'invalid-data-that-will-cause-transformer-to-throw'
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.estimate(api.inputs.quote);
expect(result.tag).toBe('left');
if (result.tag === 'left') {
expect(result.error.status).toBe(-3);
expect(result.error.message).toContain('No liquidity pools satisfy the estimate requirements');
}
});
});
describe('create()', () => {
it('calls /order if no wantedPrice', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: api.responses.create
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.create(api.inputs.create[0]);
expect(mockFetchData).toHaveBeenCalledWith(expect.objectContaining({
url: 'https://aggregator-v2.muesliswap.com/order',
method: 'post',
data: expect.any(Object)
}));
expect(result.tag).toBe('right');
});
it('calls /limit_order if wantedPrice is provided', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: api.responses.createLimit
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.create(api.inputs.createLimit[0]);
expect(mockFetchData).toHaveBeenCalledWith(expect.objectContaining({
url: 'https://aggregator-v2.muesliswap.com/limit_order',
method: 'post',
data: expect.any(Object)
}));
expect(result.tag).toBe('right');
});
it('should return error (isLeft) when the response is left', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'left',
error: {
status: 500,
message: 'Some error',
responseData: {
detail: 'random error'
}
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.create(api.inputs.createLimit[0]);
if (result.tag !== 'left') fail();
expect(result.tag).toBe('left');
expect(result.error.message).toContain('random error');
});
});
describe('cancel()', () => {
it('calls /cancel endpoint successfully', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'right',
value: {
status: Api.HttpStatusCode.Ok,
data: api.responses.cancel
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.cancel(api.inputs.cancel[0]);
expect(mockFetchData).toHaveBeenCalledWith(expect.objectContaining({
url: 'https://aggregator-v2.muesliswap.com/cancel',
method: 'post',
data: expect.any(Object)
}));
expect(result.tag).toBe('right');
});
it('handles error response', async () => {
mockFetchData.mockResolvedValueOnce({
tag: 'left',
error: {
status: 500,
message: 'Cancel error',
responseData: {
detail: 'could not cancel'
}
}
});
const muesliApi = muesliswapApiMaker(config);
const result = await muesliApi.cancel(api.inputs.cancel[1]);
if (result.tag !== 'left') fail();
expect(result.tag).toBe('left');
expect(result.error.message).toContain('could not cancel');
});
});
});
describe('parseMuesliError', () => {
it('parses error when responseData.detail is present', () => {
const input = {
tag: 'left',
error: {
status: 500,
message: 'Cancel error',
responseData: {
detail: 'could not cancel'
}
}
};
const result = parseMuesliError(input);
expect(result.tag).toBe('left');
expect(result.error.message).toBe('could not cancel');
expect(Object.isFrozen(result)).toBe(true);
});
it('falls back to default message when responseData.detail is absent', () => {
const input = {
tag: 'left',
error: {
status: 500,
message: 'Cancel error'
}
};
const result = parseMuesliError(input);
expect(result.tag).toBe('left');
expect(result.error.message).toBe('Muesliswap API error');
expect(Object.isFrozen(result)).toBe(true);
});
it('strips leading and trailing quotes if detail is a JSON string', () => {
const input = {
tag: 'left',
error: {
status: 500,
message: 'Cancel error',
responseData: {
detail: 'could not "cancel"'
}
}
};
const result = parseMuesliError(input);
expect(result.error.message).toBe('could not \\"cancel\\"');
expect(Object.isFrozen(result)).toBe(true);
});
});
//# sourceMappingURL=api-maker.test.js.map