Skip to content

Commit

Permalink
feat: solana sign all transactions (#2915)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoruka authored Sep 24, 2024
1 parent f028b39 commit 79ac86c
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -91,7 +92,8 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi
methods: [
'solana_signMessage',
'solana_signTransaction',
'solana_signAndSendTransaction'
'solana_signAndSendTransaction',
'solana_signAllTransactions'
],
events: [],
rpcMap
Expand All @@ -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
Expand All @@ -123,6 +127,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 @@ -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', {
Expand All @@ -177,9 +185,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 @@ -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 {
Expand All @@ -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
Expand Down
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`)
}
}
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 Chain } from '@web3modal/scaffold-utils/solana'
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'
)
})
})
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 79ac86c

Please sign in to comment.