Migration des Comptes de Données d'un Programme
Comment migrer les comptes de données d'un programme ?
Lorsque vous créez un programme, chaque compte de données associé à celui-ci aura une structure de données spécifique. Si vous devez un jour mettre à jour un compte dérivé de programme, vous vous retrouverez avec plusieurs comptes dérivés de programme restants avec l'ancienne structure.
Grâce à la gestion des versions de comptes, vous pouvez mettre vos anciens comptes à jour vers la nouvelle structure.
Remarque
Ce n'est qu'une des nombreuses façons de migrer des données de comptes.
Scénario
Pour versionner et migrer les données de nos comptes, nous allons fournir un id pour chaque compte. Cet id nous permettra d'identifier la version du compte lorsque nous le transmettrons au programme, et ainsi de le traiter correctement.
Prenez l'état du compte et le programme suivants :
//! @brief account_state manages account data
use arrayref::{array_ref, array_refs};
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
msg,
program_error::ProgramError,
program_pack::{IsInitialized, Pack, Sealed},
};
use std::{io::BufWriter, mem};
/// Currently using state. If version changes occur, this
/// should be copied to another serializable backlevel one
/// before adding new fields here
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentCurrent {
pub somevalue: u64,
}
/// Maintains account data
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct ProgramAccountState {
is_initialized: bool,
data_version: u8,
account_data: AccountContentCurrent,
}
impl ProgramAccountState {
/// Signal initialized
pub fn set_initialized(&mut self) {
self.is_initialized = true;
}
/// Get the initialized flag
pub fn initialized(&self) -> bool {
self.is_initialized
}
/// Gets the current data version
pub fn version(&self) -> u8 {
self.data_version
}
/// Get the reference to content structure
pub fn content(&self) -> &AccountContentCurrent {
&self.account_data
}
/// Get the mutable reference to content structure
pub fn content_mut(&mut self) -> &mut AccountContentCurrent {
&mut self.account_data
}
}
/// Declaration of the current data version.
pub const DATA_VERSION: u8 = 0;
/// Account allocated size
pub const ACCOUNT_ALLOCATION_SIZE: usize = 1024;
/// Initialized flag is 1st byte of data block
const IS_INITIALIZED: usize = 1;
/// Data version (current) is 2nd byte of data block
const DATA_VERSION_ID: usize = 1;
/// Previous content data size (before changing this is equal to current)
pub const PREVIOUS_VERSION_DATA_SIZE: usize = mem::size_of::<AccountContentCurrent>();
/// Total space occupied by previous account data
pub const PREVIOUS_ACCOUNT_SPACE: usize =
IS_INITIALIZED + DATA_VERSION_ID + PREVIOUS_VERSION_DATA_SIZE;
/// Current content data size
pub const CURRENT_VERSION_DATA_SIZE: usize = mem::size_of::<AccountContentCurrent>();
/// Total usage for data only
pub const CURRENT_USED_SIZE: usize = IS_INITIALIZED + DATA_VERSION_ID + CURRENT_VERSION_DATA_SIZE;
/// How much of 1024 is used
pub const CURRENT_UNUSED_SIZE: usize = ACCOUNT_ALLOCATION_SIZE - CURRENT_USED_SIZE;
/// Current space used by header (initialized, data version and Content)
pub const ACCOUNT_STATE_SPACE: usize = CURRENT_USED_SIZE + CURRENT_UNUSED_SIZE;
/// Future data migration logic that converts prior state of data
/// to current state of data
fn conversion_logic(src: &[u8]) -> Result<ProgramAccountState, ProgramError> {
let past = array_ref![src, 0, PREVIOUS_ACCOUNT_SPACE];
let (initialized, _, _account_space) = array_refs![
past,
IS_INITIALIZED,
DATA_VERSION_ID,
PREVIOUS_VERSION_DATA_SIZE
];
// Logic to uplift from previous version
// GOES HERE
// Give back
Ok(ProgramAccountState {
is_initialized: initialized[0] != 0u8,
data_version: DATA_VERSION,
account_data: AccountContentCurrent::default(),
})
}
impl Sealed for ProgramAccountState {}
impl IsInitialized for ProgramAccountState {
fn is_initialized(&self) -> bool {
self.is_initialized
}
}
impl Pack for ProgramAccountState {
const LEN: usize = ACCOUNT_STATE_SPACE;
/// Store 'state' of account to its data area
fn pack_into_slice(&self, dst: &mut [u8]) {
let mut bw = BufWriter::new(dst);
self.serialize(&mut bw).unwrap();
}
/// Retrieve 'state' of account from account data area
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
let initialized = src[0] != 0;
// Check initialized
if initialized {
// Version check
if src[1] == DATA_VERSION {
msg!("Processing consistent data");
Ok(
ProgramAccountState::try_from_slice(array_ref![src, 0, CURRENT_USED_SIZE])
.unwrap(),
)
} else {
msg!("Processing backlevel data");
conversion_logic(src)
}
} else {
msg!("Processing pre-initialized data");
Ok(ProgramAccountState {
is_initialized: false,
data_version: DATA_VERSION,
account_data: AccountContentCurrent::default(),
})
}
}
}
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentCurrent {
pub somevalue: u64,
}
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct ProgramAccountState {
is_initialized: bool,
data_version: u8,
account_data: AccountContentCurrent,
}
//! instruction Contains the main ProgramInstruction enum
use {
crate::error::DataVersionError,
borsh::{BorshDeserialize, BorshSerialize},
solana_program::program_error::ProgramError,
};
#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)]
/// All custom program instructions
pub enum ProgramInstruction {
InitializeAccount,
SetU64Value(u64),
FailInstruction,
}
impl ProgramInstruction {
/// Unpack inbound buffer to associated Instruction
/// The expected format for input is a Borsh serialized vector
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
let payload = ProgramInstruction::try_from_slice(input).unwrap();
match payload {
ProgramInstruction::InitializeAccount => Ok(payload),
ProgramInstruction::SetU64Value(_) => Ok(payload),
_ => Err(DataVersionError::InvalidInstruction.into()),
}
}
}
impl ProgramInstruction {
/// Unpack inbound buffer to associated Instruction
/// The expected format for input is a Borsh serialized vector
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
let payload = ProgramInstruction::try_from_slice(input).unwrap();
match payload {
ProgramInstruction::InitializeAccount => Ok(payload),
ProgramInstruction::SetU64Value(_) => Ok(payload),
_ => Err(DataVersionError::InvalidInstruction.into()),
}
}
}
//! Resolve instruction and execute
use crate::{
account_state::ProgramAccountState, error::DataVersionError, instruction::ProgramInstruction,
};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
program_pack::{IsInitialized, Pack},
pubkey::Pubkey,
};
/// Checks each tracking account to confirm it is owned by our program
/// This function assumes that the program account is always the last
/// in the array
fn check_account_ownership(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
// Accounts must be owned by the program.
for account in accounts.iter().take(accounts.len() - 1) {
if account.owner != program_id {
msg!(
"Fail: The tracking account owner is {} and it should be {}.",
account.owner,
program_id
);
return Err(ProgramError::IncorrectProgramId);
}
}
Ok(())
}
/// Initialize the programs account, which is the first in accounts
fn initialize_account(accounts: &[AccountInfo]) -> ProgramResult {
msg!("Initialize account");
let account_info_iter = &mut accounts.iter();
let program_account = next_account_info(account_info_iter)?;
let mut account_data = program_account.data.borrow_mut();
// Just using unpack will check to see if initialized and will
// fail if not
let mut account_state = ProgramAccountState::unpack_unchecked(&account_data)?;
// Where this is a logic error in trying to initialize the same account more than once
if account_state.is_initialized() {
return Err(DataVersionError::AlreadyInitializedState.into());
} else {
account_state.set_initialized();
account_state.content_mut().somevalue = 1;
}
msg!("Account Initialized");
// Serialize
ProgramAccountState::pack(account_state, &mut account_data)
}
/// Sets the u64 in the content structure
fn set_u64_value(accounts: &[AccountInfo], value: u64) -> ProgramResult {
msg!("Set new value {}", value);
let account_info_iter = &mut accounts.iter();
let program_account = next_account_info(account_info_iter)?;
let mut account_data = program_account.data.borrow_mut();
let mut account_state = ProgramAccountState::unpack(&account_data)?;
account_state.content_mut().somevalue = value;
// Serialize
ProgramAccountState::pack(account_state, &mut account_data)
}
/// Main processing entry point dispatches to specific
/// instruction handlers
pub fn process(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
msg!("Received process request");
// Check the account for program relationship
if let Err(error) = check_account_ownership(program_id, accounts) {
return Err(error);
};
// Unpack the inbound data, mapping instruction to appropriate structure
let instruction = ProgramInstruction::unpack(instruction_data)?;
match instruction {
ProgramInstruction::InitializeAccount => initialize_account(accounts),
ProgramInstruction::SetU64Value(value) => set_u64_value(accounts, value),
_ => {
msg!("Received unknown instruction");
Err(DataVersionError::InvalidInstruction.into())
}
}
}
fn check_account_ownership(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
// Accounts must be owned by the program.
for account in accounts.iter().take(accounts.len() - 1) {
if account.owner != program_id {
msg!(
"Fail: The tracking account owner is {} and it should be {}.",
account.owner,
program_id
);
return Err(ProgramError::IncorrectProgramId);
}
}
Ok(())
}
/// Initialize the programs account, which is the first in accounts
fn initialize_account(accounts: &[AccountInfo]) -> ProgramResult {
msg!("Initialize account");
let account_info_iter = &mut accounts.iter();
let program_account = next_account_info(account_info_iter)?;
let mut account_data = program_account.data.borrow_mut();
// Just using unpack will check to see if initialized and will
// fail if not
let mut account_state = ProgramAccountState::unpack_unchecked(&account_data)?;
// Where this is a logic error in trying to initialize the same account more than once
if account_state.is_initialized() {
return Err(DataVersionError::AlreadyInitializedState.into());
} else {
account_state.set_initialized();
account_state.content_mut().somevalue = 1;
}
msg!("Account Initialized");
// Serialize
ProgramAccountState::pack(account_state, &mut account_data)
}
/// Sets the u64 in the content structure
fn set_u64_value(accounts: &[AccountInfo], value: u64) -> ProgramResult {
msg!("Set new value {}", value);
let account_info_iter = &mut accounts.iter();
let program_account = next_account_info(account_info_iter)?;
let mut account_data = program_account.data.borrow_mut();
let mut account_state = ProgramAccountState::unpack(&account_data)?;
account_state.content_mut().somevalue = value;
// Serialize
ProgramAccountState::pack(account_state, &mut account_data)
}
/// Main processing entry point dispatches to specific
/// instruction handlers
pub fn process(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
msg!("Received process request");
// Check the account for program relationship
if let Err(error) = check_account_ownership(program_id, accounts) {
return Err(error);
};
// Unpack the inbound data, mapping instruction to appropriate structure
let instruction = ProgramInstruction::unpack(instruction_data)?;
match instruction {
ProgramInstruction::InitializeAccount => initialize_account(accounts),
ProgramInstruction::SetU64Value(value) => set_u64_value(accounts, value),
_ => {
msg!("Received unknown instruction");
Err(DataVersionError::InvalidInstruction.into())
}
}
}
Dans notre première version de compte, nous faisons ce qui suit :
ID | Action |
---|---|
1 | Inclure un champ "version des données (data version)" dans les données. Il peut s'agir d'un simple nombre que l'on incrémente (par exemple, u8) ou de quelque chose de plus sophistiqué |
2 | Allocation d'un espace suffisant pour la croissance des données |
3 | Initialisation d'un certain nombre de constantes à utiliser entre les différentes versions du programme |
4 | Ajout d'une fonction de mise à jour du compte sous fn conversion_logic pour les futures mises à jour |
Supposons que nous voulions mettre à jour les comptes de notre programme afin d'inclure un nouveau champ obligatoire, le champ somestring
.
Si nous n'avons pas alloué d'espace supplémentaire sur l'ancien compte, nous ne pourrions pas mettre à jour le compte et nous serions bloqués.
Mise à jour du Compte
Dans notre nouveau programme, nous voulons ajouter une nouvelle propriété pour l'état du contenu. Les modifications ci-dessous montrent comment nous avons profité de la structure initiale du programme pour ajouter le nouveau champ.
1. Ajouter une logique de conversion de compte
//! @brief account_state manages account data
use arrayref::{array_ref, array_refs};
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
borsh::try_from_slice_unchecked,
msg,
program_error::ProgramError,
program_pack::{IsInitialized, Pack, Sealed},
};
use std::{io::BufWriter, mem};
/// Current state (DATA_VERSION 1). If version changes occur, this
/// should be copied to another (see AccountContentOld below)
/// We've added a new field: 'somestring'
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentCurrent {
pub somevalue: u64,
pub somestring: String,
}
/// Old content state (DATA_VERSION 0).
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentOld {
pub somevalue: u64,
}
/// Maintains account data
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct ProgramAccountState {
is_initialized: bool,
data_version: u8,
account_data: AccountContentCurrent,
}
impl ProgramAccountState {
/// Signal initialized
pub fn set_initialized(&mut self) {
self.is_initialized = true;
}
/// Get the initialized flag
pub fn initialized(&self) -> bool {
self.is_initialized
}
/// Gets the current data version
pub fn version(&self) -> u8 {
self.data_version
}
/// Get the reference to content structure
pub fn content(&self) -> &AccountContentCurrent {
&self.account_data
}
/// Get the mutable reference to content structure
pub fn content_mut(&mut self) -> &mut AccountContentCurrent {
&mut self.account_data
}
}
/// Declaration of the current data version.
const DATA_VERSION: u8 = 1; // Adding string to content
// Previous const DATA_VERSION: u8 = 0;
/// Account allocated size
const ACCOUNT_ALLOCATION_SIZE: usize = 1024;
/// Initialized flag is 1st byte of data block
const IS_INITIALIZED: usize = 1;
/// Data version (current) is 2nd byte of data block
const DATA_VERSION_ID: usize = 1;
/// Previous content data size (before changing this is equal to current)
const PREVIOUS_VERSION_DATA_SIZE: usize = mem::size_of::<AccountContentOld>();
/// Total space occupied by previous account data
const PREVIOUS_ACCOUNT_SPACE: usize = IS_INITIALIZED + DATA_VERSION_ID + PREVIOUS_VERSION_DATA_SIZE;
/// Current content data size
const CURRENT_VERSION_DATA_SIZE: usize = mem::size_of::<AccountContentCurrent>();
/// Total usage for data only
const CURRENT_USED_SIZE: usize = IS_INITIALIZED + DATA_VERSION_ID + CURRENT_VERSION_DATA_SIZE;
/// How much of 1024 is used
const CURRENT_UNUSED_SIZE: usize = ACCOUNT_ALLOCATION_SIZE - CURRENT_USED_SIZE;
/// Current space used by header (initialized, data version and Content)
pub const ACCOUNT_STATE_SPACE: usize = CURRENT_USED_SIZE + CURRENT_UNUSED_SIZE;
/// Future data migration logic that converts prior state of data
/// to current state of data
fn conversion_logic(src: &[u8]) -> Result<ProgramAccountState, ProgramError> {
let past = array_ref![src, 0, PREVIOUS_ACCOUNT_SPACE];
let (initialized, _, account_space) = array_refs![
past,
IS_INITIALIZED,
DATA_VERSION_ID,
PREVIOUS_VERSION_DATA_SIZE
];
// Logic to upgrade from previous version
// GOES HERE
let old = try_from_slice_unchecked::<AccountContentOld>(account_space).unwrap();
// Default sets 'somevalue' to 0 and somestring to default ""
let mut new_content = AccountContentCurrent::default();
// We copy the existing 'somevalue', the program instructions will read/update 'somestring' without fail
new_content.somevalue = old.somevalue;
// Give back
Ok(ProgramAccountState {
is_initialized: initialized[0] != 0u8,
data_version: DATA_VERSION,
account_data: new_content,
})
}
impl Sealed for ProgramAccountState {}
impl IsInitialized for ProgramAccountState {
fn is_initialized(&self) -> bool {
self.is_initialized
}
}
impl Pack for ProgramAccountState {
const LEN: usize = ACCOUNT_STATE_SPACE;
/// Store 'state' of account to its data area
fn pack_into_slice(&self, dst: &mut [u8]) {
let mut bw = BufWriter::new(dst);
self.serialize(&mut bw).unwrap();
}
/// Retrieve 'state' of account from account data area
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
let initialized = src[0] != 0;
// Check initialized
if initialized {
// Version check
if src[1] == DATA_VERSION {
msg!("Processing consistent version data");
Ok(try_from_slice_unchecked::<ProgramAccountState>(src).unwrap())
} else {
msg!("Processing backlevel data");
conversion_logic(src)
}
} else {
msg!("Processing pre-initialized data");
Ok(ProgramAccountState {
is_initialized: false,
data_version: DATA_VERSION,
account_data: AccountContentCurrent::default(),
})
}
}
}
/// Current state (DATA_VERSION 1). If version changes occur, this
/// should be copied to another (see AccountContentOld below)
/// We've added a new field: 'somestring'
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentCurrent {
pub somevalue: u64,
pub somestring: String,
}
/// Old content state (DATA_VERSION 0).
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentOld {
pub somevalue: u64,
}
/// Maintains account data
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct ProgramAccountState {
is_initialized: bool,
data_version: u8,
account_data: AccountContentCurrent,
}
Ligne(s) | Note |
---|---|
6 | Nous avons ajouté la fonction solana_program::borsh::try_from_slice_unchecked de Solana pour simplifier la lecture de sous-ensembles de données d'un bloc de données plus grand |
13-26 | Ici, nous avons conservé l'ancienne structure de contenu, AccountContentOld ligne 24, avant d'étendre le AccountContentCurrent à partir de la ligne 17. |
60 | Nous actualisons la constante DATA_VERSION |
71 | Nous avons maintenant une version "précédente" et nous voulons connaître sa taille |
86 | Le coup de grâce consiste à ajouter la tuyauterie pour mettre à jour le contenu de l'état précédent vers le nouveau contenu de l'état (actuel). |
Nous mettons alors à jour nos instructions, pour en ajouter une nouvelle pour mettre à jour somestring
, et un processeur pour gérer la nouvelle instruction. Notez que la "mise à jour" de la structure de données est encapsulée derrière pack/unpack
.
//! instruction Contains the main VersionProgramInstruction enum
use {
crate::error::DataVersionError,
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{borsh::try_from_slice_unchecked, msg, program_error::ProgramError},
};
#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)]
/// All custom program instructions
pub enum VersionProgramInstruction {
InitializeAccount,
SetU64Value(u64),
SetString(String), // Added with data version change
FailInstruction,
}
impl VersionProgramInstruction {
/// Unpack inbound buffer to associated Instruction
/// The expected format for input is a Borsh serialized vector
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
let payload = try_from_slice_unchecked::<VersionProgramInstruction>(input).unwrap();
// let payload = VersionProgramInstruction::try_from_slice(input).unwrap();
match payload {
VersionProgramInstruction::InitializeAccount => Ok(payload),
VersionProgramInstruction::SetU64Value(_) => Ok(payload),
VersionProgramInstruction::SetString(_) => Ok(payload), // Added with data version change
_ => Err(DataVersionError::InvalidInstruction.into()),
}
}
}
//! Resolve instruction and execute
use crate::{
account_state::ProgramAccountState, error::DataVersionError,
instruction::VersionProgramInstruction,
};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
program_pack::{IsInitialized, Pack},
pubkey::Pubkey,
};
/// Checks each tracking account to confirm it is owned by our program
/// This function assumes that the program account is always the last
/// in the array
fn check_account_ownership(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
// Accounts must be owned by the program.
for account in accounts.iter().take(accounts.len() - 1) {
if account.owner != program_id {
msg!(
"Fail: The tracking account owner is {} and it should be {}.",
account.owner,
program_id
);
return Err(ProgramError::IncorrectProgramId);
}
}
Ok(())
}
/// Initialize the programs account, which is the first in accounts
fn initialize_account(accounts: &[AccountInfo]) -> ProgramResult {
msg!("Initialize account");
let account_info_iter = &mut accounts.iter();
let program_account = next_account_info(account_info_iter)?;
let mut account_data = program_account.data.borrow_mut();
// Just using unpack will check to see if initialized and will
// fail if not
let mut account_state = ProgramAccountState::unpack_unchecked(&account_data)?;
// Where this is a logic error in trying to initialize the same account more than once
if account_state.is_initialized() {
return Err(DataVersionError::AlreadyInitializedState.into());
} else {
account_state.set_initialized();
account_state.content_mut().somevalue = 1;
}
msg!("Account Initialized");
// Serialize
ProgramAccountState::pack(account_state, &mut account_data)
}
/// Sets the u64 in the content structure
fn set_u64_value(accounts: &[AccountInfo], value: u64) -> ProgramResult {
msg!("Set new value {}", value);
let account_info_iter = &mut accounts.iter();
let program_account = next_account_info(account_info_iter)?;
let mut account_data = program_account.data.borrow_mut();
let mut account_state = ProgramAccountState::unpack(&account_data)?;
account_state.content_mut().somevalue = value;
// Serialize
ProgramAccountState::pack(account_state, &mut account_data)
}
/// Sets the string in the content structure
fn set_string_value(accounts: &[AccountInfo], value: String) -> ProgramResult {
msg!("Set new string {}", value);
let account_info_iter = &mut accounts.iter();
let program_account = next_account_info(account_info_iter)?;
let mut account_data = program_account.data.borrow_mut();
let mut account_state = ProgramAccountState::unpack(&account_data)?;
account_state.content_mut().somestring = value;
// Serialize
ProgramAccountState::pack(account_state, &mut account_data)
}
/// Main processing entry point dispatches to specific
/// instruction handlers
pub fn process(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
msg!("Received process request 0.2.0");
// Check the account for program relationship
if let Err(error) = check_account_ownership(program_id, accounts) {
return Err(error);
};
// Unpack the inbound data, mapping instruction to appropriate structure
msg!("Attempting to unpack");
let instruction = VersionProgramInstruction::unpack(instruction_data)?;
match instruction {
VersionProgramInstruction::InitializeAccount => initialize_account(accounts),
VersionProgramInstruction::SetU64Value(value) => set_u64_value(accounts, value),
VersionProgramInstruction::SetString(value) => set_string_value(accounts, value),
_ => {
msg!("Received unknown instruction");
Err(DataVersionError::InvalidInstruction.into())
}
}
}
Après avoir construit et soumis une instruction : VersionProgramInstruction::SetString(String)
nous avons maintenant la structure de données de compte "mise à jour" suivante