Skip to content

Commit 701a06c

Browse files
authored
Merge pull request #408 from HathorNetwork/feat/partial-tx-facade
feat: partial tx facade
2 parents cb7c96b + d3f1133 commit 701a06c

File tree

12 files changed

+959
-129
lines changed

12 files changed

+959
-129
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
3535
"no-await-in-loop": "off",
3636
"no-plusplus": "off",
37-
"guard-for-in": "off"
37+
"guard-for-in": "off",
38+
"no-bitwise": "off",
3839
},
3940
"overrides": [
4041
{
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { GenesisWalletHelper } from './helpers/genesis-wallet.helper';
2+
import {
3+
createTokenHelper,
4+
DEFAULT_PIN_CODE,
5+
generateWalletHelper,
6+
stopAllWallets,
7+
waitForTxReceived,
8+
} from './helpers/wallet.helper';
9+
import { loggers } from './utils/logger.util';
10+
import { HATHOR_TOKEN_CONFIG } from '../../src/constants';
11+
import SendTransaction from '../../src/new/sendTransaction';
12+
import PartialTxProposal from '../../src/wallet/partialTxProposal';
13+
import storage from '../../src/storage';
14+
15+
describe('partial tx proposal', () => {
16+
afterEach(async () => {
17+
await stopAllWallets();
18+
await GenesisWalletHelper.clearListeners();
19+
});
20+
21+
it('Should exchange tokens between wallets', async () => {
22+
// Create the wallet
23+
const hWallet1 = await generateWalletHelper();
24+
const hWallet2 = await generateWalletHelper();
25+
const network = hWallet1.getNetworkObject();
26+
27+
// Injecting funds and creating a new custom token
28+
await GenesisWalletHelper.injectFunds(hWallet1.getAddressAtIndex(0), 103);
29+
const { hash: token1Uid } = await createTokenHelper(
30+
hWallet1,
31+
'Token1',
32+
'TK1',
33+
200,
34+
);
35+
36+
// Injecting funds and creating a new custom token
37+
await GenesisWalletHelper.injectFunds(hWallet2.getAddressAtIndex(0), 10);
38+
const { hash: token2Uid } = await createTokenHelper(
39+
hWallet2,
40+
'Token2',
41+
'TK2',
42+
1000,
43+
);
44+
45+
// Get the balance states before the exchange
46+
const w1HTRBefore = await hWallet1.getBalance(HATHOR_TOKEN_CONFIG.uid);
47+
const w1Tk1Before = await hWallet1.getBalance(token1Uid);
48+
const w1Tk2Before = await hWallet1.getBalance(token2Uid);
49+
50+
const w2HTRBefore = await hWallet2.getBalance(HATHOR_TOKEN_CONFIG.uid);
51+
const w2Tk2Before = await hWallet2.getBalance(token2Uid);
52+
const w2Tk1Before = await hWallet2.getBalance(token1Uid);
53+
loggers.test.log('Balances before', {
54+
wallet1: {
55+
HTR: w1HTRBefore,
56+
Tk1: w1Tk1Before,
57+
Tk2: w1Tk2Before,
58+
},
59+
wallet2: {
60+
HTR: w2HTRBefore,
61+
Tk1: w2Tk1Before,
62+
Tk2: w2Tk2Before,
63+
},
64+
});
65+
66+
/**
67+
* The exchange will be:
68+
*
69+
* Wallet1 will send 100 HTR and 100 TK1
70+
* Wallet2 will send 1000 TK2
71+
*/
72+
const proposal = new PartialTxProposal(network);
73+
// Wallet1 side
74+
proposal.addSend(hWallet1, HATHOR_TOKEN_CONFIG.uid, 100);
75+
proposal.addSend(hWallet1, token1Uid, 100);
76+
proposal.addReceive(hWallet1, token2Uid, 1000);
77+
expect(proposal.partialTx.isComplete()).toBeFalsy();
78+
// Wallet2 side
79+
proposal.addSend(hWallet2, token2Uid, 1000);
80+
proposal.addReceive(hWallet2, HATHOR_TOKEN_CONFIG.uid, 100);
81+
proposal.addReceive(hWallet2, token1Uid, 100);
82+
expect(proposal.partialTx.isComplete()).toBeTruthy();
83+
84+
const serialized = proposal.partialTx.serialize();
85+
const proposal1 = PartialTxProposal.fromPartialTx(serialized, network);
86+
storage.setStore(hWallet1.store);
87+
await proposal1.signData(DEFAULT_PIN_CODE, true);
88+
expect(proposal1.signatures.isComplete()).toBeFalsy();
89+
90+
const proposal2 = PartialTxProposal.fromPartialTx(serialized, network);
91+
storage.setStore(hWallet2.store);
92+
await proposal2.signData(DEFAULT_PIN_CODE, true);
93+
94+
expect(proposal2.signatures.isComplete()).toBeFalsy();
95+
96+
proposal2.signatures.addSignatures(proposal1.signatures.serialize());
97+
expect(proposal2.signatures.isComplete()).toBeTruthy();
98+
99+
const transaction = proposal2.prepareTx();
100+
const sendTransaction = new SendTransaction({ transaction, network });
101+
const tx = await sendTransaction.runFromMining();
102+
expect(tx.hash).toBeDefined();
103+
104+
await waitForTxReceived(hWallet1, tx.hash);
105+
106+
// Get the balance states before the exchange
107+
const w1HTRAfter = await hWallet1.getBalance(HATHOR_TOKEN_CONFIG.uid);
108+
const w1Tk1After = await hWallet1.getBalance(token1Uid);
109+
const w1Tk2After = await hWallet1.getBalance(token2Uid);
110+
111+
const w2HTRAfter = await hWallet2.getBalance(HATHOR_TOKEN_CONFIG.uid);
112+
const w2Tk2After = await hWallet2.getBalance(token2Uid);
113+
const w2Tk1After = await hWallet2.getBalance(token1Uid);
114+
115+
loggers.test.log('Balances after', {
116+
wallet1: {
117+
HTR: w1HTRAfter,
118+
Tk1: w1Tk1After,
119+
Tk2: w1Tk2After,
120+
},
121+
wallet2: {
122+
HTR: w2HTRAfter,
123+
Tk1: w2Tk1After,
124+
Tk2: w2Tk2After,
125+
},
126+
});
127+
128+
// Check balance HTR
129+
expect(w1HTRAfter[0].balance.unlocked - w1HTRBefore[0].balance.unlocked).toEqual(-100);
130+
expect(w1HTRAfter[0].balance.locked - w1HTRBefore[0].balance.locked).toEqual(0);
131+
expect(w2HTRAfter[0].balance.unlocked - w2HTRBefore[0].balance.unlocked).toEqual(100);
132+
expect(w2HTRAfter[0].balance.locked - w2HTRBefore[0].balance.locked).toEqual(0);
133+
134+
// Check balance token1
135+
expect(w1Tk1After[0].balance.unlocked - w1Tk1Before[0].balance.unlocked).toEqual(-100);
136+
expect(w1Tk1After[0].balance.locked - w1Tk1Before[0].balance.locked).toEqual(0);
137+
expect(w2Tk1After[0].balance.unlocked - w2Tk1Before[0].balance.unlocked).toEqual(100);
138+
expect(w2Tk1After[0].balance.locked - w2Tk1Before[0].balance.locked).toEqual(0);
139+
140+
// Check balance token2
141+
expect(w1Tk2After[0].balance.unlocked - w1Tk2Before[0].balance.unlocked).toEqual(1000);
142+
expect(w1Tk2After[0].balance.locked - w1Tk2Before[0].balance.locked).toEqual(0);
143+
expect(w2Tk2After[0].balance.unlocked - w2Tk2Before[0].balance.unlocked).toEqual(-1000);
144+
expect(w2Tk2After[0].balance.locked - w2Tk2Before[0].balance.locked).toEqual(0);
145+
});
146+
});

__tests__/models/partial_tx.test.js

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ import dateFormatter from '../../src/date';
1212

1313

1414
import { UnsupportedScriptError } from '../../src/errors';
15-
import { HATHOR_TOKEN_CONFIG, DEFAULT_TX_VERSION, TOKEN_AUTHORITY_MASK } from '../../src/constants';
15+
import {
16+
HATHOR_TOKEN_CONFIG,
17+
DEFAULT_TX_VERSION,
18+
TOKEN_AUTHORITY_MASK,
19+
TOKEN_MINT_MASK,
20+
TOKEN_MELT_MASK,
21+
} from '../../src/constants';
1622
import helpers from '../../src/utils/helpers';
1723
import txApi from '../../src/api/txApi';
1824
import P2PKH from '../../src/models/p2pkh';
@@ -132,11 +138,11 @@ describe('PartialTx.isComplete', () => {
132138
expect(partialTx.isComplete()).toBe(false);
133139

134140
// Outputs have less than inputs for 1 token
135-
partialTx.outputs.push(new ProposalOutput(1, Buffer.from([]), { token: '1', tokenData: 1 }));
141+
partialTx.outputs.push(new ProposalOutput(1, Buffer.from([]), { token: '1' }));
136142
expect(partialTx.isComplete()).toBe(false);
137143

138144
// Outputs have more than inputs for 1 token
139-
partialTx.outputs.push(new ProposalOutput(2, Buffer.from([]), { token: '2', tokenData: 2 }));
145+
partialTx.outputs.push(new ProposalOutput(2, Buffer.from([]), { token: '2' }));
140146
expect(partialTx.isComplete()).toBe(false);
141147

142148
// Missing token from inputs
@@ -178,7 +184,7 @@ describe('PartialTx.isComplete', () => {
178184
new ProposalOutput(1, Buffer.from([])),
179185
new ProposalOutput(1, Buffer.from([]), { token: '2' }),
180186
// Add authority output for token 2
181-
new ProposalOutput(1, Buffer.from([]), { token: '2', tokenData: TOKEN_AUTHORITY_MASK | 1 }),
187+
new ProposalOutput(1, Buffer.from([]), { token: '2', authorities: TOKEN_MINT_MASK }),
182188
];
183189

184190
expect(partialTx.isComplete()).toBe(true);
@@ -192,16 +198,19 @@ describe('PartialTx.addInput', () => {
192198
const partialTx = new PartialTx(testnet);
193199
const expected = []
194200

195-
expected.push(expect.objectContaining({ hash: 'hash1', index: 0, token: '1', tokenData: 1, value: 1, address: 'W123' }));
196-
partialTx.addInput('hash1', 0, 1, 'W123', { token: '1', tokenData: 1 });
201+
// Passing all optional arguments
202+
expected.push(expect.objectContaining({ hash: 'hash1', index: 0, token: '1', authorities: 0, value: 1, address: 'W123' }));
203+
partialTx.addInput('hash1', 0, 1, 'W123', { token: '1', authorities: 0 });
197204
expect(partialTx.inputs).toEqual(expected);
198205

199-
expected.push(expect.objectContaining({ hash: 'hash2', index: 1, token: '00', tokenData: 0, value: 27, address: 'Wabc' }));
206+
// Default options, HTR
207+
expected.push(expect.objectContaining({ hash: 'hash2', index: 1, token: '00', authorities: 0, value: 27, address: 'Wabc' }));
200208
partialTx.addInput('hash2', 1, 27, 'Wabc');
201209
expect(partialTx.inputs).toEqual(expected);
202210

203-
expected.push(expect.objectContaining({ hash: 'hash3', index: 10, token: '1', tokenData: TOKEN_AUTHORITY_MASK | 3, value: 1056, address: 'W1b3' }));
204-
partialTx.addInput('hash3', 10, 1056, 'W1b3', { token: '1', tokenData: TOKEN_AUTHORITY_MASK | 3 });
211+
// Authority input
212+
expected.push(expect.objectContaining({ hash: 'hash3', index: 10, token: '1', authorities: TOKEN_MINT_MASK | TOKEN_MELT_MASK, value: 1056, address: 'W1b3' }));
213+
partialTx.addInput('hash3', 10, 1056, 'W1b3', { token: '1', authorities: TOKEN_MINT_MASK | TOKEN_MELT_MASK });
205214
expect(partialTx.inputs).toEqual(expected);
206215
});
207216
});
@@ -220,17 +229,17 @@ describe('PartialTx.addOutput', () => {
220229
isChange: true,
221230
value: 27,
222231
script: expect.toMatchBuffer(Buffer.from([230, 148, 32])),
223-
tokenData: 128,
232+
authorities: TOKEN_MELT_MASK,
224233
}));
225-
partialTx.addOutput(27, Buffer.from([230, 148, 32]), { token: '1', tokenData: 128, isChange: true});
234+
partialTx.addOutput(27, Buffer.from([230, 148, 32]), { token: '1', authorities: TOKEN_MELT_MASK, isChange: true});
226235
expect(partialTx.outputs).toEqual(expected);
227236

228237
expected.push(expect.objectContaining({
229238
token: '2',
230239
isChange: false,
231240
value: 72,
232241
script: expect.toMatchBuffer(Buffer.from([1, 2, 3])),
233-
tokenData: 0,
242+
authorities: 0,
234243
}));
235244
partialTx.addOutput(72, Buffer.from([1, 2, 3]), { token: '2' });
236245
expect(partialTx.outputs).toEqual(expected);
@@ -272,14 +281,14 @@ describe('PartialTx serialization', () => {
272281
};
273282

274283
it('should serialize a transaction correctly', async () => {
275-
const expected = 'PartialTx|00010102030000389deaf5557642e5a8a26656dcf360b608160f43e7ef79b9bde8ab69a18c00000d906babfa76b092f0088530a85f4d6bae5437304820f4c7a39540d87dd00000000000584ed8ad32b00e79e1c5cf26b5969ca7cd4d93ae39b776e71cfecf7c8c780400000000000f00001976a914729181c0f3f2e3f589cc10facbb9332e0c309a7788ac0000000d01001976a9146861143f7dc6b2f9c8525315efe6fcda160a795c88ac0000000c00001976a914486bc4f1e70f242a737d3866147c7f8335c2995f88ac0000000000000000000000010000000000|WVGxdgZMHkWo2Hdrb1sEFedNdjTXzjvjPi,00,0,1b:WVGxdgZMHkWo2Hdrb1sEFedNdjTXzjvjPi,0000389deaf5557642e5a8a26656dcf360b608160f43e7ef79b9bde8ab69a18c,1,d|1:2';
284+
const expected = 'PartialTx|00010102030000389deaf5557642e5a8a26656dcf360b608160f43e7ef79b9bde8ab69a18c00000d906babfa76b092f0088530a85f4d6bae5437304820f4c7a39540d87dd00000000000584ed8ad32b00e79e1c5cf26b5969ca7cd4d93ae39b776e71cfecf7c8c780400000000000f00001976a914729181c0f3f2e3f589cc10facbb9332e0c309a7788ac0000000d01001976a9146861143f7dc6b2f9c8525315efe6fcda160a795c88ac0000000c00001976a914486bc4f1e70f242a737d3866147c7f8335c2995f88ac0000000000000000000000010000000000|WVGxdgZMHkWo2Hdrb1sEFedNdjTXzjvjPi,00,0,1b:WVGxdgZMHkWo2Hdrb1sEFedNdjTXzjvjPi,0000389deaf5557642e5a8a26656dcf360b608160f43e7ef79b9bde8ab69a18c,0,d|1:2';
276285

277286
const partialTx = new PartialTx(testnet);
278287

279288
const address = 'WVGxdgZMHkWo2Hdrb1sEFedNdjTXzjvjPi';
280289
partialTx.inputs = [
281290
new ProposalInput(txId1, 0, 27, address, { token: HATHOR_TOKEN_CONFIG.uid }),
282-
new ProposalInput(txId2, 4, 13, address, { token: testTokenConfig.uid, tokenData: 1 }),
291+
new ProposalInput(txId2, 4, 13, address, { token: testTokenConfig.uid }),
283292
];
284293
partialTx.outputs = [
285294
new ProposalOutput(15, scriptFromAddressP2PKH('WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo')),
@@ -292,7 +301,7 @@ describe('PartialTx serialization', () => {
292301
});
293302

294303
it('should deserialize a transaction correctly', () => {
295-
const serialized = 'PartialTx|00010102030000389deaf5557642e5a8a26656dcf360b608160f43e7ef79b9bde8ab69a18c00000d906babfa76b092f0088530a85f4d6bae5437304820f4c7a39540d87dd00000000000584ed8ad32b00e79e1c5cf26b5969ca7cd4d93ae39b776e71cfecf7c8c780400000000000f00001976a914729181c0f3f2e3f589cc10facbb9332e0c309a7788ac0000000d01001976a9146861143f7dc6b2f9c8525315efe6fcda160a795c88ac0000000c00001976a914486bc4f1e70f242a737d3866147c7f8335c2995f88ac0000000000000000000000010000000000|WVGxdgZMHkWo2Hdrb1sEFedNdjTXzjvjPi,00,0,1b:WVGxdgZMHkWo2Hdrb1sEFedNdjTXzjvjPi,0000389deaf5557642e5a8a26656dcf360b608160f43e7ef79b9bde8ab69a18c,1,d|1:2';
304+
const serialized = 'PartialTx|00010102030000389deaf5557642e5a8a26656dcf360b608160f43e7ef79b9bde8ab69a18c00000d906babfa76b092f0088530a85f4d6bae5437304820f4c7a39540d87dd00000000000584ed8ad32b00e79e1c5cf26b5969ca7cd4d93ae39b776e71cfecf7c8c780400000000000f00001976a914729181c0f3f2e3f589cc10facbb9332e0c309a7788ac0000000d01001976a9146861143f7dc6b2f9c8525315efe6fcda160a795c88ac0000000c00001976a914486bc4f1e70f242a737d3866147c7f8335c2995f88ac0000000000000000000000010000000000|WVGxdgZMHkWo2Hdrb1sEFedNdjTXzjvjPi,00,0,1b:WVGxdgZMHkWo2Hdrb1sEFedNdjTXzjvjPi,0000389deaf5557642e5a8a26656dcf360b608160f43e7ef79b9bde8ab69a18c,0,d|1:2';
296305
const partialTx = PartialTx.deserialize(serialized, testnet);
297306
expect(partialTx.serialize()).toBe(serialized);
298307
});
@@ -306,7 +315,7 @@ describe('PartialTx serialization', () => {
306315

307316
partialTx.inputs = [
308317
new ProposalInput(txId1, 0, 27, address, { token: HATHOR_TOKEN_CONFIG.uid }),
309-
new ProposalInput(txId2, 4, 13, address, { token: testTokenConfig.uid, tokenData: 1 }),
318+
new ProposalInput(txId2, 4, 13, address, { token: testTokenConfig.uid }),
310319
];
311320
const partialOnlyInputs = PartialTx.deserialize(partialTx.serialize(), testnet);
312321
expect(partialOnlyInputs.serialize()).toEqual(partialTx.serialize());
@@ -322,7 +331,7 @@ describe('PartialTx serialization', () => {
322331

323332
partialTx.inputs = [
324333
new ProposalInput(txId1, 0, 27, address, { token: HATHOR_TOKEN_CONFIG.uid }),
325-
new ProposalInput(txId2, 4, 13, address, { token: testTokenConfig.uid, tokenData: 1 }),
334+
new ProposalInput(txId2, 4, 13, address, { token: testTokenConfig.uid }),
326335
];
327336
const partialFull = PartialTx.deserialize(partialTx.serialize(), testnet);
328337
expect(partialFull.serialize()).toEqual(partialTx.serialize());
@@ -416,7 +425,7 @@ describe('PartialTx.validate', () => {
416425
const partialTx = new PartialTx(testnet);
417426
partialTx.inputs = [
418427
new ProposalInput(txId1, 0, 27, addr1),
419-
new ProposalInput(txId2, 4, 13, addr2, { token: testTokenConfig.uid, tokenData: 1 }),
428+
new ProposalInput(txId2, 4, 13, addr2, { token: testTokenConfig.uid }),
420429
];
421430
partialTx.outputs = [
422431
new ProposalOutput(15, scriptFromAddressP2PKH(addr2)),
@@ -427,7 +436,7 @@ describe('PartialTx.validate', () => {
427436
await expect(partialTx.validate()).resolves.toEqual(true);
428437
});
429438

430-
it('should return false if an address, value, token or tokenData is wrong', async () => {
439+
it('should return false if an address, value, token or authorities are wrong', async () => {
431440
spy.mockImplementation(async (txId, cb) => {
432441
return new Promise(resolve => {
433442
process.nextTick(() => {
@@ -440,7 +449,7 @@ describe('PartialTx.validate', () => {
440449
// Address of inputs[1] is wrong
441450
partialTx.inputs = [
442451
new ProposalInput(txId1, 0, 27, addr1),
443-
new ProposalInput(txId2, 4, 13, addr1, { token: testTokenConfig.uid, tokenData: 1 }),
452+
new ProposalInput(txId2, 4, 13, addr1, { token: testTokenConfig.uid }),
444453
];
445454
partialTx.outputs = [
446455
new ProposalOutput(15, scriptFromAddressP2PKH(addr2)),
@@ -453,23 +462,23 @@ describe('PartialTx.validate', () => {
453462
// Value of inputs[0] is wrong
454463
partialTx.inputs = [
455464
new ProposalInput(txId1, 0, 28, addr1),
456-
new ProposalInput(txId2, 4, 13, addr2, { token: testTokenConfig.uid, tokenData: 1 }),
465+
new ProposalInput(txId2, 4, 13, addr2, { token: testTokenConfig.uid }),
457466
];
458467

459468
await expect(partialTx.validate()).resolves.toEqual(false);
460469

461470
// TokenData of inputs[1] is wrong
462471
partialTx.inputs = [
463472
new ProposalInput(txId1, 0, 27, addr1),
464-
new ProposalInput(txId2, 4, 13, addr2, { token: testTokenConfig.uid, tokenData: 2 }),
473+
new ProposalInput(txId2, 4, 13, addr2, { token: testTokenConfig.uid, authorities: TOKEN_MELT_MASK }),
465474
];
466475

467476
await expect(partialTx.validate()).resolves.toEqual(false);
468477

469478
// Token of inputs[0] is wrong
470479
partialTx.inputs = [
471480
new ProposalInput(txId1, 0, 27, addr1, { token: testTokenConfig }),
472-
new ProposalInput(txId2, 4, 13, addr2, { token: testTokenConfig.uid, tokenData: 1 }),
481+
new ProposalInput(txId2, 4, 13, addr2, { token: testTokenConfig.uid }),
473482
];
474483

475484
await expect(partialTx.validate()).resolves.toEqual(false);
File renamed without changes.

0 commit comments

Comments
 (0)