Build on Solana
Anchor Test Using Typescript
Codigo Hello World
In this guide, you’ll learn how to start with Código’s Interface Description Language (CIDL) from scratch using the web-based IDE: Codigo Studio. In this section, we will implement the same counter example that we have implemented with anchor and solana native, this time using Codigo.
CIDL Counter
The following CIDL is the interface for the counter program. This CIDL will generate a Non-PDA account named GreetingAccount and two instructions, the first named increment and the second decrement.
cidl: "0.8"
info:
name: counter
title: Patika Counter
version: 0.0.1
license:
name: Unlicense
identifier: Unlicense
types:
GreetingAccount:
fields:
- name: counter
type: u32
methods:
- name: increment
inputs:
- name: greeting_account
type: GreetingAccount
solana:
attributes: [mut, init_if_needed]
- name: decrement
inputs:
- name: greeting_account
type: GreetingAccount
solana:
attributes: [mut, init_if_needed]
General information
When writing the CIDL, the first step we need to take is to specify some general information about the contract we are going to build and the CIDL specification that we want to use, the CIDL specification documentation can be found here.
cidl: "0.8"
info:
name: counter
title: Patika Counter
version: 0.0.1
license:
name: Unlicense
identifier: Unlicense
- cidl: The CIDL specification version, currently on version 0.8
- info: Here we define general information about the contract like the name, title, version, and license information. All these fields are required. We can set some additional recommended fields in the info object. But I kept it out for simplicity.
Defining the GreetingAccount
Optionally, we can define custom types for our CIDL. These types will be transpile to a Solana Account when targeting the Solana blockchain. Because we didn’t define the seeds for this type, the GreetingAccount data structure will be transpile to a Non-PDA account.
types:
GreetingAccount:
fields:
- name: counter
type: u32
We can define as many types we need for our program under the types section. The types section/block is a key-value pair, where the key is the structure's name and the value is an object where we can define additional configurations.
Under the fields block, we can specify the fields/properties for a given data structure. The fields is an array of objects, where each object must have the name and type property. The name will be the property's name, and the type will be the field's data type. All supported data types can be found here.
Defining the Increment and Decrement Instructions
A CIDL must have at least one method/instruction defined. We can define these instructions under the methods block. The methods block is an array of objects, where each object must have, at minimum, the name property defined.
methods:
- name: increment
inputs:
- name: greeting_account
type: GreetingAccount
solana:
attributes: [mut, init_if_needed]
- name: decrement
inputs:
- name: greeting_account
type: GreetingAccount
solana:
attributes: [mut, init_if_needed]
Each method can optionally define an array of inputs, where each object must have the name, and type property define. The name will be the parameter’s name, and the type will be the parameter's data type. All supported data types can be found here.
Because in Solana, we need to specify all the accounts required in instructions, for both methods increment and decrement, we define an input of type GreetingAccount; this is the data structure we defined above.
Finally, we want to tell the generator some additional information regarding the GreetingAccount; we can do this through the solana extension for the input. For more information regarding the Solana extension, check this link. Within the solana extension, we defined the property attributes, attributes it is an array of strings, and for both the increment and decrement instructions, we specified the mut and init_if_needed attributes. The mut attribute will make the account writable, and the init_if_needed will generate all the code to initialize the account if it hasn’t been initialized.
Generating the Smart Contract and Client Library
Open the terminal; if not there, go to the path where you created the cidl.yaml file and type the following command:
codigo generate cidl.yaml
This command will generate the native solana program and the client library. The name of the file doesn’t matter. It just needs to match the name the developer created.
Implementing Business Logic
After generating the solana program and client library in the directory where the cidl.yaml exists, will be generated directory sdk and generated, there’s another directory named codigolib, but this can be ignored.
Within the generated directory, we will find the directory named stubs inside the rendered directory. In the stubs directory, we will see two files, one named increment.rs and the other decrement.rs. We will implement the business logic for our counter program in this file.
increment.rs
In the increment.rs file replace the comment “// Place your custom code here...” with the code `greeting_account.data.counter += 1;`
pub fn increment(greeting_account: &mut WithMeta<GreetingAccount>) -> ProgramResult {
greeting_account.data.counter += 1;
Ok(())
}
All accounts generated by Código are wrapped with the WithMeta structure. This wrapper allows developers to access the deserialized structure of the account through the data property of the WithMeta struct. We can access the raw AccountInfo struct through the meta field of the WithMeta struct.
decrement.rs
Let’s implement the business logic for decrement instruction; it is similar to the increment.rs, the difference is that instead of increasing the value of the counter, we will decrease it.
pub fn decrement(greeting_account: &mut WithMeta<GreetingAccount>) -> ProgramResult {
// Place your custom code here...
greeting_account.data.counter -= 1;
Ok(())
}
Build and Deploy the Contract
Navigate to the generated directory and type the command `cargo build-sbf`; this command will build the contract.
Open a new terminal, and type the command `solana-test-validator`; this command will start a new solana test validator to where we will deploy the contract.
From the generated directory and after building the contract, execute the following command to deploy to the validator `solana program deploy target/deploy/counter.so
After completing the deployment, you will get a program id. Copy and save this program id so we can configure the client library.
Integrate the Client Library
From the terminal, navigate to the sdk directory and type the command `yarn install` to install the node_modules dependencies.
Within the sdk directory, create a new file named app.ts and paste the following snippet of code:
import {
getGreetingAccount,
incrementSendAndConfirm,
decrementSendAndConfirm,
SetProgramId
} from "./index";
import {Connection, Keypair} from "@solana/web3.js";
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
async function main(feePayer: Keypair) {
// TODO: Specify the smart contract Program Id we saved from when we deploy the smart contract
SetProgramId("PASTE_YOUR_PROGRAM_ID");
// Instantiate a new Solana connection
const connection = new Connection("http://127.0.0.1:8899");
// 0. Create keypair for the Greeting account
const greetingAccount = Keypair.generate();
// 1. Increment the counter by 1
await incrementSendAndConfirm(connection, greetingAccount, feePayer);
let account = await getGreetingAccount(connection, greetingAccount.publicKey);
// 2. Decrement the count by 1
await decrementSendAndConfirm(connection, greetingAccount, feePayer);
account = await getGreetingAccount(connection, greetingAccount.publicKey);
console.info(account);
}
fs.readFile(path.join(os.homedir(), ".config/solana/id.json"))
.then(file => main(Keypair.fromSecretKey(new Uint8Array(JSON.parse(file.toString())))));
The above snippet code is just a helper that allows us to get the solana wallet that it is created when first installing Solana CLI. We use this account to pay the transaction fees.
The functions that we will be calling are suffixes with SendAndConfirm; this version of the instruction prepares the transaction instruction and sends it immediately through a transaction.
Following the snippets comments:
- We need to specify the program id in the SetProgramId function. We got this program id from when we deployed the solana contract
- We create a solana connection to the local validator.
- Create a keypair for the greeting account.
- Increase the counter of greeting account by 1.
- Decrement the counter by 1.
Finally, to execute the app.ts file, go to the terminal, navigate to the sdk directory, and type the command `npx ts-node app.ts`. After executing this command, you should get the output:
GreetingAccount {
counter: 1,
pubkey: PublicKey [PublicKey(DxiUuUwz3o3F4sDHoSwWFNkKgVZjgWoReCpjNjSknKe5)] {
_bn: <BN: c0921728bc1886de1846b8cc2025496521262556bf2612b930b6b28ec0972346>
}
}
GreetingAccount {
counter: 0,
pubkey: PublicKey [PublicKey(DxiUuUwz3o3F4sDHoSwWFNkKgVZjgWoReCpjNjSknKe5)] {
_bn: <BN: c0921728bc1886de1846b8cc2025496521262556bf2612b930b6b28ec0972346>
}
}
Full CIDL Example with Documentation
Here is the CIDL but with the recommended documentation properties. Highlighted in green are the new property we added for generating the documentation.
cidl: "0.8"
info:
name: counter
title: Patika Counter
version: 0.0.1
summary: |-
The following CIDL is the interface for the counter program.
This CIDL will generate a Non-PDA account named GreetingAccount and two instructions,
the first named increment, and the second decrement.
contact:
name: Patika
web: https://www.patika.dev
git: https://github.com/EmreKeskin47/solana-intro
email: [email protected]
license:
name: Unlicense
identifier: Unlicense
types:
GreetingAccount:
summary: Account that will keep track of the incremented or decremented value.
fields:
- name: counter
type: u32
methods:
- name: increment
summary: Instruction to increment the GreetingAccount.counter by one
inputs:
- name: greeting_account
type: GreetingAccount
solana:
attributes: [mut, init_if_needed]
- name: decrement
summary: Instruction to decrement the GreetingAccount.counter by one
inputs:
- name: greeting_account
type: GreetingAccount
solana:
attributes: [mut, init_if_needed]