UNPKG

@yoroi/swap

Version:
413 lines (412 loc) 14.4 kB
"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