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 #2772

Merged
merged 12 commits into from
Aug 30, 2024
Merged
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
Loading