--- sidebar_position: 1 slug: custom-instruction-ts title: Custom Instruction authors: [nikerzetic] description: Sending custom smart account instructions using Viem. tags: [quickstart, ethereum, flare-smart-accounts] keywords: [ flare-fdc, ethereum, flare-smart-accounts, evm, flare-network, account-abstraction, ] unlisted: false --- import CodeBlock from "@theme/CodeBlock"; import CustomInstructionsScript from "!!raw-loader!/examples/developer-hub-javascript/smart-accounts/custom-instructions.ts"; The [Custom Instruction guide](/smart-accounts/custom-instruction) provides a general overview of how Flare smart account Custom Instructions work. In this guide, we will showcase an example script, which uses the TypeScript Viem library to register a Custom Instruction, send it on XRPL, and look for the `CustomInstructionExecuted` event on the Flare network. The custom instruction we will be sending will execute three different calls on the Flare network. Each will interact with a different contract, and they will get progressively more complicated. :::info The code in this guide is set up for the Coston2 testnet. Despite that, we will refer to the network as Flare and its currency as FLR instead of Coston2 and C2FLR. ::: A prerequisite for the custom instruction to work is that it is properly funded. As we will see, the custom instruction that we will be sending transfers a total of `2` FLR to other accounts. The [State Lookup guide](/smart-accounts/guides/typescript-viem/state-lookup-ts#personal-account-of-an-xrpl-address) explains how we can acquire its address. Then, we can use the [Flare faucet](https://faucet.flare.network/coston2) to fund our account. The full code showcased in this guide is available on [GitHub](https://github.com/flare-foundation/flare-smart-accounts-viem). ## Contracts {/* TODO:(Nik) fix contract addresses and links */} The first contract is a `Checkpoint` that counts how many times each user has called its `passCheckpoint` function. It is useless as anything other than an example. The first call of our custom instruction will be to call the `passCheckpoint` function of this contract, deploy at the address [`0xEE6D54382aA623f4D16e856193f5f8384E487002`](https://coston2-explorer.flare.network/address/0xEE6D54382aA623f4D16e856193f5f8384E487002?tab=contract). ```Solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; contract Checkpoint { mapping(address => uint256) public numberOfPasses; function passCheckpoint() public payable { ++numberOfPasses[msg.sender]; } } ``` The second contract is a `PiggyBank`, and is a degree more useful. It allows a user to `deposit` FLR into it, and `withdraw` it all at once. Our second call will be to deposit `1` FLR into the `PiggyBank` contract at the address [`0x42Ccd4F0aB1C6Fa36BfA37C9e30c4DC4DD94dE42`](https://coston2-explorer.flare.network/address/0x42Ccd4F0aB1C6Fa36BfA37C9e30c4DC4DD94dE42?tab=contract). ```Solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; contract PiggyBank { mapping(address => uint256) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0); delete balances[msg.sender]; (bool success, ) = payable(msg.sender).call{ value: amount }(""); require(success); } } ``` The third and last contract is a `NoticeBoard`. It allows users to `pinNotice`. The value they send in this transaction will determine how long the notice stays up; as the contract is set up, `1` FLR gets you `30` days, with fractions allowed. The last call our personal account will perform is to pin a notice with the message `Hello World!` to the `NoticeBoard` at the address [`0x59D57652BF4F6d97a6e555800b3920Bd775661Dc`](https://coston2-explorer.flare.network/address/0x59D57652BF4F6d97a6e555800b3920Bd775661Dc?tab=contract) for `30` days (which means we need to attach a value of `1` FLR). ```Solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; struct Notice { uint256 expirationTimestamp; string message; } contract NoticeBoard { uint256 public constant THIRTY_DAYS = 2592000; mapping(address => Notice) public notices; mapping(address => bool) public existingClients; address[] public clients; function pinNotice(string memory message) public payable { require(msg.value > 0); uint256 duration = THIRTY_DAYS * (msg.value / 1 ether); uint256 expirationTimestamp = block.timestamp + duration; notices[msg.sender] = Notice(expirationTimestamp, message); if (!existingClients[msg.sender]) { clients.push(msg.sender); existingClients[msg.sender] = true; } } function getNotices() public view returns (Notice[] memory) { Notice[] memory _notices = new Notice[](clients.length); for (uint256 i = 0; i < clients.length; ++i) { Notice memory notice = notices[clients[i]]; if (notice.expirationTimestamp > block.timestamp) { _notices[i] = notice; } } return _notices; } } ``` The contract also performs additional checks to ensure that each client is only added to the array of all the clients once, and that it returns only the notices that have not yet expired. ## Parameters Besides the contract addresses and their arguments, the only other parameter we need to specify is a `walletId`. This is a one-byte value, assigned by Flare on a case-by-case basis. It is intended for wallet identification by the operator. We will set it to `0`. ```typescript const walletId = 0; const checkpointAddress = "0xEE6D54382aA623f4D16e856193f5f8384E487002"; const piggyBankAddress = "0x42Ccd4F0aB1C6Fa36BfA37C9e30c4DC4DD94dE42"; const noticeBoardAddress = "0x59D57652BF4F6d97a6e555800b3920Bd775661Dc"; const depositAmount = 1 * 10 ** 18; const pinNoticeAmount = 1 * 10 ** 18; const pinNoticeMessage = "Hello World!"; ``` As described in the general [Custom Instruction guide](/smart-accounts/custom-instruction), each custom instruction call is a Solidity struct containing the following fields: - `targetContract`: the address of the contract to call - `value`: the amount of FLR to send - `data`: the function calldata (function selector and parameter encoding) :::note Behind the scenes, each custom instruction call translates to sending a payment transaction to the `targetContract` address of `value` FLR with attached `data`. ::: We define the `CustomInstruction` type: ```typescript export type CustomInstruction = { targetContract: Address; value: bigint; data: `0x${string}`; }; ``` The `targetContract` and `value` do not need additional processing, but how can we generate the calldata? For that, we will use the `encodeFunctionData` function from the Viem library. It calculates the encoding for a function from its contract's ABI, the function name, and the arguments used. With that, we can prepare an array of calls, which is our custom instruction for this example. ```typescript const customInstructions = [ { targetContract: checkpointAddress, value: BigInt(0), data: encodeFunctionData({ abi: checkpointAbi, functionName: "passCheckpoint", args: [], }), }, { targetContract: piggyBankAddress, value: BigInt(depositAmount), data: encodeFunctionData({ abi: piggyBankAbi, functionName: "deposit", args: [], }), }, { targetContract: noticeBoardAddress, value: BigInt(pinNoticeAmount), data: encodeFunctionData({ abi: noticeBoardAbi, functionName: "pinNotice", args: [pinNoticeMessage], }), }, ] as CustomInstruction[]; ``` ## Registering an instruction {/* TODO:(Nik) fix the code for this when the contract gets fixed to not revert if the instruction has already been registered */} Next, we need to register the custom instruction with the `MasterAccountController` contract. In order to read from the Flare chain, we first need to create a [Viem public client](https://viem.sh/docs/clients/public). ```typescript import { createPublicClient, http } from "viem"; import { flareTestnet } from "viem/chains"; export const publicClient = createPublicClient({ chain: flareTestnet, transport: http(), }); ``` {/* TODO:(Nik) Update once MasterAccountController gets added to the FlareContractRegistry */} We also need the address of the `MasterAccountController` contract. Since the Flare smart accounts are still in development, we cannot query the `FlareContractRegistry` for the `MasterAccountController` address. Instead, we need to hardcode it. ```typescript export const MASTER_ACCOUNT_CONTROLLER_ADDRESS = "0x32F662C63c1E24bB59B908249962F00B61C6638f"; ``` {/* TODO:(Nik) Check that the following is still relevant - how the register function works */} To register a custom instruction, we call the `registerCustomInstruction` function on the `MasterAccountController` contract and provide the custom instruction array as the argument. As recommended by the [Viem documentation](https://viem.sh/docs/contract/writeContract#writecontract), we first use the `simulateContract` function to prepare the request. Then we call the `writeContract` Viem function, and actually register the instruction. If the instruction has already been registered, a `CustomInstructionAlreadyRegistered` event is emitted. Otherwise, the instruction is registered, and a `CustomInstructionRegistered` event is emitted instead. ```typescript export async function registerCustomInstruction( instructions: CustomInstruction[], ): Promise<`0x${string}`> { const { request } = await publicClient.simulateContract({ account: account, address: MASTER_ACCOUNT_CONTROLLER_ADDRESS, abi: abi, functionName: "registerCustomInstruction", args: [instructions], }); console.log("request:", request, "\n"); const registerCustomInstructionTransaction = await walletClient.writeContract(request); console.log( "Register custom instruction transaction:", registerCustomInstructionTransaction, "\n", ); return registerCustomInstructionTransaction; } ``` ## Encoding an instruction Before the custom instruction can be sent on the XRPL, it must be properly encoded. We call the `encodeCustomInstruction` read function of the `MasterAccountController` contract with the instruction as parameter. It returns a 32-byte `callHash`, which is not yet a proper instruction encoding. We need to replace the first two bytes with the following values: - 1st byte: custom instruction command ID `0xff` - 2nd byte: wallet identifier `walletId` described above ```typescript async function encodeCustomInstruction( instructions: CustomInstruction[], walletId: number, ) { const encodedInstruction = (await publicClient.readContract({ address: MASTER_ACCOUNT_CONTROLLER_ADDRESS, abi: abi, functionName: "encodeCustomInstruction", args: [instructions], })) as `0x${string}`; // NOTE:(Nik) We cut off the `0x` prefix and the first 2 bytes to get the length down to 30 bytes return ("0xff" + toHex(walletId, { size: 1 }).slice(2) + encodedInstruction.slice(6)) as `0x${string}`; } ``` ## Sending an instruction With that, we can send our custom instruction. We make an XRPL Payment transaction to one of the operator's XRPL addresses (the [State Lookup guide](/smart-accounts/guides/typescript-viem/state-lookup-ts#operators-xrpl-addresses) explains how we can obtain these). The encoded instruction is attached as a memo of the transaction, with the preceding `0x` removed. ```typescript async function sendCustomInstruction({ encodedInstruction, xrplClient, xrplWallet, }: { encodedInstruction: `0x${string}`; xrplClient: Client; xrplWallet: Wallet; }) { const operatorXrplAddress = (await getOperatorXrplAddresses())[0] as string; const instructionFee = await getInstructionFee(encodedInstruction); console.log("Instruction fee:", instructionFee, "\n"); const customInstructionTransaction = await sendXrplPayment({ destination: operatorXrplAddress, amount: instructionFee, memos: [{ Memo: { MemoData: encodedInstruction.slice(2) } }], wallet: xrplWallet, client: xrplClient, }); return customInstructionTransaction; } ``` The instruction fee is determined by the `MasterAccountController` contract from the decimal representation of the instruction ID. In this case, the decimal value of the instruction ID (`ff`) is `255`. But we define a more general function that accepts any encoded instruction as input and extracts the necessary data from it. ```typescript export async function getInstructionFee(encodedInstruction: string) { const instructionId = encodedInstruction.slice(0, 4); const instructionIdDecimal = fromHex( instructionId as `0x${string}`, "bigint", ); console.log("instructionIdDecimal:", instructionIdDecimal, "\n"); const requestFee = await publicClient.readContract({ address: MASTER_ACCOUNT_CONTROLLER_ADDRESS, abi: coston2.iMasterAccountControllerAbi, functionName: "getInstructionFee", args: [instructionIdDecimal], }); return dropsToXrp(Number(requestFee)); } ``` The `sendCustomInstruction` function requires two additional parameters, the `xrplWallet` and `xrplClient`. These are the `Client` and the `Wallet` classes from the `xrpl` library. We initialise them with values from the `.env` file. ```typescript const xrplClient = new Client(process.env.XRPL_TESTNET_RPC_URL!); const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!); ``` ## Wait for instruction execution {/* TODO:(Nik) update ABI */} Lastly, we need to wait for the operator to bridge the instruction from XRPL to Flare, and for the instruction to be executed. Once it does, our personal account executes the instruction, and the `CustomInstructionExecuted` event is emitted by the `MasterAccountController` contract. We watch for such events with the [`watchContractEvent`](https://viem.sh/docs/contract/watchContractEvent#watchcontractevent) Viem function. We check each observed event, ensuring that: - the call hash of the event matches the one sent in the encoded instruction (here, only the last 30 bytes must match) - the personal account that executed the instruction is our personal account (lowercase to remove checksum validation) When we find the correct event, we set the `customInstructionExecutedEventFound` value to `true`. If the value is `false`, we wait for `10` seconds, then check again. Once the value has been found, we stop observing the contract and return the event. ```typescript async function waitForCustomInstructionExecutedEvent({ encodedInstruction, personalAccountAddress, }: { encodedInstruction: `0x${string}`; personalAccountAddress: string; }) { let customInstructionExecutedEvent: | CustomInstructionExecutedEventType | undefined; let customInstructionExecutedEventFound = false; const unwatchCustomInstructionExecuted = publicClient.watchContractEvent({ address: MASTER_ACCOUNT_CONTROLLER_ADDRESS, abi: iInstructionsFacetAbi, eventName: "CustomInstructionExecuted", onLogs: (logs) => { for (const log of logs) { customInstructionExecutedEvent = log as CustomInstructionExecutedEventType; if ( customInstructionExecutedEvent.args.callHash.slice(6) !== encodedInstruction.slice(6) || customInstructionExecutedEvent.args.personalAccount.toLowerCase() !== personalAccountAddress.toLowerCase() ) { continue; } customInstructionExecutedEventFound = true; break; } }, }); console.log("Waiting for CustomInstructionExecuted event..."); while (!customInstructionExecutedEventFound) { await new Promise((resolve) => setTimeout(resolve, 10000)); } unwatchCustomInstructionExecuted(); return customInstructionExecutedEvent; } ``` The vehicle for bridging the instruction is the [Flare Data Connector](/fdc/overview), which puts a maximum cap of 180 seconds on the process duration. ## Full script The repository with the above example is available on [GitHub](https://github.com/flare-foundation/flare-smart-accounts-viem). {CustomInstructionsScript} ## Expected output ```bash Custom instructions: [ { targetContract: '0xEE6D54382aA623f4D16e856193f5f8384E487002', value: 0n, data: '0x80abd133' }, { targetContract: '0x42Ccd4F0aB1C6Fa36BfA37C9e30c4DC4DD94dE42', value: 1000000000000000000n, data: '0xd0e30db0' }, { targetContract: '0x59D57652BF4F6d97a6e555800b3920Bd775661Dc', value: 1000000000000000000n, data: '0x28d106b20000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000' } ] Personal account address: 0xFd2f0eb6b9fA4FE5bb1F7B26fEE3c647ed103d9F request: { abi: [ { inputs: [Array], name: 'registerCustomInstruction', outputs: [Array], stateMutability: 'nonpayable', type: 'function' } ], address: '0x434936d47503353f06750Db1A444DBDC5F0AD37c', args: [ [ [Object], [Object], [Object] ] ], dataSuffix: undefined, functionName: 'registerCustomInstruction', account: { address: '0xF5488132432118596fa13800B68df4C0fF25131d', nonceManager: undefined, sign: [AsyncFunction: sign], signAuthorization: [AsyncFunction: signAuthorization], signMessage: [AsyncFunction: signMessage], signTransaction: [AsyncFunction: signTransaction], signTypedData: [AsyncFunction: signTypedData], source: 'privateKey', type: 'local', publicKey: '0x04cd84974fb965b048aa4230fdf0a2735c951f28b076727e1067bf56577a59b1cbc41e8d03b33acc9105828f048c0cf940c78bba518b0da2765078ca2a42e056c3' } } Register custom instruction transaction: 0x2f6500791d89ea7f8ff5f3ce8983d92d7aaf62e04207513e8e0b2a2bfdf09a1e Custom instruction call hash: 0x2f6500791d89ea7f8ff5f3ce8983d92d7aaf62e04207513e8e0b2a2bfdf09a1e Encoded instructions: 0xff00e54bf1b4e93c5306e08d919864ec111211c4fb36960261e8018da9959791 instructionIdDecimal: 255n Instruction fee: 0.001 Custom instruction transaction hash: 03165B82E4B7BB168AF7B217C9EC833896E968C796CA8BF8D2E66879ED311909 Waiting for CustomInstructionExecuted event... CustomInstructionExecuted event: { eventName: 'CustomInstructionExecuted', args: { personalAccount: '0xFd2f0eb6b9fA4FE5bb1F7B26fEE3c647ed103d9F', callHash: '0x0000e54bf1b4e93c5306e08d919864ec111211c4fb36960261e8018da9959791', customInstruction: [ [Object], [Object], [Object] ] }, address: '0x434936d47503353f06750db1a444dbdc5f0ad37c', topics: [ '0x1c09418c54894f576841186935c5f666b3bedd66c29f3a03bcab4051fe2509f3', '0x000000000000000000000000fd2f0eb6b9fa4fe5bb1f7b26fee3c647ed103d9f', '0x0000e54bf1b4e93c5306e08d919864ec111211c4fb36960261e8018da9959791' ], data: '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000ee6d54382aa623f4d16e856193f5f8384e48700200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000480abd1330000000000000000000000000000000000000000000000000000000000000000000000000000000042ccd4f0ab1c6fa36bfa37c9e30c4dc4dd94de420000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000059d57652bf4f6d97a6e555800b3920bd775661dc0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000006428d106b20000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c6421000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', blockNumber: 26663892n, transactionHash: '0xe3ba1751d63a9b11ff3b3185254c28c447f1db6ff101708f6968ff935c28e3f3', transactionIndex: 2, blockHash: '0x69a3b948a56a5195fbe0ced46b157ca33390226a4503f59a3d8e59fe07538d23', logIndex: 7, removed: false, blockTimestamp: undefined } ```