From 83c593d31c80be29d565a9744eafbbcbceb05ac9 Mon Sep 17 00:00:00 2001 From: Dust Date: Tue, 17 Dec 2024 17:29:26 -0300 Subject: [PATCH] CU-86a5xtcvh - BS Swap - Throw error message returned by SimpleSwap --- .../CU-86a5xtcvh-1_2024-12-17-22-55.json | 10 ++ .../CU-86a5xtcvh-1_2024-12-17-22-55.json | 10 ++ packages/blockchain-service/src/interfaces.ts | 1 + .../src/__tests__/SimpleSwapService.spec.ts | 112 ++++++++++++++++-- .../bs-swap/src/services/SimpleSwapService.ts | 104 ++++++++++------ 5 files changed, 191 insertions(+), 46 deletions(-) create mode 100644 common/changes/@cityofzion/blockchain-service/CU-86a5xtcvh-1_2024-12-17-22-55.json create mode 100644 common/changes/@cityofzion/bs-swap/CU-86a5xtcvh-1_2024-12-17-22-55.json diff --git a/common/changes/@cityofzion/blockchain-service/CU-86a5xtcvh-1_2024-12-17-22-55.json b/common/changes/@cityofzion/blockchain-service/CU-86a5xtcvh-1_2024-12-17-22-55.json new file mode 100644 index 0000000..6f6781d --- /dev/null +++ b/common/changes/@cityofzion/blockchain-service/CU-86a5xtcvh-1_2024-12-17-22-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/blockchain-service", + "comment": "Add new error event for SwapService interface", + "type": "minor" + } + ], + "packageName": "@cityofzion/blockchain-service" +} \ No newline at end of file diff --git a/common/changes/@cityofzion/bs-swap/CU-86a5xtcvh-1_2024-12-17-22-55.json b/common/changes/@cityofzion/bs-swap/CU-86a5xtcvh-1_2024-12-17-22-55.json new file mode 100644 index 0000000..1899c61 --- /dev/null +++ b/common/changes/@cityofzion/bs-swap/CU-86a5xtcvh-1_2024-12-17-22-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/bs-swap", + "comment": "Add new error event for SimpleSwapService", + "type": "minor" + } + ], + "packageName": "@cityofzion/bs-swap" +} \ No newline at end of file diff --git a/packages/blockchain-service/src/interfaces.ts b/packages/blockchain-service/src/interfaces.ts index 571123b..4b9dc08 100644 --- a/packages/blockchain-service/src/interfaces.ts +++ b/packages/blockchain-service/src/interfaces.ts @@ -277,6 +277,7 @@ export type SwapServiceEvents = { amountToReceive: (amount: SwapServiceLoadableValue) => void | Promise tokenToReceive: (token: SwapServiceLoadableValue>) => void | Promise availableTokensToReceive: (tokens: SwapServiceLoadableValue[]>) => void | Promise + error: (error: string) => void | Promise } export type SwapServiceSwapResult = { diff --git a/packages/bs-swap/src/__tests__/SimpleSwapService.spec.ts b/packages/bs-swap/src/__tests__/SimpleSwapService.spec.ts index a966fc6..cd40044 100644 --- a/packages/bs-swap/src/__tests__/SimpleSwapService.spec.ts +++ b/packages/bs-swap/src/__tests__/SimpleSwapService.spec.ts @@ -7,6 +7,7 @@ import { } from '@cityofzion/blockchain-service' import { SimpleSwapService } from '../services/SimpleSwapService' import { BSNeo3 } from '@cityofzion/bs-neo3' +import { SimpleSwapApi } from '../apis/SimpleSwapApi' let blockchainServicesByName: Record<'neo3', BSNeo3<'neo3'>> let simpleSwapService: SimpleSwapService<'neo3'> @@ -20,9 +21,13 @@ let amountToUseMinMax: SwapServiceLoadableValue let amountToReceive: SwapServiceLoadableValue let addressToReceive: SwapServiceValidateValue let accountToUse: SwapServiceValidateValue> +let error: string | undefined describe('SimpleSwapService', () => { beforeEach(async () => { + jest.clearAllMocks() + + error = undefined availableTokensToUse = { loading: false, value: null } availableTokensToReceive = { loading: false, value: null } tokenToUse = { loading: false, value: null } @@ -79,6 +84,10 @@ describe('SimpleSwapService', () => { simpleSwapService.eventEmitter.on('accountToUse', value => { accountToUse = value }) + + simpleSwapService.eventEmitter.on('error', value => { + error = value + }) }) it('Should not be able to set the token to use if available tokens to use is not set', async () => { @@ -226,7 +235,7 @@ describe('SimpleSwapService', () => { it("Should not be able to set the token to receive if the available tokens to receive isn't set", async () => { await simpleSwapService.init() await expect(simpleSwapService.setTokenToReceive(null)).rejects.toThrow('Available tokens to receive is not set') - }) + }, 10000) it("Should not be able to set the token to receive if it's not in the available tokens to receive", async () => { await simpleSwapService.init() @@ -337,9 +346,7 @@ describe('SimpleSwapService', () => { await simpleSwapService.setTokenToUse(tokenUse) - const account = blockchainServicesByName.neo3.generateAccountFromKey( - process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN as string - ) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) await simpleSwapService.setAccountToUse(account) await simpleSwapService.setAmountToUse('50') @@ -365,9 +372,7 @@ describe('SimpleSwapService', () => { await simpleSwapService.setTokenToUse(tokenUse) - const account = blockchainServicesByName.neo3.generateAccountFromKey( - process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN as string - ) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) await simpleSwapService.setAccountToUse(account) await simpleSwapService.setAmountToUse('50') @@ -385,6 +390,99 @@ describe('SimpleSwapService', () => { expect(amountToUseMinMax).toEqual({ loading: true, value: null }) }, 10000) + it('Should be able to set error when the API throw an error when calling init', async () => { + jest.spyOn(SimpleSwapApi.prototype, 'getCurrencies').mockRejectedValueOnce(new Error('API ERROR')) + try { + await simpleSwapService.init() + } catch { + /* empty */ + } + expect(error).toBeTruthy() + }) + + it('Should be able to set error when the API throw an error when trying to recalculate available tokens to receive', async () => { + jest.spyOn(SimpleSwapApi.prototype, 'getPairs').mockRejectedValueOnce(new Error('API ERROR')) + + await simpleSwapService.init() + const token = availableTokensToUse.value![1] + + try { + await simpleSwapService.setTokenToUse(token) + } catch { + /* empty */ + } + + expect(error).toBeTruthy() + expect(availableTokensToUse).toEqual({ loading: false, value: expect.any(Array) }) + expect(availableTokensToReceive).toEqual({ loading: false, value: null }) + expect(tokenToUse).toEqual({ loading: false, value: token }) + expect(tokenToReceive).toEqual({ loading: false, value: null }) + expect(accountToUse).toEqual({ loading: false, value: null, valid: null }) + expect(amountToUse).toEqual({ loading: false, value: null }) + expect(amountToReceive).toEqual({ loading: false, value: null }) + expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(amountToUseMinMax).toEqual({ loading: false, value: null }) + }, 10000) + + it('Should be able to set error when the API throw an error when trying to recalculate min amount to use', async () => { + jest.spyOn(SimpleSwapApi.prototype, 'getRange').mockRejectedValueOnce(new Error('API ERROR')) + + await simpleSwapService.init() + const tokenUse = availableTokensToUse.value![1] + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + await simpleSwapService.setAccountToUse(account) + + const tokenReceive = availableTokensToReceive.value![0] + + try { + await simpleSwapService.setTokenToReceive(tokenReceive) + } catch { + /* empty */ + } + + expect(error).toBeTruthy() + expect(tokenToUse).toEqual({ loading: false, value: tokenUse }) + expect(accountToUse).toEqual({ loading: false, value: account, valid: true }) + expect(availableTokensToUse).toEqual({ loading: false, value: expect.any(Array) }) + expect(availableTokensToReceive).toEqual({ loading: false, value: expect.any(Array) }) + expect(tokenToReceive).toEqual({ loading: false, value: tokenReceive }) + expect(amountToUse).toEqual({ loading: false, value: null }) + expect(amountToReceive).toEqual({ loading: false, value: null }) + expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(amountToUseMinMax).toEqual({ loading: false, value: null }) + }, 10000) + + it('Should be able to set error when the API throw an error when trying to recalculate amount to receive', async () => { + jest.spyOn(SimpleSwapApi.prototype, 'getEstimate').mockRejectedValueOnce(new Error('API ERROR')) + + await simpleSwapService.init() + const tokenUse = availableTokensToUse.value![1] + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + await simpleSwapService.setAccountToUse(account) + + const tokenReceive = availableTokensToReceive.value![0] + try { + await simpleSwapService.setTokenToReceive(tokenReceive) + } catch { + /* empty */ + } + + expect(error).toBeTruthy() + expect(tokenToUse).toEqual({ loading: false, value: tokenUse }) + expect(accountToUse).toEqual({ loading: false, value: account, valid: true }) + expect(availableTokensToUse).toEqual({ loading: false, value: expect.any(Array) }) + expect(availableTokensToReceive).toEqual({ loading: false, value: expect.any(Array) }) + expect(tokenToReceive).toEqual({ loading: false, value: tokenReceive }) + expect(amountToUse).toEqual({ loading: false, value: amountToUseMinMax.value?.min }) + expect(amountToReceive).toEqual({ loading: false, value: null }) + expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(amountToUseMinMax).toEqual({ loading: false, value: expect.objectContaining({ min: expect.any(String) }) }) + }, 10000) + it.skip('Should create a swap when all fields are filled', async () => { await simpleSwapService.init() diff --git a/packages/bs-swap/src/services/SimpleSwapService.ts b/packages/bs-swap/src/services/SimpleSwapService.ts index 80bbccf..61c5156 100644 --- a/packages/bs-swap/src/services/SimpleSwapService.ts +++ b/packages/bs-swap/src/services/SimpleSwapService.ts @@ -137,6 +137,7 @@ export class SimpleSwapService implements SwapSe this.#accountToUse = { valid: this.#tokenToUse.value.blockchain === this.#accountToUse.value.blockchain } } + const shouldRecalculateAvailableTokensToReceive = fieldsToRecalculate.includes('availableTokensToReceive') const shouldRecalculateAmountToUse = fieldsToRecalculate.includes('amountToUse') && this.#amountToUse.value === null && @@ -145,7 +146,6 @@ export class SimpleSwapService implements SwapSe fieldsToRecalculate.includes('amountToReceive') && this.#tokenToReceive.value !== null const shouldRecalculateAmountToUseMinMax = fieldsToRecalculate.includes('amountToUseMinMax') && this.#tokenToReceive.value !== null - const shouldRecalculateAvailableTokensToReceive = fieldsToRecalculate.includes('availableTokensToReceive') this.#availableTokensToReceive = { loading: shouldRecalculateAvailableTokensToReceive } this.#amountToUseMinMax = { loading: shouldRecalculateAmountToUseMinMax } @@ -153,51 +153,72 @@ export class SimpleSwapService implements SwapSe this.#amountToReceive = { loading: shouldRecalculateAmountToReceive } if (shouldRecalculateAvailableTokensToReceive) { - const pairs = await this.#api.getPairs(this.#tokenToUse.value.ticker, this.#tokenToUse.value.network) - this.#availableTokensToReceive = { value: pairs } + try { + const pairs = await this.#api.getPairs(this.#tokenToUse.value.ticker, this.#tokenToUse.value.network) + this.#availableTokensToReceive = { value: pairs } - if (this.#tokenToUse.value && !pairs.some(pair => pair.ticker === this.#tokenToUse.value!.ticker)) { + if (this.#tokenToUse.value && !pairs.some(pair => pair.ticker === this.#tokenToUse.value!.ticker)) { + this.#tokenToReceive = { value: null } + } + } catch (error: any) { + this.eventEmitter.emit('error', error.message) + this.#availableTokensToReceive = { value: null } this.#tokenToReceive = { value: null } + this.#amountToUseMinMax = { value: null } + this.#amountToReceive = { value: null } + throw error } } if (shouldRecalculateAmountToUseMinMax || shouldRecalculateAmountToUse || shouldRecalculateAmountToReceive) { let range: SwapServiceMinMaxAmount | null = this.#amountToUseMinMax.value - - if ((shouldRecalculateAmountToUseMinMax || range === null) && this.#tokenToReceive.value) { - const apiRange = await this.#api.getRange(this.#tokenToUse.value, this.#tokenToReceive.value) - - // Add 1% because the SimpleSwap sends us a smaller minimum - const rangeMin = (+apiRange.min * 1.01).toString() - - range = { - min: this.#tokenToUse.value.decimals ? formatNumber(rangeMin, this.#tokenToUse.value.decimals) : rangeMin, - max: - this.#tokenToUse.value.decimals && apiRange.max - ? formatNumber(apiRange.max, this.#tokenToUse.value.decimals) - : apiRange.max, + try { + if ((shouldRecalculateAmountToUseMinMax || range === null) && this.#tokenToReceive.value) { + const apiRange = await this.#api.getRange(this.#tokenToUse.value, this.#tokenToReceive.value) + + // Add 1% because the SimpleSwap sends us a smaller minimum + const rangeMin = (+apiRange.min * 1.01).toString() + + range = { + min: this.#tokenToUse.value.decimals ? formatNumber(rangeMin, this.#tokenToUse.value.decimals) : rangeMin, + max: + this.#tokenToUse.value.decimals && apiRange.max + ? formatNumber(apiRange.max, this.#tokenToUse.value.decimals) + : apiRange.max, + } } - } - this.#amountToUseMinMax = { value: range } + this.#amountToUseMinMax = { value: range } - if (shouldRecalculateAmountToUse && range) { - this.#amountToUse = { - value: this.#tokenToUse.value.decimals - ? formatNumber(range.min, this.#tokenToUse.value.decimals) - : range.min, + if (shouldRecalculateAmountToUse && range) { + this.#amountToUse = { + value: this.#tokenToUse.value.decimals + ? formatNumber(range.min, this.#tokenToUse.value.decimals) + : range.min, + } } + } catch (error: any) { + this.eventEmitter.emit('error', error.message) + this.#amountToUseMinMax = { value: null } + this.#amountToReceive = { value: null } + throw error } if (shouldRecalculateAmountToReceive && this.#tokenToReceive.value && this.#amountToUse.value) { - const estimate = await this.#api.getEstimate( - this.#tokenToUse.value, - this.#tokenToReceive.value, - this.#amountToUse.value - ) - - this.#amountToReceive = { - value: estimate, + try { + const estimate = await this.#api.getEstimate( + this.#tokenToUse.value, + this.#tokenToReceive.value, + this.#amountToUse.value + ) + + this.#amountToReceive = { + value: estimate, + } + } catch (error: any) { + this.eventEmitter.emit('error', error.message) + this.#amountToReceive = { value: null } + throw error } } } @@ -210,14 +231,19 @@ export class SimpleSwapService implements SwapSe } async init() { - const tokens = await this.#api.getCurrencies({ - blockchainServicesByName: this.#blockchainServicesByName, - chainsByServiceName: this.#chainsByServiceName, - }) + try { + const tokens = await this.#api.getCurrencies({ + blockchainServicesByName: this.#blockchainServicesByName, + chainsByServiceName: this.#chainsByServiceName, + }) - const filteredTokens = tokens.filter(token => token.blockchain && token.decimals !== undefined && token.hash) + const filteredTokens = tokens.filter(token => token.blockchain && token.decimals !== undefined && token.hash) - this.#availableTokensToUse = { loading: false, value: filteredTokens } + this.#availableTokensToUse = { loading: false, value: filteredTokens } + } catch (error: any) { + this.eventEmitter.emit('error', error.message) + throw error + } } async setTokenToUse(token: SwapServiceToken | null): Promise { @@ -253,7 +279,7 @@ export class SimpleSwapService implements SwapSe this.#tokenToUse.value?.decimals && amount ? formatNumber(amount, this.#tokenToUse.value.decimals) : amount, } - debounce(this.#recalculateValues.bind(this), 500)(['amountToReceive']) + debounce(this.#recalculateValues.bind(this), 1000)(['amountToReceive']) } async setTokenToReceive(token: SwapServiceToken | null): Promise {