Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for optional accounts with @solana/web3.js:2.0 Option types #67

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,

Check warning on line 3 in examples/basic-2/generated-client/instructions/create.ts

View workflow job for this annotation

GitHub Actions / Run linters

'isSome' is defined but never used
IAccountMeta,
IAccountSignerMeta,
IInstruction,
Option,

Check warning on line 7 in examples/basic-2/generated-client/instructions/create.ts

View workflow job for this annotation

GitHub Actions / Run linters

'Option' is defined but never used
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,

Check warning on line 3 in examples/basic-2/generated-client/instructions/increment.ts

View workflow job for this annotation

GitHub Actions / Run linters

'isSome' is defined but never used
IAccountMeta,
IAccountSignerMeta,
IInstruction,
Option,

Check warning on line 7 in examples/basic-2/generated-client/instructions/increment.ts

View workflow job for this annotation

GitHub Actions / Run linters

'Option' is defined but never used
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,

Check warning on line 3 in examples/tic-tac-toe/generated-client/instructions/play.ts

View workflow job for this annotation

GitHub Actions / Run linters

'isSome' is defined but never used
IAccountMeta,
IAccountSignerMeta,
IInstruction,
Option,

Check warning on line 7 in examples/tic-tac-toe/generated-client/instructions/play.ts

View workflow job for this annotation

GitHub Actions / Run linters

'Option' is defined but never used
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,

Check warning on line 3 in examples/tic-tac-toe/generated-client/instructions/setupGame.ts

View workflow job for this annotation

GitHub Actions / Run linters

'isSome' is defined but never used
IAccountMeta,
IAccountSignerMeta,
IInstruction,
Option,

Check warning on line 7 in examples/tic-tac-toe/generated-client/instructions/setupGame.ts

View workflow job for this annotation

GitHub Actions / Run linters

'Option' is defined but never used
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
@@ -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"
Expand Down
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
Loading