Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 1738 implement the netaddr class and thorest/node review #1739

Open
wants to merge 11 commits into
base: feature-thorest
Choose a base branch
from
Open
6 changes: 6 additions & 0 deletions docs/diagrams/architecture/vcdm.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ classDiagram
+FixedPointNumber parseEther(string: ether)$
+FixedPointNumber parseUnit(string exp, Unit unit)$
}
class NetAddr {
+[number,number,number,number] ipAddress
+number port
+NetAddr of(string exp)
+string toString()
}
class VeChainDataModel {
<<interface>>
+bigint bi
Expand Down
180 changes: 180 additions & 0 deletions packages/core/src/vcdm/NetAddr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { InvalidDataType, InvalidOperation } from '@vechain/sdk-errors';
import { type VeChainDataModel } from './VeChainDataModel';

/**
* Represents a network address with an IP address and port.
*/
class NetAddr implements VeChainDataModel<NetAddr> {
/**
* The IP address of the network address.
*
* @type {[number, number, number, number]}
*/
private readonly ipAddress: [number, number, number, number];
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved

/**
* The port of the network address.
*
* @type {number}
*/
private readonly port: number;

/**
* Creates a new instance of the NetAddr class.
*
* @param {string} value - The IP address of the network address with the appended port number. It should be in the format of 'x.x.x.:port'.
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved
*/
protected constructor(value: string) {
const [ip, port] = value.split(':');
this.ipAddress = ip.split('.').map(Number) as [
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved
number,
number,
number,
number
];
this.port = Number(port);
}

/**
* Returns the value of n.
*
* @return {number} The value of n.
*
* @throws {InvalidOperation<NetAddr>} Systematically throws an error because there is no number representation for NetAddr.
*/
get n(): number {
throw new InvalidOperation(
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved
'NetAddr.n',
'There is no number representation for NetAdrr',
{
hex: this.toString()
}
);
}

/**
* Converts NetAddr into a 6-byte Uint8Array representation.
* The first 4 bytes represent the IPv4 address, and the last 2 bytes represent the port number.
*
* Format:
* - Bytes 0-3: IPv4 address octets
* - Bytes 4-5: Port number in big-endian format
*
* @returns {Uint8Array} A 6-byte array containing the binary representation of the IP:Port
*
* @example
* // For IP: 192.168.1.1 and Port: 8080
* // Returns: Uint8Array [192, 168, 1, 1, 31, 144]
*/
get bytes(): Uint8Array {
// Create a new 6-byte array to store the result
const result = new Uint8Array(6);

// Copy the 4 IPv4 address bytes to the beginning of the result array
result.set(new Uint8Array(this.ipAddress), 0);

// Convert port to two bytes in big-endian format
result[4] = (this.port >> 8) & 0xff; // High byte
result[5] = this.port & 0xff; // Low byte

return result;
}

/**
* Returns the value of bi.
*
* @return {number} The value of n.
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved
*
* @throws {InvalidOperation<NetAddr>} systematically throws an error because there is no big integer representation for NetAddr.
*/
get bi(): bigint {
throw new InvalidOperation(
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved
'NetAddr.bi',
'There is no big integer representation for NetAddr',
{
hex: this.toString()
}
);
}

/**
* Throws an error because there is no comparison between network addresses.
*
* @throws {InvalidOperation<NetAddr>} Systematically throws an error.
*/
compareTo(_that: NetAddr): number {
throw new InvalidOperation(
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved
'NetAddr.compareTo',
'There is no comparison between network addresses',
{ data: '' }
);
}

/**
* Determines whether this NetAddr instance is equal to the given NetAddr instance.
*
* @param {NetAddr} that - The NetAddr instance to compare with.
* @return {boolean} - True if the NetAddr instances are equal, otherwise false.
*/
isEqual(that: NetAddr): boolean {
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved
return (
this.ipAddress.length === that.ipAddress.length &&
this.ipAddress.every(
(value, index) => value === that.ipAddress[index]
) &&
this.port === that.port
);
}

/**
* Creates a new instance of the NetAddr class from a string expression.
*
* @param {string} exp - The string expression representing the network address.
* @returns {NetAddr} - A new NetAddr instance.
*
* @throws {InvalidDataType} - If the string expression is not a valid network address.
*/
static of(exp: string): NetAddr {
const ipPortRegex = /^(?:\d{1,3}\.){3}\d{1,3}:\d{1,5}$/;
if (!ipPortRegex.test(exp)) {
throw new InvalidDataType(
'NetAddr.of',
'not a valid network address (IP:port) expression',
{
exp: `${exp}`
}
);
}

const [ip, port] = exp.split(':');
const ipParts = ip.split('.').map(Number);
const portNumber = Number(port);

if (
ipParts.length !== 4 ||
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved
ipParts.some((part) => part < 0 || part > 255) ||
portNumber < 0 ||
portNumber > 65535
) {
throw new InvalidDataType(
'NetAddr.of',
'not a valid network address (IP:port) expression',
{
exp: `${exp}`
}
);
}
return new NetAddr(exp);
}

/**
* Returns a string representation of the network address.
*
* @return {string} The string representation of the network address.
*/
public toString(): string {
return `${this.ipAddress.join('.')}:${this.port}`;
GrandinLuc marked this conversation as resolved.
Show resolved Hide resolved
}
}

export { NetAddr };
118 changes: 118 additions & 0 deletions packages/core/tests/vcdm/NetAddr.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, expect, test } from '@jest/globals';
import { NetAddr } from '../../../thorest/src';
import { InvalidDataType, InvalidOperation } from '@vechain/sdk-errors';

/**
* Test NetAddr class.
* @group unit/vcdm
*/
describe('NetAddr class tests', () => {
describe('Construction tests', () => {
test('Return a NetAddr instance if the passed argument is a valid IP:port string', () => {
const exp = '192.168.1.1:8080';
const addr = NetAddr.of(exp);
expect(addr).toBeInstanceOf(NetAddr);
expect(addr.toString()).toEqual(exp);
});

test('Return a NetAddr instance with minimum valid values', () => {
const exp = '0.0.0.0:0';
const addr = NetAddr.of(exp);
expect(addr).toBeInstanceOf(NetAddr);
expect(addr.toString()).toEqual(exp);
});

test('Return a NetAddr instance with maximum valid values', () => {
const exp = '255.255.255.255:65535';
const addr = NetAddr.of(exp);
expect(addr).toBeInstanceOf(NetAddr);
expect(addr.toString()).toEqual(exp);
});

test('Throw an exception if IP octets are invalid', () => {
expect(() => NetAddr.of('256.1.2.3:8080')).toThrow(InvalidDataType);
expect(() => NetAddr.of('1.2.3.256:8080')).toThrow(InvalidDataType);
expect(() => NetAddr.of('-1.2.3.4:8080')).toThrow(InvalidDataType);
});

test('Throw an exception if port is invalid', () => {
expect(() => NetAddr.of('192.168.1.1:65536')).toThrow(
InvalidDataType
);
expect(() => NetAddr.of('192.168.1.1:-1')).toThrow(InvalidDataType);
});

test('Throw an exception if format is invalid', () => {
expect(() => NetAddr.of('192.168.1:8080')).toThrow(InvalidDataType);
expect(() => NetAddr.of('192.168.1.1.1:8080')).toThrow(
InvalidDataType
);
expect(() => NetAddr.of('192.168.1.1:')).toThrow(InvalidDataType);
// eslint-disable-next-line sonarjs/no-hardcoded-ip
expect(() => NetAddr.of('192.168.1.1')).toThrow(InvalidDataType);
expect(() => NetAddr.of(':8080')).toThrow(InvalidDataType);
expect(() => NetAddr.of('invalid')).toThrow(InvalidDataType);
});
});

describe('Value representation tests', () => {
test('toString() returns the correct string representation', () => {
const testCases = [
'192.168.1.1:8080',
'10.0.0.1:443',
'172.16.0.1:3000'
];

testCases.forEach((testCase) => {
const addr = NetAddr.of(testCase);
expect(addr.toString()).toEqual(testCase);
});
});

test('Maintains leading zeros in port number', () => {
const addr = NetAddr.of('192.168.1.1:0080');
expect(addr.toString()).toEqual('192.168.1.1:80');
});
});

describe('Edge cases', () => {
test('Handles IP addresses with leading zeros', () => {
const addr = NetAddr.of('192.168.001.001:8080');
expect(addr.toString()).toEqual('192.168.1.1:8080');
});

test('Handles whitespace in input', () => {
expect(() => NetAddr.of(' 192.168.1.1:8080 ')).toThrow(
InvalidDataType
);
expect(() => NetAddr.of('192.168.1.1 :8080')).toThrow(
InvalidDataType
);
expect(() => NetAddr.of('192.168.1.1: 8080')).toThrow(
InvalidDataType
);
});
});

describe('Method tests', () => {
test('compareTo method always throws an error', () => {
const addr1 = NetAddr.of('192.168.1.1:8080');
const addr2 = NetAddr.of('10.0.0.1:443');
expect(() => addr1.compareTo(addr2)).toThrow(InvalidOperation);
});

test('isEqual method tests', () => {
const addr1 = NetAddr.of('192.168.1.1:8080');
const addr2 = NetAddr.of('192.168.1.1:8080');
const addr3 = NetAddr.of('10.0.0.1:443');
expect(addr1.isEqual(addr2)).toBeTruthy();
expect(addr1.isEqual(addr3)).toBeFalsy();
});

test('bytes method returns correct byte array', () => {
const addr = NetAddr.of('192.168.1.1:8080');
const expectedBytes = new Uint8Array([192, 168, 1, 1, 31, 144]);
expect(addr.bytes).toEqual(expectedBytes);
});
});
});
1 change: 1 addition & 0 deletions packages/thorest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
},
"dependencies": {
"@vechain/sdk-core": "1.0.0-rc.6",
"@vechain/sdk-errors": "1.0.0-rc.6",
"ws": "^8.18.0"
},
"devDependencies": {
Expand Down
35 changes: 34 additions & 1 deletion packages/thorest/src/thor/node/GetPeersResponse.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
import { PeerStat, type PeerStatJSON } from './PeerStat';

/**
* Represents a response containing an array of PeerStat objects.
* Extends the native Array class to provide specialized handling of PeerStat objects.
* @extends Array<PeerStat>
*/
class GetPeersResponse extends Array<PeerStat> {
/**
* Creates a new GetPeersResponse instance.
* Special constructor pattern required for Array inheritance.
* Array constructor is first called with a length parameter,
* so we need this pattern to properly handle array data instead.
*
* @param json - The JSON array containing peer statistics data
* @returns A new GetPeersResponse instance containing PeerStat objects
*/
constructor(json: GetPeersResponseJSON) {
super(...json.map((json: PeerStatJSON) => new PeerStat(json)));
super();
return Object.setPrototypeOf(
Array.from(json ?? [], (peerStat) => {
return new PeerStat(peerStat);
}),
GetPeersResponse.prototype
) as GetPeersResponse;
}

/**
* Converts the GetPeersResponse instance to a JSON array
* @returns {GetPeersResponseJSON} An array of peer statistics in JSON format
*/
toJSON(): GetPeersResponseJSON {
return this.map((peerStat: PeerStat) => peerStat.toJSON());
}
}

/**
* Interface representing the JSON structure of the peers response.
* Extends the native Array type to contain PeerStatJSON objects.
* @extends Array<PeerStatJSON>
*/
interface GetPeersResponseJSON extends Array<PeerStatJSON> {}

export { GetPeersResponse, type GetPeersResponseJSON };
Loading
Loading