Skip to content

Commit

Permalink
Merge pull request #128 from CityOfZion/CU-86a5xtcvh-1
Browse files Browse the repository at this point in the history
CU-86a5xtcvh - BS Swap - Throw error message returned by SimpleSwap
  • Loading branch information
thiagocbalducci authored Dec 18, 2024
2 parents 95412a1 + 83c593d commit 1270fb7
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cityofzion/blockchain-service",
"comment": "Add new error event for SwapService interface",
"type": "minor"
}
],
"packageName": "@cityofzion/blockchain-service"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cityofzion/bs-swap",
"comment": "Add new error event for SimpleSwapService",
"type": "minor"
}
],
"packageName": "@cityofzion/bs-swap"
}
1 change: 1 addition & 0 deletions packages/blockchain-service/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export type SwapServiceEvents<BSName extends string = string> = {
amountToReceive: (amount: SwapServiceLoadableValue<string>) => void | Promise<void>
tokenToReceive: (token: SwapServiceLoadableValue<SwapServiceToken<BSName>>) => void | Promise<void>
availableTokensToReceive: (tokens: SwapServiceLoadableValue<SwapServiceToken<BSName>[]>) => void | Promise<void>
error: (error: string) => void | Promise<void>
}

export type SwapServiceSwapResult = {
Expand Down
112 changes: 105 additions & 7 deletions packages/bs-swap/src/__tests__/SimpleSwapService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'>
Expand All @@ -20,9 +21,13 @@ let amountToUseMinMax: SwapServiceLoadableValue<SwapServiceMinMaxAmount>
let amountToReceive: SwapServiceLoadableValue<string>
let addressToReceive: SwapServiceValidateValue<string>
let accountToUse: SwapServiceValidateValue<Account<'neo3'>>
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 }
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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')
Expand All @@ -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')
Expand All @@ -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()

Expand Down
104 changes: 65 additions & 39 deletions packages/bs-swap/src/services/SimpleSwapService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export class SimpleSwapService<BSName extends string = string> 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 &&
Expand All @@ -145,59 +146,79 @@ export class SimpleSwapService<BSName extends string = string> 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 }
this.#amountToUse = { loading: shouldRecalculateAmountToUse }
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
}
}
}
Expand All @@ -210,14 +231,19 @@ export class SimpleSwapService<BSName extends string = string> 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<BSName> | null): Promise<void> {
Expand Down Expand Up @@ -253,7 +279,7 @@ export class SimpleSwapService<BSName extends string = string> 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<BSName> | null): Promise<void> {
Expand Down

0 comments on commit 1270fb7

Please sign in to comment.