From 433926ea71769d7b0eda5a3f7313b9542d2b6c07 Mon Sep 17 00:00:00 2001 From: Jeremy Bogle Date: Fri, 7 Jul 2023 14:39:15 -0500 Subject: [PATCH] Migrate v2 (#426) * Migrate v2 * Allow collector * Fix ci * Close token manager token account during migrate --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 4 +- .../src/instructions/permissioned/migrate.rs | 105 ++++---- src/idl/cardinal_token_manager.ts | 66 ++--- src/idl/cardinal_token_manager_idl.json | 33 +-- src/transaction.ts | 45 ---- tests/other/permissionedMigrate.test.ts | 241 ++++++++++++++++++ tools/ccsMigration/batchMigrate.ts | 87 ------- tools/getTotalTimeInvalidators.ts | 74 ++++++ tools/getTotalTokenManagers.ts | 65 +++++ tools/migrateToCSS.ts | 53 ---- tools/migration/batchMigrate.ts | 162 ++++++++++++ .../hyperspheres.ts | 0 tools/utils.ts | 112 ++++++++ 14 files changed, 752 insertions(+), 297 deletions(-) create mode 100644 tests/other/permissionedMigrate.test.ts delete mode 100644 tools/ccsMigration/batchMigrate.ts create mode 100644 tools/getTotalTimeInvalidators.ts create mode 100644 tools/getTotalTokenManagers.ts delete mode 100644 tools/migrateToCSS.ts create mode 100644 tools/migration/batchMigrate.ts rename tools/{ccsMigration => migration}/hyperspheres.ts (100%) create mode 100644 tools/utils.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fe5c5175..9123ae832 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ env: CARGO_TERM_COLOR: always RUST_TOOLCHAIN: nightly NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - SOLANA_VERSION: 1.10.41 + SOLANA_VERSION: 1.14.15 ANCHOR_GIT: https://github.com/project-serum/anchor ANCHOR_VERSION: 0.26.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 576a4a566..4918eb7b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,8 +15,8 @@ permissions: env: CARGO_TERM_COLOR: always - SOLANA_VERSION: 1.10.41 - RUST_TOOLCHAIN: nightly + SOLANA_VERSION: 1.14.15 + RUST_TOOLCHAIN: 1.69.0 SOTERIA_VERSION: 0.0.0 ANCHOR_GIT: https://github.com/project-serum/anchor ANCHOR_VERSION: 0.26.0 diff --git a/programs/cardinal-token-manager/src/instructions/permissioned/migrate.rs b/programs/cardinal-token-manager/src/instructions/permissioned/migrate.rs index 511623b81..6213355cd 100644 --- a/programs/cardinal-token-manager/src/instructions/permissioned/migrate.rs +++ b/programs/cardinal-token-manager/src/instructions/permissioned/migrate.rs @@ -1,109 +1,110 @@ -use std::str::FromStr; - use crate::errors::ErrorCode; use crate::state::MintManager; use crate::state::TokenManager; use crate::state::MINT_MANAGER_SEED; +use crate::state::*; +use anchor_lang::prelude::*; +use anchor_spl::token::CloseAccount; use anchor_spl::token::Mint; use anchor_spl::token::ThawAccount; use anchor_spl::token::Token; use anchor_spl::token::TokenAccount; use anchor_spl::token::{self}; -use cardinal_creator_standard::instructions::init_mint_manager; +use mpl_token_metadata::instruction::create_master_edition_v3; use solana_program::program::invoke_signed; -use anchor_lang::prelude::*; -use anchor_lang::AccountsClose; - #[derive(Accounts)] - pub struct MigrateCtx<'info> { - current_mint_manager: Box>, - /// CHECK: no checks required - #[account(mut)] - mint_manager: UncheckedAccount<'info>, + #[account(mut, close = collector, constraint = token_manager.kind == TokenManagerKind::Permissioned as u8 && token_manager.state == TokenManagerState::Claimed as u8 @ ErrorCode::InvalidTokenManagerState)] + mint_manager: Box>, + #[account(mut, close = collector)] + token_manager: Box>, + #[account(mut, constraint = + token_manager_token_account.owner == token_manager.key() + && token_manager_token_account.mint == token_manager.mint + @ ErrorCode::InvalidTokenManagerTokenAccount + )] + token_manager_token_account: Box>, #[account(mut, constraint = token_manager.mint == mint.key() @ ErrorCode::InvalidMint )] mint: Box>, /// CHECK: no checks required + #[account(mut)] mint_metadata: UncheckedAccount<'info>, - /// CHECK: no checks required - ruleset: UncheckedAccount<'info>, - #[account(mut)] - token_manager: Box>, + mint_edition: UncheckedAccount<'info>, #[account(mut)] holder_token_account: Box>, - /// CHECK: no checks required - token_authority: UncheckedAccount<'info>, /// CHECK: no checks required - authority: UncheckedAccount<'info>, + #[account(constraint = token_manager.invalidators.contains(&invalidator.key()) @ ErrorCode::InvalidInvalidator)] + invalidator: Signer<'info>, #[account(mut)] payer: Signer<'info>, - rent: Sysvar<'info, Rent>, + /// CHECK: no checks required + #[account(mut)] + collector: UncheckedAccount<'info>, token_program: Program<'info, Token>, system_program: Program<'info, System>, /// CHECK: This is not dangerous because the ID is checked with instructions sysvar - #[account(address = cardinal_creator_standard::id())] - cardinal_creator_standard: UncheckedAccount<'info>, + #[account(address = mpl_token_metadata::id())] + mpl_token_metadata: UncheckedAccount<'info>, } pub fn handler(ctx: Context) -> Result<()> { - if ctx.accounts.payer.key() != Pubkey::from_str("gmdS6fDgVbeCCYwwvTPJRKM9bFbAgSZh6MTDUT2DcgV").unwrap() { - return Err(error!(ErrorCode::InvalidMigrateAuthority)); - } - - if ctx.accounts.holder_token_account.delegate.is_some() { - return Err(error!(ErrorCode::CannotMigrateDelegatedToken)); - } - let mint_manager_key = ctx.accounts.mint.key(); - let current_mint_manager_seeds = &[MINT_MANAGER_SEED.as_bytes(), mint_manager_key.as_ref(), &[ctx.accounts.current_mint_manager.bump]]; - let current_mint_manager_signer = &[¤t_mint_manager_seeds[..]]; + let mint_manager_seeds = &[MINT_MANAGER_SEED.as_bytes(), mint_manager_key.as_ref(), &[ctx.accounts.mint_manager.bump]]; + let mint_manager_signer = &[&mint_manager_seeds[..]]; // thaw recipient account let cpi_accounts = ThawAccount { account: ctx.accounts.holder_token_account.to_account_info(), mint: ctx.accounts.mint.to_account_info(), - authority: ctx.accounts.current_mint_manager.to_account_info(), + authority: ctx.accounts.mint_manager.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); - let cpi_context = CpiContext::new(cpi_program, cpi_accounts).with_signer(current_mint_manager_signer); + let cpi_context = CpiContext::new(cpi_program, cpi_accounts).with_signer(mint_manager_signer); token::thaw_account(cpi_context)?; invoke_signed( - &init_mint_manager( - ctx.accounts.cardinal_creator_standard.key(), - ctx.accounts.mint_manager.key(), + &create_master_edition_v3( + ctx.accounts.mpl_token_metadata.key(), + ctx.accounts.mint_edition.key(), ctx.accounts.mint.key(), + ctx.accounts.invalidator.key(), + ctx.accounts.mint_manager.key(), ctx.accounts.mint_metadata.key(), - ctx.accounts.ruleset.key(), - ctx.accounts.holder_token_account.key(), - ctx.accounts.token_authority.key(), - ctx.accounts.authority.key(), ctx.accounts.payer.key(), - )?, + Some(0), + ), &[ - ctx.accounts.mint_manager.to_account_info(), + ctx.accounts.mpl_token_metadata.to_account_info(), + ctx.accounts.mint_edition.to_account_info(), ctx.accounts.mint.to_account_info(), + ctx.accounts.invalidator.to_account_info(), + ctx.accounts.mint_manager.to_account_info(), ctx.accounts.mint_metadata.to_account_info(), - ctx.accounts.ruleset.to_account_info(), - ctx.accounts.holder_token_account.to_account_info(), - ctx.accounts.token_authority.to_account_info(), - ctx.accounts.authority.to_account_info(), ctx.accounts.payer.to_account_info(), - ctx.accounts.rent.to_account_info(), - ctx.accounts.token_program.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ctx.accounts.cardinal_creator_standard.to_account_info(), + ctx.accounts.mpl_token_metadata.to_account_info(), ], - current_mint_manager_signer, + mint_manager_signer, )?; - ctx.accounts.token_manager.close(ctx.accounts.payer.to_account_info())?; + let mint = ctx.accounts.token_manager.mint; + let token_manager_seeds = &[TOKEN_MANAGER_SEED.as_bytes(), mint.as_ref(), &[ctx.accounts.token_manager.bump]]; + let token_manager_signer = &[&token_manager_seeds[..]]; + + // close token_manager_token_account + let cpi_accounts = CloseAccount { + account: ctx.accounts.token_manager_token_account.to_account_info(), + destination: ctx.accounts.collector.to_account_info(), + authority: ctx.accounts.token_manager.to_account_info(), + }; + let cpi_program = ctx.accounts.token_program.to_account_info(); + let cpi_context = CpiContext::new(cpi_program, cpi_accounts).with_signer(token_manager_signer); + token::close_account(cpi_context)?; Ok(()) } diff --git a/src/idl/cardinal_token_manager.ts b/src/idl/cardinal_token_manager.ts index 25121d4ef..b18e0caf2 100644 --- a/src/idl/cardinal_token_manager.ts +++ b/src/idl/cardinal_token_manager.ts @@ -810,50 +810,45 @@ export type CardinalTokenManager = { { name: "migrate"; accounts: [ - { - name: "currentMintManager"; - isMut: false; - isSigner: false; - }, { name: "mintManager"; isMut: true; isSigner: false; }, { - name: "mint"; + name: "tokenManager"; isMut: true; isSigner: false; }, { - name: "mintMetadata"; - isMut: false; + name: "tokenManagerTokenAccount"; + isMut: true; isSigner: false; }, { - name: "ruleset"; - isMut: false; + name: "mint"; + isMut: true; isSigner: false; }, { - name: "tokenManager"; + name: "mintMetadata"; isMut: true; isSigner: false; }, { - name: "holderTokenAccount"; + name: "mintEdition"; isMut: true; isSigner: false; }, { - name: "tokenAuthority"; - isMut: false; + name: "holderTokenAccount"; + isMut: true; isSigner: false; }, { - name: "authority"; + name: "invalidator"; isMut: false; - isSigner: false; + isSigner: true; }, { name: "payer"; @@ -861,8 +856,8 @@ export type CardinalTokenManager = { isSigner: true; }, { - name: "rent"; - isMut: false; + name: "collector"; + isMut: true; isSigner: false; }, { @@ -876,7 +871,7 @@ export type CardinalTokenManager = { isSigner: false; }, { - name: "cardinalCreatorStandard"; + name: "mplTokenMetadata"; isMut: false; isSigner: false; } @@ -2166,50 +2161,45 @@ export const IDL: CardinalTokenManager = { { name: "migrate", accounts: [ - { - name: "currentMintManager", - isMut: false, - isSigner: false, - }, { name: "mintManager", isMut: true, isSigner: false, }, { - name: "mint", + name: "tokenManager", isMut: true, isSigner: false, }, { - name: "mintMetadata", - isMut: false, + name: "tokenManagerTokenAccount", + isMut: true, isSigner: false, }, { - name: "ruleset", - isMut: false, + name: "mint", + isMut: true, isSigner: false, }, { - name: "tokenManager", + name: "mintMetadata", isMut: true, isSigner: false, }, { - name: "holderTokenAccount", + name: "mintEdition", isMut: true, isSigner: false, }, { - name: "tokenAuthority", - isMut: false, + name: "holderTokenAccount", + isMut: true, isSigner: false, }, { - name: "authority", + name: "invalidator", isMut: false, - isSigner: false, + isSigner: true, }, { name: "payer", @@ -2217,8 +2207,8 @@ export const IDL: CardinalTokenManager = { isSigner: true, }, { - name: "rent", - isMut: false, + name: "collector", + isMut: true, isSigner: false, }, { @@ -2232,7 +2222,7 @@ export const IDL: CardinalTokenManager = { isSigner: false, }, { - name: "cardinalCreatorStandard", + name: "mplTokenMetadata", isMut: false, isSigner: false, }, diff --git a/src/idl/cardinal_token_manager_idl.json b/src/idl/cardinal_token_manager_idl.json index 09e122df7..405b044d2 100644 --- a/src/idl/cardinal_token_manager_idl.json +++ b/src/idl/cardinal_token_manager_idl.json @@ -810,50 +810,45 @@ { "name": "migrate", "accounts": [ - { - "name": "currentMintManager", - "isMut": false, - "isSigner": false - }, { "name": "mintManager", "isMut": true, "isSigner": false }, { - "name": "mint", + "name": "tokenManager", "isMut": true, "isSigner": false }, { - "name": "mintMetadata", - "isMut": false, + "name": "tokenManagerTokenAccount", + "isMut": true, "isSigner": false }, { - "name": "ruleset", - "isMut": false, + "name": "mint", + "isMut": true, "isSigner": false }, { - "name": "tokenManager", + "name": "mintMetadata", "isMut": true, "isSigner": false }, { - "name": "holderTokenAccount", + "name": "mintEdition", "isMut": true, "isSigner": false }, { - "name": "tokenAuthority", - "isMut": false, + "name": "holderTokenAccount", + "isMut": true, "isSigner": false }, { - "name": "authority", + "name": "invalidator", "isMut": false, - "isSigner": false + "isSigner": true }, { "name": "payer", @@ -861,8 +856,8 @@ "isSigner": true }, { - "name": "rent", - "isMut": false, + "name": "collector", + "isMut": true, "isSigner": false }, { @@ -876,7 +871,7 @@ "isSigner": false }, { - "name": "cardinalCreatorStandard", + "name": "mplTokenMetadata", "isMut": false, "isSigner": false } diff --git a/src/transaction.ts b/src/transaction.ts index 3ef2f67b7..02fbaab15 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -11,11 +11,6 @@ import { tryNull, withFindOrInitAssociatedTokenAccount, } from "@cardinal/common"; -import { - findMintManagerId as findCCSMintManagerId, - findRulesetId, - PROGRAM_ADDRESS, -} from "@cardinal/creator-standard"; import { PAYMENT_MANAGER_ADDRESS } from "@cardinal/payment-manager"; import { withRemainingAccountsForPayment } from "@cardinal/payment-manager/dist/cjs/utils"; import { @@ -1458,46 +1453,6 @@ export const withSend = async ( return transaction; }; -export const withMigrate = async ( - transaction: Transaction, - connection: Connection, - wallet: Wallet, - mintId: PublicKey, - rulesetName: string, - holderTokenAccountId: PublicKey, - authority: PublicKey -): Promise => { - const tmManagerProgram = tokenManagerProgram(connection, wallet); - const currentMintManagerId = findMintManagerId(mintId); - const mintManagerId = findCCSMintManagerId(mintId); - const tokenManagerId = findTokenManagerAddress(mintId); - const rulesetId = findRulesetId(rulesetName); - const mintMetadataId = findMintMetadataId(mintId); - - const migrateIx = await tmManagerProgram.methods - .migrate() - .accountsStrict({ - currentMintManager: currentMintManagerId, - mintManager: mintManagerId, - mint: mintId, - mintMetadata: mintMetadataId, - ruleset: rulesetId, - tokenManager: tokenManagerId, - holderTokenAccount: holderTokenAccountId, - tokenAuthority: currentMintManagerId, - authority: authority, - payer: wallet.publicKey, - rent: SYSVAR_RENT_PUBKEY, - tokenProgram: TOKEN_PROGRAM_ID, - systemProgram: SystemProgram.programId, - cardinalCreatorStandard: PROGRAM_ADDRESS, - }) - .instruction(); - transaction.add(migrateIx); - - return transaction; -}; - export const withReplaceInvalidator = async ( transaction: Transaction, connection: Connection, diff --git a/tests/other/permissionedMigrate.test.ts b/tests/other/permissionedMigrate.test.ts new file mode 100644 index 000000000..8479e2c53 --- /dev/null +++ b/tests/other/permissionedMigrate.test.ts @@ -0,0 +1,241 @@ +import type { CardinalProvider } from "@cardinal/common"; +import { + createMintTx, + executeTransaction, + findMintEditionId, + findMintMetadataId, + getTestProvider, + METADATA_PROGRAM_ID, + tryGetAccount, +} from "@cardinal/common"; +import { beforeAll, expect } from "@jest/globals"; +import { createCreateMetadataAccountV3Instruction } from "@metaplex-foundation/mpl-token-metadata"; +import { BN, Wallet } from "@project-serum/anchor"; +import { TOKEN_PROGRAM_ID } from "@project-serum/anchor/dist/cjs/utils/token"; +import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; +import type { PublicKey } from "@solana/web3.js"; +import { + Keypair, + LAMPORTS_PER_SOL, + SystemProgram, + Transaction, +} from "@solana/web3.js"; + +import { claimToken, issueToken } from "../../src"; +import { tokenManager } from "../../src/programs"; +import { + InvalidationType, + TokenManagerKind, + tokenManagerProgram, + TokenManagerState, +} from "../../src/programs/tokenManager"; +import { + getMintManager, + getTokenManager, +} from "../../src/programs/tokenManager/accounts"; +import { + findMintManagerId, + findTokenManagerAddress, +} from "../../src/programs/tokenManager/pda"; + +describe("Permissioned migrate", () => { + let provider: CardinalProvider; + const recipient = Keypair.generate(); + const invalidator = Keypair.generate(); + const user = Keypair.generate(); + let issuerTokenAccountId: PublicKey; + let rentalMint: PublicKey; + + beforeAll(async () => { + provider = await getTestProvider(); + const airdropCreator = await provider.connection.requestAirdrop( + user.publicKey, + LAMPORTS_PER_SOL + ); + await provider.connection.confirmTransaction(airdropCreator); + + const airdropInvalidator = await provider.connection.requestAirdrop( + invalidator.publicKey, + LAMPORTS_PER_SOL + ); + await provider.connection.confirmTransaction(airdropInvalidator); + + const airdropRecipient = await provider.connection.requestAirdrop( + recipient.publicKey, + LAMPORTS_PER_SOL + ); + await provider.connection.confirmTransaction(airdropRecipient); + + // create rental mint + const mintKeypair = Keypair.generate(); + const mintId = mintKeypair.publicKey; + const [tx, ata] = await createMintTx( + provider.connection, + mintKeypair.publicKey, + user.publicKey + ); + tx.add( + createCreateMetadataAccountV3Instruction( + { + metadata: findMintMetadataId(mintId), + mint: mintId, + mintAuthority: user.publicKey, + payer: user.publicKey, + updateAuthority: invalidator.publicKey, + }, + { + createMetadataAccountArgsV3: { + data: { + name: "", + symbol: "", + uri: "", + sellerFeeBasisPoints: 0, + creators: [ + { address: invalidator.publicKey, share: 100, verified: false }, + ], + collection: null, + uses: null, + }, + isMutable: true, + collectionDetails: null, + }, + } + ) + ); + await executeTransaction(provider.connection, tx, new Wallet(user), { + signers: [mintKeypair], + }); + issuerTokenAccountId = ata; + rentalMint = mintId; + }); + + it("Issue token", async () => { + const [transaction, tokenManagerId] = await issueToken( + provider.connection, + new Wallet(user), + { + timeInvalidation: { maxExpiration: Date.now() / 1000 }, + mint: rentalMint, + kind: TokenManagerKind.Permissioned, + issuerTokenAccountId: issuerTokenAccountId, + amount: new BN(1), + invalidationType: InvalidationType.Release, + customInvalidators: [invalidator.publicKey], + } + ); + await executeTransaction( + provider.connection, + transaction, + new Wallet(user) + ); + + const tokenManagerData = await tokenManager.accounts.getTokenManager( + provider.connection, + tokenManagerId + ); + expect(tokenManagerData.parsed.state).toEqual(TokenManagerState.Issued); + expect(tokenManagerData.parsed.amount.toNumber()).toEqual(1); + expect(tokenManagerData.parsed.mint.toString()).toEqual( + rentalMint.toString() + ); + expect(tokenManagerData.parsed.invalidators.length).toBeGreaterThanOrEqual( + 1 + ); + expect(tokenManagerData.parsed.issuer.toString()).toEqual( + user.publicKey.toString() + ); + + const checkIssuerTokenAccount = await getAccount( + provider.connection, + issuerTokenAccountId + ); + expect(checkIssuerTokenAccount.amount.toString()).toEqual("0"); + + // check receipt-index + const tokenManagers = await tokenManager.accounts.getTokenManagersForIssuer( + provider.connection, + user.publicKey + ); + expect(tokenManagers.map((i) => i.pubkey.toString())).toContain( + tokenManagerId.toString() + ); + }); + + it("Claim token", async () => { + const transaction = await claimToken( + provider.connection, + new Wallet(user), + findTokenManagerAddress(rentalMint) + ); + await executeTransaction( + provider.connection, + transaction, + new Wallet(user) + ); + + const tokenManagerData = await tokenManager.accounts.getTokenManager( + provider.connection, + findTokenManagerAddress(rentalMint) + ); + expect(tokenManagerData.parsed.state).toEqual(TokenManagerState.Claimed); + expect(tokenManagerData.parsed.amount.toNumber()).toEqual(1); + expect(tokenManagerData.parsed.mint.toString()).toEqual( + rentalMint.toString() + ); + expect(tokenManagerData.parsed.invalidators.length).toBeGreaterThanOrEqual( + 1 + ); + }); + + it("Migrate", async () => { + const tokenManager = await getTokenManager( + provider.connection, + findTokenManagerAddress(rentalMint) + ); + const mintManagerId = findMintManagerId(rentalMint); + const ix = await tokenManagerProgram(provider.connection, provider.wallet) + .methods.migrate() + .accountsStrict({ + mintManager: mintManagerId, + tokenManager: findTokenManagerAddress(rentalMint), + tokenManagerTokenAccount: getAssociatedTokenAddressSync( + rentalMint, + findTokenManagerAddress(rentalMint), + true + ), + mint: rentalMint, + mintMetadata: findMintMetadataId(rentalMint), + mintEdition: findMintEditionId(rentalMint), + holderTokenAccount: tokenManager.parsed.recipientTokenAccount, + invalidator: invalidator.publicKey, + payer: invalidator.publicKey, + collector: user.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + mplTokenMetadata: METADATA_PROGRAM_ID, + }) + .instruction(); + const transaction = new Transaction(); + transaction.add(ix); + + await executeTransaction( + provider.connection, + transaction, + new Wallet(invalidator) + ); + const checkMintManager = await tryGetAccount(async () => + getMintManager(provider.connection, findMintManagerId(rentalMint)) + ); + expect(checkMintManager).toEqual(null); + + const checkTokenManager = await tryGetAccount(async () => + getTokenManager(provider.connection, findTokenManagerAddress(rentalMint)) + ); + expect(checkTokenManager).toEqual(null); + + const editionInfo = await provider.connection.getAccountInfo( + findMintEditionId(rentalMint) + ); + expect(editionInfo?.data.length).toBeGreaterThan(0); + }); +}); diff --git a/tools/ccsMigration/batchMigrate.ts b/tools/ccsMigration/batchMigrate.ts deleted file mode 100644 index 39528dd0d..000000000 --- a/tools/ccsMigration/batchMigrate.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { chunkArray, executeTransaction } from "@cardinal/common"; -import { utils, Wallet } from "@project-serum/anchor"; -import type { PublicKey, TokenAccountBalancePair } from "@solana/web3.js"; -import { Keypair, Transaction } from "@solana/web3.js"; -import * as dotenv from "dotenv"; - -import { withMigrate } from "../../src"; -import { connectionFor } from "../connection"; -import { spheres } from "./hyperspheres"; - -dotenv.config(); - -const RULESET_NAME = ""; -const BATCH_SIZE = 2; -const PARALLET_BATCH_SIZE = 50; - -const wallet = Keypair.fromSecretKey( - utils.bytes.bs58.decode(utils.bytes.bs58.encode([])) -); // your wallet's secret key // your wallet's secret key - -const main = async (mints: PublicKey[], cluster = "devnet") => { - const connection = connectionFor(cluster); - - const chunkedMintIds = chunkArray(mints, BATCH_SIZE); - const batchedChunks = chunkArray(chunkedMintIds, PARALLET_BATCH_SIZE); - for (let i = 0; i < batchedChunks.length; i++) { - const chunk = batchedChunks[i]!; - console.log(`${i + 1}/${batchedChunks.length}`); - await Promise.all( - chunk.map(async (mintIds, c) => { - const transaction = new Transaction(); - const tokenAccounts = ( - await Promise.all( - mintIds.map((mintId) => connection.getTokenLargestAccounts(mintId)) - ) - ).map( - (res) => - res.value.find((tks) => tks.uiAmount && tks.uiAmount > 0) || null - ); - const mintsToMigrate = mintIds.reduce( - (acc, mintId, index) => { - if (tokenAccounts[index] === null) { - return acc; - } - return [ - ...acc, - { mintId: mintId, tokenAccount: tokenAccounts[index]! }, - ]; - }, - [] as { - mintId: PublicKey; - tokenAccount: TokenAccountBalancePair; - }[] - ); - for (let j = 0; j < mintsToMigrate.length; j++) { - const mint = mintsToMigrate[j]!; - console.log( - `>>[${c + 1}/${chunk.length}][${j + 1}/${ - mintsToMigrate.length - }] (${mint.mintId.toString()})` - ); - await withMigrate( - transaction, - connection, - new Wallet(wallet), - mint.mintId, - RULESET_NAME, - mint.tokenAccount.address, - wallet.publicKey - ); - } - const txid = await executeTransaction( - connection, - transaction, - new Wallet(wallet) - ); - console.log( - `[success] ${mintsToMigrate - .map((e) => e.mintId.toString()) - .join()} (https://explorer.solana.com/tx/${txid})` - ); - }) - ); - } -}; - -main(spheres, "mainnet-beta").catch((e) => console.log(e)); diff --git a/tools/getTotalTimeInvalidators.ts b/tools/getTotalTimeInvalidators.ts new file mode 100644 index 000000000..ae39672d7 --- /dev/null +++ b/tools/getTotalTimeInvalidators.ts @@ -0,0 +1,74 @@ +import type { AccountData } from "@cardinal/common"; +import { chunkArray } from "@cardinal/common"; +import dotenv from "dotenv"; + +import type { TimeInvalidatorData } from "../src/programs/timeInvalidator"; +import { getAllTimeInvalidators } from "../src/programs/timeInvalidator/accounts"; +import type { TokenManagerData } from "../src/programs/tokenManager"; +import { getTokenManagers } from "../src/programs/tokenManager/accounts"; +import { connectionFor } from "./connection"; + +dotenv.config(); + +export const main = async (cluster: string) => { + const connection = connectionFor(cluster); + const timeInvalidators = await getAllTimeInvalidators(connection); + + const allTokenManagerIds = timeInvalidators.map( + (timeInvalidator) => timeInvalidator.parsed.tokenManager + ); + const tokenManagerIdChunks = chunkArray(allTokenManagerIds, 100); + console.log(`> Looking up ${timeInvalidators.length} token managers`); + + const tokenManagers: AccountData[] = []; + for (let i = 0; i < tokenManagerIdChunks.length; i++) { + const tokenManagerIds = tokenManagerIdChunks[i]!; + console.log( + `>> [${i}/${ + tokenManagerIdChunks.length - 1 + }] batch token manager lookup [${tokenManagerIds.length}]` + ); + const singleBatch = ( + await getTokenManagers(connection, tokenManagerIds) + ).filter((x): x is AccountData => x.parsed !== null); + tokenManagers.push(...singleBatch); + await new Promise((r) => setTimeout(r, 100)); + } + const tokenManagersById = tokenManagers.reduce((acc, tm) => { + acc[tm.pubkey?.toString()] = tm; + return acc; + }, {} as { [str: string]: AccountData }); + + console.log(timeInvalidators.length); + console.log( + JSON.stringify( + timeInvalidators + .sort( + (a, b) => + minExpiration(b, tokenManagersById) - + minExpiration(a, tokenManagersById) + ) + .map((ti) => ({ + t: minExpiration(ti, tokenManagersById), + s: tokenManagersById[ti.parsed.tokenManager.toString()]?.parsed.state, + })) + .reverse() + ) + ); +}; + +const minExpiration = ( + timeInvalidator: AccountData, + tokenManagerdsById: { [str: string]: AccountData } +) => { + return timeInvalidator.parsed.expiration + ? timeInvalidator.parsed.expiration?.toNumber() ?? 0 + : timeInvalidator.parsed.durationSeconds + ? (tokenManagerdsById[ + timeInvalidator.parsed.tokenManager.toString() + ]?.parsed.stateChangedAt.toNumber() ?? 0) + + timeInvalidator.parsed.durationSeconds?.toNumber() + : 0; +}; + +main("mainnet-beta").catch((e) => console.log(e)); diff --git a/tools/getTotalTokenManagers.ts b/tools/getTotalTokenManagers.ts new file mode 100644 index 000000000..c427a2860 --- /dev/null +++ b/tools/getTotalTokenManagers.ts @@ -0,0 +1,65 @@ +import { BorshAccountsCoder, utils } from "@project-serum/anchor"; +import dotenv from "dotenv"; + +import { TIME_INVALIDATOR_ADDRESS } from "../src/programs/timeInvalidator"; +import type { TokenManagerData } from "../src/programs/tokenManager"; +import { + TOKEN_MANAGER_ADDRESS, + TOKEN_MANAGER_IDL, +} from "../src/programs/tokenManager"; +import { connectionFor } from "./connection"; + +dotenv.config(); + +export const main = async (cluster: string) => { + const connection = connectionFor(cluster); + const timeInvalidatorAccounts = await connection.getProgramAccounts( + TIME_INVALIDATOR_ADDRESS, + { + filters: [ + { + memcmp: { + offset: 0, + bytes: utils.bytes.bs58.encode( + BorshAccountsCoder.accountDiscriminator("timeInvalidator") + ), + }, + }, + ], + } + ); + console.log(timeInvalidatorAccounts.length); + + const tokenManagerAccounts = await connection.getProgramAccounts( + TOKEN_MANAGER_ADDRESS, + { + filters: [ + { + memcmp: { + offset: 0, + bytes: utils.bytes.bs58.encode( + BorshAccountsCoder.accountDiscriminator("tokenManager") + ), + }, + }, + ], + } + ); + const coder = new BorshAccountsCoder(TOKEN_MANAGER_IDL); + const tokenManagers: TokenManagerData[] = []; + tokenManagerAccounts.forEach((account) => { + try { + const entry = coder.decode( + "tokenManager", + account.account.data + ); + tokenManagers.push(entry); + } catch (e) { + console.log(`Failed to decode ${account.pubkey.toString()}`); + } + }); + + console.log(tokenManagerAccounts.length, tokenManagers.length); +}; + +main("mainnet-beta").catch((e) => console.log(e)); diff --git a/tools/migrateToCSS.ts b/tools/migrateToCSS.ts deleted file mode 100644 index 5cc4e409f..000000000 --- a/tools/migrateToCSS.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { executeTransaction } from "@cardinal/common"; -import * as anchor from "@project-serum/anchor"; -import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; -import dotenv from "dotenv"; - -import { withMigrate } from "../src"; -import { connectionFor } from "./connection"; - -dotenv.config(); - -const wallet = new anchor.Wallet( - Keypair.fromSecretKey( - anchor.utils.bytes.bs58.decode(process.env.MIGRATE_KEYPAIR || "") - ) -); // your wallet's secret key -const rulesetName = "ruleset-no-checks"; - -const main = async (cluster = "devnet") => { - const connection = connectionFor(cluster); - const transaction = new Transaction(); - - // TODO replace with get all token managers of kind Permissioned - const mintId = new PublicKey("pubkey"); - const accounts = ( - await connection.getTokenLargestAccounts(mintId) - ).value.filter((account) => account.uiAmount && account.uiAmount > 0); - if (accounts.length > 1) { - throw "Invalid mint, supply greater that one"; - } - const holderTokenAccount = accounts[0]!; - - await withMigrate( - transaction, - connection, - wallet, - mintId, - rulesetName, - holderTokenAccount.address, - wallet.publicKey - ); - - try { - const txid = await executeTransaction(connection, transaction, wallet, {}); - console.log( - `Successfully migrated token to CSS standard https://explorer.solana.com/tx/${txid}?cluster=${cluster}.` - ); - } catch (e) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - console.log(`Transactionn failed: ${e}`); - } -}; - -main().catch((e) => console.log(e)); diff --git a/tools/migration/batchMigrate.ts b/tools/migration/batchMigrate.ts new file mode 100644 index 000000000..2c1482ed0 --- /dev/null +++ b/tools/migration/batchMigrate.ts @@ -0,0 +1,162 @@ +import type { AccountData } from "@cardinal/common"; +import { + chunkArray, + findMintEditionId, + findMintMetadataId, + METADATA_PROGRAM_ID, +} from "@cardinal/common"; +import { BorshAccountsCoder, utils, Wallet } from "@project-serum/anchor"; +import { + getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { SystemProgram, Transaction } from "@solana/web3.js"; +import { BN } from "bn.js"; +import * as dotenv from "dotenv"; + +import type { TokenManagerData } from "../../src/programs/tokenManager"; +import { + TOKEN_MANAGER_ADDRESS, + TOKEN_MANAGER_IDL, + TokenManagerKind, + tokenManagerProgram, +} from "../../src/programs/tokenManager"; +import { + findMintManagerId, + findTokenManagerAddress, +} from "../../src/programs/tokenManager/pda"; +import { connectionFor } from "../connection"; +import { executeTransactionBatches, keypairFrom } from "../utils"; + +dotenv.config(); + +const BATCH_SIZE = 4; +const PARALLET_BATCH_SIZE = 50; +const DRY_RUN = true; + +const wallet = keypairFrom(process.env.WALLET ?? ""); + +const main = async (cluster = "devnet") => { + const connection = connectionFor(cluster); + console.log(wallet.publicKey.toString()); + + console.log(`\n1/3 Fetching data...`); + const programAccounts = await connection.getProgramAccounts( + TOKEN_MANAGER_ADDRESS, + { + filters: [ + { + memcmp: { + offset: 0, + bytes: utils.bytes.bs58.encode( + BorshAccountsCoder.accountDiscriminator("tokenManager") + ), + }, + }, + { + memcmp: { + offset: 91, + bytes: utils.bytes.bs58.encode( + new BN(TokenManagerKind.Permissioned).toArrayLike(Buffer, "le", 1) + ), + }, + }, + ], + } + ); + console.log( + "Total found ", + programAccounts.length, + programAccounts.map((p) => p.pubkey.toString()) + ); + + const tokenManagerDatas: AccountData[] = []; + const coder = new BorshAccountsCoder(TOKEN_MANAGER_IDL); + programAccounts.forEach((account) => { + try { + const tokenManagerData: TokenManagerData = coder.decode( + "tokenManager", + account.account.data + ); + if ( + tokenManagerData + // tokenManagerData.invalidators + // .map((s) => s.toString()) + // .includes("brvnPPYVUpU2ZQqmTX7XxzZErdtFj4brXH8CCicbbB9") + ) { + tokenManagerDatas.push({ + ...account, + parsed: tokenManagerData, + }); + } + } catch (e) { + console.log(`Failed to decode token manager data`); + } + }); + console.log("Total found ", tokenManagerDatas.length); + + console.log( + `\n1/3 Building transactions ${tokenManagerDatas.length} data...` + ); + const chunks = chunkArray(tokenManagerDatas, BATCH_SIZE); + const txs: Transaction[] = []; + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]!; + console.log(`${i + 1}/${chunks.length}`); + const tx = new Transaction(); + for (let j = 0; j < chunk.length; j++) { + const tokenManagerData = chunk[j]!; + console.log( + `>>[${i}/${chunks.length}][${j + 1}/${ + chunk.length + }] (${tokenManagerData.parsed.mint.toString()})` + ); + const mintId = tokenManagerData.parsed.mint; + const ix = await tokenManagerProgram(connection, new Wallet(wallet)) + .methods.migrate() + .accountsStrict({ + mintManager: findMintManagerId(mintId), + tokenManager: findTokenManagerAddress(mintId), + tokenManagerTokenAccount: getAssociatedTokenAddressSync( + mintId, + findTokenManagerAddress(mintId), + true + ), + mint: mintId, + mintMetadata: findMintMetadataId(mintId), + mintEdition: findMintEditionId(mintId), + holderTokenAccount: tokenManagerData.parsed.recipientTokenAccount, + invalidator: wallet.publicKey, + collector: wallet.publicKey, + payer: wallet.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + mplTokenMetadata: METADATA_PROGRAM_ID, + }) + .instruction(); + tx.add(ix); + } + if (tx.instructions.length > 0) { + txs.push(tx); + } + } + + console.log( + `\n3/3 Executing ${txs.length} transactions batches=${PARALLET_BATCH_SIZE}...` + ); + if (!DRY_RUN) { + await executeTransactionBatches(connection, txs, new Wallet(wallet), { + batchSize: PARALLET_BATCH_SIZE, + successHandler: (txid, { i, j, it, jt }) => + console.log( + `>> ${i + 1}/${it} ${ + j + 1 + }/${jt} https://explorer.solana.com/tx/${txid}` + ), + errorHandler: (e, { i, j, it, jt }) => + console.log(`>> ${i + 1}/${it} ${j + 1}/${jt} error=`, e), + }); + } +}; + +main("mainnet-beta").catch((e) => console.log(e)); diff --git a/tools/ccsMigration/hyperspheres.ts b/tools/migration/hyperspheres.ts similarity index 100% rename from tools/ccsMigration/hyperspheres.ts rename to tools/migration/hyperspheres.ts diff --git a/tools/utils.ts b/tools/utils.ts new file mode 100644 index 000000000..65059c43e --- /dev/null +++ b/tools/utils.ts @@ -0,0 +1,112 @@ +import { chunkArray, logError } from "@cardinal/common"; +import type { Wallet as IWallet } from "@coral-xyz/anchor/dist/cjs/provider"; +import { utils } from "@project-serum/anchor"; +import type { ConfirmOptions, Connection, Transaction } from "@solana/web3.js"; +import { Keypair, sendAndConfirmRawTransaction } from "@solana/web3.js"; + +export const keypairFrom = (s: string, n?: string): Keypair => { + try { + if (s.includes("[")) { + return Keypair.fromSecretKey( + Buffer.from( + s + .replace("[", "") + .replace("]", "") + .split(",") + .map((c) => parseInt(c)) + ) + ); + } else { + return Keypair.fromSecretKey(utils.bytes.bs58.decode(s)); + } + } catch (e) { + try { + return Keypair.fromSecretKey( + Buffer.from( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + JSON.parse( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-var-requires + require("fs").readFileSync(s, { + encoding: "utf-8", + }) + ) + ) + ); + } catch (e) { + process.stdout.write(`${n ?? "keypair"} is not valid keypair`); + process.exit(1); + } + } +}; + +export async function executeTransactionBatches( + connection: Connection, + txs: Transaction[], + wallet: IWallet, + config?: { + signers?: Keypair[][]; + batchSize?: number; + successHandler?: ( + txid: string, + ix: { i: number; j: number; it: number; jt: number } + ) => void; + errorHandler?: ( + e: unknown, + ix: { i: number; j: number; it: number; jt: number } + ) => T; + confirmOptions?: ConfirmOptions; + } +): Promise<(string | null | T)[]> { + const batchLength = config?.batchSize ?? txs.length; + const batchedTxs = chunkArray(txs, batchLength); + const txids: (string | T | null)[] = []; + for (let i = 0; i < batchedTxs.length; i++) { + const batch = batchedTxs[i]; + if (batch) { + const latestBlockhash = (await connection.getLatestBlockhash()).blockhash; + const batchSignedTxs = await wallet.signAllTransactions( + batch.map((tx, j) => { + tx.recentBlockhash = latestBlockhash; + tx.feePayer = wallet.publicKey; + if (config?.signers?.at(i * batchLength + j)) { + tx.partialSign(...(config?.signers.at(i * batchLength + j) ?? [])); + } + return tx; + }) + ); + const batchTxids = await Promise.all( + batchSignedTxs.map(async (tx, j) => { + try { + const txid = await sendAndConfirmRawTransaction( + connection, + tx.serialize(), + config?.confirmOptions + ); + if (config?.successHandler) { + config?.successHandler(txid, { + i, + it: batchedTxs.length, + j, + jt: batchSignedTxs.length, + }); + } + return txid; + } catch (e) { + if (config?.errorHandler) { + return config?.errorHandler(e, { + i, + it: batchedTxs.length, + j, + jt: batchSignedTxs.length, + }); + } + logError(e); + return null; + } + }) + ); + txids.push(...batchTxids); + } + } + return txids; +}