Skip to content

Commit

Permalink
Token Metadata v1.12 -- Initial Fees (#1106)
Browse files Browse the repository at this point in the history
* add fee fee collection and create fees

* add lock file

* add fee tests

* fix RefCell borrow panic; change fee flag value

* update to leave fee accounts open when burning

* regen JS API & update test setup

* remove bitflags

* fix JS burn tests

* handle cases where fee amounts are less than min rent exempt

* address PR feedback; regen JS API

* add test for burned metadata account with fees stored

* remove unwrap

* clean up burn JS test to address PR feedback

* refactor to only have Create fee

* revert non-create fees account additions

* refactor close-program-account

* regen JS API

* update shank annotations and regen JS API

* remove sysvar instructions in txs-init

* remove extra accounts

* regen JS API

* address Febo's review changes

* address Michael's PR feedback

* fix final issue
  • Loading branch information
samuelvanderwaal authored May 31, 2023
1 parent e4df367 commit e462a01
Show file tree
Hide file tree
Showing 45 changed files with 808 additions and 201 deletions.
44 changes: 44 additions & 0 deletions token-metadata/js/idl/mpl_token_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,13 @@
"isMut": false,
"isSigner": false,
"desc": "MasterEdition2 Account of the Collection Token"
},
{
"name": "collectionAuthorityRecord",
"isMut": false,
"isSigner": false,
"desc": "Collection Authority Record PDA",
"optional": true
}
],
"args": [],
Expand Down Expand Up @@ -3556,6 +3563,28 @@
"type": "u8",
"value": 53
}
},
{
"name": "Collect",
"accounts": [
{
"name": "authority",
"isMut": false,
"isSigner": true,
"desc": "Authority to collect fees"
},
{
"name": "pdaAccount",
"isMut": false,
"isSigner": false,
"desc": "PDA to retrieve fees from"
}
],
"args": [],
"discriminant": {
"type": "u8",
"value": 54
}
}
],
"accounts": [
Expand Down Expand Up @@ -6573,6 +6602,21 @@
"code": 189,
"name": "InvalidInstruction",
"msg": "Invalid or removed instruction"
},
{
"code": 190,
"name": "MissingDelegateRecord",
"msg": "Missing delegate record"
},
{
"code": 191,
"name": "InvalidFeeAccount",
"msg": ""
},
{
"code": 192,
"name": "InvalidMetadataFlags",
"msg": ""
}
],
"metadata": {
Expand Down
60 changes: 60 additions & 0 deletions token-metadata/js/src/generated/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4069,6 +4069,66 @@ export class InvalidInstructionError extends Error {
createErrorFromCodeLookup.set(0xbd, () => new InvalidInstructionError());
createErrorFromNameLookup.set('InvalidInstruction', () => new InvalidInstructionError());

/**
* MissingDelegateRecord: 'Missing delegate record'
*
* @category Errors
* @category generated
*/
export class MissingDelegateRecordError extends Error {
readonly code: number = 0xbe;
readonly name: string = 'MissingDelegateRecord';
constructor() {
super('Missing delegate record');
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, MissingDelegateRecordError);
}
}
}

createErrorFromCodeLookup.set(0xbe, () => new MissingDelegateRecordError());
createErrorFromNameLookup.set('MissingDelegateRecord', () => new MissingDelegateRecordError());

/**
* InvalidFeeAccount: ''
*
* @category Errors
* @category generated
*/
export class InvalidFeeAccountError extends Error {
readonly code: number = 0xbf;
readonly name: string = 'InvalidFeeAccount';
constructor() {
super('');
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, InvalidFeeAccountError);
}
}
}

createErrorFromCodeLookup.set(0xbf, () => new InvalidFeeAccountError());
createErrorFromNameLookup.set('InvalidFeeAccount', () => new InvalidFeeAccountError());

/**
* InvalidMetadataFlags: ''
*
* @category Errors
* @category generated
*/
export class InvalidMetadataFlagsError extends Error {
readonly code: number = 0xc0;
readonly name: string = 'InvalidMetadataFlags';
constructor() {
super('');
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, InvalidMetadataFlagsError);
}
}
}

createErrorFromCodeLookup.set(0xc0, () => new InvalidMetadataFlagsError());
createErrorFromNameLookup.set('InvalidMetadataFlags', () => new InvalidMetadataFlagsError());

/**
* Attempts to resolve a custom program error from the provided error code.
* @category Errors
Expand Down
70 changes: 70 additions & 0 deletions token-metadata/js/src/generated/instructions/Collect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* This code was GENERATED using the solita package.
* Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality.
*
* See: https://github.com/metaplex-foundation/solita
*/

import * as beet from '@metaplex-foundation/beet';
import * as web3 from '@solana/web3.js';

/**
* @category Instructions
* @category Collect
* @category generated
*/
export const CollectStruct = new beet.BeetArgsStruct<{ instructionDiscriminator: number }>(
[['instructionDiscriminator', beet.u8]],
'CollectInstructionArgs',
);
/**
* Accounts required by the _Collect_ instruction
*
* @property [**signer**] authority Authority to collect fees
* @property [] pdaAccount PDA to retrieve fees from
* @category Instructions
* @category Collect
* @category generated
*/
export type CollectInstructionAccounts = {
authority: web3.PublicKey;
pdaAccount: web3.PublicKey;
};

export const collectInstructionDiscriminator = 54;

/**
* Creates a _Collect_ instruction.
*
* @param accounts that will be accessed while the instruction is processed
* @category Instructions
* @category Collect
* @category generated
*/
export function createCollectInstruction(
accounts: CollectInstructionAccounts,
programId = new web3.PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'),
) {
const [data] = CollectStruct.serialize({
instructionDiscriminator: collectInstructionDiscriminator,
});
const keys: web3.AccountMeta[] = [
{
pubkey: accounts.authority,
isWritable: false,
isSigner: true,
},
{
pubkey: accounts.pdaAccount,
isWritable: false,
isSigner: false,
},
];

const ix = new web3.TransactionInstruction({
programId,
keys,
data,
});
return ix;
}
15 changes: 15 additions & 0 deletions token-metadata/js/src/generated/instructions/VerifyCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const VerifyCollectionStruct = new beet.BeetArgsStruct<{ instructionDiscr
* @property [] collectionMint Mint of the Collection
* @property [] collection Metadata Account of the Collection
* @property [] collectionMasterEditionAccount MasterEdition2 Account of the Collection Token
* @property [] collectionAuthorityRecord (optional) Collection Authority Record PDA
* @category Instructions
* @category VerifyCollection
* @category generated
Expand All @@ -37,13 +38,19 @@ export type VerifyCollectionInstructionAccounts = {
collectionMint: web3.PublicKey;
collection: web3.PublicKey;
collectionMasterEditionAccount: web3.PublicKey;
collectionAuthorityRecord?: web3.PublicKey;
};

export const verifyCollectionInstructionDiscriminator = 18;

/**
* Creates a _VerifyCollection_ instruction.
*
* Optional accounts that are not provided will be omitted from the accounts
* array passed with the instruction.
* An optional account that is set cannot follow an optional account that is unset.
* Otherwise an Error is raised.
*
* @param accounts that will be accessed while the instruction is processed
* @category Instructions
* @category VerifyCollection
Expand Down Expand Up @@ -89,6 +96,14 @@ export function createVerifyCollectionInstruction(
},
];

if (accounts.collectionAuthorityRecord != null) {
keys.push({
pubkey: accounts.collectionAuthorityRecord,
isWritable: false,
isSigner: false,
});
}

const ix = new web3.TransactionInstruction({
programId,
keys,
Expand Down
1 change: 1 addition & 0 deletions token-metadata/js/src/generated/instructions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './Burn';
export * from './BurnEditionNft';
export * from './BurnNft';
export * from './CloseEscrowAccount';
export * from './Collect';
export * from './ConvertMasterEditionV1ToV2';
export * from './Create';
export * from './CreateEscrowAccount';
Expand Down
8 changes: 4 additions & 4 deletions token-metadata/js/test/burn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getAccount, TOKEN_PROGRAM_ID } from '@solana/spl-token';

killStuckProcess();

test('Burn: NonFungible asset', async (t) => {
test.only('Burn: NonFungible asset', async (t) => {
const API = new InitTransactions();
const { fstTxHandler: handler, payerPair: payer, connection } = await API.payer();

Expand Down Expand Up @@ -37,12 +37,12 @@ test('Burn: NonFungible asset', async (t) => {

await updateTx.assertSuccess(t);

// All three accounts are closed.
// All three accounts are closed. Metadata account should have a data length of 0 but may be open if it contains fees.
const metadataAccount = await connection.getAccountInfo(metadata);
const editionAccount = await connection.getAccountInfo(masterEdition);
const tokenAccount = await connection.getAccountInfo(token);

t.equal(metadataAccount, null);
t?.equal(metadataAccount.data.length, 0);
t.equal(editionAccount, null);
t.equal(tokenAccount, null);
});
Expand Down Expand Up @@ -85,7 +85,7 @@ test('Burn: ProgrammableNonFungible asset', async (t) => {
const tokenAccount = await connection.getAccountInfo(token);
const tokenRecordAccount = await connection.getAccountInfo(tokenRecord);

t.equal(metadataAccount, null);
t?.equal(metadataAccount.data.length, 0);
t.equal(editionAccount, null);
t.equal(tokenAccount, null);
t.equal(tokenRecordAccount, null);
Expand Down
8 changes: 8 additions & 0 deletions token-metadata/program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,14 @@ pub enum MetadataError {
/// 190
#[error("Missing delegate record")]
MissingDelegateRecord,

/// 191
#[error("")]
InvalidFeeAccount,

/// 192
#[error("")]
InvalidMetadataFlags,
}

impl PrintProgramError for MetadataError {
Expand Down
3 changes: 2 additions & 1 deletion token-metadata/program/src/instruction/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
system_program,
};

use crate::instruction::MetadataInstruction;
Expand Down Expand Up @@ -41,7 +42,7 @@ pub fn approve_collection_authority(
AccountMeta::new(payer, true),
AccountMeta::new_readonly(metadata, false),
AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
],
data: MetadataInstruction::ApproveCollectionAuthority
.try_to_vec()
Expand Down
5 changes: 3 additions & 2 deletions token-metadata/program/src/instruction/edition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
system_program,
};

use crate::{
Expand Down Expand Up @@ -60,7 +61,7 @@ pub fn create_master_edition_v3(
AccountMeta::new(payer, true),
AccountMeta::new(metadata, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
];

Instruction {
Expand Down Expand Up @@ -122,7 +123,7 @@ pub fn mint_new_edition_from_master_edition_via_token(
AccountMeta::new_readonly(new_metadata_update_authority, false),
AccountMeta::new_readonly(metadata, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
];

Instruction {
Expand Down
24 changes: 24 additions & 0 deletions token-metadata/program/src/instruction/fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};

use crate::state::fee::FEE_AUTHORITY;

use super::*;

pub fn collect_fees(recipient: Pubkey, fee_accounts: Vec<Pubkey>) -> Instruction {
let mut accounts = vec![
AccountMeta::new(FEE_AUTHORITY, true),
AccountMeta::new(recipient, false),
];

for fee_account in fee_accounts {
accounts.push(AccountMeta::new(fee_account, false));
}
Instruction {
program_id: crate::ID,
accounts,
data: MetadataInstruction::Collect.try_to_vec().unwrap(),
}
}
3 changes: 2 additions & 1 deletion token-metadata/program/src/instruction/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
system_program,
};
#[cfg(feature = "serde-feature")]
use {
Expand Down Expand Up @@ -481,7 +482,7 @@ pub fn create_metadata_accounts_v3(
AccountMeta::new_readonly(mint_authority, true),
AccountMeta::new(payer, true),
AccountMeta::new_readonly(update_authority, update_authority_is_signer),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
],
data: MetadataInstruction::CreateMetadataAccountV3(CreateMetadataAccountArgsV3 {
data: DataV2 {
Expand Down
Loading

0 comments on commit e462a01

Please sign in to comment.