Skip to content

Commit

Permalink
- Add support for optional accounts with @solana/web3.js:2 Option
Browse files Browse the repository at this point in the history
… types

- Do not mark none optional accounts as mutable
  • Loading branch information
elliotkennedy committed Dec 3, 2024
1 parent e41f6cf commit 0e419ce
Show file tree
Hide file tree
Showing 16 changed files with 662 additions and 298 deletions.
2 changes: 2 additions & 0 deletions examples/basic-2/generated-client/instructions/create.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/basic-2/generated-client/instructions/increment.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/tic-tac-toe/generated-client/instructions/play.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
73 changes: 60 additions & 13 deletions src/instructions.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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[]
Expand All @@ -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},`)
}
})
}

Expand Down
122 changes: 122 additions & 0 deletions tests/example-program-gen/exp/accounts/OptionalState.ts
Original file line number Diff line number Diff line change
@@ -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<OptionalState>([
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<GetAccountInfoApi>,
address: Address,
programId: Address = PROGRAM_ID
): Promise<OptionalState | null> {
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<GetMultipleAccountsApi>,
addresses: Address[],
programId: Address = PROGRAM_ID
): Promise<Array<OptionalState | null>> {
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,
})
}
}
2 changes: 2 additions & 0 deletions tests/example-program-gen/exp/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { State } from "./State"
export type { StateFields, StateJSON } from "./State"
export { State2 } from "./State2"
export type { State2Fields, State2JSON } from "./State2"
export { OptionalState } from "./OptionalState"
export type { OptionalStateFields, OptionalStateJSON } from "./OptionalState"
2 changes: 2 additions & 0 deletions tests/example-program-gen/exp/instructions/causeError.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/example-program-gen/exp/instructions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export type {
InitializeWithValues2Accounts,
} from "./initializeWithValues2"
export { causeError } from "./causeError"
export { optional } from "./optional"
export type { OptionalAccounts } from "./optional"
2 changes: 2 additions & 0 deletions tests/example-program-gen/exp/instructions/initialize.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
63 changes: 63 additions & 0 deletions tests/example-program-gen/exp/instructions/optional.ts
Original file line number Diff line number Diff line change
@@ -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<TransactionSigner>
mutableSignerOption: Option<TransactionSigner>
readonlyOption: Option<Address>
mutableOption: Option<Address>
payer: TransactionSigner
systemProgram: Address
}

export function optional(
accounts: OptionalAccounts,
programAddress: Address = PROGRAM_ID
) {
const keys: Array<IAccountMeta | IAccountSignerMeta> = [
{
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
}
Loading

0 comments on commit 0e419ce

Please sign in to comment.