solana-program
builds upon @solana/web3.js with the intent of reducing the
amount of boilerplate code and lower-level data wrangling necessary when
developing client-side Solana apps. Our goal is to provide a more intuitive,
object-oriented API.
yarn add solana-program
or npm install solana-program
.
Custom instructions can be built using the InstructionBuilder
API. For example:
const instr = program
.instruction()
.account(user.publicKey, { isSigner: true, isWritable: true })
.account(dataAccountPublicKey, { isWritable: true })
.data(data);
You can add one or more InstructionBuilder
objects to a transaction, using
TransactionBuilder
, like so:
const tx = Solana.begin().add(instr).sign(user);
const signature = await tx.execute();
In order to retrieve a program's accounts using @solana/web3.js
, you normally
have to use some form of the getProgramAccounts
function. Queries can be
filtered according to two main criteria:
- The size (in bytes) of the target account's data.
- Contents of specific slices of base58-encoded data.
Suppose we have an account that stores an initial "tag" byte, which indicates what "type" of account something is, as well as a "username" string and an "age" int. Using only web3.js to select accounts that match this pattern looks somehing like this:
const results = await getParsedProgramAccounts(programId, {
encoding: 'jsonParsed',
filters: [
{
memcmp: {
offset: 0,
bytes: b58.encode([1]),
},
},
{
memcmp: {
offset: 2,
bytes: b58.encode(Buffer.from('coolperson123')),
},
},
{
dataSize: 1 + 1 + 256,
},
],
});
In contrast, solana-program
provides a more intuitive interface. The same
query would be written like this:
// example data structure we're storing in account
class Comment extends ProgramObject {
@field('u8')
tag: number = 1;
@field('u8')
age?: number;
@field('String', { space: 256 })
username?: string;
}
// select all accounts with username glorp. note that the dataSize filter is
// automatically added; however, it can be set explicitly, via
// program.select(Comment).size(...)...
const accounts = await program
.select(Comment)
.match({ tag: 1, username: 'coolperson123' })
.execute();
This is a complete example of building a client for a Solana program that simply initializes and sets a "lucky number" in an account.
class LuckyNumber extends ProgramObject {
@field('u8')
value?: number;
}
class LuckyNumberProgram extends Program {
public async main(user: Keypair) {
await Solana.initialize();
const seed = 'lucky-number'
const key = await this.deriveAddress(user.publicKey, seed);
const account = await this.getAccount(LuckyNumber, luckNumberKey);
const luckyNumber = Math.round(100 * Math.random());
const tx = Solana.begin();
if (account === null) {
const space = 1;
const instr = this.createAccountWithSeed(user, seed, key, space)
console.log('creating "lucky number" account');
tx.add(instr);
}
console.log('setting new "lucky number"');
tx.add(this.setLuckyNumber(user, key, luckyNumber));
const signature = await tx.sign(user).execute();
console.log('transaction signature:', signature);
}
public setLuckyNumber(
user: Address, key: Address, value: number,
): CustomInstructionBuilder {
return this.instruction(0) // 0 is the OP code of the instruction
.account(user, { isWritable: true, isSigner: true });
.account(key, { isWritable: true, });
.data(new LuckyNumber({ value }))
}
}