Skip to content

Commit

Permalink
refactor(experimental): add function to assert account(s) are decoded (
Browse files Browse the repository at this point in the history
  • Loading branch information
mcintyre94 authored Jan 4, 2024
1 parent a157265 commit 974fa30
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 19 deletions.
44 changes: 44 additions & 0 deletions packages/accounts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,47 @@ const myDecoder: Decoder<MyAccountData> = getStructDecoder([
const myDecodedAccount = decodeAccount(myAccount, myDecoder);
myDecodedAccount satisfies Account<MyAccountData, '1234..5678'>;
```

### `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<MyAccountData | Uint8Array, '1234..5678'>;
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<MockData | Uint8Array> = await fetchJsonParsedAccount<MockData>(
rpc,
'1234..5678' as Address,
)

assertAccountDecoded(account);
// now we have a MaybeAccount<MockData>
account satisfies MaybeAccount<MockData>
```

### `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<MyAccountData | Uint8Array, Address>[];
assertAccountsDecoded(myAccounts);

// now the account data can be used as MyAccountData
for(const a of account) {
account.data satisfies MyAccountData;
}
```
130 changes: 127 additions & 3 deletions packages/accounts/src/__tests__/decode-account-test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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 = <EncodedAccount>{
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 = <Account<MockData>>{
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 = <MaybeAccount<MockData>>{
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 = [
<EncodedAccount>{
address: '1111' as Address,
data: new Uint8Array([]),
},
<EncodedAccount>{
address: '2222' as Address,
data: new Uint8Array([]),
},
<Account<MockData>>{
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 = [
<Account<MockData>>{
address: '1111' as Address,
data: { foo: 42 },
},
<Account<MockData>>{
address: '2222' as Address,
data: { foo: 42 },
},
<Account<MockData>>{
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 = [
<MaybeAccount<MockData>>{
address: '1111' as Address,
exists: false,
},
<MaybeAccount<MockData>>{
address: '2222' as Address,
exists: false,
},
<MaybeAccount<MockData>>{
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();
});
});
10 changes: 5 additions & 5 deletions packages/accounts/src/__tests__/maybe-account-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,28 @@ describe('assertAccountsExist', () => {
const maybeAccounts = [
<MaybeEncodedAccount>{ address: '1111', exists: false },
<MaybeEncodedAccount>{ address: '2222', exists: false },
<MaybeEncodedAccount>{ address: '3333', exists: true }
<MaybeEncodedAccount>{ address: '3333', exists: true },
];

// When we assert that all the accounts exist.
const fn = () => assertAccountsExist(maybeAccounts);

// 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 = [
<MaybeEncodedAccount>{ address: '1111', exists: true },
<MaybeEncodedAccount>{ address: '2222', exists: true },
<MaybeEncodedAccount>{ address: '3333', exists: true }
<MaybeEncodedAccount>{ address: '3333', exists: true },
];

// When we assert that all the accounts exist.
const fn = () => assertAccountsExist(maybeAccounts);

// Then we expect an error not to be thrown
expect(fn).not.toThrow();
})
})
});
});
25 changes: 24 additions & 1 deletion packages/accounts/src/__typetests__/decode-account-typetest.ts
Original file line number Diff line number Diff line change
@@ -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 };
Expand All @@ -20,3 +21,25 @@ type MockDataDecoder = Decoder<MockData>;
// @ts-expect-error The account should not be of type Account as it may not exist.
account satisfies Account<MockData, '1111'>;
}

{
// It narrows an account with data MockData | Uint8Array to MockData
const account = {} as unknown as Account<MockData | Uint8Array, '1111'>;
assertAccountDecoded(account);
account satisfies Account<MockData, '1111'>;
account.data satisfies MockData;
}

{
// It narrows a list of accounts with data MockData | Uint8Array to MockData
const accounts = [
{} as unknown as Account<MockData | Uint8Array, '1111'>,
{} as unknown as Account<MockData | Uint8Array, '2222'>,
{} as unknown as Account<MockData | Uint8Array, '3333'>,
];
assertAccountsDecoded(accounts);
accounts satisfies Account<MockData, Address>[];
for (const a of accounts) {
a.data satisfies MockData;
}
}
8 changes: 4 additions & 4 deletions packages/accounts/src/__typetests__/maybe-account-typetest.ts
Original file line number Diff line number Diff line change
@@ -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 };

Expand All @@ -21,4 +21,4 @@ type MockData = { foo: 42 };
];
assertAccountsExist(accounts);
accounts satisfies Account<MockData, Address>[];
}
}
44 changes: 41 additions & 3 deletions packages/accounts/src/decode-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TData extends object, TAddress extends string = string>(
encodedAccount: EncodedAccount<TAddress>,
decoder: Decoder<TData>
decoder: Decoder<TData>,
): Account<TData, TAddress>;
export function decodeAccount<TData extends object, TAddress extends string = string>(
encodedAccount: MaybeEncodedAccount<TAddress>,
decoder: Decoder<TData>
decoder: Decoder<TData>,
): MaybeAccount<TData, TAddress>;
export function decodeAccount<TData extends object, TAddress extends string = string>(
encodedAccount: EncodedAccount<TAddress> | MaybeEncodedAccount<TAddress>,
decoder: Decoder<TData>
decoder: Decoder<TData>,
): Account<TData, TAddress> | MaybeAccount<TData, TAddress> {
try {
if ('exists' in encodedAccount && !encodedAccount.exists) {
Expand All @@ -28,3 +28,41 @@ export function decodeAccount<TData extends object, TAddress extends string = st
throw newError;
}
}

function accountExists<TData extends object>(account: Account<TData> | MaybeAccount<TData>): account is Account<TData> {
return !('exists' in account) || ('exists' in account && account.exists);
}

/** Asserts that an account has been decoded. */
export function assertAccountDecoded<TData extends object, TAddress extends string = string>(
account: Account<TData | Uint8Array, TAddress>,
): asserts account is Account<TData, TAddress>;
export function assertAccountDecoded<TData extends object, TAddress extends string = string>(
account: MaybeAccount<TData | Uint8Array, TAddress>,
): asserts account is MaybeAccount<TData, TAddress>;
export function assertAccountDecoded<TData extends object, TAddress extends string = string>(
account: Account<TData | Uint8Array, TAddress> | MaybeAccount<TData | Uint8Array, TAddress>,
): asserts account is Account<TData, TAddress> | MaybeAccount<TData, TAddress> {
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<TData extends object, TAddress extends string = string>(
accounts: Account<TData | Uint8Array, TAddress>[],
): asserts accounts is Account<TData, TAddress>[];
export function assertAccountsDecoded<TData extends object, TAddress extends string = string>(
accounts: MaybeAccount<TData | Uint8Array, TAddress>[],
): asserts accounts is MaybeAccount<TData, TAddress>[];
export function assertAccountsDecoded<TData extends object, TAddress extends string = string>(
accounts: (Account<TData | Uint8Array, TAddress> | MaybeAccount<TData | Uint8Array, TAddress>)[],
): asserts accounts is (Account<TData, TAddress> | MaybeAccount<TData, TAddress>)[] {
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.`);
}
}
6 changes: 3 additions & 3 deletions packages/accounts/src/maybe-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type MaybeEncodedAccount<TAddress extends string = string> = MaybeAccount

/** Asserts that an account that may or may not exists, actually exists. */
export function assertAccountExists<TData extends object | Uint8Array, TAddress extends string = string>(
account: MaybeAccount<TData, TAddress>
account: MaybeAccount<TData, TAddress>,
): asserts account is Account<TData, TAddress> & { exists: true } {
if (!account.exists) {
// TODO: Coded error.
Expand All @@ -22,10 +22,10 @@ export function assertAccountExists<TData extends object | Uint8Array, TAddress

/** Asserts that all accounts that may or may not exist, actually all exist. */
export function assertAccountsExist<TData extends object | Uint8Array, TAddress extends string = string>(
accounts: MaybeAccount<TData, TAddress>[]
accounts: MaybeAccount<TData, TAddress>[],
): asserts accounts is (Account<TData, TAddress> & { 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.`);
Expand Down

0 comments on commit 974fa30

Please sign in to comment.