diff --git a/Cargo.lock b/Cargo.lock index 9786c60d..a33562f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,6 +571,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" name = "autocrat" version = "1.0.0" dependencies = [ + "amm", "anchor-lang 0.28.0", "anchor-spl 0.28.0", "conditional_vault", diff --git a/app/src/AmmClient.ts b/app/src/AmmClient.ts index 6e10c507..dce34742 100644 --- a/app/src/AmmClient.ts +++ b/app/src/AmmClient.ts @@ -38,6 +38,10 @@ export class AmmClient { return new AmmClient(provider, programId || AMM_PROGRAM_ID, luts); } + getProgramId(): PublicKey { + return this.program.programId; + } + // both twap values need to be scaled beforehand createAmm( baseMint: PublicKey, diff --git a/app/src/AutocratClient.ts b/app/src/AutocratClient.ts new file mode 100644 index 00000000..36e27b85 --- /dev/null +++ b/app/src/AutocratClient.ts @@ -0,0 +1,106 @@ +import { AnchorProvider, Program } from "@coral-xyz/anchor"; +import { AddressLookupTableAccount, Keypair, PublicKey } from "@solana/web3.js"; + +import { Autocrat, IDL as AutocratIDL } from "./types/autocrat"; +import { + ConditionalVault, + IDL as ConditionalVaultIDL, +} from "./types/conditional_vault"; + +import * as ixs from "./instructions/amm"; +import BN from "bn.js"; +import { + AMM_PROGRAM_ID, + AUTOCRAT_PROGRAM_ID, + CONDITIONAL_VAULT_PROGRAM_ID, +} from "./constants"; +import { Amm, AmmWrapper } from "./types"; +import { getDaoTreasuryAddr } from "./utils"; +import { ConditionalVaultClient } from "./ConditionalVaultClient"; + +export type CreateClientParams = { + provider: AnchorProvider; + autocratProgramId?: PublicKey; + conditionalVaultProgramId?: PublicKey; +}; + +export class AutocratClient { + public readonly provider: AnchorProvider; + public readonly autocrat: Program; + public readonly vaultClient: ConditionalVaultClient; + public readonly luts: AddressLookupTableAccount[]; + + constructor( + provider: AnchorProvider, + autocratProgramId: PublicKey, + conditionalVaultProgramId: PublicKey, + luts: AddressLookupTableAccount[] + ) { + this.provider = provider; + this.autocrat = new Program( + AutocratIDL, + autocratProgramId, + provider + ); + this.vaultClient = ConditionalVaultClient.createClient({ provider, conditionalVaultProgramId }) + this.luts = luts; + } + + public static async createClient( + createAutocratClientParams: CreateClientParams + ): Promise { + let { provider, autocratProgramId, conditionalVaultProgramId } = + createAutocratClientParams; + + const luts: AddressLookupTableAccount[] = []; + + return new AutocratClient( + provider, + autocratProgramId || AUTOCRAT_PROGRAM_ID, + conditionalVaultProgramId || CONDITIONAL_VAULT_PROGRAM_ID, + luts + ); + } + + finalizeProposalIx( + proposal: PublicKey, + instruction: any, + dao: PublicKey, + passAmm: PublicKey, + failAmm: PublicKey, + openbookTwapPassMarket: PublicKey, + openbookTwapFailMarket: PublicKey, + baseVault: PublicKey, + quoteVault: PublicKey + ) { + const [daoTreasury] = getDaoTreasuryAddr(this.autocrat.programId, dao); + + return this.autocrat.methods + .finalizeProposal() + .accounts({ + proposal, + passAmm, + failAmm, + openbookTwapPassMarket, + openbookTwapFailMarket, + dao, + baseVault, + quoteVault, + vaultProgram: this.vaultClient.vaultProgram.programId, + daoTreasury, + }) + .remainingAccounts( + instruction.accounts + .concat({ + pubkey: instruction.programId, + isWritable: false, + isSigner: false, + }) + .map((meta: any) => + meta.pubkey.equals(daoTreasury) + ? { ...meta, isSigner: false } + : meta + ) + ); + } +} diff --git a/app/src/ConditionalVaultClient.ts b/app/src/ConditionalVaultClient.ts new file mode 100644 index 00000000..085c811f --- /dev/null +++ b/app/src/ConditionalVaultClient.ts @@ -0,0 +1,100 @@ +import { AnchorProvider, Program } from "@coral-xyz/anchor"; +import { AddressLookupTableAccount, Keypair, PublicKey } from "@solana/web3.js"; + +import { + ConditionalVault, + IDL as ConditionalVaultIDL, +} from "./types/conditional_vault"; + +import BN from "bn.js"; +import { + CONDITIONAL_VAULT_PROGRAM_ID, +} from "./constants"; +import { getATA, getVaultAddr, getVaultFinalizeMintAddr, getVaultRevertMintAddr } from "./utils"; +import { MethodsBuilder } from "@coral-xyz/anchor/dist/cjs/program/namespace/methods"; + +export type CreateClientParams = { + provider: AnchorProvider; + conditionalVaultProgramId?: PublicKey; +}; + +export class ConditionalVaultClient { + public readonly provider: AnchorProvider; + public readonly vaultProgram: Program; + public readonly luts: AddressLookupTableAccount[]; + + constructor( + provider: AnchorProvider, + conditionalVaultProgramId: PublicKey, + luts: AddressLookupTableAccount[] + ) { + this.provider = provider; + this.vaultProgram = new Program( + ConditionalVaultIDL, + conditionalVaultProgramId, + provider + ); + this.luts = luts; + } + + public static createClient( + createVaultClientParams: CreateClientParams + ): ConditionalVaultClient { + let { provider, conditionalVaultProgramId } = + createVaultClientParams; + + const luts: AddressLookupTableAccount[] = []; + + return new ConditionalVaultClient( + provider, + conditionalVaultProgramId || CONDITIONAL_VAULT_PROGRAM_ID, + luts + ); + } + + initializeVaultIx( + settlementAuthority: PublicKey, + underlyingTokenMint: PublicKey, + proposal: PublicKey, + ): MethodsBuilder { + const [vault] = getVaultAddr( + this.vaultProgram.programId, + settlementAuthority, + underlyingTokenMint, + proposal + ); + + const [conditionalOnFinalizeTokenMint] = getVaultFinalizeMintAddr(this.vaultProgram.programId, vault); + const [conditionalOnRevertTokenMint] = getVaultRevertMintAddr(this.vaultProgram.programId, vault); + + return this.vaultProgram.methods + .initializeConditionalVault( + settlementAuthority, + proposal + ) + .accounts({ + vault, + underlyingTokenMint, + vaultUnderlyingTokenAccount: getATA(underlyingTokenMint, vault)[0], + conditionalOnFinalizeTokenMint, + conditionalOnRevertTokenMint, + }) + } + + async initializeVault( + settlementAuthority: PublicKey, + underlyingTokenMint: PublicKey, + proposal: PublicKey, + ): Promise { + const [vault] = getVaultAddr( + this.vaultProgram.programId, + settlementAuthority, + underlyingTokenMint, + proposal + ); + + await this.initializeVaultIx(settlementAuthority, underlyingTokenMint, proposal).rpc(); + + return vault; + } +} diff --git a/app/src/constants.ts b/app/src/constants.ts index c4b490e1..99d6ea10 100644 --- a/app/src/constants.ts +++ b/app/src/constants.ts @@ -1,11 +1,14 @@ import { PublicKey } from "@solana/web3.js"; export const AUTOCRAT_PROGRAM_ID = new PublicKey( - "66629qDqH5vJuz4ZgaL1HVpeAC9kJXnzamMpvMJfr3kE" + "FuTPR6ScKMPHtZFwacq9qrtf9VjscawNEFTb2wSYr1gY" ); export const AMM_PROGRAM_ID = new PublicKey( "Ens7Gx99whnA8zZm6ZiFnWgGq3x76nXbSmh5gaaJqpAz" ); +export const CONDITIONAL_VAULT_PROGRAM_ID = new PublicKey( + "vAuLTQjV5AZx5f3UgE75wcnkxnQowWxThn1hGjfCVwP" +); export const META_MINT = new PublicKey( "3gN1WVEJwSHNWjo7hr87DgZp6zkf8kWgAJD29DmfE2Gr" diff --git a/app/src/types/autocrat.ts b/app/src/types/autocrat.ts index 96c93753..b3735217 100644 --- a/app/src/types/autocrat.ts +++ b/app/src/types/autocrat.ts @@ -70,6 +70,16 @@ export type Autocrat = { "isMut": false, "isSigner": false }, + { + "name": "passAmm", + "isMut": false, + "isSigner": false + }, + { + "name": "failAmm", + "isMut": false, + "isSigner": false + }, { "name": "openbookPassMarket", "isMut": false, @@ -122,6 +132,16 @@ export type Autocrat = { "isMut": true, "isSigner": false }, + { + "name": "passAmm", + "isMut": false, + "isSigner": false + }, + { + "name": "failAmm", + "isMut": false, + "isSigner": false + }, { "name": "openbookTwapPassMarket", "isMut": false, @@ -155,7 +175,10 @@ export type Autocrat = { { "name": "daoTreasury", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "TODO: use a different thing to prevent collision" + ] } ], "args": [] @@ -303,6 +326,18 @@ export type Autocrat = { "defined": "ProposalInstruction" } }, + { + "name": "passAmm", + "type": "publicKey" + }, + { + "name": "failAmm", + "type": "publicKey" + }, + { + "name": "ammNonce", + "type": "u64" + }, { "name": "openbookTwapPassMarket", "type": "publicKey" @@ -583,6 +618,16 @@ export const IDL: Autocrat = { "isMut": false, "isSigner": false }, + { + "name": "passAmm", + "isMut": false, + "isSigner": false + }, + { + "name": "failAmm", + "isMut": false, + "isSigner": false + }, { "name": "openbookPassMarket", "isMut": false, @@ -635,6 +680,16 @@ export const IDL: Autocrat = { "isMut": true, "isSigner": false }, + { + "name": "passAmm", + "isMut": false, + "isSigner": false + }, + { + "name": "failAmm", + "isMut": false, + "isSigner": false + }, { "name": "openbookTwapPassMarket", "isMut": false, @@ -668,7 +723,10 @@ export const IDL: Autocrat = { { "name": "daoTreasury", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "TODO: use a different thing to prevent collision" + ] } ], "args": [] @@ -816,6 +874,18 @@ export const IDL: Autocrat = { "defined": "ProposalInstruction" } }, + { + "name": "passAmm", + "type": "publicKey" + }, + { + "name": "failAmm", + "type": "publicKey" + }, + { + "name": "ammNonce", + "type": "u64" + }, { "name": "openbookTwapPassMarket", "type": "publicKey" diff --git a/app/src/types/conditional_vault.ts b/app/src/types/conditional_vault.ts index ba1f4132..282bde58 100644 --- a/app/src/types/conditional_vault.ts +++ b/app/src/types/conditional_vault.ts @@ -18,12 +18,12 @@ export type ConditionalVault = { { "name": "conditionalOnFinalizeTokenMint", "isMut": true, - "isSigner": true + "isSigner": false }, { "name": "conditionalOnRevertTokenMint", "isMut": true, - "isSigner": true + "isSigner": false }, { "name": "vaultUnderlyingTokenAccount", @@ -57,8 +57,8 @@ export type ConditionalVault = { "type": "publicKey" }, { - "name": "nonce", - "type": "u64" + "name": "proposal", + "type": "publicKey" } ] }, @@ -352,12 +352,12 @@ export type ConditionalVault = { "type": "publicKey" }, { - "name": "nonce", + "name": "proposal", "docs": [ - "A nonce to allow a single account to be the settlement authority of multiple", - "vaults with the same underlying token mints." + "We need to be able to create multiple vault for a single underlying token", + "account, so we use proposal as a PDA seed." ], - "type": "u64" + "type": "publicKey" }, { "name": "underlyingTokenAccount", @@ -450,12 +450,12 @@ export const IDL: ConditionalVault = { { "name": "conditionalOnFinalizeTokenMint", "isMut": true, - "isSigner": true + "isSigner": false }, { "name": "conditionalOnRevertTokenMint", "isMut": true, - "isSigner": true + "isSigner": false }, { "name": "vaultUnderlyingTokenAccount", @@ -489,8 +489,8 @@ export const IDL: ConditionalVault = { "type": "publicKey" }, { - "name": "nonce", - "type": "u64" + "name": "proposal", + "type": "publicKey" } ] }, @@ -784,12 +784,12 @@ export const IDL: ConditionalVault = { "type": "publicKey" }, { - "name": "nonce", + "name": "proposal", "docs": [ - "A nonce to allow a single account to be the settlement authority of multiple", - "vaults with the same underlying token mints." + "We need to be able to create multiple vault for a single underlying token", + "account, so we use proposal as a PDA seed." ], - "type": "u64" + "type": "publicKey" }, { "name": "underlyingTokenAccount", diff --git a/app/src/utils/pda.ts b/app/src/utils/pda.ts index ebc1bb54..44a48c55 100644 --- a/app/src/utils/pda.ts +++ b/app/src/utils/pda.ts @@ -5,7 +5,49 @@ import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, } from "@solana/spl-token"; -import BN from 'bn.js'; +import BN from "bn.js"; + +export const getVaultAddr = ( + programId: PublicKey, + settlementAuthority: PublicKey, + underlyingTokenMint: PublicKey, + proposal: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [ + utils.bytes.utf8.encode("conditional_vault"), + settlementAuthority.toBuffer(), + underlyingTokenMint.toBuffer(), + proposal.toBuffer(), + ], + programId + ); +}; + +export const getVaultFinalizeMintAddr = ( + programId: PublicKey, + vault: PublicKey, +) => { + return getVaultMintAddr(programId, vault, "conditional_on_finalize_mint"); +} + +export const getVaultRevertMintAddr = ( + programId: PublicKey, + vault: PublicKey, +) => { + return getVaultMintAddr(programId, vault, "conditional_on_revert_mint"); +} + +const getVaultMintAddr = ( + programId: PublicKey, + vault: PublicKey, + seed: string +) => { + return PublicKey.findProgramAddressSync( + [utils.bytes.utf8.encode(seed), vault.toBuffer()], + programId + ); +}; export const getDaoAddr = (programId: PublicKey): [PublicKey, number] => { return PublicKey.findProgramAddressSync( @@ -15,13 +57,10 @@ export const getDaoAddr = (programId: PublicKey): [PublicKey, number] => { }; export const getDaoTreasuryAddr = ( - programId: PublicKey + programId: PublicKey, + dao: PublicKey ): [PublicKey, number] => { - let [dao] = getDaoAddr(programId); - return PublicKey.findProgramAddressSync( - [utils.bytes.utf8.encode("dao_treasury"), dao.toBuffer()], - programId - ); + return PublicKey.findProgramAddressSync([dao.toBuffer()], programId); }; export const getProposalAddr = ( @@ -65,22 +104,18 @@ export const getAmmAddr = ( utils.bytes.utf8.encode("amm__"), baseMint.toBuffer(), quoteMint.toBuffer(), - nonce.toBuffer('le', 8) + nonce.toBuffer("le", 8), ], programId ); }; - export const getAmmLpMintAddr = ( programId: PublicKey, - amm: PublicKey, + amm: PublicKey ): [PublicKey, number] => { return PublicKey.findProgramAddressSync( - [ - utils.bytes.utf8.encode("amm_lp_mint"), - amm.toBuffer(), - ], + [utils.bytes.utf8.encode("amm_lp_mint"), amm.toBuffer()], programId ); }; diff --git a/justfile b/justfile index 1b9e8747..3edd2977 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,9 @@ test: (find programs && find tests) | entr -s 'clear && RUST_LOG= anchor test' +test-only: + (find programs && find tests) | entr -sc 'RUST_LOG= anchor test --skip-build' + # build-verifiable autocrat_v0 build-verifiable PROGRAM_NAME: solana-verify build --library-name {{ PROGRAM_NAME }} -b ellipsislabs/solana:1.16.10 @@ -15,7 +18,7 @@ upgrade-idl PROGRAM_NAME PROGRAM_ID CLUSTER: anchor idl upgrade --filepath ./target/idl/{{ PROGRAM_NAME }}.json {{ PROGRAM_ID }} --provider.cluster {{ CLUSTER }} bankrun: - (find programs && find tests) | entr -csr 'anchor build -p autocrat_v0 && RUST_LOG= yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/autocratV0.ts' + (find programs && find tests) | entr -csr 'anchor build -p autocrat && RUST_LOG= yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/autocrat.ts' test-amm: find programs tests | entr -csr 'anchor build -p amm && RUST_LOG= yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/amm.ts' diff --git a/programs/autocrat/Cargo.toml b/programs/autocrat/Cargo.toml index 9df3690e..11983895 100644 --- a/programs/autocrat/Cargo.toml +++ b/programs/autocrat/Cargo.toml @@ -23,3 +23,4 @@ openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", feature openbook-twap = { git = "https://github.com/metaDAOproject/openbook-twap.git", features = ["cpi"] } solana-program = "~1.16.1" conditional_vault = { path = "../conditional_vault", features = ["cpi"] } +amm = { path = "../amm", features = ["cpi"] } diff --git a/programs/autocrat/src/lib.rs b/programs/autocrat/src/lib.rs index 7813dd8b..211a0cfb 100644 --- a/programs/autocrat/src/lib.rs +++ b/programs/autocrat/src/lib.rs @@ -34,6 +34,9 @@ use conditional_vault::cpi::accounts::SettleConditionalVault; use conditional_vault::program::ConditionalVault as ConditionalVaultProgram; use conditional_vault::ConditionalVault as ConditionalVaultAccount; use conditional_vault::VaultStatus; + +use amm::state::Amm; + use openbook_twap::TWAPMarket; use openbook_v2::state::Market; use solana_program::instruction::Instruction; @@ -120,6 +123,9 @@ pub struct Proposal { pub slot_enqueued: u64, pub state: ProposalState, pub instruction: ProposalInstruction, + pub pass_amm: Pubkey, + pub fail_amm: Pubkey, + pub amm_nonce: u64, pub openbook_twap_pass_market: Pubkey, pub openbook_twap_fail_market: Pubkey, pub openbook_pass_market: Pubkey, @@ -301,6 +307,9 @@ pub mod autocrat { slot_enqueued: clock.slot, state: ProposalState::Pending, instruction, + pass_amm: ctx.accounts.pass_amm.key(), + fail_amm: ctx.accounts.fail_amm.key(), + amm_nonce: ctx.accounts.pass_amm.nonce, openbook_twap_pass_market: pass_twap_market.key(), openbook_twap_fail_market: fail_twap_market.key(), openbook_pass_market: ctx.accounts.openbook_pass_market.key(), @@ -310,18 +319,18 @@ pub mod autocrat { dao: dao.key(), }); - // least signficant 32 bits of nonce are proposal number - // most significant bit of nonce is 0 for base (META) and 1 for quote (USDC) - require_eq!( - base_vault.nonce, - proposal.number as u64, - AutocratError::InvalidVaultNonce - ); - require_eq!( - quote_vault.nonce, - proposal.number as u64, - AutocratError::InvalidVaultNonce - ); + // // least signficant 32 bits of nonce are proposal number + // // most significant bit of nonce is 0 for base (META) and 1 for quote (USDC) + // require_eq!( + // base_vault.nonce, + // proposal.number as u64, + // AutocratError::InvalidVaultNonce + // ); + // require_eq!( + // quote_vault.nonce, + // proposal.number as u64, + // AutocratError::InvalidVaultNonce + // ); Ok(()) } @@ -471,15 +480,28 @@ pub struct InitializeProposal<'info> { )] pub dao_treasury: UncheckedAccount<'info>, #[account( + has_one = proposal, constraint = quote_vault.underlying_token_mint == dao.usdc_mint, constraint = quote_vault.settlement_authority == dao.treasury @ AutocratError::InvalidSettlementAuthority, )] pub quote_vault: Account<'info, ConditionalVaultAccount>, #[account( + has_one = proposal, constraint = base_vault.underlying_token_mint == dao.token_mint, constraint = base_vault.settlement_authority == dao.treasury @ AutocratError::InvalidSettlementAuthority, )] pub base_vault: Account<'info, ConditionalVaultAccount>, + #[account( + constraint = pass_amm.base_mint == base_vault.conditional_on_finalize_token_mint, + constraint = pass_amm.quote_mint == quote_vault.conditional_on_finalize_token_mint, + constraint = pass_amm.nonce == fail_amm.nonce + )] + pub pass_amm: Box>, + #[account( + constraint = fail_amm.base_mint == base_vault.conditional_on_revert_token_mint, + constraint = fail_amm.quote_mint == quote_vault.conditional_on_revert_token_mint + )] + pub fail_amm: Box>, pub openbook_pass_market: AccountLoader<'info, Market>, pub openbook_fail_market: AccountLoader<'info, Market>, #[account(constraint = openbook_twap_pass_market.market == openbook_pass_market.key())] @@ -498,9 +520,13 @@ pub struct FinalizeProposal<'info> { has_one = quote_vault, has_one = openbook_twap_pass_market, has_one = openbook_twap_fail_market, + has_one = pass_amm, + has_one = fail_amm, has_one = dao, )] pub proposal: Account<'info, Proposal>, + pub pass_amm: Account<'info, Amm>, + pub fail_amm: Account<'info, Amm>, pub openbook_twap_pass_market: Account<'info, TWAPMarket>, pub openbook_twap_fail_market: Account<'info, TWAPMarket>, pub dao: Box>, @@ -510,6 +536,7 @@ pub struct FinalizeProposal<'info> { pub quote_vault: Box>, pub vault_program: Program<'info, ConditionalVaultProgram>, /// CHECK: never read + /// TODO: use a different thing to prevent collision #[account( seeds = [dao.key().as_ref()], bump = dao.treasury_pda_bump, diff --git a/programs/conditional_vault/src/lib.rs b/programs/conditional_vault/src/lib.rs index ef5b6584..61021281 100644 --- a/programs/conditional_vault/src/lib.rs +++ b/programs/conditional_vault/src/lib.rs @@ -41,9 +41,9 @@ pub struct ConditionalVault { pub settlement_authority: Pubkey, /// The mint of the tokens that are deposited into the vault. pub underlying_token_mint: Pubkey, - /// A nonce to allow a single account to be the settlement authority of multiple - /// vaults with the same underlying token mints. - pub nonce: u64, + /// We need to be able to create multiple vault for a single underlying token + /// account, so we use proposal as a PDA seed. + pub proposal: Pubkey, /// The vault's storage account for deposited funds. pub underlying_token_account: Pubkey, pub conditional_on_finalize_token_mint: Pubkey, @@ -58,7 +58,7 @@ macro_rules! generate_vault_seeds { b"conditional_vault", $vault.settlement_authority.as_ref(), $vault.underlying_token_mint.as_ref(), - &$vault.nonce.to_le_bytes(), + $vault.proposal.as_ref(), &[$vault.pda_bump], ] }}; @@ -71,7 +71,7 @@ pub mod conditional_vault { pub fn initialize_conditional_vault( ctx: Context, settlement_authority: Pubkey, - nonce: u64, + proposal: Pubkey, ) -> Result<()> { let vault = &mut ctx.accounts.vault; @@ -79,7 +79,7 @@ pub mod conditional_vault { status: VaultStatus::Active, settlement_authority, underlying_token_mint: ctx.accounts.underlying_token_mint.key(), - nonce, + proposal, underlying_token_account: ctx.accounts.vault_underlying_token_account.key(), conditional_on_finalize_token_mint: ctx .accounts @@ -484,7 +484,7 @@ pub mod conditional_vault { } #[derive(Accounts)] -#[instruction(settlement_authority: Pubkey, nonce: u64)] +#[instruction(settlement_authority: Pubkey, proposal: Pubkey)] pub struct InitializeConditionalVault<'info> { #[account( init, @@ -494,7 +494,7 @@ pub struct InitializeConditionalVault<'info> { b"conditional_vault", settlement_authority.key().as_ref(), underlying_token_mint.key().as_ref(), - &nonce.to_le_bytes() + proposal.as_ref() ], bump )] @@ -503,19 +503,23 @@ pub struct InitializeConditionalVault<'info> { #[account( init, payer = payer, + seeds = [b"conditional_on_finalize_mint", vault.key().as_ref()], + bump, mint::authority = vault, mint::freeze_authority = vault, mint::decimals = underlying_token_mint.decimals )] - pub conditional_on_finalize_token_mint: Account<'info, Mint>, + pub conditional_on_finalize_token_mint: Box>, #[account( init, payer = payer, + seeds = [b"conditional_on_revert_mint", vault.key().as_ref()], + bump, mint::authority = vault, mint::freeze_authority = vault, mint::decimals = underlying_token_mint.decimals )] - pub conditional_on_revert_token_mint: Account<'info, Mint>, + pub conditional_on_revert_token_mint: Box>, #[account( init, payer = payer, diff --git a/tests/autocrat.ts b/tests/autocrat.ts index af0e36cc..c73aacfd 100644 --- a/tests/autocrat.ts +++ b/tests/autocrat.ts @@ -39,6 +39,11 @@ import { AutocratMigrator } from "../target/types/autocrat_migrator"; const { PublicKey, Keypair } = anchor.web3; import { OpenbookTwap } from "./fixtures/openbook_twap"; +import { AmmClient, getAmmAddr, getVaultAddr } from "../app/src"; +import { PriceMath } from "../app/src/utils/priceMath"; +import { AutocratClient } from "../app/src/AutocratClient"; +import { TransactionInstruction } from "@solana/web3.js"; +import { ConditionalVaultClient } from "../app/src/ConditionalVaultClient"; const OpenbookTwapIDL: OpenbookTwap = require("./fixtures/openbook_twap.json"); const AutocratIDL: Autocrat = require("../target/idl/autocrat.json"); @@ -99,6 +104,9 @@ describe("autocrat", async function () { vaultProgram, openbook: OpenBookV2Client, openbookTwap, + ammClient: AmmClient, + autocratClient: AutocratClient, + vaultClient: ConditionalVaultClient, migrator, treasuryMetaAccount, treasuryUsdcAccount, @@ -124,6 +132,10 @@ describe("autocrat", async function () { provider = new BankrunProvider(context); anchor.setProvider(provider); + ammClient = await AmmClient.createClient({ provider }); + vaultClient = await ConditionalVaultClient.createClient({ provider }); + autocratClient = await AutocratClient.createClient({ provider }); + autocrat = new anchor.Program( AutocratIDL, AUTOCRAT_PROGRAM_ID, @@ -288,13 +300,15 @@ describe("autocrat", async function () { await initializeProposal( autocrat, + autocratClient, instruction, vaultProgram, dao, context, payer, openbook, - openbookTwap + openbookTwap, + ammClient ); let balanceAfter = await banksClient.getBalance(payer.publicKey); @@ -312,6 +326,8 @@ describe("autocrat", async function () { openbookFailMarket, openbookTwapPassMarket, openbookTwapFailMarket, + passAmm, + failAmm, baseVault, quoteVault, basePassVaultUnderlyingTokenAccount, @@ -368,13 +384,15 @@ describe("autocrat", async function () { proposal = await initializeProposal( autocrat, + autocratClient, instruction, vaultProgram, dao, context, payer, openbook, - openbookTwap + openbookTwap, + ammClient ); ({ @@ -384,6 +402,8 @@ describe("autocrat", async function () { openbookTwapFailMarket, baseVault, quoteVault, + passAmm, + failAmm, } = await autocrat.account.proposal.fetch(proposal)); mm0 = await generateMarketMaker( @@ -506,6 +526,8 @@ describe("autocrat", async function () { proposal, openbookTwapPassMarket, openbookTwapFailMarket, + passAmm, + failAmm, dao, baseVault, quoteVault, @@ -632,6 +654,8 @@ describe("autocrat", async function () { proposal, openbookTwapPassMarket, openbookTwapFailMarket, + passAmm, + failAmm, dao, baseVault, quoteVault, @@ -875,6 +899,8 @@ describe("autocrat", async function () { proposal, openbookTwapPassMarket, openbookTwapFailMarket, + passAmm, + failAmm, dao, baseVault, quoteVault, @@ -958,6 +984,8 @@ describe("autocrat", async function () { let proposal, openbookPassMarket, openbookFailMarket, + passAmm, + failAmm, openbookTwapPassMarket, openbookTwapFailMarket, baseVault, @@ -977,13 +1005,15 @@ describe("autocrat", async function () { proposal = await initializeProposal( autocrat, + autocratClient, instruction, vaultProgram, dao, context, payer, openbook, - openbookTwap + openbookTwap, + ammClient ); ({ @@ -993,6 +1023,8 @@ describe("autocrat", async function () { openbookTwapFailMarket, baseVault, quoteVault, + passAmm, + failAmm, } = await autocrat.account.proposal.fetch(proposal)); mm0 = await generateMarketMaker( @@ -1086,6 +1118,8 @@ describe("autocrat", async function () { proposal, openbookTwapPassMarket, openbookTwapFailMarket, + passAmm, + failAmm, dao, baseVault, quoteVault, @@ -1199,6 +1233,8 @@ describe("autocrat", async function () { proposal, openbookTwapPassMarket, openbookTwapFailMarket, + passAmm, + failAmm, dao, baseVault, quoteVault, @@ -1292,6 +1328,8 @@ describe("autocrat", async function () { openbookTwapFailMarket, baseVault, quoteVault, + passAmm, + failAmm, basePassVaultUnderlyingTokenAccount, basePassConditionalTokenMint, baseFailConditionalTokenMint, @@ -1339,13 +1377,15 @@ describe("autocrat", async function () { proposal = await initializeProposal( autocrat, + autocratClient, instruction, vaultProgram, mertdDao, context, payer, openbook, - openbookTwap + openbookTwap, + ammClient ); ({ @@ -1355,6 +1395,8 @@ describe("autocrat", async function () { openbookTwapFailMarket, baseVault, quoteVault, + passAmm, + failAmm, } = await autocrat.account.proposal.fetch(proposal)); mm0 = await generateMarketMaker( @@ -1471,18 +1513,18 @@ describe("autocrat", async function () { "finalize succeeded despite proposal being too young" ); - await autocrat.methods - .finalizeProposal() - .accounts({ + await autocratClient + .finalizeProposalIx( proposal, + instruction, + mertdDao, + passAmm, + failAmm, openbookTwapPassMarket, openbookTwapFailMarket, - dao: mertdDao, baseVault, - quoteVault, - vaultProgram: vaultProgram.programId, - daoTreasury: mertdDaoTreasury, - }) + quoteVault + ) .rpc() .then(callbacks[0], callbacks[1]); }); @@ -1597,30 +1639,17 @@ describe("autocrat", async function () { 7_500 ); - await autocrat.methods - .finalizeProposal() - .accounts({ + await autocratClient + .finalizeProposalIx( proposal, + instruction, + mertdDao, + passAmm, + failAmm, openbookTwapPassMarket, openbookTwapFailMarket, - dao: mertdDao, baseVault, - quoteVault, - vaultProgram: vaultProgram.programId, - daoTreasury: mertdDaoTreasury, - }) - .remainingAccounts( - instruction.accounts - .concat({ - pubkey: instruction.programId, - isWritable: false, - isSigner: false, - }) - .map((meta) => - meta.pubkey.equals(mertdDaoTreasury) - ? { ...meta, isSigner: false } - : meta - ) + quoteVault ) .rpc(); @@ -1833,34 +1862,17 @@ describe("autocrat", async function () { let storedDao = await autocrat.account.dao.fetch(mertdDao); const passThresholdBpsBefore = storedDao.passThresholdBps; - await autocrat.methods - .finalizeProposal() - .accounts({ + await autocratClient + .finalizeProposalIx( proposal, + instruction, + mertdDao, + passAmm, + failAmm, openbookTwapPassMarket, openbookTwapFailMarket, - dao: mertdDao, baseVault, - quoteVault, - vaultProgram: vaultProgram.programId, - daoTreasury: mertdDaoTreasury, - }) - .remainingAccounts( - autocrat.instruction.updateDao - .accounts({ - dao: mertdDao, - daoTreasury: mertdDaoTreasury, - }) - .concat({ - pubkey: autocrat.programId, - isWritable: false, - isSigner: false, - }) - .map((meta) => - meta.pubkey.equals(mertdDaoTreasury) - ? { ...meta, isSigner: false } - : meta - ) + quoteVault ) .rpc(); @@ -2145,14 +2157,18 @@ async function placeOrdersAroundMid( async function initializeProposal( autocrat: Program, + autocratClient: AutocratClient, ix: ProposalInstruction, vaultProgram: Program, dao: PublicKey, context: ProgramTestContext, payer: Keypair, openbook: OpenBookV2Client, - openbookTwap: Program + openbookTwap: Program, + ammClient: AmmClient ): Promise { + const vaultClient = autocratClient.vaultClient; + const proposalKeypair = Keypair.generate(); const currentClock = await context.banksClient.getClock(); @@ -2173,23 +2189,21 @@ async function initializeProposal( // most significant bit of nonce is 0 for pass and 1 for fail // second most significant bit of nonce is 0 for base and 1 for quote - let baseNonce = new BN(storedDAO.proposalCount + 1); + let vaultNonce = new BN(storedDAO.proposalCount + 1); - const baseVault = await initializeVault( - vaultProgram, - storedDAO.treasury, - storedDAO.tokenMint, - baseNonce, - payer - ); + const baseVault = await vaultClient + .initializeVault( + storedDAO.treasury, + storedDAO.tokenMint, + proposalKeypair.publicKey + ); - const quoteVault = await initializeVault( - vaultProgram, - storedDAO.treasury, - storedDAO.usdcMint, - baseNonce, - payer - ); + const quoteVault = await vaultClient + .initializeVault( + storedDAO.treasury, + storedDAO.usdcMint, + proposalKeypair.publicKey + ); const passBaseMint = ( await vaultProgram.account.conditionalVault.fetch(baseVault) @@ -2204,6 +2218,45 @@ async function initializeProposal( await vaultProgram.account.conditionalVault.fetch(quoteVault) ).conditionalOnRevertTokenMint; + let ammNonce = new BN(Math.random() * 100_000_000); + + let [twapFirstObservationScaled, twapMaxObservationChangePerUpdateScaled] = + PriceMath.scalePrices(9, 6, 100, 1); + + await ammClient + .createAmm( + passBaseMint, + passQuoteMint, + twapFirstObservationScaled, + twapMaxObservationChangePerUpdateScaled, + ammNonce + ) + .rpc(); + + const [passAmm] = getAmmAddr( + ammClient.getProgramId(), + passBaseMint, + passQuoteMint, + ammNonce + ); + + await ammClient + .createAmm( + failBaseMint, + failQuoteMint, + twapFirstObservationScaled, + twapMaxObservationChangePerUpdateScaled, + ammNonce + ) + .rpc(); + + const [failAmm] = getAmmAddr( + ammClient.getProgramId(), + failBaseMint, + failQuoteMint, + ammNonce + ); + const [daoTreasury] = PublicKey.findProgramAddressSync( [dao.toBuffer()], autocrat.programId @@ -2315,7 +2368,7 @@ async function initializeProposal( await autocrat.methods .initializeProposal(dummyURL, ix) .preInstructions([ - await autocrat.account.proposal.createInstruction(proposalKeypair, 1500), + await autocrat.account.proposal.createInstruction(proposalKeypair, 2500), ]) .accounts({ proposal: proposalKeypair.publicKey, @@ -2323,12 +2376,13 @@ async function initializeProposal( daoTreasury, baseVault, quoteVault, + passAmm, + failAmm, openbookTwapPassMarket, openbookTwapFailMarket, openbookPassMarket, openbookFailMarket, proposer: payer.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, }) .signers([proposalKeypair]) .rpc(); @@ -2355,6 +2409,9 @@ async function initializeProposal( assert.deepEqual(storedIx.accounts, ix.accounts); assert.deepEqual(storedIx.data, ix.data); + assert.ok(storedProposal.passAmm.equals(passAmm)); + assert.ok(storedProposal.failAmm.equals(failAmm)); + assert.equal(storedProposal.ammNonce.toString(), ammNonce.toString()); assert.ok( storedProposal.openbookTwapFailMarket.equals(openbookTwapFailMarket) ); @@ -2370,51 +2427,51 @@ async function initializeProposal( return proposalKeypair.publicKey; } -async function initializeVault( - vaultProgram: Program, - settlementAuthority: PublicKey, - underlyingTokenMint: PublicKey, - nonce: BN, - payer: Keypair -): Promise { - const [vault] = anchor.web3.PublicKey.findProgramAddressSync( - [ - anchor.utils.bytes.utf8.encode("conditional_vault"), - settlementAuthority.toBuffer(), - underlyingTokenMint.toBuffer(), - nonce.toBuffer("le", 8), - ], - vaultProgram.programId - ); - const conditionalOnFinalizeTokenMintKeypair = Keypair.generate(); - const conditionalOnRevertTokenMintKeypair = Keypair.generate(); - - const vaultUnderlyingTokenAccount = await token.getAssociatedTokenAddress( - underlyingTokenMint, - vault, - true - ); - - await vaultProgram.methods - .initializeConditionalVault(settlementAuthority, nonce) - .accounts({ - vault, - underlyingTokenMint, - vaultUnderlyingTokenAccount, - conditionalOnFinalizeTokenMint: - conditionalOnFinalizeTokenMintKeypair.publicKey, - conditionalOnRevertTokenMint: - conditionalOnRevertTokenMintKeypair.publicKey, - payer: payer.publicKey, - tokenProgram: token.TOKEN_PROGRAM_ID, - associatedTokenProgram: token.ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([ - conditionalOnFinalizeTokenMintKeypair, - conditionalOnRevertTokenMintKeypair, - ]) - .rpc(); - - return vault; -} +// async function initializeVault( +// vaultClient: ConditionalVaultClient, +// settlementAuthority: PublicKey, +// underlyingTokenMint: PublicKey, +// nonce: BN, +// payer: Keypair +// ): Promise { +// const [vault] = anchor.web3.PublicKey.findProgramAddressSync( +// [ +// anchor.utils.bytes.utf8.encode("conditional_vault"), +// settlementAuthority.toBuffer(), +// underlyingTokenMint.toBuffer(), +// nonce.toBuffer("le", 8), +// ], +// vaultProgram.programId +// ); +// const conditionalOnFinalizeTokenMintKeypair = Keypair.generate(); +// const conditionalOnRevertTokenMintKeypair = Keypair.generate(); + +// const vaultUnderlyingTokenAccount = await token.getAssociatedTokenAddress( +// underlyingTokenMint, +// vault, +// true +// ); + +// await vaultProgram.methods +// .initializeConditionalVault(settlementAuthority, nonce) +// .accounts({ +// vault, +// underlyingTokenMint, +// vaultUnderlyingTokenAccount, +// conditionalOnFinalizeTokenMint: +// conditionalOnFinalizeTokenMintKeypair.publicKey, +// conditionalOnRevertTokenMint: +// conditionalOnRevertTokenMintKeypair.publicKey, +// payer: payer.publicKey, +// tokenProgram: token.TOKEN_PROGRAM_ID, +// associatedTokenProgram: token.ASSOCIATED_TOKEN_PROGRAM_ID, +// systemProgram: anchor.web3.SystemProgram.programId, +// }) +// .signers([ +// conditionalOnFinalizeTokenMintKeypair, +// conditionalOnRevertTokenMintKeypair, +// ]) +// .rpc(); + +// return vault; +// } diff --git a/tests/conditionalVault.ts b/tests/conditionalVault.ts index 4e7f21f3..069e0cf6 100644 --- a/tests/conditionalVault.ts +++ b/tests/conditionalVault.ts @@ -34,6 +34,12 @@ const { PublicKey, Keypair } = web3; import { ConditionalVault } from "../target/types/conditional_vault"; import { expectError } from "./utils/utils"; +import { + getVaultAddr, + getVaultFinalizeMintAddr, + getVaultRevertMintAddr, +} from "../app/src"; +import { ConditionalVaultClient } from "../app/src/ConditionalVaultClient"; const ConditionalVaultIDL: ConditionalVault = require("../target/idl/conditional_vault.json"); export type VaultProgram = anchor.Program; @@ -61,8 +67,10 @@ export enum VaultStatus { describe("conditional_vault", async function () { let provider: anchor.Provider; + let vaultClient: ConditionalVaultClient; let vault: PublicKey; + let proposal: PublicKey; let vaultUnderlyingTokenAccount: anchor.web3.PublicKey; let underlyingTokenMint: anchor.web3.PublicKey; let conditionalOnFinalizeMint: anchor.web3.PublicKey; @@ -104,6 +112,8 @@ describe("conditional_vault", async function () { provider ); + vaultClient = await ConditionalVaultClient.createClient({ provider }); + payer = vaultProgram.provider.wallet.payer; alice = anchor.web3.Keypair.generate(); settlementAuthority = anchor.web3.Keypair.generate(); @@ -118,16 +128,13 @@ describe("conditional_vault", async function () { 8 ); - nonce = new anchor.BN(10); + proposal = Keypair.generate().publicKey; - [vault] = anchor.web3.PublicKey.findProgramAddressSync( - [ - anchor.utils.bytes.utf8.encode("conditional_vault"), - settlementAuthority.publicKey.toBuffer(), - underlyingTokenMint.toBuffer(), - nonce.toBuffer("le", 8), - ], - vaultProgram.programId + [vault] = getVaultAddr( + vaultProgram.programId, + settlementAuthority.publicKey, + underlyingTokenMint, + proposal ); vaultUnderlyingTokenAccount = await token.getAssociatedTokenAddress( @@ -139,31 +146,24 @@ describe("conditional_vault", async function () { describe("#initialize_conditional_vault", async function () { it("initializes vaults", async function () { - let conditionalOnFinalizeTokenMintKeypair = - anchor.web3.Keypair.generate(); - let conditionalOnRevertTokenMintKeypair = anchor.web3.Keypair.generate(); - - await vaultProgram.methods - .initializeConditionalVault(settlementAuthority.publicKey, nonce) - .accounts({ - vault, + await vaultClient + .initializeVaultIx( + settlementAuthority.publicKey, underlyingTokenMint, - vaultUnderlyingTokenAccount, - conditionalOnFinalizeTokenMint: - conditionalOnFinalizeTokenMintKeypair.publicKey, - conditionalOnRevertTokenMint: - conditionalOnRevertTokenMintKeypair.publicKey, - payer: payer.publicKey, - tokenProgram: token.TOKEN_PROGRAM_ID, - associatedTokenProgram: token.ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([ - conditionalOnFinalizeTokenMintKeypair, - conditionalOnRevertTokenMintKeypair, - ]) + proposal + ) .rpc(); + conditionalOnFinalizeMint = getVaultFinalizeMintAddr( + vaultProgram.programId, + vault + )[0]; + + conditionalOnRevertMint = getVaultRevertMintAddr( + vaultProgram.programId, + vault + )[0]; + const storedVault = await vaultProgram.account.conditionalVault.fetch( vault ); @@ -172,24 +172,18 @@ describe("conditional_vault", async function () { storedVault.settlementAuthority.equals(settlementAuthority.publicKey) ); assert.ok(storedVault.underlyingTokenMint.equals(underlyingTokenMint)); - assert.ok(storedVault.nonce.eq(nonce)); + assert.ok(storedVault.proposal.equals(proposal)); assert.ok( storedVault.underlyingTokenAccount.equals(vaultUnderlyingTokenAccount) ); assert.ok( storedVault.conditionalOnFinalizeTokenMint.equals( - conditionalOnFinalizeTokenMintKeypair.publicKey + conditionalOnFinalizeMint ) ); assert.ok( - storedVault.conditionalOnRevertTokenMint.equals( - conditionalOnRevertTokenMintKeypair.publicKey - ) + storedVault.conditionalOnRevertTokenMint.equals(conditionalOnRevertMint) ); - - conditionalOnFinalizeMint = - conditionalOnFinalizeTokenMintKeypair.publicKey; - conditionalOnRevertMint = conditionalOnRevertTokenMintKeypair.publicKey; }); }); @@ -493,6 +487,7 @@ describe("conditional_vault", async function () { it("allows vaults to be finalized", async function () { let [vault, _, settlementAuthority] = await generateRandomVault( vaultProgram, + vaultClient, payer, banksClient, umi @@ -511,6 +506,7 @@ describe("conditional_vault", async function () { it("allows vaults to be reverted", async function () { let [vault, _, settlementAuthority] = await generateRandomVault( vaultProgram, + vaultClient, payer, banksClient, umi @@ -529,6 +525,7 @@ describe("conditional_vault", async function () { it("disallows vaults from being finalized twice", async function () { let [vault, _, settlementAuthority] = await generateRandomVault( vaultProgram, + vaultClient, payer, banksClient, umi @@ -570,7 +567,7 @@ describe("conditional_vault", async function () { beforeEach(async function () { [vault, underlyingMintAuthority, settlementAuthority] = - await generateRandomVault(vaultProgram, payer, banksClient, umi); + await generateRandomVault(vaultProgram, vaultClient, payer, banksClient, umi); let storedVault = await vaultProgram.account.conditionalVault.fetch( vault ); @@ -772,6 +769,7 @@ describe("conditional_vault", async function () { async function generateRandomVault( vaultProgram: VaultProgram, + vaultClient: ConditionalVaultClient, payer: Keypair, banksClient: BanksClient, umi: Umi, @@ -820,25 +818,23 @@ async function generateRandomVault( ); // console.log(createMetadataResult); - const nonce = new BN(1003239); + const proposal = Keypair.generate().publicKey; - const [vault] = anchor.web3.PublicKey.findProgramAddressSync( - [ - anchor.utils.bytes.utf8.encode("conditional_vault"), - settlementAuthority.publicKey.toBuffer(), - underlyingTokenMint.toBuffer(), - nonce.toBuffer("le", 8), - ], - vaultProgram.programId + const [vault] = getVaultAddr( + vaultProgram.programId, + settlementAuthority.publicKey, + underlyingTokenMint, + proposal ); + const conditionalOnFinalizeTokenMint = getVaultFinalizeMintAddr(vaultProgram.programId, vault)[0]; + const conditionalOnRevertTokenMint = getVaultRevertMintAddr(vaultProgram.programId, vault)[0]; + const vaultUnderlyingTokenAccount = await token.getAssociatedTokenAddress( underlyingTokenMint, vault, true ); - let conditionalOnFinalizeTokenMintKeypair = anchor.web3.Keypair.generate(); - let conditionalOnRevertTokenMintKeypair = anchor.web3.Keypair.generate(); // when we have a ts lib, we can consolidate this logic there const [conditionalOnFinalizeTokenMetadata] = @@ -846,7 +842,7 @@ async function generateRandomVault( [ anchor.utils.bytes.utf8.encode("metadata"), MPL_TOKEN_METADATA_PROGRAM_ID.toBuffer(), - conditionalOnFinalizeTokenMintKeypair.publicKey.toBuffer(), + conditionalOnFinalizeTokenMint.toBuffer(), ], MPL_TOKEN_METADATA_PROGRAM_ID ); @@ -856,7 +852,7 @@ async function generateRandomVault( [ anchor.utils.bytes.utf8.encode("metadata"), MPL_TOKEN_METADATA_PROGRAM_ID.toBuffer(), - conditionalOnRevertTokenMintKeypair.publicKey.toBuffer(), + conditionalOnRevertTokenMint.toBuffer(), ], MPL_TOKEN_METADATA_PROGRAM_ID ); @@ -882,10 +878,8 @@ async function generateRandomVault( vault, underlyingTokenMint, underlyingTokenMetadata, - conditionalOnFinalizeTokenMint: - conditionalOnFinalizeTokenMintKeypair.publicKey, - conditionalOnRevertTokenMint: - conditionalOnRevertTokenMintKeypair.publicKey, + conditionalOnFinalizeTokenMint, + conditionalOnRevertTokenMint, conditionalOnFinalizeTokenMetadata, conditionalOnRevertTokenMetadata, tokenMetadataProgram: MPL_TOKEN_METADATA_PROGRAM_ID, @@ -894,25 +888,12 @@ async function generateRandomVault( }) .instruction(); - await vaultProgram.methods - .initializeConditionalVault(settlementAuthority.publicKey, nonce) - .accounts({ - vault, + await vaultClient + .initializeVaultIx( + settlementAuthority.publicKey, underlyingTokenMint, - vaultUnderlyingTokenAccount, - conditionalOnFinalizeTokenMint: - conditionalOnFinalizeTokenMintKeypair.publicKey, - conditionalOnRevertTokenMint: - conditionalOnRevertTokenMintKeypair.publicKey, - payer: payer.publicKey, - tokenProgram: token.TOKEN_PROGRAM_ID, - associatedTokenProgram: token.ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([ - conditionalOnFinalizeTokenMintKeypair, - conditionalOnRevertTokenMintKeypair, - ]) + proposal + ) .postInstructions([addMetadataToConditionalTokensIx]) .rpc();