From eb312662b0314b7043775b7c034298c76080cb52 Mon Sep 17 00:00:00 2001 From: blockiosaurus <90809591+blockiosaurus@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:24:28 -0700 Subject: [PATCH] Adding an extra account to act as a signing authority instead of the payer. (#95) --- .../js/src/generated/instructions/printV2.ts | 21 ++++- clients/js/test/printV2.test.ts | 88 +++++++++++++++++++ .../src/generated/instructions/print_v2.rs | 72 +++++++++++++-- configs/kinobi.cjs | 16 +++- .../program/src/instruction/mod.rs | 1 + ...w_edition_from_master_edition_via_token.rs | 1 + .../program/src/processor/metadata/print.rs | 22 ++++- .../program/src/utils/master_edition.rs | 12 ++- 8 files changed, 215 insertions(+), 18 deletions(-) diff --git a/clients/js/src/generated/instructions/printV2.ts b/clients/js/src/generated/instructions/printV2.ts index 93a03869..a83ec2b3 100644 --- a/clients/js/src/generated/instructions/printV2.ts +++ b/clients/js/src/generated/instructions/printV2.ts @@ -77,8 +77,10 @@ export type PrintV2InstructionAccounts = { sysvarInstructions?: PublicKey | Pda; /** System program */ systemProgram?: PublicKey | Pda; - /** The Delegate Record authorizing escrowless edition printing. */ + /** The Delegate Record authorizing escrowless edition printing */ holderDelegateRecord?: PublicKey | Pda; + /** The authority printing the edition for a delegated print */ + delegate?: Signer; }; // Data. @@ -225,6 +227,11 @@ export function printV2( isWritable: false as boolean, value: input.holderDelegateRecord ?? null, }, + delegate: { + index: 19, + isWritable: false as boolean, + value: input.delegate ?? null, + }, } satisfies ResolvedAccountsWithIndices; // Arguments. @@ -259,9 +266,15 @@ export function printV2( } if (!resolvedAccounts.editionMintAuthority.value) { if (resolvedAccounts.holderDelegateRecord.value) { - resolvedAccounts.editionMintAuthority.value = expectSome( - resolvedAccounts.payer.value - ); + if (resolvedAccounts.delegate.value) { + resolvedAccounts.editionMintAuthority.value = expectSome( + resolvedAccounts.delegate.value + ); + } else { + resolvedAccounts.editionMintAuthority.value = expectSome( + resolvedAccounts.payer.value + ); + } } else { resolvedAccounts.editionMintAuthority.value = context.identity; } diff --git a/clients/js/test/printV2.test.ts b/clients/js/test/printV2.test.ts index 561eee5a..925cd150 100644 --- a/clients/js/test/printV2.test.ts +++ b/clients/js/test/printV2.test.ts @@ -569,3 +569,91 @@ test('it can still print as the master edition holder even after delegating', as }, }); }); + +test('it can delegate the authority to print a new edition with a separate payer', async (t) => { + // Given an existing master edition asset. + const umi = await createUmi(); + const originalOwner = generateSigner(umi); + const payer = generateSigner(umi); + const delegate = generateSigner(umi); + umi.rpc.airdrop(payer.publicKey, sol(1)); + const originalMint = await createDigitalAssetWithToken(umi, { + name: 'My NFT', + symbol: 'MNFT', + uri: 'https://example.com/nft.json', + sellerFeeBasisPoints: percentAmount(5.42), + tokenOwner: originalOwner.publicKey, + printSupply: printSupply('Limited', [10]), + tokenStandard: TokenStandard.NonFungible, + }); + + const holderDelegateRecord = findHolderDelegateRecordPda(umi, { + mint: originalMint.publicKey, + delegateRole: 'print_delegate', + owner: originalOwner.publicKey, + delegate: delegate.publicKey, + }); + + const digitalAssetWithToken = await fetchDigitalAssetWithAssociatedToken( + umi, + originalMint.publicKey, + originalOwner.publicKey + ); + + await delegatePrintDelegateV1(umi, { + delegate: delegate.publicKey, + mint: originalMint.publicKey, + tokenStandard: TokenStandard.NonFungible, + token: digitalAssetWithToken.token.publicKey, + authority: originalOwner, + delegateRecord: holderDelegateRecord[0], + }).sendAndConfirm(umi); + + // When the delegate prints a new edition of the asset. + const editionMint = generateSigner(umi); + const editionOwner = generateSigner(umi); + + await printV2(umi, { + masterTokenAccountOwner: originalOwner.publicKey, + masterEditionMint: originalMint.publicKey, + editionMint, + editionTokenAccountOwner: editionOwner.publicKey, + editionNumber: 1, + tokenStandard: TokenStandard.NonFungible, + masterTokenAccount: digitalAssetWithToken.token.publicKey, + payer, + holderDelegateRecord: holderDelegateRecord[0], + delegate, + }).sendAndConfirm(umi); + + // Then the original NFT was updated. + const originalAsset = await fetchDigitalAsset(umi, originalMint.publicKey); + t.like(originalAsset, { + edition: { supply: 1n, maxSupply: some(10n) }, + }); + + // And the printed NFT was created with the same data. + const editionAsset = await fetchDigitalAssetWithAssociatedToken( + umi, + editionMint.publicKey, + editionOwner.publicKey + ); + t.like(editionAsset, { + publicKey: editionMint.publicKey, + metadata: { + name: 'My NFT', + symbol: 'MNFT', + uri: 'https://example.com/nft.json', + sellerFeeBasisPoints: 542, + }, + token: { + owner: editionOwner.publicKey, + amount: 1n, + }, + edition: { + isOriginal: false, + parent: findMasterEditionPda(umi, { mint: originalMint.publicKey })[0], + edition: 1n, + }, + }); +}); diff --git a/clients/rust/src/generated/instructions/print_v2.rs b/clients/rust/src/generated/instructions/print_v2.rs index b98eeb94..c009431c 100644 --- a/clients/rust/src/generated/instructions/print_v2.rs +++ b/clients/rust/src/generated/instructions/print_v2.rs @@ -46,8 +46,10 @@ pub struct PrintV2 { pub sysvar_instructions: solana_program::pubkey::Pubkey, /// System program pub system_program: solana_program::pubkey::Pubkey, - /// The Delegate Record authorizing escrowless edition printing. + /// The Delegate Record authorizing escrowless edition printing pub holder_delegate_record: Option, + /// The authority printing the edition for a delegated print + pub delegate: Option, } impl PrintV2 { @@ -63,7 +65,7 @@ impl PrintV2 { args: PrintV2InstructionArgs, remaining_accounts: &[solana_program::instruction::AccountMeta], ) -> solana_program::instruction::Instruction { - let mut accounts = Vec::with_capacity(19 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(20 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( self.edition_metadata, false, @@ -153,6 +155,16 @@ impl PrintV2 { false, )); } + if let Some(delegate) = self.delegate { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + delegate, true, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_TOKEN_METADATA_ID, + false, + )); + } accounts.extend_from_slice(remaining_accounts); let mut data = PrintV2InstructionData::new().try_to_vec().unwrap(); let mut args = args.try_to_vec().unwrap(); @@ -210,6 +222,7 @@ pub struct PrintV2InstructionArgs { /// 16. `[optional]` sysvar_instructions (default to `Sysvar1nstructions1111111111111111111111111`) /// 17. `[optional]` system_program (default to `11111111111111111111111111111111`) /// 18. `[optional]` holder_delegate_record +/// 19. `[signer, optional]` delegate #[derive(Default)] pub struct PrintV2Builder { edition_metadata: Option, @@ -231,6 +244,7 @@ pub struct PrintV2Builder { sysvar_instructions: Option, system_program: Option, holder_delegate_record: Option, + delegate: Option, edition_number: Option, __remaining_accounts: Vec, } @@ -397,7 +411,7 @@ impl PrintV2Builder { self } /// `[optional account]` - /// The Delegate Record authorizing escrowless edition printing. + /// The Delegate Record authorizing escrowless edition printing #[inline(always)] pub fn holder_delegate_record( &mut self, @@ -406,6 +420,13 @@ impl PrintV2Builder { self.holder_delegate_record = holder_delegate_record; self } + /// `[optional account]` + /// The authority printing the edition for a delegated print + #[inline(always)] + pub fn delegate(&mut self, delegate: Option) -> &mut Self { + self.delegate = delegate; + self + } #[inline(always)] pub fn edition_number(&mut self, edition_number: u64) -> &mut Self { self.edition_number = Some(edition_number); @@ -471,6 +492,7 @@ impl PrintV2Builder { .system_program .unwrap_or(solana_program::pubkey!("11111111111111111111111111111111")), holder_delegate_record: self.holder_delegate_record, + delegate: self.delegate, }; let args = PrintV2InstructionArgs { edition_number: self @@ -521,8 +543,10 @@ pub struct PrintV2CpiAccounts<'a, 'b> { pub sysvar_instructions: &'b solana_program::account_info::AccountInfo<'a>, /// System program pub system_program: &'b solana_program::account_info::AccountInfo<'a>, - /// The Delegate Record authorizing escrowless edition printing. + /// The Delegate Record authorizing escrowless edition printing pub holder_delegate_record: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The authority printing the edition for a delegated print + pub delegate: Option<&'b solana_program::account_info::AccountInfo<'a>>, } /// `print_v2` CPI instruction. @@ -565,8 +589,10 @@ pub struct PrintV2Cpi<'a, 'b> { pub sysvar_instructions: &'b solana_program::account_info::AccountInfo<'a>, /// System program pub system_program: &'b solana_program::account_info::AccountInfo<'a>, - /// The Delegate Record authorizing escrowless edition printing. + /// The Delegate Record authorizing escrowless edition printing pub holder_delegate_record: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// The authority printing the edition for a delegated print + pub delegate: Option<&'b solana_program::account_info::AccountInfo<'a>>, /// The arguments for the instruction. pub __args: PrintV2InstructionArgs, } @@ -598,6 +624,7 @@ impl<'a, 'b> PrintV2Cpi<'a, 'b> { sysvar_instructions: accounts.sysvar_instructions, system_program: accounts.system_program, holder_delegate_record: accounts.holder_delegate_record, + delegate: accounts.delegate, __args: args, } } @@ -634,7 +661,7 @@ impl<'a, 'b> PrintV2Cpi<'a, 'b> { bool, )], ) -> solana_program::entrypoint::ProgramResult { - let mut accounts = Vec::with_capacity(19 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(20 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( *self.edition_metadata.key, false, @@ -725,6 +752,17 @@ impl<'a, 'b> PrintV2Cpi<'a, 'b> { false, )); } + if let Some(delegate) = self.delegate { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *delegate.key, + true, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_TOKEN_METADATA_ID, + false, + )); + } remaining_accounts.iter().for_each(|remaining_account| { accounts.push(solana_program::instruction::AccountMeta { pubkey: *remaining_account.0.key, @@ -741,7 +779,7 @@ impl<'a, 'b> PrintV2Cpi<'a, 'b> { accounts, data, }; - let mut account_infos = Vec::with_capacity(19 + 1 + remaining_accounts.len()); + let mut account_infos = Vec::with_capacity(20 + 1 + remaining_accounts.len()); account_infos.push(self.__program.clone()); account_infos.push(self.edition_metadata.clone()); account_infos.push(self.edition.clone()); @@ -766,6 +804,9 @@ impl<'a, 'b> PrintV2Cpi<'a, 'b> { if let Some(holder_delegate_record) = self.holder_delegate_record { account_infos.push(holder_delegate_record.clone()); } + if let Some(delegate) = self.delegate { + account_infos.push(delegate.clone()); + } remaining_accounts .iter() .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); @@ -801,6 +842,7 @@ impl<'a, 'b> PrintV2Cpi<'a, 'b> { /// 16. `[]` sysvar_instructions /// 17. `[]` system_program /// 18. `[optional]` holder_delegate_record +/// 19. `[signer, optional]` delegate pub struct PrintV2CpiBuilder<'a, 'b> { instruction: Box>, } @@ -828,6 +870,7 @@ impl<'a, 'b> PrintV2CpiBuilder<'a, 'b> { sysvar_instructions: None, system_program: None, holder_delegate_record: None, + delegate: None, edition_number: None, __remaining_accounts: Vec::new(), }); @@ -996,7 +1039,7 @@ impl<'a, 'b> PrintV2CpiBuilder<'a, 'b> { self } /// `[optional account]` - /// The Delegate Record authorizing escrowless edition printing. + /// The Delegate Record authorizing escrowless edition printing #[inline(always)] pub fn holder_delegate_record( &mut self, @@ -1005,6 +1048,16 @@ impl<'a, 'b> PrintV2CpiBuilder<'a, 'b> { self.instruction.holder_delegate_record = holder_delegate_record; self } + /// `[optional account]` + /// The authority printing the edition for a delegated print + #[inline(always)] + pub fn delegate( + &mut self, + delegate: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.delegate = delegate; + self + } #[inline(always)] pub fn edition_number(&mut self, edition_number: u64) -> &mut Self { self.instruction.edition_number = Some(edition_number); @@ -1143,6 +1196,8 @@ impl<'a, 'b> PrintV2CpiBuilder<'a, 'b> { .expect("system_program is not set"), holder_delegate_record: self.instruction.holder_delegate_record, + + delegate: self.instruction.delegate, __args: args, }; instruction.invoke_signed_with_remaining_accounts( @@ -1173,6 +1228,7 @@ struct PrintV2CpiBuilderInstruction<'a, 'b> { sysvar_instructions: Option<&'b solana_program::account_info::AccountInfo<'a>>, system_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, holder_delegate_record: Option<&'b solana_program::account_info::AccountInfo<'a>>, + delegate: Option<&'b solana_program::account_info::AccountInfo<'a>>, edition_number: Option, /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. __remaining_accounts: Vec<( diff --git a/configs/kinobi.cjs b/configs/kinobi.cjs index 4d3c254d..fe063640 100755 --- a/configs/kinobi.cjs +++ b/configs/kinobi.cjs @@ -694,8 +694,15 @@ kinobi.update( k.instructionAccountNode({ name: "holderDelegateRecord", isOptional: true, - docs: ["The Delegate Record authorizing escrowless edition printing."], - })], + docs: ["The Delegate Record authorizing escrowless edition printing"], + }), + k.instructionAccountNode({ + name: "delegate", + isOptional: true, + isSigner: true, + docs: ["The authority printing the edition for a delegated print"], + }) + ], }); }, }, @@ -921,7 +928,10 @@ kinobi.update( }, editionMintAuthority: { defaultsTo: k.conditionalDefault("account", "holderDelegateRecord", { - ifTrue: k.accountDefault("payer"), + ifTrue: k.conditionalDefault("account", "delegate", { + ifTrue: k.accountDefault("delegate"), + ifFalse: k.accountDefault("payer"), + }), ifFalse: k.identityDefault(), }), }, diff --git a/programs/token-metadata/program/src/instruction/mod.rs b/programs/token-metadata/program/src/instruction/mod.rs index 5bc6d57f..cc46c7b7 100644 --- a/programs/token-metadata/program/src/instruction/mod.rs +++ b/programs/token-metadata/program/src/instruction/mod.rs @@ -825,6 +825,7 @@ pub enum MetadataInstruction { #[account(16, name="sysvar_instructions", desc="Instructions sysvar account")] #[account(17, name="system_program", desc="System program")] // #[account(18, optional, name="holder_delegate_record", desc="The Delegate Record authorizing escrowless edition printing")] + // #[account(19, optional, signer, name="delegate", desc="The authority printing the edition for a delegated print")] #[args(initialize_mint: bool)] Print(PrintArgs), } diff --git a/programs/token-metadata/program/src/processor/edition/mint_new_edition_from_master_edition_via_token.rs b/programs/token-metadata/program/src/processor/edition/mint_new_edition_from_master_edition_via_token.rs index baa30355..ef5734dc 100644 --- a/programs/token-metadata/program/src/processor/edition/mint_new_edition_from_master_edition_via_token.rs +++ b/programs/token-metadata/program/src/processor/edition/mint_new_edition_from_master_edition_via_token.rs @@ -60,6 +60,7 @@ pub fn process_mint_new_edition_from_master_edition_via_token<'a>( token_program_account_info, system_account_info, holder_delegate_record_info: None, + delegate_info: None, }, edition, )?; diff --git a/programs/token-metadata/program/src/processor/metadata/print.rs b/programs/token-metadata/program/src/processor/metadata/print.rs index 3a5a6b1c..fa307e17 100644 --- a/programs/token-metadata/program/src/processor/metadata/print.rs +++ b/programs/token-metadata/program/src/processor/metadata/print.rs @@ -39,7 +39,7 @@ pub fn print_v1<'a>( ) -> ProgramResult { let context = Print::to_context(accounts)?; - print_logic(program_id, context, args, None) + print_logic(program_id, context, args, None, None) } pub fn print_v2<'a>( @@ -49,13 +49,29 @@ pub fn print_v2<'a>( ) -> ProgramResult { let context = Print::to_context(&accounts[0..18])?; + if accounts.len() < 19 { + return Err(ProgramError::NotEnoughAccountKeys); + } + let holder_delegate_record_info = if accounts[18].key == &crate::ID { None } else { Some(&accounts[18]) }; - print_logic(program_id, context, args, holder_delegate_record_info) + let delegate_info = if accounts.len() < 20 || accounts[19].key == &crate::ID { + None + } else { + Some(&accounts[19]) + }; + + print_logic( + program_id, + context, + args, + holder_delegate_record_info, + delegate_info, + ) } fn print_logic<'a>( @@ -63,6 +79,7 @@ fn print_logic<'a>( ctx: Context>, args: PrintArgs, holder_delegate_record_info: Option<&'a AccountInfo<'a>>, + delegate_info: Option<&'a AccountInfo<'a>>, ) -> ProgramResult { // Get the args for the instruction let edition = match args { @@ -267,6 +284,7 @@ fn print_logic<'a>( token_program_account_info: token_program, system_account_info: system_program, holder_delegate_record_info, + delegate_info, }, edition, )?; diff --git a/programs/token-metadata/program/src/utils/master_edition.rs b/programs/token-metadata/program/src/utils/master_edition.rs index dec73ffb..eeb64989 100644 --- a/programs/token-metadata/program/src/utils/master_edition.rs +++ b/programs/token-metadata/program/src/utils/master_edition.rs @@ -42,6 +42,7 @@ pub struct MintNewEditionFromMasterEditionViaTokenLogicArgs<'a> { pub token_program_account_info: &'a AccountInfo<'a>, pub system_account_info: &'a AccountInfo<'a>, pub holder_delegate_record_info: Option<&'a AccountInfo<'a>>, + pub delegate_info: Option<&'a AccountInfo<'a>>, } pub fn process_mint_new_edition_from_master_edition_via_token_logic<'a>( @@ -64,6 +65,7 @@ pub fn process_mint_new_edition_from_master_edition_via_token_logic<'a>( token_program_account_info, system_account_info, holder_delegate_record_info, + delegate_info, } = accounts; assert_token_program_matches_package(token_program_account_info)?; @@ -82,6 +84,14 @@ pub fn process_mint_new_edition_from_master_edition_via_token_logic<'a>( match holder_delegate_record_info { Some(delegate_record_info) => { + let delegate_authority = match delegate_info { + Some(delegate) => { + assert_signer(delegate)?; + Ok::<&Pubkey, ProgramError>(delegate.key) + } + None => Ok(payer_account_info.key), + }?; + assert_owned_by(delegate_record_info, &crate::ID)?; let role = HolderDelegateRole::PrintDelegate.to_string(); let seeds = vec![ @@ -90,7 +100,7 @@ pub fn process_mint_new_edition_from_master_edition_via_token_logic<'a>( master_metadata.mint.as_ref(), role.as_bytes(), owner_account_info.key.as_ref(), - payer_account_info.key.as_ref(), + delegate_authority.as_ref(), ]; assert_derivation(program_id, delegate_record_info, &seeds)?; Ok(())