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),