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

Functions you can use to assert that a group of wallet accounts support a given feature #2106

Draft
wants to merge 1 commit into
base: 02-06-A-use-account-hook
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 9 additions & 5 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,13 @@ export const SOLANA_ERROR__TRANSACTION_ERROR__UNBALANCED_TRANSACTION = 7050036 a

// Wallet-related errors
// Reserve error codes in the range [7718000, 7718999]
export const SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND = 7718000 as const;
export const SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED = 7718001 as const;
export const SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS = 7718002 as const;
export const SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN = 7718003 as const;
export const SOLANA_ERROR__WALLET__INVALID_SOLANA_CHAIN = 7718004 as const;
export const SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED = 7718000 as const;
export const SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND = 7718001 as const;
export const SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED = 7718002 as const;
export const SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS = 7718003 as const;
export const SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED = 7718004 as const;
export const SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN = 7718005 as const;
export const SOLANA_ERROR__WALLET__INVALID_SOLANA_CHAIN = 7718006 as const;

// Codec-related errors.
// Reserve error codes in the range [8078000-8078999].
Expand Down Expand Up @@ -539,8 +541,10 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_ACCOUNT_COST_LIMIT
| typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_BLOCK_COST_LIMIT
| typeof SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_VOTE_COST_LIMIT
| typeof SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED
| typeof SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND
| typeof SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED
| typeof SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS
| typeof SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN
| typeof SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED
| typeof SOLANA_ERROR__WALLET__INVALID_SOLANA_CHAIN;
10 changes: 10 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,12 @@ import {
SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_RENT,
SOLANA_ERROR__TRANSACTION_ERROR__PROGRAM_EXECUTION_TEMPORARILY_RESTRICTED,
SOLANA_ERROR__TRANSACTION_ERROR__UNKNOWN,
SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED,
SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED,
SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS,
SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN,
SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED,
SOLANA_ERROR__WALLET__INVALID_SOLANA_CHAIN,
SolanaErrorCode,
} from './codes';
Expand Down Expand Up @@ -583,6 +585,10 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
[SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE]: {
actualVersion: number;
};
[SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED]: {
accountAddress: string;
featureNames: readonly `${string}:${string}`[];
};
[SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND]: {
accountAddress: string;
walletName: string;
Expand All @@ -598,6 +604,10 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
chain: `${string}:${string}`;
walletName: string;
};
[SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED]: {
featureNames: readonly `${string}:${string}`[];
walletName: string;
};
[SOLANA_ERROR__WALLET__INVALID_SOLANA_CHAIN]: {
chain: `${string}:${string}`;
};
Expand Down
6 changes: 6 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,12 @@ import {
SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_ACCOUNT_COST_LIMIT,
SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_BLOCK_COST_LIMIT,
SOLANA_ERROR__TRANSACTION_ERROR__WOULD_EXCEED_MAX_VOTE_COST_LIMIT,
SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED,
SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND,
SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED,
SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS,
SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN,
SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED,
SOLANA_ERROR__WALLET__INVALID_SOLANA_CHAIN,
SolanaErrorCode,
} from './codes';
Expand Down Expand Up @@ -594,12 +596,16 @@ export const SolanaErrorMessages: Readonly<{
[SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING]: 'Transaction is missing signatures for addresses: $addresses.',
[SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE]:
'Transaction version must be in the range [0, 127]. `$actualVersion` given',
[SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED]:
'The $featureName` feature is not supported by the account `$accountAddress` belonging to the wallet `$walletName`',
[SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND]:
'No account having address `$accountAddress` could be found in the wallet `$walletName}`',
[SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED]:
"The wallet '$walletName' does not support connecting to the chain `$chain`",
[SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS]: "The wallet '$walletName' has no connected accounts",
[SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN]:
"The wallet '$walletName' has no connected accounts for the chain `$chain`",
[SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED]:
'The `$featureName` feature is not supported by the wallet `$walletName`',
[SOLANA_ERROR__WALLET__INVALID_SOLANA_CHAIN]: 'The chain `$chain` is not supported',
};
63 changes: 63 additions & 0 deletions packages/react/src/__tests__/assertions-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { address } from '@solana/addresses';
import {
SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED,
SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED,
SolanaError,
} from '@solana/errors';

import { assertWalletAccountSupportsFeatures, assertWalletSupportsFeatures } from '../assertions';

describe('assertWalletAccountSupportsFeatures', () => {
it('does not fatal when a wallet account supports all features', () => {
const mockAccount = {
address: address('3kjLdPpu1ayfyxRzGFLX352NfKTFL8b4wb8GA8aFxaR3'),
features: ['solana:signTransaction', 'solana:signAndSendTransaction', 'solana:signIn'] as const,
};
expect(() => {
assertWalletAccountSupportsFeatures(['solana:signAndSendTransaction', 'solana:signIn'], mockAccount);
}).not.toThrow();
});
it('fatals when a wallet account does not support a feature', () => {
const mockAccount = {
address: address('3kjLdPpu1ayfyxRzGFLX352NfKTFL8b4wb8GA8aFxaR3'),
features: ['solana:signTransaction'] as const,
};
expect(() => {
assertWalletAccountSupportsFeatures(
['solana:signTransaction', 'solana:signAndSendTransaction'],
mockAccount,
);
}).toThrow(
new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED, {
accountAddress: mockAccount.address,
featureNames: ['solana:signAndSendTransaction'],
}),
);
});
});

describe('assertWalletSupportsFeatures', () => {
it('does not fatal when a wallet supports all features', () => {
const mockWallet = {
features: { 'solana:signAndSendTransaction': {}, 'solana:signIn': {}, 'solana:signTransaction': {} },
name: 'Mock Wallet',
};
expect(() => {
assertWalletSupportsFeatures(['solana:signAndSendTransaction', 'solana:signIn'], mockWallet);
}).not.toThrow();
});
it('fatals when a wallet does not support a feature', () => {
const mockWallet = {
features: { 'solana:signTransaction': {} },
name: 'Mock Wallet',
};
expect(() => {
assertWalletSupportsFeatures(['solana:signTransaction', 'solana:signAndSendTransaction'], mockWallet);
}).toThrow(
new SolanaError(SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED, {
featureNames: ['solana:signAndSendTransaction'],
walletName: 'Mock Wallet',
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
SolanaSignAndSendTransaction,
SolanaSignAndSendTransactionFeature,
SolanaSignIn,
SolanaSignInFeature,
SolanaSignMessage,
SolanaSignMessageFeature,
SolanaSignTransaction,
SolanaSignTransactionFeature,
} from '@solana/wallet-standard-features';
import { Wallet } from '@wallet-standard/base';

import { assertWalletSupportsFeatures } from '../assertions';

const wallet = null as unknown as Wallet;

assertWalletSupportsFeatures([SolanaSignMessage], wallet);
assertWalletSupportsFeatures([SolanaSignIn], wallet);
wallet.features satisfies SolanaSignMessageFeature;
wallet.features satisfies SolanaSignInFeature;

// @ts-expect-error Not one of the features asserted on
wallet.features satisfies SolanaSignTransactionFeature;

// @ts-expect-error Not one of the features asserted on
wallet.features satisfies SolanaSignAndSendTransactionFeature;

assertWalletSupportsFeatures([SolanaSignTransaction, SolanaSignAndSendTransaction], wallet);
wallet.features satisfies SolanaSignTransactionFeature;
wallet.features satisfies SolanaSignAndSendTransactionFeature;
43 changes: 43 additions & 0 deletions packages/react/src/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED,
SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED,
SolanaError,
} from '@solana/errors';
import { SolanaFeatures } from '@solana/wallet-standard-features';
import { Wallet, WalletAccount, WalletWithFeatures } from '@wallet-standard/base';

type AllSolanaWalletFeatures = UnionToIntersection<SolanaFeatures>;

type FilterKeys<V, K> = { [P in keyof V]: P extends K ? P : never }[keyof V];

type UnionToIntersection<T> = (T extends unknown ? (x: T) => unknown : never) extends (x: infer R) => unknown
? R
: never;

export function assertWalletAccountSupportsFeatures(
featureNames: (keyof AllSolanaWalletFeatures)[],
account: Pick<WalletAccount, 'address' | 'features'>,
): void {
const unsupportedFeatures = featureNames.filter(featureName => !account.features.includes(featureName));
if (unsupportedFeatures.length) {
throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_FEATURE_UNSUPPORTED, {
accountAddress: account.address,
featureNames: unsupportedFeatures,
});
}
}

export function assertWalletSupportsFeatures<TFeatureNames extends (keyof AllSolanaWalletFeatures)[]>(
featureNames: TFeatureNames,
wallet: Pick<Wallet, 'features' | 'name'>,
): asserts wallet is WalletWithFeatures<{
[P in FilterKeys<AllSolanaWalletFeatures, TFeatureNames[number]>]: AllSolanaWalletFeatures[P];
}> {
const unsupportedFeatures = featureNames.filter(featureName => !(featureName in wallet.features));
if (unsupportedFeatures.length) {
throw new SolanaError(SOLANA_ERROR__WALLET__FEATURE_UNSUPPORTED, {
featureNames: unsupportedFeatures,
walletName: wallet.name,
});
}
}