Rust
How to CPI into a Metaplex Program
Introduction
You may have heard the term "CPI'ing into a program" or "Call a CPI on the program" terms thrown around before and be thinking "what are they talking about?".
A CPI (Cross Program Invocation) is the interaction of one program invoking an instruction on another program.
An example would be that I make a program and during this transaction I need to transfer an NFT or Asset during this transaction. Well my program can CPI call and ask the Token Metadata or Core programs to execute the transfer instruction for me if I give it all the correct details.
Using Metaplex Rust Transaction CPI Builders
Each instruction that comes from Metaplex Rust crate will also currently come with a CpiBuilder
version of that instruction which you can import. This abstracts a massive amount code for you and can be invoked straight from the CpiBuilder
itself.
Lets take the Transfer
instruction from Core as an example here (this applies to all other instructions from this Crate and all other Metaplex crates too.)
If we look through the instructions in the MPL Core crate type docs we can see we have a number of instructions available to us.
TransferV1
TransferV1Builder
TransferV1Cpi
TransferV1CpiAccounts
TransferV1CpiBuilder
TransferV1InstructionArgs
TransferV1InstructionData
The one we are interested in here is the TransferV1CpiBuilder
.
To initialize the builder we can call new
on the CpiBuilder
and pass in the program AccountInfo
of the program address the CPI call is being made to.
TransferV1CpiBuilder::new(ctx.accounts.mpl_core_program);
From this point we can ctrl + click
(PC) or cmd + click
(Mac) into the new
function generated from the CpiBuilder::
which presents us with all the CPI arguments (accounts and data) required for this particular CPI call.
//new() function for TransferV1CpiBuilder
pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self {
let instruction = Box::new(TransferV1CpiBuilderInstruction {
__program: program,
asset: None,
collection: None,
payer: None,
authority: None,
new_owner: None,
system_program: None,
log_wrapper: None,
compression_proof: None,
__remaining_accounts: Vec::new(),
});
Self { instruction }
}
As we can see this one requires all accounts and no data and is a fairly easy CPI call to fill out.
If we look at a second CpiBuilder but this time for CreateV1 we can see extra data here that is required such as name
and uri
which are both strings.
//new() function for CreateV1CpiBuilder
pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self {
let instruction = Box::new(CreateV1CpiBuilderInstruction {
__program: program,
asset: None,
collection: None,
authority: None,
payer: None,
owner: None,
update_authority: None,
system_program: None,
log_wrapper: None,
data_state: None,
name: None,
uri: None,
plugins: None,
__remaining_accounts: Vec::new(),
});
Self { instruction }
}
Some accounts may be optional within a CpiBuilder
so you may have to check what you do and do not need for your use case.
Below are both CpiBuilder
versions for Transfer and Create filled out.
TransferV1CpiBuilder::new()
.asset(ctx.accounts.asset)
.collection(context.accounts.collection)
.payer(context.accounts.payer)
.authority(context.accounts.authority)
.new_owner(context.accounts.new_owner)
.system_program(context.accounts.system_program)
CreateV1CpiBuilder::new()
.asset(context.accounts,asset)
.collection(context.accounts.collection)
.authority(context.accounts.authority)
.payer(context.accounts.payer)
.owner(context.accounts.owner)
.update_authority(context.accounts.update_authority)
.system_program(context.accounts.system_program)
.data_state(input.data_state.unwrap_or(DataState::AccountState))
.name(args.asset_name)
.uri(arts.asset_uri)
.plugins(args.plugins)
Invoking
Invoking is the term used to execute the CPI call to the other program, a programs version of "sending a transaction" if you may.
We have two options when it comes to invoking a CPI call. invoke()
and invoke_signed()
invoke()
invoke()
is used when no PDA signer seeds need to be passed through to the instruction being called for the transaction to succeed. Though accounts that have signed into your original instruction will automatically pass signer validations into the cpi calls.
CreateV1CpiBuilder::new()
.asset(context.accounts,asset)
...
.invoke()
invoke_signed()
invoke_signed()
is used when a PDA is one of the accounts that needs to be a signer in a cpi call. Lets say for example we had a program that took possession of our Asset and one of our programs PDA addresses became the other of it. In order to transfer it and change the owner to someone else that PDA will have sign transaction.
You'll need to pass in the original PDA seeds and bump so that the PDA can be recreated can sign the cpi call on your programs behalf.
let signers = &[&[b"escrow", ctx.accounts.asset.key(), &[ctx.bumps.pda_escrow]]]
CreateV1CpiBuilder::new()
.asset(context.accounts,asset)
...
.invoke(signers)
Full CpiBuilder Example
Here is a full example of using a CpiBuilder
using the TransferV1 instruction from the Core program.
TransferV1CpiBuilder::new()
.asset(ctx.accounts.asset)
.collection(context.accounts.collection)
.payer(context.accounts.payer)
.authority(context.accounts.authority)
.new_owner(context.accounts.new_owner)
.system_program(context.accounts.system_program)
.invoke()