diff --git a/apps/laboratory/src/components/Solana/SolanaSendTransactionTest.tsx b/apps/laboratory/src/components/Solana/SolanaSendTransactionTest.tsx index 4e02003aa3..68ee572b38 100644 --- a/apps/laboratory/src/components/Solana/SolanaSendTransactionTest.tsx +++ b/apps/laboratory/src/components/Solana/SolanaSendTransactionTest.tsx @@ -6,8 +6,7 @@ import { Transaction, TransactionMessage, VersionedTransaction, - SystemProgram, - Connection + SystemProgram } from '@solana/web3.js' import { solana } from '../../utils/ChainsUtil' @@ -15,7 +14,7 @@ import { useChakraToast } from '../Toast' const PHANTOM_TESTNET_ADDRESS = '8vCyX7oB6Pc3pbWMGYYZF5pbSnAdQ7Gyr32JqxqCy8ZR' const recipientAddress = new PublicKey(PHANTOM_TESTNET_ADDRESS) -const amountInLamports = 100000000 +const amountInLamports = 10_000_000 export function SolanaSendTransactionTest() { const toast = useChakraToast() @@ -53,7 +52,8 @@ export function SolanaSendTransactionTest() { transaction.recentBlockhash = blockhash - const signature = await walletProvider.sendTransaction(transaction, connection as Connection) + const signature = await walletProvider.sendTransaction(transaction, connection) + toast({ title: 'Success', description: signature, @@ -106,10 +106,7 @@ export function SolanaSendTransactionTest() { // Make a versioned transaction const transactionV0 = new VersionedTransaction(messageV0) - const signature = await walletProvider.sendTransaction( - transactionV0, - connection as Connection - ) + const signature = await walletProvider.sendTransaction(transactionV0, connection) toast({ title: 'Success', diff --git a/apps/laboratory/src/components/Solana/SolanaSignAndSendTransactionTest.tsx b/apps/laboratory/src/components/Solana/SolanaSignAndSendTransactionTest.tsx index 1fe29a18ab..da9c1a5728 100644 --- a/apps/laboratory/src/components/Solana/SolanaSignAndSendTransactionTest.tsx +++ b/apps/laboratory/src/components/Solana/SolanaSignAndSendTransactionTest.tsx @@ -1,14 +1,20 @@ import { useState } from 'react' import { Button, Stack, Text, Spacer, Link } from '@chakra-ui/react' import { useWeb3ModalAccount, useWeb3ModalProvider } from '@web3modal/solana/react' -import { PublicKey, Transaction, SystemProgram } from '@solana/web3.js' +import { + PublicKey, + Transaction, + SystemProgram, + TransactionMessage, + VersionedTransaction +} from '@solana/web3.js' import { solana } from '../../utils/ChainsUtil' import { useChakraToast } from '../Toast' const PHANTOM_TESTNET_ADDRESS = '8vCyX7oB6Pc3pbWMGYYZF5pbSnAdQ7Gyr32JqxqCy8ZR' const recipientAddress = new PublicKey(PHANTOM_TESTNET_ADDRESS) -const amountInLamports = 50000000 +const amountInLamports = 10_000_000 export function SolanaSignAndSendTransaction() { const toast = useChakraToast() @@ -16,7 +22,7 @@ export function SolanaSignAndSendTransaction() { const { walletProvider, connection } = useWeb3ModalProvider() const [loading, setLoading] = useState(false) - async function onSendTransaction() { + async function onSendTransaction(mode: 'legacy' | 'versioned') { try { setLoading(true) if (!walletProvider || !address) { @@ -32,16 +38,34 @@ export function SolanaSignAndSendTransaction() { throw Error('Not enough SOL in wallet') } - // Create a new transaction - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: walletProvider.publicKey, - toPubkey: recipientAddress, - lamports: amountInLamports - }) - ) - transaction.feePayer = walletProvider.publicKey - const signature = await walletProvider.signAndSendTransaction(transaction) + const instruction = SystemProgram.transfer({ + fromPubkey: walletProvider.publicKey, + toPubkey: recipientAddress, + lamports: amountInLamports + }) + const { blockhash } = await connection.getLatestBlockhash() + + let signature = '' + + if (mode === 'versioned') { + // Create v0 compatible message + const messageV0 = new TransactionMessage({ + payerKey: walletProvider.publicKey, + recentBlockhash: blockhash, + instructions: [instruction] + }).compileToV0Message() + + // Make a versioned transaction + const versionedTranasction = new VersionedTransaction(messageV0) + + signature = await walletProvider.signAndSendTransaction(versionedTranasction) + } else { + // Create a new transaction + const transaction = new Transaction().add(instruction) + transaction.feePayer = walletProvider.publicKey + transaction.recentBlockhash = blockhash + signature = await walletProvider.signAndSendTransaction(transaction) + } toast({ title: 'Success', @@ -75,11 +99,18 @@ export function SolanaSignAndSendTransaction() { + diff --git a/apps/laboratory/src/components/Solana/SolanaSignMessageTest.tsx b/apps/laboratory/src/components/Solana/SolanaSignMessageTest.tsx index 1a0405e149..eba14f5a77 100644 --- a/apps/laboratory/src/components/Solana/SolanaSignMessageTest.tsx +++ b/apps/laboratory/src/components/Solana/SolanaSignMessageTest.tsx @@ -19,25 +19,15 @@ export function SolanaSignMessageTest() { const encodedMessage = new TextEncoder().encode('Hello from Web3Modal') const signature = await walletProvider.signMessage(encodedMessage) - // Backpack has specific signature format now - if ((signature as { signature: Uint8Array }).signature) { - toast({ - title: ConstantsUtil.SigningSucceededToastTitle, - description: (signature as { signature: Uint8Array }).signature, - type: 'success' - }) - - return - } toast({ title: ConstantsUtil.SigningSucceededToastTitle, - description: signature as Uint8Array, + description: signature, type: 'success' }) } catch (err) { toast({ title: ConstantsUtil.SigningFailedToastTitle, - description: 'Failed to sign message', + description: (err as Error).message, type: 'error' }) } diff --git a/apps/laboratory/src/components/Solana/SolanaSignTransactionTest.tsx b/apps/laboratory/src/components/Solana/SolanaSignTransactionTest.tsx index 3ae8c327c1..eb1eacd852 100644 --- a/apps/laboratory/src/components/Solana/SolanaSignTransactionTest.tsx +++ b/apps/laboratory/src/components/Solana/SolanaSignTransactionTest.tsx @@ -15,7 +15,7 @@ import { useChakraToast } from '../Toast' const PHANTOM_DEVNET_ADDRESS = '8vCyX7oB6Pc3pbWMGYYZF5pbSnAdQ7Gyr32JqxqCy8ZR' const recipientAddress = new PublicKey(PHANTOM_DEVNET_ADDRESS) -const amountInLamports = 100000000 +const amountInLamports = 10_000_000 export function SolanaSignTransactionTest() { const toast = useChakraToast() @@ -46,18 +46,23 @@ export function SolanaSignTransactionTest() { const { blockhash } = await connection.getLatestBlockhash() transaction.recentBlockhash = blockhash - const tx = await walletProvider.signTransaction(transaction) - const signature = tx.signatures[0]?.signature + + const signedTransaction = await walletProvider.signTransaction(transaction) + const signature = signedTransaction.signatures[0]?.signature + + if (!signature) { + throw Error('Failed to sign transaction') + } toast({ title: 'Success', - description: signature, + description: Uint8Array.from(signature), type: 'success' }) } catch (err) { toast({ title: 'Error', - description: 'Failed to sign transaction', + description: (err as Error).message, type: 'error' }) } finally { @@ -94,8 +99,12 @@ export function SolanaSignTransactionTest() { // Make a versioned transaction const transactionV0 = new VersionedTransaction(messageV0) - const tx = await walletProvider.signTransaction(transactionV0) - const signature = tx.signatures[0]?.signature + const signedTransaction = await walletProvider.signTransaction(transactionV0) + const signature = signedTransaction.signatures[0] + + if (!signature) { + throw Error('Failed to sign transaction') + } toast({ title: 'Success', diff --git a/apps/laboratory/src/components/Solana/SolanaTests.tsx b/apps/laboratory/src/components/Solana/SolanaTests.tsx index 1668e19210..b940c1d24b 100644 --- a/apps/laboratory/src/components/Solana/SolanaTests.tsx +++ b/apps/laboratory/src/components/Solana/SolanaTests.tsx @@ -7,7 +7,8 @@ import { CardBody, Box, Stack, - Text + Text, + Tooltip } from '@chakra-ui/react' import { SolanaSignTransactionTest } from './SolanaSignTransactionTest' @@ -49,13 +50,23 @@ export function SolanaTests() { - Sign and Send Transaction (dApp) + Sign and Send Transaction (Dapp) + + + ℹ️ + + Sign and Send Transaction (Wallet) + + + ℹ️ + + diff --git a/packages/solana/exports/react.tsx b/packages/solana/exports/react.tsx index 96ec8759f4..ce080913cd 100644 --- a/packages/solana/exports/react.tsx +++ b/packages/solana/exports/react.tsx @@ -9,7 +9,7 @@ import { Web3Modal } from '../src/client.js' import { SolStoreUtil } from '../src/utils/scaffold/index.js' import type { Web3ModalOptions } from '../src/client.js' -import type { Provider } from '../src/utils/scaffold/index.js' +import type { Connection, Provider } from '../src/utils/scaffold/index.js' // -- Setup ------------------------------------------------------------------- let modal: Web3Modal | undefined = undefined @@ -36,7 +36,7 @@ export function useWeb3ModalProvider() { return { walletProvider: provider as Provider, walletProviderType: providerType, - connection + connection: connection as Connection } } diff --git a/packages/solana/src/connectors/baseConnector.ts b/packages/solana/src/connectors/baseConnector.ts index 8d9436a264..69d6bea7b6 100644 --- a/packages/solana/src/connectors/baseConnector.ts +++ b/packages/solana/src/connectors/baseConnector.ts @@ -16,8 +16,6 @@ import { SolConstantsUtil, SolStoreUtil } from '../utils/scaffold/index.js' import { getHashedName, getNameAccountKey } from '../utils/hash.js' import { NameRegistry } from '../utils/nameService.js' -import type { SendOptions, TransactionSignature } from '@solana/web3.js' - import type { BlockResult, AccountInfo, @@ -26,7 +24,8 @@ import type { FilterObject, RequestMethods, TransactionArgs, - TransactionType + TransactionType, + Provider } from '../utils/scaffold/index.js' export interface Connector { @@ -36,15 +35,10 @@ export interface Connector { getConnectorName: () => string disconnect: () => Promise connect: () => Promise - signMessage: (message: Uint8Array) => Promise - signTransaction: ( - transaction: Transaction | VersionedTransaction - ) => Promise<{ signatures: { signature: string }[] }> - sendTransaction: (transaction: Transaction | VersionedTransaction) => Promise - signAndSendTransaction: ( - transaction: Transaction | VersionedTransaction, - options?: SendOptions - ) => Promise + signMessage: Provider['signMessage'] + signTransaction: Provider['signTransaction'] + signAndSendTransaction: Provider['signAndSendTransaction'] + sendTransaction: Provider['sendTransaction'] getAccount: ( requestedAddress?: string, encoding?: 'base58' | 'base64' | 'jsonParsed' diff --git a/packages/solana/src/connectors/walletConnectConnector.ts b/packages/solana/src/connectors/walletConnectConnector.ts index 38c1faff04..5a1778ae55 100644 --- a/packages/solana/src/connectors/walletConnectConnector.ts +++ b/packages/solana/src/connectors/walletConnectConnector.ts @@ -1,20 +1,21 @@ import base58 from 'bs58' -import { PublicKey, Transaction, VersionedTransaction, type SendOptions } from '@solana/web3.js' +import { Connection, Transaction, VersionedTransaction, type SendOptions } from '@solana/web3.js' import { OptionsController } from '@web3modal/core' import { SolStoreUtil } from '../utils/scaffold/index.js' import { UniversalProviderFactory } from './universalProvider.js' import { BaseConnector } from './baseConnector.js' +import type { Connector } from './baseConnector.js' import type UniversalProvider from '@walletconnect/universal-provider' -import type { Connector } from './baseConnector.js' -import type { Chain } from '../utils/scaffold/SolanaTypesUtil.js' +import type { Chain, AnyTransaction } from '../utils/scaffold/SolanaTypesUtil.js' import { getChainsFromChainId, getDefaultChainFromSession, type ChainIDType } from '../utils/chainPath/index.js' +import { isVersionedTransaction } from '@solana/wallet-adapter-base' export interface WalletConnectAppMetadata { name: string @@ -75,115 +76,53 @@ export class WalletConnectConnector extends BaseConnector implements Connector { } public async signMessage(message: Uint8Array) { - const address = SolStoreUtil.state.address - if (!address) { - throw new Error('No signer connected') - } - const signedMessage = await this.request('solana_signMessage', { message: base58.encode(message), - pubkey: address + pubkey: this.getPubkey() }) - const { signature } = signedMessage - - return signature - } - public async signVersionedTransaction(transaction: VersionedTransaction) { - if (!SolStoreUtil.state.address) { - throw new Error('No signer connected') - } - const transactionParams = { - feePayer: new PublicKey(SolStoreUtil.state.address).toBase58(), - instructions: transaction.message.compiledInstructions.map(instruction => ({ - ...instruction, - data: base58.encode(instruction.data) - })), - recentBlockhash: transaction.message.recentBlockhash ?? '' - } - await this.request('solana_signTransaction', transactionParams) - - return { signatures: [{ signature: base58.encode(transaction.serialize()) }] } + return base58.decode(signedMessage.signature) } - public async signTransaction(transactionParam: Transaction | VersionedTransaction) { - const version = (transactionParam as VersionedTransaction).version - if (typeof version === 'number') { - return this.signVersionedTransaction(transactionParam as VersionedTransaction) - } - const transaction = transactionParam as Transaction - const transactionParams = { - feePayer: transaction.feePayer?.toBase58() ?? '', - instructions: transaction.instructions.map(instruction => ({ - data: base58.encode(instruction.data), - keys: instruction.keys.map(key => ({ - isWritable: key.isWritable, - isSigner: key.isSigner, - pubkey: key.pubkey.toBase58() - })), - programId: instruction.programId.toBase58() - })), - recentBlockhash: transaction.recentBlockhash ?? '' - } - - const res = await this.request('solana_signTransaction', transactionParams) + public async signTransaction(transaction: T) { + const serializedTransaction = this.serializeTransaction(transaction) - transaction.addSignature( - new PublicKey(SolStoreUtil.state.address ?? ''), - Buffer.from(base58.decode(res.signature)) - ) + const result = await this.request('solana_signTransaction', { + transaction: serializedTransaction, + pubkey: this.getPubkey() + }) - const validSig = transaction.verifySignatures() + const decodedTransaction = base58.decode(result.transaction) - if (!validSig) { - throw new Error('Signature invalid.') + if (isVersionedTransaction(transaction)) { + return VersionedTransaction.deserialize(decodedTransaction) as T } - return { signatures: [{ signature: base58.encode(transaction.serialize()) }] } + return Transaction.from(decodedTransaction) as T } - private async _sendTransaction(transactionParam: Transaction | VersionedTransaction) { - const encodedTransaction = (await this.signTransaction(transactionParam)) as { - signatures: { - signature: string - }[] - } - const signedTransaction = base58.decode(encodedTransaction.signatures[0]?.signature ?? '') - const txHash = await SolStoreUtil.state.connection?.sendRawTransaction(signedTransaction) - - return { - tx: txHash, - signature: base58.encode(signedTransaction) - } - } + public async signAndSendTransaction( + transaction: T, + sendOptions?: SendOptions + ) { + const serializedTransaction = this.serializeTransaction(transaction) - public async sendTransaction(transactionParam: Transaction | VersionedTransaction) { - const { signature } = await this._sendTransaction(transactionParam) + const result = await this.request('solana_signAndSendTransaction', { + transaction: serializedTransaction, + pubkey: this.getPubkey(), + sendOptions + }) - return signature + return result.signature } - public async signAndSendTransaction( - transaction: T, + public async sendTransaction( + transaction: AnyTransaction, + connection: Connection, options?: SendOptions ) { - if (transaction instanceof VersionedTransaction) { - throw Error('Versioned transactions are not supported') - } - - const { signature } = await this.request('solana_signAndSendTransaction', { - feePayer: transaction.feePayer?.toBase58() ?? '', - instructions: transaction.instructions.map(instruction => ({ - data: base58.encode(instruction.data), - keys: instruction.keys.map(key => ({ - isWritable: key.isWritable, - isSigner: key.isSigner, - pubkey: key.pubkey.toBase58() - })), - programId: instruction.programId.toBase58() - })), - options - }) + const signedTransaction = await this.signTransaction(transaction) + const signature = await connection.sendRawTransaction(signedTransaction.serialize(), options) return signature } @@ -253,4 +192,17 @@ export class WalletConnectConnector extends BaseConnector implements Connector { public async onConnector() { await this.connect() } + + private serializeTransaction(transaction: AnyTransaction) { + return base58.encode(transaction.serialize({ verifySignatures: false })) + } + + private getPubkey() { + const address = SolStoreUtil.state.address + if (!address) { + throw new Error('No signer connected') + } + + return address + } } diff --git a/packages/solana/src/utils/scaffold/SolanaStoreUtil.ts b/packages/solana/src/utils/scaffold/SolanaStoreUtil.ts index e6a42b596c..7d59aa1c6d 100644 --- a/packages/solana/src/utils/scaffold/SolanaStoreUtil.ts +++ b/packages/solana/src/utils/scaffold/SolanaStoreUtil.ts @@ -1,11 +1,10 @@ import { proxy, ref, subscribe as sub } from 'valtio/vanilla' import { subscribeKey as subKey } from 'valtio/vanilla/utils' -import { Connection } from '@solana/web3.js' import { OptionsController } from '@web3modal/core' import UniversalProvider from '@walletconnect/universal-provider' -import type { Chain, CombinedProvider, Provider } from './SolanaTypesUtil.js' +import type { Chain, CombinedProvider, Provider, Connection } from './SolanaTypesUtil.js' import { SolConstantsUtil } from './SolanaConstantsUtil.js' import { SolHelpersUtil } from './SolanaHelpersUtils.js' diff --git a/packages/solana/src/utils/scaffold/SolanaTypesUtil.ts b/packages/solana/src/utils/scaffold/SolanaTypesUtil.ts index f66255dca2..c647848cba 100644 --- a/packages/solana/src/utils/scaffold/SolanaTypesUtil.ts +++ b/packages/solana/src/utils/scaffold/SolanaTypesUtil.ts @@ -40,28 +40,26 @@ export interface Provider { wallet: Provider removeListener: (event: string, listener: (data: T) => void) => void emit: (event: string) => void - connect: () => Promise + connect: () => Promise disconnect: () => Promise request: (config: { method: string; params?: object }) => Promise + signMessage: (message: Uint8Array) => Promise + signTransaction: (transaction: T) => Promise + signAndSendTransaction: ( + transaction: AnyTransaction, + options?: SendOptions + ) => Promise signAllTransactions: (transactions: SolanaWeb3Transaction[]) => Promise signAndSendAllTransactions: ( transactions: SolanaWeb3Transaction[] ) => Promise - signAndSendTransaction: ( - transaction: SolanaWeb3Transaction | VersionedTransaction, - options?: SendOptions - ) => Promise - signMessage: (message: Uint8Array) => Promise | Promise<{ signature: Uint8Array }> - signTransaction: (transaction: SolanaWeb3Transaction | VersionedTransaction) => Promise<{ - signatures: { signature: Uint8Array }[] - }> sendTransaction: ( - transaction: SolanaWeb3Transaction | VersionedTransaction, + transaction: AnyTransaction, connection: Connection, options?: SendTransactionOptions ) => Promise sendAndConfirm: ( - transaction: SolanaWeb3Transaction | VersionedTransaction, + transaction: AnyTransaction, connection: Connection, options?: SendTransactionOptions ) => Promise @@ -186,22 +184,6 @@ export type FilterObject = } | { dataSize: number } -export interface TransactionInstructionRequest { - programId: string - data: string - keys: { - isSigner: boolean - isWritable: boolean - pubkey: string - }[] -} - -interface VersionedInstructionRequest { - data: string - programIdIndex: number - accountKeyIndexes: number[] -} - /** * Request methods to the solana RPC. * @see {@link https://solana.com/docs/rpc/http} @@ -212,33 +194,24 @@ export interface RequestMethods { message: string pubkey: string } - returns: { - signature: string - } + returns: { signature: string } } solana_signTransaction: { params: { - feePayer: string - instructions: TransactionInstructionRequest[] | VersionedInstructionRequest[] - recentBlockhash: string - signatures?: { - pubkey: string - signature: string - }[] + transaction: string + pubkey: string } returns: { - signature: string + transaction: string } } solana_signAndSendTransaction: { params: { - feePayer: string - instructions: TransactionInstructionRequest[] - options?: SendOptions - } - returns: { - signature: string + transaction: string + pubkey: string + sendOptions?: SendOptions } + returns: { signature: string } } signMessage: { @@ -357,3 +330,5 @@ export interface ClusterSubscribeRequestMethods { returns: unknown } } + +export type AnyTransaction = SolanaWeb3Transaction | VersionedTransaction