diff --git a/__tests__/__fixtures__/http-fixtures.js b/__tests__/__fixtures__/http-fixtures.js index e87ea227..126a8841 100644 --- a/__tests__/__fixtures__/http-fixtures.js +++ b/__tests__/__fixtures__/http-fixtures.js @@ -696,7 +696,47 @@ 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, + }, + { + value: 200, + token_data: 1, + script: 'dqkU/qcZVmiK7oEMzDyVX9kwfldkR8CIrA==', + decoded: { + type: 'P2SH', + address: 'wgyUgNjqZ18uYr4YfE2ALW6tP5hd8MumH5', + timelock: null + }, + token: + '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + spent_by: null, + selected_as_input: false, + }, + { + value: 2, + token_data: 129, + script: 'qRTqJUJmzEmBNvhkmDuZ4JxcMh5/ioc=', + decoded: { + type: 'P2SH', + address: 'wgyUgNjqZ18uYr4YfE2ALW6tP5hd8MumH5', + timelock: null + }, + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', spent_by: null, selected_as_input: false, }, @@ -720,7 +760,7 @@ export default { '00e161a6b0bee1781ea9300680913fb76fd0fac4acab527cd9626cc1514abdc9', ], height: 19, - tokens: ['03'] + tokens: ['0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee'] }, { tx_id: diff --git a/__tests__/decode.test.js b/__tests__/decode.test.js index 598e7a75..a904021d 100644 --- a/__tests__/decode.test.js +++ b/__tests__/decode.test.js @@ -1,5 +1,6 @@ import { PartialTx, ProposalInput, ProposalOutput } from '@hathor/wallet-lib/lib/models/partial_tx'; import { Network, Address, P2PKH } from '@hathor/wallet-lib'; +import httpFixtures from './__fixtures__/http-fixtures'; import TestUtils from './test-utils'; const walletId = 'stub_decode'; @@ -84,10 +85,12 @@ describe('decode api', () => { const expected = { success: true, tx: { + completeSignatures: false, tokens: [], inputs: [], outputs: [], - } + }, + balance: {}, }; let response = await TestUtils.request @@ -135,7 +138,11 @@ describe('decode api', () => { let address = new Address(TestUtils.addresses[0]); let script = new P2PKH(address); partialTx.outputs.push( - new ProposalOutput(10, script.createScript(), { token: fakeToken1, tokenData: 1 }) + new ProposalOutput( + 10, + script.createScript(), + { token: fakeToken1, tokenData: 1 } + ) ); address = new Address(TestUtils.addresses[1]); @@ -152,6 +159,18 @@ describe('decode api', () => { ) ); + const txHistoryResponse = httpFixtures['/thin_wallet/address_history']; + const txHistory = txHistoryResponse.history; + const fakeTx = txHistory[0]; + const fakeTxResponse = { + success: true, + tx: fakeTx, + meta: { + first_block_height: 1234, + }, + }; + TestUtils.httpMock.onGet('/transaction').reply(200, fakeTxResponse); + // 1 input, 2 outputs const response = await TestUtils.request .post('/wallet/decode') @@ -162,22 +181,64 @@ describe('decode api', () => { expect(response.body).toEqual({ success: true, tx: expect.objectContaining({ - tokens: [fakeToken1, fakeToken2], - inputs: [{ txId: fakeInputHash, index: 1 }], + completeSignatures: false, + tokens: expect.arrayContaining([fakeToken1, fakeToken2]), + inputs: [ + { + txId: fakeInputHash, + index: 1, + value: 6400, + decoded: { + address: 'wgyUgNjqZ18uYr4YfE2ALW6tP5hd8MumH5', + type: 'MultiSig', + timelock: null, + }, + script: expect.any(String), + token: '00', + tokenData: 0, + token_data: 0, + mine: true, + signed: false, + }, + ], outputs: [ expect.objectContaining({ value: 10, tokenData: 1, + token_data: 1, token: fakeToken1, - decoded: expect.objectContaining({ address: TestUtils.addresses[0] }) + decoded: { + address: TestUtils.addresses[0], + timelock: null, + mine: false, + }, + script: expect.any(String), + type: 'p2pkh', }), expect.objectContaining({ value: 20, tokenData: 0, - decoded: expect.objectContaining({ address: TestUtils.addresses[1] }) + token_data: 0, + decoded: { + address: TestUtils.addresses[1], + timelock: null, + mine: false, + }, + script: expect.any(String), + token: '00', + type: 'p2pkh', }), ], }), + balance: { + '00': { + tokens: { available: -6400, locked: 0 }, + authorities: { + melt: { available: 0, locked: 0 }, + mint: { available: 0, locked: 0 }, + }, + }, + }, }); spy.mockRestore(); diff --git a/__tests__/integration/atomic-swap.test.js b/__tests__/integration/atomic-swap.test.js index 3c974941..d87ec2ba 100644 --- a/__tests__/integration/atomic-swap.test.js +++ b/__tests__/integration/atomic-swap.test.js @@ -453,10 +453,10 @@ describe('send tx (HTR)', () => { ]), }), }); - decodeResponse.body.tx.inputs.map(input => expect(input).toEqual({ + decodeResponse.body.tx.inputs.map(input => expect(input).toEqual(expect.objectContaining({ txId: expect.any(String), index: expect.any(Number), - })); + }))); expect(response.body.isComplete).toBe(false); @@ -507,10 +507,10 @@ describe('send tx (HTR)', () => { ]), }, }); - decodeResponse.body.tx.inputs.map(input => expect(input).toEqual({ + decodeResponse.body.tx.inputs.map(input => expect(input).toEqual(expect.objectContaining({ txId: expect.any(String), index: expect.any(Number), - })); + }))); // wallet1: sign data response = await TestUtils.request diff --git a/__tests__/integration/multisig.test.js b/__tests__/integration/multisig.test.js index 4c60116f..1150fe3c 100644 --- a/__tests__/integration/multisig.test.js +++ b/__tests__/integration/multisig.test.js @@ -12,6 +12,12 @@ describe('send tx (HTR)', () => { let wallet4; let wallet5; let walletExtra; + let wallets; + const tokenA = { + name: 'Token A', + symbol: 'TKA', + uid: null + }; const { words, pubkeys, walletConfig } = multisigWalletsData; @@ -84,6 +90,21 @@ describe('send tx (HTR)', () => { // Awaiting for updated balances to be received by the websocket await TestUtils.pauseForWsUpdate(); + + // Set wallets + wallets = [wallet1, wallet2, wallet3, wallet4, wallet5]; + + // Creating a token for the tests + const tkAtxHex = await wallet1.buildCreateToken({ + name: tokenA.name, + symbol: tokenA.symbol, + amount: 500, + address: await wallet1.getAddressAt(0), + }); + + // try to send + const tkAtx = await wallet1.signAndPush({ txHex: tkAtxHex, wallets, xSignatures: 3 }); + tokenA.uid = tkAtx.hash; } catch (err) { TestUtils.logError(err.stack); } @@ -300,4 +321,716 @@ describe('send tx (HTR)', () => { expect(wallet1.addresses).toContain(decoded.address.base58); } }); + + // create-tokens, mint-tokens, melt-tokens, decode + it('should create, mint and melt tokens with minimum signatures, and decode', async () => { + // # Create Token + const address = precalculatedMultisig[0].addresses[0]; + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 100, + address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex: txHexCreateTokenToDecode } = response.body; + + // collect signatures from 3 wallets + let signatures = await TestUtils.getXSignatures(txHexCreateTokenToDecode, wallets, 3); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex: txHexCreateTokenToDecode, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + const txCreateToken = response.body; + const tokenUid = txCreateToken.hash; + + await TestUtils.pauseForWsUpdate(); + await TestUtils.waitForTxReceived(wallet1.walletId, response.body.hash); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.hash).toBeDefined(); + + // # Mint + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: tokenUid, + amount: 1, + address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex: txHexMintTokenToDecode } = response.body; + + // collect signatures from 3 wallets + signatures = await TestUtils.getXSignatures(txHexMintTokenToDecode, wallets, 3); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex: txHexMintTokenToDecode, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + await TestUtils.waitForTxReceived(wallet1.walletId, response.body.hash); + + // # Melt + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: tokenUid, + amount: 100, + deposit_address: address, + change_address: address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex: txHexMeltTokenToDecode } = response.body; + + // collect signatures from 3 wallets + signatures = await TestUtils.getXSignatures(txHexMeltTokenToDecode, wallets, 3); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex: txHexMeltTokenToDecode, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + await TestUtils.pauseForWsUpdate(); + await TestUtils.waitForTxReceived(wallet1.walletId, response.body.hash); + + const mctBalance = await wallet1.getBalance(tokenUid); + expect(mctBalance.available).toBe(1); + + // Decode create token txHex + response = await TestUtils.request + .post('/wallet/decode') + .send({ txHex: txHexCreateTokenToDecode }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + tx: { + completeSignatures: false, + tokens: [], + inputs: [ + { + decoded: { + type: 'MultiSig', + address: expect.any(String), + timelock: null, + }, + txId: expect.any(String), + index: 0, + token: '00', + value: expect.any(Number), // we have little control over input value + tokenData: 0, + token_data: 0, + script: expect.any(String), + signed: false, + mine: true, + }, + ], + outputs: expect.arrayContaining([ + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + token: '00', + value: expect.any(Number), // change output, we have little control over its value + tokenData: 0, + token_data: 0, + script: expect.any(String), + type: 'p2sh', + }, + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + value: 100, + tokenData: 1, + token_data: 1, + script: expect.any(String), + type: 'p2sh', + }, + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + value: 1, + tokenData: 129, + token_data: 129, + script: expect.any(String), + type: 'p2sh', + }, + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + value: 2, + tokenData: 129, + token_data: 129, + script: expect.any(String), + type: 'p2sh', + }, + ]), + }, + balance: { + '00': { + tokens: { available: -1, locked: 0 }, + authorities: { + melt: { available: 0, locked: 0 }, + mint: { available: 0, locked: 0 }, + }, + }, + undefined: { // token here is undefined because it is not already created + tokens: { available: 100, locked: 0 }, + authorities: { + melt: { available: 1, locked: 0 }, + mint: { available: 1, locked: 0 }, + }, + }, + }, + }); + + // Decode mint token txHex + response = await TestUtils.request + .post('/wallet/decode') + .send({ txHex: txHexMintTokenToDecode }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + tx: expect.objectContaining({ + completeSignatures: false, + tokens: [txCreateToken.hash], + inputs: [ + expect.objectContaining({ + decoded: { + type: 'MultiSig', + address: expect.any(String), + timelock: null, + }, + txId: expect.any(String), + index: 0, + token: '00', + value: expect.any(Number), + tokenData: 0, + token_data: 0, + script: expect.any(String), + signed: false, + mine: true, + }), + expect.objectContaining({ + decoded: { + type: 'MultiSig', + address: expect.any(String), + timelock: null, + }, + txId: expect.any(String), + index: 2, + token: tokenUid, + value: 1, + tokenData: 129, + token_data: 129, + script: expect.any(String), + signed: false, + mine: true, + }), + ], + outputs: expect.arrayContaining([ + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + token: '00', + value: expect.any(Number), + tokenData: 0, + token_data: 0, + script: expect.any(String), + type: 'p2sh', + }, + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + token: tokenUid, + value: 1, + tokenData: 1, + token_data: 1, + script: expect.any(String), + type: 'p2sh', + }, + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + token: tokenUid, + value: 1, + tokenData: 129, + token_data: 129, + script: expect.any(String), + type: 'p2sh', + }, + ]), + }), + balance: { + '00': { + tokens: { available: -1, locked: 0 }, + authorities: { + melt: { available: 0, locked: 0 }, + mint: { available: 0, locked: 0 }, + }, + }, + [tokenUid]: { + tokens: { available: 1, locked: 0 }, + authorities: { + melt: { available: 0, locked: 0 }, + mint: { available: 0, locked: 0 }, + }, + }, + }, + }); + + // Decode melt token txHex + response = await TestUtils.request + .post('/wallet/decode') + .send({ txHex: txHexMeltTokenToDecode }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + tx: { + tokens: [ + txCreateToken.hash, + ], + completeSignatures: false, + inputs: [ + { + decoded: { + address: expect.any(String), + timelock: null, + type: 'MultiSig', + }, + index: 3, + mine: true, + script: expect.any(String), + signed: false, + token: tokenUid, + tokenData: 129, + token_data: 129, + txId: expect.any(String), + value: 2, + }, + { + decoded: { + address: expect.any(String), + timelock: null, + type: 'MultiSig', + }, + index: 1, + mine: true, + script: expect.any(String), + signed: false, + token: tokenUid, + tokenData: 1, + token_data: 1, + txId: expect.any(String), + value: 1, + }, + { + decoded: { + address: expect.any(String), + timelock: null, + type: 'MultiSig', + }, + index: 1, + mine: true, + script: expect.any(String), + signed: false, + token: tokenUid, + tokenData: 1, + token_data: 1, + txId: expect.any(String), + value: 100, + }, + ], + outputs: [ + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + script: expect.any(String), + token: tokenUid, + tokenData: 1, + token_data: 1, + type: 'p2sh', + value: 1, + }, + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + script: expect.any(String), + token: tokenUid, + tokenData: 129, + token_data: 129, + type: 'p2sh', + value: 2, + }, + { + decoded: { + address: expect.any(String), + mine: true, + timelock: null, + }, + script: expect.any(String), + token: '00', + tokenData: 0, + token_data: 0, + type: 'p2sh', + value: 1, + }, + ], + }, + balance: { + '00': { + tokens: { available: 1, locked: 0 }, + authorities: { + melt: { available: 0, locked: 0 }, + mint: { available: 0, locked: 0 }, + }, + }, + [tokenUid]: { + tokens: { available: -100, locked: 0 }, + authorities: { + melt: { available: 0, locked: 0 }, + mint: { available: 0, locked: 0 }, + }, + }, + }, + }); + }); + + // create-token + it('should fail to send a create-token transaction with less than minimum signatures', async () => { + const address = precalculatedMultisig[0].addresses[0]; + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 100, + address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex } = response.body; + + // collect signatures from 3 wallets + const signatures = await TestUtils.getXSignatures(txHex, wallets, 2); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + + await TestUtils.pauseForWsUpdate(); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toBeDefined(); + expect(response.body.error).toEqual('Quantity of signatures different than expected. Expected 3 Received 2'); + }); + + it('Should fail to send a create-token transaction with incorrect signatures', async () => { + const address = await wallet1.getAddressAt(0); + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 100, + address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex } = response.body; + + // collect signatures from 3 wallets + const signatures = await TestUtils.getXSignatures(txHex, wallets, 2); + const pubkey = precalculatedMultisig[0].multisigDebugData.pubkeys[0]; + const invalidP2shSig = new hathorLib.P2SHSignature(pubkey, {}); + signatures.push(invalidP2shSig.serialize()); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + + await TestUtils.pauseForWsUpdate(); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toBeDefined(); + expect(response.body.error).toEqual('Signatures are incompatible with redeemScript'); + }); + + it('Should fail to send a create-token transaction with more than min signatures', async () => { + const address = await wallet1.getAddressAt(0); + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 100, + address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex } = response.body; + + // collect signatures from 3 wallets + const signatures = await TestUtils.getXSignatures(txHex, wallets, 4); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + + await TestUtils.pauseForWsUpdate(); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toBeDefined(); + }); + + // mint-token + it('Should fail to send a mint-token transaction with less than minimum signatures', async () => { + const address = await wallet1.getAddressAt(0); + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: tokenA.uid, + amount: 500, + address, + change_address: address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex } = response.body; + + // collect signatures from 3 wallets + const signatures = await TestUtils.getXSignatures(txHex, wallets, 2); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + + await TestUtils.pauseForWsUpdate(); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toBeDefined(); + }); + + it('Should fail to send a mint-token transaction with incorrect signatures', async () => { + const address = precalculatedMultisig[0].addresses[0]; + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: tokenA.uid, + amount: 500, + address, + change_address: address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex } = response.body; + + // collect signatures from 3 wallets + const signatures = await TestUtils.getXSignatures(txHex, wallets, 2); + const pubkey = precalculatedMultisig[0].multisigDebugData.pubkeys[0]; + const invalidP2shSig = new hathorLib.P2SHSignature(pubkey, {}); + signatures.push(invalidP2shSig.serialize()); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + + await TestUtils.pauseForWsUpdate(); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toBeDefined(); + }); + + it('Should fail to send a mint-token transaction with more than min signatures', async () => { + const address = precalculatedMultisig[0].addresses[0]; + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: tokenA.uid, + amount: 500, + address, + change_address: address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex } = response.body; + + // collect signatures from 3 wallets + const signatures = await TestUtils.getXSignatures(txHex, wallets, 4); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + + await TestUtils.pauseForWsUpdate(); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toBeDefined(); + }); + + // melt-token + it('Should fail to send a melt-token transaction with less than minimum signatures', async () => { + const address = precalculatedMultisig[0].addresses[0]; + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: tokenA.uid, + amount: 500, + deposit_address: address, + change_address: address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex } = response.body; + + // collect signatures from 3 wallets + const signatures = await TestUtils.getXSignatures(txHex, wallets, 2); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + + await TestUtils.pauseForWsUpdate(); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toBeDefined(); + }); + + it('Should fail to send a melt-token transaction with incorrect signatures', async () => { + const address = precalculatedMultisig[0].addresses[0]; + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: tokenA.uid, + amount: 500, + deposit_address: address, + change_address: address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex } = response.body; + + // collect signatures from 3 wallets + const signatures = await TestUtils.getXSignatures(txHex, wallets, 2); + const pubkey = precalculatedMultisig[0].multisigDebugData.pubkeys[0]; + const invalidP2shSig = new hathorLib.P2SHSignature(pubkey, {}); + signatures.push(invalidP2shSig.serialize()); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + + await TestUtils.pauseForWsUpdate(); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toBeDefined(); + }); + + it('Should fail to send a melt-token transaction with more than min signatures', async () => { + const address = precalculatedMultisig[0].addresses[0]; + let response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: tokenA.uid, + amount: 500, + deposit_address: address, + change_address: address, + }) + .set({ 'x-wallet-id': wallet1.walletId }); + expect(response.body.success).toBe(true); + + const { txHex } = response.body; + + // collect signatures from 3 wallets + const signatures = await TestUtils.getXSignatures(txHex, wallets, 4); + + // try to send + response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send({ txHex, signatures }) + .set({ 'x-wallet-id': wallet1.walletId }); + + await TestUtils.pauseForWsUpdate(); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.error).toBeDefined(); + }); }); diff --git a/__tests__/integration/utils/test-utils-integration.js b/__tests__/integration/utils/test-utils-integration.js index 4b3096c5..b5f3cb4b 100644 --- a/__tests__/integration/utils/test-utils-integration.js +++ b/__tests__/integration/utils/test-utils-integration.js @@ -870,4 +870,53 @@ export class TestUtils { }); /* eslint-enable no-async-promise-executor */ } + + /** + * Generate X signatures from a set of wallets for a given txHex. + * The wallets to compose the signature are selected randomly. + * + * @param {string} txHex the encoded transaction proposal to be signed. + * @param {WalletHelper[]} wallets a set of wallets corresponding to the multisig wallet. + * @param {number} xSignatures amount of signatures. + * + * @return {Promise} a set of signatures of X degree. + */ + static async getXSignatures(txHex, wallets, xSignatures) { + if (!txHex) { + throw new Error(`'txHex' cannot be null or undefined`); + } + if (!Array.isArray(wallets)) { + throw new Error(`'wallets' is not an array.`); + } + if (!(Number.isInteger(xSignatures) && Number.isFinite(xSignatures))) { + throw new Error(`'xSignatures' is not a finite integer.`); + } + if (xSignatures < 1) { + throw new Error(`'xSignatures' should be at least 1.`); + } + if (xSignatures > wallets.length) { + throw new Error(`'xSignatures' can not be greater than the number of wallets.`); + } + + const selectedWallets = {}; + let xLeft = xSignatures; + while (xLeft > 0) { + const rndIdx = Math.floor(Math.random() * wallets.length); + if (selectedWallets[rndIdx]) { + continue; + } + selectedWallets[rndIdx] = wallets[rndIdx]; + xLeft--; + } + + const signatures = []; + for (const wallet of Object.values(selectedWallets)) { + if (!('getSignatures' in wallet)) { + throw new Error(`'wallet' is not an instance of WalletHelper.`); + } + signatures.push(await wallet.getSignatures(txHex)); + } + + return signatures; + } } diff --git a/__tests__/integration/utils/wallet-helper.js b/__tests__/integration/utils/wallet-helper.js index 2c70fe18..825472e3 100644 --- a/__tests__/integration/utils/wallet-helper.js +++ b/__tests__/integration/utils/wallet-helper.js @@ -301,6 +301,101 @@ export class WalletHelper { return transaction; } + /** + * Build a create-token transaction proposal. + * + * @param params + * @param {string} params.name Long name of the token + * @param {string} params.symbol Token symbol + * @param {number} params.amount of tokens to generate + * @param {string} [params.address] Destination address for the custom token + * @returns {Promise} txHex as the transaction proposal + * + * XXX: only supports multisig + */ + async buildCreateToken(params) { + if (!this.#multisig) { + throw new Error('The wallet is not a multisig.'); + } + + // no param null is allowed + const paramKeys = ['name', 'symbol', 'amount', 'address']; + for (const key of paramKeys) { + if (!params[key]) { + throw new Error(`'${key} param can not be null or undefined.'`); + } + } + + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: params.name, + symbol: params.symbol, + amount: params.amount, + address: params.address, + change_address: params.address, + }) + .set({ 'x-wallet-id': this.#walletId }); + return response.body.txHex; + } + + /** + * Sign the transaction proposal and send the signed transaction. + * + * @param params + * @param {string} params.txHex Transaction proposal + * @param {string} params.wallets Set of all wallets composing the multisig wallet + * @param {number} params.xSignatures Number X of signatures to be generated for the transaction + * @param {boolean} [params.dontLogErrors] Skip logging errors. + * @returns {Promise} Sent transaction + * + * XXX: only supports multisig + */ + async signAndPush(params) { + if (!this.#multisig) { + throw new Error('The wallet is not a multisig.'); + } + const { + txHex = null, + wallets = null, + xSignatures = null, + } = params; + const _params = { txHex, wallets, xSignatures }; + + // no param null is allowed + for (const key of Object.keys(_params)) { + if (!_params[key]) { + throw new Error(`'${key} param can not be null.'`); + } + } + + // Creating the request body from mandatory and optional parameters + const tokenCreationBody = { + txHex: _params.txHex, + signatures: await TestUtils.getXSignatures( + _params.txHex, + _params.wallets, + _params.xSignatures + ), + }; + + const newTokenResponse = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/sign-and-push') + .send(tokenCreationBody) + .set({ 'x-wallet-id': this.#walletId }); + await TestUtils.pauseForWsUpdate(); + + const transaction = TestUtils.handleTransactionResponse({ + methodName: 'createToken', + requestBody: tokenCreationBody, + txResponse: newTokenResponse, + dontLogErrors: params.dontLogErrors + }); + + TestUtils.log('Sign and push transaction', { transaction }); + return transaction; + } + /** * @typedef SendTxInputParam * @property {string} [hash] UTXO transaction hash 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-melt-tokens.test.js b/__tests__/p2sh/tx-proposal-melt-tokens.test.js new file mode 100644 index 00000000..695e5101 --- /dev/null +++ b/__tests__/p2sh/tx-proposal-melt-tokens.test.js @@ -0,0 +1,254 @@ +import hathorLib from '@hathor/wallet-lib'; +import TestUtils from '../test-utils'; +import { TOKEN_DATA, AUTHORITY_VALUE } from '../integration/configuration/test-constants'; + +const walletId = 'stub_melt_tokens'; + +describe('melt-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/melt-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 melt token with empty token', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-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 melt token with amount 0', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-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 melt token without funds to cover it', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-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); + expect(response.body.error).toEqual('Not enough tokens to melt: 1000000000000 requested, 200 available'); + }); + + it('should return 200 with a valid body selecting deposit address', async () => { + const depositAddress = TestUtils.multisigAddresses[2]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 100, + deposit_address: depositAddress, + }) + .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', depositAddress])); + expect(tx.inputs).toEqual(expect.not.arrayContaining([ + expect.objectContaining({ + data: expect.any(Object), + }), + ])); + }); + + it('should not accept melt token with empty deposit address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + deposit_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/melt-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 melt token with empty change address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-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 melt 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/melt-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 without melt authority', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + create_melt: false, + }) + .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'])); + expect(tx.outputs).toHaveLength(1); + }); + + it('should not accept melt token with empty melt authority address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + melt_authority_address: '', + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + describe('should return 200 with a valid body selecting an external authority address', () => { + const testCases = [ + // amount under 100 do not generate withdraw output + { title: 'without withdrawal', amount: 1, expectedOutputLen: 2 }, + // amount equal or greater than 100 generates withdraw output + { title: 'with withdrawal', amount: 100, expectedOutputLen: 3 } + ]; + for (const cut of testCases) { + it(cut.title, async () => { + const mintAuthorityAddress = TestUtils.addresses[1]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: cut.amount, + melt_authority_address: mintAuthorityAddress, + allow_external_melt_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(cut.expectedOutputLen); + const authorityOutputs = tx.outputs.filter(o => TOKEN_DATA.isAuthorityToken(o.tokenData)); + expect(authorityOutputs).toHaveLength(1); + expect(authorityOutputs[0].value).toBe(AUTHORITY_VALUE.MELT); + }); + } + }); + + describe('should return 200 with a valid body selecting an authority address', () => { + const testCases = [ + // amount under 100 do not generate withdraw output + { title: 'without withdrawal', amount: 1, expectedOutputLen: 2 }, + // amount equal or greater than 100 generates withdraw output + { title: 'with withdrawal', amount: 100, expectedOutputLen: 3 } + ]; + for (const cut of testCases) { + it(cut.title, async () => { + const mintAuthorityAddress = TestUtils.multisigAddresses[2]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/melt-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: cut.amount, + melt_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(cut.expectedOutputLen); + const authorityOutputs = tx.outputs.filter(o => TOKEN_DATA.isAuthorityToken(o.tokenData)); + expect(authorityOutputs).toHaveLength(1); + expect(authorityOutputs[0].value).toBe(AUTHORITY_VALUE.MELT); + }); + } + }); +}); 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..52ffe324 --- /dev/null +++ b/__tests__/p2sh/tx-proposal-mint-tokens.test.js @@ -0,0 +1,233 @@ +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'])); + expect(tx.inputs).toEqual(expect.not.arrayContaining([ + expect.objectContaining({ + data: expect.any(Object), + }), + ])); + }); + + 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.error).toEqual('Not enough HTR tokens for deposit: 10000000000 required, 89600 available'); + }); + + 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([TestUtils.multisigAddresses[1], 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([TestUtils.multisigAddresses[1], 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 without mint authority', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/mint-tokens') + .send({ + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 1, + create_mint: false, + }) + .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([TestUtils.multisigAddresses[1]])); + expect(tx.outputs).toHaveLength(2); + }); + + 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([TestUtils.multisigAddresses[1], 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([TestUtils.multisigAddresses[1], 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/__tests__/p2sh/tx-proposal-new-token.test.js b/__tests__/p2sh/tx-proposal-new-token.test.js new file mode 100644 index 00000000..20078443 --- /dev/null +++ b/__tests__/p2sh/tx-proposal-new-token.test.js @@ -0,0 +1,438 @@ +import hathorLib from '@hathor/wallet-lib'; +import TestUtils from '../test-utils'; +import { TOKEN_DATA, AUTHORITY_VALUE } from '../integration/configuration/test-constants'; + +const walletId = 'stub_p2sh_create_tx_proposal'; + +describe('create-token tx-proposal api', () => { + beforeAll(async () => { + global.config.multisig = TestUtils.multisigData; + await TestUtils.startWallet({ walletId, multisig: true }); + }); + + 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/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + 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([TestUtils.multisigAddresses[1]])); + expect(tx.inputs).toEqual(expect.not.arrayContaining([ + expect.objectContaining({ + data: expect.any(Object), + }), + ])); + }); + + it('should not accept create token with empty name', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: '', + symbol: 'MCT', + amount: 1, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should not accept create token with empty symbol', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: '', + amount: 1, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should not accept create token with amount 0', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 0, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should not accept create token without funds to cover it', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + 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/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + 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([TestUtils.multisigAddresses[1], address])); + }); + + it('should not accept create token with empty address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + 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/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + 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([TestUtils.multisigAddresses[1], changeAddress])); + }); + + it('should not accept create token with empty change address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + change_address: '', + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should not accept create 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/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + 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 without mint or melt authority', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: false, + create_melt: false, + }) + .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([TestUtils.multisigAddresses[1]])); + expect(tx.outputs).toHaveLength(2); + }); + + it('should return 200 with a valid body with a mint authority only', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: true, + create_melt: false, + }) + .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([TestUtils.multisigAddresses[1]])); + 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 return 200 with a valid body with a mint authority selecting an authority address', async () => { + const mintAuthorityAddress = TestUtils.multisigAddresses[2]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: true, + mint_authority_address: mintAuthorityAddress, + create_melt: false, + }) + .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([TestUtils.multisigAddresses[1], 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 create token with empty mint authority address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: true, + mint_authority_address: '', + create_melt: false, + }) + .set({ 'x-wallet-id': walletId }); + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + }); + + it('should return 200 with a valid body with a mint authority selecting an external authority address', async () => { + const mintAuthorityAddress = TestUtils.addresses[1]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: true, + mint_authority_address: mintAuthorityAddress, + allow_external_mint_authority_address: true, + create_melt: false, + }) + .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([TestUtils.multisigAddresses[1], 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 return 200 with a valid body with a melt authority only', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: false, + create_melt: 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([TestUtils.multisigAddresses[1]])); + 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.MELT); + }); + + it('should return 200 with a valid body with a melt authority selecting an authority address', async () => { + const meltAuthorityAddress = TestUtils.multisigAddresses[2]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: false, + create_melt: true, + melt_authority_address: meltAuthorityAddress, + }) + .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([TestUtils.multisigAddresses[1], meltAuthorityAddress])); + 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.MELT); + }); + + it('should not accept create token with empty melt authority address', async () => { + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: false, + create_melt: true, + melt_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 with a melt authority selecting an external authority address', async () => { + const meltAuthorityAddress = TestUtils.addresses[1]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: false, + create_melt: true, + melt_authority_address: meltAuthorityAddress, + allow_external_melt_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([TestUtils.multisigAddresses[1], meltAuthorityAddress])); + 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.MELT); + }); + + it('should return 200 with a valid body with a mint and melt authority selecting an authority address', async () => { + const mintAuthorityAddress = TestUtils.multisigAddresses[2]; + const meltAuthorityAddress = TestUtils.multisigAddresses[3]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: true, + mint_authority_address: mintAuthorityAddress, + create_melt: true, + melt_authority_address: meltAuthorityAddress, + }) + .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([ + TestUtils.multisigAddresses[1], + mintAuthorityAddress, + meltAuthorityAddress + ])); + expect(tx.outputs).toHaveLength(4); + const authorityOutputs = tx.outputs.filter(o => TOKEN_DATA.isAuthorityToken(o.tokenData)); + expect(authorityOutputs).toHaveLength(2); + expect(authorityOutputs[0].value).toBe(AUTHORITY_VALUE.MINT); + expect(authorityOutputs[1].value).toBe(AUTHORITY_VALUE.MELT); + }); + + it('should return 200 with a valid body with a mint and melt authority selecting an external authority address', async () => { + const mintAuthorityAddress = TestUtils.addresses[2]; + const meltAuthorityAddress = TestUtils.addresses[3]; + const response = await TestUtils.request + .post('/wallet/p2sh/tx-proposal/create-token') + .send({ + name: 'My Custom Token', + symbol: 'MCT', + amount: 1, + create_mint: true, + mint_authority_address: mintAuthorityAddress, + allow_external_mint_authority_address: true, + create_melt: true, + melt_authority_address: meltAuthorityAddress, + allow_external_melt_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([ + TestUtils.multisigAddresses[1], + mintAuthorityAddress, + meltAuthorityAddress + ])); + expect(tx.outputs).toHaveLength(4); + const authorityOutputs = tx.outputs.filter(o => TOKEN_DATA.isAuthorityToken(o.tokenData)); + expect(authorityOutputs).toHaveLength(2); + expect(authorityOutputs[0].value).toBe(AUTHORITY_VALUE.MINT); + expect(authorityOutputs[1].value).toBe(AUTHORITY_VALUE.MELT); + }); +}); diff --git a/package-lock.json b/package-lock.json index 5495be1b..49f068fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet-headless", - "version": "0.22.0-rc3", + "version": "0.22.0-rc4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1197,13 +1197,14 @@ } }, "@hathor/wallet-lib": { - "version": "1.0.0-rc5", - "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.0.0-rc5.tgz", - "integrity": "sha512-RHjYWhRI9eaaVJaycpz3hZIXhRd/fAvJMnKHCv4mW/TrybU+Q+QIk2Qdiqd1rvgYMukParoKCuclGqqjTHWM/A==", + "version": "1.0.0-rc8", + "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.0.0-rc8.tgz", + "integrity": "sha512-Ti65+yYkSf5TMNHjS+iX4UQUfq2wPCGBufR5BRfq+PU7wAcxftEgSlZat/Qdcent9/fEE59V0bLg7uH1JeYvEQ==", "requires": { "axios": "^0.21.4", "bitcore-lib": "^8.25.10", "bitcore-mnemonic": "^8.25.10", + "buffer": "^6.0.3", "crypto-js": "^3.1.9-1", "isomorphic-ws": "^4.0.1", "level": "^8.0.0", @@ -1695,9 +1696,9 @@ } }, "acorn": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", - "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "acorn-globals": { @@ -4231,12 +4232,6 @@ "eslint-visitor-keys": "^3.3.0" }, "dependencies": { - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true - }, "eslint-visitor-keys": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", @@ -6263,6 +6258,12 @@ "xml-name-validator": "^3.0.0" }, "dependencies": { + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + }, "ws": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", diff --git a/package.json b/package.json index 40a4eb09..54464fa8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet-headless", - "version": "0.22.0-rc3", + "version": "0.22.0-rc4", "description": "Hathor Wallet Headless, i.e., without graphical user interface", "main": "index.js", "engines": { @@ -12,7 +12,7 @@ "@babel/node": "^7.13.0", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/preset-env": "^7.13.5", - "@hathor/wallet-lib": "^1.0.0-rc5", + "@hathor/wallet-lib": "^1.0.0-rc8", "express": "^4.17.1", "express-validator": "^6.10.0", "lodash": "^4.17.11", diff --git a/src/api-docs.js b/src/api-docs.js index 4e65811a..a695367a 100644 --- a/src/api-docs.js +++ b/src/api-docs.js @@ -6,7 +6,7 @@ const apiDoc = { info: { title: 'Headless Hathor Wallet API', description: 'This wallet is fully controlled through an HTTP API.', - version: '0.22.0-rc3', + version: '0.22.0-rc4', }, produces: ['application/json'], components: { @@ -542,9 +542,9 @@ const apiDoc = { }, '/wallet/decode': { post: { - summary: 'Decode tx hex into human readable inputs and outputs.', + summary: 'Decode tx hex or serialized partial tx into human readable inputs and outputs with metadata to assist informed decision-making. To obtain input metadata, this method retrieves a transaction per input from the wallet\'s transaction history. If the required transaction is not located, the method queries the fullnode for the transaction details.', requestBody: { - description: 'Transaction hex representation', + description: 'Transaction hex representation or a partial transaction serialization.', required: true, content: { 'application/json': { @@ -575,21 +575,51 @@ const apiDoc = { value: { success: true, tx: { + completeSignatures: false, + tokens: [], outputs: [ { - address: 'Wk2j7odPbC4Y98xKYBCFyNogxaRimU6BUj', + decoded: { + address: 'Wk2j7odPbC4Y98xKYBCFyNogxaRimU6BUj', + mine: true, + timelock: null, + }, + token: '00', value: 100, - tokenData: 1, - token: '006e18f3c303892076a12e68b5c9c30afe9a96a528f0f3385898001858f9c35d' + tokenData: 0, + token_data: 0, + script: 'dqkUISAnpOn9Vo269QBvOfBeWJTLx82IrA==', + type: 'p2sh', } ], inputs: [ { + decoded: { + type: 'MultiSig', + address: 'Wk2j7odPbC4Y98xKYBCFyNogxaRimU6BUj', + timelock: null, + }, txId: '006e18f3c303892076a12e68b5c9c30afe9a96a528f0f3385898001858f9c35d', index: 0, + token: '00', + value: 100, + tokenData: 0, + token_data: 0, + script: 'dqkUISAnpOn9Vo269QBvOfBeWJTLx82IrA==', + signed: false, + mine: true, } ] }, + balance: { + '00': { + tokens: { available: 0, locked: 0 }, + authorities: { + melt: { available: 0, locked: 0 }, + mint: { available: 0, locked: 0 }, + }, + }, + }, }, }, }, @@ -786,6 +816,100 @@ const apiDoc = { }, }, }, + '/wallet/tx-proposal/melt-tokens': { + post: { + summary: 'Get the hex representation of a melt tokens transaction without input data.', + parameters: [ + { + name: 'x-wallet-id', + in: 'header', + description: 'Define the key of the corresponding wallet it will be executed the request.', + required: true, + schema: { + type: 'string', + }, + }, + ], + requestBody: { + description: 'Data to melt tokens.', + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['token', 'amount'], + properties: { + token: { + type: 'string', + description: 'UID of the token to melt.' + }, + amount: { + type: 'integer', + description: 'The amount of tokens to melt. It must be an integer with the value in cents, i.e., 123 means 1.23.' + }, + deposit_address: { + type: 'string', + description: 'Optional deposit_address to send the deposit HTR received after the melt.' + }, + change_address: { + type: 'string', + description: 'Optional address to send the change amount of custom tokens after melt.' + }, + melt_authority_address: { + type: 'string', + description: 'Optional address to send the new melt authority output created.' + }, + allow_external_melt_authority_address: { + type: 'boolean', + description: 'If the melt authority address is allowed to be from another wallet. Default is false.' + }, + } + }, + examples: { + data: { + summary: 'Data to melt tokens.', + value: { + token: '000016392ed330ed99ff0f74e4169a8d257fd1d07d3b38c4f8ecf21a78f10efa', + amount: 100, + } + } + } + } + } + }, + responses: { + 200: { + description: 'Melt tokens.', + content: { + 'application/json': { + examples: { + error: { + summary: 'Insuficient amount of tokens', + value: { success: false, error: "There aren't enough tokens in the inputs to melt." } + }, + success: { + summary: 'Success', + value: { success: true, txHex: '0001010201000016392ed330ed99ff0f74e4169a8d257fd1d07d3b38c4f8ecf21a78f10efa000016392ed330ed99ff0f74e4169a8d257fd1d07d3b38c4f8ecf21a78f10efa030069463044022011ebd6bfa5e49d504542e58b55dc79cea70e97069546eae2d4b7f470f7b9d6d302203cb7739de69eded37a5ef15e1d669768057a68d4b6089911ee63d746100a6a1b2102a5c1b462ccdcd8b4bb2cf672e0672576420c3102ecbe74da15b2cf56cf49b4a5000016392ed330ed99ff0f74e4169a8d257fd1d07d3b38c4f8ecf21a78f10efa010069463044022011ebd6bfa5e49d504542e58b55dc79cea70e97069546eae2d4b7f470f7b9d6d302203cb7739de69eded37a5ef15e1d669768057a68d4b6089911ee63d746100a6a1b2102a5c1b462ccdcd8b4bb2cf672e0672576420c3102ecbe74da15b2cf56cf49b4a500000002810017a91462d397b360118b99a8d35892366074fe16fa6f09874031fc9b86a7279e649b63f60000000000' } + }, + 'wallet-not-ready': { + summary: 'Wallet is not ready yet', + value: { success: false, message: 'Wallet is not ready.', state: 1 } + }, + 'no-wallet-id': { + summary: 'No wallet id parameter', + value: { success: false, message: "Parameter 'wallet-id' is required." } + }, + 'invalid-wallet-id': { + summary: 'Wallet id parameter is invalid', + value: { success: false, message: 'Invalid wallet-id parameter.' } + }, + }, + }, + }, + }, + }, + }, + }, '/wallet/tx-proposal/add-signatures': { post: { summary: 'Add signatures to the transaction and return the txHex with the signatures.', @@ -1175,6 +1299,216 @@ const apiDoc = { }, }, }, + '/wallet/p2sh/tx-proposal/create-token': { + post: { + summary: 'Get the hex representation of a create a token transaction without input data.', + parameters: [ + { + name: 'x-wallet-id', + in: 'header', + description: 'Define the key of the corresponding wallet it will be executed the request.', + required: true, + schema: { + type: 'string', + }, + } + ], + requestBody: { + description: 'Data to create the token.', + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['name', 'symbol', 'amount'], + properties: { + name: { + type: 'string', + description: 'Name of the token.' + }, + symbol: { + type: 'string', + description: 'Symbol of the token.' + }, + amount: { + type: 'integer', + description: 'The amount of tokens to mint. It must be an integer with the value in cents, i.e., 123 means 1.23.' + }, + address: { + type: 'string', + description: 'Optional destination address of the minted tokens.' + }, + change_address: { + type: 'string', + description: 'Optional address to send the change amount.' + }, + create_mint: { + type: 'boolean', + description: 'If should create mint authority for the created token. Default is true.' + }, + mint_authority_address: { + type: 'string', + description: 'Optional address to send the mint authority output created.' + }, + allow_external_mint_authority_address: { + type: 'boolean', + description: 'If the mint authority address is allowed to be from another wallet. Default is false.' + }, + create_melt: { + type: 'boolean', + description: 'If should create melt authority for the created token. Default is true.' + }, + melt_authority_address: { + type: 'string', + description: 'Optional address to send the melt authority output created.' + }, + allow_external_melt_authority_address: { + type: 'boolean', + description: 'If the melt authority address is allowed to be from another wallet. Default is false.' + }, + } + }, + examples: { + data: { + summary: 'Data to create the token', + value: { + name: 'Test Coin', + symbol: 'TSC', + amount: 100, + } + } + } + } + } + }, + responses: { + 200: { + description: 'Create the token', + content: { + 'application/json': { + examples: { + error: { + summary: 'Insuficient amount of tokens', + value: { success: false, error: "Don't have enough HTR funds to mint this amount." } + }, + success: { + summary: 'Success', + value: { success: true, txHex: '00020104000016392ed330ed99ff0f74e4169a8d257fd1d07d3b38c4f8ecf21a78f10efa000069463044022074a1bf9c2d56e887558f459573d75df647acbde7b90de3502b7220425ff69dcb022000e0690e43ad306adef7f59bd07cf817ab8da29bce5cd82ede61ffb99cb460022102a5c1b462ccdcd8b4bb2cf672e0672576420c3102ecbe74da15b2cf56cf49b4a5000001f1000017a91462d397b360118b99a8d35892366074fe16fa6f098700000001010017a91462d397b360118b99a8d35892366074fe16fa6f098700000001810017a91462d397b360118b99a8d35892366074fe16fa6f098700000002810017a91462d397b360118b99a8d35892366074fe16fa6f098701164d7920437573746f6d20546f6b656e204d616e75616c034d43544031dbcd5cef20c5649b59130000000000' } + }, + 'wallet-not-ready': { + summary: 'Wallet is not ready yet', + value: { success: false, message: 'Wallet is not ready.', state: 1 } + }, + 'no-wallet-id': { + summary: 'No wallet id parameter', + value: { success: false, message: "Parameter 'wallet-id' is required." } + }, + 'invalid-wallet-id': { + summary: 'Wallet id parameter is invalid', + value: { success: false, message: 'Invalid wallet-id parameter.' } + }, + }, + }, + }, + }, + }, + }, + }, + '/wallet/p2sh/tx-proposal/mint-tokens': { + post: { + summary: 'Get the hex representation of a mint tokens transaction without input data.', + parameters: [ + { + name: 'x-wallet-id', + in: 'header', + description: 'Define the key of the corresponding wallet it will be executed the request.', + required: true, + schema: { + type: 'string', + }, + } + ], + requestBody: { + description: 'Data to mint tokens.', + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['token', 'amount'], + properties: { + token: { + type: 'string', + description: 'UID of the token to mint.' + }, + amount: { + type: 'integer', + description: 'The amount of tokens to mint. It must be an integer with the value in cents, i.e., 123 means 1.23.' + }, + address: { + type: 'string', + description: 'Optional destination address of the minted tokens.' + }, + change_address: { + type: 'string', + description: 'Optional address to send the change amount.' + }, + create_mint: { + type: 'boolean', + description: 'If should create another mint authority for the created token. Default is true.' + }, + mint_authority_address: { + type: 'string', + description: 'Optional address to send the new mint authority output created.' + }, + allow_external_mint_authority_address: { + type: 'boolean', + description: 'If the mint authority address is allowed to be from another wallet. Default is false.' + }, + } + }, + examples: { + data: { + token: '0000073b972162f70061f61cf0082b7a47263cc1659a05976aca5cd01b3351ee', + amount: 100, + } + } + } + } + } + }, + responses: { + 200: { + description: 'Mint tokens.', + content: { + 'application/json': { + examples: { + error: { + summary: 'Insufficient amount of tokens', + value: { success: false, error: "Don't have enough HTR funds to mint this amount." } + }, + success: { + summary: 'Success', + value: { success: true, txHex: '0001010203000016392ed330ed99ff0f74e4169a8d257fd1d07d3b38c4f8ecf21a78f10efa000016392ed330ed99ff0f74e4169a8d257fd1d07d3b38c4f8ecf21a78f10efa00006946304402201166baf8513c0bfd21edcb169a4df5645ca826b22b6ed22d13945628094a04c502204f382ef9e6b903397b2bcaaed5316b0bb54212037a30e5cda7a5cf4d785b8f332102a5c1b462ccdcd8b4bb2cf672e0672576420c3102ecbe74da15b2cf56cf49b4a5000016392ed330ed99ff0f74e4169a8d257fd1d07d3b38c4f8ecf21a78f10efa02006946304402201166baf8513c0bfd21edcb169a4df5645ca826b22b6ed22d13945628094a04c502204f382ef9e6b903397b2bcaaed5316b0bb54212037a30e5cda7a5cf4d785b8f332102a5c1b462ccdcd8b4bb2cf672e0672576420c3102ecbe74da15b2cf56cf49b4a5000001f1000017a91462d397b360118b99a8d35892366074fe16fa6f098700000001010017a91462d397b360118b99a8d35892366074fe16fa6f098700000001810017a91462d397b360118b99a8d35892366074fe16fa6f098740327a9b3baad50b649b5f1d0000000000' } + }, + 'wallet-not-ready': { + summary: 'Wallet is not ready yet', + value: { success: false, message: 'Wallet is not ready.', state: 1 } + }, + 'no-wallet-id': { + summary: 'No wallet id parameter', + value: { success: false, message: "Parameter 'wallet-id' is required." } + }, + 'invalid-wallet-id': { + summary: 'Wallet id parameter is invalid', + value: { success: false, message: 'Invalid wallet-id parameter.' } + }, + }, + }, + }, + }, + }, + }, '/wallet/p2sh/tx-proposal/get-my-signatures': { post: { summary: 'Get the signatures for all inputs from the wallet', diff --git a/src/controllers/wallet/p2sh/tx-proposal.controller.js b/src/controllers/wallet/p2sh/tx-proposal.controller.js index f6b7b277..32ae1c06 100644 --- a/src/controllers/wallet/p2sh/tx-proposal.controller.js +++ b/src/controllers/wallet/p2sh/tx-proposal.controller.js @@ -56,6 +56,129 @@ async function buildTxProposal(req, res) { } } +async function buildCreateTokenTxProposal(req, res) { + const validationResult = parametersValidation(req); + if (!validationResult.success) { + res.status(400).json(validationResult); + return; + } + + const { + name, + symbol, + amount, + } = req.body; + const address = req.body.address || null; + const changeAddress = req.body.change_address || null; + const createMint = req.body.create_mint ?? true; + const mintAuthorityAddress = req.body.mint_authority_address || null; + const allowExternalMintAuthorityAddress = req.body.allow_external_mint_authority_address || null; + const createMelt = req.body.create_melt ?? true; + const meltAuthorityAddress = req.body.melt_authority_address || null; + const allowExternalMeltAuthorityAddress = req.body.allow_external_melt_authority_address || null; + + try { + const createTokenTransaction = await req.wallet.prepareCreateNewToken(name, symbol, amount, { + address, + changeAddress, + createMint, + mintAuthorityAddress, + allowExternalMintAuthorityAddress, + createMelt, + meltAuthorityAddress, + allowExternalMeltAuthorityAddress, + signTx: false, + }); + + res.send({ success: true, txHex: createTokenTransaction.toHex() }); + } catch (err) { + res.send({ success: false, error: err.message }); + } +} + +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 createAnotherMint = req.body.create_mint ?? true; + 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, + createAnotherMint, + mintAuthorityAddress, + allowExternalMintAuthorityAddress, + signTx: false, + } + ); + res.send({ success: true, txHex: mintTokenTransaction.toHex() }); + } catch (err) { + res.send({ success: false, error: err.message }); + } +} + +async function buildMeltTokensTxProposal(req, res) { + const validationResult = parametersValidation(req); + if (!validationResult.success) { + res.status(400).json(validationResult); + return; + } + + const { + token, + amount, + } = req.body; + + const depositAddress = req.body.deposit_address || null; + const changeAddress = req.body.change_address || null; + const createAnotherMelt = req.body.create_melt ?? true; + const meltAuthorityAddress = req.body.melt_authority_address || null; + const allowExternalMeltAuthorityAddress = req.body.allow_external_melt_authority_address || null; + + try { + if (changeAddress && !await req.wallet.isAddressMine(changeAddress)) { + throw new Error('Change address is not from this wallet'); + } + + const meltTokenTransaction = await req.wallet.prepareMeltTokensData( + token, + amount, + { + address: depositAddress, + changeAddress, + createAnotherMelt, + meltAuthorityAddress, + allowExternalMeltAuthorityAddress, + signTx: false, + } + ); + + res.send({ success: true, txHex: meltTokenTransaction.toHex() }); + } catch (err) { + console.error(err); + res.send({ success: false, error: err.message }); + } +} + async function getMySignatures(req, res) { const validationResult = parametersValidation(req); if (!validationResult.success) { @@ -144,6 +267,9 @@ async function signAndPush(req, res) { module.exports = { buildTxProposal, + buildCreateTokenTxProposal, + buildMintTokensTxProposal, + buildMeltTokensTxProposal, getMySignatures, signTx, signAndPush, diff --git a/src/controllers/wallet/wallet.controller.js b/src/controllers/wallet/wallet.controller.js old mode 100644 new mode 100755 index dd878dff..8611d863 --- a/src/controllers/wallet/wallet.controller.js +++ b/src/controllers/wallet/wallet.controller.js @@ -5,15 +5,15 @@ * LICENSE file in the root directory of this source tree. */ -// import is used because there is an issue with winston logger when using require ref: #262 -import logger from '../../logger'; // eslint-disable-line import/no-import-module-exports +const { HATHOR_TOKEN_CONFIG } = require('@hathor/wallet-lib/lib/constants'); // eslint-disable-line import/no-import-module-exports -const { txApi, walletApi, WalletType, constants: hathorLibConstants, helpersUtils, errors, tokensUtils, PartialTx } = require('@hathor/wallet-lib'); +const { txApi, walletApi, WalletType, constants: hathorLibConstants, helpersUtils, errors, tokensUtils, transactionUtils, PartialTx } = require('@hathor/wallet-lib'); const { matchedData } = require('express-validator'); +// import is used because there is an issue with winston logger when using require ref: #262 const { parametersValidation } = require('../../helpers/validations.helper'); const { lock, lockTypes } = require('../../lock'); const { cantSendTxErrorMessage, friendlyWalletState } = require('../../helpers/constants'); -const { mapTxReturn, prepareTxFunds } = require('../../helpers/tx.helper'); +const { mapTxReturn, prepareTxFunds, getTx } = require('../../helpers/tx.helper'); const { initializedWallets } = require('../../services/wallets.service'); const { removeAllWalletProposals } = require('../../services/atomic-swap.service'); @@ -318,15 +318,49 @@ async function decodeTx(req, res) { } tx = partial.getTx(); } + const data = { tokens: tx.tokens, - inputs: tx.inputs.map(input => ({ txId: input.hash, index: input.index })), + inputs: await tx.inputs.reduce(async (acc, input) => { + // the accumulator must be awaited to enforce the sequence processing + const results = await acc; + + const _tx = await getTx(req.wallet, input.hash); + if (!_tx) { + throw new Error(`Could not find input transaction for txId ${input.hash}`); + } + + const utxo = _tx.outputs[input.index]; + return [ + ...results, + { + txId: input.hash, + index: input.index, + decoded: utxo.decoded, + token: utxo.token, + value: utxo.value, + // This is required by transactionUtils.getTxBalance + // It should be ignored by users + token_data: utxo.token_data, + // User facing duplication to keep scheme consistency + tokenData: utxo.token_data, + script: utxo.script, + signed: !!input.data, + mine: await req.wallet.isAddressMine(utxo.decoded.address), + }, + ]; + }, []), outputs: [], }; + for (const output of tx.outputs) { output.parseScript(req.wallet.getNetworkObject()); const outputData = { value: output.value, + // This is required by transactionUtils.getTxBalance + // It should be ignored by users + token_data: output.tokenData, + // User facing duplication to keep scheme consistency tokenData: output.tokenData, script: output.script.toString('base64'), type: output.decodedScript.getType(), @@ -334,6 +368,8 @@ async function decodeTx(req, res) { }; if (output.tokenData !== 0) { outputData.token = tx.tokens[output.getTokenIndex()]; + } else { + outputData.token = HATHOR_TOKEN_CONFIG.uid; } switch (outputData.type) { case 'data': @@ -347,11 +383,40 @@ async function decodeTx(req, res) { outputData.decoded = { address: output.decodedScript.address.base58, timelock: output.decodedScript.timelock, + mine: await req.wallet.isAddressMine(output.decodedScript.address.base58) }; } data.outputs.push(outputData); } - res.send({ success: true, tx: data }); + + // True if all the inputs are signed, false otherwise + data.completeSignatures = data.inputs.length > 0 + ? data.inputs.every(input => input.signed) // true until find a false statement + : false; // empty data.inputs + + // Get balance + const balance = {}; + const balanceObj = await transactionUtils.getTxBalance(data, req.wallet.storage); + for (const token of Object.keys(balanceObj)) { + balance[token] = ({ + tokens: { + available: balanceObj[token].tokens.unlocked, + locked: balanceObj[token].tokens.locked, + }, + authorities: { + melt: { + available: balanceObj[token].authorities.melt.unlocked, + locked: balanceObj[token].authorities.melt.locked, + }, + mint: { + available: balanceObj[token].authorities.mint.unlocked, + locked: balanceObj[token].authorities.mint.locked, + }, + } + }); + } + + res.send({ success: true, tx: data, balance }); } catch (err) { res.send({ success: false, error: err.message }); } @@ -403,7 +468,7 @@ async function sendTx(req, res) { } catch (err) { const ret = { success: false, error: err.message }; if (debug) { - logger.debug('/send-tx failed', { + console.debug('/send-tx failed', { body: JSON.stringify(req.body), response: JSON.stringify(ret), }); diff --git a/src/helpers/tx.helper.js b/src/helpers/tx.helper.js index 11003cbd..afcd87bd 100644 --- a/src/helpers/tx.helper.js +++ b/src/helpers/tx.helper.js @@ -201,8 +201,42 @@ async function prepareTxFunds(wallet, outputs, inputs, defaultToken = HATHOR_TOK }; } +/** + * Best effort to get a transaction. First look up at the storage to get a trasaction + * from the history. If not found, query the fullnode about the transaction. + * If the transaction is not found in the fullnode, return null. + * + * @param {HathorWallet} wallet + * @param {string} id Hash of the transaction to get data from + * @return {Promise} Data from the transaction to get. + * Can be null if both the wallet and fullnode does not contain the tx. + * + * @see DecodedTx at {@link https://github.com/HathorNetwork/hathor-wallet-lib/blob/bc94221cece2bd6d7b64d971ef30b7d593f07e42/src/new/wallet.js#L1058} + * @see FullNodeTx at {@link https://github.com/HathorNetwork/hathor-wallet-lib/blob/bc94221cece2bd6d7b64d971ef30b7d593f07e42/src/wallet/types.ts#L500} + */ +async function getTx(wallet, id) { + const tx = await wallet.getTx(id); + if (tx) { + return tx; + } + + try { + const response = await wallet.getFullTxById(id); + if (response.success) { + return response.tx; + } + + console.warn('Failed to get transaction from fullnode.', response.message); + return null; + } catch (error) { + console.error('Error while getting transaction from fullnode.', error); + return null; + } +} + module.exports = { mapTxReturn, getUtxosToFillTx, prepareTxFunds, + getTx }; diff --git a/src/routes/wallet/p2sh/tx-proposal.routes.js b/src/routes/wallet/p2sh/tx-proposal.routes.js index 694007e5..cff2b845 100644 --- a/src/routes/wallet/p2sh/tx-proposal.routes.js +++ b/src/routes/wallet/p2sh/tx-proposal.routes.js @@ -6,10 +6,13 @@ */ const { Router } = require('express'); -const { checkSchema } = require('express-validator'); +const { checkSchema, body } = require('express-validator'); const { buildTxProposal, + buildCreateTokenTxProposal, getMySignatures, + buildMintTokensTxProposal, + buildMeltTokensTxProposal, signTx, signAndPush, } = require('../../../controllers/wallet/p2sh/tx-proposal.controller'); @@ -81,6 +84,46 @@ txProposalRouter.post( buildTxProposal, ); +txProposalRouter.post( + '/create-token', + body('name').isString().notEmpty(), + body('symbol').isString().notEmpty(), + body('amount').isInt({ min: 1 }).toInt(), + body('address').isString().notEmpty().optional(), + body('change_address').isString().notEmpty().optional(), + body('create_mint').isBoolean().optional(), + body('mint_authority_address').isString().notEmpty().optional(), + body('allow_external_mint_authority_address').isBoolean().optional().toBoolean(), + body('create_melt').isBoolean().optional(), + body('melt_authority_address').isString().notEmpty().optional(), + body('allow_external_melt_authority_address').isBoolean().optional().toBoolean(), + buildCreateTokenTxProposal, +); + +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('create_mint').isBoolean().optional(), + body('mint_authority_address').isString().notEmpty().optional(), + body('allow_external_mint_authority_address').isBoolean().optional().toBoolean(), + buildMintTokensTxProposal, +); + +txProposalRouter.post( + '/melt-tokens', + body('token').isString().notEmpty(), + body('amount').isInt({ min: 1 }).toInt(), + body('deposit_address').isString().notEmpty().optional(), + body('change_address').isString().notEmpty().optional(), + body('create_melt').isBoolean().optional(), + body('melt_authority_address').isString().notEmpty().optional(), + body('allow_external_melt_authority_address').isBoolean().optional().toBoolean(), + buildMeltTokensTxProposal, +); + /* * XXX: Currently only works for P2SH MultiSig signatures, but can be enhanced to * include P2PKH Signatures once the wallet-lib adds support.