Skip to content

Commit

Permalink
A useSignMessage hook for the new wallet adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Feb 8, 2024
1 parent 14e378f commit c9a99fa
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 0 deletions.
113 changes: 113 additions & 0 deletions packages/react/src/__tests__/useSignMessageFeature-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 { SOLANA_CHAINS } from '@solana/wallet-standard-chains';
import { SolanaSignMessage } from '@solana/wallet-standard-features';
import { Wallet } from '@wallet-standard/base';

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

describe('useSignMessageFeature', () => {
let mockSignMessage: jest.Mock;
let mockWallet: Wallet;
beforeEach(() => {
mockSignMessage = jest.fn();
mockWallet = {
accounts: [
{
address: address('Httx5rAMNW3zA6NtXbgpnq22RdS9qK6rRBiNi8Msoc8a'),
chains: ['solana:devnet'],
features: [],
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('Bho2jw9KVthJ4eXHu91gFT4pmTHWBP8kXnB1DkxvB9gx'),
chains: ['solana:devnet', 'solana:mainnet', 'solana:localnet', 'solana:testnet'],
features: ['solana:signMessage'],
icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=',
label: 'My Personal Account',
publicKey: new Uint8Array([
159, 8, 37, 221, 244, 25, 37, 131, 55, 40, 233, 211, 111, 235, 4, 250, 61, 170, 129, 95, 102,
117, 14, 137, 115, 154, 196, 5, 68, 224, 212, 45,
]),
},
],
chains: SOLANA_CHAINS,
features: {
[SolanaSignMessage]: {
signMessage: mockSignMessage,
version: '1.1.0',
},
},
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 a function that proxies calls to the wallet's `signMessage` function", () => {
const { result } = renderHook(() =>
useSignMessageFeature(mockWallet, address('Bho2jw9KVthJ4eXHu91gFT4pmTHWBP8kXnB1DkxvB9gx'), 'devnet'),
);
expect(result.__type).toBe('result');
const signMessages = result.current as NonNullable<Extract<typeof result, { __type: 'result' }>['current']>;
const messageA = new Uint8Array([1, 2, 3]);
const messageB = new Uint8Array([4, 5, 6]);
signMessages(messageA, messageB);
expect(mockSignMessage).toHaveBeenCalledWith(
{ account: mockWallet.accounts[1], message: messageA },
{ account: mockWallet.accounts[1], message: messageB },
);
});
it("returns a function that returns the result of the wallet's `signMessage` function", async () => {
expect.assertions(2);
const mockSignMessageResult = [new Uint8Array([100, 100, 100])] as readonly Uint8Array[];
mockSignMessage.mockResolvedValue(mockSignMessageResult);
const { result } = renderHook(() =>
useSignMessageFeature(mockWallet, address('Bho2jw9KVthJ4eXHu91gFT4pmTHWBP8kXnB1DkxvB9gx'), 'devnet'),
);
expect(result.__type).toBe('result');
const signMessages = result.current as NonNullable<Extract<typeof result, { __type: 'result' }>['current']>;
await expect(signMessages(new Uint8Array([1, 2, 3]))).resolves.toBe(mockSignMessageResult);
});
it('fatals when passed a wallet that does not support the `SolanaSignMessage` feature', () => {
const { result } = renderHook(() =>
useSignMessageFeature(
{ ...mockWallet, features: {} },
address('Bho2jw9KVthJ4eXHu91gFT4pmTHWBP8kXnB1DkxvB9gx'),
'devnet',
),
);
expect(result.__type).toBe('error');
expect(result.current).toEqual(
new SolanaError(SOLANA_ERROR__WALLET_DOES_NOT_SUPPORT_FEATURE, {
featureNames: [SolanaSignMessage],
walletName: mockWallet.name,
}),
);
});
it('fatals when passed an address of an account that does not support the `SolanaSignMessage` feature', () => {
const { result } = renderHook(() =>
useSignMessageFeature(mockWallet, address('Httx5rAMNW3zA6NtXbgpnq22RdS9qK6rRBiNi8Msoc8a'), 'devnet'),
);
expect(result.__type).toBe('error');
expect(result.current).toEqual(
new SolanaError(SOLANA_ERROR__WALLET_ACCOUNT_DOES_NOT_SUPPORT_FEATURE, {
accountAddress: 'Httx5rAMNW3zA6NtXbgpnq22RdS9qK6rRBiNi8Msoc8a',
featureNames: [SolanaSignMessage],
}),
);
});
});
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './useSignMessageFeature';
export * from './useWalletAccounts';
20 changes: 20 additions & 0 deletions packages/react/src/useSignMessageFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Address } from '@solana/addresses';
import { SolanaChain } from '@solana/wallet-standard-chains';
import { SolanaSignMessage, SolanaSignMessageInput, SolanaSignMessageOutput } from '@solana/wallet-standard-features';
import { Wallet } from '@wallet-standard/base';

import { assertWalletAccountSupportsFeatures, assertWalletSupportsFeatures } from './assertions';
import { ChainToCluster } from './chain';
import { useWalletAccount_INTERNAL_ONLY_DO_NOT_EXPORT } from './wallet-accounts-internal';

export function useSignMessageFeature<TWallet extends Wallet>(
wallet: TWallet,
address: Address,
cluster: ChainToCluster<TWallet['chains'][number] & SolanaChain>,
): (...messages: readonly SolanaSignMessageInput['message'][]) => Promise<readonly SolanaSignMessageOutput[]> {
assertWalletSupportsFeatures([SolanaSignMessage], wallet);
const account = useWalletAccount_INTERNAL_ONLY_DO_NOT_EXPORT(wallet, address, cluster);
assertWalletAccountSupportsFeatures([SolanaSignMessage], account);
const { signMessage } = wallet.features[SolanaSignMessage];
return (...messages) => signMessage(...messages.map(message => ({ account, message })));
}

0 comments on commit c9a99fa

Please sign in to comment.