Skip to content

Commit

Permalink
Add properties extension (#69)
Browse files Browse the repository at this point in the history
* Use nitrate entrypoint

* Add resize instruction

* Add properties extension

* Remove unused trait

* Fix properties getter

* Add bpf test

* Update program interface

* Add resize wrapper

* Rename resize argument

* Add boolean property type

* Add test

* Fix linting
  • Loading branch information
febo authored May 14, 2024
1 parent da0dd9f commit f8c2d0d
Show file tree
Hide file tree
Showing 14 changed files with 943 additions and 4 deletions.
9 changes: 7 additions & 2 deletions clients/js/asset/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ import {
getProxySerializer,
} from '../generated';
import { Royalties, getRoyaltiesSerializer } from './royalties';
import { Properties, getPropertiesSerializer } from './properties';

export * from './attributes';
export * from './blob';
export * from './creators';
export * from './grouping';
export * from './links';
export * from './manager';
export * from './metadata';
export * from './properties';
export * from './royalties';
export * from './manager';

export type TypedExtension =
| ({ type: ExtensionType.Attributes } & Attributes)
Expand All @@ -39,7 +41,8 @@ export type TypedExtension =
| ({ type: ExtensionType.Grouping } & Grouping)
| ({ type: ExtensionType.Royalties } & Royalties)
| ({ type: ExtensionType.Manager } & Manager)
| ({ type: ExtensionType.Proxy } & Proxy);
| ({ type: ExtensionType.Proxy } & Proxy)
| ({ type: ExtensionType.Properties } & Properties);

export const getExtensionSerializerFromType = <T extends TypedExtension>(
type: ExtensionType
Expand All @@ -64,6 +67,8 @@ export const getExtensionSerializerFromType = <T extends TypedExtension>(
return getManagerSerializer();
case ExtensionType.Proxy:
return getProxySerializer();
case ExtensionType.Properties:
return getPropertiesSerializer();
default:
throw new Error(`Unknown extension type: ${type}`);
}
Expand Down
162 changes: 162 additions & 0 deletions clients/js/asset/src/extensions/properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
Serializer,
array,
bool,
string,
struct,
u64,
u8,
} from '@metaplex-foundation/umi/serializers';
import { TypedExtension, getExtension } from '.';
import { Asset, ExtensionType, Type, getTypeSerializer } from '../generated';

type Property =
| Omit<Text, 'type'>
| Omit<Number, 'type'>
| Omit<Boolean, 'type'>;

export const properties = (values: Property[]): TypedExtension => ({
type: ExtensionType.Properties,
values: values.map(
(property) =>
({
type: getTypeFromString(typeof property.value),
...property,
} as TypedProperty)
),
});

type TypedPropertyfromEnum<T extends Type> = Extract<
TypedProperty,
{ type: T }
>;

export function getProperty<T extends Type>(
asset: Asset,
name: string,
type: T
): TypedPropertyfromEnum<T> | undefined {
const extension = getExtension(asset, ExtensionType.Properties);

if (!extension) {
return undefined;
}

const property = extension.values.find(
(p) => 'type' in p && p.type === type && p.name === name
);

return property ? (property as TypedPropertyfromEnum<T>) : undefined;
}

// Properties.

export type Properties = { values: Array<TypedProperty> };

export type PropertiesArgs = { values: Array<TypedProperty> };

export function getPropertiesSerializer(): Serializer<
PropertiesArgs,
Properties
> {
return struct<Properties>(
[['values', array(getPropertySerializer(), { size: 'remainder' })]],
{ description: 'Properties' }
) as Serializer<PropertiesArgs, Properties>;
}

export const getPropertySerializer = (): Serializer<TypedProperty> => ({
description: 'TypedProperty',
fixedSize: null,
maxSize: null,
serialize: (property: TypedProperty) =>
getPropertySerializerFromType(property.type).serialize(property),
deserialize: (buffer, offset = 0) => {
const [, nameOffset] = string({ size: u8() }).deserialize(buffer, offset);
const type = buffer[nameOffset] as Type;
return getPropertySerializerFromType(type).deserialize(buffer, offset);
},
});

export type TypedProperty =
| ({ type: Type.Text } & Omit<Text, 'type'>)
| ({ type: Type.Number } & Omit<Number, 'type'>)
| ({ type: Type.Boolean } & Omit<Boolean, 'type'>);

export const getPropertySerializerFromType = <T extends TypedProperty>(
type: Type
): Serializer<T> =>
((): Serializer<any> => {
switch (type) {
case Type.Text:
return getTextSerializer();
case Type.Number:
return getNumberSerializer();
case Type.Boolean:
return getBooleanSerializer();
default:
throw new Error(`Unknown property type: ${type}`);
}
})() as Serializer<T>;

export const getTypeFromString = (type: String): Type => {
switch (type) {
case 'string':
return Type.Text;
case 'boolean':
return Type.Boolean;
default:
return Type.Number;
}
};

// Text property.

export type Text = { name: string; type: Type; value: string };

export type TextArgs = Text;

export function getTextSerializer(): Serializer<TextArgs, Text> {
return struct<Text>(
[
['name', string({ size: u8() })],
['type', getTypeSerializer()],
['value', string({ size: u8() })],
],
{ description: 'Text' }
) as Serializer<TextArgs, Text>;
}

// Number property.

export type Number = { name: string; type: Type; value: bigint };

export type NumberArgs = Number;

export function getNumberSerializer(): Serializer<NumberArgs, Number> {
return struct<Number>(
[
['name', string({ size: u8() })],
['type', getTypeSerializer()],
['value', u64()],
],
{ description: 'Number' }
) as Serializer<NumberArgs, Number>;
}

// Boolean property.

export type Boolean = { name: string; type: Type; value: boolean };

export type BooleanArgs = Boolean;

export function getBooleanSerializer(): Serializer<BooleanArgs, Boolean> {
return struct<Boolean>(
[
['name', string({ size: u8() })],
['type', getTypeSerializer()],
['value', bool()],
],
{ description: 'Boolean' }
) as Serializer<BooleanArgs, Boolean>;
}
1 change: 1 addition & 0 deletions clients/js/asset/src/generated/types/extensionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum ExtensionType {
Royalties,
Manager,
Proxy,
Properties,
}

export type ExtensionTypeArgs = ExtensionType;
Expand Down
1 change: 1 addition & 0 deletions clients/js/asset/src/generated/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export * from './standard';
export * from './state';
export * from './strategy';
export * from './trait';
export * from './type';
24 changes: 24 additions & 0 deletions clients/js/asset/src/generated/types/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* This code was AUTOGENERATED using the kinobi library.
* Please DO NOT EDIT THIS FILE, instead use visitors
* to add features, then rerun kinobi to update it.
*
* @see https://github.com/metaplex-foundation/kinobi
*/

import { Serializer, scalarEnum } from '@metaplex-foundation/umi/serializers';

export enum Type {
Text,
Number,
Boolean,
}

export type TypeArgs = Type;

export function getTypeSerializer(): Serializer<TypeArgs, Type> {
return scalarEnum<Type>(Type, { description: 'Type' }) as Serializer<
TypeArgs,
Type
>;
}
90 changes: 90 additions & 0 deletions clients/js/asset/test/extensions/properties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { generateSigner } from '@metaplex-foundation/umi';
import test from 'ava';
import {
Asset,
Discriminator,
ExtensionType,
Standard,
State,
Type,
create,
fetchAsset,
getProperty,
initialize,
properties,
} from '../../src';
import { createUmi } from '../_setup';

test('it can create a new asset with properties', async (t) => {
// Given a Umi instance and a new signer.
const umi = await createUmi();
const asset = generateSigner(umi);
const owner = generateSigner(umi);

// And we initialize an asset with properties.
await initialize(umi, {
asset,
payer: umi.identity,
extension: properties([
{ name: 'name', value: 'nifty' },
{ name: 'version', value: 1n },
{ name: 'alpha', value: false },
]),
}).sendAndConfirm(umi);

t.true(await umi.rpc.accountExists(asset.publicKey), 'asset exists');

// When we create the asset.
await create(umi, {
asset,
owner: owner.publicKey,
name: 'Asset with creators',
}).sendAndConfirm(umi);

// Then an asset was created with the correct data.
const assetAccount = await fetchAsset(umi, asset.publicKey);
t.like(assetAccount, <Asset>{
discriminator: Discriminator.Asset,
state: State.Unlocked,
standard: Standard.NonFungible,
owner: owner.publicKey,
authority: umi.identity.publicKey,
extensions: [
{
type: ExtensionType.Properties,
values: [
{
type: Type.Text,
name: 'name',
value: 'nifty',
},
{
type: Type.Number,
name: 'version',
value: 1n,
},
{
type: Type.Boolean,
name: 'alpha',
value: false,
},
],
},
],
});

const name = getProperty(assetAccount, 'name', Type.Text);
if (name) {
t.deepEqual(name.value, 'nifty');
}

const version = getProperty(assetAccount, 'version', Type.Number);
if (version) {
t.deepEqual(version.value, 1n);
}

const alpha = getProperty(assetAccount, 'alpha', Type.Boolean);
if (alpha) {
t.deepEqual(alpha.value, false);
}
});
1 change: 1 addition & 0 deletions clients/rust/asset/src/generated/types/extension_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ pub enum ExtensionType {
Royalties,
Manager,
Proxy,
Properties,
}
2 changes: 2 additions & 0 deletions clients/rust/asset/src/generated/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub(crate) mod r#standard;
pub(crate) mod r#state;
pub(crate) mod r#strategy;
pub(crate) mod r#trait;
pub(crate) mod r#type;

pub use self::r#attributes::*;
pub use self::r#blob::*;
Expand All @@ -48,3 +49,4 @@ pub use self::r#standard::*;
pub use self::r#state::*;
pub use self::r#strategy::*;
pub use self::r#trait::*;
pub use self::r#type::*;
17 changes: 17 additions & 0 deletions clients/rust/asset/src/generated/types/type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//! This code was AUTOGENERATED using the kinobi library.
//! Please DO NOT EDIT THIS FILE, instead use visitors
//! to add features, then rerun kinobi to update it.
//!
//! [https://github.com/metaplex-foundation/kinobi]
//!
use borsh::BorshDeserialize;
use borsh::BorshSerialize;

#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Type {
Text,
Number,
Boolean,
}
2 changes: 2 additions & 0 deletions clients/rust/asset/tests/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#![cfg(feature = "test-sbf")]
pub mod properties;
Loading

0 comments on commit f8c2d0d

Please sign in to comment.