diff --git a/src/sse/event.ts b/src/sse/event.ts index 9fc8fa9c1..84dbde961 100644 --- a/src/sse/event.ts +++ b/src/sse/event.ts @@ -265,8 +265,16 @@ export class DeployProcessedPayload { }) account: PublicKey; - @jsonMember({ name: 'timestamp', constructor: Date }) - timestamp: Date; + @jsonMember({ + name: 'timestamp', + constructor: Timestamp, + deserializer: json => { + if (!json) return; + return Timestamp.fromJSON(json); + }, + serializer: value => value.toJSON() + }) + timestamp: Timestamp; @jsonMember({ name: 'ttl', constructor: String }) ttl: string; @@ -438,8 +446,16 @@ export class TransactionProcessedPayload { }) initiatorAddr: InitiatorAddr; - @jsonMember({ name: 'timestamp', constructor: Date }) - timestamp: Date; + @jsonMember({ + name: 'timestamp', + constructor: Timestamp, + deserializer: json => { + if (!json) return; + return Timestamp.fromJSON(json); + }, + serializer: value => value.toJSON() + }) + timestamp: Timestamp; @jsonMember({ name: 'ttl', constructor: String }) ttl: string; diff --git a/src/types/Bid.ts b/src/types/Bid.ts index 16a320002..c68e6fac0 100644 --- a/src/types/Bid.ts +++ b/src/types/Bid.ts @@ -132,7 +132,10 @@ export class DelegationKind { @jsonMember({ name: 'Purse', constructor: URef, - deserializer: json => URef.fromJSON(json), + deserializer: json => { + if (!json) return; + return URef.fromJSON(json); + }, serializer: value => value.toJSON(), preserveNull: true }) diff --git a/src/types/MessageTopic.ts b/src/types/MessageTopic.ts index 02105ce45..12535fe32 100644 --- a/src/types/MessageTopic.ts +++ b/src/types/MessageTopic.ts @@ -1,5 +1,5 @@ import { jsonObject, jsonMember } from 'typedjson'; -import { EntityAddr, Hash } from './key'; +import { Hash } from './key'; import { ModuleBytes } from './ExecutableDeployItem'; /** @@ -104,12 +104,12 @@ export class Message { * The entity address associated with the message, often the sender or origin. */ @jsonMember({ - name: 'entity_hash', - constructor: EntityAddr, - deserializer: json => EntityAddr.fromJSON(json), + name: 'hash_addr', + constructor: Hash, + deserializer: json => Hash.fromJSON(json), serializer: value => value.toJSON() }) - entityHash: EntityAddr; + hashAddr: Hash; /** * The index of the block where the message was included. diff --git a/src/types/StoredValue.test.ts b/src/types/StoredValue.test.ts index a3347b833..ebfeac986 100644 --- a/src/types/StoredValue.test.ts +++ b/src/types/StoredValue.test.ts @@ -1,8 +1,197 @@ import { TypedJSON } from 'typedjson'; +import { expect } from 'chai'; + import { StoredValue } from './StoredValue'; -import { expect } from "chai"; +import { DelegatorAllocation, ValidatorAllocation } from './EraInfo'; describe('Test StoredValue', () => { + const json = { + EraInfo: { + seigniorage_allocations: [ + { + Delegator: { + delegator_kind: { + PublicKey: + '018b46617b2b97e633b36530f2964b3f4c15916235910a2737e83d4fa2c7fad542' + }, + validator_public_key: + '01509254f22690fbe7fb6134be574c4fbdb060dfa699964653b99753485e518ea6', + amount: '2515330120214391' + } + }, + { + Validator: { + validator_public_key: + '01509254f22690fbe7fb6134be574c4fbdb060dfa699964653b99753485e518ea6', + amount: '2728720430156545' + } + }, + { + Delegator: { + delegator_kind: { + PublicKey: + '018b46617b2b97e633b36530f2964b3f4c15916235910a2737e83d4fa2c7fad542' + }, + validator_public_key: + '01509254f22690fbe7fb6134be574c4fbdb060dfa699964653b99753485e518ea6', + amount: '109303520813010' + } + }, + { + Validator: { + validator_public_key: + '01509254f22690fbe7fb6134be574c4fbdb060dfa699964653b99753485e518ea6', + amount: '118554941151112' + } + }, + { + Delegator: { + delegator_kind: { + PublicKey: + '0197b79d1a1351f8fb922b9f7f556d2bbfdba5105df9eaa6caa07804c703a641ed' + }, + validator_public_key: + '0190664e16a17594ed2d0e3c279c4cf5894e8db0da15e3b91c938562a1caae32ab', + amount: '8599696498056110' + } + }, + { + Validator: { + validator_public_key: + '0190664e16a17594ed2d0e3c279c4cf5894e8db0da15e3b91c938562a1caae32ab', + amount: '9377950843219784' + } + }, + { + Delegator: { + delegator_kind: { + PublicKey: + '0197b79d1a1351f8fb922b9f7f556d2bbfdba5105df9eaa6caa07804c703a641ed' + }, + validator_public_key: + '0190664e16a17594ed2d0e3c279c4cf5894e8db0da15e3b91c938562a1caae32ab', + amount: '285067736921916' + } + }, + { + Validator: { + validator_public_key: + '0190664e16a17594ed2d0e3c279c4cf5894e8db0da15e3b91c938562a1caae32ab', + amount: '310701366981535' + } + }, + { + Delegator: { + delegator_kind: { + PublicKey: + '01a5a5b7328118681638be3e06c8749609280dba4c9daf9aeb3d3464b8839b018a' + }, + validator_public_key: + '01c867ff3cf1d4e4e68fc00922fdcb740304def196e223091dee62012f444b9eba', + amount: '5976757455713484' + } + }, + { + Validator: { + validator_public_key: + '01c867ff3cf1d4e4e68fc00922fdcb740304def196e223091dee62012f444b9eba', + amount: '6492754998004464' + } + }, + { + Delegator: { + delegator_kind: { + PublicKey: + '01a5a5b7328118681638be3e06c8749609280dba4c9daf9aeb3d3464b8839b018a' + }, + validator_public_key: + '01c867ff3cf1d4e4e68fc00922fdcb740304def196e223091dee62012f444b9eba', + amount: '162277940193805' + } + }, + { + Validator: { + validator_public_key: + '01c867ff3cf1d4e4e68fc00922fdcb740304def196e223091dee62012f444b9eba', + amount: '176125500882714' + } + }, + { + Delegator: { + delegator_kind: { + PublicKey: + '0106ed45915392c02b37136618372ac8dde8e0e3b8ee6190b2ca6db539b354ede4' + }, + validator_public_key: + '01f58b94526d280881f79744effebc555426190950d5dfdd2f8aaf10ceaec010c6', + amount: '6111063397723576' + } + }, + { + Validator: { + validator_public_key: + '01f58b94526d280881f79744effebc555426190950d5dfdd2f8aaf10ceaec010c6', + amount: '6660504858490961' + } + }, + { + Delegator: { + delegator_kind: { + PublicKey: + '0106ed45915392c02b37136618372ac8dde8e0e3b8ee6190b2ca6db539b354ede4' + }, + validator_public_key: + '01f58b94526d280881f79744effebc555426190950d5dfdd2f8aaf10ceaec010c6', + amount: '183204228041446' + } + }, + { + Validator: { + validator_public_key: + '01f58b94526d280881f79744effebc555426190950d5dfdd2f8aaf10ceaec010c6', + amount: '199637730476608' + } + }, + { + Delegator: { + delegator_kind: { + PublicKey: + '0184f6d260f4ee6869ddb36affe15456de6ae045278fa2f467bb677561ce0dad55' + }, + validator_public_key: + '01fed662dc7f1f7af43ad785ba07a8cc05b7a96f9ee69613cfde43bc56bec1140b', + amount: '2170319328593039' + } + }, + { + Validator: { + validator_public_key: + '01fed662dc7f1f7af43ad785ba07a8cc05b7a96f9ee69613cfde43bc56bec1140b', + amount: '2366902069827651' + } + }, + { + Delegator: { + delegator_kind: { + PublicKey: + '0184f6d260f4ee6869ddb36affe15456de6ae045278fa2f467bb677561ce0dad55' + }, + validator_public_key: + '01fed662dc7f1f7af43ad785ba07a8cc05b7a96f9ee69613cfde43bc56bec1140b', + amount: '217749920954248' + } + }, + { + Validator: { + validator_public_key: + '01fed662dc7f1f7af43ad785ba07a8cc05b7a96f9ee69613cfde43bc56bec1140b', + amount: '237377113583604' + } + } + ] + } + }; + it('should parse json to StoredValue', async () => { const json = { jsonrpc: '2.0', @@ -35,4 +224,29 @@ describe('Test StoredValue', () => { const parsed = serializer.parse(json.result.stored_value); expect(parsed).to.be.instanceOf(StoredValue); }); + + it('should parse the EraInfo JSON into a StoredValue instance', () => { + const parsedValue = new TypedJSON(StoredValue).parse(json); + expect(parsedValue).to.be.an.instanceOf(StoredValue); + expect(parsedValue?.eraInfo).to.exist; + }); + + it('should contain valid seigniorage allocations', () => { + const parsedValue = new TypedJSON(StoredValue).parse(json); + const allocations = parsedValue?.eraInfo?.seigniorageAllocations; + expect(allocations).to.exist; + expect(allocations).to.have.lengthOf(20); + }); + + it('should ensure each allocation is an instance of the correct class', () => { + const parsedValue = new TypedJSON(StoredValue).parse(json); + const allocations = parsedValue?.eraInfo?.seigniorageAllocations || []; + allocations.forEach(allocation => { + if (allocation.delegator) { + expect(allocation.delegator).to.be.an.instanceOf(DelegatorAllocation); + } else if (allocation.validator) { + expect(allocation.validator).to.be.an.instanceOf(ValidatorAllocation); + } + }); + }); }); diff --git a/src/types/key/Key.test.ts b/src/types/key/Key.test.ts new file mode 100644 index 000000000..253ef0e07 --- /dev/null +++ b/src/types/key/Key.test.ts @@ -0,0 +1,34 @@ +import { expect } from 'chai'; + +import { MessageAddr } from './MessageAddr'; +import { Key, KeyTypeID } from './Key'; + +describe('Key', () => { + const hashAddr = + '55d4a6915291da12afded37fa5bc01f0803a2f0faf6acb7ec4c7ca6ab76f3330'; + const topicStr = + '5721a6d9d7a9afe5dfdb35276fb823bed0f825350e4d865a5ec0110c380de4e1'; + const msgKeyStr = `message-topic-${hashAddr}-${topicStr}`; + + it('should correctly parse a key with hash address, topic hash, and index', () => { + const messageAddr = MessageAddr.fromString(msgKeyStr); + expect(messageAddr.hashAddr.toHex()).to.equal(hashAddr); + expect(messageAddr.topicNameHash.toHex()).to.equal(topicStr); + }); + + it('should correctly create a new key for message by type', () => { + const key = Key.createByType(msgKeyStr, KeyTypeID.Message); + + expect(key.toPrefixedString()).to.equal(msgKeyStr); + expect(key.message?.hashAddr.toHex()).to.equal(hashAddr); + expect(key.message?.topicNameHash.toHex()).to.equal(topicStr); + }); + + it('should correctly create a new key for message', () => { + const key = Key.newKey(msgKeyStr); + + expect(key.toPrefixedString()).to.equal(msgKeyStr); + expect(key.message?.hashAddr.toHex()).to.equal(hashAddr); + expect(key.message?.topicNameHash.toHex()).to.equal(topicStr); + }); +}); diff --git a/src/types/key/Key.ts b/src/types/key/Key.ts index 11e49005d..0e3090c82 100644 --- a/src/types/key/Key.ts +++ b/src/types/key/Key.ts @@ -778,9 +778,7 @@ export class Key { ); break; case KeyTypeID.Message: - result.message = MessageAddr.fromString( - source.replace(PrefixName.Message, '') - ); + result.message = MessageAddr.fromString(source); break; case KeyTypeID.NamedKey: result.namedKey = NamedKeyAddr.fromString( diff --git a/src/types/key/MessageAddr.ts b/src/types/key/MessageAddr.ts index 8b343aa4e..bdc52fe05 100644 --- a/src/types/key/MessageAddr.ts +++ b/src/types/key/MessageAddr.ts @@ -1,6 +1,6 @@ import { concat } from '@ethersproject/bytes'; import { jsonMember, jsonObject } from 'typedjson'; -import { EntityAddr } from './EntityAddr'; + import { Hash } from './Hash'; import { IResultWithBytes } from '../clvalue'; @@ -10,9 +10,6 @@ const TopicPrefix = 'topic-'; /** Prefix for messages in MessageAddr. */ const PrefixNameMessage = 'message-'; -/** Prefix for addressable entities in MessageAddr. */ -const PrefixNameAddressableEntity = 'entity-'; - /** * Represents an addressable message within the system. The address is composed of an associated entity address, * a hashed topic name, and an optional message index. It offers various utilities for serialization, deserialization, @@ -22,28 +19,27 @@ const PrefixNameAddressableEntity = 'entity-'; export class MessageAddr { /** * Creates an instance of MessageAddr. - * @param entityAddr - The address of the associated entity. + * @param hashAddr - The address of the associated entity. * @param topicNameHash - The hash of the topic name. * @param messageIndex - Optional index of the message. */ - constructor( - entityAddr: EntityAddr, - topicNameHash: Hash, - messageIndex?: number - ) { - this.entityAddr = entityAddr; + constructor(hashAddr: Hash, topicNameHash: Hash, messageIndex?: number) { + this.hashAddr = hashAddr; this.topicNameHash = topicNameHash; this.messageIndex = messageIndex; } /** The address of the associated entity. */ @jsonMember({ - name: 'EntityAddr', - constructor: EntityAddr, - deserializer: json => EntityAddr.fromJSON(json), + name: 'HashAddr', + constructor: Hash, + deserializer: json => { + if (!json) return; + return Hash.fromJSON(json); + }, serializer: value => value.toJSON() }) - public entityAddr: EntityAddr; + public hashAddr: Hash; /** The hash of the topic name associated with this message. */ @jsonMember({ name: 'TopicNameHash', constructor: Hash }) @@ -54,38 +50,64 @@ export class MessageAddr { public messageIndex?: number; /** - * Instantiates a `MessageAddr` from its string representation. + * Instantiates a MessageAddr from its string representation. * The string should follow the prefixed format used in the system. * @param source - The string representation of the MessageAddr. * @returns A new MessageAddr instance. * @throws Error if the provided string does not match the expected format. */ static fromString(source: string): MessageAddr { - let messageIndex: number | undefined; + if (!source.startsWith(PrefixNameMessage)) { + throw new Error( + `Key not valid. It should start with '${PrefixNameMessage}'.` + ); + } + + source = source.substring(PrefixNameMessage.length); - if (!source.startsWith(TopicPrefix)) { - const lastHyphen = source.lastIndexOf('-'); - const rawId = source.substring(lastHyphen + 1); - source = source.substring(0, lastHyphen); + let hashAddr: string; + let topicHash: string; + let index: number | undefined; - const idx = parseInt(rawId, 10); - if (isNaN(idx)) { - throw new Error('Invalid MessageAddr format: invalid index.'); + if (source.startsWith(TopicPrefix)) { + source = source.substring(TopicPrefix.length); + const parts = source.split('-'); + + if (parts.length === 2) { + hashAddr = parts[0]; + topicHash = parts[1]; + } else { + throw new Error( + 'Key not valid. It should have a hash address and a topic hash.' + ); } - messageIndex = idx; } else { - source = source.slice(TopicPrefix.length); + const parts = source.split('-'); + + if (parts.length === 3) { + hashAddr = parts[0]; + topicHash = parts[1]; + + if (parts[2].length === 0) { + throw new Error('Key not valid. Expected a non-empty message index.'); + } + index = parseInt(parts[2], 16); + + if (isNaN(index)) { + throw new Error('Key not valid. Index is not a valid number.'); + } + } else { + throw new Error( + 'Key not valid. It should have a hash address, a topic hash, and a message index.' + ); + } } - const topicNameHashStr = source.substring(source.lastIndexOf('-') + 1); - const topicNameHash = Hash.fromHex(topicNameHashStr); - - const entityAddrStr = source - .substring(0, source.lastIndexOf('-')) - .replace(PrefixNameAddressableEntity, ''); - const entityAddr = EntityAddr.fromPrefixedString(entityAddrStr); - - return new MessageAddr(entityAddr, topicNameHash, messageIndex); + return new MessageAddr( + Hash.fromHex(hashAddr), + Hash.fromHex(topicHash), + index + ); } /** @@ -98,7 +120,7 @@ export class MessageAddr { if (!this.messageIndex) { result += TopicPrefix; } - result += this.entityAddr.toPrefixedString(); + result += this.hashAddr.toHex(); result += '-' + this.topicNameHash.toHex(); if (this.messageIndex !== undefined) { @@ -125,7 +147,7 @@ export class MessageAddr { * @returns A new `MessageAddr` instance wrapped in an `IResultWithBytes`. */ static fromBytes(bytes: Uint8Array): IResultWithBytes { - const entityAddr = EntityAddr.fromBytes(bytes); + const entityAddr = Hash.fromBytes(bytes); const topicNameHash = Hash.fromBytes(bytes); let messageIndex: number | undefined; @@ -150,7 +172,7 @@ export class MessageAddr { * @returns A `Uint8Array` representing the `MessageAddr`. */ toBytes(): Uint8Array { - const entityBytes = this.entityAddr.toBytes(); + const entityBytes = this.hashAddr.toBytes(); const topicBytes = this.topicNameHash.toBytes(); const result = new Uint8Array( entityBytes.length +