diff --git a/Cargo.lock b/Cargo.lock index 6694cb1..a55019e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,7 +384,7 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "asset-program" -version = "0.6.0" +version = "0.6.1" dependencies = [ "borsh 0.10.3", "bytemuck", diff --git a/clients/js/proxy/test/_setup.ts b/clients/js/proxy/test/_setup.ts index c3a5c4a..2647906 100644 --- a/clients/js/proxy/test/_setup.ts +++ b/clients/js/proxy/test/_setup.ts @@ -10,3 +10,7 @@ export const STUB_KEY = Uint8Array.from([ 198, 193, 21, 158, 198, 203, 241, 229, 179, 162, 229, 129, 109, 151, 51, 135, 240, 128, 114, 242, 103, 170, 154, 47, 218, 130, 218, 139, 45, 47, ]); + +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/clients/js/proxy/test/transfer.test.ts b/clients/js/proxy/test/transfer.test.ts index b2d1e2e..2041b8e 100644 --- a/clients/js/proxy/test/transfer.test.ts +++ b/clients/js/proxy/test/transfer.test.ts @@ -9,15 +9,17 @@ import { ExtensionType, Standard, State, + Type, fetchAsset, getExtension, + getProperty, transfer, } from '@nifty-oss/asset'; import { Keypair } from '@solana/web3.js'; import test from 'ava'; import { create } from '../src'; import { findProxiedAssetPda } from '../src/pda'; -import { STUB_KEY, createUmi } from './_setup'; +import { STUB_KEY, createUmi, sleep } from './_setup'; test('it cannot transfer a non-signer proxied asset', async (t) => { // Given a Umi instance and a new signer. @@ -114,6 +116,12 @@ test('it can execute custom logic on transfer', async (t) => { const initial = attributes?.values[0].value; t.true(parseInt(initial!) === 0); + // initial value for the last transferred timestamp + const timestamp = getProperty(asset, 'last_transferred', Type.Number)!.value; + + // We wait for a second to ensure the timestamp is different. + await sleep(1000); + const recipient = generateSigner(umi).publicKey; const proxy = getExtension(asset, ExtensionType.Proxy); // And we transfer the proxied asset through the proxy program (using @@ -139,6 +147,14 @@ test('it can execute custom logic on transfer', async (t) => { const current = parseInt(attributes?.values[0].value!); t.true(current === 1); t.assert(parseInt(initial!) < current); + + // And the last transferred timestamp is updated. + const lastTransferred = getProperty( + asset, + 'last_transferred', + Type.Number + )!.value; + t.assert(timestamp < lastTransferred); }); test('it can transfer the proxy asset multiple times', async (t) => { diff --git a/idls/asset_program.json b/idls/asset_program.json index 0142f88..6300965 100644 --- a/idls/asset_program.json +++ b/idls/asset_program.json @@ -1,5 +1,5 @@ { - "version": "0.6.0", + "version": "0.6.1", "name": "asset_program", "instructions": [ { diff --git a/programs/asset/program/Cargo.toml b/programs/asset/program/Cargo.toml index 86d8794..4b6bb40 100644 --- a/programs/asset/program/Cargo.toml +++ b/programs/asset/program/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "asset-program" -version = "0.6.0" +version = "0.6.1" authors = ["nifty-oss maintainers "] edition = "2021" readme = "./README.md" diff --git a/programs/asset/types/src/extensions/grouping.rs b/programs/asset/types/src/extensions/grouping.rs index 4b08e99..11d28f8 100644 --- a/programs/asset/types/src/extensions/grouping.rs +++ b/programs/asset/types/src/extensions/grouping.rs @@ -10,6 +10,9 @@ use crate::{error::Error, state::NullablePubkey}; use super::{ExtensionBuilder, ExtensionData, ExtensionDataMut, ExtensionType, Lifecycle}; +/// Empty string used for backwards compatibility with metadata extension. +const EMPTY: [u8; 32] = [0u8; 32]; + /// Extension to define a group of assets. /// /// Assets that are intented to be use as group "markers" must have this extension @@ -36,10 +39,17 @@ impl<'a> ExtensionData<'a> for Grouping<'a> { fn from_bytes(bytes: &'a [u8]) -> Self { let (size, rest) = bytes.split_at(std::mem::size_of::()); let (max_size, delegate) = rest.split_at(std::mem::size_of::()); + Self { size: bytemuck::from_bytes(size), max_size: bytemuck::from_bytes(max_size), - delegate: bytemuck::from_bytes(delegate), + // backwards compatibility for grouping extension: if there are not enough + // bytes to read the delegate, we assume it is empty + delegate: bytemuck::from_bytes(if delegate.len() < EMPTY.len() { + &EMPTY + } else { + delegate + }), } } @@ -72,10 +82,17 @@ impl<'a> ExtensionDataMut<'a> for GroupingMut<'a> { fn from_bytes_mut(bytes: &'a mut [u8]) -> Self { let (size, rest) = bytes.split_at_mut(std::mem::size_of::()); let (max_size, delegate) = rest.split_at_mut(std::mem::size_of::()); + Self { size: bytemuck::from_bytes_mut(size), max_size: bytemuck::from_bytes_mut(max_size), - delegate: bytemuck::from_bytes_mut(delegate), + // backwards compatibility for grouping extension: if there are not enough + // bytes to read the delegate, we assume it is empty + delegate: bytemuck::from_bytes_mut(if delegate.len() < EMPTY.len() { + unsafe { (&EMPTY as *const [u8] as *mut [u8]).as_mut().unwrap() } + } else { + delegate + }), } } } diff --git a/programs/proxy/src/processor/create.rs b/programs/proxy/src/processor/create.rs index b9add4a..3e3e3e5 100644 --- a/programs/proxy/src/processor/create.rs +++ b/programs/proxy/src/processor/create.rs @@ -1,10 +1,15 @@ use nifty_asset_interface::{ - extensions::{AttributesBuilder, BlobBuilder, ExtensionBuilder, ProxyBuilder}, + extensions::{ + AttributesBuilder, BlobBuilder, ExtensionBuilder, PropertiesBuilder, ProxyBuilder, + }, instructions::CreateCpiBuilder, types::ExtensionInput, ExtensionType, Standard, }; -use solana_program::{entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey}; +use solana_program::{ + clock::Clock, entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey, + sysvar::Sysvar, +}; use crate::{ instruction::{ @@ -86,6 +91,15 @@ pub fn process_create( data: Some(data), }; + let data = PropertiesBuilder::with_capacity(30) + .add_number("last_transferred", Clock::get()?.unix_timestamp as u64) + .data(); + let properties = ExtensionInput { + extension_type: ExtensionType::Properties, + length: data.len() as u32, + data: Some(data), + }; + // creates the proxied asset CreateCpiBuilder::new(ctx.accounts.nifty_asset_program) @@ -97,6 +111,6 @@ pub fn process_create( .system_program(ctx.accounts.system_program) .name(metadata.name) .standard(Standard::Proxied) - .extensions(vec![attributes, blob, proxy]) + .extensions(vec![attributes, blob, proxy, properties]) .invoke_signed(&[&signer]) } diff --git a/programs/proxy/src/processor/transfer.rs b/programs/proxy/src/processor/transfer.rs index aa0dc35..1a2ec38 100644 --- a/programs/proxy/src/processor/transfer.rs +++ b/programs/proxy/src/processor/transfer.rs @@ -1,6 +1,6 @@ use nifty_asset_interface::{ accounts::TransferAccounts, - extensions::{Attributes, AttributesBuilder, BlobBuilder, ExtensionBuilder}, + extensions::{Attributes, AttributesBuilder, BlobBuilder, ExtensionBuilder, PropertiesBuilder}, fetch_proxy_data, instructions::{TransferCpiBuilder, UpdateCpiBuilder}, state::Asset, @@ -93,6 +93,22 @@ pub fn process_transfer<'a>( }) .invoke_signed(&[&signer])?; + // updates the properties (last tranferred) + + let data = PropertiesBuilder::with_capacity(30) + .add_number("last_transferred", Clock::get()?.unix_timestamp as u64) + .data(); + + UpdateCpiBuilder::new(nifty_asset_program) + .asset(ctx.accounts.asset) + .authority(ctx.accounts.asset) + .extension(ExtensionInput { + extension_type: ExtensionType::Properties, + length: data.len() as u32, + data: Some(data), + }) + .invoke_signed(&[&signer])?; + // cpi into the Nifty Asset program to perform the transfer TransferCpiBuilder::new(nifty_asset_program)