Skip to content

Commit 78f6c1f

Browse files
Merge branch 'master' into fix/invalid-ws-schema
2 parents 8053930 + 5752bae commit 78f6c1f

27 files changed

+900
-191
lines changed

__tests__/integration/configuration/docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ services:
66

77
fullnode:
88
image:
9-
${HATHOR_LIB_INTEGRATION_TESTS_FULLNODE_IMAGE:-hathornetwork/hathor-core:experimental-nano-testnet-v1.7.3}
9+
${HATHOR_LIB_INTEGRATION_TESTS_FULLNODE_IMAGE:-hathornetwork/hathor-core:experimental-nano-preview-20250428}
1010
command: [
1111
"run_node",
1212
"--listen", "tcp:40404",

__tests__/integration/nanocontracts/bet.test.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { NATIVE_TOKEN_UID, NANO_CONTRACTS_INITIALIZE_METHOD } from '../../../src
1212
import ncApi from '../../../src/api/nano';
1313
import dateFormatter from '../../../src/utils/date';
1414
import { bufferToHex } from '../../../src/utils/buffer';
15+
import helpersUtils from '../../../src/utils/helpers';
1516
import Address from '../../../src/models/address';
1617
import P2PKH from '../../../src/models/p2pkh';
1718
import P2SH from '../../../src/models/p2sh';
@@ -55,7 +56,15 @@ describe('full cycle of bet nano contract', () => {
5556
await GenesisWalletHelper.clearListeners();
5657
});
5758

58-
const checkTxValid = async (wallet, txId) => {
59+
const checkTxValid = async (wallet, tx) => {
60+
const txId = tx.hash;
61+
// Check that serialization and deserialization match
62+
const network = wallet.getNetworkObject();
63+
const txBytes = tx.toBytes();
64+
const deserializedTx = helpersUtils.createTxFromBytes(txBytes, network);
65+
const deserializedTxBytes = deserializedTx.toBytes();
66+
expect(bufferToHex(txBytes)).toBe(bufferToHex(deserializedTxBytes));
67+
5968
expect(txId).toBeDefined();
6069
await waitForTxReceived(wallet, txId);
6170
// We need to wait for the tx to get a first block, so we guarantee it was executed
@@ -93,7 +102,7 @@ describe('full cycle of bet nano contract', () => {
93102
args: [bufferToHex(oracleData), NATIVE_TOKEN_UID, dateLastBet],
94103
}
95104
);
96-
await checkTxValid(wallet, tx1.hash);
105+
await checkTxValid(wallet, tx1);
97106
const tx1Data = await wallet.getFullTxById(tx1.hash);
98107
expect(isNanoContractCreateTx(tx1Data.tx)).toBe(true);
99108

@@ -165,7 +174,7 @@ describe('full cycle of bet nano contract', () => {
165174
},
166175
],
167176
});
168-
await checkTxValid(wallet, txBet.hash);
177+
await checkTxValid(wallet, txBet);
169178
const txBetData = await wallet.getFullTxById(txBet.hash);
170179
expect(isNanoContractCreateTx(txBetData.tx)).toBe(false);
171180

@@ -213,7 +222,7 @@ describe('full cycle of bet nano contract', () => {
213222
},
214223
],
215224
});
216-
await checkTxValid(wallet, txBet2.hash);
225+
await checkTxValid(wallet, txBet2);
217226
const txBet2Data = await wallet.getFullTxById(txBet2.hash);
218227
expect(isNanoContractCreateTx(txBet2Data.tx)).toBe(false);
219228

@@ -291,7 +300,7 @@ describe('full cycle of bet nano contract', () => {
291300
ncId: tx1.hash,
292301
args: [`${bufferToHex(inputData)},${result},str`],
293302
});
294-
await checkTxValid(wallet, txSetResult.hash);
303+
await checkTxValid(wallet, txSetResult);
295304
txIds.push(txSetResult.hash);
296305
const txSetResultData = await wallet.getFullTxById(txSetResult.hash);
297306
expect(isNanoContractCreateTx(txSetResultData.tx)).toBe(false);
@@ -330,7 +339,7 @@ describe('full cycle of bet nano contract', () => {
330339
},
331340
],
332341
});
333-
await checkTxValid(wallet, txWithdrawal.hash);
342+
await checkTxValid(wallet, txWithdrawal);
334343
txIds.push(txWithdrawal.hash);
335344

336345
const txWithdrawalData = await wallet.getFullTxById(txWithdrawal.hash);

__tests__/nano_contracts/deserializer.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,21 @@ test('Address', () => {
227227

228228
expect(() => deserializer.deserializeFromType(wrongNetworkAddressBuffer, 'Address')).toThrow();
229229
});
230+
231+
test('VarInt', () => {
232+
const network = new Network('testnet');
233+
const deserializer = new Deserializer(network);
234+
const DWARF5TestCases = [
235+
[2n, Buffer.from([2])],
236+
[-2n, Buffer.from([0x7e])],
237+
[127n, Buffer.from([127 + 0x80, 0])],
238+
[-127n, Buffer.from([1 + 0x80, 0x7f])],
239+
[128n, Buffer.from([0 + 0x80, 1])],
240+
[-128n, Buffer.from([0 + 0x80, 0x7f])],
241+
[129n, Buffer.from([1 + 0x80, 1])],
242+
[-129n, Buffer.from([0x7f + 0x80, 0x7e])],
243+
];
244+
for (const testCase of DWARF5TestCases) {
245+
expect(deserializer.toVarInt(testCase[1] as Buffer)).toEqual(testCase[0] as bigint);
246+
}
247+
});

__tests__/nano_contracts/serializer.test.ts

+22-6
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ test('Int', () => {
2727
test('Bytes', () => {
2828
const serializer = new Serializer();
2929
expect(
30-
serializer.fromBytes([0x74, 0x65, 0x73, 0x74]).equals(Buffer.from([0x74, 0x65, 0x73, 0x74]))
30+
serializer
31+
.fromBytes(Buffer.from([0x74, 0x65, 0x73, 0x74]))
32+
.equals(Buffer.from([0x74, 0x65, 0x73, 0x74]))
3133
).toBe(true);
3234
});
3335

@@ -67,10 +69,7 @@ test('List', () => {
6769
expect(
6870
serializer
6971
.fromList(
70-
[
71-
[0x74, 0x65, 0x73, 0x74],
72-
[0x74, 0x65, 0x73, 0x74],
73-
],
72+
[Buffer.from([0x74, 0x65, 0x73, 0x74]), Buffer.from([0x74, 0x65, 0x73, 0x74])],
7473
'bytes'
7574
)
7675
.equals(Buffer.from([0x00, 0x02, 0x74, 0x65, 0x73, 0x74, 0x74, 0x65, 0x73, 0x74]))
@@ -106,7 +105,7 @@ test('Optional', () => {
106105
expect(serializer.fromOptional(null, 'bytes').equals(Buffer.from([0x00]))).toBe(true);
107106
expect(
108107
serializer
109-
.fromOptional([0x74, 0x65, 0x73, 0x74], 'bytes')
108+
.fromOptional(Buffer.from([0x74, 0x65, 0x73, 0x74]), 'bytes')
110109
.equals(Buffer.from([0x01, 0x74, 0x65, 0x73, 0x74]))
111110
).toBe(true);
112111

@@ -159,3 +158,20 @@ test('Signed', () => {
159158
.equals(Buffer.from([0x00, 0x01, 0x01, 0x74, 0x65, 0x73, 0x74]))
160159
).toBe(true);
161160
});
161+
162+
test('VarInt', () => {
163+
const DWARF5TestCases = [
164+
[2n, Buffer.from([2])],
165+
[-2n, Buffer.from([0x7e])],
166+
[127n, Buffer.from([127 + 0x80, 0])],
167+
[-127n, Buffer.from([1 + 0x80, 0x7f])],
168+
[128n, Buffer.from([0 + 0x80, 1])],
169+
[-128n, Buffer.from([0 + 0x80, 0x7f])],
170+
[129n, Buffer.from([1 + 0x80, 1])],
171+
[-129n, Buffer.from([0x7f + 0x80, 0x7e])],
172+
];
173+
const serializer = new Serializer();
174+
for (const testCase of DWARF5TestCases) {
175+
expect(serializer.fromVarInt(testCase[0] as bigint)).toEqual(testCase[1] as Buffer);
176+
}
177+
});

__tests__/utils/leb128.test.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { encodeSigned, decodeSigned } from '../../src/utils/leb128';
2+
3+
/**
4+
* Examples from the DWARF 5 standard, section 7.6, table 7.8.
5+
* https://dwarfstd.org/doc/DWARF5.pdf
6+
*/
7+
const DWARF5TestCases = [
8+
[2n, Buffer.from([2])],
9+
[-2n, Buffer.from([0x7e])],
10+
[127n, Buffer.from([127 + 0x80, 0])],
11+
[-127n, Buffer.from([1 + 0x80, 0x7f])],
12+
[128n, Buffer.from([0 + 0x80, 1])],
13+
[-128n, Buffer.from([0 + 0x80, 0x7f])],
14+
[129n, Buffer.from([1 + 0x80, 1])],
15+
[-129n, Buffer.from([0x7f + 0x80, 0x7e])],
16+
];
17+
18+
test('Encoded values should be decoded as the same values', () => {
19+
const testCases = Array.from(DWARF5TestCases.map(v => v[0] as bigint)).concat([
20+
0n,
21+
1n,
22+
27n,
23+
100n,
24+
1023n,
25+
1024n,
26+
BigInt(Number.MAX_SAFE_INTEGER),
27+
BigInt(Number.MAX_SAFE_INTEGER) + 2n,
28+
BigInt(Number.MAX_SAFE_INTEGER) + 1000n,
29+
0xcafecafecafe01n,
30+
0xcafecafecafecafen,
31+
0x0afecafecafecafen,
32+
]);
33+
34+
for (const value of testCases) {
35+
const encodedBuffer = encodeSigned(value);
36+
const decoded = decodeSigned(encodedBuffer);
37+
expect(decoded.value).toEqual(value);
38+
expect(decoded.rest).toHaveLength(0);
39+
// Negative values should also work
40+
const encodedNegBuffer = encodeSigned(-value);
41+
const decodedNeg = decodeSigned(encodedNegBuffer);
42+
expect(decodedNeg.value).toEqual(-value);
43+
expect(decodedNeg.rest).toHaveLength(0);
44+
}
45+
});
46+
47+
test('leb128 should work with fullnode docstring examples', () => {
48+
const tests = [
49+
[0, Buffer.from([0x00])],
50+
[624485, Buffer.from([0xe5, 0x8e, 0x26])],
51+
[-123456, Buffer.from([0xc0, 0xbb, 0x78])],
52+
];
53+
54+
for (const value of tests) {
55+
const encoded = encodeSigned(value[0] as number);
56+
expect(encoded).toEqual(value[1]);
57+
58+
const decoded = decodeSigned(Buffer.concat([value[1] as Buffer, Buffer.from('cafe')]));
59+
expect(decoded.value).toEqual(BigInt(value[0] as number));
60+
expect(decoded.rest).toEqual(Buffer.from('cafe'));
61+
}
62+
});
63+
64+
test('leb128 should work with DWARF 5 examples', () => {
65+
for (const value of DWARF5TestCases) {
66+
const encoded = encodeSigned(value[0] as bigint);
67+
expect(encoded).toEqual(value[1]);
68+
69+
const decoded = decodeSigned(Buffer.concat([value[1] as Buffer, Buffer.from('cafe')]));
70+
expect(decoded.value).toEqual(BigInt(value[0] as bigint));
71+
expect(decoded.rest).toEqual(Buffer.from('cafe'));
72+
}
73+
});
74+
75+
test('leb128 should fail if maxBytes is lower than required', () => {
76+
for (const value of DWARF5TestCases) {
77+
const expectedLen = (value[1] as Buffer).length;
78+
const val = value[0] as bigint;
79+
// Encode should throw if maxBytes is expectedLen-1
80+
expect(() => {
81+
return encodeSigned(val, expectedLen - 1);
82+
}).toThrow();
83+
// Decode should throw if maxBytes is expectedLen-1
84+
const buf = Buffer.concat([value[1] as Buffer, Buffer.from('cafe')]);
85+
expect(() => {
86+
return decodeSigned(buf, expectedLen - 1);
87+
}).toThrow();
88+
}
89+
});

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hathor/wallet-lib",
3-
"version": "2.2.0",
3+
"version": "2.3.1",
44
"description": "Library used by Hathor Wallet",
55
"main": "lib/index.js",
66
"engines": {

src/errors.ts

+2
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,5 @@ export class GlobalLoadLockTaskError extends Error {
398398
this.innerError = innerError;
399399
}
400400
}
401+
402+
export class NanoHeaderNotFound extends Error {}

src/headers/base.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Hathor Labs and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
export interface HeaderStaticType {
9+
deserialize(srcBuf: Buffer): [Header, Buffer];
10+
}
11+
12+
export default abstract class Header {
13+
abstract serialize(array: Buffer[]): void;
14+
15+
abstract serializeSighash(array: Buffer[]): void;
16+
17+
// XXX In typescript we can't have an abstract and static method
18+
static deserialize(srcBuf: Buffer): [Header, Buffer] {
19+
throw new Error('Not implemented: deserialize must be implemented in subclass');
20+
}
21+
}

src/headers/parser.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright (c) Hathor Labs and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { VertexHeaderId } from './types';
9+
import { HeaderStaticType } from './base';
10+
import NanoContractHeader from '../nano_contracts/header';
11+
12+
export default class HeaderParser {
13+
static getSupportedHeaders(): Record<VertexHeaderId, HeaderStaticType> {
14+
return {
15+
[VertexHeaderId.NANO_HEADER]: NanoContractHeader,
16+
};
17+
}
18+
19+
static getHeader(id: string): HeaderStaticType {
20+
const headers = HeaderParser.getSupportedHeaders();
21+
if (!(id in headers)) {
22+
throw new Error(`Header id not supported: ${id}`);
23+
}
24+
25+
return headers[id];
26+
}
27+
}

src/headers/types.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Copyright (c) Hathor Labs and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
/**
9+
* The hathor-core has a similar enum that maps to bytes.
10+
* In typescript this is not easy to manipulate so I decided
11+
* to have the same enum but with hex values instead.
12+
*/
13+
export const enum VertexHeaderId {
14+
NANO_HEADER = '10',
15+
}
16+
17+
export function getVertexHeaderIdBuffer(id: VertexHeaderId): Buffer {
18+
return Buffer.from(id, 'hex');
19+
}
20+
21+
export function getVertexHeaderIdFromBuffer(buf: Buffer): VertexHeaderId {
22+
const vertexId = buf.readUInt8().toString(16);
23+
switch (vertexId) {
24+
case VertexHeaderId.NANO_HEADER:
25+
return VertexHeaderId.NANO_HEADER;
26+
default:
27+
throw new Error('Invalid VertexHeaderId');
28+
}
29+
}

src/models/create_token_transaction.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Network from './network';
2222
import { CreateTokenTxInvalid, InvalidOutputsError, NftValidationError } from '../errors';
2323
import ScriptData from './script_data';
2424
import { OutputType } from '../wallet/types';
25+
import type Header from '../headers/base';
2526

2627
type optionsType = {
2728
signalBits?: number;
@@ -31,6 +32,7 @@ type optionsType = {
3132
parents?: string[];
3233
tokens?: string[];
3334
hash?: string | null;
35+
headers?: Header[];
3436
};
3537

3638
class CreateTokenTransaction extends Transaction {
@@ -53,6 +55,7 @@ class CreateTokenTransaction extends Transaction {
5355
parents: [],
5456
tokens: [],
5557
hash: null,
58+
headers: [],
5659
};
5760
const newOptions = Object.assign(defaultOptions, options);
5861

@@ -238,7 +241,8 @@ class CreateTokenTransaction extends Transaction {
238241

239242
txBuffer = tx.getFundsFieldsFromBytes(txBuffer, network);
240243
txBuffer = tx.getTokenInfoFromBytes(txBuffer);
241-
tx.getGraphFieldsFromBytes(txBuffer);
244+
txBuffer = tx.getGraphFieldsFromBytes(txBuffer);
245+
tx.getHeadersFromBytes(txBuffer);
242246

243247
tx.updateHash();
244248

0 commit comments

Comments
 (0)