Skip to content

Commit 130f0aa

Browse files
authored
feat: tx-template api (#518)
* feat: tx-template route
1 parent a612c5b commit 130f0aa

File tree

9 files changed

+661
-5
lines changed

9 files changed

+661
-5
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { TestUtils } from './utils/test-utils-integration';
2+
import { WalletHelper } from './utils/wallet-helper';
3+
4+
describe('tx-template build', () => {
5+
let wallet;
6+
7+
beforeAll(async () => {
8+
try {
9+
// Wallet with initial funds to send a transaction
10+
wallet = WalletHelper.getPrecalculatedWallet('tx-template');
11+
12+
await WalletHelper.startMultipleWalletsForTest([wallet]);
13+
await wallet.injectFunds(100);
14+
} catch (err) {
15+
TestUtils.logError(err.stack);
16+
}
17+
});
18+
19+
afterAll(async () => {
20+
await wallet.stop();
21+
});
22+
23+
it('should not allow an invalid template', async () => {
24+
const response = await TestUtils.request
25+
.post('/wallet/tx-template/build')
26+
.send([
27+
{ type: 'invalid-action' },
28+
])
29+
.set({ 'x-wallet-id': wallet.walletId });
30+
31+
expect(response.status).toBe(400);
32+
expect(response.body.success).toBe(false);
33+
});
34+
35+
it('should fail from a valid template with invalid data', async () => {
36+
const response = await TestUtils.request
37+
.post('/wallet/tx-template/build')
38+
.send([
39+
{ type: 'action/setvar', name: 'addr', value: Array(34).fill('H').join('') },
40+
{ type: 'input/utxo', fill: 1 },
41+
{ type: 'output/token', amount: 1, address: '{addr}' },
42+
])
43+
.set({ 'x-wallet-id': wallet.walletId });
44+
45+
expect(response.status).toBe(200);
46+
expect(response.body.success).toBe(false);
47+
});
48+
49+
it('should build a transaction from a valid template', async () => {
50+
const response = await TestUtils.request
51+
.post('/wallet/tx-template/build')
52+
.send([
53+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
54+
{ type: 'input/utxo', fill: 1 },
55+
{ type: 'output/token', amount: 1, address: '{addr}' },
56+
])
57+
.set({ 'x-wallet-id': wallet.walletId });
58+
59+
expect(response.status).toBe(200);
60+
expect(response.body.success).toBe(true);
61+
});
62+
63+
it('should build a transaction from a valid template sending query args', async () => {
64+
const response = await TestUtils.request
65+
.post('/wallet/tx-template/build')
66+
.send([
67+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
68+
{ type: 'input/utxo', fill: 1 },
69+
{ type: 'output/token', amount: 1, address: '{addr}' },
70+
])
71+
.query({ debug: true })
72+
.set({ 'x-wallet-id': wallet.walletId });
73+
74+
expect(response.status).toBe(200);
75+
expect(response.body.success).toBe(true);
76+
});
77+
78+
it('should build a transaction that creates a token', async () => {
79+
const response = await TestUtils.request
80+
.post('/wallet/tx-template/build')
81+
.send([
82+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
83+
{ type: 'action/config', tokenName: 'Test Token', tokenSymbol: 'TT' },
84+
{ type: 'input/utxo', fill: 1 },
85+
{ type: 'output/token', amount: 100, address: '{addr}', useCreatedToken: true },
86+
])
87+
.query({ debug: true })
88+
.set({ 'x-wallet-id': wallet.walletId });
89+
90+
expect(response.status).toBe(200);
91+
expect(response.body.success).toBe(true);
92+
});
93+
});
94+
95+
describe('tx-template run', () => {
96+
let wallet;
97+
let tokenId;
98+
99+
beforeAll(async () => {
100+
try {
101+
// Wallet with initial funds to send a transaction
102+
wallet = WalletHelper.getPrecalculatedWallet('tx-template');
103+
104+
await WalletHelper.startMultipleWalletsForTest([wallet]);
105+
await wallet.injectFunds(100);
106+
} catch (err) {
107+
TestUtils.logError(err.stack);
108+
}
109+
});
110+
111+
afterAll(async () => {
112+
await wallet.stop();
113+
});
114+
115+
it('should not allow an invalid template', async () => {
116+
const response = await TestUtils.request
117+
.post('/wallet/tx-template/run')
118+
.send([
119+
{ type: 'invalid-action' },
120+
])
121+
.set({ 'x-wallet-id': wallet.walletId });
122+
123+
expect(response.status).toBe(400);
124+
expect(response.body.success).toBe(false);
125+
});
126+
127+
it('should fail from a valid template with invalid data', async () => {
128+
const response = await TestUtils.request
129+
.post('/wallet/tx-template/run')
130+
.send([
131+
{ type: 'action/setvar', name: 'addr', value: Array(34).fill('H').join('') },
132+
{ type: 'input/utxo', fill: 1 },
133+
{ type: 'output/token', amount: 1, address: '{addr}' },
134+
])
135+
.set({ 'x-wallet-id': wallet.walletId });
136+
137+
expect(response.status).toBe(200);
138+
expect(response.body.success).toBe(false);
139+
});
140+
141+
it('should build a transaction from a valid template', async () => {
142+
const response = await TestUtils.request
143+
.post('/wallet/tx-template/run')
144+
.send([
145+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
146+
{ type: 'input/utxo', fill: 1 },
147+
{ type: 'output/token', amount: 1, address: '{addr}' },
148+
])
149+
.set({ 'x-wallet-id': wallet.walletId });
150+
151+
expect(response.status).toBe(200);
152+
expect(response.body.success).toBe(true);
153+
expect(response.body.hash).toBeDefined();
154+
await TestUtils.waitForTxReceived(wallet.walletId, response.body.hash);
155+
});
156+
157+
it('should build a transaction from a valid template sending query args', async () => {
158+
const response = await TestUtils.request
159+
.post('/wallet/tx-template/run')
160+
.send([
161+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
162+
{ type: 'input/utxo', fill: 1 },
163+
{ type: 'output/token', amount: 1, address: '{addr}' },
164+
])
165+
.query({ debug: true })
166+
.set({ 'x-wallet-id': wallet.walletId });
167+
168+
expect(response.status).toBe(200);
169+
expect(response.body.success).toBe(true);
170+
expect(response.body.hash).toBeDefined();
171+
await TestUtils.waitForTxReceived(wallet.walletId, response.body.hash);
172+
});
173+
174+
it('should build a transaction that creates a token', async () => {
175+
const response = await TestUtils.request
176+
.post('/wallet/tx-template/run')
177+
.send([
178+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
179+
{ type: 'action/config', tokenName: 'Test Token', tokenSymbol: 'TT' },
180+
{ type: 'input/utxo', fill: 1 },
181+
{ type: 'output/token', amount: 100, address: '{addr}', useCreatedToken: true },
182+
{ type: 'output/authority', authority: 'mint', address: '{addr}', useCreatedToken: true },
183+
])
184+
.query({ debug: true })
185+
.set({ 'x-wallet-id': wallet.walletId });
186+
187+
expect(response.status).toBe(200);
188+
expect(response.body.success).toBe(true);
189+
expect(response.body.hash).toBeDefined();
190+
expect(response.body.version).toEqual(2);
191+
tokenId = response.body.hash;
192+
193+
await TestUtils.waitForTxReceived(wallet.walletId, response.body.hash);
194+
195+
const balance = await wallet.getBalance(tokenId);
196+
expect(balance.available).toEqual(100);
197+
const balanceHTR = await wallet.getBalance('00');
198+
expect(balanceHTR.available).toEqual(99);
199+
});
200+
201+
it('should build a transaction that mint tokens', async () => {
202+
const response = await TestUtils.request
203+
.post('/wallet/tx-template/run')
204+
.send([
205+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
206+
{ type: 'input/utxo', fill: 1 },
207+
{ type: 'input/authority', authority: 'mint', token: tokenId },
208+
{ type: 'output/token', amount: 100, address: '{addr}', token: tokenId },
209+
{ type: 'output/authority', authority: 'mint', address: '{addr}', token: tokenId },
210+
])
211+
.query({ debug: true })
212+
.set({ 'x-wallet-id': wallet.walletId });
213+
214+
expect(response.status).toBe(200);
215+
expect(response.body.success).toBe(true);
216+
expect(response.body.hash).toBeDefined();
217+
expect(response.body.version).toEqual(1);
218+
219+
await TestUtils.waitForTxReceived(wallet.walletId, response.body.hash);
220+
221+
const balance = await wallet.getBalance(tokenId);
222+
expect(balance.available).toEqual(200);
223+
const balanceHTR = await wallet.getBalance('00');
224+
expect(balanceHTR.available).toEqual(98);
225+
});
226+
});

__tests__/tx-template.test.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { HathorWallet, SendTransaction } from '@hathor/wallet-lib';
2+
import TestUtils from './test-utils';
3+
// eslint-disable-next-line import/named
4+
import { cantSendTxErrorMessage } from '../src/helpers/constants';
5+
6+
const walletId = 'stub_simple_send_tx';
7+
8+
describe('tx-template build api', () => {
9+
beforeAll(async () => {
10+
await TestUtils.startWallet({ walletId, preCalculatedAddresses: TestUtils.addresses });
11+
});
12+
13+
afterAll(async () => {
14+
await TestUtils.stopWallet({ walletId });
15+
});
16+
17+
it('should receive an error when trying to do concurrent builds (lock/unlock behavior)', async () => {
18+
const spy = jest.spyOn(HathorWallet.prototype, 'buildTxTemplate')
19+
.mockImplementation(async () => (new Promise(resolve => {
20+
setTimeout(() => resolve({
21+
toHex: () => ('tx-hex'),
22+
}), 1000);
23+
})));
24+
try {
25+
const promise1 = TestUtils.request
26+
.post('/wallet/tx-template/build')
27+
.send([
28+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
29+
{ type: 'input/utxo', fill: 1 },
30+
{ type: 'output/token', amount: 1, address: '{addr}' },
31+
])
32+
.set({ 'x-wallet-id': walletId });
33+
const promise2 = TestUtils.request
34+
.post('/wallet/tx-template/build')
35+
.send([
36+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
37+
{ type: 'input/utxo', fill: 1 },
38+
{ type: 'output/token', amount: 1, address: '{addr}' },
39+
])
40+
.set({ 'x-wallet-id': walletId });
41+
42+
const [response1, response2] = await Promise.all([promise1, promise2]);
43+
expect(response1.status).toBe(200);
44+
expect(response1.body.txHex).toEqual('tx-hex');
45+
expect(response2.status).toBe(200);
46+
expect(response2.body.success).toBe(false);
47+
expect(response2.body.error).toBe(cantSendTxErrorMessage);
48+
} finally {
49+
spy.mockRestore();
50+
}
51+
});
52+
});
53+
54+
describe('tx-template run api', () => {
55+
beforeAll(async () => {
56+
await TestUtils.startWallet({ walletId, preCalculatedAddresses: TestUtils.addresses });
57+
});
58+
59+
afterAll(async () => {
60+
await TestUtils.stopWallet({ walletId });
61+
});
62+
63+
it('should receive an error when trying to do concurrent builds (lock/unlock behavior)', async () => {
64+
const spy = jest.spyOn(SendTransaction.prototype, 'updateOutputSelected').mockImplementation(async () => {
65+
await new Promise(resolve => {
66+
setTimeout(resolve, 1000);
67+
});
68+
});
69+
try {
70+
const promise1 = TestUtils.request
71+
.post('/wallet/tx-template/run')
72+
.send([
73+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
74+
{ type: 'input/utxo', fill: 1 },
75+
{ type: 'output/token', amount: 1, address: '{addr}' },
76+
])
77+
.set({ 'x-wallet-id': walletId });
78+
const promise2 = TestUtils.request
79+
.post('/wallet/tx-template/run')
80+
.send([
81+
{ type: 'action/setvar', name: 'addr', call: { method: 'get_wallet_address' } },
82+
{ type: 'input/utxo', fill: 1 },
83+
{ type: 'output/token', amount: 1, address: '{addr}' },
84+
])
85+
.set({ 'x-wallet-id': walletId });
86+
87+
const [response1, response2] = await Promise.all([promise1, promise2]);
88+
expect(response1.status).toBe(200);
89+
expect(response1.body.hash).toBeTruthy();
90+
expect(response2.status).toBe(200);
91+
expect(response2.body.success).toBe(false);
92+
expect(response2.body.error).toBe(cantSendTxErrorMessage);
93+
} finally {
94+
spy.mockRestore();
95+
}
96+
});
97+
});

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
"axios": "1.7.7",
1515
"express": "4.18.2",
1616
"express-validator": "6.10.0",
17-
"validator": "13.11.0",
1817
"jsonwebtoken": "9.0.2",
1918
"lodash": "4.17.21",
2019
"morgan": "1.10.0",
2120
"uuid4": "2.0.3",
21+
"validator": "13.11.0",
2222
"winston": "3.12.0",
23-
"yargs": "17.7.2"
23+
"yargs": "17.7.2",
24+
"zod": "3.23.8"
2425
},
2526
"scripts": {
2627
"build": "babel src -d dist --source-maps",
@@ -68,7 +69,7 @@
6869
"jest": "29.7.0",
6970
"mock-socket": "9.3.1",
7071
"nodemon": "3.1.0",
71-
"supertest": "7.0.0",
72-
"superagent": "9.0.2"
72+
"superagent": "9.0.2",
73+
"supertest": "7.0.0"
7374
}
7475
}

0 commit comments

Comments
 (0)