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)
serde
process
STEP 2: Implementation Of The 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