- 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>
265 lines
8.6 KiB
TypeScript
265 lines
8.6 KiB
TypeScript
import type { Address, Narrow } from 'abitype'
|
|
import { parseAccount } from '../../accounts/utils/parseAccount.js'
|
|
import type { Client } from '../../clients/createClient.js'
|
|
import type { Transport } from '../../clients/transports/createTransport.js'
|
|
import { BaseError } from '../../errors/base.js'
|
|
import {
|
|
AtomicityNotSupportedError,
|
|
UnsupportedNonOptionalCapabilityError,
|
|
} from '../../errors/rpc.js'
|
|
import type { ErrorType } from '../../errors/utils.js'
|
|
import type { Account, GetAccountParameter } from '../../types/account.js'
|
|
import type { Call, Calls } from '../../types/calls.js'
|
|
import type { ExtractCapabilities } from '../../types/capabilities.js'
|
|
import type { Chain, DeriveChain } from '../../types/chain.js'
|
|
import type { WalletSendCallsParameters } from '../../types/eip1193.js'
|
|
import type { Hex } from '../../types/misc.js'
|
|
import type { Prettify } from '../../types/utils.js'
|
|
import { encodeFunctionData } from '../../utils/abi/encodeFunctionData.js'
|
|
import type { RequestErrorType } from '../../utils/buildRequest.js'
|
|
import { concat } from '../../utils/data/concat.js'
|
|
import { hexToBigInt } from '../../utils/encoding/fromHex.js'
|
|
import { numberToHex } from '../../utils/encoding/toHex.js'
|
|
import { getTransactionError } from '../../utils/errors/getTransactionError.js'
|
|
import { sendTransaction } from './sendTransaction.js'
|
|
|
|
export const fallbackMagicIdentifier =
|
|
'0x5792579257925792579257925792579257925792579257925792579257925792'
|
|
export const fallbackTransactionErrorMagicIdentifier = numberToHex(0, {
|
|
size: 32,
|
|
})
|
|
|
|
export type SendCallsParameters<
|
|
chain extends Chain | undefined = Chain | undefined,
|
|
account extends Account | undefined = Account | undefined,
|
|
chainOverride extends Chain | undefined = Chain | undefined,
|
|
calls extends readonly unknown[] = readonly unknown[],
|
|
//
|
|
_chain extends Chain | undefined = DeriveChain<chain, chainOverride>,
|
|
> = {
|
|
chain?: chainOverride | Chain | undefined
|
|
calls: Calls<Narrow<calls>>
|
|
capabilities?: ExtractCapabilities<'sendCalls', 'Request'> | undefined
|
|
experimental_fallback?: boolean | undefined
|
|
experimental_fallbackDelay?: number | undefined
|
|
forceAtomic?: boolean | undefined
|
|
id?: string | undefined
|
|
version?: WalletSendCallsParameters[number]['version'] | undefined
|
|
} & GetAccountParameter<account, Account | Address, false, true>
|
|
|
|
export type SendCallsReturnType = Prettify<{
|
|
capabilities?: ExtractCapabilities<'sendCalls', 'ReturnType'> | undefined
|
|
id: string
|
|
}>
|
|
|
|
export type SendCallsErrorType = RequestErrorType | ErrorType
|
|
|
|
/**
|
|
* Requests the connected wallet to send a batch of calls.
|
|
*
|
|
* - Docs: https://viem.sh/docs/actions/wallet/sendCalls
|
|
* - JSON-RPC Methods: [`wallet_sendCalls`](https://eips.ethereum.org/EIPS/eip-5792)
|
|
*
|
|
* @param client - Client to use
|
|
* @returns Transaction identifier. {@link SendCallsReturnType}
|
|
*
|
|
* @example
|
|
* import { createWalletClient, custom } from 'viem'
|
|
* import { mainnet } from 'viem/chains'
|
|
* import { sendCalls } from 'viem/actions'
|
|
*
|
|
* const client = createWalletClient({
|
|
* chain: mainnet,
|
|
* transport: custom(window.ethereum),
|
|
* })
|
|
* const id = await sendCalls(client, {
|
|
* account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
|
|
* calls: [
|
|
* {
|
|
* data: '0xdeadbeef',
|
|
* to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
|
|
* },
|
|
* {
|
|
* to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
|
|
* value: 69420n,
|
|
* },
|
|
* ],
|
|
* })
|
|
*/
|
|
export async function sendCalls<
|
|
const calls extends readonly unknown[],
|
|
chain extends Chain | undefined,
|
|
account extends Account | undefined = undefined,
|
|
chainOverride extends Chain | undefined = undefined,
|
|
>(
|
|
client: Client<Transport, chain, account>,
|
|
parameters: SendCallsParameters<chain, account, chainOverride, calls>,
|
|
): Promise<SendCallsReturnType> {
|
|
const {
|
|
account: account_ = client.account,
|
|
chain = client.chain,
|
|
experimental_fallback,
|
|
experimental_fallbackDelay = 32,
|
|
forceAtomic = false,
|
|
id,
|
|
version = '2.0.0',
|
|
} = parameters
|
|
|
|
const account = account_ ? parseAccount(account_) : null
|
|
|
|
let capabilities = parameters.capabilities
|
|
|
|
if (client.dataSuffix && !parameters.capabilities?.dataSuffix) {
|
|
if (typeof client.dataSuffix === 'string')
|
|
capabilities = {
|
|
...parameters.capabilities,
|
|
dataSuffix: { value: client.dataSuffix, optional: true },
|
|
}
|
|
else
|
|
capabilities = {
|
|
...parameters.capabilities,
|
|
dataSuffix: {
|
|
value: client.dataSuffix.value,
|
|
...(client.dataSuffix.required ? {} : { optional: true }),
|
|
},
|
|
}
|
|
}
|
|
|
|
const calls = parameters.calls.map((call_: unknown) => {
|
|
const call = call_ as Call
|
|
|
|
const data = call.abi
|
|
? encodeFunctionData({
|
|
abi: call.abi,
|
|
functionName: call.functionName,
|
|
args: call.args,
|
|
})
|
|
: call.data
|
|
|
|
return {
|
|
data: call.dataSuffix && data ? concat([data, call.dataSuffix]) : data,
|
|
to: call.to,
|
|
value: call.value ? numberToHex(call.value) : undefined,
|
|
}
|
|
})
|
|
|
|
try {
|
|
const response = await client.request(
|
|
{
|
|
method: 'wallet_sendCalls',
|
|
params: [
|
|
{
|
|
atomicRequired: forceAtomic,
|
|
calls,
|
|
capabilities,
|
|
chainId: numberToHex(chain!.id),
|
|
from: account?.address,
|
|
id,
|
|
version,
|
|
},
|
|
],
|
|
},
|
|
{ retryCount: 0 },
|
|
)
|
|
if (typeof response === 'string') return { id: response }
|
|
return response as never
|
|
} catch (err) {
|
|
const error = err as BaseError
|
|
|
|
// If the transport does not support EIP-5792, fall back to
|
|
// `eth_sendTransaction`.
|
|
if (
|
|
experimental_fallback &&
|
|
(error.name === 'MethodNotFoundRpcError' ||
|
|
error.name === 'MethodNotSupportedRpcError' ||
|
|
error.name === 'UnknownRpcError' ||
|
|
error.details
|
|
.toLowerCase()
|
|
.includes('does not exist / is not available') ||
|
|
error.details.toLowerCase().includes('missing or invalid. request()') ||
|
|
error.details
|
|
.toLowerCase()
|
|
.includes('did not match any variant of untagged enum') ||
|
|
error.details
|
|
.toLowerCase()
|
|
.includes('account upgraded to unsupported contract') ||
|
|
error.details.toLowerCase().includes('eip-7702 not supported') ||
|
|
error.details.toLowerCase().includes('unsupported wc_ method') ||
|
|
// magic.link
|
|
error.details
|
|
.toLowerCase()
|
|
.includes('feature toggled misconfigured') ||
|
|
// Trust Wallet
|
|
error.details
|
|
.toLowerCase()
|
|
.includes(
|
|
'jsonrpcengine: response has no error or result for request',
|
|
))
|
|
) {
|
|
if (capabilities) {
|
|
const hasNonOptionalCapability = Object.values(capabilities).some(
|
|
(capability) => !capability.optional,
|
|
)
|
|
if (hasNonOptionalCapability) {
|
|
const message =
|
|
'non-optional `capabilities` are not supported on fallback to `eth_sendTransaction`.'
|
|
throw new UnsupportedNonOptionalCapabilityError(
|
|
new BaseError(message, {
|
|
details: message,
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
if (forceAtomic && calls.length > 1) {
|
|
const message =
|
|
'`forceAtomic` is not supported on fallback to `eth_sendTransaction`.'
|
|
throw new AtomicityNotSupportedError(
|
|
new BaseError(message, {
|
|
details: message,
|
|
}),
|
|
)
|
|
}
|
|
|
|
const promises: Promise<Hex>[] = []
|
|
for (const call of calls) {
|
|
const promise = sendTransaction(client, {
|
|
account,
|
|
chain,
|
|
data: call.data,
|
|
to: call.to,
|
|
value: call.value ? hexToBigInt(call.value) : undefined,
|
|
})
|
|
promises.push(promise)
|
|
|
|
// Note: some browser wallets require a small delay between transactions
|
|
// to prevent duplicate JSON-RPC requests.
|
|
if (experimental_fallbackDelay > 0)
|
|
await new Promise((resolve) =>
|
|
setTimeout(resolve, experimental_fallbackDelay),
|
|
)
|
|
}
|
|
|
|
const results = await Promise.allSettled(promises)
|
|
if (results.every((r) => r.status === 'rejected')) throw results[0].reason
|
|
|
|
const hashes = results.map((result) => {
|
|
if (result.status === 'fulfilled') return result.value
|
|
return fallbackTransactionErrorMagicIdentifier
|
|
})
|
|
return {
|
|
id: concat([
|
|
...hashes,
|
|
numberToHex(chain!.id, { size: 32 }),
|
|
fallbackMagicIdentifier,
|
|
]),
|
|
}
|
|
}
|
|
|
|
throw getTransactionError(err as BaseError, {
|
|
...parameters,
|
|
account,
|
|
chain: parameters.chain!,
|
|
})
|
|
}
|
|
}
|