diff --git a/Anchor.toml b/Anchor.toml index d98f3fa4b..f9fc91415 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -26,6 +26,7 @@ initialize-launch = "yarn run tsx scripts/initializeLaunch.ts" create-proposal = "yarn run tsx scripts/createProposal.ts" create-v04-dao = "yarn run tsx scripts/createV04DAO.ts" create-v04-proposal = "yarn run tsx scripts/createV04Proposal.ts" +create-streamflow-proposal = "yarn run tsx scripts/createStreamflowProposal.ts" add-v04-metadata = "yarn run tsx scripts/addV04Metadata.ts" [test] diff --git a/scripts/createStreamflowProposal.ts b/scripts/createStreamflowProposal.ts new file mode 100644 index 000000000..72f547c72 --- /dev/null +++ b/scripts/createStreamflowProposal.ts @@ -0,0 +1,334 @@ +import * as token from "@solana/spl-token"; +import { Keypair, PublicKey, AddressLookupTableProgram, Transaction, AddressLookupTableAccount, TransactionMessage, VersionedTransaction, MessageV0, SystemProgram } from "@solana/web3.js"; +import * as anchor from "@coral-xyz/anchor"; +import { + AutocratClient, + AUTOCRAT_PROGRAM_ID, + AutocratIDL, + PriceMath, +} from "@metadaoproject/futarchy/v0.4"; +import { IDL as StreamflowEscrowIDL } from "../tests/fixtures/streamflow_escrow.js"; +import { BN } from "bn.js"; +import * as fs from "fs"; +import { assert } from "console"; +import { USDC } from "./consts.js"; + +const STREAMFLOW_ESCROW_PROGRAM_ID = new PublicKey("ESCRoWj8QUJ5cTXCBWbGpW6AzaaEAtRbZuwKp8c4YYGs"); +const STREAMFLOW_VESTING_PROGRAM_ID = new PublicKey("strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m"); + +const ORDER_PUBKEY = new PublicKey("AS9b7ZQ89DZDdzE3y7mTqHFxoxEJp3gGR2UTRbAVHiFE"); +const MTNCAP_DAO = new PublicKey("6uY9NohsBFGF1BafttPNZtJ4uyNbYKaNToL169nEegox"); +const RAY = new PublicKey("4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R"); +const LUT = new PublicKey("5m1KBVQBrWbpX3qe8AG7tUzrV4yLdMPGbDkHNN9cZ7Z1"); +const PROPOSAL_NONCE = new BN(1337); + +export const ORDER_PREFIX = Buffer.from('order', 'utf-8'); +export const EXECUTION_RECORD_PREFIX = Buffer.from('execution-record', 'utf-8'); +export const ESCROW_PREFIX = Buffer.from('strm', 'utf-8'); + +export const deriveOrderPDA = ( + programId: anchor.web3.PublicKey, + creator: anchor.web3.PublicKey, + baseMint: anchor.web3.PublicKey, + nonce: number, +): anchor.web3.PublicKey => { + return anchor.web3.PublicKey.findProgramAddressSync( + [ORDER_PREFIX, creator.toBuffer(), baseMint.toBuffer(), new BN(nonce).toArrayLike(Buffer, 'le', 4)], + programId, + )[0]; +}; + +export const deriveExecutionRecordPDA = ( + programId: anchor.web3.PublicKey, + order: anchor.web3.PublicKey, + executor: anchor.web3.PublicKey, + nonce: number, +): anchor.web3.PublicKey => { + return anchor.web3.PublicKey.findProgramAddressSync( + [EXECUTION_RECORD_PREFIX, order.toBuffer(), executor.toBuffer(), new BN(nonce).toArrayLike(Buffer, 'le', 4)], + programId, + )[0]; +}; + +export const deriveEscrowPDA = ( + programId: anchor.web3.PublicKey, + contractKey: anchor.web3.PublicKey, +): anchor.web3.PublicKey => { + return anchor.web3.PublicKey.findProgramAddressSync([ESCROW_PREFIX, contractKey.toBuffer()], programId)[0]; +}; + +async function main() { + // Initialize clients + const provider = anchor.AnchorProvider.env(); + const payer = provider.wallet["payer"]; + const autocratProgram = AutocratClient.createClient({ provider }); + const escrowProgram = new anchor.Program(StreamflowEscrowIDL as anchor.Idl, STREAMFLOW_ESCROW_PROGRAM_ID, provider); + + const storedDao = await autocratProgram.getDao(MTNCAP_DAO); + const daoTreasury = storedDao.treasury; + + const storedOrder = await escrowProgram.account.order.fetch(ORDER_PUBKEY); + + const fillNonce = storedOrder.nonce as number + 12345; + + // Import contract keypair from file + const contractKeypair = Keypair.fromSecretKey(Buffer.from(JSON.parse(fs.readFileSync("../contract-keypair.json", "utf8")))); + const contractKey = contractKeypair.publicKey; + const escrowKey = deriveEscrowPDA(STREAMFLOW_VESTING_PROGRAM_ID, contractKey); + const recordKey = deriveExecutionRecordPDA(STREAMFLOW_ESCROW_PROGRAM_ID, ORDER_PUBKEY, daoTreasury, fillNonce); + + assert(USDC.equals(storedOrder.quoteMint as PublicKey)); + assert(RAY.equals(storedOrder.baseMint as PublicKey)); + + const daoUsdc = (await token.getOrCreateAssociatedTokenAccount(provider.connection, payer, USDC, daoTreasury, true)).address; + const daoRay = (await token.getOrCreateAssociatedTokenAccount(provider.connection, payer, RAY, daoTreasury, true)).address; + const creatorUsdc = (await token.getOrCreateAssociatedTokenAccount(provider.connection, payer, USDC, storedOrder.creator as PublicKey, true)).address; + + console.log(daoUsdc); + console.log(daoRay); + console.log(creatorUsdc); + + console.log(storedDao); + console.log(storedDao.minBaseFutarchicLiquidity.toString()); + console.log(storedDao.minQuoteFutarchicLiquidity.toString()); + + // Create the fillOrderVested instruction + const fillOrderIx = await escrowProgram.methods + .fillOrderVested(fillNonce, storedOrder.amount, storedOrder.startPrice, false) + .accounts({ + common: { + executor: daoTreasury, + from: daoUsdc, + toBase: daoRay, + order: ORDER_PUBKEY, + toQuote: creatorUsdc, + baseTokenProgram: token.TOKEN_PROGRAM_ID, + quotaTokenProgram: token.TOKEN_PROGRAM_ID, + vault: storedOrder.vault as PublicKey, + creator: storedOrder.creator as PublicKey, + baseMint: RAY, + quoteMint: USDC, + }, + streamMetadata: contractKey, + withdrawor: new PublicKey("wdrwhnCv4pzW8beKsbPa4S2UDZrXenjg16KJdKSpb5u"), + feeOracle: new PublicKey("B743wFVk2pCYhV91cn287e1xY7f1vt4gdY48hhNiuQmT"), + escrowTokens: escrowKey, + executionRecord: recordKey, + streamflowProgram: STREAMFLOW_VESTING_PROGRAM_ID, + }) + .transaction(); + +// console.log() + const payerUsdcBalance = new BN((await provider.connection.getTokenAccountBalance(token.getAssociatedTokenAddressSync(USDC, payer.publicKey))).value.amount); + const payerMtnBalance = new BN((await provider.connection.getTokenAccountBalance(token.getAssociatedTokenAddressSync(storedDao.tokenMint, payer.publicKey))).value.amount); + + const { transactions, proposal } = await autocratProgram.initializeProposalTx( + MTNCAP_DAO, + "", + { + programId: fillOrderIx.instructions[0].programId, + accounts: fillOrderIx.instructions[0].keys.map(key => ({ + pubkey: key.pubkey, + isSigner: key.isSigner, + isWritable: key.isWritable + })), + data: fillOrderIx.instructions[0].data, + }, + payerMtnBalance, + payerUsdcBalance, + PROPOSAL_NONCE + ); + + console.log(proposal); + + let encodedTransactions = []; + + for (let i = 0; i < transactions.length - 1; i++) { + const tx = transactions[i]; + tx.recentBlockhash = (await provider.connection.getLatestBlockhash("confirmed")).blockhash; + tx.feePayer = payer.publicKey; + tx.sign(payer); + + encodedTransactions.push(Buffer.from(tx.serialize()).toString('base64')); + } + const initProposalTx = transactions[transactions.length - 1]; + + let rawLookupTable = await provider.connection.getAccountInfo(LUT); + let lookupTable = new AddressLookupTableAccount({ + key: LUT, + state: AddressLookupTableAccount.deserialize(rawLookupTable!.data), + }); + + let tipee = new PublicKey("ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49"); + + const tipIx = SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: tipee, + lamports: 1000, + }); + + const initProposalMessage = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: (await provider.connection.getLatestBlockhash()).blockhash, + instructions: initProposalTx.instructions.concat(tipIx), + }).compileToV0Message([lookupTable]); + const initProposalTxV0 = new VersionedTransaction(initProposalMessage); + initProposalTxV0.sign([payer]); + encodedTransactions.push(Buffer.from(initProposalTxV0.serialize()).toString('base64')); + + console.log(encodedTransactions.map(tx => `"${tx}"`).join(',\n\t')); + + +// const accountsToAdd = initProposalTx.instructions.map(instruction => instruction.keys.map(key => key.pubkey)); +// const uniqueAccounts = [...new Set(accountsToAdd.flat())] as PublicKey[]; +// console.log(uniqueAccounts.length); +// const extendInstruction = AddressLookupTableProgram.extendLookupTable({ +// payer: payer.publicKey, +// authority: payer.publicKey, +// lookupTable: LUT, +// addresses: uniqueAccounts, +// }); + +// const extendTx = new Transaction().add(extendInstruction); +// extendTx.recentBlockhash = (await provider.connection.getLatestBlockhash()).blockhash; +// extendTx.feePayer = payer.publicKey; +// extendTx.sign(payer); +// await provider.sendAndConfirm(extendTx); + + return; + + + + console.log(lookupTable) + + return; + + console.log(initProposalTxV0.message.staticAccountKeys.length); + initProposalTxV0.sign([payer]); + console.log(Buffer.from(initProposalTxV0.serialize()).toString('base64')); + return; + + return; + + + console.log(Buffer.from(initProposalTxV0.serialize()).toString('base64')); + + return; + + + // Extract unique accounts from the last instruction to add to lookup table +// const lastTx = transactions[transactions.length - 1]; +// const accountsToAdd = lastTx.instructions.map(instruction => instruction.keys.map(key => key.pubkey)); +// const uniqueAccounts = [...new Set(accountsToAdd.flat())] as PublicKey[]; + +// const slot = await provider.connection.getSlot(); +// const [lookupTableInst, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({ +// authority: payer.publicKey, +// payer: payer.publicKey, +// recentSlot: slot - 1, +// }); + +// // Create extend instruction for the last transaction's accounts + + +// // Execute extend instruction +// let extendTx = new Transaction().add(lookupTableInst, extendInstruction); +// extendTx.recentBlockhash = (await provider.connection.getLatestBlockhash()).blockhash; +// extendTx.feePayer = payer.publicKey; +// extendTx.sign(payer); +// await provider.sendAndConfirm(extendTx); + + + + + + + + return; + + + // Use the existing DAO address + console.log(storedDao); +// console.log("DAO Token Mint:", storedDao.tokenMint.toString()); +// console.log("DAO USDC Mint:", storedDao.usdcMint.toString()); + +// // Create or get token accounts for the payer +// const metaAccount = await token.getOrCreateAssociatedTokenAccount( +// provider.connection, +// payer, +// storedDao.tokenMint, +// payer.publicKey +// ); +// console.log("META account:", metaAccount.address.toString()); + +// const usdcAccount = await token.getOrCreateAssociatedTokenAccount( +// provider.connection, +// payer, +// storedDao.usdcMint, +// payer.publicKey +// ); +// console.log("USDC account:", usdcAccount.address.toString()); + +// // Check balances +// const metaBalance = metaAccount.amount; +// const usdcBalance = usdcAccount.amount; +// console.log("Current META balance:", metaBalance.toString()); +// console.log("Current USDC balance:", usdcBalance.toString()); + +// // Ensure we have enough tokens for the proposal +// const requiredMeta = PriceMath.getChainAmount(10, 9); // 10 META for more liquidity +// const requiredUsdc = PriceMath.getChainAmount(10000, 6); // 10000 USDC for more liquidity + +// if (metaBalance < BigInt(requiredMeta.toString()) || usdcBalance < BigInt(requiredUsdc.toString())) { +// console.log("Insufficient balance for proposal creation"); +// console.log("Required META:", requiredMeta.toString()); +// console.log("Required USDC:", requiredUsdc.toString()); +// return; +// } + +// const autocrat = new anchor.Program( +// AutocratIDL, +// AUTOCRAT_PROGRAM_ID, +// provider +// ); + +// const accounts = [ +// { +// pubkey: dao, +// isSigner: true, +// isWritable: true, +// }, +// ]; + +// const data = autocrat.coder.instruction.encode("update_dao", { +// daoParams: { +// passThresholdBps: 500, +// baseBurnLamports: null, +// burnDecayPerSlotLamports: null, +// slotsPerProposal: null, +// marketTakerFee: null, +// }, +// }); + +// const ix = { +// programId: autocratProgram.getProgramId(), +// accounts, +// data, +// }; + +// // Initialize the proposal +// const proposal = await autocratProgram.initializeProposal( +// dao, +// "https://example.com/proposal", // proposal description URL +// ix, +// PriceMath.getChainAmount(10, 9), // 10 META for more liquidity +// PriceMath.getChainAmount(10000, 6) // 10000 USDC for more liquidity +// ); + +// console.log("Proposal created with address:", proposal.toString()); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/sdk/src/v0.4/AutocratClient.ts b/sdk/src/v0.4/AutocratClient.ts index 3aa690c9b..5e7d566dd 100644 --- a/sdk/src/v0.4/AutocratClient.ts +++ b/sdk/src/v0.4/AutocratClient.ts @@ -3,12 +3,16 @@ import { AccountInfo, AccountMeta, AddressLookupTableAccount, + AddressLookupTableProgram, ComputeBudgetProgram, Connection, Keypair, PublicKey, + Signer, Transaction, TransactionInstruction, + TransactionMessage, + VersionedTransaction, } from "@solana/web3.js"; import { PriceMath } from "./utils/priceMath.js"; import { ProposalInstruction, InitializeDaoParams } from "./types/index.js"; @@ -311,7 +315,8 @@ export class AutocratClient { descriptionUrl: string, instruction: ProposalInstruction, baseTokensToLP: BN, - quoteTokensToLP: BN + quoteTokensToLP: BN, + lookupTableAccount?: AddressLookupTableAccount ): Promise { const storedDao = await this.getDao(dao); @@ -417,7 +422,7 @@ export class AutocratClient { // this is how many original tokens are created const lpTokens = quoteTokensToLP; - await this.initializeProposalIx( + const builder = this.initializeProposalIx( descriptionUrl, instruction, dao, @@ -427,139 +432,227 @@ export class AutocratClient { lpTokens, nonce, question - ).rpc(); + ); + + if (!lookupTableAccount) { + await builder.rpc(); + } else { + const tx = await builder.transaction(); + + return { tx: tx, proposal: proposal } as any; + + // const banksClient = (this.provider.connection as any).banksClient; + + // const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + // payer: this.provider.publicKey, + // authority: this.provider.publicKey, + // lookupTable: lookupTableAccount.key, + // addresses: [question, dao, passAmm, failAmm], + // }); + + // // await this.advanceBySlots(1n) + // const currentClock = await banksClient.getClock(); + // banksClient.setClock( + // new Clock( + // currentClock.slot + 1n, + // currentClock.epochStartTimestamp, + // currentClock.epoch, + // currentClock.leaderScheduleEpoch, + // 50n + // ) + // ); + + // // Create and extend the lookup table + // let tx2 = new Transaction().add(extendInstruction); + // tx2.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + // tx2.feePayer = this.provider.publicKey; + // tx2.sign((this.provider.wallet as any).payer); + // await banksClient.processTransaction(tx2); + + // const extendInstruction2 = AddressLookupTableProgram.extendLookupTable({ + // payer: this.provider.publicKey, + // authority: this.provider.publicKey, + // lookupTable: lookupTableAccount.key, + // addresses: [question], + // }); + + // // await this.advanceBySlots(1n) + + // // Create and extend the lookup table + // let tx3 = new Transaction().add(extendInstruction2); + // tx3.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + // tx3.feePayer = this.provider.publicKey; + // tx3.sign((this.provider.wallet as any).payer); + // await banksClient.processTransaction(tx3); + + // // console.log((this.provider.connection as any).banksClient); + + // console.log(tx.instructions); + + // const messageV0 = new TransactionMessage({ + // payerKey: this.provider.publicKey, + // recentBlockhash: (await banksClient.getLatestBlockhash())[0], + // instructions: tx.instructions, + // }).compileToV0Message([lookupTableAccount]); + + // console.log(messageV0.addressTableLookups); + + // const transactionV0 = new VersionedTransaction(messageV0); + + // console.log((this.provider.wallet as any).payer); + + // transactionV0.sign([(this.provider.wallet as any).payer as any]); + + // console.log(transactionV0.serialize().length); + + // tx.instructions = tx.instructions.slice(2); + // console.log(tx.instructions[0].data.length); + + // // await this.provider.connection.sendRawTransaction( + // // transactionV0.serialize() + // // ); + } return proposal; } - // async createProposalTxAndPDAs( - // dao: PublicKey, - // descriptionUrl: string, - // instruction: ProposalInstruction, - // baseTokensToLP: BN, - // quoteTokensToLP: BN - // ): Promise< - // [ - // Transaction[], - // { - // proposalAcct: PublicKey; - // baseCondVaultAcct: PublicKey; - // quoteCondVaultAcct: PublicKey; - // passMarketAcct: PublicKey; - // failMarketAcct: PublicKey; - // } - // ] - // > { - // const storedDao = await this.getDao(dao); - - // const nonce = new BN(Math.random() * 2 ** 50); - - // let [proposal] = getProposalAddr( - // this.autocrat.programId, - // this.provider.publicKey, - // nonce - // ); - - // const { - // baseVault, - // quoteVault, - // passAmm, - // failAmm, - // passBaseMint, - // passQuoteMint, - // failBaseMint, - // failQuoteMint, - // } = this.getProposalPdas( - // proposal, - // storedDao.tokenMint, - // storedDao.usdcMint, - // dao - // ); - - // // it's important that these happen in a single atomic transaction - // const initVaultTx = await this.vaultClient - // .initializeVaultIx(proposal, storedDao.tokenMint) - // .postInstructions( - // await InstructionUtils.getInstructions( - // this.vaultClient.initializeVaultIx(proposal, storedDao.usdcMint), - // this.ammClient.createAmmIx( - // passBaseMint, - // passQuoteMint, - // storedDao.twapInitialObservation, - // storedDao.twapMaxObservationChangePerUpdate - // ), - // this.ammClient.createAmmIx( - // failBaseMint, - // failQuoteMint, - // storedDao.twapInitialObservation, - // storedDao.twapMaxObservationChangePerUpdate - // ) - // ) - // ) - // .transaction(); - - // const mintConditionalTokensTx = await this.vaultClient - // .mintConditionalTokensIx(baseVault, storedDao.tokenMint, baseTokensToLP) - // .postInstructions( - // await InstructionUtils.getInstructions( - // this.vaultClient.mintConditionalTokensIx( - // quoteVault, - // storedDao.usdcMint, - // quoteTokensToLP - // ) - // ) - // ) - // .transaction(); - - // const addLiquidityTx = await this.ammClient - // .addLiquidityIx( - // passAmm, - // passBaseMint, - // passQuoteMint, - // quoteTokensToLP, - // baseTokensToLP, - // new BN(0) - // ) - // .postInstructions( - // await InstructionUtils.getInstructions( - // this.ammClient.addLiquidityIx( - // failAmm, - // failBaseMint, - // failQuoteMint, - // quoteTokensToLP, - // baseTokensToLP, - // new BN(0) - // ) - // ) - // ) - // .transaction(); - - // // this is how many original tokens are created - // const lpTokens = quoteTokensToLP; - - // const initTx = await this.initializeProposalIx( - // descriptionUrl, - // instruction, - // dao, - // storedDao.tokenMint, - // storedDao.usdcMint, - // lpTokens, - // lpTokens, - // nonce, - // question - // ).transaction(); - - // return [ - // [initVaultTx, mintConditionalTokensTx, addLiquidityTx, initTx], - // { - // baseCondVaultAcct: baseVault, - // quoteCondVaultAcct: quoteVault, - // failMarketAcct: failAmm, - // passMarketAcct: passAmm, - // proposalAcct: proposal, - // }, - // ]; - // } + async initializeProposalTx( + dao: PublicKey, + descriptionUrl: string, + instruction: ProposalInstruction, + baseTokensToLP: BN, + quoteTokensToLP: BN, + nonce: BN = new BN(Math.random() * 2 ** 50) + ): Promise<{ transactions: Transaction[]; proposal: PublicKey }> { + const storedDao = await this.getDao(dao); + + let [proposal] = getProposalAddr( + this.autocrat.programId, + this.provider.publicKey, + nonce + ); + + // Get the initialize question transaction + const questionTx = await this.vaultClient + .initializeQuestionIx( + sha256(`Will ${proposal} pass?/FAIL/PASS`), + proposal, + 2 + ) + .transaction(); + + const { + baseVault, + quoteVault, + passAmm, + failAmm, + passBaseMint, + passQuoteMint, + failBaseMint, + failQuoteMint, + question, + } = this.getProposalPdas( + proposal, + storedDao.tokenMint, + storedDao.usdcMint, + dao + ); + + // Get the initialize vaults and AMMs transaction + const initializeVaultsTx = await this.vaultClient + .initializeVaultIx(question, storedDao.tokenMint, 2) + .postInstructions( + await InstructionUtils.getInstructions( + this.vaultClient.initializeVaultIx(question, storedDao.usdcMint, 2), + this.ammClient.initializeAmmIx( + passBaseMint, + passQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ), + this.ammClient.initializeAmmIx( + failBaseMint, + failQuoteMint, + storedDao.twapStartDelaySlots, + storedDao.twapInitialObservation, + storedDao.twapMaxObservationChangePerUpdate + ) + ) + ) + .transaction(); + + // Get the split tokens transaction + const splitTokensTx = await this.vaultClient + .splitTokensIx( + question, + baseVault, + storedDao.tokenMint, + baseTokensToLP, + 2 + ) + .postInstructions( + await InstructionUtils.getInstructions( + this.vaultClient.splitTokensIx( + question, + quoteVault, + storedDao.usdcMint, + quoteTokensToLP, + 2 + ) + ) + ) + .transaction(); + + // Get the add liquidity transaction + const addLiquidityTx = await this.ammClient + .addLiquidityIx( + passAmm, + passBaseMint, + passQuoteMint, + quoteTokensToLP, + baseTokensToLP, + new BN(0) + ) + .postInstructions( + await InstructionUtils.getInstructions( + this.ammClient.addLiquidityIx( + failAmm, + failBaseMint, + failQuoteMint, + quoteTokensToLP, + baseTokensToLP, + new BN(0) + ) + ) + ) + .transaction(); + + // Get the initialize proposal transaction + const lpTokens = quoteTokensToLP; + const initializeProposalTx = await this.initializeProposalIx( + descriptionUrl, + instruction, + dao, + storedDao.tokenMint, + storedDao.usdcMint, + lpTokens, + lpTokens, + nonce, + question + ).transaction(); + + return { + transactions: [ + questionTx, + initializeVaultsTx, + splitTokensTx, + addLiquidityTx, + initializeProposalTx, + ], + proposal, + }; + } initializeProposalIx( descriptionUrl: string, diff --git a/sdk/src/v0.4/types/futarchy_amm.ts b/sdk/src/v0.4/types/futarchy_amm.ts new file mode 100644 index 000000000..b839c0906 --- /dev/null +++ b/sdk/src/v0.4/types/futarchy_amm.ts @@ -0,0 +1,23 @@ +export type FutarchyAmm = { + version: "0.1.0"; + name: "futarchy_amm"; + instructions: [ + { + name: "initialize"; + accounts: []; + args: []; + } + ]; +}; + +export const IDL: FutarchyAmm = { + version: "0.1.0", + name: "futarchy_amm", + instructions: [ + { + name: "initialize", + accounts: [], + args: [], + }, + ], +}; diff --git a/sdk/src/v0.4/types/shared_liquidity_manager.ts b/sdk/src/v0.4/types/shared_liquidity_manager.ts new file mode 100644 index 000000000..db77d8152 --- /dev/null +++ b/sdk/src/v0.4/types/shared_liquidity_manager.ts @@ -0,0 +1,659 @@ +export type SharedLiquidityManager = { + version: "0.1.0"; + name: "shared_liquidity_manager"; + instructions: [ + { + name: "initializePool"; + accounts: [ + { + name: "pool"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolState"; + isMut: false; + isSigner: false; + }, + { + name: "dao"; + isMut: false; + isSigner: false; + }, + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: []; + }, + { + name: "deposit"; + accounts: [ + { + name: "pool"; + isMut: true; + isSigner: false; + }, + { + name: "spotPoolState"; + isMut: true; + isSigner: false; + }, + { + name: "dao"; + isMut: false; + isSigner: false; + }, + { + name: "userTokenA"; + isMut: true; + isSigner: false; + docs: ["The user's token accounts for the pool tokens"]; + }, + { + name: "userTokenB"; + isMut: true; + isSigner: false; + }, + { + name: "token0Vault"; + isMut: true; + isSigner: false; + docs: ["The pool's token accounts"]; + }, + { + name: "token1Vault"; + isMut: true; + isSigner: false; + }, + { + name: "vault0Mint"; + isMut: false; + isSigner: false; + docs: ["The vault mints"]; + }, + { + name: "vault1Mint"; + isMut: false; + isSigner: false; + }, + { + name: "lpMint"; + isMut: true; + isSigner: false; + docs: ["The LP token mint and destination"]; + }, + { + name: "userLpToken"; + isMut: true; + isSigner: false; + }, + { + name: "position"; + isMut: true; + isSigner: false; + docs: ["The user's liquidity position"]; + }, + { + name: "user"; + isMut: true; + isSigner: true; + }, + { + name: "raydiumAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram2022"; + isMut: false; + isSigner: false; + }, + { + name: "cpSwapProgram"; + isMut: false; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: "args"; + type: { + defined: "DepositArgs"; + }; + } + ]; + }, + { + name: "withdraw"; + accounts: [ + { + name: "pool"; + isMut: true; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: []; + }, + { + name: "initializeProposalWithLiquidity"; + accounts: [ + { + name: "pool"; + isMut: true; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: []; + }, + { + name: "removeProposalLiquidity"; + accounts: [ + { + name: "pool"; + isMut: true; + isSigner: false; + }, + { + name: "eventAuthority"; + isMut: false; + isSigner: false; + }, + { + name: "program"; + isMut: false; + isSigner: false; + } + ]; + args: []; + } + ]; + accounts: [ + { + name: "liquidityPosition"; + type: { + kind: "struct"; + fields: [ + { + name: "owner"; + docs: ["The owner of this position"]; + type: "publicKey"; + }, + { + name: "pool"; + docs: ["The shared liquidity pool this position belongs to"]; + type: "publicKey"; + }, + { + name: "underlyingSpotLpShares"; + docs: [ + "The amount of underlying spot LP shares this position represents" + ]; + type: "u64"; + }, + { + name: "bump"; + docs: ["The PDA bump"]; + type: "u8"; + } + ]; + }; + }, + { + name: "sharedLiquidityPool"; + type: { + kind: "struct"; + fields: [ + { + name: "pdaBump"; + docs: ["The PDA bump."]; + type: "u8"; + }, + { + name: "spotPoolState"; + docs: ["The Raydium spot pool state."]; + type: "publicKey"; + }, + { + name: "dao"; + docs: ["The DAO."]; + type: "publicKey"; + }, + { + name: "isActiveProposal"; + docs: [ + "Whether there's an active proposal using liquidity from this pool." + ]; + type: "bool"; + }, + { + name: "seqNum"; + docs: [ + "The sequence number of this shared liquidity pool. Useful for sorting events." + ]; + type: "u64"; + } + ]; + }; + } + ]; + types: [ + { + name: "DepositArgs"; + type: { + kind: "struct"; + fields: [ + { + name: "lpTokenAmount"; + docs: ["The amount of LP tokens to mint"]; + type: "u64"; + }, + { + name: "maximumToken0Amount"; + docs: ["The maximum amount of token 0 to deposit"]; + type: "u64"; + }, + { + name: "maximumToken1Amount"; + docs: ["The maximum amount of token 1 to deposit"]; + type: "u64"; + } + ]; + }; + } + ]; + errors: [ + { + code: 6000; + name: "PoolInUse"; + msg: "Pool is currently being used by an active proposal"; + } + ]; +}; + +export const IDL: SharedLiquidityManager = { + version: "0.1.0", + name: "shared_liquidity_manager", + instructions: [ + { + name: "initializePool", + accounts: [ + { + name: "pool", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolState", + isMut: false, + isSigner: false, + }, + { + name: "dao", + isMut: false, + isSigner: false, + }, + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + { + name: "deposit", + accounts: [ + { + name: "pool", + isMut: true, + isSigner: false, + }, + { + name: "spotPoolState", + isMut: true, + isSigner: false, + }, + { + name: "dao", + isMut: false, + isSigner: false, + }, + { + name: "userTokenA", + isMut: true, + isSigner: false, + docs: ["The user's token accounts for the pool tokens"], + }, + { + name: "userTokenB", + isMut: true, + isSigner: false, + }, + { + name: "token0Vault", + isMut: true, + isSigner: false, + docs: ["The pool's token accounts"], + }, + { + name: "token1Vault", + isMut: true, + isSigner: false, + }, + { + name: "vault0Mint", + isMut: false, + isSigner: false, + docs: ["The vault mints"], + }, + { + name: "vault1Mint", + isMut: false, + isSigner: false, + }, + { + name: "lpMint", + isMut: true, + isSigner: false, + docs: ["The LP token mint and destination"], + }, + { + name: "userLpToken", + isMut: true, + isSigner: false, + }, + { + name: "position", + isMut: true, + isSigner: false, + docs: ["The user's liquidity position"], + }, + { + name: "user", + isMut: true, + isSigner: true, + }, + { + name: "raydiumAuthority", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram2022", + isMut: false, + isSigner: false, + }, + { + name: "cpSwapProgram", + isMut: false, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: "args", + type: { + defined: "DepositArgs", + }, + }, + ], + }, + { + name: "withdraw", + accounts: [ + { + name: "pool", + isMut: true, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + { + name: "initializeProposalWithLiquidity", + accounts: [ + { + name: "pool", + isMut: true, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + { + name: "removeProposalLiquidity", + accounts: [ + { + name: "pool", + isMut: true, + isSigner: false, + }, + { + name: "eventAuthority", + isMut: false, + isSigner: false, + }, + { + name: "program", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + ], + accounts: [ + { + name: "liquidityPosition", + type: { + kind: "struct", + fields: [ + { + name: "owner", + docs: ["The owner of this position"], + type: "publicKey", + }, + { + name: "pool", + docs: ["The shared liquidity pool this position belongs to"], + type: "publicKey", + }, + { + name: "underlyingSpotLpShares", + docs: [ + "The amount of underlying spot LP shares this position represents", + ], + type: "u64", + }, + { + name: "bump", + docs: ["The PDA bump"], + type: "u8", + }, + ], + }, + }, + { + name: "sharedLiquidityPool", + type: { + kind: "struct", + fields: [ + { + name: "pdaBump", + docs: ["The PDA bump."], + type: "u8", + }, + { + name: "spotPoolState", + docs: ["The Raydium spot pool state."], + type: "publicKey", + }, + { + name: "dao", + docs: ["The DAO."], + type: "publicKey", + }, + { + name: "isActiveProposal", + docs: [ + "Whether there's an active proposal using liquidity from this pool.", + ], + type: "bool", + }, + { + name: "seqNum", + docs: [ + "The sequence number of this shared liquidity pool. Useful for sorting events.", + ], + type: "u64", + }, + ], + }, + }, + ], + types: [ + { + name: "DepositArgs", + type: { + kind: "struct", + fields: [ + { + name: "lpTokenAmount", + docs: ["The amount of LP tokens to mint"], + type: "u64", + }, + { + name: "maximumToken0Amount", + docs: ["The maximum amount of token 0 to deposit"], + type: "u64", + }, + { + name: "maximumToken1Amount", + docs: ["The maximum amount of token 1 to deposit"], + type: "u64", + }, + ], + }, + }, + ], + errors: [ + { + code: 6000, + name: "PoolInUse", + msg: "Pool is currently being used by an active proposal", + }, + ], +}; diff --git a/tests/autocrat/integration/streamflow.test.ts b/tests/autocrat/integration/streamflow.test.ts new file mode 100644 index 000000000..1f3778683 --- /dev/null +++ b/tests/autocrat/integration/streamflow.test.ts @@ -0,0 +1,484 @@ +import { + AmmClient, + AUTOCRAT_PROGRAM_ID, + AutocratClient, + getAmmAddr, + getAmmLpMintAddr, + initializeProposalTx, +} from "@metadaoproject/futarchy/v0.4"; +import { Keypair, PublicKey, AddressLookupTableProgram, Transaction, AddressLookupTableAccount, TransactionMessage, VersionedTransaction, SystemProgram } from "@solana/web3.js"; +import { assert } from "chai"; +import { + createMint, + createAssociatedTokenAccount, + mintTo, + getAccount, + getMint, +} from "spl-token-bankrun"; +import * as anchor from "@coral-xyz/anchor"; +import * as token from "@solana/spl-token"; +import { DAY_IN_SLOTS, expectError, toBN } from "../../utils.js"; +import { BN } from "bn.js"; + +import { StreamflowEscrow, IDL as StreamflowEscrowIDL } from "../../fixtures/streamflow_escrow.js"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { STREAMFLOW_VESTING_PROGRAM_ID } from "../../main.test.js"; +import { Clock } from "solana-bankrun"; +import { ComputeBudgetProgram } from "@solana/web3.js"; +import { ConditionalVaultClient } from "@metadaoproject/futarchy/v0.4"; +// import { IDL as StreamflowEscrowIDL } from "../../fixtures/streamflow_escrow.json"; + + +export const ORDER_PREFIX = Buffer.from('order', 'utf-8'); +export const EXECUTION_RECORD_PREFIX = Buffer.from('execution-record', 'utf-8'); +export const ESCROW_PREFIX = Buffer.from('strm', 'utf-8'); + +export const deriveOrderPDA = ( + programId: anchor.web3.PublicKey, + creator: anchor.web3.PublicKey, + baseMint: anchor.web3.PublicKey, + nonce: number, +): anchor.web3.PublicKey => { + return anchor.web3.PublicKey.findProgramAddressSync( + [ORDER_PREFIX, creator.toBuffer(), baseMint.toBuffer(), new BN(nonce).toArrayLike(Buffer, 'le', 4)], + programId, + )[0]; +}; + +export const deriveExecutionRecordPDA = ( + programId: anchor.web3.PublicKey, + order: anchor.web3.PublicKey, + executor: anchor.web3.PublicKey, + nonce: number, +): anchor.web3.PublicKey => { + return anchor.web3.PublicKey.findProgramAddressSync( + [EXECUTION_RECORD_PREFIX, order.toBuffer(), executor.toBuffer(), new BN(nonce).toArrayLike(Buffer, 'le', 4)], + programId, + )[0]; +}; + +export const deriveEscrowPDA = ( + programId: anchor.web3.PublicKey, + contractKey: anchor.web3.PublicKey, +): anchor.web3.PublicKey => { + return anchor.web3.PublicKey.findProgramAddressSync([ESCROW_PREFIX, contractKey.toBuffer()], programId)[0]; +}; + + +export default async function () { + let ammClient: AmmClient; + let autocratClient: AutocratClient; + let vaultClient: ConditionalVaultClient; + let RAY: PublicKey; + let USDC: PublicKey; + let amm: PublicKey; + + RAY = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 9 + ); + USDC = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + this.payer.publicKey, + 6 + ); + + await this.createTokenAccount(RAY, this.payer.publicKey); + await this.createTokenAccount(USDC, this.payer.publicKey); + + await this.mintTo(RAY, this.payer.publicKey, this.payer, 10000000 * 10 ** 9); + await this.mintTo(USDC, this.payer.publicKey, this.payer, 100000000000 * 10 ** 6); + + autocratClient = this.autocratClient; + ammClient = this.ammClient; + vaultClient = this.vaultClient; + + const STREAMFLOW_ESCROW_PROGRAM_ID = new PublicKey("ESCRoWj8QUJ5cTXCBWbGpW6AzaaEAtRbZuwKp8c4YYGs"); + const escrow = new anchor.Program(StreamflowEscrowIDL as anchor.Idl, STREAMFLOW_ESCROW_PROGRAM_ID); + + const authority = this.payer.publicKey; + // const baseMint = new PublicKey(baseAddr); + // const quoteMint = new PublicKey(quoteAddr); + const amount = new BN(684_208_000_000); + const price = new BN(1_000_000_000_000); + const orderNonce = 1; + const vestingStartTs = new BN(Math.floor(Date.now() / 1000) + 3600); + const vestingPeriod = new BN(30); + const vestingAmountPerPeriod = new BN(3_759_384_615); + const vestingCliffAmount = new BN(0); + + const orderKey = deriveOrderPDA(STREAMFLOW_ESCROW_PROGRAM_ID, authority, RAY, orderNonce); + const vaultKey = token.getAssociatedTokenAddressSync(RAY, orderKey, true); + + + // const treasury = Keypair.generate(); + + // // Send 1 SOL to treasury + // const tx = new anchor.web3.Transaction(); + // tx.add( + // anchor.web3.SystemProgram.transfer({ + // fromPubkey: this.payer.publicKey, + // toPubkey: treasury.publicKey, + // lamports: 1_000_000_000, // 1 SOL = 1 billion lamports + // }) + // ); + // tx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + // console.log('recentBlockhash', tx.recentBlockhash); + // tx.feePayer = this.payer.publicKey; + // tx.sign(this.payer); + // await this.banksClient.processTransaction(tx); + + const daoKeypair = Keypair.generate(); + const dao = await autocratClient.initializeDao( + RAY, + 1000, // tokenPriceUiAmount + 5, // minBaseFutarchicLiquidity + 5000, // minQuoteFutarchicLiquidity + USDC, + daoKeypair, + new BN(Number(DAY_IN_SLOTS)) + ); + const daoTreasury = await autocratClient.getDao(dao).then(dao => dao.treasury); + + console.log('Creating vested order:', orderKey.toBase58()); + await escrow.methods + .createOrderFixed({ + nonce: orderNonce, + amount, + startPrice: price, + partialAllowed: false, + expiryTs: new BN(1_752_508_800), + claimType: { vested: {} }, + vestingStartTs, + vestingPeriod, + vestingAmountPerPeriod, + vestingCliffAmount, + }) + .accounts({ + creator: authority, + baseMint: RAY, + quoteMint: USDC, + order: orderKey, + vault: vaultKey, + from: token.getAssociatedTokenAddressSync(RAY, authority), + executor: daoTreasury, + partner: null, + }) + .rpc(); + + const fillNonce = 0; + + // await this.createTokenAccount(RAY, treasury.publicKey); + // await this.createTokenAccount(USDC, treasury.publicKey); + // await this.mintTo(USDC, treasury.publicKey, this.payer, 1000000 * 10 ** 6); + + + // Create a lookup table for the accounts + const slot = await this.banksClient.getSlot(); + const [lookupTableInst, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({ + authority: this.payer.publicKey, + payer: this.payer.publicKey, + recentSlot: slot - 1n, + }); + + + + // Add all the accounts needed for fillOrderVested to the lookup table + const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + payer: this.payer.publicKey, + authority: this.payer.publicKey, + lookupTable: lookupTableAddress, + addresses: [ + // authority, + token.getAssociatedTokenAddressSync(USDC, authority), + token.getAssociatedTokenAddressSync(RAY, authority), + // orderKey, + token.getAssociatedTokenAddressSync(USDC, this.payer.publicKey), + token.TOKEN_PROGRAM_ID, + // vaultKey, + // RAY, + // USDC, + this.payer.publicKey, + daoKeypair.publicKey, + // contractKey, + // new PublicKey("wdrwhnCv4pzW8beKsbPa4S2UDZrXenjg16KJdKSpb5u"), + // new PublicKey("B743wFVk2pCYhV91cn287e1xY7f1vt4gdY48hhNiuQmT"), + // escrowKey, + // recordKey, + // STREAMFLOW_VESTING_PROGRAM_ID, + // AUTOCRAT_PROGRAM_ID, + ], + }); + + await this.advanceBySlots(1n) + + // Create and extend the lookup table + let tx = new Transaction().add(lookupTableInst, extendInstruction); + tx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + tx.feePayer = this.payer.publicKey; + tx.sign(this.payer); + await this.banksClient.processTransaction(tx); + + await this.advanceBySlots(1n) + + const extentIx2 = AddressLookupTableProgram.extendLookupTable({ + payer: this.payer.publicKey, + authority: this.payer.publicKey, + lookupTable: lookupTableAddress, + addresses: [ + this.payer.publicKey, + ], + }); + + tx = new Transaction().add(extentIx2); + tx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + tx.feePayer = this.payer.publicKey; + tx.sign(this.payer); + await this.banksClient.processTransaction(tx); + + + let lookupTable = await this.banksClient.getAccount(lookupTableAddress); + + console.log(lookupTable); + let acc = new AddressLookupTableAccount({ + key: lookupTableAddress, + state: AddressLookupTableAccount.deserialize(lookupTable.data), + }); + + // Create a DAO if it doesn't exist + + + await this.createTokenAccount(RAY, daoTreasury); + await this.createTokenAccount(USDC, daoTreasury); + await this.mintTo(USDC, daoTreasury, this.payer, 1000000000000 * 10 ** 6); + // await this.mintTo(USDC, this.payer.publicKey, this.payer, 100000000000 * 10 ** 6); + + + const contractKeypair = Keypair.generate(); + const contractKey = contractKeypair.publicKey; + const escrowKey = deriveEscrowPDA(STREAMFLOW_VESTING_PROGRAM_ID, contractKey); + const recordKey = deriveExecutionRecordPDA(STREAMFLOW_ESCROW_PROGRAM_ID, orderKey, daoTreasury, fillNonce); + + + // Create the fillOrderVested instruction + const fillOrderIx = await escrow.methods + .fillOrderVested(fillNonce, amount, price, false) + .accounts({ + common: { + executor: daoTreasury, + from: token.getAssociatedTokenAddressSync(USDC, daoTreasury, true), + toBase: token.getAssociatedTokenAddressSync(RAY, daoTreasury, true), + order: orderKey, + toQuote: token.getAssociatedTokenAddressSync(USDC, this.payer.publicKey), + baseTokenProgram: token.TOKEN_PROGRAM_ID, + quotaTokenProgram: token.TOKEN_PROGRAM_ID, + vault: vaultKey, + creator: authority, + baseMint: RAY, + quoteMint: USDC, + }, + streamMetadata: contractKey, + withdrawor: new PublicKey("wdrwhnCv4pzW8beKsbPa4S2UDZrXenjg16KJdKSpb5u"), + feeOracle: new PublicKey("B743wFVk2pCYhV91cn287e1xY7f1vt4gdY48hhNiuQmT"), + escrowTokens: escrowKey, + executionRecord: recordKey, + streamflowProgram: STREAMFLOW_VESTING_PROGRAM_ID, + }) + .transaction(); + + fillOrderIx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + fillOrderIx.feePayer = this.payer.publicKey; + fillOrderIx.sign(this.payer); + + // console.log(fillOrderIx.serialize().length) + + + // console.log(fillOrderIx.keys.length) + // console.log(fillOrderIx.data.length) + + // Create a proposal with the fillOrderVested instruction using lookup table addresses + const { transactions, proposal } = await (autocratClient as any).initializeProposalTx( + dao, + "", + { + programId: fillOrderIx.instructions[0].programId, + accounts: fillOrderIx.instructions[0].keys.map(key => ({ + pubkey: key.pubkey, + isSigner: key.isSigner, + isWritable: key.isWritable + })), + data: fillOrderIx.instructions[0].data, + }, + new BN(5 * 10 ** 9), // baseTokensToLP + new BN(5000 * 10 ** 6) // quoteTokensToLP + ); + + const transferIx = SystemProgram.transfer({ + fromPubkey: this.payer.publicKey, + toPubkey: daoTreasury, + lamports: 1_000_000_000, + }); + + const transferTx = new Transaction().add(transferIx); + transferTx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + transferTx.feePayer = this.payer.publicKey; + transferTx.sign(this.payer); + await this.banksClient.processTransaction(transferTx); + + await this.advanceBySlots(1n) + + // Process all transactions except the last one + for (let i = 0; i < transactions.length - 1; i++) { + const tx = transactions[i]; + tx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + tx.feePayer = this.payer.publicKey; + tx.sign(this.payer); + await this.banksClient.processTransaction(tx); + await this.advanceBySlots(1n); + } + + // Extract unique accounts from the last instruction to add to lookup table + const lastTx = transactions[transactions.length - 1]; + const accountsToAdd = lastTx.instructions.map(instruction => instruction.keys.map(key => key.pubkey)); + const uniqueAccounts = [...new Set(accountsToAdd.flat())] as PublicKey[]; + + // Create extend instruction + const extendInstruction3 = AddressLookupTableProgram.extendLookupTable({ + payer: this.payer.publicKey, + authority: this.payer.publicKey, + lookupTable: acc.key, + addresses: uniqueAccounts + }); + + // Execute extend instruction + let extendTx = new Transaction().add(extendInstruction3); + extendTx.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + extendTx.feePayer = this.payer.publicKey; + extendTx.sign(this.payer); + await this.banksClient.processTransaction(extendTx); + + await this.advanceBySlots(1n) + + const extendInstruction4 = AddressLookupTableProgram.extendLookupTable({ + payer: this.payer.publicKey, + authority: this.payer.publicKey, + lookupTable: acc.key, + addresses: [daoTreasury] + }); + + // Execute extend instruction + let extendTx4 = new Transaction().add(extendInstruction4); + extendTx4.recentBlockhash = (await this.banksClient.getLatestBlockhash())[0]; + extendTx4.feePayer = this.payer.publicKey; + extendTx4.sign(this.payer); + await this.banksClient.processTransaction(extendTx4); + + await this.advanceBySlots(1n) + + lookupTable = await this.banksClient.getAccount(lookupTableAddress); + + console.log(lookupTable); + let acc2 = new AddressLookupTableAccount({ + key: lookupTableAddress, + state: AddressLookupTableAccount.deserialize(lookupTable.data), + }); + + console.log(proposal); + + const messageV0 = new TransactionMessage({ + payerKey: this.payer.publicKey, + recentBlockhash: (await this.banksClient.getLatestBlockhash())[0], + instructions: lastTx.instructions, + }).compileToV0Message([acc2]); + + console.log(messageV0.addressTableLookups); + + return; + + const transactionV0 = new VersionedTransaction(messageV0); + + transactionV0.sign([this.payer]); + + console.log(transactionV0.serialize().length); + + await this.banksClient.processTransaction(transactionV0); + + console.log(await autocratClient.getProposal(proposal)); + + // Wait for proposal to be old enough + const storedProposal = await autocratClient.getProposal(proposal); + const storedDao = await autocratClient.getDao(dao); + const slotsToWait = Number(storedDao.slotsPerProposal) + 1; + await this.advanceBySlots(BigInt(slotsToWait + 100)); + + // Get the AMMs and vaults for the proposal + const { passAmm, failAmm, passBaseMint, passQuoteMint, baseVault, quoteVault, question } = autocratClient.getProposalPdas( + proposal, + RAY, + USDC, + dao + ); + + // Split tokens in the vaults + await vaultClient + .splitTokensIx(question, baseVault, RAY, new BN(10 * 10 ** 9), 2) + .rpc(); + await vaultClient + .splitTokensIx(question, quoteVault, USDC, new BN(10_000 * 1_000_000), 2) + .rpc(); + + // Swap in the pass market to make it pass + await ammClient + .swapIx( + passAmm, + passBaseMint, + passQuoteMint, + { buy: {} }, + new BN(1000).muln(1_000_000), // Swap $1 worth + new BN(0) + ) + .rpc(); + + // Crank the TWAP multiple times to ensure good price data + for (let i = 0; i < 50; i++) { + await this.advanceBySlots(20_000n); + + await ammClient + .crankThatTwapIx(passAmm) + .preInstructions([ + // this is to get around bankrun thinking we've processed the same transaction multiple times + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: i, + }), + await ammClient.crankThatTwapIx(failAmm).instruction(), + ]) + .rpc(); + } + + // Finalize the proposal + await autocratClient.finalizeProposal(proposal); + + // Check if proposal passed and execute if it did + const updatedProposal = await autocratClient.getProposal(proposal); + console.log(updatedProposal); + // if (updatedProposal.state.passed) { + await autocratClient.executeProposalIx(proposal, dao, updatedProposal.instruction).preInstructions([ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 })]).signers([contractKeypair]).rpc(); + // } + + // transactionV0.sign([(this.payer as any).payer as any]); + + // this.banksClient.sendRawTransaction(transactionV0.serialize()); + + // console.log(transactionV0.serialize().length); + + + + // Execute the proposal + // await autocratClient.executeProposal(proposal); +} + diff --git a/tests/autocrat/main.test.ts b/tests/autocrat/main.test.ts index e69de29bb..472fb8d44 100644 --- a/tests/autocrat/main.test.ts +++ b/tests/autocrat/main.test.ts @@ -0,0 +1,7 @@ +import autocrat from "./autocrat.js"; +import streamflow from "./integration/streamflow.test.js"; + +export default function suite() { + describe("#autocrat", autocrat); + it.only("Streamflow integration test", streamflow); +} diff --git a/tests/fixtures/fee-oracle b/tests/fixtures/fee-oracle new file mode 100644 index 000000000..8502250db Binary files /dev/null and b/tests/fixtures/fee-oracle differ diff --git a/tests/fixtures/streamflow_escrow.so b/tests/fixtures/streamflow_escrow.so new file mode 100644 index 000000000..dbc78a2ac Binary files /dev/null and b/tests/fixtures/streamflow_escrow.so differ diff --git a/tests/fixtures/streamflow_escrow.ts b/tests/fixtures/streamflow_escrow.ts new file mode 100644 index 000000000..252f73bfc --- /dev/null +++ b/tests/fixtures/streamflow_escrow.ts @@ -0,0 +1,2627 @@ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/streamflow_escrow.json`. + */ +export type StreamflowEscrow = { + "address": "ESCRoWj8QUJ5cTXCBWbGpW6AzaaEAtRbZuwKp8c4YYGs", + "name": "streamflow_escrow", + "version": "0.1.0", + "metadata": { + "name": "streamflowEscrow", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "cancelOrder", + "docs": [ + "Cancel an order.", + "", + "- Order creator creator can cancel anytime;", + "- pre-configured `executor` can also cancel the order, meaning that they reject it;", + "- anyone can cancel an order after it has been expired, tokens will always be returned to the order creator;" + ], + "discriminator": [ + 95, + 129, + 237, + 240, + 8, + 49, + 223, + 132 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "Account that will cover tx fees, should be equal to creator if not is not expired" + ], + "writable": true, + "signer": true + }, + { + "name": "order", + "docs": [ + "Order to cancel" + ], + "writable": true + }, + { + "name": "vault", + "docs": [ + "Vault that stores base tokens" + ], + "writable": true, + "relations": [ + "order" + ] + }, + { + "name": "creator", + "writable": true, + "relations": [ + "order" + ] + }, + { + "name": "to", + "docs": [ + "Token account that will receive back base mint tokens" + ], + "writable": true + }, + { + "name": "baseMint", + "docs": [ + "Quote mint of the order" + ], + "writable": true, + "relations": [ + "order" + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL token program interface." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program." + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "createOrderFixed", + "docs": [ + "Create an order with fixed `start_price`" + ], + "discriminator": [ + 238, + 32, + 66, + 30, + 214, + 227, + 110, + 135 + ], + "accounts": [ + { + "name": "creator", + "docs": [ + "Order creator" + ], + "writable": true, + "signer": true + }, + { + "name": "from", + "docs": [ + "Token account from which base token will be transferred" + ], + "writable": true + }, + { + "name": "order", + "docs": [ + "Order to cancel" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 111, + 114, + 100, + 101, + 114 + ] + }, + { + "kind": "account", + "path": "creator" + }, + { + "kind": "account", + "path": "baseMint" + }, + { + "kind": "arg", + "path": "ix.nonce" + } + ] + } + }, + { + "name": "vault", + "docs": [ + "Vault that stores base tokens" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "order" + }, + { + "kind": "account", + "path": "tokenProgram" + }, + { + "kind": "account", + "path": "baseMint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "executor", + "optional": true + }, + { + "name": "partner", + "optional": true + }, + { + "name": "baseMint", + "docs": [ + "Quote mint of the order" + ] + }, + { + "name": "quoteMint", + "docs": [ + "Quote mint of the order" + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL token program interface." + ] + }, + { + "name": "associatedTokenProgram", + "docs": [ + "The [Associated Token] program." + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "systemProgram", + "docs": [ + "System program." + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "ix", + "type": { + "defined": { + "name": "CreateOrderFixedIx" + } + } + } + ] + }, + { + "name": "fillOrderInstant", + "docs": [ + "Fill an order, claim `amount` of base tokens instantly." + ], + "discriminator": [ + 247, + 221, + 198, + 23, + 4, + 169, + 68, + 121 + ], + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "executor", + "docs": [ + "Order executor" + ], + "writable": true, + "signer": true + }, + { + "name": "from", + "docs": [ + "Token account from which quote token will be transferred" + ], + "writable": true + }, + { + "name": "toBase", + "docs": [ + "Executor TA that will receive base tokens" + ], + "writable": true + }, + { + "name": "order", + "docs": [ + "Order to cancel" + ], + "writable": true + }, + { + "name": "vault", + "docs": [ + "Vault that stores base tokens" + ], + "writable": true, + "relations": [ + "order" + ] + }, + { + "name": "creator", + "writable": true, + "relations": [ + "order" + ] + }, + { + "name": "toQuote", + "docs": [ + "Creator TA that will receive quote mint tokens" + ], + "writable": true + }, + { + "name": "baseMint", + "docs": [ + "Base mint of the order" + ], + "writable": true, + "relations": [ + "order" + ] + }, + { + "name": "quoteMint", + "docs": [ + "Quote mint of the order" + ], + "relations": [ + "order" + ] + }, + { + "name": "baseTokenProgram", + "docs": [ + "Token program used for base mint tokens" + ] + }, + { + "name": "quotaTokenProgram", + "docs": [ + "Token program used for quote mint tokens" + ] + } + ] + }, + { + "name": "executionRecord", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 120, + 101, + 99, + 117, + 116, + 105, + 111, + 110, + 45, + 114, + 101, + 99, + 111, + 114, + 100 + ] + }, + { + "kind": "account", + "path": "common.order", + "account": "fillCommon" + }, + { + "kind": "account", + "path": "common.executor", + "account": "fillCommon" + }, + { + "kind": "arg", + "path": "nonce" + } + ] + } + }, + { + "name": "systemProgram", + "docs": [ + "System program." + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "ceilAllowed", + "type": "bool" + } + ] + }, + { + "name": "fillOrderVested", + "docs": [ + "Fill an order, claim `amount` of base tokens in a vested fashion." + ], + "discriminator": [ + 69, + 211, + 62, + 131, + 240, + 89, + 30, + 218 + ], + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "executor", + "docs": [ + "Order executor" + ], + "writable": true, + "signer": true + }, + { + "name": "from", + "docs": [ + "Token account from which quote token will be transferred" + ], + "writable": true + }, + { + "name": "toBase", + "docs": [ + "Executor TA that will receive base tokens" + ], + "writable": true + }, + { + "name": "order", + "docs": [ + "Order to cancel" + ], + "writable": true + }, + { + "name": "vault", + "docs": [ + "Vault that stores base tokens" + ], + "writable": true, + "relations": [ + "order" + ] + }, + { + "name": "creator", + "writable": true, + "relations": [ + "order" + ] + }, + { + "name": "toQuote", + "docs": [ + "Creator TA that will receive quote mint tokens" + ], + "writable": true + }, + { + "name": "baseMint", + "docs": [ + "Base mint of the order" + ], + "writable": true, + "relations": [ + "order" + ] + }, + { + "name": "quoteMint", + "docs": [ + "Quote mint of the order" + ], + "relations": [ + "order" + ] + }, + { + "name": "baseTokenProgram", + "docs": [ + "Token program used for base mint tokens" + ] + }, + { + "name": "quotaTokenProgram", + "docs": [ + "Token program used for quote mint tokens" + ] + } + ] + }, + { + "name": "executionRecord", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 120, + 101, + 99, + 117, + 116, + 105, + 111, + 110, + 45, + 114, + 101, + 99, + 111, + 114, + 100 + ] + }, + { + "kind": "account", + "path": "common.order", + "account": "fillCommon" + }, + { + "kind": "account", + "path": "common.executor", + "account": "fillCommon" + }, + { + "kind": "arg", + "path": "nonce" + } + ] + } + }, + { + "name": "streamMetadata", + "writable": true, + "signer": true + }, + { + "name": "escrowTokens", + "writable": true + }, + { + "name": "withdrawor", + "writable": true, + "address": "wdrwhnCv4pzW8beKsbPa4S2UDZrXenjg16KJdKSpb5u" + }, + { + "name": "streamflowProgram", + "address": "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m" + }, + { + "name": "feeOracle", + "address": "B743wFVk2pCYhV91cn287e1xY7f1vt4gdY48hhNiuQmT" + }, + { + "name": "associatedTokenProgram", + "docs": [ + "Associated token program." + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "rent", + "docs": [ + "Sysvar rent." + ], + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "systemProgram", + "docs": [ + "System program." + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "ceilAllowed", + "type": "bool" + } + ] + } + ], + "accounts": [ + { + "name": "executionRecord", + "discriminator": [ + 133, + 201, + 182, + 206, + 224, + 111, + 45, + 168 + ] + }, + { + "name": "order", + "discriminator": [ + 134, + 173, + 223, + 185, + 77, + 86, + 28, + 51 + ] + } + ], + "events": [ + { + "name": "cancelEvent", + "discriminator": [ + 71, + 137, + 239, + 100, + 220, + 3, + 242, + 47 + ] + }, + { + "name": "createEvent", + "discriminator": [ + 27, + 114, + 169, + 77, + 222, + 235, + 99, + 118 + ] + }, + { + "name": "fillEvent", + "discriminator": [ + 13, + 89, + 41, + 228, + 105, + 178, + 45, + 112 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "unauthorized", + "msg": "Authority does not have permission for this action" + }, + { + "code": 6001, + "name": "arithmeticError", + "msg": "Arithmetic error" + }, + { + "code": 6002, + "name": "invalidStreamMetadata", + "msg": "Invalid Stream Metadata" + }, + { + "code": 6003, + "name": "invalidAmount", + "msg": "Amount should be greater than 0" + }, + { + "code": 6004, + "name": "invalidPrice", + "msg": "Price should be greater than 0" + }, + { + "code": 6005, + "name": "invalidExpiryTs", + "msg": "Expiry ts should be in the future" + }, + { + "code": 6006, + "name": "invalidVestingOptions", + "msg": "Provided vesting claim options are invalid" + }, + { + "code": 6007, + "name": "invalidMints", + "msg": "Base mint can not equal quote mint" + }, + { + "code": 6008, + "name": "unsupportedTokenExtensions", + "msg": "Mint has unsupported Token Extensions" + }, + { + "code": 6009, + "name": "invalidFillPrice", + "msg": "Price to fill the order does not match the order price" + }, + { + "code": 6010, + "name": "invalidFillAmount", + "msg": "Provided amount to fill is not valid" + }, + { + "code": 6011, + "name": "invalidClaimType", + "msg": "Provided claim type is invalid" + }, + { + "code": 6012, + "name": "orderFilled", + "msg": "Order is already fully filled" + }, + { + "code": 6013, + "name": "orderCancelled", + "msg": "Order is cancelled" + }, + { + "code": 6014, + "name": "orderExpired", + "msg": "Order is expired" + }, + { + "code": 6015, + "name": "accurateTransferNotPossible", + "msg": "Cannot accurately transfer the requested amount accounted for transfer fees" + } + ], + "types": [ + { + "name": "cancelEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "order", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "u64" + } + ] + } + }, + { + "name": "claimType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "instant" + }, + { + "name": "vested" + } + ] + } + }, + { + "name": "createEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "baseMint", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "quoteMint", + "type": "pubkey" + }, + { + "name": "pricingModel", + "type": { + "defined": { + "name": "pricingModel" + } + } + }, + { + "name": "startPrice", + "type": "u64" + }, + { + "name": "timestamp", + "type": "u64" + } + ] + } + }, + { + "name": "CreateOrderFixedIx", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "docs": [ + "Amount of `base_mint` tokens" + ], + "type": "u64" + }, + { + "name": "startPrice", + "docs": [ + "Start Price in `quote_mint` decimal value for base tokens" + ], + "type": "u64" + }, + { + "name": "partialAllowed", + "docs": [ + "Whether to allow partial fulfillment" + ], + "type": "bool" + }, + { + "name": "expiryTs", + "docs": [ + "Timestamp when order expires" + ], + "type": "u64" + }, + { + "name": "claimType", + "docs": [ + "How the token should be claimed after order fulfillment" + ], + "type": { + "defined": { + "name": "claimType" + } + } + }, + { + "name": "vestingStartTs", + "docs": [ + "Vesting Claim Type options", + "When vesting contract start time should be" + ], + "type": "u64" + }, + { + "name": "vestingPeriod", + "docs": [ + "Vesting release period in seconds" + ], + "type": "u64" + }, + { + "name": "vestingAmountPerPeriod", + "docs": [ + "When vesting contract end time should be" + ], + "type": "u64" + }, + { + "name": "vestingCliffAmount", + "docs": [ + "Amount of tokens to be unlocked right at the start of vesting" + ], + "type": "u64" + } + ] + } + }, + { + "name": "executionRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nonce", + "docs": [ + "Nonce used to differentiate records of the same executor" + ], + "type": "u32" + }, + { + "name": "order", + "docs": [ + "Order id" + ], + "type": "pubkey" + }, + { + "name": "executor", + "docs": [ + "Counterparty that filled the order" + ], + "type": "pubkey" + }, + { + "name": "contract", + "docs": [ + "Vesting contract created after the order was filled" + ], + "type": "pubkey" + }, + { + "name": "filledAmount", + "docs": [ + "Filled amount" + ], + "type": "u64" + }, + { + "name": "paidAmount", + "docs": [ + "How much quote tokens were paid" + ], + "type": "u64" + }, + { + "name": "createdTs", + "docs": [ + "Timestamp when the record was created" + ], + "type": "u64" + }, + { + "name": "buffer", + "docs": [ + "Buffer for additional fields" + ], + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "fillEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "order", + "type": "pubkey" + }, + { + "name": "executor", + "type": "pubkey" + }, + { + "name": "contract", + "type": { + "option": "pubkey" + } + }, + { + "name": "fillAmount", + "type": "u64" + }, + { + "name": "paidAmount", + "type": "u64" + }, + { + "name": "timestamp", + "type": "u64" + } + ] + } + }, + { + "name": "order", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "docs": [ + "Bump Seed used to sign transactions" + ], + "type": "u8" + }, + { + "name": "nonce", + "docs": [ + "Nonce to differentiate orders for the same mint" + ], + "type": "u32" + }, + { + "name": "baseMint", + "docs": [ + "Mint of the Token stored in Escrow" + ], + "type": "pubkey" + }, + { + "name": "quoteMint", + "docs": [ + "Mint of the Token to exchange base mint tokens for" + ], + "type": "pubkey" + }, + { + "name": "creator", + "docs": [ + "Current authority" + ], + "type": "pubkey" + }, + { + "name": "executor", + "docs": [ + "Optional constraint of the counterparty to fulfill the order" + ], + "type": "pubkey" + }, + { + "name": "partner", + "docs": [ + "Optional partner/referral account" + ], + "type": "pubkey" + }, + { + "name": "vault", + "docs": [ + "Token Account that stores deposited tokens" + ], + "type": "pubkey" + }, + { + "name": "config", + "type": { + "defined": { + "name": "orderConfig" + } + } + }, + { + "name": "vestingConfig", + "type": { + "defined": { + "name": "vestingConfig" + } + } + }, + { + "name": "state", + "type": { + "defined": { + "name": "orderState" + } + } + }, + { + "name": "buffer1", + "docs": [ + "Buffer for additional fields" + ], + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "buffer2", + "docs": [ + "Buffer for additional fields" + ], + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "orderConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "docs": [ + "Amount of base tokens" + ], + "type": "u64" + }, + { + "name": "startPrice", + "docs": [ + "Price in `quote_mint` decimal value for every whole `base_mint` token" + ], + "type": "u64" + }, + { + "name": "partialAllowed", + "docs": [ + "Whether to allow partial fulfillment" + ], + "type": "bool" + }, + { + "name": "expiryTs", + "docs": [ + "Timestamp when order should expire if not fulfilled by that point" + ], + "type": "u64" + }, + { + "name": "pricingModel", + "docs": [ + "Model used for pricing:", + "- fixed stands for instant sell at `start_price`" + ], + "type": { + "defined": { + "name": "pricingModel" + } + } + }, + { + "name": "claimType", + "docs": [ + "How base token should be claimed on order fulfillment" + ], + "type": { + "defined": { + "name": "claimType" + } + } + }, + { + "name": "buffer", + "docs": [ + "Buffer for additional fields" + ], + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "orderState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "filledAmount", + "docs": [ + "Filled amount of the order" + ], + "type": "u64" + }, + { + "name": "createdTs", + "docs": [ + "Timestamp when order was created" + ], + "type": "u64" + }, + { + "name": "lastFilledTs", + "docs": [ + "Timestamp when order was filled last time" + ], + "type": "u64" + }, + { + "name": "cancelledTs", + "docs": [ + "Timestamp when order was cancelled" + ], + "type": "u64" + }, + { + "name": "buffer", + "docs": [ + "Buffer for additional fields" + ], + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "pricingModel", + "type": { + "kind": "enum", + "variants": [ + { + "name": "fixed" + } + ] + } + }, + { + "name": "vestingConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "startTs", + "docs": [ + "When vesting contract start time should be, use 0 to start immediately after fulfillment" + ], + "type": "u64" + }, + { + "name": "period", + "docs": [ + "Vesting unlock period in seconds" + ], + "type": "u64" + }, + { + "name": "amountPerPeriod", + "docs": [ + "Vesting unlock amount" + ], + "type": "u64" + }, + { + "name": "cliffAmount", + "docs": [ + "Amount to be unlocked right at the start of vesting" + ], + "type": "u64" + }, + { + "name": "buffer", + "docs": [ + "Buffer for additional fields" + ], + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + } + ] +}; + +export const IDL = { + "version": "0.1.0", + "name": "streamflow_escrow", + "instructions": [ + { + "name": "cancelOrder", + "docs": [ + "Cancel an order.", + "", + "- Order creator creator can cancel anytime;", + "- pre-configured `executor` can also cancel the order, meaning that they reject it;", + "- anyone can cancel an order after it has been expired, tokens will always be returned to the order creator;" + ], + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Account that will cover tx fees, should be equal to creator if not is not expired" + ] + }, + { + "name": "order", + "isMut": true, + "isSigner": false, + "docs": [ + "Order to cancel" + ] + }, + { + "name": "vault", + "isMut": true, + "isSigner": false, + "docs": [ + "Vault that stores base tokens" + ] + }, + { + "name": "creator", + "isMut": true, + "isSigner": false + }, + { + "name": "to", + "isMut": true, + "isSigner": false, + "docs": [ + "Token account that will receive back base mint tokens" + ] + }, + { + "name": "baseMint", + "isMut": true, + "isSigner": false, + "docs": [ + "Quote mint of the order" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "SPL token program interface." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "System program." + ] + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "createOrderFixed", + "docs": [ + "Create an order with fixed `start_price`" + ], + "accounts": [ + { + "name": "creator", + "isMut": true, + "isSigner": true, + "docs": [ + "Order creator" + ] + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "Token account from which base token will be transferred" + ] + }, + { + "name": "order", + "isMut": true, + "isSigner": false, + "docs": [ + "Order to cancel" + ] + }, + { + "name": "vault", + "isMut": true, + "isSigner": false, + "docs": [ + "Vault that stores base tokens" + ] + }, + { + "name": "executor", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "partner", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "baseMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Quote mint of the order" + ] + }, + { + "name": "quoteMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Quote mint of the order" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "SPL token program interface." + ] + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [Associated Token] program." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "System program." + ] + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ix", + "type": { + "defined": "CreateOrderFixedIx" + } + } + ] + }, + { + "name": "fillOrderInstant", + "docs": [ + "Fill an order, claim `amount` of base tokens instantly." + ], + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "executor", + "isMut": true, + "isSigner": true, + "docs": [ + "Order executor" + ] + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "Token account from which quote token will be transferred" + ] + }, + { + "name": "toBase", + "isMut": true, + "isSigner": false, + "docs": [ + "Executor TA that will receive base tokens" + ] + }, + { + "name": "order", + "isMut": true, + "isSigner": false, + "docs": [ + "Order to cancel" + ] + }, + { + "name": "vault", + "isMut": true, + "isSigner": false, + "docs": [ + "Vault that stores base tokens" + ] + }, + { + "name": "creator", + "isMut": true, + "isSigner": false + }, + { + "name": "toQuote", + "isMut": true, + "isSigner": false, + "docs": [ + "Creator TA that will receive quote mint tokens" + ] + }, + { + "name": "baseMint", + "isMut": true, + "isSigner": false, + "docs": [ + "Base mint of the order" + ] + }, + { + "name": "quoteMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Quote mint of the order" + ] + }, + { + "name": "baseTokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Token program used for base mint tokens" + ] + }, + { + "name": "quotaTokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Token program used for quote mint tokens" + ] + } + ] + }, + { + "name": "executionRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "System program." + ] + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "ceilAllowed", + "type": "bool" + } + ] + }, + { + "name": "fillOrderVested", + "docs": [ + "Fill an order, claim `amount` of base tokens in a vested fashion." + ], + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "executor", + "isMut": true, + "isSigner": true, + "docs": [ + "Order executor" + ] + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "Token account from which quote token will be transferred" + ] + }, + { + "name": "toBase", + "isMut": true, + "isSigner": false, + "docs": [ + "Executor TA that will receive base tokens" + ] + }, + { + "name": "order", + "isMut": true, + "isSigner": false, + "docs": [ + "Order to cancel" + ] + }, + { + "name": "vault", + "isMut": true, + "isSigner": false, + "docs": [ + "Vault that stores base tokens" + ] + }, + { + "name": "creator", + "isMut": true, + "isSigner": false + }, + { + "name": "toQuote", + "isMut": true, + "isSigner": false, + "docs": [ + "Creator TA that will receive quote mint tokens" + ] + }, + { + "name": "baseMint", + "isMut": true, + "isSigner": false, + "docs": [ + "Base mint of the order" + ] + }, + { + "name": "quoteMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Quote mint of the order" + ] + }, + { + "name": "baseTokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Token program used for base mint tokens" + ] + }, + { + "name": "quotaTokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Token program used for quote mint tokens" + ] + } + ] + }, + { + "name": "executionRecord", + "isMut": true, + "isSigner": false + }, + { + "name": "streamMetadata", + "isMut": true, + "isSigner": true + }, + { + "name": "escrowTokens", + "isMut": true, + "isSigner": false + }, + { + "name": "withdrawor", + "isMut": true, + "isSigner": false + }, + { + "name": "streamflowProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "feeOracle", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Associated token program." + ] + }, + { + "name": "rent", + "isMut": false, + "isSigner": false, + "docs": [ + "Sysvar rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "System program." + ] + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "ceilAllowed", + "type": "bool" + } + ] + } + ], + "accounts": [ + { + "name": "executionRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nonce", + "type": "u32", + "docs": [ + "Nonce used to differentiate records of the same executor" + ] + }, + { + "name": "order", + "type": "publicKey", + "docs": [ + "Order id" + ] + }, + { + "name": "executor", + "type": "publicKey", + "docs": [ + "Counterparty that filled the order" + ] + }, + { + "name": "contract", + "type": "publicKey", + "docs": [ + "Vesting contract created after the order was filled" + ] + }, + { + "name": "filledAmount", + "type": "u64", + "docs": [ + "Filled amount" + ] + }, + { + "name": "paidAmount", + "type": "u64", + "docs": [ + "How much quote tokens were paid" + ] + }, + { + "name": "createdTs", + "type": "u64", + "docs": [ + "Timestamp when the record was created" + ] + }, + { + "name": "buffer", + "type": { + "array": [ + "u8", + 64 + ] + }, + "docs": [ + "Buffer for additional fields" + ] + } + ] + } + }, + { + "name": "order", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8", + "docs": [ + "Bump Seed used to sign transactions" + ] + }, + { + "name": "nonce", + "type": "u32", + "docs": [ + "Nonce to differentiate orders for the same mint" + ] + }, + { + "name": "baseMint", + "type": "publicKey", + "docs": [ + "Mint of the Token stored in Escrow" + ] + }, + { + "name": "quoteMint", + "type": "publicKey", + "docs": [ + "Mint of the Token to exchange base mint tokens for" + ] + }, + { + "name": "creator", + "type": "publicKey", + "docs": [ + "Current authority" + ] + }, + { + "name": "executor", + "type": "publicKey", + "docs": [ + "Optional constraint of the counterparty to fulfill the order" + ] + }, + { + "name": "partner", + "type": "publicKey", + "docs": [ + "Optional partner/referral account" + ] + }, + { + "name": "vault", + "type": "publicKey", + "docs": [ + "Token Account that stores deposited tokens" + ] + }, + { + "name": "config", + "type": { + "defined": "OrderConfig" + } + }, + { + "name": "vestingConfig", + "type": { + "defined": "VestingConfig" + } + }, + { + "name": "state", + "type": { + "defined": "OrderState" + } + }, + { + "name": "buffer1", + "type": { + "array": [ + "u8", + 64 + ] + }, + "docs": [ + "Buffer for additional fields" + ] + }, + { + "name": "buffer2", + "type": { + "array": [ + "u8", + 64 + ] + }, + "docs": [ + "Buffer for additional fields" + ] + } + ] + } + } + ], + "types": [ + { + "name": "CreateOrderFixedIx", + "type": { + "kind": "struct", + "fields": [ + { + "name": "nonce", + "type": "u32" + }, + { + "name": "amount", + "type": "u64", + "docs": [ + "Amount of `base_mint` tokens" + ] + }, + { + "name": "startPrice", + "type": "u64", + "docs": [ + "Start Price in `quote_mint` decimal value for base tokens" + ] + }, + { + "name": "partialAllowed", + "type": "bool", + "docs": [ + "Whether to allow partial fulfillment" + ] + }, + { + "name": "expiryTs", + "type": "u64", + "docs": [ + "Timestamp when order expires" + ] + }, + { + "name": "claimType", + "type": { + "defined": "ClaimType" + }, + "docs": [ + "How the token should be claimed after order fulfillment" + ] + }, + { + "name": "vestingStartTs", + "type": "u64", + "docs": [ + "Vesting Claim Type options", + "When vesting contract start time should be" + ] + }, + { + "name": "vestingPeriod", + "type": "u64", + "docs": [ + "Vesting release period in seconds" + ] + }, + { + "name": "vestingAmountPerPeriod", + "type": "u64", + "docs": [ + "When vesting contract end time should be" + ] + }, + { + "name": "vestingCliffAmount", + "type": "u64", + "docs": [ + "Amount of tokens to be unlocked right at the start of vesting" + ] + } + ] + } + }, + { + "name": "OrderConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64", + "docs": [ + "Amount of base tokens" + ] + }, + { + "name": "startPrice", + "type": "u64", + "docs": [ + "Price in `quote_mint` decimal value for every whole `base_mint` token" + ] + }, + { + "name": "partialAllowed", + "type": "bool", + "docs": [ + "Whether to allow partial fulfillment" + ] + }, + { + "name": "expiryTs", + "type": "u64", + "docs": [ + "Timestamp when order should expire if not fulfilled by that point" + ] + }, + { + "name": "pricingModel", + "type": { + "defined": "PricingModel" + }, + "docs": [ + "Model used for pricing:", + "- fixed stands for instant sell at `start_price`" + ] + }, + { + "name": "claimType", + "type": { + "defined": "ClaimType" + }, + "docs": [ + "How base token should be claimed on order fulfillment" + ] + }, + { + "name": "buffer", + "type": { + "array": [ + "u8", + 64 + ] + }, + "docs": [ + "Buffer for additional fields" + ] + } + ] + } + }, + { + "name": "OrderState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "filledAmount", + "type": "u64", + "docs": [ + "Filled amount of the order" + ] + }, + { + "name": "createdTs", + "type": "u64", + "docs": [ + "Timestamp when order was created" + ] + }, + { + "name": "lastFilledTs", + "type": "u64", + "docs": [ + "Timestamp when order was filled last time" + ] + }, + { + "name": "cancelledTs", + "type": "u64", + "docs": [ + "Timestamp when order was cancelled" + ] + }, + { + "name": "buffer", + "type": { + "array": [ + "u8", + 64 + ] + }, + "docs": [ + "Buffer for additional fields" + ] + } + ] + } + }, + { + "name": "VestingConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "startTs", + "type": "u64", + "docs": [ + "When vesting contract start time should be, use 0 to start immediately after fulfillment" + ] + }, + { + "name": "period", + "type": "u64", + "docs": [ + "Vesting unlock period in seconds" + ] + }, + { + "name": "amountPerPeriod", + "type": "u64", + "docs": [ + "Vesting unlock amount" + ] + }, + { + "name": "cliffAmount", + "type": "u64", + "docs": [ + "Amount to be unlocked right at the start of vesting" + ] + }, + { + "name": "buffer", + "type": { + "array": [ + "u8", + 64 + ] + }, + "docs": [ + "Buffer for additional fields" + ] + } + ] + } + }, + { + "name": "ClaimType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Instant" + }, + { + "name": "Vested" + } + ] + } + }, + { + "name": "PricingModel", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Fixed" + } + ] + } + } + ], + "events": [ + { + "name": "CancelEvent", + "fields": [ + { + "name": "order", + "type": "publicKey", + "index": false + }, + { + "name": "timestamp", + "type": "u64", + "index": false + } + ] + }, + { + "name": "CreateEvent", + "fields": [ + { + "name": "baseMint", + "type": "publicKey", + "index": false + }, + { + "name": "creator", + "type": "publicKey", + "index": false + }, + { + "name": "quoteMint", + "type": "publicKey", + "index": false + }, + { + "name": "pricingModel", + "type": { + "defined": "PricingModel" + }, + "index": false + }, + { + "name": "startPrice", + "type": "u64", + "index": false + }, + { + "name": "timestamp", + "type": "u64", + "index": false + } + ] + }, + { + "name": "FillEvent", + "fields": [ + { + "name": "order", + "type": "publicKey", + "index": false + }, + { + "name": "executor", + "type": "publicKey", + "index": false + }, + { + "name": "contract", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "fillAmount", + "type": "u64", + "index": false + }, + { + "name": "paidAmount", + "type": "u64", + "index": false + }, + { + "name": "timestamp", + "type": "u64", + "index": false + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "Unauthorized", + "msg": "Authority does not have permission for this action" + }, + { + "code": 6001, + "name": "ArithmeticError", + "msg": "Arithmetic error" + }, + { + "code": 6002, + "name": "InvalidStreamMetadata", + "msg": "Invalid Stream Metadata" + }, + { + "code": 6003, + "name": "InvalidAmount", + "msg": "Amount should be greater than 0" + }, + { + "code": 6004, + "name": "InvalidPrice", + "msg": "Price should be greater than 0" + }, + { + "code": 6005, + "name": "InvalidExpiryTs", + "msg": "Expiry ts should be in the future" + }, + { + "code": 6006, + "name": "InvalidVestingOptions", + "msg": "Provided vesting claim options are invalid" + }, + { + "code": 6007, + "name": "InvalidMints", + "msg": "Base mint can not equal quote mint" + }, + { + "code": 6008, + "name": "UnsupportedTokenExtensions", + "msg": "Mint has unsupported Token Extensions" + }, + { + "code": 6009, + "name": "InvalidFillPrice", + "msg": "Price to fill the order does not match the order price" + }, + { + "code": 6010, + "name": "InvalidFillAmount", + "msg": "Provided amount to fill is not valid" + }, + { + "code": 6011, + "name": "InvalidClaimType", + "msg": "Provided claim type is invalid" + }, + { + "code": 6012, + "name": "OrderFilled", + "msg": "Order is already fully filled" + }, + { + "code": 6013, + "name": "OrderCancelled", + "msg": "Order is cancelled" + }, + { + "code": 6014, + "name": "OrderExpired", + "msg": "Order is expired" + }, + { + "code": 6015, + "name": "AccurateTransferNotPossible", + "msg": "Cannot accurately transfer the requested amount accounted for transfer fees" + } + ], + "metadata": { + "address": "ESCRoWj8QUJ5cTXCBWbGpW6AzaaEAtRbZuwKp8c4YYGs" + } +}; diff --git a/tests/fixtures/streamflow_vesting.so b/tests/fixtures/streamflow_vesting.so new file mode 100644 index 000000000..ac783e800 Binary files /dev/null and b/tests/fixtures/streamflow_vesting.so differ diff --git a/tests/fixtures/withdrawor b/tests/fixtures/withdrawor new file mode 100644 index 000000000..e69de29bb diff --git a/tests/main.test.ts b/tests/main.test.ts index ea9f8ebdd..3b994b9d1 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -1,6 +1,6 @@ import conditionalVault from "./conditionalVault/main.test.js"; import amm from "./amm/main.test.js"; -import autocrat from "./autocrat/autocrat.js"; +import autocrat from "./autocrat/main.test.js"; import launchpad from "./launchpad/main.test.js"; import { Clock, startAnchor } from "solana-bankrun"; @@ -21,7 +21,7 @@ import { // getVersion, // VersionKey // } from "@metadaoproject/futarchy"; -import { PublicKey, Keypair } from "@solana/web3.js"; +import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js"; import { createAssociatedTokenAccount, createMint, @@ -44,6 +44,12 @@ const MPL_TOKEN_METADATA_PROGRAM_ID = toWeb3JsPublicKey( const RAYDIUM_CP_SWAP_PROGRAM_ID = new PublicKey( "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C" ); +export const STREAMFLOW_ESCROW_PROGRAM_ID = new PublicKey( + "ESCRoWj8QUJ5cTXCBWbGpW6AzaaEAtRbZuwKp8c4YYGs" +); +export const STREAMFLOW_VESTING_PROGRAM_ID = new PublicKey( + "strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m" +); import mintAndSwap from "./integration/mintAndSwap.test.js"; import scalarMarkets from "./integration/scalarMarkets.test.js"; @@ -67,6 +73,14 @@ before(async function () { name: "raydium_cp_swap", programId: RAYDIUM_CP_SWAP_PROGRAM_ID, }, + { + name: "streamflow_escrow", + programId: STREAMFLOW_ESCROW_PROGRAM_ID, + }, + { + name: "streamflow_vesting", + programId: STREAMFLOW_VESTING_PROGRAM_ID, + } ], [ { @@ -98,6 +112,24 @@ before(async function () { lamports: 377_950_832_219, }, }, + { + address: new PublicKey("B743wFVk2pCYhV91cn287e1xY7f1vt4gdY48hhNiuQmT"), + info: { + data: fs.readFileSync("./tests/fixtures/fee-oracle"), + executable: false, + owner: new PublicKey("pardpVtPjC8nLj1Dwncew62mUzfChdCX1EaoZe8oCAa"), + lamports: 1_000_000_000, + } + }, + { + address: new PublicKey("wdrwhnCv4pzW8beKsbPa4S2UDZrXenjg16KJdKSpb5u"), + info: { + data: Buffer.alloc(0), + executable: false, + owner: SystemProgram.programId, + lamports: 32_000_000_000, + } + }, ] ); this.banksClient = this.context.banksClient;