Implementing Transaction Handlers
The previous two classes Builder and Transaction, introduced a new transaction type, implemented the serde process, and created signed transaction payload. Handler class is connected with the blockchain protocol, following its strict mechanics such as consensus rules, transaction and block processing.
By inheriting default TransactionHandler
behavior we enforce existing GTI engine rules and provide options to implement additional transaction apply logic.
We will also make use of dependency injection to query the pool and the forged transactions (more details about that later).
1export class BusinessRegistrationTransactionHandler extends Handlers.TransactionHandler { 2 @Container.inject(Container.Identifiers.TransactionPoolQuery) 3 private readonly poolQuery!: Contracts.TransactionPool.Query; 4 5 @Container.inject(Container.Identifiers.TransactionHistoryService) 6 private readonly transactionHistoryService!: Contracts.Shared.TransactionHistoryService; 7 8 public getConstructor(): Transactions.TransactionConstructor { 9 return BusinessRegistrationTransaction;10 }11 // ...12}
Information
Apply logic consists of basic validation and verification protocol rules, for example, i.) check if there are enough funds in the wallet, ii.) check for duplicate transactions, iii.) if the received transaction is on the correct network (correct bridgechain), and many, many more.
We need to implement the following handler methods:
-
bootstrap
-
throwIfCannotBeApplied
-
emitEvents
-
throwIfCannotEnterPool
-
applyToSender
-
revertForSender
-
applyToRecipient
-
revertForRecipient
-
isActivated
You can have a look at the full implementation in the custom transaction github repository .
Each of the methods above has a special place in the blockchain protocol validation and verification process.
We will explain GTI TransactionHandler and the role it plays in our blockchain protocol in the following sections.
STEP 1: Define Custom Transaction Dependencies
We must define the Transaction Type registration order if our custom transaction (e.g.BusinessRegistrationTransaction ) depends on other transactions (e.g. MultiSignature )— in short, the MultiSignature transaction must be registered before ours. We define transaction dependencies by using the dependencies() method call, where we return an array of dependent classes.
1export class BusinessRegistrationTransactionHandler extends Handlers.TransactionHandler { 2 // ... 3 4 public getConstructor(): Transactions.TransactionConstructor { 5 return BusinessRegistrationTransaction; 6 } 7 8 public dependencies(): ReadonlyArray<Handlers.TransactionHandlerConstructor> { 9 return [];10 }11 // ...12}
STEP 2: Adding Attributes To Global State
Usually, we want to add custom properties to our global state (the walletManager class). These properties need to be quickly accessible (memoization) and searchable (indexed). We defined custom transaction fields and structure in here:
Usually, we want to add custom properties to our global state (the walletManager class). These properties need to be quickly accessible (memoization) and searchable (indexed) and are computed during the bootstrap process.
We will accomplish this with the walletAttributes()
method, where we define the keys for our wallet attributes. Keys can be set during runtime by calling wallet.setAttribute(key, value) method. Make sure the keys you use are unique.
1export class BusinessRegistrationTransactionHandler extends Handlers.TransactionHandler {2 // ...3 4 // defining the wallet attribute key5 public walletAttributes(): ReadonlyArray<string> {6 return [7 "business",8 ];9 }
STEP 3: Tapping Into the Transaction Bootstrap Process
Bootstrap process is run each time a core node is started. The process evaluates all of the transactions in the local database and applies them to the corresponding wallets. All of the amounts, votes, and other custom properties are calculated and applied to the global state — walletManager.
The source-code below shows implementing our bootstrap
method to set our custom business
attribute from existing custom transactions in database. When we are done with custom wallet attribute value changes, a index call is recommended on the walletManager.index(wallet).
Also note that we use this.transactionHistoryService
from the injected transaction history service to read forged business registration transactions from database.
1export class BusinessRegistrationTransactionHandler extends Handlers.TransactionHandler { 2 // ... 3 4 // defining the wallet attribute key 5 public walletAttributes(): ReadonlyArray<string> { 6 return [ 7 "business", 8 ]; 9 }10 11 // reading and setting wallet attribute value for defined key above12 public async bootstrap(): Promise<void> {13 const criteria = {14 typeGroup: this.getConstructor().typeGroup,15 type: this.getConstructor().type,16 };17 18 for await (const transaction of this.transactionHistoryService.streamByCriteria(criteria)) {19 const wallet: Contracts.State.Wallet = this.walletRepository.findByPublicKey(transaction.senderPublicKey);20 const asset = {21 businessAsset: transaction.asset.businessRegistration,22 };23 24 wallet.setAttribute("business", asset);25 this.walletRepository.index(wallet);26 }27 }28}
STEP 4: Implement apply and revert methods
While bootstrap
method is run on startup, built from transactions in database, apply and revert methods are used as we receive new transactions (new blocks received or new transactions yet to be forged).
Here we only need to implement applyToSender
and revertForSender
as we just want to set/remove the business
attribute from the sender’s wallet.
1export class BusinessRegistrationTransactionHandler extends Handlers.TransactionHandler { 2 // ... 3 4 public async applyToSender(transaction: Interfaces.ITransaction): Promise<void> { 5 await super.applyToSender(transaction); 6 7 Utils.assert.defined<string>(transaction.data.senderPublicKey); 8 9 const sender: Contracts.State.Wallet = this.walletRepository.findByPublicKey(transaction.data.senderPublicKey);10 11 sender.setAttribute("business", {12 businessAsset: transaction.data.asset.businessRegistration,13 });14 15 this.walletRepository.index(sender);16 }17 18 public async revertForSender(transaction: Interfaces.ITransaction): Promise<void> {19 await super.revertForSender(transaction);20 21 Utils.assert.defined<string>(transaction.data.senderPublicKey);22 23 const sender: Contracts.State.Wallet = this.walletRepository.findByPublicKey(transaction.data.senderPublicKey);24 25 sender.forgetAttribute("business");26 27 this.walletRepository.index(sender);28 }29 30 public async applyToRecipient(31 transaction: Interfaces.ITransaction,32 // tslint:disable-next-line: no-empty33 ): Promise<void> {}34 35 public async revertForRecipient(36 transaction: Interfaces.ITransaction,37 // tslint:disable-next-line:no-empty38 ): Promise<void> {}39}
STEP 5: Implementing Transaction-Pool Validation
The Transaction Pool serves as a temporary layer where valid and verified transactions are stored locally until it is their turn to be included in the newly forged (created) blocks. Each new custom transaction type needs to be verified and accepted by the same strict limitation rules that are enforced for our core transactions. We need to implement throwIfCannotEnterPool
method (see source-code snippet below) to follow the rules and execution structure. The method is called from the core GTI Engine.
Note that we use this.poolQuery
from the injected transaction pool query service.
1export class BusinessRegistrationTransactionHandler extends Handlers.TransactionHandler { 2 // ... 3 public async throwIfCannotEnterPool(transaction: Interfaces.ITransaction): Promise<void> { 4 const hasSender: boolean = this.poolQuery 5 .getAllBySender(transaction.data.senderPublicKey) 6 .whereKind(transaction) 7 .has(); 8 9 if (hasSender) {10 throw new Contracts.TransactionPool.PoolError(`Business registration already in the pool`, "ERR_PENDING");11 }12 }13}
STEP 5: Register The New Transaction Type Within Core GTI Engine
Success
You made it. The final step awaits, and it is the easiest: registration of the newly implemented BusinessRegistrationTransaction type. To accomplish this, we just need to bind the TransactionHandler
identifier to our custom transaction handler in our service provider declaration (see code below).
1export class ServiceProvider extends Providers.ServiceProvider {2 public async register(): Promise<void> {3 const logger: Contracts.Kernel.Logger = this.app.get(Container.Identifiers.LogService);4 logger.info(`Loading plugin: ${plugin.name} with version ${plugin.version}.`);5 6 this.app.bind(Container.Identifiers.TransactionHandler).to(BusinessRegistrationTransactionHandler);7 }8}
Your custom transaction type implementation is now COMPLETE. Follow the next steps to add and load your plugin during the CORE node bootstrap process.