From cf1388b0d64f1f6df9ede5782f3c3c7879e99111 Mon Sep 17 00:00:00 2001 From: blockiosaurus Date: Thu, 22 Feb 2024 23:11:41 -0700 Subject: [PATCH] Fixing update. --- .../js/src/hooked/fetchAssetWithPlugins.ts | 48 +++-- clients/js/test/update.test.ts | 184 ++++++++++++++++++ programs/mpl-asset/src/plugins/utils.rs | 1 + programs/mpl-asset/src/processor/update.rs | 102 +++++++++- 4 files changed, 314 insertions(+), 21 deletions(-) create mode 100644 clients/js/test/update.test.ts diff --git a/clients/js/src/hooked/fetchAssetWithPlugins.ts b/clients/js/src/hooked/fetchAssetWithPlugins.ts index f62ca00b..a59d1251 100644 --- a/clients/js/src/hooked/fetchAssetWithPlugins.ts +++ b/clients/js/src/hooked/fetchAssetWithPlugins.ts @@ -1,12 +1,32 @@ -import { Context, Pda, PublicKey, RpcGetAccountOptions, assertAccountExists, publicKey as toPublicKey } from "@metaplex-foundation/umi"; -import { Asset, Authority, Plugin, PluginHeader, PluginRegistry, deserializeAsset, getAssetAccountDataSerializer, getPluginHeaderAccountDataSerializer, getPluginRegistryAccountDataSerializer, getPluginSerializer } from "../generated"; +import { + Context, + Pda, + PublicKey, + RpcGetAccountOptions, + assertAccountExists, + publicKey as toPublicKey +} from "@metaplex-foundation/umi"; +import { + Asset, + Authority, + Plugin, + PluginHeader, + PluginHeaderAccountData, + PluginRegistry, + PluginRegistryAccountData, + deserializeAsset, + getAssetAccountDataSerializer, + getPluginHeaderAccountDataSerializer, + getPluginRegistryAccountDataSerializer, + getPluginSerializer +} from "../generated"; export type PluginWithAuthorities = { plugin: Plugin, authorities: Authority[] }; export type PluginList = { - pluginHeader: Omit, - plugins: PluginWithAuthorities[], - pluginRegistry: Omit, + pluginHeader?: Omit, + plugins?: PluginWithAuthorities[], + pluginRegistry?: Omit, }; export type AssetWithPlugins = Asset & PluginList; @@ -22,12 +42,18 @@ export async function fetchAssetWithPlugins( assertAccountExists(maybeAccount, 'Asset'); const asset = deserializeAsset(maybeAccount); const assetData = getAssetAccountDataSerializer().serialize(asset); - const pluginHeader = getPluginHeaderAccountDataSerializer().deserialize(maybeAccount.data, assetData.length)[0]; - const pluginRegistry = getPluginRegistryAccountDataSerializer().deserialize(maybeAccount.data, Number(pluginHeader.pluginRegistryOffset))[0]; - const plugins = pluginRegistry.registry.map((record) => ({ - plugin: getPluginSerializer().deserialize(maybeAccount.data, Number(record.data.offset))[0], - authorities: record.data.authorities, - })); + + let pluginHeader: PluginHeaderAccountData | undefined; + let pluginRegistry: PluginRegistryAccountData | undefined; + let plugins: PluginWithAuthorities[] | undefined; + if (maybeAccount.data.length !== assetData.length) { + [pluginHeader] = getPluginHeaderAccountDataSerializer().deserialize(maybeAccount.data, assetData.length); + [pluginRegistry] = getPluginRegistryAccountDataSerializer().deserialize(maybeAccount.data, Number(pluginHeader.pluginRegistryOffset)); + plugins = pluginRegistry.registry.map((record) => ({ + plugin: getPluginSerializer().deserialize(maybeAccount.data, Number(record.data.offset))[0], + authorities: record.data.authorities, + })); + } const assetWithPlugins: AssetWithPlugins = { pluginHeader, diff --git a/clients/js/test/update.test.ts b/clients/js/test/update.test.ts new file mode 100644 index 00000000..da69061c --- /dev/null +++ b/clients/js/test/update.test.ts @@ -0,0 +1,184 @@ +import { generateSigner } from '@metaplex-foundation/umi'; +import test from 'ava'; +// import { base58 } from '@metaplex-foundation/umi/serializers'; +import { AssetWithPlugins, DataState, Key, addPlugin, create, fetchAssetWithPlugins, update } from '../src'; +import { createUmi } from './_setup'; + +test('it can update an asset to be larger', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const assetAddress = generateSigner(umi); + + // When we create a new account. + await create(umi, { + dataState: DataState.AccountState, + assetAddress, + name: 'Test Bread', + uri: 'https://example.com/bread', + }).sendAndConfirm(umi); + + await update(umi, { + assetAddress: assetAddress.publicKey, + newName: 'Test Bread 2', + newUri: 'https://example.com/bread2', + }).sendAndConfirm(umi); + + // Then an account was created with the correct data. + const asset = await fetchAssetWithPlugins(umi, assetAddress.publicKey); + t.like(asset, { + key: Key.Asset, + updateAuthority: umi.identity.publicKey, + owner: umi.identity.publicKey, + name: "Test Bread 2", + uri: "https://example.com/bread2" + }); +}); + +test('it can update an asset to be smaller', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const assetAddress = generateSigner(umi); + + // When we create a new account. + await create(umi, { + dataState: DataState.AccountState, + assetAddress, + name: 'Test Bread', + uri: 'https://example.com/bread', + }).sendAndConfirm(umi); + + await update(umi, { + assetAddress: assetAddress.publicKey, + newName: '', + newUri: '', + }).sendAndConfirm(umi); + + // Then an account was created with the correct data. + const asset = await fetchAssetWithPlugins(umi, assetAddress.publicKey); + t.like(asset, { + key: Key.Asset, + updateAuthority: umi.identity.publicKey, + owner: umi.identity.publicKey, + name: "", + uri: "" + }); +}); + +test('it can update an asset with plugins to be larger', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const assetAddress = generateSigner(umi); + + // When we create a new account. + await create(umi, { + dataState: DataState.AccountState, + assetAddress, + name: 'Test Bread', + uri: 'https://example.com/bread', + }).sendAndConfirm(umi); + + await addPlugin(umi, { + assetAddress: assetAddress.publicKey, + plugin: { + __kind: 'Freeze', + fields: [{ frozen: false }], + } + }).sendAndConfirm(umi); + + await update(umi, { + assetAddress: assetAddress.publicKey, + newName: 'Test Bread 2', + newUri: 'https://example.com/bread2', + }).sendAndConfirm(umi); + + // Then an account was created with the correct data. + const asset = await fetchAssetWithPlugins(umi, assetAddress.publicKey); + // console.log(JSON.stringify(asset, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)); + t.like(asset, { + publicKey: assetAddress.publicKey, + updateAuthority: umi.identity.publicKey, + owner: umi.identity.publicKey, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + pluginHeader: { + key: 3, + pluginRegistryOffset: BigInt(122), + }, + pluginRegistry: { + key: 4, + registry: [{ + pluginType: 2, + data: { + offset: BigInt(120), + authorities: [{ __kind: "Owner" }] + } + }], + }, + plugins: [{ + authorities: [{ __kind: "Owner" }], + plugin: { + __kind: 'Freeze', + fields: [{ frozen: false }], + }, + }], + }); +}); + +test('it can update an asset with plugins to be smaller', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const assetAddress = generateSigner(umi); + + // When we create a new account. + await create(umi, { + dataState: DataState.AccountState, + assetAddress, + name: 'Test Bread', + uri: 'https://example.com/bread', + }).sendAndConfirm(umi); + + await addPlugin(umi, { + assetAddress: assetAddress.publicKey, + plugin: { + __kind: 'Freeze', + fields: [{ frozen: false }], + } + }).sendAndConfirm(umi); + + await update(umi, { + assetAddress: assetAddress.publicKey, + newName: '', + newUri: '', + }).sendAndConfirm(umi); + + // Then an account was created with the correct data. + const asset = await fetchAssetWithPlugins(umi, assetAddress.publicKey); + t.like(asset, { + publicKey: assetAddress.publicKey, + updateAuthority: umi.identity.publicKey, + owner: umi.identity.publicKey, + name: '', + uri: '', + pluginHeader: { + key: 3, + pluginRegistryOffset: BigInt(84), + }, + pluginRegistry: { + key: 4, + registry: [{ + pluginType: 2, + data: { + offset: BigInt(82), + authorities: [{ __kind: "Owner" }] + } + }], + }, + plugins: [{ + authorities: [{ __kind: "Owner" }], + plugin: { + __kind: 'Freeze', + fields: [{ frozen: false }], + }, + }], + }); +}); \ No newline at end of file diff --git a/programs/mpl-asset/src/plugins/utils.rs b/programs/mpl-asset/src/plugins/utils.rs index 6c94b0f8..bc2e51b8 100644 --- a/programs/mpl-asset/src/plugins/utils.rs +++ b/programs/mpl-asset/src/plugins/utils.rs @@ -331,6 +331,7 @@ pub fn delete_plugin<'a>( .ok_or(MplAssetError::NumericalOverflow)?; solana_program::msg!("data_to_move: {:?}", data_to_move); + //TODO: This is memory intensive, we should use memmove instead probably. let src = account.data.borrow()[next_plugin_offset..].to_vec(); solana_program::msg!("src: {:?}", src); sol_memcpy( diff --git a/programs/mpl-asset/src/processor/update.rs b/programs/mpl-asset/src/processor/update.rs index 85b724e6..271868d1 100644 --- a/programs/mpl-asset/src/processor/update.rs +++ b/programs/mpl-asset/src/processor/update.rs @@ -1,12 +1,14 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use mpl_utils::assert_signer; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult}; +use mpl_utils::{assert_signer, resize_or_reallocate_account_raw}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_memory::sol_memcpy, +}; use crate::{ error::MplAssetError, instruction::accounts::UpdateAccounts, - plugins::{CheckResult, Plugin, ValidationResult}, - state::{Asset, SolanaAccount}, + plugins::{CheckResult, Plugin, RegistryData, RegistryRecord, ValidationResult}, + state::{Asset, DataBlob, SolanaAccount}, utils::fetch_core_data, }; @@ -23,11 +25,17 @@ pub(crate) fn update<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateArgs) -> P // Guards. assert_signer(ctx.accounts.authority)?; - if let Some(payer) = ctx.accounts.payer { - assert_signer(payer)?; - } + let payer = match ctx.accounts.payer { + Some(payer) => { + assert_signer(payer)?; + payer + } + None => ctx.accounts.authority, + }; - let (mut asset, _, plugin_registry) = fetch_core_data(ctx.accounts.asset_address)?; + let (mut asset, plugin_header, plugin_registry) = fetch_core_data(ctx.accounts.asset_address)?; + let asset_size = asset.get_size() as isize; + solana_program::msg!("asset_size: {:?}", asset_size); let mut approved = false; match Asset::check_update() { @@ -40,7 +48,7 @@ pub(crate) fn update<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateArgs) -> P CheckResult::None => (), }; - if let Some(plugin_registry) = plugin_registry { + if let Some(plugin_registry) = plugin_registry.clone() { for record in plugin_registry.registry { if matches!( record.plugin_type.check_transfer(), @@ -75,7 +83,81 @@ pub(crate) fn update<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateArgs) -> P dirty = true; } if dirty { - //TODO: Handle resize! + if let (Some(mut plugin_header), Some(mut plugin_registry)) = + (plugin_header, plugin_registry) + { + let new_asset_size = asset.get_size() as isize; + solana_program::msg!("new_asset_size: {:?}", new_asset_size); + let size_diff = new_asset_size + .checked_sub(asset_size) + .ok_or(MplAssetError::NumericalOverflow)?; + let new_size = (ctx.accounts.asset_address.data_len() as isize) + .checked_add(size_diff) + .ok_or(MplAssetError::NumericalOverflow)?; + solana_program::msg!("size_diff: {:?}", size_diff); + solana_program::msg!( + "old plugin_registry_offset: {:?}", + plugin_header.plugin_registry_offset + ); + let new_registry_offset = (plugin_header.plugin_registry_offset as isize) + .checked_add(size_diff) + .ok_or(MplAssetError::NumericalOverflow)?; + solana_program::msg!("new_registry_offset: {:?}", new_registry_offset); + let registry_offset = plugin_header.plugin_registry_offset; + plugin_header.plugin_registry_offset = new_registry_offset as usize; + + let plugin_offset = asset_size + .checked_add(size_diff) + .ok_or(MplAssetError::NumericalOverflow)?; + let new_plugin_offset = new_asset_size + .checked_add(size_diff) + .ok_or(MplAssetError::NumericalOverflow)?; + + // //TODO: This is memory intensive, we should use memmove instead probably. + let src = ctx.accounts.asset_address.data.borrow() + [(plugin_offset as usize)..registry_offset] + .to_vec(); + + resize_or_reallocate_account_raw( + ctx.accounts.asset_address, + payer, + ctx.accounts.system_program, + new_size as usize, + )?; + + sol_memcpy( + &mut ctx.accounts.asset_address.data.borrow_mut()[(new_plugin_offset as usize)..], + &src, + src.len(), + ); + + plugin_header.save(ctx.accounts.asset_address, new_asset_size as usize)?; + plugin_registry.registry = plugin_registry + .registry + .iter_mut() + .map(|record| { + let new_offset = (record.data.offset as isize) + .checked_add(size_diff) + .ok_or(MplAssetError::NumericalOverflow)?; + Ok(RegistryRecord { + plugin_type: record.plugin_type, + data: RegistryData { + offset: new_offset as usize, + authorities: record.data.authorities.clone(), + }, + }) + }) + .collect::, MplAssetError>>()?; + plugin_registry.save(ctx.accounts.asset_address, new_registry_offset as usize)?; + } else { + resize_or_reallocate_account_raw( + ctx.accounts.asset_address, + payer, + ctx.accounts.system_program, + asset.get_size(), + )?; + } + asset.save(ctx.accounts.asset_address, 0)?; }