Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: solana sign all transactions #2915

Merged
merged 1 commit into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading