Skip to content

Commit

Permalink
React hooks you can use to obtain @solana/signers types from Wallet…
Browse files Browse the repository at this point in the history
… Standard wallet accounts (#2795)

# Summary

This is the big one; the hooks that most applications should use when dealing with wallets.

1. Select a wallet account, using the `@wallet-standard/react` hooks
2. Create a signer out of that wallet account
3. Create a transaction message with that signer
4. Pass it to `signAndSendTransactionMessageWithSigners()`
5. ???
6. Success

# Notes

* I made all of the `*Signer` types as conservative as possible, meaning that they presume the _maximum_ wallet interference with messages and transactions. Because an application can not guarantee that a wallet won't modify a transaction or message, I've used the `MessageModifying` and `TransactionModifying` variants of the `@solana/signers` types.

# Example

```tsx
const signer = useWalletAccountTransactionSendingSigner(walletAccount);
function onDoTransaction() {
  const message = pipe(
    createTransaction({ version: 0 }),
    m => setTransactionMessageFeePayerSigner(signer, m),
    m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
    m => addTransactionMessageInstruction(
      getTransferSolInstruction({
        amount: lamports(1_000_000_000),
        destination: address('...'),
        source: signer,
      }),
      m,
    ),
  );
  await signAndSendTransactionMessageWithSigners(message);
}
```
  • Loading branch information
steveluscher committed Jun 13, 2024
1 parent 8fe4551 commit ce876d9
Show file tree
Hide file tree
Showing 16 changed files with 1,069 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-kids-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/react': patch
---

Added React hooks to which you can pass a Wallet Standard `UiWalletAccount` and obtain a `MessageModifyingSigner`, `TransactionModifyingSigner`, or `TransactionSendingSigner` for use in constructing, signing, and sending Solana transactions and messages
2 changes: 2 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export const SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_PARTIAL_SIGNER = 5508007
export const SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_SENDING_SIGNER = 5508008 as const;
export const SOLANA_ERROR__SIGNER__TRANSACTION_CANNOT_HAVE_MULTIPLE_SENDING_SIGNERS = 5508009 as const;
export const SOLANA_ERROR__SIGNER__TRANSACTION_SENDING_SIGNER_MISSING = 5508010 as const;
export const SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED = 5508011 as const;

// Transaction-related errors.
// Reserve error codes in the range [5663000-5663999].
Expand Down Expand Up @@ -468,6 +469,7 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_SIGNER
| typeof SOLANA_ERROR__SIGNER__TRANSACTION_CANNOT_HAVE_MULTIPLE_SENDING_SIGNERS
| typeof SOLANA_ERROR__SIGNER__TRANSACTION_SENDING_SIGNER_MISSING
| typeof SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED
| typeof SOLANA_ERROR__SUBTLE_CRYPTO__DIGEST_UNIMPLEMENTED
| typeof SOLANA_ERROR__SUBTLE_CRYPTO__DISALLOWED_IN_INSECURE_CONTEXT
| typeof SOLANA_ERROR__SUBTLE_CRYPTO__ED25519_ALGORITHM_UNIMPLEMENTED
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 @@ -154,6 +154,7 @@ import {
SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_SIGNER,
SOLANA_ERROR__SIGNER__TRANSACTION_CANNOT_HAVE_MULTIPLE_SENDING_SIGNERS,
SOLANA_ERROR__SIGNER__TRANSACTION_SENDING_SIGNER_MISSING,
SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED,
SOLANA_ERROR__SUBTLE_CRYPTO__DIGEST_UNIMPLEMENTED,
SOLANA_ERROR__SUBTLE_CRYPTO__DISALLOWED_IN_INSECURE_CONTEXT,
SOLANA_ERROR__SUBTLE_CRYPTO__ED25519_ALGORITHM_UNIMPLEMENTED,
Expand Down Expand Up @@ -481,6 +482,8 @@ export const SolanaErrorMessages: Readonly<{
[SOLANA_ERROR__SIGNER__TRANSACTION_SENDING_SIGNER_MISSING]:
'No `TransactionSendingSigner` was identified. Please provide a valid ' +
'`ITransactionWithSingleSendingSigner` transaction.',
[SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED]:
'Wallet account signers do not support signing multiple messages/transactions in a single operation',
[SOLANA_ERROR__SUBTLE_CRYPTO__DIGEST_UNIMPLEMENTED]: 'No digest implementation could be found.',
[SOLANA_ERROR__SUBTLE_CRYPTO__DISALLOWED_IN_INSECURE_CONTEXT]:
'Cryptographic operations are only allowed in secure browser contexts. Read more ' +
Expand Down
127 changes: 118 additions & 9 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,124 @@ This package contains React hooks for building Solana apps.

## Hooks

### `useWalletAccountMessageSigner(uiWalletAccount)`

Given a `UiWalletAccount`, this hook returns an object that conforms to the `MessageModifyingSigner` interface of `@solana/signers`.

#### Example

```tsx
import { useWalletAccountMessageSigner } from '@solana/react';
import { createSignableMessage } from '@solana/signers';

function SignMessageButton({ account, text }) {
const messageSigner = useWalletAccountMessageSigner(account);
return (
<button
onClick={async () => {
try {
const signableMessage = createSignableMessage(text);
const [signedMessage] = await messageSigner.modifyAndSignMessages([signableMessage]);
const messageWasModified = signableMessage.content !== signedMessage.content;
const signatureBytes = signedMessage.signatures[messageSigner.address];
window.alert(
`Signature bytes: ${signatureBytes.toString()}${
messageWasModified ? ' (message was modified)' : ''
}`,
);
} catch (e) {
console.error('Failed to sign message', e);
}
}}
>
Sign Message: {text}
</button>
);
}
```

> [!NOTE]
> The type `MessageModifyingSigner` is returned from this hook instead of `MessageSigner` or `MessagePartialSigner`. This is a conservative assumption based on the fact that your application can not control whether or not the wallet will modify the message before signing it.
### `useWalletAccountTransactionSigner(uiWalletAccount, chain)`

Given a `UiWalletAccount` and a chain that begins with `solana:`, this hook returns an object that conforms to the `TransactionModifyingSigner` interface of `@solana/signers`.

#### Example

```tsx
import { useWalletAccountTransactionSigner } from '@solana/react';

function SignTransactionButton({ account, transaction }) {
const transactionSigner = useWalletAccountTransactionSigner(account, 'solana:devnet');
return (
<button
onClick={async () => {
try {
const [{ signatures }] = await transactionSigner.modifyAndSignTransactions([transaction]);
const signatureBytes = signatures[transactionSigner.address];
window.alert(`Signature bytes: ${signatureBytes.toString()}`);
} catch (e) {
console.error('Failed to sign transaction', e);
}
}}
>
Sign Transaction
</button>
);
}
```

> [!NOTE]
> The type `TransactionModifyingSigner` is returned from this hook instead of `TransactionSigner` or `TransactionPartialSigner`. This is a conservative assumption based on the fact that your application can not control whether or not the wallet will modify the transaction before signing it (eg. to add guard instructions, or a priority fee budget).
### `useWalletAccountTransactionSendingSigner(uiWalletAccount, chain)`

Given a `UiWalletAccount` and a chain that begins with `solana:`, this hook returns an object that conforms to the `TransactionSendingSigner` interface of `@solana/signers`.

#### Example

```tsx
import { useWalletAccountTransactionSendingSigner } from '@solana/react';
import {
appendTransactionMessageInstruction,
createSolanaRpc,
getBase58Decoder,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signAndSendTransactionMessageWithSigners,
} from '@solana/web3.js';

function RecordMemoButton({ account, rpc, text }) {
const signer = useWalletAccountTransactionSendingSigner(account, 'solana:devnet');
return (
<button
onClick={async () => {
try {
const { value: latestBlockhash } = await createSolanaRpc('https://api.devnet.solana.com')
.getLatestBlockhash()
.send();
const message = pipe(
createTransactionMessage({ version: 'legacy' }),
m => setTransactionMessageFeePayerSigner(signer, m),
m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
m => appendTransactionMessageInstruction(getAddMemoInstruction({ memo: text }), m),
);
const signatureBytes = await signAndSendTransactionMessageWithSigners(message);
const base58Signature = getBase58Decoder().decode(signature);
window.alert(`View transaction: https://explorer.solana.com/tx/${base58Signature}?cluster=devnet`);
} catch (e) {
console.error('Failed to record memo', e);
}
}}
>
Record Memo
</button>
);
}
```

### `useSignMessage(uiWalletAccount)`

Given a `UiWalletAccount`, this hook returns a function you can call to sign a byte array.
Expand Down Expand Up @@ -61,9 +179,6 @@ function SignMessageButton({ account, messageBytes }) {
}
```

> [!NOTE]
> There exists a plural version of this hook &ndash; `useSignMessages` &ndash; that accepts multiple inputs and returns an array of outputs.
### `useSignTransaction(uiWalletAccount, chain)`

Given a `UiWalletAccount` and a chain that begins with `solana:`, this hook returns a function you can call to sign a serialized transaction.
Expand Down Expand Up @@ -108,9 +223,6 @@ function SignTransactionButton({ account, transactionBytes }) {
}
```

> [!NOTE]
> There exists a plural version of this hook &ndash; `useSignTransactions` &ndash; that accepts multiple inputs and returns an array of outputs.
### `useSignAndSendTransaction(uiWalletAccount, chain)`

Given a `UiWalletAccount` and a chain that begins with `solana:`, this hook returns a function you can call to sign and send a serialized transaction.
Expand Down Expand Up @@ -158,6 +270,3 @@ function SignAndSendTransactionButton({ account, transactionBytes }) {
);
}
```

> [!NOTE]
> There exists a plural version of this hook &ndash; `useSignAndSendTransactions` &ndash; that accepts multiple inputs and returns an array of outputs.
6 changes: 6 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,19 @@
"node": ">=17.4"
},
"dependencies": {
"@solana/addresses": "workspace:*",
"@solana/errors": "workspace:*",
"@solana/keys": "workspace:*",
"@solana/signers": "workspace:*",
"@solana/transactions": "workspace:*",
"@solana/wallet-standard-features": "^1.2.0",
"@wallet-standard/base": "pre",
"@wallet-standard/errors": "pre",
"@wallet-standard/ui": "pre",
"@wallet-standard/ui-registry": "pre"
},
"devDependencies": {
"@solana/codecs-core": "workspace:*",
"@types/react": "^18",
"@types/react-test-renderer": "^18",
"react": "^18",
Expand Down
Loading

0 comments on commit ce876d9

Please sign in to comment.