diff --git a/.changeset/cyan-hounds-repeat.md b/.changeset/cyan-hounds-repeat.md new file mode 100644 index 00000000..7e345ebd --- /dev/null +++ b/.changeset/cyan-hounds-repeat.md @@ -0,0 +1,5 @@ +--- +'@wallet-standard/ui-features': patch +--- + +A specialized function that fetches the underlying feature object from a `UiWalletAccount`, ensuring that both the wallet _and_ the account indicate support for that feature. diff --git a/packages/core/errors/src/codes.ts b/packages/core/errors/src/codes.ts index b4b14776..a7997076 100644 --- a/packages/core/errors/src/codes.ts +++ b/packages/core/errors/src/codes.ts @@ -31,6 +31,8 @@ export const WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND = 3834001 // Feature-related errors. // Reserve error codes in the range [6160000-6160999]. +export const WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED = 6160000 as const; +export const WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED = 6160001 as const; export const WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED = 6160002 as const; /** @@ -50,6 +52,8 @@ export const WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED = 616 * https://stackoverflow.com/a/28818850 */ export type WalletStandardErrorCode = + | typeof WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED + | typeof WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED | typeof WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED | typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND | typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND; diff --git a/packages/core/errors/src/context.ts b/packages/core/errors/src/context.ts index a3a0f9e5..8daee405 100644 --- a/packages/core/errors/src/context.ts +++ b/packages/core/errors/src/context.ts @@ -1,4 +1,6 @@ import type { + WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED, + WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED, WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND, WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED, WalletStandardErrorCode, @@ -16,6 +18,19 @@ type DefaultUnspecifiedErrorContextToUndefined = { * - Don't change or remove members of an error's context. */ export type WalletStandardErrorContext = DefaultUnspecifiedErrorContextToUndefined<{ + [WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED]: { + address: string; + chain: string; + featureName: string; + supportedChains: string[]; + supportedFeatures: string[]; + }; + [WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED]: { + address: string; + featureName: string; + supportedChains: string[]; + supportedFeatures: string[]; + }; [WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED]: { featureName: string; supportedFeatures: string[]; diff --git a/packages/core/errors/src/messages.ts b/packages/core/errors/src/messages.ts index 557eaec0..2dbc31de 100644 --- a/packages/core/errors/src/messages.ts +++ b/packages/core/errors/src/messages.ts @@ -1,5 +1,7 @@ import type { WalletStandardErrorCode } from './codes.js'; import { + WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED, + WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED, WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED, WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND, WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND, @@ -17,6 +19,10 @@ export const WalletStandardErrorMessages: Readonly<{ // TypeScript will fail to build this project if add an error code without a message. [P in WalletStandardErrorCode]: string; }> = { + [WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED]: + 'The wallet account $address does not support the chain `$chain`', + [WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED]: + 'The wallet account $address does not support the `$featureName` feature', [WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED]: "The wallet '$walletName' does not support the `$featureName` feature", [WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND]: diff --git a/packages/ui/features/README.md b/packages/ui/features/README.md index 5bf9f964..211e2df8 100644 --- a/packages/ui/features/README.md +++ b/packages/ui/features/README.md @@ -33,3 +33,7 @@ function App() { ); } ``` + +### `getWalletAccountFeature(uiWalletAccount, featureName)` + +Given a `UiWalletAccount` and the name of a feature, this function returns the feature object from the underlying Wallet Standard `Wallet`. This is a specialization of `getWalletFeature()` that takes into consideration that the features supported by a wallet might not be supported by every account in that wallet. In the event that the wallet or account does not support the feature, a `WalletStandardError` will be thrown. diff --git a/packages/ui/features/src/__tests__/getWalletAccountFeature-test.ts b/packages/ui/features/src/__tests__/getWalletAccountFeature-test.ts new file mode 100644 index 00000000..3150740c --- /dev/null +++ b/packages/ui/features/src/__tests__/getWalletAccountFeature-test.ts @@ -0,0 +1,44 @@ +import { + WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED, + WalletStandardError, +} from '@wallet-standard/errors'; +import type { UiWalletAccount } from '@wallet-standard/ui-core'; + +import { getWalletAccountFeature } from '../getWalletAccountFeature.js'; +import { getWalletFeature } from '../getWalletFeature.js'; + +jest.mock('../getWalletFeature.js'); + +describe('getWalletAccountFeature', () => { + let mockWalletAccount: UiWalletAccount; + beforeEach(() => { + mockWalletAccount = { + '~uiWalletHandle': Symbol() as UiWalletAccount['~uiWalletHandle'], + address: 'abc', + chains: ['solana:mainnet'], + features: ['feature:a'], + publicKey: new Uint8Array([1, 2, 3]), + }; + // Suppresses console output when an `ErrorBoundary` is hit. + // See https://stackoverflow.com/a/72632884/802047 + jest.spyOn(console, 'error').mockImplementation(); + jest.spyOn(console, 'warn').mockImplementation(); + }); + it('throws if the account does not support the feature requested', () => { + expect(() => { + getWalletAccountFeature(mockWalletAccount, 'feature:b'); + }).toThrow( + new WalletStandardError(WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED, { + address: 'abc', + supportedChains: ['solana:mainnet'], + supportedFeatures: ['feature:a'], + featureName: 'feature:b', + }) + ); + }); + it('returns the feature of the underlying wallet', () => { + const mockFeature = {}; + jest.mocked(getWalletFeature).mockReturnValue(mockFeature); + expect(getWalletAccountFeature(mockWalletAccount, 'feature:a')).toBe(mockFeature); + }); +}); diff --git a/packages/ui/features/src/getWalletAccountFeature.ts b/packages/ui/features/src/getWalletAccountFeature.ts new file mode 100644 index 00000000..68949801 --- /dev/null +++ b/packages/ui/features/src/getWalletAccountFeature.ts @@ -0,0 +1,30 @@ +import { + WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED, + WalletStandardError, + safeCaptureStackTrace, +} from '@wallet-standard/errors'; +import type { UiWalletAccount } from '@wallet-standard/ui-core'; + +import { getWalletFeature } from './getWalletFeature.js'; + +/** + * Returns the feature object from the Wallet Standard `Wallet` that underlies a + * `UiWalletAccount`. In the event that either the wallet or the account do not support the + * feature, a `WalletStandardError` will be thrown. + */ +export function getWalletAccountFeature( + walletAccount: TWalletAccount, + featureName: TWalletAccount['features'][number] +): unknown { + if (!walletAccount.features.includes(featureName)) { + const err = new WalletStandardError(WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED, { + address: walletAccount.address, + featureName, + supportedChains: [...walletAccount.chains], + supportedFeatures: [...walletAccount.features], + }); + safeCaptureStackTrace(err, getWalletAccountFeature); + throw err; + } + return getWalletFeature(walletAccount, featureName); +} diff --git a/packages/ui/features/src/index.ts b/packages/ui/features/src/index.ts index 3e380b5f..f5e97aa4 100644 --- a/packages/ui/features/src/index.ts +++ b/packages/ui/features/src/index.ts @@ -1 +1,2 @@ +export * from './getWalletAccountFeature.js'; export * from './getWalletFeature.js';