Skip to content

Commit

Permalink
Merge branch 'main' into chore/add-base-sepolia
Browse files Browse the repository at this point in the history
  • Loading branch information
tomiir authored Sep 10, 2024
2 parents b909292 + 4830c46 commit 10ab9d5
Show file tree
Hide file tree
Showing 20 changed files with 421 additions and 66 deletions.
8 changes: 8 additions & 0 deletions packages/base/adapters/evm/ethers/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,10 @@ export class EVMEthersClient implements ChainAdapter<EthersStoreUtilState, numbe
async estimateGas(data) {
const { chainId, provider, address } = EthersStoreUtil.state

if (data.chainNamespace && data.chainNamespace !== 'eip155') {
throw new Error('connectionControllerClient:estimateGas - invalid chain namespace')
}

if (!provider) {
throw new Error('connectionControllerClient:sendTransaction - provider is undefined')
}
Expand All @@ -418,6 +422,10 @@ export class EVMEthersClient implements ChainAdapter<EthersStoreUtilState, numbe
sendTransaction: async (data: SendTransactionArgs) => {
const { chainId, provider, address } = EthersStoreUtil.state

if (data.chainNamespace && data.chainNamespace !== 'eip155') {
throw new Error('ethersClient:sendTransaction - invalid chain namespace')
}

if (!provider) {
throw new Error('ethersClient:sendTransaction - provider is undefined')
}
Expand Down
4 changes: 4 additions & 0 deletions packages/base/adapters/evm/ethers5/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,10 @@ export class EVMEthers5Client implements ChainAdapter<EthersStoreUtilState, numb
const provider = EthersStoreUtil.state.provider
const address = EthersStoreUtil.state.address

if (data.chainNamespace && data.chainNamespace !== 'eip155') {
throw new Error('connectionControllerClient:sendTransaction - invalid chain namespace')
}

if (!provider) {
throw new Error('connectionControllerClient:sendTransaction - provider is undefined')
}
Expand Down
8 changes: 8 additions & 0 deletions packages/base/adapters/evm/wagmi/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ export class EVMWagmiClient implements ChainAdapter {
},

estimateGas: async args => {
if (args.chainNamespace && args.chainNamespace !== 'eip155') {
throw new Error('connectionControllerClient:estimateGas - invalid chain namespace')
}

try {
return await wagmiEstimateGas(this.wagmiConfig, {
account: args.address,
Expand All @@ -292,6 +296,10 @@ export class EVMWagmiClient implements ChainAdapter {
},

sendTransaction: async (data: SendTransactionArgs) => {
if (data.chainNamespace && data.chainNamespace !== 'eip155') {
throw new Error('connectionControllerClient:sendTransaction - invalid chain namespace')
}

const { chainId } = getAccount(this.wagmiConfig)

const txParams = {
Expand Down
78 changes: 64 additions & 14 deletions packages/base/adapters/solana/web3js/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import type { AppKit } from '../../../src/client.js'
import type { AppKitOptions } from '../../../utils/TypesUtil.js'
import type { OptionsControllerState } from '@web3modal/core'
import { SafeLocalStorage } from '../../../utils/SafeLocalStorage.js'
import { createSendTransaction } from './utils/createSendTransaction.js'

export interface Web3ModalClientOptions
extends Omit<AppKitOptions, 'defaultChain' | 'tokens' | 'sdkType' | 'sdkVersion'> {
Expand Down Expand Up @@ -183,7 +184,30 @@ export class SolanaWeb3JsClient implements ChainAdapter<SolStoreUtilState, CaipN
return new TextDecoder().decode(signature)
},

estimateGas: async () => await Promise.resolve(BigInt(0)),
estimateGas: async params => {
if (params.chainNamespace !== 'solana') {
throw new Error('Chain namespace is not supported')
}

const connection = SolStoreUtil.state.connection

if (!connection) {
throw new Error('Connection is not set')
}

const provider = this.getProvider()

const transaction = await createSendTransaction({
provider,
connection,
to: '11111111111111111111111111111111',
value: 1
})

const fee = await transaction.getEstimatedFee(connection)

return BigInt(fee || 0)
},
// -- Transaction methods ---------------------------------------------------
/**
*
Expand All @@ -196,7 +220,44 @@ export class SolanaWeb3JsClient implements ChainAdapter<SolStoreUtilState, CaipN

writeContract: async () => await Promise.resolve('0x'),

sendTransaction: async () => await Promise.resolve('0x'),
sendTransaction: async params => {
if (params.chainNamespace !== 'solana') {
throw new Error('Chain namespace is not supported')
}

const connection = SolStoreUtil.state.connection
const address = SolStoreUtil.state.address

if (!connection || !address) {
throw new Error('Connection is not set')
}

const provider = this.getProvider()

const transaction = await createSendTransaction({
provider,
connection,
to: params.to,
value: params.value
})

const result = await provider.sendTransaction(transaction, connection)

await new Promise<void>(resolve => {
const interval = setInterval(async () => {
const status = await connection.getSignatureStatus(result)

if (status?.value) {
clearInterval(interval)
resolve()
}
}, 1000)
})

await this.syncBalance(address)

return result
},

parseUnits: () => BigInt(0),

Expand Down Expand Up @@ -493,18 +554,7 @@ export class SolanaWeb3JsClient implements ChainAdapter<SolStoreUtilState, CaipN

if (W3mFrameHelpers.checkIfRequestExists(request)) {
if (!W3mFrameHelpers.checkIfRequestIsSafe(request)) {
if (this.appKit.isOpen()) {
if (this.appKit.isTransactionStackEmpty()) {
return
}
if (this.appKit.isTransactionShouldReplaceView()) {
this.appKit.replace('ApproveTransaction')
} else {
this.appKit.redirect('ApproveTransaction')
}
} else {
this.appKit.open({ view: 'ApproveTransaction' })
}
this.appKit.handleUnsafeRPCRequest()
}
} else {
this.appKit.open()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { mockConnection } from './mocks/Connection'
import { createSendTransaction } from '../utils/createSendTransaction'
import type { Provider } from '@web3modal/scaffold-utils/solana'
import { PublicKey } from '@solana/web3.js'

const mockProvider = () => {
return {
publicKey: new PublicKey('2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgoP')
} as unknown as Provider
}

describe('createSendTransaction', () => {
let provider = mockProvider()
let connection = mockConnection()

beforeEach(() => {
provider = mockProvider()
connection = mockConnection()
})

it('should create a transaction', async () => {
const transaction = await createSendTransaction({
provider,
connection,
to: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgoP',
value: 10
})

expect(transaction).toBeDefined()
})

it('should create a correct serialized transaction', async () => {
const transaction = await createSendTransaction({
provider,
connection,
to: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgoP',
value: 10
})

// Serializing to base64 only for comparison of the transaction bytes
const serialized = transaction.serialize({ verifySignatures: false }).toString('base64')
expect(serialized).toBe(
'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIDFj6WhBP/eepC4T4bDgYuJMiSVXNh9IvPWv1ZDUV52gYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAyZpToWInFL+zDFy34fwit57sURE//y+sa4B0X3QA16UDAgAJAwAtMQEAAAAAAgAFAvQBAAABAgAADAIAAAAA5AtUAgAAAA=='
)
})
})
13 changes: 13 additions & 0 deletions packages/base/adapters/solana/web3js/tests/mocks/Connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Connection } from '@solana/web3.js'
import { vi } from 'vitest'

export function mockConnection() {
const connection = new Connection('https://mocked.api.connection')

return Object.assign(connection, {
getLatestBlockhash: vi.fn().mockResolvedValue({
blockhash: 'EZySCpmzXRuUtM95P2JGv9SitqYph6Nv6HaYBK7a8PKJ',
lastValidBlockHeight: 1
})
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
PublicKey,
SystemProgram,
type Connection,
Transaction,
LAMPORTS_PER_SOL,
ComputeBudgetProgram
} from '@solana/web3.js'
import type { Provider } from '@web3modal/scaffold-utils/solana'

type SendTransactionArgs = {
provider: Provider
connection: Connection
to: string
value: number
}

/**
* These constants defines the cost of running the program, allowing to calculate the maximum
* amount of SOL that can be sent in case of cleaning the account and remove the rent exemption error.
*/
const COMPUTE_BUDGET_CONSTANTS = {
UNIT_PRICE_MICRO_LAMPORTS: 20000000,
UNIT_LIMIT: 500
}

export async function createSendTransaction({
provider,
to,
value,
connection
}: SendTransactionArgs): Promise<Transaction> {
if (!provider.publicKey) {
throw Error('No public key found')
}

const toPubkey = new PublicKey(to)
const lamports = Math.floor(value * LAMPORTS_PER_SOL)

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

const instructions = [
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: COMPUTE_BUDGET_CONSTANTS.UNIT_PRICE_MICRO_LAMPORTS
}),
ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_BUDGET_CONSTANTS.UNIT_LIMIT }),
SystemProgram.transfer({
fromPubkey: provider.publicKey,
toPubkey,
lamports
})
]

return new Transaction({ feePayer: provider.publicKey, blockhash, lastValidBlockHeight }).add(
...instructions
)
}
2 changes: 1 addition & 1 deletion packages/core/src/controllers/ConnectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface ConnectionControllerClient {
connectWalletConnect: (onUri: (uri: string) => void) => Promise<void>
disconnect: () => Promise<void>
signMessage: (message: string) => Promise<string>
sendTransaction: (args: SendTransactionArgs) => Promise<`0x${string}` | null>
sendTransaction: (args: SendTransactionArgs) => Promise<string | null>
estimateGas: (args: EstimateGasTransactionArgs) => Promise<bigint>
parseUnits: (value: string, decimals: number) => bigint
formatUnits: (value: bigint, decimals: number) => string
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/controllers/NetworkController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CoreHelperUtil } from '../utils/CoreHelperUtil.js'
import { NetworkUtil, type Chain } from '@web3modal/common'
import { ChainController } from './ChainController.js'
import { PublicStateController } from './PublicStateController.js'
import { ConstantsUtil } from '../utils/ConstantsUtil.js'

// -- Types --------------------------------------------- //
export interface NetworkControllerClient {
Expand Down Expand Up @@ -312,5 +313,11 @@ export const NetworkController = {
setTimeout(() => {
ModalController.open({ view: 'UnsupportedChain' })
}, 300)
},

getActiveNetworkTokenAddress() {
const address = ConstantsUtil.NATIVE_TOKEN_ADDRESS[this.state.caipNetwork?.chain || 'evm']

return `${this.state.caipNetwork?.id || 'eip155:1'}:${address}`
}
}
44 changes: 44 additions & 0 deletions packages/core/src/controllers/SendController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CoreHelperUtil } from '../utils/CoreHelperUtil.js'
import { EventsController } from './EventsController.js'
import { NetworkController } from './NetworkController.js'
import { W3mFrameRpcConstants } from '@web3modal/wallet'
import { ChainController } from './ChainController.js'

// -- Types --------------------------------------------- //

Expand Down Expand Up @@ -93,6 +94,21 @@ export const SendController = {
},

sendToken() {
switch (ChainController.state.activeCaipNetwork?.chain) {
case 'evm':
this.sendEvmToken()

return
case 'solana':
this.sendSolanaToken()

return
default:
throw new Error('Unsupported chain')
}
},

sendEvmToken() {
if (this.state.token?.address && this.state.sendTokenAmount && this.state.receiverAddress) {
EventsController.sendEvent({
type: 'track',
Expand Down Expand Up @@ -228,6 +244,34 @@ export const SendController = {
}
},

sendSolanaToken() {
if (!this.state.sendTokenAmount || !this.state.receiverAddress) {
SnackController.showError('Please enter a valid amount and receiver address')

return
}

RouterController.pushTransactionStack({
view: 'Account',
goBack: false
})

ConnectionController.sendTransaction({
chainNamespace: 'solana',
to: this.state.receiverAddress,
value: this.state.sendTokenAmount
})
.then(() => {
this.resetSend()
AccountController.fetchTokenBalance()
})
.catch(error => {
SnackController.showError('Failed to send transaction. Please try again.')
// eslint-disable-next-line no-console
console.error('SendController:sendToken - failed to send solana transaction', error)
})
},

resetSend() {
state.token = undefined
state.sendTokenAmount = undefined
Expand Down
Loading

0 comments on commit 10ab9d5

Please sign in to comment.