This proposal discusses storing image data in base64 SVG format directly to a Solana address, using a data URL header to make it natively readable by browsers. The image storage account is a PDA generated with the NFT token pubkey as a seed, making it easy to retrieve the image knowing just the programID and the NFT token pubkey.
Showcased below is the code used to implement Blockrons on-chain images, open sourced by me under CC1 (just give a thanks/attribution or something)
1. Main program - Lib.rs (written in solpg)
use anchor_lang::prelude::*;
declare_id!("BLKRNygNc7mWMpxQPXs4UoVPyApvyqy4v8uK9F3iJmqS");
#[program]
// Smart contract functions
pub mod imager {
use super::*;
// creates the image storage account
pub fn create_imager(ctx: Context<CreateImager>,token: Pubkey) -> Result<()> {
let imager = &mut ctx.accounts.imager;
imager.authority = ctx.accounts.authority.key();
imager.token = token;
imager.img = ("").to_string();
Ok(())
}
// puts data inside the image storage account by concatenating strings <- client-side to fit the solana txn limit
pub fn update_imager(ctx: Context<UpdateImager>,token: Pubkey, image: String) -> Result<()> {
let imager = &mut ctx.accounts.imager;
imager.authority = ctx.accounts.authority.key();
imager.token = token;
imager.img = format!("{}{}", imager.img, image);
Ok(())
}
}
// Data validators
//token input (nft token addy) + programID are the main seeds for PDA
#[derive(Accounts)]
#[instruction(token: Pubkey)]
pub struct CreateImager<'info> {
#[account(mut)]
authority: Signer<'info>,
#[account(
init_if_needed,
seeds = [token.as_ref()],
bump,
payer = authority,
space = 10000
)]
imager: Account<'info, Imager>,
system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateImager<'info> {
authority: Signer<'info>,
#[account(mut, has_one = authority)]
imager: Account<'info, Imager>,
}
// Data structures
#[account]
pub struct Imager {
authority: Pubkey,
token: Pubkey,
img: String,
}
^^On-chain program is simple and only uses 2 functions - 1. create the PDA and 2. store data to the PDA thru string concatenation with multiple txns (to bypass solana’s byte txn limit)
2. Client side implementation - also deployed using solpg:
const systemProgram = anchor.web3.SystemProgram;
// EDIT THIS: token = public nft account addy you want to tie your on-chain image to
const token = new web3.PublicKey("NFT PUBKEY HERE");
// EDIT THIS: dataS = string of the base64 info
const dataS = new String("BASE64 SVG DATA HERE");
// do not touch, automatically divide dataS into strings to split transactions
var string1 = new String(dataS.slice(0,800));
var string2 = new String(dataS.slice(800,1600));
var string3 = new String(dataS.slice(1600,2400));
var string4 = new String(dataS.slice(2400,3200));
var string5 = new String(dataS.slice(3200,4000));
var string6 = new String(dataS.slice(4000,4800));
var string7 = new String(dataS.slice(4800,5600));
// program logic
const [imagerPubkey, _] = await anchor.web3.PublicKey.findProgramAddress(
[token.toBytes()],
pg.program.programId
);
console.log("Your imager address", imagerPubkey.toString());
// create image storage account
const [imager, _imagerBump] =
await anchor.web3.PublicKey.findProgramAddress(
[token.toBytes()],
pg.program.programId
);
const tx = await pg.program.methods
.createImager(token)
.accounts({
authority: pg.wallet.publicKey,
imager: imager,
systemProgram: systemProgram.programId,
})
.rpc();
// transact all 7 string parts
const tx1 = await pg.program.methods
.updateImager(token,string1.toString())
.accounts({
imager: imagerPubkey,
})
.rpc();
const tx2 = await pg.program.methods
.updateImager(token,string2.toString())
.accounts({
imager: imagerPubkey,
})
.rpc();
const tx3 = await pg.program.methods
.updateImager(token,string3.toString())
.accounts({
imager: imagerPubkey,
})
.rpc();
const tx4 = await pg.program.methods
.updateImager(token,string4.toString())
.accounts({
imager: imagerPubkey,
})
.rpc();
const tx5 = await pg.program.methods
.updateImager(token,string5.toString())
.accounts({
imager: imagerPubkey,
})
.rpc();
const tx6 = await pg.program.methods
.updateImager(token,string6.toString())
.accounts({
imager: imagerPubkey,
})
.rpc();
const tx7 = await pg.program.methods
.updateImager(token,string7.toString())
.accounts({
imager: imagerPubkey,
})
.rpc();
console.log("Done!");
// displays current data
// console.log("Your imager", imager);
^^Client side program is a little bit more complex as it is built to breakdown a long string into 7 diff txns of 800 bytes each - to fit solana’s txn limit. Currently handles for strings upto 5600 bytes but of course this can be extended until the PDA max of 10,000 bytes.
Typically, each Blockron image is 32x32 pixels and takes up 3-4kb of space.