Skip to content

Commit

Permalink
Add ability to specify custom program for custom PDA (#117)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
danenbm authored May 10, 2024
1 parent 1d2c01f commit ca11aab
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 14 deletions.
2 changes: 1 addition & 1 deletion clients/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 8 additions & 8 deletions clients/js/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion clients/js/src/generated/types/baseExtraAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
* @see https://github.com/metaplex-foundation/kinobi
*/

import { PublicKey } from '@metaplex-foundation/umi';
import { Option, OptionOrNullable, PublicKey } from '@metaplex-foundation/umi';
import {
GetDataEnumKind,
GetDataEnumKindContent,
Serializer,
array,
bool,
dataEnum,
option,
publicKey as publicKeySerializer,
struct,
} from '@metaplex-foundation/umi/serializers';
Expand All @@ -32,6 +33,7 @@ export type BaseExtraAccount =
| {
__kind: 'CustomPda';
seeds: Array<BaseSeed>;
customProgramId: Option<PublicKey>;
isSigner: boolean;
isWritable: boolean;
}
Expand All @@ -55,6 +57,7 @@ export type BaseExtraAccountArgs =
| {
__kind: 'CustomPda';
seeds: Array<BaseSeedArgs>;
customProgramId: OptionOrNullable<PublicKey>;
isSigner: boolean;
isWritable: boolean;
}
Expand Down Expand Up @@ -116,6 +119,7 @@ export function getBaseExtraAccountSerializer(): Serializer<
'CustomPda',
struct<GetDataEnumKindContent<BaseExtraAccount, 'CustomPda'>>([
['seeds', array(getBaseSeedSerializer())],
['customProgramId', option(publicKeySerializer())],
['isSigner', bool()],
['isWritable', bool()],
]),
Expand Down
7 changes: 5 additions & 2 deletions clients/js/src/plugins/extraAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -33,6 +33,7 @@ export type ExtraAccount =
| {
type: 'CustomPda';
seeds: Array<Seed>;
customProgramId?: PublicKey;
isSigner?: boolean;
isWritable?: boolean;
}
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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') {
Expand All @@ -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') {
Expand Down
140 changes: 140 additions & 0 deletions clients/js/test/externalPlugins/oracle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
1 change: 1 addition & 0 deletions clients/rust/src/generated/types/extra_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub enum ExtraAccount {
},
CustomPda {
seeds: Vec<Seed>,
custom_program_id: Option<Pubkey>,
is_signer: bool,
is_writable: bool,
},
Expand Down
6 changes: 6 additions & 0 deletions idls/mpl_core.json
Original file line number Diff line number Diff line change
Expand Up @@ -3638,6 +3638,12 @@
}
}
},
{
"name": "custom_program_id",
"type": {
"option": "publicKey"
}
},
{
"name": "is_signer",
"type": "bool"
Expand Down
13 changes: 11 additions & 2 deletions programs/mpl-core/src/plugins/external_plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ pub enum ExtraAccount {
CustomPda {
/// Seeds used to derive the PDA.
seeds: Vec<Seed>,
/// Program ID if not the base address/program ID for the external plugin.
custom_program_id: Option<Pubkey>,
/// Account is a signer
is_signer: bool,
/// Account is writable.
Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit ca11aab

Please sign in to comment.