diff --git a/docs/diagrams/architecture/vcdm.md b/docs/diagrams/architecture/vcdm.md index 1235e54c9..be2c3ec48 100644 --- a/docs/diagrams/architecture/vcdm.md +++ b/docs/diagrams/architecture/vcdm.md @@ -1,5 +1,16 @@ ```mermaid classDiagram + class Account { + <> + #address: Address + #balance: Currency + #mnemonic: Mnemonic + } + class ExternallyOwnedAccount + class Contract + class Currency { + <> + } class Address { +string checksum(HexUInt huint)$ +boolean isValid(string exp)$ @@ -58,6 +69,11 @@ classDiagram +boolean isEqual(~T~ that) +boolean isNumber() } + Account "1" ..|> "1" Address : has + Account "1" ..|> "1" Mnemonic : has + Account "1" ..|> "1" Currency : has + Account <|-- ExternallyOwnedAccount + Account <|-- Contract Hash <|.. Blake2b256 Hash <|.. Keccak256 Hash <|.. Sha256 diff --git a/packages/core/src/hash/Blake2b256.ts b/packages/core/src/hash/Blake2b256.ts index eb4e8db0c..2f2ac1314 100644 --- a/packages/core/src/hash/Blake2b256.ts +++ b/packages/core/src/hash/Blake2b256.ts @@ -1,6 +1,6 @@ -import { Hex, HexUInt, Txt, type Hash } from '../vcdm'; -import { InvalidOperation } from '@vechain/sdk-errors'; import { blake2b as nh_blake2b } from '@noble/hashes/blake2b'; +import { InvalidOperation } from '@vechain/sdk-errors'; +import { Hex, HexUInt, Txt, type Hash } from '../vcdm'; /** * Represents the result of an [BLAKE](https://en.wikipedia.org/wiki/BLAKE_(hash_function)) [BlAKE2B 256](https://www.blake2.net/) hash operation. * @@ -38,7 +38,7 @@ class Blake2b256 extends HexUInt implements Hash { } } -// TODO: Backwards compatibility, remove in future release. +// Backwards compatibility, remove in future release #1184 const blake2b256 = ( hex: string, diff --git a/packages/core/src/hash/Keccak256.ts b/packages/core/src/hash/Keccak256.ts index 77961bd74..9d5f02ac0 100644 --- a/packages/core/src/hash/Keccak256.ts +++ b/packages/core/src/hash/Keccak256.ts @@ -36,7 +36,7 @@ class Keccak256 extends HexUInt implements Hash { } } -// TODO: Backwards compatibility, remove in future release. +// Backwards compatibility, remove in future release #1184 const keccak256 = ( hex: string, diff --git a/packages/core/src/hash/Sha256.ts b/packages/core/src/hash/Sha256.ts index 8a2f12a20..788ce3c61 100644 --- a/packages/core/src/hash/Sha256.ts +++ b/packages/core/src/hash/Sha256.ts @@ -34,7 +34,7 @@ class Sha256 extends HexUInt implements Hash { } } -// TODO: Backwards compatibility, remove in future release. +// Backwards compatibility, remove in future release #1184 const sha256 = ( hex: string, diff --git a/packages/core/src/vcdm/Address.ts b/packages/core/src/vcdm/Address.ts index 77d1316d9..56386f756 100644 --- a/packages/core/src/vcdm/Address.ts +++ b/packages/core/src/vcdm/Address.ts @@ -187,7 +187,7 @@ class Address extends HexUInt { } } -// TODO: Backwards compatibility, remove when it is matured enough +// Backwards compatibility, remove when it is matured enough #1184 const addressUtils = { fromPrivateKey: (privateKey: Uint8Array): string => diff --git a/packages/core/src/vcdm/Mnemonic.ts b/packages/core/src/vcdm/Mnemonic.ts index 353056ea4..911e04e0d 100644 --- a/packages/core/src/vcdm/Mnemonic.ts +++ b/packages/core/src/vcdm/Mnemonic.ts @@ -114,7 +114,7 @@ class Mnemonic extends Txt { } } - // TODO: Legacy method, probably should be part of a Private Key class (ofMnemonic) + // Legacy method, probably should be part of a Private Key class (ofMnemonic) #1122 /** * Derives a private key from a given list of * [BIP39 Mnemonic Words](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) @@ -211,9 +211,18 @@ class Mnemonic extends Txt { const wordsToValidate = Array.isArray(words) ? words.join(' ') : words; return validateMnemonic(wordsToValidate, wordlist); } + + /** + * Returns an empty string to prevent printing the mnemonic. + * + * @returns {string} An empty string + */ + public toString(): string { + return ''; + } } -// TODO: Backwards compatibility, remove in future versions +// Backwards compatibility, remove in future versions #1184 const mnemonic = { deriveAddress: (words: string[], path: string = 'm/0'): string => diff --git a/packages/core/src/vcdm/Revision.ts b/packages/core/src/vcdm/Revision.ts index 32fc420f6..e35926736 100644 --- a/packages/core/src/vcdm/Revision.ts +++ b/packages/core/src/vcdm/Revision.ts @@ -82,7 +82,7 @@ class Revision extends Txt { } } -// TODO: Backwards compatibility, remove when it is matured enough +// Backwards compatibility, remove when it is matured enough #1184 const revisionUtils = { isRevisionAccount: (revision: string | number): boolean => diff --git a/packages/core/src/vcdm/VeChainDataModel.ts b/packages/core/src/vcdm/VeChainDataModel.ts index 994349949..8a5249bcd 100644 --- a/packages/core/src/vcdm/VeChainDataModel.ts +++ b/packages/core/src/vcdm/VeChainDataModel.ts @@ -8,7 +8,7 @@ export interface VeChainDataModel { // Properties. /** * Return this instance cast to a big integer value - * @throws InvalidCastType if this object can't cast to a big integer. + * @throws InvalidOperation if this object can't cast to a big integer. */ get bi(): bigint; @@ -19,7 +19,7 @@ export interface VeChainDataModel { /** * Return this object cast to number value. - * @throws InvalidCastType if this object can't cast to a big integer. + * @throws InvalidOperation if this object can't cast to a big integer. */ get n(): number; diff --git a/packages/core/src/vcdm/account/Account.ts b/packages/core/src/vcdm/account/Account.ts new file mode 100644 index 000000000..1068d46e4 --- /dev/null +++ b/packages/core/src/vcdm/account/Account.ts @@ -0,0 +1,121 @@ +import { InvalidOperation } from '@vechain/sdk-errors'; +import { type Address } from '../Address'; +import { type Currency } from '../Currency'; +import { type VeChainDataModel } from '../VeChainDataModel'; + +type AccountType = 'EOA' | 'Contract'; + +/** + * Represents a VeChain account. + * + * @implements {VeChainDataModel} + */ +abstract class Account implements VeChainDataModel { + public readonly address: Address; + public readonly balance: Currency; + // Replace the string array with a Transaction class #1162 + public readonly transactions: string[]; + protected abstract get type(): AccountType; + + constructor(address: Address, balance: Currency, transactions?: string[]) { + this.address = address; + this.balance = balance; + this.transactions = transactions ?? []; + } + + /** + * Throws an exception because the account cannot be represented as a big integer. + * @returns {bigint} The BigInt representation of the account. + * @throws {InvalidOperation} The account cannot be represented as a bigint. + * @override {@link VeChainDataModel#bi} + * @remark The conversion to BigInt is not supported for an account. + */ + public get bi(): bigint { + throw new InvalidOperation( + 'Account.bi', + 'There is no big integer representation for an account.', + { data: '' } + ); + } + + /** + * Throws an exception because the account cannot be represented as a byte array. + * @returns {Uint8Array} The byte array representation of the account. + * @throws {InvalidOperation} The account cannot be represented as a byte array. + * @override {@link VeChainDataModel#bytes} + * @remark The conversion to byte array is not supported for an account. + */ + public get bytes(): Uint8Array { + throw new InvalidOperation( + 'Account.bytes', + 'There is no bytes representation for an account.', + { data: '' } + ); + } + + /** + * Throws an exception because the account cannot be represented as a number. + * @returns {bigint} The number representation of the account. + * @throws {InvalidOperation} The account cannot be represented as a number. + * @override {@link VeChainDataModel#n} + * @remark The conversion to number is not supported for an account. + */ + public get n(): number { + throw new InvalidOperation( + 'Account.n', + 'There is no number representation for an account.', + { data: '' } + ); + } + + /** + * Adds a transaction to the account. + * @param {string} transaction The transaction to add. + */ + public addTransaction(transaction: string): void { + // Replace body once Transaction class is implemented #1162 + this.transactions.push(transaction); + } + + /** + * Compare this instance with `that` in a meaningful way. + * + * @param {Account} that object to compare. + * @return a negative number if `this` < `that`, zero if `this` = `that`, a positive number if `this` > that`. + * @override {@link VeChainDataModel#compareTo} + */ + public compareTo(that: Account): number { + const typeCompareTo = this.type.localeCompare(that.type); + if (typeCompareTo !== 0) { + return typeCompareTo; + } + const addressCompareTo = this.address.compareTo(that.address); + if (addressCompareTo !== 0) { + return addressCompareTo; + } + return this.balance.compareTo(that.balance); + } + + /** + * Checks if the given value is equal to the current instance. + * + * @param {Account} that - The value to compare. + * @returns {boolean} - True if the values are equal, false otherwise. + * @override {@link VeChainDataModel#isEqual} + */ + public isEqual(that: Account): boolean { + return this.compareTo(that) === 0; + } + + /** + * Returns a string representation of the account. + * + * @returns {string} A string representation of the account. + */ + public toString(): string { + return `${this.type} Address: ${this.address.toString()} Balance: ${this.balance.bi.toString()}`; + } +} + +export { Account }; +export type { AccountType }; diff --git a/packages/core/src/vcdm/account/ExternallyOwnedAccount.ts b/packages/core/src/vcdm/account/ExternallyOwnedAccount.ts new file mode 100644 index 000000000..7027ff516 --- /dev/null +++ b/packages/core/src/vcdm/account/ExternallyOwnedAccount.ts @@ -0,0 +1,83 @@ +import { InvalidDataType } from '@vechain/sdk-errors'; +import { Address } from '../Address'; +import { type Currency } from '../Currency'; +import { type Mnemonic } from '../Mnemonic'; +import { Account, type AccountType } from './Account'; + +/** + * Represents an externally owned account (EOA) on the VeChainThor blockchain. + * + * @extends {Account} + */ +class ExternallyOwnedAccount extends Account { + readonly type: AccountType = 'EOA'; + + private readonly mnemonic: Mnemonic; + // Review whether we need to add the SECP256k1 key pair here #1122 + + constructor( + address: Address, + balance: Currency, + mnemonic: Mnemonic, + transactions?: string[] + ) { + if (!ExternallyOwnedAccount.isValid(address, mnemonic)) { + throw new InvalidDataType( + 'ExternallyOwnedAccount.constructor', + 'The address and mnemonic do not match.', + { address: address.toString() } + ); + } + super(address, balance, transactions); + this.mnemonic = mnemonic; + } + + /** + * Validates that the given address and mnemonic's address match. + * @param {Address} address Address to validate. + * @param {Mnemonic} mnemonic Mnemonic to validate. + * @returns {boolean} True if the address and mnemonic's address match, false otherwise. + */ + public static isValid(address: Address, mnemonic: Mnemonic): boolean { + const addressFromMnemonic = Address.ofMnemonic(mnemonic); + return address.isEqual(addressFromMnemonic); + } + + /** + * Compares the current ExternallyOwnedAccount object with the given ExternallyOwnedAccount object. + * @param {ExternallyOwnedAccount} that - The ExternallyOwnedAccount object to compare with. + * @return {number} - A negative number if the current object is less than the given object, + * zero if they are equal, or a positive number if the current object is greater. + * @override {@link Account#compareTo} + * @remark The comparison is based on the address and mnemonic of the ExternallyOwnedAccount. + */ + public compareTo(that: ExternallyOwnedAccount): number { + const accountCompareTo = super.compareTo(that); + if (accountCompareTo !== 0) { + return accountCompareTo; + } + return this.mnemonic.compareTo(that.mnemonic); + } + + /** + * Checks if the current ExternallyOwnedAccount object is equal to the given ExternallyOwnedAccount object. + * @param {ExternallyOwnedAccount} that - The ExternallyOwnedAccount object to compare with. + * @return {boolean} - True if the objects are equal, false otherwise. + * @override {@link Account#isEqual} + * @remark The comparison is based on the address and mnemonic of the ExternallyOwnedAccount. + */ + public isEqual(that: ExternallyOwnedAccount): boolean { + return super.isEqual(that) && this.mnemonic.isEqual(that.mnemonic); + } + + /** + * Returns a string representation of the ExternallyOwnedAccount. + * + * @returns {string} A string representation of the ExternallyOwnedAccount. + */ + public toString(): string { + return `${super.toString()} Mnemonic: ${this.mnemonic.toString()}`; + } +} + +export { ExternallyOwnedAccount }; diff --git a/packages/core/src/vcdm/account/index.ts b/packages/core/src/vcdm/account/index.ts new file mode 100644 index 000000000..55386a0b7 --- /dev/null +++ b/packages/core/src/vcdm/account/index.ts @@ -0,0 +1,2 @@ +export * from './Account'; +export * from './ExternallyOwnedAccount'; diff --git a/packages/core/src/vcdm/index.ts b/packages/core/src/vcdm/index.ts index 7ab09bb64..bf0544b78 100644 --- a/packages/core/src/vcdm/index.ts +++ b/packages/core/src/vcdm/index.ts @@ -1,3 +1,4 @@ +export * from './account'; export * from './Address'; export * from './Currency'; export * from './Hash'; diff --git a/packages/core/tests/vcdm/account/ExternallyOwnedAccount.unit.test.ts b/packages/core/tests/vcdm/account/ExternallyOwnedAccount.unit.test.ts new file mode 100644 index 000000000..d8d07034b --- /dev/null +++ b/packages/core/tests/vcdm/account/ExternallyOwnedAccount.unit.test.ts @@ -0,0 +1,145 @@ +import { InvalidDataType } from '@vechain/sdk-errors'; +import { Address, Mnemonic } from '../../../src'; +import { Account, ExternallyOwnedAccount } from '../../../src/vcdm/account'; +import { type Currency } from '../../../src/vcdm/Currency'; + +// Use actual Currency subclasses once they are implemented. +const mockCurrency: Currency = { + compareTo: jest.fn().mockReturnValue(0), + bi: 0n, + code: 'VET', + n: 0, + isEqual: jest.fn().mockReturnValue(true), + bytes: new Uint8Array(0) +}; + +/** + * Test ExternallyOwnedAccount class. + * @group unit/vcdm + */ +describe('Account class tests', () => { + const balance = mockCurrency; + const mnemonic = Mnemonic.generate(); + describe('Construction tests', () => { + test('Return an Account instance if the passed arguments are valid', () => { + const address = Address.ofMnemonic(mnemonic); + const account = new ExternallyOwnedAccount( + address, + balance, + mnemonic + ); + expect(account).toBeInstanceOf(Account); + expect(account).toBeInstanceOf(ExternallyOwnedAccount); + account.addTransaction('0x1234567890abcdef'); + expect(account.transactions.length).toBe(1); + }); + test('Throw an error if the passed arguments are invalid', () => { + const address = Address.of( + '0x7Fa3c67d905886Cf5A4E4243F557d69282393693' + ); + const createAccount = (): ExternallyOwnedAccount => { + return new ExternallyOwnedAccount(address, balance, mnemonic); + }; + + expect(createAccount).toThrow(InvalidDataType); + try { + createAccount(); + } catch (e) { + if (e instanceof InvalidDataType) { + expect(e.message).toBe( + `Method 'ExternallyOwnedAccount.constructor' failed.` + + `\n-Reason: 'The address and mnemonic do not match.'` + + `\n-Parameters: \n\t{"address":"${address}"}` + + `\n-Internal error: \n\tNo internal error given` + ); + } + } + }); + }); + describe('Unused methods tests', () => { + test('bi - throw an error', () => { + const address = Address.ofMnemonic(mnemonic); + const account = new ExternallyOwnedAccount( + address, + balance, + mnemonic + ); + expect(() => account.bi).toThrow(); + }); + test('bytes - throw an error', () => { + const address = Address.ofMnemonic(mnemonic); + const account = new ExternallyOwnedAccount( + address, + balance, + mnemonic + ); + expect(() => account.bytes).toThrow(); + }); + test('n - throw an error', () => { + const address = Address.ofMnemonic(mnemonic); + const account = new ExternallyOwnedAccount( + address, + balance, + mnemonic + ); + expect(() => account.n).toThrow(); + }); + }); + describe('VCDM interface tests', () => { + test('compareTo - compare two ExternallyOwnedAccount instances', () => { + const address = Address.ofMnemonic(mnemonic); + const account1 = new ExternallyOwnedAccount( + address, + balance, + mnemonic + ); + const account2 = new ExternallyOwnedAccount( + address, + balance, + mnemonic + ); + expect(account1.compareTo(account2)).toBe(0); + }); + test('compareTo - compare two ExternallyOwnedAccount instances with different addresses', () => { + const address1 = Address.ofMnemonic(mnemonic); + const mnemonic2 = Mnemonic.generate(); + const address2 = Address.ofMnemonic(mnemonic2); + const account1 = new ExternallyOwnedAccount( + address1, + balance, + mnemonic + ); + const account2 = new ExternallyOwnedAccount( + address2, + balance, + mnemonic2 + ); + expect(account1.compareTo(account2)).not.toBe(0); + }); + test('isEqual - compare two ExternallyOwnedAccount instances', () => { + const address = Address.ofMnemonic(mnemonic); + const account1 = new ExternallyOwnedAccount( + address, + balance, + mnemonic + ); + const account2 = new ExternallyOwnedAccount( + address, + balance, + mnemonic + ); + expect(account1.isEqual(account2)).toBe(true); + }); + test('toString - get a string representation of the ExternallyOwnedAccount', () => { + const address = Address.ofMnemonic(mnemonic); + const account = new ExternallyOwnedAccount( + address, + balance, + mnemonic + ); + expect(account.toString()).toBe( + `EOA Address: ${address.toString()} Balance: 0 Mnemonic: ${mnemonic.toString()}` + ); + }); + }); +});