diff --git a/example/src/bin/migrate.rs b/example/src/bin/migrate.rs new file mode 100644 index 0000000..1d4cba3 --- /dev/null +++ b/example/src/bin/migrate.rs @@ -0,0 +1,57 @@ +use byte_unit::Byte; +use shadow_drive_rust::{ShadowDriveClient, StorageAccountVersion}; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::{ + commitment_config::CommitmentConfig, pubkey::Pubkey, signer::keypair::read_keypair_file, +}; +use std::str::FromStr; +use std::time::Duration; + +const KEYPAIR_PATH: &str = "keypair.json"; + +#[tokio::main] +async fn main() { + //load keypair from file + let keypair = read_keypair_file(KEYPAIR_PATH).expect("failed to load keypair at path"); + + //create shdw drive client + let shdw_drive_client = ShadowDriveClient::new(keypair, "https://ssc-dao.genesysgo.net"); + + // create V1 storage account + let v1_response = shdw_drive_client + .create_storage_account( + "1.5-test", + Byte::from_str("1MB").expect("invalid byte string"), + StorageAccountVersion::v1(), + ) + .await + .expect("error creating storage account"); + + println!("v1: {:?} \n", v1_response); + + let key_string: String = v1_response.shdw_bucket.unwrap(); + let v1_pubkey: Pubkey = Pubkey::from_str(&key_string).unwrap(); + + // can migrate all at once + let migrate = shdw_drive_client + .migrate(&v1_pubkey) + .await + .expect("failed to migrate"); + println!("Migrated {:?} \n", migrate); + + // alternatively can split migration into 2 steps (boths steps are exposed) + + // // step 1 + // let migrate_step_1 = shdw_drive_client + // .migrate_step_1(&v1_pubkey) + // .await + // .expect("failed to migrate v1 step 1"); + // println!("Step 1 complete {:?} \n", migrate_step_1); + + // // step 2 + // let migrate_step_2 = shdw_drive_client + // .migrate_step_2(&v1_pubkey) + // .await + // .expect("failed to migrate v1 step 2"); + // println!("Step 2 complete {:?} \n", migrate_step_2); +} diff --git a/src/client.rs b/src/client.rs index 83957f9..97d6145 100644 --- a/src/client.rs +++ b/src/client.rs @@ -16,6 +16,7 @@ mod edit_file; mod get_storage_account; mod list_objects; mod make_storage_immutable; +mod migrate; mod reduce_storage; mod store_files; // mod upload_multiple_files; @@ -31,6 +32,7 @@ pub use edit_file::*; pub use get_storage_account::*; pub use list_objects::*; pub use make_storage_immutable::*; +pub use migrate::*; pub use reduce_storage::*; pub use store_files::*; diff --git a/src/client/migrate.rs b/src/client/migrate.rs new file mode 100644 index 0000000..758fe5e --- /dev/null +++ b/src/client/migrate.rs @@ -0,0 +1,123 @@ +use anchor_lang::{system_program, InstructionData, ToAccountMetas}; +use shadow_drive_user_staking::accounts as shdw_drive_accounts; +use shadow_drive_user_staking::instruction as shdw_drive_instructions; +use solana_sdk::{ + instruction::Instruction, pubkey::Pubkey, signer::Signer, transaction::Transaction, +}; + +use super::ShadowDriveClient; + +use crate::{constants::PROGRAM_ADDRESS, derived_addresses, models::*}; + +impl ShadowDriveClient +where + T: Signer + Send + Sync, +{ + /// Migrates a v1 [`StorageAccount`](crate::models::StorageAccount) to v2. + /// This requires two separate transactions to reuse the original pubkey. To minimize chance of failure, it is recommended to call this method with a [commitment level][cl] of [`Finalized`](solana_sdk::commitment_config::CommitmentLevel::Finalized) + /// + /// [cl]: https://docs.solana.com/developing/clients/jsonrpc-api#configuring-state-commitment + /// + /// * `storage_account_key` - The public key of the [`StorageAccount`](crate::models::StorageAccount) to be migrated. + /// + /// # Example + /// + /// ``` + /// # use shadow_drive_rust::{ShadowDriveClient, derived_addresses::storage_account}; + /// # use solana_client::rpc_client::RpcClient; + /// # use solana_sdk::{ + /// # pubkey::Pubkey, + /// # signature::Keypair, + /// # signer::{keypair::read_keypair_file, Signer}, + /// # }; + /// # + /// # let keypair = read_keypair_file(KEYPAIR_PATH).expect("failed to load keypair at path"); + /// # let user_pubkey = keypair.pubkey(); + /// # let rpc_client = RpcClient::new("https://ssc-dao.genesysgo.net"); + /// # let shdw_drive_client = ShadowDriveClient::new(keypair, rpc_client); + /// # let (storage_account_key, _) = storage_account(&user_pubkey, 0); + /// # + ///let migrate_response = shdw_drive_client + /// .migrate(&storage_account_key) + /// .await?; + /// ``` + pub async fn migrate( + &self, + storage_account_key: &Pubkey, + ) -> ShadowDriveResult<(ShdwDriveResponse, ShdwDriveResponse)> { + let step_1_response = self.migrate_step_1(storage_account_key).await?; + let step_2_response = self.migrate_step_2(storage_account_key).await?; + Ok((step_1_response, step_2_response)) + } + + /// First transaction step that migrates a v1 [`StorageAccount`](crate::models::StorageAccount) to v2. + /// Consists of copying the existing account's data into an intermediate account, and deleting the v1 storage account + pub async fn migrate_step_1( + &self, + storage_account_key: &Pubkey, + ) -> ShadowDriveResult { + let wallet_pubkey = self.wallet.pubkey(); + let (migration, _) = derived_addresses::migration_helper(storage_account_key); + + let accounts = shdw_drive_accounts::MigrateStep1 { + storage_account: *storage_account_key, + migration, + owner: wallet_pubkey, + system_program: system_program::ID, + }; + + let args = shdw_drive_instructions::MigrateStep1 {}; + + let instruction = Instruction { + program_id: PROGRAM_ADDRESS, + accounts: accounts.to_account_metas(None), + data: args.data(), + }; + + let mut txn = Transaction::new_with_payer(&[instruction], Some(&wallet_pubkey)); + txn.try_sign( + &[&self.wallet], + self.rpc_client.get_latest_blockhash().await?, + )?; + let txn_result = self.rpc_client.send_and_confirm_transaction(&txn).await?; + + Ok(ShdwDriveResponse { + txid: txn_result.to_string(), + }) + } + /// Second transaction step that migrates a v1 [`StorageAccount`](crate::models::StorageAccount) to v2. + /// Consists of recreating the storage account using the original pubkey, and deleting the intermediate account + pub async fn migrate_step_2( + &self, + storage_account_key: &Pubkey, + ) -> ShadowDriveResult { + let wallet_pubkey = self.wallet.pubkey(); + let (migration, _) = derived_addresses::migration_helper(storage_account_key); + + let accounts = shdw_drive_accounts::MigrateStep2 { + storage_account: *storage_account_key, + migration, + owner: wallet_pubkey, + system_program: system_program::ID, + }; + + let args = shdw_drive_instructions::MigrateStep2 {}; + + let instruction = Instruction { + program_id: PROGRAM_ADDRESS, + accounts: accounts.to_account_metas(None), + data: args.data(), + }; + + let mut txn = Transaction::new_with_payer(&[instruction], Some(&wallet_pubkey)); + txn.try_sign( + &[&self.wallet], + self.rpc_client.get_latest_blockhash().await?, + )?; + let txn_result = self.rpc_client.send_and_confirm_transaction(&txn).await?; + + Ok(ShdwDriveResponse { + txid: txn_result.to_string(), + }) + } +} diff --git a/src/derived_addresses.rs b/src/derived_addresses.rs index 77140fb..1088e7f 100644 --- a/src/derived_addresses.rs +++ b/src/derived_addresses.rs @@ -54,3 +54,10 @@ pub fn unstake_info(storage_account: &Pubkey) -> (Pubkey, u8) { &PROGRAM_ADDRESS, ) } + +pub fn migration_helper(storage_account: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[&b"migration-helper"[..], &storage_account.to_bytes()], + &PROGRAM_ADDRESS, + ) +}