Skip to content

Commit

Permalink
feat: solana sign all transactions (#2772)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoruka authored Aug 30, 2024
1 parent f59d39a commit be93b51
Show file tree
Hide file tree
Showing 17 changed files with 459 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { useState } from 'react'
import { Button, Stack, Text, Spacer } from '@chakra-ui/react'
import {
PublicKey,
Transaction,
TransactionMessage,
VersionedTransaction,
SystemProgram
} from '@solana/web3.js'

import { useWeb3ModalAccount, useWeb3ModalProvider, type Provider } from '@web3modal/solana/react'

import { solana } from '../../utils/ChainsUtil'
import { useChakraToast } from '../Toast'
import type { Connection } from '@web3modal/base/adapters/solana/web3js'
import bs58 from 'bs58'

const PHANTOM_DEVNET_ADDRESS = '8vCyX7oB6Pc3pbWMGYYZF5pbSnAdQ7Gyr32JqxqCy8ZR'
const recipientAddress = new PublicKey(PHANTOM_DEVNET_ADDRESS)
const amountInLamports = 1_000_000

export function SolanaSignAllTransactionsTest() {
const toast = useChakraToast()
const { chainId } = useWeb3ModalAccount()
const { walletProvider, connection } = useWeb3ModalProvider()
const [loading, setLoading] = useState(false)

async function onSignTransaction(type: 'legacy' | 'versioned') {
try {
setLoading(true)
if (!walletProvider?.publicKey) {
throw Error('user is disconnected')
}

if (!connection) {
throw Error('no connection set')
}

const transactions = await Promise.all(
Array.from({ length: 5 }, () => createTransaction(walletProvider, connection, type))
)
const response = await walletProvider.signAllTransactions(transactions)

const description = response
.map(transaction => {
const signature =
transaction.signatures[0] instanceof Uint8Array
? transaction.signatures[0]
: transaction.signatures[0]?.signature

if (!signature) {
throw Error('Empty signature')
}

return bs58.encode(signature)
})
.join('\n\n')

toast({
title: 'Success',
description,
type: 'success'
})
} catch (err) {
toast({
title: 'Error',
description: (err as Error).message,
type: 'error'
})
} finally {
setLoading(false)
}
}

if (chainId === solana.chainId) {
return (
<Text fontSize="md" color="yellow">
Switch to Solana Devnet or Testnet to test this feature
</Text>
)
}

return (
<Stack direction={['column', 'column', 'row']}>
<Button
data-testid="sign-transaction-button"
onClick={onSignTransaction.bind(null, 'legacy')}
isDisabled={loading}
>
Sign All Transactions
</Button>
<Button
data-test-id="sign-transaction-button"
onClick={onSignTransaction.bind(null, 'versioned')}
isDisabled={loading}
>
Sign All Versioned Transactions
</Button>
<Spacer />
</Stack>
)
}

async function createTransaction(
provider: Provider,
connection: Connection,
type: 'legacy' | 'versioned'
) {
if (!provider.publicKey) {
throw Error('No public key found')
}

const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash()

const instructions = [
SystemProgram.transfer({
fromPubkey: provider.publicKey,
toPubkey: recipientAddress,
lamports: amountInLamports
})
]

if (type === 'legacy') {
return new Transaction({ feePayer: provider.publicKey, blockhash, lastValidBlockHeight }).add(
...instructions
)
}

return new VersionedTransaction(
new TransactionMessage({
payerKey: provider.publicKey,
recentBlockhash: blockhash,
instructions
}).compileToV0Message()
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Button, Stack, Text, Spacer, Link } from '@chakra-ui/react'
import { Button, Stack, Text, Spacer } from '@chakra-ui/react'
import {
PublicKey,
Transaction,
Expand All @@ -12,6 +12,7 @@ import { useWeb3ModalAccount, useWeb3ModalProvider } from '@web3modal/solana/rea

import { solana } from '../../utils/ChainsUtil'
import { useChakraToast } from '../Toast'
import bs58 from 'bs58'

const PHANTOM_DEVNET_ADDRESS = '8vCyX7oB6Pc3pbWMGYYZF5pbSnAdQ7Gyr32JqxqCy8ZR'
const recipientAddress = new PublicKey(PHANTOM_DEVNET_ADDRESS)
Expand Down Expand Up @@ -56,7 +57,7 @@ export function SolanaSignTransactionTest() {

toast({
title: 'Success',
description: Uint8Array.from(signature),
description: bs58.encode(signature),
type: 'success'
})
} catch (err) {
Expand Down Expand Up @@ -108,7 +109,7 @@ export function SolanaSignTransactionTest() {

toast({
title: 'Success',
description: signature,
description: bs58.encode(signature),
type: 'success'
})
} catch (err) {
Expand Down Expand Up @@ -147,12 +148,6 @@ export function SolanaSignTransactionTest() {
Sign Versioned Transaction
</Button>
<Spacer />

<Link isExternal href="https://solfaucet.com/">
<Button variant="outline" colorScheme="blue">
Solana Faucet
</Button>
</Link>
</Stack>
)
}
13 changes: 13 additions & 0 deletions apps/laboratory/src/components/Solana/SolanaTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { SolanaSignMessageTest } from './SolanaSignMessageTest'
import { SolanaWriteContractTest } from './SolanaWriteContractTest'
import { solana, solanaDevnet, solanaTestnet } from '../../utils/ChainsUtil'
import { SolanaSignAndSendTransaction } from './SolanaSignAndSendTransactionTest'
import { SolanaSignAllTransactionsTest } from './SolanaSignAllTransactionsTest'

export function SolanaTests() {
const { isConnected, currentChain } = useWeb3ModalAccount()
Expand Down Expand Up @@ -48,6 +49,18 @@ export function SolanaTests() {
</Heading>
<SolanaSignTransactionTest />
</Box>
<Box>
<Heading size="xs" textTransform="uppercase" pb="2">
Sign All Transactions
<Tooltip label="Request the signature for 5 transactions at once">
<Text as="span" fontSize="sm" ml="2">
ℹ️
</Text>
</Tooltip>
</Heading>
<SolanaSignAllTransactionsTest />
</Box>

<Box>
<Heading size="xs" textTransform="uppercase" pb="2">
Sign and Send Transaction (Dapp)
Expand Down
25 changes: 25 additions & 0 deletions packages/base/adapters/solana/web3js/providers/AuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,31 @@ export class AuthProvider extends ProviderEventEmitter implements Provider, Prov
return signature
}

public async signAllTransactions<T extends AnyTransaction[]>(transactions: T): Promise<T> {
const result = await this.provider.request({
method: 'solana_signAllTransactions',
params: {
transactions: transactions.map(transaction => this.serializeTransaction(transaction))
}
})

return (result.transactions as string[]).map((encodedTransaction, index) => {
const transaction = transactions[index]

if (!transaction) {
throw new Error('Invalid solana_signAllTransactions response')
}

const decodedTransaction = base58.decode(encodedTransaction)

if (isVersionedTransaction(transaction)) {
return VersionedTransaction.deserialize(decodedTransaction)
}

return Transaction.from(decodedTransaction)
}) as T
}

// -- W3mFrameProvider methods ------------------------------------------- //
connectEmail: ProviderAuthMethods['connectEmail'] = args => this.provider.connectEmail(args)
connectOtp: ProviderAuthMethods['connectOtp'] = args => this.provider.connectOtp(args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi
return signature
}

public async signAllTransactions<T extends AnyTransaction[]>(transactions: T): Promise<T> {
return (await Promise.all(
transactions.map(transaction => this.signTransaction(transaction))
)) as T
}

// -- Private ------------------------------------------ //
private request<Method extends WalletConnectProvider.RequestMethod>(
method: Method,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,35 @@ export class WalletStandardProvider extends ProviderEventEmitter implements Prov
return signature
}

public async signAllTransactions<T extends AnyTransaction[]>(transactions: T): Promise<T> {
const feature = this.getWalletFeature(SolanaSignTransaction)

const account = this.getAccount(true)
const chain = this.getActiveChainName()

const result = await feature.signTransaction(
...transactions.map(transaction => ({
transaction: this.serializeTransaction(transaction),
account,
chain
}))
)

return result.map(({ signedTransaction }, index) => {
const transaction = transactions[index]

if (!transaction) {
throw new WalletSignTransactionError('Invalid transaction signature response')
}

if (isVersionedTransaction(transaction)) {
return VersionedTransaction.deserialize(signedTransaction)
}

return Transaction.from(signedTransaction)
}) as T
}

// -- Private ------------------------------------------- //
private serializeTransaction(transaction: AnyTransaction) {
return transaction.serialize({ verifySignatures: false })
Expand Down
16 changes: 16 additions & 0 deletions packages/base/adapters/solana/web3js/tests/AuthProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,20 @@ describe('AuthProvider specific tests', () => {
expect(provider.switchNetwork).toHaveBeenCalledWith(newChain.chainId)
expect(listener).toHaveBeenCalledWith(newChain.chainId)
})

it('should call signAllTransactions with correct params', async () => {
await authProvider.connect()
const transactions = [mockLegacyTransaction(), mockVersionedTransaction()]
await authProvider.signAllTransactions(transactions)

expect(provider.request).toHaveBeenCalledWith({
method: 'solana_signAllTransactions',
params: {
transactions: [
'AKhoybLLJS1deDJDyjELDNhfkBBX3k4dt4bBfmppjfPVVimhQdFEfDo8AiFcCBCC9VkYWV2r3jkh9n1DAXEhnJPwMmnsrx6huAVrhHAbmRUqfUuWZ9aWMGmdEWaeroCnPR6jkEnjJcn14a59TZhkiTXMygMqu4KaqD1TqzE8vNHSw3YgbW24cfqWfQczGysuy4ugxj4TGSpqRtNmf5D7zRRa76eJTeZEaBcBQGkqxb31vBRXDMdQzGEbq',
'48ckoQL1HhH5aqU1ifKqpQkwq3WPDgMnsHHQkVfddisxYcapwAVXr8hejTi2jeJpMPkZMsF72SwmJFDByyfRtaknz4ytCYNAcdHrxtrHa9hTjMKckVQrFFqS8zG63Wj5mJ6wPfj8dv1wKu2XkU6GSXSGdQmuvfRv3K6LUSMbK5XSP3yBGb1SDZKCuoFX4qDKcKhCG7Awn3ssAWB1yRaXMd6mS6HQHKSF11FTp3jTH2HKUNbKyyuGh4tYtq8b'
]
}
})
})
})
21 changes: 21 additions & 0 deletions packages/base/adapters/solana/web3js/tests/GenericProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Transaction, VersionedTransaction } from '@solana/web3.js'
import { mockLegacyTransaction, mockVersionedTransaction } from './mocks/Transaction.js'
import { AuthProvider } from '../providers/AuthProvider.js'
import { mockW3mFrameProvider } from './mocks/W3mFrameProvider.js'
import { isVersionedTransaction } from '@solana/wallet-adapter-base'

const getActiveChain = vi.fn(() => TestConstants.chains[0])

Expand Down Expand Up @@ -99,4 +100,24 @@ describe.each(providers)('Generic provider tests for $name', ({ provider }) => {

expect(result).toBeTypeOf('string')
})

it('should signAllTransactions with AnyTransaction', async () => {
const transactions = [
mockLegacyTransaction(),
mockVersionedTransaction(),
mockLegacyTransaction(),
mockVersionedTransaction()
]
const result = await provider.signAllTransactions(transactions)

expect(result).toHaveLength(transactions.length)

transactions.forEach((transaction, index) => {
if (isVersionedTransaction(transaction)) {
expect(result[index]).toBeInstanceOf(VersionedTransaction)
} else {
expect(result[index]).toBeInstanceOf(Transaction)
}
})
})
})
Loading

0 comments on commit be93b51

Please sign in to comment.