Skip to content

Commit

Permalink
A useAccount hook that throws if a wallet does not have an account …
Browse files Browse the repository at this point in the history
…matching a given address
  • Loading branch information
steveluscher committed Apr 18, 2024
1 parent 2dffcb0 commit fc6bcfb
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 5 deletions.
12 changes: 7 additions & 5 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,11 @@ 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__CHAIN_UNSUPPORTED = 7718000 as const;
export const SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS = 7718001 as const;
export const SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN = 7718002 as const;
export const SOLANA_ERROR__WALLET__INVALID_SOLANA_CHAIN = 7718003 as const;
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;

// Codec-related errors.
// Reserve error codes in the range [8078000-8078999].
Expand Down Expand Up @@ -538,7 +539,8 @@ 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_NOT_FOUND
| typeof SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED
| typeof SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN
| typeof SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS
| typeof SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN
| typeof SOLANA_ERROR__WALLET__INVALID_SOLANA_CHAIN;
5 changes: 5 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ 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_NOT_FOUND,
SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED,
SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS,
SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN,
Expand Down Expand Up @@ -582,6 +583,10 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
[SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE]: {
actualVersion: number;
};
[SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND]: {
accountAddress: string;
walletName: string;
};
[SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED]: {
chain: `${string}:${string}`;
walletName: string;
Expand Down
3 changes: 3 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ 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_NOT_FOUND,
SOLANA_ERROR__WALLET__CHAIN_UNSUPPORTED,
SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS,
SOLANA_ERROR__WALLET__EXPECTED_CONNECTED_ACCOUNTS_FOR_CHAIN,
Expand Down Expand Up @@ -593,6 +594,8 @@ 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_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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { address } from '@solana/addresses';
import { SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND, SolanaError } from '@solana/errors';
import { SOLANA_CHAINS } from '@solana/wallet-standard-chains';
import { Wallet } from '@wallet-standard/base';

import { renderHook } from '../test-renderer';
import { useWalletAccountForCluster_INTERNAL_ONLY_DO_NOT_EXPORT } from '../useWalletAccountForCluster_INTERNAL_ONLY_DO_NOT_EXPORT';

describe('useWalletAccountForCluster_INTERNAL_ONLY_DO_NOT_EXPORT', () => {
let mockWallet: Wallet;
beforeEach(() => {
mockWallet = {
accounts: [
{
address: address('Httx5rAMNW3zA6NtXbgpnq22RdS9qK6rRBiNi8Msoc8a'),
chains: ['solana:devnet'],
features: ['solana:signMessage', 'solana:signAndSendTransaction'],
icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=',
label: 'My Test Account',
publicKey: new Uint8Array([
251, 6, 90, 16, 167, 85, 10, 206, 169, 88, 60, 180, 238, 49, 109, 108, 152, 101, 243, 178, 93,
190, 195, 73, 206, 97, 76, 131, 200, 38, 175, 179,
]),
},
{
address: address('6hNHHnX1Mas5TLBZoVm6dcH4oXTemB1Njxr6hiSks8PQ'),
chains: ['solana:testnet'],
features: ['solana:signMessage', 'solana:signAndSendTransaction'],
icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=',
label: 'My Test Account',
publicKey: new Uint8Array([
84, 161, 186, 181, 101, 79, 164, 30, 119, 128, 137, 78, 165, 169, 15, 231, 112, 143, 114, 249,
10, 66, 139, 186, 56, 119, 12, 205, 158, 28, 106, 111,
]),
},
],
chains: SOLANA_CHAINS,
features: {},
icon: 'data:image/svg+xml;base64,ABC',
name: 'Mock Wallet',
version: '1.0.0',
};
// 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('returns the account matching the given address and cluster', () => {
const { result } = renderHook(() =>
useWalletAccountForCluster_INTERNAL_ONLY_DO_NOT_EXPORT(
mockWallet,
address('Httx5rAMNW3zA6NtXbgpnq22RdS9qK6rRBiNi8Msoc8a'),
'devnet',
),
);
expect(result.current).toBe(mockWallet.accounts[0]);
});
it('fatals when a wallet has no account matching the given address and cluster', () => {
const { result } = renderHook(() =>
useWalletAccountForCluster_INTERNAL_ONLY_DO_NOT_EXPORT(
mockWallet,
address('6bDQKLGyVpAUzhZa8jDvKbAPPs33ESMdTAjN4HX5PEVu'),
'devnet',
),
);
expect(result.__type).toBe('error');
expect(result.current).toEqual(
new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND, {
accountAddress: '6bDQKLGyVpAUzhZa8jDvKbAPPs33ESMdTAjN4HX5PEVu',
walletName: 'Mock Wallet',
}),
);
});
it('fatals when a wallet has an account matching the given address but not the cluster', () => {
const { result } = renderHook(() =>
useWalletAccountForCluster_INTERNAL_ONLY_DO_NOT_EXPORT(
mockWallet,
address('6bDQKLGyVpAUzhZa8jDvKbAPPs33ESMdTAjN4HX5PEVu'),
'testnet',
),
);
expect(result.__type).toBe('error');
expect(result.current).toEqual(
new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND, {
accountAddress: '6bDQKLGyVpAUzhZa8jDvKbAPPs33ESMdTAjN4HX5PEVu',
walletName: 'Mock Wallet',
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { address } from '@solana/addresses';
import { SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND, SolanaError } from '@solana/errors';
import { SOLANA_CHAINS } from '@solana/wallet-standard-chains';
import { Wallet } from '@wallet-standard/base';

import { renderHook } from '../test-renderer';
import { useWalletAccount_INTERNAL_ONLY_DO_NOT_EXPORT } from '../useWalletAccount_INTERNAL_ONLY_DO_NOT_EXPORT';

describe('useWalletAccount_INTERNAL_ONLY_DO_NOT_EXPORT', () => {
let mockWallet: Wallet;
beforeEach(() => {
mockWallet = {
accounts: [
{
address: address('Httx5rAMNW3zA6NtXbgpnq22RdS9qK6rRBiNi8Msoc8a'),
chains: ['solana:devnet'],
features: ['solana:signMessage', 'solana:signAndSendTransaction'],
icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=',
label: 'My Test Account',
publicKey: new Uint8Array([
251, 6, 90, 16, 167, 85, 10, 206, 169, 88, 60, 180, 238, 49, 109, 108, 152, 101, 243, 178, 93,
190, 195, 73, 206, 97, 76, 131, 200, 38, 175, 179,
]),
},
],
chains: SOLANA_CHAINS,
features: {},
icon: 'data:image/svg+xml;base64,ABC',
name: 'Mock Wallet',
version: '1.0.0',
};
// 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('returns the account matching the given address', () => {
const { result } = renderHook(() =>
useWalletAccount_INTERNAL_ONLY_DO_NOT_EXPORT(
mockWallet,
address('Httx5rAMNW3zA6NtXbgpnq22RdS9qK6rRBiNi8Msoc8a'),
),
);
expect(result.current).toBe(mockWallet.accounts[0]);
});
it('fatals when a wallet has no account matching the given address', () => {
const { result } = renderHook(() =>
useWalletAccount_INTERNAL_ONLY_DO_NOT_EXPORT(
mockWallet,
address('6bDQKLGyVpAUzhZa8jDvKbAPPs33ESMdTAjN4HX5PEVu'),
),
);
expect(result.__type).toBe('error');
expect(result.current).toEqual(
new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND, {
accountAddress: '6bDQKLGyVpAUzhZa8jDvKbAPPs33ESMdTAjN4HX5PEVu',
walletName: 'Mock Wallet',
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Address } from '@solana/addresses';
import { SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND, SolanaError } from '@solana/errors';
import { SolanaChain } from '@solana/wallet-standard-chains';
import { Wallet, WalletAccount } from '@wallet-standard/base';

import { ChainToCluster } from './chain';
import { useWalletAccountsForCluster_INTERNAL_ONLY_DO_NOT_EXPORT } from './useWalletAccountsForCluster_INTERNAL_ONLY_DO_NOT_EXPORT';

export function useWalletAccountForCluster_INTERNAL_ONLY_DO_NOT_EXPORT<TWallet extends Wallet>(
wallet: TWallet,
address: Address,
cluster: ChainToCluster<SolanaChain & TWallet['chains'][number]>,
): WalletAccount {
const accounts = useWalletAccountsForCluster_INTERNAL_ONLY_DO_NOT_EXPORT(wallet, cluster);
const account = accounts.find(account => account.address === address);
if (!account) {
throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND, {
accountAddress: address,
walletName: wallet.name,
});
}
return account;
}
20 changes: 20 additions & 0 deletions packages/react/src/useWalletAccount_INTERNAL_ONLY_DO_NOT_EXPORT.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Address } from '@solana/addresses';
import { SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND, SolanaError } from '@solana/errors';
import { Wallet, WalletAccount } from '@wallet-standard/base';

import { useWalletAccounts_INTERNAL_ONLY_DO_NOT_EXPORT } from './useWalletAccounts_INTERNAL_ONLY_DO_NOT_EXPORT';

export function useWalletAccount_INTERNAL_ONLY_DO_NOT_EXPORT<TWallet extends Wallet>(
wallet: TWallet,
address: Address,
): WalletAccount {
const accounts = useWalletAccounts_INTERNAL_ONLY_DO_NOT_EXPORT(wallet);
const account = accounts.find(account => account.address === address);
if (!account) {
throw new SolanaError(SOLANA_ERROR__WALLET__ACCOUNT_NOT_FOUND, {
accountAddress: address,
walletName: wallet.name,
});
}
return account;
}

0 comments on commit fc6bcfb

Please sign in to comment.