Skip to content

Commit

Permalink
feat: add implementation for solana_signAllTransactions rpc request
Browse files Browse the repository at this point in the history
  • Loading branch information
zoruka committed Sep 24, 2024
1 parent 2f4a764 commit 8fef249
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 4 deletions.
56 changes: 52 additions & 4 deletions packages/adapters/solana/src/providers/WalletConnectProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { isVersionedTransaction } from '@solana/wallet-adapter-base'
import type { CaipNetwork, ChainId } from '@reown/appkit-common'
import { withSolanaNamespace } from '../utils/withSolanaNamespace.js'
import { WalletConnectMethodNotSupportedError } from './shared/Errors.js'

export type WalletConnectProviderConfig = {
provider: UniversalProvider
Expand Down Expand Up @@ -94,7 +95,8 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi
methods: [
'solana_signMessage',
'solana_signTransaction',
'solana_signAndSendTransaction'
'solana_signAndSendTransaction',
'solana_signAllTransactions'
],
events: [],
rpcMap
Expand All @@ -117,6 +119,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
Expand All @@ -126,6 +130,8 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi
}

public async signTransaction<T extends AnyTransaction>(transaction: T) {
this.checkIfMethodIsSupported('solana_signTransaction')

const serializedTransaction = this.serializeTransaction(transaction)

const result = await this.request('solana_signTransaction', {
Expand Down Expand Up @@ -157,6 +163,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', {
Expand All @@ -180,9 +188,42 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi
}

public async signAllTransactions<T extends AnyTransaction[]>(transactions: T): Promise<T> {
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 ------------------------------------------ //
Expand Down Expand Up @@ -318,6 +359,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 {
Expand All @@ -336,6 +383,7 @@ export namespace WalletConnectProvider {
{ transaction: string; pubkey: string; sendOptions?: SendOptions },
{ signature: string }
>
solana_signAllTransactions: Request<{ transactions: string[] }, { transactions: string[] }>
}

export type RequestMethod = keyof RequestMethods
Expand Down
8 changes: 8 additions & 0 deletions packages/adapters/solana/src/providers/shared/Errors.ts
Original file line number Diff line number Diff line change
@@ -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`)
}
}
138 changes: 138 additions & 0 deletions packages/adapters/solana/src/tests/WalletConnectProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { CaipNetwork } from '@reown/appkit-common'
import { WalletConnectMethodNotSupportedError } from '../providers/shared/Errors.js'

describe('WalletConnectProvider specific tests', () => {
let provider = mockUniversalProvider()
Expand Down Expand Up @@ -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'
)
})
})
7 changes: 7 additions & 0 deletions packages/adapters/solana/src/tests/mocks/UniversalProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
}
Expand Down

0 comments on commit 8fef249

Please sign in to comment.