Skip to content

Commit

Permalink
Feature - grouping delegate (#52)
Browse files Browse the repository at this point in the history
* WIP - Feature - grouping delegate

This PR adds an optional delegate field to a grouping extension. The delegate is authorised to mint new assets into the group.

The main use case for this change is a program controlling either a mint (candy machine) or a conversion from another asset type.

* Added delegate field to grouping extension
* Updated group processor to check grouping delegate if present
* Updated create instruction to accept an optional grouping_delegate signer

Todo: Build changes into interface program - this isn't rebuilt when running programs:build

Also perhaps worth considering not reinitialising the data when the max_size is written as there are likely cases where this may need to be updated post creation.

* * Updated logic in group processor to ensure the authority can still authorise changes to the collection is there is a delegate present.
* Replicated logic in ungroup processor to ensure delegate can also ungroup assets

* fix lint
  • Loading branch information
joefitter authored Apr 10, 2024
1 parent 9d4f660 commit ec4a3dd
Show file tree
Hide file tree
Showing 18 changed files with 472 additions and 52 deletions.
5 changes: 4 additions & 1 deletion clients/js/asset/src/extensions/grouping.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { PublicKey, defaultPublicKey } from '@metaplex-foundation/umi';
import { TypedExtension } from '.';
import { ExtensionType, Grouping } from '../generated';

export const grouping = (
maxSize: Grouping['size'] | number = 0n
maxSize: Grouping['size'] | number = 0n,
delegate: Grouping['delegate'] | PublicKey = defaultPublicKey()
): TypedExtension => ({
type: ExtensionType.Grouping,
size: BigInt(0),
maxSize: BigInt(maxSize),
delegate,
});
11 changes: 9 additions & 2 deletions clients/js/asset/src/generated/instructions/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export type CreateInstructionAccounts = {
owner?: PublicKey | Pda;
/** Asset account of the group */
group?: PublicKey | Pda;
/** Optional authority for minting assets into a group */
groupAuthority?: Signer;
/** The account paying for the storage fees */
payer?: Signer;
/** The system program */
Expand Down Expand Up @@ -134,13 +136,18 @@ export function create(
isWritable: true as boolean,
value: input.group ?? null,
},
payer: {
groupAuthority: {
index: 4,
isWritable: false as boolean,
value: input.groupAuthority ?? null,
},
payer: {
index: 5,
isWritable: true as boolean,
value: input.payer ?? null,
},
systemProgram: {
index: 5,
index: 6,
isWritable: false as boolean,
value: input.systemProgram ?? null,
},
Expand Down
18 changes: 16 additions & 2 deletions clients/js/asset/src/generated/types/grouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,30 @@
*/

import { Serializer, struct, u64 } from '@metaplex-foundation/umi/serializers';
import {
NullablePublicKey,
NullablePublicKeyArgs,
getNullablePublicKeySerializer,
} from '../../hooked';

export type Grouping = { size: bigint; maxSize: bigint };
export type Grouping = {
size: bigint;
maxSize: bigint;
delegate: NullablePublicKey;
};

export type GroupingArgs = { size: number | bigint; maxSize: number | bigint };
export type GroupingArgs = {
size: number | bigint;
maxSize: number | bigint;
delegate: NullablePublicKeyArgs;
};

export function getGroupingSerializer(): Serializer<GroupingArgs, Grouping> {
return struct<Grouping>(
[
['size', u64()],
['maxSize', u64()],
['delegate', getNullablePublicKeySerializer()],
],
{ description: 'Grouping' }
) as Serializer<GroupingArgs, Grouping>;
Expand Down
50 changes: 50 additions & 0 deletions clients/js/asset/test/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,56 @@ test('it cannot set a group on create with authority not a signer', async (t) =>
await t.throwsAsync(promise, { message: /missing required signature/ });
});

test('it set a group on create with authority as a delegate', async (t) => {
// Given a Umi instance and an authority signer.
const umi = await createUmi();
const authority = generateSigner(umi);
const delegate = generateSigner(umi);

// And we create a group asset.
const groupAsset = generateSigner(umi);
await initialize(umi, {
asset: groupAsset,
payer: umi.identity,
extension: grouping(10, delegate.publicKey),
}).sendAndConfirm(umi);

await create(umi, {
asset: groupAsset,
authority: authority.publicKey,
name: 'Group',
payer: umi.identity,
}).sendAndConfirm(umi);

t.like(await fetchAsset(umi, groupAsset.publicKey), <Asset>{
extensions: [
{
type: ExtensionType.Grouping,
size: 0n,
maxSize: 10n,
delegate: delegate.publicKey,
},
],
});

// When we create an asset with a group with the delegate as a authority.
const asset = generateSigner(umi);
await create(umi, {
asset,
authority: authority.publicKey,
name: 'Asset',
group: groupAsset.publicKey,
groupAuthority: delegate,
payer: umi.identity,
}).sendAndConfirm(umi);

// Then we expect the item to be minted into the collection retaining original authority.
t.like(await fetchAsset(umi, asset.publicKey), <Asset>{
group: groupAsset.publicKey,
authority: authority.publicKey,
});
});

test('it can create an asset with a collection', async (t) => {
// Given a Umi instance and a new signer.
const umi = await createUmi();
Expand Down
68 changes: 68 additions & 0 deletions clients/js/asset/test/extensions/grouping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,71 @@ test('it can increase the size of a group', async (t) => {
],
});
});

test('it can add a delegate to the grouping on init', async (t) => {
// Given a Umi instance.
const umi = await createUmi();

const delegate = generateSigner(umi);

// And we create a group asset.
const groupAsset = generateSigner(umi);
await mint(umi, {
asset: groupAsset,
payer: umi.identity,
name: 'Group',
extensions: [grouping(10, delegate.publicKey)],
}).sendAndConfirm(umi);

t.like(await fetchAsset(umi, groupAsset.publicKey), <Asset>{
group: null,
extensions: [
{
type: ExtensionType.Grouping,
size: 0n,
maxSize: 10n,
delegate: delegate.publicKey,
},
],
});

// When we update the maximum size of the group.
await update(umi, {
asset: groupAsset.publicKey,
payer: umi.identity,
extension: grouping(100, delegate.publicKey),
}).sendAndConfirm(umi);

// Then the group maximum size has been updated, and the delegate is unchanged
t.like(await fetchAsset(umi, groupAsset.publicKey), <Asset>{
group: null,
extensions: [
{
type: ExtensionType.Grouping,
size: 0n,
maxSize: 100n,
delegate: delegate.publicKey,
},
],
});

// When we remove the delegate.
await update(umi, {
asset: groupAsset.publicKey,
payer: umi.identity,
extension: grouping(100),
}).sendAndConfirm(umi);

// Then the delegate is removed
t.like(await fetchAsset(umi, groupAsset.publicKey), <Asset>{
group: null,
extensions: [
{
type: ExtensionType.Grouping,
size: 0n,
maxSize: 100n,
delegate: null,
},
],
});
});
38 changes: 38 additions & 0 deletions clients/js/asset/test/group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,41 @@ test('it cannot replace the group of an asset', async (t) => {
group: groupAsset.publicKey,
});
});

test('it can be grouped using a group delegate', async (t) => {
// Given a Umi instance.
const umi = await createUmi();

const delegate = generateSigner(umi);
const authority = generateSigner(umi).publicKey;

// And we create a group asset.
const groupAsset = generateSigner(umi);
await mint(umi, {
asset: groupAsset,
payer: umi.identity,
name: 'Group',
authority,
extensions: [grouping(10, delegate.publicKey)],
}).sendAndConfirm(umi);

// And a "normal" asset.
const asset = generateSigner(umi);
await mint(umi, {
asset,
payer: umi.identity,
authority,
name: 'Asset',
}).sendAndConfirm(umi);

// Then we can group the assets using a delegate signer
await group(umi, {
asset: asset.publicKey,
authority: delegate,
group: groupAsset.publicKey,
}).sendAndConfirm(umi);

t.like(await fetchAsset(umi, asset.publicKey), <Asset>{
group: groupAsset.publicKey,
});
});
83 changes: 83 additions & 0 deletions clients/js/asset/test/ungroup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,86 @@ test('it can ungroup an asset', async (t) => {
],
});
});

test('It can ungroup an asset as a group delegate', async (t) => {
// Given a Umi instance.
const umi = await createUmi();

const delegate = generateSigner(umi);
const authority = generateSigner(umi);

// And we create a group asset.
const groupAsset = generateSigner(umi);
await mint(umi, {
asset: groupAsset,
payer: umi.identity,
name: 'Group',
authority,
extensions: [grouping(10, delegate.publicKey)],
}).sendAndConfirm(umi);

t.like(await fetchAsset(umi, groupAsset.publicKey), <Asset>{
group: null,
authority: authority.publicKey,
extensions: [
{
type: ExtensionType.Grouping,
size: 0n,
maxSize: 10n,
delegate: delegate.publicKey,
},
],
});

// And a "normal" asset.
const asset = generateSigner(umi);
await mint(umi, {
asset,
payer: umi.identity,
name: 'Asset',
authority,
group: groupAsset.publicKey,
}).sendAndConfirm(umi);

t.like(await fetchAsset(umi, asset.publicKey), <Asset>{
group: groupAsset.publicKey,
});

t.like(await fetchAsset(umi, groupAsset.publicKey), <Asset>{
group: null,
authority: authority.publicKey,
extensions: [
{
type: ExtensionType.Grouping,
size: 1n,
maxSize: 10n,
delegate: delegate.publicKey,
},
],
});

// When we ungroup the asset as the delegate.
await ungroup(umi, {
asset: asset.publicKey,
group: groupAsset.publicKey,
authority: delegate,
}).sendAndConfirm(umi);

// Then the group is removed from the asset.
t.like(await fetchAsset(umi, asset.publicKey), <Asset>{
group: null,
});

// And the group size has decreased.
t.like(await fetchAsset(umi, groupAsset.publicKey), <Asset>{
group: null,
extensions: [
{
type: ExtensionType.Grouping,
size: 0n,
maxSize: 10n,
delegate: delegate.publicKey,
},
],
});
});
Loading

0 comments on commit ec4a3dd

Please sign in to comment.