import { BN, Program, web3 } from "@project-serum/anchor"; import { SendTxRequest } from "@project-serum/anchor/dist/provider"; import { Provider, Wallet } from "@project-serum/anchor"; import { u64 } from "@solana/spl-token"; import { Keypair, PublicKey, SystemProgram, Transaction, Signer, TransactionInstruction, } from "@solana/web3.js"; import { Action, ActionVertexInfo, MallocIdl, NextNode, PartialNextActionCtx, ProcessActionNext, _ActionVertexInfoGeneric, _NextNodeGeneric, } from "../index"; import { removeSignerDuplicates } from "../utils/solana"; import { breakUpInstrDataArrays, getRandomNotCryptoSecureNonce, } from "./helpers"; let ADD_ACTION_INST_IDL; /** * Create the instructions for adding actions. If the actions array is too large, * split this into multiple transactions * * if skipInTokenCheck is true in the opts, then the in token account will not be added on chain * and the check will be skipped */ export const addActionsToConstruction = ( program: Program, construction: PublicKey, actions: Action[], authority: Signer | Wallet ): SendTxRequest[] => { ADD_ACTION_INST_IDL = MallocIdl.instructions.find( (i) => i.name === "addActions" ); const actionInfos = actions.map(actionToActionsWithVertexInfo); const createTxFn = (items: _ActionVertexInfoGeneric[]) => createAddActionsTx(program, construction, items, authority); return breakUpInstrDataArrays(actionInfos, createTxFn); }; const findIdx = (action: Action, pk: PublicKey, isOutput: boolean): number => { const idx = action.actionAccounts.findIndex((account) => account.address.equals(pk) ); if (idx === -1) throw `Expected to find ${ isOutput ? "output" : "input" } token ${pk.toBase58()} in ${action.actionName}'s accounts`; return idx; }; /** * Find all the action accounts if the action type requires it, otherwise return null */ const findAllPkIdxs = ( action: Action, pks: PublicKey[], isOutput: boolean ): number[] | null => { if ( action.actionVertexInfo.actionType.normalCpi || (!isOutput && action.opts?.checks?.skipInTokenChecks) || (isOutput && action.opts?.checks?.skipOutTokenChecks) ) return null; else return pks.map((pk) => findIdx(action, pk, isOutput)); }; const actionToActionsWithVertexInfo = (action: Action) => { const inputTokenIdxs = findAllPkIdxs( action, action.inputTokenAccounts, false ); const outputTokenIdxs = findAllPkIdxs(action, action.tokenAccountOuts, true); return { inDegree: action.actionVertexInfo.inDegree, inputTokenIdxs, outputTokenIdxs, numberInputMints: action.inputTokenAccounts.length, actionType: action.actionVertexInfo.actionType, minOuts: action.actionVertexInfo.minOuts?.map((minOut) => new BN(minOut)) || null, nextNodes: action.actionVertexInfo.nextNodes.map((nextNode) => nextNode.map((n) => { return { actionIdx: n.actionIdx, fraction: new u64(n.fraction), nextInputMintIdx: n.nextInputMintIdx, } as _NextNodeGeneric; }) ), } as _ActionVertexInfoGeneric; }; const createAddActionsTx = ( program: Program, construction: PublicKey, actions: _ActionVertexInfoGeneric[], authority: Signer | Wallet ) => { const signers = (authority as Signer).secretKey ? [authority as Signer] : []; return { tx: program.transaction.addActions( actions, getRandomNotCryptoSecureNonce(0, 0xffffffff), { accounts: { authority: authority.publicKey, construction: construction, rent: web3.SYSVAR_RENT_PUBKEY, }, signers: signers, } ), signers, }; }; /** * out of all the transactions associated with the action call, * get the index of the tx which does the actual action call */ export const getMainActionTxIndex = (action: Action): number => { const offset = (action.opts?.preActionTxs ?? []).length; return offset; }; export const getProcessActionsTx = ( program: Program, actionIdxs: number[], construction: PublicKey, actionAccountsCtx: PartialNextActionCtx[], authority: Signer | Wallet, opts: { data?: Buffer; tokenAuthority?: Signer | Wallet; instructions?: TransactionInstruction[]; additionalSigners?: Signer[]; preActionTxs?: SendTxRequest[]; }[] ): SendTxRequest[] => { const processRets = actionAccountsCtx.map((ctx, i) => getProcessSingleActionInstr( program, actionIdxs[i], construction, actionAccountsCtx[i], authority, opts[i] ) ); const instsPrior = opts .map((opt) => opt.instructions) .filter((x) => x) .flat(); const processInsts = [...instsPrior, ...processRets.map((p) => p.inst)]; const processSigners = processRets.map((p) => p.signers); const allSigners = processSigners.flat(); const tx = new Transaction(); tx.add(...processInsts); const sendTxProc: SendTxRequest = { tx, signers: removeSignerDuplicates(allSigners), }; return [ ...opts .map((opt) => opt.preActionTxs) .filter((tx) => !!tx) .flat(), sendTxProc, ]; }; /** * @param tokenAuthority - defaults to the authority if not provided */ const getProcessSingleActionInstr = ( program: Program, actionIdx: number, construction: PublicKey, actionAccountsCtx: PartialNextActionCtx, authority: Signer | Wallet, opts: { data?: Buffer; tokenAuthority?: Signer | Wallet; additionalSigners?: Signer[]; preActionTxs?: SendTxRequest[]; } ): { inst: TransactionInstruction; signers: Signer[] } => { const auth = (authority as Signer)?.secretKey ? [authority as Signer] : []; const tokauth = (opts.tokenAuthority as Signer)?.secretKey ? [opts.tokenAuthority as Signer] : []; const signersIsolated = opts.tokenAuthority ? [...tokauth, ...auth] : [...auth]; const signers = [...signersIsolated, ...(opts.additionalSigners || [])]; const inst = program.instruction.processAction( actionIdx, opts.data || Buffer.from([]), { accounts: { construction: construction, rent: web3.SYSVAR_RENT_PUBKEY, authority: authority.publicKey, actionProgram: actionAccountsCtx.actionProgram, actionAccounts: actionAccountsCtx.actionAccounts, } as ProcessActionNext, signers, } ); return { inst, signers }; };