57
loading...
This website collects cookies to deliver better user experience
describe("ticketing-system", () => {
const anchor = require("@project-serum/anchor");
const assert = require("assert");
const { SystemProgram } = anchor.web3;
// Configure the client to use the local cluster.
const provider = anchor.Provider.env();
anchor.setProvider(provider);
const program = anchor.workspace.TicketingSystem;
const _ticketingSystem = anchor.web3.Keypair.generate();
const tickets = [1111, 2222, 3333];
it("Is initializes the ticketing system", async () => {
const ticketingSystem = _ticketingSystem;
await program.rpc.initialize(tickets, {
accounts: {
ticketingSystem: ticketingSystem.publicKey,
user: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [ticketingSystem],
});
const account = await program.account.ticketingSystem.fetch(
ticketingSystem.publicKey
);
assert.ok(account.tickets.length === 3);
assert.ok(
account.tickets[0].owner.toBase58() ==
ticketingSystem.publicKey.toBase58()
);
});
});
lib.rs
). First, let's create the structs that represent both our Ticket and the TicketingSystem#[account]
#[derive(Default)]
pub struct TicketingSystem {
pub tickets: [Ticket; 3],
}
#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone, Copy)]
pub struct Ticket {
pub owner: Pubkey,
pub id: u32,
pub available: bool,
pub idx: u32,
}
#[account]
on the TicketingSystem
automatically prepend the first 8 bytes of the SHA256 of the account’s Rust ident (e.g., what's inside the declare_id
). This is a security check that ensures that a malicious actor could not just inject a different type and pretend to be that program account.Ticket
, so we have to make it serializable. The other thing to note is that I'm specifying the owner to be of type Pubkey
. The idea is that upon creation, the ticket will be initially owned by the program and when I make a purchase the ownership will be transferred.#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user)]
pub ticketing_system: Account<'info, TicketingSystem>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct PurchaseTicket<'info> {
#[account(mut)]
pub ticketing_system: Account<'info, TicketingSystem>,
pub user: Signer<'info>,
}
#[derive(Accounts)]
implements an Accounts
deserializer. This applies any constraints specified by the #[account(...)]
attributes. For instance, on the Initialize
struct we have had the payer = user
constrains specifying who's paying for the initialization cost (e.g., when the program is deploying).pub fn initialize(ctx: Context<Initialize>, tickets: Vec<u32>) -> ProgramResult {
let ticketingSystem = &mut ctx.accounts.ticketing_system;
let owner = ticketingSystem.to_account_info().key;
for (idx, ticket) in tickets.iter().enumerate() {
ticketingSystem.tickets[idx] = Ticket {
owner: *owner,
id: *ticket,
available: true,
idx: idx as u32,
};
}
Ok(())
}
anchor test
:ticketing-system
✔ Is initializes the ticketing system (422ms)
1 passing (426ms)
✨ Done in 8.37s.
/app
folder, let's use it.App.tsx
contains code to detect if we're connected to a wallet or not:...
function App() {
const wallet = useWallet();
if (!wallet.connected) {
return (
<div className="main-container p-4">
<div className="flex flex-col lg:w-1/4 sm:w-full md:w-1/2">
<WalletMultiButton />
</div>
</div>
);
} else {
return (
<div className="main-container">
<div className="border-b-4 border-brand-border self-stretch">
<h1 className="font-bold text-4xl text-center p-4 text-brand-border">Ticket Sales</h1>
</div>
<Tickets />
</div>
);
}
}
export default App;
Ticket
and Tickets
. I also used tailwindcss
to style them.Tickets
look like:function Tickets() {
const wallet = useWallet();
const [tickets, setTickets] = useState<TicketInfo[]>([]);
const initializeTicketingSystem = async () => {
const provider = await getProvider((wallet as any) as NodeWallet);
const program = new Program((idl as any) as Idl, programID, provider);
try {
await program.rpc.initialize(generateTickets(3), {
accounts: {
ticketingSystem: ticketingSystem.publicKey,
user: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
},
signers: [ticketingSystem],
});
const account = await program.account.ticketingSystem.fetch(
ticketingSystem.publicKey
);
setTickets(account.tickets);
} catch (err) {
console.log("Transaction error: ", err);
}
};
return (
<div>
{tickets.length === 0 && (
<button className="bg-brand-btn rounded-xl font-bold text-xl m-4 p-2 hover:bg-brand-btn-active" onClick={initializeTicketingSystem}>
Generate Tickets
</button>
)}
{tickets.map((ticket) => (
<Ticket
key={ticket.id}
ticket={ticket}
ticketingSystem={ticketingSystem}
setTickets={setTickets}
/>
))}
</div>
);
}
export default Tickets;
Generate Tickets
button that will initialize the tickets on-chain. These RPC calls could be moved to an API file, but I'll keep there since it is the only place that needs it. The code for the Ticket
is similar in structure. Here will call the purchase
RPC call:....
const purchase = async (ticket: TicketInfo) => {
const provider = await getProvider((wallet as any) as NodeWallet);
const program = new Program((idl as any) as Idl, programID, provider);
try {
await program.rpc.purchase(ticket.id, ticket.idx, {
accounts: {
ticketingSystem: ticketingSystem.publicKey,
user: provider.wallet.publicKey,
},
});
const account = await program.account.ticketingSystem.fetch(
ticketingSystem.publicKey
);
setTickets(account.tickets);
} catch (err) {
console.log("Transaction error: ", err);
}
};
....
Anchor
is abstracting away. There's also good practices built into it (e.g., the 8 bytes discriminator for the program's account, lack of order when accessing accounts, etc.). I'll be spending more time with both Anchor and the Solana SDK itself to make sure I understand what's being abstracted away.anchor build
and anchor deploy
before running anchor test
. That ensures that you have the latest bytecode on the runtime. You will encounter a serialization error if you don't."Transaction simulation failed: Error processing Instruction 0: custom program error: 0x66"
. Convert the number from hex -> integer, if the number is >=300 it's an error from your program, look into the errors section of the idl that gets generated when building your anchor project. If it is < 300, then search the matching error number here
"error: Error: 163: Failed to deserialize the account"
. Very often it's because you haven't allocated enough space (anchor tried to write the account back out to storage and failed). This is solved by allocating more space during the initialization....
#[account(init, payer = user, space = 64 + 64)]
pub ticketing_system: Account<'info, TicketingSystem>,
...
devent
or testdeve
made that account address in use and is not upgradeable). You can simply delete the /deploy
folder under target (e.g /root-of-your-anchor-project/target/deploy
) and run anchor build
again. That will regenerate the /deploy
folder. After that, you just need to run this from your root project directory solana address -k target/deploy/name-of-your-file-keypair.json
. You can take that output and upgrade both the declare_id()
in your lib.rs
and Anchor.toml
with the new program ID. Finally, you have to run anchor build
again to rebuild with the new program ID.Anchor
and the current Solana ecosystem very exciting. Will continue to post my progress. Until the next time.