From b05289fcfa25ff24dfd520e21a45630d633ac87e Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Mon, 23 Sep 2024 01:11:51 +0800 Subject: [PATCH 1/3] Moving ATA checks and creation internal to the IX for more efficient stack usage. --- .gitignore | 2 + Cargo.lock | 1 + clients/js/src/generated/errors/mplHybrid.ts | 39 ++++++++++ clients/js/test/capture.test.ts | 24 +++++- clients/js/test/release.test.ts | 39 +++++++++- .../rust/src/generated/errors/mpl_hybrid.rs | 9 +++ idls/mpl_hybrid.json | 15 ++++ programs/mpl-hybrid/Cargo.toml | 19 +++-- programs/mpl-hybrid/src/error.rs | 37 +++++++++ .../mpl-hybrid/src/instructions/capture.rs | 70 ++++++++++++----- .../mpl-hybrid/src/instructions/release.rs | 75 +++++++++++++------ programs/mpl-hybrid/src/lib.rs | 1 + programs/mpl-hybrid/src/utils.rs | 49 ++++++++++++ 13 files changed, 329 insertions(+), 51 deletions(-) create mode 100644 programs/mpl-hybrid/src/utils.rs diff --git a/.gitignore b/.gitignore index 299cfee..b5631f6 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ dist .yarn programs/.bin clients/js/output.json +mp14o4AQcmE5meFDxCscervMc1E4zyKEyDp3398PcwU.json +MPL4o4wMzndgh8T1NVDxELQCj5UQfYTYEkabX3wNKtb.json diff --git a/Cargo.lock b/Cargo.lock index 99e8878..4f13dd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2512,6 +2512,7 @@ dependencies = [ "mpl-utils", "proptest", "solana-program", + "spl-associated-token-account", "spl-token 3.5.0", "spl-token-2022 1.0.0", "spl-token-metadata-interface", diff --git a/clients/js/src/generated/errors/mplHybrid.ts b/clients/js/src/generated/errors/mplHybrid.ts index 18a0412..3cb91d8 100644 --- a/clients/js/src/generated/errors/mplHybrid.ts +++ b/clients/js/src/generated/errors/mplHybrid.ts @@ -187,6 +187,45 @@ export class InvalidUpdateAuthorityError extends ProgramError { codeToErrorMap.set(0x177c, InvalidUpdateAuthorityError); nameToErrorMap.set('InvalidUpdateAuthority', InvalidUpdateAuthorityError); +/** InvalidTokenAccount: Invalid Token Account */ +export class InvalidTokenAccountError extends ProgramError { + override readonly name: string = 'InvalidTokenAccount'; + + readonly code: number = 0x177d; // 6013 + + constructor(program: Program, cause?: Error) { + super('Invalid Token Account', program, cause); + } +} +codeToErrorMap.set(0x177d, InvalidTokenAccountError); +nameToErrorMap.set('InvalidTokenAccount', InvalidTokenAccountError); + +/** InvalidTokenAccountOwner: Invalid Token Account Owner */ +export class InvalidTokenAccountOwnerError extends ProgramError { + override readonly name: string = 'InvalidTokenAccountOwner'; + + readonly code: number = 0x177e; // 6014 + + constructor(program: Program, cause?: Error) { + super('Invalid Token Account Owner', program, cause); + } +} +codeToErrorMap.set(0x177e, InvalidTokenAccountOwnerError); +nameToErrorMap.set('InvalidTokenAccountOwner', InvalidTokenAccountOwnerError); + +/** InvalidTokenAccountMint: Invalid Token Account Mint */ +export class InvalidTokenAccountMintError extends ProgramError { + override readonly name: string = 'InvalidTokenAccountMint'; + + readonly code: number = 0x177f; // 6015 + + constructor(program: Program, cause?: Error) { + super('Invalid Token Account Mint', program, cause); + } +} +codeToErrorMap.set(0x177f, InvalidTokenAccountMintError); +nameToErrorMap.set('InvalidTokenAccountMint', InvalidTokenAccountMintError); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/js/test/capture.test.ts b/clients/js/test/capture.test.ts index 9a876a9..d648074 100644 --- a/clients/js/test/capture.test.ts +++ b/clients/js/test/capture.test.ts @@ -2,6 +2,7 @@ import test from 'ava'; import { generateSigner, publicKey } from '@metaplex-foundation/umi'; import { createFungible, + fetchDigitalAssetWithAssociatedToken, mintV1, TokenStandard, } from '@metaplex-foundation/mpl-token-metadata'; @@ -9,7 +10,7 @@ import { string, publicKey as publicKeySerializer, } from '@metaplex-foundation/umi/serializers'; -import { addCollectionPlugin, transfer } from '@metaplex-foundation/mpl-core'; +import { addCollectionPlugin, fetchAsset, transfer } from '@metaplex-foundation/mpl-core'; import { captureV1, EscrowV1, @@ -94,6 +95,18 @@ test('it can swap tokens for an asset', async (t) => { solFeeAmount: 1_000_000n, }); + const userTokenBefore = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + t.deepEqual(userTokenBefore.token.amount, 1000n); + try { + await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + t.fail('Escrow token account should not exist'); + } catch (e) { + t.is(e.name, 'AccountNotFoundError'); + } + + const assetBefore = await fetchAsset(umi, assets[0].publicKey); + t.is(assetBefore.owner, publicKey(escrow)); + await captureV1(umi, { owner: umi.identity, escrow, @@ -102,6 +115,15 @@ test('it can swap tokens for an asset', async (t) => { feeProjectAccount: escrowData.feeLocation, token: tokenMint.publicKey, }).sendAndConfirm(umi); + + const escrowTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + t.deepEqual(escrowTokenAfter.token.amount, 5n); + const userTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + t.deepEqual(userTokenAfter.token.amount, 994n); + const feeTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, escrowData.feeLocation); + t.deepEqual(feeTokenAfter.token.amount, 1n); + const assetAfter = await fetchAsset(umi, assets[0].publicKey); + t.is(assetAfter.owner, umi.identity.publicKey); }); test('it can swap tokens for an asset as UpdateDelegate', async (t) => { diff --git a/clients/js/test/release.test.ts b/clients/js/test/release.test.ts index df0c6f2..2a80dc3 100644 --- a/clients/js/test/release.test.ts +++ b/clients/js/test/release.test.ts @@ -2,6 +2,7 @@ import test from 'ava'; import { generateSigner, publicKey } from '@metaplex-foundation/umi'; import { createFungible, + fetchDigitalAssetWithAssociatedToken, mintV1, TokenStandard, } from '@metaplex-foundation/mpl-token-metadata'; @@ -9,7 +10,7 @@ import { string, publicKey as publicKeySerializer, } from '@metaplex-foundation/umi/serializers'; -import { addCollectionPlugin } from '@metaplex-foundation/mpl-core'; +import { addCollectionPlugin, fetchAsset } from '@metaplex-foundation/mpl-core'; import { EscrowV1, fetchEscrowV1, @@ -83,6 +84,17 @@ test('it can swap an asset for tokens', async (t) => { solFeeAmount: 1_000_000n, }); + const escrowTokenBefore = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + t.deepEqual(escrowTokenBefore.token.amount, 1000n); + try { + await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + t.fail('User token account should not exist'); + } catch (e) { + t.is(e.name, 'AccountNotFoundError'); + } + + t.is(assets[0].owner, umi.identity.publicKey); + await releaseV1(umi, { owner: umi.identity, escrow, @@ -91,6 +103,13 @@ test('it can swap an asset for tokens', async (t) => { feeProjectAccount: escrowData.feeLocation, token: tokenMint.publicKey, }).sendAndConfirm(umi); + + const escrowTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + t.deepEqual(escrowTokenAfter.token.amount, 995n); + const userTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + t.deepEqual(userTokenAfter.token.amount, 5n); + const assetAfter = await fetchAsset(umi, assets[0].publicKey); + t.is(assetAfter.owner, publicKey(escrow)); }); test('it can swap an asset for tokens as UpdateDelegate', async (t) => { @@ -167,6 +186,17 @@ test('it can swap an asset for tokens as UpdateDelegate', async (t) => { solFeeAmount: 1_000_000n, }); + const escrowTokenBefore = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + t.deepEqual(escrowTokenBefore.token.amount, 1000n); + try { + await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + t.fail('User token account should not exist'); + } catch (e) { + t.is(e.name, 'AccountNotFoundError'); + } + + t.is(assets[0].owner, umi.identity.publicKey); + await releaseV1(umi, { owner: umi.identity, authority: escrow, @@ -176,4 +206,11 @@ test('it can swap an asset for tokens as UpdateDelegate', async (t) => { feeProjectAccount: escrowData.feeLocation, token: tokenMint.publicKey, }).sendAndConfirm(umi); + + const escrowTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + t.deepEqual(escrowTokenAfter.token.amount, 995n); + const userTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + t.deepEqual(userTokenAfter.token.amount, 5n); + const assetAfter = await fetchAsset(umi, assets[0].publicKey); + t.is(assetAfter.owner, publicKey(escrow)); }); diff --git a/clients/rust/src/generated/errors/mpl_hybrid.rs b/clients/rust/src/generated/errors/mpl_hybrid.rs index e93cfe0..9834ad8 100644 --- a/clients/rust/src/generated/errors/mpl_hybrid.rs +++ b/clients/rust/src/generated/errors/mpl_hybrid.rs @@ -49,6 +49,15 @@ pub enum MplHybridError { /// 6012 (0x177C) - Invalid Update Authority #[error("Invalid Update Authority")] InvalidUpdateAuthority, + /// 6013 (0x177D) - Invalid Token Account + #[error("Invalid Token Account")] + InvalidTokenAccount, + /// 6014 (0x177E) - Invalid Token Account Owner + #[error("Invalid Token Account Owner")] + InvalidTokenAccountOwner, + /// 6015 (0x177F) - Invalid Token Account Mint + #[error("Invalid Token Account Mint")] + InvalidTokenAccountMint, } impl solana_program::program_error::PrintProgramError for MplHybridError { diff --git a/idls/mpl_hybrid.json b/idls/mpl_hybrid.json index 620cd53..01c60b2 100644 --- a/idls/mpl_hybrid.json +++ b/idls/mpl_hybrid.json @@ -771,6 +771,21 @@ "code": 6012, "name": "InvalidUpdateAuthority", "msg": "Invalid Update Authority" + }, + { + "code": 6013, + "name": "InvalidTokenAccount", + "msg": "Invalid Token Account" + }, + { + "code": 6014, + "name": "InvalidTokenAccountOwner", + "msg": "Invalid Token Account Owner" + }, + { + "code": 6015, + "name": "InvalidTokenAccountMint", + "msg": "Invalid Token Account Mint" } ], "metadata": { diff --git a/programs/mpl-hybrid/Cargo.toml b/programs/mpl-hybrid/Cargo.toml index ec9fdb3..9d99330 100644 --- a/programs/mpl-hybrid/Cargo.toml +++ b/programs/mpl-hybrid/Cargo.toml @@ -1,19 +1,19 @@ [package] -name = "mpl-hybrid-program" -version = "0.0.1" description = "The MPL Hybrid program" edition = "2021" +name = "mpl-hybrid-program" +version = "0.0.1" [lib] crate-type = ["cdylib", "lib"] name = "mpl_hybrid" [features] +cpi = ["no-entrypoint"] +default = [] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] # idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] @@ -21,14 +21,17 @@ anchor-lang = { version = "~0.29", features = ["init-if-needed"] } anchor-spl = { version = "~0.29" } arrayref = "0.3.6" getrandom = { version = "0.2.9", features = ["custom"] } +mpl-core = "0.7.1" +mpl-utils = "0.3.5" solana-program = "=1.17.22" -winnow = "=0.4.1" -toml_datetime = "=0.6.5" +spl-associated-token-account = { version = "2.3.0", features = [ + "no-entrypoint", +] } spl-token = { version = "3.2.0", features = ["no-entrypoint"] } spl-token-2022 = { version = "~1.0", features = ["no-entrypoint"] } spl-token-metadata-interface = { version = "0.2.0" } -mpl-core = "0.7.1" -mpl-utils = "0.3.5" +toml_datetime = "=0.6.5" +winnow = "=0.4.1" [dev-dependencies] proptest = { version = "1.0" } diff --git a/programs/mpl-hybrid/src/error.rs b/programs/mpl-hybrid/src/error.rs index 5972cc1..21896b1 100644 --- a/programs/mpl-hybrid/src/error.rs +++ b/programs/mpl-hybrid/src/error.rs @@ -2,30 +2,67 @@ use anchor_lang::prelude::*; #[error_code] pub enum MplHybridError { + /// 6000 (0x1770) - Invalid Collection #[msg("Invalid Collection")] InvalidCollection, + + /// 6001 (0x1771) - Collection Authority does not match signer #[msg("Collection Authority does not match signer")] InvalidCollectionAuthority, + + /// 6002 (0x1772) - Error in the randomness #[msg("Error in the randomness")] RandomnessError, + + /// 6003 (0x1773) - Invalid Fee Constant Wallet #[msg("Invalid Fee Constant Wallet")] InvalidConstantFeeWallet, + + /// 6004 (0x1774) - Invalid Project Fee Wallet #[msg("Invalid Project Fee Wallet")] InvalidProjectFeeWallet, + + /// 6005 (0x1775) - Invalid SlotHash Program Account #[msg("Invalid SlotHash Program Account")] InvalidSlotHash, + + /// 6006 (0x1776) - Invalid MPL CORE Program Account #[msg("Invalid MPL CORE Program Account")] InvalidMplCore, + + /// 6007 (0x1777) - Invalid Collection Account #[msg("Invalid Collection Account")] InvalidCollectionAccount, + + /// 6008 (0x1778) - Invalid Asset Account #[msg("Invalid Asset Account")] InvalidAssetAccount, + + /// 6009 (0x1779) - Max must be greater than Min #[msg("Max must be greater than Min")] MaxMustBeGreaterThanMin, + + /// 6010 (0x177A) - Invalid Mint Account #[msg("Invalid Mint Account")] InvalidMintAccount, + + /// 6011 (0x177B) - Numerical Overflow #[msg("Numerical Overflow")] NumericalOverflow, + + /// 6012 (0x177C) - Invalid Update Authority #[msg("Invalid Update Authority")] InvalidUpdateAuthority, + + /// 6013 (0x177D) - Invalid Token Account + #[msg("Invalid Token Account")] + InvalidTokenAccount, + + /// 6014 (0x177E) - Invalid Token Account Owner + #[msg("Invalid Token Account Owner")] + InvalidTokenAccountOwner, + + /// 6015 (0x177F) - Invalid Token Account Mint + #[msg("Invalid Token Account Mint")] + InvalidTokenAccountMint, } diff --git a/programs/mpl-hybrid/src/instructions/capture.rs b/programs/mpl-hybrid/src/instructions/capture.rs index 91ff2d1..780b6c0 100644 --- a/programs/mpl-hybrid/src/instructions/capture.rs +++ b/programs/mpl-hybrid/src/instructions/capture.rs @@ -1,15 +1,16 @@ -use crate::constants::*; use crate::error::MplHybridError; use crate::state::*; -use anchor_lang::prelude::*; +use crate::utils::validate_token_account; +use crate::{constants::*, utils::create_associated_token_account}; use anchor_lang::{ accounts::{program::Program, signer::Signer, unchecked_account::UncheckedAccount}, system_program::System, }; +use anchor_lang::{prelude::*, system_program}; use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token; use anchor_spl::token::Mint; -use anchor_spl::token::{Token, TokenAccount, Transfer}; +use anchor_spl::token::{Token, Transfer}; use arrayref::array_ref; use mpl_core::accounts::BaseAssetV1; use mpl_core::instructions::{ @@ -48,19 +49,13 @@ pub struct CaptureV1Ctx<'info> { )] collection: AccountInfo<'info>, - #[account(init_if_needed, - payer = owner, - associated_token::mint = token, - associated_token::authority = owner - )] - user_token_account: Account<'info, TokenAccount>, + /// CHECK: We check and initialize the token account below. + #[account(mut)] + user_token_account: AccountInfo<'info>, - #[account(init_if_needed, - payer = owner, - associated_token::mint = token, - associated_token::authority = escrow - )] - escrow_token_account: Account<'info, TokenAccount>, + /// CHECK: We check and initialize the token account below. + #[account(mut)] + escrow_token_account: AccountInfo<'info>, /// CHECK: This is a user defined account #[account( @@ -68,11 +63,9 @@ pub struct CaptureV1Ctx<'info> { )] token: Account<'info, Mint>, - #[account(init_if_needed, - payer = owner, - associated_token::mint = token, - associated_token::authority = fee_project_account)] - fee_token_account: Account<'info, TokenAccount>, + /// CHECK: We check and initialize the token account below. + #[account(mut)] + fee_token_account: AccountInfo<'info>, /// CHECK: We check against constant #[account(mut, @@ -123,6 +116,43 @@ pub fn handler_capture_v1(ctx: Context) -> Result<()> { let escrow_info = &escrow.to_account_info(); let system_info = &system_program.to_account_info(); + // The user token account should already exist. + validate_token_account(user_token_account, &owner.key(), &ctx.accounts.token.key())?; + + if escrow_token_account.owner == &system_program::ID { + create_associated_token_account( + owner, + &escrow.to_account_info(), + &ctx.accounts.token.to_account_info(), + escrow_token_account, + token_program, + system_program, + )?; + } else { + validate_token_account( + escrow_token_account, + &escrow.key(), + &ctx.accounts.token.key(), + )?; + } + + if fee_token_account.owner == &system_program::ID { + create_associated_token_account( + owner, + &fee_project_account.to_account_info(), + &ctx.accounts.token.to_account_info(), + fee_token_account, + token_program, + system_program, + )?; + } else { + validate_token_account( + fee_token_account, + &fee_project_account.key(), + &ctx.accounts.token.key(), + )?; + } + // We only fetch the Base assets because we only need to check the collection here. let asset_data = BaseAssetV1::from_bytes(&asset.to_account_info().data.borrow())?; // Check that the collection that the asset is a part of is the one this escrow is configured for. diff --git a/programs/mpl-hybrid/src/instructions/release.rs b/programs/mpl-hybrid/src/instructions/release.rs index 9031cbd..8cc2afc 100644 --- a/programs/mpl-hybrid/src/instructions/release.rs +++ b/programs/mpl-hybrid/src/instructions/release.rs @@ -1,15 +1,16 @@ use crate::constants::*; use crate::error::MplHybridError; use crate::state::*; +use crate::utils::{create_associated_token_account, validate_token_account}; use anchor_lang::prelude::*; use anchor_lang::{ - accounts::{program::Program, signer::Signer, unchecked_account::UncheckedAccount}, + accounts::{program::Program, signer::Signer}, system_program::System, }; use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token; use anchor_spl::token::Mint; -use anchor_spl::token::{Token, TokenAccount, Transfer}; +use anchor_spl::token::{Token, Transfer}; use mpl_core::accounts::BaseAssetV1; use mpl_core::instructions::{ TransferV1Cpi, TransferV1InstructionArgs, UpdateV1Cpi, UpdateV1InstructionArgs, @@ -17,6 +18,7 @@ use mpl_core::instructions::{ use mpl_core::types::UpdateAuthority; use mpl_utils::assert_signer; use solana_program::program::invoke; +use solana_program::system_program; #[derive(Accounts)] pub struct ReleaseV1Ctx<'info> { @@ -39,7 +41,7 @@ pub struct ReleaseV1Ctx<'info> { /// CHECK: We check the asset bellow #[account(mut)] - asset: UncheckedAccount<'info>, + asset: AccountInfo<'info>, /// CHECK: We check against escrow #[account(mut, @@ -47,19 +49,13 @@ pub struct ReleaseV1Ctx<'info> { )] collection: AccountInfo<'info>, - #[account(init_if_needed, - payer = owner, - associated_token::mint = token, - associated_token::authority = owner - )] - user_token_account: Account<'info, TokenAccount>, + /// CHECK: We check and initialize the token account below. + #[account(mut)] + user_token_account: AccountInfo<'info>, - #[account(init_if_needed, - payer = owner, - associated_token::mint = token, - associated_token::authority = escrow - )] - escrow_token_account: Account<'info, TokenAccount>, + /// CHECK: We check the token account below. + #[account(mut)] + escrow_token_account: AccountInfo<'info>, /// CHECK: This is a user defined account #[account( @@ -67,11 +63,9 @@ pub struct ReleaseV1Ctx<'info> { )] token: Account<'info, Mint>, - #[account(init_if_needed, - payer = owner, - associated_token::mint = token, - associated_token::authority = fee_project_account)] - fee_token_account: Account<'info, TokenAccount>, + /// CHECK: We check and initialize the token account below. + #[account(mut)] + fee_token_account: AccountInfo<'info>, /// CHECK: We check against constant #[account(mut, @@ -112,7 +106,7 @@ pub fn handler_release_v1(ctx: Context) -> Result<()> { let mpl_core = &mut ctx.accounts.mpl_core; let user_token_account = &mut ctx.accounts.user_token_account; let escrow_token_account = &mut ctx.accounts.escrow_token_account; - let _fee_token_account = &mut ctx.accounts.fee_token_account; + let fee_token_account = &mut ctx.accounts.fee_token_account; let fee_sol_account = &mut ctx.accounts.fee_sol_account; let fee_project_account = &mut ctx.accounts.fee_project_account; let system_program = &mut ctx.accounts.system_program; @@ -123,6 +117,45 @@ pub fn handler_release_v1(ctx: Context) -> Result<()> { let owner_info = &owner.to_account_info(); let system_info = &system_program.to_account_info(); + // Create idempotent + if user_token_account.owner == &system_program::ID { + solana_program::msg!("Creating user token account"); + create_associated_token_account( + owner, + owner, + &ctx.accounts.token.to_account_info(), + user_token_account, + token_program, + system_program, + )?; + } else { + validate_token_account(user_token_account, &owner.key(), &ctx.accounts.token.key())?; + } + + // The escrow token account should already exist. + validate_token_account( + escrow_token_account, + &escrow.key(), + &ctx.accounts.token.key(), + )?; + + if fee_token_account.owner == &system_program::ID { + create_associated_token_account( + owner, + &fee_project_account.to_account_info(), + &ctx.accounts.token.to_account_info(), + fee_token_account, + token_program, + system_program, + )?; + } else { + validate_token_account( + fee_token_account, + &fee_project_account.key(), + &ctx.accounts.token.key(), + )?; + } + // We only fetch the Base assets because we only need to check the collection here. let asset_data = BaseAssetV1::from_bytes(&asset.to_account_info().data.borrow())?; // Check that the collection that the asset is a part of is the one this escrow is configured for. diff --git a/programs/mpl-hybrid/src/lib.rs b/programs/mpl-hybrid/src/lib.rs index f19212b..abfa798 100644 --- a/programs/mpl-hybrid/src/lib.rs +++ b/programs/mpl-hybrid/src/lib.rs @@ -7,6 +7,7 @@ pub mod constants; pub mod error; pub mod instructions; pub mod state; +pub mod utils; #[program] pub mod mpl_hybrid { diff --git a/programs/mpl-hybrid/src/utils.rs b/programs/mpl-hybrid/src/utils.rs new file mode 100644 index 0000000..4613f9f --- /dev/null +++ b/programs/mpl-hybrid/src/utils.rs @@ -0,0 +1,49 @@ +use anchor_lang::prelude::*; +use solana_program::{program::invoke, program_pack::Pack}; +use spl_token::state::Account; + +use crate::error::MplHybridError; + +pub fn create_associated_token_account<'info>( + payer: &AccountInfo<'info>, + owner: &AccountInfo<'info>, + mint: &AccountInfo<'info>, + token_account: &AccountInfo<'info>, + token_program: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, +) -> Result<()> { + invoke( + &spl_associated_token_account::instruction::create_associated_token_account( + &payer.key(), + &owner.key(), + &mint.key(), + &spl_token::ID, + ), + &[ + payer.clone(), + owner.clone(), + mint.clone(), + token_account.clone(), + token_program.clone(), + system_program.clone(), + ], + ) + .map_err(Into::into) +} + +pub fn validate_token_account( + account: &AccountInfo<'_>, + owner: &Pubkey, + mint: &Pubkey, +) -> Result<()> { + let account_data = Account::unpack(&account.data.borrow())?; + if account.owner != &spl_token::ID { + return Err(MplHybridError::InvalidTokenAccount.into()); + } else if account_data.owner != *owner { + return Err(MplHybridError::InvalidTokenAccountOwner.into()); + } else if account_data.mint != *mint { + return Err(MplHybridError::InvalidTokenAccountMint.into()); + } + + Ok(()) +} From 7dbf198b34d5d000261c8228de026918d78f8671 Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Tue, 24 Sep 2024 10:44:30 -0400 Subject: [PATCH 2/3] Fix upload artifacts. --- .github/workflows/build-programs.yml | 1 + .github/workflows/build-rust-client.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build-programs.yml b/.github/workflows/build-programs.yml index 489f367..b245c0a 100644 --- a/.github/workflows/build-programs.yml +++ b/.github/workflows/build-programs.yml @@ -62,4 +62,5 @@ jobs: name: program-builds # First wildcard ensures exported paths are consistently under the programs folder. path: ./program*/.bin/*.so + include-hidden-files: true if-no-files-found: error diff --git a/.github/workflows/build-rust-client.yml b/.github/workflows/build-rust-client.yml index ef13be3..eb222d7 100644 --- a/.github/workflows/build-rust-client.yml +++ b/.github/workflows/build-rust-client.yml @@ -68,4 +68,5 @@ jobs: name: rust-client-builds # First wildcard ensures exported paths are consistently under the clients folder. path: ./targe*/release/*mpl_hybrid* + include-hidden-files: true if-no-files-found: error From c2e2718e14cc71f6082083709d6d205eba990732 Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Tue, 24 Sep 2024 13:29:45 -0400 Subject: [PATCH 3/3] Fixing formatting. --- clients/js/test/capture.test.ts | 36 ++++++++++++++++++++----- clients/js/test/release.test.ts | 48 +++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/clients/js/test/capture.test.ts b/clients/js/test/capture.test.ts index d648074..d60ae93 100644 --- a/clients/js/test/capture.test.ts +++ b/clients/js/test/capture.test.ts @@ -10,7 +10,11 @@ import { string, publicKey as publicKeySerializer, } from '@metaplex-foundation/umi/serializers'; -import { addCollectionPlugin, fetchAsset, transfer } from '@metaplex-foundation/mpl-core'; +import { + addCollectionPlugin, + fetchAsset, + transfer, +} from '@metaplex-foundation/mpl-core'; import { captureV1, EscrowV1, @@ -95,10 +99,18 @@ test('it can swap tokens for an asset', async (t) => { solFeeAmount: 1_000_000n, }); - const userTokenBefore = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + const userTokenBefore = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + umi.identity.publicKey + ); t.deepEqual(userTokenBefore.token.amount, 1000n); try { - await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + publicKey(escrow) + ); t.fail('Escrow token account should not exist'); } catch (e) { t.is(e.name, 'AccountNotFoundError'); @@ -116,11 +128,23 @@ test('it can swap tokens for an asset', async (t) => { token: tokenMint.publicKey, }).sendAndConfirm(umi); - const escrowTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + const escrowTokenAfter = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + publicKey(escrow) + ); t.deepEqual(escrowTokenAfter.token.amount, 5n); - const userTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + const userTokenAfter = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + umi.identity.publicKey + ); t.deepEqual(userTokenAfter.token.amount, 994n); - const feeTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, escrowData.feeLocation); + const feeTokenAfter = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + escrowData.feeLocation + ); t.deepEqual(feeTokenAfter.token.amount, 1n); const assetAfter = await fetchAsset(umi, assets[0].publicKey); t.is(assetAfter.owner, umi.identity.publicKey); diff --git a/clients/js/test/release.test.ts b/clients/js/test/release.test.ts index 2a80dc3..973dc79 100644 --- a/clients/js/test/release.test.ts +++ b/clients/js/test/release.test.ts @@ -84,10 +84,18 @@ test('it can swap an asset for tokens', async (t) => { solFeeAmount: 1_000_000n, }); - const escrowTokenBefore = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + const escrowTokenBefore = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + publicKey(escrow) + ); t.deepEqual(escrowTokenBefore.token.amount, 1000n); try { - await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + umi.identity.publicKey + ); t.fail('User token account should not exist'); } catch (e) { t.is(e.name, 'AccountNotFoundError'); @@ -104,9 +112,17 @@ test('it can swap an asset for tokens', async (t) => { token: tokenMint.publicKey, }).sendAndConfirm(umi); - const escrowTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + const escrowTokenAfter = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + publicKey(escrow) + ); t.deepEqual(escrowTokenAfter.token.amount, 995n); - const userTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + const userTokenAfter = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + umi.identity.publicKey + ); t.deepEqual(userTokenAfter.token.amount, 5n); const assetAfter = await fetchAsset(umi, assets[0].publicKey); t.is(assetAfter.owner, publicKey(escrow)); @@ -186,10 +202,18 @@ test('it can swap an asset for tokens as UpdateDelegate', async (t) => { solFeeAmount: 1_000_000n, }); - const escrowTokenBefore = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + const escrowTokenBefore = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + publicKey(escrow) + ); t.deepEqual(escrowTokenBefore.token.amount, 1000n); try { - await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + umi.identity.publicKey + ); t.fail('User token account should not exist'); } catch (e) { t.is(e.name, 'AccountNotFoundError'); @@ -207,9 +231,17 @@ test('it can swap an asset for tokens as UpdateDelegate', async (t) => { token: tokenMint.publicKey, }).sendAndConfirm(umi); - const escrowTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, publicKey(escrow)); + const escrowTokenAfter = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + publicKey(escrow) + ); t.deepEqual(escrowTokenAfter.token.amount, 995n); - const userTokenAfter = await fetchDigitalAssetWithAssociatedToken(umi, tokenMint.publicKey, umi.identity.publicKey); + const userTokenAfter = await fetchDigitalAssetWithAssociatedToken( + umi, + tokenMint.publicKey, + umi.identity.publicKey + ); t.deepEqual(userTokenAfter.token.amount, 5n); const assetAfter = await fetchAsset(umi, assets[0].publicKey); t.is(assetAfter.owner, publicKey(escrow));