Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Namespaced Instructions #2786

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lang/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ anchor-debug = [
"anchor-attribute-error/anchor-debug",
"anchor-attribute-event/anchor-debug",
"anchor-attribute-program/anchor-debug",
"anchor-derive-accounts/anchor-debug"
"anchor-derive-accounts/anchor-debug",
]
derive = []
event-cpi = ["anchor-attribute-event/event-cpi"]
Expand Down
12 changes: 6 additions & 6 deletions lang/attribute/program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ pub fn program(
.into()
}

/// The `#[interface]` attribute is used to mark an instruction as belonging
/// to an interface implementation, thus transforming its discriminator to the
/// proper bytes for that interface instruction.
/// The `#[ix]` attribute is used to mark an instruction as belonging
/// to a namespace, and optionally rename the instruction so it has a specific
/// 8-byte discriminator.
///
/// # Example
///
Expand All @@ -28,7 +28,7 @@ pub fn program(
/// //
/// // This instruction is invoked by Token-2022 when a transfer occurs,
/// // if a mint has specified this program as its transfer hook.
/// #[interface(spl_transfer_hook_interface::execute)]
/// #[ix(namespace="spl_transfer_hook_interface", name="execute")]
/// pub fn execute_transfer(ctx: Context<Execute>, amount: u64) -> Result<()> {
/// // Check that all extra accounts were provided
/// let data = ctx.accounts.extra_metas_account.try_borrow_data()?;
Expand All @@ -49,12 +49,12 @@ pub fn program(
/// ```
#[cfg(feature = "interface-instructions")]
#[proc_macro_attribute]
pub fn interface(
pub fn ix(
_args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
// This macro itself is a no-op, but must be defined as a proc-macro
// attribute to be used on a function as the `#[interface]` attribute.
// attribute to be used on a function as the `#[ix]` attribute.
//
// The `#[program]` macro will detect this attribute and transform the
// discriminator.
Expand Down
4 changes: 2 additions & 2 deletions lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pub use anchor_attribute_event::{emit_cpi, event_cpi};
pub use anchor_syn::{self, idl::build::IdlBuild};

#[cfg(feature = "interface-instructions")]
pub use anchor_attribute_program::interface;
pub use anchor_attribute_program::ix;

pub type Result<T> = std::result::Result<T, error::Error>;

Expand Down Expand Up @@ -425,7 +425,7 @@ pub mod prelude {
pub use super::IdlBuild;

#[cfg(feature = "interface-instructions")]
pub use super::interface;
pub use super::ix;
}

/// Internal module used by macros and unstable apis.
Expand Down
6 changes: 5 additions & 1 deletion lang/syn/src/idl/parse/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,12 @@ pub fn parse(
_ => Some(ret_type_str.parse().unwrap()),
};
IdlInstruction {
name: ix.ident.to_string().to_mixed_case(),
name: ix
.name_override
.clone()
.unwrap_or(ix.ident.to_string().to_mixed_case()),
docs: ix.docs.clone(),
namespace: ix.namespace.clone(),
accounts,
args,
returns,
Expand Down
2 changes: 2 additions & 0 deletions lang/syn/src/idl/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub struct IdlState {
pub struct IdlInstruction {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docs: Option<Vec<String>>,
pub accounts: Vec<IdlAccountItem>,
pub args: Vec<IdlField>,
Expand Down
2 changes: 2 additions & 0 deletions lang/syn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ impl ToTokens for Program {
pub struct Ix {
pub raw_method: ItemFn,
pub ident: Ident,
pub namespace: Option<String>,
pub name_override: Option<String>,
pub docs: Option<Vec<String>>,
pub args: Vec<IxArg>,
pub returns: IxReturn,
Expand Down
68 changes: 68 additions & 0 deletions lang/syn/src/parser/ix_interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#[cfg(feature = "interface-instructions")]
use syn::{Lit, Meta, NestedMeta};

#[derive(Clone)]
pub struct NameOverrides {
pub namespace: Option<String>,
pub name: Option<String>,
}

#[cfg(not(feature = "interface-instructions"))]
pub fn parse(_attrs: &[syn::Attribute]) -> NameOverrides {
NameOverrides {
namespace: None,
name: None,
}
}

#[cfg(feature = "interface-instructions")]
pub fn parse(attrs: &[syn::Attribute]) -> NameOverrides {
let interfaces: Vec<NameOverrides> = attrs
.iter()
.filter_map(|attr| {
if attr.path.is_ident("ix") {
let mut namespace: Option<String> = None;
let mut ix_name: Option<String> = None;

if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
for nested_meta in meta_list.nested.iter() {
if let NestedMeta::Meta(Meta::NameValue(namevalue)) = nested_meta {
if namevalue.path.segments[0].ident.to_string() == "namespace" {
if let Lit::Str(namespace_override) = &namevalue.lit {
namespace = Some(namespace_override.value().clone());
}
}

if namevalue.path.segments[0].ident.to_string() == "name" {
if let Lit::Str(name_override) = &namevalue.lit {
ix_name = Some(name_override.value().clone());
}
}
}
}
return Some(NameOverrides {
namespace,
name: ix_name,
});
}
panic!(
"Failed to parse interface instruction:\n{:?} {:?} {:?}",
quote::quote!(#attr),
ix_name,
namespace
);
}
None
})
.collect();
if interfaces.len() > 1 {
panic!("An instruction can only implement one interface instruction");
} else if interfaces.is_empty() {
NameOverrides {
namespace: None,
name: None,
}
} else {
interfaces[0].clone()
}
}
2 changes: 1 addition & 1 deletion lang/syn/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ pub mod accounts;
pub mod context;
pub mod docs;
pub mod error;
pub mod ix_interface;
pub mod program;
pub mod spl_interface;

pub fn tts_to_string<T: quote::ToTokens>(item: T) -> String {
let mut tts = proc_macro2::TokenStream::new();
Expand Down
22 changes: 19 additions & 3 deletions lang/syn/src/parser/program/instructions.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::codegen::program::common::sighash;
use crate::codegen::program::common::SIGHASH_GLOBAL_NAMESPACE;
use crate::parser::docs;
use crate::parser::ix_interface;
use crate::parser::program::ctx_accounts_ident;
use crate::parser::spl_interface;
use crate::{FallbackFn, Ix, IxArg, IxReturn};
use syn::parse::{Error as ParseError, Result as ParseResult};
use syn::spanned::Spanned;
Expand All @@ -25,18 +27,32 @@ pub fn parse(program_mod: &syn::ItemMod) -> ParseResult<(Vec<Ix>, Option<Fallbac
})
.map(|method: &syn::ItemFn| {
let (ctx, args) = parse_args(method)?;
let interface_discriminator = spl_interface::parse(&method.attrs);
let ix_interface = ix_interface::parse(&method.attrs);
let docs = docs::parse(&method.attrs);
let returns = parse_return(method)?;
let anchor_ident = ctx_accounts_ident(&ctx.raw_arg)?;

let name_override = ix_interface.name.clone();
let namespace = ix_interface
.namespace
.unwrap_or_else(|| SIGHASH_GLOBAL_NAMESPACE.to_string());

let discriminator: Option<[u8; 8]> = if let Some(ix_name_override) = name_override {
Some(sighash(&namespace, &ix_name_override))
} else {
Some(sighash(&namespace, &method.sig.ident.to_string()))
};

Ok(Ix {
raw_method: method.clone(),
ident: method.sig.ident.clone(),
docs,
args,
anchor_ident,
returns,
interface_discriminator,
name_override: ix_interface.name,
namespace: Some(namespace),
interface_discriminator: discriminator,
})
})
.collect::<ParseResult<Vec<Ix>>>()?;
Expand Down
63 changes: 0 additions & 63 deletions lang/syn/src/parser/spl_interface.rs

This file was deleted.

9 changes: 6 additions & 3 deletions tests/spl/transfer-hook/programs/transfer-hook/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ fn check_token_account_is_transferring(account_data: &[u8]) -> Result<()> {
pub mod transfer_hook {
use super::*;

#[interface(spl_transfer_hook_interface::initialize_extra_account_meta_list)]
#[ix(
namespace = "spl-transfer-hook-interface",
name = "initialize-extra-account-metas"
)]
pub fn initialize(ctx: Context<Initialize>, metas: Vec<AnchorExtraAccountMeta>) -> Result<()> {
let extra_metas_account = &ctx.accounts.extra_metas_account;
let mint = &ctx.accounts.mint;
Expand All @@ -68,7 +71,7 @@ pub mod transfer_hook {
Ok(())
}

#[interface(spl_transfer_hook_interface::execute)]
#[ix(namespace = "spl-transfer-hook-interface")]
pub fn execute(ctx: Context<Execute>, amount: u64) -> Result<()> {
let source_account = &ctx.accounts.source_account;
let destination_account = &ctx.accounts.destination_account;
Expand All @@ -82,7 +85,7 @@ pub mod transfer_hook {
ExtraAccountMetaList::check_account_infos::<ExecuteInstruction>(
&ctx.accounts.to_account_infos(),
&TransferHookInstruction::Execute { amount }.pack(),
&ctx.program_id,
ctx.program_id,
&data,
)?;

Expand Down
28 changes: 16 additions & 12 deletions tests/spl/transfer-hook/tests/transfer-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ describe("transfer hook", () => {
});

it("can create an `InitializeExtraAccountMetaList` instruction with the proper discriminator", async () => {
const ix = await program.methods
.initialize(extraMetas as any[])
.interface("spl_transfer_hook_interface::initialize_extra_account_metas")
console.log("namespace", program.namespace);
const ix = await program.namespace.splTransferHookInterface
.initializeExtraAccountMetas(extraMetas as any[])
.accounts({
extraMetasAccount: extraMetasAddress,
mint: mint.publicKey,
Expand All @@ -163,18 +163,22 @@ describe("transfer hook", () => {
const { name, data } = new anchor.BorshInstructionCoder(program.idl).decode(
ix.data,
"hex",
"initialize"
"initialize-extra-account-metas",
"spl-transfer-hook-interface"
);
assert.equal(
name,
// Snake cased full-name
"spl-transfer-hook-interface:initialize-extra-account-metas"
);
assert.equal(name, "initialize");
assert.property(data, "metas");
assert.isArray(data.metas);
assert.equal(data.metas.length, extraMetas.length);
});

it("can create an `Execute` instruction with the proper discriminator", async () => {
const ix = await program.methods
const ix = await program.namespace.splTransferHookInterface
.execute(new anchor.BN(transferAmount))
.interface("spl_transfer_hook_interface::execute")
.accounts({
sourceAccount: source,
mint: mint.publicKey,
Expand All @@ -194,19 +198,19 @@ describe("transfer hook", () => {
const { name, data } = new anchor.BorshInstructionCoder(program.idl).decode(
ix.data,
"hex",
"execute"
"execute",
"spl-transfer-hook-interface"
);
assert.equal(name, "execute");
assert.equal(name, "spl-transfer-hook-interface:execute");
assert.property(data, "amount");
assert.isTrue(anchor.BN.isBN(data.amount));
assert.isTrue(data.amount.eq(new anchor.BN(transferAmount)));
});

it("can transfer with extra account metas", async () => {
// Initialize the extra metas
await program.methods
.initialize(extraMetas as any[])
.interface("spl_transfer_hook_interface::initialize_extra_account_metas")
await program.namespace.splTransferHookInterface
.initializeExtraAccountMetas(extraMetas as any[])
.accounts({
extraMetasAccount: extraMetasAddress,
mint: mint.publicKey,
Expand Down
Loading
Loading