Skip to content

Commit

Permalink
A generic hook for fetching concrete features for wallet accounts (#55)
Browse files Browse the repository at this point in the history
Given a `UiWalletAccount` and the name of a feature, this hook returns the feature object from the underlying Wallet Standard `Wallet`. This is a specialization of `useWalletFeature()` 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.
  • Loading branch information
steveluscher authored Jun 7, 2024
2 parents fb5ba28 + b7e79c9 commit c88d687
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-hounds-repeat.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions packages/core/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
15 changes: 15 additions & 0 deletions packages/core/errors/src/context.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -16,6 +18,19 @@ type DefaultUnspecifiedErrorContextToUndefined<T> = {
* - 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[];
Expand Down
6 changes: 6 additions & 0 deletions packages/core/errors/src/messages.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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]:
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
44 changes: 44 additions & 0 deletions packages/ui/features/src/__tests__/getWalletAccountFeature-test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
30 changes: 30 additions & 0 deletions packages/ui/features/src/getWalletAccountFeature.ts
Original file line number Diff line number Diff line change
@@ -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<TWalletAccount extends UiWalletAccount>(
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);
}
1 change: 1 addition & 0 deletions packages/ui/features/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './getWalletAccountFeature.js';
export * from './getWalletFeature.js';

0 comments on commit c88d687

Please sign in to comment.