diff --git a/src/types/AddressableEntity.ts b/src/types/AddressableEntity.ts index df6afa3c..650dd562 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 c590f7fb..a2e58304 100644 --- a/src/types/Args.ts +++ b/src/types/Args.ts @@ -1,13 +1,6 @@ import { concat } from '@ethersproject/bytes'; -import { - CLTypeString, - CLValue, - CLValueParser, - CLValueString, - CLValueUInt32, - IResultWithBytes -} from './clvalue'; +import { CLValue, CLValueParser } from './clvalue'; import { jsonMapMember, jsonObject } from 'typedjson'; import { toBytesString, toBytesU32 } from './ByteConverters'; @@ -35,21 +28,21 @@ export class NamedArg { /** * Creates a `NamedArg` instance from a byte array. * @param bytes - The byte array to parse. - * @returns A new `NamedArg` instance. - * @throws Error if the value data is missing. + * @returns A `NamedArg` instance. */ public static fromBytes(bytes: Uint8Array): NamedArg { - const stringValue = CLValueString.fromBytes(bytes); + let offset = 0; - if (!stringValue.bytes) { - throw new Error('Missing data for value of named arg'); - } + const nameLength = new DataView(bytes.buffer).getUint32(offset, true); + offset += 4; + const nameBytes = bytes.slice(offset, offset + nameLength); + offset += nameLength; + const name = new TextDecoder().decode(nameBytes); - const value = CLValueParser.fromBytesByType( - stringValue.bytes, - CLTypeString - ); - return new NamedArg(value.result.toString(), value.result); + const valueBytes = bytes.slice(offset); + const value = CLValueParser.fromBytesWithType(valueBytes); + + return new NamedArg(name, value.result); } } @@ -156,28 +149,30 @@ export class Args { /** * Creates an `Args` instance from a byte array. - * Parses the byte array to extract each argument. * @param bytes - The byte array to parse. - * @returns An object containing a new `Args` instance and any remaining bytes. - * @throws Error if there is an issue parsing the bytes. + * @returns An `Args` instance. */ - public static fromBytes(bytes: Uint8Array): IResultWithBytes { - const uint32 = CLValueUInt32.fromBytes(bytes); - const size = uint32.result.getValue().toNumber(); - - let remainBytes: Uint8Array | undefined = uint32.bytes; - const res: NamedArg[] = []; - for (let i = 0; i < size; i++) { - if (!remainBytes) { - throw new Error('Error while parsing bytes'); - } - const namedArg = NamedArg.fromBytes(remainBytes); - res.push(namedArg); - remainBytes = undefined; + public static fromBytes(bytes: Uint8Array): Args { + let offset = 0; + + const numArgs = new DataView(bytes.buffer).getUint32(offset, true); + offset += 4; + + const args = new Map(); + + for (let i = 0; i < numArgs; i++) { + const namedArgBytes = bytes.slice(offset); + const namedArg = NamedArg.fromBytes(namedArgBytes); + + const nameLength = new DataView(namedArgBytes.buffer).getUint32(0, true); + const valueBytes = CLValueParser.toBytesWithType(namedArg.value); + const consumedBytes = 4 + nameLength + valueBytes.length; + + offset += consumedBytes; + + args.set(namedArg.name, namedArg.value); } - return { - result: Args.fromNamedArgs(res), - bytes: remainBytes || Uint8Array.from([]) - }; + + return new Args(args); } } diff --git a/src/types/Bid.ts b/src/types/Bid.ts index 0061132d..f9cd9d75 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 17020965..a997da72 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 e7e2d70d..786c194a 100644 --- a/src/types/ByteConverters.ts +++ b/src/types/ByteConverters.ts @@ -132,3 +132,44 @@ export function toBytesArrayU8(arr: Uint8Array): Uint8Array { export function byteHash(x: Uint8Array): Uint8Array { return blake2b(x, { dkLen: 32 }); } + +/** + * Parses a 16-bit unsigned integer (`u16`) from a little-endian byte array. + * @param bytes - The byte array containing the `u16` value. + * @returns The parsed 16-bit unsigned integer. + */ +export function parseU16(bytes: Uint8Array): number { + if (bytes.length < 2) { + throw new Error('Invalid byte array for u16 parsing'); + } + return bytes[0] | (bytes[1] << 8); +} + +/** + * Parses a 32-bit unsigned integer (`u32`) from a little-endian byte array. + * @param bytes - The byte array containing the `u32` value. + * @returns The parsed 32-bit unsigned integer. + */ +export function parseU32(bytes: Uint8Array): number { + if (bytes.length < 4) { + throw new Error('Invalid byte array for u32 parsing'); + } + + return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); +} + +/** + * Parses a 64-bit unsigned integer (`u64`) from a little-endian byte array. + * @param bytes - A `Uint8Array` containing the serialized 64-bit unsigned integer. + * @returns A `BigNumber` representing the parsed value. + */ +export const fromBytesU64 = (bytes: Uint8Array): BigNumber => { + if (bytes.length !== 8) { + throw new Error( + `Invalid input length for u64: expected 8 bytes, got ${bytes.length}` + ); + } + + // Convert the little-endian bytes into a BigNumber + return BigNumber.from(bytes.reverse()); +}; diff --git a/src/types/CalltableSerialization.ts b/src/types/CalltableSerialization.ts index 8aaa9298..11556666 100644 --- a/src/types/CalltableSerialization.ts +++ b/src/types/CalltableSerialization.ts @@ -1,11 +1,21 @@ import { concat } from '@ethersproject/bytes'; -import { toBytesU16, toBytesU32 } from './ByteConverters'; +import { parseU16, parseU32, toBytesU16, toBytesU32 } from './ByteConverters'; +/** + * Represents a single field in the call table. + */ export class Field { readonly index: number; readonly offset: number; - readonly value: Uint8Array; + value: Uint8Array; + /** + * Constructs a new `Field` instance. + * + * @param index - The index of the field. + * @param offset - The offset of the field in the payload. + * @param value - The byte array value of the field. + */ constructor(index: number, offset: number, value: Uint8Array) { this.index = index; this.offset = offset; @@ -14,22 +24,35 @@ export class Field { /** * Calculates the serialized vector size for the given number of fields. - * @returns The size of the serialized vector. + * + * This method determines the size of the serialized vector required + * to store all fields, including their indices and offsets. + * + * @returns The size of the serialized vector in bytes. */ static serializedVecSize(): number { - return 4 + 4 * 2; + return 4 + 4 * 2; // Number of fields (4 bytes) + index/offset pairs (4 bytes each) } } +/** + * Handles serialization and deserialization of call table data. + * + * The `CalltableSerialization` class is responsible for managing a collection + * of fields and converting them into a byte array for serialization. It can + * also reconstruct the fields from a serialized byte array. + */ export class CalltableSerialization { private fields: Field[] = []; private currentOffset = 0; /** * Adds a field to the call table. - * @param index The field index. - * @param value The field value as a byte array. - * @returns The current instance of CalltableSerialization. + * + * @param index - The field index. + * @param value - The field value as a byte array. + * @returns The current instance of `CalltableSerialization`. + * @throws An error if the fields are not added in the correct index order. */ addField(index: number, value: Uint8Array): CalltableSerialization { if (this.fields.length !== index) { @@ -44,8 +67,9 @@ export class CalltableSerialization { } /** - * Serializes the call table to a byte array. - * @returns A Uint8Array representing the serialized call table. + * Serializes the call table into a byte array. + * + * @returns A `Uint8Array` representing the serialized call table. */ toBytes(): Uint8Array { const calltableBytes: Uint8Array[] = []; @@ -63,4 +87,59 @@ export class CalltableSerialization { return concat([...calltableBytes, ...payloadBytes]); } + + /** + * Retrieves a specific field by its index. + * + * @param index - The index of the field to retrieve. + * @returns The field value as a `Uint8Array`, or `undefined` if the field is not found. + */ + getField(index: number): Uint8Array | undefined { + const field = this.fields.find(f => f.index === index); + return field ? field.value : undefined; + } + + /** + * Deserializes a byte array into a `CalltableSerialization` object. + * + * This method reconstructs the call table and its fields from a serialized byte array. + * + * @param bytes - The serialized byte array. + * @returns A `CalltableSerialization` instance containing the deserialized fields. + * @throws An error if the byte array is invalid or missing required fields. + */ + static fromBytes(bytes: Uint8Array): CalltableSerialization { + const instance = new CalltableSerialization(); + let offset = 0; + + // Read the number of fields + const fieldCount = parseU32(bytes.slice(offset, offset + 4)); + offset += 4; + + const fields: Field[] = []; + for (let i = 0; i < fieldCount; i++) { + const index = parseU16(bytes.slice(offset, offset + 2)); + offset += 2; + const fieldOffset = parseU32(bytes.slice(offset, offset + 4)); + offset += 4; + + // Initialize each field with an empty value + fields.push(new Field(index, fieldOffset, new Uint8Array())); + } + + // Read the total payload size + const payloadSize = parseU32(bytes.slice(offset, offset + 4)); + offset += 4; + + // Extract field values based on their offsets + for (let i = 0; i < fields.length; i++) { + const start = fields[i].offset; + const end = i + 1 < fields.length ? fields[i + 1].offset : payloadSize; + fields[i].value = bytes.slice(offset + start, offset + end); + } + + instance.fields = fields; + instance.currentOffset = payloadSize; + return instance; + } } diff --git a/src/types/Deploy.ts b/src/types/Deploy.ts index ba05d48e..e6c846d7 100644 --- a/src/types/Deploy.ts +++ b/src/types/Deploy.ts @@ -13,7 +13,10 @@ import { TransactionCategory, TransactionHeader } from './Transaction'; -import { TransactionEntryPoint } from './TransactionEntryPoint'; +import { + TransactionEntryPoint, + TransactionEntryPointEnum +} from './TransactionEntryPoint'; import { InitiatorAddr } from './InitiatorAddr'; import { PaymentLimitedMode, PricingMode } from './PricingMode'; import { TransactionTarget } from './TransactionTarget'; @@ -360,14 +363,18 @@ export class Deploy { */ static newTransactionFromDeploy(deploy: Deploy): Transaction { let paymentAmount = 0; - const transactionEntryPoint: TransactionEntryPoint = new TransactionEntryPoint(); + let transactionEntryPoint: TransactionEntryPoint; let transactionCategory = TransactionCategory.Large; if (deploy.session.transfer) { transactionCategory = TransactionCategory.Mint; - transactionEntryPoint.transfer = {}; + transactionEntryPoint = new TransactionEntryPoint( + TransactionEntryPointEnum.Transfer + ); } else if (deploy.session.moduleBytes) { - transactionEntryPoint.call = {}; + transactionEntryPoint = new TransactionEntryPoint( + TransactionEntryPointEnum.Call + ); } else { let entryPoint = ''; @@ -380,7 +387,10 @@ export class Deploy { } else if (deploy.session.storedVersionedContractByName) { entryPoint = deploy.session.storedVersionedContractByName.entryPoint; } - transactionEntryPoint.custom = entryPoint; + transactionEntryPoint = new TransactionEntryPoint( + TransactionEntryPointEnum.Custom, + entryPoint + ); } const amountArgument = deploy.payment.getArgs(); diff --git a/src/types/Prepayment.ts b/src/types/Prepayment.ts new file mode 100644 index 00000000..836ee73e --- /dev/null +++ b/src/types/Prepayment.ts @@ -0,0 +1,46 @@ +import { jsonMember, jsonObject } from 'typedjson'; +import { HexBytes } from './HexBytes'; +import { Hash } from './key'; + +/** + * Represents a gas pre-payment in the blockchain system. + * + * This container includes details about the receipt, prepayment kind, + * and associated data required for the gas pre-payment process. + */ +@jsonObject +export class PrepaymentKind { + /** + * The receipt identifier for the gas pre-payment. + * + * This is a string representation that uniquely identifies the pre-payment receipt. + */ + @jsonMember({ + name: 'receipt', + constructor: Hash, + deserializer: json => Hash.fromJSON(json), + serializer: value => value.toJSON() + }) + receipt: Hash; + + /** + * The kind of pre-payment, represented as a byte. + * + * This value specifies the type or category of the pre-payment. + */ + @jsonMember({ + name: 'prepayment_data', + constructor: HexBytes, + deserializer: json => HexBytes.fromJSON(json), + serializer: value => value.toJSON() + }) + prepaymentData: HexBytes; + + /** + * The pre-payment data associated with this transaction. + * + * This is a string containing additional information or metadata for the pre-payment. + */ + @jsonMember({ name: 'prepayment_kind', constructor: Number }) + prepaymentKind: number; +} diff --git a/src/types/Reservation.ts b/src/types/Reservation.ts deleted file mode 100644 index 09e7b9e4..00000000 --- a/src/types/Reservation.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { jsonMember, jsonObject } from 'typedjson'; -import { HexBytes } from './HexBytes'; -import { Hash } from './key'; - -/** - * Represents a reservation, including a receipt, reservation data, and the type of reservation. - */ -@jsonObject -export class ReservationKind { - /** - * The receipt associated with the reservation. - * This is typically a unique identifier for the reservation. - */ - @jsonMember({ - name: 'receipt', - constructor: Hash, - deserializer: json => Hash.fromJSON(json), - serializer: value => value.toJSON() - }) - receipt: Hash; - - /** - * The reservation data, represented as a `HexBytes` object. - * This can contain specific details regarding the reservation, encoded as hex. - */ - @jsonMember({ - name: 'reservation_data', - constructor: HexBytes, - deserializer: json => HexBytes.fromJSON(json), - serializer: value => value.toJSON() - }) - reservationData: HexBytes; - - /** - * The kind of reservation, represented as a number. - * This field can be used to distinguish different types of reservations. - */ - @jsonMember({ name: 'reservation_kind', constructor: Number }) - reservationKind: number; - - /** - * Creates a new instance of `ReservationKind`. - * - * @param receipt The receipt associated with the reservation. - * @param reservationData The reservation data encoded as hex. - * @param reservationKind The type of the reservation, represented by a number. - */ - constructor( - receipt: Hash, - reservationData: HexBytes, - reservationKind: number - ) { - this.receipt = receipt; - this.reservationData = reservationData; - this.reservationKind = reservationKind; - } -} diff --git a/src/types/StoredValue.ts b/src/types/StoredValue.ts index 1e064ceb..b605d376 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 f8e5e2e6..abe69ac9 100644 --- a/src/types/Transaction.test.ts +++ b/src/types/Transaction.test.ts @@ -9,7 +9,10 @@ import { FixedMode, PricingMode } from './PricingMode'; import { KeyAlgorithm } from './keypair/Algorithm'; import { SessionTarget, TransactionTarget } from './TransactionTarget'; -import { TransactionEntryPoint } from './TransactionEntryPoint'; +import { + TransactionEntryPoint, + TransactionEntryPointEnum +} from './TransactionEntryPoint'; import { TransactionScheduling } from './TransactionScheduling'; import { Args } from './Args'; import { @@ -19,7 +22,9 @@ import { CLValueUInt64 } from './clvalue'; import { PublicKey } from './keypair'; -import { TransactionV1Payload } from './TransactionPayload'; +import { TransactionV1Payload } from './TransactionV1Payload'; +import { Hash } from './key'; +import { assert, expect } from 'chai'; describe('Test Transaction', () => { it('should create a Transaction from TransactionV1', async () => { @@ -42,15 +47,29 @@ describe('Test Transaction', () => { id: CLValueOption.newCLOption(CLValueUInt64.newCLUint64(3)) }); + const sessionTarget = new SessionTarget(); + + sessionTarget.runtime = 'VmCasperV1'; + sessionTarget.transferredValue = 1000; + sessionTarget.moduleBytes = Uint8Array.from([1]); + sessionTarget.isInstallUpgrade = false; + sessionTarget.seed = Hash.fromHex( + '8bf9d406ab901428d43ecd3a6f214b864e7ef8316934e5e0f049650a65b40d73' + ); + const transactionPayload = TransactionV1Payload.build({ initiatorAddr: new InitiatorAddr(keys.publicKey), ttl: new Duration(1800000), args, timestamp: new Timestamp(new Date()), category: 2, - entryPoint: new TransactionEntryPoint(undefined, {}), + entryPoint: new TransactionEntryPoint(TransactionEntryPointEnum.Call), scheduling: new TransactionScheduling({}), - transactionTarget: new TransactionTarget(new SessionTarget()), + transactionTarget: new TransactionTarget( + undefined, + undefined, + sessionTarget + ), chainName: 'casper-net-1', pricingMode }); @@ -59,17 +78,14 @@ describe('Test Transaction', () => { await transaction.sign(keys); const toJson = TransactionV1.toJson(transaction); - console.log(toJson); + const parsed = TransactionV1.fromJSON(toJson); - // const parsed = TransactionV1.fromJSON(toJson); + const transactionPaymentAmount = parsed.payload.args.args + .get('amount')! + .toString(); - // const transactionPaymentAmount = parsed.body.args.args - // .get('amount')! - // .toString(); - // - // assert.deepEqual(parsed.approvals[0].signer, keys.publicKey); - // expect(transaction.body).to.deep.equal(transactionBody); - // expect(transaction.header).to.deep.equal(transactionHeader); - // assert.deepEqual(parseInt(transactionPaymentAmount, 10), paymentAmount); + assert.deepEqual(parsed.approvals[0].signer, keys.publicKey); + expect(transaction.payload).to.deep.equal(transactionPayload); + assert.deepEqual(parseInt(transactionPaymentAmount, 10), paymentAmount); }); }); diff --git a/src/types/Transaction.ts b/src/types/Transaction.ts index 506e7eee..e3d1cb5e 100644 --- a/src/types/Transaction.ts +++ b/src/types/Transaction.ts @@ -14,18 +14,13 @@ import { PrivateKey } from './keypair/PrivateKey'; import { Args } from './Args'; import { deserializeArgs, serializeArgs } from './SerializationUtils'; import { byteHash } from './ByteConverters'; -import { TransactionV1Payload } from './TransactionPayload'; +import { TransactionV1Payload } from './TransactionV1Payload'; /** * Custom error class for handling transaction-related errors. */ export class TransactionError extends Error {} -/** - * Error to indicate an invalid body hash in a transaction. - */ -export const ErrInvalidBodyHash = new TransactionError('invalid body hash'); - /** * Error to indicate an invalid transaction hash. */ @@ -124,7 +119,14 @@ export class TransactionV1 { /** * The header of the transaction. */ - @jsonMember({ name: 'payload', constructor: TransactionV1Payload }) + @jsonMember({ + name: 'payload', + constructor: TransactionV1Payload, + deserializer: json => { + if (!json) return; + return TransactionV1Payload.fromJSON(json); + } + }) public payload: TransactionV1Payload; /** diff --git a/src/types/TransactionEntryPoint.ts b/src/types/TransactionEntryPoint.ts index b20816d3..9b5317ba 100644 --- a/src/types/TransactionEntryPoint.ts +++ b/src/types/TransactionEntryPoint.ts @@ -1,5 +1,4 @@ import { jsonObject, jsonMember } from 'typedjson'; - import { CLValueString } from './clvalue'; import { CalltableSerialization } from './CalltableSerialization'; @@ -22,163 +21,94 @@ export enum TransactionEntryPointEnum { } /** - * Enum representing the tags for different transaction entry points. This is used for efficient storage and comparison. + * Enum representing the unique tags associated with each transaction entry point. + * These tags are used to simplify storage and facilitate efficient comparison of entry points. */ export enum TransactionEntryPointTag { + Custom = 0, Call = 1, - Transfer, - AddBid, - WithdrawBid, - Delegate, - Undelegate, - Redelegate, - ActivateBid, - ChangeBidPublicKey, - AddReservations, - CancelReservations + Transfer = 2, + AddBid = 3, + WithdrawBid = 4, + Delegate = 5, + Undelegate = 6, + Redelegate = 7, + ActivateBid = 8, + ChangeBidPublicKey = 9, + AddReservations = 10, + CancelReservations = 11 } /** - * Represents a transaction entry point, which can be one of several predefined actions or a custom action. - * This class contains multiple fields that correspond to different transaction actions. + * Represents a transaction entry point, which defines an action to be executed within the system. + * This class supports predefined entry points as well as custom-defined actions. */ @jsonObject export class TransactionEntryPoint { /** - * Custom entry point, where the value can be a string representing a custom action. - */ - @jsonMember({ constructor: String, name: 'Custom' }) - custom?: string; - - /** - * The transfer action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'Transfer' }) - transfer?: Record; - - /** - * The add bid action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'AddBid' }) - addBid?: Record; - - /** - * The withdraw bid action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'WithdrawBid' }) - withdrawBid?: Record; - - /** - * The delegate action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'Delegate' }) - delegate?: Record; - - /** - * The undelegate action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'Undelegate' }) - undelegate?: Record; - - /** - * The redelegate action as a generic object. + * The type of transaction entry point, represented as an enum. */ - @jsonMember({ constructor: Object, name: 'Redelegate' }) - redelegate?: Record; + @jsonMember({ constructor: String }) + type: TransactionEntryPointEnum; /** - * The activate bid action as a generic object. + * Custom entry point identifier, used when the `type` is `Custom`. */ - @jsonMember({ constructor: Object, name: 'ActivateBid' }) - activateBid?: Record; + @jsonMember({ constructor: String }) + customEntryPoint?: string; /** - * The change bid public key action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'ChangeBidPublicKey' }) - changeBidPublicKey?: Record; - - /** - * The call action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'Call' }) - call?: Record; - - /** - * The call action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'AddReservations' }) - addReservations?: Record; - - /** - * The call action as a generic object. - */ - @jsonMember({ constructor: Object, name: 'CancelReservations' }) - cancelReservations?: Record; - - /** - * Creates a new `TransactionEntryPoint` instance, where each parameter corresponds to a specific entry point action. + * Initializes a new `TransactionEntryPoint` instance. * - * @param custom A custom entry point action represented as a string. - * @param transfer The transfer action, represented as a generic object. - * @param addBid The add bid action, represented as a generic object. - * @param withdrawBid The withdraw bid action, represented as a generic object. - * @param delegate The delegate action, represented as a generic object. - * @param undelegate The undelegate action, represented as a generic object. - * @param redelegate The redelegate action, represented as a generic object. - * @param activateBid The activate bid action, represented as a generic object. - * @param changeBidPublicKey The change bid public key action, represented as a generic object. - * @param call The call action, represented as a generic object. + * @param type - The type of transaction entry point. + * @param customEntryPoint - An optional identifier for custom entry points. */ - constructor( - custom?: string, - transfer?: Record, - addBid?: Record, - withdrawBid?: Record, - delegate?: Record, - undelegate?: Record, - redelegate?: Record, - activateBid?: Record, - changeBidPublicKey?: Record, - call?: Record, - addReservations?: Record, - cancelReservations?: Record - ) { - this.custom = custom; - this.transfer = transfer; - this.addBid = addBid; - this.withdrawBid = withdrawBid; - this.delegate = delegate; - this.undelegate = undelegate; - this.redelegate = redelegate; - this.activateBid = activateBid; - this.changeBidPublicKey = changeBidPublicKey; - this.call = call; - this.addReservations = addReservations; - this.cancelReservations = cancelReservations; + constructor(type: TransactionEntryPointEnum, customEntryPoint?: string) { + if (type === TransactionEntryPointEnum.Custom && !customEntryPoint) { + throw new Error( + 'When specifying Custom entry point, customEntryPoint must be provided' + ); + } + this.type = type; + this.customEntryPoint = customEntryPoint; } /** - * Returns the tag corresponding to the transaction entry point. This helps identify the entry point in a compact manner. + * Retrieves the unique tag associated with the transaction entry point. + * Tags are used to identify entry points in a compact and efficient manner. * - * @returns The tag number associated with the entry point. + * @returns The tag number for the entry point. + * @throws An error if the entry point is unknown. */ - private tag(): number { - if (this.transfer) return TransactionEntryPointTag.Transfer; - if (this.addBid) return TransactionEntryPointTag.AddBid; - if (this.withdrawBid) return TransactionEntryPointTag.WithdrawBid; - if (this.delegate) return TransactionEntryPointTag.Delegate; - if (this.undelegate) return TransactionEntryPointTag.Undelegate; - if (this.redelegate) return TransactionEntryPointTag.Redelegate; - if (this.activateBid) return TransactionEntryPointTag.ActivateBid; - if (this.changeBidPublicKey) - return TransactionEntryPointTag.ChangeBidPublicKey; - if (this.call) return TransactionEntryPointTag.Call; - if (this.addReservations) return TransactionEntryPointTag.AddReservations; - if (this.cancelReservations) - return TransactionEntryPointTag.CancelReservations; - - throw new Error('Unknown TransactionEntryPointTag'); + public tag(): number { + switch (this.type) { + case TransactionEntryPointEnum.Transfer: + return TransactionEntryPointTag.Transfer; + case TransactionEntryPointEnum.AddBid: + return TransactionEntryPointTag.AddBid; + case TransactionEntryPointEnum.WithdrawBid: + return TransactionEntryPointTag.WithdrawBid; + case TransactionEntryPointEnum.Delegate: + return TransactionEntryPointTag.Delegate; + case TransactionEntryPointEnum.Undelegate: + return TransactionEntryPointTag.Undelegate; + case TransactionEntryPointEnum.Redelegate: + return TransactionEntryPointTag.Redelegate; + case TransactionEntryPointEnum.ActivateBid: + return TransactionEntryPointTag.ActivateBid; + case TransactionEntryPointEnum.ChangeBidPublicKey: + return TransactionEntryPointTag.ChangeBidPublicKey; + case TransactionEntryPointEnum.Call: + return TransactionEntryPointTag.Call; + case TransactionEntryPointEnum.AddReservations: + return TransactionEntryPointTag.AddReservations; + case TransactionEntryPointEnum.CancelReservations: + return TransactionEntryPointTag.CancelReservations; + case TransactionEntryPointEnum.Custom: + return TransactionEntryPointTag.Custom; + default: + throw new Error('Unknown TransactionEntryPointTag'); + } } /** @@ -188,98 +118,177 @@ export class TransactionEntryPoint { */ bytes(): Uint8Array { const calltableSerialization = new CalltableSerialization(); - calltableSerialization.addField(0, Uint8Array.of(this.tag())); + const tag = this.tag(); + calltableSerialization.addField(0, Uint8Array.from([tag])); - if (this.custom) { - const calltableSerialization = new CalltableSerialization(); - calltableSerialization.addField(0, Uint8Array.of(1)); - calltableSerialization.addField( + if ( + this.type === TransactionEntryPointEnum.Custom && + this.customEntryPoint + ) { + const customSerialization = new CalltableSerialization(); + customSerialization.addField(0, Uint8Array.from([1])); + customSerialization.addField( 1, - CLValueString.newCLString(this.custom).bytes() + CLValueString.newCLString(this.customEntryPoint).bytes() ); - return calltableSerialization.toBytes(); + calltableSerialization.addField(1, customSerialization.toBytes()); } + return calltableSerialization.toBytes(); } /** * Converts the transaction entry point to a JSON-compatible format. * - * @returns A JSON-compatible representation of the transaction entry point. - * @throws An error if the entry point is unknown. + * @returns A JSON object representing the transaction entry point. */ toJSON(): unknown { - if (this.custom) { - return { Custom: this.custom }; + if ( + this.type === TransactionEntryPointEnum.Custom && + this.customEntryPoint + ) { + return { Custom: this.customEntryPoint }; } - if (this.transfer) return TransactionEntryPointEnum.Transfer; - if (this.addBid) return TransactionEntryPointEnum.AddBid; - if (this.withdrawBid) return TransactionEntryPointEnum.WithdrawBid; - if (this.delegate) return TransactionEntryPointEnum.Delegate; - if (this.undelegate) return TransactionEntryPointEnum.Undelegate; - if (this.redelegate) return TransactionEntryPointEnum.Redelegate; - if (this.activateBid) return TransactionEntryPointEnum.ActivateBid; - if (this.changeBidPublicKey) - return TransactionEntryPointEnum.ChangeBidPublicKey; - if (this.call) return TransactionEntryPointEnum.Call; - - throw new Error('Unknown entry point'); + return this.type; } /** * Creates a `TransactionEntryPoint` instance from a JSON representation. * - * @param json The JSON representation of the entry point. + * @param json - The JSON representation of the transaction entry point. * @returns A `TransactionEntryPoint` instance. - * @throws An error if the entry point is unknown. + * @throws An error if the JSON is invalid or the entry point is unknown. */ static fromJSON(json: any): TransactionEntryPoint { - const entryPoint = new TransactionEntryPoint(); if (json instanceof Object && json.Custom) { - entryPoint.custom = json.Custom; - return entryPoint; + return new TransactionEntryPoint( + TransactionEntryPointEnum.Custom, + json.Custom + ); } switch (json) { case TransactionEntryPointEnum.Transfer: - entryPoint.transfer = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Transfer); case TransactionEntryPointEnum.AddBid: - entryPoint.addBid = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.AddBid); case TransactionEntryPointEnum.WithdrawBid: - entryPoint.withdrawBid = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.WithdrawBid); case TransactionEntryPointEnum.Delegate: - entryPoint.delegate = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Delegate); case TransactionEntryPointEnum.Undelegate: - entryPoint.undelegate = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Undelegate); case TransactionEntryPointEnum.Redelegate: - entryPoint.redelegate = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Redelegate); case TransactionEntryPointEnum.ActivateBid: - entryPoint.activateBid = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.ActivateBid); case TransactionEntryPointEnum.ChangeBidPublicKey: - entryPoint.changeBidPublicKey = {}; - break; + return new TransactionEntryPoint( + TransactionEntryPointEnum.ChangeBidPublicKey + ); case TransactionEntryPointEnum.Call: - entryPoint.call = {}; - break; - case TransactionEntryPointEnum.CancelReservations: - entryPoint.cancelReservations = {}; - break; + return new TransactionEntryPoint(TransactionEntryPointEnum.Call); case TransactionEntryPointEnum.AddReservations: - entryPoint.addReservations = {}; - break; + return new TransactionEntryPoint( + TransactionEntryPointEnum.AddReservations + ); + case TransactionEntryPointEnum.CancelReservations: + return new TransactionEntryPoint( + TransactionEntryPointEnum.CancelReservations + ); default: throw new Error('Unknown entry point'); } + } + + /** + * Deserializes a `TransactionEntryPoint` from its byte representation. + * + * This method takes a serialized byte array and reconstructs a `TransactionEntryPoint` object. + * It supports multiple entry point types, including both predefined and custom entry points. + * + * @param bytes - The byte array representing the serialized `TransactionEntryPoint`. + * @returns A deserialized `TransactionEntryPoint` instance. + * @throws Will throw an error if the byte array is invalid or has missing fields. + * + * ### Example + * ```typescript + * const serializedBytes = new Uint8Array([0, 1, 2, 3, ...]); + * const entryPoint = TransactionEntryPoint.fromBytes(serializedBytes); + * console.log(entryPoint.type); // Logs the entry point type + * ``` + */ + static fromBytes(bytes: Uint8Array): TransactionEntryPoint { + const calltableSerialization = CalltableSerialization.fromBytes(bytes); + const tagBytes = calltableSerialization.getField(0); + + if (!tagBytes || tagBytes.length !== 1) { + throw new Error('Invalid tag bytes'); + } + + const tag = tagBytes[0]; + + const type = (() => { + switch (tag) { + case TransactionEntryPointTag.Transfer: + return TransactionEntryPointEnum.Transfer; + case TransactionEntryPointTag.AddBid: + return TransactionEntryPointEnum.AddBid; + case TransactionEntryPointTag.WithdrawBid: + return TransactionEntryPointEnum.WithdrawBid; + case TransactionEntryPointTag.Delegate: + return TransactionEntryPointEnum.Delegate; + case TransactionEntryPointTag.Undelegate: + return TransactionEntryPointEnum.Undelegate; + case TransactionEntryPointTag.Redelegate: + return TransactionEntryPointEnum.Redelegate; + case TransactionEntryPointTag.ActivateBid: + return TransactionEntryPointEnum.ActivateBid; + case TransactionEntryPointTag.ChangeBidPublicKey: + return TransactionEntryPointEnum.ChangeBidPublicKey; + case TransactionEntryPointTag.Call: + return TransactionEntryPointEnum.Call; + case TransactionEntryPointTag.AddReservations: + return TransactionEntryPointEnum.AddReservations; + case TransactionEntryPointTag.CancelReservations: + return TransactionEntryPointEnum.CancelReservations; + case TransactionEntryPointTag.Custom: + return TransactionEntryPointEnum.Custom; + default: + throw new Error('Unknown tag'); + } + })(); + + if (type === TransactionEntryPointEnum.Custom) { + const customBytes = calltableSerialization.getField(1); + + if (!customBytes) { + throw new Error('Missing custom entry point bytes for Custom type'); + } + + const customSerialization = CalltableSerialization.fromBytes(customBytes); + + const customFlag = customSerialization.getField(0); + + if (!customFlag || customFlag[0] !== 1) { + throw new Error('Invalid flag for Custom type'); + } + + const customEntryPointBytes = customSerialization.getField(1); + + if (!customEntryPointBytes) { + throw new Error('Invalid custom entry point bytes'); + } + + const customEntryPoint = CLValueString.fromBytes( + customEntryPointBytes + ).result.toString(); + + return new TransactionEntryPoint(type, customEntryPoint); + } - return entryPoint; + return new TransactionEntryPoint(type); } } diff --git a/src/types/TransactionScheduling.ts b/src/types/TransactionScheduling.ts index b797032e..7514d02d 100644 --- a/src/types/TransactionScheduling.ts +++ b/src/types/TransactionScheduling.ts @@ -3,7 +3,7 @@ import { jsonObject, jsonMember } from 'typedjson'; import { Timestamp } from './Time'; import { CLValueUInt64 } from './clvalue'; import { CalltableSerialization } from './CalltableSerialization'; -import { toBytesU64 } from './ByteConverters'; +import { fromBytesU64, toBytesU64 } from './ByteConverters'; /** * Enum representing the scheduling tags for transaction scheduling types. @@ -21,7 +21,7 @@ export enum TransactionSchedulingTag { * Represents the scheduling for a transaction in a future era. */ @jsonObject -class FutureEraScheduling { +export class FutureEraScheduling { /** * The ID of the future era when the transaction is scheduled to occur. */ @@ -53,7 +53,7 @@ class FutureEraScheduling { * Represents the scheduling for a transaction in a future timestamp. */ @jsonObject -class FutureTimestampScheduling { +export class FutureTimestampScheduling { /** * The timestamp when the transaction is scheduled to occur. */ @@ -209,4 +209,76 @@ export class TransactionScheduling { } throw new Error('Unknown scheduling type'); } + + /** + * Deserializes a `Uint8Array` into a `TransactionScheduling` instance. + * + * This method parses a byte array representation of a `TransactionScheduling` + * object, determines the type of scheduling based on the tag, and reconstructs + * the appropriate instance. + * + * @param bytes - The byte array to be deserialized. + * @returns A `TransactionScheduling` instance based on the serialized data. + * @throws Error - If the byte array is invalid, missing required fields, or contains + * an unrecognized scheduling tag. + * + * ### Tags and Their Associated Schedulers: + * - `TransactionSchedulingTag.Native`: Represents a native scheduling target. + * - `TransactionSchedulingTag.FutureEra`: Represents a scheduling target tied to a future era. + * - `TransactionSchedulingTag.FutureTimestamp`: Represents a scheduling target tied to a future timestamp. + * + * ### Example + * ```typescript + * const bytes = new Uint8Array([...]); // Provide valid TransactionScheduling bytes + * const scheduling = TransactionScheduling.fromBytes(bytes); + * console.log(scheduling); // Parsed TransactionScheduling instance + * ``` + */ + static fromBytes(bytes: Uint8Array): TransactionScheduling { + const calltable = CalltableSerialization.fromBytes(bytes); + + const tagBytes = calltable.getField(0); + if (!tagBytes || tagBytes.length !== 1) { + throw new Error( + 'Invalid or missing tag in serialized TransactionScheduling' + ); + } + const tag = tagBytes[0]; + + switch (tag) { + case TransactionSchedulingTag.Native: + return new TransactionScheduling({}); + + case TransactionSchedulingTag.FutureEra: { + const eraIDBytes = calltable.getField(1); + if (!eraIDBytes) { + throw new Error('Missing eraID field for FutureEra scheduling'); + } + const eraID = fromBytesU64(eraIDBytes).toNumber(); + return new TransactionScheduling( + undefined, + new FutureEraScheduling(eraID) + ); + } + + case TransactionSchedulingTag.FutureTimestamp: { + const timestampBytes = calltable.getField(1); + if (!timestampBytes) { + throw new Error( + 'Missing timestamp field for FutureTimestamp scheduling' + ); + } + const timestampMs = fromBytesU64(timestampBytes).toNumber(); + const timestamp = new Timestamp(new Date(timestampMs)); + return new TransactionScheduling( + undefined, + undefined, + new FutureTimestampScheduling(timestamp) + ); + } + + default: + throw new Error(`Unknown TransactionSchedulingTag: ${tag}`); + } + } } diff --git a/src/types/TransactionTarget.ts b/src/types/TransactionTarget.ts index f52563cc..8a23e4e6 100644 --- a/src/types/TransactionTarget.ts +++ b/src/types/TransactionTarget.ts @@ -19,6 +19,7 @@ import { byteArrayJsonDeserializer, byteArrayJsonSerializer } from './SerializationUtils'; +import { fromBytesU64 } from './ByteConverters'; /** * Represents the invocation target for a transaction identified by a package hash. @@ -173,6 +174,107 @@ export class TransactionInvocationTarget { 'Can not convert TransactionInvocationTarget to bytes. Missing values from initialization' ); } + + /** + * Deserializes a `Uint8Array` into a `TransactionInvocationTarget` instance. + * + * This method reconstructs a `TransactionInvocationTarget` object from its serialized byte array representation. + * The type of invocation target is determined by the tag extracted from the serialized data. + * + * @param bytes - The serialized byte array representing a `TransactionInvocationTarget`. + * @returns A deserialized `TransactionInvocationTarget` instance. + * @throws Error - If the byte array is invalid, missing required fields, or contains an unrecognized tag. + * + * ### Tags and Their Associated Targets: + * - `0`: Represents an invocation target identified by a hash (`ByHash`). + * - `1`: Represents an invocation target identified by a name (`ByName`). + * - `2`: Represents an invocation target identified by a package hash and an optional version (`ByPackageHash`). + * - `3`: Represents an invocation target identified by a package name and an optional version (`ByPackageName`). + * + * ### Example + * ```typescript + * const bytes = new Uint8Array([...]); // Provide valid TransactionInvocationTarget bytes + * const invocationTarget = TransactionInvocationTarget.fromBytes(bytes); + * console.log(invocationTarget); // Parsed TransactionInvocationTarget instance + * ``` + */ + static fromBytes(bytes: Uint8Array): TransactionInvocationTarget { + const calltable = CalltableSerialization.fromBytes(bytes); + + const tagBytes = calltable.getField(0); + if (!tagBytes || tagBytes.length !== 1) { + throw new Error( + 'Invalid or missing tag in serialized TransactionInvocationTarget' + ); + } + const tag = tagBytes[0]; + const invocationTarget = new TransactionInvocationTarget(); + + switch (tag) { + case 0: { + const hashBytes = calltable.getField(1); + if (!hashBytes) { + throw new Error('Missing hash field for ByHash target'); + } + invocationTarget.byHash = Hash.fromBytes(hashBytes).result; + return invocationTarget; + } + + case 1: { + const nameBytes = calltable.getField(1); + if (!nameBytes) { + throw new Error('Missing name field for ByName target'); + } + invocationTarget.byName = CLValueString.fromBytes( + nameBytes + ).result.toString(); + return invocationTarget; + } + + case 2: { + const packageHashBytes = calltable.getField(1); + const versionBytes = calltable.getField(2); + + if (!packageHashBytes || !versionBytes) { + throw new Error('Missing fields for ByPackageHash target'); + } + + const packageHash = Hash.fromBytes(packageHashBytes); + const version = CLValueOption.fromBytes( + versionBytes, + new CLTypeOption(CLTypeUInt32) + ).result.toString(); + const byPackageHash = new ByPackageHashInvocationTarget(); + byPackageHash.addr = packageHash.result; + byPackageHash.version = BigNumber.from(version).toNumber(); + invocationTarget.byPackageHash = byPackageHash; + return invocationTarget; + } + + case 3: { + const nameBytes = calltable.getField(1); + const versionBytes = calltable.getField(2); + + if (!nameBytes || !versionBytes) { + throw new Error('Missing fields for ByPackageName target'); + } + + const name = CLValueString.fromBytes(nameBytes).result.toString(); + const version = CLValueOption.fromBytes( + versionBytes, + new CLTypeOption(CLTypeUInt32) + ).result.toString(); + const byPackageName = new ByPackageNameInvocationTarget(); + byPackageName.version = BigNumber.from(version).toNumber(); + byPackageName.name = name; + invocationTarget.byPackageName = byPackageName; + return invocationTarget; + } + + default: + throw new Error(`Unknown TransactionInvocationTarget tag: ${tag}`); + } + } } /** @@ -500,4 +602,98 @@ export class TransactionTarget { return new TransactionTarget(); } + + /** + * Deserializes a `Uint8Array` into a `TransactionTarget` instance. + * + * This method reconstructs a `TransactionTarget` object from its serialized byte array representation. + * The type of transaction target is determined by the tag extracted from the serialized data. + * + * @param bytes - The serialized byte array representing a `TransactionTarget`. + * @returns A deserialized `TransactionTarget` instance. + * @throws Error - If the byte array is invalid, missing required fields, or contains an unrecognized tag. + * + * ### Tags and Their Associated Targets: + * - `0`: Represents a Native target. + * - `1`: Represents a Stored target, including an invocation target, runtime, and transferred value. + * - `2`: Represents a Session target, including module bytes, runtime, transferred value, install upgrade flag, and seed. + * + * ### Example + * ```typescript + * const bytes = new Uint8Array([...]); // Provide valid TransactionTarget bytes + * const transactionTarget = TransactionTarget.fromBytes(bytes); + * console.log(transactionTarget); // Parsed TransactionTarget instance + * ``` + */ + static fromBytes(bytes: Uint8Array): TransactionTarget { + const calltable = CalltableSerialization.fromBytes(bytes); + + const tagBytes = calltable.getField(0); + if (!tagBytes || tagBytes.length !== 1) { + throw new Error('Invalid or missing tag in serialized TransactionTarget'); + } + + const tag = tagBytes[0]; + switch (tag) { + case 0: + return new TransactionTarget({}); + + case 1: { + const storedBytes = calltable.getField(1); + const runtimeBytes = calltable.getField(2); + const transferredValueBytes = calltable.getField(3); + + if (!storedBytes || !runtimeBytes || !transferredValueBytes) { + throw new Error('Incomplete serialized data for Stored target'); + } + + const storedTarget = new StoredTarget(); + storedTarget.id = TransactionInvocationTarget.fromBytes(storedBytes); + storedTarget.runtime = CLValueString.fromBytes( + runtimeBytes + ).result.toString() as TransactionRuntime; + storedTarget.transferredValue = fromBytesU64( + transferredValueBytes + ).toNumber(); + + return new TransactionTarget(undefined, storedTarget); + } + + case 2: { + const moduleBytes = calltable.getField(3); + const runtimeBytesSession = calltable.getField(2); + const transferredValueBytesSession = calltable.getField(4); + const isInstallUpgradeBytes = calltable.getField(1); + const seedBytes = calltable.getField(5); + + if ( + !moduleBytes || + !runtimeBytesSession || + !transferredValueBytesSession || + !isInstallUpgradeBytes || + !seedBytes + ) { + throw new Error('Incomplete serialized data for Session target'); + } + + const sessionTarget = new SessionTarget(); + sessionTarget.moduleBytes = moduleBytes; + sessionTarget.runtime = CLValueString.fromBytes( + runtimeBytesSession + ).result.toString() as TransactionRuntime; + sessionTarget.transferredValue = fromBytesU64( + transferredValueBytesSession + ).toNumber(); + sessionTarget.isInstallUpgrade = CLValueBool.fromBytes( + isInstallUpgradeBytes + ).result.getValue(); + sessionTarget.seed = Hash.fromBytes(seedBytes).result; + + return new TransactionTarget(undefined, undefined, sessionTarget); + } + + default: + throw new Error(`Unknown TransactionTarget tag: ${tag}`); + } + } } diff --git a/src/types/TransactionPayload.ts b/src/types/TransactionV1Payload.ts similarity index 55% rename from src/types/TransactionPayload.ts rename to src/types/TransactionV1Payload.ts index 41c05a13..eacda320 100644 --- a/src/types/TransactionPayload.ts +++ b/src/types/TransactionV1Payload.ts @@ -5,7 +5,7 @@ import { toBytesU32, toBytesU64 } from './ByteConverters'; -import { jsonMember, jsonObject } from 'typedjson'; +import { jsonMember, jsonObject, TypedJSON } from 'typedjson'; import { InitiatorAddr } from './InitiatorAddr'; import { Duration, Timestamp } from './Time'; import { PricingMode } from './PricingMode'; @@ -19,6 +19,9 @@ import { byteArrayJsonSerializer } from './SerializationUtils'; +/** + * Interface representing the parameters required to build a `TransactionV1Payload`. + */ interface ITransactionPayloadBuildParams { initiatorAddr: InitiatorAddr; args: Args; @@ -32,24 +35,49 @@ interface ITransactionPayloadBuildParams { chainName: string; } +/** + * Class representing a collection of payload fields used in transaction serialization. + */ export class PayloadFields { + /** + * Map storing the fields of the payload where the key is the field identifier and the value is the serialized data. + */ public fields: Map = new Map(); + /** + * Adds a field to the payload. + * + * @param field - The identifier of the field. + * @param value - The serialized value of the field. + */ addField(field: number, value: Uint8Array): void { this.fields.set(field, value); } + getFieldValue(fieldIndex: number) { + return this.fields.get(fieldIndex); + } + + /** + * Serializes the payload fields into a `Uint8Array`. + * + * @returns A `Uint8Array` containing the serialized payload fields. + */ toBytes(): Uint8Array { const fieldsCount = toBytesU32(this.fields.size); - const fieldEntries = Array.from(this.fields.entries()).map( - ([key, value]) => { - return concat([toBytesU16(key), value]); - } + const fieldEntries = Array.from(this.fields.entries()).map(([key, value]) => + concat([toBytesU16(key), value]) ); return concat([fieldsCount, ...fieldEntries]); } + /** + * Deserializes a JSON object into a `PayloadFields` instance. + * + * @param json - The JSON representation of the payload fields. + * @returns A `PayloadFields` instance. + */ static fromJSON(json: Record): PayloadFields { const payload = new PayloadFields(); for (const [key, value] of Object.entries(json)) { @@ -61,6 +89,11 @@ export class PayloadFields { return payload; } + /** + * Converts the payload fields to a JSON object. + * + * @returns A JSON representation of the payload fields. + */ toJSON(): Record { const result: Record = {}; const fieldEntries = Array.from(this.fields.entries()); @@ -71,6 +104,9 @@ export class PayloadFields { } } +/** + * Class representing the payload for a V1 transaction. + */ @jsonObject export class TransactionV1Payload { /** @@ -113,33 +149,51 @@ export class TransactionV1Payload { public pricingMode: PricingMode; /** - * The name of the blockchain. + * The name of the blockchain on which the transaction is executed. */ @jsonMember({ name: 'chain_name', constructor: String }) public chainName: string; /** - * The name of the blockchain. + * Additional serialized fields associated with the transaction. */ @jsonMember({ name: 'fields', - serializer: value => { - if (!value) return; - return value.toJSON(); - }, - deserializer: json => { - if (!json) return; - return PayloadFields.fromJSON(json); - } + serializer: value => (value ? value.toJSON() : undefined), + deserializer: json => (json ? PayloadFields.fromJSON(json) : undefined) }) public fields: PayloadFields; + /** + * Arguments associated with the transaction. + */ public args: Args; + + /** + * The target of the transaction. + */ public target: TransactionTarget; + + /** + * The entry point of the transaction. + */ public entryPoint: TransactionEntryPoint; + + /** + * The scheduling information for the transaction. + */ public scheduling: TransactionScheduling; + + /** + * Optional category of the transaction. + */ public category?: number; + /** + * Serializes the transaction payload into a `Uint8Array`. + * + * @returns A `Uint8Array` representing the serialized transaction payload. + */ public toBytes(): Uint8Array { const calltableSerialization = new CalltableSerialization(); const fields = new PayloadFields(); @@ -161,6 +215,12 @@ export class TransactionV1Payload { return calltableSerialization.toBytes(); } + /** + * Creates a `TransactionV1Payload` instance from the provided parameters. + * + * @param params - The parameters for building the transaction payload. + * @returns A new `TransactionV1Payload` instance. + */ public static build({ initiatorAddr, args, @@ -194,4 +254,61 @@ export class TransactionV1Payload { return transactionPayload; } + + /** + * Deserializes a JSON object into a `TransactionV1Payload` instance. + * + * This method parses a JSON object to create a `TransactionV1Payload` instance. + * Additionally, it deserializes nested fields such as `args`, `target`, `entryPoint`, + * and `scheduling` from their respective byte representations if they are present. + * + * @param json - The JSON object representing a serialized `TransactionV1Payload`. + * @returns A deserialized `TransactionV1Payload` instance, or `undefined` if parsing fails. + * + * ### Example + * ```typescript + * const json = { + * fields: { + * // Provide serialized fields in JSON format + * } + * }; + * const transactionPayload = TransactionV1Payload.fromJSON(json); + * console.log(transactionPayload); // Parsed TransactionV1Payload instance or undefined + * ``` + */ + public static fromJSON(json: any): TransactionV1Payload | undefined { + const serializer = new TypedJSON(TransactionV1Payload); + const transactionPayload = serializer.parse(json); + + if (!transactionPayload) { + return undefined; + } + + const argsBytes = transactionPayload.fields.getFieldValue(0); + const targetBytes = transactionPayload.fields.getFieldValue(1); + const entryPointBytes = transactionPayload.fields.getFieldValue(2); + const schedulingBytes = transactionPayload.fields.getFieldValue(3); + + if (argsBytes) { + transactionPayload.args = Args.fromBytes(argsBytes); + } + + if (targetBytes) { + transactionPayload.target = TransactionTarget.fromBytes(targetBytes); + } + + if (entryPointBytes) { + transactionPayload.entryPoint = TransactionEntryPoint.fromBytes( + entryPointBytes + ); + } + + if (schedulingBytes) { + transactionPayload.scheduling = TransactionScheduling.fromBytes( + schedulingBytes + ); + } + + return transactionPayload; + } } diff --git a/src/types/clvalue/Parser.ts b/src/types/clvalue/Parser.ts index 59785aa4..98e877d8 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 0df2c5f2..ba93f653 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,7 +23,7 @@ export * from './MinimalBlockInfo'; export * from './NamedKey'; export * from './Package'; export * from './PricingMode'; -export * from './Reservation'; +export * from './Prepayment'; export * from './StoredValue'; export * from './Time'; export * from './Transaction';