diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 25b57f6bfc..74167f69cb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -329,6 +329,8 @@ jobs: path: tests/custom-coder - cmd: cd tests/validator-clone && yarn --frozen-lockfile && anchor test --skip-lint path: tests/validator-clone + - cmd: cd tests/cpi-returns && anchor test --skip-lint + path: tests/cpi-returns steps: - uses: actions/checkout@v2 - uses: ./.github/actions/setup/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ae6c18f379..6e2c2cf4ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The minor version will be incremented upon a breaking change and the patch versi ### Features +* lang: Add return values to CPI client. ([#1598](https://github.com/project-serum/anchor/pull/1598)). * avm: New `avm update` command to update the Anchor CLI to the latest version ([#1670](https://github.com/project-serum/anchor/pull/1670)). ### Fixes diff --git a/lang/syn/src/codegen/program/cpi.rs b/lang/syn/src/codegen/program/cpi.rs index 768292c651..aa2aec5631 100644 --- a/lang/syn/src/codegen/program/cpi.rs +++ b/lang/syn/src/codegen/program/cpi.rs @@ -2,7 +2,7 @@ use crate::codegen::program::common::{generate_ix_variant, sighash, SIGHASH_GLOB use crate::Program; use crate::StateIx; use heck::SnakeCase; -use quote::quote; +use quote::{quote, ToTokens}; pub fn generate(program: &Program) -> proc_macro2::TokenStream { // Generate cpi methods for the state struct. @@ -70,11 +70,20 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, name); let sighash_tts: proc_macro2::TokenStream = format!("{:?}", sighash_arr).parse().unwrap(); + let ret_type = &ix.returns.ty.to_token_stream(); + let (method_ret, maybe_return) = match ret_type.to_string().as_str() { + "()" => (quote! {anchor_lang::Result<()> }, quote! { Ok(()) }), + _ => ( + quote! { anchor_lang::Result> }, + quote! { Ok(crate::cpi::Return::<#ret_type> { phantom: crate::cpi::PhantomData }) } + ) + }; + quote! { pub fn #method_name<'a, 'b, 'c, 'info>( ctx: anchor_lang::context::CpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>, #(#args),* - ) -> anchor_lang::Result<()> { + ) -> #method_ret { let ix = { let ix = instruction::#ix_variant; let mut ix_data = AnchorSerialize::try_to_vec(&ix) @@ -93,7 +102,11 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &ix, &acc_infos, ctx.signer_seeds, - ).map_err(Into::into) + ).map_or_else( + |e| Err(Into::into(e)), + // Maybe handle Solana return data. + |_| { #maybe_return } + ) } } }; @@ -108,6 +121,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { #[cfg(feature = "cpi")] pub mod cpi { use super::*; + use std::marker::PhantomData; pub mod state { use super::*; @@ -115,6 +129,17 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { #(#state_cpi_methods)* } + pub struct Return { + phantom: std::marker::PhantomData + } + + impl Return { + pub fn get(&self) -> T { + let (_key, data) = anchor_lang::solana_program::program::get_return_data().unwrap(); + T::try_from_slice(&data).unwrap() + } + } + #(#global_cpi_methods)* #accounts diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index 462e3727c1..543e14af76 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -1,7 +1,7 @@ use crate::codegen::program::common::*; use crate::{Program, State}; use heck::CamelCase; -use quote::quote; +use quote::{quote, ToTokens}; // Generate non-inlined wrappers for each instruction handler, since Solana's // BPF max stack size can't handle reasonable sized dispatch trees without doing @@ -694,6 +694,13 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { let anchor = &ix.anchor_ident; let variant_arm = generate_ix_variant(ix.raw_method.sig.ident.to_string(), &ix.args); let ix_name_log = format!("Instruction: {}", ix_name); + let ret_type = &ix.returns.ty.to_token_stream(); + let maybe_set_return_data = match ret_type.to_string().as_str() { + "()" => quote! {}, + _ => quote! { + anchor_lang::solana_program::program::set_return_data(&result.try_to_vec().unwrap()); + }, + }; quote! { #[inline(never)] pub fn #ix_method_name( @@ -722,7 +729,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { )?; // Invoke user defined handler. - #program_name::#ix_method_name( + let result = #program_name::#ix_method_name( anchor_lang::context::Context::new( program_id, &mut accounts, @@ -732,6 +739,9 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { #(#ix_arg_names),* )?; + // Maybe set Solana return data. + #maybe_set_return_data + // Exit routine. accounts.exit(program_id) } diff --git a/lang/syn/src/idl/file.rs b/lang/syn/src/idl/file.rs index 793a380016..9b5c2b3c49 100644 --- a/lang/syn/src/idl/file.rs +++ b/lang/syn/src/idl/file.rs @@ -66,6 +66,7 @@ pub fn parse( name, accounts, args, + returns: None, } }) .collect::>() @@ -105,6 +106,7 @@ pub fn parse( name, accounts, args, + returns: None, } }; @@ -164,10 +166,16 @@ pub fn parse( // todo: don't unwrap let accounts_strct = accs.get(&ix.anchor_ident.to_string()).unwrap(); let accounts = idl_accounts(&ctx, accounts_strct, &accs, seeds_feature); + let ret_type_str = ix.returns.ty.to_token_stream().to_string(); + let returns = match ret_type_str.as_str() { + "()" => None, + _ => Some(ret_type_str.parse().unwrap()), + }; IdlInstruction { name: ix.ident.to_string().to_mixed_case(), accounts, args, + returns, } }) .collect::>(); diff --git a/lang/syn/src/idl/mod.rs b/lang/syn/src/idl/mod.rs index f3fb1ac685..a92d9baa1b 100644 --- a/lang/syn/src/idl/mod.rs +++ b/lang/syn/src/idl/mod.rs @@ -45,6 +45,7 @@ pub struct IdlInstruction { pub name: String, pub accounts: Vec, pub args: Vec, + pub returns: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index cf00ca23ed..63ad84d8f1 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -14,7 +14,7 @@ use syn::spanned::Spanned; use syn::token::Comma; use syn::{ Expr, Generics, Ident, ImplItemMethod, ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, LitInt, - LitStr, PatType, Token, TypePath, + LitStr, PatType, Token, Type, TypePath, }; pub mod codegen; @@ -85,6 +85,7 @@ pub struct Ix { pub raw_method: ItemFn, pub ident: Ident, pub args: Vec, + pub returns: IxReturn, // The ident for the struct deriving Accounts. pub anchor_ident: Ident, } @@ -95,6 +96,11 @@ pub struct IxArg { pub raw_arg: PatType, } +#[derive(Debug)] +pub struct IxReturn { + pub ty: Type, +} + #[derive(Debug)] pub struct FallbackFn { raw_method: ItemFn, diff --git a/lang/syn/src/parser/program/instructions.rs b/lang/syn/src/parser/program/instructions.rs index a0cfd22e77..4cf5aa80d4 100644 --- a/lang/syn/src/parser/program/instructions.rs +++ b/lang/syn/src/parser/program/instructions.rs @@ -1,5 +1,5 @@ use crate::parser::program::ctx_accounts_ident; -use crate::{FallbackFn, Ix, IxArg}; +use crate::{FallbackFn, Ix, IxArg, IxReturn}; use syn::parse::{Error as ParseError, Result as ParseResult}; use syn::spanned::Spanned; @@ -23,12 +23,14 @@ pub fn parse(program_mod: &syn::ItemMod) -> ParseResult<(Vec, Option>>()?; @@ -91,3 +93,34 @@ pub fn parse_args(method: &syn::ItemFn) -> ParseResult<(IxArg, Vec)> { Ok((ctx, args)) } + +pub fn parse_return(method: &syn::ItemFn) -> ParseResult { + match method.sig.output { + syn::ReturnType::Type(_, ref ty) => { + let ty = match ty.as_ref() { + syn::Type::Path(ty) => ty, + _ => return Err(ParseError::new(ty.span(), "expected a return type")), + }; + // Assume unit return by default + let default_generic_arg = syn::GenericArgument::Type(syn::parse_str("()").unwrap()); + let generic_args = match &ty.path.segments.last().unwrap().arguments { + syn::PathArguments::AngleBracketed(params) => params.args.iter().last().unwrap(), + _ => &default_generic_arg, + }; + let ty = match generic_args { + syn::GenericArgument::Type(ty) => ty.clone(), + _ => { + return Err(ParseError::new( + ty.span(), + "expected generic return type to be a type", + )) + } + }; + Ok(IxReturn { ty }) + } + _ => Err(ParseError::new( + method.sig.output.span(), + "expected a return type", + )), + } +} diff --git a/tests/cpi-returns/.gitignore b/tests/cpi-returns/.gitignore new file mode 100644 index 0000000000..51448d4dab --- /dev/null +++ b/tests/cpi-returns/.gitignore @@ -0,0 +1,6 @@ + +.anchor +.DS_Store +target +**/*.rs.bk +node_modules diff --git a/tests/cpi-returns/Anchor.toml b/tests/cpi-returns/Anchor.toml new file mode 100644 index 0000000000..99bb4acbfc --- /dev/null +++ b/tests/cpi-returns/Anchor.toml @@ -0,0 +1,16 @@ +[features] +seeds = false + +[programs.localnet] +callee = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" +caller = "HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L" + +[registry] +url = "https://anchor.projectserum.com" + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/tests/cpi-returns/Cargo.toml b/tests/cpi-returns/Cargo.toml new file mode 100644 index 0000000000..a60de986d3 --- /dev/null +++ b/tests/cpi-returns/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/tests/cpi-returns/migrations/deploy.ts b/tests/cpi-returns/migrations/deploy.ts new file mode 100644 index 0000000000..5e3df0dc30 --- /dev/null +++ b/tests/cpi-returns/migrations/deploy.ts @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@project-serum/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; diff --git a/tests/cpi-returns/package.json b/tests/cpi-returns/package.json new file mode 100644 index 0000000000..e0376e7317 --- /dev/null +++ b/tests/cpi-returns/package.json @@ -0,0 +1,19 @@ +{ + "name": "cpi-returns", + "version": "0.23.0", + "license": "(MIT OR Apache-2.0)", + "homepage": "https://github.com/project-serum/anchor#readme", + "bugs": { + "url": "https://github.com/project-serum/anchor/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/project-serum/anchor.git" + }, + "engines": { + "node": ">=11" + }, + "scripts": { + "test": "anchor run test-with-build" + } +} diff --git a/tests/cpi-returns/programs/callee/Cargo.toml b/tests/cpi-returns/programs/callee/Cargo.toml new file mode 100644 index 0000000000..c964a4aa4d --- /dev/null +++ b/tests/cpi-returns/programs/callee/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "callee" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "callee" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] } diff --git a/tests/cpi-returns/programs/callee/Xargo.toml b/tests/cpi-returns/programs/callee/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/tests/cpi-returns/programs/callee/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tests/cpi-returns/programs/callee/src/lib.rs b/tests/cpi-returns/programs/callee/src/lib.rs new file mode 100644 index 0000000000..5cce63ec95 --- /dev/null +++ b/tests/cpi-returns/programs/callee/src/lib.rs @@ -0,0 +1,50 @@ +use anchor_lang::prelude::*; + +declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); + +#[program] +pub mod callee { + use super::*; + + #[derive(AnchorSerialize, AnchorDeserialize)] + pub struct StructReturn { + pub value: u64, + } + + pub fn initialize(_ctx: Context) -> Result<()> { + Ok(()) + } + + pub fn return_u64(_ctx: Context) -> Result { + Ok(10) + } + + pub fn return_struct(_ctx: Context) -> Result { + let s = StructReturn { value: 11 }; + Ok(s) + } + + pub fn return_vec(_ctx: Context) -> Result> { + Ok(vec![12, 13, 14, 100]) + } +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account(init, payer = user, space = 8 + 8)] + pub account: Account<'info, CpiReturnAccount>, + #[account(mut)] + pub user: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct CpiReturn<'info> { + #[account(mut)] + pub account: Account<'info, CpiReturnAccount>, +} + +#[account] +pub struct CpiReturnAccount { + pub value: u64, +} diff --git a/tests/cpi-returns/programs/caller/Cargo.toml b/tests/cpi-returns/programs/caller/Cargo.toml new file mode 100644 index 0000000000..2aaa551085 --- /dev/null +++ b/tests/cpi-returns/programs/caller/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "caller" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "caller" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { path = "../../../../lang", features = ["init-if-needed"] } +callee = { path = "../callee", features = ["cpi"] } diff --git a/tests/cpi-returns/programs/caller/Xargo.toml b/tests/cpi-returns/programs/caller/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/tests/cpi-returns/programs/caller/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tests/cpi-returns/programs/caller/src/lib.rs b/tests/cpi-returns/programs/caller/src/lib.rs new file mode 100644 index 0000000000..2070e35906 --- /dev/null +++ b/tests/cpi-returns/programs/caller/src/lib.rs @@ -0,0 +1,54 @@ +use anchor_lang::prelude::*; +use callee::cpi::accounts::CpiReturn; +use callee::program::Callee; +use callee::{self, CpiReturnAccount}; + +declare_id!("HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L"); + +#[program] +pub mod caller { + use super::*; + + pub fn cpi_call_return_u64(ctx: Context) -> Result<()> { + let cpi_program = ctx.accounts.cpi_return_program.to_account_info(); + let cpi_accounts = CpiReturn { + account: ctx.accounts.cpi_return.to_account_info(), + }; + let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); + let result = callee::cpi::return_u64(cpi_ctx)?; + let solana_return = result.get(); + anchor_lang::solana_program::log::sol_log_data(&[&solana_return.try_to_vec().unwrap()]); + Ok(()) + } + + pub fn cpi_call_return_struct(ctx: Context) -> Result<()> { + let cpi_program = ctx.accounts.cpi_return_program.to_account_info(); + let cpi_accounts = CpiReturn { + account: ctx.accounts.cpi_return.to_account_info(), + }; + let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); + let result = callee::cpi::return_struct(cpi_ctx)?; + let solana_return = result.get(); + anchor_lang::solana_program::log::sol_log_data(&[&solana_return.try_to_vec().unwrap()]); + Ok(()) + } + + pub fn cpi_call_return_vec(ctx: Context) -> Result<()> { + let cpi_program = ctx.accounts.cpi_return_program.to_account_info(); + let cpi_accounts = CpiReturn { + account: ctx.accounts.cpi_return.to_account_info(), + }; + let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); + let result = callee::cpi::return_vec(cpi_ctx)?; + let solana_return = result.get(); + anchor_lang::solana_program::log::sol_log_data(&[&solana_return.try_to_vec().unwrap()]); + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CpiReturnContext<'info> { + #[account(mut)] + pub cpi_return: Account<'info, CpiReturnAccount>, + pub cpi_return_program: Program<'info, Callee>, +} diff --git a/tests/cpi-returns/tests/cpi-return.ts b/tests/cpi-returns/tests/cpi-return.ts new file mode 100644 index 0000000000..c9848f7141 --- /dev/null +++ b/tests/cpi-returns/tests/cpi-return.ts @@ -0,0 +1,161 @@ +import assert from "assert"; +import * as anchor from "@project-serum/anchor"; +import * as borsh from "borsh"; +import { Program } from "@project-serum/anchor"; +import { Callee } from "../target/types/callee"; +import { Caller } from "../target/types/caller"; + +const { SystemProgram } = anchor.web3; + +describe("CPI return", () => { + const provider = anchor.Provider.env(); + anchor.setProvider(provider); + + const callerProgram = anchor.workspace.Caller as Program; + const calleeProgram = anchor.workspace.Callee as Program; + + const getReturnLog = (confirmedTransaction) => { + const prefix = "Program return: "; + let log = confirmedTransaction.meta.logMessages.find((log) => + log.startsWith(prefix) + ); + log = log.slice(prefix.length); + const [key, data] = log.split(" ", 2); + const buffer = Buffer.from(data, "base64"); + return [key, data, buffer]; + }; + + const cpiReturn = anchor.web3.Keypair.generate(); + + const confirmOptions = { commitment: "confirmed" }; + + it("can initialize", async () => { + await calleeProgram.methods + .initialize() + .accounts({ + account: cpiReturn.publicKey, + user: provider.wallet.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([cpiReturn]) + .rpc(); + }); + + it("can return u64 from a cpi", async () => { + const tx = await callerProgram.methods + .cpiCallReturnU64() + .accounts({ + cpiReturn: cpiReturn.publicKey, + cpiReturnProgram: calleeProgram.programId, + }) + .rpc(confirmOptions); + let t = await provider.connection.getTransaction(tx, { + commitment: "confirmed", + }); + + const [key, data, buffer] = getReturnLog(t); + assert.equal(key, calleeProgram.programId); + + // Check for matching log on receive side + let receiveLog = t.meta.logMessages.find( + (log) => log == `Program data: ${data}` + ); + assert(receiveLog !== undefined); + + const reader = new borsh.BinaryReader(buffer); + assert.equal(reader.readU64().toNumber(), 10); + }); + + it("can make a non-cpi call to a function that returns a u64", async () => { + const tx = await calleeProgram.methods + .returnU64() + .accounts({ + account: cpiReturn.publicKey, + }) + .rpc(confirmOptions); + let t = await provider.connection.getTransaction(tx, { + commitment: "confirmed", + }); + const [key, , buffer] = getReturnLog(t); + assert.equal(key, calleeProgram.programId); + const reader = new borsh.BinaryReader(buffer); + assert.equal(reader.readU64().toNumber(), 10); + }); + + it("can return a struct from a cpi", async () => { + const tx = await callerProgram.methods + .cpiCallReturnStruct() + .accounts({ + cpiReturn: cpiReturn.publicKey, + cpiReturnProgram: calleeProgram.programId, + }) + .rpc(confirmOptions); + let t = await provider.connection.getTransaction(tx, { + commitment: "confirmed", + }); + + const [key, data, buffer] = getReturnLog(t); + assert.equal(key, calleeProgram.programId); + + // Check for matching log on receive side + let receiveLog = t.meta.logMessages.find( + (log) => log == `Program data: ${data}` + ); + assert(receiveLog !== undefined); + + // Deserialize the struct and validate + class Assignable { + constructor(properties) { + Object.keys(properties).map((key) => { + this[key] = properties[key]; + }); + } + } + class Data extends Assignable {} + const schema = new Map([ + [Data, { kind: "struct", fields: [["value", "u64"]] }], + ]); + const deserialized = borsh.deserialize(schema, Data, buffer); + assert(deserialized.value.toNumber() === 11); + }); + + it("can return a vec from a cpi", async () => { + const tx = await callerProgram.methods + .cpiCallReturnVec() + .accounts({ + cpiReturn: cpiReturn.publicKey, + cpiReturnProgram: calleeProgram.programId, + }) + .rpc(confirmOptions); + let t = await provider.connection.getTransaction(tx, { + commitment: "confirmed", + }); + + const [key, data, buffer] = getReturnLog(t); + assert.equal(key, calleeProgram.programId); + + // Check for matching log on receive side + let receiveLog = t.meta.logMessages.find( + (log) => log == `Program data: ${data}` + ); + assert(receiveLog !== undefined); + + const reader = new borsh.BinaryReader(buffer); + const array = reader.readArray(() => reader.readU8()); + assert.deepStrictEqual(array, [12, 13, 14, 100]); + }); + + it("sets a return value in idl", async () => { + const returnu64Instruction = calleeProgram._idl.instructions.find( + (f) => f.name == "returnU64" + ); + assert.equal(returnu64Instruction.returns, "u64"); + + const returnStructInstruction = calleeProgram._idl.instructions.find( + (f) => f.name == "returnStruct" + ); + assert.deepStrictEqual(returnStructInstruction.returns, { + defined: "StructReturn", + }); + }); +}); diff --git a/tests/cpi-returns/tsconfig.json b/tests/cpi-returns/tsconfig.json new file mode 100644 index 0000000000..cd5d2e3d06 --- /dev/null +++ b/tests/cpi-returns/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +} diff --git a/tests/package.json b/tests/package.json index 489cda4ae2..5787588712 100644 --- a/tests/package.json +++ b/tests/package.json @@ -31,7 +31,8 @@ "typescript", "validator-clone", "zero-copy", - "declare-id" + "declare-id", + "cpi-returns" ], "dependencies": { "@project-serum/anchor": "^0.23.0", diff --git a/ts/src/idl.ts b/ts/src/idl.ts index 3fe002bd83..efda32c172 100644 --- a/ts/src/idl.ts +++ b/ts/src/idl.ts @@ -38,6 +38,7 @@ export type IdlInstruction = { name: string; accounts: IdlAccountItem[]; args: IdlField[]; + returns?: IdlType; }; export type IdlState = {