diff --git a/__tests__/__fixtures__/http-fixtures.js b/__tests__/__fixtures__/http-fixtures.js index e87ea227..5ad89a5a 100644 --- a/__tests__/__fixtures__/http-fixtures.js +++ b/__tests__/__fixtures__/http-fixtures.js @@ -696,7 +696,20 @@ export default { address: 'WewDeXWyvHP7jJTs7tjLoQfoB72LLxJQqN', timelock: 32522094000, }, - token: '03', + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + spent_by: null, + selected_as_input: false, + }, + { + value: 1, + token_data: 129, + script: 'qRTqJUJmzEmBNvhkmDuZ4JxcMh5/ioc=', + decoded: { + type: 'P2SH', + address: 'wgyUgNjqZ18uYr4YfE2ALW6tP5hd8MumH5', + timelock: null + }, + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', spent_by: null, selected_as_input: false, }, @@ -720,7 +733,7 @@ export default { '00e161a6b0bee1781ea9300680913fb76fd0fac4acab527cd9626cc1514abdc9', ], height: 19, - tokens: ['03'] + tokens: ['0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee'] }, { tx_id: diff --git a/__tests__/mint-tokens.test.js b/__tests__/mint-tokens.test.js index 3b26df52..ebb2b604 100644 --- a/__tests__/mint-tokens.test.js +++ b/__tests__/mint-tokens.test.js @@ -15,7 +15,7 @@ describe('mint-tokens api', () => { const response = await TestUtils.request .post('/wallet/mint-tokens') .send({ - token: '03', + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', amount: 1, }) .set({ 'x-wallet-id': walletId }); @@ -27,7 +27,7 @@ describe('mint-tokens api', () => { const response = await TestUtils.request .post('/wallet/mint-tokens') .send({ - token: '03', + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', amount: '1', }) .set({ 'x-wallet-id': walletId }); @@ -38,7 +38,7 @@ describe('mint-tokens api', () => { it('should not mint a token without the required parameters', async () => { ['token', 'amount'].forEach(async field => { const token = { - token: '03', + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', amount: 1, }; delete token[field]; @@ -55,7 +55,7 @@ describe('mint-tokens api', () => { const promise1 = TestUtils.request .post('/wallet/mint-tokens') .send({ - token: '03', + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', amount: 1, }) .set({ 'x-wallet-id': walletId }); diff --git a/__tests__/p2sh/tx-proposal-mint-tokens.test.js b/__tests__/p2sh/tx-proposal-mint-tokens.test.js new file mode 100644 index 00000000..577417ab --- /dev/null +++ b/__tests__/p2sh/tx-proposal-mint-tokens.test.js @@ -0,0 +1,210 @@ +import hathorLib from '@hathor/wallet-lib'; +import TestUtils from '../test-utils'; +import { TOKEN_DATA, AUTHORITY_VALUE } from '../integration/configuration/test-constants'; + +const walletId = 'stub_mint_tokens'; + +describe('mint-tokens tx-proposal api', () => { + beforeAll(async () => { + global.config.multisig = TestUtils.multisigData; + await TestUtils.startWallet( + { + walletId, + multisig: true, + preCalculatedAddresses: TestUtils.multisigAddresses + } + ); + }); + + afterAll(async () => { + global.config.multisig = {}; + await TestUtils.stopWallet({ walletId }); + }); + + it('should return 200 with a valid body', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.txHex).toBeDefined(); + const tx = hathorLib.helpersUtils + .createTxFromHex(response.body.txHex, new hathorLib.Network('testnet')); + expect(tx.outputs.map(o => o.decodedScript.address.base58)) + .toEqual(expect.arrayContaining(['wbe2eJdyZVimA7nJjmBQnKYJSXmpnpMKgG'])); + }); + + it('should not accept mint token with empty token', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '', + amount: 1, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should not accept mint token with amount 0', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 0, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should not accept mint token without funds to cover it', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1_000_000_000_000, // 1 trillion + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + }); + + it('should return 200 with a valid body selecting address', async () => { + const address = TestUtils.multisigAddresses[2]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + address, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.txHex).toBeDefined(); + const tx = hathorLib.helpersUtils.createTxFromHex(response.body.txHex, new hathorLib.Network('testnet')); + expect(tx.outputs.map(o => o.decodedScript.address.base58)) + .toEqual(expect.arrayContaining(['wbe2eJdyZVimA7nJjmBQnKYJSXmpnpMKgG', address])); + }); + + it('should not accept mint token with empty address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + address: '', + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should return 200 with a valid body selecting change address', async () => { + const changeAddress = TestUtils.multisigAddresses[3]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + change_address: changeAddress, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.txHex).toBeDefined(); + const tx = hathorLib.helpersUtils.createTxFromHex(response.body.txHex, new hathorLib.Network('testnet')); + expect(tx.outputs.map(o => o.decodedScript.address.base58)) + .toEqual(expect.arrayContaining(['wbe2eJdyZVimA7nJjmBQnKYJSXmpnpMKgG', changeAddress])); + }); + + it('should not accept mint token with empty change address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + change_address: '', + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should not accept mint token with a change address that does not belong to the wallet', async () => { + const changeAddress = TestUtils.addresses[0]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + change_address: changeAddress, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + }); + + it('should return 200 with a valid body selecting an authority address', async () => { + const mintAuthorityAddress = TestUtils.multisigAddresses[2]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + mint_authority_address: mintAuthorityAddress, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.txHex).toBeDefined(); + const tx = hathorLib.helpersUtils.createTxFromHex(response.body.txHex, new hathorLib.Network('testnet')); + expect(tx.outputs.map(o => o.decodedScript.address.base58)) + .toEqual(expect.arrayContaining(['wbe2eJdyZVimA7nJjmBQnKYJSXmpnpMKgG', mintAuthorityAddress])); + expect(tx.outputs).toHaveLength(3); + const authorityOutputs = tx.outputs.filter(o => TOKEN_DATA.isAuthorityToken(o.tokenData)); + expect(authorityOutputs).toHaveLength(1); + expect(authorityOutputs[0].value).toBe(AUTHORITY_VALUE.MINT); + }); + + it('should not accept mint token with empty mint authority address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + mint_authority_address: '', + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should return 200 with a valid body selecting an external authority address', async () => { + const mintAuthorityAddress = TestUtils.addresses[1]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + mint_authority_address: mintAuthorityAddress, + allow_external_mint_authority_address: true, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.txHex).toBeDefined(); + const tx = hathorLib.helpersUtils.createTxFromHex(response.body.txHex, new hathorLib.Network('testnet')); + expect(tx.outputs.map(o => o.decodedScript.address.base58)) + .toEqual(expect.arrayContaining(['wbe2eJdyZVimA7nJjmBQnKYJSXmpnpMKgG', mintAuthorityAddress])); + expect(tx.outputs).toHaveLength(3); + const authorityOutputs = tx.outputs.filter(o => TOKEN_DATA.isAuthorityToken(o.tokenData)); + expect(authorityOutputs).toHaveLength(1); + expect(authorityOutputs[0].value).toBe(AUTHORITY_VALUE.MINT); + }); +}); diff --git a/src/controllers/wallet/p2sh/tx-proposal.controller.js b/src/controllers/wallet/p2sh/tx-proposal.controller.js index f6b7b277..fa233652 100644 --- a/src/controllers/wallet/p2sh/tx-proposal.controller.js +++ b/src/controllers/wallet/p2sh/tx-proposal.controller.js @@ -56,6 +56,43 @@ async function buildTxProposal(req, res) { } } +async function buildMintTokensTxProposal(req, res) { + const validationResult = parametersValidation(req); + if (!validationResult.success) { + res.status(400).json(validationResult); + return; + } + + const { + token, + amount, + } = req.body; + const address = req.body.address || null; + const changeAddress = req.body.change_address || null; + const mintAuthorityAddress = req.body.mint_authority_address || null; + const allowExternalMintAuthorityAddress = req.body.allow_external_mint_authority_address || null; + + try { + if (changeAddress && !await req.wallet.isAddressMine(changeAddress)) { + throw new Error('Change address is not from this wallet'); + } + + const mintTokenTransaction = await req.wallet.prepareMintTokensData( + token, + amount, + { + address, + changeAddress, + mintAuthorityAddress, + allowExternalMintAuthorityAddress, + } + ); + res.send({ success: true, txHex: mintTokenTransaction.toHex() }); + } catch (err) { + res.send({ success: false, error: err.message }); + } +} + async function getMySignatures(req, res) { const validationResult = parametersValidation(req); if (!validationResult.success) { @@ -144,6 +181,7 @@ async function signAndPush(req, res) { module.exports = { buildTxProposal, + buildMintTokensTxProposal, getMySignatures, signTx, signAndPush, diff --git a/src/routes/wallet/p2sh/tx-proposal.routes.js b/src/routes/wallet/p2sh/tx-proposal.routes.js index 694007e5..dbe1c681 100644 --- a/src/routes/wallet/p2sh/tx-proposal.routes.js +++ b/src/routes/wallet/p2sh/tx-proposal.routes.js @@ -6,10 +6,11 @@ */ const { Router } = require('express'); -const { checkSchema } = require('express-validator'); +const { checkSchema, body } = require('express-validator'); const { buildTxProposal, getMySignatures, + buildMintTokensTxProposal, signTx, signAndPush, } = require('../../../controllers/wallet/p2sh/tx-proposal.controller'); @@ -81,6 +82,17 @@ txProposalRouter.post( buildTxProposal, ); +txProposalRouter.post( + '/mint-tokens', + body('token').isString().notEmpty(), + body('amount').isInt({ min: 1 }).toInt(), + body('address').isString().notEmpty().optional(), + body('change_address').isString().notEmpty().optional(), + body('mint_authority_address').isString().notEmpty().optional(), + body('allow_external_mint_authority_address').isBoolean().optional().toBoolean(), + buildMintTokensTxProposal, +); + /* * XXX: Currently only works for P2SH MultiSig signatures, but can be enhanced to * include P2PKH Signatures once the wallet-lib adds support.