-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Add design proposal for ProgramInstruction procedural macro #10763
Add design proposal for ProgramInstruction procedural macro #10763
Conversation
ba9fc4c
to
fa02ce2
Compare
The new syntax looks much nicer, thanks. Seems like there should be an option to generate all the instruction functions. Looks like a reasonable alg would be to snake case the instruction name, then one parameter per account, followed by one per instruction parameter. Coincidentally, those account names will allow us to generate readable parameter names. |
Also, consider generating the enum from an annotated trait definition instead of generating functions from an annotated enum definition. Starting with a trait would better reflect that each instruction effectively defines a function on some set of accounts. Also, individual parameter docs would map cleanly to those account descriptions. |
@garious Do you mind sharing some stub code of what you're thinking here? |
fa02ce2
to
c2e8069
Compare
Ideally, something like this: #[program]
pub trait Test {
/// Consumes a stored nonce, replacing it with a successor
fn advance_nonce_account(
/// Nonce account
#[signer] #[writable]
nonce_account: &KeyedAccount,
/// RecentBlockhashes sysvar
recent_blockhashes_sysvar: &KeyedAccount,
);
} This would generate Anyway, that's probably a lot more than you signed up for, so please just consider that to be the long-term goal, not the first step. If you only add the enum attribute at this time, imagine how that code would be generated from an annotated trait definition. Feels like the annotated enum might be a reasonable target for the annotated trait, such that the annotated enum is responsible for generating the instruction constructors. If so, it'd make sense to move forward with the annotated enum. |
Thanks, @garious! One question: what would the user implement trait To me, it makes sense to switch to whatever input type we want to land on now, and add output types incrementally. That is to say, if we prefer the trait attribute long-term, I think it would work to implement that attribute now to output the regular and verbose enums. Instruction constructors could happen now as well, or as a second step. Automatically generating serializers and/or process_instruction is a bit thornier, since we aren't using the same serialization between native programs and SPL. So I imagine that might be a longer-term goal. |
The trait would be for an empty struct. I don't think we need to take on the trait approach just yet. Adding attributes to the enum would be an immediate win, because then we could generate most of the functions in today's "instruction" modules. I think I'd prioritize that deduplication before docs generation. Sound like docs generation is then the next step after that. A trait to generate the enum is probably a ways down the road. |
By the way, instead of a trait, adding attributes to a module of functions might be more natural. Then there's no trait to implement. Parity's |
Oh, nice. That seems much cleaner to me than having to implement the trait and declare a bunch of empty methods for no reason. It seems to me that it would save a bit of work to settle on the input format now. That way we'll only need to write one parser, instead of writing a parser for the tagged enum now and another parser down the road. And it's not any more difficult to parse, say, a module of functions than an enum. But if you feel strongly about pushing that off, I'll concede in the interest of getting partner work unblocked. Do you have any thoughts about the considerations in my proposal? |
I believe this change should be backward compatible, since
Great question. I sure would love some brilliant suggestions in this area.
In either case, mixing a multiple account list with other account declarations would get complicated (around indexing for docs and rpc decoding). |
@CriesofCarrots Referring back to Greg's comments here: #10783 I agree with Greg that tying the comments, the enum, and the instruction constructor together via a generator ensures that they are in sync and stay that way and we should probably approach this effort via that lens. For the most part, optional accounts should only be used for simple situations and more complex optional account logic should be broken into multiple instructions. A couple of cases:
|
c5b4418
to
3c1d7bf
Compare
Agreed. However, I think we can only support one named optional account per instruction, unless we include some extra data for process_instruction to use to determine which optional account is present, when something between all and none are provided.
Yep, this will also work, but similar to the Optional account, I think we can only support one multiple account collection and no named optional accounts, unless extra data etc.
+1, I do think different instructions would add clarity in the NewToken case. I've updated the doc with a possible example of optional/multiple handling. How is this looking? |
|
||
/// Provide M of N required signatures | ||
#[accounts( | ||
data_account(writable, desc = "Data account"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This inclusion of this account is just to demonstrate how parsing might work with some named accounts and a multiple account set, not to indicate anything about how multisig ought to work.
#[accounts( | ||
nonce_account(signer, writable, desc = "Nonce account"), | ||
recent_blockhashes_sysvar(signer, writable, desc = "RecentBlockhashes sysvar"), | ||
nonce_authority(signer, optional, desc = "Nonce authority"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just made this account optional for demonstration purposes.
3c1d7bf
to
aeb8d06
Compare
pub enum TestInstruction { | ||
/// Transfer lamports | ||
#[accounts( | ||
from_account(signer, writable, desc = "Funding account"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: (from_account, signer, ...
instead of from_account(
? Something to distinguish it from the function call syntax? Also, signer
instead of SIGNER
looks like an identifier, not a constant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used the from_account(..
syntax because it is a standard attribute syntax, and a bit easier to parse in the macro. But I'll give your suggestion a go in implementation.
signer/writer
updated to SIGNER/WRITER
/// | ||
/// * `from_account` - `[writable, signer]` Funding account | ||
/// * `to_account` - `[writable]` Recipient account | ||
pub fn transfer(from_account: &Pubkey, to_account: &Pubkey, lamports: u64) -> Instruction { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Pubkey
will play out better than &Pubkey
. It'll be a bunch of changes to the existing code, but I don't see a problem with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All right. I'd like to implement that change separately from the macro application. Does that work?
/// | ||
/// * Accounts expected by this instruction: | ||
/// 0. `[writable]` Data account | ||
/// * (Multiple) `[signer]` Signers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does that *
get rendered well by cargo doc
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better than a big header line in the middle of the description :-)
/// Provide M of N required signatures | ||
/// | ||
/// * `data_account` - `[writable]` Data account | ||
/// * `signers` - (Multiple) `[signer]` Signers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't look very general. I think you could use this example as an opportunity to show how to opt out of code generation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we want a variant-level #[ignore]
tag? I'll have to think about how that should operate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just the nits. I think we could move back to implementation now and see how this first phase plays out before doing further design work.
310ae7a
to
046e8b9
Compare
…abs#10763) * Add design proposal for ProgramInstruction procedural macro * Update examples and some verbiage * More constant-like * Generated helpers expect Pubkey by value
Problem
It's difficult to parse on-chain instructions and get useful information about the expected accounts, as that information is buried in Instruction enum. We could make it easier by using a procedural macro to generate a 2nd enum that exposes account information. This would also have the benefit of enforcing consistency on program Instruction enum docs.
Summary of Changes