diff --git a/.github/.env b/.github/.env index 4dc2a34..e620655 100644 --- a/.github/.env +++ b/.github/.env @@ -2,7 +2,7 @@ CARGO_TERM_COLOR=always NODE_VERSION=20.x PROGRAMS=["mpl-hybrid"] RUST_VERSION=1.75.0 -SOLANA_VERSION=1.18.15 +SOLANA_VERSION=1.18.21 COMMIT_USER_NAME=github-actions COMMIT_USER_EMAIL=github-actions@github.com -DEPLOY_SOLANA_VERSION=1.18.15 +DEPLOY_SOLANA_VERSION=1.18.21 diff --git a/clients/js/src/generated/errors/mplHybrid.ts b/clients/js/src/generated/errors/mplHybrid.ts index 2a2d82f..18a0412 100644 --- a/clients/js/src/generated/errors/mplHybrid.ts +++ b/clients/js/src/generated/errors/mplHybrid.ts @@ -174,6 +174,19 @@ export class NumericalOverflowError extends ProgramError { codeToErrorMap.set(0x177b, NumericalOverflowError); nameToErrorMap.set('NumericalOverflow', NumericalOverflowError); +/** InvalidUpdateAuthority: Invalid Update Authority */ +export class InvalidUpdateAuthorityError extends ProgramError { + override readonly name: string = 'InvalidUpdateAuthority'; + + readonly code: number = 0x177c; // 6012 + + constructor(program: Program, cause?: Error) { + super('Invalid Update Authority', program, cause); + } +} +codeToErrorMap.set(0x177c, InvalidUpdateAuthorityError); +nameToErrorMap.set('InvalidUpdateAuthority', InvalidUpdateAuthorityError); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/js/src/generated/instructions/captureV1.ts b/clients/js/src/generated/instructions/captureV1.ts index 43eb44c..b63a1ba 100644 --- a/clients/js/src/generated/instructions/captureV1.ts +++ b/clients/js/src/generated/instructions/captureV1.ts @@ -33,7 +33,7 @@ import { // Accounts. export type CaptureV1InstructionAccounts = { owner: Signer; - authority?: Signer; + authority?: PublicKey | Pda | Signer; escrow: PublicKey | Pda; asset: PublicKey | Pda; collection: PublicKey | Pda; diff --git a/clients/js/src/generated/instructions/releaseV1.ts b/clients/js/src/generated/instructions/releaseV1.ts index 5b2f189..1362e2f 100644 --- a/clients/js/src/generated/instructions/releaseV1.ts +++ b/clients/js/src/generated/instructions/releaseV1.ts @@ -33,7 +33,7 @@ import { // Accounts. export type ReleaseV1InstructionAccounts = { owner: Signer; - authority?: Signer; + authority?: PublicKey | Pda | Signer; escrow: PublicKey | Pda; asset: PublicKey | Pda; collection: PublicKey | Pda; diff --git a/clients/js/test/capture.test.ts b/clients/js/test/capture.test.ts index 86f05a1..9a876a9 100644 --- a/clients/js/test/capture.test.ts +++ b/clients/js/test/capture.test.ts @@ -9,7 +9,7 @@ import { string, publicKey as publicKeySerializer, } from '@metaplex-foundation/umi/serializers'; -import { transfer } from '@metaplex-foundation/mpl-core'; +import { addCollectionPlugin, transfer } from '@metaplex-foundation/mpl-core'; import { captureV1, EscrowV1, @@ -103,3 +103,99 @@ test('it can swap tokens for an asset', async (t) => { token: tokenMint.publicKey, }).sendAndConfirm(umi); }); + +test('it can swap tokens for an asset as UpdateDelegate', async (t) => { + // Given a Umi instance using the project's plugin. + const umi = await createUmi(); + const feeLocation = generateSigner(umi); + const { assets, collection } = await createCoreCollection(umi); + const tokenMint = generateSigner(umi); + await createFungible(umi, { + name: 'Test Token', + uri: 'www.fungible.com', + sellerFeeBasisPoints: { + basisPoints: 0n, + identifier: '%', + decimals: 2, + }, + mint: tokenMint, + }).sendAndConfirm(umi); + + await mintV1(umi, { + mint: tokenMint.publicKey, + tokenStandard: TokenStandard.Fungible, + tokenOwner: umi.identity.publicKey, + amount: 1000, + }).sendAndConfirm(umi); + + const escrow = umi.eddsa.findPda(MPL_HYBRID_PROGRAM_ID, [ + string({ size: 'variable' }).serialize('escrow'), + publicKeySerializer().serialize(collection.publicKey), + ]); + + // Transfer the assets to the escrow. + // eslint-disable-next-line no-restricted-syntax + for (const asset of assets) { + // eslint-disable-next-line no-await-in-loop + await transfer(umi, { + asset, + collection, + newOwner: escrow, + }).sendAndConfirm(umi); + } + + await initEscrowV1(umi, { + escrow, + collection: collection.publicKey, + token: tokenMint.publicKey, + feeLocation: feeLocation.publicKey, + name: 'Test Escrow', + uri: 'www.test.com', + max: 9, + min: 0, + amount: 5, + feeAmount: 1, + // eslint-disable-next-line no-bitwise + path: 1 << Path.RerollMetadata, + solFeeAmount: 1000000n, + }).sendAndConfirm(umi); + + await addCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { type: 'Address', address: publicKey(escrow) }, + }, + }).sendAndConfirm(umi); + + const escrowData = await fetchEscrowV1(umi, escrow); + + t.like(escrowData, { + publicKey: publicKey(escrow), + collection: collection.publicKey, + token: tokenMint.publicKey, + feeLocation: feeLocation.publicKey, + name: 'Test Escrow', + uri: 'www.test.com', + max: 9n, + min: 0n, + amount: 5n, + feeAmount: 1n, + count: 1n, + // eslint-disable-next-line no-bitwise + path: 1 << Path.RerollMetadata, + bump: escrow[1], + solFeeAmount: 1_000_000n, + }); + + await captureV1(umi, { + owner: umi.identity, + authority: escrow, + escrow, + asset: assets[0].publicKey, + collection: collection.publicKey, + feeProjectAccount: escrowData.feeLocation, + token: tokenMint.publicKey, + }).sendAndConfirm(umi); +}); diff --git a/clients/js/test/release.test.ts b/clients/js/test/release.test.ts index 248d0b5..df0c6f2 100644 --- a/clients/js/test/release.test.ts +++ b/clients/js/test/release.test.ts @@ -9,6 +9,7 @@ import { string, publicKey as publicKeySerializer, } from '@metaplex-foundation/umi/serializers'; +import { addCollectionPlugin } from '@metaplex-foundation/mpl-core'; import { EscrowV1, fetchEscrowV1, @@ -91,3 +92,88 @@ test('it can swap an asset for tokens', async (t) => { token: tokenMint.publicKey, }).sendAndConfirm(umi); }); + +test('it can swap an asset for tokens as UpdateDelegate', async (t) => { + // Given a Umi instance using the project's plugin. + const umi = await createUmi(); + const feeLocation = generateSigner(umi); + const { assets, collection } = await createCoreCollection(umi); + const tokenMint = generateSigner(umi); + await createFungible(umi, { + name: 'Test Token', + uri: 'www.fungible.com', + sellerFeeBasisPoints: { + basisPoints: 0n, + identifier: '%', + decimals: 2, + }, + mint: tokenMint, + }).sendAndConfirm(umi); + + const escrow = umi.eddsa.findPda(MPL_HYBRID_PROGRAM_ID, [ + string({ size: 'variable' }).serialize('escrow'), + publicKeySerializer().serialize(collection.publicKey), + ]); + + await mintV1(umi, { + mint: tokenMint.publicKey, + tokenStandard: TokenStandard.Fungible, + tokenOwner: escrow, + amount: 1000, + }).sendAndConfirm(umi); + + await initEscrowV1(umi, { + escrow, + collection: collection.publicKey, + token: tokenMint.publicKey, + feeLocation: feeLocation.publicKey, + name: 'Test Escrow', + uri: 'www.test.com', + max: 9, + min: 0, + amount: 5, + feeAmount: 1, + // eslint-disable-next-line no-bitwise + path: 1 << Path.RerollMetadata, + solFeeAmount: 1000000n, + }).sendAndConfirm(umi); + + await addCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'UpdateDelegate', + additionalDelegates: [], + authority: { type: 'Address', address: publicKey(escrow) }, + }, + }).sendAndConfirm(umi); + + const escrowData = await fetchEscrowV1(umi, escrow); + + t.like(escrowData, { + publicKey: publicKey(escrow), + collection: collection.publicKey, + token: tokenMint.publicKey, + feeLocation: feeLocation.publicKey, + name: 'Test Escrow', + uri: 'www.test.com', + max: 9n, + min: 0n, + amount: 5n, + feeAmount: 1n, + count: 1n, + // eslint-disable-next-line no-bitwise + path: 1 << Path.RerollMetadata, + bump: escrow[1], + solFeeAmount: 1_000_000n, + }); + + await releaseV1(umi, { + owner: umi.identity, + authority: escrow, + escrow, + asset: assets[0].publicKey, + collection: collection.publicKey, + feeProjectAccount: escrowData.feeLocation, + token: tokenMint.publicKey, + }).sendAndConfirm(umi); +}); diff --git a/clients/rust/src/generated/errors/mpl_hybrid.rs b/clients/rust/src/generated/errors/mpl_hybrid.rs index b7bfa56..e93cfe0 100644 --- a/clients/rust/src/generated/errors/mpl_hybrid.rs +++ b/clients/rust/src/generated/errors/mpl_hybrid.rs @@ -46,6 +46,9 @@ pub enum MplHybridError { /// 6011 (0x177B) - Numerical Overflow #[error("Numerical Overflow")] NumericalOverflow, + /// 6012 (0x177C) - Invalid Update Authority + #[error("Invalid Update Authority")] + InvalidUpdateAuthority, } impl solana_program::program_error::PrintProgramError for MplHybridError { diff --git a/clients/rust/src/generated/instructions/capture_v1.rs b/clients/rust/src/generated/instructions/capture_v1.rs index 901ca1c..cb73090 100644 --- a/clients/rust/src/generated/instructions/capture_v1.rs +++ b/clients/rust/src/generated/instructions/capture_v1.rs @@ -14,7 +14,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; pub struct CaptureV1 { pub owner: solana_program::pubkey::Pubkey, - pub authority: solana_program::pubkey::Pubkey, + pub authority: (solana_program::pubkey::Pubkey, bool), pub escrow: solana_program::pubkey::Pubkey, @@ -59,8 +59,8 @@ impl CaptureV1 { self.owner, true, )); accounts.push(solana_program::instruction::AccountMeta::new( - self.authority, - true, + self.authority.0, + self.authority.1, )); accounts.push(solana_program::instruction::AccountMeta::new( self.escrow, @@ -164,7 +164,7 @@ impl CaptureV1InstructionData { #[derive(Default)] pub struct CaptureV1Builder { owner: Option, - authority: Option, + authority: Option<(solana_program::pubkey::Pubkey, bool)>, escrow: Option, asset: Option, collection: Option, @@ -192,8 +192,12 @@ impl CaptureV1Builder { self } #[inline(always)] - pub fn authority(&mut self, authority: solana_program::pubkey::Pubkey) -> &mut Self { - self.authority = Some(authority); + pub fn authority( + &mut self, + authority: solana_program::pubkey::Pubkey, + as_signer: bool, + ) -> &mut Self { + self.authority = Some((authority, as_signer)); self } #[inline(always)] @@ -360,7 +364,7 @@ impl CaptureV1Builder { pub struct CaptureV1CpiAccounts<'a, 'b> { pub owner: &'b solana_program::account_info::AccountInfo<'a>, - pub authority: &'b solana_program::account_info::AccountInfo<'a>, + pub authority: (&'b solana_program::account_info::AccountInfo<'a>, bool), pub escrow: &'b solana_program::account_info::AccountInfo<'a>, @@ -398,7 +402,7 @@ pub struct CaptureV1Cpi<'a, 'b> { pub owner: &'b solana_program::account_info::AccountInfo<'a>, - pub authority: &'b solana_program::account_info::AccountInfo<'a>, + pub authority: (&'b solana_program::account_info::AccountInfo<'a>, bool), pub escrow: &'b solana_program::account_info::AccountInfo<'a>, @@ -493,8 +497,8 @@ impl<'a, 'b> CaptureV1Cpi<'a, 'b> { true, )); accounts.push(solana_program::instruction::AccountMeta::new( - *self.authority.key, - true, + *self.authority.0.key, + self.authority.1, )); accounts.push(solana_program::instruction::AccountMeta::new( *self.escrow.key, @@ -569,7 +573,7 @@ impl<'a, 'b> CaptureV1Cpi<'a, 'b> { let mut account_infos = Vec::with_capacity(16 + 1 + remaining_accounts.len()); account_infos.push(self.__program.clone()); account_infos.push(self.owner.clone()); - account_infos.push(self.authority.clone()); + account_infos.push(self.authority.0.clone()); account_infos.push(self.escrow.clone()); account_infos.push(self.asset.clone()); account_infos.push(self.collection.clone()); @@ -653,8 +657,9 @@ impl<'a, 'b> CaptureV1CpiBuilder<'a, 'b> { pub fn authority( &mut self, authority: &'b solana_program::account_info::AccountInfo<'a>, + as_signer: bool, ) -> &mut Self { - self.instruction.authority = Some(authority); + self.instruction.authority = Some((authority, as_signer)); self } #[inline(always)] @@ -876,7 +881,7 @@ impl<'a, 'b> CaptureV1CpiBuilder<'a, 'b> { struct CaptureV1CpiBuilderInstruction<'a, 'b> { __program: &'b solana_program::account_info::AccountInfo<'a>, owner: Option<&'b solana_program::account_info::AccountInfo<'a>>, - authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + authority: Option<(&'b solana_program::account_info::AccountInfo<'a>, bool)>, escrow: Option<&'b solana_program::account_info::AccountInfo<'a>>, asset: Option<&'b solana_program::account_info::AccountInfo<'a>>, collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, diff --git a/clients/rust/src/generated/instructions/release_v1.rs b/clients/rust/src/generated/instructions/release_v1.rs index 9f764a8..dbe072c 100644 --- a/clients/rust/src/generated/instructions/release_v1.rs +++ b/clients/rust/src/generated/instructions/release_v1.rs @@ -14,7 +14,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; pub struct ReleaseV1 { pub owner: solana_program::pubkey::Pubkey, - pub authority: solana_program::pubkey::Pubkey, + pub authority: (solana_program::pubkey::Pubkey, bool), pub escrow: solana_program::pubkey::Pubkey, @@ -59,8 +59,8 @@ impl ReleaseV1 { self.owner, true, )); accounts.push(solana_program::instruction::AccountMeta::new( - self.authority, - true, + self.authority.0, + self.authority.1, )); accounts.push(solana_program::instruction::AccountMeta::new( self.escrow, @@ -164,7 +164,7 @@ impl ReleaseV1InstructionData { #[derive(Default)] pub struct ReleaseV1Builder { owner: Option, - authority: Option, + authority: Option<(solana_program::pubkey::Pubkey, bool)>, escrow: Option, asset: Option, collection: Option, @@ -192,8 +192,12 @@ impl ReleaseV1Builder { self } #[inline(always)] - pub fn authority(&mut self, authority: solana_program::pubkey::Pubkey) -> &mut Self { - self.authority = Some(authority); + pub fn authority( + &mut self, + authority: solana_program::pubkey::Pubkey, + as_signer: bool, + ) -> &mut Self { + self.authority = Some((authority, as_signer)); self } #[inline(always)] @@ -360,7 +364,7 @@ impl ReleaseV1Builder { pub struct ReleaseV1CpiAccounts<'a, 'b> { pub owner: &'b solana_program::account_info::AccountInfo<'a>, - pub authority: &'b solana_program::account_info::AccountInfo<'a>, + pub authority: (&'b solana_program::account_info::AccountInfo<'a>, bool), pub escrow: &'b solana_program::account_info::AccountInfo<'a>, @@ -398,7 +402,7 @@ pub struct ReleaseV1Cpi<'a, 'b> { pub owner: &'b solana_program::account_info::AccountInfo<'a>, - pub authority: &'b solana_program::account_info::AccountInfo<'a>, + pub authority: (&'b solana_program::account_info::AccountInfo<'a>, bool), pub escrow: &'b solana_program::account_info::AccountInfo<'a>, @@ -493,8 +497,8 @@ impl<'a, 'b> ReleaseV1Cpi<'a, 'b> { true, )); accounts.push(solana_program::instruction::AccountMeta::new( - *self.authority.key, - true, + *self.authority.0.key, + self.authority.1, )); accounts.push(solana_program::instruction::AccountMeta::new( *self.escrow.key, @@ -569,7 +573,7 @@ impl<'a, 'b> ReleaseV1Cpi<'a, 'b> { let mut account_infos = Vec::with_capacity(16 + 1 + remaining_accounts.len()); account_infos.push(self.__program.clone()); account_infos.push(self.owner.clone()); - account_infos.push(self.authority.clone()); + account_infos.push(self.authority.0.clone()); account_infos.push(self.escrow.clone()); account_infos.push(self.asset.clone()); account_infos.push(self.collection.clone()); @@ -653,8 +657,9 @@ impl<'a, 'b> ReleaseV1CpiBuilder<'a, 'b> { pub fn authority( &mut self, authority: &'b solana_program::account_info::AccountInfo<'a>, + as_signer: bool, ) -> &mut Self { - self.instruction.authority = Some(authority); + self.instruction.authority = Some((authority, as_signer)); self } #[inline(always)] @@ -876,7 +881,7 @@ impl<'a, 'b> ReleaseV1CpiBuilder<'a, 'b> { struct ReleaseV1CpiBuilderInstruction<'a, 'b> { __program: &'b solana_program::account_info::AccountInfo<'a>, owner: Option<&'b solana_program::account_info::AccountInfo<'a>>, - authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + authority: Option<(&'b solana_program::account_info::AccountInfo<'a>, bool)>, escrow: Option<&'b solana_program::account_info::AccountInfo<'a>>, asset: Option<&'b solana_program::account_info::AccountInfo<'a>>, collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, diff --git a/configs/kinobi.cjs b/configs/kinobi.cjs index 1d92f6c..4e5b1db 100644 --- a/configs/kinobi.cjs +++ b/configs/kinobi.cjs @@ -36,6 +36,7 @@ kinobi.update( }, captureV1: { accounts: { + authority: { isSigner: 'either' }, feeTokenAccount: { defaultValue: ataPdaDefault("token", "feeProjectAccount") }, escrowTokenAccount: { defaultValue: ataPdaDefault("token", "escrow") }, userTokenAccount: { defaultValue: ataPdaDefault("token", "owner") }, @@ -49,6 +50,7 @@ kinobi.update( }, releaseV1: { accounts: { + authority: { isSigner: 'either' }, feeTokenAccount: { defaultValue: ataPdaDefault("token", "feeProjectAccount") }, escrowTokenAccount: { defaultValue: ataPdaDefault("token", "escrow") }, userTokenAccount: { defaultValue: ataPdaDefault("token", "owner") }, diff --git a/idls/mplHybrid.json b/idls/mplHybrid.json index ccf8726..620cd53 100644 --- a/idls/mplHybrid.json +++ b/idls/mplHybrid.json @@ -122,7 +122,7 @@ { "name": "authority", "isMut": true, - "isSigner": true + "isSigner": false }, { "name": "escrow", @@ -208,7 +208,7 @@ { "name": "authority", "isMut": true, - "isSigner": true + "isSigner": false }, { "name": "escrow", @@ -766,6 +766,11 @@ "code": 6011, "name": "NumericalOverflow", "msg": "Numerical Overflow" + }, + { + "code": 6012, + "name": "InvalidUpdateAuthority", + "msg": "Invalid Update Authority" } ], "metadata": { diff --git a/programs/mpl-hybrid/src/error.rs b/programs/mpl-hybrid/src/error.rs index 5d1605e..5972cc1 100644 --- a/programs/mpl-hybrid/src/error.rs +++ b/programs/mpl-hybrid/src/error.rs @@ -26,4 +26,6 @@ pub enum MplHybridError { InvalidMintAccount, #[msg("Numerical Overflow")] NumericalOverflow, + #[msg("Invalid Update Authority")] + InvalidUpdateAuthority, } diff --git a/programs/mpl-hybrid/src/instructions/capture.rs b/programs/mpl-hybrid/src/instructions/capture.rs index fcf41ab..91ff2d1 100644 --- a/programs/mpl-hybrid/src/instructions/capture.rs +++ b/programs/mpl-hybrid/src/instructions/capture.rs @@ -16,6 +16,7 @@ use mpl_core::instructions::{ TransferV1Cpi, TransferV1InstructionArgs, UpdateV1Cpi, UpdateV1InstructionArgs, }; use mpl_core::types::UpdateAuthority; +use mpl_utils::assert_signer; use solana_program::program::invoke; #[derive(Accounts)] @@ -23,8 +24,9 @@ pub struct CaptureV1Ctx<'info> { #[account(mut)] owner: Signer<'info>, + /// CHECK: Optional signer, which we check in the handler. #[account(mut)] - authority: Signer<'info>, + authority: AccountInfo<'info>, #[account( mut, @@ -128,6 +130,10 @@ pub fn handler_capture_v1(ctx: Context) -> Result<()> { return Err(MplHybridError::InvalidCollection.into()); } + if authority_info.key == &escrow.authority { + assert_signer(&ctx.accounts.authority)?; + } + //If the path has bit 0 set, we need to update the metadata onchain if Path::RerollMetadata.check(escrow.path) { let clock = Clock::get()?; @@ -171,8 +177,15 @@ pub fn handler_capture_v1(ctx: Context) -> Result<()> { }, }; - //invoke the update instruction - let _update_result = update_ix.invoke(); + if authority_info.key == &escrow.authority { + //invoke the update instruction + update_ix.invoke()?; + } else if authority_info.key == &escrow.key() { + // The auth has been delegated as the UpdateDelegate on the asset. + update_ix.invoke_signed(&[&[b"escrow", collection.key.as_ref(), &[escrow.bump]]])?; + } else { + return Err(MplHybridError::InvalidUpdateAuthority.into()); + } } //create transfer instruction diff --git a/programs/mpl-hybrid/src/instructions/release.rs b/programs/mpl-hybrid/src/instructions/release.rs index 5d811b0..9031cbd 100644 --- a/programs/mpl-hybrid/src/instructions/release.rs +++ b/programs/mpl-hybrid/src/instructions/release.rs @@ -15,6 +15,7 @@ use mpl_core::instructions::{ TransferV1Cpi, TransferV1InstructionArgs, UpdateV1Cpi, UpdateV1InstructionArgs, }; use mpl_core::types::UpdateAuthority; +use mpl_utils::assert_signer; use solana_program::program::invoke; #[derive(Accounts)] @@ -22,8 +23,9 @@ pub struct ReleaseV1Ctx<'info> { #[account(mut)] owner: Signer<'info>, + /// CHECK: Optional signer, which we check in the handler. #[account(mut)] - authority: Signer<'info>, + authority: AccountInfo<'info>, #[account( mut, @@ -128,6 +130,10 @@ pub fn handler_release_v1(ctx: Context) -> Result<()> { return Err(MplHybridError::InvalidCollection.into()); } + if authority_info.key == &escrow.authority { + assert_signer(&ctx.accounts.authority)?; + } + //If the path has bit 0 set, we need to update the metadata onchain if Path::RerollMetadata.check(escrow.path) { //construct the captured uri @@ -154,8 +160,15 @@ pub fn handler_release_v1(ctx: Context) -> Result<()> { }, }; - //invoke the update instruction - let _update_result = update_ix.invoke(); + if authority_info.key == &escrow.authority { + //invoke the update instruction + update_ix.invoke()?; + } else if authority_info.key == &escrow.key() { + // The auth has been delegated as the UpdateDelegate on the asset. + update_ix.invoke_signed(&[&[b"escrow", collection.key.as_ref(), &[escrow.bump]]])?; + } else { + return Err(MplHybridError::InvalidUpdateAuthority.into()); + } } //create transfer instruction