diff --git a/README.md b/README.md index 6958f9efb..e87834763 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); @@ -145,17 +144,10 @@ const paymentAmount = '20000000000000'; const pricingMode = new PricingMode(); const fixedMode = new FixedMode(); -fixedMode.gasPriceTolerance = 3; +fixedMode.gasPriceTolerance = 1; +fixedMode.additionalComputationFactor = 0; 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( @@ -163,24 +155,29 @@ 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(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); 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..277a4ec5e 100644 --- a/src/types/Args.ts +++ b/src/types/Args.ts @@ -1,15 +1,8 @@ import { concat } from '@ethersproject/bytes'; - -import { - CLTypeString, - CLValue, - CLValueParser, - CLValueString, - CLValueUInt32, - IResultWithBytes -} from './clvalue'; import { jsonMapMember, jsonObject } from 'typedjson'; -import { toBytesString, toBytesU32 } from './ByteConverters'; + +import { CLValue, CLValueParser } from './clvalue'; +import { toBytesString, toBytesU32, writeInteger } from './ByteConverters'; /** * Represents a named argument with a name and associated `CLValue`, which can be serialized to bytes. @@ -32,24 +25,61 @@ 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. - * @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 +186,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 3ec3777d7..33babb034 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. */ @@ -127,3 +132,130 @@ 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()); +}; + +/** + * 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/CalltableSerialization.ts b/src/types/CalltableSerialization.ts new file mode 100644 index 000000000..11556666e --- /dev/null +++ b/src/types/CalltableSerialization.ts @@ -0,0 +1,145 @@ +import { concat } from '@ethersproject/bytes'; +import { parseU16, parseU32, toBytesU16, toBytesU32 } from './ByteConverters'; + +/** + * Represents a single field in the call table. + */ +export class Field { + readonly index: number; + readonly offset: number; + 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; + this.value = value; + } + + /** + * Calculates the serialized vector size for the given number of fields. + * + * 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; // 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`. + * @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) { + 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 into 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]); + } + + /** + * 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 a15da4992..b4c3c39e6 100644 --- a/src/types/Deploy.ts +++ b/src/types/Deploy.ts @@ -5,16 +5,13 @@ import { Hash } from './key'; import { HexBytes } from './HexBytes'; import { PublicKey, PrivateKey } from './keypair'; import { Duration, Timestamp } from './Time'; +import { Approval, Transaction } from './Transaction'; import { - Approval, - Transaction, - TransactionBody, - TransactionCategory, - TransactionHeader -} from './Transaction'; -import { TransactionEntryPoint } from './TransactionEntryPoint'; + TransactionEntryPoint, + TransactionEntryPointEnum +} 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'; @@ -359,14 +356,16 @@ export class Deploy { */ static newTransactionFromDeploy(deploy: Deploy): Transaction { let paymentAmount = 0; - const transactionEntryPoint: TransactionEntryPoint = new TransactionEntryPoint(); - let transactionCategory = TransactionCategory.Large; + let transactionEntryPoint: TransactionEntryPoint; 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 +378,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(); @@ -393,27 +395,22 @@ export class Deploy { const standardPayment = paymentAmount === 0 && !deploy.payment.moduleBytes; const pricingMode = new PricingMode(); - const classicMode = new ClassicMode(); - 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, - 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, undefined, deploy diff --git a/src/types/ExecutableDeployItem.ts b/src/types/ExecutableDeployItem.ts index 1f683e469..4d34d3252 100644 --- a/src/types/ExecutableDeployItem.ts +++ b/src/types/ExecutableDeployItem.ts @@ -15,9 +15,13 @@ 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'; /** * 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,12 +75,11 @@ 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) + BigNumber.from(this.moduleBytes.length) ).bytes(); const bytesArrayBytes = CLValueByteArray.newCLByteArray( - moduleBytes + this.moduleBytes ).bytes(); let result = concat([lengthBytes, bytesArrayBytes]); @@ -365,7 +373,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 +394,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); @@ -545,7 +555,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 +628,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/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/PricingMode.ts b/src/types/PricingMode.ts index 19ecbfe1c..e27eed321 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, + * Represents the payment limited ( classic before ) 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. + * Represents the prepair ( reserved before ) pricing mode, which includes a receipt hash. */ @jsonObject -export class ReservedMode { +export class PrepaidMode { /** * The receipt associated with the reserved pricing mode. */ @@ -68,18 +104,26 @@ 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(); + } } /** - * 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: 'Classic', constructor: ClassicMode }) - classic?: ClassicMode; + @jsonMember({ name: 'PaymentLimited', constructor: PaymentLimitedMode }) + paymentLimited?: PaymentLimitedMode; /** * The fixed pricing mode, if applicable. @@ -88,10 +132,10 @@ export class PricingMode { fixed?: FixedMode; /** - * The reserved pricing mode, if applicable. + * The Prepaid 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/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/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/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 de5e2c1c8..b1a4bf329 100644 --- a/src/types/Transaction.test.ts +++ b/src/types/Transaction.test.ts @@ -2,19 +2,15 @@ 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'; - -import { KeyAlgorithm } from './keypair/Algorithm'; - +import { KeyAlgorithm, PrivateKey, PublicKey } from './keypair'; import { SessionTarget, TransactionTarget } from './TransactionTarget'; -import { TransactionEntryPoint } from './TransactionEntryPoint'; +import { + TransactionEntryPoint, + TransactionEntryPointEnum +} from './TransactionEntryPoint'; import { TransactionScheduling } from './TransactionScheduling'; import { Args } from './Args'; import { @@ -23,27 +19,20 @@ import { CLValueUInt512, CLValueUInt64 } from './clvalue'; -import { PublicKey } from './keypair'; +import { TransactionV1Payload } from './TransactionV1Payload'; +import { Hash } from './key'; 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 timestamp = new Timestamp(new Date()); 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(keys.publicKey), - pricingMode - }); - const args = Args.fromMap({ target: CLValue.newCLPublicKey( PublicKey.fromHex( @@ -54,34 +43,52 @@ describe('Test Transaction', () => { id: CLValueOption.newCLOption(CLValueUInt64.newCLUint64(3)) }); - const transactionTarget = new TransactionTarget(new SessionTarget()); - const entryPoint = new TransactionEntryPoint(undefined, {}); + const sessionTarget = new SessionTarget(); + + sessionTarget.runtime = 'VmCasperV1'; + sessionTarget.transferredValue = 1000; + sessionTarget.moduleBytes = Uint8Array.from([1]); + sessionTarget.isInstallUpgrade = false; + sessionTarget.seed = Hash.fromHex( + '8bf9d406ab901428d43ecd3a6f214b864e7ef8316934e5e0f049650a65b40d73' + ); + + const transactionTarget = new TransactionTarget( + undefined, + undefined, + sessionTarget + ); const scheduling = new TransactionScheduling({}); + const entryPoint = new TransactionEntryPoint( + TransactionEntryPointEnum.Call + ); - const transactionBody = TransactionV1Body.build({ - args: args, - target: transactionTarget, - transactionEntryPoint: entryPoint, - transactionScheduling: scheduling, - transactionCategory: 2 + const transactionPayload = TransactionV1Payload.build({ + initiatorAddr: new InitiatorAddr(keys.publicKey), + ttl: new Duration(1800000), + args, + timestamp: new Timestamp(new Date()), + entryPoint, + scheduling, + transactionTarget, + chainName: 'casper-net-1', + pricingMode }); - 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 transactionPaymentAmount = parsed.body.args.args + const transactionPaymentAmount = transaction.payload.fields.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(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 8822a25b6..601d3c4e2 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'; @@ -9,28 +8,18 @@ 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 { 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 './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. */ @@ -111,233 +100,7 @@ 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. + * Represents a TransactionV1 object, including its hash, payload, and approvals. */ @jsonObject export class TransactionV1 { @@ -353,16 +116,14 @@ export class TransactionV1 { public hash: Hash; /** - * The header of the transaction. - */ - @jsonMember({ name: 'header', constructor: TransactionV1Header }) - public header: TransactionV1Header; - - /** - * The body of the transaction. + * The payload of the transaction. + * A merge of header and body concepts from before. */ - @jsonMember({ name: 'body', constructor: TransactionV1Body }) - public body: TransactionV1Body; + @jsonMember({ + name: 'payload', + constructor: TransactionV1Payload + }) + public payload: TransactionV1Payload; /** * The approvals for the transaction. @@ -372,30 +133,23 @@ 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; } /** - * 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 { - const bodyBytes = this.body.toBytes(); - - if (!arrayEquals(byteHash(bodyBytes), this.header.bodyHash.toBytes())) - throw ErrInvalidBodyHash; - - const headerBytes = this.header.toBytes(); + const payloadBytes = this.payload!.toBytes(); + const calculatedHash = new Hash(byteHash(payloadBytes)); - if (!arrayEquals(byteHash(headerBytes), this.hash.toBytes())) - throw ErrInvalidTransactionHash; + if (!this.hash.equals(calculatedHash)) throw ErrInvalidTransactionHash; for (const approval of this.approvals) { if ( @@ -447,41 +201,27 @@ 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. */ 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); } /** * 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( - 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 +239,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; } @@ -531,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. */ @@ -581,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. */ @@ -652,64 +373,6 @@ export class TransactionBody { }) public scheduling: TransactionScheduling; - /** - * The category of the transaction, indicating its type (e.g., minting, auction). - */ - @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. */ @@ -731,24 +394,47 @@ 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[], 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.originDeployV1 = originDeployV1; this.originTransactionV1 = originTransactionV1; } @@ -777,22 +463,18 @@ export class Transaction { static fromTransactionV1(v1: TransactionV1): Transaction { return new Transaction( v1.hash, - new TransactionHeader( - v1.header.chainName, - v1.header.timestamp, - v1.header.ttl, - v1.header.initiatorAddr, - v1.header.pricingMode - ), - new TransactionBody( - v1.body.args, - v1.body.target, - v1.body.entryPoint, - v1.body.scheduling, - v1.body.category - ), + 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 + v1, // originTransactionV1 + undefined // originDeployV1 is not applicable for this method ); } } 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..4dd1c16d5 100644 --- a/src/types/TransactionEntryPoint.ts +++ b/src/types/TransactionEntryPoint.ts @@ -1,7 +1,6 @@ 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,146 +15,100 @@ 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. + * 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, - Transfer, - AddBid, - WithdrawBid, - Delegate, - Undelegate, - Redelegate, - ActivateBid, - ChangeBidPublicKey, - Call + Call = 1, + 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. + * The type of transaction entry point, represented as an enum. */ - @jsonMember({ constructor: Object, name: 'Undelegate' }) - undelegate?: Record; + @jsonMember({ constructor: String }) + type: TransactionEntryPointEnum; /** - * The redelegate action as a generic object. + * Custom entry point identifier, used when the `type` is `Custom`. */ - @jsonMember({ constructor: Object, name: 'Redelegate' }) - redelegate?: Record; + @jsonMember({ constructor: String }) + customEntryPoint?: string; /** - * The activate bid action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'ActivateBid' }) - activateBid?: Record; - - /** - * 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; - - /** - * 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 - ) { - 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; + 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; - return TransactionEntryPointTag.Custom; + 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'); + } } /** @@ -163,86 +116,179 @@ export class TransactionEntryPoint { * * @returns A `Uint8Array` representing the transaction entry point and any associated data. */ - bytes(): Uint8Array { - let result = new Uint8Array([this.tag()]); - if (this.custom) { - const customBytes = new CLValueString(this.custom).bytes(); - result = concat([result, customBytes]); + toBytes(): Uint8Array { + const calltableSerialization = new CalltableSerialization(); + const tag = this.tag(); + calltableSerialization.addField(0, Uint8Array.from([tag])); + + if ( + this.type === TransactionEntryPointEnum.Custom && + this.customEntryPoint + ) { + const customSerialization = new CalltableSerialization(); + customSerialization.addField(0, Uint8Array.from([1])); + customSerialization.addField( + 1, + CLValueString.newCLString(this.customEntryPoint).bytes() + ); + + calltableSerialization.addField(1, customSerialization.toBytes()); } - return result; + + 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; + return new TransactionEntryPoint(TransactionEntryPointEnum.Call); + case TransactionEntryPointEnum.AddReservations: + 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 90af45e98..570efe10e 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 { fromBytesU64, toBytesU64 } from './ByteConverters'; /** * Enum representing the scheduling tags for transaction scheduling types. @@ -20,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. */ @@ -35,13 +36,24 @@ 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(); + } } /** * 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. */ @@ -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(); + } } /** @@ -123,20 +146,18 @@ 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]); + toBytes(): Uint8Array { + 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'); } /** @@ -188,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.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..8a23e4e6b 100644 --- a/src/types/TransactionTarget.ts +++ b/src/types/TransactionTarget.ts @@ -1,37 +1,25 @@ 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'; +import { fromBytesU64 } from './ByteConverters'; /** * Represents the invocation target for a transaction identified by a package hash. @@ -54,6 +42,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 +76,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 +149,132 @@ 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' + ); + } + + /** + * 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}`); + } + } } /** @@ -144,6 +293,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 +325,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 +423,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 +458,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 +478,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 +515,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; @@ -494,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/TransactionV1Payload.ts b/src/types/TransactionV1Payload.ts new file mode 100644 index 000000000..09bd000cd --- /dev/null +++ b/src/types/TransactionV1Payload.ts @@ -0,0 +1,382 @@ +import { jsonMember, jsonObject } from 'typedjson'; + +import { InitiatorAddr } from './InitiatorAddr'; +import { Duration, Timestamp } from './Time'; +import { PricingMode } from './PricingMode'; +import { Args, NamedArg } from './Args'; +import { TransactionTarget } from './TransactionTarget'; +import { TransactionEntryPoint } from './TransactionEntryPoint'; +import { TransactionScheduling } from './TransactionScheduling'; +import { CalltableSerialization } from './CalltableSerialization'; +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`. + * 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; + + /** + * 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 fields used in transaction serialization. + * This class handles serialization and deserialization of transaction data fields. + */ +@jsonObject +export class PayloadFields { + /** + * Arguments for the transaction. + */ + @jsonMember(() => Args, { + deserializer: deserializeArgs, + serializer: (args: Args) => serializeArgs(args, true) + }) + 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(); + + /** + * 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 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; + + return payloadFields; + } + + /** + * Adds a serialized field to the payload. + * + * @param field - Field identifier. + * @param value - Serialized value of the field. + */ + public addField(field: number, value: Uint8Array): void { + this.fields.set(field, value); + } + + /** + * 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 fields of the object into a `Uint8Array` for transmission or storage. + * + * 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. + * + * @returns A `Uint8Array` containing the serialized representation of the fields. + * + */ + 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 new Uint8Array(fieldsBytes, 0, offset); + } +} + +/** + * Class representing the payload for a V1 transaction. + */ +@jsonObject +export class TransactionV1Payload { + /** + * Address of the transaction initiator. + */ + @jsonMember({ + name: 'initiator_addr', + constructor: InitiatorAddr, + deserializer: json => InitiatorAddr.fromJSON(json), + serializer: value => value.toJSON() + }) + public initiatorAddr: InitiatorAddr; + + /** + * Timestamp when the transaction was created. + */ + @jsonMember({ + name: 'timestamp', + constructor: Timestamp, + deserializer: json => Timestamp.fromJSON(json), + serializer: value => value.toJSON() + }) + public timestamp: Timestamp; + + /** + * 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; + + /** + * Pricing mode used for the transaction. + */ + @jsonMember({ name: 'pricing_mode', constructor: PricingMode }) + public pricingMode: PricingMode; + + /** + * Name of the chain the transaction should be executed on. + */ + @jsonMember({ name: 'chain_name', constructor: String }) + public chainName: string; + + /** + * Serialized fields associated with the transaction. + */ + @jsonMember({ + name: 'fields', + constructor: PayloadFields + }) + public fields: PayloadFields; + + /** + * Serializes the transaction payload into a `Uint8Array`. + * + * @returns A `Uint8Array` representing the serialized transaction payload. + */ + 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); + + const fields = new PayloadFields(); + + 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(); + } + + /** + * Constructs a `TransactionV1Payload` instance with specified parameters. + * + * @param params - Parameters for building the transaction payload. + * @returns A new `TransactionV1Payload` instance. + */ + public static build({ + initiatorAddr, + args, + ttl, + entryPoint, + pricingMode, + timestamp, + transactionTarget, + scheduling, + chainName + }: ITransactionPayloadBuildParams): TransactionV1Payload { + const payloadFields = PayloadFields.build( + args, + transactionTarget, + entryPoint, + scheduling + ); + + const payload = new TransactionV1Payload(); + payload.initiatorAddr = initiatorAddr; + payload.ttl = ttl; + payload.pricingMode = pricingMode; + payload.timestamp = timestamp; + payload.chainName = chainName; + payload.fields = payloadFields; + + return payload; + } +} 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..e74192079 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,10 +23,11 @@ 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'; +export * from './TransactionV1Payload'; export * from './TransactionEntryPoint'; export * from './TransactionScheduling'; export * from './TransactionTarget';