Skip to content

Commit

Permalink
[xp] refactor address class
Browse files Browse the repository at this point in the history
  • Loading branch information
peterjah committed Apr 26, 2024
1 parent 9a0fe42 commit 5a38cf3
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 68 deletions.
112 changes: 64 additions & 48 deletions packages/massa-web3/src/experimental/basicElements/address.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import Base58 from '../crypto/base58'
import Serializer from '../crypto/interfaces/serializer'
import { Version } from '../crypto/interfaces/versioner'
import { checkPrefix } from './internal'
import { Version, Versioner } from '../crypto/interfaces/versioner'
import VarintVersioner from '../crypto/varintVersioner'
import { PublicKey } from './keys'
import varint from 'varint'

const ADDRESS_PREFIX = 'A'
const ADDRESS_USER_PREFIX = 'U'
const ADDRESS_CONTRACT_PREFIX = 'S'
const ADDRESS_USER_PREFIX = 'AU'
const ADDRESS_CONTRACT_PREFIX = 'AS'
const UNDERLYING_HASH_LEN = 32

const DEFAULT_VERSION = Version.V0

/**
* A class representing an address.
*
Expand All @@ -17,19 +19,22 @@ const UNDERLYING_HASH_LEN = 32
*
* @privateRemarks
* Interfaces are used to make the code more modular. To add a new version, you simply need to:
* - extend the `initFromVersion` method:
* - Add a new case in the switch statement with the new algorithms to use.
* - Add a new default version matching the last version.
* - Add a new case in the switch statement with the new algorithms to use.
* - Change the DEFAULT_VERSION version matching the last version.
* - Change the getVersion method to detect the version from user input.
* - check the `fromPublicKey` method to potentially adapt how an address is derived from a public key.
* - Voila! The code will automatically handle the new version.
*/
export class Address {
// The address in byte format. Address type and version included.
private bytes: Uint8Array

public isEOA = false

protected constructor(
public serializer: Serializer,
public version: Version,
public isEOA: boolean
public serializer: Serializer = new Base58(),

Check warning on line 35 in packages/massa-web3/src/experimental/basicElements/address.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
public versioner: Versioner = new VarintVersioner(),

Check warning on line 36 in packages/massa-web3/src/experimental/basicElements/address.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
public version: Version
) {}

/**
Expand All @@ -41,15 +46,29 @@ export class Address {
*
* @throws If the version is not supported.
*/
protected static initFromVersion(version: Version = Version.V0): Address {
protected static initFromVersion(
version: Version = DEFAULT_VERSION

Check warning on line 50 in packages/massa-web3/src/experimental/basicElements/address.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
): Address {
switch (version) {
case Version.V0:
return new Address(new Base58(), version, false)
return new Address(new Base58(), new VarintVersioner(), version)
default:
throw new Error(`unsupported version: ${version}`)
}
}

private getPrefix(str: string): string {
const expected = [ADDRESS_USER_PREFIX, ADDRESS_CONTRACT_PREFIX]
for (let prefix of expected) {
if (str.startsWith(prefix)) {
return prefix
}
}
throw new Error(
`invalid address prefix: one of ${expected.join(' or ')} was expected.`
)
}

/**
* Initializes a new address object from a serialized string.
*
Expand All @@ -60,29 +79,24 @@ export class Address {
* @throws If the address string is invalid.
*/
public static fromString(str: string): Address {
const address = Address.initFromVersion()
const version = Address.getVersion(str)
const address = Address.initFromVersion(version)

try {
const prefix = checkPrefix(
str,
ADDRESS_PREFIX + ADDRESS_USER_PREFIX,
ADDRESS_PREFIX + ADDRESS_CONTRACT_PREFIX
)
const prefix = address.getPrefix(str)

address.isEOA = str[ADDRESS_PREFIX.length] === ADDRESS_USER_PREFIX
const versionedHash = address.serializer.deserialize(
address.isEOA = prefix === ADDRESS_USER_PREFIX
const versionedBytes = address.serializer.deserialize(
str.slice(prefix.length)
)

address.bytes = Uint8Array.from([
...varint.encode(address.isEOA ? 0 : 1),
...versionedHash,
...versionedBytes,
])
address.version = address.getVersion()
} catch (e) {
throw new Error(`invalid address string: ${e.message}`)
}

return address
}

Expand All @@ -99,16 +113,15 @@ export class Address {
}

/**
* Get the address version from bytes.
* Get the address version.
* *
* @returns the address type enum.
*/
private getVersion(): Version {
if (!this.bytes) {
throw new Error('address bytes is not initialized')
}
varint.decode(this.bytes)
return varint.decode(this.bytes, varint.decode.bytes)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private static getVersion(data: string | Uint8Array): Version {
// when a new version will come, implement the logic to detect version here
// This should be done without serializer and versionner as they are potentially not known at this point
return Version.V0
}

/**
Expand All @@ -118,13 +131,15 @@ export class Address {
*
* @returns A new address object.
*/
public static fromPublicKey(publicKey: PublicKey): Address {
const address = Address.initFromVersion()
const hash = publicKey.hasher.hash(publicKey.toBytes())
public static fromPublicKey(
publicKey: PublicKey,
version = DEFAULT_VERSION
): Address {
const address = Address.initFromVersion(version)
const rawBytes = publicKey.hasher.hash(publicKey.toBytes())
address.bytes = Uint8Array.from([
0 /* EOA*/,
...varint.encode(address.version),
...hash,
...address.versioner.attach(version, rawBytes),
])
address.isEOA = true
return address
Expand All @@ -138,10 +153,10 @@ export class Address {
* @returns A new address object.
*/
public static fromBytes(bytes: Uint8Array): Address {
const address = Address.initFromVersion()
const version = Address.getVersion(bytes)
const address = Address.initFromVersion(version)
address.bytes = bytes
address.isEOA = address.getType() === 0
address.version = address.getVersion()
return address
}

Expand All @@ -165,28 +180,29 @@ export class Address {
* @returns The serialized address string.
*/
toString(): string {
// decode version
varint.decode(this.bytes)
const versionedBytes = this.bytes.slice(varint.decode.bytes)
return `${ADDRESS_PREFIX}${
// skip address type bytes
const versionedBytes = this.bytes.slice(
varint.encodingLength(this.getType())
)
return `${
this.isEOA ? ADDRESS_USER_PREFIX : ADDRESS_CONTRACT_PREFIX
}${this.serializer.serialize(versionedBytes)}`
}

/**
* Get byte length of address in binary format .
* Get address in binary format from a bytes buffer.
*
* @returns The address length in bytes.
* @returns The address in bytes format.
*/
static getByteLength(data: Uint8Array): number {
static extractFromBuffer(data: Uint8Array, offset = 0): Uint8Array {
// addr type
varint.decode(data)
varint.decode(data, offset)
let addrByteLen = varint.decode.bytes
// version
varint.decode(data, addrByteLen)
varint.decode(data, offset + addrByteLen)
addrByteLen += varint.decode.bytes

addrByteLen += UNDERLYING_HASH_LEN
return addrByteLen
return data.slice(offset, offset + addrByteLen)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function extractData(
return extractedData
}

export function checkPrefix(str: string, ...expected: string[]): string {
export function mustExtractPrefix(str: string, ...expected: string[]): string {
const prefix = str.slice(0, expected[0].length)
if (!expected.includes(prefix)) {
throw new Error(
Expand Down
6 changes: 3 additions & 3 deletions packages/massa-web3/src/experimental/basicElements/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Serializer from '../crypto/interfaces/serializer'
import Signer from '../crypto/interfaces/signer'
import { Version, Versioner } from '../crypto/interfaces/versioner'
import VarintVersioner from '../crypto/varintVersioner'
import { checkPrefix, extractData } from './internal'
import { mustExtractPrefix, extractData } from './internal'

const PRIVATE_KEY_PREFIX = 'S'
const PUBLIC_KEY_PREFIX = 'P'
Expand Down Expand Up @@ -73,7 +73,7 @@ export class PrivateKey {
const privateKey = PrivateKey.initFromVersion(version)

try {
checkPrefix(str, PRIVATE_KEY_PREFIX)
mustExtractPrefix(str, PRIVATE_KEY_PREFIX)
privateKey.bytes = extractData(
privateKey.serializer,
privateKey.versioner,
Expand Down Expand Up @@ -242,7 +242,7 @@ export class PublicKey {
const publicKey = PublicKey.initFromVersion(version)

try {
checkPrefix(str, PUBLIC_KEY_PREFIX)
mustExtractPrefix(str, PUBLIC_KEY_PREFIX)
publicKey.bytes = extractData(
publicKey.serializer,
publicKey.versioner,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,9 @@ export class OperationManager {

switch (operationDetails.type) {
case OperationType.Transaction: {
const addrLen = Address.getByteLength(data.slice(offset))
const recipientAddress = Address.fromBytes(
data.slice(offset, offset + addrLen)
)
offset += addrLen
const addrBytes = Address.extractFromBuffer(data, offset)
const recipientAddress = Address.fromBytes(addrBytes)
offset += addrBytes.length
return {
...operationDetails,
recipientAddress,
Expand Down
4 changes: 2 additions & 2 deletions packages/massa-web3/src/experimental/utils/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const MAS_DECIMALS = 9
/**
* Convert Mas float amount to 9 decimals bigint
*
* @param amount MAS amount in floating point representation
* @param amount - MAS amount in floating point representation
* @returns amount in nanoMAS
*/
export const toNanoMas = (amount: string | number): bigint => {
Expand All @@ -18,7 +18,7 @@ export const toNanoMas = (amount: string | number): bigint => {
/**
* Convert nanoMas bigint amount to floating point Mas string
*
* @param amount nanoMAS amount in bigint representation
* @param amount - nanoMAS amount in bigint representation
* @returns amount in MAS
*/
export const toMas = (amount: bigint): string => {
Expand Down
17 changes: 12 additions & 5 deletions packages/massa-web3/test/experimental/unit/address.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Account } from '../../../src/experimental/account'
import { Address } from '../../../src/experimental/basicElements'
import { Version } from '../../../src/experimental/crypto/interfaces/versioner'
import { Address as LegacyAddress } from '../../../src/utils/keyAndAddresses'

const contractAddress = 'AS1eK3SEXGDAWN6pZhdr4Q7WJv6UHss55EB14hPy4XqBpiktfPu6'
Expand Down Expand Up @@ -30,6 +31,13 @@ describe('Address tests', () => {
)
})

test('fromPublicKey throws error for invalid version', () => {
const invalidVersion = -1 as Version
expect(() =>
Address.fromPublicKey(account.publicKey, invalidVersion)
).toThrow(/unsupported version:/)
})

test('toString returns string with user prefix for EOA', () => {
const address = Address.fromPublicKey(account.publicKey)
expect(address.toString()).toMatch(/^AU/)
Expand All @@ -43,7 +51,6 @@ describe('Address tests', () => {
test('toBytes returns bytes with EOA prefix for EOA', () => {
const address = Address.fromPublicKey(account.publicKey)
const bytes = address.toBytes()

// The first byte should be 0 for EOA
expect(bytes[0]).toBe(0)
})
Expand All @@ -55,11 +62,11 @@ describe('Address tests', () => {
expect(bytes[0]).toBe(1)
})

test('getByteLength returns correct length', () => {
test('extractFromBuffer returns extracted address bytes', () => {
const address = Address.fromPublicKey(account.publicKey)
const bytes = address.toBytes()
const byteLength = Address.getByteLength(bytes)
const buffer = Uint8Array.from([...address.toBytes(), 1, 2, 3, 4])
const addressBytes = Address.extractFromBuffer(buffer)

expect(byteLength).toBe(bytes.length)
expect(addressBytes).toStrictEqual(address.toBytes())
})
})
8 changes: 4 additions & 4 deletions packages/massa-web3/test/experimental/unit/internal.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
extractData,
checkPrefix,
mustExtractPrefix,
} from '../../../src/experimental/basicElements/internal'
import Base58 from '../../../src/experimental/crypto/base58'
import VarintVersioner from '../../../src/experimental/crypto/varintVersioner'
Expand All @@ -23,12 +23,12 @@ describe('Internal functions tests', () => {
expect(extractedData).toEqual(Uint8Array.from([1, 2, 3, 4]))
})

test('checkPrefix throws error for invalid prefix', () => {
expect(() => checkPrefix('IP_invalid_prefix', 'AS')).toThrow(
test('mustExtractPrefix throws error for invalid prefix', () => {
expect(() => mustExtractPrefix('IP_invalid_prefix', 'AS')).toThrow(
'invalid prefix: IP. AS was expected.'
)

expect(() => checkPrefix('IP_invalid_prefix', 'AS', 'AU')).toThrow(
expect(() => mustExtractPrefix('IP_invalid_prefix', 'AS', 'AU')).toThrow(
'invalid prefix: IP. one of AS or AU was expected.'
)
})
Expand Down

0 comments on commit 5a38cf3

Please sign in to comment.