- Consolidated duplicate UndoManagers to single instance - Fixed connection promise to only resolve on 'connected' status - Fixed WebSocketProvider import (WebsocketProvider) - Added proper doc.destroy() cleanup - Renamed isPresenceInitialized property to avoid conflict Co-Authored-By: Paperclip <noreply@paperclip.ing>
405 lines
12 KiB
TypeScript
405 lines
12 KiB
TypeScript
import type { AbiStateMutability, Address, Narrow } from 'abitype'
|
|
import * as AbiConstructor from 'ox/AbiConstructor'
|
|
import * as AbiFunction from 'ox/AbiFunction'
|
|
|
|
import { parseAccount } from '../../accounts/utils/parseAccount.js'
|
|
import type { Client } from '../../clients/createClient.js'
|
|
import type { Transport } from '../../clients/transports/createTransport.js'
|
|
import { ethAddress, zeroAddress } from '../../constants/address.js'
|
|
import { deploylessCallViaBytecodeBytecode } from '../../constants/contracts.js'
|
|
import { BaseError } from '../../errors/base.js'
|
|
import type { ErrorType } from '../../errors/utils.js'
|
|
import type { Account } from '../../types/account.js'
|
|
import type { Block } from '../../types/block.js'
|
|
import type { Call, Calls } from '../../types/calls.js'
|
|
import type { Chain } from '../../types/chain.js'
|
|
import type { Log } from '../../types/log.js'
|
|
import type { Hex } from '../../types/misc.js'
|
|
import type { MulticallResults } from '../../types/multicall.js'
|
|
import type { StateOverride } from '../../types/stateOverride.js'
|
|
import type { Mutable } from '../../types/utils.js'
|
|
import {
|
|
type EncodeFunctionDataErrorType,
|
|
encodeFunctionData,
|
|
} from '../../utils/abi/encodeFunctionData.js'
|
|
import { hexToBigInt } from '../../utils/index.js'
|
|
import {
|
|
type CreateAccessListErrorType,
|
|
createAccessList,
|
|
} from './createAccessList.js'
|
|
import {
|
|
type SimulateBlocksErrorType,
|
|
type SimulateBlocksParameters,
|
|
simulateBlocks,
|
|
} from './simulateBlocks.js'
|
|
|
|
const getBalanceCode =
|
|
'0x6080604052348015600e575f80fd5b5061016d8061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063f8b2cb4f1461002d575b5f80fd5b610047600480360381019061004291906100db565b61005d565b604051610054919061011e565b60405180910390f35b5f8173ffffffffffffffffffffffffffffffffffffffff16319050919050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100aa82610081565b9050919050565b6100ba816100a0565b81146100c4575f80fd5b50565b5f813590506100d5816100b1565b92915050565b5f602082840312156100f0576100ef61007d565b5b5f6100fd848285016100c7565b91505092915050565b5f819050919050565b61011881610106565b82525050565b5f6020820190506101315f83018461010f565b9291505056fea26469706673582212203b9fe929fe995c7cf9887f0bdba8a36dd78e8b73f149b17d2d9ad7cd09d2dc6264736f6c634300081a0033'
|
|
|
|
export type SimulateCallsParameters<
|
|
calls extends readonly unknown[] = readonly unknown[],
|
|
account extends Account | Address | undefined = Account | Address | undefined,
|
|
> = Omit<SimulateBlocksParameters, 'blocks' | 'returnFullTransactions'> & {
|
|
/** Account attached to the calls (msg.sender). */
|
|
account?: account | undefined
|
|
/** Calls to simulate. */
|
|
calls: Calls<Narrow<calls>>
|
|
/** State overrides. */
|
|
stateOverrides?: StateOverride | undefined
|
|
/** Whether to trace asset changes. */
|
|
traceAssetChanges?: boolean | undefined
|
|
}
|
|
|
|
export type SimulateCallsReturnType<
|
|
calls extends readonly unknown[] = readonly unknown[],
|
|
> = {
|
|
/** Asset changes. */
|
|
assetChanges: readonly {
|
|
token: {
|
|
address: Address
|
|
decimals?: number | undefined
|
|
symbol?: string | undefined
|
|
}
|
|
value: { pre: bigint; post: bigint; diff: bigint }
|
|
}[]
|
|
/** Block results. */
|
|
block: Block
|
|
/** Call results. */
|
|
results: MulticallResults<
|
|
Narrow<calls>,
|
|
true,
|
|
{
|
|
extraProperties: {
|
|
data: Hex
|
|
gasUsed: bigint
|
|
logs?: Log[] | undefined
|
|
}
|
|
error: Error
|
|
mutability: AbiStateMutability
|
|
}
|
|
>
|
|
}
|
|
|
|
export type SimulateCallsErrorType =
|
|
| AbiFunction.encodeData.ErrorType
|
|
| AbiFunction.from.ErrorType
|
|
| CreateAccessListErrorType
|
|
| EncodeFunctionDataErrorType
|
|
| SimulateBlocksErrorType
|
|
| ErrorType
|
|
|
|
/**
|
|
* Simulates execution of a batch of calls.
|
|
*
|
|
* @param client - Client to use
|
|
* @param parameters - {@link SimulateCallsParameters}
|
|
* @returns Results. {@link SimulateCallsReturnType}
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* import { createPublicClient, http, parseEther } from 'viem'
|
|
* import { mainnet } from 'viem/chains'
|
|
* import { simulateCalls } from 'viem/actions'
|
|
*
|
|
* const client = createPublicClient({
|
|
* chain: mainnet,
|
|
* transport: http(),
|
|
* })
|
|
*
|
|
* const result = await simulateCalls(client, {
|
|
* account: '0x5a0b54d5dc17e482fe8b0bdca5320161b95fb929',
|
|
* calls: [{
|
|
* {
|
|
* data: '0xdeadbeef',
|
|
* to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
|
|
* },
|
|
* {
|
|
* to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
|
|
* value: parseEther('1'),
|
|
* },
|
|
* ]
|
|
* })
|
|
* ```
|
|
*/
|
|
export async function simulateCalls<
|
|
const calls extends readonly unknown[],
|
|
chain extends Chain | undefined,
|
|
account extends Account | Address | undefined = undefined,
|
|
>(
|
|
client: Client<Transport, chain>,
|
|
parameters: SimulateCallsParameters<calls, account>,
|
|
): Promise<SimulateCallsReturnType<calls>> {
|
|
const {
|
|
blockNumber,
|
|
blockTag,
|
|
calls,
|
|
stateOverrides,
|
|
traceAssetChanges,
|
|
traceTransfers,
|
|
validation,
|
|
} = parameters
|
|
|
|
const account = parameters.account
|
|
? parseAccount(parameters.account)
|
|
: undefined
|
|
|
|
if (traceAssetChanges && !account)
|
|
throw new BaseError(
|
|
'`account` is required when `traceAssetChanges` is true',
|
|
)
|
|
|
|
// Derive bytecode to extract ETH balance via a contract call.
|
|
const getBalanceData = account
|
|
? AbiConstructor.encode(AbiConstructor.from('constructor(bytes, bytes)'), {
|
|
bytecode: deploylessCallViaBytecodeBytecode,
|
|
args: [
|
|
getBalanceCode,
|
|
AbiFunction.encodeData(
|
|
AbiFunction.from('function getBalance(address)'),
|
|
[account.address],
|
|
),
|
|
],
|
|
})
|
|
: undefined
|
|
|
|
// Fetch ERC20/721 addresses that were "touched" from the calls.
|
|
const assetAddresses = traceAssetChanges
|
|
? await Promise.all(
|
|
parameters.calls.map(async (call: any) => {
|
|
if (!call.data && !call.abi) return
|
|
const { accessList } = await createAccessList(client, {
|
|
account: account!.address,
|
|
...call,
|
|
data: call.abi ? encodeFunctionData(call) : call.data,
|
|
})
|
|
return accessList.map(({ address, storageKeys }) =>
|
|
storageKeys.length > 0 ? address : null,
|
|
)
|
|
}),
|
|
).then((x) => x.flat().filter(Boolean))
|
|
: []
|
|
|
|
const blocks = await simulateBlocks(client, {
|
|
blockNumber,
|
|
blockTag: blockTag as undefined,
|
|
blocks: [
|
|
...(traceAssetChanges
|
|
? [
|
|
// ETH pre balances
|
|
{
|
|
calls: [{ data: getBalanceData }],
|
|
stateOverrides,
|
|
},
|
|
|
|
// Asset pre balances
|
|
{
|
|
calls: assetAddresses.map((address, i) => ({
|
|
abi: [
|
|
AbiFunction.from(
|
|
'function balanceOf(address) returns (uint256)',
|
|
),
|
|
],
|
|
functionName: 'balanceOf',
|
|
args: [account!.address],
|
|
to: address,
|
|
from: zeroAddress,
|
|
nonce: i,
|
|
})),
|
|
stateOverrides: [
|
|
{
|
|
address: zeroAddress,
|
|
nonce: 0,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
: []),
|
|
|
|
{
|
|
calls: [...calls, { to: zeroAddress }].map((call) => ({
|
|
...(call as Call),
|
|
from: account?.address,
|
|
})) as any,
|
|
stateOverrides,
|
|
},
|
|
|
|
...(traceAssetChanges
|
|
? [
|
|
// ETH post balances
|
|
{
|
|
calls: [{ data: getBalanceData }],
|
|
},
|
|
|
|
// Asset post balances
|
|
{
|
|
calls: assetAddresses.map((address, i) => ({
|
|
abi: [
|
|
AbiFunction.from(
|
|
'function balanceOf(address) returns (uint256)',
|
|
),
|
|
],
|
|
functionName: 'balanceOf',
|
|
args: [account!.address],
|
|
to: address,
|
|
from: zeroAddress,
|
|
nonce: i,
|
|
})),
|
|
stateOverrides: [
|
|
{
|
|
address: zeroAddress,
|
|
nonce: 0,
|
|
},
|
|
],
|
|
},
|
|
|
|
// Decimals
|
|
{
|
|
calls: assetAddresses.map((address, i) => ({
|
|
to: address,
|
|
abi: [
|
|
AbiFunction.from('function decimals() returns (uint256)'),
|
|
],
|
|
functionName: 'decimals',
|
|
from: zeroAddress,
|
|
nonce: i,
|
|
})),
|
|
stateOverrides: [
|
|
{
|
|
address: zeroAddress,
|
|
nonce: 0,
|
|
},
|
|
],
|
|
},
|
|
|
|
// Token URI
|
|
{
|
|
calls: assetAddresses.map((address, i) => ({
|
|
to: address,
|
|
abi: [
|
|
AbiFunction.from(
|
|
'function tokenURI(uint256) returns (string)',
|
|
),
|
|
],
|
|
functionName: 'tokenURI',
|
|
args: [0n],
|
|
from: zeroAddress,
|
|
nonce: i,
|
|
})),
|
|
stateOverrides: [
|
|
{
|
|
address: zeroAddress,
|
|
nonce: 0,
|
|
},
|
|
],
|
|
},
|
|
|
|
// Symbols
|
|
{
|
|
calls: assetAddresses.map((address, i) => ({
|
|
to: address,
|
|
abi: [AbiFunction.from('function symbol() returns (string)')],
|
|
functionName: 'symbol',
|
|
from: zeroAddress,
|
|
nonce: i,
|
|
})),
|
|
stateOverrides: [
|
|
{
|
|
address: zeroAddress,
|
|
nonce: 0,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
traceTransfers,
|
|
validation,
|
|
})
|
|
|
|
const block_results = traceAssetChanges ? blocks[2] : blocks[0]
|
|
const [
|
|
block_ethPre,
|
|
block_assetsPre,
|
|
,
|
|
block_ethPost,
|
|
block_assetsPost,
|
|
block_decimals,
|
|
block_tokenURI,
|
|
block_symbols,
|
|
] = traceAssetChanges ? blocks : []
|
|
|
|
// Extract call results from the simulation.
|
|
const { calls: block_calls, ...block } = block_results
|
|
const results = block_calls.slice(0, -1) ?? []
|
|
|
|
// Extract pre-execution ETH and asset balances.
|
|
const ethPre = block_ethPre?.calls ?? []
|
|
const assetsPre = block_assetsPre?.calls ?? []
|
|
const balancesPre = [...ethPre, ...assetsPre].map((call) =>
|
|
call.status === 'success' ? hexToBigInt(call.data) : null,
|
|
)
|
|
|
|
// Extract post-execution ETH and asset balances.
|
|
const ethPost = block_ethPost?.calls ?? []
|
|
const assetsPost = block_assetsPost?.calls ?? []
|
|
const balancesPost = [...ethPost, ...assetsPost].map((call) =>
|
|
call.status === 'success' ? hexToBigInt(call.data) : null,
|
|
)
|
|
|
|
// Extract asset symbols & decimals.
|
|
const decimals = (block_decimals?.calls ?? []).map((x) =>
|
|
x.status === 'success' ? x.result : null,
|
|
) as (number | null)[]
|
|
const symbols = (block_symbols?.calls ?? []).map((x) =>
|
|
x.status === 'success' ? x.result : null,
|
|
) as (string | null)[]
|
|
const tokenURI = (block_tokenURI?.calls ?? []).map((x) =>
|
|
x.status === 'success' ? x.result : null,
|
|
) as (string | null)[]
|
|
|
|
const changes: Mutable<SimulateCallsReturnType<calls>['assetChanges']> = []
|
|
for (const [i, balancePost] of balancesPost.entries()) {
|
|
const balancePre = balancesPre[i]
|
|
|
|
if (typeof balancePost !== 'bigint') continue
|
|
if (typeof balancePre !== 'bigint') continue
|
|
|
|
const decimals_ = decimals[i - 1]
|
|
const symbol_ = symbols[i - 1]
|
|
const tokenURI_ = tokenURI[i - 1]
|
|
|
|
const token = (() => {
|
|
if (i === 0)
|
|
return {
|
|
address: ethAddress,
|
|
decimals: 18,
|
|
symbol: 'ETH',
|
|
}
|
|
|
|
return {
|
|
address: assetAddresses[i - 1]! as Address,
|
|
decimals: tokenURI_ || decimals_ ? Number(decimals_ ?? 1) : undefined,
|
|
symbol: symbol_ ?? undefined,
|
|
}
|
|
})()
|
|
|
|
if (changes.some((change) => change.token.address === token.address))
|
|
continue
|
|
|
|
changes.push({
|
|
token,
|
|
value: {
|
|
pre: balancePre,
|
|
post: balancePost,
|
|
diff: balancePost - balancePre,
|
|
},
|
|
})
|
|
}
|
|
|
|
return {
|
|
assetChanges: changes,
|
|
block,
|
|
results,
|
|
} as unknown as SimulateCallsReturnType<calls>
|
|
}
|