From 03c86796a1bb0582b14859ffbf02d7c3e1b9e654 Mon Sep 17 00:00:00 2001 From: Elliot Kennedy Date: Fri, 29 Nov 2024 02:23:08 +0000 Subject: [PATCH] - Add support for optional accounts with `@solana/web3.js:2.0` `Option` types - Do not mark none optional accounts as mutable --- .../generated-client/instructions/create.ts | 2 + .../instructions/increment.ts | 2 + .../generated-client/instructions/play.ts | 2 + .../instructions/setupGame.ts | 2 + src/instructions.ts | 73 ++- .../exp/accounts/OptionalState.ts | 122 ++++ .../example-program-gen/exp/accounts/index.ts | 2 + .../exp/instructions/causeError.ts | 2 + .../exp/instructions/index.ts | 2 + .../exp/instructions/initialize.ts | 2 + .../exp/instructions/initializeWithValues.ts | 2 + .../exp/instructions/initializeWithValues2.ts | 2 + .../exp/instructions/optional.ts | 63 ++ tests/example-program-gen/idl.json | 69 +++ .../programs/example-program/src/lib.rs | 39 ++ tests/test.ts | 574 +++++++++--------- 16 files changed, 662 insertions(+), 298 deletions(-) create mode 100644 tests/example-program-gen/exp/accounts/OptionalState.ts create mode 100644 tests/example-program-gen/exp/instructions/optional.ts diff --git a/examples/basic-2/generated-client/instructions/create.ts b/examples/basic-2/generated-client/instructions/create.ts index ed54058..708549d 100644 --- a/examples/basic-2/generated-client/instructions/create.ts +++ b/examples/basic-2/generated-client/instructions/create.ts @@ -1,8 +1,10 @@ import { Address, + isSome, IAccountMeta, IAccountSignerMeta, IInstruction, + Option, TransactionSigner, } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars diff --git a/examples/basic-2/generated-client/instructions/increment.ts b/examples/basic-2/generated-client/instructions/increment.ts index 4bcfcd2..2f7c5bf 100644 --- a/examples/basic-2/generated-client/instructions/increment.ts +++ b/examples/basic-2/generated-client/instructions/increment.ts @@ -1,8 +1,10 @@ import { Address, + isSome, IAccountMeta, IAccountSignerMeta, IInstruction, + Option, TransactionSigner, } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars diff --git a/examples/tic-tac-toe/generated-client/instructions/play.ts b/examples/tic-tac-toe/generated-client/instructions/play.ts index 7b743ec..c77f6ff 100644 --- a/examples/tic-tac-toe/generated-client/instructions/play.ts +++ b/examples/tic-tac-toe/generated-client/instructions/play.ts @@ -1,8 +1,10 @@ import { Address, + isSome, IAccountMeta, IAccountSignerMeta, IInstruction, + Option, TransactionSigner, } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars diff --git a/examples/tic-tac-toe/generated-client/instructions/setupGame.ts b/examples/tic-tac-toe/generated-client/instructions/setupGame.ts index 970aff8..27cfb17 100644 --- a/examples/tic-tac-toe/generated-client/instructions/setupGame.ts +++ b/examples/tic-tac-toe/generated-client/instructions/setupGame.ts @@ -1,8 +1,10 @@ import { Address, + isSome, IAccountMeta, IAccountSignerMeta, IInstruction, + Option, TransactionSigner, } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars diff --git a/src/instructions.ts b/src/instructions.ts index 1dd014d..c8f16c4 100644 --- a/src/instructions.ts +++ b/src/instructions.ts @@ -1,5 +1,5 @@ import { Idl } from "@coral-xyz/anchor" -import { IdlAccountItem } from "@coral-xyz/anchor/dist/cjs/idl" +import { IdlAccount, IdlAccountItem } from "@coral-xyz/anchor/dist/cjs/idl" import { CodeBlockWriter, Project, VariableDeclarationKind } from "ts-morph" import { fieldToEncodable, @@ -82,7 +82,7 @@ function genInstructionFiles( // imports src.addStatements([ - `import { Address, IAccountMeta, IAccountSignerMeta, IInstruction, TransactionSigner } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars`, + `import { Address, isSome, IAccountMeta, IAccountSignerMeta, IInstruction, Option, TransactionSigner } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars`, `import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars`, `import * as borsh from "@coral-xyz/borsh" // eslint-disable-line @typescript-eslint/no-unused-vars`, `import { borshAddress } from "../utils" // eslint-disable-line @typescript-eslint/no-unused-vars`, @@ -114,11 +114,17 @@ function genInstructionFiles( writer: CodeBlockWriter ) { if (!("accounts" in accItem)) { + if (accItem.isOptional) { + writer.write("Option<") + } if (accItem.isSigner) { writer.write("TransactionSigner") } else { writer.write("Address") } + if (accItem.isOptional) { + writer.write(">") + } return } writer.block(() => { @@ -224,6 +230,32 @@ function genInstructionFiles( return AccountRole.READONLY } + function getAddressProps( + item: IdlAccount, + baseProps: string[] + ): string[] { + if (item.isOptional && item.isSigner) { + return [...baseProps, "value", "address"] + } else if (item.isOptional && !item.isSigner) { + return [...baseProps, "value"] + } else if (!item.isOptional && item.isSigner) { + return [...baseProps, "address"] + } else { + return baseProps + } + } + + function getSignerProps( + item: IdlAccount, + baseProps: string[] + ): string[] { + if (item.isOptional) { + return [...baseProps, "value"] + } else { + return [...baseProps] + } + } + function recurseAccounts( accs: IdlAccountItem[], nestedNames: string[] @@ -233,18 +265,33 @@ function genInstructionFiles( recurseAccounts(item.accounts, [...nestedNames, item.name]) return } - const props = [...nestedNames, item.name] - const addressProps = item.isSigner - ? [...props, "address"] - : props - writer.writeLine( - `{ address: accounts.${addressProps.join( - "." - )}, role: ${getAccountRole(item)}${ - item.isSigner ? `, signer: accounts.${props}` : "" - } },` - ) + const baseProps = [...nestedNames, item.name] + const addressProps = getAddressProps(item, baseProps) + const role = getAccountRole(item) + + const meta = `{ address: accounts.${addressProps.join( + "." + )}, role: ${role}${ + item.isSigner + ? `, signer: accounts.${getSignerProps( + item, + baseProps + ).join(".")}` + : "" + } }` + + if (item.isOptional) { + writer.writeLine( + `isSome(accounts.${baseProps.join( + "." + )}) ? ${meta} : { address: programAddress, role: ${ + AccountRole.READONLY + } },` + ) + } else { + writer.writeLine(`${meta},`) + } }) } diff --git a/tests/example-program-gen/exp/accounts/OptionalState.ts b/tests/example-program-gen/exp/accounts/OptionalState.ts new file mode 100644 index 0000000..0668078 --- /dev/null +++ b/tests/example-program-gen/exp/accounts/OptionalState.ts @@ -0,0 +1,122 @@ +import { + address, + Address, + fetchEncodedAccount, + fetchEncodedAccounts, + GetAccountInfoApi, + GetMultipleAccountsApi, + Rpc, +} from "@solana/web3.js" +import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars +import * as borsh from "@coral-xyz/borsh" // eslint-disable-line @typescript-eslint/no-unused-vars +import { borshAddress } from "../utils" // eslint-disable-line @typescript-eslint/no-unused-vars +import * as types from "../types" // eslint-disable-line @typescript-eslint/no-unused-vars +import { PROGRAM_ID } from "../programId" + +export interface OptionalStateFields { + readonlySignerOption: boolean + mutableSignerOption: boolean + readonlyOption: boolean + mutableOption: boolean +} + +export interface OptionalStateJSON { + readonlySignerOption: boolean + mutableSignerOption: boolean + readonlyOption: boolean + mutableOption: boolean +} + +export class OptionalState { + readonly readonlySignerOption: boolean + readonly mutableSignerOption: boolean + readonly readonlyOption: boolean + readonly mutableOption: boolean + + static readonly discriminator = Buffer.from([ + 182, 31, 131, 174, 98, 39, 6, 20, + ]) + + static readonly layout = borsh.struct([ + borsh.bool("readonlySignerOption"), + borsh.bool("mutableSignerOption"), + borsh.bool("readonlyOption"), + borsh.bool("mutableOption"), + ]) + + constructor(fields: OptionalStateFields) { + this.readonlySignerOption = fields.readonlySignerOption + this.mutableSignerOption = fields.mutableSignerOption + this.readonlyOption = fields.readonlyOption + this.mutableOption = fields.mutableOption + } + + static async fetch( + rpc: Rpc, + address: Address, + programId: Address = PROGRAM_ID + ): Promise { + const info = await fetchEncodedAccount(rpc, address) + + if (!info.exists) { + return null + } + if (info.programAddress !== programId) { + throw new Error("account doesn't belong to this program") + } + + return this.decode(Buffer.from(info.data)) + } + + static async fetchMultiple( + rpc: Rpc, + addresses: Address[], + programId: Address = PROGRAM_ID + ): Promise> { + const infos = await fetchEncodedAccounts(rpc, addresses) + + return infos.map((info) => { + if (!info.exists) { + return null + } + if (info.programAddress !== programId) { + throw new Error("account doesn't belong to this program") + } + + return this.decode(Buffer.from(info.data)) + }) + } + + static decode(data: Buffer): OptionalState { + if (!data.slice(0, 8).equals(OptionalState.discriminator)) { + throw new Error("invalid account discriminator") + } + + const dec = OptionalState.layout.decode(data.slice(8)) + + return new OptionalState({ + readonlySignerOption: dec.readonlySignerOption, + mutableSignerOption: dec.mutableSignerOption, + readonlyOption: dec.readonlyOption, + mutableOption: dec.mutableOption, + }) + } + + toJSON(): OptionalStateJSON { + return { + readonlySignerOption: this.readonlySignerOption, + mutableSignerOption: this.mutableSignerOption, + readonlyOption: this.readonlyOption, + mutableOption: this.mutableOption, + } + } + + static fromJSON(obj: OptionalStateJSON): OptionalState { + return new OptionalState({ + readonlySignerOption: obj.readonlySignerOption, + mutableSignerOption: obj.mutableSignerOption, + readonlyOption: obj.readonlyOption, + mutableOption: obj.mutableOption, + }) + } +} diff --git a/tests/example-program-gen/exp/accounts/index.ts b/tests/example-program-gen/exp/accounts/index.ts index 89e4838..9e048ec 100644 --- a/tests/example-program-gen/exp/accounts/index.ts +++ b/tests/example-program-gen/exp/accounts/index.ts @@ -1,3 +1,5 @@ +export { OptionalState } from "./OptionalState" +export type { OptionalStateFields, OptionalStateJSON } from "./OptionalState" export { State } from "./State" export type { StateFields, StateJSON } from "./State" export { State2 } from "./State2" diff --git a/tests/example-program-gen/exp/instructions/causeError.ts b/tests/example-program-gen/exp/instructions/causeError.ts index e72d9e7..8a78f5f 100644 --- a/tests/example-program-gen/exp/instructions/causeError.ts +++ b/tests/example-program-gen/exp/instructions/causeError.ts @@ -1,8 +1,10 @@ import { Address, + isSome, IAccountMeta, IAccountSignerMeta, IInstruction, + Option, TransactionSigner, } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars diff --git a/tests/example-program-gen/exp/instructions/index.ts b/tests/example-program-gen/exp/instructions/index.ts index 0dc58a3..fcb252c 100644 --- a/tests/example-program-gen/exp/instructions/index.ts +++ b/tests/example-program-gen/exp/instructions/index.ts @@ -11,3 +11,5 @@ export type { InitializeWithValues2Accounts, } from "./initializeWithValues2" export { causeError } from "./causeError" +export { optional } from "./optional" +export type { OptionalAccounts } from "./optional" diff --git a/tests/example-program-gen/exp/instructions/initialize.ts b/tests/example-program-gen/exp/instructions/initialize.ts index ed3998a..98b7648 100644 --- a/tests/example-program-gen/exp/instructions/initialize.ts +++ b/tests/example-program-gen/exp/instructions/initialize.ts @@ -1,8 +1,10 @@ import { Address, + isSome, IAccountMeta, IAccountSignerMeta, IInstruction, + Option, TransactionSigner, } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars diff --git a/tests/example-program-gen/exp/instructions/initializeWithValues.ts b/tests/example-program-gen/exp/instructions/initializeWithValues.ts index ea92767..333ad13 100644 --- a/tests/example-program-gen/exp/instructions/initializeWithValues.ts +++ b/tests/example-program-gen/exp/instructions/initializeWithValues.ts @@ -1,8 +1,10 @@ import { Address, + isSome, IAccountMeta, IAccountSignerMeta, IInstruction, + Option, TransactionSigner, } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars diff --git a/tests/example-program-gen/exp/instructions/initializeWithValues2.ts b/tests/example-program-gen/exp/instructions/initializeWithValues2.ts index 78c834d..23aca86 100644 --- a/tests/example-program-gen/exp/instructions/initializeWithValues2.ts +++ b/tests/example-program-gen/exp/instructions/initializeWithValues2.ts @@ -1,8 +1,10 @@ import { Address, + isSome, IAccountMeta, IAccountSignerMeta, IInstruction, + Option, TransactionSigner, } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars diff --git a/tests/example-program-gen/exp/instructions/optional.ts b/tests/example-program-gen/exp/instructions/optional.ts new file mode 100644 index 0000000..43b397b --- /dev/null +++ b/tests/example-program-gen/exp/instructions/optional.ts @@ -0,0 +1,63 @@ +import { + Address, + isSome, + IAccountMeta, + IAccountSignerMeta, + IInstruction, + Option, + TransactionSigner, +} from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars +import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars +import * as borsh from "@coral-xyz/borsh" // eslint-disable-line @typescript-eslint/no-unused-vars +import { borshAddress } from "../utils" // eslint-disable-line @typescript-eslint/no-unused-vars +import * as types from "../types" // eslint-disable-line @typescript-eslint/no-unused-vars +import { PROGRAM_ID } from "../programId" + +export interface OptionalAccounts { + optionalState: TransactionSigner + readonlySignerOption: Option + mutableSignerOption: Option + readonlyOption: Option
+ mutableOption: Option
+ payer: TransactionSigner + systemProgram: Address +} + +export function optional( + accounts: OptionalAccounts, + programAddress: Address = PROGRAM_ID +) { + const keys: Array = [ + { + address: accounts.optionalState.address, + role: 3, + signer: accounts.optionalState, + }, + isSome(accounts.readonlySignerOption) + ? { + address: accounts.readonlySignerOption.value.address, + role: 2, + signer: accounts.readonlySignerOption.value, + } + : { address: programAddress, role: 0 }, + isSome(accounts.mutableSignerOption) + ? { + address: accounts.mutableSignerOption.value.address, + role: 3, + signer: accounts.mutableSignerOption.value, + } + : { address: programAddress, role: 0 }, + isSome(accounts.readonlyOption) + ? { address: accounts.readonlyOption.value, role: 0 } + : { address: programAddress, role: 0 }, + isSome(accounts.mutableOption) + ? { address: accounts.mutableOption.value, role: 1 } + : { address: programAddress, role: 0 }, + { address: accounts.payer.address, role: 3, signer: accounts.payer }, + { address: accounts.systemProgram, role: 0 }, + ] + const identifier = Buffer.from([199, 182, 147, 252, 17, 246, 54, 225]) + const data = identifier + const ix: IInstruction = { accounts: keys, programAddress, data } + return ix +} diff --git a/tests/example-program-gen/idl.json b/tests/example-program-gen/idl.json index 372c625..35a7614 100644 --- a/tests/example-program-gen/idl.json +++ b/tests/example-program-gen/idl.json @@ -262,9 +262,78 @@ "name": "causeError", "accounts": [], "args": [] + }, + { + "name": "optional", + "accounts": [ + { + "name": "optionalState", + "isMut": true, + "isSigner": true + }, + { + "name": "readonlySignerOption", + "isMut": false, + "isSigner": true, + "isOptional": true + }, + { + "name": "mutableSignerOption", + "isMut": true, + "isSigner": true, + "isOptional": true + }, + { + "name": "readonlyOption", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "mutableOption", + "isMut": true, + "isSigner": false, + "isOptional": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ + { + "name": "OptionalState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "readonlySignerOption", + "type": "bool" + }, + { + "name": "mutableSignerOption", + "type": "bool" + }, + { + "name": "readonlyOption", + "type": "bool" + }, + { + "name": "mutableOption", + "type": "bool" + } + ] + } + }, { "name": "State", "docs": [ diff --git a/tests/example-program/programs/example-program/src/lib.rs b/tests/example-program/programs/example-program/src/lib.rs index 55d840a..4763a89 100644 --- a/tests/example-program/programs/example-program/src/lib.rs +++ b/tests/example-program/programs/example-program/src/lib.rs @@ -89,6 +89,16 @@ pub mod example_program { pub fn cause_error(_ctx: Context) -> Result<()> { return Err(error!(ErrorCode::SomeError)); } + + pub fn optional(ctx: Context) -> Result<()> { + ctx.accounts.optional_state.set_inner(OptionalState { + readonly_signer_option: ctx.accounts.readonly_signer_option.is_some(), + mutable_signer_option: ctx.accounts.mutable_signer_option.is_some(), + readonly_option: ctx.accounts.readonly_option.is_some(), + mutable_option: ctx.accounts.mutable_option.is_some(), + }); + Ok(()) + } } /// Enum type @@ -237,6 +247,15 @@ impl Default for State2 { } } +#[account] +#[derive(Default)] +pub struct OptionalState { + readonly_signer_option: bool, + mutable_signer_option: bool, + readonly_option: bool, + mutable_option: bool, +} + #[derive(Accounts)] pub struct NestedAccounts<'info> { /// Sysvar clock @@ -278,6 +297,26 @@ pub struct Initialize2<'info> { #[derive(Accounts)] pub struct CauseError {} +#[derive(Accounts)] +pub struct Optional<'info> { + #[account( + init, + space = 8 + (1 + 1 + 1 + 1), + payer = payer, + )] + optional_state: Account<'info, OptionalState>, + readonly_signer_option: Option>, + #[account(mut)] + mutable_signer_option: Option>, + readonly_option: Option>, + #[account(mut)] + mutable_option: Option>, + + #[account(mut)] + payer: Signer<'info>, + system_program: Program<'info, System>, +} + #[error_code] pub enum ErrorCode { #[msg("Example error.")] diff --git a/tests/test.ts b/tests/test.ts index fe09d6c..fb15188 100644 --- a/tests/test.ts +++ b/tests/test.ts @@ -6,18 +6,25 @@ import { createTransactionMessage, appendTransactionMessageInstructions, setTransactionMessageFeePayerSigner, - appendTransactionMessageInstruction, address, signTransactionMessageWithSigners, setTransactionMessageLifetimeUsingBlockhash, sendAndConfirmTransactionFactory, createSolanaRpcSubscriptions, + some, + none, + IInstruction, + TransactionSigner, } from "@solana/web3.js" import { expect, it } from "vitest" import BN from "bn.js" import * as dircompare from "dir-compare" import * as fs from "fs" -import { State, State2 } from "./example-program-gen/act/accounts" +import { + OptionalState, + State, + State2, +} from "./example-program-gen/act/accounts" import { fromTxError } from "./example-program-gen/act/errors" import { InvalidProgramId } from "./example-program-gen/act/errors/anchor" import { @@ -25,6 +32,7 @@ import { initialize, initializeWithValues, initializeWithValues2, + optional, } from "./example-program-gen/act/instructions" import { BarStruct, FooStruct } from "./example-program-gen/act/types" import { @@ -71,45 +79,17 @@ it("init and account fetch", async () => { const payer = await createKeyPairSignerFromBytes(Uint8Array.from(faucet)) const state = await generateKeyPairSigner() - const blockhash = await rpc - .getLatestBlockhash({ commitment: "finalized" }) - .send() - - const tx = await pipe( - createTransactionMessage({ version: 0 }), - (tx) => - appendTransactionMessageInstruction( - initialize({ - state: state, - payer: payer, - nested: { - clock: SYSVAR_CLOCK_ADDRESS, - rent: SYSVAR_RENT_ADDRESS, - }, - systemProgram: SYSTEM_PROGRAM_ADDRESS, - }), - tx - ), - (tx) => setTransactionMessageFeePayerSigner(payer, tx), - (tx) => - setTransactionMessageLifetimeUsingBlockhash( - { - blockhash: blockhash.value.blockhash, - lastValidBlockHeight: blockhash.value.lastValidBlockHeight, - }, - tx - ), - (tx) => signTransactionMessageWithSigners(tx) - ) - - const sendAndConfirmFn = sendAndConfirmTransactionFactory({ - rpc, - rpcSubscriptions, - }) - await sendAndConfirmFn(tx, { - commitment: "confirmed", - }) - + await sendTx(payer, [ + initialize({ + state: state, + payer: payer, + nested: { + clock: SYSVAR_CLOCK_ADDRESS, + rent: SYSVAR_RENT_ADDRESS, + }, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + }), + ]) const res = await State.fetch(rpc, state.address) if (res === null) { throw new Error("account not found") @@ -270,55 +250,26 @@ it("fetch multiple", async () => { const another_state = await generateKeyPairSigner() const non_state = await generateKeyPairSigner() - const blockhash = await rpc - .getLatestBlockhash({ commitment: "finalized" }) - .send() - - const tx = await pipe( - createTransactionMessage({ version: 0 }), - (tx) => - appendTransactionMessageInstructions( - [ - initialize({ - state: state, - payer: payer, - nested: { - clock: SYSVAR_CLOCK_ADDRESS, - rent: SYSVAR_RENT_ADDRESS, - }, - systemProgram: SYSTEM_PROGRAM_ADDRESS, - }), - initialize({ - state: another_state, - payer: payer, - nested: { - clock: SYSVAR_CLOCK_ADDRESS, - rent: SYSVAR_RENT_ADDRESS, - }, - systemProgram: SYSTEM_PROGRAM_ADDRESS, - }), - ], - tx - ), - (tx) => setTransactionMessageFeePayerSigner(payer, tx), - (tx) => - setTransactionMessageLifetimeUsingBlockhash( - { - blockhash: blockhash.value.blockhash, - lastValidBlockHeight: blockhash.value.lastValidBlockHeight, - }, - tx - ), - (tx) => signTransactionMessageWithSigners(tx) - ) - - const sendAndConfirmFn = sendAndConfirmTransactionFactory({ - rpc, - rpcSubscriptions, - }) - await sendAndConfirmFn(tx, { - commitment: "confirmed", - }) + await sendTx(payer, [ + initialize({ + state: state, + payer: payer, + nested: { + clock: SYSVAR_CLOCK_ADDRESS, + rent: SYSVAR_RENT_ADDRESS, + }, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + }), + initialize({ + state: another_state, + payer: payer, + nested: { + clock: SYSVAR_CLOCK_ADDRESS, + rent: SYSVAR_RENT_ADDRESS, + }, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + }), + ]) const res = await State.fetchMultiple(rpc, [ state.address, @@ -334,152 +285,119 @@ it("instruction with args", async () => { const state = await generateKeyPairSigner() const state2 = await generateKeyPairSigner() - const blockhash = await rpc - .getLatestBlockhash({ commitment: "finalized" }) - .send() - - const tx = await pipe( - createTransactionMessage({ version: 0 }), - (tx) => - appendTransactionMessageInstructions( - [ - initializeWithValues( - { - boolField: true, - u8Field: 253, - i8Field: -120, - u16Field: 61234, - i16Field: -31253, - u32Field: 1234567899, - i32Field: -123456789, - f32Field: 123458.5, - u64Field: new BN("9223372036854775810"), - i64Field: new BN("-4611686018427387912"), - f64Field: 1234567892.445, - u128Field: new BN("170141183460469231731687303715884105740"), - i128Field: new BN("-85070591730234615865843651857942052877"), - bytesField: Uint8Array.from([5, 10, 255]), - stringField: "string value", - pubkeyField: address( - "GDddEKTjLBqhskzSMYph5o54VYLQfPCR3PoFqKHLJK6s" - ), - vecField: [new BN(1), new BN("123456789123456789")], - vecStructField: [ - new FooStruct({ - field1: 1, - field2: 2, - nested: new BarStruct({ - someField: true, - otherField: 55, - }), - vecNested: [ - new BarStruct({ - someField: false, - otherField: 11, - }), - ], - optionNested: null, - enumField: new Unnamed([ - true, - 22, - new BarStruct({ - someField: true, - otherField: 33, - }), - ]), - pubkeyField: address( - "EPZP2wrcRtMxrAPJCXVEQaYD9eH7fH7h12YqKDcd4aS7" - ), - }), - ], - optionField: true, - optionStructField: null, - structField: new FooStruct({ - field1: 1, - field2: 2, - nested: new BarStruct({ - someField: true, - otherField: 55, - }), - vecNested: [ - new BarStruct({ - someField: false, - otherField: 11, - }), - ], - optionNested: null, - enumField: new NoFields(), - pubkeyField: address( - "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So" - ), + await sendTx(payer, [ + initializeWithValues( + { + boolField: true, + u8Field: 253, + i8Field: -120, + u16Field: 61234, + i16Field: -31253, + u32Field: 1234567899, + i32Field: -123456789, + f32Field: 123458.5, + u64Field: new BN("9223372036854775810"), + i64Field: new BN("-4611686018427387912"), + f64Field: 1234567892.445, + u128Field: new BN("170141183460469231731687303715884105740"), + i128Field: new BN("-85070591730234615865843651857942052877"), + bytesField: Uint8Array.from([5, 10, 255]), + stringField: "string value", + pubkeyField: address("GDddEKTjLBqhskzSMYph5o54VYLQfPCR3PoFqKHLJK6s"), + vecField: [new BN(1), new BN("123456789123456789")], + vecStructField: [ + new FooStruct({ + field1: 1, + field2: 2, + nested: new BarStruct({ + someField: true, + otherField: 55, + }), + vecNested: [ + new BarStruct({ + someField: false, + otherField: 11, }), - arrayField: [true, true, false], - enumField1: new Unnamed([ - true, - 15, - new BarStruct({ - someField: false, - otherField: 200, - }), - ]), - enumField2: new Named({ - boolField: true, - u8Field: 128, - nested: new BarStruct({ - someField: false, - otherField: 1, - }), + ], + optionNested: null, + enumField: new Unnamed([ + true, + 22, + new BarStruct({ + someField: true, + otherField: 33, }), - enumField3: new Struct([ - new BarStruct({ - someField: true, - otherField: 15, - }), - ]), - enumField4: new NoFields(), - }, - { - state: state, - payer: payer, - nested: { - clock: SYSVAR_CLOCK_ADDRESS, - rent: SYSVAR_RENT_ADDRESS, - }, - systemProgram: SYSTEM_PROGRAM_ADDRESS, - } - ), - initializeWithValues2( - { - vecOfOption: [null, new BN(20)], - }, - { - state: state2, - payer: payer, - systemProgram: SYSTEM_PROGRAM_ADDRESS, - } - ), + ]), + pubkeyField: address( + "EPZP2wrcRtMxrAPJCXVEQaYD9eH7fH7h12YqKDcd4aS7" + ), + }), ], - tx - ), - (tx) => setTransactionMessageFeePayerSigner(payer, tx), - (tx) => - setTransactionMessageLifetimeUsingBlockhash( - { - blockhash: blockhash.value.blockhash, - lastValidBlockHeight: blockhash.value.lastValidBlockHeight, + optionField: true, + optionStructField: null, + structField: new FooStruct({ + field1: 1, + field2: 2, + nested: new BarStruct({ + someField: true, + otherField: 55, + }), + vecNested: [ + new BarStruct({ + someField: false, + otherField: 11, + }), + ], + optionNested: null, + enumField: new NoFields(), + pubkeyField: address("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So"), + }), + arrayField: [true, true, false], + enumField1: new Unnamed([ + true, + 15, + new BarStruct({ + someField: false, + otherField: 200, + }), + ]), + enumField2: new Named({ + boolField: true, + u8Field: 128, + nested: new BarStruct({ + someField: false, + otherField: 1, + }), + }), + enumField3: new Struct([ + new BarStruct({ + someField: true, + otherField: 15, + }), + ]), + enumField4: new NoFields(), + }, + { + state: state, + payer: payer, + nested: { + clock: SYSVAR_CLOCK_ADDRESS, + rent: SYSVAR_RENT_ADDRESS, }, - tx - ), - (tx) => signTransactionMessageWithSigners(tx) - ) - - const sendAndConfirmFn = sendAndConfirmTransactionFactory({ - rpc, - rpcSubscriptions, - }) - await sendAndConfirmFn(tx, { - commitment: "confirmed", - }) + systemProgram: SYSTEM_PROGRAM_ADDRESS, + } + ), + initializeWithValues2( + { + vecOfOption: [null, new BN(20)], + }, + { + state: state2, + payer: payer, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + } + ), + ]) const res = await State.fetch(rpc, state.address) if (res === null) { @@ -638,37 +556,111 @@ it("instruction with args", async () => { expect(res2.vecOfOption[1] !== null && res2.vecOfOption[1].eqn(20)).toBe(true) }) -it("tx error", async () => { +it("optional with some readonly signer", async () => { const payer = await createKeyPairSignerFromBytes(Uint8Array.from(faucet)) + const optionalState = await generateKeyPairSigner() + const signer = await generateKeyPairSigner() + + await sendTx(payer, [ + optional({ + optionalState, + readonlySignerOption: some(signer), + mutableSignerOption: none(), + readonlyOption: none(), + mutableOption: none(), + payer, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + }), + ]) - const blockhash = await rpc - .getLatestBlockhash({ commitment: "finalized" }) - .send() + const state = await OptionalState.fetch(rpc, optionalState.address) + expect(state).not.toBeNull() + expect(state?.readonlySignerOption).true + expect(state?.mutableSignerOption).false + expect(state?.readonlyOption).false + expect(state?.mutableOption).false +}) - const tx = await pipe( - createTransactionMessage({ version: 0 }), - (tx) => appendTransactionMessageInstruction(causeError(), tx), - (tx) => setTransactionMessageFeePayerSigner(payer, tx), - (tx) => - setTransactionMessageLifetimeUsingBlockhash( - { - blockhash: blockhash.value.blockhash, - lastValidBlockHeight: blockhash.value.lastValidBlockHeight, - }, - tx - ), - (tx) => signTransactionMessageWithSigners(tx) - ) +it("optional with some mutable signer", async () => { + const payer = await createKeyPairSignerFromBytes(Uint8Array.from(faucet)) + const optionalState = await generateKeyPairSigner() + const signer = await generateKeyPairSigner() + + await sendTx(payer, [ + optional({ + optionalState, + readonlySignerOption: none(), + mutableSignerOption: some(signer), + readonlyOption: none(), + mutableOption: none(), + payer, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + }), + ]) - const sendAndConfirmFn = sendAndConfirmTransactionFactory({ - rpc, - rpcSubscriptions, - }) + const state = await OptionalState.fetch(rpc, optionalState.address) + expect(state).not.toBeNull() + expect(state?.readonlySignerOption).false + expect(state?.mutableSignerOption).true + expect(state?.readonlyOption).false + expect(state?.mutableOption).false +}) + +it("optional with some readonly account", async () => { + const payer = await createKeyPairSignerFromBytes(Uint8Array.from(faucet)) + const optionalState = await generateKeyPairSigner() + const signer = await generateKeyPairSigner() + + await sendTx(payer, [ + optional({ + optionalState, + readonlySignerOption: none(), + mutableSignerOption: none(), + readonlyOption: some(signer.address), + mutableOption: none(), + payer, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + }), + ]) + + const state = await OptionalState.fetch(rpc, optionalState.address) + expect(state).not.toBeNull() + expect(state?.readonlySignerOption).false + expect(state?.mutableSignerOption).false + expect(state?.readonlyOption).true + expect(state?.mutableOption).false +}) + +it("optional with some mutable account", async () => { + const payer = await createKeyPairSignerFromBytes(Uint8Array.from(faucet)) + const optionalState = await generateKeyPairSigner() + const signer = await generateKeyPairSigner() + + await sendTx(payer, [ + optional({ + optionalState, + readonlySignerOption: none(), + mutableSignerOption: none(), + readonlyOption: none(), + mutableOption: some(signer.address), + payer, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + }), + ]) + + const state = await OptionalState.fetch(rpc, optionalState.address) + expect(state).not.toBeNull() + expect(state?.readonlySignerOption).false + expect(state?.mutableSignerOption).false + expect(state?.readonlyOption).false + expect(state?.mutableOption).true +}) + +it("tx error", async () => { + const payer = await createKeyPairSignerFromBytes(Uint8Array.from(faucet)) try { - await sendAndConfirmFn(tx, { - commitment: "processed", - }) + await sendTx(payer, [causeError()]) } catch (e) { const parsed = fromTxError(e) @@ -696,35 +688,8 @@ it("tx error", async () => { it("tx error skip preflight", async () => { const payer = await createKeyPairSignerFromBytes(Uint8Array.from(faucet)) - const blockhash = await rpc - .getLatestBlockhash({ commitment: "finalized" }) - .send() - - const tx = await pipe( - createTransactionMessage({ version: 0 }), - (tx) => appendTransactionMessageInstruction(causeError(), tx), - (tx) => setTransactionMessageFeePayerSigner(payer, tx), - (tx) => - setTransactionMessageLifetimeUsingBlockhash( - { - blockhash: blockhash.value.blockhash, - lastValidBlockHeight: blockhash.value.lastValidBlockHeight, - }, - tx - ), - (tx) => signTransactionMessageWithSigners(tx) - ) - - const sendAndConfirmFn = sendAndConfirmTransactionFactory({ - rpc, - rpcSubscriptions, - }) - try { - await sendAndConfirmFn(tx, { - commitment: "processed", - skipPreflight: true, - }) + await sendTx(payer, [causeError()], { skipPreflight: true }) } catch (e) { const parsed = fromTxError(e) @@ -1207,3 +1172,42 @@ it("toJSON", async () => { expect(act.kind).toBe("NoFields") } }) + +type SendAndConfirmTransactionFactoryFn = ReturnType< + typeof sendAndConfirmTransactionFactory +> +type SendConfig = Parameters[1] + +async function sendTx( + payer: TransactionSigner, + ixs: IInstruction[], + config: Partial = {} +) { + const blockhash = await rpc + .getLatestBlockhash({ commitment: "finalized" }) + .send() + + const tx = await pipe( + createTransactionMessage({ version: 0 }), + (tx) => appendTransactionMessageInstructions(ixs, tx), + (tx) => setTransactionMessageFeePayerSigner(payer, tx), + (tx) => + setTransactionMessageLifetimeUsingBlockhash( + { + blockhash: blockhash.value.blockhash, + lastValidBlockHeight: blockhash.value.lastValidBlockHeight, + }, + tx + ), + (tx) => signTransactionMessageWithSigners(tx) + ) + + const sendAndConfirmFn = sendAndConfirmTransactionFactory({ + rpc, + rpcSubscriptions, + }) + await sendAndConfirmFn(tx, { + commitment: "confirmed", + ...config, + }) +}