Develop & Test a Custom Transaction
Welcome everyone to the second part in our series of tutorials based on the ARK Messenger Proof-of-Concept (PoC). During Part One, we set up a development environment and deployed our own custom bridgechain. This series was created in-part with documentation provided by Delegate Lemii as part of his ARK Messenger Proof-of-Concept, funded by the ARK Grants Program .
One of the main goals for this series is to successfully demonstrate the different ways developers can work with ARK Core. In this part of the series, we will learn to develop and test a custom transaction for our bridgechain. In addition, we will also be developing the client, which is the front-end application users will use to interact with the messaging service.
Getting Started
Before we get started with developing and testing our custom transactions, we need to outline the technical specifications for the application. This is important because it is very difficult to make changes once these parameters have been established. Let’s take a look at ARK Messenger’s technical specifications as an example.
ARK Messenger implements a custom Message Transaction. This is a transaction that will hold the encrypted message that is being sent by the user. The transaction should follow these rules:
- Set transfer amount to 0, and apply a low static fee (currently 0.01).
- Be able to store a relatively large amount of message data (1024 bytes).
- Must have a unique type and type group (101, 1001).
These were considerations made prior to developing the custom transaction. In addition to outlining your technical specifications, you may want to familiarize yourself with how custom transactions work on ARK. The article listed below will give you a solid base in understanding how smart/custom transactions work on the ARK Core Blockchain Framework.
Creating a Custom Transaction
Creating a custom transaction is a straightforward process. We will be using ARK Messenger’s custom transaction — Message Transaction as our template.
The custom Message Transaction is based on the Business Registration Transaction from the guide above . For reference, you can find the code here .
In order to create our custom transaction, we will be going over the transaction class, the transaction handler, and the transaction builder below. Please use the files below while working on each one:
Message Transaction (Creating our Custom Transaction)
At the very top, we define our custom type and type group:
1const MESSAGE_TYPE = 101;2const MESSAGE_TYPE_GROUP = 1001;
Just below that, we define our transaction schema:
1public static getSchema(): Transactions.schemas.TransactionSchema {2 return schemas.extend(schemas.transactionBaseSchema, {…}3}
One difference you will notice, unlike the Business Registration Transaction, we included the recipientId field in our schema. This is because this field is used as the ‘channel id’ for our purposes. Apart from that, we define the rest of the transaction and our message object that will be used for message data.
Below the schema, we define our static fee of 0.01. The value we use is 1000000 because the ARK SDK does not work with floating-point numbers and the number of decimal places is 8.
As for the serde (serializer/Deserializer) functionality; it mostly works the same as the Business Registration Transaction, with the exception of two important things:
- The writeUint8 buffer function is limited to a size, 256 bytes. Since we want to process message data of 1024 bytes maximum, we must use the writeUint16 variant instead.
- Because we want to use the recipientId in the transaction, we must include it in serde process as well:
Serializer:
1const { addressBuffer, addressError } = Identities.Address.toBuffer(data.recipientId);2options.addressError = addressError;3buffer.append(addressBuffer);
Deserializer:
1data.recipientId = Identities.Address.fromBuffer(buf.readBytes(21).toBuffer());
Message Transaction Handler
Next, we will work on the Message Transaction handler. Since the Message Transaction is relatively simple, we can omit most of the logic from the Business Registration Transaction handler.
Using throwIfCannotBeApplied we explicitly check if the message data is valid:
1const { data }: Interfaces.ITransaction = transaction;2const { message }: { message: string } = data.asset.messageData;3 4if (!message) {5 throw new MessageTransactionAssetError();6}7 8await super.throwIfCannotBeApplied(transaction, wallet, databaseWalletManager);
At applyToSender and revertForSender we perform the default apply and revert actions. The methods applyToRecipient and revertForRecipient are not utilized because none of the recipient’s attributes are mutated by processing the Message Transaction.
Apart from that, we let the base TransactionHandler… “handle” the rest
Message Transaction Builder
The builder that is present in this file will be used in the ARK Messenger Client to create Message Transactions whenever a user submits a message.
First, we initialize the data object with the class constructor()
method:
1constructor() { 2 super(); 3 4 this.data.type = MessageTransaction.type; 5 this.data.typeGroup = MessageTransaction.typeGroup; 6 this.data.version = 2; 7 this.data.fee = Utils.BigNumber.make("1000000"); 8 this.data.amount = Utils.BigNumber.ZERO; 9 this.data.asset = { messageData: {} };10 this.data.recipientId = undefined;11};
Next up, we create a method that we can use to add message data to the transaction:
1public messageData(message: string): MessageTransactionBuilder {2 this.data.asset.messageData = {3 message,4 };5};
Finally, we need to add the non-standard fields to the transaction data object for when it is called with getStruct()
:
1public getStruct(): Interfaces.ITransactionData {2 const struct: Interfaces.ITransactionData = super.getStruct();3 struct.amount = this.data.amount;4 struct.asset = this.data.asset;5 struct.recipientId = this.data.recipientId;6 7 return struct;8};
Testing the Custom Transaction Builder
There are a number of ways to test your custom transaction. One of the best options is to create a test file with Jest. This tester is also used for other ARK packages, so this is a nice synergy.
At the top, we import all the required packages:
1import "jest-extended";2import { Managers, Transactions } from "@arkecosystem/crypto";3import { MessageTransactionBuilder } from "../src/builders";4import { MessageTransaction } from "../src/transactions";
Next, we need to make some adjustments with the config manager to be able to actually create the transaction. First, we select the testnet network preset and set the height to 2. This will enable the AIP11 milestone.
1Managers.configManager.setFromPreset("testnet");2Managers.configManager.setHeight(2);
Now we include our new custom transaction to the transaction registry:
1Transactions.TransactionRegistry.registerTransactionType(MessageTransaction);
The configuration is now complete. What’s left is to create a test that verifies that the transaction can be built and verified:
1describe("Test builder", () => { 2 it("should verify correctly", () => { 3 const builder = new MessageTransactionBuilder(); 4 const tx = builder 5 .messageData("SomeMessage") 6 .nonce("3") 7 .recipientId("AYeceuGa7tTsyG6jgq7X6qKdoXt9iJJKN6").sign("clay harbor enemy utility margin pretty hub comic piece aerobic umbrella acquire"); 8 expect(tx.build().verified).toBeTrue(); 9 expect(tx.verify()).toBeTrue();10 });11});
Lastly, we will verify these transactions on the network.
Testing the Custom Transaction on the Network
When the builder test has been passed, we can continue with the custom transaction integration for the bridgechain. First, we need to include the custom transaction as a plugin in the bridgechain.
Information on how to create a plugin can be found here: How to Write a Core Plugin
You can view our Message Transaction plugin file here: plugin.js .
To integrate the Message Transaction plugin in the bridgechain, we simply have to include it at the bottom of the plugins.js file found in ./packages/core/bin/config/{network}/plugins.js
1module.exports = {2 …,3 "message-transaction": {},4};
Now, when we run yarn setup to install the bridgechain, it will automatically install the custom transaction as well and include it as a plugin on startup.
When the bridgechain is running (locally), we can verify that the custom transaction is integrated correctly by checking the following API endpoint: http://127.0.0.1:11003/api/transactions/types
If our custom transaction is present in the list of available transaction types, we can try and post an actual transaction to the blockchain to see if it processes correctly. This can be done in many ways, but the preferred way is utilizing simple scripts.
The test is complete when the transaction is broadcast and processed by the network. We have now successfully implemented our custom Message Transaction! The next step is to develop the chat client that the user will interact with.
Using the custom transaction in the client
As mentioned earlier, the client has been created with ReactJS and is written in TypeScript. The full codebase can be viewed here .
To make use of the custom transaction in our chat client, we first have to include some of the files in our project. The parts that we need are the Message Transaction and Message Transaction Builder. We can simply copy-paste these from before, and place them in the /src/ folder . Since the Message Transaction Handler is only used for the Core, this can be omitted.
Now, we can simply import the Message Transaction Builder anywhere in our app to make use of it. In the ARK Messenger client, it is being imported in a separate utils file, as you can see here .
1import { encryptMessage, fetchRemoteNonce, broadcastTransaction } from "./index";2import { Transactions } from "@arkecosystem/crypto";3import { ITransactionData, IPostTransactionResponse } from "../interfaces";4import { MessageTransaction } from "../custom-transactions/message-transaction/transactions";5import { MessageTransactionBuilder } from "../custom-transactions/message-transaction/builders";
It’s required to register the new transaction type in the Transaction Registry:
1Transactions.TransactionRegistry.registerTransactionType(MessageTransaction);
We’re using a custom network version defined in an environment variable. We store this in a const variable that will be passed on to the transaction builder:
1const NETWORK = Number(process.env.REACT_APP_NETWORK);
Finally, we can call the builder in our custom transaction functions that will be used for the chat functionality:
1const createMessageTransaction = async ( 2 recipientId: string, 3 encryptedMessage: string, 4 passphrase: string, 5 senderId: string 6): Promise<ITransactionData> => { 7 8 const nonce = await fetchRemoteNonce(senderId); 9 const tx = new MessageTransactionBuilder()10 .network(NETWORK)11 .recipientId(recipientId)12 .messageData(encryptedMessage)13 .nonce(nonce)14 .sign(passphrase);15 16 return tx.getStruct();17};18 19export const sendMessage = async (20 recipientId: string,21 text: string,22 channelPassphrase: string,23 userPassphrase: string,24 userAddress: string25): Promise<IPostTransactionResponse | void> => {26 const encryptedMessage = encryptMessage(text, channelPassphrase);27 28 try {29 const tx = await createMessageTransaction(30 recipientId,31 encryptedMessage,32 userPassphrase,33 userAddress34 );35 36 return broadcastTransaction(tx);37 } catch (err) {38 console.error(err);39 }40};
Alternatives to this approach are:
- Upload the custom transaction as a module to NPM.
- Make use of Git Submodules .
Next Steps
In our next and final tutorial, we will be deploying a bridgechain and launching the client application. In addition, we will be highlighting all of the examples pertaining to how developers can work with the ARK Core. With future updates and improvements, ARK Core will continue to be one of the simplest and most flexible ways to build blockchain solutions.
If you become stuck at any point make sure to consult our documents on our Core Developer Docs. In addition, you can reach out via our contact form.