diff --git a/clients/js/test/plugins/asset/burnDelegate.test.ts b/clients/js/test/plugins/asset/burnDelegate.test.ts new file mode 100644 index 00000000..adf3dc28 --- /dev/null +++ b/clients/js/test/plugins/asset/burnDelegate.test.ts @@ -0,0 +1,252 @@ +import { generateSigner } from '@metaplex-foundation/umi'; +import test from 'ava'; +import { addPlugin, revokePluginAuthority, burn } from '../../../src'; +import { + DEFAULT_ASSET, + assertAsset, + createUmi, + assertBurned, +} from '../../_setupRaw'; +import { createAsset, createCollection } from '../../_setupSdk'; + +test('it can create an asset with burnDelegate', async (t) => { + const umi = await createUmi(); + + const asset = await createAsset(umi, { + plugins: [ + { + type: 'BurnDelegate', + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + burnDelegate: { + authority: { + type: 'Owner', + }, + }, + }); +}); + +test('it can add burnDelegate to an asset', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi, {}); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + burnDelegate: undefined, + }); + + const burnDelegate = generateSigner(umi); + + await addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'BurnDelegate', + authority: { + type: 'Address', + address: burnDelegate.publicKey, + }, + }, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + burnDelegate: { + authority: { + type: 'Address', + address: burnDelegate.publicKey, + }, + }, + }); +}); + +test('a burnDelegate can burn an asset', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi, {}); + const burnDelegate = generateSigner(umi); + + await addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'BurnDelegate', + authority: { + type: 'Address', + address: burnDelegate.publicKey, + }, + }, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + burnDelegate: { + authority: { + type: 'Address', + address: burnDelegate.publicKey, + }, + }, + }); + + await burn(umi, { + asset, + authority: burnDelegate, + }).sendAndConfirm(umi); + + await assertBurned(t, umi, asset.publicKey); +}); + +test('an burnDelegate cannot burn an asset after delegate authority revoked', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi, {}); + const burnDelegate = generateSigner(umi); + + await addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'BurnDelegate', + authority: { + type: 'Address', + address: burnDelegate.publicKey, + }, + }, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + burnDelegate: { + authority: { + type: 'Address', + address: burnDelegate.publicKey, + }, + }, + }); + + await revokePluginAuthority(umi, { + asset: asset.publicKey, + plugin: { + type: 'BurnDelegate', + }, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + burnDelegate: { + authority: { + type: 'Owner', + }, + }, + }); + + const result = burn(umi, { + asset, + authority: burnDelegate, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'NoApprovals' }); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + burnDelegate: { + authority: { + type: 'Owner', + }, + }, + }); +}); + +test('a burnDelegate can burn using delegated update authority', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const asset = await createAsset(umi, { + updateAuthority: updateAuthority.publicKey, + owner: owner.publicKey, + plugins: [ + { + type: 'BurnDelegate', + authority: { + type: 'UpdateAuthority', + }, + }, + ], + }); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + burnDelegate: { + authority: { + type: 'UpdateAuthority', + }, + }, + }); + + await burn(umi, { + asset, + authority: updateAuthority, + }).sendAndConfirm(umi); + + await assertBurned(t, umi, asset.publicKey); +}); + +test('a burnDelegate can burn using delegated update authority from collection', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const collection = await createCollection(umi, { + updateAuthority: updateAuthority.publicKey, + }); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + collection: collection.publicKey, + plugins: [ + { + type: 'BurnDelegate', + authority: { + type: 'UpdateAuthority', + }, + }, + ], + authority: updateAuthority, + }); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + burnDelegate: { + authority: { + type: 'UpdateAuthority', + }, + }, + }); + + await burn(umi, { + asset, + collection, + authority: updateAuthority, + }).sendAndConfirm(umi); + + await assertBurned(t, umi, asset.publicKey); +}); diff --git a/clients/js/test/plugins/asset/delegateTransfer.test.ts b/clients/js/test/plugins/asset/delegateTransfer.test.ts index ab3cfdc7..9804a561 100644 --- a/clients/js/test/plugins/asset/delegateTransfer.test.ts +++ b/clients/js/test/plugins/asset/delegateTransfer.test.ts @@ -163,6 +163,58 @@ test('it cannot transfer after delegate authority has been revoked', async (t) = }); }); +test('it can transfer using delegated update authority', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const newOwner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + updateAuthority: updateAuthority.publicKey, + plugins: [ + { + type: 'TransferDelegate', + authority: { + type: 'UpdateAuthority', + }, + }, + ], + authority: updateAuthority, + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + transferDelegate: { + authority: { + type: 'UpdateAuthority', + }, + }, + }); + + await transfer(umi, { + asset, + newOwner: newOwner.publicKey, + authority: updateAuthority, + }).sendAndConfirm(umi); + + // Resets to `Owner` after the transfer. + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + transferDelegate: { + authority: { + type: 'Owner', + }, + }, + }); +}); + test('it can transfer using delegated update authority from collection', async (t) => { const umi = await createUmi(); const owner = generateSigner(umi); @@ -187,6 +239,18 @@ test('it can transfer using delegated update authority from collection', async ( authority: updateAuthority, }); + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Collection' }, + transferDelegate: { + authority: { + type: 'UpdateAuthority', + }, + }, + }); + await transfer(umi, { asset, collection, @@ -194,6 +258,7 @@ test('it can transfer using delegated update authority from collection', async ( authority: updateAuthority, }).sendAndConfirm(umi); + // Resets to `Owner` after the transfer. await assertAsset(t, umi, { ...DEFAULT_ASSET, asset: asset.publicKey, diff --git a/clients/js/test/plugins/asset/freeze.test.ts b/clients/js/test/plugins/asset/freeze.test.ts index fa6f0f7e..a6df6d91 100644 --- a/clients/js/test/plugins/asset/freeze.test.ts +++ b/clients/js/test/plugins/asset/freeze.test.ts @@ -11,11 +11,13 @@ import { createPlugin, ownerPluginAuthority, removePluginV1, + updatePluginAuthority, } from '../../../src'; import { DEFAULT_ASSET, assertAsset, createAsset, + createCollection, createUmi, } from '../../_setupRaw'; @@ -346,3 +348,105 @@ test('it update authority cannot unfreeze frozen asset', async (t) => { }, }); }); + +test('a freezeDelegate can freeze using delegated update authority', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const asset = await createAsset(umi, { + updateAuthority: updateAuthority.publicKey, + owner: owner.publicKey, + plugins: [ + pluginAuthorityPair({ + type: 'FreezeDelegate', + data: { frozen: false }, + authority: updatePluginAuthority(), + }), + ], + }); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + freezeDelegate: { + authority: { + type: 'UpdateAuthority', + }, + frozen: false, + }, + }); + + await updatePluginV1(umi, { + asset: asset.publicKey, + plugin: createPlugin({ type: 'FreezeDelegate', data: { frozen: true } }), + authority: updateAuthority, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + freezeDelegate: { + authority: { + type: 'UpdateAuthority', + }, + frozen: true, + }, + }); +}); + +test('a freezeDelegate can freeze using delegated update authority from collection', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const collection = await createCollection(umi, { + updateAuthority: updateAuthority.publicKey, + }); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + collection: collection.publicKey, + plugins: [ + pluginAuthorityPair({ + type: 'FreezeDelegate', + data: { frozen: false }, + authority: updatePluginAuthority(), + }), + ], + authority: updateAuthority, + }); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + freezeDelegate: { + authority: { + type: 'UpdateAuthority', + }, + frozen: false, + }, + }); + + await updatePluginV1(umi, { + asset: asset.publicKey, + collection: collection.publicKey, + plugin: createPlugin({ type: 'FreezeDelegate', data: { frozen: true } }), + authority: updateAuthority, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + freezeDelegate: { + authority: { + type: 'UpdateAuthority', + }, + frozen: true, + }, + }); +}); diff --git a/clients/js/test/plugins/asset/updateDelegate.test.ts b/clients/js/test/plugins/asset/updateDelegate.test.ts index ffc066d4..9598851a 100644 --- a/clients/js/test/plugins/asset/updateDelegate.test.ts +++ b/clients/js/test/plugins/asset/updateDelegate.test.ts @@ -3,6 +3,7 @@ import test from 'ava'; import { addPlugin, approvePluginAuthority, + removePlugin, revokePluginAuthority, update, updateAuthority, @@ -182,7 +183,6 @@ test('an updateDelegate can update an asset', async (t) => { }).sendAndConfirm(umi); await assertAsset(t, umi, { - ...DEFAULT_ASSET, asset: asset.publicKey, owner: umi.identity.publicKey, updateAuthority: { type: 'Address', address: umi.identity.publicKey }, @@ -503,7 +503,7 @@ test('it cannot remove another additional delegate as additional delegate', asyn await t.throwsAsync(result, { name: 'NoApprovals' }); }); -test('it cannot approve the update plugin authority as additional delegate', async (t) => { +test('it cannot approve the update delegate plugin authority as additional delegate', async (t) => { const umi = await createUmi(); const updateDelegate = generateSigner(umi); const updateDelegate2 = generateSigner(umi); @@ -531,7 +531,7 @@ test('it cannot approve the update plugin authority as additional delegate', asy await t.throwsAsync(result, { name: 'NoApprovals' }); }); -test('it cannot revoke the update plugin authority as additional delegate', async (t) => { +test('it cannot revoke the update delegate plugin authority as additional delegate', async (t) => { const umi = await createUmi(); const updateDelegate = generateSigner(umi); const updateDelegate2 = generateSigner(umi); @@ -559,7 +559,7 @@ test('it cannot revoke the update plugin authority as additional delegate', asyn await t.throwsAsync(result, { name: 'NoApprovals' }); }); -test('it can approve/revoke the update plugin authority of non-updateDelegate plugins as additional delegate', async (t) => { +test('it can approve/revoke the plugin authority of non-updateDelegate plugins as additional delegate', async (t) => { const umi = await createUmi(); const updateDelegate = generateSigner(umi); const updateDelegate2 = generateSigner(umi); @@ -735,3 +735,739 @@ test('it cannot update the update authority of the asset as an updateDelegate ro await t.throwsAsync(result, { name: 'NoApprovals' }); }); + +test('an updateDelegate can add a plugin to an asset', async (t) => { + const umi = await createUmi(); + const updateDelegate = generateSigner(umi); + + const asset = await createAsset(umi, { + plugins: [ + { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + }); + + await addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Attributes', + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + authority: updateDelegate, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + }); +}); + +test('an updateDelegate can add a plugin to an asset using delegated owner', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + updateAuthority: updateAuthority.publicKey, + plugins: [ + { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { + type: 'Owner', + }, + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + }); + + await addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Attributes', + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + }); +}); + +test('an updateDelegate can remove a plugin from an asset', async (t) => { + const umi = await createUmi(); + const updateDelegate = generateSigner(umi); + + const asset = await createAsset(umi, { + plugins: [ + { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + }, + { + type: 'Attributes', + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + }); + + await removePlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Attributes', + }, + authority: updateDelegate, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + }); +}); + +test('an updateDelegate can remove a plugin from an asset using delegated owner', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + updateAuthority: updateAuthority.publicKey, + plugins: [ + { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { + type: 'Owner', + }, + }, + { + type: 'Attributes', + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + }); + + await removePlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Attributes', + }, + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + }); +}); + +test('it can approve/revoke the plugin authority of other plugins', async (t) => { + const umi = await createUmi(); + const updateDelegate = generateSigner(umi); + + const asset = await createAsset(umi, { + plugins: [ + { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + }, + { + type: 'Edition', + number: 1, + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + edition: { + authority: { + type: 'UpdateAuthority', + }, + number: 1, + }, + }); + + const editionAuthority = generateSigner(umi); + await approvePluginAuthority(umi, { + asset: asset.publicKey, + plugin: { + type: 'Edition', + }, + newAuthority: { + type: 'Address', + address: editionAuthority.publicKey, + }, + authority: updateDelegate, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + edition: { + authority: { + type: 'Address', + address: editionAuthority.publicKey, + }, + number: 1, + }, + }); + + await revokePluginAuthority(umi, { + asset: asset.publicKey, + plugin: { + type: 'Edition', + }, + authority: updateDelegate, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + edition: { + authority: { + type: 'UpdateAuthority', + }, + number: 1, + }, + }); +}); + +test('it can approve/revoke the plugin authority of other plugins as delegated owner', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + updateAuthority: updateAuthority.publicKey, + plugins: [ + { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { + type: 'Owner', + }, + }, + { + type: 'Edition', + number: 1, + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + edition: { + authority: { + type: 'UpdateAuthority', + }, + number: 1, + }, + }); + + const editionAuthority = generateSigner(umi); + await approvePluginAuthority(umi, { + asset: asset.publicKey, + plugin: { + type: 'Edition', + }, + newAuthority: { + type: 'Address', + address: editionAuthority.publicKey, + }, + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + edition: { + authority: { + type: 'Address', + address: editionAuthority.publicKey, + }, + number: 1, + }, + }); + + await revokePluginAuthority(umi, { + asset: asset.publicKey, + plugin: { + type: 'Edition', + }, + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + edition: { + authority: { + type: 'UpdateAuthority', + }, + number: 1, + }, + }); +}); + +test('it can update an asset as delegated owner', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + updateAuthority: updateAuthority.publicKey, + plugins: [ + { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { + type: 'Owner', + }, + }, + ], + name: 'short', + uri: 'https://short.com', + }); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + name: 'short', + uri: 'https://short.com', + }); + + await update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + }); +}); + +test('an updateDelegate can update a plugin on an asset', async (t) => { + const umi = await createUmi(); + const updateDelegate = generateSigner(umi); + + const asset = await createAsset(umi, { + plugins: [ + { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + }, + { + type: 'Attributes', + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + }); + + await updatePlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Attributes', + attributeList: [ + { + key: '789', + value: '012', + }, + ], + }, + authority: updateDelegate, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + updateDelegate: { + authority: { + type: 'Address', + address: updateDelegate.publicKey, + }, + additionalDelegates: [], + }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [ + { + key: '789', + value: '012', + }, + ], + }, + }); +}); + +test('an updateDelegate can update a plugin on an asset using delegated owner', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const updateAuthority = generateSigner(umi); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + updateAuthority: updateAuthority.publicKey, + plugins: [ + { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { + type: 'Owner', + }, + }, + { + type: 'Attributes', + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [ + { + key: '123', + value: '456', + }, + ], + }, + }); + + await updatePlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Attributes', + attributeList: [ + { + key: '789', + value: '012', + }, + ], + }, + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: updateAuthority.publicKey }, + updateDelegate: { + authority: { + type: 'Owner', + }, + additionalDelegates: [], + }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [ + { + key: '789', + value: '012', + }, + ], + }, + }); +}); diff --git a/programs/mpl-core/src/plugins/burn_delegate.rs b/programs/mpl-core/src/plugins/burn_delegate.rs index 0d6c145a..a2c8d71f 100644 --- a/programs/mpl-core/src/plugins/burn_delegate.rs +++ b/programs/mpl-core/src/plugins/burn_delegate.rs @@ -3,7 +3,7 @@ use solana_program::program_error::ProgramError; use crate::{ plugins::{abstain, approve}, - state::{Authority, DataBlob}, + state::DataBlob, }; use super::{PluginValidation, PluginValidationContext, ValidationResult}; @@ -42,10 +42,11 @@ impl PluginValidation for BurnDelegate { &self, ctx: &PluginValidationContext, ) -> Result { - if ctx.self_authority - == (&Authority::Address { - address: *ctx.authority_info.key, - }) + if ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority) { approve!() } else { diff --git a/programs/mpl-core/src/plugins/freeze_delegate.rs b/programs/mpl-core/src/plugins/freeze_delegate.rs index 160a5c89..e63e9a28 100644 --- a/programs/mpl-core/src/plugins/freeze_delegate.rs +++ b/programs/mpl-core/src/plugins/freeze_delegate.rs @@ -1,7 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::program_error::ProgramError; -use crate::state::{Authority, DataBlob}; +use crate::state::DataBlob; use super::{ abstain, approve, reject, Plugin, PluginValidation, PluginValidationContext, ValidationResult, @@ -82,10 +82,11 @@ impl PluginValidation for FreezeDelegate { if let Some(Plugin::FreezeDelegate(freeze)) = ctx.target_plugin { if freeze.frozen { return reject!(); - } else if ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) + } else if ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority) { return approve!(); } diff --git a/programs/mpl-core/src/plugins/transfer.rs b/programs/mpl-core/src/plugins/transfer.rs index 033dc047..375346e6 100644 --- a/programs/mpl-core/src/plugins/transfer.rs +++ b/programs/mpl-core/src/plugins/transfer.rs @@ -1,7 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::program_error::ProgramError; -use crate::state::{Authority, DataBlob}; +use crate::state::DataBlob; use super::{abstain, approve, PluginValidation, PluginValidationContext, ValidationResult}; @@ -35,41 +35,19 @@ impl DataBlob for TransferDelegate { } impl PluginValidation for TransferDelegate { - fn validate_burn( + fn validate_transfer( &self, ctx: &PluginValidationContext, ) -> Result { - if ctx.self_authority - == (&Authority::Address { - address: *ctx.authority_info.key, - }) + if ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority) { - approve!() - } else { - abstain!() + return approve!(); } - } - fn validate_transfer( - &self, - ctx: &PluginValidationContext, - ) -> Result { - match ctx.self_authority { - Authority::Address { address } if address == ctx.authority_info.key => { - return approve!(); - } - - Authority::UpdateAuthority => { - if ctx - .resolved_authorities - .map_or(false, |auths| auths.contains(&Authority::UpdateAuthority)) - { - return approve!(); - } - } - - _ => {} - } abstain!() } } diff --git a/programs/mpl-core/src/plugins/update_delegate.rs b/programs/mpl-core/src/plugins/update_delegate.rs index cc43973d..7b6d5644 100644 --- a/programs/mpl-core/src/plugins/update_delegate.rs +++ b/programs/mpl-core/src/plugins/update_delegate.rs @@ -70,10 +70,11 @@ impl PluginValidation for UpdateDelegate { ctx: &PluginValidationContext, ) -> Result { if let Some(new_plugin) = ctx.target_plugin { - if (ctx.self_authority - == (&Authority::Address { - address: *ctx.authority_info.key, - }) + if ((ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority)) || self.additional_delegates.contains(ctx.authority_info.key)) && new_plugin.manager() == Authority::UpdateAuthority { @@ -91,10 +92,11 @@ impl PluginValidation for UpdateDelegate { ctx: &PluginValidationContext, ) -> Result { if let Some(plugin_to_remove) = ctx.target_plugin { - if (ctx.self_authority - == (&Authority::Address { - address: *ctx.authority_info.key, - }) + if ((ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority)) || self.additional_delegates.contains(ctx.authority_info.key)) && plugin_to_remove.manager() == Authority::UpdateAuthority { @@ -115,10 +117,11 @@ impl PluginValidation for UpdateDelegate { let plugin = ctx.target_plugin.ok_or(MplCoreError::InvalidPlugin)?; // If the plugin authority is the authority signing. - if (ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) + if ((ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority)) // Or the authority is one of the additional delegates. || self.additional_delegates.contains(ctx.authority_info.key)) // And it's an authority-managed plugin. @@ -141,10 +144,11 @@ impl PluginValidation for UpdateDelegate { let plugin = ctx.target_plugin.ok_or(MplCoreError::InvalidPlugin)?; // If the plugin authority is the authority signing. - if ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) + if (ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority)) // Or the authority is one of the additional delegates. || (self.additional_delegates.contains(ctx.authority_info.key) && PluginType::from(plugin) != PluginType::UpdateDelegate) // And it's an authority-managed plugin. @@ -160,10 +164,11 @@ impl PluginValidation for UpdateDelegate { &self, ctx: &PluginValidationContext, ) -> Result { - if (ctx.self_authority - == (&Authority::Address { - address: *ctx.authority_info.key, - }) + if ((ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority)) || self.additional_delegates.contains(ctx.authority_info.key)) // We do not allow the root authority (either Collection or Address) to be changed by this delegate. && ctx.new_collection_authority.is_none() && ctx.new_asset_authority.is_none() @@ -181,10 +186,11 @@ impl PluginValidation for UpdateDelegate { let plugin = ctx.target_plugin.ok_or(MplCoreError::InvalidPlugin)?; // If the plugin itself is being updated. - if ctx.self_authority - == (&Authority::Address { - address: *ctx.authority_info.key, - }) + if (ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority)) || self.additional_delegates.contains(ctx.authority_info.key) { if let Plugin::UpdateDelegate(update_delegate) = plugin { diff --git a/programs/mpl-core/src/state/update_authority.rs b/programs/mpl-core/src/state/update_authority.rs index 2c495bd6..27245421 100644 --- a/programs/mpl-core/src/state/update_authority.rs +++ b/programs/mpl-core/src/state/update_authority.rs @@ -1,12 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use solana_program::{program_error::ProgramError, pubkey::Pubkey}; - -use crate::{ - instruction::accounts::{ - BurnV1Accounts, CompressV1Accounts, DecompressV1Accounts, TransferV1Accounts, - }, - plugins::{abstain, CheckResult, ValidationResult}, -}; +use solana_program::pubkey::Pubkey; /// An enum representing the types of accounts that can update data on an asset. #[derive(Clone, BorshSerialize, BorshDeserialize, Debug, Eq, PartialEq)] @@ -28,43 +21,4 @@ impl UpdateAuthority { Self::Collection(address) => *address, } } - - /// Check permissions for the create lifecycle event. - pub fn check_create() -> CheckResult { - CheckResult::CanReject - } - - /// Check permissions for the update lifecycle event. - pub fn check_update() -> CheckResult { - CheckResult::CanApprove - } - - /// Validate the burn lifecycle event. - pub fn validate_burn(&self, _ctx: &BurnV1Accounts) -> Result { - abstain!() - } - - /// Validate the transfer lifecycle event. - pub fn validate_transfer( - &self, - _ctx: &TransferV1Accounts, - ) -> Result { - abstain!() - } - - /// Validate the compress lifecycle event. - pub fn validate_compress( - &self, - _ctx: &CompressV1Accounts, - ) -> Result { - abstain!() - } - - /// Validate the decompress lifecycle event. - pub fn validate_decompress( - &self, - _ctx: &DecompressV1Accounts, - ) -> Result { - abstain!() - } }