Skip to content

Commit

Permalink
Merge pull request #783 from solana-labs/sign-in
Browse files Browse the repository at this point in the history
Sign In With Solana
  • Loading branch information
jordaaash committed Aug 7, 2023
2 parents 8745f34 + a3d35a1 commit e9be008
Show file tree
Hide file tree
Showing 21 changed files with 222 additions and 108 deletions.
10 changes: 10 additions & 0 deletions .changeset/young-pears-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@solana/wallet-adapter-material-ui-starter': patch
'@solana/wallet-adapter-react-ui-starter': patch
'@solana/wallet-adapter-unsafe-burner': patch
'@solana/wallet-adapter-example': patch
'@solana/wallet-adapter-react': patch
'@solana/wallet-adapter-base': patch
---

Add `signIn` (Sign In With Solana) method
41 changes: 0 additions & 41 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,44 +114,3 @@ This can happen if you try to use `signTransaction`, `signAllTransactions`, or `
The other methods are optional APIs, so you have to feature-detect them before using them.

Please see [issue #72](https://github.com/solana-labs/wallet-adapter/issues/72#issuecomment-919232595).

## How can I sign and verify messages?

Some wallet adapters provide a `signMessage` method for signing arbitrary bytes.

The signature string returned by this method can be verified using [tweetnacl-js](https://github.com/dchest/tweetnacl-js/blob/master/README.md#naclsigndetachedverifymessage-signature-publickey) using the public key from the adapter.

This can be used to sign offline — without sending a transaction — and prove a user controls a given private key.

```tsx
import { ed25519 } from '@noble/curves/ed25519';
import { useWallet } from '@solana/wallet-adapter-react';
import bs58 from 'bs58';
import React, { FC, useCallback } from 'react';

export const SignMessageButton: FC = () => {
const { publicKey, signMessage } = useWallet();

const onClick = useCallback(async () => {
try {
// `publicKey` will be null if the wallet isn't connected
if (!publicKey) throw new Error('Wallet not connected!');
// `signMessage` will be undefined if the wallet doesn't support it
if (!signMessage) throw new Error('Wallet does not support message signing!');

// Encode anything as bytes
const message = new TextEncoder().encode('Hello, world!');
// Sign the bytes using the wallet
const signature = await signMessage(message);
// Verify that the bytes were signed using the private key that matches the known public key
if (!ed25519.verify(signature, message, publicKey.toBytes())) throw new Error('Invalid signature!');

alert(`Message signature: ${bs58.encode(signature)}`);
} catch (error: any) {
alert(`Signing failed: ${error?.message}`);
}
}, [publicKey, signMessage]);

return signMessage ? (<button onClick={onClick} disabled={!publicKey}>Sign Message</button>) : null;
};
```
2 changes: 1 addition & 1 deletion packages/core/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@solana/web3.js": "^1.77.3"
},
"dependencies": {
"@solana/wallet-standard-features": "^1.0.1",
"@solana/wallet-standard-features": "^1.1.0",
"@wallet-standard/base": "^1.0.1",
"@wallet-standard/features": "^1.0.3",
"eventemitter3": "^4.0.7"
Expand Down
8 changes: 6 additions & 2 deletions packages/core/base/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,16 @@ export class WalletSendTransactionError extends WalletError {
name = 'WalletSendTransactionError';
}

export class WalletSignTransactionError extends WalletError {
name = 'WalletSignTransactionError';
}

export class WalletSignMessageError extends WalletError {
name = 'WalletSignMessageError';
}

export class WalletSignTransactionError extends WalletError {
name = 'WalletSignTransactionError';
export class WalletSignInError extends WalletError {
name = 'WalletSignInError';
}

export class WalletTimeoutError extends WalletError {
Expand Down
15 changes: 15 additions & 0 deletions packages/core/base/src/signer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';
import type { Connection, TransactionSignature } from '@solana/web3.js';
import {
BaseWalletAdapter,
Expand Down Expand Up @@ -127,3 +128,17 @@ export abstract class BaseMessageSignerWalletAdapter<Name extends string = strin
{
abstract signMessage(message: Uint8Array): Promise<Uint8Array>;
}

export interface SignInMessageSignerWalletAdapterProps<Name extends string = string> extends WalletAdapterProps<Name> {
signIn(input?: SolanaSignInInput): Promise<SolanaSignInOutput>;
}

export type SignInMessageSignerWalletAdapter<Name extends string = string> = WalletAdapter<Name> &
SignInMessageSignerWalletAdapterProps<Name>;

export abstract class BaseSignInMessageSignerWalletAdapter<Name extends string = string>
extends BaseMessageSignerWalletAdapter<Name>
implements SignInMessageSignerWalletAdapter<Name>
{
abstract signIn(input?: SolanaSignInInput): Promise<SolanaSignInOutput>;
}
3 changes: 2 additions & 1 deletion packages/core/base/src/standard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
SolanaSignAndSendTransaction,
type SolanaSignAndSendTransactionFeature,
type SolanaSignInFeature,
type SolanaSignMessageFeature,
SolanaSignTransaction,
type SolanaSignTransactionFeature,
Expand All @@ -19,7 +20,7 @@ export type WalletAdapterCompatibleStandardWallet = StandardWalletWithFeatures<
StandardConnectFeature &
StandardEventsFeature &
(SolanaSignAndSendTransactionFeature | SolanaSignTransactionFeature) &
(StandardDisconnectFeature | SolanaSignMessageFeature | object)
(StandardDisconnectFeature | SolanaSignMessageFeature | SolanaSignInFeature | object)
>;

export interface StandardWalletAdapterProps<Name extends string = string> extends WalletAdapterProps<Name> {
Expand Down
9 changes: 7 additions & 2 deletions packages/core/base/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { WalletAdapter } from './adapter.js';
import type { MessageSignerWalletAdapter, SignerWalletAdapter } from './signer.js';
import type { MessageSignerWalletAdapter, SignerWalletAdapter, SignInMessageSignerWalletAdapter } from './signer.js';
import type { StandardWalletAdapter } from './standard.js';

export type Adapter = WalletAdapter | SignerWalletAdapter | MessageSignerWalletAdapter | StandardWalletAdapter;
export type Adapter =
| WalletAdapter
| SignerWalletAdapter
| MessageSignerWalletAdapter
| SignInMessageSignerWalletAdapter
| StandardWalletAdapter;

export enum WalletAdapterNetwork {
Mainnet = 'mainnet-beta',
Expand Down
2 changes: 1 addition & 1 deletion packages/core/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"dependencies": {
"@solana-mobile/wallet-adapter-mobile": "^2.0.0",
"@solana/wallet-adapter-base": "workspace:^",
"@solana/wallet-standard-wallet-adapter-react": "^1.0.2"
"@solana/wallet-standard-wallet-adapter-react": "^1.1.0"
},
"devDependencies": {
"@solana/web3.js": "^1.77.3",
Expand Down
42 changes: 19 additions & 23 deletions packages/core/react/src/WalletProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { WalletProviderBase } from './WalletProviderBase.js';
export interface WalletProviderProps {
children: ReactNode;
wallets: Adapter[];
autoConnect?: boolean;
autoConnect?: boolean | ((adapter: Adapter) => Promise<boolean>);
localStorageKey?: string;
onError?: (error: WalletError, adapter?: Adapter) => void;
}
Expand All @@ -37,9 +37,7 @@ function getIsMobile(adapters: Adapter[]) {

function getUriForAppIdentity() {
const location = globalThis.location;
if (location == null) {
return;
}
if (!location) return;
return `${location.protocol}//${location.host}`;
}

Expand Down Expand Up @@ -88,9 +86,7 @@ export function WalletProvider({
);
const changeWallet = useCallback(
(nextWalletName: WalletName<string> | null) => {
if (walletName === nextWalletName) {
return;
}
if (walletName === nextWalletName) return;
if (
adapter &&
// Selecting a wallet other than the mobile wallet adapter is not
Expand All @@ -106,17 +102,11 @@ export function WalletProvider({
[adapter, setWalletName, walletName]
);
useEffect(() => {
if (adapter == null) {
return;
}
if (!adapter) return;
function handleDisconnect() {
if (isUnloadingRef.current) {
return;
}
if (walletName === SolanaMobileWalletAdapterWalletName && getIsMobile(adaptersWithStandardAdapters)) {
// Leave the adapter selected in the event of a disconnection.
return;
}
if (isUnloadingRef.current) return;
// Leave the adapter selected in the event of a disconnection.
if (walletName === SolanaMobileWalletAdapterWalletName && getIsMobile(adaptersWithStandardAdapters)) return;
setWalletName(null);
}
adapter.on('disconnect', handleDisconnect);
Expand All @@ -126,12 +116,18 @@ export function WalletProvider({
}, [adapter, adaptersWithStandardAdapters, setWalletName, walletName]);
const hasUserSelectedAWallet = useRef(false);
const handleAutoConnectRequest = useMemo(() => {
if (autoConnect !== true || !adapter) {
return;
}

return () => (hasUserSelectedAWallet.current ? adapter.connect() : adapter.autoConnect());
}, [adapter, autoConnect]);
if (!autoConnect || !adapter) return;
return async () => {
// If autoConnect is true or returns true, use the default autoConnect behavior.
if (autoConnect === true || (await autoConnect(adapter))) {
if (hasUserSelectedAWallet.current) {
await adapter.connect();
} else {
await adapter.autoConnect();
}
}
};
}, [autoConnect, adapter]);
const isUnloadingRef = useRef(false);
useEffect(() => {
if (walletName === SolanaMobileWalletAdapterWalletName && getIsMobile(adaptersWithStandardAdapters)) {
Expand Down
17 changes: 15 additions & 2 deletions packages/core/react/src/WalletProviderBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type Adapter,
type MessageSignerWalletAdapterProps,
type SignerWalletAdapterProps,
type SignInMessageSignerWalletAdapterProps,
type WalletAdapterProps,
type WalletError,
type WalletName,
Expand Down Expand Up @@ -174,9 +175,9 @@ export function WalletProviderBase({
connected ||
!onAutoConnectRequest ||
!(wallet?.readyState === WalletReadyState.Installed || wallet?.readyState === WalletReadyState.Loadable)
) {
)
return;
}

isConnectingRef.current = true;
setConnecting(true);
didAttemptAutoConnectRef.current = true;
Expand Down Expand Up @@ -239,6 +240,17 @@ export function WalletProviderBase({
[adapter, connected]
);

// Sign in if the wallet supports it
const signIn: SignInMessageSignerWalletAdapterProps['signIn'] | undefined = useMemo(
() =>
adapter && 'signIn' in adapter
? async (input) => {
return await adapter.signIn(input);
}
: undefined,
[adapter]
);

const handleConnect = useCallback(async () => {
if (isConnectingRef.current || isDisconnectingRef.current || wallet?.adapter.connected) return;
if (!wallet) throw handleErrorRef.current(new WalletNotSelectedError());
Expand Down Expand Up @@ -288,6 +300,7 @@ export function WalletProviderBase({
signTransaction,
signAllTransactions,
signMessage,
signIn,
}}
>
{children}
Expand Down
5 changes: 5 additions & 0 deletions packages/core/react/src/useWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type Adapter,
type MessageSignerWalletAdapterProps,
type SignerWalletAdapterProps,
type SignInMessageSignerWalletAdapterProps,
type WalletAdapterProps,
type WalletName,
type WalletReadyState,
Expand Down Expand Up @@ -31,6 +32,7 @@ export interface WalletContextState {
signTransaction: SignerWalletAdapterProps['signTransaction'] | undefined;
signAllTransactions: SignerWalletAdapterProps['signAllTransactions'] | undefined;
signMessage: MessageSignerWalletAdapterProps['signMessage'] | undefined;
signIn: SignInMessageSignerWalletAdapterProps['signIn'] | undefined;
}

const EMPTY_ARRAY: ReadonlyArray<never> = [];
Expand Down Expand Up @@ -61,6 +63,9 @@ const DEFAULT_CONTEXT: Partial<WalletContextState> = {
signMessage() {
return Promise.reject(logMissingProviderError('call', 'signMessage'));
},
signIn() {
return Promise.reject(logMissingProviderError('call', 'signIn'));
},
};
Object.defineProperty(DEFAULT_CONTEXT, 'wallets', {
get() {
Expand Down
2 changes: 2 additions & 0 deletions packages/starter/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"@solana/wallet-adapter-react": "workspace:^",
"@solana/wallet-adapter-react-ui": "workspace:^",
"@solana/wallet-adapter-wallets": "workspace:^",
"@solana/wallet-standard-features": "^1.1.0",
"@solana/wallet-standard-util": "^1.1.0",
"@solana/web3.js": "^1.77.3",
"antd": "^4.24.10",
"bs58": "^4.0.1",
Expand Down
19 changes: 18 additions & 1 deletion packages/starter/example/src/components/ContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { WalletDialogProvider as MaterialUIWalletDialogProvider } from '@solana/
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider as ReactUIWalletModalProvider } from '@solana/wallet-adapter-react-ui';
import { UnsafeBurnerWalletAdapter } from '@solana/wallet-adapter-wallets';
import { type SolanaSignInInput } from '@solana/wallet-standard-features';
import { verifySignIn } from '@solana/wallet-standard-util';
import { clusterApiUrl } from '@solana/web3.js';
import { SnackbarProvider, useSnackbar } from 'notistack';
import type { FC, ReactNode } from 'react';
Expand Down Expand Up @@ -86,9 +88,24 @@ const WalletContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
[enqueueSnackbar]
);

const autoSignIn = useCallback(async (adapter: Adapter) => {
if (!('signIn' in adapter)) return true;

const input: SolanaSignInInput = {
domain: window.location.host,
address: adapter.publicKey ? adapter.publicKey.toBase58() : undefined,
statement: 'Please sign in.',
};
const output = await adapter.signIn(input);

if (!verifySignIn(input, output)) throw new Error('Sign In verification failed!');

return false;
}, []);

return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} onError={onError} autoConnect={autoConnect}>
<WalletProvider wallets={wallets} onError={onError} autoConnect={autoConnect && autoSignIn}>
<MaterialUIWalletDialogProvider>
<AntDesignWalletModalProvider>
<ReactUIWalletModalProvider>{children}</ReactUIWalletModalProvider>
Expand Down
37 changes: 37 additions & 0 deletions packages/starter/example/src/components/SignIn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Button } from '@mui/material';
import { useWallet } from '@solana/wallet-adapter-react';
import type { SolanaSignInInput } from '@solana/wallet-standard-features';
import { verifySignIn } from '@solana/wallet-standard-util';
import bs58 from 'bs58';
import type { FC } from 'react';
import React, { useCallback } from 'react';
import { useNotify } from './notify';

export const SignIn: FC = () => {
const { signIn, publicKey } = useWallet();
const notify = useNotify();

const onClick = useCallback(async () => {
try {
if (!signIn) throw new Error('Wallet does not support Sign In With Solana!');

const input: SolanaSignInInput = {
domain: window.location.host,
address: publicKey ? publicKey.toBase58() : undefined,
statement: 'Please sign in.',
};
const output = await signIn(input);

if (!verifySignIn(input, output)) throw new Error('Sign In verification failed!');
notify('success', `Message signature: ${bs58.encode(output.signature)}`);
} catch (error: any) {
notify('error', `Sign In failed: ${error?.message}`);
}
}, [signIn, publicKey, notify]);

return (
<Button variant="contained" color="secondary" onClick={onClick} disabled={!signIn}>
Sign In
</Button>
);
};
8 changes: 6 additions & 2 deletions packages/starter/example/src/components/SignMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ export const SignMessage: FC = () => {
if (!publicKey) throw new Error('Wallet not connected!');
if (!signMessage) throw new Error('Wallet does not support message signing!');

const message = new TextEncoder().encode('Hello, world!');
const message = new TextEncoder().encode(
`${
window.location.host
} wants you to sign in with your Solana account:\n${publicKey.toBase58()}\n\nPlease sign in.`
);
const signature = await signMessage(message);
if (!ed25519.verify(signature, message, publicKey.toBytes())) throw new Error('Message signature invalid!');

if (!ed25519.verify(signature, message, publicKey.toBytes())) throw new Error('Message signature invalid!');
notify('success', `Message signature: ${bs58.encode(signature)}`);
} catch (error: any) {
notify('error', `Sign Message failed: ${error?.message}`);
Expand Down

0 comments on commit e9be008

Please sign in to comment.