Skip to content

Commit

Permalink
Merge pull request #472 from casper-ecosystem/update-message-serializ…
Browse files Browse the repository at this point in the history
…ation

Update MessageTopic serialization, MessageAddr key parsing
alexmyshchyshyn authored Dec 20, 2024
2 parents 466fcdb + 1d536e5 commit 5a2dd95
Showing 7 changed files with 339 additions and 52 deletions.
24 changes: 20 additions & 4 deletions src/sse/event.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 4 additions & 1 deletion src/types/Bid.ts
Original file line number Diff line number Diff line change
@@ -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
})
10 changes: 5 additions & 5 deletions src/types/MessageTopic.ts
Original file line number Diff line number Diff line change
@@ -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.
216 changes: 215 additions & 1 deletion src/types/StoredValue.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
});
34 changes: 34 additions & 0 deletions src/types/key/Key.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
4 changes: 1 addition & 3 deletions src/types/key/Key.ts
Original file line number Diff line number Diff line change
@@ -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(
98 changes: 60 additions & 38 deletions src/types/key/MessageAddr.ts
Original file line number Diff line number Diff line change
@@ -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<MessageAddr> {
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 +

0 comments on commit 5a2dd95

Please sign in to comment.