Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sign In With Solana #783

Merged
merged 26 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
160034b
add sign in to base package
jordaaash Jun 22, 2023
34d2cea
add sign in to burner wallet
jordaaash Jun 22, 2023
22d662c
add sign in to react package
jordaaash Jun 22, 2023
e25f9db
add sign in to example package
jordaaash Jun 22, 2023
23b965e
add FIXME to faq
jordaaash Jun 22, 2023
c393bd4
update lockfile
jordaaash Jun 22, 2023
0927f14
add WalletSignInError
jordaaash Jun 22, 2023
06bda69
use latest alpha of @solana/wallet-standard packages
jordaaash Jun 22, 2023
06b8e05
Merge branch 'master' into sign-in
jordaaash Jul 3, 2023
686ce15
remove message verification from example
jordaaash Jul 3, 2023
96c48b4
remove sign and verify messages from FAQ
jordaaash Jul 3, 2023
a45b77f
update to latest @solana/wallet-standard alpha versions
jordaaash Jul 3, 2023
be3b932
add autoConnect callback function
jordaaash Jul 4, 2023
cad037c
shorten simple return statements
jordaaash Jul 4, 2023
efbaa03
use autoConnect to do asynchronous auto sign in and verify
jordaaash Jul 4, 2023
448a7de
update lockfile
jordaaash Jul 4, 2023
1bff7e9
adjust error message
jordaaash Jul 4, 2023
ff53139
Merge branch 'master' into sign-in
jordaaash Jul 20, 2023
f5b968f
reconcile lockfile
jordaaash Jul 20, 2023
a9d4f51
update to latest @solana/wallet-standard alpha versions
jordaaash Aug 5, 2023
fd40090
update to latest @solana/wallet-standard alpha versions
jordaaash Aug 7, 2023
81d156f
verify signed messages, use SIWS format
jordaaash Aug 7, 2023
aef5453
add domain, address, and statement to example SIWS
jordaaash Aug 7, 2023
9cbc406
use production @solana/wallet-standard packages
jordaaash Aug 7, 2023
f5bf12d
lint fixes
jordaaash Aug 7, 2023
a3d35a1
add changeset
jordaaash Aug 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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