From b0dd1d541da62eb3c4ae607755b1435c2295b108 Mon Sep 17 00:00:00 2001 From: Oleksandr Myshchyshyn Date: Thu, 21 Nov 2024 20:33:26 +0200 Subject: [PATCH 1/8] feature: Added calltable serialization for TransactionV1, Changing TransactionV1 structure --- src/types/ByteConverters.ts | 5 + src/types/CalltableSerialization.ts | 66 +++++ src/types/Deploy.ts | 4 +- src/types/ExecutableDeployItem.ts | 49 ++-- src/types/InitiatorAddr.ts | 25 +- src/types/PricingMode.ts | 124 +++++---- src/types/Transaction.test.ts | 67 +++-- src/types/Transaction.ts | 309 ++-------------------- src/types/TransactionEntryPoint.test.ts | 146 ---------- src/types/TransactionEntryPoint.ts | 57 +++- src/types/TransactionPayload.ts | 150 +++++++++++ src/types/TransactionScheduling.ts | 43 ++- src/types/TransactionTarget.test.ts | 121 --------- src/types/TransactionTarget.ts | 336 ++++++++++++------------ 14 files changed, 641 insertions(+), 861 deletions(-) create mode 100644 src/types/CalltableSerialization.ts delete mode 100644 src/types/TransactionEntryPoint.test.ts create mode 100644 src/types/TransactionPayload.ts delete mode 100644 src/types/TransactionTarget.test.ts diff --git a/src/types/ByteConverters.ts b/src/types/ByteConverters.ts index 3ec3777d7..e7e2d70d3 100644 --- a/src/types/ByteConverters.ts +++ b/src/types/ByteConverters.ts @@ -56,6 +56,11 @@ export const toBytesNumber = (bitSize: number, signed: boolean) => ( */ export const toBytesU8 = toBytesNumber(8, false); +/** + * Converts an 16-bit unsigned integer (`u16`) to little-endian byte format. + */ +export const toBytesU16 = toBytesNumber(16, false); + /** * Converts a 32-bit signed integer (`i32`) to little-endian byte format. */ diff --git a/src/types/CalltableSerialization.ts b/src/types/CalltableSerialization.ts new file mode 100644 index 000000000..8aaa92989 --- /dev/null +++ b/src/types/CalltableSerialization.ts @@ -0,0 +1,66 @@ +import { concat } from '@ethersproject/bytes'; +import { toBytesU16, toBytesU32 } from './ByteConverters'; + +export class Field { + readonly index: number; + readonly offset: number; + readonly value: Uint8Array; + + constructor(index: number, offset: number, value: Uint8Array) { + this.index = index; + this.offset = offset; + this.value = value; + } + + /** + * Calculates the serialized vector size for the given number of fields. + * @returns The size of the serialized vector. + */ + static serializedVecSize(): number { + return 4 + 4 * 2; + } +} + +export class CalltableSerialization { + private fields: Field[] = []; + private currentOffset = 0; + + /** + * Adds a field to the call table. + * @param index The field index. + * @param value The field value as a byte array. + * @returns The current instance of CalltableSerialization. + */ + addField(index: number, value: Uint8Array): CalltableSerialization { + if (this.fields.length !== index) { + throw new Error('Add fields in correct index order.'); + } + + const field = new Field(index, this.currentOffset, value); + this.fields.push(field); + this.currentOffset += value.length; + + return this; + } + + /** + * Serializes the call table to a byte array. + * @returns A Uint8Array representing the serialized call table. + */ + toBytes(): Uint8Array { + const calltableBytes: Uint8Array[] = []; + const payloadBytes: Uint8Array[] = []; + + calltableBytes.push(toBytesU32(this.fields.length)); + + for (const field of this.fields) { + calltableBytes.push(toBytesU16(field.index)); + calltableBytes.push(toBytesU32(field.offset)); + payloadBytes.push(field.value); + } + + calltableBytes.push(toBytesU32(this.currentOffset)); + + return concat([...calltableBytes, ...payloadBytes]); + } +} diff --git a/src/types/Deploy.ts b/src/types/Deploy.ts index a15da4992..ff0dff8c9 100644 --- a/src/types/Deploy.ts +++ b/src/types/Deploy.ts @@ -14,7 +14,7 @@ import { } from './Transaction'; import { TransactionEntryPoint } from './TransactionEntryPoint'; import { InitiatorAddr } from './InitiatorAddr'; -import { ClassicMode, PricingMode } from './PricingMode'; +import { PaymentLimitedMode, PricingMode } from './PricingMode'; import { TransactionTarget } from './TransactionTarget'; import { TransactionScheduling } from './TransactionScheduling'; import { ExecutableDeployItem } from './ExecutableDeployItem'; @@ -393,7 +393,7 @@ export class Deploy { const standardPayment = paymentAmount === 0 && !deploy.payment.moduleBytes; const pricingMode = new PricingMode(); - const classicMode = new ClassicMode(); + const classicMode = new PaymentLimitedMode(); classicMode.gasPriceTolerance = 1; classicMode.paymentAmount = paymentAmount; classicMode.standardPayment = standardPayment; diff --git a/src/types/ExecutableDeployItem.ts b/src/types/ExecutableDeployItem.ts index 1f683e469..2dfece826 100644 --- a/src/types/ExecutableDeployItem.ts +++ b/src/types/ExecutableDeployItem.ts @@ -7,7 +7,6 @@ import { CLTypeOption, CLTypeUInt64, CLValue, - CLValueByteArray, CLValueOption, CLValueString, CLValueUInt32, @@ -15,9 +14,14 @@ import { CLValueUInt64 } from './clvalue'; import { ContractHash, URef } from './key'; -import { deserializeArgs, serializeArgs } from './SerializationUtils'; +import { + byteArrayJsonDeserializer, + byteArrayJsonSerializer, + deserializeArgs, + serializeArgs +} from './SerializationUtils'; import { PublicKey } from './keypair'; -import { Conversions } from './Conversions'; +import { toBytesArrayU8 } from './ByteConverters'; /** * Enum representing the different types of executable deploy items. @@ -39,8 +43,13 @@ export class ModuleBytes { /** * The module bytes in hexadecimal format. */ - @jsonMember({ name: 'module_bytes', constructor: String }) - moduleBytes!: string; + @jsonMember({ + name: 'module_bytes', + constructor: Uint8Array, + serializer: byteArrayJsonSerializer, + deserializer: byteArrayJsonDeserializer + }) + moduleBytes: Uint8Array; /** * The arguments passed to the module. @@ -56,7 +65,7 @@ export class ModuleBytes { * @param moduleBytes The module bytes in hexadecimal format. * @param args The arguments for the module. */ - constructor(moduleBytes: string, args: Args) { + constructor(moduleBytes: Uint8Array, args: Args) { this.moduleBytes = moduleBytes; this.args = args; } @@ -66,22 +75,13 @@ export class ModuleBytes { * @returns The serialized byte array. */ bytes(): Uint8Array { - const moduleBytes = new Uint8Array(Buffer.from(this.moduleBytes, 'hex')); - const lengthBytes = CLValueUInt32.newCLUInt32( - BigNumber.from(moduleBytes.length) - ).bytes(); - const bytesArrayBytes = CLValueByteArray.newCLByteArray( - moduleBytes - ).bytes(); + if (!this.args) throw new Error('Missing arguments for ModuleBytes'); - let result = concat([lengthBytes, bytesArrayBytes]); - - if (this.args) { - const argBytes = this.args.toBytes(); - result = concat([result, argBytes]); - } - - return result; + return concat([ + Uint8Array.from([0]), + toBytesArrayU8(this.moduleBytes), + this.args.toBytes() + ]); } } @@ -545,7 +545,7 @@ export class ExecutableDeployItem { ): ExecutableDeployItem { const executableDeployItem = new ExecutableDeployItem(); executableDeployItem.moduleBytes = new ModuleBytes( - '', + Uint8Array.from([]), Args.fromMap({ amount: CLValueUInt512.newCLUInt512(amount) }) ); return executableDeployItem; @@ -618,10 +618,7 @@ export class ExecutableDeployItem { args: Args ): ExecutableDeployItem { const executableDeployItem = new ExecutableDeployItem(); - executableDeployItem.moduleBytes = new ModuleBytes( - Conversions.encodeBase16(moduleBytes), - args - ); + executableDeployItem.moduleBytes = new ModuleBytes(moduleBytes, args); return executableDeployItem; } diff --git a/src/types/InitiatorAddr.ts b/src/types/InitiatorAddr.ts index 428b68680..60eb95cf7 100644 --- a/src/types/InitiatorAddr.ts +++ b/src/types/InitiatorAddr.ts @@ -1,8 +1,8 @@ import { jsonObject, jsonMember } from 'typedjson'; -import { concat } from '@ethersproject/bytes'; import { PublicKey } from './keypair'; import { AccountHash } from './key'; +import { CalltableSerialization } from './CalltableSerialization'; /** * Represents an address for an initiator, which can either be a public key or an account hash. @@ -45,20 +45,23 @@ export class InitiatorAddr { * @returns A `Uint8Array` representing the initiator address. */ public toBytes(): Uint8Array { - let result: Uint8Array; - if (this.accountHash) { - const prefix = new Uint8Array([1]); - result = concat([prefix, this.accountHash.toBytes()]); + const calltableSerialization = new CalltableSerialization(); + + calltableSerialization.addField(0, Uint8Array.of(1)); + calltableSerialization.addField(1, this.accountHash.toBytes()); + + return calltableSerialization.toBytes(); } else if (this.publicKey) { - const prefix = new Uint8Array([0]); - const publicKeyBytes = this.publicKey.bytes() || new Uint8Array(0); - result = concat([prefix, publicKeyBytes]); - } else { - result = new Uint8Array(0); + const calltableSerialization = new CalltableSerialization(); + + calltableSerialization.addField(0, Uint8Array.of(0)); + calltableSerialization.addField(1, this.publicKey.bytes()); + + return calltableSerialization.toBytes(); } - return result; + throw new Error('Unable to serialize InitiatorAddr'); } /** diff --git a/src/types/PricingMode.ts b/src/types/PricingMode.ts index 19ecbfe1c..b23555ecf 100644 --- a/src/types/PricingMode.ts +++ b/src/types/PricingMode.ts @@ -1,27 +1,14 @@ -import { concat } from '@ethersproject/bytes'; - import { jsonObject, jsonMember } from 'typedjson'; import { Hash } from './key'; -import { CLValueUInt64 } from './clvalue'; - -/** - * Enum representing the different pricing modes available. - */ -export enum PricingModeTag { - /** Classic pricing mode */ - Classic = 0, - /** Fixed pricing mode */ - Fixed = 1, - /** Reserved pricing mode */ - Reserved = 2 -} +import { CLValueBool, CLValueUInt64, CLValueUInt8 } from './clvalue'; +import { CalltableSerialization } from './CalltableSerialization'; /** * Represents the classic pricing mode, including parameters for gas price tolerance, * payment amount, and standard payment. */ @jsonObject -export class ClassicMode { +export class PaymentLimitedMode { /** * The tolerance for gas price fluctuations in classic pricing mode. */ @@ -39,6 +26,25 @@ export class ClassicMode { */ @jsonMember({ name: 'standard_payment', constructor: Boolean }) standardPayment: boolean; + + public toBytes(): Uint8Array { + const calltableSerializer = new CalltableSerialization(); + calltableSerializer.addField(0, CLValueUInt8.newCLUint8(0).bytes()); + calltableSerializer.addField( + 1, + CLValueUInt64.newCLUint64(this.paymentAmount).bytes() + ); + calltableSerializer.addField( + 2, + CLValueUInt8.newCLUint8(this.gasPriceTolerance).bytes() + ); + calltableSerializer.addField( + 3, + CLValueBool.fromBoolean(this.standardPayment).bytes() + ); + + return calltableSerializer.toBytes(); + } } /** @@ -51,13 +57,43 @@ export class FixedMode { */ @jsonMember({ name: 'gas_price_tolerance', constructor: Number }) gasPriceTolerance: number; + + /** + * User-specified additional computation factor (minimum 0). + * + * - If `0` is provided, no additional logic is applied to the computation limit. + * - Each value above `0` tells the node that it needs to treat the transaction + * as if it uses more gas than its serialized size indicates. + * - Each increment of `1` increases the "wasm lane" size bucket for this transaction by `1`. + * + * For example: + * - If the transaction's size indicates bucket `0` and `additionalComputationFactor = 2`, + * the transaction will be treated as if it belongs to bucket `2`. + */ + @jsonMember({ name: 'additional_computation_factor', constructor: Number }) + additionalComputationFactor!: number; + + public toBytes(): Uint8Array { + const calltableSerializer = new CalltableSerialization(); + calltableSerializer.addField(0, CLValueUInt8.newCLUint8(1).bytes()); + calltableSerializer.addField( + 1, + CLValueUInt8.newCLUint8(this.gasPriceTolerance).bytes() + ); + calltableSerializer.addField( + 2, + CLValueUInt8.newCLUint8(this.additionalComputationFactor).bytes() + ); + + return calltableSerializer.toBytes(); + } } /** * Represents the reserved pricing mode, which includes a receipt hash. */ @jsonObject -export class ReservedMode { +export class PrepaidMode { /** * The receipt associated with the reserved pricing mode. */ @@ -68,6 +104,14 @@ export class ReservedMode { serializer: value => value.toJSON() }) receipt: Hash; + + public toBytes(): Uint8Array { + const calltableSerializer = new CalltableSerialization(); + calltableSerializer.addField(0, CLValueUInt8.newCLUint8(2).bytes()); + calltableSerializer.addField(1, this.receipt.toBytes()); + + return calltableSerializer.toBytes(); + } } /** @@ -78,8 +122,8 @@ export class PricingMode { /** * The classic pricing mode, if applicable. */ - @jsonMember({ name: 'Classic', constructor: ClassicMode }) - classic?: ClassicMode; + @jsonMember({ name: 'PaymentLimited', constructor: PaymentLimitedMode }) + paymentLimited?: PaymentLimitedMode; /** * The fixed pricing mode, if applicable. @@ -90,8 +134,8 @@ export class PricingMode { /** * The reserved pricing mode, if applicable. */ - @jsonMember({ name: 'reserved', constructor: ReservedMode }) - reserved?: ReservedMode; + @jsonMember({ name: 'Prepaid', constructor: PrepaidMode }) + prepaid?: PrepaidMode; /** * Converts the pricing mode instance into a byte array representation. @@ -100,40 +144,14 @@ export class PricingMode { * @returns A `Uint8Array` representing the serialized pricing mode. */ toBytes(): Uint8Array { - let result: Uint8Array; - - if (this.classic) { - const classicPaymentBytes = new CLValueUInt64( - BigInt(this.classic.paymentAmount) - ).bytes(); - const gasPriceToleranceByte = new Uint8Array([ - this.classic.gasPriceTolerance - ]); - const standardPaymentByte = new Uint8Array([ - this.classic.standardPayment ? 1 : 0 - ]); - - result = concat([ - Uint8Array.of(PricingModeTag.Classic), - classicPaymentBytes, - gasPriceToleranceByte, - standardPaymentByte - ]); + if (this.paymentLimited) { + return this.paymentLimited.toBytes(); } else if (this.fixed) { - const gasPriceToleranceByte = new Uint8Array([ - this.fixed.gasPriceTolerance - ]); - result = concat([ - Uint8Array.of(PricingModeTag.Fixed), - gasPriceToleranceByte - ]); - } else if (this.reserved) { - const receiptBytes = this.reserved.receipt.toBytes(); - result = concat([Uint8Array.of(PricingModeTag.Reserved), receiptBytes]); - } else { - result = new Uint8Array(0); // empty array if none of the conditions match + return this.fixed.toBytes(); + } else if (this.prepaid) { + return this.prepaid.toBytes(); } - return result; + throw new Error('Unable to serialize PricingMode'); } } diff --git a/src/types/Transaction.test.ts b/src/types/Transaction.test.ts index de5e2c1c8..d5978e4bd 100644 --- a/src/types/Transaction.test.ts +++ b/src/types/Transaction.test.ts @@ -1,12 +1,7 @@ import { BigNumber } from '@ethersproject/bignumber'; -import { assert, expect } from 'chai'; import { Duration, Timestamp } from './Time'; -import { - TransactionV1, - TransactionV1Body, - TransactionV1Header -} from './Transaction'; +import { TransactionV1 } from './Transaction'; import { InitiatorAddr } from './InitiatorAddr'; import { PrivateKey } from './keypair/PrivateKey'; import { FixedMode, PricingMode } from './PricingMode'; @@ -24,6 +19,7 @@ import { CLValueUInt64 } from './clvalue'; import { PublicKey } from './keypair'; +import { PayloadFields, TransactionV1Payload } from './TransactionPayload'; describe('Test Transaction', () => { it('should create a Transaction from TransactionV1', async () => { @@ -34,16 +30,9 @@ describe('Test Transaction', () => { const pricingMode = new PricingMode(); const fixedMode = new FixedMode(); fixedMode.gasPriceTolerance = 3; + fixedMode.additionalComputationFactor = 1; pricingMode.fixed = fixedMode; - const transactionHeader = TransactionV1Header.build({ - chainName: 'casper-net-1', - timestamp, - ttl: new Duration(1800000), - initiatorAddr: new InitiatorAddr(keys.publicKey), - pricingMode - }); - const args = Args.fromMap({ target: CLValue.newCLPublicKey( PublicKey.fromHex( @@ -58,30 +47,40 @@ describe('Test Transaction', () => { const entryPoint = new TransactionEntryPoint(undefined, {}); const scheduling = new TransactionScheduling({}); - const transactionBody = TransactionV1Body.build({ - args: args, - target: transactionTarget, - transactionEntryPoint: entryPoint, - transactionScheduling: scheduling, - transactionCategory: 2 - }); + const payloadFields = new PayloadFields(); + payloadFields.addField(0, args.toBytes()); + payloadFields.addField(1, transactionTarget.toBytes()); + payloadFields.addField(2, entryPoint.bytes()); + payloadFields.addField(3, scheduling.bytes()); + + const transactionPayload = new TransactionV1Payload(); + transactionPayload.initiatorAddr = new InitiatorAddr(keys.publicKey); + transactionPayload.ttl = new Duration(1800000); + transactionPayload.args = args; + transactionPayload.entryPoint = entryPoint; + transactionPayload.pricingMode = pricingMode; + transactionPayload.timestamp = timestamp; + transactionPayload.category = 2; + transactionPayload.target = transactionTarget; + transactionPayload.scheduling = scheduling; + transactionPayload.chainName = 'casper-net-1'; + transactionPayload.fields = payloadFields; - const transaction = TransactionV1.makeTransactionV1( - transactionHeader, - transactionBody - ); + const transaction = TransactionV1.makeTransactionV1(transactionPayload); await transaction.sign(keys); - const toJson = TransactionV1.toJson(transaction); - const parsed = TransactionV1.fromJSON(toJson); + // const toJson = TransactionV1.toJson(transaction); + // const parsed = TransactionV1.fromJSON(toJson); - const transactionPaymentAmount = parsed.body.args.args - .get('amount')! - .toString(); + // console.log(Args.fromBytes(transaction.payload.fields.fields.get(0)!)); - assert.deepEqual(parsed.approvals[0].signer, keys.publicKey); - expect(transaction.body).to.deep.equal(transactionBody); - expect(transaction.header).to.deep.equal(transactionHeader); - assert.deepEqual(parseInt(transactionPaymentAmount, 10), paymentAmount); + // const transactionPaymentAmount = parsed.body.args.args + // .get('amount')! + // .toString(); + // + // assert.deepEqual(parsed.approvals[0].signer, keys.publicKey); + // expect(transaction.body).to.deep.equal(transactionBody); + // expect(transaction.header).to.deep.equal(transactionHeader); + // assert.deepEqual(parseInt(transactionPaymentAmount, 10), paymentAmount); }); }); diff --git a/src/types/Transaction.ts b/src/types/Transaction.ts index 8822a25b6..bed56ddbb 100644 --- a/src/types/Transaction.ts +++ b/src/types/Transaction.ts @@ -1,5 +1,4 @@ import { jsonObject, jsonMember, jsonArrayMember, TypedJSON } from 'typedjson'; -import { concat } from '@ethersproject/bytes'; import { Hash } from './key'; import { Deploy } from './Deploy'; @@ -12,14 +11,10 @@ import { TransactionScheduling } from './TransactionScheduling'; import { PublicKey } from './keypair'; import { HexBytes } from './HexBytes'; import { PrivateKey } from './keypair/PrivateKey'; -import { CLValueString, CLValueUInt64 } from './clvalue'; import { Args } from './Args'; -import { - arrayEquals, - deserializeArgs, - serializeArgs -} from './SerializationUtils'; +import { deserializeArgs, serializeArgs } from './SerializationUtils'; import { byteHash } from './ByteConverters'; +import { TransactionV1Payload } from './TransactionPayload'; /** * Custom error class for handling transaction-related errors. @@ -110,232 +105,6 @@ export class Approval { } } -/** - * Represents the header of a TransactionV1. - */ -@jsonObject -export class TransactionV1Header { - /** - * The name of the blockchain. - */ - @jsonMember({ name: 'chain_name', constructor: String }) - public chainName: string; - - /** - * The timestamp of the transaction. - */ - @jsonMember({ - name: 'timestamp', - constructor: Timestamp, - deserializer: json => Timestamp.fromJSON(json), - serializer: value => value.toJSON() - }) - public timestamp: Timestamp; - - /** - * The time-to-live (TTL) duration of the transaction. - */ - @jsonMember({ - name: 'ttl', - constructor: Duration, - deserializer: json => Duration.fromJSON(json), - serializer: value => value.toJSON() - }) - public ttl: Duration; - - /** - * The address of the transaction initiator. - */ - @jsonMember({ - name: 'initiator_addr', - constructor: InitiatorAddr, - deserializer: json => InitiatorAddr.fromJSON(json), - serializer: value => value.toJSON() - }) - public initiatorAddr: InitiatorAddr; - - /** - * The pricing mode used for the transaction. - */ - @jsonMember({ name: 'pricing_mode', constructor: PricingMode }) - public pricingMode: PricingMode; - - /** - * The hash of the transaction body. - */ - @jsonMember({ - name: 'body_hash', - constructor: Hash, - deserializer: json => Hash.fromJSON(json), - serializer: value => value.toJSON() - }) - public bodyHash: Hash; - - /** - * Builds a `TransactionV1Header` from the provided properties. - * @param initiatorAddr The initiator's address. - * @param timestamp The timestamp of the transaction. - * @param ttl The TTL of the transaction. - * @param chainName The chain name. - * @param pricingMode The pricing mode for the transaction. - * @returns The constructed `TransactionV1Header`. - */ - static build({ - initiatorAddr, - timestamp, - ttl, - chainName, - pricingMode - }: { - initiatorAddr: InitiatorAddr; - timestamp: Timestamp; - ttl: Duration; - chainName: string; - bodyHash?: Hash; - pricingMode: PricingMode; - }): TransactionV1Header { - const header = new TransactionV1Header(); - header.initiatorAddr = initiatorAddr; - header.timestamp = timestamp; - header.ttl = ttl; - header.pricingMode = pricingMode; - header.chainName = chainName; - return header; - } - - /** - * Serializes the header to a byte array. - * @returns The serialized byte array representing the header. - */ - public toBytes(): Uint8Array { - const chainNameBytes = CLValueString.newCLString(this.chainName).bytes(); - const timestampMillis = this.timestamp.toMilliseconds(); - const timestampBytes = CLValueUInt64.newCLUint64( - BigInt(timestampMillis) - ).bytes(); - const ttlBytes = CLValueUInt64.newCLUint64( - BigInt(this.ttl.toMilliseconds()) - ).bytes(); - const bodyHashBytes = this.bodyHash.toBytes(); - const pricingModeBytes = this.pricingMode.toBytes(); - const initiatorAddrBytes = this.initiatorAddr.toBytes(); - - return concat([ - chainNameBytes, - timestampBytes, - ttlBytes, - bodyHashBytes, - pricingModeBytes, - initiatorAddrBytes - ]); - } -} - -/** - * Represents the body of a TransactionV1. - */ -@jsonObject -export class TransactionV1Body { - /** - * The arguments for the transaction. - */ - @jsonMember(() => Args, { - deserializer: deserializeArgs, - serializer: serializeArgs - }) - public args: Args; - - /** - * The target of the transaction. - */ - @jsonMember({ - name: 'target', - constructor: TransactionTarget, - deserializer: json => TransactionTarget.fromJSON(json), - serializer: value => value.toJSON() - }) - public target: TransactionTarget; - - /** - * The entry point for the transaction. - */ - @jsonMember({ - name: 'entry_point', - constructor: TransactionEntryPoint, - deserializer: json => TransactionEntryPoint.fromJSON(json), - serializer: value => value.toJSON() - }) - public entryPoint: TransactionEntryPoint; - - /** - * The category of the transaction. - */ - @jsonMember({ name: 'transaction_category', constructor: Number }) - public category: number; - - /** - * The scheduling information for the transaction. - */ - @jsonMember({ - name: 'scheduling', - constructor: TransactionScheduling, - deserializer: json => TransactionScheduling.fromJSON(json), - serializer: value => value.toJSON() - }) - public scheduling: TransactionScheduling; - - /** - * Builds a `TransactionV1Body` from the provided properties. - * @param args The arguments for the transaction. - * @param target The target of the transaction. - * @param transactionEntryPoint The entry point for the transaction. - * @param transactionScheduling The scheduling for the transaction. - * @param transactionCategory The category of the transaction. - * @returns The constructed `TransactionV1Body`. - */ - static build({ - args, - target, - transactionEntryPoint, - transactionScheduling, - transactionCategory - }: { - args: Args; - target: TransactionTarget; - transactionEntryPoint: TransactionEntryPoint; - transactionScheduling: TransactionScheduling; - transactionCategory: number; - }): TransactionV1Body { - const body = new TransactionV1Body(); - body.args = args; - body.target = target; - body.entryPoint = transactionEntryPoint; - body.scheduling = transactionScheduling; - body.category = transactionCategory; - return body; - } - - /** - * Serializes the body to a byte array. - * @returns The serialized byte array representing the body. - */ - toBytes(): Uint8Array { - const argsBytes = this.args?.toBytes() || new Uint8Array(); - const targetBytes = this.target.toBytes(); - const entryPointBytes = this.entryPoint.bytes(); - const categoryBytes = new Uint8Array([this.category]); - const schedulingBytes = this.scheduling.bytes(); - - return concat([ - argsBytes, - targetBytes, - entryPointBytes, - categoryBytes, - schedulingBytes - ]); - } -} - /** * Represents a TransactionV1 object, including its header, body, and approvals. */ @@ -355,14 +124,8 @@ export class TransactionV1 { /** * The header of the transaction. */ - @jsonMember({ name: 'header', constructor: TransactionV1Header }) - public header: TransactionV1Header; - - /** - * The body of the transaction. - */ - @jsonMember({ name: 'body', constructor: TransactionV1Body }) - public body: TransactionV1Body; + @jsonMember({ name: 'payload', constructor: TransactionV1Payload }) + public payload: TransactionV1Payload; /** * The approvals for the transaction. @@ -372,13 +135,11 @@ export class TransactionV1 { constructor( hash: Hash, - header: TransactionV1Header, - body: TransactionV1Body, + payload: TransactionV1Payload, approvals: Approval[] ) { this.hash = hash; - this.header = header; - this.body = body; + this.payload = payload; this.approvals = approvals; } @@ -387,15 +148,11 @@ export class TransactionV1 { * @throws {TransactionError} Throws errors if validation fails. */ public validate(): boolean { - const bodyBytes = this.body.toBytes(); - - if (!arrayEquals(byteHash(bodyBytes), this.header.bodyHash.toBytes())) - throw ErrInvalidBodyHash; + console.log(this.payload); + const payloadBytes = this.payload!.toBytes(); + const calculatedHash = new Hash(byteHash(payloadBytes)); - const headerBytes = this.header.toBytes(); - - if (!arrayEquals(byteHash(headerBytes), this.hash.toBytes())) - throw ErrInvalidTransactionHash; + if (!this.hash.equals(calculatedHash)) throw ErrInvalidTransactionHash; for (const approval of this.approvals) { if ( @@ -454,11 +211,10 @@ export class TransactionV1 { */ static newTransactionV1( hash: Hash, - header: TransactionV1Header, - body: TransactionV1Body, + payload: TransactionV1Payload, approvals: Approval[] ): TransactionV1 { - return new TransactionV1(hash, header, body, approvals); + return new TransactionV1(hash, payload, approvals); } /** @@ -467,21 +223,10 @@ export class TransactionV1 { * @param transactionBody The body of the transaction. * @returns A new `TransactionV1` instance. */ - static makeTransactionV1( - transactionHeader: TransactionV1Header, - transactionBody: TransactionV1Body - ): TransactionV1 { - const bodyBytes = transactionBody.toBytes(); - transactionHeader.bodyHash = new Hash(new Uint8Array(byteHash(bodyBytes))); - - const headerBytes = transactionHeader.toBytes(); - const transactionHash = new Hash(new Uint8Array(byteHash(headerBytes))); - return new TransactionV1( - transactionHash, - transactionHeader, - transactionBody, - [] - ); + static makeTransactionV1(payload: TransactionV1Payload): TransactionV1 { + const payloadBytes = payload.toBytes(); + const transactionHash = new Hash(byteHash(payloadBytes)); + return new TransactionV1(transactionHash, payload, []); } /** @@ -499,7 +244,7 @@ export class TransactionV1 { const txData: Record | null = data?.transaction?.Version1 ?? data?.Version1 ?? data ?? null; - if (!(txData?.hash && txData?.header?.initiator_addr)) { + if (!(txData?.hash && txData?.payload?.initiator_addr)) { throw ErrTransactionV1FromJson; } @@ -778,18 +523,18 @@ export class Transaction { return new Transaction( v1.hash, new TransactionHeader( - v1.header.chainName, - v1.header.timestamp, - v1.header.ttl, - v1.header.initiatorAddr, - v1.header.pricingMode + v1.payload.chainName, + v1.payload.timestamp, + v1.payload.ttl, + v1.payload.initiatorAddr, + v1.payload.pricingMode ), new TransactionBody( - v1.body.args, - v1.body.target, - v1.body.entryPoint, - v1.body.scheduling, - v1.body.category + v1.payload.args, + v1.payload.target, + v1.payload.entryPoint, + v1.payload.scheduling, + v1.payload.category ), v1.approvals, v1 diff --git a/src/types/TransactionEntryPoint.test.ts b/src/types/TransactionEntryPoint.test.ts deleted file mode 100644 index 55c0564e0..000000000 --- a/src/types/TransactionEntryPoint.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { TypedJSON, jsonObject, jsonMember } from 'typedjson'; -import { expect } from 'chai'; -import assert from 'assert'; -import { TransactionEntryPoint } from './TransactionEntryPoint'; - -@jsonObject -class UnderTest { - @jsonMember({ - serializer: value => value.toJSON(), - deserializer: json => TransactionEntryPoint.fromJSON(json) - }) - public a: TransactionEntryPoint; -} -const customMockJson = { a: { Custom: 'asd' } }; -const activateMockJson = { a: 'ActivateBid' }; -const addBidMockJson = { a: 'AddBid' }; -const changeBidPublicKeyMockJson = { a: 'ChangeBidPublicKey' }; -const delegateMockJson = { a: 'Delegate' }; -const redelegateMockJson = { a: 'Redelegate' }; -const transferMockJson = { a: 'Transfer' }; -const undelegateMockJson = { a: 'Undelegate' }; -const withdrawMockJson = { a: 'WithdrawBid' }; -const callMockJson = { a: 'Call' }; - -describe('TransactionEntryPoint', () => { - const serializer = new TypedJSON(UnderTest); - it('should byte-serialize TransactionEntryPoint::Custom correctly', () => { - const parsed = serializer.parse(customMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [0, 3, 0, 0, 0, 97, 115, 100]); - }); - - it('should parse TransactionEntryPoint::Custom correctly', () => { - const parsed = serializer.parse(customMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(customMockJson); - }); - - it('should parse TransactionEntryPoint::ActivateBid correctly', () => { - const parsed = serializer.parse(activateMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(activateMockJson); - }); - - it('should byte-serialize TransactionEntryPoint::ActivateBid correctly', () => { - const parsed = serializer.parse(activateMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [7]); - }); - - it('should parse TransactionEntryPoint::AddBid correctly', () => { - const parsed = serializer.parse(addBidMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(addBidMockJson); - }); - - it('should byte-serialize TransactionEntryPoint::AddBid correctly', () => { - const parsed = serializer.parse(addBidMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [2]); - }); - - it('should parse TransactionEntryPoint::ChangeBidPublicKey correctly', () => { - const parsed = serializer.parse(changeBidPublicKeyMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(changeBidPublicKeyMockJson); - }); - - it('should byte-serialize TransactionEntryPoint::ChangeBidPublicKey correctly', () => { - const parsed = serializer.parse(changeBidPublicKeyMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [8]); - }); - - it('should parse TransactionEntryPoint::Delegate correctly', () => { - const parsed = serializer.parse(delegateMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(delegateMockJson); - }); - - it('should byte-serialize TransactionEntryPoint::Delegate correctly', () => { - const parsed = serializer.parse(delegateMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [4]); - }); - - it('should parse TransactionEntryPoint::Redelegate correctly', () => { - const parsed = serializer.parse(redelegateMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(redelegateMockJson); - }); - - it('should byte-serialize TransactionEntryPoint::Redelegate correctly', () => { - const parsed = serializer.parse(redelegateMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [6]); - }); - - it('should parse TransactionEntryPoint::Transfer correctly', () => { - const parsed = serializer.parse(transferMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(transferMockJson); - }); - - it('should byte-serialize TransactionEntryPoint::Transfer correctly', () => { - const parsed = serializer.parse(transferMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [1]); - }); - - it('should parse TransactionEntryPoint::Undelegate correctly', () => { - const parsed = serializer.parse(undelegateMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(undelegateMockJson); - }); - - it('should byte-serialize TransactionEntryPoint::Undelegate correctly', () => { - const parsed = serializer.parse(undelegateMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [5]); - }); - - it('should parse TransactionEntryPoint::WithdrawBid correctly', () => { - const parsed = serializer.parse(withdrawMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(withdrawMockJson); - }); - - it('should byte-serialize TransactionEntryPoint::WithdrawBid correctly', () => { - const parsed = serializer.parse(withdrawMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [3]); - }); - - it('should parse TransactionEntryPoint::Call correctly', () => { - const parsed = serializer.parse(callMockJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(callMockJson); - }); - - it('should byte-serialize TransactionEntryPoint::Call correctly', () => { - const parsed = serializer.parse(callMockJson); - const bytes = parsed!.a.bytes(); - assert.deepStrictEqual(Array.from(bytes), [9]); - }); -}); diff --git a/src/types/TransactionEntryPoint.ts b/src/types/TransactionEntryPoint.ts index b26050f3b..b20816d32 100644 --- a/src/types/TransactionEntryPoint.ts +++ b/src/types/TransactionEntryPoint.ts @@ -1,7 +1,7 @@ import { jsonObject, jsonMember } from 'typedjson'; -import { concat } from '@ethersproject/bytes'; import { CLValueString } from './clvalue'; +import { CalltableSerialization } from './CalltableSerialization'; /** * Enum representing the available transaction entry points, each representing a different operation in the system. @@ -16,14 +16,16 @@ export enum TransactionEntryPointEnum { Redelegate = 'Redelegate', ActivateBid = 'ActivateBid', ChangeBidPublicKey = 'ChangeBidPublicKey', - Call = 'Call' + Call = 'Call', + AddReservations = 'AddReservations', + CancelReservations = 'CancelReservations' } /** * Enum representing the tags for different transaction entry points. This is used for efficient storage and comparison. */ export enum TransactionEntryPointTag { - Custom = 0, + Call = 1, Transfer, AddBid, WithdrawBid, @@ -32,7 +34,8 @@ export enum TransactionEntryPointTag { Redelegate, ActivateBid, ChangeBidPublicKey, - Call + AddReservations, + CancelReservations } /** @@ -101,6 +104,18 @@ export class TransactionEntryPoint { @jsonMember({ constructor: Object, name: 'Call' }) call?: Record; + /** + * The call action as a generic object. + */ + @jsonMember({ constructor: Object, name: 'AddReservations' }) + addReservations?: Record; + + /** + * The call action as a generic object. + */ + @jsonMember({ constructor: Object, name: 'CancelReservations' }) + cancelReservations?: Record; + /** * Creates a new `TransactionEntryPoint` instance, where each parameter corresponds to a specific entry point action. * @@ -125,7 +140,9 @@ export class TransactionEntryPoint { redelegate?: Record, activateBid?: Record, changeBidPublicKey?: Record, - call?: Record + call?: Record, + addReservations?: Record, + cancelReservations?: Record ) { this.custom = custom; this.transfer = transfer; @@ -137,6 +154,8 @@ export class TransactionEntryPoint { this.activateBid = activateBid; this.changeBidPublicKey = changeBidPublicKey; this.call = call; + this.addReservations = addReservations; + this.cancelReservations = cancelReservations; } /** @@ -155,7 +174,11 @@ export class TransactionEntryPoint { if (this.changeBidPublicKey) return TransactionEntryPointTag.ChangeBidPublicKey; if (this.call) return TransactionEntryPointTag.Call; - return TransactionEntryPointTag.Custom; + if (this.addReservations) return TransactionEntryPointTag.AddReservations; + if (this.cancelReservations) + return TransactionEntryPointTag.CancelReservations; + + throw new Error('Unknown TransactionEntryPointTag'); } /** @@ -164,12 +187,20 @@ export class TransactionEntryPoint { * @returns A `Uint8Array` representing the transaction entry point and any associated data. */ bytes(): Uint8Array { - let result = new Uint8Array([this.tag()]); + const calltableSerialization = new CalltableSerialization(); + calltableSerialization.addField(0, Uint8Array.of(this.tag())); + if (this.custom) { - const customBytes = new CLValueString(this.custom).bytes(); - result = concat([result, customBytes]); + const calltableSerialization = new CalltableSerialization(); + calltableSerialization.addField(0, Uint8Array.of(1)); + calltableSerialization.addField( + 1, + CLValueString.newCLString(this.custom).bytes() + ); + + return calltableSerialization.toBytes(); } - return result; + return calltableSerialization.toBytes(); } /** @@ -239,6 +270,12 @@ export class TransactionEntryPoint { case TransactionEntryPointEnum.Call: entryPoint.call = {}; break; + case TransactionEntryPointEnum.CancelReservations: + entryPoint.cancelReservations = {}; + break; + case TransactionEntryPointEnum.AddReservations: + entryPoint.addReservations = {}; + break; default: throw new Error('Unknown entry point'); } diff --git a/src/types/TransactionPayload.ts b/src/types/TransactionPayload.ts new file mode 100644 index 000000000..e1450d2d2 --- /dev/null +++ b/src/types/TransactionPayload.ts @@ -0,0 +1,150 @@ +import { concat } from '@ethersproject/bytes'; +import { + toBytesString, + toBytesU16, + toBytesU32, + toBytesU64 +} from './ByteConverters'; +import { jsonMember, jsonObject } from 'typedjson'; +import { InitiatorAddr } from './InitiatorAddr'; +import { Duration, Timestamp } from './Time'; +import { PricingMode } from './PricingMode'; +import { Args } from './Args'; +import { TransactionTarget } from './TransactionTarget'; +import { TransactionEntryPoint } from './TransactionEntryPoint'; +import { TransactionScheduling } from './TransactionScheduling'; +import { CalltableSerialization } from './CalltableSerialization'; +import { + byteArrayJsonDeserializer, + byteArrayJsonSerializer +} from './SerializationUtils'; + +export class PayloadFields { + public fields: Map = new Map(); + + addField(field: number, value: Uint8Array): void { + this.fields.set(field, value); + } + + toBytes(): Uint8Array { + const fieldsCount = toBytesU32(this.fields.size); + const fieldEntries = Array.from(this.fields.entries()).map( + ([key, value]) => { + return concat([toBytesU16(key), value]); + } + ); + + return concat([fieldsCount, ...fieldEntries]); + } + + static fromJSON(json: Record): PayloadFields { + const payload = new PayloadFields(); + for (const [key, value] of Object.entries(json)) { + const field = parseInt(key); + if (!isNaN(field)) { + payload.addField(field, byteArrayJsonDeserializer(value)); + } + } + return payload; + } + + toJSON(): Record { + const result: Record = {}; + const fieldEntries = Array.from(this.fields.entries()); + for (const [key, value] of fieldEntries) { + result[key.toString()] = byteArrayJsonSerializer(value); + } + return result; + } +} + +@jsonObject +export class TransactionV1Payload { + /** + * The address of the transaction initiator. + */ + @jsonMember({ + name: 'initiator_addr', + constructor: InitiatorAddr, + deserializer: json => InitiatorAddr.fromJSON(json), + serializer: value => value.toJSON() + }) + public initiatorAddr: InitiatorAddr; + + /** + * The timestamp of the transaction. + */ + @jsonMember({ + name: 'timestamp', + constructor: Timestamp, + deserializer: json => Timestamp.fromJSON(json), + serializer: value => value.toJSON() + }) + public timestamp: Timestamp; + + /** + * The time-to-live (TTL) duration of the transaction. + */ + @jsonMember({ + name: 'ttl', + constructor: Duration, + deserializer: json => Duration.fromJSON(json), + serializer: value => value.toJSON() + }) + public ttl: Duration; + + /** + * The pricing mode used for the transaction. + */ + @jsonMember({ name: 'pricing_mode', constructor: PricingMode }) + public pricingMode: PricingMode; + + /** + * The name of the blockchain. + */ + @jsonMember({ name: 'chain_name', constructor: String }) + public chainName: string; + + /** + * The name of the blockchain. + */ + @jsonMember({ + name: 'fields', + serializer: value => { + if (!value) return; + return value.toJSON(); + }, + deserializer: json => { + if (!json) return; + return PayloadFields.fromJSON(json); + } + }) + public fields: PayloadFields; + + public args: Args; + public target: TransactionTarget; + public entryPoint: TransactionEntryPoint; + public scheduling: TransactionScheduling; + public category: number; + + public toBytes(): Uint8Array { + const calltableSerialization = new CalltableSerialization(); + const fields = new PayloadFields(); + fields.addField(0, this.args.toBytes()); + fields.addField(1, this.target.toBytes()); + fields.addField(2, this.entryPoint.bytes()); + fields.addField(3, this.scheduling.bytes()); + + calltableSerialization.addField(0, this.initiatorAddr.toBytes()); + calltableSerialization.addField( + 1, + toBytesU64(Date.parse(this.timestamp.toJSON())) + ); + calltableSerialization.addField(2, toBytesU64(this.ttl.duration)); + calltableSerialization.addField(3, toBytesString(this.chainName)); + calltableSerialization.addField(4, this.pricingMode.toBytes()); + calltableSerialization.addField(5, fields.toBytes()); + + return calltableSerialization.toBytes(); + } +} diff --git a/src/types/TransactionScheduling.ts b/src/types/TransactionScheduling.ts index 90af45e98..b797032e0 100644 --- a/src/types/TransactionScheduling.ts +++ b/src/types/TransactionScheduling.ts @@ -1,8 +1,9 @@ import { jsonObject, jsonMember } from 'typedjson'; -import { concat } from '@ethersproject/bytes'; import { Timestamp } from './Time'; import { CLValueUInt64 } from './clvalue'; +import { CalltableSerialization } from './CalltableSerialization'; +import { toBytesU64 } from './ByteConverters'; /** * Enum representing the scheduling tags for transaction scheduling types. @@ -35,6 +36,17 @@ class FutureEraScheduling { constructor(eraID: number) { this.eraID = eraID; } + + public toBytes(): Uint8Array { + const calltableSerialization = new CalltableSerialization(); + calltableSerialization.addField(0, Uint8Array.of(1)); + calltableSerialization.addField( + 1, + CLValueUInt64.newCLUint64(this.eraID).bytes() + ); + + return calltableSerialization.toBytes(); + } } /** @@ -61,6 +73,17 @@ class FutureTimestampScheduling { constructor(timestamp: Timestamp) { this.timestamp = timestamp; } + + public toBytes(): Uint8Array { + const calltableSerialization = new CalltableSerialization(); + calltableSerialization.addField(0, Uint8Array.of(2)); + calltableSerialization.addField( + 1, + toBytesU64(Date.parse(this.timestamp.toJSON())) + ); + + return calltableSerialization.toBytes(); + } } /** @@ -124,19 +147,17 @@ export class TransactionScheduling { * @returns A `Uint8Array` representing the transaction scheduling. */ bytes(): Uint8Array { - const tagBytes = Uint8Array.of(this.tag()); - - if (this.futureEra) { - const eraBytes = new CLValueUInt64(BigInt(this.futureEra.eraID)).bytes(); - return concat([tagBytes, eraBytes]); + if (this.standard) { + const calltableSerialization = new CalltableSerialization(); + calltableSerialization.addField(0, Uint8Array.of(0)); + return calltableSerialization.toBytes(); + } else if (this.futureEra) { + return this.futureEra.toBytes(); } else if (this.futureTimestamp) { - const timestampBytes = new CLValueUInt64( - BigInt(this.futureTimestamp.timestamp.toMilliseconds()) - ).bytes(); - return concat([tagBytes, timestampBytes]); + return this.futureTimestamp.toBytes(); } - return tagBytes; + throw new Error('Unable to serialize TransactionScheduling'); } /** diff --git a/src/types/TransactionTarget.test.ts b/src/types/TransactionTarget.test.ts deleted file mode 100644 index b3ab6bc89..000000000 --- a/src/types/TransactionTarget.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { TypedJSON, jsonMember, jsonObject } from 'typedjson'; -import { expect } from 'chai'; -import { TransactionTarget } from './TransactionTarget'; -import assert from 'assert'; - -@jsonObject -class UnderTest { - @jsonMember({ - deserializer: json => TransactionTarget.fromJSON(json), - serializer: value => value.toJSON() - }) - public a: TransactionTarget; -} - -const expectedStoredVariantBytes = [ - 1, - 2, - 211, - 230, - 150, - 66, - 179, - 61, - 20, - 111, - 121, - 120, - 11, - 22, - 180, - 170, - 219, - 114, - 32, - 82, - 135, - 179, - 232, - 232, - 154, - 12, - 202, - 185, - 217, - 134, - 159, - 86, - 38, - 8, - 1, - 43, - 2, - 0, - 0, - 0 -]; -const expectedSessionVariantBytes = [2, 4, 0, 0, 0, 81, 5, 6, 10, 0]; -const mockSessionJson = { - a: { - Session: { - module_bytes: '5105060a', - runtime: 'VmCasperV1' - } - } -}; -const mockStoredJson = { - a: { - Stored: { - id: { - byPackageHash: { - addr: - 'd3e69642b33d146f79780b16b4aadb72205287b3e8e89a0ccab9d9869f562608', - version: 555 - } - }, - runtime: 'VmCasperV1' - } - } -}; - -const mockNativeJson = { a: 'Native' }; - -describe('TransactionTarget', () => { - const serializer = new TypedJSON(UnderTest); - - it('should parse TransactionTarget::Native correctly', () => { - const parsed = serializer.parse(mockNativeJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(mockNativeJson); - }); - - it('should byte-serialize TransactionTarget::Native correctly', () => { - const parsed = serializer.parse(mockNativeJson); - const bytes = parsed!.a.toBytes(); - assert.deepStrictEqual(Array.from(bytes), [0]); - }); - - it('should parse TransactionTarget::Stored correctly', () => { - const parsed = serializer.parse(mockStoredJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(mockStoredJson); - }); - - it('should byte-serialize TransactionTarget::Stored correctly', () => { - const parsed = serializer.parse(mockStoredJson); - const bytes = parsed!.a.toBytes(); - assert.deepStrictEqual(Array.from(bytes), expectedStoredVariantBytes); - }); - - it('should parse TransactionTarget::Session correctly', () => { - const parsed = serializer.parse(mockSessionJson); - const reserialized = JSON.parse(serializer.stringify(parsed!)); - expect(reserialized).to.deep.eq(mockSessionJson); - }); - - it('should byte-serialize TransactionTarget::Session correctly', () => { - const parsed = serializer.parse(mockSessionJson); - const bytes = parsed!.a.toBytes(); - assert.deepStrictEqual(Array.from(bytes), expectedSessionVariantBytes); - }); -}); diff --git a/src/types/TransactionTarget.ts b/src/types/TransactionTarget.ts index b7925f65e..f52563ccf 100644 --- a/src/types/TransactionTarget.ts +++ b/src/types/TransactionTarget.ts @@ -1,37 +1,24 @@ import isNull from 'lodash/isNull'; -import { concat } from '@ethersproject/bytes'; +import { BigNumber } from '@ethersproject/bignumber'; -import { jsonMember, jsonObject } from 'typedjson'; -import { getRuntimeTag, TransactionRuntime } from './AddressableEntity'; +import { jsonMember, jsonObject, TypedJSON } from 'typedjson'; +import { TransactionRuntime } from './AddressableEntity'; import { Hash } from './key'; -import { CLValueString } from './clvalue'; +import { + CLTypeOption, + CLTypeUInt32, + CLValueBool, + CLValueOption, + CLValueString, + CLValueUInt32, + CLValueUInt64 +} from './clvalue'; import { ExecutableDeployItem } from './ExecutableDeployItem'; - -/** - * Enum representing different types of transaction targets. - */ -enum TransactionTargetType { - /** Native target type, used for transactions without a specific target. */ - Native = 0, - /** Stored target type, used for contracts or stored items. */ - Stored = 1, - /** Session target type, used for session-based transactions. */ - Session = 2 -} - -/** - * Enum representing different invocation target tags for identifying transaction target types. - */ -enum InvocationTargetTag { - /** Invocation target by hash. */ - ByHash = 0, - /** Invocation target by name. */ - ByName = 1, - /** Invocation target by package hash. */ - ByPackageHash = 2, - /** Invocation target by package name. */ - ByPackageName = 3 -} +import { CalltableSerialization } from './CalltableSerialization'; +import { + byteArrayJsonDeserializer, + byteArrayJsonSerializer +} from './SerializationUtils'; /** * Represents the invocation target for a transaction identified by a package hash. @@ -54,6 +41,22 @@ export class ByPackageHashInvocationTarget { */ @jsonMember({ name: 'version', isRequired: false, constructor: Number }) version?: number; + + public toBytes(): Uint8Array { + const calltableSerialization = new CalltableSerialization(); + + const versionBytes = this.version + ? CLValueOption.newCLOption( + CLValueUInt32.newCLUInt32(BigNumber.from(this.version)) + ).bytes() + : new CLValueOption(null, new CLTypeOption(CLTypeUInt32)).bytes(); + + calltableSerialization.addField(0, Uint8Array.of(2)); + calltableSerialization.addField(1, this.addr.toBytes()); + calltableSerialization.addField(2, versionBytes); + + return calltableSerialization.toBytes(); + } } /** @@ -72,6 +75,25 @@ export class ByPackageNameInvocationTarget { */ @jsonMember({ name: 'version', isRequired: false, constructor: Number }) version?: number; + + public toBytes(): Uint8Array { + const calltableSerialization = new CalltableSerialization(); + + const versionBytes = this.version + ? CLValueOption.newCLOption( + CLValueUInt32.newCLUInt32(BigNumber.from(this.version)) + ).bytes() + : new CLValueOption(null, new CLTypeOption(CLTypeUInt32)).bytes(); + + calltableSerialization.addField(0, Uint8Array.of(3)); + calltableSerialization.addField( + 1, + CLValueString.newCLString(this.name).bytes() + ); + calltableSerialization.addField(2, versionBytes); + + return calltableSerialization.toBytes(); + } } /** @@ -126,6 +148,31 @@ export class TransactionInvocationTarget { constructor: ByPackageNameInvocationTarget }) byPackageName?: ByPackageNameInvocationTarget; + + public toBytes(): Uint8Array { + if (this.byHash) { + const calltableSerializer = new CalltableSerialization(); + calltableSerializer.addField(0, Uint8Array.of(0)); + calltableSerializer.addField(1, this.byHash.toBytes()); + return calltableSerializer.toBytes(); + } else if (this.byName) { + const calltableSerializer = new CalltableSerialization(); + calltableSerializer.addField(0, Uint8Array.of(1)); + calltableSerializer.addField( + 1, + CLValueString.newCLString(this.byName).bytes() + ); + return calltableSerializer.toBytes(); + } else if (this.byPackageHash) { + return this.byPackageHash.toBytes(); + } else if (this.byPackageName) { + return this.byPackageName.toBytes(); + } + + throw new Error( + 'Can not convert TransactionInvocationTarget to bytes. Missing values from initialization' + ); + } } /** @@ -144,6 +191,28 @@ export class StoredTarget { */ @jsonMember({ name: 'runtime', constructor: String }) runtime: TransactionRuntime; + + /** + * The runtime associated with the stored transaction. + */ + @jsonMember({ name: 'transferred_value', constructor: Number }) + transferredValue: number; + + public toBytes() { + const calltableSerializer = new CalltableSerialization(); + calltableSerializer.addField(0, Uint8Array.of(1)); + calltableSerializer.addField(1, this.id.toBytes()); + calltableSerializer.addField( + 2, + CLValueString.newCLString(this.runtime).bytes() + ); + calltableSerializer.addField( + 3, + CLValueUInt64.newCLUint64(this.transferredValue).bytes() + ); + + return calltableSerializer.toBytes(); + } } /** @@ -154,14 +223,63 @@ export class SessionTarget { /** * The module bytes associated with the session target. */ - @jsonMember({ name: 'module_bytes', constructor: String }) - moduleBytes: string; + @jsonMember({ + name: 'module_bytes', + constructor: Uint8Array, + deserializer: byteArrayJsonDeserializer, + serializer: byteArrayJsonSerializer + }) + moduleBytes: Uint8Array; /** * The runtime associated with the session target. */ @jsonMember({ name: 'runtime', constructor: String }) runtime: TransactionRuntime; + + /** + * The runtime associated with the session target. + */ + @jsonMember({ name: 'is_install_upgrade', constructor: Boolean }) + isInstallUpgrade: boolean; + + /** + * The runtime associated with the stored transaction. + */ + @jsonMember({ name: 'transferred_value', constructor: Number }) + transferredValue: number; + + /** + * The runtime associated with the stored transaction. + */ + @jsonMember({ + name: 'seed', + constructor: Hash, + deserializer: json => Hash.fromJSON(json), + serializer: value => value.toJSON() + }) + seed: Hash; + + public toBytes(): Uint8Array { + const calltableSerializer = new CalltableSerialization(); + calltableSerializer.addField(0, Uint8Array.of(2)); + calltableSerializer.addField( + 1, + CLValueBool.fromBoolean(this.isInstallUpgrade).bytes() + ); + calltableSerializer.addField( + 2, + CLValueString.newCLString(this.runtime).bytes() + ); + calltableSerializer.addField(3, this.moduleBytes); + calltableSerializer.addField( + 4, + CLValueUInt64.newCLUint64(this.transferredValue).bytes() + ); + calltableSerializer.addField(5, this.seed.toBytes()); + + return calltableSerializer.toBytes(); + } } /** @@ -203,105 +321,26 @@ export class TransactionTarget { this.session = session; } - /** - * Converts a 32-bit unsigned integer to a byte array. - * - * @param value The 32-bit unsigned integer to convert. - * @returns A `Uint8Array` representing the value. - */ - private uint32ToBytes(value: number): Uint8Array { - const buffer = new ArrayBuffer(4); - new DataView(buffer).setUint32(0, value, true); - return new Uint8Array(buffer); - } - - /** - * Converts a hexadecimal string to a byte array. - * - * @param hexString The hexadecimal string to convert. - * @returns A `Uint8Array` representing the hexadecimal string. - */ - private hexStringToBytes(hexString: string): Uint8Array { - return Uint8Array.from(Buffer.from(hexString, 'hex')); - } - /** * Serializes the `TransactionTarget` into a byte array. * * @returns A `Uint8Array` representing the serialized transaction target. */ toBytes(): Uint8Array { - let result: Uint8Array = new Uint8Array(); - - if (this.native !== undefined) { - result = concat([result, Uint8Array.of(TransactionTargetType.Native)]); - } else if (this.stored !== undefined) { - result = concat([result, Uint8Array.of(TransactionTargetType.Stored)]); - - if (this.stored.id.byHash !== undefined) { - result = concat([ - result, - Uint8Array.of(InvocationTargetTag.ByHash), - this.stored.id.byHash.toBytes() - ]); - } else if (this.stored.id.byName !== undefined) { - const nameBytes = new CLValueString(this.stored.id.byName).bytes(); - result = concat([ - result, - Uint8Array.of(InvocationTargetTag.ByName), - nameBytes - ]); - } else if (this.stored.id.byPackageHash !== undefined) { - result = concat([ - result, - Uint8Array.of(InvocationTargetTag.ByPackageHash), - this.stored.id.byPackageHash.addr.toBytes() - ]); - - if (this.stored.id.byPackageHash.version !== undefined) { - const versionBytes = this.uint32ToBytes( - this.stored.id.byPackageHash.version - ); - result = concat([result, Uint8Array.of(1), versionBytes]); - } else { - result = concat([result, Uint8Array.of(0)]); - } - } else if (this.stored.id.byPackageName !== undefined) { - const nameBytes = new CLValueString( - this.stored.id.byPackageName.name - ).bytes(); - result = concat([ - result, - Uint8Array.of(InvocationTargetTag.ByPackageName), - nameBytes - ]); - - if (this.stored.id.byPackageName.version !== undefined) { - const versionBytes = this.uint32ToBytes( - this.stored.id.byPackageName.version - ); - result = concat([result, Uint8Array.of(1), versionBytes]); - } else { - result = concat([result, Uint8Array.of(0)]); - } - } - - const runtimeTag = getRuntimeTag(this.stored.runtime); - result = concat([result, Uint8Array.of(runtimeTag)]); - } else if (this.session !== undefined) { - result = concat([result, Uint8Array.of(TransactionTargetType.Session)]); - - const moduleBytes = this.session.moduleBytes - ? this.hexStringToBytes(this.session.moduleBytes) - : new Uint8Array([0]); - const moduleLengthBytes = this.uint32ToBytes(moduleBytes.length); - result = concat([result, moduleLengthBytes, moduleBytes]); - - const runtimeTag = getRuntimeTag(this.session.runtime); - result = concat([result, Uint8Array.of(runtimeTag)]); + if (this.native) { + const calltableSerializer = new CalltableSerialization(); + calltableSerializer.addField(0, Uint8Array.of(0)); + + return calltableSerializer.toBytes(); + } else if (this.stored) { + return this.stored.toBytes(); + } else if (this.session) { + return this.session.toBytes(); } - return result; + throw new Error( + 'Can not convert TransactionTarget to bytes. Missing values ( native | stored | session ) from initialization' + ); } /** @@ -317,35 +356,11 @@ export class TransactionTarget { if (typeof json === 'string' && json === 'Native') { target.native = {}; } else if (json.Stored) { - const storedTarget = new StoredTarget(); - storedTarget.runtime = json.Stored.runtime; - - const invocationTarget = new TransactionInvocationTarget(); - if (json.Stored.id.byHash) { - invocationTarget.byHash = Hash.fromHex(json.Stored.id.byHash); - } else if (json.Stored.id.byName) { - invocationTarget.byName = json.Stored.id.byName; - } else if (json.Stored.id.byPackageHash) { - invocationTarget.byPackageHash = new ByPackageHashInvocationTarget(); - invocationTarget.byPackageHash.addr = Hash.fromHex( - json.Stored.id.byPackageHash.addr - ); - invocationTarget.byPackageHash.version = - json.Stored.id.byPackageHash.version; - } else if (json.Stored.id.byPackageName) { - invocationTarget.byPackageName = new ByPackageNameInvocationTarget(); - invocationTarget.byPackageName.name = json.Stored.id.byPackageName.name; - invocationTarget.byPackageName.version = - json.Stored.id.byPackageName.version; - } - - storedTarget.id = invocationTarget; - target.stored = storedTarget; + const serializer = new TypedJSON(StoredTarget); + target.stored = serializer.parse(json.Stored); } else if (json.Session) { - const sessionTarget = new SessionTarget(); - sessionTarget.runtime = json.Session.runtime; - sessionTarget.moduleBytes = json.Session.module_bytes; - target.session = sessionTarget; + const serializer = new TypedJSON(SessionTarget); + target.session = serializer.parse(json.Session); } return target; @@ -361,19 +376,11 @@ export class TransactionTarget { if (this.native !== undefined) { return 'Native'; } else if (this.stored !== undefined) { - return { - Stored: { - id: this.stored.id, - runtime: this.stored.runtime - } - }; + const serializer = new TypedJSON(StoredTarget); + return serializer.toPlainJson(this.stored); } else if (this.session !== undefined) { - return { - Session: { - module_bytes: this.session.moduleBytes, - runtime: this.session.runtime - } - }; + const serializer = new TypedJSON(SessionTarget); + return serializer.toPlainJson(this.session); } else { throw new Error('unknown target type'); } @@ -406,8 +413,7 @@ export class TransactionTarget { if (session.storedContractByHash !== undefined) { const storedTarget = new StoredTarget(); const invocationTarget = new TransactionInvocationTarget(); - const hash = session.storedContractByHash.hash.hash; - invocationTarget.byHash = hash; + invocationTarget.byHash = session.storedContractByHash.hash.hash; storedTarget.runtime = 'VmCasperV1'; storedTarget.id = invocationTarget; From 15982b6c3338244433d378ac60cac4604dc49901 Mon Sep 17 00:00:00 2001 From: Oleksandr Myshchyshyn Date: Thu, 21 Nov 2024 20:56:25 +0200 Subject: [PATCH 2/8] Added build method for TransactionV1Payload --- src/types/Transaction.test.ts | 43 +++++++++++------------------ src/types/Transaction.ts | 5 ++-- src/types/TransactionPayload.ts | 49 ++++++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/src/types/Transaction.test.ts b/src/types/Transaction.test.ts index d5978e4bd..f8e5e2e68 100644 --- a/src/types/Transaction.test.ts +++ b/src/types/Transaction.test.ts @@ -19,12 +19,11 @@ import { CLValueUInt64 } from './clvalue'; import { PublicKey } from './keypair'; -import { PayloadFields, TransactionV1Payload } from './TransactionPayload'; +import { TransactionV1Payload } from './TransactionPayload'; describe('Test Transaction', () => { it('should create a Transaction from TransactionV1', async () => { const keys = await PrivateKey.generate(KeyAlgorithm.ED25519); - const timestamp = new Timestamp(new Date()); const paymentAmount = 20000000000000; const pricingMode = new PricingMode(); @@ -43,36 +42,26 @@ describe('Test Transaction', () => { id: CLValueOption.newCLOption(CLValueUInt64.newCLUint64(3)) }); - const transactionTarget = new TransactionTarget(new SessionTarget()); - const entryPoint = new TransactionEntryPoint(undefined, {}); - const scheduling = new TransactionScheduling({}); - - const payloadFields = new PayloadFields(); - payloadFields.addField(0, args.toBytes()); - payloadFields.addField(1, transactionTarget.toBytes()); - payloadFields.addField(2, entryPoint.bytes()); - payloadFields.addField(3, scheduling.bytes()); - - const transactionPayload = new TransactionV1Payload(); - transactionPayload.initiatorAddr = new InitiatorAddr(keys.publicKey); - transactionPayload.ttl = new Duration(1800000); - transactionPayload.args = args; - transactionPayload.entryPoint = entryPoint; - transactionPayload.pricingMode = pricingMode; - transactionPayload.timestamp = timestamp; - transactionPayload.category = 2; - transactionPayload.target = transactionTarget; - transactionPayload.scheduling = scheduling; - transactionPayload.chainName = 'casper-net-1'; - transactionPayload.fields = payloadFields; + const transactionPayload = TransactionV1Payload.build({ + initiatorAddr: new InitiatorAddr(keys.publicKey), + ttl: new Duration(1800000), + args, + timestamp: new Timestamp(new Date()), + category: 2, + entryPoint: new TransactionEntryPoint(undefined, {}), + scheduling: new TransactionScheduling({}), + transactionTarget: new TransactionTarget(new SessionTarget()), + chainName: 'casper-net-1', + pricingMode + }); const transaction = TransactionV1.makeTransactionV1(transactionPayload); await transaction.sign(keys); - // const toJson = TransactionV1.toJson(transaction); - // const parsed = TransactionV1.fromJSON(toJson); + const toJson = TransactionV1.toJson(transaction); + console.log(toJson); - // console.log(Args.fromBytes(transaction.payload.fields.fields.get(0)!)); + // const parsed = TransactionV1.fromJSON(toJson); // const transactionPaymentAmount = parsed.body.args.args // .get('amount')! diff --git a/src/types/Transaction.ts b/src/types/Transaction.ts index bed56ddbb..506e7eeee 100644 --- a/src/types/Transaction.ts +++ b/src/types/Transaction.ts @@ -148,7 +148,6 @@ export class TransactionV1 { * @throws {TransactionError} Throws errors if validation fails. */ public validate(): boolean { - console.log(this.payload); const payloadBytes = this.payload!.toBytes(); const calculatedHash = new Hash(byteHash(payloadBytes)); @@ -401,7 +400,7 @@ export class TransactionBody { * The category of the transaction, indicating its type (e.g., minting, auction). */ @jsonMember({ name: 'transaction_category', constructor: Number }) - public category: number; + public category?: number; /** * Constructs a `TransactionBody` with the given arguments, target, entry point, scheduling, and category. @@ -416,7 +415,7 @@ export class TransactionBody { target: TransactionTarget, entryPoint: TransactionEntryPoint, scheduling: TransactionScheduling, - category: number + category?: number ) { this.args = args; this.target = target; diff --git a/src/types/TransactionPayload.ts b/src/types/TransactionPayload.ts index e1450d2d2..41c05a130 100644 --- a/src/types/TransactionPayload.ts +++ b/src/types/TransactionPayload.ts @@ -19,6 +19,19 @@ import { byteArrayJsonSerializer } from './SerializationUtils'; +interface ITransactionPayloadBuildParams { + initiatorAddr: InitiatorAddr; + args: Args; + ttl: Duration; + entryPoint: TransactionEntryPoint; + pricingMode: PricingMode; + timestamp: Timestamp; + category?: number; + transactionTarget: TransactionTarget; + scheduling: TransactionScheduling; + chainName: string; +} + export class PayloadFields { public fields: Map = new Map(); @@ -125,7 +138,7 @@ export class TransactionV1Payload { public target: TransactionTarget; public entryPoint: TransactionEntryPoint; public scheduling: TransactionScheduling; - public category: number; + public category?: number; public toBytes(): Uint8Array { const calltableSerialization = new CalltableSerialization(); @@ -147,4 +160,38 @@ export class TransactionV1Payload { return calltableSerialization.toBytes(); } + + public static build({ + initiatorAddr, + args, + ttl, + entryPoint, + pricingMode, + timestamp, + category, + transactionTarget, + scheduling, + chainName + }: ITransactionPayloadBuildParams): TransactionV1Payload { + const payloadFields = new PayloadFields(); + payloadFields.addField(0, args.toBytes()); + payloadFields.addField(1, transactionTarget.toBytes()); + payloadFields.addField(2, entryPoint.bytes()); + payloadFields.addField(3, scheduling.bytes()); + + const transactionPayload = new TransactionV1Payload(); + transactionPayload.initiatorAddr = initiatorAddr; + transactionPayload.ttl = ttl; + transactionPayload.args = args; + transactionPayload.entryPoint = entryPoint; + transactionPayload.pricingMode = pricingMode; + transactionPayload.timestamp = timestamp; + transactionPayload.category = category; + transactionPayload.target = transactionTarget; + transactionPayload.scheduling = scheduling; + transactionPayload.chainName = chainName; + transactionPayload.fields = payloadFields; + + return transactionPayload; + } } From 5b6895fed86bc9da8c3a9908b44b4931a8779628 Mon Sep 17 00:00:00 2001 From: Oleksandr Myshchyshyn Date: Sat, 23 Nov 2024 12:55:05 +0200 Subject: [PATCH 3/8] Added Deserializer for TransactionV1Payload, TransactionEntryPoint, TransactionScheduling, TransactionTarget, CalltableSerialization to convert fromBytes to instances, Updated Reservation for BidKind, Added Prepaid for StoredValue, added fromBytesWithType for CLValueParser --- src/types/AddressableEntity.ts | 18 - src/types/Args.ts | 73 ++-- src/types/Bid.ts | 44 ++ src/types/BidKind.ts | 15 +- src/types/ByteConverters.ts | 41 ++ src/types/CalltableSerialization.ts | 97 ++++- src/types/Deploy.ts | 20 +- src/types/Prepayment.ts | 46 +++ src/types/Reservation.ts | 57 --- src/types/StoredValue.ts | 8 +- src/types/Transaction.test.ts | 44 +- src/types/Transaction.ts | 16 +- src/types/TransactionEntryPoint.ts | 383 +++++++++--------- src/types/TransactionScheduling.ts | 78 +++- src/types/TransactionTarget.ts | 196 +++++++++ ...tionPayload.ts => TransactionV1Payload.ts} | 147 ++++++- src/types/clvalue/Parser.ts | 33 ++ src/types/index.ts | 2 +- 18 files changed, 958 insertions(+), 360 deletions(-) create mode 100644 src/types/Prepayment.ts delete mode 100644 src/types/Reservation.ts rename src/types/{TransactionPayload.ts => TransactionV1Payload.ts} (55%) diff --git a/src/types/AddressableEntity.ts b/src/types/AddressableEntity.ts index df6afa3ca..650dd5629 100644 --- a/src/types/AddressableEntity.ts +++ b/src/types/AddressableEntity.ts @@ -152,21 +152,3 @@ export class NamedEntryPoint { @jsonMember({ name: 'name', constructor: String }) name: string; } - -/** - * Returns the numeric tag associated with a given transaction runtime version. - * Useful for distinguishing between different virtual machine versions. - * - * @param runtime - The transaction runtime to retrieve the tag for. - * @returns A number representing the tag for the given runtime. - */ -export function getRuntimeTag(runtime: TransactionRuntime): number { - switch (runtime) { - case 'VmCasperV1': - return 0; - case 'VmCasperV2': - return 1; - default: - return 0; - } -} diff --git a/src/types/Args.ts b/src/types/Args.ts index c590f7fb8..a2e58304c 100644 --- a/src/types/Args.ts +++ b/src/types/Args.ts @@ -1,13 +1,6 @@ import { concat } from '@ethersproject/bytes'; -import { - CLTypeString, - CLValue, - CLValueParser, - CLValueString, - CLValueUInt32, - IResultWithBytes -} from './clvalue'; +import { CLValue, CLValueParser } from './clvalue'; import { jsonMapMember, jsonObject } from 'typedjson'; import { toBytesString, toBytesU32 } from './ByteConverters'; @@ -35,21 +28,21 @@ export class NamedArg { /** * Creates a `NamedArg` instance from a byte array. * @param bytes - The byte array to parse. - * @returns A new `NamedArg` instance. - * @throws Error if the value data is missing. + * @returns A `NamedArg` instance. */ public static fromBytes(bytes: Uint8Array): NamedArg { - const stringValue = CLValueString.fromBytes(bytes); + let offset = 0; - if (!stringValue.bytes) { - throw new Error('Missing data for value of named arg'); - } + const nameLength = new DataView(bytes.buffer).getUint32(offset, true); + offset += 4; + const nameBytes = bytes.slice(offset, offset + nameLength); + offset += nameLength; + const name = new TextDecoder().decode(nameBytes); - const value = CLValueParser.fromBytesByType( - stringValue.bytes, - CLTypeString - ); - return new NamedArg(value.result.toString(), value.result); + const valueBytes = bytes.slice(offset); + const value = CLValueParser.fromBytesWithType(valueBytes); + + return new NamedArg(name, value.result); } } @@ -156,28 +149,30 @@ export class Args { /** * Creates an `Args` instance from a byte array. - * Parses the byte array to extract each argument. * @param bytes - The byte array to parse. - * @returns An object containing a new `Args` instance and any remaining bytes. - * @throws Error if there is an issue parsing the bytes. + * @returns An `Args` instance. */ - public static fromBytes(bytes: Uint8Array): IResultWithBytes { - const uint32 = CLValueUInt32.fromBytes(bytes); - const size = uint32.result.getValue().toNumber(); - - let remainBytes: Uint8Array | undefined = uint32.bytes; - const res: NamedArg[] = []; - for (let i = 0; i < size; i++) { - if (!remainBytes) { - throw new Error('Error while parsing bytes'); - } - const namedArg = NamedArg.fromBytes(remainBytes); - res.push(namedArg); - remainBytes = undefined; + public static fromBytes(bytes: Uint8Array): Args { + let offset = 0; + + const numArgs = new DataView(bytes.buffer).getUint32(offset, true); + offset += 4; + + const args = new Map(); + + for (let i = 0; i < numArgs; i++) { + const namedArgBytes = bytes.slice(offset); + const namedArg = NamedArg.fromBytes(namedArgBytes); + + const nameLength = new DataView(namedArgBytes.buffer).getUint32(0, true); + const valueBytes = CLValueParser.toBytesWithType(namedArg.value); + const consumedBytes = 4 + nameLength + valueBytes.length; + + offset += consumedBytes; + + args.set(namedArg.name, namedArg.value); } - return { - result: Args.fromNamedArgs(res), - bytes: remainBytes || Uint8Array.from([]) - }; + + return new Args(args); } } diff --git a/src/types/Bid.ts b/src/types/Bid.ts index 0061132de..f9cd9d754 100644 --- a/src/types/Bid.ts +++ b/src/types/Bid.ts @@ -74,6 +74,12 @@ export class ValidatorBid { @jsonMember({ name: 'maximum_delegation_amount', constructor: Number }) maximumDelegationAmount: number; + /** + * Number of slots reserved for specific delegators + */ + @jsonMember({ name: 'reserved_slots', constructor: Number }) + reservedSlots: number; + /** * The vesting schedule for this validator’s stake. */ @@ -301,3 +307,41 @@ export class Bridge { }) newValidatorPublicKey: PublicKey; } + +@jsonObject +/** + * Represents a reservation in the blockchain system, including delegation details and associated public keys. + */ +export class Reservation { + /** + * The delegation rate, representing the percentage of rewards allocated to the delegator. + */ + @jsonMember({ name: 'delegation_rate', constructor: Number }) + delegationRate: number; + + /** + * The public key of the validator associated with this reservation. + * + * This key is used to identify the validator in the blockchain system. + */ + @jsonMember({ + name: 'validator_public_key', + constructor: PublicKey, + deserializer: json => PublicKey.fromJSON(json), + serializer: value => value.toJSON() + }) + validatorPublicKey: PublicKey; + + /** + * The public key of the delegator associated with this reservation. + * + * This key is used to identify the delegator who initiated the reservation. + */ + @jsonMember({ + name: 'delegator_public_key', + constructor: PublicKey, + deserializer: json => PublicKey.fromJSON(json), + serializer: value => value.toJSON() + }) + delegatorPublicKey: PublicKey; +} diff --git a/src/types/BidKind.ts b/src/types/BidKind.ts index 170209656..a997da72b 100644 --- a/src/types/BidKind.ts +++ b/src/types/BidKind.ts @@ -1,5 +1,12 @@ import { jsonObject, jsonMember } from 'typedjson'; -import { Bid, Bridge, Credit, Delegator, ValidatorBid } from './Bid'; +import { + Bid, + Bridge, + Credit, + Delegator, + Reservation, + ValidatorBid +} from './Bid'; /** * Represents a polymorphic bid kind, allowing for different types of bid-related entities. @@ -37,4 +44,10 @@ export class BidKind { */ @jsonMember({ name: 'Credit', constructor: Credit }) credit?: Credit; + + /** + * Represents a validator reserving a slot for specific delegator + */ + @jsonMember({ name: 'Reservation', constructor: Reservation }) + reservation?: Reservation; } diff --git a/src/types/ByteConverters.ts b/src/types/ByteConverters.ts index e7e2d70d3..786c194a2 100644 --- a/src/types/ByteConverters.ts +++ b/src/types/ByteConverters.ts @@ -132,3 +132,44 @@ export function toBytesArrayU8(arr: Uint8Array): Uint8Array { export function byteHash(x: Uint8Array): Uint8Array { return blake2b(x, { dkLen: 32 }); } + +/** + * Parses a 16-bit unsigned integer (`u16`) from a little-endian byte array. + * @param bytes - The byte array containing the `u16` value. + * @returns The parsed 16-bit unsigned integer. + */ +export function parseU16(bytes: Uint8Array): number { + if (bytes.length < 2) { + throw new Error('Invalid byte array for u16 parsing'); + } + return bytes[0] | (bytes[1] << 8); +} + +/** + * Parses a 32-bit unsigned integer (`u32`) from a little-endian byte array. + * @param bytes - The byte array containing the `u32` value. + * @returns The parsed 32-bit unsigned integer. + */ +export function parseU32(bytes: Uint8Array): number { + if (bytes.length < 4) { + throw new Error('Invalid byte array for u32 parsing'); + } + + return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); +} + +/** + * Parses a 64-bit unsigned integer (`u64`) from a little-endian byte array. + * @param bytes - A `Uint8Array` containing the serialized 64-bit unsigned integer. + * @returns A `BigNumber` representing the parsed value. + */ +export const fromBytesU64 = (bytes: Uint8Array): BigNumber => { + if (bytes.length !== 8) { + throw new Error( + `Invalid input length for u64: expected 8 bytes, got ${bytes.length}` + ); + } + + // Convert the little-endian bytes into a BigNumber + return BigNumber.from(bytes.reverse()); +}; diff --git a/src/types/CalltableSerialization.ts b/src/types/CalltableSerialization.ts index 8aaa92989..11556666e 100644 --- a/src/types/CalltableSerialization.ts +++ b/src/types/CalltableSerialization.ts @@ -1,11 +1,21 @@ import { concat } from '@ethersproject/bytes'; -import { toBytesU16, toBytesU32 } from './ByteConverters'; +import { parseU16, parseU32, toBytesU16, toBytesU32 } from './ByteConverters'; +/** + * Represents a single field in the call table. + */ export class Field { readonly index: number; readonly offset: number; - readonly value: Uint8Array; + value: Uint8Array; + /** + * Constructs a new `Field` instance. + * + * @param index - The index of the field. + * @param offset - The offset of the field in the payload. + * @param value - The byte array value of the field. + */ constructor(index: number, offset: number, value: Uint8Array) { this.index = index; this.offset = offset; @@ -14,22 +24,35 @@ export class Field { /** * Calculates the serialized vector size for the given number of fields. - * @returns The size of the serialized vector. + * + * This method determines the size of the serialized vector required + * to store all fields, including their indices and offsets. + * + * @returns The size of the serialized vector in bytes. */ static serializedVecSize(): number { - return 4 + 4 * 2; + return 4 + 4 * 2; // Number of fields (4 bytes) + index/offset pairs (4 bytes each) } } +/** + * Handles serialization and deserialization of call table data. + * + * The `CalltableSerialization` class is responsible for managing a collection + * of fields and converting them into a byte array for serialization. It can + * also reconstruct the fields from a serialized byte array. + */ export class CalltableSerialization { private fields: Field[] = []; private currentOffset = 0; /** * Adds a field to the call table. - * @param index The field index. - * @param value The field value as a byte array. - * @returns The current instance of CalltableSerialization. + * + * @param index - The field index. + * @param value - The field value as a byte array. + * @returns The current instance of `CalltableSerialization`. + * @throws An error if the fields are not added in the correct index order. */ addField(index: number, value: Uint8Array): CalltableSerialization { if (this.fields.length !== index) { @@ -44,8 +67,9 @@ export class CalltableSerialization { } /** - * Serializes the call table to a byte array. - * @returns A Uint8Array representing the serialized call table. + * Serializes the call table into a byte array. + * + * @returns A `Uint8Array` representing the serialized call table. */ toBytes(): Uint8Array { const calltableBytes: Uint8Array[] = []; @@ -63,4 +87,59 @@ export class CalltableSerialization { return concat([...calltableBytes, ...payloadBytes]); } + + /** + * Retrieves a specific field by its index. + * + * @param index - The index of the field to retrieve. + * @returns The field value as a `Uint8Array`, or `undefined` if the field is not found. + */ + getField(index: number): Uint8Array | undefined { + const field = this.fields.find(f => f.index === index); + return field ? field.value : undefined; + } + + /** + * Deserializes a byte array into a `CalltableSerialization` object. + * + * This method reconstructs the call table and its fields from a serialized byte array. + * + * @param bytes - The serialized byte array. + * @returns A `CalltableSerialization` instance containing the deserialized fields. + * @throws An error if the byte array is invalid or missing required fields. + */ + static fromBytes(bytes: Uint8Array): CalltableSerialization { + const instance = new CalltableSerialization(); + let offset = 0; + + // Read the number of fields + const fieldCount = parseU32(bytes.slice(offset, offset + 4)); + offset += 4; + + const fields: Field[] = []; + for (let i = 0; i < fieldCount; i++) { + const index = parseU16(bytes.slice(offset, offset + 2)); + offset += 2; + const fieldOffset = parseU32(bytes.slice(offset, offset + 4)); + offset += 4; + + // Initialize each field with an empty value + fields.push(new Field(index, fieldOffset, new Uint8Array())); + } + + // Read the total payload size + const payloadSize = parseU32(bytes.slice(offset, offset + 4)); + offset += 4; + + // Extract field values based on their offsets + for (let i = 0; i < fields.length; i++) { + const start = fields[i].offset; + const end = i + 1 < fields.length ? fields[i + 1].offset : payloadSize; + fields[i].value = bytes.slice(offset + start, offset + end); + } + + instance.fields = fields; + instance.currentOffset = payloadSize; + return instance; + } } diff --git a/src/types/Deploy.ts b/src/types/Deploy.ts index ff0dff8c9..006ac4142 100644 --- a/src/types/Deploy.ts +++ b/src/types/Deploy.ts @@ -12,7 +12,10 @@ import { TransactionCategory, TransactionHeader } from './Transaction'; -import { TransactionEntryPoint } from './TransactionEntryPoint'; +import { + TransactionEntryPoint, + TransactionEntryPointEnum +} from './TransactionEntryPoint'; import { InitiatorAddr } from './InitiatorAddr'; import { PaymentLimitedMode, PricingMode } from './PricingMode'; import { TransactionTarget } from './TransactionTarget'; @@ -359,14 +362,18 @@ export class Deploy { */ static newTransactionFromDeploy(deploy: Deploy): Transaction { let paymentAmount = 0; - const transactionEntryPoint: TransactionEntryPoint = new TransactionEntryPoint(); + let transactionEntryPoint: TransactionEntryPoint; let transactionCategory = TransactionCategory.Large; if (deploy.session.transfer) { transactionCategory = TransactionCategory.Mint; - transactionEntryPoint.transfer = {}; + transactionEntryPoint = new TransactionEntryPoint( + TransactionEntryPointEnum.Transfer + ); } else if (deploy.session.moduleBytes) { - transactionEntryPoint.call = {}; + transactionEntryPoint = new TransactionEntryPoint( + TransactionEntryPointEnum.Call + ); } else { let entryPoint = ''; @@ -379,7 +386,10 @@ export class Deploy { } else if (deploy.session.storedVersionedContractByName) { entryPoint = deploy.session.storedVersionedContractByName.entryPoint; } - transactionEntryPoint.custom = entryPoint; + transactionEntryPoint = new TransactionEntryPoint( + TransactionEntryPointEnum.Custom, + entryPoint + ); } const amountArgument = deploy.payment.getArgs(); diff --git a/src/types/Prepayment.ts b/src/types/Prepayment.ts new file mode 100644 index 000000000..836ee73e2 --- /dev/null +++ b/src/types/Prepayment.ts @@ -0,0 +1,46 @@ +import { jsonMember, jsonObject } from 'typedjson'; +import { HexBytes } from './HexBytes'; +import { Hash } from './key'; + +/** + * Represents a gas pre-payment in the blockchain system. + * + * This container includes details about the receipt, prepayment kind, + * and associated data required for the gas pre-payment process. + */ +@jsonObject +export class PrepaymentKind { + /** + * The receipt identifier for the gas pre-payment. + * + * This is a string representation that uniquely identifies the pre-payment receipt. + */ + @jsonMember({ + name: 'receipt', + constructor: Hash, + deserializer: json => Hash.fromJSON(json), + serializer: value => value.toJSON() + }) + receipt: Hash; + + /** + * The kind of pre-payment, represented as a byte. + * + * This value specifies the type or category of the pre-payment. + */ + @jsonMember({ + name: 'prepayment_data', + constructor: HexBytes, + deserializer: json => HexBytes.fromJSON(json), + serializer: value => value.toJSON() + }) + prepaymentData: HexBytes; + + /** + * The pre-payment data associated with this transaction. + * + * This is a string containing additional information or metadata for the pre-payment. + */ + @jsonMember({ name: 'prepayment_kind', constructor: Number }) + prepaymentKind: number; +} diff --git a/src/types/Reservation.ts b/src/types/Reservation.ts deleted file mode 100644 index 09e7b9e41..000000000 --- a/src/types/Reservation.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { jsonMember, jsonObject } from 'typedjson'; -import { HexBytes } from './HexBytes'; -import { Hash } from './key'; - -/** - * Represents a reservation, including a receipt, reservation data, and the type of reservation. - */ -@jsonObject -export class ReservationKind { - /** - * The receipt associated with the reservation. - * This is typically a unique identifier for the reservation. - */ - @jsonMember({ - name: 'receipt', - constructor: Hash, - deserializer: json => Hash.fromJSON(json), - serializer: value => value.toJSON() - }) - receipt: Hash; - - /** - * The reservation data, represented as a `HexBytes` object. - * This can contain specific details regarding the reservation, encoded as hex. - */ - @jsonMember({ - name: 'reservation_data', - constructor: HexBytes, - deserializer: json => HexBytes.fromJSON(json), - serializer: value => value.toJSON() - }) - reservationData: HexBytes; - - /** - * The kind of reservation, represented as a number. - * This field can be used to distinguish different types of reservations. - */ - @jsonMember({ name: 'reservation_kind', constructor: Number }) - reservationKind: number; - - /** - * Creates a new instance of `ReservationKind`. - * - * @param receipt The receipt associated with the reservation. - * @param reservationData The reservation data encoded as hex. - * @param reservationKind The type of the reservation, represented by a number. - */ - constructor( - receipt: Hash, - reservationData: HexBytes, - reservationKind: number - ) { - this.receipt = receipt; - this.reservationData = reservationData; - this.reservationKind = reservationKind; - } -} diff --git a/src/types/StoredValue.ts b/src/types/StoredValue.ts index 1e064ceb2..b605d376c 100644 --- a/src/types/StoredValue.ts +++ b/src/types/StoredValue.ts @@ -12,7 +12,7 @@ import { Package } from './Package'; import { MessageChecksum, MessageTopicSummary } from './MessageTopic'; import { NamedKeyValue } from './NamedKey'; import { EntryPointValue } from './EntryPoint'; -import { ReservationKind } from './Reservation'; +import { PrepaymentKind } from './Prepayment'; import { Contract } from './Contract'; import { ContractPackage } from './ContractPackage'; import { CLValue, CLValueParser } from './clvalue'; @@ -152,10 +152,10 @@ export class StoredValue { namedKey?: NamedKeyValue; /** - * The reservation information related to this stored value. + * Stores location, type and data for a gas pre-payment. */ - @jsonMember({ name: 'Reservation', constructor: ReservationKind }) - reservation?: ReservationKind; + @jsonMember({ name: 'Prepaid', constructor: PrepaymentKind }) + prepaid?: PrepaymentKind; /** * The stored entry point value, typically representing an entry point in a smart contract. diff --git a/src/types/Transaction.test.ts b/src/types/Transaction.test.ts index f8e5e2e68..abe69ac9a 100644 --- a/src/types/Transaction.test.ts +++ b/src/types/Transaction.test.ts @@ -9,7 +9,10 @@ import { FixedMode, PricingMode } from './PricingMode'; import { KeyAlgorithm } from './keypair/Algorithm'; import { SessionTarget, TransactionTarget } from './TransactionTarget'; -import { TransactionEntryPoint } from './TransactionEntryPoint'; +import { + TransactionEntryPoint, + TransactionEntryPointEnum +} from './TransactionEntryPoint'; import { TransactionScheduling } from './TransactionScheduling'; import { Args } from './Args'; import { @@ -19,7 +22,9 @@ import { CLValueUInt64 } from './clvalue'; import { PublicKey } from './keypair'; -import { TransactionV1Payload } from './TransactionPayload'; +import { TransactionV1Payload } from './TransactionV1Payload'; +import { Hash } from './key'; +import { assert, expect } from 'chai'; describe('Test Transaction', () => { it('should create a Transaction from TransactionV1', async () => { @@ -42,15 +47,29 @@ describe('Test Transaction', () => { id: CLValueOption.newCLOption(CLValueUInt64.newCLUint64(3)) }); + const sessionTarget = new SessionTarget(); + + sessionTarget.runtime = 'VmCasperV1'; + sessionTarget.transferredValue = 1000; + sessionTarget.moduleBytes = Uint8Array.from([1]); + sessionTarget.isInstallUpgrade = false; + sessionTarget.seed = Hash.fromHex( + '8bf9d406ab901428d43ecd3a6f214b864e7ef8316934e5e0f049650a65b40d73' + ); + const transactionPayload = TransactionV1Payload.build({ initiatorAddr: new InitiatorAddr(keys.publicKey), ttl: new Duration(1800000), args, timestamp: new Timestamp(new Date()), category: 2, - entryPoint: new TransactionEntryPoint(undefined, {}), + entryPoint: new TransactionEntryPoint(TransactionEntryPointEnum.Call), scheduling: new TransactionScheduling({}), - transactionTarget: new TransactionTarget(new SessionTarget()), + transactionTarget: new TransactionTarget( + undefined, + undefined, + sessionTarget + ), chainName: 'casper-net-1', pricingMode }); @@ -59,17 +78,14 @@ describe('Test Transaction', () => { await transaction.sign(keys); const toJson = TransactionV1.toJson(transaction); - console.log(toJson); + const parsed = TransactionV1.fromJSON(toJson); - // const parsed = TransactionV1.fromJSON(toJson); + const transactionPaymentAmount = parsed.payload.args.args + .get('amount')! + .toString(); - // const transactionPaymentAmount = parsed.body.args.args - // .get('amount')! - // .toString(); - // - // assert.deepEqual(parsed.approvals[0].signer, keys.publicKey); - // expect(transaction.body).to.deep.equal(transactionBody); - // expect(transaction.header).to.deep.equal(transactionHeader); - // assert.deepEqual(parseInt(transactionPaymentAmount, 10), paymentAmount); + assert.deepEqual(parsed.approvals[0].signer, keys.publicKey); + expect(transaction.payload).to.deep.equal(transactionPayload); + assert.deepEqual(parseInt(transactionPaymentAmount, 10), paymentAmount); }); }); diff --git a/src/types/Transaction.ts b/src/types/Transaction.ts index 506e7eeee..e3d1cb5ef 100644 --- a/src/types/Transaction.ts +++ b/src/types/Transaction.ts @@ -14,18 +14,13 @@ import { PrivateKey } from './keypair/PrivateKey'; import { Args } from './Args'; import { deserializeArgs, serializeArgs } from './SerializationUtils'; import { byteHash } from './ByteConverters'; -import { TransactionV1Payload } from './TransactionPayload'; +import { TransactionV1Payload } from './TransactionV1Payload'; /** * Custom error class for handling transaction-related errors. */ export class TransactionError extends Error {} -/** - * Error to indicate an invalid body hash in a transaction. - */ -export const ErrInvalidBodyHash = new TransactionError('invalid body hash'); - /** * Error to indicate an invalid transaction hash. */ @@ -124,7 +119,14 @@ export class TransactionV1 { /** * The header of the transaction. */ - @jsonMember({ name: 'payload', constructor: TransactionV1Payload }) + @jsonMember({ + name: 'payload', + constructor: TransactionV1Payload, + deserializer: json => { + if (!json) return; + return TransactionV1Payload.fromJSON(json); + } + }) public payload: TransactionV1Payload; /** diff --git a/src/types/TransactionEntryPoint.ts b/src/types/TransactionEntryPoint.ts index b20816d32..9b5317bae 100644 --- a/src/types/TransactionEntryPoint.ts +++ b/src/types/TransactionEntryPoint.ts @@ -1,5 +1,4 @@ import { jsonObject, jsonMember } from 'typedjson'; - import { CLValueString } from './clvalue'; import { CalltableSerialization } from './CalltableSerialization'; @@ -22,163 +21,94 @@ export enum TransactionEntryPointEnum { } /** - * Enum representing the tags for different transaction entry points. This is used for efficient storage and comparison. + * Enum representing the unique tags associated with each transaction entry point. + * These tags are used to simplify storage and facilitate efficient comparison of entry points. */ export enum TransactionEntryPointTag { + Custom = 0, Call = 1, - Transfer, - AddBid, - WithdrawBid, - Delegate, - Undelegate, - Redelegate, - ActivateBid, - ChangeBidPublicKey, - AddReservations, - CancelReservations + Transfer = 2, + AddBid = 3, + WithdrawBid = 4, + Delegate = 5, + Undelegate = 6, + Redelegate = 7, + ActivateBid = 8, + ChangeBidPublicKey = 9, + AddReservations = 10, + CancelReservations = 11 } /** - * Represents a transaction entry point, which can be one of several predefined actions or a custom action. - * This class contains multiple fields that correspond to different transaction actions. + * Represents a transaction entry point, which defines an action to be executed within the system. + * This class supports predefined entry points as well as custom-defined actions. */ @jsonObject export class TransactionEntryPoint { /** - * Custom entry point, where the value can be a string representing a custom action. - */ - @jsonMember({ constructor: String, name: 'Custom' }) - custom?: string; - - /** - * The transfer action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'Transfer' }) - transfer?: Record; - - /** - * The add bid action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'AddBid' }) - addBid?: Record; - - /** - * The withdraw bid action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'WithdrawBid' }) - withdrawBid?: Record; - - /** - * The delegate action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'Delegate' }) - delegate?: Record; - - /** - * The undelegate action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'Undelegate' }) - undelegate?: Record; - - /** - * The redelegate action as a generic object. + * The type of transaction entry point, represented as an enum. */ - @jsonMember({ constructor: Object, name: 'Redelegate' }) - redelegate?: Record; + @jsonMember({ constructor: String }) + type: TransactionEntryPointEnum; /** - * The activate bid action as a generic object. + * Custom entry point identifier, used when the `type` is `Custom`. */ - @jsonMember({ constructor: Object, name: 'ActivateBid' }) - activateBid?: Record; + @jsonMember({ constructor: String }) + customEntryPoint?: string; /** - * The change bid public key action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'ChangeBidPublicKey' }) - changeBidPublicKey?: Record; - - /** - * The call action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'Call' }) - call?: Record; - - /** - * The call action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'AddReservations' }) - addReservations?: Record; - - /** - * The call action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'CancelReservations' }) - cancelReservations?: Record; - - /** - * Creates a new `TransactionEntryPoint` instance, where each parameter corresponds to a specific entry point action. + * Initializes a new `TransactionEntryPoint` instance. * - * @param custom A custom entry point action represented as a string. - * @param transfer The transfer action, represented as a generic object. - * @param addBid The add bid action, represented as a generic object. - * @param withdrawBid The withdraw bid action, represented as a generic object. - * @param delegate The delegate action, represented as a generic object. - * @param undelegate The undelegate action, represented as a generic object. - * @param redelegate The redelegate action, represented as a generic object. - * @param activateBid The activate bid action, represented as a generic object. - * @param changeBidPublicKey The change bid public key action, represented as a generic object. - * @param call The call action, represented as a generic object. + * @param type - The type of transaction entry point. + * @param customEntryPoint - An optional identifier for custom entry points. */ - constructor( - custom?: string, - transfer?: Record, - addBid?: Record, - withdrawBid?: Record, - delegate?: Record, - undelegate?: Record, - redelegate?: Record, - activateBid?: Record, - changeBidPublicKey?: Record, - call?: Record, - addReservations?: Record, - cancelReservations?: Record - ) { - this.custom = custom; - this.transfer = transfer; - this.addBid = addBid; - this.withdrawBid = withdrawBid; - this.delegate = delegate; - this.undelegate = undelegate; - this.redelegate = redelegate; - this.activateBid = activateBid; - this.changeBidPublicKey = changeBidPublicKey; - this.call = call; - this.addReservations = addReservations; - this.cancelReservations = cancelReservations; + constructor(type: TransactionEntryPointEnum, customEntryPoint?: string) { + if (type === TransactionEntryPointEnum.Custom && !customEntryPoint) { + throw new Error( + 'When specifying Custom entry point, customEntryPoint must be provided' + ); + } + this.type = type; + this.customEntryPoint = customEntryPoint; } /** - * Returns the tag corresponding to the transaction entry point. This helps identify the entry point in a compact manner. + * Retrieves the unique tag associated with the transaction entry point. + * Tags are used to identify entry points in a compact and efficient manner. * - * @returns The tag number associated with the entry point. + * @returns The tag number for the entry point. + * @throws An error if the entry point is unknown. */ - private tag(): number { - if (this.transfer) return TransactionEntryPointTag.Transfer; - if (this.addBid) return TransactionEntryPointTag.AddBid; - if (this.withdrawBid) return TransactionEntryPointTag.WithdrawBid; - if (this.delegate) return TransactionEntryPointTag.Delegate; - if (this.undelegate) return TransactionEntryPointTag.Undelegate; - if (this.redelegate) return TransactionEntryPointTag.Redelegate; - if (this.activateBid) return TransactionEntryPointTag.ActivateBid; - if (this.changeBidPublicKey) - return TransactionEntryPointTag.ChangeBidPublicKey; - if (this.call) return TransactionEntryPointTag.Call; - if (this.addReservations) return TransactionEntryPointTag.AddReservations; - if (this.cancelReservations) - return TransactionEntryPointTag.CancelReservations; - - throw new Error('Unknown TransactionEntryPointTag'); + public tag(): number { + switch (this.type) { + case TransactionEntryPointEnum.Transfer: + return TransactionEntryPointTag.Transfer; + case TransactionEntryPointEnum.AddBid: + return TransactionEntryPointTag.AddBid; + case TransactionEntryPointEnum.WithdrawBid: + return TransactionEntryPointTag.WithdrawBid; + case TransactionEntryPointEnum.Delegate: + return TransactionEntryPointTag.Delegate; + case TransactionEntryPointEnum.Undelegate: + return TransactionEntryPointTag.Undelegate; + case TransactionEntryPointEnum.Redelegate: + return TransactionEntryPointTag.Redelegate; + case TransactionEntryPointEnum.ActivateBid: + return TransactionEntryPointTag.ActivateBid; + case TransactionEntryPointEnum.ChangeBidPublicKey: + return TransactionEntryPointTag.ChangeBidPublicKey; + case TransactionEntryPointEnum.Call: + return TransactionEntryPointTag.Call; + case TransactionEntryPointEnum.AddReservations: + return TransactionEntryPointTag.AddReservations; + case TransactionEntryPointEnum.CancelReservations: + return TransactionEntryPointTag.CancelReservations; + case TransactionEntryPointEnum.Custom: + return TransactionEntryPointTag.Custom; + default: + throw new Error('Unknown TransactionEntryPointTag'); + } } /** @@ -188,98 +118,177 @@ export class TransactionEntryPoint { */ bytes(): Uint8Array { const calltableSerialization = new CalltableSerialization(); - calltableSerialization.addField(0, Uint8Array.of(this.tag())); + const tag = this.tag(); + calltableSerialization.addField(0, Uint8Array.from([tag])); - if (this.custom) { - const calltableSerialization = new CalltableSerialization(); - calltableSerialization.addField(0, Uint8Array.of(1)); - calltableSerialization.addField( + if ( + this.type === TransactionEntryPointEnum.Custom && + this.customEntryPoint + ) { + const customSerialization = new CalltableSerialization(); + customSerialization.addField(0, Uint8Array.from([1])); + customSerialization.addField( 1, - CLValueString.newCLString(this.custom).bytes() + CLValueString.newCLString(this.customEntryPoint).bytes() ); - return calltableSerialization.toBytes(); + calltableSerialization.addField(1, customSerialization.toBytes()); } + return calltableSerialization.toBytes(); } /** * Converts the transaction entry point to a JSON-compatible format. * - * @returns A JSON-compatible representation of the transaction entry point. - * @throws An error if the entry point is unknown. + * @returns A JSON object representing the transaction entry point. */ toJSON(): unknown { - if (this.custom) { - return { Custom: this.custom }; + if ( + this.type === TransactionEntryPointEnum.Custom && + this.customEntryPoint + ) { + return { Custom: this.customEntryPoint }; } - if (this.transfer) return TransactionEntryPointEnum.Transfer; - if (this.addBid) return TransactionEntryPointEnum.AddBid; - if (this.withdrawBid) return TransactionEntryPointEnum.WithdrawBid; - if (this.delegate) return TransactionEntryPointEnum.Delegate; - if (this.undelegate) return TransactionEntryPointEnum.Undelegate; - if (this.redelegate) return TransactionEntryPointEnum.Redelegate; - if (this.activateBid) return TransactionEntryPointEnum.ActivateBid; - if (this.changeBidPublicKey) - return TransactionEntryPointEnum.ChangeBidPublicKey; - if (this.call) return TransactionEntryPointEnum.Call; - - throw new Error('Unknown entry point'); + return this.type; } /** * Creates a `TransactionEntryPoint` instance from a JSON representation. * - * @param json The JSON representation of the entry point. + * @param json - The JSON representation of the transaction entry point. * @returns A `TransactionEntryPoint` instance. - * @throws An error if the entry point is unknown. + * @throws An error if the JSON is invalid or the entry point is unknown. */ static fromJSON(json: any): TransactionEntryPoint { - const entryPoint = new TransactionEntryPoint(); if (json instanceof Object && json.Custom) { - entryPoint.custom = json.Custom; - return entryPoint; + return new TransactionEntryPoint( + TransactionEntryPointEnum.Custom, + json.Custom + ); } switch (json) { case TransactionEntryPointEnum.Transfer: - entryPoint.transfer = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Transfer); case TransactionEntryPointEnum.AddBid: - entryPoint.addBid = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.AddBid); case TransactionEntryPointEnum.WithdrawBid: - entryPoint.withdrawBid = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.WithdrawBid); case TransactionEntryPointEnum.Delegate: - entryPoint.delegate = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Delegate); case TransactionEntryPointEnum.Undelegate: - entryPoint.undelegate = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Undelegate); case TransactionEntryPointEnum.Redelegate: - entryPoint.redelegate = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Redelegate); case TransactionEntryPointEnum.ActivateBid: - entryPoint.activateBid = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.ActivateBid); case TransactionEntryPointEnum.ChangeBidPublicKey: - entryPoint.changeBidPublicKey = {}; - break; + return new TransactionEntryPoint( + TransactionEntryPointEnum.ChangeBidPublicKey + ); case TransactionEntryPointEnum.Call: - entryPoint.call = {}; - break; - case TransactionEntryPointEnum.CancelReservations: - entryPoint.cancelReservations = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Call); case TransactionEntryPointEnum.AddReservations: - entryPoint.addReservations = {}; - break; + return new TransactionEntryPoint( + TransactionEntryPointEnum.AddReservations + ); + case TransactionEntryPointEnum.CancelReservations: + return new TransactionEntryPoint( + TransactionEntryPointEnum.CancelReservations + ); default: throw new Error('Unknown entry point'); } + } + + /** + * Deserializes a `TransactionEntryPoint` from its byte representation. + * + * This method takes a serialized byte array and reconstructs a `TransactionEntryPoint` object. + * It supports multiple entry point types, including both predefined and custom entry points. + * + * @param bytes - The byte array representing the serialized `TransactionEntryPoint`. + * @returns A deserialized `TransactionEntryPoint` instance. + * @throws Will throw an error if the byte array is invalid or has missing fields. + * + * ### Example + * ```typescript + * const serializedBytes = new Uint8Array([0, 1, 2, 3, ...]); + * const entryPoint = TransactionEntryPoint.fromBytes(serializedBytes); + * console.log(entryPoint.type); // Logs the entry point type + * ``` + */ + static fromBytes(bytes: Uint8Array): TransactionEntryPoint { + const calltableSerialization = CalltableSerialization.fromBytes(bytes); + const tagBytes = calltableSerialization.getField(0); + + if (!tagBytes || tagBytes.length !== 1) { + throw new Error('Invalid tag bytes'); + } + + const tag = tagBytes[0]; + + const type = (() => { + switch (tag) { + case TransactionEntryPointTag.Transfer: + return TransactionEntryPointEnum.Transfer; + case TransactionEntryPointTag.AddBid: + return TransactionEntryPointEnum.AddBid; + case TransactionEntryPointTag.WithdrawBid: + return TransactionEntryPointEnum.WithdrawBid; + case TransactionEntryPointTag.Delegate: + return TransactionEntryPointEnum.Delegate; + case TransactionEntryPointTag.Undelegate: + return TransactionEntryPointEnum.Undelegate; + case TransactionEntryPointTag.Redelegate: + return TransactionEntryPointEnum.Redelegate; + case TransactionEntryPointTag.ActivateBid: + return TransactionEntryPointEnum.ActivateBid; + case TransactionEntryPointTag.ChangeBidPublicKey: + return TransactionEntryPointEnum.ChangeBidPublicKey; + case TransactionEntryPointTag.Call: + return TransactionEntryPointEnum.Call; + case TransactionEntryPointTag.AddReservations: + return TransactionEntryPointEnum.AddReservations; + case TransactionEntryPointTag.CancelReservations: + return TransactionEntryPointEnum.CancelReservations; + case TransactionEntryPointTag.Custom: + return TransactionEntryPointEnum.Custom; + default: + throw new Error('Unknown tag'); + } + })(); + + if (type === TransactionEntryPointEnum.Custom) { + const customBytes = calltableSerialization.getField(1); + + if (!customBytes) { + throw new Error('Missing custom entry point bytes for Custom type'); + } + + const customSerialization = CalltableSerialization.fromBytes(customBytes); + + const customFlag = customSerialization.getField(0); + + if (!customFlag || customFlag[0] !== 1) { + throw new Error('Invalid flag for Custom type'); + } + + const customEntryPointBytes = customSerialization.getField(1); + + if (!customEntryPointBytes) { + throw new Error('Invalid custom entry point bytes'); + } + + const customEntryPoint = CLValueString.fromBytes( + customEntryPointBytes + ).result.toString(); + + return new TransactionEntryPoint(type, customEntryPoint); + } - return entryPoint; + return new TransactionEntryPoint(type); } } diff --git a/src/types/TransactionScheduling.ts b/src/types/TransactionScheduling.ts index b797032e0..7514d02da 100644 --- a/src/types/TransactionScheduling.ts +++ b/src/types/TransactionScheduling.ts @@ -3,7 +3,7 @@ import { jsonObject, jsonMember } from 'typedjson'; import { Timestamp } from './Time'; import { CLValueUInt64 } from './clvalue'; import { CalltableSerialization } from './CalltableSerialization'; -import { toBytesU64 } from './ByteConverters'; +import { fromBytesU64, toBytesU64 } from './ByteConverters'; /** * Enum representing the scheduling tags for transaction scheduling types. @@ -21,7 +21,7 @@ export enum TransactionSchedulingTag { * Represents the scheduling for a transaction in a future era. */ @jsonObject -class FutureEraScheduling { +export class FutureEraScheduling { /** * The ID of the future era when the transaction is scheduled to occur. */ @@ -53,7 +53,7 @@ class FutureEraScheduling { * Represents the scheduling for a transaction in a future timestamp. */ @jsonObject -class FutureTimestampScheduling { +export class FutureTimestampScheduling { /** * The timestamp when the transaction is scheduled to occur. */ @@ -209,4 +209,76 @@ export class TransactionScheduling { } throw new Error('Unknown scheduling type'); } + + /** + * Deserializes a `Uint8Array` into a `TransactionScheduling` instance. + * + * This method parses a byte array representation of a `TransactionScheduling` + * object, determines the type of scheduling based on the tag, and reconstructs + * the appropriate instance. + * + * @param bytes - The byte array to be deserialized. + * @returns A `TransactionScheduling` instance based on the serialized data. + * @throws Error - If the byte array is invalid, missing required fields, or contains + * an unrecognized scheduling tag. + * + * ### Tags and Their Associated Schedulers: + * - `TransactionSchedulingTag.Native`: Represents a native scheduling target. + * - `TransactionSchedulingTag.FutureEra`: Represents a scheduling target tied to a future era. + * - `TransactionSchedulingTag.FutureTimestamp`: Represents a scheduling target tied to a future timestamp. + * + * ### Example + * ```typescript + * const bytes = new Uint8Array([...]); // Provide valid TransactionScheduling bytes + * const scheduling = TransactionScheduling.fromBytes(bytes); + * console.log(scheduling); // Parsed TransactionScheduling instance + * ``` + */ + static fromBytes(bytes: Uint8Array): TransactionScheduling { + const calltable = CalltableSerialization.fromBytes(bytes); + + const tagBytes = calltable.getField(0); + if (!tagBytes || tagBytes.length !== 1) { + throw new Error( + 'Invalid or missing tag in serialized TransactionScheduling' + ); + } + const tag = tagBytes[0]; + + switch (tag) { + case TransactionSchedulingTag.Native: + return new TransactionScheduling({}); + + case TransactionSchedulingTag.FutureEra: { + const eraIDBytes = calltable.getField(1); + if (!eraIDBytes) { + throw new Error('Missing eraID field for FutureEra scheduling'); + } + const eraID = fromBytesU64(eraIDBytes).toNumber(); + return new TransactionScheduling( + undefined, + new FutureEraScheduling(eraID) + ); + } + + case TransactionSchedulingTag.FutureTimestamp: { + const timestampBytes = calltable.getField(1); + if (!timestampBytes) { + throw new Error( + 'Missing timestamp field for FutureTimestamp scheduling' + ); + } + const timestampMs = fromBytesU64(timestampBytes).toNumber(); + const timestamp = new Timestamp(new Date(timestampMs)); + return new TransactionScheduling( + undefined, + undefined, + new FutureTimestampScheduling(timestamp) + ); + } + + default: + throw new Error(`Unknown TransactionSchedulingTag: ${tag}`); + } + } } diff --git a/src/types/TransactionTarget.ts b/src/types/TransactionTarget.ts index f52563ccf..8a23e4e6b 100644 --- a/src/types/TransactionTarget.ts +++ b/src/types/TransactionTarget.ts @@ -19,6 +19,7 @@ import { byteArrayJsonDeserializer, byteArrayJsonSerializer } from './SerializationUtils'; +import { fromBytesU64 } from './ByteConverters'; /** * Represents the invocation target for a transaction identified by a package hash. @@ -173,6 +174,107 @@ export class TransactionInvocationTarget { 'Can not convert TransactionInvocationTarget to bytes. Missing values from initialization' ); } + + /** + * Deserializes a `Uint8Array` into a `TransactionInvocationTarget` instance. + * + * This method reconstructs a `TransactionInvocationTarget` object from its serialized byte array representation. + * The type of invocation target is determined by the tag extracted from the serialized data. + * + * @param bytes - The serialized byte array representing a `TransactionInvocationTarget`. + * @returns A deserialized `TransactionInvocationTarget` instance. + * @throws Error - If the byte array is invalid, missing required fields, or contains an unrecognized tag. + * + * ### Tags and Their Associated Targets: + * - `0`: Represents an invocation target identified by a hash (`ByHash`). + * - `1`: Represents an invocation target identified by a name (`ByName`). + * - `2`: Represents an invocation target identified by a package hash and an optional version (`ByPackageHash`). + * - `3`: Represents an invocation target identified by a package name and an optional version (`ByPackageName`). + * + * ### Example + * ```typescript + * const bytes = new Uint8Array([...]); // Provide valid TransactionInvocationTarget bytes + * const invocationTarget = TransactionInvocationTarget.fromBytes(bytes); + * console.log(invocationTarget); // Parsed TransactionInvocationTarget instance + * ``` + */ + static fromBytes(bytes: Uint8Array): TransactionInvocationTarget { + const calltable = CalltableSerialization.fromBytes(bytes); + + const tagBytes = calltable.getField(0); + if (!tagBytes || tagBytes.length !== 1) { + throw new Error( + 'Invalid or missing tag in serialized TransactionInvocationTarget' + ); + } + const tag = tagBytes[0]; + const invocationTarget = new TransactionInvocationTarget(); + + switch (tag) { + case 0: { + const hashBytes = calltable.getField(1); + if (!hashBytes) { + throw new Error('Missing hash field for ByHash target'); + } + invocationTarget.byHash = Hash.fromBytes(hashBytes).result; + return invocationTarget; + } + + case 1: { + const nameBytes = calltable.getField(1); + if (!nameBytes) { + throw new Error('Missing name field for ByName target'); + } + invocationTarget.byName = CLValueString.fromBytes( + nameBytes + ).result.toString(); + return invocationTarget; + } + + case 2: { + const packageHashBytes = calltable.getField(1); + const versionBytes = calltable.getField(2); + + if (!packageHashBytes || !versionBytes) { + throw new Error('Missing fields for ByPackageHash target'); + } + + const packageHash = Hash.fromBytes(packageHashBytes); + const version = CLValueOption.fromBytes( + versionBytes, + new CLTypeOption(CLTypeUInt32) + ).result.toString(); + const byPackageHash = new ByPackageHashInvocationTarget(); + byPackageHash.addr = packageHash.result; + byPackageHash.version = BigNumber.from(version).toNumber(); + invocationTarget.byPackageHash = byPackageHash; + return invocationTarget; + } + + case 3: { + const nameBytes = calltable.getField(1); + const versionBytes = calltable.getField(2); + + if (!nameBytes || !versionBytes) { + throw new Error('Missing fields for ByPackageName target'); + } + + const name = CLValueString.fromBytes(nameBytes).result.toString(); + const version = CLValueOption.fromBytes( + versionBytes, + new CLTypeOption(CLTypeUInt32) + ).result.toString(); + const byPackageName = new ByPackageNameInvocationTarget(); + byPackageName.version = BigNumber.from(version).toNumber(); + byPackageName.name = name; + invocationTarget.byPackageName = byPackageName; + return invocationTarget; + } + + default: + throw new Error(`Unknown TransactionInvocationTarget tag: ${tag}`); + } + } } /** @@ -500,4 +602,98 @@ export class TransactionTarget { return new TransactionTarget(); } + + /** + * Deserializes a `Uint8Array` into a `TransactionTarget` instance. + * + * This method reconstructs a `TransactionTarget` object from its serialized byte array representation. + * The type of transaction target is determined by the tag extracted from the serialized data. + * + * @param bytes - The serialized byte array representing a `TransactionTarget`. + * @returns A deserialized `TransactionTarget` instance. + * @throws Error - If the byte array is invalid, missing required fields, or contains an unrecognized tag. + * + * ### Tags and Their Associated Targets: + * - `0`: Represents a Native target. + * - `1`: Represents a Stored target, including an invocation target, runtime, and transferred value. + * - `2`: Represents a Session target, including module bytes, runtime, transferred value, install upgrade flag, and seed. + * + * ### Example + * ```typescript + * const bytes = new Uint8Array([...]); // Provide valid TransactionTarget bytes + * const transactionTarget = TransactionTarget.fromBytes(bytes); + * console.log(transactionTarget); // Parsed TransactionTarget instance + * ``` + */ + static fromBytes(bytes: Uint8Array): TransactionTarget { + const calltable = CalltableSerialization.fromBytes(bytes); + + const tagBytes = calltable.getField(0); + if (!tagBytes || tagBytes.length !== 1) { + throw new Error('Invalid or missing tag in serialized TransactionTarget'); + } + + const tag = tagBytes[0]; + switch (tag) { + case 0: + return new TransactionTarget({}); + + case 1: { + const storedBytes = calltable.getField(1); + const runtimeBytes = calltable.getField(2); + const transferredValueBytes = calltable.getField(3); + + if (!storedBytes || !runtimeBytes || !transferredValueBytes) { + throw new Error('Incomplete serialized data for Stored target'); + } + + const storedTarget = new StoredTarget(); + storedTarget.id = TransactionInvocationTarget.fromBytes(storedBytes); + storedTarget.runtime = CLValueString.fromBytes( + runtimeBytes + ).result.toString() as TransactionRuntime; + storedTarget.transferredValue = fromBytesU64( + transferredValueBytes + ).toNumber(); + + return new TransactionTarget(undefined, storedTarget); + } + + case 2: { + const moduleBytes = calltable.getField(3); + const runtimeBytesSession = calltable.getField(2); + const transferredValueBytesSession = calltable.getField(4); + const isInstallUpgradeBytes = calltable.getField(1); + const seedBytes = calltable.getField(5); + + if ( + !moduleBytes || + !runtimeBytesSession || + !transferredValueBytesSession || + !isInstallUpgradeBytes || + !seedBytes + ) { + throw new Error('Incomplete serialized data for Session target'); + } + + const sessionTarget = new SessionTarget(); + sessionTarget.moduleBytes = moduleBytes; + sessionTarget.runtime = CLValueString.fromBytes( + runtimeBytesSession + ).result.toString() as TransactionRuntime; + sessionTarget.transferredValue = fromBytesU64( + transferredValueBytesSession + ).toNumber(); + sessionTarget.isInstallUpgrade = CLValueBool.fromBytes( + isInstallUpgradeBytes + ).result.getValue(); + sessionTarget.seed = Hash.fromBytes(seedBytes).result; + + return new TransactionTarget(undefined, undefined, sessionTarget); + } + + default: + throw new Error(`Unknown TransactionTarget tag: ${tag}`); + } + } } diff --git a/src/types/TransactionPayload.ts b/src/types/TransactionV1Payload.ts similarity index 55% rename from src/types/TransactionPayload.ts rename to src/types/TransactionV1Payload.ts index 41c05a130..eacda320e 100644 --- a/src/types/TransactionPayload.ts +++ b/src/types/TransactionV1Payload.ts @@ -5,7 +5,7 @@ import { toBytesU32, toBytesU64 } from './ByteConverters'; -import { jsonMember, jsonObject } from 'typedjson'; +import { jsonMember, jsonObject, TypedJSON } from 'typedjson'; import { InitiatorAddr } from './InitiatorAddr'; import { Duration, Timestamp } from './Time'; import { PricingMode } from './PricingMode'; @@ -19,6 +19,9 @@ import { byteArrayJsonSerializer } from './SerializationUtils'; +/** + * Interface representing the parameters required to build a `TransactionV1Payload`. + */ interface ITransactionPayloadBuildParams { initiatorAddr: InitiatorAddr; args: Args; @@ -32,24 +35,49 @@ interface ITransactionPayloadBuildParams { chainName: string; } +/** + * Class representing a collection of payload fields used in transaction serialization. + */ export class PayloadFields { + /** + * Map storing the fields of the payload where the key is the field identifier and the value is the serialized data. + */ public fields: Map = new Map(); + /** + * Adds a field to the payload. + * + * @param field - The identifier of the field. + * @param value - The serialized value of the field. + */ addField(field: number, value: Uint8Array): void { this.fields.set(field, value); } + getFieldValue(fieldIndex: number) { + return this.fields.get(fieldIndex); + } + + /** + * Serializes the payload fields into a `Uint8Array`. + * + * @returns A `Uint8Array` containing the serialized payload fields. + */ toBytes(): Uint8Array { const fieldsCount = toBytesU32(this.fields.size); - const fieldEntries = Array.from(this.fields.entries()).map( - ([key, value]) => { - return concat([toBytesU16(key), value]); - } + const fieldEntries = Array.from(this.fields.entries()).map(([key, value]) => + concat([toBytesU16(key), value]) ); return concat([fieldsCount, ...fieldEntries]); } + /** + * Deserializes a JSON object into a `PayloadFields` instance. + * + * @param json - The JSON representation of the payload fields. + * @returns A `PayloadFields` instance. + */ static fromJSON(json: Record): PayloadFields { const payload = new PayloadFields(); for (const [key, value] of Object.entries(json)) { @@ -61,6 +89,11 @@ export class PayloadFields { return payload; } + /** + * Converts the payload fields to a JSON object. + * + * @returns A JSON representation of the payload fields. + */ toJSON(): Record { const result: Record = {}; const fieldEntries = Array.from(this.fields.entries()); @@ -71,6 +104,9 @@ export class PayloadFields { } } +/** + * Class representing the payload for a V1 transaction. + */ @jsonObject export class TransactionV1Payload { /** @@ -113,33 +149,51 @@ export class TransactionV1Payload { public pricingMode: PricingMode; /** - * The name of the blockchain. + * The name of the blockchain on which the transaction is executed. */ @jsonMember({ name: 'chain_name', constructor: String }) public chainName: string; /** - * The name of the blockchain. + * Additional serialized fields associated with the transaction. */ @jsonMember({ name: 'fields', - serializer: value => { - if (!value) return; - return value.toJSON(); - }, - deserializer: json => { - if (!json) return; - return PayloadFields.fromJSON(json); - } + serializer: value => (value ? value.toJSON() : undefined), + deserializer: json => (json ? PayloadFields.fromJSON(json) : undefined) }) public fields: PayloadFields; + /** + * Arguments associated with the transaction. + */ public args: Args; + + /** + * The target of the transaction. + */ public target: TransactionTarget; + + /** + * The entry point of the transaction. + */ public entryPoint: TransactionEntryPoint; + + /** + * The scheduling information for the transaction. + */ public scheduling: TransactionScheduling; + + /** + * Optional category of the transaction. + */ public category?: number; + /** + * Serializes the transaction payload into a `Uint8Array`. + * + * @returns A `Uint8Array` representing the serialized transaction payload. + */ public toBytes(): Uint8Array { const calltableSerialization = new CalltableSerialization(); const fields = new PayloadFields(); @@ -161,6 +215,12 @@ export class TransactionV1Payload { return calltableSerialization.toBytes(); } + /** + * Creates a `TransactionV1Payload` instance from the provided parameters. + * + * @param params - The parameters for building the transaction payload. + * @returns A new `TransactionV1Payload` instance. + */ public static build({ initiatorAddr, args, @@ -194,4 +254,61 @@ export class TransactionV1Payload { return transactionPayload; } + + /** + * Deserializes a JSON object into a `TransactionV1Payload` instance. + * + * This method parses a JSON object to create a `TransactionV1Payload` instance. + * Additionally, it deserializes nested fields such as `args`, `target`, `entryPoint`, + * and `scheduling` from their respective byte representations if they are present. + * + * @param json - The JSON object representing a serialized `TransactionV1Payload`. + * @returns A deserialized `TransactionV1Payload` instance, or `undefined` if parsing fails. + * + * ### Example + * ```typescript + * const json = { + * fields: { + * // Provide serialized fields in JSON format + * } + * }; + * const transactionPayload = TransactionV1Payload.fromJSON(json); + * console.log(transactionPayload); // Parsed TransactionV1Payload instance or undefined + * ``` + */ + public static fromJSON(json: any): TransactionV1Payload | undefined { + const serializer = new TypedJSON(TransactionV1Payload); + const transactionPayload = serializer.parse(json); + + if (!transactionPayload) { + return undefined; + } + + const argsBytes = transactionPayload.fields.getFieldValue(0); + const targetBytes = transactionPayload.fields.getFieldValue(1); + const entryPointBytes = transactionPayload.fields.getFieldValue(2); + const schedulingBytes = transactionPayload.fields.getFieldValue(3); + + if (argsBytes) { + transactionPayload.args = Args.fromBytes(argsBytes); + } + + if (targetBytes) { + transactionPayload.target = TransactionTarget.fromBytes(targetBytes); + } + + if (entryPointBytes) { + transactionPayload.entryPoint = TransactionEntryPoint.fromBytes( + entryPointBytes + ); + } + + if (schedulingBytes) { + transactionPayload.scheduling = TransactionScheduling.fromBytes( + schedulingBytes + ); + } + + return transactionPayload; + } } diff --git a/src/types/clvalue/Parser.ts b/src/types/clvalue/Parser.ts index 59785aa46..98e877d82 100644 --- a/src/types/clvalue/Parser.ts +++ b/src/types/clvalue/Parser.ts @@ -228,4 +228,37 @@ export class CLValueParser { throw ErrUnsupportedCLType; } } + + /** + * Parses a `Uint8Array` to extract a `CLValue` with its corresponding type. + * + * This method takes a byte array and interprets it as a `CLValue` by first extracting + * the length of the value, then splitting the bytes into the value's data and its type. + * + * @param bytes - The byte array to be parsed. + * @returns An `IResultWithBytes` containing the parsed `CLValue` and its remaining bytes. + * @throws Error - If the length of the value extracted from the bytes is invalid. + * + * ### Example + * ```typescript + * const bytes = new Uint8Array([...]); // Provide valid CLValue bytes + * const result = CLValueParser.fromBytesWithType(bytes); + * console.log(result.result); // Parsed CLValue + * ``` + */ + public static fromBytesWithType( + bytes: Uint8Array + ): IResultWithBytes { + const u32 = CLValueUInt32.fromBytes(bytes); + const length = u32.result.getValue().toNumber(); + + if (!length) { + throw new Error(`Invalid length for bytes: ${length}`); + } + + const valueBytes = u32.bytes.subarray(0, length); + const typeBytes = u32.bytes.subarray(length); + const clType = CLTypeParser.matchBytesToCLType(typeBytes); + return this.fromBytesByType(valueBytes, clType.result); + } } diff --git a/src/types/index.ts b/src/types/index.ts index 0df2c5f2f..ba93f653d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,7 +23,7 @@ export * from './MinimalBlockInfo'; export * from './NamedKey'; export * from './Package'; export * from './PricingMode'; -export * from './Reservation'; +export * from './Prepayment'; export * from './StoredValue'; export * from './Time'; export * from './Transaction'; From 6b74c4001f5892ac565d068f2c3c3e28cee3d828 Mon Sep 17 00:00:00 2001 From: Oleksandr Myshchyshyn Date: Sun, 24 Nov 2024 12:31:59 +0200 Subject: [PATCH 4/8] Update serialization and deserialization due to node changes --- src/types/Transaction.test.ts | 43 ++-- src/types/Transaction.ts | 26 +-- src/types/TransactionV1Payload.ts | 363 +++++++++++++++++------------- src/types/index.ts | 1 + 4 files changed, 242 insertions(+), 191 deletions(-) diff --git a/src/types/Transaction.test.ts b/src/types/Transaction.test.ts index abe69ac9a..b1a4bf329 100644 --- a/src/types/Transaction.test.ts +++ b/src/types/Transaction.test.ts @@ -1,13 +1,11 @@ import { BigNumber } from '@ethersproject/bignumber'; +import { assert, expect } from 'chai'; import { Duration, Timestamp } from './Time'; import { TransactionV1 } from './Transaction'; import { InitiatorAddr } from './InitiatorAddr'; -import { PrivateKey } from './keypair/PrivateKey'; import { FixedMode, PricingMode } from './PricingMode'; - -import { KeyAlgorithm } from './keypair/Algorithm'; - +import { KeyAlgorithm, PrivateKey, PublicKey } from './keypair'; import { SessionTarget, TransactionTarget } from './TransactionTarget'; import { TransactionEntryPoint, @@ -21,13 +19,11 @@ import { CLValueUInt512, CLValueUInt64 } from './clvalue'; -import { PublicKey } from './keypair'; import { TransactionV1Payload } from './TransactionV1Payload'; import { Hash } from './key'; -import { assert, expect } from 'chai'; describe('Test Transaction', () => { - it('should create a Transaction from TransactionV1', async () => { + it('should create a TransactionV1 with correct payload instance', async () => { const keys = await PrivateKey.generate(KeyAlgorithm.ED25519); const paymentAmount = 20000000000000; @@ -57,19 +53,24 @@ describe('Test Transaction', () => { '8bf9d406ab901428d43ecd3a6f214b864e7ef8316934e5e0f049650a65b40d73' ); + const transactionTarget = new TransactionTarget( + undefined, + undefined, + sessionTarget + ); + const scheduling = new TransactionScheduling({}); + const entryPoint = new TransactionEntryPoint( + TransactionEntryPointEnum.Call + ); + const transactionPayload = TransactionV1Payload.build({ initiatorAddr: new InitiatorAddr(keys.publicKey), ttl: new Duration(1800000), args, timestamp: new Timestamp(new Date()), - category: 2, - entryPoint: new TransactionEntryPoint(TransactionEntryPointEnum.Call), - scheduling: new TransactionScheduling({}), - transactionTarget: new TransactionTarget( - undefined, - undefined, - sessionTarget - ), + entryPoint, + scheduling, + transactionTarget, chainName: 'casper-net-1', pricingMode }); @@ -77,15 +78,17 @@ describe('Test Transaction', () => { const transaction = TransactionV1.makeTransactionV1(transactionPayload); await transaction.sign(keys); - const toJson = TransactionV1.toJson(transaction); - const parsed = TransactionV1.fromJSON(toJson); - - const transactionPaymentAmount = parsed.payload.args.args + const transactionPaymentAmount = transaction.payload.fields.args.args .get('amount')! .toString(); - assert.deepEqual(parsed.approvals[0].signer, keys.publicKey); + assert.deepEqual(transaction.approvals[0].signer, keys.publicKey); expect(transaction.payload).to.deep.equal(transactionPayload); assert.deepEqual(parseInt(transactionPaymentAmount, 10), paymentAmount); + expect(transaction.payload.chainName).to.deep.equal('casper-net-1'); + expect(transaction.payload.fields.target).to.deep.equal(transactionTarget); + expect(transaction.payload.fields.args).to.deep.equal(args); + expect(transaction.payload.fields.scheduling).to.deep.equal(scheduling); + expect(transaction.payload.fields.entryPoint).to.deep.equal(entryPoint); }); }); diff --git a/src/types/Transaction.ts b/src/types/Transaction.ts index e3d1cb5ef..26a6c984a 100644 --- a/src/types/Transaction.ts +++ b/src/types/Transaction.ts @@ -117,15 +117,12 @@ export class TransactionV1 { public hash: Hash; /** - * The header of the transaction. + * The payload of the transaction. + * A merge of header and body concepts from before. */ @jsonMember({ name: 'payload', - constructor: TransactionV1Payload, - deserializer: json => { - if (!json) return; - return TransactionV1Payload.fromJSON(json); - } + constructor: TransactionV1Payload }) public payload: TransactionV1Payload; @@ -146,7 +143,7 @@ export class TransactionV1 { } /** - * Validates the transaction by checking the body hash and the approval signatures. + * Validates the transaction by checking the transaction hash and the approval signatures. * @throws {TransactionError} Throws errors if validation fails. */ public validate(): boolean { @@ -205,8 +202,7 @@ export class TransactionV1 { /** * Creates a new `TransactionV1` instance. * @param hash The hash of the transaction. - * @param header The header of the transaction. - * @param body The body of the transaction. + * @param payload The payload of the transaction. A merge of header and body concepts from before. * @param approvals The approvals for the transaction. * @returns A new `TransactionV1` instance. */ @@ -220,8 +216,7 @@ export class TransactionV1 { /** * Creates a new `TransactionV1` instance with a header and body. - * @param transactionHeader The header of the transaction. - * @param transactionBody The body of the transaction. + * @param payload The payload of the transaction. A merge of header and body concepts from before. * @returns A new `TransactionV1` instance. */ static makeTransactionV1(payload: TransactionV1Payload): TransactionV1 { @@ -531,11 +526,10 @@ export class Transaction { v1.payload.pricingMode ), new TransactionBody( - v1.payload.args, - v1.payload.target, - v1.payload.entryPoint, - v1.payload.scheduling, - v1.payload.category + v1.payload.fields.args, + v1.payload.fields.target, + v1.payload.fields.entryPoint, + v1.payload.fields.scheduling ), v1.approvals, v1 diff --git a/src/types/TransactionV1Payload.ts b/src/types/TransactionV1Payload.ts index eacda320e..91bd7139d 100644 --- a/src/types/TransactionV1Payload.ts +++ b/src/types/TransactionV1Payload.ts @@ -15,78 +15,226 @@ import { TransactionEntryPoint } from './TransactionEntryPoint'; import { TransactionScheduling } from './TransactionScheduling'; import { CalltableSerialization } from './CalltableSerialization'; import { - byteArrayJsonDeserializer, - byteArrayJsonSerializer + byteArrayJsonSerializer, + deserializeArgs, + serializeArgs } from './SerializationUtils'; /** * Interface representing the parameters required to build a `TransactionV1Payload`. + * Contains all necessary data to construct a valid V1 transaction payload. */ interface ITransactionPayloadBuildParams { + /** + * The address of the transaction initiator. + */ initiatorAddr: InitiatorAddr; + + /** + * Arguments for the transaction. + */ args: Args; + + /** + * The time-to-live (TTL) duration of the transaction. + */ ttl: Duration; + + /** + * Entry point for the transaction execution. + */ entryPoint: TransactionEntryPoint; + + /** + * Pricing mode for the transaction. + */ pricingMode: PricingMode; + + /** + * Timestamp indicating when the transaction was created. + */ timestamp: Timestamp; - category?: number; + + /** + * Target destination of the transaction. + */ transactionTarget: TransactionTarget; + + /** + * Scheduling details for the transaction. + */ scheduling: TransactionScheduling; + + /** + * Name of the chain the transaction should be executed on. + */ chainName: string; } /** - * Class representing a collection of payload fields used in transaction serialization. + * Class representing a collection of fields used in transaction serialization. + * This class handles serialization and deserialization of transaction data fields. */ +@jsonObject export class PayloadFields { /** - * Map storing the fields of the payload where the key is the field identifier and the value is the serialized data. + * Arguments for the transaction. + */ + @jsonMember(() => Args, { + deserializer: deserializeArgs, + serializer: serializeArgs + }) + public args: Args; + + /** + * Target destination of the transaction. + */ + @jsonMember({ + name: 'target', + constructor: TransactionTarget, + deserializer: json => TransactionTarget.fromJSON(json), + serializer: value => value.toJSON() + }) + public target: TransactionTarget; + + /** + * Entry point for the transaction execution. + */ + @jsonMember({ + name: 'entry_point', + constructor: TransactionEntryPoint, + deserializer: json => TransactionEntryPoint.fromJSON(json), + serializer: value => value.toJSON() + }) + public entryPoint: TransactionEntryPoint; + + /** + * Scheduling details for the transaction execution. + */ + @jsonMember({ + name: 'scheduling', + constructor: TransactionScheduling, + deserializer: json => TransactionScheduling.fromJSON(json), + serializer: value => value.toJSON() + }) + public scheduling: TransactionScheduling; + + /** + * Internal map to store serialized fields, where the key is the field identifier. + */ + private fields: Map = new Map(); + + /** + * Utility method to map field identifiers to serialized values. + * Ensures that all fields are properly initialized before serialization. + * @returns A map of field identifiers to their serialized values. + * @throws Error if any required field is uninitialized or invalid. + */ + private toSerializedFields(): Map { + if (!this.args) throw new Error('args field is uninitialized.'); + if (!this.target) throw new Error('target field is uninitialized.'); + if (!this.entryPoint) throw new Error('entryPoint field is uninitialized.'); + if (!this.scheduling) throw new Error('scheduling field is uninitialized.'); + + return new Map([ + [0, this.args.toBytes()], + [1, this.target.toBytes()], + [2, this.entryPoint.bytes()], + [3, this.scheduling.bytes()] + ]); + } + + /** + * Builds a `PayloadFields` instance from provided transaction details. + * + * @param args - Transaction arguments. + * @param transactionTarget - Transaction target. + * @param transactionEntryPoint - Transaction entry point. + * @param transactionScheduling - Scheduling information for the transaction. + * @returns A new `PayloadFields` instance. + * @throws Error if any of the required parameters are missing or invalid. */ - public fields: Map = new Map(); + public static build( + args: Args, + transactionTarget: TransactionTarget, + transactionEntryPoint: TransactionEntryPoint, + transactionScheduling: TransactionScheduling + ): PayloadFields { + const missingFields = []; + if (!args) missingFields.push('args'); + if (!transactionTarget) missingFields.push('transactionTarget'); + if (!transactionEntryPoint) missingFields.push('transactionEntryPoint'); + if (!transactionScheduling) missingFields.push('transactionScheduling'); + + if (missingFields.length > 0) { + throw new Error( + `Failed to build PayloadFields: Missing or invalid fields: ${missingFields.join( + ', ' + )}.` + ); + } + + const payloadFields = new PayloadFields(); + payloadFields.args = args; + payloadFields.target = transactionTarget; + payloadFields.entryPoint = transactionEntryPoint; + payloadFields.scheduling = transactionScheduling; + + payloadFields.fields = payloadFields.toSerializedFields(); + + return payloadFields; + } /** - * Adds a field to the payload. + * Adds a serialized field to the payload. * - * @param field - The identifier of the field. - * @param value - The serialized value of the field. + * @param field - Field identifier. + * @param value - Serialized value of the field. */ - addField(field: number, value: Uint8Array): void { + public addField(field: number, value: Uint8Array): void { this.fields.set(field, value); } - getFieldValue(fieldIndex: number) { + /** + * Retrieves the value of a specific field. + * + * @param fieldIndex - Identifier of the field. + * @returns Serialized value of the field. + */ + public getFieldValue(fieldIndex: number): Uint8Array | undefined { return this.fields.get(fieldIndex); } /** - * Serializes the payload fields into a `Uint8Array`. + * Serializes all fields into a `Uint8Array`. * - * @returns A `Uint8Array` containing the serialized payload fields. + * @returns Serialized fields as a `Uint8Array`. */ - toBytes(): Uint8Array { + public toBytes(): Uint8Array { const fieldsCount = toBytesU32(this.fields.size); - const fieldEntries = Array.from(this.fields.entries()).map(([key, value]) => - concat([toBytesU16(key), value]) - ); + const serializedFields = Array.from( + this.fields.entries() + ).map(([key, value]) => concat([toBytesU16(key), value])); - return concat([fieldsCount, ...fieldEntries]); + return concat([fieldsCount, ...serializedFields]); } /** - * Deserializes a JSON object into a `PayloadFields` instance. + * Deserializes JSON data into a `PayloadFields` instance. * - * @param json - The JSON representation of the payload fields. + * @param json - JSON representation of the payload fields. * @returns A `PayloadFields` instance. */ - static fromJSON(json: Record): PayloadFields { - const payload = new PayloadFields(); - for (const [key, value] of Object.entries(json)) { - const field = parseInt(key); - if (!isNaN(field)) { - payload.addField(field, byteArrayJsonDeserializer(value)); - } + public static fromJSON(json: any): PayloadFields { + const deserialized = new TypedJSON(PayloadFields).parse(json); + + if (!deserialized) { + throw new Error('Failed to deserialize PayloadFields.'); } - return payload; + + deserialized.fields = deserialized.toSerializedFields(); + + return deserialized; } /** @@ -94,7 +242,7 @@ export class PayloadFields { * * @returns A JSON representation of the payload fields. */ - toJSON(): Record { + public toJSON(): Record { const result: Record = {}; const fieldEntries = Array.from(this.fields.entries()); for (const [key, value] of fieldEntries) { @@ -110,7 +258,7 @@ export class PayloadFields { @jsonObject export class TransactionV1Payload { /** - * The address of the transaction initiator. + * Address of the transaction initiator. */ @jsonMember({ name: 'initiator_addr', @@ -121,7 +269,7 @@ export class TransactionV1Payload { public initiatorAddr: InitiatorAddr; /** - * The timestamp of the transaction. + * Timestamp when the transaction was created. */ @jsonMember({ name: 'timestamp', @@ -132,7 +280,7 @@ export class TransactionV1Payload { public timestamp: Timestamp; /** - * The time-to-live (TTL) duration of the transaction. + * Time-to-live (TTL) duration of the transaction. */ @jsonMember({ name: 'ttl', @@ -143,19 +291,19 @@ export class TransactionV1Payload { public ttl: Duration; /** - * The pricing mode used for the transaction. + * Pricing mode used for the transaction. */ @jsonMember({ name: 'pricing_mode', constructor: PricingMode }) public pricingMode: PricingMode; /** - * The name of the blockchain on which the transaction is executed. + * Name of the chain the transaction should be executed on. */ @jsonMember({ name: 'chain_name', constructor: String }) public chainName: string; /** - * Additional serialized fields associated with the transaction. + * Serialized fields associated with the transaction. */ @jsonMember({ name: 'fields', @@ -164,61 +312,28 @@ export class TransactionV1Payload { }) public fields: PayloadFields; - /** - * Arguments associated with the transaction. - */ - public args: Args; - - /** - * The target of the transaction. - */ - public target: TransactionTarget; - - /** - * The entry point of the transaction. - */ - public entryPoint: TransactionEntryPoint; - - /** - * The scheduling information for the transaction. - */ - public scheduling: TransactionScheduling; - - /** - * Optional category of the transaction. - */ - public category?: number; - /** * Serializes the transaction payload into a `Uint8Array`. * * @returns A `Uint8Array` representing the serialized transaction payload. */ public toBytes(): Uint8Array { - const calltableSerialization = new CalltableSerialization(); - const fields = new PayloadFields(); - fields.addField(0, this.args.toBytes()); - fields.addField(1, this.target.toBytes()); - fields.addField(2, this.entryPoint.bytes()); - fields.addField(3, this.scheduling.bytes()); - - calltableSerialization.addField(0, this.initiatorAddr.toBytes()); - calltableSerialization.addField( - 1, - toBytesU64(Date.parse(this.timestamp.toJSON())) - ); - calltableSerialization.addField(2, toBytesU64(this.ttl.duration)); - calltableSerialization.addField(3, toBytesString(this.chainName)); - calltableSerialization.addField(4, this.pricingMode.toBytes()); - calltableSerialization.addField(5, fields.toBytes()); + const calltable = new CalltableSerialization(); - return calltableSerialization.toBytes(); + calltable.addField(0, this.initiatorAddr.toBytes()); + calltable.addField(1, toBytesU64(Date.parse(this.timestamp.toJSON()))); + calltable.addField(2, toBytesU64(this.ttl.duration)); + calltable.addField(3, toBytesString(this.chainName)); + calltable.addField(4, this.pricingMode.toBytes()); + calltable.addField(5, this.fields.toBytes()); + + return calltable.toBytes(); } /** - * Creates a `TransactionV1Payload` instance from the provided parameters. + * Constructs a `TransactionV1Payload` instance with specified parameters. * - * @param params - The parameters for building the transaction payload. + * @param params - Parameters for building the transaction payload. * @returns A new `TransactionV1Payload` instance. */ public static build({ @@ -228,87 +343,25 @@ export class TransactionV1Payload { entryPoint, pricingMode, timestamp, - category, transactionTarget, scheduling, chainName }: ITransactionPayloadBuildParams): TransactionV1Payload { - const payloadFields = new PayloadFields(); - payloadFields.addField(0, args.toBytes()); - payloadFields.addField(1, transactionTarget.toBytes()); - payloadFields.addField(2, entryPoint.bytes()); - payloadFields.addField(3, scheduling.bytes()); - - const transactionPayload = new TransactionV1Payload(); - transactionPayload.initiatorAddr = initiatorAddr; - transactionPayload.ttl = ttl; - transactionPayload.args = args; - transactionPayload.entryPoint = entryPoint; - transactionPayload.pricingMode = pricingMode; - transactionPayload.timestamp = timestamp; - transactionPayload.category = category; - transactionPayload.target = transactionTarget; - transactionPayload.scheduling = scheduling; - transactionPayload.chainName = chainName; - transactionPayload.fields = payloadFields; - - return transactionPayload; - } - - /** - * Deserializes a JSON object into a `TransactionV1Payload` instance. - * - * This method parses a JSON object to create a `TransactionV1Payload` instance. - * Additionally, it deserializes nested fields such as `args`, `target`, `entryPoint`, - * and `scheduling` from their respective byte representations if they are present. - * - * @param json - The JSON object representing a serialized `TransactionV1Payload`. - * @returns A deserialized `TransactionV1Payload` instance, or `undefined` if parsing fails. - * - * ### Example - * ```typescript - * const json = { - * fields: { - * // Provide serialized fields in JSON format - * } - * }; - * const transactionPayload = TransactionV1Payload.fromJSON(json); - * console.log(transactionPayload); // Parsed TransactionV1Payload instance or undefined - * ``` - */ - public static fromJSON(json: any): TransactionV1Payload | undefined { - const serializer = new TypedJSON(TransactionV1Payload); - const transactionPayload = serializer.parse(json); - - if (!transactionPayload) { - return undefined; - } - - const argsBytes = transactionPayload.fields.getFieldValue(0); - const targetBytes = transactionPayload.fields.getFieldValue(1); - const entryPointBytes = transactionPayload.fields.getFieldValue(2); - const schedulingBytes = transactionPayload.fields.getFieldValue(3); - - if (argsBytes) { - transactionPayload.args = Args.fromBytes(argsBytes); - } - - if (targetBytes) { - transactionPayload.target = TransactionTarget.fromBytes(targetBytes); - } + const payloadFields = PayloadFields.build( + args, + transactionTarget, + entryPoint, + scheduling + ); - if (entryPointBytes) { - transactionPayload.entryPoint = TransactionEntryPoint.fromBytes( - entryPointBytes - ); - } + const payload = new TransactionV1Payload(); + payload.initiatorAddr = initiatorAddr; + payload.ttl = ttl; + payload.pricingMode = pricingMode; + payload.timestamp = timestamp; + payload.chainName = chainName; + payload.fields = payloadFields; - if (schedulingBytes) { - transactionPayload.scheduling = TransactionScheduling.fromBytes( - schedulingBytes - ); - } - - return transactionPayload; + return payload; } } diff --git a/src/types/index.ts b/src/types/index.ts index ba93f653d..e74192079 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,6 +27,7 @@ export * from './Prepayment'; export * from './StoredValue'; export * from './Time'; export * from './Transaction'; +export * from './TransactionV1Payload'; export * from './TransactionEntryPoint'; export * from './TransactionScheduling'; export * from './TransactionTarget'; From 6e7946d9b30870bf98f9da3dba6911b7f07a4ad8 Mon Sep 17 00:00:00 2001 From: Oleksandr Myshchyshyn Date: Sun, 24 Nov 2024 12:48:39 +0200 Subject: [PATCH 5/8] Update README.md for transaction creation --- README.md | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 6958f9efb..862165185 100644 --- a/README.md +++ b/README.md @@ -126,15 +126,14 @@ import { PrivateKey, PublicKey, RpcClient, - SessionTarget, Timestamp, TransactionEntryPoint, TransactionScheduling, TransactionTarget, TransactionV1, - TransactionV1Body, - TransactionV1Header -} from 'casper-js-sdk-new'; + TransactionV1Payload, + TransactionEntryPointEnum +} from 'casper-js-sdk'; const rpcHandler = new HttpHandler('http://:7777/rpc'); const rpcClient = new RpcClient(rpcHandler); @@ -146,16 +145,9 @@ const paymentAmount = '20000000000000'; const pricingMode = new PricingMode(); const fixedMode = new FixedMode(); fixedMode.gasPriceTolerance = 3; +fixedMode.additionalComputationFactor = 1; pricingMode.fixed = fixedMode; -const transactionHeader = TransactionV1Header.build({ - chainName: 'casper-net-1', - timestamp, - ttl: new Duration(1800000), - initiatorAddr: new InitiatorAddr(privateKey.publicKey), - pricingMode -}); - const args = Args.fromMap({ target: CLValue.newCLPublicKey( PublicKey.fromHex( @@ -166,21 +158,26 @@ const args = Args.fromMap({ id: CLValueOption.newCLOption(CLValueUInt64.newCLUint64(3)) }); -const transactionTarget = new TransactionTarget(new SessionTarget()); -const entryPoint = new TransactionEntryPoint(undefined, {}); -const scheduling = new TransactionScheduling({}); +const transactionTarget = new TransactionTarget({}); // Native target; +const entryPoint = new TransactionEntryPoint( + TransactionEntryPointEnum.Transfer +); +const scheduling = new TransactionScheduling({}); // Standard; -const transactionBody = TransactionV1Body.build({ - args: args, - target: transactionTarget, - transactionEntryPoint: entryPoint, - transactionScheduling: scheduling, - transactionCategory: 2 +const transactionPayload = TransactionV1Payload.build({ + initiatorAddr: new InitiatorAddr(privateKey.publicKey), + ttl: new Duration(1800000), + args, + timestamp, + entryPoint, + scheduling, + transactionTarget, + chainName: 'casper-net-1', + pricingMode }); const transaction = TransactionV1.makeTransactionV1( - transactionHeader, - transactionBody + transactionPayload ); await transaction.sign(privateKey); From d1a3f8ca363963175a671d3f9a64ea46dec5527d Mon Sep 17 00:00:00 2001 From: Oleksandr Myshchyshyn Date: Sun, 24 Nov 2024 18:03:20 +0200 Subject: [PATCH 6/8] Renamed variables due to updated PricingMode --- src/types/Deploy.ts | 8 ++++---- src/types/PricingMode.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/types/Deploy.ts b/src/types/Deploy.ts index 006ac4142..dd3748e73 100644 --- a/src/types/Deploy.ts +++ b/src/types/Deploy.ts @@ -403,10 +403,10 @@ export class Deploy { const standardPayment = paymentAmount === 0 && !deploy.payment.moduleBytes; const pricingMode = new PricingMode(); - const classicMode = new PaymentLimitedMode(); - classicMode.gasPriceTolerance = 1; - classicMode.paymentAmount = paymentAmount; - classicMode.standardPayment = standardPayment; + const paymentLimitedMode = new PaymentLimitedMode(); + paymentLimitedMode.gasPriceTolerance = 1; + paymentLimitedMode.paymentAmount = paymentAmount; + paymentLimitedMode.standardPayment = standardPayment; return new Transaction( deploy.hash, diff --git a/src/types/PricingMode.ts b/src/types/PricingMode.ts index b23555ecf..e27eed321 100644 --- a/src/types/PricingMode.ts +++ b/src/types/PricingMode.ts @@ -4,7 +4,7 @@ import { CLValueBool, CLValueUInt64, CLValueUInt8 } from './clvalue'; import { CalltableSerialization } from './CalltableSerialization'; /** - * Represents the classic pricing mode, including parameters for gas price tolerance, + * Represents the payment limited ( classic before ) pricing mode, including parameters for gas price tolerance, * payment amount, and standard payment. */ @jsonObject @@ -90,7 +90,7 @@ export class FixedMode { } /** - * Represents the reserved pricing mode, which includes a receipt hash. + * Represents the prepair ( reserved before ) pricing mode, which includes a receipt hash. */ @jsonObject export class PrepaidMode { @@ -115,12 +115,12 @@ export class PrepaidMode { } /** - * Represents the pricing mode, which can be one of the following: Classic, Fixed, or Reserved. + * Represents the pricing mode, which can be one of the following: PaymentLimited, Fixed, or Prepaid. */ @jsonObject export class PricingMode { /** - * The classic pricing mode, if applicable. + * The PaymentLimited pricing mode, if applicable. */ @jsonMember({ name: 'PaymentLimited', constructor: PaymentLimitedMode }) paymentLimited?: PaymentLimitedMode; @@ -132,7 +132,7 @@ export class PricingMode { fixed?: FixedMode; /** - * The reserved pricing mode, if applicable. + * The Prepaid pricing mode, if applicable. */ @jsonMember({ name: 'Prepaid', constructor: PrepaidMode }) prepaid?: PrepaidMode; From 01b8942345f295755f17424a4f3dd9c7ec44e8a9 Mon Sep 17 00:00:00 2001 From: Oleksandr Myshchyshyn Date: Mon, 25 Nov 2024 16:49:44 +0200 Subject: [PATCH 7/8] Flatten TransactionHeader and TransactionBody into single Transaction class --- src/types/Deploy.ts | 32 ++---- src/types/ExecutableDeployItem.ts | 6 +- src/types/Transaction.ts | 166 +++++++++++------------------- 3 files changed, 74 insertions(+), 130 deletions(-) diff --git a/src/types/Deploy.ts b/src/types/Deploy.ts index dd3748e73..4b29802a4 100644 --- a/src/types/Deploy.ts +++ b/src/types/Deploy.ts @@ -5,13 +5,7 @@ import { Hash } from './key'; import { HexBytes } from './HexBytes'; import { PublicKey, PrivateKey } from './keypair'; import { Duration, Timestamp } from './Time'; -import { - Approval, - Transaction, - TransactionBody, - TransactionCategory, - TransactionHeader -} from './Transaction'; +import { Approval, Transaction, TransactionCategory } from './Transaction'; import { TransactionEntryPoint, TransactionEntryPointEnum @@ -410,21 +404,17 @@ export class Deploy { return new Transaction( deploy.hash, - new TransactionHeader( - deploy.header.chainName, - deploy.header.timestamp, - deploy.header.ttl, - new InitiatorAddr(deploy.header.account), - pricingMode - ), - new TransactionBody( - deploy.session.getArgs(), - TransactionTarget.newTransactionTargetFromSession(deploy.session), - transactionEntryPoint, - new TransactionScheduling({ standard: {} }), - transactionCategory - ), + deploy.header.chainName, + deploy.header.timestamp, + deploy.header.ttl, + new InitiatorAddr(deploy.header.account), + pricingMode, + deploy.session.getArgs(), + TransactionTarget.newTransactionTargetFromSession(deploy.session), + transactionEntryPoint, + new TransactionScheduling({ standard: {} }), deploy.approvals, + transactionCategory, undefined, deploy ); diff --git a/src/types/ExecutableDeployItem.ts b/src/types/ExecutableDeployItem.ts index 2dfece826..7d7c65dba 100644 --- a/src/types/ExecutableDeployItem.ts +++ b/src/types/ExecutableDeployItem.ts @@ -365,7 +365,7 @@ export class TransferDeployItem { amount: BigNumber | string, target: URef | PublicKey, sourcePurse: URef | null = null, - id?: BigNumberish, + id?: BigNumberish ): TransferDeployItem { const runtimeArgs = Args.fromMap({}); runtimeArgs.insert('amount', CLValueUInt512.newCLUInt512(amount)); @@ -386,7 +386,9 @@ export class TransferDeployItem { runtimeArgs.insert( 'id', - id ? CLValueOption.newCLOption(CLValueUInt64.newCLUint64(id)) : defaultClValue + id + ? CLValueOption.newCLOption(CLValueUInt64.newCLUint64(id)) + : defaultClValue ); return new TransferDeployItem(runtimeArgs); diff --git a/src/types/Transaction.ts b/src/types/Transaction.ts index 26a6c984a..69157ad02 100644 --- a/src/types/Transaction.ts +++ b/src/types/Transaction.ts @@ -8,9 +8,8 @@ import { PricingMode } from './PricingMode'; import { TransactionTarget } from './TransactionTarget'; import { TransactionEntryPoint } from './TransactionEntryPoint'; import { TransactionScheduling } from './TransactionScheduling'; -import { PublicKey } from './keypair'; +import { PublicKey, PrivateKey } from './keypair'; import { HexBytes } from './HexBytes'; -import { PrivateKey } from './keypair/PrivateKey'; import { Args } from './Args'; import { deserializeArgs, serializeArgs } from './SerializationUtils'; import { byteHash } from './ByteConverters'; @@ -101,7 +100,7 @@ export class Approval { } /** - * Represents a TransactionV1 object, including its header, body, and approvals. + * Represents a TransactionV1 object, including its hash, payload, and approvals. */ @jsonObject export class TransactionV1 { @@ -272,11 +271,21 @@ export class TransactionV1 { } /** - * Represents the header of a transaction, including details like chain name, timestamp, - * time-to-live (TTL), initiator address, and pricing mode. + * A wrapper for a TransactionV1 or Deploy. */ @jsonObject -export class TransactionHeader { +export class Transaction { + /** + * The hash of the transaction. + */ + @jsonMember({ + name: 'hash', + constructor: Hash, + deserializer: json => Hash.fromJSON(json), + serializer: value => value.toJSON() + }) + public hash: Hash; + /** * The name of the blockchain chain associated with this transaction. */ @@ -322,35 +331,6 @@ export class TransactionHeader { @jsonMember({ name: 'pricing_mode', constructor: PricingMode }) public pricingMode: PricingMode; - /** - * Creates a new `TransactionHeader` instance with the given properties. - * @param chainName The name of the blockchain chain. - * @param timestamp The timestamp of the transaction. - * @param ttl The TTL (Time-To-Live) for the transaction. - * @param initiatorAddr The address of the transaction initiator. - * @param pricingMode The pricing mode for the transaction. - */ - constructor( - chainName: string, - timestamp: Timestamp, - ttl: Duration, - initiatorAddr: InitiatorAddr, - pricingMode: PricingMode - ) { - this.chainName = chainName; - this.timestamp = timestamp; - this.ttl = ttl; - this.initiatorAddr = initiatorAddr; - this.pricingMode = pricingMode; - } -} - -/** - * Represents the body of a transaction, containing the arguments, target, - * entry point, scheduling information, and transaction category. - */ -@jsonObject -export class TransactionBody { /** * The arguments for the transaction, which can be a map of values required by the entry point. */ @@ -395,62 +375,11 @@ export class TransactionBody { /** * The category of the transaction, indicating its type (e.g., minting, auction). + * Using TransactionCategory as enum */ @jsonMember({ name: 'transaction_category', constructor: Number }) public category?: number; - /** - * Constructs a `TransactionBody` with the given arguments, target, entry point, scheduling, and category. - * @param args The arguments for the transaction. - * @param target The target of the transaction (e.g., a contract or account). - * @param entryPoint The entry point to specify the method or action of the transaction. - * @param scheduling The scheduling information for the transaction's execution. - * @param category The category/type of the transaction (e.g., mint, auction). - */ - constructor( - args: Args, - target: TransactionTarget, - entryPoint: TransactionEntryPoint, - scheduling: TransactionScheduling, - category?: number - ) { - this.args = args; - this.target = target; - this.entryPoint = entryPoint; - this.scheduling = scheduling; - this.category = category; - } -} - -/** - * Represents a transaction in the system, containing information such as its hash, - * header, body, approvals, and optionally its associated deployment and transaction details. - */ -@jsonObject -export class Transaction { - /** - * The hash of the transaction. - */ - @jsonMember({ - name: 'hash', - constructor: Hash, - deserializer: json => Hash.fromJSON(json), - serializer: value => value.toJSON() - }) - public hash: Hash; - - /** - * The header of the transaction, which includes metadata about the transaction. - */ - @jsonMember({ name: 'header', constructor: TransactionHeader }) - public header: TransactionHeader; - - /** - * The body of the transaction, containing details such as the target, entry point, and arguments. - */ - @jsonMember({ name: 'body', constructor: TransactionBody }) - public body: TransactionBody; - /** * The list of approvals for this transaction. */ @@ -472,24 +401,49 @@ export class Transaction { /** * Creates a new `Transaction` instance with the specified values. * @param hash The hash of the transaction. - * @param header The header of the transaction. - * @param body The body of the transaction. + * @param chainName The blockchain chain name associated with this transaction. + * @param timestamp The timestamp of transaction creation. + * @param ttl The time-to-live duration of the transaction. + * @param initiatorAddr The address of the transaction initiator. + * @param pricingMode The pricing mode for this transaction. + * @param args The arguments for the transaction. + * @param target The target of the transaction. + * @param entryPoint The entry point of the transaction. + * @param scheduling The scheduling information for the transaction. * @param approvals The list of approvals for this transaction. + * @param category The category of the transaction, indicating its type (e.g., minting, auction). * @param originTransactionV1 The original TransactionV1, if applicable. * @param originDeployV1 The original deploy, if applicable. */ constructor( hash: Hash, - header: TransactionHeader, - body: TransactionBody, + chainName: string, + timestamp: Timestamp, + ttl: Duration, + initiatorAddr: InitiatorAddr, + pricingMode: PricingMode, + args: Args, + target: TransactionTarget, + entryPoint: TransactionEntryPoint, + scheduling: TransactionScheduling, approvals: Approval[], + category?: TransactionCategory, originTransactionV1?: TransactionV1, originDeployV1?: Deploy ) { this.hash = hash; - this.header = header; - this.body = body; + this.chainName = chainName; + this.timestamp = timestamp; + this.ttl = ttl; + this.initiatorAddr = initiatorAddr; + this.pricingMode = pricingMode; + this.args = args; + this.target = target; + this.entryPoint = entryPoint; + this.scheduling = scheduling; this.approvals = approvals; + this.category = category; + this.originDeployV1 = originDeployV1; this.originTransactionV1 = originTransactionV1; } @@ -518,21 +472,19 @@ export class Transaction { static fromTransactionV1(v1: TransactionV1): Transaction { return new Transaction( v1.hash, - new TransactionHeader( - v1.payload.chainName, - v1.payload.timestamp, - v1.payload.ttl, - v1.payload.initiatorAddr, - v1.payload.pricingMode - ), - new TransactionBody( - v1.payload.fields.args, - v1.payload.fields.target, - v1.payload.fields.entryPoint, - v1.payload.fields.scheduling - ), + v1.payload.chainName, + v1.payload.timestamp, + v1.payload.ttl, + v1.payload.initiatorAddr, + v1.payload.pricingMode, + v1.payload.fields.args, + v1.payload.fields.target, + v1.payload.fields.entryPoint, + v1.payload.fields.scheduling, v1.approvals, - v1 + undefined, + v1, // originTransactionV1 + undefined // originDeployV1 is not applicable for this method ); } } From 961d82911839aa206d99bf30568a9d4106db9da0 Mon Sep 17 00:00:00 2001 From: Oleksandr Myshchyshyn Date: Wed, 27 Nov 2024 22:54:40 +0200 Subject: [PATCH 8/8] Fixed serialization and deserialization for transaction hash with TransactionV1Payload, removed TransactionCategory from Transaction --- README.md | 6 +- src/types/Args.ts | 41 ++++++- src/types/ByteConverters.ts | 86 +++++++++++++ src/types/Deploy.ts | 5 +- src/types/ExecutableDeployItem.ts | 24 ++-- src/types/SerializationUtils.ts | 56 +++++---- src/types/Transaction.ts | 10 -- src/types/TransactionEntryPoint.ts | 2 +- src/types/TransactionScheduling.ts | 2 +- src/types/TransactionV1Payload.ts | 187 ++++++++++++++++------------- 10 files changed, 281 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index 862165185..e87834763 100644 --- a/README.md +++ b/README.md @@ -144,8 +144,8 @@ const paymentAmount = '20000000000000'; const pricingMode = new PricingMode(); const fixedMode = new FixedMode(); -fixedMode.gasPriceTolerance = 3; -fixedMode.additionalComputationFactor = 1; +fixedMode.gasPriceTolerance = 1; +fixedMode.additionalComputationFactor = 0; pricingMode.fixed = fixedMode; const args = Args.fromMap({ @@ -155,7 +155,7 @@ const args = Args.fromMap({ ) ), amount: CLValueUInt512.newCLUInt512(paymentAmount), - id: CLValueOption.newCLOption(CLValueUInt64.newCLUint64(3)) + id: CLValueOption.newCLOption(CLValueUInt64.newCLUint64(3)) // memo ( optional ) }); const transactionTarget = new TransactionTarget({}); // Native target; diff --git a/src/types/Args.ts b/src/types/Args.ts index a2e58304c..277a4ec5e 100644 --- a/src/types/Args.ts +++ b/src/types/Args.ts @@ -1,8 +1,8 @@ import { concat } from '@ethersproject/bytes'; +import { jsonMapMember, jsonObject } from 'typedjson'; import { CLValue, CLValueParser } from './clvalue'; -import { jsonMapMember, jsonObject } from 'typedjson'; -import { toBytesString, toBytesU32 } from './ByteConverters'; +import { toBytesString, toBytesU32, writeInteger } from './ByteConverters'; /** * Represents a named argument with a name and associated `CLValue`, which can be serialized to bytes. @@ -25,6 +25,43 @@ export class NamedArg { return concat([name, value]); } + /** + * Converts a `NamedArg` object to a `Uint8Array` for serialization. + * + * The method encodes the name of the argument as a UTF-8 string, followed by the serialized + * bytes of its value. The resulting `Uint8Array` can be used for further processing, such as + * storage or transmission. + * + * @param source - The `NamedArg` object to serialize. It contains a name and a value. + * @returns A `Uint8Array` representing the serialized `NamedArg`. + * + * @example + * ```typescript + * const namedArg = new NamedArg("arg1", CLValue.u32(42)); + * const serializedBytes = YourClass.toBytesWithNamedArg(namedArg); + * console.log(serializedBytes); // Logs the serialized bytes. + * ``` + */ + public static toBytesWithNamedArg(source: NamedArg): Uint8Array { + // The buffer size is fixed at 1024 bytes based on the expected maximum size of + // encoded data, with room for edge cases. If inputs exceed this size, revisit + // the implementation. + const buffer = new ArrayBuffer(1024); + const view = new DataView(buffer); + let offset = 0; + + const nameBytes = new TextEncoder().encode(source.name); + offset = writeInteger(view, offset, nameBytes.length); + new Uint8Array(buffer, offset).set(nameBytes); + offset += nameBytes.length; + + const valueBytes = CLValueParser.toBytesWithType(source.value); + new Uint8Array(buffer, offset).set(valueBytes); + offset += valueBytes.length; + + return new Uint8Array(buffer, 0, offset); + } + /** * Creates a `NamedArg` instance from a byte array. * @param bytes - The byte array to parse. diff --git a/src/types/ByteConverters.ts b/src/types/ByteConverters.ts index 786c194a2..33babb034 100644 --- a/src/types/ByteConverters.ts +++ b/src/types/ByteConverters.ts @@ -173,3 +173,89 @@ export const fromBytesU64 = (bytes: Uint8Array): BigNumber => { // Convert the little-endian bytes into a BigNumber return BigNumber.from(bytes.reverse()); }; + +/** + * Writes a 32-bit signed integer to a `DataView` at the specified offset. + * + * The integer is written in little-endian format. + * + * @param view - The `DataView` instance where the integer will be written. + * @param offset - The offset (in bytes) at which to start writing. + * @param value - The 32-bit signed integer to write. + * @returns The new offset after writing the integer. + * + * @example + * ```typescript + * const buffer = new ArrayBuffer(8); + * const view = new DataView(buffer); + * let offset = 0; + * offset = writeInteger(view, offset, 42); + * console.log(new Int32Array(buffer)); // Logs: Int32Array [42, 0] + * ``` + */ +export const writeInteger = ( + view: DataView, + offset: number, + value: number +): number => { + view.setInt32(offset, value, true); + return offset + 4; +}; + +/** + * Writes a 16-bit unsigned integer to a `DataView` at the specified offset. + * + * The integer is written in little-endian format. + * + * @param view - The `DataView` instance where the integer will be written. + * @param offset - The offset (in bytes) at which to start writing. + * @param value - The 16-bit unsigned integer to write. + * @returns The new offset after writing the integer. + * + * @example + * ```typescript + * const buffer = new ArrayBuffer(4); + * const view = new DataView(buffer); + * let offset = 0; + * offset = writeUShort(view, offset, 65535); + * console.log(new Uint16Array(buffer)); // Logs: Uint16Array [65535, 0] + * ``` + */ +export const writeUShort = ( + view: DataView, + offset: number, + value: number +): number => { + view.setUint16(offset, value, true); + return offset + 2; +}; + +/** + * Writes a sequence of bytes (as a `Uint8Array`) to a `DataView` at the specified offset. + * + * Each byte in the array is written in sequence, starting from the given offset. + * + * @param view - The `DataView` instance where the bytes will be written. + * @param offset - The offset (in bytes) at which to start writing. + * @param value - The `Uint8Array` containing the bytes to write. + * @returns The new offset after writing the bytes. + * + * @example + * ```typescript + * const buffer = new ArrayBuffer(10); + * const view = new DataView(buffer); + * let offset = 0; + * offset = writeBytes(view, offset, new Uint8Array([1, 2, 3, 4])); + * console.log(new Uint8Array(buffer)); // Logs: Uint8Array [1, 2, 3, 4, 0, 0, 0, 0, 0, 0] + * ``` + */ +export const writeBytes = ( + view: DataView, + offset: number, + value: Uint8Array +): number => { + value.forEach((byte, index) => { + view.setUint8(offset + index, byte); + }); + return offset + value.length; +}; diff --git a/src/types/Deploy.ts b/src/types/Deploy.ts index 4b29802a4..b4c3c39e6 100644 --- a/src/types/Deploy.ts +++ b/src/types/Deploy.ts @@ -5,7 +5,7 @@ import { Hash } from './key'; import { HexBytes } from './HexBytes'; import { PublicKey, PrivateKey } from './keypair'; import { Duration, Timestamp } from './Time'; -import { Approval, Transaction, TransactionCategory } from './Transaction'; +import { Approval, Transaction } from './Transaction'; import { TransactionEntryPoint, TransactionEntryPointEnum @@ -357,10 +357,8 @@ export class Deploy { static newTransactionFromDeploy(deploy: Deploy): Transaction { let paymentAmount = 0; let transactionEntryPoint: TransactionEntryPoint; - let transactionCategory = TransactionCategory.Large; if (deploy.session.transfer) { - transactionCategory = TransactionCategory.Mint; transactionEntryPoint = new TransactionEntryPoint( TransactionEntryPointEnum.Transfer ); @@ -414,7 +412,6 @@ export class Deploy { transactionEntryPoint, new TransactionScheduling({ standard: {} }), deploy.approvals, - transactionCategory, undefined, deploy ); diff --git a/src/types/ExecutableDeployItem.ts b/src/types/ExecutableDeployItem.ts index 7d7c65dba..4d34d3252 100644 --- a/src/types/ExecutableDeployItem.ts +++ b/src/types/ExecutableDeployItem.ts @@ -7,6 +7,7 @@ import { CLTypeOption, CLTypeUInt64, CLValue, + CLValueByteArray, CLValueOption, CLValueString, CLValueUInt32, @@ -21,7 +22,6 @@ import { serializeArgs } from './SerializationUtils'; import { PublicKey } from './keypair'; -import { toBytesArrayU8 } from './ByteConverters'; /** * Enum representing the different types of executable deploy items. @@ -49,7 +49,7 @@ export class ModuleBytes { serializer: byteArrayJsonSerializer, deserializer: byteArrayJsonDeserializer }) - moduleBytes: Uint8Array; + moduleBytes!: Uint8Array; /** * The arguments passed to the module. @@ -75,13 +75,21 @@ export class ModuleBytes { * @returns The serialized byte array. */ bytes(): Uint8Array { - if (!this.args) throw new Error('Missing arguments for ModuleBytes'); + const lengthBytes = CLValueUInt32.newCLUInt32( + BigNumber.from(this.moduleBytes.length) + ).bytes(); + const bytesArrayBytes = CLValueByteArray.newCLByteArray( + this.moduleBytes + ).bytes(); + + let result = concat([lengthBytes, bytesArrayBytes]); + + if (this.args) { + const argBytes = this.args.toBytes(); + result = concat([result, argBytes]); + } - return concat([ - Uint8Array.from([0]), - toBytesArrayU8(this.moduleBytes), - this.args.toBytes() - ]); + return result; } } diff --git a/src/types/SerializationUtils.ts b/src/types/SerializationUtils.ts index 716e44e5b..d3d3e5593 100644 --- a/src/types/SerializationUtils.ts +++ b/src/types/SerializationUtils.ts @@ -126,39 +126,49 @@ export const dehumanizerTTL = (ttl: string): number => { /** * Deserializes an array of runtime arguments to a `RuntimeArgs` object. * - * @param arr The array of serialized runtime arguments. + * @param arr The array of serialized runtime arguments or a Named wrapper. * @returns A `RuntimeArgs` object containing the deserialized arguments. + * @throws Error if the input format is invalid. */ -export const deserializeArgs = (arr: any) => { +export const deserializeArgs = (arr: any): Args | undefined => { const raSerializer = new TypedJSON(Args); - const value = { - args: arr - }; - return raSerializer.parse(value); + + if (arr.Named && Array.isArray(arr.Named)) { + // If the arguments are wrapped in a "Named" property + return raSerializer.parse({ args: arr.Named }); + } + + if (Array.isArray(arr)) { + // If the input is directly an array of arguments + return raSerializer.parse({ args: arr }); + } + + throw new Error('Invalid argument format for deserialization.'); }; /** - * Serializes a `RuntimeArgs` object to a byte array. + * Serializes a `RuntimeArgs` object to a byte array or an object representation. + * + * This function converts the `RuntimeArgs` (or `Args`) object into a serialized format. + * If `asNamed` is set to `true`, the serialized arguments are wrapped in a `Named` property + * for more structured output. Otherwise, the plain array of serialized arguments is returned. + * + * @param ra - The `Args` object to be serialized. It contains the runtime arguments. + * @param asNamed - A boolean flag indicating whether to wrap the serialized output in a `Named` property. Defaults to `false`. + * @returns A serialized representation of the runtime arguments. + * If `asNamed` is `true`, the output is an object with a `Named` property. Otherwise, it is a plain array. * - * @param ra The `RuntimeArgs` object to be serialized. - * @returns A byte array representing the serialized runtime arguments. */ -export const serializeArgs = (ra: Args) => { +export const serializeArgs = (ra: Args, asNamed = false) => { const raSerializer = new TypedJSON(Args); const json = raSerializer.toPlainJson(ra); - return Object.values(json as any)[0]; -}; + const argsArray = Object.values(json as any)[0]; -/** - * Compares two arrays for equality. - * @param a The first array. - * @param b The second array. - * @returns `true` if the arrays are equal, `false` otherwise. - */ -export const arrayEquals = (a: Uint8Array, b: Uint8Array): boolean => { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; + if (asNamed) { + return { + Named: argsArray + }; } - return true; + + return argsArray; }; diff --git a/src/types/Transaction.ts b/src/types/Transaction.ts index 69157ad02..601d3c4e2 100644 --- a/src/types/Transaction.ts +++ b/src/types/Transaction.ts @@ -373,13 +373,6 @@ export class Transaction { }) public scheduling: TransactionScheduling; - /** - * The category of the transaction, indicating its type (e.g., minting, auction). - * Using TransactionCategory as enum - */ - @jsonMember({ name: 'transaction_category', constructor: Number }) - public category?: number; - /** * The list of approvals for this transaction. */ @@ -427,7 +420,6 @@ export class Transaction { entryPoint: TransactionEntryPoint, scheduling: TransactionScheduling, approvals: Approval[], - category?: TransactionCategory, originTransactionV1?: TransactionV1, originDeployV1?: Deploy ) { @@ -442,7 +434,6 @@ export class Transaction { this.entryPoint = entryPoint; this.scheduling = scheduling; this.approvals = approvals; - this.category = category; this.originDeployV1 = originDeployV1; this.originTransactionV1 = originTransactionV1; @@ -482,7 +473,6 @@ export class Transaction { v1.payload.fields.entryPoint, v1.payload.fields.scheduling, v1.approvals, - undefined, v1, // originTransactionV1 undefined // originDeployV1 is not applicable for this method ); diff --git a/src/types/TransactionEntryPoint.ts b/src/types/TransactionEntryPoint.ts index 9b5317bae..4dd1c16d5 100644 --- a/src/types/TransactionEntryPoint.ts +++ b/src/types/TransactionEntryPoint.ts @@ -116,7 +116,7 @@ export class TransactionEntryPoint { * * @returns A `Uint8Array` representing the transaction entry point and any associated data. */ - bytes(): Uint8Array { + toBytes(): Uint8Array { const calltableSerialization = new CalltableSerialization(); const tag = this.tag(); calltableSerialization.addField(0, Uint8Array.from([tag])); diff --git a/src/types/TransactionScheduling.ts b/src/types/TransactionScheduling.ts index 7514d02da..570efe10e 100644 --- a/src/types/TransactionScheduling.ts +++ b/src/types/TransactionScheduling.ts @@ -146,7 +146,7 @@ export class TransactionScheduling { * * @returns A `Uint8Array` representing the transaction scheduling. */ - bytes(): Uint8Array { + toBytes(): Uint8Array { if (this.standard) { const calltableSerialization = new CalltableSerialization(); calltableSerialization.addField(0, Uint8Array.of(0)); diff --git a/src/types/TransactionV1Payload.ts b/src/types/TransactionV1Payload.ts index 91bd7139d..09bd000cd 100644 --- a/src/types/TransactionV1Payload.ts +++ b/src/types/TransactionV1Payload.ts @@ -1,24 +1,16 @@ -import { concat } from '@ethersproject/bytes'; -import { - toBytesString, - toBytesU16, - toBytesU32, - toBytesU64 -} from './ByteConverters'; -import { jsonMember, jsonObject, TypedJSON } from 'typedjson'; +import { jsonMember, jsonObject } from 'typedjson'; + import { InitiatorAddr } from './InitiatorAddr'; import { Duration, Timestamp } from './Time'; import { PricingMode } from './PricingMode'; -import { Args } from './Args'; +import { Args, NamedArg } from './Args'; import { TransactionTarget } from './TransactionTarget'; import { TransactionEntryPoint } from './TransactionEntryPoint'; import { TransactionScheduling } from './TransactionScheduling'; import { CalltableSerialization } from './CalltableSerialization'; -import { - byteArrayJsonSerializer, - deserializeArgs, - serializeArgs -} from './SerializationUtils'; +import { deserializeArgs, serializeArgs } from './SerializationUtils'; +import { CLValueString, CLValueUInt64 } from './clvalue'; +import { writeBytes, writeInteger, writeUShort } from './ByteConverters'; /** * Interface representing the parameters required to build a `TransactionV1Payload`. @@ -82,7 +74,7 @@ export class PayloadFields { */ @jsonMember(() => Args, { deserializer: deserializeArgs, - serializer: serializeArgs + serializer: (args: Args) => serializeArgs(args, true) }) public args: Args; @@ -124,26 +116,6 @@ export class PayloadFields { */ private fields: Map = new Map(); - /** - * Utility method to map field identifiers to serialized values. - * Ensures that all fields are properly initialized before serialization. - * @returns A map of field identifiers to their serialized values. - * @throws Error if any required field is uninitialized or invalid. - */ - private toSerializedFields(): Map { - if (!this.args) throw new Error('args field is uninitialized.'); - if (!this.target) throw new Error('target field is uninitialized.'); - if (!this.entryPoint) throw new Error('entryPoint field is uninitialized.'); - if (!this.scheduling) throw new Error('scheduling field is uninitialized.'); - - return new Map([ - [0, this.args.toBytes()], - [1, this.target.toBytes()], - [2, this.entryPoint.bytes()], - [3, this.scheduling.bytes()] - ]); - } - /** * Builds a `PayloadFields` instance from provided transaction details. * @@ -180,8 +152,6 @@ export class PayloadFields { payloadFields.entryPoint = transactionEntryPoint; payloadFields.scheduling = transactionScheduling; - payloadFields.fields = payloadFields.toSerializedFields(); - return payloadFields; } @@ -206,49 +176,31 @@ export class PayloadFields { } /** - * Serializes all fields into a `Uint8Array`. + * Serializes the fields of the object into a `Uint8Array` for transmission or storage. * - * @returns Serialized fields as a `Uint8Array`. - */ - public toBytes(): Uint8Array { - const fieldsCount = toBytesU32(this.fields.size); - const serializedFields = Array.from( - this.fields.entries() - ).map(([key, value]) => concat([toBytesU16(key), value])); - - return concat([fieldsCount, ...serializedFields]); - } - - /** - * Deserializes JSON data into a `PayloadFields` instance. + * This method iterates over the `fields` map, serializing each key-value pair. The key is + * written as a 16-bit unsigned integer, and the value is written as a sequence of bytes. + * The resulting byte array contains all serialized fields in order, preceded by the number of fields. * - * @param json - JSON representation of the payload fields. - * @returns A `PayloadFields` instance. - */ - public static fromJSON(json: any): PayloadFields { - const deserialized = new TypedJSON(PayloadFields).parse(json); - - if (!deserialized) { - throw new Error('Failed to deserialize PayloadFields.'); - } - - deserialized.fields = deserialized.toSerializedFields(); - - return deserialized; - } - - /** - * Converts the payload fields to a JSON object. + * @returns A `Uint8Array` containing the serialized representation of the fields. * - * @returns A JSON representation of the payload fields. */ - public toJSON(): Record { - const result: Record = {}; - const fieldEntries = Array.from(this.fields.entries()); - for (const [key, value] of fieldEntries) { - result[key.toString()] = byteArrayJsonSerializer(value); + toBytes(): Uint8Array { + // The buffer size is fixed at 1024 bytes based on the expected maximum size of + // encoded data, with room for edge cases. If inputs exceed this size, revisit + // the implementation. + const fieldsBytes = new ArrayBuffer(1024); + const view = new DataView(fieldsBytes); + let offset = 0; + + offset = writeInteger(view, offset, this.fields.size); + + for (const [field, value] of Array.from(this.fields.entries())) { + offset = writeUShort(view, offset, field); + offset = writeBytes(view, offset, value); } - return result; + + return new Uint8Array(fieldsBytes, 0, offset); } } @@ -307,8 +259,7 @@ export class TransactionV1Payload { */ @jsonMember({ name: 'fields', - serializer: value => (value ? value.toJSON() : undefined), - deserializer: json => (json ? PayloadFields.fromJSON(json) : undefined) + constructor: PayloadFields }) public fields: PayloadFields; @@ -317,17 +268,81 @@ export class TransactionV1Payload { * * @returns A `Uint8Array` representing the serialized transaction payload. */ - public toBytes(): Uint8Array { - const calltable = new CalltableSerialization(); + toBytes(): Uint8Array { + // The buffer size is fixed at 1024 bytes based on the expected maximum size of + // encoded data, with room for edge cases. If inputs exceed this size, revisit + // the implementation. + const runtimeArgsBuffer = new ArrayBuffer(1024); + const runtimeArgsView = new DataView(runtimeArgsBuffer); + let offset = 0; + + runtimeArgsView.setUint8(offset, 0x00); + offset += 1; + + runtimeArgsView.setUint32(offset, this.fields.args.args.size, true); + offset += 4; + + for (const [name, value] of Array.from(this.fields.args.args.entries())) { + const namedArg = new NamedArg(name, value); + const argBytes = NamedArg.toBytesWithNamedArg(namedArg); + new Uint8Array(runtimeArgsBuffer, offset).set(argBytes); + offset += argBytes.length; + } + + const runtimeArgsBytes = new Uint8Array(runtimeArgsBuffer, 0, offset); - calltable.addField(0, this.initiatorAddr.toBytes()); - calltable.addField(1, toBytesU64(Date.parse(this.timestamp.toJSON()))); - calltable.addField(2, toBytesU64(this.ttl.duration)); - calltable.addField(3, toBytesString(this.chainName)); - calltable.addField(4, this.pricingMode.toBytes()); - calltable.addField(5, this.fields.toBytes()); + const fields = new PayloadFields(); - return calltable.toBytes(); + const runtimeArgsWithLength = new Uint8Array(runtimeArgsBytes.length + 4); + new DataView(runtimeArgsWithLength.buffer).setUint32( + 0, + runtimeArgsBytes.length, + true + ); + runtimeArgsWithLength.set(runtimeArgsBytes, 4); + fields.addField(0, runtimeArgsWithLength); + + const targetBytes = this.fields.target.toBytes(); + const targetWithLength = new Uint8Array(targetBytes.length + 4); + new DataView(targetWithLength.buffer).setUint32( + 0, + targetBytes.length, + true + ); + targetWithLength.set(targetBytes, 4); + fields.addField(1, targetWithLength); + + const entryPointBytes = this.fields.entryPoint.toBytes(); + const entryPointWithLength = new Uint8Array(entryPointBytes.length + 4); + new DataView(entryPointWithLength.buffer).setUint32( + 0, + entryPointBytes.length, + true + ); + entryPointWithLength.set(entryPointBytes, 4); + fields.addField(2, entryPointWithLength); + + const schedulingBytes = this.fields.scheduling.toBytes(); + const schedulingWithLength = new Uint8Array(schedulingBytes.length + 4); + new DataView(schedulingWithLength.buffer).setUint32( + 0, + schedulingBytes.length, + true + ); + schedulingWithLength.set(schedulingBytes, 4); + fields.addField(3, schedulingWithLength); + + return new CalltableSerialization() + .addField(0, this.initiatorAddr.toBytes()) + .addField( + 1, + CLValueUInt64.newCLUint64(this.timestamp.toMilliseconds()).bytes() + ) + .addField(2, CLValueUInt64.newCLUint64(this.ttl.duration).bytes()) + .addField(3, CLValueString.newCLString(this.chainName).bytes()) + .addField(4, this.pricingMode.toBytes()) + .addField(5, fields.toBytes()) + .toBytes(); } /**