diff --git a/.gitignore b/.gitignore index 5fc35f30..49eeeae7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ config.js node_modules/ coverage +coverage-integration .DS_Store diff --git a/__tests__/integration/address-info.test.js b/__tests__/integration/address-info.test.js index bbd90cfb..7b55a7f4 100644 --- a/__tests__/integration/address-info.test.js +++ b/__tests__/integration/address-info.test.js @@ -16,12 +16,11 @@ describe('address-info routes', () => { try { // A random HTR value for the first wallet wallet1 = new WalletHelper('addinfo-1'); - await wallet1.start(); - await wallet1.injectFunds(address1balance, 1); - // A fixed custom token amount for the second wallet wallet2 = new WalletHelper('addinfo-2'); - await wallet2.start(); + + await WalletHelper.startMultipleWalletsForTest([wallet1, wallet2]); + await wallet1.injectFunds(address1balance, 1); await wallet2.injectFunds(10); const customToken = await wallet2.createToken({ amount: 500, @@ -32,6 +31,8 @@ describe('address-info routes', () => { }); customTokenHash = customToken.hash; + await TestUtils.pauseForWsUpdate(); + /* * The state here should be: * wallet1[1] with some value between 100 and 200 HTR diff --git a/__tests__/integration/balance.test.js b/__tests__/integration/balance.test.js index 0adb7af0..5286a920 100644 --- a/__tests__/integration/balance.test.js +++ b/__tests__/integration/balance.test.js @@ -12,17 +12,16 @@ describe('balance routes', () => { try { // First wallet, no balance wallet1 = new WalletHelper('balance1'); - await wallet1.start(); - // Second wallet, random balance wallet2 = new WalletHelper('balance2'); - await wallet2.start(); - await wallet2.injectFunds(wallet2Balance); - // Third wallet, balance to be used for custom tokens wallet3 = new WalletHelper('custom3'); - await wallet3.start(); + + await WalletHelper.startMultipleWalletsForTest([wallet1, wallet2, wallet3]); + await wallet2.injectFunds(wallet2Balance); await wallet3.injectFunds(100); + + await TestUtils.pauseForWsUpdate(); } catch (err) { TestUtils.logError(err.stack); } @@ -90,6 +89,8 @@ describe('balance routes', () => { }); const tokenHash = newToken.hash; + await TestUtils.pauseForWsUpdate(); + const balanceResult = await TestUtils.request .get('/wallet/balance') .query({ token: tokenHash }) diff --git a/__tests__/integration/configuration/config.js.template b/__tests__/integration/configuration/config.js.template index e69145ae..38f59078 100644 --- a/__tests__/integration/configuration/config.js.template +++ b/__tests__/integration/configuration/config.js.template @@ -46,11 +46,6 @@ module.exports = { }, */ - // Integration test transaction logging - integrationTestLog: { - outputFolder: 'tmp/', // Should match .github/workflows/integration-test.yml -> upload-artifact - }, - // Optional config so you can set the token you want to use in this wallet // If this parameter is set you don't need to pass your token when getting balance or sending tokens tokenUid: '', diff --git a/__tests__/integration/configuration/test.config.js b/__tests__/integration/configuration/test.config.js new file mode 100644 index 00000000..795b6a09 --- /dev/null +++ b/__tests__/integration/configuration/test.config.js @@ -0,0 +1,18 @@ +/* + * This file contains the configurations specific for the integration tests on the Wallet Headless. + * Those values are also editable via envrionment variables + */ + +module.exports = { + // On CI, should match .github/workflows/integration-test.yml -> upload-artifact + logOutputFolder: process.env.TEST_LOG_OUTPUT_FOLDER || 'tmp/', + + // Console level used on winston + consoleLevel: process.env.TEST_CONSOLE_LEVEL || 'silly', + + // Defines how long tests should wait before consulting balances after transactions + wsUpdateDelay: process.env.TEST_WS_UPDATE_DELAY || 1000, + + // Defines for how long the startMultipleWalletsForTest can run + walletStartTimeout: process.env.TEST_WALLET_START_TIMEOUT || 60000, +}; diff --git a/__tests__/integration/create-token.test.js b/__tests__/integration/create-token.test.js new file mode 100644 index 00000000..c29f0235 --- /dev/null +++ b/__tests__/integration/create-token.test.js @@ -0,0 +1,275 @@ +import { getRandomInt, TestUtils, WALLET_CONSTANTS } from './utils/test-utils-integration'; +import { WalletHelper } from './utils/wallet-helper'; + +describe('create token', () => { + let wallet1; + let wallet2; + + const tokenA = { + name: 'Token A', + symbol: 'TKA', + uid: null + }; + + beforeAll(async () => { + wallet1 = new WalletHelper('create-token-1'); + wallet2 = new WalletHelper('create-token-2'); + + await WalletHelper.startMultipleWalletsForTest([wallet1, wallet2]); + await wallet1.injectFunds(10, 0); + await wallet1.injectFunds(10, 1); + await wallet2.injectFunds(10, 0); + }); + + afterAll(async () => { + await wallet1.stop(); + await wallet2.stop(); + }); + + // Testing failures first, that do not cause side-effects on the blockchain + + it('should reject missing name parameter', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + symbol: tokenA.symbol, + amount: 1000 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + done(); + }); + + it('should reject missing symbol parameter', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: tokenA.name, + amount: 1000 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + done(); + }); + + it.skip('should reject a name with more than 30 characters', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: 'Name input with more than 30 characters', + symbol: tokenA.symbol, + amount: 2000 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('maximum size'); + done(); + }); + + // The result is an error with the message "maximum size", but consumes the funds. Must be fixed. + it.skip('should reject a symbol with more than 5 characters', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: tokenA.name, + symbol: 'TKABCD', + amount: 2000 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('maximum size'); + done(); + }); + + it('should reject an invalid destination address', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: tokenA.name, + symbol: tokenA.symbol, + amount: 1000, + address: 'invalidAddress' + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('base58'); + done(); + }); + + it('should reject an invalid change address', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: tokenA.name, + symbol: tokenA.symbol, + amount: 500, + address: await wallet1.getAddressAt(0), + change_address: 'invalidAddress' + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('Change address'); + done(); + }); + + // The application is incorrectly allowing external addresses to receive the change + it.skip('should reject creating token for change address not in the wallet', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: tokenA.name, + symbol: tokenA.symbol, + amount: 500, + address: await wallet1.getAddressAt(0), + change_address: WALLET_CONSTANTS.genesis.addresses[3] + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.text).toContain('wallet'); + done(); + }); + + // insufficient funds + + it('should reject for insufficient funds', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: tokenA.name, + symbol: tokenA.symbol, + amount: 3000 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + it('should not create a token with the reserved HTR symbol', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: 'Hathor', + symbol: 'HTR', + amount: 100 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + expect(response.body.error).toContain('Invalid token name'); + done(); + }); + + it('should create a token with only required parameters', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: tokenA.name, + symbol: tokenA.symbol, + amount: 100 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(true); + expect(response.body.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const htrBalance = await wallet1.getBalance(); + const tkaBalance = await wallet1.getBalance(response.body.hash); + expect(htrBalance.available).toBe(19); // The initial 20 minus 1 + expect(tkaBalance.available).toBe(100); // The newly minted TKA tokens + done(); + }); + + it('should send the created tokens to the correct address', async done => { + const amountTokens = getRandomInt(100, 200); + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: 'Token B', + symbol: 'TKB', + amount: amountTokens, + address: await wallet1.getAddressAt(9) + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + const transaction = response.body; + expect(transaction.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const addr9 = await wallet1.getAddressInfo(9, transaction.hash); + expect(addr9.total_amount_received).toBe(amountTokens); + done(); + }); + + it('should send the change to the correct address', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: 'Token C', + symbol: 'TKC', + amount: 100, + change_address: await wallet2.getAddressAt(5) + }) + .set({ 'x-wallet-id': wallet2.walletId }); + + const transaction = response.body; + expect(transaction.success).toBe(true); + + // The only output with token_data equals zero is the one containing the HTR change + const htrOutputIndex = transaction.outputs.findIndex(o => o.token_data === 0); + const htrChange = transaction.outputs[htrOutputIndex].value; + + await TestUtils.pauseForWsUpdate(); + + const addr5 = await wallet2.getAddressInfo(5); + expect(addr5.total_amount_received).toBe(htrChange); + done(); + }); + + it('should create a token with all available inputs', async done => { + const response = await TestUtils.request + .post('/wallet/create-token') + .send({ + name: 'Token D', + symbol: 'TKD', + amount: 200, + address: await wallet2.getAddressAt(4), + change_address: await wallet2.getAddressAt(4) + }) + .set({ 'x-wallet-id': wallet2.walletId }); + + const transaction = response.body; + expect(transaction.success).toBe(true); + + // The only output with token_data equals zero is the one containing the HTR change + const htrOutputIndex = transaction.outputs.findIndex(o => o.token_data === 0); + const htrChange = transaction.outputs[htrOutputIndex].value; + + await TestUtils.pauseForWsUpdate(); + + const addr4 = await wallet2.getAddressInfo(4); + expect(addr4.total_amount_received).toBe(htrChange); + const addr4C = await wallet2.getAddressInfo(4, transaction.hash); + expect(addr4C.total_amount_available).toBe(200); + done(); + }); +}); diff --git a/__tests__/integration/docker-compose.yml b/__tests__/integration/docker-compose.yml index 4b86166e..72a654e5 100644 --- a/__tests__/integration/docker-compose.yml +++ b/__tests__/integration/docker-compose.yml @@ -33,7 +33,7 @@ services: depends_on: - fullnode ports: - - "8034:8034" # Not mandatory to keep these ports open, but helpful for developer machine debugging + - "8034:8034" # Not mandatory to keep this port open, but helpful for developer machine debugging - "8035:8035" command: [ "http://fullnode:8080", diff --git a/__tests__/integration/melt-tokens.test.js b/__tests__/integration/melt-tokens.test.js new file mode 100644 index 00000000..2b6ba7cf --- /dev/null +++ b/__tests__/integration/melt-tokens.test.js @@ -0,0 +1,322 @@ +import { TestUtils, WALLET_CONSTANTS } from './utils/test-utils-integration'; +import { WalletHelper } from './utils/wallet-helper'; + +describe('melt tokens', () => { + let wallet1; + const tokenA = { + name: 'Token A', + symbol: 'TKA', + uid: null + }; + + beforeAll(async () => { + wallet1 = new WalletHelper('melt-token-1'); + + // Starting the wallets + await WalletHelper.startMultipleWalletsForTest([wallet1]); + + // Creating a token for the tests + await wallet1.injectFunds(10, 0); + const tkAtx = await wallet1.createToken({ + name: tokenA.name, + symbol: tokenA.symbol, + amount: 800, + address: await wallet1.getAddressAt(0), + change_address: await wallet1.getAddressAt(0) + }); + tokenA.uid = tkAtx.hash; + + /** + * Status: + * wallet1[0]: 2 HTR , 800 TKA + */ + }); + + afterAll(async () => { + await wallet1.stop(); + }); + + // Testing failures first, that do not cause side-effects on the blockchain + + it('should not melt an invalid token', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: 'invalidToken', + amount: 100 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + + // TODO: Even though the result is correct, the error thrown is not related. + // expect(response.body.error).toContain('invalid'); + done(); + }); + + it('should not melt with an invalid amount', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: 'invalidAmount' + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.text).toContain('invalid'); + done(); + }); + + it('should not melt with zero amount', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: 0 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.text).toContain('amount'); + done(); + }); + + it('should not melt with a negative amount', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: -1 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.text).toContain('amount'); + done(); + }); + + it('should not melt with an invalid deposit_address', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + deposit_address: 'invalidAddress', + amount: 200 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.text).toContain('invalid'); + done(); + }); + + it('should not melt with an invalid change_address', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: 200, + change_address: 'invalidAddress' + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.text).toContain('invalid'); + done(); + }); + + // The application is incorrectly allowing a change address outside the wallet + it.skip('should not melt with a change_address outside the wallet', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: 200, + change_address: WALLET_CONSTANTS.genesis.addresses[4] + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.text).toContain('invalid'); + done(); + }); + + // Insufficient funds + + it('should not melt with insufficient tokens', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + address: await wallet1.getAddressAt(1), + amount: 1000 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('enough inputs to melt'); + done(); + }); + + // Success + + it('should melt with address and change address', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: 300, + deposit_address: await wallet1.getAddressAt(3), + change_address: await wallet1.getAddressAt(4), + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const addr3htr = await wallet1.getAddressInfo(3); + const addr3tka = await wallet1.getAddressInfo(3, tokenA.uid); + expect(addr3htr.total_amount_available).toBe(3); + expect(addr3tka.total_amount_available).toBe(0); + + const addr4htr = await wallet1.getAddressInfo(4); + const addr4tka = await wallet1.getAddressInfo(4, tokenA.uid); + expect(addr4htr.total_amount_available).toBe(0); + expect(addr4tka.total_amount_available).toBe(500); + + const balance1htr = await wallet1.getBalance(); + const balance1tka = await wallet1.getBalance(tokenA.uid); + expect(balance1htr.available).toBe(2 + 3); + expect(balance1tka.available).toBe(800 - 300); + + done(); + }); + + it('should melt with deposit address only', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: 100, + deposit_address: await wallet1.getAddressAt(5), + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const addr5htr = await wallet1.getAddressInfo(5); + const addr5tka = await wallet1.getAddressInfo(5, tokenA.uid); + expect(addr5htr.total_amount_available).toBe(1); + expect(addr5tka.total_amount_available).toBe(0); + + const balance1htr = await wallet1.getBalance(); + const balance1tka = await wallet1.getBalance(tokenA.uid); + expect(balance1htr.available).toBe(2 + 3 + 1); + expect(balance1tka.available).toBe(800 - 300 - 100); + + done(); + }); + + it('should melt with change address only', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: 100, + change_address: await wallet1.getAddressAt(7), + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const addr7htr = await wallet1.getAddressInfo(7); + const addr7tka = await wallet1.getAddressInfo(7, tokenA.uid); + expect(addr7htr.total_amount_available).toBe(0); + expect(addr7tka.total_amount_available).toBe(300); + + const balance1htr = await wallet1.getBalance(); + const balance1tka = await wallet1.getBalance(tokenA.uid); + expect(balance1htr.available).toBe(2 + 3 + 1 + 1); + expect(balance1tka.available).toBe(800 - 300 - 100 - 100); + + done(); + }); + + it('should melt with mandatory parameters', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: 100 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const balance1htr = await wallet1.getBalance(); + const balance1tka = await wallet1.getBalance(tokenA.uid); + expect(balance1htr.available).toBe(2 + 3 + 1 + 1 + 1); // 8 + expect(balance1tka.available).toBe(800 - 300 - 100 - 100 - 100); // 200 + + done(); + }); + + it('should not retrieve funds when melting below 100 tokens', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + amount: 50 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const balance1htr = await wallet1.getBalance(); + const balance1tka = await wallet1.getBalance(tokenA.uid); + expect(balance1htr.available).toBe(8); + expect(balance1tka.available).toBe(150); + + done(); + }); + + it('should retrieve funds rounded down when not melting multiples of 100', async done => { + const response = await TestUtils.request + .post('/wallet/melt-tokens') + .send({ + token: tokenA.uid, + address: await wallet1.getAddressAt(1), + amount: 150 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const balance1htr = await wallet1.getBalance(); + const balance1tka = await wallet1.getBalance(tokenA.uid); + expect(balance1htr.available).toBe(9); + expect(balance1tka.available).toBe(0); + + done(); + }); +}); diff --git a/__tests__/integration/mint-tokens.test.js b/__tests__/integration/mint-tokens.test.js new file mode 100644 index 00000000..e2eeafc3 --- /dev/null +++ b/__tests__/integration/mint-tokens.test.js @@ -0,0 +1,235 @@ +import { TestUtils, WALLET_CONSTANTS } from './utils/test-utils-integration'; +import { WalletHelper } from './utils/wallet-helper'; + +describe('mint token', () => { + let wallet1; + const tokenA = { + name: 'Token A', + symbol: 'TKA', + uid: null + }; + + beforeAll(async () => { + wallet1 = new WalletHelper('mint-token-1'); + + // Starting the wallets + await WalletHelper.startMultipleWalletsForTest([wallet1]); + + // Creating a token for the tests + await wallet1.injectFunds(10, 0); + const tkAtx = await wallet1.createToken({ + name: tokenA.name, + symbol: tokenA.symbol, + amount: 500, + address: await wallet1.getAddressAt(0), + change_address: await wallet1.getAddressAt(0) + }); + tokenA.uid = tkAtx.hash; + }); + + afterAll(async () => { + await wallet1.stop(); + }); + + // Testing failures first, that do not cause side-effects on the blockchain + + it('should not mint an invalid token', async done => { + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: 'invalidToken', + amount: 100 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + + // TODO: Even though the result is correct, the error thrown is not related. + // expect(response.body.message).toContain('invalid'); + done(); + }); + + it('should not mint with an invalid address', async done => { + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: tokenA.uid, + address: 'invalidAddress', + amount: 100 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('base58'); + done(); + }); + + it('should not mint with an invalid change address', async done => { + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: tokenA.uid, + change_address: 'invalidAddress', + amount: 100 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('Change address'); + done(); + }); + + it('should not mint with an invalid amount', async done => { + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: tokenA.uid, + amount: 'invalidVamount' + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.text).toContain('amount'); + done(); + }); + + // The application is allowing a change_address outside the wallet + it.skip('should not mint with change_address outside the wallet', async done => { + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: tokenA.uid, + change_address: WALLET_CONSTANTS.genesis.addresses[3], + amount: 100 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('Change address'); + done(); + }); + + // Insufficient funds + + it('should not mint with insufficient funds', async done => { + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: tokenA.uid, + amount: 1000 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('HTR funds'); + done(); + }); + + // Success + + it('should mint with destination address', async done => { + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: tokenA.uid, + address: await wallet1.getAddressAt(1), + amount: 50 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const addr1 = await wallet1.getAddressInfo(1, tokenA.uid); + expect(addr1.total_amount_available).toBe(50); + + done(); + }); + + it('should mint with a change address', async done => { + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: tokenA.uid, + change_address: await wallet1.getAddressAt(10), // Index 10 is supposed to be not used yet + amount: 60 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + const transaction = response.body; + expect(transaction.success).toBe(true); + const htrOutputIndex = transaction.outputs.findIndex(o => o.token_data === 0); + const htrChange = transaction.outputs[htrOutputIndex].value; + + await TestUtils.pauseForWsUpdate(); + + const addr10 = await wallet1.getAddressInfo(10); + expect(addr10.total_amount_received).toBe(htrChange); + + const tkaBalance = await wallet1.getBalance(tokenA.uid); + expect(tkaBalance.available).toBe(500 + 50 + 60); + done(); + }); + + it('should mint with only mandatory parameters', async done => { + const destinationAddress = await wallet1.getNextAddress(); + + // By default, will mint tokens into the next unused address + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: tokenA.uid, + amount: 70 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const addrNew = await TestUtils.getAddressInfo( + destinationAddress, + wallet1.walletId, + tokenA.uid + ); + expect(addrNew.total_amount_available).toBe(70); + + const tkaBalance = await wallet1.getBalance(tokenA.uid); + expect(tkaBalance.available).toBe(500 + 50 + 60 + 70); + done(); + }); + + it('should mint with all parameters', async done => { + // By default, will mint tokens into the next unused address + const response = await TestUtils.request + .post('/wallet/mint-tokens') + .send({ + token: tokenA.uid, + address: await wallet1.getAddressAt(15), + change_address: await wallet1.getAddressAt(14), + amount: 80 + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + const transaction = response.body; + expect(transaction.success).toBe(true); + const htrOutputIndex = transaction.outputs.findIndex(o => o.token_data === 0); + const htrChange = transaction.outputs[htrOutputIndex].value; + + await TestUtils.pauseForWsUpdate(); + + const addr15 = await wallet1.getAddressInfo(15, tokenA.uid); + expect(addr15.total_amount_available).toBe(80); + + const addr14 = await wallet1.getAddressInfo(14); + expect(addr14.total_amount_available).toBe(htrChange); + done(); + }); +}); diff --git a/__tests__/integration/send-tx.test.js b/__tests__/integration/send-tx.test.js new file mode 100644 index 00000000..5526c250 --- /dev/null +++ b/__tests__/integration/send-tx.test.js @@ -0,0 +1,1361 @@ +import { TestUtils } from './utils/test-utils-integration'; +import { WalletHelper } from './utils/wallet-helper'; + +describe('send tx (HTR)', () => { + let wallet1; // Receives funds + let wallet2; // Main destination for test transactions + let wallet3; // For transactions with more than one input + + const fundTx1 = { + hash: null, + index: null + }; // Fund for auto-input transactions + const fundTx2 = { + hash: null, + index: null + }; // Fund for manual input transactions + const fundTx3 = { + hash: null, + index: null + }; // Two funds for multi-input transactions + const fundTx4 = { + hash: null, + index: null + }; + const tx5 = { + hash: null, + index: null + }; // This will be executed on a multiple input test + + beforeAll(async () => { + try { + wallet1 = new WalletHelper('send-tx-1'); + wallet2 = new WalletHelper('send-tx-2'); + wallet3 = new WalletHelper('send-tx-3'); + + await WalletHelper.startMultipleWalletsForTest([wallet1, wallet2, wallet3]); + + // Funds for single input/output tests + const fundTxObj1 = await wallet1.injectFunds(1000, 0); + // Funds for multiple input/output tests + const fundTxObj2 = await wallet3.injectFunds(1000, 0); + const fundTxObj3 = await wallet3.injectFunds(1000, 1); + const fundTxObj4 = await wallet3.injectFunds(1000, 4); + + fundTx1.hash = fundTxObj1.hash; + fundTx1.index = TestUtils.getOutputIndexFromTx(fundTxObj1, 1000); + fundTx2.hash = fundTxObj2.hash; + fundTx2.index = TestUtils.getOutputIndexFromTx(fundTxObj2, 1000); + fundTx3.hash = fundTxObj3.hash; + fundTx3.index = TestUtils.getOutputIndexFromTx(fundTxObj3, 1000); + fundTx4.hash = fundTxObj4.hash; + fundTx4.index = TestUtils.getOutputIndexFromTx(fundTxObj4, 1000); + + // Awaiting for updated balances to be received by the websocket + await TestUtils.pauseForWsUpdate(); + } catch (err) { + TestUtils.logError(err.stack); + } + }); + + afterAll(async () => { + await wallet1.stop(); + await wallet2.stop(); + await wallet3.stop(); + }); + + // Starting with all the rejection tests, that do not have side-effects + + // Invalid inputs + it('should reject an invalid address', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: 'invalidAddress', + value: 10 + }], + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + done(); + }); + + it('should reject an invalid filterAddress input', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [{ + type: 'query', + filter_address: 'invalidAddress' + }], + outputs: [{ + address: await wallet1.getAddressAt(5), + value: 10 + }], + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + done(); + }); + + it('should reject an invalid change address', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + change_address: 'invalidAddress', + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('invalid'); + done(); + }); + + it('should reject an invalid input', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [{ + hash: 'invalidInput', + index: 0 + }], + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + change_address: 'invalidAddress', + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('invalid'); + done(); + }); + + it('should reject an invalid input, even with a correct one', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [ + fundTx1, + { + hash: 'invalidInput', + index: 0 + } + ], + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + change_address: 'invalidAddress', + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('invalid'); + done(); + }); + + it('should reject a change address that does not belong to the wallet', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + change_address: wallet2.getAddressAt(1), + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + const errorElement = response.body.error[0]; + expect(errorElement.param).toBe('change_address'); + expect(errorElement.msg).toContain('Invalid'); + done(); + }); + + it('should reject an invalid value', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 'incorrectValue' + }], + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + expect(response.body).toHaveProperty('error'); + expect(response.body.error[0].msg).toContain('Invalid'); + done(); + }); + + it('should reject zero value', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 0 + }], + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.text).toContain('Invalid'); + expect(response.text).toContain('value'); + done(); + }); + + it('should reject a negative value', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: -1 + }], + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.text).toContain('Invalid'); + expect(response.text).toContain('value'); + done(); + }); + + // insufficient funds + it('should reject for insufficient funds', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + }) + .set({ 'x-wallet-id': wallet2.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + done(); + }); + + it('should reject for insufficient funds with two outputs', async done => { + // Both outputs are below the 1000 HTR available + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [ + { + address: await wallet2.getAddressAt(1), + value: 800 + }, + { + address: await wallet2.getAddressAt(2), + value: 800 + }, + ], + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + done(); + }); + + it('should reject for insufficient funds with two inputs', async done => { + // Both inputs are have only 2000 HTR + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [fundTx2, fundTx3], + outputs: [ + { + address: await wallet2.getAddressAt(1), + value: 3000 + }, + ], + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + done(); + }); + + it('should reject for insufficient funds on queryAddress', async done => { + // Wallet1 has enough funds, but none of them are on index 5 + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [{ + type: 'query', + filter_address: await wallet1.getAddressAt(5) + }], + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + done(); + }); + + it('should reject for insufficient funds on input', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [fundTx1], + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 1001 + }], + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.hash).toBeUndefined(); + expect(response.body.success).toBe(false); + done(); + }); + + it('should reject for an invalid input', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [{ hash: fundTx1.hash, index: -1 }], + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 500 + }], + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.text).toContain('Invalid'); + expect(response.text).toContain('input'); + done(); + }); + + // Lastly, testing success cases, which have side-effects + + it('should send with only the output address and value', async done => { + const tx = await wallet1.sendTx({ + fullObject: { + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + } + }); + + expect(tx.hash).toBeDefined(); + expect(tx.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const destination0 = await wallet2.getAddressInfo(0); + expect(destination0.total_amount_available).toBe(10); + + done(); + }); + + it('should send with only the output address and value and change', async done => { + const tx = await wallet1.sendTx({ + fullObject: { + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + change_address: await wallet1.getAddressAt(0) + } + }); + + expect(tx.hash).toBeDefined(); + expect(tx.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const destination = await wallet2.getAddressInfo(0); + expect(destination.total_amount_available).toBe(20); + + const changeAddr = await wallet1.getAddressInfo(0); + const txSummary = TestUtils.getOutputSummaryHtr(tx, 10); + expect(changeAddr.total_amount_available).toBe(txSummary.change.value); + + done(); + }); + + it('should send with only the filterAddress', async done => { + const inputAddrBefore = await wallet2.getAddressInfo(0); + const destinationAddrBefore = await wallet1.getAddressInfo(0); + const sourceBeforeTx = inputAddrBefore.total_amount_available; + const destinationBeforeTx = destinationAddrBefore.total_amount_available; + + const tx = await wallet2.sendTx({ + fullObject: { + inputs: [{ + type: 'query', + filter_address: await wallet2.getAddressAt(0) + }], + outputs: [{ + address: await wallet1.getAddressAt(0), + value: 20 + }], + } + }); + + expect(tx.hash).toBeDefined(); + expect(tx.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + + const inputAddrAfter = await wallet2.getAddressInfo(0); + const destinationAddrAfter = await wallet1.getAddressInfo(0); + expect(inputAddrAfter.total_amount_available).toBe(sourceBeforeTx - 20); + expect(destinationAddrAfter.total_amount_available).toBe(destinationBeforeTx + 20); + + done(); + }); + + it('should send with two outputs', async done => { + const destination1Before = await wallet2.getAddressInfo(1); + const destination2Before = await wallet1.getAddressInfo(2); + + const tx = await wallet1.sendTx({ + fullObject: { + outputs: [ + { + address: await wallet2.getAddressAt(1), + value: 20 + }, + { + address: await wallet2.getAddressAt(2), + value: 30 + }, + ], + } + }); + + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const destination1After = await wallet2.getAddressInfo(1); + const destination2After = await wallet2.getAddressInfo(2); + expect(destination1After.total_amount_available) + .toBe(destination1Before.total_amount_available + 20); + expect(destination2After.total_amount_available) + .toBe(destination2Before.total_amount_available + 30); + done(); + }); + + it('should send with two inputs', async done => { + const tx = await wallet3.sendTx({ + fullObject: { + inputs: [ + fundTx2, + fundTx3 + ], + outputs: [ + { + address: await wallet2.getAddressAt(6), + value: 1500 + }, + ], + change_address: await wallet3.getAddressAt(2) + } + }); + + expect(tx.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const destination = await wallet2.getAddressInfo(6); + const changeAddr = await wallet3.getAddressInfo(2); + + expect(destination.total_amount_received).toBe(1500); + expect(changeAddr.total_amount_received).toBe(500); + + tx5.hash = tx.hash; + tx5.index = TestUtils.getOutputIndexFromTx(tx, 500); + done(); + }); + + it('should send with correct input', async done => { + // Injecting 2000 HTR on wallet2[3], to ensure the funds would not be available otherwise + const fundTxObj = await wallet2.injectFunds(2000, 3); + const fundTxInput = { + hash: fundTxObj.hash, + index: TestUtils.getOutputIndexFromTx(fundTxObj, 2000) + }; + + // The change address should be the next available address on wallet2 + const changeAddrHash = await wallet2.getNextAddress(); + + const tx = await wallet2.sendTx({ + fullObject: { + inputs: [fundTxInput], + outputs: [{ + address: await wallet1.getAddressAt(4), + value: 1100 + }], + } + }); + + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const sourceAddress = await wallet2.getAddressInfo(3); + expect(sourceAddress.total_amount_received).toBe(2000); + expect(sourceAddress.total_amount_sent).toBe(2000); + + const destination = await wallet1.getAddressInfo(4); + expect(destination.total_amount_received).toBe(1100); + + const changeAddr = await TestUtils.getAddressInfo(changeAddrHash, wallet2.walletId); + expect(changeAddr.total_amount_available).toBe(900); + + done(); + }); + + it('should send with zero change even with change address', async done => { + // This test depends on the above transaction of 1100 from wallet2[3] to wallet1[4] + const tx = await wallet2.sendTx({ + fullObject: { + outputs: [{ + address: await wallet1.getAddressAt(4), + value: 900 + }], + change_address: await wallet2.getAddressAt(5) + } + }); + + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const addr5 = await wallet2.getAddressInfo(5); + expect(addr5.total_amount_received).toBe(0); + + done(); + }); + + it('should send with two inputs and two outputs', async done => { + const tx = await wallet3.sendTx({ + fullObject: { + inputs: [ + fundTx4, + tx5 + ], + outputs: [ + { + address: await wallet2.getAddressAt(10), + value: 760 + }, + { + address: await wallet2.getAddressAt(11), + value: 740 + }, + ], + } + }); + + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const addr7 = await wallet2.getAddressInfo(10); + expect(addr7.total_amount_received).toBe(760); + + const addr8 = await wallet2.getAddressInfo(11); + expect(addr8.total_amount_received).toBe(740); + done(); + }); + + it( + 'should confirm that, if not informed, the change address is the next empty one', + async done => { + const nextAddressHash = await wallet1.getNextAddress(); + + const tx = await wallet1.sendTx({ + fullObject: { + inputs: [{ type: 'query', address: await wallet1.getAddressAt(4) }], + outputs: [{ address: await wallet2.getAddressAt(5), value: 100 }] + } + }); + const txSummary = TestUtils.getOutputSummaryHtr(tx, 100); + + await TestUtils.pauseForWsUpdate(); + + const destination = await wallet2.getAddressInfo(5); + const changeAddr = await TestUtils.getAddressInfo(nextAddressHash, wallet1.walletId); + expect(destination.total_amount_available).toBe(100); + expect(changeAddr.total_amount_available).toBe(txSummary.change.value); + + done(); + } + ); +}); + +describe('send tx (custom tokens)', () => { + let wallet1; // Auto-input funds + let wallet2; // Destination + let wallet3; // More than one token in the same transaction + let wallet4; // Tests using token key inside output + let wallet5; + + const tokenA = { + name: 'Token A', + symbol: 'TKA', + uid: null + }; + const tokenB = { + name: 'Token B', + symbol: 'TKB', + uid: null + }; + const tokenWallet4 = { + uid: null + }; + + const fundTx1 = { + hash: null, + index: null + }; // Auto-input transactions + const fundTx2 = { + hash: null, + index: null + }; // Token B transactions + const tkaTx1 = { hash: null }; // Token A transaction to have two inputs + + beforeAll(async () => { + wallet1 = new WalletHelper('custom-tx-1'); + wallet2 = new WalletHelper('custom-tx-2'); + wallet3 = new WalletHelper('custom-tx-3'); + wallet4 = new WalletHelper('custom-tx-4'); + wallet5 = new WalletHelper('custom-tx-5'); + + await WalletHelper.startMultipleWalletsForTest([wallet1, wallet2, wallet3, wallet4, wallet5]); + + // Funds for single input/output tests - 1000 HTR + 2000 custom A + await wallet1.injectFunds(1020, 0); + // Funds for multiple token tests - 990 HTR + 1000 custom B + await wallet3.injectFunds(1000, 0); + const tokenCreationA = await wallet1.createToken({ + name: tokenA.name, + symbol: tokenA.symbol, + amount: 2000, + address: await wallet1.getAddressAt(0), + change_address: await wallet1.getAddressAt(0) + }); + tokenA.uid = tokenCreationA.hash; + const tokenAtransfer = await wallet1.sendTx({ + outputs: [ + { + address: await wallet1.getAddressAt(0), + value: 1000, + token: tokenA.uid + }, + { + address: await wallet1.getAddressAt(1), + value: 1000, + token: tokenA.uid + }, + ], + }); + tkaTx1.hash = tokenAtransfer.hash; + fundTx1.hash = tokenCreationA.hash; + fundTx1.index = TestUtils.getOutputIndexFromTx(tokenCreationA, 1000); + + const tokenCreationB = await wallet3.createToken({ + name: tokenB.name, + symbol: tokenB.symbol, + amount: 1000, + address: await wallet3.getAddressAt(0), + change_address: await wallet3.getAddressAt(0) + }); + tokenB.uid = tokenCreationB.hash; + fundTx2.hash = tokenCreationB.hash; + fundTx2.index = TestUtils.getOutputIndexFromTx(tokenCreationB, 990); + }); + + afterAll(async () => { + await wallet1.stop(); + await wallet2.stop(); + await wallet3.stop(); + await wallet4.stop(); + await wallet5.stop(); + }); + + // Starting with all the rejection tests, that do not have side-effects + + // Invalid inputs + it('should reject an invalid input hash on body', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + token: { + name: tokenA.name, + symbol: tokenA.symbol, + uid: 'invalidHash' + } + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + it.skip('should reject an invalid input name on body', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + token: { + name: 'invalidName', + symbol: tokenA.symbol, + uid: tokenA.uid + } + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + // Currently ignoring the wrong name. To be fixed later + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + it('should reject an invalid input on a multi-input request', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [ + { + hash: tkaTx1, + index: 0 + }, + { + hash: 'invalidInput', + index: 5 + }, + ], + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + token: tokenA + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + it.skip('should reject an invalid input symbol on body', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 10 + }], + token: { + name: tokenA.name, + symbol: 'invalidSymbol', + uid: tokenA.uid + } + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + // Currently ignoring the wrong symbol. To be fixed later + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + // insufficient funds + it('should reject a transaction with insufficient funds', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 3000 + }], + token: tokenA + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + it('should reject a single-input transaction with insufficient funds', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [ + { + hash: tkaTx1, + index: 0 + }, + ], + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 1001 + }], + token: tokenA + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + it('should reject a multi-input transaction with insufficient funds', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [ + { + hash: tkaTx1, + index: 0 + }, + { + hash: tkaTx1, + index: 1 + }, + ], + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 2001 + }], + token: tokenA + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + it('should reject a multi-input, multi token transaction with insufficient funds (custom)', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [ + { + hash: tokenB.uid, + index: 0, + }, + fundTx2, + ], + outputs: [ + { + address: await wallet2.getAddressAt(7), + value: 1001, + token: tokenB.uid + }, + { + address: await wallet2.getAddressAt(8), + value: 500 + } + ], + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + it('should reject a multi-input, multi token transaction with insufficient funds (htr)', async done => { + const response = await TestUtils.request + .post('/wallet/send-tx') + .send({ + inputs: [ + { + hash: tokenB.uid, + index: 0, + }, + fundTx2, + ], + outputs: [ + { + address: await wallet2.getAddressAt(7), + value: 500, + token: tokenB.uid + }, + { + address: await wallet2.getAddressAt(8), + value: 1001 + } + ], + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.body.success).toBe(false); + expect(response.body.hash).toBeUndefined(); + done(); + }); + + // Success transaction tests + + const tkaTx2 = { + hash: null, + index: null + }; // Change that will remain on wallet1 + it('should send a custom token with a single input (deprecated token api)', async done => { + const sendOptions = { + inputs: [{ + hash: tkaTx1.hash, + index: 0 + }], // Using index 0 of main transaction + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 200 + }], + token: tokenA, + change_address: await wallet1.getAddressAt(0) + }; + const tx = await wallet1.sendTx({ + fullObject: sendOptions + }); + + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + tkaTx2.hash = tx.hash; + tkaTx2.index = TestUtils.getOutputIndexFromTx(tx, 800); + + await TestUtils.pauseForWsUpdate(); + + // Checking wallet balances + const balance2tka = await wallet2.getBalance(tokenA.uid); + expect(balance2tka.available).toBe(200); + const balance1tka = await wallet1.getBalance(tokenA.uid); + expect(balance1tka.available).toBe(1800); + + // Checking specific addresses balances + const destination = await wallet2.getAddressInfo(0, tokenA.uid); + expect(destination.total_amount_available).toBe(200); + const change = await wallet1.getAddressInfo(0, tokenA.uid); + expect(change.total_amount_available).toBe(800); + done(); + }); + + it('should send a custom token with a single input', async done => { + await wallet4.injectFunds(10); + const w4TokenTx = await wallet4.createToken({ + address: await wallet4.getAddressAt(0), + change_address: await wallet4.getAddressAt(0), + symbol: 'TKW4', + name: 'Token Wallet 4', + amount: 1000 + }); + const tk4OutputIndex = TestUtils.getOutputIndexFromTx(w4TokenTx, 1000); + tokenWallet4.uid = w4TokenTx.hash; + + await TestUtils.pauseForWsUpdate(); + + const sendOptions = { + inputs: [{ + hash: tokenWallet4.uid, + index: tk4OutputIndex + }], + outputs: [{ + address: await wallet5.getAddressAt(0), + value: 200, + token: tokenWallet4.uid + }], + change_address: await wallet4.getAddressAt(0) + }; + const tx = await wallet4.sendTx({ + fullObject: sendOptions + }); + + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + // Checking wallet balances + const balance5tka = await wallet5.getBalance(tokenWallet4.uid); + expect(balance5tka.available).toBe(200); + const balance4tka = await wallet4.getBalance(tokenWallet4.uid); + expect(balance4tka.available).toBe(800); + + // Checking specific addresses balances + const destination = await wallet5.getAddressInfo(0, tokenWallet4.uid); + expect(destination.total_amount_available).toBe(200); + const change = await wallet4.getAddressInfo(0, tokenWallet4.uid); + expect(change.total_amount_available).toBe(800); + done(); + }); + + it('should send a custom token with multiple inputs', async done => { + const sendOptions = { + inputs: [ + { + hash: tkaTx1.hash, + index: 1 + }, // Using index 1 of main transaction + tkaTx2 // Change on wallet 1 + ], + outputs: [{ + address: await wallet2.getAddressAt(0), + value: 1800 + }], + token: tokenA, + }; + const tx = await wallet1.sendTx({ + fullObject: sendOptions + }); + + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + + const balance2tka = await wallet2.getBalance(tokenA.uid); + expect(balance2tka.available).toBe(2000); + + const destination = await wallet2.getAddressInfo(0, tokenA.uid); + expect(destination.total_amount_available).toBe(2000); + + const balance1tka = await wallet1.getBalance(tokenA.uid); + expect(balance1tka.available).toBe(0); + done(); + }); + + it('should send a custom token (deprecated token api)', async done => { + // Sending all TokenA back to wallet 1, address 0 + const sendOptions = { + outputs: [{ + address: await wallet1.getAddressAt(0), + value: 2000 + }], + token: tokenA, + }; + + const tx = await wallet2.sendTx({ + fullObject: sendOptions + }); + + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const destination = await wallet1.getAddressInfo(0, tokenA.uid); + expect(destination.total_amount_available).toBe(2000); + + const balance2tka = await wallet2.getBalance(tokenA.uid); + expect(balance2tka.available).toBe(0); + + done(); + }); + + it('should send a custom token', async done => { + // Sending all TokenA back to wallet 1, address 0 + const sendOptions = { + outputs: [{ + address: await wallet4.getAddressAt(0), + value: 200, + token: tokenWallet4.uid + }], + }; + + const tx = await wallet5.sendTx({ + fullObject: sendOptions + }); + + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const destination = await wallet4.getAddressInfo(0, tokenWallet4.uid); + expect(destination.total_amount_available).toBe(1000); + + const balance5tka = await wallet5.getBalance(tokenWallet4.uid); + expect(balance5tka.available).toBe(0); + + done(); + }); + + it('should send a custom token with multi inputs and outputs', async done => { + // Spreading Token A into three output addresses on wallet2 + const spreadTxOptions = { + outputs: [ + { + address: await wallet2.getAddressAt(2), + value: 900 + }, + { + address: await wallet2.getAddressAt(3), + value: 800 + }, + { + address: await wallet2.getAddressAt(4), + value: 300 + } + ], + token: tokenA, + }; + const spreadTx = await wallet1.sendTx({ + fullObject: spreadTxOptions + }); + + // Consolidating these funds back into wallet1 in two addresses + const consolidateTxOptions = { + outputs: [ + { + address: await wallet1.getAddressAt(3), + value: 1600 + }, + { + address: await wallet1.getAddressAt(4), + value: 400 + } + ], + inputs: [ + { + hash: spreadTx.hash, + index: 0 + }, + { + hash: spreadTx.hash, + index: 1 + }, + { + hash: spreadTx.hash, + index: 2 + } + ], + token: tokenA + }; + const consolidateTx = await wallet2.sendTx({ + fullObject: consolidateTxOptions + }); + + expect(consolidateTx.success).toBe(true); + expect(consolidateTx.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const destination3 = await wallet1.getAddressInfo(3, tokenA.uid); + const destination4 = await wallet1.getAddressInfo(4, tokenA.uid); + expect(destination3.total_amount_available).toBe(1600); + expect(destination4.total_amount_available).toBe(400); + + done(); + }); + + let tkbTx1 = null; + it('should send a multi-input, multi token transaction', async done => { + tkbTx1 = await wallet3.sendTx({ + fullObject: { + inputs: [ + { + hash: tokenB.uid, + index: 0, + }, + { + hash: tokenB.uid, + index: 1 + }, + ], + outputs: [ + { + address: await wallet3.getAddressAt(7), + value: 1000, + token: tokenB.uid + }, + { + address: await wallet3.getAddressAt(8), + value: 990 + } + ], + } + }); + + expect(tkbTx1.success).toBe(true); + expect(tkbTx1.hash).toBeDefined(); + + await TestUtils.pauseForWsUpdate(); + + const destination7 = await wallet3.getAddressInfo(7, tokenB.uid); + const destination8 = await wallet3.getAddressInfo(8); + expect(destination7.total_amount_available).toBe(1000); + expect(destination8.total_amount_available).toBe(990); + + done(); + }); + + it('should send a multi-input/token transaction with change address', async done => { + const outputIndexTKB = TestUtils.getOutputIndexFromTx(tkbTx1, 1000); + + /* We need to have a deep understanding of the wallet and transaction in order to validate + * its results. First, let's build a "summary" object to help identify the main data here + * */ + const txOutputSummary = { + htr: { + address: await wallet3.getAddressAt(11), + value: 200, + index: null, + change: null, + changeAddress: null, + changeIndex: null + }, + tkb: { + address: await wallet3.getAddressAt(10), + value: 650, + index: null, + change: null, + changeIndex: null, + changeAddress: null + } + }; + + const nextEmptyAddress = await wallet3.getNextAddress(); + + // One manual UXTO with 1000 TKB, and automatic UTXO's for HTR + const tx = await wallet3.sendTx({ + fullObject: { + inputs: [ + { + hash: tkbTx1.hash, + index: outputIndexTKB, // Input for token B + }, + ], + outputs: [ + { + address: txOutputSummary.tkb.address, + value: txOutputSummary.tkb.value, + token: tokenB.uid + }, + { + address: txOutputSummary.htr.address, + value: txOutputSummary.htr.value + } + ], + } + }); + + // Basic validation of success + expect(tx.success).toBe(true); + expect(tx.hash).toBeDefined(); + + // Obtaining the fully decoded transaction above from the http endpoint + const decodedTx = await TestUtils.getDecodedTransaction(tx.hash, wallet3.walletId); + + // Analyzing the decoded output data to identify addresses and values + for (const index in decodedTx.outputs) { + const output = decodedTx.outputs[index]; + + if (output.token_data === 0) { + // If token_data === 0 , this is a HTR output + if (output.value === txOutputSummary.htr.value) { + txOutputSummary.htr.index = index; + } else { + txOutputSummary.htr.changeIndex = index; + txOutputSummary.htr.change = output.value; + txOutputSummary.htr.changeAddress = output.decoded.address; + } + } else if (output.token_data === 1) { + // If token_data === 1, this is a custom token (TKB) output + if (output.value === txOutputSummary.tkb.value) { + txOutputSummary.tkb.index = index; + } else { + txOutputSummary.tkb.changeIndex = index; + txOutputSummary.tkb.change = output.value; + txOutputSummary.tkb.changeAddress = output.decoded.address; + } + } + } + + await TestUtils.pauseForWsUpdate(); + + // Validating all the outputs' balances + const destination10 = await wallet3.getAddressInfo(10, tokenB.uid); + const destination11 = await wallet3.getAddressInfo(11); + const changeHtr = await TestUtils.getAddressInfo( + txOutputSummary.htr.changeAddress, + wallet3.walletId + ); + const changeTkb = await TestUtils.getAddressInfo( + txOutputSummary.tkb.changeAddress, + wallet3.walletId, + tokenB.uid + ); + expect(destination10.total_amount_available).toBe(txOutputSummary.tkb.value); + expect(destination11.total_amount_available).toBe(txOutputSummary.htr.value); + expect(changeHtr.total_amount_available).toBe(txOutputSummary.htr.change); + expect(changeTkb.total_amount_available).toBe(txOutputSummary.tkb.change); + + // Validating that the change addresses are not the same + expect(txOutputSummary.htr.changeAddress === txOutputSummary.tkb.changeAddress).toBe(false); + + // One of these addresses is actually the nextEmptyAddress + expect((txOutputSummary.htr.changeAddress === nextEmptyAddress) + || (txOutputSummary.tkb.changeAddress === nextEmptyAddress)).toBe(true); + + // Both these addresses have adjacent indexes: the empty addresses are consumed sequentially + const htrChangeIndex = await TestUtils.getAddressIndex( + wallet3.walletId, + txOutputSummary.htr.changeAddress + ); + const tkbChangeIndex = await TestUtils.getAddressIndex( + wallet3.walletId, + txOutputSummary.tkb.changeAddress + ); + + // Note: this test result may change if the addresses are consumed in a non-linear order + expect(Math.abs(htrChangeIndex - tkbChangeIndex)).toBe(1); + + done(); + }); +}); diff --git a/__tests__/integration/simple-send-tx.test.js b/__tests__/integration/simple-send-tx.test.js new file mode 100644 index 00000000..86a2492c --- /dev/null +++ b/__tests__/integration/simple-send-tx.test.js @@ -0,0 +1,380 @@ +import { TestUtils } from './utils/test-utils-integration'; +import { WalletHelper } from './utils/wallet-helper'; + +describe('simple-send-tx (HTR)', () => { + let wallet1; + let wallet2; + + beforeAll(async () => { + try { + // Wallet with initial funds to send a transaction + wallet1 = new WalletHelper('simple-tx-1'); + // Empty wallet to receive transactions and validate tests + wallet2 = new WalletHelper('simple-tx-2'); + + await WalletHelper.startMultipleWalletsForTest([wallet1, wallet2]); + await wallet1.injectFunds(1000); + } catch (err) { + TestUtils.logError(err.stack); + } + }); + + afterAll(async () => { + await wallet1.stop(); + await wallet2.stop(); + }); + + // Testing all transaction failures first, to have a easier starting test scenario + + it('should not allow a transaction with an invalid value', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet2.getAddressAt(0), + value: 'invalidValue', + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + // It would be good to have a error message assertion + + done(); + }); + + it('should not allow a transaction with a negative value', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet2.getAddressAt(0), + value: -1, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.text).toContain('value'); + + done(); + }); + + it('should not allow a transaction with an invalid address', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: 'invalidAddress', + value: 500, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('Invalid'); + + done(); + }); + + it('should not allow a transaction with an invalid change address', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet2.getAddressAt(0), + value: 500, + change_address: 'invalidChangeAddress', + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('invalid'); + + done(); + }); + + it('should not allow a transaction with a change address outside the wallet', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet2.getAddressAt(0), + value: 500, + change_address: wallet2.getAddressAt(2), + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(400); + + const transaction = response.body; + expect(transaction.success).toBe(false); + const errorElement = transaction.error[0]; + expect(errorElement).toHaveProperty('param', 'change_address'); + expect(errorElement.msg).toContain('Invalid'); + + done(); + }); + + it('should not allow a transaction with insufficient balance', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet2.getAddressAt(0), + value: 2000, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('Insufficient'); + + done(); + }); + + // Executing all successful transactions + + it('should make a successful transaction', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet2.getAddressAt(0), + value: 200, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.outputs).toHaveLength(2); + + await TestUtils.pauseForWsUpdate(); + + const addr0 = await wallet2.getAddressInfo(0); + expect(addr0.total_amount_available).toBe(200); + + const balance1 = await wallet1.getBalance(); + expect(balance1.available).toBe(800); + + done(); + }); + + it('should make a successful transaction with change address', async done => { + const changeAddress = await wallet1.getAddressAt(5); + + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet2.getAddressAt(0), + value: 200, + change_address: changeAddress, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.outputs).toHaveLength(2); + + // Check if the transaction arrived at the correct address + await TestUtils.pauseForWsUpdate(); + + // The wallet1 started with 1000, transferred 400 to wallet2. Change should be 600 + const addr5 = await wallet1.getAddressInfo(5); + expect(addr5.total_amount_received).toBe(600); + + const addr0 = await wallet2.getAddressInfo(0); + expect(addr0.total_amount_available).toBe(400); + done(); + }); +}); + +describe('simple-send-tx (custom token)', () => { + let wallet3; + let wallet4; + const tokenData = { + name: 'SimpleTx Token', + symbol: 'STX', + uid: undefined, + }; + + beforeAll(async () => { + try { + // Wallet with initial 1000 Custom Token funds + wallet3 = new WalletHelper('simple-tx-3'); + // Empty wallet to receive transactions and validate tests + wallet4 = new WalletHelper('simple-tx-4'); + + await WalletHelper.startMultipleWalletsForTest([wallet3, wallet4]); + await wallet3.injectFunds(10); + const tokenResponse = await wallet3.createToken({ + amount: 1000, + name: tokenData.name, + symbol: tokenData.symbol, + doNotWait: true, + }); + tokenData.uid = tokenResponse.hash; + } catch (err) { + TestUtils.logError(err.stack); + } + }); + + afterAll(async () => { + await wallet3.stop(); + await wallet4.stop(); + }); + + // Testing all transaction failures first, to have a easier starting test scenario + + it('should not allow a transaction with an invalid value', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet4.getAddressAt(0), + value: 'invalidValue', + token: tokenData.hash, + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + // It would be good to have a error message assertion + + done(); + }); + + it('should not allow a transaction with a negative value', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet4.getAddressAt(0), + value: -1, + token: tokenData.hash, + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.text).toContain('value'); + + done(); + }); + + it('should not allow a transaction with an invalid address', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: 'invalidAddress', + value: 300, + token: tokenData.uid, + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('Invalid'); + + done(); + }); + + it('should not allow a transaction with an invalid change address', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet4.getAddressAt(0), + value: 300, + token: tokenData.uid, + change_address: 'invalidChangeAddress', + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('invalid'); + + done(); + }); + + it('should not allow a transaction with a change address outside the wallet', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet4.getAddressAt(0), + value: 300, + token: tokenData.uid, + change_address: await wallet4.getAddressAt(2), + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + const transaction = response.body; + expect(transaction.success).toBe(false); + expect(transaction.error).toContain('Change address'); + done(); + }); + + it('should not allow a transaction with insufficient balance', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet4.getAddressAt(0), + value: 3000, + token: tokenData.uid, + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toContain('Insufficient'); + + done(); + }); + + it('should should make a successful transaction', async done => { + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet4.getAddressAt(0), + value: 300, + token: tokenData.uid, + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.outputs).toHaveLength(2); + + await TestUtils.pauseForWsUpdate(); + + const addr0 = await wallet4.getAddressInfo(0, tokenData.uid); + expect(addr0.total_amount_available).toBe(300); + + const balance3 = await wallet3.getBalance(tokenData.uid); + expect(balance3.available).toBe(700); + + done(); + }); + + it('should should make a successful transaction with change address', async done => { + const changeAddress = await wallet3.getAddressAt(5); + + const response = await TestUtils.request + .post('/wallet/simple-send-tx') + .send({ + address: await wallet4.getAddressAt(0), + value: 300, + token: tokenData.uid, + change_address: changeAddress + }) + .set({ 'x-wallet-id': wallet3.walletId }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.outputs).toHaveLength(2); + + // Check if the transaction arrived at the correct address + await TestUtils.pauseForWsUpdate(); + + // The wallet1 started with 1000, transferred 600 to wallet2. Change should be 400 + const addr5 = await wallet3.getAddressInfo(5, tokenData.uid); + expect(addr5.total_amount_received).toBe(400); + + const addr0 = await wallet4.getAddressInfo(0, tokenData.uid); + expect(addr0.total_amount_available).toBe(600); + + done(); + }); +}); diff --git a/__tests__/integration/transaction.test.js b/__tests__/integration/transaction.test.js index 98c13a8b..0b4f38ec 100644 --- a/__tests__/integration/transaction.test.js +++ b/__tests__/integration/transaction.test.js @@ -8,7 +8,7 @@ describe('transaction routes', () => { try { // An empty wallet wallet1 = new WalletHelper('transaction-1'); - await wallet1.start(); + await WalletHelper.startMultipleWalletsForTest([wallet1]); } catch (err) { TestUtils.logError(err.stack); } diff --git a/__tests__/integration/tx-history.test.js b/__tests__/integration/tx-history.test.js index 46ff319f..7c681b48 100644 --- a/__tests__/integration/tx-history.test.js +++ b/__tests__/integration/tx-history.test.js @@ -24,16 +24,18 @@ describe('tx-history routes', () => { try { // An empty wallet wallet1 = new WalletHelper('txHistory1'); - await wallet1.start(); - // A wallet with 5 transactions containing 10, 20, 30, 40 and 50 HTR each wallet2 = new WalletHelper('txHistory2'); - await wallet2.start(); + + await WalletHelper.startMultipleWalletsForTest([wallet1, wallet2]); + for (let amount = 10; amount < 60; amount += 10) { const fundTx = await wallet2.injectFunds(amount, 1); fundTransactions[fundTx.hash] = fundTx; fundHashes[`tx${amount}`] = fundTx.hash; } + + await TestUtils.pauseForWsUpdate(); } catch (err) { TestUtils.logError(err.stack); } diff --git a/__tests__/integration/txLogger.js b/__tests__/integration/txLogger.js index ec5869c2..03f93bfe 100644 --- a/__tests__/integration/txLogger.js +++ b/__tests__/integration/txLogger.js @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import winston from 'winston'; -import config from '../../src/config'; +import testConfig from './configuration/test.config'; export const loggers = { /** @@ -26,10 +26,6 @@ export class TxLogger { */ #logger; - get filename() { - return this.#instanceFilename; - } - /** * Builds the log filename based on current time and an optional title. * The resulting filename will be in the format: @@ -55,6 +51,10 @@ export class TxLogger { this.#instanceFilename = filename; } + get filename() { + return this.#instanceFilename; + } + /** * Initializes the helper with a winston logger instance * @returns {void} @@ -68,21 +68,21 @@ export class TxLogger { winston.format.timestamp(), winston.format.colorize(), ), - level: config.consoleLevel || 'silly', + level: testConfig.consoleLevel || 'silly', }), new winston.transports.File({ format: winston.format.combine( winston.format.timestamp(), winston.format.prettyPrint() ), - filename: `${config.integrationTestLog.outputFolder}${this.#instanceFilename}`, - level: config.consoleLevel || 'silly', + filename: `${testConfig.logOutputFolder}${this.#instanceFilename}`, + level: testConfig.consoleLevel || 'silly', colorize: false, }) ] }); - this.#logger.info(`Log initialized`); + this.#logger.info('Log initialized'); } /** @@ -109,11 +109,11 @@ export class TxLogger { /** * Wrapper for adding a "Wallet Addresses" message * @param {string} walletId - * @param {string} addresses + * @param {string[]} addresses * @returns {void} */ informWalletAddresses(walletId, addresses) { - this.#logger.info(`Sample of wallet addresses.`, { walletId, addresses }); + this.#logger.info('Sample of wallet addresses.', { walletId, addresses }); } /** diff --git a/__tests__/integration/utils/test-utils-integration.js b/__tests__/integration/utils/test-utils-integration.js index b86c19a6..b8923bb5 100644 --- a/__tests__/integration/utils/test-utils-integration.js +++ b/__tests__/integration/utils/test-utils-integration.js @@ -1,9 +1,10 @@ /* eslint-disable no-console */ import supertest from 'supertest'; -import { wallet, HathorWallet } from '@hathor/wallet-lib'; +import { HathorWallet, wallet } from '@hathor/wallet-lib'; import app from '../../../src'; import { loggers } from '../txLogger'; +import testConfig from '../configuration/test.config'; const request = supertest(app); @@ -14,12 +15,6 @@ const request = supertest(app); * @property {string[]} [addresses] Some sample addresses to help with testing */ -/** - * @typedef FundInjectionOptions - * @property {boolean} [doNotWait] If true, will not wait a while for the transaction - * to "settle" on the fullnode - */ - /** * @type {Record} */ @@ -60,6 +55,14 @@ export function getRandomInt(max, min = 0) { } export class TestUtils { + /** + * Returns the Supertest `request` object for this application + * @returns {Test} + */ + static get request() { + return request; + } + /** * Simple way to wait asynchronously before continuing the funcion. Does not block the JS thread. * @param {number} ms Amount of milliseconds to delay @@ -72,11 +75,14 @@ export class TestUtils { } /** - * Returns the Supertest `request` object for this application - * @returns {Test} + * Whenever the tests need to check for a wallet/address balance, there should be a small pause + * to allow for the Fullnode's Websocket connection to update the Wallet Headless' local caches. + * + * The delay period here should be optimized for this purpose. + * @returns {Promise} */ - static get request() { - return request; + static async pauseForWsUpdate() { + await TestUtils.delay(testConfig.wsUpdateDelay); } /** @@ -92,7 +98,7 @@ export class TestUtils { * * @see TestUtils.stopWallet * @param {string} walletId - * @returns {{"x-wallet-id"}} + * @returns {{'x-wallet-id'}} */ static generateHeader(walletId) { return { 'x-wallet-id': walletId }; @@ -120,9 +126,11 @@ export class TestUtils { /** * Starts a wallet. Prefer instantiating a WalletHelper instead. * @param {WalletData} walletObj + * @param [options] + * @param {boolean} [options.waitWalletReady] If true, will only return when wallet is ready * @returns {Promise<{start:unknown,status:unknown}>} */ - static async startWallet(walletObj) { + static async startWallet(walletObj, options = {}) { // Request the Wallet start const response = await request .post('/start') @@ -139,22 +147,33 @@ export class TestUtils { const start = response.body; // Wait until the wallet is actually started - let status; - while (true) { - const res = await request - .get('/wallet/status') - .set(TestUtils.generateHeader(walletObj.walletId)); - if (res.body?.statusCode === HathorWallet.READY) { - status = res.body; - break; + if (options.waitWalletReady) { + while (true) { + const walletReady = await TestUtils.isWalletReady(walletObj.walletId); + if (walletReady) { + break; + } + await TestUtils.delay(500); } - await TestUtils.delay(500); } - // Log the success and return loggers.test.informNewWallet(walletObj.walletId, walletObj.words); - return { start, status }; + return { start }; + } + + /** + * Makes a http request to check if the wallet is ready. + * Returns only the boolean result. + * @param {string} walletId Identification of the wallet + * @returns {Promise} `true` if the wallet is ready + */ + static async isWalletReady(walletId) { + const res = await request + .get('/wallet/status') + .set(TestUtils.generateHeader(walletId)); + + return res.body?.statusCode === HathorWallet.READY; } /** @@ -167,9 +186,10 @@ export class TestUtils { } /** - * Gets the address from a walletId and an index, using the Wallet Headless endpoint - * @param {string} walletId - * @param {number} index + * Gets the address from a walletId and an index, using the Wallet Headless endpoint. + * If no index is informed, the next address without transactions will be returned + * @param {string} walletId Walled identification + * @param {number} [index] * @returns {Promise} */ static async getAddressAt(walletId, index) { @@ -181,16 +201,85 @@ export class TestUtils { return response.body.address; } + /** + * Retrieves the Address Index on the Wallet + * @param {string} walletId Wallet identification + * @param {string} address Address to find the index + * @returns {Promise} + */ + static async getAddressIndex(walletId, address) { + const response = await TestUtils.request + .get('/wallet/address-index') + .query({ address }) + .set(TestUtils.generateHeader(walletId)); + + return response.body.index; + } + + /** + * Retrieves address information based on the address index inside the wallet. + * This is very close to the tests on `address-info.test.js` and as such should reflect any + * changes that are made to the calls there. + * @param {string} address Address hash + * @param {string} walletId Wallet identification + * @param {string} [token] Token hash, defaults to HTR + * @returns {Promise<{ + * token: (string), index: (number), + * total_amount_received: (number), total_amount_sent: (number), + * total_amount_locked: (number), total_amount_available: (number) + * }>} + */ + static async getAddressInfo(address, walletId, token) { + const response = await TestUtils.request + .get('/wallet/address-info') + .query({ + address, + token + }) + .set(TestUtils.generateHeader(walletId)); + + // An error happened + if (response.status !== 200 || response.body.success !== true) { + throw new Error(`Failure on /wallet/address-info: ${response.text}`); + } + + // Returning explicitly each property to help with code completion / test writing + const addrInfo = response.body; + return { + token: addrInfo.token, + index: addrInfo.index, + total_amount_received: addrInfo.total_amount_received, + total_amount_sent: addrInfo.total_amount_sent, + total_amount_available: addrInfo.total_amount_available, + total_amount_locked: addrInfo.total_amount_locked, + }; + } + + /** + * Get the all addresses on this wallet, limited by the current gap limit. + * @param {string} walletId + * @returns {Promise} + */ + static async getSomeAddresses(walletId) { + const response = await TestUtils.request + .get('/wallet/addresses') + .set(TestUtils.generateHeader(walletId)); + + if (!response.body.addresses) { + throw new Error(response.text); + } + return response.body.addresses; + } + /** * Transfers funds to a destination address. * By default, this method also waits for a second to let the indexes build before returning. * @param {string} address Destination address * @param {number} value Amount of tokens, in cents * @param {string} [destinationWalletId] walletId of the destination address. Useful for debugging - * @param {FundInjectionOptions} [options] * @returns {Promise} */ - static async injectFundsIntoAddress(address, value, destinationWalletId, options = {}) { + static async injectFundsIntoAddress(address, value, destinationWalletId) { // Requests the transaction const response = await TestUtils.request .post('/wallet/simple-send-tx') @@ -207,7 +296,7 @@ export class TestUtils { // Logs the results await loggers.test.informSimpleTransaction({ - title: `Injecting funds`, + title: 'Injecting funds', originWallet: WALLET_CONSTANTS.genesis.walletId, value, destinationAddress: address, @@ -215,18 +304,115 @@ export class TestUtils { id: transaction.hash }); - /* - * The balance in the storage is updated after the wallet receives a message via websocket - * from the full node. A simple wait is built here to allow for this message before continuing. - * - * In case there is a need to do multliple transactions before any assertion is executed, - * please use the `doNotWait` option and explicitly insert the delay only once. - * This will improve the test speed. - */ - if (!options.doNotWait) { - await TestUtils.delay(1000); + return transaction; + } + + /** + * Searches the transaction outputs and retrieves the first index containing the desired value. + * @example + * // The txObject transaction contains many outputs, the second's value is 15 + * TestUtils.getOutputIndexFromTx(txObject, 15) + * // will return 1 + * + * @param {unknown} transaction Transaction object, as returned in the `response.body` + * @param {number} value Value to search for + * @returns {number|null} Zero-based index containing the desired output + */ + static getOutputIndexFromTx(transaction, value) { + if (!transaction?.outputs?.length) { + return null; } - return transaction; + for (const index in transaction.outputs) { + if (transaction.outputs[index].value !== value) { + continue; + } + return parseInt(index, 10); + } + + return null; + } + + /** + * A helper method for fetching the change output. Only useful when the transaction has exactly + * two HTR outputs: one for the destination and one for the change address + * @param {unknown} transaction Transaction as received in the response.body + * @param {number} destinationValue Value transferred to the destination + * @returns {{ + * change: {index: number, value: number}, + * destination: {index: number, value: number} + * }|null} + */ + static getOutputSummaryHtr(transaction, destinationValue) { + const returnValue = { + destination: { index: null, value: destinationValue }, + change: { index: null, value: null } + }; + + if (!transaction.outputs?.length) { + return null; + } + + for (const index in transaction.outputs) { + const output = transaction.outputs[index]; + + // Skipping all that outputs not involving HTR + if (output.token_data !== 0) { + continue; + } + + // If the value is destinationValue, we assume this is the destination + if (output.value === destinationValue) { + returnValue.destination.index = index; + continue; + } + + // Any other value, we assume it's the change + returnValue.change.index = index; + returnValue.change.value = output.value; + } + + return returnValue; + } + + static async getTxHistory(walletId) { + const response = await TestUtils.request + .get('/wallet/tx-history') + .set(TestUtils.generateHeader(walletId)); + + return response.body; + } + + static async getBalance(walletId, tokenUid) { + const queryParams = {}; + if (tokenUid) { + queryParams.token = tokenUid; + } + + const response = await TestUtils.request + .get('/wallet/balance') + .query(queryParams) + .set(TestUtils.generateHeader(walletId)); + + return response.body; + } + + /** + * Returns a fully decoded transaction to allow for more complete data analysis. + * This is done through an HTTP request on the Wallet Headless, in behalf of a started wallet id. + * + * @param {string} txHash Transaction id + * @param {string} walletId Mandatory wallet id for requesting the Wallet Headless + * @returns {Promise<*>} + */ + static async getDecodedTransaction(txHash, walletId) { + const response = await TestUtils.request + .get('/wallet/transaction') + .query({ + id: txHash + }) + .set(TestUtils.generateHeader(walletId)); + + return response.body; } } diff --git a/__tests__/integration/utils/wallet-helper.js b/__tests__/integration/utils/wallet-helper.js index 6e5680c1..3573abb0 100644 --- a/__tests__/integration/utils/wallet-helper.js +++ b/__tests__/integration/utils/wallet-helper.js @@ -1,5 +1,6 @@ import { loggers } from '../txLogger'; -import { TestUtils } from './test-utils-integration'; +import { TestUtils, WALLET_CONSTANTS } from './test-utils-integration'; +import testConfig from '../configuration/test.config'; /** * A helper for testing the wallet @@ -27,6 +28,20 @@ export class WalletHelper { */ #started = false; + /** + * Creates a wallet object but does not start it on server + * @param {string} walletId + * @param {string} [words] 24 words + */ + constructor(walletId, words) { + if (!walletId) { + throw new Error('Wallet must have a walletId'); + } + this.#walletId = walletId; + + this.#words = words || TestUtils.generateWords(); + } + get walletId() { return this.#walletId; } @@ -39,30 +54,108 @@ export class WalletHelper { return this.#addresses; } + get started() { + return this.#started; + } + /** - * Creates a wallet object but does not start it on server - * @param {string} walletId - * @param {string} [words] 24 words + * Starts all the wallets needed for the test suite. + * This is the preferred way of starting wallets on the Integration Tests, + * performance-wise. + * @param {WalletHelper[]} walletsArr Array of WalletHelpers + * @param [options] + * @param {boolean} [options.skipAddresses] Skips the getSomeAddresses command + * @param {number} [options.amountOfAddresses=10] How many addresses should be cached per wallet + * @returns {Promise} */ - constructor(walletId, words) { - if (!walletId) throw new Error(`Wallet must have a walletId`); - this.#walletId = walletId; + static async startMultipleWalletsForTest(walletsArr, options) { + /** + * A map of `WalletHelper`s indexed by their `walletId`s + * @type {Record} + */ + const walletsPendingReady = {}; - this.#words = words || TestUtils.generateWords(); + // If the genesis wallet is not instantiated, start it. It should be always available + const { genesis } = WALLET_CONSTANTS; + const isGenesisStarted = await TestUtils.isWalletReady(genesis.walletId); + if (!isGenesisStarted) { + walletsArr.unshift(new WalletHelper(genesis.walletId, genesis.words)); + } + + // Requests the start of all the wallets in quick succession + const startPromisesArray = []; + for (const wallet of walletsArr) { + const promise = TestUtils.startWallet({ + walletId: wallet.walletId, + words: wallet.words, + }); + walletsPendingReady[wallet.walletId] = wallet; + startPromisesArray.push(promise); + } + await Promise.all(startPromisesArray); + + // Enters the loop checking each wallet for its status + const timestampStart = Date.now().valueOf(); + const timestampTimeout = timestampStart + testConfig.walletStartTimeout; + while (true) { + const pendingWalletIds = Object.keys(walletsPendingReady); + // If all wallets were started, return to the caller. + if (!pendingWalletIds.length) { + break; + } + + // If this process took too long, the connection with the fullnode may be irreparably broken. + const timestamp = Date.now().valueOf(); + if (timestamp > timestampTimeout) { + const errMsg = `Wallet init failure: Timeout after ${timestamp - timestampStart}ms.`; + TestUtils.logError(errMsg); + throw new Error(errMsg); + } + + // First we add a delay + await TestUtils.delay(500); + + // Checking the status of each wallet + for (const walletId of pendingWalletIds) { + const isReady = await TestUtils.isWalletReady(walletId); + if (!isReady) { + continue; + } + + // If the wallet is ready, we remove it from the status check loop + walletsPendingReady[walletId].__setStarted(); + delete walletsPendingReady[walletId]; + + const addresses = await TestUtils.getSomeAddresses(walletId); + await loggers.test.informWalletAddresses(walletId, addresses); + } + } + + const timestamp = Date.now().valueOf(); + TestUtils.logTx(`Finished multiple wallet initialization.`, { + timestampStart, + timestampNow: timestamp, + diffSinceStart: timestamp - timestampStart + }); } /** - * Starts this wallet and returns a formatted object with relevant wallet data + * Starts this wallet and returns a formatted object with relevant wallet data. + * Because the wallet takes time to instantiate, prefer the `startMultipleWalletsForTest` method. + * @see startMultipleWalletsForTest * @param [options] * @param {boolean} [options.skipAddresses] Skips the getSomeAddresses command * @param {number} [options.amountOfAddresses=10] How many addresses should be cached (default 10) * @returns {Promise} */ async start(options = {}) { - await TestUtils.startWallet({ - walletId: this.#walletId, - words: this.#words, - }); + await TestUtils.startWallet( + { + walletId: this.#walletId, + words: this.#words, + }, + { waitWalletReady: true } + ); this.#started = true; // Populating some addressess for this wallet @@ -82,6 +175,8 @@ export class WalletHelper { }; } + __setStarted() { this.#started = true; } + /** * Stops this wallet * @returns {Promise} @@ -108,16 +203,42 @@ export class WalletHelper { return addressAt; } + /** + * Returns the next address without transactions for this wallet + * @returns {Promise} + */ + async getNextAddress() { + return TestUtils.getAddressAt(this.#walletId); + } + + /** + * Retrieves address information based on the address index inside the wallet. + * @param {number} index Address index + * @param {string} [token] Token hash, defaults to HTR + * @returns {Promise<{ + * token: (string), index: (number), + * total_amount_received: (number), total_amount_sent: (number), + * total_amount_locked: (number), total_amount_available: (number) + * }>} + */ + async getAddressInfo(index, token) { + const address = await this.getAddressAt(index); + + return TestUtils.getAddressInfo(address, this.#walletId, token); + } + /** * Retrieves funds from the Genesis wallet and injects into this wallet at a specified address. * @param {number} value Value to be transferred * @param {number} [addressIndex=0] Address index. Defaults to 0 - * @param {FundInjectionOptions} [options] * @returns {Promise<{success}|*>} */ - async injectFunds(value, addressIndex = 0, options = {}) { + async injectFunds(value, addressIndex = 0) { + if (!this.#started) { + throw new Error(`Cannot inject funds: wallet ${this.#walletId} is not started.`); + } const destinationAddress = await this.getAddressAt(addressIndex); - return TestUtils.injectFundsIntoAddress(destinationAddress, value, this.#walletId, options); + return TestUtils.injectFundsIntoAddress(destinationAddress, value, this.#walletId); } /** @@ -128,7 +249,6 @@ export class WalletHelper { * @param {string} params.symbol Token symbol * @param {string} [params.address] Destination address for the custom token * @param {string} [params.change_address] Destination address for the HTR change - * @param {boolean} [params.doNotWait] Skip waiting after the transaction * @returns {Promise} Token creation transaction */ async createToken(params) { @@ -136,8 +256,12 @@ export class WalletHelper { // Creating the request body from mandatory and optional parameters const tokenCreationBody = { name, symbol, amount }; - if (params.address) tokenCreationBody.address = params.address; - if (params.change_address) tokenCreationBody.change_address = params.change_address; + if (params.address) { + tokenCreationBody.address = params.address; + } + if (params.change_address) { + tokenCreationBody.change_address = params.change_address; + } // Executing the request const newTokenResponse = await TestUtils.request @@ -162,10 +286,105 @@ export class WalletHelper { throw injectError; } - // Returning the Create Token transaction - if (!params.doNotWait) { - await TestUtils.delay(1000); + return transaction; + } + + /** + * @typedef SendTxInputParam + * @property {string} [hash] UTXO transaction hash + * @property {number} [index] UTXO output index + * @property {string} [token] Optional token hash. Defaults to HTR + * @property {'query'} [type] Optional command instead of a UTXO + * @property {string} [filter_address] Optional command data + * @see The source code for route /wallet/send-tx + * @example + * { hash: '123abc', index: 0 } + * { hash: '123abc', index: 1, token: '234def' } + * { type: 'query', filter_address: '567acf' } + */ + + /** + * @typedef SendTxOutputParam + * @property {string} [address] Destination address hash + * @property {number} [value] Amount of tokens to transfer on this output + * @property {string} [token] Optional token hash. Defaults to HTR + */ + + /** + * Sends a transaction. + * + * @example + * wallet.sendTx({ + * destination: 'abc123', + * value: 100, + * token: 'def456' + * }) + * @see https://wallet-headless.docs.hathor.network/#/paths/~1wallet~1simple-send-tx/post + * @param options + * @param {unknown} [options.fullObject] Advanced usage: a full body to send to post on 'send-tx' + * @param {SendTxInputParam[]} [options.inputs] Optional Inputs + * @param {SendTxOutputParam[]} [options.outputs] Complete Outputs + * @param {string} [options.destination] Simpler way to inform output address instead of "outputs" + * @param {number} [options.value] Simpler way to inform transfer value instead of "outputs" + * @param {string} [options.token] Simpler way to inform transfer token instead of "outputs" + * @param {string} [options.destinationWallet] Optional parameter to explain the funds destination + * @param {string} [options.change_address] Optional parameter to set the change address + * @returns {Promise} Returns the transaction + */ + async sendTx(options) { + const sendOptions = options.fullObject || {}; + if (options.inputs) { + sendOptions.inputs = options.inputs; + } + if (options.outputs) { + sendOptions.outputs = options.outputs; + } else if (options.destination && options.value) { + const sendObj = { + address: options.destination, + value: options.value, + }; + if (options.token) { + sendObj.token = options.token; + } + sendOptions.outputs = [sendObj]; + } + if (options.change_address) { + sendOptions.change_address = options.change_address; + } + + const response = await TestUtils.request + .post('/wallet/send-tx') + .send(sendOptions) + .set(TestUtils.generateHeader(this.#walletId)); + + // Error handling + const transaction = response.body; + if (!transaction.success) { + const txError = new Error(transaction.message); + txError.innerError = response; + txError.sendOptions = sendOptions; + throw txError; + } + + // Logs the results + const metadata = { + originWallet: this.#walletId, + hash: transaction.hash, + ...sendOptions + }; + if (options.destinationWallet) { + metadata.destinationWallet = options.destinationWallet; } + await loggers.test.insertLineToLog('Transferring funds', metadata); + return transaction; } + + async getTxHistory() { + return TestUtils.getTxHistory(this.#walletId); + } + + async getBalance(tokenUid = null) { + return TestUtils.getBalance(this.#walletId, tokenUid); + } } diff --git a/jest-integration.config.js b/jest-integration.config.js index 0967192d..84d4b243 100644 --- a/jest-integration.config.js +++ b/jest-integration.config.js @@ -1,11 +1,11 @@ module.exports = { clearMocks: true, - coverageDirectory: "coverage-integration", - testEnvironment: "node", + coverageDirectory: 'coverage-integration', + testEnvironment: 'node', collectCoverage: true, - collectCoverageFrom: ["/src/**/*.js"], - testMatch: ["/__tests__/integration/**/*.test.js"], - coverageReporters: ["text-summary", "lcov", "clover"], + collectCoverageFrom: ['/src/**/*.js'], + testMatch: ['/__tests__/integration/**/*.test.js'], + coverageReporters: ['text-summary', 'lcov', 'clover'], testTimeout: 20 * 60 * 1000, // 20 minutes seems reasonable for slow integration tests. May be adjusted with optimizations - setupFilesAfterEnv: ["/setupTests-integration.js"], + setupFilesAfterEnv: ['/setupTests-integration.js'], }; diff --git a/setupTests-integration.js b/setupTests-integration.js index 610deaaa..2124c758 100644 --- a/setupTests-integration.js +++ b/setupTests-integration.js @@ -1,5 +1,4 @@ import * as Path from 'path'; -import { TestUtils, WALLET_CONSTANTS } from './__tests__/integration/utils/test-utils-integration'; import { loggers, TxLogger } from './__tests__/integration/txLogger'; /** @@ -21,11 +20,4 @@ beforeAll(async () => { const testLogger = new TxLogger(testName); testLogger.init(); loggers.test = testLogger; - - await TestUtils.startWallet(WALLET_CONSTANTS.genesis); -}); - -// This function will run after each test file is executed -afterAll(async () => { - await TestUtils.stopWallet(WALLET_CONSTANTS.genesis.walletId); });