From c01514eec8456987d1b4f1cec467901930da40ca Mon Sep 17 00:00:00 2001 From: Dmytro Vynnyk Date: Wed, 29 Jan 2025 00:20:53 +0100 Subject: [PATCH 1/3] Fix issue with BlockTransaction serialisation and rewardedSignatures type --- src/rpc/rpc_client.test.ts | 101 ++++++++++++++++++++++++++++++++++++- src/types/Block.ts | 51 +++++++++++-------- 2 files changed, 130 insertions(+), 22 deletions(-) diff --git a/src/rpc/rpc_client.test.ts b/src/rpc/rpc_client.test.ts index 4aa5361b9..f8f88a0f7 100644 --- a/src/rpc/rpc_client.test.ts +++ b/src/rpc/rpc_client.test.ts @@ -1,7 +1,11 @@ import { TypedJSON } from 'typedjson'; import { expect } from 'chai'; -import { InfoGetTransactionResultV1Compatible } from './response'; +import { + ChainGetBlockResult, + ChainGetBlockResultV1Compatible, + InfoGetTransactionResultV1Compatible +} from './response'; describe('RPC Client', () => { it('should be able to parse getTransactionByTransactionHash response', () => { @@ -460,4 +464,99 @@ describe('RPC Client', () => { txResult.executionInfo?.executionResult.transfers[0].transactionHash.toString() ).to.deep.equal(txResult.transaction.hash.toHex()); }); + + it('Should process RPC.getBlockByHash response', () => { + const json = { + jsonrpc: '2.0', + id: '1', + result: { + api_version: '2.0.0', + block_with_signatures: { + block: { + Version2: { + hash: + 'b51e5d16944c63eab276e6c5494fd39a454418e3a3f6aa519797882b6c3c0d4b', + header: { + parent_hash: + '11040b60955bc9921dfbeff3abf4ee3e1c702865b0183638cd5eba922cd34fe3', + state_root_hash: + 'cf1d7c8d33ce0fbc6c5211000ade4cc88ab671cb51e8be6e458c774dd82f8ab0', + body_hash: + 'ab363e6bf68b4190176b53196caa16954d2eb97f3f0dbe004b16f0e6951893cc', + random_bit: true, + accumulated_seed: + '22cfe5e17ebb2f79af99556e2f134051d8a64e6ff6cb96eac5378f24b8b8a486', + era_end: null, + timestamp: '2025-01-27T10:51:36.846Z', + era_id: 14828, + height: 3444515, + protocol_version: '2.0.1', + proposer: + '0140afe8f752e5ff100e0189c080bc207e8805b3e5e82f792ec608de2f11f39f6c', + current_gas_price: 1, + last_switch_block_hash: + 'a265470a3663bbf88ab37d276cd1786c5e0f53f3da5a8fc268dfddedaf890b93' + }, + body: { + transactions: { + '3': [ + { + Deploy: + 'c102a6fed7f4b9a435ca8584b4d2f704f419d523a1c3f7199799b534df264c9a' + } + ] + }, + rewarded_signatures: [[240], [0], [0]] + } + } + }, + proofs: [ + { + public_key: + '01032146b0b9de01e26aaec7b0d1769920de94681dbd432c3530bfe591752ded6c', + signature: + '0120730fd151cfd40d8b86abfc679c66dd50f49d0c76fe6d68a75d7f18efd05b0363009b98f2c0e760e5426abdaab11401e0223e1306d13474eb910434da197d07' + }, + { + public_key: + '0126d4637eb0c0769274f03a696df1112383fa621c9f73f57af4c5c0fbadafa8cf', + signature: + '01ffcb8fdcf25cda1a673a5e43c37990147d0e6091f2eb51b5e295594270b4dd57b32c06e00275acbd8398ed6ee6f454d3431e2724a8fc8d37eeece9badb2bad00' + }, + { + public_key: + '0140afe8f752e5ff100e0189c080bc207e8805b3e5e82f792ec608de2f11f39f6c', + signature: + '018fa481deb7701276f13e1a6d97cc9fa3c4b720eaf357e217caf687082401d4803d580a41247a3588e78a210622745e282b3429420c1a184a8cda8ff6d9886f07' + }, + { + public_key: + '017536433a73f7562526f3e9fcb8d720428ae2d28788a9909f3c6f637a9d848a4b', + signature: + '01527594370613aa56be947badb595f2fbd96611debda997afca05c3b4a93ecca59e2fbc0fb34174ba222824e03476272ee0c1c236bfbb086fc899b921f3da140e' + } + ] + } + } + }; + + const serializer = new TypedJSON(ChainGetBlockResultV1Compatible); + const result = serializer.parse(json.result)!; + result.rawJSON = json.result; + + const blockResult = ChainGetBlockResult.newChainGetBlockResultFromV1Compatible( + result, + result.rawJSON + ); + blockResult.rawJSON = json.result; + + expect(blockResult).to.be.not.undefined; + expect(blockResult).to.be.not.empty; + expect(blockResult.block?.hash.toHex()).to.deep.equal( + json.result.block_with_signatures.block.Version2.hash + ); + expect( + result.blockWithSignatures?.block.blockV2?.hash?.toHex() + ).to.deep.equal(json.result.block_with_signatures.block.Version2.hash); + }); }); diff --git a/src/types/Block.ts b/src/types/Block.ts index fbeb53d98..8089887e4 100644 --- a/src/types/Block.ts +++ b/src/types/Block.ts @@ -1,4 +1,4 @@ -import { jsonObject, jsonMember, jsonArrayMember } from 'typedjson'; +import { jsonObject, jsonMember, jsonArrayMember, TypedJSON } from 'typedjson'; import { Hash } from './key'; import { Timestamp } from './Time'; import { Proposer } from './BlockProposer'; @@ -180,8 +180,8 @@ export class Block { /** * A list of signature IDs that were rewarded in this block. */ - @jsonArrayMember(Number, { name: 'rewarded_signatures' }) - public rewardedSignatures: number[]; + @jsonArrayMember(Number, { dimensions: 2 }) + public rewardedSignatures: number[][]; /** * A list of proofs associated with this block. @@ -235,7 +235,7 @@ export class Block { protocolVersion: string | undefined, eraEnd: EraEnd | undefined, transactions: BlockTransaction[], - rewardedSignatures: number[], + rewardedSignatures: number[][], proofs: Proof[], originBlockV1?: BlockV1, originBlockV2?: BlockV2 @@ -427,13 +427,14 @@ export class BlockTransaction { * console.log(transactions); // Outputs an array of BlockTransaction instances. */ public static fromJSON(data: any): BlockTransaction[] { + const serializer = new TypedJSON(TransactionHash) const source = { - Mint: data['0'] || [], - Auction: data['1'] || [], - InstallUpgrade: data['2'] || [], - Large: data['3'] || [], - Medium: data['4'] || [], - Small: data['5'] || [] + Mint: (data['0'] || []).map((json: any) => serializer.parse(json)), + Auction: (data['1'] || []).map((json: any) => serializer.parse(json)), + InstallUpgrade: (data['2'] || []).map((json: any) => serializer.parse(json)), + Large: (data['3'] || []).map((json: any) => serializer.parse(json)), + Medium: (data['4'] || []).map((json: any) => serializer.parse(json)), + Small: (data['5'] || []).map((json: any) => serializer.parse(json)) }; const transactions: BlockTransaction[] = []; @@ -490,15 +491,24 @@ function getBlockTransactionsFromTransactionHashes( return hashes.map(hash => { const transactionHash = hash.transactionV1; + const deployHash = hash.deploy; - if (!transactionHash) { - throw new Error('Invalid TransactionHash: transactionV1 is undefined'); + if (transactionHash) { + return new BlockTransaction( + category, + TransactionVersion.V1, + transactionHash + ); + } else if (deployHash) { + return new BlockTransaction( + category, + TransactionVersion.Deploy, + deployHash + ); } - return new BlockTransaction( - category, - TransactionVersion.V1, - transactionHash + throw new Error( + 'Invalid TransactionHash: transactionV1 and deploy is undefined' ); }); } @@ -895,18 +905,17 @@ export class BlockBodyV2 { /** * The list of transactions included in the block. */ - @jsonArrayMember(BlockTransaction, { + @jsonMember(BlockTransaction, { name: 'transactions', - deserializer: (json: any) => - json.map((it: string) => BlockTransaction.fromJSON(it)) + deserializer: (json: any) => BlockTransaction.fromJSON(json) }) public transactions: BlockTransaction[]; /** * The list of signature IDs that were rewarded in this block. */ - @jsonArrayMember(Number, { name: 'rewarded_signatures' }) - public rewardedSignatures: number[]; + @jsonArrayMember(Number, { dimensions: 2 }) + public rewardedSignatures: number[][]; } /** From e5a8dd9f5f912edaba947058612c91b3cdb21dbf Mon Sep 17 00:00:00 2001 From: Dmytro Vynnyk Date: Thu, 30 Jan 2025 12:02:28 +0100 Subject: [PATCH 2/3] Add BlockTransaction serialisation --- src/rpc/rpc_client.test.ts | 9 +++++++++ src/types/Block.ts | 20 +++++++++++++++++--- src/utils/common.ts | 3 +++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 src/utils/common.ts diff --git a/src/rpc/rpc_client.test.ts b/src/rpc/rpc_client.test.ts index f8f88a0f7..021c51a5f 100644 --- a/src/rpc/rpc_client.test.ts +++ b/src/rpc/rpc_client.test.ts @@ -6,6 +6,7 @@ import { ChainGetBlockResultV1Compatible, InfoGetTransactionResultV1Compatible } from './response'; +import { BlockBodyV2 } from '../types'; describe('RPC Client', () => { it('should be able to parse getTransactionByTransactionHash response', () => { @@ -544,6 +545,11 @@ describe('RPC Client', () => { const result = serializer.parse(json.result)!; result.rawJSON = json.result; + const ser = new TypedJSON(BlockBodyV2); + const blockV2BodyJson = JSON.stringify( + ser.toPlainJson(result.blockWithSignatures!.block.blockV2!.body) + ); + const blockResult = ChainGetBlockResult.newChainGetBlockResultFromV1Compatible( result, result.rawJSON @@ -558,5 +564,8 @@ describe('RPC Client', () => { expect( result.blockWithSignatures?.block.blockV2?.hash?.toHex() ).to.deep.equal(json.result.block_with_signatures.block.Version2.hash); + expect(blockV2BodyJson).to.equal( + JSON.stringify(json.result.block_with_signatures.block.Version2.body) + ); }); }); diff --git a/src/types/Block.ts b/src/types/Block.ts index 8089887e4..9fa6a260f 100644 --- a/src/types/Block.ts +++ b/src/types/Block.ts @@ -10,6 +10,7 @@ import { } from './Transaction'; import { PublicKey } from './keypair'; import { HexBytes } from './HexBytes'; +import { getEnumKeyByValue } from "../utils"; /** * Represents a proof containing a public key and a signature, used for validating the authenticity of data. @@ -180,7 +181,7 @@ export class Block { /** * A list of signature IDs that were rewarded in this block. */ - @jsonArrayMember(Number, { dimensions: 2 }) + @jsonArrayMember(Number, { dimensions: 2, name: 'rewarded_signatures' }) public rewardedSignatures: number[][]; /** @@ -467,6 +468,12 @@ export class BlockTransaction { return transactions; } + + public toJSON(): string { + return JSON.stringify({ + [this.category.toString()]: [{[getEnumKeyByValue(TransactionVersion, this.version) ?? '']: this.hash.toJSON()}] + }); + } } /** @@ -907,14 +914,21 @@ export class BlockBodyV2 { */ @jsonMember(BlockTransaction, { name: 'transactions', - deserializer: (json: any) => BlockTransaction.fromJSON(json) + deserializer: (json: any) => BlockTransaction.fromJSON(json), + serializer: (value: BlockTransaction[]) => { + return { + ...value.reduce((acc, tx) => { + return {...acc, ...JSON.parse(tx.toJSON())}; + }, {}) + }; + } }) public transactions: BlockTransaction[]; /** * The list of signature IDs that were rewarded in this block. */ - @jsonArrayMember(Number, { dimensions: 2 }) + @jsonArrayMember(Number, { dimensions: 2, name: 'rewarded_signatures' }) public rewardedSignatures: number[][]; } diff --git a/src/utils/common.ts b/src/utils/common.ts new file mode 100644 index 000000000..4050f62a3 --- /dev/null +++ b/src/utils/common.ts @@ -0,0 +1,3 @@ +export function getEnumKeyByValue(enumObj: T, value: T[keyof T]): string | undefined { + return Object.keys(enumObj).find(key => enumObj[key as keyof T] === value); +} From a9890c7e29092b102166c02213260608842407fe Mon Sep 17 00:00:00 2001 From: Dmytro Vynnyk Date: Thu, 30 Jan 2025 12:03:07 +0100 Subject: [PATCH 3/3] Add timestamp optional param --- src/utils/auction-manager.ts | 17 ++++++++++++++--- src/utils/cep-18-transfer.ts | 15 ++++++++++++--- src/utils/cep-nft-transfer.ts | 12 ++++++++++-- src/utils/cspr-transfer.ts | 10 +++++++++- src/utils/index.ts | 1 + 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/utils/auction-manager.ts b/src/utils/auction-manager.ts index 85edcf39a..2fca29a69 100644 --- a/src/utils/auction-manager.ts +++ b/src/utils/auction-manager.ts @@ -9,13 +9,17 @@ import { Duration, ExecutableDeployItem, PublicKey, - StoredContractByHash + StoredContractByHash, + Timestamp } from '../types'; import { AuctionManagerEntryPoint, CasperNetworkName } from '../@types'; import { AuctionManagerContractHashMap } from './constants'; export interface IMakeAuctionManagerDeployParams { - contractEntryPoint: AuctionManagerEntryPoint.delegate | AuctionManagerEntryPoint.undelegate | AuctionManagerEntryPoint.redelegate; + contractEntryPoint: + | AuctionManagerEntryPoint.delegate + | AuctionManagerEntryPoint.undelegate + | AuctionManagerEntryPoint.redelegate; delegatorPublicKeyHex: string; validatorPublicKeyHex: string; newValidatorPublicKeyHex?: string; @@ -23,6 +27,7 @@ export interface IMakeAuctionManagerDeployParams { paymentAmount?: string; chainName?: CasperNetworkName; ttl?: number; + timestamp?: string; } /** @@ -49,6 +54,7 @@ export interface IMakeAuctionManagerDeployParams { * @param params.ttl - (Optional) The time-to-live (TTL) for the `Deploy` in milliseconds. * Specifies how long the `Deploy` is valid before it expires. * Defaults 1800000 (30 minutes) + * @param params.timestamp - (Optional) The timestamp in ISO 8601 format * * @returns A deploy object that can be signed and sent to the network. * @@ -74,7 +80,8 @@ export const makeAuctionManagerDeploy = ({ paymentAmount = '2500000000', chainName = CasperNetworkName.Mainnet, newValidatorPublicKeyHex, - ttl = DEFAULT_DEPLOY_TTL + ttl = DEFAULT_DEPLOY_TTL, + timestamp }: IMakeAuctionManagerDeployParams) => { const delegatorPublicKey = PublicKey.newPublicKey(delegatorPublicKeyHex); const validatorPublicKey = PublicKey.newPublicKey(validatorPublicKeyHex); @@ -107,5 +114,9 @@ export const makeAuctionManagerDeploy = ({ deployHeader.chainName = chainName; deployHeader.ttl = new Duration(ttl); + if (timestamp) { + deployHeader.timestamp = Timestamp.fromJSON(timestamp); + } + return Deploy.makeDeploy(deployHeader, payment, session); }; diff --git a/src/utils/cep-18-transfer.ts b/src/utils/cep-18-transfer.ts index e27113739..d464c8f50 100644 --- a/src/utils/cep-18-transfer.ts +++ b/src/utils/cep-18-transfer.ts @@ -11,7 +11,8 @@ import { Key, KeyTypeID, PublicKey, - StoredVersionedContractByHash + StoredVersionedContractByHash, + Timestamp } from '../types'; import { CasperNetworkName } from '../@types'; @@ -23,6 +24,7 @@ export interface IMakeCep18TransferDeployParams { paymentAmount: string; chainName?: string; ttl?: number; + timestamp?: string; } /** @@ -43,6 +45,8 @@ export interface IMakeCep18TransferDeployParams { * @param params.ttl - (Optional) The time-to-live (TTL) for the `Deploy` in milliseconds. * Specifies how long the `Deploy` is valid before it expires. * Defaults 1800000 (30 minutes) + * @param params.timestamp - (Optional) The timestamp in ISO 8601 format + * * @returns A promise that resolves to the created Deploy instance, ready to be sent to the Casper network. * * @example @@ -68,7 +72,8 @@ export const makeCep18TransferDeploy = ({ transferAmount, paymentAmount, chainName = CasperNetworkName.Mainnet, - ttl = DEFAULT_DEPLOY_TTL + ttl = DEFAULT_DEPLOY_TTL, + timestamp }: IMakeCep18TransferDeployParams): Deploy => { const senderPublicKey = PublicKey.newPublicKey(senderPublicKeyHex); const recipientPublicKey = PublicKey.newPublicKey(recipientPublicKeyHex); @@ -86,7 +91,7 @@ export const makeCep18TransferDeploy = ({ ) ), amount: CLValueUInt256.newCLUInt256(transferAmount) - }), + }) ); const payment = ExecutableDeployItem.standardPayment(paymentAmount); @@ -96,5 +101,9 @@ export const makeCep18TransferDeploy = ({ deployHeader.chainName = chainName; deployHeader.ttl = new Duration(ttl); + if (timestamp) { + deployHeader.timestamp = Timestamp.fromJSON(timestamp); + } + return Deploy.makeDeploy(deployHeader, payment, session); }; diff --git a/src/utils/cep-nft-transfer.ts b/src/utils/cep-nft-transfer.ts index 325729b0d..79463ed20 100644 --- a/src/utils/cep-nft-transfer.ts +++ b/src/utils/cep-nft-transfer.ts @@ -15,7 +15,8 @@ import { Key, KeyTypeID, PublicKey, - StoredVersionedContractByHash + StoredVersionedContractByHash, + Timestamp } from '../types'; import { CasperNetworkName, NFTTokenStandard } from '../@types'; @@ -29,6 +30,7 @@ export interface IMakeNftTransferDeployParams { ttl?: number; tokenId?: string; tokenHash?: string; + timestamp?: string; } /** @@ -45,6 +47,7 @@ export interface IMakeNftTransferDeployParams { * @param params.ttl - The time-to-live (TTL) for the deploy in milliseconds. Defaults to the constant `DEFAULT_DEPLOY_TTL`. * @param params.tokenId - The ID of the token to transfer. Optional and used if the standard requires it. * @param params.tokenHash - The hash of the token to transfer. Optional and used if the standard requires it. + * @param params.timestamp - (Optional) The timestamp in ISO 8601 format * * @returns A deploy object representing the NFT transfer operation. * @@ -73,7 +76,8 @@ export const makeNftTransferDeploy = ({ chainName = CasperNetworkName.Mainnet, ttl = DEFAULT_DEPLOY_TTL, tokenId, - tokenHash + tokenHash, + timestamp }: IMakeNftTransferDeployParams): Deploy => { const senderPublicKey = PublicKey.newPublicKey(senderPublicKeyHex); @@ -119,6 +123,10 @@ export const makeNftTransferDeploy = ({ deployHeader.chainName = chainName; deployHeader.ttl = new Duration(ttl); + if (timestamp) { + deployHeader.timestamp = Timestamp.fromJSON(timestamp); + } + return Deploy.makeDeploy(deployHeader, payment, session); }; diff --git a/src/utils/cspr-transfer.ts b/src/utils/cspr-transfer.ts index 89cd41775..dfc297056 100644 --- a/src/utils/cspr-transfer.ts +++ b/src/utils/cspr-transfer.ts @@ -5,6 +5,7 @@ import { Duration, ExecutableDeployItem, PublicKey, + Timestamp, TransferDeployItem } from '../types'; import { CasperNetworkName } from '../@types'; @@ -16,6 +17,7 @@ export interface IMakeCsprTransferDeployParams { chainName?: string; memo?: string; ttl?: number; + timestamp?: string; } /** @@ -37,6 +39,7 @@ export interface IMakeCsprTransferDeployParams { * @param params.ttl - (Optional) The time-to-live (TTL) for the `Deploy` in milliseconds. * Specifies how long the `Deploy` is valid before it expires. * Defaults 1800000 (30 minutes) + * @param params.timestamp - (Optional) The timestamp in ISO 8601 format * * @returns A promise that resolves to the created Deploy instance, ready to be sent to the Casper network. * @@ -59,7 +62,8 @@ export const makeCsprTransferDeploy = ({ transferAmount, chainName = CasperNetworkName.Mainnet, memo, - ttl = DEFAULT_DEPLOY_TTL + ttl = DEFAULT_DEPLOY_TTL, + timestamp }: IMakeCsprTransferDeployParams) => { const recipientKey = PublicKey.newPublicKey(recipientPublicKeyHex); const senderKey = PublicKey.newPublicKey(senderPublicKeyHex); @@ -79,5 +83,9 @@ export const makeCsprTransferDeploy = ({ deployHeader.chainName = chainName; deployHeader.ttl = new Duration(ttl); + if (timestamp) { + deployHeader.timestamp = Timestamp.fromJSON(timestamp); + } + return Deploy.makeDeploy(deployHeader, payment, session); }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 0b790f218..070da3c72 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,3 +4,4 @@ export * from './auction-manager'; export * from './constants'; export * from './cep-18-transfer'; export * from './cep-nft-transfer'; +export * from './common';