From 57f0f7e660ce3a559fa6b225ad2c772de5a7db13 Mon Sep 17 00:00:00 2001 From: acheron Date: Wed, 31 Aug 2022 19:55:35 +0200 Subject: [PATCH] Build and link new SPL packages before tests and decode token account for AccountStore --- .github/actions/setup-ts/action.yaml | 9 +- ts/build-packages.sh | 1 - .../anchor/src/program/accounts-resolver.ts | 24 ++- .../src/program/token-account-layout.ts | 146 ++++++++++++++++++ 4 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 ts/packages/anchor/src/program/token-account-layout.ts diff --git a/.github/actions/setup-ts/action.yaml b/.github/actions/setup-ts/action.yaml index ed9684fe6c..a3b6901a1c 100644 --- a/.github/actions/setup-ts/action.yaml +++ b/.github/actions/setup-ts/action.yaml @@ -20,10 +20,13 @@ runs: path: | ./ts/dist/ key: solana-${{ runner.os }}-v0000-${{ env.NODE_VERSION }}-${{ hashFiles('./ts/**/*.ts') }} - - run: cd ts/packages/anchor && yarn --frozen-lockfile && yarn build:node && yarn link && cd ../../../ + - run: cd ts/packages/anchor && yarn --frozen-lockfile && yarn build:node && yarn link && cd ../ + shell: bash + - run: cd spl-associated-token-account && yarn --frozen-lockfile && yarn build:node && yarn link && cd ../ + shell: bash + - run: cd spl-token && yarn --frozen-lockfile && yarn build:node && yarn link && cd ../../../ shell: bash - run: cd examples/tutorial && yarn link @project-serum/anchor && yarn --frozen-lockfile && cd ../../ shell: bash - - run: cd tests && yarn link @project-serum/anchor && yarn --frozen-lockfile && cd .. + - run: cd tests && yarn link @project-serum/anchor && yarn link @project-serum/spl-associated-token-account && yarn link @project-serum/spl-token && yarn --frozen-lockfile && cd .. shell: bash - diff --git a/ts/build-packages.sh b/ts/build-packages.sh index 2991c815f3..fb78a090a1 100644 --- a/ts/build-packages.sh +++ b/ts/build-packages.sh @@ -6,4 +6,3 @@ for D in */; cd $D && yarn init:yarn; cd ..; fi done - diff --git a/ts/packages/anchor/src/program/accounts-resolver.ts b/ts/packages/anchor/src/program/accounts-resolver.ts index 6190336929..90e2c8b4fb 100644 --- a/ts/packages/anchor/src/program/accounts-resolver.ts +++ b/ts/packages/anchor/src/program/accounts-resolver.ts @@ -6,6 +6,7 @@ import { TOKEN_PROGRAM_ID, ASSOCIATED_PROGRAM_ID } from "../utils/token.js"; import { AllInstructions } from "./namespace/types.js"; import Provider from "../provider.js"; import { AccountNamespace } from "./namespace/account.js"; +import { decodeTokenAccount } from "./token-account-layout.js"; // Populates a given accounts context with PDAs and common missing accounts. export class AccountsResolver { @@ -26,7 +27,7 @@ export class AccountsResolver { private _idlIx: AllInstructions, _accountNamespace: AccountNamespace ) { - this._accountStore = new AccountStore(_accountNamespace); + this._accountStore = new AccountStore(_provider, _accountNamespace); } // Note: We serially resolve PDAs one by one rather than doing them @@ -230,7 +231,10 @@ export class AccountsResolver { export class AccountStore { private _cache = new Map(); - constructor(private _accounts: AccountNamespace) {} + constructor( + private _provider: Provider, + private _accounts: AccountNamespace + ) {} public async fetchAccount( name: string, @@ -238,9 +242,19 @@ export class AccountStore { ): Promise { const address = publicKey.toString(); if (!this._cache.has(address)) { - if (name === "TokenAccount") name = "Account"; - const account = this._accounts[camelCase(name)].fetch(publicKey); - this._cache.set(address, account); + if (name === "TokenAccount") { + const accountInfo = await this._provider.connection.getAccountInfo( + publicKey + ); + if (accountInfo === null) { + throw new Error(`invalid account info for ${address}`); + } + const decodedAccount = decodeTokenAccount(accountInfo.data); + this._cache.set(address, decodedAccount); + } else { + const account = this._accounts[camelCase(name)].fetch(publicKey); + this._cache.set(address, account); + } } return this._cache.get(address); } diff --git a/ts/packages/anchor/src/program/token-account-layout.ts b/ts/packages/anchor/src/program/token-account-layout.ts new file mode 100644 index 0000000000..3bd74cfa04 --- /dev/null +++ b/ts/packages/anchor/src/program/token-account-layout.ts @@ -0,0 +1,146 @@ +import BN from "bn.js"; +import * as BufferLayout from "buffer-layout"; +import { Layout } from "buffer-layout"; +import { PublicKey } from "@solana/web3.js"; + +function uint64(property?: string): Layout { + return new WrappedLayout( + BufferLayout.blob(8), + (b: Buffer) => u64.fromBuffer(b), + (n: u64) => n.toBuffer(), + property + ); +} + +function publicKey(property?: string): Layout { + return new WrappedLayout( + BufferLayout.blob(32), + (b: Buffer) => new PublicKey(b), + (key: PublicKey) => key.toBuffer(), + property + ); +} + +function coption(layout: Layout, property?: string): Layout { + return new COptionLayout(layout, property); +} + +class WrappedLayout extends Layout { + layout: Layout; + decoder: (data: T) => U; + encoder: (src: U) => T; + + constructor( + layout: Layout, + decoder: (data: T) => U, + encoder: (src: U) => T, + property?: string + ) { + super(layout.span, property); + this.layout = layout; + this.decoder = decoder; + this.encoder = encoder; + } + + decode(b: Buffer, offset?: number): U { + return this.decoder(this.layout.decode(b, offset)); + } + + encode(src: U, b: Buffer, offset?: number): number { + return this.layout.encode(this.encoder(src), b, offset); + } + + getSpan(b: Buffer, offset?: number): number { + return this.layout.getSpan(b, offset); + } +} + +class COptionLayout extends Layout { + layout: Layout; + discriminator: Layout; + + constructor(layout: Layout, property?: string) { + super(-1, property); + this.layout = layout; + this.discriminator = BufferLayout.u32(); + } + + encode(src: T | null, b: Buffer, offset = 0): number { + if (src === null || src === undefined) { + return this.layout.span + this.discriminator.encode(0, b, offset); + } + this.discriminator.encode(1, b, offset); + return this.layout.encode(src, b, offset + 4) + 4; + } + + decode(b: Buffer, offset = 0): T | null { + const discriminator = this.discriminator.decode(b, offset); + if (discriminator === 0) { + return null; + } else if (discriminator === 1) { + return this.layout.decode(b, offset + 4); + } + throw new Error("Invalid coption " + this.layout.property); + } + + getSpan(b: Buffer, offset = 0): number { + return this.layout.getSpan(b, offset + 4) + 4; + } +} + +class u64 extends BN { + /** + * Convert to Buffer representation + */ + toBuffer(): Buffer { + const a = super.toArray().reverse(); + const b = Buffer.from(a); + if (length === 8) { + return b; + } + if (length >= 8) { + throw new Error("u64 too large"); + } + + const zeroPad = Buffer.alloc(8); + b.copy(zeroPad); + return zeroPad; + } + + /** + * Construct a u64 from Buffer representation + */ + static fromBuffer(buffer: Buffer): u64 { + if (buffer.length !== 8) { + throw new Error(`Invalid buffer length: ${buffer.length}`); + } + return new u64( + [...buffer] + .reverse() + .map((i) => `00${i.toString(16)}`.slice(-2)) + .join(""), + 16 + ); + } +} + +const TOKEN_ACCOUNT_LAYOUT = BufferLayout.struct([ + publicKey("mint"), + publicKey("owner"), + uint64("amount"), + coption(publicKey(), "delegate"), + ((p: string) => { + const U = BufferLayout.union(BufferLayout.u8("discriminator"), null, p); + U.addVariant(0, BufferLayout.struct([]), "uninitialized"); + U.addVariant(1, BufferLayout.struct([]), "initialized"); + U.addVariant(2, BufferLayout.struct([]), "frozen"); + return U; + })("state"), + coption(uint64(), "isNative"), + uint64("delegatedAmount"), + coption(publicKey(), "closeAuthority"), +]); + +export function decodeTokenAccount(b: Buffer) { + return TOKEN_ACCOUNT_LAYOUT.decode(b); +}