Defining the Transaction Structure

We need to inherit (extend) base Transaction class to follow the GTI engine rules. The following steps are a walkthrough of how to develop a new Transaction structure class.

STEP 1: Define Your New Transaction Structure

Your custom transaction fields must be defined inside the BusinessRegistrationTransaction class. They follow the rules of the inherited Transaction class.

You can introduce any number of new fields and their respectful types. All new fields will be stored in the base transaction field called transaction.assets . The source-code snippet below introduces custom fields with the help of an IBusinessData interface.

1export interface IBusinessData {
2 name: string;
3 website: string;
4}

The defined interface makes use of new custom transaction fields stricter and is part of the serde process.

Information

Our Public API enables searching of transactions with new custom fields by design (no API changes needed)

STEP 2: Implementation Of The serde process

Information

The use of the term serde throughout this document refers to the processes of transaction serialization and deserialization.

We need to implement custom serde methods that will take care of the serde process for our newly introduced transaction fields. Abstract methods serialize() and deserialize() are defined by the base Transaction class, and are automatically called inside our custom class during the serde process.

The code snippet below is an excerpt example showing implementation of serde methods for a new BusinessRegistration transaction.

1export class BusinessRegistrationTransaction extends Transactions.Transaction {
2 public serialize(): ByteBuffer {
3 const { data } = this;
4 
5 const businessData = data.asset.businessData as IBusinessData;
6 
7 const nameBytes = Buffer.from(businessData.name, "utf8");
8 const websiteBytes = Buffer.from(businessData.website, "utf8");
9 
10 const buffer = new ByteBuffer(nameBytes.length + websiteBytes.length + 2, true);
11 
12 buffer.writeUint8(nameBytes.length);
13 buffer.append(nameBytes, "hex");
14 
15 buffer.writeUint8(websiteBytes.length);
16 buffer.append(websiteBytes, "hex");
17 
18 return buffer;
19 }
20 
21 public deserialize(buf: ByteBuffer): void {
22 const { data } = this;
23 const businessData = {} as IBusinessData;
24 const nameLength = buf.readUint8();
25 businessData.name = buf.readString(nameLength);
26 
27 const websiteLength = buf.readUint8();
28 businessData.website = buf.readString(websiteLength);
29 
30 data.asset = {
31 businessData,
32 };
33 }
34}

STEP 3: Define Schema Validation For The New Transaction Fields

Each custom transaction is accompanied by enforced schema validation. To achieve this we must extend base TransactionSchema and provide rules for the custom field validation. Schema is defined with AJV and we can access it by calling the getSchema() method inside your new transaction class, in our case the BusinessRegistrationTransaction class.

Warning

When implementing new transaction types, never allow plain strings in the transaction.asset, but always restrict to something that excludes null bytes (\u0000).

To forbid plain strings in the transaction.assets you can reuse some of already defined schemas , for example: { $ref: "hex" } or { $ref: "alphanumeric" } or { $ref: "publicKey" }. If no schema fits your requirements refer to the null byte regex { type: "string", pattern: "^[^\u0000]+$"} for the transaction.asset fields.

1public static getSchema(): Transactions.schemas.TransactionSchema {
2 return schemas.extend(schemas.transactionBaseSchema, {
3 $id: "businessData",
4 required: ["asset", "typeGroup"],
5 properties: {
6 type: { transactionType: BUSINESS_REGISTRATION_TYPE },
7 typeGroup: { const: 1001 },
8 amount: { bignumber: { minimum: 0, maximum: 0 } },
9 asset: {
10 type: "object",
11 required: ["businessData"],
12 properties: {
13 businessData: {
14 type: "object",
15 required: ["name", "website"],
16 properties: {
17 name: {
18 $ref: "genericName",
19 },
20 website: {
21 $ref: "uri",
22 },
23 },
24 },
25 },
26 },
27 },
28 });
29}

STEP 4: Define TypeGroup and Type

The typeGroup + type are used internally by Core to register a transaction. Non-core transactions have to define the typeGroup otherwise Core won’t be able to categorize them. All transactions (from the release of core v2.6) will be signed with typeGroup and type. By omitting the typeGroup value, core will fall back to typeGroup: 1, which is the default Core group. We define typeGroup + type in our BusinessRegistration class, like this:

1const BUSINESS_REGISTRATION_TYPE = 100;
2const BUSINESS_REGISTRATION_TYPE_GROUP = 1001;
3 
4export class BusinessRegistrationTransaction extends Transactions.Transaction {
5 public static typeGroup: number = BUSINESS_REGISTRATION_TYPE_GROUP;
6 public static type: number = BUSINESS_REGISTRATION_TYPE;
7 
8 // ... other source-code
Last updated 2 years ago
Edit Page
Share: