diff --git a/packages/accounts/README.md b/packages/accounts/README.md index 9833e068717e..0657da0c6c77 100644 --- a/packages/accounts/README.md +++ b/packages/accounts/README.md @@ -267,3 +267,47 @@ const myDecoder: Decoder = getStructDecoder([ const myDecodedAccount = decodeAccount(myAccount, myDecoder); myDecodedAccount satisfies Account; ``` + +### `assertAccountDecoded()` + +This function asserts that an account stores decoded data, ie not a Uint8Array. Note that it does not check the shape of the data matches the decoded type, only that it is not a Uint8Array. + +```ts +type MyAccountData = { name: string; age: number }; + +const myAccount: Account; +assertAccountDecoded(myAccount); + +// now the account data can be used as MyAccountData +account.data satisfies MyAccountData; +``` + +This is particularly useful for narrowing the result of fetching a JSON parsed account. + + +```ts + const account: MaybeAccount = await fetchJsonParsedAccount( + rpc, + '1234..5678' as Address, + ) + + assertAccountDecoded(account); + // now we have a MaybeAccount + account satisfies MaybeAccount +``` + +### `assertAccountsDecoded` + +This function asserts that all input accounts store decoded data, ie not a Uint8Array. As with `assertAccountDecoded` it does not check the shape of the data matches the decoded type, only that it is not a Uint8Array. + +```ts +type MyAccountData = { name: string; age: number }; + +const myAccounts: Account[]; +assertAccountsDecoded(myAccounts); + +// now the account data can be used as MyAccountData +for(const a of account) { + account.data satisfies MyAccountData; +} +``` diff --git a/packages/accounts/src/__tests__/decode-account-test.ts b/packages/accounts/src/__tests__/decode-account-test.ts index bda249523305..5a158eea5d45 100644 --- a/packages/accounts/src/__tests__/decode-account-test.ts +++ b/packages/accounts/src/__tests__/decode-account-test.ts @@ -1,8 +1,10 @@ import 'test-matchers/toBeFrozenObject'; -import { EncodedAccount } from '../account'; -import { decodeAccount } from '../decode-account'; -import { MaybeEncodedAccount } from '../maybe-account'; +import { Address } from '@solana/addresses'; + +import { Account, EncodedAccount } from '../account'; +import { assertAccountDecoded, assertAccountsDecoded, decodeAccount } from '../decode-account'; +import { MaybeAccount, MaybeEncodedAccount } from '../maybe-account'; import { getMockDecoder } from './__setup__'; describe('decodeAccount', () => { @@ -69,3 +71,125 @@ describe('decodeAccount', () => { expect(account).toBe(encodedAccount); }); }); + +describe('assertDecodedAccount', () => { + type MockData = { foo: 42 }; + + it('throws if the provided account is encoded', () => { + // Given an account with Uint8Array data + const account = { + address: '1111' as Address, + data: new Uint8Array([]), + }; + + // When we assert that the account is decoded + const fn = () => assertAccountDecoded(account); + + // Then we expect an error to be thrown + expect(fn).toThrow('Expected account [1111] to be decoded.'); + }); + + it('does not throw if the provided account is decoded', () => { + // Given an account with decoded data + const account = >{ + address: '1111' as Address, + data: { foo: 42 }, + }; + + // When we assert that the account is decoded + const fn = () => assertAccountDecoded(account); + + // Then we expect an error not to be thrown + expect(fn).not.toThrow(); + }); + + it('does not throw if the input account does not exist', () => { + // Given an account that does not exist + const account = >{ + address: '1111' as Address, + exists: false, + }; + + // When we assert that the account is decoded + const fn = () => assertAccountDecoded(account); + + // Then we expect an error not to be thrown + expect(fn).not.toThrow(); + }); +}); + +describe('assertDecodedAccounts', () => { + type MockData = { foo: 42 }; + + it('throws if any of the provided accounts are encoded', () => { + // Given two encoded accounts and one decoded account + const accounts = [ + { + address: '1111' as Address, + data: new Uint8Array([]), + }, + { + address: '2222' as Address, + data: new Uint8Array([]), + }, + >{ + address: '3333' as Address, + data: { foo: 42 }, + }, + ]; + + // When we assert that the accounts are decoded + const fn = () => assertAccountsDecoded(accounts); + + // Then we expect an error to be thrown + expect(fn).toThrow('Expected accounts [1111, 2222] to be decoded.'); + }); + + it('does not throw if all of the provided accounts are decoded', () => { + // Given three decoded accounts + const accounts = [ + >{ + address: '1111' as Address, + data: { foo: 42 }, + }, + >{ + address: '2222' as Address, + data: { foo: 42 }, + }, + >{ + address: '3333' as Address, + data: { foo: 42 }, + }, + ]; + + // When we assert that the accounts are decoded + const fn = () => assertAccountsDecoded(accounts); + + // Then we expect an error not to be thrown + expect(fn).not.toThrow(); + }); + + it('does not throw if all provided accounts are missing', () => { + // Given three missing accounts + const accounts = [ + >{ + address: '1111' as Address, + exists: false, + }, + >{ + address: '2222' as Address, + exists: false, + }, + >{ + address: '3333' as Address, + exists: false, + }, + ]; + + // When we assert that the accounts are decoded + const fn = () => assertAccountsDecoded(accounts); + + // Then we expect an error not to be thrown + expect(fn).not.toThrow(); + }); +}); diff --git a/packages/accounts/src/__tests__/maybe-account-test.ts b/packages/accounts/src/__tests__/maybe-account-test.ts index 9b5f412e41e2..26d56ab9d959 100644 --- a/packages/accounts/src/__tests__/maybe-account-test.ts +++ b/packages/accounts/src/__tests__/maybe-account-test.ts @@ -21,7 +21,7 @@ describe('assertAccountsExist', () => { const maybeAccounts = [ { address: '1111', exists: false }, { address: '2222', exists: false }, - { address: '3333', exists: true } + { address: '3333', exists: true }, ]; // When we assert that all the accounts exist. @@ -29,14 +29,14 @@ describe('assertAccountsExist', () => { // Then we expect an error to be thrown with the non-existent accounts expect(fn).toThrow('Expected accounts [1111, 2222] to exist'); - }) + }); it('does not fail if all accounts exist', () => { // Given three accounts that all exist const maybeAccounts = [ { address: '1111', exists: true }, { address: '2222', exists: true }, - { address: '3333', exists: true } + { address: '3333', exists: true }, ]; // When we assert that all the accounts exist. @@ -44,5 +44,5 @@ describe('assertAccountsExist', () => { // Then we expect an error not to be thrown expect(fn).not.toThrow(); - }) -}) + }); +}); diff --git a/packages/accounts/src/__typetests__/decode-account-typetest.ts b/packages/accounts/src/__typetests__/decode-account-typetest.ts index a24bc0662272..3bf7d92e3d82 100644 --- a/packages/accounts/src/__typetests__/decode-account-typetest.ts +++ b/packages/accounts/src/__typetests__/decode-account-typetest.ts @@ -1,7 +1,8 @@ +import { Address } from '@solana/addresses'; import { Decoder } from '@solana/codecs-core'; import { Account, EncodedAccount } from '../account'; -import { decodeAccount } from '../decode-account'; +import { assertAccountDecoded, assertAccountsDecoded, decodeAccount } from '../decode-account'; import { MaybeAccount, MaybeEncodedAccount } from '../maybe-account'; type MockData = { foo: 42 }; @@ -20,3 +21,25 @@ type MockDataDecoder = Decoder; // @ts-expect-error The account should not be of type Account as it may not exist. account satisfies Account; } + +{ + // It narrows an account with data MockData | Uint8Array to MockData + const account = {} as unknown as Account; + assertAccountDecoded(account); + account satisfies Account; + account.data satisfies MockData; +} + +{ + // It narrows a list of accounts with data MockData | Uint8Array to MockData + const accounts = [ + {} as unknown as Account, + {} as unknown as Account, + {} as unknown as Account, + ]; + assertAccountsDecoded(accounts); + accounts satisfies Account[]; + for (const a of accounts) { + a.data satisfies MockData; + } +} diff --git a/packages/accounts/src/__typetests__/maybe-account-typetest.ts b/packages/accounts/src/__typetests__/maybe-account-typetest.ts index 247931c69a5d..fa2a159e201f 100644 --- a/packages/accounts/src/__typetests__/maybe-account-typetest.ts +++ b/packages/accounts/src/__typetests__/maybe-account-typetest.ts @@ -1,7 +1,7 @@ -import { Address } from "@solana/addresses"; +import { Address } from '@solana/addresses'; -import { Account } from "../account"; -import { assertAccountExists, assertAccountsExist,MaybeAccount } from "../maybe-account"; +import { Account } from '../account'; +import { assertAccountExists, assertAccountsExist, MaybeAccount } from '../maybe-account'; type MockData = { foo: 42 }; @@ -21,4 +21,4 @@ type MockData = { foo: 42 }; ]; assertAccountsExist(accounts); accounts satisfies Account[]; -} \ No newline at end of file +} diff --git a/packages/accounts/src/decode-account.ts b/packages/accounts/src/decode-account.ts index ad895f042aa1..a4b6740ca214 100644 --- a/packages/accounts/src/decode-account.ts +++ b/packages/accounts/src/decode-account.ts @@ -6,15 +6,15 @@ import type { MaybeAccount, MaybeEncodedAccount } from './maybe-account'; /** Decodes the data of a given account using the provided decoder. */ export function decodeAccount( encodedAccount: EncodedAccount, - decoder: Decoder + decoder: Decoder, ): Account; export function decodeAccount( encodedAccount: MaybeEncodedAccount, - decoder: Decoder + decoder: Decoder, ): MaybeAccount; export function decodeAccount( encodedAccount: EncodedAccount | MaybeEncodedAccount, - decoder: Decoder + decoder: Decoder, ): Account | MaybeAccount { try { if ('exists' in encodedAccount && !encodedAccount.exists) { @@ -28,3 +28,41 @@ export function decodeAccount(account: Account | MaybeAccount): account is Account { + return !('exists' in account) || ('exists' in account && account.exists); +} + +/** Asserts that an account has been decoded. */ +export function assertAccountDecoded( + account: Account, +): asserts account is Account; +export function assertAccountDecoded( + account: MaybeAccount, +): asserts account is MaybeAccount; +export function assertAccountDecoded( + account: Account | MaybeAccount, +): asserts account is Account | MaybeAccount { + if (accountExists(account) && account.data instanceof Uint8Array) { + // TODO: coded error. + throw new Error(`Expected account [${account.address}] to be decoded.`); + } +} + +/** Asserts that all accounts have been decoded. */ +export function assertAccountsDecoded( + accounts: Account[], +): asserts accounts is Account[]; +export function assertAccountsDecoded( + accounts: MaybeAccount[], +): asserts accounts is MaybeAccount[]; +export function assertAccountsDecoded( + accounts: (Account | MaybeAccount)[], +): asserts accounts is (Account | MaybeAccount)[] { + const encoded = accounts.filter(a => accountExists(a) && a.data instanceof Uint8Array); + if (encoded.length > 0) { + const encodedAddresses = encoded.map(a => a.address).join(', '); + // TODO: Coded error. + throw new Error(`Expected accounts [${encodedAddresses}] to be decoded.`); + } +} diff --git a/packages/accounts/src/maybe-account.ts b/packages/accounts/src/maybe-account.ts index 0170f48174c9..bd9ea603f98f 100644 --- a/packages/accounts/src/maybe-account.ts +++ b/packages/accounts/src/maybe-account.ts @@ -12,7 +12,7 @@ export type MaybeEncodedAccount = MaybeAccount /** Asserts that an account that may or may not exists, actually exists. */ export function assertAccountExists( - account: MaybeAccount + account: MaybeAccount, ): asserts account is Account & { exists: true } { if (!account.exists) { // TODO: Coded error. @@ -22,10 +22,10 @@ export function assertAccountExists( - accounts: MaybeAccount[] + accounts: MaybeAccount[], ): asserts accounts is (Account & { exists: true })[] { const missingAccounts = accounts.filter(a => !a.exists); - if(missingAccounts.length > 0) { + if (missingAccounts.length > 0) { const missingAddresses = missingAccounts.map(a => a.address); // TODO: Coded error. throw new Error(`Expected accounts [${missingAddresses.join(', ')}] to exist.`);