From d2e0547296da1afaeb9d8ecf3f500b274a009121 Mon Sep 17 00:00:00 2001 From: Flappy <85501841+flappy-07@users.noreply.github.com> Date: Tue, 10 Aug 2021 18:49:00 +0530 Subject: [PATCH 1/5] Secondary sales flickering (#202) * Resales tab added * updated review changes * secondary sales flickering updated Co-authored-by: Jordan Prince --- js/packages/web/src/views/home/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/packages/web/src/views/home/index.tsx b/js/packages/web/src/views/home/index.tsx index 1b5dc19..d6a3235 100644 --- a/js/packages/web/src/views/home/index.tsx +++ b/js/packages/web/src/views/home/index.tsx @@ -57,7 +57,9 @@ export const HomeView = () => { if (flag == 1) return true; else return false; }; - const resaleAuctions = auctions.filter(m => checkPrimarySale(m) == true); + const resaleAuctions = auctions + .sort((a, b) => a.auction.info.endedAt?.sub(b.auction.info.endedAt || new BN(0)).toNumber() || 0) + .filter(m => checkPrimarySale(m) == true); // Removed resales from live auctions const liveAuctions = auctions @@ -242,4 +244,4 @@ export const HomeView = () => { ); -}; +}; \ No newline at end of file From 57f42d128eaeb50650694bf4bd4421ead4ff91d7 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 10 Aug 2021 08:42:27 -0500 Subject: [PATCH 2/5] solflare wallet adapter (#199) --- js/packages/common/src/contexts/wallet.tsx | 8 +- .../src/wallet-adapters/solflare/index.tsx | 112 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 js/packages/common/src/wallet-adapters/solflare/index.tsx diff --git a/js/packages/common/src/contexts/wallet.tsx b/js/packages/common/src/contexts/wallet.tsx index 2d3553e..9282f46 100644 --- a/js/packages/common/src/contexts/wallet.tsx +++ b/js/packages/common/src/contexts/wallet.tsx @@ -15,8 +15,8 @@ import { useLocalStorageState } from '../utils/utils'; import { PhantomWalletAdapter } from '../wallet-adapters/phantom'; import { useLocation } from 'react-router'; import { MetaplexModal } from '../components/MetaplexModal'; - import { TorusWalletAdapter } from '../wallet-adapters/torus'; +import { SolflareWalletAdapter } from '../wallet-adapters/solflare'; const ASSETS_URL = 'https://raw.githubusercontent.com/solana-labs/oyster/main/assets/wallets/'; @@ -27,6 +27,12 @@ export const WALLET_PROVIDERS = [ icon: `https://www.phantom.app/img/logo.png`, adapter: PhantomWalletAdapter, }, + { + name: 'Solflare', + url: 'https://solflare.com', + icon: `${ASSETS_URL}solflare.svg`, + adapter: SolflareWalletAdapter, + }, { name: 'Sollet', url: 'https://www.sollet.io', diff --git a/js/packages/common/src/wallet-adapters/solflare/index.tsx b/js/packages/common/src/wallet-adapters/solflare/index.tsx new file mode 100644 index 0000000..018bfab --- /dev/null +++ b/js/packages/common/src/wallet-adapters/solflare/index.tsx @@ -0,0 +1,112 @@ +import EventEmitter from 'eventemitter3'; +import { PublicKey, Transaction } from '@solana/web3.js'; +import { notify } from '../../utils/notifications'; +import { WalletAdapter } from '@solana/wallet-base'; + +interface SolflareWalletEvents { + connect: (...args: unknown[]) => unknown; + disconnect: (...args: unknown[]) => unknown; +} + +interface SolflareWallet extends EventEmitter { + isSolflare?: boolean; + publicKey?: { toBuffer(): Buffer }; + isConnected: boolean; + autoApprove: boolean; + signTransaction: (transaction: Transaction) => Promise; + signAllTransactions: (transactions: Transaction[]) => Promise; + connect: () => Promise; + disconnect: () => Promise; +} + +interface SolflareWindow extends Window { + solflare?: SolflareWallet; +} + +declare const window: SolflareWindow; + +export class SolflareWalletAdapter + extends EventEmitter + implements WalletAdapter { + + _wallet: SolflareWallet | undefined; + _publicKey: PublicKey | undefined; + + constructor() { + super(); + this.connect = this.connect.bind(this); + } + + get connected() { + return !!this._wallet?.isConnected; + } + + get autoApprove() { + return !!this._wallet?.autoApprove; + } + + async signAllTransactions( + transactions: Transaction[], + ): Promise { + if (!this._wallet) { + return transactions; + } + + return this._wallet.signAllTransactions(transactions); + } + + get publicKey() { + if (!this._publicKey && this._wallet?.publicKey) + this._publicKey = new PublicKey( + this._wallet.publicKey.toBuffer(), + ); + + return this._publicKey || null; + } + + async signTransaction(transaction: Transaction) { + if (!this._wallet) { + return transaction; + } + + return this._wallet.signTransaction(transaction); + } + + connect = async () => { + if (this._wallet) { + return; + } + + let wallet: SolflareWallet; + if (window.solflare?.isSolflare) { + wallet = window.solflare; + } else { + window.open('https://solflare.com', '_blank'); + notify({ + message: 'Solflare Error', + description: 'Please install Solflare wallet', + }); + return; + } + + wallet.on('connect', () => { + this._wallet = wallet; + this.emit('connect'); + }); + + if (!wallet.isConnected) { + await wallet.connect(); + } + + this._wallet = wallet; + this.emit('connect'); + }; + + disconnect() { + if (this._wallet) { + this._wallet.disconnect(); + this._wallet = undefined; + this.emit('disconnect'); + } + } +} From 2be94432bb7741ffef3b00b5e5a3bd957a28480c Mon Sep 17 00:00:00 2001 From: SajidXRizvi <88670957+SajidXRizvi@users.noreply.github.com> Date: Tue, 10 Aug 2021 20:17:34 +0530 Subject: [PATCH 3/5] Fix NFT Mint via Custom URL (#204) Signed-off-by: Sajid Rizvi --- js/packages/web/src/views/artCreate/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/web/src/views/artCreate/index.tsx b/js/packages/web/src/views/artCreate/index.tsx index 83f4deb..f45e173 100644 --- a/js/packages/web/src/views/artCreate/index.tsx +++ b/js/packages/web/src/views/artCreate/index.tsx @@ -295,7 +295,7 @@ const UploadStep = (props: { const [customURL, setCustomURL] = useState(''); const [customURLErr, setCustomURLErr] = useState(''); - const disableContinue = !coverFile || !!customURLErr; + const disableContinue = !!coverFile || !!customURLErr; useEffect(() => { props.setAttributes({ From c310080fe0248e3ae1a368e291b72076b1b08036 Mon Sep 17 00:00:00 2001 From: Jordan Prince Date: Tue, 10 Aug 2021 12:24:42 -0500 Subject: [PATCH 4/5] Revert "Fix NFT Mint via Custom URL (#204)" (#205) This reverts commit 2be94432bb7741ffef3b00b5e5a3bd957a28480c. --- js/packages/web/src/views/artCreate/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/packages/web/src/views/artCreate/index.tsx b/js/packages/web/src/views/artCreate/index.tsx index f45e173..83f4deb 100644 --- a/js/packages/web/src/views/artCreate/index.tsx +++ b/js/packages/web/src/views/artCreate/index.tsx @@ -295,7 +295,7 @@ const UploadStep = (props: { const [customURL, setCustomURL] = useState(''); const [customURLErr, setCustomURLErr] = useState(''); - const disableContinue = !!coverFile || !!customURLErr; + const disableContinue = !coverFile || !!customURLErr; useEffect(() => { props.setAttributes({ From 8904e67630d32abb10d3958b6e370fd84c5a7c07 Mon Sep 17 00:00:00 2001 From: Jordan Prince Date: Fri, 13 Aug 2021 08:03:08 -0500 Subject: [PATCH 5/5] Operation: Warp Speed I (#203) * Work in progress on speed fix * Oh yeaj, it works and it's fast as sh*t...just get the metadata oyu ned * Separate the metadata gets into additional promises since technically they are async * Holy cow, now we have editions too * remove a print * Safer script * Minor script fix --- js/packages/common/src/actions/metadata.ts | 3 + .../web/src/contexts/meta/loadAccounts.ts | 147 +++++++++++++++++- .../web/src/contexts/meta/processMetaData.ts | 8 +- .../contexts/meta/processMetaplexAccounts.ts | 1 - js/packages/web/src/models/metaplex/index.ts | 1 + .../token-metadata/program/src/instruction.rs | 21 +++ rust/token-metadata/program/src/processor.rs | 39 +++++ rust/token-metadata/program/src/state.rs | 12 +- rust/token-metadata/program/src/utils.rs | 33 ++++ rust/token-metadata/test/src/main.rs | 63 +++++++- 10 files changed, 313 insertions(+), 15 deletions(-) diff --git a/js/packages/common/src/actions/metadata.ts b/js/packages/common/src/actions/metadata.ts index feb788f..9070d2c 100644 --- a/js/packages/common/src/actions/metadata.ts +++ b/js/packages/common/src/actions/metadata.ts @@ -213,6 +213,7 @@ export class Metadata { data: Data; primarySaleHappened: boolean; isMutable: boolean; + editionNonce: number | null; // set lazy masterEdition?: PublicKey; @@ -224,6 +225,7 @@ export class Metadata { data: Data; primarySaleHappened: boolean; isMutable: boolean; + editionNonce: number | null; }) { this.key = MetadataKey.MetadataV1; this.updateAuthority = args.updateAuthority; @@ -231,6 +233,7 @@ export class Metadata { this.data = args.data; this.primarySaleHappened = args.primarySaleHappened; this.isMutable = args.isMutable; + this.editionNonce = args.editionNonce; } public async init() { diff --git a/js/packages/web/src/contexts/meta/loadAccounts.ts b/js/packages/web/src/contexts/meta/loadAccounts.ts index aca10cc..0fd831a 100644 --- a/js/packages/web/src/contexts/meta/loadAccounts.ts +++ b/js/packages/web/src/contexts/meta/loadAccounts.ts @@ -4,14 +4,30 @@ import { METAPLEX_ID, VAULT_ID, } from '@oyster/common/dist/lib/utils/ids'; -import { Connection } from '@solana/web3.js'; +import { Connection, PublicKey } from '@solana/web3.js'; import { AccountAndPubkey, MetaState, ProcessAccountsFunc } from './types'; import { isMetadataPartOfStore } from './isMetadataPartOfStore'; import { processAuctions } from './processAuctions'; import { processMetaplexAccounts } from './processMetaplexAccounts'; import { processMetaData } from './processMetaData'; import { processVaultData } from './processVaultData'; -import { Metadata, ParsedAccount } from '../../../../common/dist/lib'; +import { + findProgramAddress, + getEdition, + getMultipleAccounts, + MAX_CREATOR_LEN, + MAX_CREATOR_LIMIT, + MAX_NAME_LENGTH, + MAX_SYMBOL_LENGTH, + MAX_URI_LENGTH, + Metadata, + METADATA_PREFIX, + ParsedAccount, +} from '../../../../common/dist/lib'; +import { + MAX_WHITELISTED_CREATOR_SIZE, + MetaplexKey, +} from '../../models/metaplex'; export const loadAccounts = async (connection: Connection, all: boolean) => { const tempCache: MetaState = { @@ -47,18 +63,135 @@ export const loadAccounts = async (connection: Connection, all: boolean) => { } }; - await Promise.all([ + const additionalPromises: Promise[] = []; + + const promises = [ connection.getProgramAccounts(VAULT_ID).then(forEach(processVaultData)), connection.getProgramAccounts(AUCTION_ID).then(forEach(processAuctions)), - connection - .getProgramAccounts(METADATA_PROGRAM_ID) - .then(forEach(processMetaData)), + , connection .getProgramAccounts(METAPLEX_ID) .then(forEach(processMetaplexAccounts)), - ]); + connection + .getProgramAccounts(METAPLEX_ID, { + filters: [ + { + dataSize: MAX_WHITELISTED_CREATOR_SIZE, + }, + ], + }) + .then(async creators => { + await forEach(processMetaplexAccounts)(creators); + + const whitelistedCreators = Object.values( + tempCache.whitelistedCreatorsByCreator, + ); + + if (whitelistedCreators.length > 3) { + console.log(' too many creators, pulling all nfts in one go'); + additionalPromises.push( + connection + .getProgramAccounts(METADATA_PROGRAM_ID) + .then(forEach(processMetaData)), + ); + } else { + console.log('pulling optimized nfts'); + + for (let i = 0; i < MAX_CREATOR_LIMIT; i++) { + for (let j = 0; j < whitelistedCreators.length; j++) { + additionalPromises.push( + connection + .getProgramAccounts(METADATA_PROGRAM_ID, { + filters: [ + { + memcmp: { + offset: + 1 + // key + 32 + // update auth + 32 + // mint + 4 + // name string length + MAX_NAME_LENGTH + // name + 4 + // uri string length + MAX_URI_LENGTH + // uri + 4 + // symbol string length + MAX_SYMBOL_LENGTH + // symbol + 2 + // seller fee basis points + 1 + // whether or not there is a creators vec + 4 + // creators vec length + i * MAX_CREATOR_LEN, + bytes: whitelistedCreators[j].info.address.toBase58(), + }, + }, + ], + }) + .then(forEach(processMetaData)), + ); + } + } + } + }), + ]; + await Promise.all(promises); + await Promise.all(additionalPromises); await postProcessMetadata(tempCache, all); + console.log('Metadata size', tempCache.metadata.length); + + if (additionalPromises.length > 0) { + console.log('Pulling editions for optimized metadata'); + let setOf100MetadataEditionKeys: string[] = []; + const editionPromises = []; + + for (let i = 0; i < tempCache.metadata.length; i++) { + let edition: PublicKey; + if (tempCache.metadata[i].info.editionNonce != null) { + edition = await PublicKey.createProgramAddress( + [ + Buffer.from(METADATA_PREFIX), + METADATA_PROGRAM_ID.toBuffer(), + tempCache.metadata[i].info.mint.toBuffer(), + new Uint8Array([tempCache.metadata[i].info.editionNonce || 0]), + ], + METADATA_PROGRAM_ID, + ); + } else { + edition = await getEdition(tempCache.metadata[i].info.mint); + } + + setOf100MetadataEditionKeys.push(edition.toBase58()); + + if (setOf100MetadataEditionKeys.length >= 100) { + editionPromises.push( + getMultipleAccounts( + connection, + setOf100MetadataEditionKeys, + 'recent', + ), + ); + setOf100MetadataEditionKeys = []; + } + } + + const responses = await Promise.all(editionPromises); + for (let i = 0; i < responses.length; i++) { + const returnedAccounts = responses[i]; + for (let j = 0; j < returnedAccounts.array.length; j++) { + processMetaData( + { + pubkey: new PublicKey(returnedAccounts.keys[j]), + account: returnedAccounts.array[j], + }, + updateTemp, + all, + ); + } + } + console.log( + 'Edition size', + Object.keys(tempCache.editions).length, + Object.keys(tempCache.masterEditions).length, + ); + } return tempCache; }; diff --git a/js/packages/web/src/contexts/meta/processMetaData.ts b/js/packages/web/src/contexts/meta/processMetaData.ts index 8f07480..c36f3b2 100644 --- a/js/packages/web/src/contexts/meta/processMetaData.ts +++ b/js/packages/web/src/contexts/meta/processMetaData.ts @@ -84,8 +84,12 @@ export const processMetaData: ProcessAccountsFunc = ( } }; -const isMetadataAccount = (account: AccountInfo) => - account.owner.equals(METADATA_PROGRAM_ID); +const isMetadataAccount = (account: AccountInfo) => { + return account.owner.equals + ? account.owner.equals(METADATA_PROGRAM_ID) + : //@ts-ignore + account.owner === METADATA_PROGRAM_ID.toBase58(); +}; const isMetadataV1Account = (account: AccountInfo) => account.data[0] === MetadataKey.MetadataV1; diff --git a/js/packages/web/src/contexts/meta/processMetaplexAccounts.ts b/js/packages/web/src/contexts/meta/processMetaplexAccounts.ts index 9f018b8..6bdf225 100644 --- a/js/packages/web/src/contexts/meta/processMetaplexAccounts.ts +++ b/js/packages/web/src/contexts/meta/processMetaplexAccounts.ts @@ -131,7 +131,6 @@ export const processMetaplexAccounts: ProcessAccountsFunc = async ( if (isWhitelistedCreatorV1Account(account)) { const whitelistedCreator = decodeWhitelistedCreator(account.data); - // TODO: figure out a way to avoid generating creator addresses during parsing // should we store store id inside creator? const creatorKeyIfCreatorWasPartOfThisStore = await getWhitelistedCreator( diff --git a/js/packages/web/src/models/metaplex/index.ts b/js/packages/web/src/models/metaplex/index.ts index 34de25c..5869259 100644 --- a/js/packages/web/src/models/metaplex/index.ts +++ b/js/packages/web/src/models/metaplex/index.ts @@ -38,6 +38,7 @@ export const METAPLEX_PREFIX = 'metaplex'; export const TOTALS = 'totals'; export const ORIGINAL_AUTHORITY_LOOKUP_SIZE = 33; export const MAX_PRIZE_TRACKING_TICKET_SIZE = 1 + 32 + 8 + 8 + 8 + 50; +export const MAX_WHITELISTED_CREATOR_SIZE = 2 + 32 + 10; export enum MetaplexKey { Uninitialized = 0, OriginalAuthorityLookupV1 = 1, diff --git a/rust/token-metadata/program/src/instruction.rs b/rust/token-metadata/program/src/instruction.rs index bd1f95d..82fe0a7 100644 --- a/rust/token-metadata/program/src/instruction.rs +++ b/rust/token-metadata/program/src/instruction.rs @@ -238,6 +238,11 @@ pub enum MetadataInstruction { /// 15. `[]` System program /// 16. `[]` Rent info MintNewEditionFromMasterEditionViaVaultProxy(MintNewEditionFromMasterEditionViaTokenArgs), + + /// Puff a Metadata - make all of it's variable length fields (name/uri/symbol) a fixed length using a null character + /// so that it can be found using offset searches by the RPC to make client lookups cheaper. + /// 0. `[writable]` Metadata account + PuffMetadata, } /// Creates an CreateMetadataAccounts instruction @@ -308,6 +313,22 @@ pub fn update_metadata_accounts( } } +/// puff metadata account instruction +pub fn puff_metadata_account( + program_id: Pubkey, + metadata_account: Pubkey, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(metadata_account, false), + ], + data: MetadataInstruction::PuffMetadata + .try_to_vec() + .unwrap(), + } +} + /// creates a update_primary_sale_happened_via_token instruction #[allow(clippy::too_many_arguments)] pub fn update_primary_sale_happened_via_token( diff --git a/rust/token-metadata/program/src/processor.rs b/rust/token-metadata/program/src/processor.rs index 2abd665..1f798ad 100644 --- a/rust/token-metadata/program/src/processor.rs +++ b/rust/token-metadata/program/src/processor.rs @@ -20,6 +20,7 @@ use { create_or_allocate_account_raw, get_owner_from_token_account, process_create_metadata_accounts_logic, process_mint_new_edition_from_master_edition_via_token_logic, transfer_mint_authority, + puff_out_data_fields, CreateMetadataAccountsLogicArgs, MintNewEditionFromMasterEditionViaTokenLogicArgs, }, }, @@ -129,6 +130,13 @@ pub fn process_instruction<'a>( args.edition, ) } + MetadataInstruction::PuffMetadata => { + msg!("Instruction: Puff Metadata"); + process_puff_metadata_account( + program_id, + accounts + ) + } } } @@ -203,6 +211,8 @@ pub fn process_update_metadata_accounts( } } + puff_out_data_fields(&mut metadata); + metadata.serialize(&mut *metadata_account_info.data.borrow_mut())?; Ok(()) } @@ -567,3 +577,32 @@ pub fn process_mint_new_edition_from_master_edition_via_vault_proxy<'a>( process_mint_new_edition_from_master_edition_via_token_logic(program_id, args, edition, true) } + +/// Puff out the variable length fields to a fixed length on a metadata +/// account in a permissionless way. +pub fn process_puff_metadata_account( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let metadata_account_info = next_account_info(account_info_iter)?; + let mut metadata = Metadata::from_account_info(metadata_account_info)?; + + assert_owned_by(metadata_account_info, program_id)?; + + puff_out_data_fields(&mut metadata); + + let edition_seeds = &[ + PREFIX.as_bytes(), + program_id.as_ref(), + metadata.mint.as_ref(), + EDITION.as_bytes() + ]; + let (_, edition_bump_seed) = + Pubkey::find_program_address(edition_seeds, program_id); + metadata.edition_nonce = Some(edition_bump_seed); + + metadata.serialize(&mut *metadata_account_info.data.borrow_mut())?; + Ok(()) +} \ No newline at end of file diff --git a/rust/token-metadata/program/src/state.rs b/rust/token-metadata/program/src/state.rs index 8a13fdf..656a5b0 100644 --- a/rust/token-metadata/program/src/state.rs +++ b/rust/token-metadata/program/src/state.rs @@ -23,14 +23,20 @@ pub const MAX_URI_LENGTH: usize = 200; pub const MAX_METADATA_LEN: usize = 1 + 32 + 32 + + 4 + MAX_NAME_LENGTH + + 4 + MAX_SYMBOL_LENGTH + + 4 + MAX_URI_LENGTH - + MAX_CREATOR_LIMIT * MAX_CREATOR_LEN + 2 + 1 + + 4 + + MAX_CREATOR_LIMIT * MAX_CREATOR_LEN + 1 - + 198; + + 1 + + 9 + + 172; pub const MAX_EDITION_LEN: usize = 1 + 32 + 8 + 200; @@ -93,6 +99,8 @@ pub struct Metadata { pub primary_sale_happened: bool, // Whether or not the data struct is mutable, default is not pub is_mutable: bool, + /// nonce for easy calculation of editions, if present + pub edition_nonce: Option } impl Metadata { diff --git a/rust/token-metadata/program/src/utils.rs b/rust/token-metadata/program/src/utils.rs index 7960e62..a764e58 100644 --- a/rust/token-metadata/program/src/utils.rs +++ b/rust/token-metadata/program/src/utils.rs @@ -850,10 +850,43 @@ pub fn process_create_metadata_accounts_logic( metadata.is_mutable = is_mutable; metadata.update_authority = *update_authority_info.key; + puff_out_data_fields(&mut metadata); + + let edition_seeds = &[ + PREFIX.as_bytes(), + program_id.as_ref(), + metadata.mint.as_ref(), + EDITION.as_bytes() + ]; + let (_, edition_bump_seed) = + Pubkey::find_program_address(edition_seeds, program_id); + metadata.edition_nonce = Some(edition_bump_seed); + metadata.serialize(&mut *metadata_account_info.data.borrow_mut())?; Ok(()) } + +pub fn puff_out_data_fields(metadata: &mut Metadata) { + let mut array_of_zeroes = vec![]; + while array_of_zeroes.len() < MAX_NAME_LENGTH - metadata.data.name.len() { + array_of_zeroes.push(0u8); + } + metadata.data.name = metadata.data.name.clone() + std::str::from_utf8(&array_of_zeroes).unwrap(); + + let mut array_of_zeroes = vec![]; + while array_of_zeroes.len() < MAX_SYMBOL_LENGTH - metadata.data.symbol.len() { + array_of_zeroes.push(0u8); + } + metadata.data.symbol = metadata.data.symbol.clone() + std::str::from_utf8(&array_of_zeroes).unwrap(); + + let mut array_of_zeroes = vec![]; + while array_of_zeroes.len() < MAX_URI_LENGTH - metadata.data.uri.len() { + array_of_zeroes.push(0u8); + } + metadata.data.uri = metadata.data.uri.clone() + std::str::from_utf8(&array_of_zeroes).unwrap(); +} + pub struct MintNewEditionFromMasterEditionViaTokenLogicArgs<'a> { pub new_metadata_account_info: &'a AccountInfo<'a>, pub new_edition_account_info: &'a AccountInfo<'a>, diff --git a/rust/token-metadata/test/src/main.rs b/rust/token-metadata/test/src/main.rs index 638d689..e976b83 100644 --- a/rust/token-metadata/test/src/main.rs +++ b/rust/token-metadata/test/src/main.rs @@ -23,17 +23,68 @@ use { spl_token_metadata::{ instruction::{ create_master_edition, create_metadata_accounts, - mint_new_edition_from_master_edition_via_token, update_metadata_accounts, + mint_new_edition_from_master_edition_via_token, update_metadata_accounts,puff_metadata_account }, state::{ get_reservation_list, Data, Edition, Key, MasterEditionV1, MasterEditionV2, Metadata, - EDITION, PREFIX, + EDITION, PREFIX,MAX_NAME_LENGTH, MAX_URI_LENGTH, MAX_SYMBOL_LENGTH }, }, std::str::FromStr, }; const TOKEN_PROGRAM_PUBKEY: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; +fn puff_unpuffed_metadata(_app_matches: &ArgMatches, payer: Keypair, client: RpcClient) { + let metadata_accounts = client.get_program_accounts(&spl_token_metadata::id()).unwrap(); + let mut needing_puffing = vec![]; + for acct in metadata_accounts { + if acct.1.data[0] == Key::MetadataV1 as u8 { + match try_from_slice_unchecked(&acct.1.data) { + Ok(val) => { + let account: Metadata = val; + if account.data.name.len() < MAX_NAME_LENGTH || account.data.uri.len() < MAX_URI_LENGTH || account.data.symbol.len() < MAX_SYMBOL_LENGTH || account.edition_nonce.is_none() { + needing_puffing.push(acct.0); + } + }, + Err(_) => { println!("Skipping {}", acct.0)}, + }; + } + } + println!("Found {} accounts needing puffing", needing_puffing.len()); + + let mut instructions = vec![]; + let mut i = 0; + while i < needing_puffing.len() { + let pubkey = needing_puffing[i]; + instructions.push(puff_metadata_account(spl_token_metadata::id(), pubkey)); + if instructions.len() >= 20 { + let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey())); + let recent_blockhash = client.get_recent_blockhash().unwrap().0; + + transaction.sign(&[&payer], recent_blockhash); + match client.send_and_confirm_transaction(&transaction) { + Ok(_) => { + println!("Another 20 down. At {} / {}", i, needing_puffing.len()); + instructions = vec![]; + i += 1; + }, + Err(_) => { + println!("Txn failed. Retry."); + std::thread::sleep(std::time::Duration::from_millis(1000)); + }, + } + } else { + i += 1; + } + } + + if instructions.len() > 0 { + let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey())); + let recent_blockhash = client.get_recent_blockhash().unwrap().0; + transaction.sign(&[&payer], recent_blockhash); + client.send_and_confirm_transaction(&transaction).unwrap(); + } +} fn mint_coins(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) { let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap(); @@ -780,7 +831,10 @@ fn main() { .takes_value(true) .help("Account's authority, defaults to you"), ) - ).get_matches(); + + ).subcommand( + SubCommand::with_name("puff_unpuffed_metadata") + .about("Take metadata that still have variable length name, symbol, and uri fields and stretch them out with null symbols so they can be searched more easily by RPC.")).get_matches(); let client = RpcClient::new( app_matches @@ -831,6 +885,9 @@ fn main() { ("mint_coins", Some(arg_matches)) => { mint_coins(arg_matches, payer, client); } + ("puff_unpuffed_metadata", Some(arg_matches)) => { + puff_unpuffed_metadata(arg_matches, payer, client); + } _ => unreachable!(), } }