diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index b05f1f2..14d0888 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,4 +1,4 @@ -name: Publish all Blockchain Services Libs (Blockchain-Service, BS-Asteroid-sdk, BS-Ethereum, BS-Neo-Legacy, BS-Neo3) +name: Publish all Blockchain Services Libs (Blockchain-Service, BS-Asteroid-sdk, BS-Ethereum, BS-Neo-Legacy, BS-Neo3, BS-Swap) on: workflow_dispatch diff --git a/common/changes/@cityofzion/blockchain-service/CU-86a65zwt2_2025-01-10-21-00.json b/common/changes/@cityofzion/blockchain-service/CU-86a65zwt2_2025-01-10-21-00.json new file mode 100644 index 0000000..3fe07a0 --- /dev/null +++ b/common/changes/@cityofzion/blockchain-service/CU-86a65zwt2_2025-01-10-21-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/blockchain-service", + "comment": "Add types for extraIdToReceive and setExtraIdToReceive", + "type": "minor" + } + ], + "packageName": "@cityofzion/blockchain-service" +} \ No newline at end of file diff --git a/common/changes/@cityofzion/bs-swap/CU-86a65zwt2_2025-01-10-21-00.json b/common/changes/@cityofzion/bs-swap/CU-86a65zwt2_2025-01-10-21-00.json new file mode 100644 index 0000000..3ef69c1 --- /dev/null +++ b/common/changes/@cityofzion/bs-swap/CU-86a65zwt2_2025-01-10-21-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/bs-swap", + "comment": "Add extraId feature", + "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 0cae461..c4f571d 100644 --- a/packages/blockchain-service/src/interfaces.ts +++ b/packages/blockchain-service/src/interfaces.ts @@ -261,6 +261,7 @@ export type SwapServiceToken = { addressTemplateUrl?: string txTemplateUrl?: string network?: string + hasExtraId: boolean } export type SwapServiceLoadableValue = { loading: boolean; value: T | null } @@ -277,8 +278,8 @@ export type SwapServiceEvents = { amountToUseMinMax: (minMax: SwapServiceLoadableValue) => void | Promise tokenToUse: (token: SwapServiceLoadableValue>) => void | Promise availableTokensToUse: (tokens: SwapServiceLoadableValue[]>) => void | Promise - addressToReceive: (account: SwapServiceValidateValue) => void | Promise + extraIdToReceive: (extraIdToReceive: SwapServiceValidateValue) => void amountToReceive: (amount: SwapServiceLoadableValue) => void | Promise tokenToReceive: (token: SwapServiceLoadableValue>) => void | Promise availableTokensToReceive: (tokens: SwapServiceLoadableValue[]>) => void | Promise @@ -310,6 +311,7 @@ export interface SwapService { setAmountToUse(amount: string | null): Promise setTokenToReceive(token: SwapServiceToken | null): Promise setAddressToReceive(address: string | null): Promise + setExtraIdToReceive(extraId: string | null): Promise swap(): Promise calculateFee(): Promise } diff --git a/packages/bs-swap/src/__tests__/SimpleSwapApi.spec.ts b/packages/bs-swap/src/__tests__/SimpleSwapApi.spec.ts index 3fb1ecc..5e52bf8 100644 --- a/packages/bs-swap/src/__tests__/SimpleSwapApi.spec.ts +++ b/packages/bs-swap/src/__tests__/SimpleSwapApi.spec.ts @@ -13,6 +13,8 @@ describe('SimpleSwapApi', () => { imageUrl: 'https://static.simpleswap.io/images/currencies-logo/gasn3.svg', hash: '0xd2a4cff31913016155e38e474a2c06d08be276cf', decimals: undefined, + hasExtraId: false, + validationExtra: null, validationAddress: '^(N)[A-Za-z0-9]{33}$', addressTemplateUrl: 'https://dora.coz.io/address/neo3/mainnet/{address}', txTemplateUrl: 'https://dora.coz.io/transaction/neo3/mainnet/{txId}', @@ -28,15 +30,97 @@ describe('SimpleSwapApi', () => { imageUrl: 'https://static.simpleswap.io/images/currencies-logo/neo3.svg', hash: 'ef4073a0f2b305a38ec4050e4d3d28bc40ea63f5', decimals: 0, + hasExtraId: false, + validationExtra: null, validationAddress: '^(N)[A-Za-z0-9]{33}$', addressTemplateUrl: 'https://dora.coz.io/address/neo3/mainnet/{address}', txTemplateUrl: 'https://dora.coz.io/transaction/neo3/mainnet/{txId}', blockchain: 'neo3', } + const xrpCurrency: SimpleSwapApiCurrency = { + id: 'xrp:xrp', + ticker: 'xrp', + symbol: 'XRP', + network: 'xrp', + name: 'XRP', + imageUrl: 'https://static.simpleswap.io/images/currencies-logo/xrp.svg', + hasExtraId: true, + validationExtra: '^r[1-9A-HJ-NP-Za-km-z]{25,34}$', + validationAddress: '^((?!0)[0-9]{1,10})$', + addressTemplateUrl: 'https://xrpscan.com/account/{address}', + txTemplateUrl: 'https://xrpscan.com/tx/{txId}', + } + + const notcoinCurrency: SimpleSwapApiCurrency = { + id: 'not:ton', + ticker: 'not', + symbol: 'NOT', + network: 'ton', + name: 'Notcoin', + imageUrl: 'https://static.simpleswap.io/images/currencies-logo/not.svg', + hash: 'EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT', + hasExtraId: true, + validationExtra: '^[0-9A-Za-z\\-_]{1,120}$', + validationAddress: '^[UE][Qf][0-9a-z-A-Z\\-\\_]{46}$', + addressTemplateUrl: 'https://tonscan.org/address/{address}', + txTemplateUrl: 'https://tonscan.org/tx/{txId}', + } + it.skip('Should create the exchange with params', async () => { - const address = process.env.TEST_ADDRESS_TO_SWAP_TOKEN as string - const result = await simpleSwapApi.createExchange(gasCurrency, neoCurrency, '1000', address, address) + const address = process.env.TEST_ADDRESS_TO_SWAP_TOKEN + const result = await simpleSwapApi.createExchange({ + currencyFrom: gasCurrency, + currencyTo: neoCurrency, + amount: '89', + refundAddress: address, + address, + extraIdToReceive: null, + }) + + expect(result).toEqual( + expect.objectContaining({ + id: expect.any(String), + depositAddress: expect.any(String), + log: expect.any(String), + }) + ) + }, 10000) + + it.skip('Should create the exchange to XRP with extraIdToReceive', async () => { + const addressFrom = process.env.TEST_ADDRESS_TO_SWAP_TOKEN + const addressTo = process.env.TEST_XRP_ADDRESS_TO_SWAP_TOKEN + const extraIdToReceive = process.env.TEST_XRP_EXTRA_ID_TO_SWAP_TOKEN + const result = await simpleSwapApi.createExchange({ + currencyFrom: gasCurrency, + currencyTo: xrpCurrency, + amount: '89', + refundAddress: addressFrom, + address: addressTo, + extraIdToReceive, + }) + + expect(result).toEqual( + expect.objectContaining({ + id: expect.any(String), + depositAddress: expect.any(String), + log: expect.any(String), + }) + ) + }, 10000) + + it.skip('Should create the exchange to Notcoin with extraIdToReceive', async () => { + const addressFrom = process.env.TEST_ADDRESS_TO_SWAP_TOKEN + const addressTo = process.env.TEST_NOTCOIN_ADDRESS_TO_SWAP_TOKEN + const extraIdToReceive = process.env.TEST_NOTCOIN_EXTRA_ID_TO_SWAP_TOKEN + const result = await simpleSwapApi.createExchange({ + currencyFrom: gasCurrency, + currencyTo: notcoinCurrency, + amount: '89', + refundAddress: addressFrom, + address: addressTo, + extraIdToReceive, + }) expect(result).toEqual( expect.objectContaining({ @@ -48,7 +132,7 @@ describe('SimpleSwapApi', () => { }, 10000) it('Should get the exchange by swap id', async () => { - const result = await simpleSwapApi.getExchange(process.env.TEST_SWAP_ID as string) + const result = await simpleSwapApi.getExchange(process.env.TEST_SWAP_ID) expect(result).toEqual( expect.objectContaining({ diff --git a/packages/bs-swap/src/__tests__/SimpleSwapService.spec.ts b/packages/bs-swap/src/__tests__/SimpleSwapService.spec.ts index ecff56a..1303782 100644 --- a/packages/bs-swap/src/__tests__/SimpleSwapService.spec.ts +++ b/packages/bs-swap/src/__tests__/SimpleSwapService.spec.ts @@ -21,6 +21,7 @@ let amountToUse: SwapServiceLoadableValue let amountToUseMinMax: SwapServiceLoadableValue let amountToReceive: SwapServiceLoadableValue let addressToReceive: SwapServiceValidateValue +let extraIdToReceive: SwapServiceValidateValue let accountToUse: SwapServiceValidateValue> let error: string | undefined @@ -37,6 +38,7 @@ describe('SimpleSwapService', () => { amountToUseMinMax = { loading: false, value: null } amountToReceive = { loading: false, value: null } addressToReceive = { loading: false, value: null, valid: null } + extraIdToReceive = { loading: false, value: null, valid: null } accountToUse = { loading: false, value: null, valid: null } blockchainServicesByName = { @@ -82,6 +84,10 @@ describe('SimpleSwapService', () => { addressToReceive = value }) + simpleSwapService.eventEmitter.on('extraIdToReceive', value => { + extraIdToReceive = value + }) + simpleSwapService.eventEmitter.on('accountToUse', value => { accountToUse = value }) @@ -98,7 +104,13 @@ describe('SimpleSwapService', () => { it("Should not be able to set the token to use if it's not in the available tokens to use", async () => { await simpleSwapService.init() await expect( - simpleSwapService.setTokenToUse({ symbol: 'INVALID', blockchain: 'neo3', name: 'INVALID', id: 'INVALID' }) + simpleSwapService.setTokenToUse({ + symbol: 'INVALID', + blockchain: 'neo3', + name: 'INVALID', + id: 'INVALID', + hasExtraId: false, + }) ).rejects.toThrow('You are trying to use a token that is not available') expect(availableTokensToUse).toEqual({ @@ -111,6 +123,7 @@ describe('SimpleSwapService', () => { name: expect.any(String), hash: expect.any(String), imageUrl: expect.any(String), + hasExtraId: expect.any(Boolean), addressTemplateUrl: 'https://dora.coz.io/address/neo3/mainnet/{address}', txTemplateUrl: 'https://dora.coz.io/transaction/neo3/mainnet/{txId}', }), @@ -129,6 +142,7 @@ describe('SimpleSwapService', () => { expect(amountToReceive).toEqual({ loading: false, value: null }) expect(tokenToReceive).toEqual({ loading: false, value: null }) expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(availableTokensToReceive).toEqual({ loading: false, value: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: null }) }) @@ -136,7 +150,7 @@ describe('SimpleSwapService', () => { it('Should be able to set the token to use', async () => { await simpleSwapService.init() - const token = availableTokensToUse.value![1] + const token = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(token) @@ -145,8 +159,10 @@ describe('SimpleSwapService', () => { loading: false, value: expect.arrayContaining([ expect.objectContaining({ + id: expect.any(String), symbol: expect.any(String), name: expect.any(String), + hasExtraId: expect.any(Boolean), }), ]), }) @@ -156,14 +172,15 @@ describe('SimpleSwapService', () => { expect(amountToUse).toEqual({ loading: false, value: null }) expect(amountToReceive).toEqual({ loading: false, value: null }) expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: null }) }, 10000) it('Should not be able to set the account to use if account blockchain is different of token to use blockchain', async () => { await simpleSwapService.init() - await simpleSwapService.setTokenToUse(availableTokensToUse.value![1]) + await simpleSwapService.setTokenToUse(availableTokensToUse.value![0]) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) account.blockchain = 'NONEXISTENT' as any simpleSwapService.setAccountToUse(account) @@ -173,7 +190,7 @@ describe('SimpleSwapService', () => { it('Should be able to set the account to use to null', async () => { await simpleSwapService.init() - const token = availableTokensToUse.value![1] + const token = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(token) await simpleSwapService.setAccountToUse(null) @@ -186,17 +203,18 @@ describe('SimpleSwapService', () => { expect(amountToUse).toEqual({ loading: false, value: null }) expect(amountToReceive).toEqual({ loading: false, value: null }) expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: null }) }, 10000) it('Should be able to set the account to use', async () => { await simpleSwapService.init() - const token = availableTokensToUse.value![1] + const token = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(token) expect(accountToUse).toEqual({ loading: false, value: null, valid: null }) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) expect(tokenToUse).toEqual({ loading: false, value: token }) @@ -207,19 +225,22 @@ describe('SimpleSwapService', () => { expect(amountToUse).toEqual({ loading: false, value: null }) expect(amountToReceive).toEqual({ loading: false, value: null }) expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: null }) }, 10000) it('Should be able to set the amount to use', async () => { await simpleSwapService.init() - const token = availableTokensToUse.value![1] + const token = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(token) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) - await simpleSwapService.setAmountToUse('1') + const amount = '89' + + await simpleSwapService.setAmountToUse(amount) await wait(1000) @@ -228,9 +249,10 @@ describe('SimpleSwapService', () => { expect(availableTokensToUse).toEqual({ loading: false, value: expect.any(Array) }) expect(availableTokensToReceive).toEqual({ loading: false, value: expect.any(Array) }) expect(tokenToReceive).toEqual({ loading: false, value: null }) - expect(amountToUse).toEqual({ loading: false, value: '1' }) + expect(amountToUse).toEqual({ loading: false, value: amount }) expect(amountToReceive).toEqual({ loading: false, value: null }) expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: null }) }, 10000) @@ -241,21 +263,27 @@ describe('SimpleSwapService', () => { 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() - const token = availableTokensToUse.value![1] + const token = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(token) await expect( - simpleSwapService.setTokenToReceive({ symbol: 'INVALID', blockchain: 'neo3', name: 'INVALID', id: 'INVALID' }) + simpleSwapService.setTokenToReceive({ + symbol: 'INVALID', + blockchain: 'neo3', + name: 'INVALID', + id: 'INVALID', + hasExtraId: false, + }) ).rejects.toThrow('You are trying to use a token that is not available') }, 10000) it('Should be able to set the token to receive to null', async () => { await simpleSwapService.init() - const token = availableTokensToUse.value![1] + const token = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(token) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) await simpleSwapService.setTokenToReceive(null) @@ -268,18 +296,19 @@ describe('SimpleSwapService', () => { expect(amountToUse).toEqual({ loading: false, value: amountToUse.value }) expect(amountToReceive).toEqual({ loading: false, value: null }) expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: null }) }, 10000) it('Should be able to set the token to receive', async () => { await simpleSwapService.init() - const tokenUse = availableTokensToUse.value![1] + const tokenUse = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(tokenUse) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) - const tokenReceive = availableTokensToReceive.value![0] + const tokenReceive = availableTokensToReceive.value![1] await simpleSwapService.setTokenToReceive(tokenReceive) expect(tokenToUse).toEqual({ loading: false, value: tokenUse }) @@ -290,15 +319,16 @@ describe('SimpleSwapService', () => { expect(amountToUse).toEqual({ loading: false, value: amountToUseMinMax.value?.min }) expect(amountToReceive).toEqual({ loading: false, value: expect.any(String) }) expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: expect.objectContaining({ min: expect.any(String) }) }) }, 10000) it('Should be able to set an invalid address', async () => { await simpleSwapService.init() - const tokenUse = availableTokensToUse.value![1] + const tokenUse = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(tokenUse) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) const tokenReceive = availableTokensToReceive.value![1] @@ -314,16 +344,17 @@ describe('SimpleSwapService', () => { expect(amountToUse).toEqual({ loading: false, value: amountToUseMinMax.value?.min }) expect(amountToReceive).toEqual({ loading: false, value: expect.any(String) }) expect(addressToReceive).toEqual({ loading: false, value: 'INVALID', valid: false }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: expect.objectContaining({ min: expect.any(String) }) }) }, 10000) it('Should be able to set a valid address', async () => { await simpleSwapService.init() - const tokenUse = availableTokensToUse.value![1] - const tokenReceive = availableTokensToUse.value![0] + const tokenUse = availableTokensToUse.value![0] + const tokenReceive = availableTokensToUse.value![1] await simpleSwapService.setTokenToUse(tokenUse) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) await simpleSwapService.setTokenToReceive(tokenReceive) @@ -337,21 +368,143 @@ describe('SimpleSwapService', () => { expect(amountToUse).toEqual({ loading: false, value: amountToUse.value }) expect(amountToReceive).toEqual({ loading: false, value: expect.any(String) }) expect(addressToReceive).toEqual({ loading: false, value: account.address, valid: true }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: expect.objectContaining({ min: expect.any(String) }) }) }, 20000) + it('Should be able to set an invalid extraIdToReceive to XRP', async () => { + await simpleSwapService.init() + + const tokenUse = availableTokensToUse.value![0] + + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN) + + await simpleSwapService.setAccountToUse(account) + + const xrpToken = availableTokensToReceive.value!.find(({ id }) => id === 'xrp:xrp')! + + await simpleSwapService.setTokenToReceive(xrpToken) + + const extraId = 'INVALID'.repeat(20) + + await simpleSwapService.setAddressToReceive(process.env.TEST_XRP_ADDRESS_TO_SWAP_TOKEN) + await simpleSwapService.setExtraIdToReceive(extraId) + + expect(extraIdToReceive).toEqual({ loading: false, value: extraId, valid: false }) + }, 10000) + + it('Should be able to set a valid extraIdToReceive to XRP', async () => { + await simpleSwapService.init() + + const tokenUse = availableTokensToUse.value![0] + + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN) + + await simpleSwapService.setAccountToUse(account) + + const xrpToken = availableTokensToReceive.value!.find(({ id }) => id === 'xrp:xrp')! + + await simpleSwapService.setTokenToReceive(xrpToken) + + const extraId = process.env.TEST_XRP_EXTRA_ID_TO_SWAP_TOKEN + + await simpleSwapService.setAddressToReceive(process.env.TEST_XRP_ADDRESS_TO_SWAP_TOKEN) + await simpleSwapService.setExtraIdToReceive(extraId) + + expect(extraIdToReceive).toEqual({ loading: false, value: extraId, valid: true }) + }, 10000) + + it('Should be able to set an invalid extraIdToReceive to Notcoin', async () => { + await simpleSwapService.init() + + const tokenUse = availableTokensToUse.value![0] + + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN) + + await simpleSwapService.setAccountToUse(account) + + const notcoinToken = availableTokensToReceive.value!.find(({ id }) => id === 'not:ton')! + + await simpleSwapService.setTokenToReceive(notcoinToken) + + const extraId = 'INVALID'.repeat(20) + + await simpleSwapService.setAddressToReceive(process.env.TEST_NOTCOIN_ADDRESS_TO_SWAP_TOKEN) + await simpleSwapService.setExtraIdToReceive(extraId) + + expect(extraIdToReceive).toEqual({ loading: false, value: extraId, valid: false }) + }, 10000) + + it('Should be able to set a valid extraIdToReceive to Notcoin', async () => { + await simpleSwapService.init() + + const tokenUse = availableTokensToUse.value![0] + + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN) + + await simpleSwapService.setAccountToUse(account) + + const notcoinToken = availableTokensToReceive.value!.find(({ id }) => id === 'not:ton')! + + await simpleSwapService.setTokenToReceive(notcoinToken) + + const extraId = process.env.TEST_NOTCOIN_EXTRA_ID_TO_SWAP_TOKEN + + await simpleSwapService.setAddressToReceive(process.env.TEST_NOTCOIN_ADDRESS_TO_SWAP_TOKEN) + await simpleSwapService.setExtraIdToReceive(extraId) + + expect(extraIdToReceive).toEqual({ loading: false, value: extraId, valid: true }) + }, 10000) + + it('Should clear extraIdToReceive when changes the tokenToReceive', async () => { + await simpleSwapService.init() + + const tokenUse = availableTokensToUse.value![0] + + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN) + + await simpleSwapService.setAccountToUse(account) + await simpleSwapService.setAmountToUse('89') + + const xrpToken = availableTokensToReceive.value!.find(({ id }) => id === 'xrp:xrp')! + const notcoinToken = availableTokensToReceive.value!.find(({ id }) => id === 'not:ton')! + + await simpleSwapService.setTokenToReceive(xrpToken) + + const extraId = process.env.TEST_XRP_EXTRA_ID_TO_SWAP_TOKEN + + await simpleSwapService.setAddressToReceive(process.env.TEST_XRP_ADDRESS_TO_SWAP_TOKEN) + await simpleSwapService.setExtraIdToReceive(extraId) + + expect(extraIdToReceive).toEqual({ loading: false, value: extraId, valid: true }) + + await simpleSwapService.setTokenToReceive(notcoinToken) + + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) + }, 20000) + it('Should clear amountToReceive and amountToUseMinMax when setTokenToUse is called', async () => { await simpleSwapService.init() - const tokenUse = availableTokensToUse.value![1] - const tokenReceive = availableTokensToUse.value![0] + const tokenUse = availableTokensToUse.value![0] + const tokenReceive = availableTokensToUse.value![1] await simpleSwapService.setTokenToUse(tokenUse) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) - await simpleSwapService.setAmountToUse('50') + await simpleSwapService.setAmountToUse('89') await simpleSwapService.setTokenToReceive(tokenReceive) await simpleSwapService.setAddressToReceive(account.address) @@ -369,15 +522,15 @@ describe('SimpleSwapService', () => { it('Should clear amountToReceive and amountToUseMinMax when setTokenToReceive is called', async () => { await simpleSwapService.init() - const tokenUse = availableTokensToUse.value![1] - const tokenReceive = availableTokensToUse.value![0] + const tokenUse = availableTokensToUse.value![0] + const tokenReceive = availableTokensToUse.value![1] await simpleSwapService.setTokenToUse(tokenUse) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) - await simpleSwapService.setAmountToUse('50') + await simpleSwapService.setAmountToUse('89') await simpleSwapService.setTokenToReceive(tokenReceive) await simpleSwapService.setAddressToReceive(account.address) @@ -406,7 +559,7 @@ describe('SimpleSwapService', () => { jest.spyOn(SimpleSwapApi.prototype, 'getPairs').mockRejectedValueOnce(new Error('API ERROR')) await simpleSwapService.init() - const token = availableTokensToUse.value![1] + const token = availableTokensToUse.value![0] try { await simpleSwapService.setTokenToUse(token) @@ -423,6 +576,7 @@ describe('SimpleSwapService', () => { expect(amountToUse).toEqual({ loading: false, value: null }) expect(amountToReceive).toEqual({ loading: false, value: null }) expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: null }) }, 10000) @@ -430,13 +584,13 @@ describe('SimpleSwapService', () => { jest.spyOn(SimpleSwapApi.prototype, 'getRange').mockRejectedValueOnce(new Error('API ERROR')) await simpleSwapService.init() - const tokenUse = availableTokensToUse.value![1] + const tokenUse = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(tokenUse) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) - const tokenReceive = availableTokensToReceive.value![0] + const tokenReceive = availableTokensToReceive.value![1] try { await simpleSwapService.setTokenToReceive(tokenReceive) @@ -453,6 +607,7 @@ describe('SimpleSwapService', () => { expect(amountToUse).toEqual({ loading: false, value: null }) expect(amountToReceive).toEqual({ loading: false, value: null }) expect(addressToReceive).toEqual({ loading: false, value: null, valid: null }) + expect(extraIdToReceive).toEqual({ loading: false, value: null, valid: null }) expect(amountToUseMinMax).toEqual({ loading: false, value: null }) }, 10000) @@ -460,13 +615,13 @@ describe('SimpleSwapService', () => { jest.spyOn(SimpleSwapApi.prototype, 'getEstimate').mockRejectedValueOnce(new Error('API ERROR')) await simpleSwapService.init() - const tokenUse = availableTokensToUse.value![1] + const tokenUse = availableTokensToUse.value![0] await simpleSwapService.setTokenToUse(tokenUse) - const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY as string) + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY) await simpleSwapService.setAccountToUse(account) - const tokenReceive = availableTokensToReceive.value![0] + const tokenReceive = availableTokensToReceive.value![1] try { await simpleSwapService.setTokenToReceive(tokenReceive) } catch { @@ -482,23 +637,22 @@ describe('SimpleSwapService', () => { 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(extraIdToReceive).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() - const tokenUse = availableTokensToUse.value![1] - const tokenReceive = availableTokensToUse.value![0] + const tokenUse = availableTokensToUse.value![0] + const tokenReceive = availableTokensToUse.value![1] 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_TO_SWAP_TOKEN) await simpleSwapService.setAccountToUse(account) - await simpleSwapService.setAmountToUse('1000') + await simpleSwapService.setAmountToUse('89') await simpleSwapService.setTokenToReceive(tokenReceive) await simpleSwapService.setAddressToReceive(account.address) @@ -513,4 +667,118 @@ describe('SimpleSwapService', () => { }) ) }, 20000) + + it("Should return an error on create a swap to XRP when extraIdToReceive isn't filled", async () => { + const swapSpy = jest.spyOn(simpleSwapService, 'swap') + + await simpleSwapService.init() + + const tokenUse = availableTokensToUse.value![0] + + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN) + + await simpleSwapService.setAccountToUse(account) + await simpleSwapService.setAmountToUse('89') + + const xrpToken = availableTokensToReceive.value!.find(({ id }) => id === 'xrp:xrp')! + + await simpleSwapService.setTokenToReceive(xrpToken) + await simpleSwapService.setAddressToReceive(process.env.TEST_XRP_ADDRESS_TO_SWAP_TOKEN) + + try { + await simpleSwapService.swap() + } catch { + /* empty */ + } + + await expect(swapSpy).rejects.toThrow() + }, 20000) + + it.skip('Should create a swap to XRP when all fields are filled with extraIdToReceive', async () => { + await simpleSwapService.init() + + const tokenUse = availableTokensToUse.value![0] + + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN) + + await simpleSwapService.setAccountToUse(account) + await simpleSwapService.setAmountToUse('89') + + const xrpToken = availableTokensToReceive.value!.find(({ id }) => id === 'xrp:xrp')! + + await simpleSwapService.setTokenToReceive(xrpToken) + await simpleSwapService.setAddressToReceive(process.env.TEST_XRP_ADDRESS_TO_SWAP_TOKEN) + await simpleSwapService.setExtraIdToReceive(process.env.TEST_XRP_EXTRA_ID_TO_SWAP_TOKEN) + + const result = await simpleSwapService.swap() + + expect(result).toEqual( + expect.objectContaining({ + id: expect.any(String), + txFrom: undefined, + log: expect.any(String), + }) + ) + }, 20000) + + it("Should return an error on create a swap to Notcoin when extraIdToReceive isn't filled", async () => { + const swapSpy = jest.spyOn(simpleSwapService, 'swap') + + await simpleSwapService.init() + + const tokenUse = availableTokensToUse.value![0] + + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN) + + await simpleSwapService.setAccountToUse(account) + await simpleSwapService.setAmountToUse('89') + + const notcoinToken = availableTokensToReceive.value!.find(({ id }) => id === 'not:ton')! + + await simpleSwapService.setTokenToReceive(notcoinToken) + await simpleSwapService.setAddressToReceive(process.env.TEST_NOTCOIN_ADDRESS_TO_SWAP_TOKEN) + + try { + await simpleSwapService.swap() + } catch { + /* empty */ + } + + await expect(swapSpy).rejects.toThrow() + }, 20000) + + it.skip('Should create a swap to Notcoin when all fields are filled with extraIdToReceive', async () => { + await simpleSwapService.init() + + const tokenUse = availableTokensToUse.value![0] + + await simpleSwapService.setTokenToUse(tokenUse) + + const account = blockchainServicesByName.neo3.generateAccountFromKey(process.env.TEST_PRIVATE_KEY_TO_SWAP_TOKEN) + + await simpleSwapService.setAccountToUse(account) + await simpleSwapService.setAmountToUse('89') + + const notcoinToken = availableTokensToReceive.value!.find(({ id }) => id === 'not:ton')! + + await simpleSwapService.setTokenToReceive(notcoinToken) + await simpleSwapService.setAddressToReceive(process.env.TEST_NOTCOIN_ADDRESS_TO_SWAP_TOKEN) + await simpleSwapService.setExtraIdToReceive(process.env.TEST_NOTCOIN_EXTRA_ID_TO_SWAP_TOKEN) + + const result = await simpleSwapService.swap() + + expect(result).toEqual( + expect.objectContaining({ + id: expect.any(String), + txFrom: undefined, + log: expect.any(String), + }) + ) + }, 20000) }) diff --git a/packages/bs-swap/src/__tests__/SimpleSwapServiceHelper.spec.ts b/packages/bs-swap/src/__tests__/SimpleSwapServiceHelper.spec.ts index bbf986d..a928e9a 100644 --- a/packages/bs-swap/src/__tests__/SimpleSwapServiceHelper.spec.ts +++ b/packages/bs-swap/src/__tests__/SimpleSwapServiceHelper.spec.ts @@ -4,7 +4,7 @@ describe('SimpleSwapServiceHelper', () => { const simpleSwapServiceHelper = new SimpleSwapServiceHelper() it('Should get the swap status by swap id', async () => { - const result = await simpleSwapServiceHelper.getStatus(process.env.TEST_SWAP_ID as string) + const result = await simpleSwapServiceHelper.getStatus(process.env.TEST_SWAP_ID) expect(result).toEqual( expect.objectContaining({ diff --git a/packages/bs-swap/src/apis/SimpleSwapApi.ts b/packages/bs-swap/src/apis/SimpleSwapApi.ts index c319e8b..0e557de 100644 --- a/packages/bs-swap/src/apis/SimpleSwapApi.ts +++ b/packages/bs-swap/src/apis/SimpleSwapApi.ts @@ -1,5 +1,6 @@ import axios, { AxiosInstance } from 'axios' import { + SimpleSwapApiCreateExchangeParams, SimpleSwapApiCreateExchangeResponse, SimpleSwapApiCurrency, SimpleSwapApiCurrencyResponse, @@ -92,6 +93,8 @@ export class SimpleSwapApi { imageUrl: currency.image, hash, decimals, + hasExtraId: currency.hasExtraId, + validationExtra: currency.validationExtra, validationAddress: currency.validationAddress, addressTemplateUrl: this.#createAddressTemplateUrl(blockchainService, currency.addressExplorer), txTemplateUrl: this.#createTxTemplateUrl(blockchainService, currency.txExplorer), @@ -194,24 +197,26 @@ export class SimpleSwapApi { } } - async createExchange( - currencyTo: SimpleSwapApiCurrency, - currencyFrom: SimpleSwapApiCurrency, - amount: string, - address: string, - refundAddress: string - ) { + async createExchange({ + currencyFrom, + currencyTo, + amount, + refundAddress, + address, + extraIdToReceive, + }: SimpleSwapApiCreateExchangeParams) { try { const { data: { result }, } = await this.#axios.post('/exchanges', { tickerFrom: currencyFrom.ticker, - tickerTo: currencyTo.ticker, networkFrom: currencyFrom.network, + tickerTo: currencyTo.ticker, networkTo: currencyTo.network, amount, - addressTo: address, userRefundAddress: refundAddress, + addressTo: address, + extraIdTo: extraIdToReceive?.trim() ?? null, }) return { diff --git a/packages/bs-swap/src/env.d.ts b/packages/bs-swap/src/env.d.ts new file mode 100644 index 0000000..e879597 --- /dev/null +++ b/packages/bs-swap/src/env.d.ts @@ -0,0 +1,16 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + TEST_PRIVATE_KEY: string + TEST_PRIVATE_KEY_TO_SWAP_TOKEN: string + TEST_ADDRESS_TO_SWAP_TOKEN: string + TEST_SWAP_ID: string + TEST_XRP_ADDRESS_TO_SWAP_TOKEN: string + TEST_XRP_EXTRA_ID_TO_SWAP_TOKEN: string + TEST_NOTCOIN_ADDRESS_TO_SWAP_TOKEN: string + TEST_NOTCOIN_EXTRA_ID_TO_SWAP_TOKEN: string + } + } +} + +export {} diff --git a/packages/bs-swap/src/services/SimpleSwapService.ts b/packages/bs-swap/src/services/SimpleSwapService.ts index 2440d4c..4e6d615 100644 --- a/packages/bs-swap/src/services/SimpleSwapService.ts +++ b/packages/bs-swap/src/services/SimpleSwapService.ts @@ -40,6 +40,7 @@ export class SimpleSwapService implements SwapSe } #internalTokenToReceive: SwapServiceLoadableValue> = { loading: false, value: null } #internalAddressToReceive: SwapServiceValidateValue = { loading: false, value: null, valid: null } + #internalExtraIdToReceive: SwapServiceValidateValue = { loading: false, value: null, valid: null } #internalAmountToReceive: SwapServiceLoadableValue = { loading: false, value: null } constructor(params: SimpleSwapServiceInitParams) { @@ -61,6 +62,7 @@ export class SimpleSwapService implements SwapSe addressTemplateUrl: token.addressTemplateUrl, txTemplateUrl: token.txTemplateUrl, network: token.network, + hasExtraId: token.hasExtraId, } } @@ -152,6 +154,18 @@ export class SimpleSwapService implements SwapSe this.eventEmitter.emit('addressToReceive', this.#internalAddressToReceive) } + get #extraIdToReceive(): SwapServiceValidateValue { + return this.#internalExtraIdToReceive + } + + set #extraIdToReceive(extraIdToReceive: Partial>) { + if (extraIdToReceive.value === '') extraIdToReceive.value = null + + this.#internalExtraIdToReceive = { ...this.#internalExtraIdToReceive, ...extraIdToReceive } + + this.eventEmitter.emit('extraIdToReceive', this.#internalExtraIdToReceive) + } + get #amountToReceive(): SwapServiceLoadableValue { return this.#internalAmountToReceive } @@ -172,6 +186,17 @@ export class SimpleSwapService implements SwapSe } } + if (this.#extraIdToReceive.value && this.#tokenToReceive.value) { + const extraIdToReceive = this.#extraIdToReceive.value.trim() + + this.#extraIdToReceive = { + valid: + !extraIdToReceive || !this.#tokenToReceive.value.validationExtra + ? true + : RegExp(this.#tokenToReceive.value.validationExtra).test(extraIdToReceive), + } + } + if (this.#accountToUse.value) { this.#accountToUse = { valid: this.#tokenToUse.value.blockchain === this.#accountToUse.value.blockchain } } @@ -206,6 +231,7 @@ export class SimpleSwapService implements SwapSe this.#amountToUseMinMax = { value: null } this.#amountToReceive = { value: null } this.#addressToReceive = { value: null, valid: null } + this.#extraIdToReceive = { value: null, valid: null } throw error } } @@ -343,6 +369,7 @@ export class SimpleSwapService implements SwapSe } async setTokenToReceive(token: SwapServiceToken | null): Promise { + this.#extraIdToReceive = { value: null, valid: null } this.#amountToReceive = { loading: false, value: null } this.#amountToUseMinMax = { loading: false, value: null } @@ -367,6 +394,15 @@ export class SimpleSwapService implements SwapSe loading: false, value: address, } + + await this.#recalculateValues([]) + } + + async setExtraIdToReceive(extraIdToReceive: string | null): Promise { + if (!this.#tokenToReceive.value?.hasExtraId) return + + this.#extraIdToReceive = { value: extraIdToReceive, valid: null } + await this.#recalculateValues([]) } @@ -379,7 +415,9 @@ export class SimpleSwapService implements SwapSe !this.#addressToReceive.valid || !this.#amountToUse.value || !this.#amountToReceive.value || - !this.#tokenToUse.value.hash + !this.#tokenToUse.value.hash || + (this.#tokenToReceive.value.hasExtraId && + (!this.#extraIdToReceive.valid || !this.#extraIdToReceive.value?.trim())) ) { throw new Error('Not all required fields are set') } @@ -391,13 +429,14 @@ export class SimpleSwapService implements SwapSe } try { - const { depositAddress, id, log } = await this.#api.createExchange( - this.#tokenToReceive.value, - this.#tokenToUse.value, - this.#amountToUse.value, - this.#addressToReceive.value, - this.#accountToUse.value.address - ) + const { depositAddress, id, log } = await this.#api.createExchange({ + currencyFrom: this.#tokenToUse.value, + currencyTo: this.#tokenToReceive.value, + amount: this.#amountToUse.value, + refundAddress: this.#accountToUse.value.address, + address: this.#addressToReceive.value, + extraIdToReceive: this.#extraIdToReceive.value, + }) result.id = id result.log = log diff --git a/packages/bs-swap/src/types/simpleSwap.ts b/packages/bs-swap/src/types/simpleSwap.ts index e576d45..ab5d7f6 100644 --- a/packages/bs-swap/src/types/simpleSwap.ts +++ b/packages/bs-swap/src/types/simpleSwap.ts @@ -8,13 +8,26 @@ export type SimpleSwapServiceInitParams = { export type SimpleSwapApiCurrency = SwapServiceToken & { network: string ticker: string + hasExtraId: boolean + validationExtra: string | null validationAddress: string } +export type SimpleSwapApiCreateExchangeParams = { + currencyFrom: SimpleSwapApiCurrency + currencyTo: SimpleSwapApiCurrency + amount: string + refundAddress: string + address: string + extraIdToReceive: string | null +} + export type SimpleSwapApiCurrencyResponse = { name: string | null ticker: string | null network: string | null + hasExtraId: boolean + validationExtra: string | null validationAddress: string | null image: string | null contractAddress: string | null