From 8b436f67da4dbcec16bb07bd99b9a4184679069d Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 9 May 2024 00:04:03 -0700 Subject: [PATCH 1/5] Add uninitialized variant to OracleValidation --- programs/mpl-core/src/error.rs | 4 ++++ programs/mpl-core/src/plugins/oracle.rs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index 7b7003bb..214f7173 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -164,6 +164,10 @@ pub enum MplCoreError { /// 38 - Could not read from oracle account #[error("Could not read from oracle account")] InvalidOracleAccountData, + + /// 39 - Oracle account is uninitialized + #[error("Oracle account is uninitialized")] + UninitializedOracleAccount, } impl PrintProgramError for MplCoreError { diff --git a/programs/mpl-core/src/plugins/oracle.rs b/programs/mpl-core/src/plugins/oracle.rs index 441dfedc..3d0842c9 100644 --- a/programs/mpl-core/src/plugins/oracle.rs +++ b/programs/mpl-core/src/plugins/oracle.rs @@ -103,6 +103,7 @@ impl Oracle { .map_err(|_| MplCoreError::InvalidOracleAccountData)?; match validation_result { + OracleValidation::Uninitialized => Err(MplCoreError::UninitializedOracleAccount.into()), OracleValidation::V1 { create, transfer, @@ -185,6 +186,8 @@ impl ValidationResultsOffset { /// Validation results struct for an Oracle account. #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Eq, PartialEq)] pub enum OracleValidation { + /// Uninitialized data. This is intended to prevent leaving an account zeroed out by mistake. + Uninitialized, /// Version 1 of the format. V1 { /// Validation for the the create lifecycle action. From b8c337b6da07950b8b629efa5cd6859b17e41ae3 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 9 May 2024 00:08:30 -0700 Subject: [PATCH 2/5] Regenerate IDL and clients --- clients/js/src/generated/errors/mplCore.ts | 16 ++++++++ .../src/generated/types/oracleValidation.ts | 37 ++++++++++++------- clients/rust/src/generated/errors/mpl_core.rs | 3 ++ .../src/generated/types/oracle_validation.rs | 1 + idls/mpl_core.json | 8 ++++ 5 files changed, 51 insertions(+), 14 deletions(-) diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index 724769a6..e4055ee8 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -556,6 +556,22 @@ export class InvalidOracleAccountDataError extends ProgramError { codeToErrorMap.set(0x26, InvalidOracleAccountDataError); nameToErrorMap.set('InvalidOracleAccountData', InvalidOracleAccountDataError); +/** UninitializedOracleAccount: Oracle account is uninitialized */ +export class UninitializedOracleAccountError extends ProgramError { + override readonly name: string = 'UninitializedOracleAccount'; + + readonly code: number = 0x27; // 39 + + constructor(program: Program, cause?: Error) { + super('Oracle account is uninitialized', program, cause); + } +} +codeToErrorMap.set(0x27, UninitializedOracleAccountError); +nameToErrorMap.set( + 'UninitializedOracleAccount', + UninitializedOracleAccountError +); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/js/src/generated/types/oracleValidation.ts b/clients/js/src/generated/types/oracleValidation.ts index 16984db5..e92efa8c 100644 --- a/clients/js/src/generated/types/oracleValidation.ts +++ b/clients/js/src/generated/types/oracleValidation.ts @@ -12,6 +12,7 @@ import { Serializer, dataEnum, struct, + unit, } from '@metaplex-foundation/umi/serializers'; import { ExternalValidationResult, @@ -19,21 +20,25 @@ import { getExternalValidationResultSerializer, } from '.'; -export type OracleValidation = { - __kind: 'V1'; - create: ExternalValidationResult; - transfer: ExternalValidationResult; - burn: ExternalValidationResult; - update: ExternalValidationResult; -}; +export type OracleValidation = + | { __kind: 'Uninitialized' } + | { + __kind: 'V1'; + create: ExternalValidationResult; + transfer: ExternalValidationResult; + burn: ExternalValidationResult; + update: ExternalValidationResult; + }; -export type OracleValidationArgs = { - __kind: 'V1'; - create: ExternalValidationResultArgs; - transfer: ExternalValidationResultArgs; - burn: ExternalValidationResultArgs; - update: ExternalValidationResultArgs; -}; +export type OracleValidationArgs = + | { __kind: 'Uninitialized' } + | { + __kind: 'V1'; + create: ExternalValidationResultArgs; + transfer: ExternalValidationResultArgs; + burn: ExternalValidationResultArgs; + update: ExternalValidationResultArgs; + }; export function getOracleValidationSerializer(): Serializer< OracleValidationArgs, @@ -41,6 +46,7 @@ export function getOracleValidationSerializer(): Serializer< > { return dataEnum( [ + ['Uninitialized', unit()], [ 'V1', struct>([ @@ -56,6 +62,9 @@ export function getOracleValidationSerializer(): Serializer< } // Data Enum Helpers. +export function oracleValidation( + kind: 'Uninitialized' +): GetDataEnumKind; export function oracleValidation( kind: 'V1', data: GetDataEnumKindContent diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index 81aaff8b..2c148460 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -127,6 +127,9 @@ pub enum MplCoreError { /// 38 (0x26) - Could not read from oracle account #[error("Could not read from oracle account")] InvalidOracleAccountData, + /// 39 (0x27) - Oracle account is uninitialized + #[error("Oracle account is uninitialized")] + UninitializedOracleAccount, } impl solana_program::program_error::PrintProgramError for MplCoreError { diff --git a/clients/rust/src/generated/types/oracle_validation.rs b/clients/rust/src/generated/types/oracle_validation.rs index 543223ba..deb89deb 100644 --- a/clients/rust/src/generated/types/oracle_validation.rs +++ b/clients/rust/src/generated/types/oracle_validation.rs @@ -16,6 +16,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; #[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] #[derive(Clone, Debug, Eq, PartialEq)] pub enum OracleValidation { + Uninitialized, V1 { create: ExternalValidationResult, transfer: ExternalValidationResult, diff --git a/idls/mpl_core.json b/idls/mpl_core.json index a75de249..a8e90d67 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -3871,6 +3871,9 @@ "type": { "kind": "enum", "variants": [ + { + "name": "Uninitialized" + }, { "name": "V1", "fields": [ @@ -4215,6 +4218,11 @@ "code": 38, "name": "InvalidOracleAccountData", "msg": "Could not read from oracle account" + }, + { + "code": 39, + "name": "UninitializedOracleAccount", + "msg": "Oracle account is uninitialized" } ], "metadata": { From 597d9ee8a7f9228a6a74069f12bc07694e375623 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 9 May 2024 00:08:59 -0700 Subject: [PATCH 3/5] Handle uninitialized kind in JS --- clients/js/src/helpers/lifecycle.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/clients/js/src/helpers/lifecycle.ts b/clients/js/src/helpers/lifecycle.ts index c35e74da..16f26650 100644 --- a/clients/js/src/helpers/lifecycle.ts +++ b/clients/js/src/helpers/lifecycle.ts @@ -148,9 +148,12 @@ export async function validateTransfer( return null; }); - const oraclePass = oracleValidations.every( - (v) => v?.transfer === ExternalValidationResult.Pass - ); + const oraclePass = oracleValidations.every((v) => { + if (v?.__kind === 'Uninitialized') { + return false; + } + return v?.transfer === ExternalValidationResult.Pass; + }); if (!oraclePass) { return LifecycleValidationError.OracleValidationFailed; } @@ -284,9 +287,12 @@ export async function validateBurn( return null; }); - const oraclePass = oracleValidations.every( - (v) => v?.burn === ExternalValidationResult.Pass - ); + const oraclePass = oracleValidations.every((v) => { + if (v?.__kind === 'Uninitialized') { + return false; + } + return v?.burn === ExternalValidationResult.Pass; + }); if (!oraclePass) { return LifecycleValidationError.OracleValidationFailed; } @@ -373,9 +379,12 @@ export async function validateUpdate( return null; }); - const oraclePass = oracleValidations.every( - (v) => v?.update === ExternalValidationResult.Pass - ); + const oraclePass = oracleValidations.every((v) => { + if (v?.__kind === 'Uninitialized') { + return false; + } + return v?.update === ExternalValidationResult.Pass; + }); if (!oraclePass) { return LifecycleValidationError.OracleValidationFailed; } From 1d2c01f917c336f9863116b4f8c8e75030b2426d Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 9 May 2024 00:15:15 -0700 Subject: [PATCH 4/5] Add test for empty account --- .../js/test/externalPlugins/oracle.test.ts | 64 ++++++++++++++++++- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index be1fd6ed..faa348ae 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -2390,7 +2390,7 @@ test('it can update oracle to different size external plugin', async (t) => { }); }); -test('create fails but does not panic when oracle account does not exist', async (t) => { +test('it create fails but does not panic when oracle account does not exist', async (t) => { const umi = await createUmi(); const oracleSigner = generateSigner(umi); @@ -2419,7 +2419,7 @@ test('create fails but does not panic when oracle account does not exist', async await t.throwsAsync(result, { name: 'InvalidOracleAccountData' }); }); -test('transfer fails but does not panic when oracle account does not exist', async (t) => { +test('it transfer fails but does not panic when oracle account does not exist', async (t) => { const umi = await createUmi(); const oracleSigner = generateSigner(umi); @@ -2469,7 +2469,7 @@ test('transfer fails but does not panic when oracle account does not exist', asy await t.throwsAsync(result, { name: 'InvalidOracleAccountData' }); }); -test('transfer fails but does not panic when oracle account is too small', async (t) => { +test('it transfer fails but does not panic when oracle account is too small', async (t) => { const umi = await createUmi(); const newAccount = generateSigner(umi); @@ -2526,3 +2526,61 @@ test('transfer fails but does not panic when oracle account is too small', async await t.throwsAsync(result, { name: 'InvalidOracleAccountData' }); }); + +test('it empty account does not default to valid oracle', async (t) => { + const umi = await createUmi(); + const newAccount = generateSigner(umi); + + // Create an invalid oracle account that is an account with 42 bytes. + await createAccount(umi, { + newAccount, + lamports: sol(0.1), + space: 42, + programId: umi.programs.get('mplCore').publicKey, + }).sendAndConfirm(umi); + + const asset = await createAsset(umi, { + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'NoOffset', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: newAccount.publicKey, + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + oracles: [ + { + type: 'Oracle', + resultsOffset: { + type: 'NoOffset', + }, + authority: { + type: 'UpdateAuthority', + }, + baseAddress: newAccount.publicKey, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + pda: undefined, + }, + ], + }); + + const newOwner = generateSigner(umi); + const result = transfer(umi, { + asset, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'UninitializedOracleAccount' }); +}); From ca11aab5cf4cb7f7009b9ea20482658aca17c087 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 9 May 2024 21:17:36 -0700 Subject: [PATCH 5/5] Add ability to specify custom program for custom PDA (#117) * Add ability to specify custom program for custom PDA * Regenerate IDL and JS SDK * Add customProgramId JS handling * Add test * Update oracle example js package --- clients/js/package.json | 2 +- clients/js/pnpm-lock.yaml | 16 +- .../src/generated/types/baseExtraAccount.ts | 6 +- clients/js/src/plugins/extraAccount.ts | 7 +- .../js/test/externalPlugins/oracle.test.ts | 140 ++++++++++++++++++ .../rust/src/generated/types/extra_account.rs | 1 + idls/mpl_core.json | 6 + .../mpl-core/src/plugins/external_plugins.rs | 13 +- 8 files changed, 177 insertions(+), 14 deletions(-) diff --git a/clients/js/package.json b/clients/js/package.json index 209f4b92..d511f0b7 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@ava/typescript": "^5.0.0", - "@metaplex-foundation/mpl-core-oracle-example": "^0.0.1", + "@metaplex-foundation/mpl-core-oracle-example": "^0.0.2", "@metaplex-foundation/mpl-toolbox": "^0.8.0", "@metaplex-foundation/umi": "^0.8.10", "@metaplex-foundation/umi-bundle-tests": "^0.8.10", diff --git a/clients/js/pnpm-lock.yaml b/clients/js/pnpm-lock.yaml index d09617b0..5a66d381 100644 --- a/clients/js/pnpm-lock.yaml +++ b/clients/js/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@noble/hashes': specifier: ^1.3.1 @@ -10,8 +14,8 @@ devDependencies: specifier: ^5.0.0 version: 5.0.0 '@metaplex-foundation/mpl-core-oracle-example': - specifier: ^0.0.1 - version: 0.0.1(@metaplex-foundation/umi@0.8.10)(@noble/hashes@1.3.1) + specifier: ^0.0.2 + version: 0.0.2(@metaplex-foundation/umi@0.8.10)(@noble/hashes@1.3.1) '@metaplex-foundation/mpl-toolbox': specifier: ^0.8.0 version: 0.8.0(@metaplex-foundation/umi@0.8.10) @@ -1860,8 +1864,8 @@ packages: - supports-color dev: true - /@metaplex-foundation/mpl-core-oracle-example@0.0.1(@metaplex-foundation/umi@0.8.10)(@noble/hashes@1.3.1): - resolution: {integrity: sha512-z2432DCY6eqPSUbMAqHNpZoN2keDu9kLo5tr0d/Kx7GbZ3LHIg7tiyber9gtIUJAJx3c0k3DX/awoA9Fd5kbdA==} + /@metaplex-foundation/mpl-core-oracle-example@0.0.2(@metaplex-foundation/umi@0.8.10)(@noble/hashes@1.3.1): + resolution: {integrity: sha512-DxHKLaM04YGMv/EGgIzzxSJnBPv+OTBKeekL7mm7Pcu/DqH+xIuy4K7h9mMhI3E1gSbWIaxtzlbitbvwXvYRcw==} peerDependencies: '@metaplex-foundation/umi': '>=0.8.2 < 1' '@noble/hashes': ^1.3.1 @@ -9060,7 +9064,3 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/clients/js/src/generated/types/baseExtraAccount.ts b/clients/js/src/generated/types/baseExtraAccount.ts index f9796ccf..68a3d576 100644 --- a/clients/js/src/generated/types/baseExtraAccount.ts +++ b/clients/js/src/generated/types/baseExtraAccount.ts @@ -6,7 +6,7 @@ * @see https://github.com/metaplex-foundation/kinobi */ -import { PublicKey } from '@metaplex-foundation/umi'; +import { Option, OptionOrNullable, PublicKey } from '@metaplex-foundation/umi'; import { GetDataEnumKind, GetDataEnumKindContent, @@ -14,6 +14,7 @@ import { array, bool, dataEnum, + option, publicKey as publicKeySerializer, struct, } from '@metaplex-foundation/umi/serializers'; @@ -32,6 +33,7 @@ export type BaseExtraAccount = | { __kind: 'CustomPda'; seeds: Array; + customProgramId: Option; isSigner: boolean; isWritable: boolean; } @@ -55,6 +57,7 @@ export type BaseExtraAccountArgs = | { __kind: 'CustomPda'; seeds: Array; + customProgramId: OptionOrNullable; isSigner: boolean; isWritable: boolean; } @@ -116,6 +119,7 @@ export function getBaseExtraAccountSerializer(): Serializer< 'CustomPda', struct>([ ['seeds', array(getBaseSeedSerializer())], + ['customProgramId', option(publicKeySerializer())], ['isSigner', bool()], ['isWritable', bool()], ]), diff --git a/clients/js/src/plugins/extraAccount.ts b/clients/js/src/plugins/extraAccount.ts index 071c0a4c..cacab94b 100644 --- a/clients/js/src/plugins/extraAccount.ts +++ b/clients/js/src/plugins/extraAccount.ts @@ -5,7 +5,7 @@ import { } from '@metaplex-foundation/umi/serializers'; import { BaseExtraAccount } from '../generated'; import { Seed, seedFromBase, seedToBase } from './seed'; -import { RenameToType } from '../utils'; +import { RenameToType, someOrNone, unwrapOption } from '../utils'; export const PRECONFIGURED_SEED = 'mpl-core'; @@ -33,6 +33,7 @@ export type ExtraAccount = | { type: 'CustomPda'; seeds: Array; + customProgramId?: PublicKey; isSigner?: boolean; isWritable?: boolean; } @@ -120,7 +121,7 @@ export function extraAccountToAccountMeta( case 'CustomPda': return { pubkey: context.eddsa.findPda( - inputs.program!, + e.customProgramId ? e.customProgramId : inputs.program!, e.seeds.map((seed) => { switch (seed.type) { case 'Collection': @@ -162,6 +163,7 @@ export function extraAccountToBase(s: ExtraAccount): BaseExtraAccount { __kind: 'CustomPda', ...acccountMeta, seeds: s.seeds.map(seedToBase), + customProgramId: someOrNone(s.customProgramId), }; } if (s.type === 'Address') { @@ -185,6 +187,7 @@ export function extraAccountFromBase(s: BaseExtraAccount): ExtraAccount { isSigner: s.isSigner, isWritable: s.isWritable, seeds: s.seeds.map(seedFromBase), + customProgramId: unwrapOption(s.customProgramId), }; } if (s.__kind === 'Address') { diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index faa348ae..8c1e5ea1 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -1924,6 +1924,146 @@ test('it can use custom pda (typical) oracle to deny transfer', async (t) => { }); }); +test('it can use custom pda (with custom program ID) oracle to deny transfer', async (t) => { + const umi = await createUmi(); + const owner = generateSigner(umi); + const newOwner = generateSigner(umi); + + // Configure an Oracle plugin to have a custom program ID. In order to reuse the oracle + // example program we will set the base address to a random Pubkey, and set the custom program + // ID to the oracle example program ID. + const randomProgramId = generateSigner(umi).publicKey; + const oraclePlugin: OracleInitInfoArgs = { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: randomProgramId, + pda: { + type: 'CustomPda', + seeds: [ + { + type: 'Bytes', + bytes: Buffer.from('prefix-seed-bytes', 'utf8'), + }, + { type: 'Collection' }, + { + type: 'Bytes', + bytes: Buffer.from('additional-bytes-seed-bytes', 'utf8'), + }, + ], + customProgramId: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID + }, + }; + + const { asset, collection } = await createAssetWithCollection( + umi, + { + owner, + plugins: [oraclePlugin], + }, + {} + ); + + // Find the oracle PDA based on the asset we just created + const account = findOracleAccount(umi, oraclePlugin, { + collection: collection.publicKey, + }); + + // write to the PDA + await customPdaTypicalInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + prefixBytes: Buffer.from('prefix-seed-bytes', 'utf8'), + collection: collection.publicKey, + additionalBytes: Buffer.from('additional-bytes-seed-bytes', 'utf8'), + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const result = transfer(umi, { + asset, + collection, + newOwner: newOwner.publicKey, + authority: owner, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await customPdaTypicalSet(umi, { + account, + signer: umi.identity, + args: { + prefixBytes: Buffer.from('prefix-seed-bytes', 'utf8'), + collection: collection.publicKey, + additionalBytes: Buffer.from('additional-bytes-seed-bytes', 'utf8'), + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await transfer(umi, { + asset, + collection, + newOwner: newOwner.publicKey, + authority: owner, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + oracles: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + authority: { + type: 'UpdateAuthority', + }, + baseAddress: randomProgramId, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + pda: { + type: 'CustomPda', + seeds: [ + { + type: 'Bytes', + bytes: new Uint8Array(Buffer.from('prefix-seed-bytes', 'utf8')), + }, + { type: 'Collection' }, + { + type: 'Bytes', + bytes: new Uint8Array( + Buffer.from('additional-bytes-seed-bytes', 'utf8') + ), + }, + ], + customProgramId: MPL_CORE_ORACLE_EXAMPLE_PROGRAM_ID + }, + }, + ], + }); +}); + test('it can use preconfigured asset pda custom offset oracle to deny update', async (t) => { const umi = await createUmi(); diff --git a/clients/rust/src/generated/types/extra_account.rs b/clients/rust/src/generated/types/extra_account.rs index 8ebab8ec..55eb3d50 100644 --- a/clients/rust/src/generated/types/extra_account.rs +++ b/clients/rust/src/generated/types/extra_account.rs @@ -39,6 +39,7 @@ pub enum ExtraAccount { }, CustomPda { seeds: Vec, + custom_program_id: Option, is_signer: bool, is_writable: bool, }, diff --git a/idls/mpl_core.json b/idls/mpl_core.json index a8e90d67..d171b50c 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -3638,6 +3638,12 @@ } } }, + { + "name": "custom_program_id", + "type": { + "option": "publicKey" + } + }, { "name": "is_signer", "type": "bool" diff --git a/programs/mpl-core/src/plugins/external_plugins.rs b/programs/mpl-core/src/plugins/external_plugins.rs index 0f4f4770..d9412acd 100644 --- a/programs/mpl-core/src/plugins/external_plugins.rs +++ b/programs/mpl-core/src/plugins/external_plugins.rs @@ -278,6 +278,8 @@ pub enum ExtraAccount { CustomPda { /// Seeds used to derive the PDA. seeds: Vec, + /// Program ID if not the base address/program ID for the external plugin. + custom_program_id: Option, /// Account is a signer is_signer: bool, /// Account is writable. @@ -334,13 +336,20 @@ impl ExtraAccount { let (pubkey, _bump) = Pubkey::find_program_address(seeds, program_id); Ok(pubkey) } - ExtraAccount::CustomPda { seeds, .. } => { + ExtraAccount::CustomPda { + seeds, + custom_program_id, + .. + } => { let seeds = transform_seeds(seeds, ctx)?; // Convert the Vec of Vec into Vec of u8 slices. let vec_of_slices: Vec<&[u8]> = seeds.iter().map(Vec::as_slice).collect(); - let (pubkey, _bump) = Pubkey::find_program_address(&vec_of_slices, program_id); + let (pubkey, _bump) = Pubkey::find_program_address( + &vec_of_slices, + custom_program_id.as_ref().unwrap_or(program_id), + ); Ok(pubkey) } ExtraAccount::Address { address, .. } => Ok(*address),