Skip to content

Commit

Permalink
Functions you can use to assert that a group of wallet accounts suppo…
Browse files Browse the repository at this point in the history
…rt a given feature
  • Loading branch information
steveluscher committed Feb 7, 2024
1 parent 82daf8f commit f6a2b5c
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const SOLANA_ERROR__RPC_INTEGER_OVERFLOW = 3 as const;
export const SOLANA_ERROR__CHAIN_NOT_SUPPORTED = 4 as const;
export const SOLANA_ERROR__WALLET_HAS_NO_CONNECTED_ACCOUNTS = 5 as const;
export const SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_CHAIN = 6 as const;
export const SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_FEATURE = 7 as const;
export const SOLANA_ERROR__WALLET_ACCOUNT_DOES_NOT_SUPPORT_FEATURE = 8 as const;
export const SOLANA_ERROR__WALLET_ACCOUNT_NOT_FOUND_IN_WALLET = 9 as const;

/**
Expand All @@ -36,4 +38,6 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__CHAIN_NOT_SUPPORTED
| typeof SOLANA_ERROR__WALLET_HAS_NO_CONNECTED_ACCOUNTS
| typeof SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_CHAIN
| typeof SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_FEATURE
| typeof SOLANA_ERROR__WALLET_ACCOUNT_DOES_NOT_SUPPORT_FEATURE
| typeof SOLANA_ERROR__WALLET_ACCOUNT_NOT_FOUND_IN_WALLET;
10 changes: 10 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import {
SOLANA_ERROR__CHAIN_NOT_SUPPORTED,
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES,
SOLANA_ERROR__WALLET_ACCOUNT_DOES_NOT_SUPPORT_FEATURE,
SOLANA_ERROR__WALLET_ACCOUNT_NOT_FOUND_IN_WALLET,
SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_CHAIN,
SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_FEATURE,
SOLANA_ERROR__WALLET_HAS_NO_CONNECTED_ACCOUNTS,
SolanaErrorCode,
} from './codes';
Expand Down Expand Up @@ -34,6 +36,10 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
path?: string;
value: bigint;
};
[SOLANA_ERROR__WALLET_ACCOUNT_DOES_NOT_SUPPORT_FEATURE]: {
accountAddress: string;
featureNames: readonly `${string}:${string}`[];
};
[SOLANA_ERROR__WALLET_ACCOUNT_NOT_FOUND_IN_WALLET]: {
accountAddress: string;
walletName: string;
Expand All @@ -42,6 +48,10 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
chain: `${string}:${string}`;
walletName: string;
};
[SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_FEATURE]: {
featureNames: readonly `${string}:${string}`[];
walletName: string;
};
[SOLANA_ERROR__WALLET_HAS_NO_CONNECTED_ACCOUNTS]: {
chain: `${string}:${string}`;
walletName: 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 @@ -3,8 +3,10 @@ import {
SOLANA_ERROR__RPC_INTEGER_OVERFLOW,
SOLANA_ERROR__TRANSACTION_MISSING_SIGNATURES,
SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE,
SOLANA_ERROR__WALLET_ACCOUNT_DOES_NOT_SUPPORT_FEATURE,
SOLANA_ERROR__WALLET_ACCOUNT_NOT_FOUND_IN_WALLET,
SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_CHAIN,
SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_FEATURE,
SOLANA_ERROR__WALLET_HAS_NO_CONNECTED_ACCOUNTS,
SolanaErrorCode,
} from './codes';
Expand All @@ -30,10 +32,14 @@ export const SolanaErrorMessages: Readonly<{
[SOLANA_ERROR__TRANSACTION_SIGNATURE_NOT_COMPUTABLE]:
"Could not determine this transaction's signature. Make sure that the transaction has " +
'been signed by its fee payer.',
[SOLANA_ERROR__WALLET_ACCOUNT_DOES_NOT_SUPPORT_FEATURE]:
'The $featureName` feature is not supported by the account `$accountAddress` belonging to the wallet `$walletName`',
[SOLANA_ERROR__WALLET_ACCOUNT_NOT_FOUND_IN_WALLET]:
'No account having address `$accountAddress` could be found in the wallet `$walletName}`',
[SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_CHAIN]:
"The wallet '$walletName' does not support connecting to the chain `$chain`",
[SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_FEATURE]:
'The `$featureName` feature is not supported by the wallet `$walletName`',
[SOLANA_ERROR__WALLET_HAS_NO_CONNECTED_ACCOUNTS]:
"The wallet '$walletName' has no connected accounts for the chain `$chain`",
};
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_DOES_NOT_SUPPORT_FEATURE,
SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_FEATURE,
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_DOES_NOT_SUPPORT_FEATURE, {
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_DOES_NOT_SUPPORT_FEATURE, {
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_DOES_NOT_SUPPORT_FEATURE,
SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_FEATURE,
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_DOES_NOT_SUPPORT_FEATURE, {
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_DOES_NOT_SUPPORT_FEATURE, {
featureNames: unsupportedFeatures,
walletName: wallet.name,
});
}
}

0 comments on commit f6a2b5c

Please sign in to comment.