Макро инструкции программы
Проблема
В настоящее время для проверки транзакции в цепочке требуется зависимость от клиентской библиотеки декодирования для конкретного языка для анализа инструкции. Если бы методы rpc могли возвращать детали декодированных инструкций, в этих специальных решениях не было бы необходимости.
Мы можем десериализовать данные инструкций, используя перечисление Instruction программы, но декодирование списка ключей учетных записей в удобочитаемые идентификаторы требует ручного анализа. Наши текущие перечисления Instruction содержат информацию об этой учетной записи, но только в различных документах.
Точно так же у нас есть функции конструктора инструкций, которые дублируют почти всю информацию в перечислении, но мы не можем сгенерировать этот конструктор из определения перечисления, потому что список ссылок на учетные записи находится в комментариях к коду.
Кроме того, документы с инструкциями могут различаться в зависимости от реализации, поскольку нет механизм, обеспечивающий согласованность.
Предложенное решение
Переместите данные из комментариев кода в атрибуты, чтобы можно было сгенерировать конструкторы, и включите всю документацию из определения перечисления.
Вот пример перечисления Instruction с использованием нового формата учетных записей:
#[instructions(test_program::id())]
pub enum TestInstruction {
/// Transfer lamports
#[accounts(
from_account(SIGNER, WRITABLE, desc = "Funding account"),
to_account(WRITABLE, desc = "Recipient account"),
)]
Transfer {
lamports: u64,
},
/// Provide M of N required signatures
#[accounts(
data_account(WRITABLE, desc = "Data account"),
signers(SIGNER, multiple, desc = "Signer"),
)]
Multisig,
/// Consumes a stored nonce, replacing it with a successor
#[accounts(
nonce_account(SIGNER, WRITABLE, desc = "Nonce account"),
recent_blockhashes_sysvar(desc = "RecentBlockhashes sysvar"),
nonce_authority(SIGNER, optional, desc = "Nonce authority"),
)]
AdvanceNonceAccount,
}
Пример сгенерированной TestInstruction с документами:
pub enum TestInstruction {
/// Transfer lamports
///
/// * Accounts expected by this instruction:
/// 0. `[WRITABLE, SIGNER]` Funding account
/// 1. `[WRITABLE]` Recipient account
Transfer {
lamports: u64,
},
/// Provide M of N required signatures
///
/// * Accounts expected by this instruction:
/// 0. `[WRITABLE]` Data account
/// * (Multiple) `[SIGNER]` Signers
Multisig,
/// Consumes a stored nonce, replacing it with a successor
///
/// * Accounts expected by this instruction:
/// 0. `[WRITABLE, SIGNER]` Nonce account
/// 1. `[]` RecentBlockhashes sysvar
/// 2. (Optional) `[SIGNER]` Nonce authority
AdvanceNonceAccount,
}
Сгенерированные конструкторы:
/// Transfer lamports
///
/// * `from_account` - `[WRITABLE, SIGNER]` Funding account
/// * `to_account` - `[WRITABLE]` Recipient account
pub fn transfer(from_account: Pubkey, to_account: Pubkey, lamports: u64) -> Instruction {
let account_metas = vec![
AccountMeta::new(from_pubkey, true),
AccountMeta::new(to_pubkey, false),
];
Instruction::new_with_bincode(
test_program::id(),
&SystemInstruction::Transfer { lamports },
account_metas,
)
}
/// Provide M of N required signatures
///
/// * `data_account` - `[WRITABLE]` Data account
/// * `signers` - (Multiple) `[SIGNER]` Signers
pub fn multisig(data_account: Pubkey, signers: &[Pubkey]) -> Instruction {
let mut account_metas = vec![
AccountMeta::new(nonce_pubkey, false),
];
for pubkey in signers.iter() {
account_metas.push(AccountMeta::new_readonly(pubkey, true));
}
Instruction::new_with_bincode(
test_program::id(),
&TestInstruction::Multisig,
account_metas,
)
}
/// Consumes a stored nonce, replacing it with a successor
///
/// * nonce_account - `[WRITABLE, SIGNER]` Nonce account
/// * recent_blockhashes_sysvar - `[]` RecentBlockhashes sysvar
/// * nonce_authority - (Optional) `[SIGNER]` Nonce authority
pub fn advance_nonce_account(
nonce_account: Pubkey,
recent_blockhashes_sysvar: Pubkey,
nonce_authority: Option<Pubkey>,
) -> Instruction {
let mut account_metas = vec![
AccountMeta::new(nonce_account, false),
AccountMeta::new_readonly(recent_blockhashes_sysvar, false),
];
if let Some(pubkey) = authorized_pubkey {
account_metas.push(AccountMeta::new_readonly*nonce_authority, true));
}
Instruction::new_with_bincode(
test_program::id(),
&TestInstruction::AdvanceNonceAccount,
account_metas,
)
}
Сгенерированное перечисление TestInstructionVerbose:
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum TestInstruction {
/// Transfer lamports
Transfer {
/// Funding account
funding_account: u8
/// Recipient account
recipient_account: u8
lamports: u64,
},
/// Provide M of N required signatures
Multisig {
data_account: u8,
signers: Vec<u8>,
},
/// Consumes a stored nonce, replacing it with a successor
AdvanceNonceAccount {
nonce_account: u8,
recent_blockhashes_sysvar: u8,
nonce_authority: Option<u8>,
}
}
impl TestInstructionVerbose {
pub fn from_instruction(instruction: TestInstruction, account_keys: Vec<u8>) -> Self {
match instruction {
TestInstruction::Transfer { lamports } => TestInstructionVerbose::Transfer {
funding_account: account_keys[0],
recipient_account: account_keys[1],
lamports,
}
TestInstruction::Multisig => TestInstructionVerbose::Multisig {
data_account: account_keys[0],
signers: account_keys[1..],
}
TestInstruction::AdvanceNonceAccount => TestInstructionVerbose::AdvanceNonceAccount {
nonce_account: account_keys[0],
recent_blockhashes_sysvar: account_keys[1],
nonce_authority: &account_keys.get(2),
}
}
}
}
Соображения
- Именованные поля. Поскольку итоговое перечисление Verbose создает варианты с именованными полями, для любых неименованных полей в исходном варианте инструкции необходимо будет сгенерировать имена. Таким образом, было бы значительно проще, если бы все поля перечисления Instruction были преобразованы в именованные типы, а не в безымянные кортежи. Кажется, это стоит сделать в любом случае, добавив больше точности к вариантам и включив реальную документацию (чтобы разработчикам не нужно было делать [это](https://github.com/solana-labs/solana/blob/3aab13a1679ba2b7846d9ba39b04a52f2017d3e0/sdk/src /system_instruction.rs#L140) Это вызовет небольшое изменение нашей текущей кодовой базы, но не сильное.
- Переменные списки учетных записей. Этот подход предлагает несколько вариантов переменных списков учетных записей. Во-первых, необязательные учетные записи могут быть добавлены и помечены ключевым словом «необязательный». Однако в настоящее время для каждой инструкции поддерживается только одна необязательная учетная запись. В инструкции необходимо будет добавить дополнительные данные для поддержки множественных счетов, что позволит определить, какие учетные записи присутствуют, когда включены некоторые, но не все. Во-вторых, учетные записи с одинаковыми функциями могут быть добавлены как набор, помеченный ключевым словом «несколько». Как и в случае с необязательными учетными записями, для каждой инструкции поддерживается только один набор из нескольких учетных записей (и необязательные и несколько учетных записей могут не сосуществовать). Более сложные инструкции, которые не могут быть реализованы с помощью «необязательных» или «множественных», требующих логики для определения порядка/представления учетных записей, вероятно, должны быть выделены в отдельные инструкции.