From 1cff87703170a98b705fb7afcfb4787c3433a28c Mon Sep 17 00:00:00 2001 From: Felipe Mendes Date: Tue, 24 Sep 2024 12:49:00 -0300 Subject: [PATCH] feat: add implementation for solana_signAllTransactions rpc request --- .../web3js/providers/WalletConnectProvider.ts | 56 ++++++- .../solana/web3js/providers/shared/Errors.ts | 8 + .../tests/WalletConnectProvider.test.ts | 138 ++++++++++++++++++ .../web3js/tests/mocks/UniversalProvider.ts | 7 + 4 files changed, 205 insertions(+), 4 deletions(-) diff --git a/packages/base/adapters/solana/web3js/providers/WalletConnectProvider.ts b/packages/base/adapters/solana/web3js/providers/WalletConnectProvider.ts index 8815d752b3..907314bb3f 100644 --- a/packages/base/adapters/solana/web3js/providers/WalletConnectProvider.ts +++ b/packages/base/adapters/solana/web3js/providers/WalletConnectProvider.ts @@ -17,6 +17,7 @@ import { } from '@solana/web3.js' import { isVersionedTransaction } from '@solana/wallet-adapter-base' import { withSolanaNamespace } from '../utils/withSolanaNamespace.js' +import { WalletConnectMethodNotSupportedError } from './shared/Errors.js' export type WalletConnectProviderConfig = { provider: UniversalProvider @@ -91,7 +92,8 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi methods: [ 'solana_signMessage', 'solana_signTransaction', - 'solana_signAndSendTransaction' + 'solana_signAndSendTransaction', + 'solana_signAllTransactions' ], events: [], rpcMap @@ -114,6 +116,8 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi } public async signMessage(message: Uint8Array) { + this.checkIfMethodIsSupported('solana_signMessage') + const signedMessage = await this.request('solana_signMessage', { message: base58.encode(message), pubkey: this.getAccount(true).address @@ -123,6 +127,8 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi } public async signTransaction(transaction: T) { + this.checkIfMethodIsSupported('solana_signTransaction') + const serializedTransaction = this.serializeTransaction(transaction) const result = await this.request('solana_signTransaction', { @@ -154,6 +160,8 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi transaction: T, sendOptions?: SendOptions ) { + this.checkIfMethodIsSupported('solana_signAndSendTransaction') + const serializedTransaction = this.serializeTransaction(transaction) const result = await this.request('solana_signAndSendTransaction', { @@ -177,9 +185,42 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi } public async signAllTransactions(transactions: T): Promise { - return (await Promise.all( - transactions.map(transaction => this.signTransaction(transaction)) - )) as T + try { + this.checkIfMethodIsSupported('solana_signAllTransactions') + + const result = await this.request('solana_signAllTransactions', { + transactions: transactions.map(transaction => this.serializeTransaction(transaction)) + }) + + return result.transactions.map((serializedTransaction, index) => { + const transaction = transactions[index] + + if (!transaction) { + throw new Error('Invalid transactions response') + } + + const decodedTransaction = base58.decode(serializedTransaction) + + if (isVersionedTransaction(transaction)) { + return VersionedTransaction.deserialize(decodedTransaction) + } + + return Transaction.from(decodedTransaction) + }) as T + } catch (error) { + if (error instanceof WalletConnectMethodNotSupportedError) { + const signedTransactions = [] as AnyTransaction[] as T + + for (const transaction of transactions) { + // eslint-disable-next-line no-await-in-loop + signedTransactions.push(await this.signTransaction(transaction)) + } + + return signedTransactions + } + + throw error + } } // -- Private ------------------------------------------ // @@ -315,6 +356,12 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi recentBlockhash: transaction.recentBlockhash ?? '' } } + + private checkIfMethodIsSupported(method: WalletConnectProvider.RequestMethod) { + if (!this.session?.namespaces['solana']?.methods.includes(method)) { + throw new WalletConnectMethodNotSupportedError(method) + } + } } export namespace WalletConnectProvider { @@ -333,6 +380,7 @@ export namespace WalletConnectProvider { { transaction: string; pubkey: string; sendOptions?: SendOptions }, { signature: string } > + solana_signAllTransactions: Request<{ transactions: string[] }, { transactions: string[] }> } export type RequestMethod = keyof RequestMethods diff --git a/packages/base/adapters/solana/web3js/providers/shared/Errors.ts b/packages/base/adapters/solana/web3js/providers/shared/Errors.ts index 2ab66b6012..e7a054ae4f 100644 --- a/packages/base/adapters/solana/web3js/providers/shared/Errors.ts +++ b/packages/base/adapters/solana/web3js/providers/shared/Errors.ts @@ -1,5 +1,13 @@ +/* eslint-disable max-classes-per-file */ + export class WalletStandardFeatureNotSupportedError extends Error { constructor(feature: string) { super(`The wallet does not support the "${feature}" feature`) } } + +export class WalletConnectMethodNotSupportedError extends Error { + constructor(method: string) { + super(`The method "${method}" is not supported by the wallet`) + } +} diff --git a/packages/base/adapters/solana/web3js/tests/WalletConnectProvider.test.ts b/packages/base/adapters/solana/web3js/tests/WalletConnectProvider.test.ts index 262dbf9fc4..f09ebf8467 100644 --- a/packages/base/adapters/solana/web3js/tests/WalletConnectProvider.test.ts +++ b/packages/base/adapters/solana/web3js/tests/WalletConnectProvider.test.ts @@ -4,6 +4,7 @@ import { WalletConnectProvider } from '../providers/WalletConnectProvider.js' import { TestConstants } from './util/TestConstants.js' import { mockLegacyTransaction, mockVersionedTransaction } from './mocks/Transaction.js' import { type Chain } from '@web3modal/scaffold-utils/solana' +import { WalletConnectMethodNotSupportedError } from '../providers/shared/Errors.js' describe('WalletConnectProvider specific tests', () => { let provider = mockUniversalProvider() @@ -316,4 +317,141 @@ describe('WalletConnectProvider specific tests', () => { expect(walletConnectProvider.chains).toEqual([TestConstants.chains[0]]) }) + + it('should throw an error if the wallet does not support the signMessage method', async () => { + vi.spyOn(provider, 'connect').mockImplementationOnce(() => + Promise.resolve( + mockUniversalProviderSession({ + namespaces: { + solana: { + chains: undefined, + methods: [], + events: [], + accounts: [ + `solana:${TestConstants.chains[0]?.chainId}:${TestConstants.accounts[0].address}` + ] + } + } + }) + ) + ) + + await walletConnectProvider.connect() + + await expect(() => + walletConnectProvider.signMessage(new Uint8Array([1, 2, 3, 4, 5])) + ).rejects.toThrow(WalletConnectMethodNotSupportedError) + }) + + it('should throw an error if the wallet does not support the signTransaction method', async () => { + vi.spyOn(provider, 'connect').mockImplementationOnce(() => + Promise.resolve( + mockUniversalProviderSession({ + namespaces: { + solana: { + chains: undefined, + methods: ['solana_signMessage'], + events: [], + accounts: [ + `solana:${TestConstants.chains[0]?.chainId}:${TestConstants.accounts[0].address}` + ] + } + } + }) + ) + ) + + await walletConnectProvider.connect() + + await expect(() => + walletConnectProvider.signTransaction(mockLegacyTransaction()) + ).rejects.toThrow(WalletConnectMethodNotSupportedError) + }) + + it('should throw an error if the wallet does not support the signAndSendTransaction method', async () => { + vi.spyOn(provider, 'connect').mockImplementationOnce(() => + Promise.resolve( + mockUniversalProviderSession({ + namespaces: { + solana: { + chains: undefined, + methods: ['solana_signMessage'], + events: [], + accounts: [ + `solana:${TestConstants.chains[0]?.chainId}:${TestConstants.accounts[0].address}` + ] + } + } + }) + ) + ) + + await walletConnectProvider.connect() + + await expect(() => + walletConnectProvider.signAndSendTransaction(mockLegacyTransaction()) + ).rejects.toThrow(WalletConnectMethodNotSupportedError) + }) + + it('should throw an error if the wallet does not support the signAllTransactions method', async () => { + vi.spyOn(provider, 'connect').mockImplementationOnce(() => + Promise.resolve( + mockUniversalProviderSession({ + namespaces: { + solana: { + chains: undefined, + methods: ['solana_signMessage'], + events: [], + accounts: [ + `solana:${TestConstants.chains[0]?.chainId}:${TestConstants.accounts[0].address}` + ] + } + } + }) + ) + ) + + await walletConnectProvider.connect() + + await expect(() => + walletConnectProvider.signAllTransactions([mockLegacyTransaction()]) + ).rejects.toThrow(WalletConnectMethodNotSupportedError) + }) + + it('should request signAllTransactions with batched transactions', async () => { + vi.spyOn(provider, 'connect').mockImplementationOnce(() => + Promise.resolve( + mockUniversalProviderSession({ + namespaces: { + solana: { + chains: undefined, + methods: ['solana_signAllTransactions'], + events: [], + accounts: [ + `solana:${TestConstants.chains[0]?.chainId}:${TestConstants.accounts[0].address}` + ] + } + } + }) + ) + ) + + await walletConnectProvider.connect() + + const transactions = [mockLegacyTransaction(), mockVersionedTransaction()] + await walletConnectProvider.signAllTransactions(transactions) + + expect(provider.request).toHaveBeenCalledWith( + { + method: 'solana_signAllTransactions', + params: { + transactions: [ + 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAECFj6WhBP/eepC4T4bDgYuJMiSVXNh9IvPWv1ZDUV52gYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMmaU6FiJxS/swxct+H8Iree7FERP/8vrGuAdF90ANelAQECAAAMAgAAAICWmAAAAAAA', + 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQABAhY+loQT/3nqQuE+Gw4GLiTIklVzYfSLz1r9WQ1FedoGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADJmlOhYicUv7MMXLfh/CK3nuxRET//L6xrgHRfdADXpQEBAgAADAIAAACAlpgAAAAAAAA=' + ] + } + }, + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' + ) + }) }) diff --git a/packages/base/adapters/solana/web3js/tests/mocks/UniversalProvider.ts b/packages/base/adapters/solana/web3js/tests/mocks/UniversalProvider.ts index e2bdab3dd9..2848f927c5 100644 --- a/packages/base/adapters/solana/web3js/tests/mocks/UniversalProvider.ts +++ b/packages/base/adapters/solana/web3js/tests/mocks/UniversalProvider.ts @@ -30,6 +30,13 @@ export function mockUniversalProvider() { signature: '2Lb1KQHWfbV3pWMqXZveFWqneSyhH95YsgCENRWnArSkLydjN1M42oB82zSd6BBdGkM9pE6sQLQf1gyBh8KWM2c4' } satisfies WalletConnectProvider.RequestMethods['solana_signAndSendTransaction']['returns']) + case 'solana_signAllTransactions': + return Promise.resolve({ + transactions: [ + '4zZMC2ddAFY1YHcA2uFCqbuTHmD1xvB5QLzgNnT3dMb4aQT98md8jVm1YRGUsKJkYkLPYarnkobvESUpjqEUnDmoG76e9cgNJzLuFXBW1i6njs2Sy1Lnr9TZmLnhif5CYjh1agVJEvjfYpTq1QbTnLS3rBt4yKVjQ6FcV3x22Vm3XBPqodTXz17o1YcHMcvYQbHZfVUyikQ3Nmv6ktZzWe36D6ceKCVBV88VvYkkFhwWUWkA5ErPvsHWQU64VvbtENaJXFUUnuqTFSX4q3ccHuHdmtnhWQ7Mv8Xkb', + '4zZMC2ddAFY1YHcA2uFCqbuTHmD1xvB5QLzgNnT3dMb4aQT98md8jVm1YRGUsKJkYkLPYarnkobvESUpjqEUnDmoG76e9cgNJzLuFXBW1i6njs2Sy1Lnr9TZmLnhif5CYjh1agVJEvjfYpTq1QbTnLS3rBt4yKVjQ6FcV3x22Vm3XBPqodTXz17o1YcHMcvYQbHZfVUyikQ3Nmv6ktZzWe36D6ceKCVBV88VvYkkFhwWUWkA5ErPvsHWQU64VvbtENaJXFUUnuqTFSX4q3ccHuHdmtnhWQ7Mv8Xkb' + ] + }) default: return Promise.reject(new Error('not implemented')) }