viem
Version:
368 lines (341 loc) • 12 kB
text/typescript
import type { Abi, AbiStateMutability, Address } from 'abitype'
import type { Account } from '../../accounts/types.js'
import type { Client } from '../../clients/createClient.js'
import type { Transport } from '../../clients/transports/createTransport.js'
import { erc20Abi } from '../../constants/abis.js'
import type { ResolvedToken, Token, Tokens } from '../../tokens/defineToken.js'
import type { Chain } from '../../types/chain.js'
import type {
ContractFunctionName,
ContractFunctionParameters,
ExtractAbiItem,
} from '../../types/contract.js'
import type { Hex } from '../../types/misc.js'
import type { UnionPick } from '../../types/utils.js'
import { isAddress } from '../../utils/address/isAddress.js'
import { isAddressEqual } from '../../utils/address/isAddressEqual.js'
import { encodeFunctionData } from '../../utils/index.js'
import { formatUnits } from '../../utils/unit/formatUnits.js'
import { parseUnits } from '../../utils/unit/parseUnits.js'
import type { ReadContractParameters as viem_ReadContractParameters } from '../public/readContract.js'
import { readContract } from '../public/readContract.js'
import type { WriteContractSyncParameters as viem_WriteContractSyncParameters } from '../wallet/writeContractSync.js'
/**
* Union of symbols of the Client's declared `tokens` whose `addresses` include
* the Client's `chain.id`. Collapses to `never` when there is no chain mapping
* (no `tokens`, no `chain`, a widened `chain.id`, or widened token symbols).
*
* @internal
*/
export type TokenName<
chain extends Chain | undefined,
tokens extends Tokens | undefined,
> = chain extends { id: infer chainId extends number }
? number extends chainId
? never
: tokens extends Tokens
? TokenSymbolForChain<tokens[number], chainId>
: never
: never
type TokenSymbolForChain<token, chainId extends number> = token extends Token
? chainId extends keyof token['addresses']
? token['symbol'] extends string
? string extends token['symbol']
? never
: Lowercase<token['symbol']>
: never
: never
: never
/**
* Selects an ERC-20 token by `token`, which is either the symbol of a token
* declared on the Client's `tokens` array or a contract `address`.
*
* When `token` is a declared symbol (or an address that matches a declared
* token), token metadata is resolved from the Client's `tokens` array.
*/
export type TokenParameter<
chain extends Chain | undefined,
tokens extends Tokens | undefined,
> = {
/**
* Token to operate on: either the symbol of a token declared on the Client's
* `tokens` array (with an address for the Client's `chain.id`), or a
* contract `address`.
*/
token: TokenName<chain, tokens> | Address
}
export type TokenParameters<
chain extends Chain | undefined,
tokens extends Tokens | undefined,
> = TokenParameter<chain, tokens> & {
/**
* Decimals used to convert between base units and the human-readable amount.
* Inferred from the Client's `tokens` array when `token` matches a declared
* token; otherwise fetched from the token contract when needed.
*/
decimals?: number | undefined
}
/**
* A token amount, expressed both in the token's base units (`amount`) and as a
* human-readable decimal string (`formatted`, derived from the token's
* `decimals`).
*/
export type Amount = {
/** Amount in the token's base units. */
amount: bigint
/** Token decimals used to derive `formatted`. */
decimals: number
/** Amount formatted as a human-readable decimal string. */
formatted: string
}
/** A write amount: a base-unit bigint, or an explicit formatted helper. */
export type AmountInput =
| bigint
| {
/** Token decimals used to parse `formatted`. */
decimals?: number | undefined
/** Human-readable decimal amount. */
formatted: string
}
/**
* Shapes a base-unit value into an {@link Amount} (base units, `decimals`, and
* human-readable `formatted` string).
*
* @param amount - Amount in base units.
* @param decimals - Token decimals used to format the amount.
* @returns The {@link Amount}.
*/
export function toAmount(amount: bigint, decimals: number): Amount {
return { amount, decimals, formatted: formatUnits(amount, decimals) }
}
/**
* Resolves a write amount to base units.
*
* @param amount - Base-unit amount, or formatted helper.
* @param decimals - Token decimals used to parse formatted amounts.
* @returns Amount in base units.
*/
export function toBaseUnits(
amount: AmountInput,
decimals: number | undefined,
): bigint {
if (typeof amount === 'bigint') return amount
const resolved = amount.decimals ?? decimals
return parseUnits(amount.formatted, requireTokenDecimals(resolved))
}
/**
* Requires resolved token decimals for parsing or formatting amounts.
*
* @param decimals - Token decimals.
* @returns Token decimals.
* @internal
*/
export function requireTokenDecimals(decimals: number | undefined): number {
if (decimals === undefined)
throw new Error(
'Token decimals are required. Pass `amount.decimals` or select a declared token.',
)
return decimals
}
/**
* Resolves the decimals used for formatting a write amount.
*
* @param amount - Base-unit amount, or formatted helper.
* @param decimals - Token decimals.
* @returns Token decimals, overridden by the formatted helper when present.
*/
export function resolveAmountDecimals(
amount: AmountInput,
decimals: number | undefined,
): number | undefined {
if (typeof amount === 'bigint') return decimals
return amount.decimals ?? decimals
}
/**
* Resolves the token contract `address` and `decimals` from a `token`, which is
* either the symbol of a token declared on the client's `tokens` array or a
* contract `address`.
*
* When `token` is a declared symbol, the `address` and `decimals` are resolved
* from the Client's `tokens` array for the Client's `chain.id` (`decimals` can
* be overridden via the explicit `decimals`). When `token` is an address, its
* `decimals` is inferred from the Client's `tokens` array when the address
* matches a declared token, otherwise taken from the explicit `decimals`.
*
* @param client - Client.
* @param parameters - Parameters.
* @returns The resolved `address` and `decimals`.
*/
export function resolveToken(
client: Client<Transport, Chain | undefined>,
parameters: resolveToken.Parameters,
): { address: Address; decimals: number | undefined } {
const { decimals, token } = parameters
const declared = findDeclaredToken(client, token)
if (declared)
return {
address: declared.address,
decimals: decimals ?? declared.decimals,
}
if (isAddress(token, { strict: false }))
return {
address: token,
decimals: decimals ?? inferDecimals(client, token),
}
throw new Error(
`Token "${token}" is not a declared ERC-20 token on the client's \`tokens\` array (with an address for the client's chain), and is not a valid address.`,
)
}
export namespace resolveToken {
export type Parameters = {
/** Decimals, if provided explicitly. */
decimals?: number | undefined
/** Token symbol (declared on the client's `tokens` array) or contract address. */
token: string
}
}
/**
* Finds the declared token on the Client's `tokens` array matching `token`
* (either a token symbol or contract `address`), resolved to a {@link ResolvedToken}
* for the Client's `chain.id`. Returns `undefined` when no declared token
* matches, or the matching token has no address for the Client's chain.
*
* @param client - Client.
* @param token - Token symbol (declared on the client's `tokens` array) or contract address.
* @returns The matching resolved token config, or `undefined`.
*/
export function findDeclaredToken(
client: Client<Transport, Chain | undefined>,
token: string,
): ResolvedToken | undefined {
const tokens = client.tokens
const chainId = client.chain?.id
if (!tokens || chainId === undefined) return undefined
const bySymbol = findTokenBySymbol(tokens, token)
if (bySymbol) return resolveTokenForChain(bySymbol, chainId)
if (isAddress(token, { strict: false }))
for (const token_ of tokens) {
const resolved = resolveTokenForChain(token_, chainId)
if (resolved && isAddressEqual(resolved.address, token)) return resolved
}
return undefined
}
/**
* Resolves a {@link Token} to a {@link ResolvedToken} for `chainId`, or
* `undefined` when the token has no address for `chainId`. @internal
*/
function resolveTokenForChain(
token: Token,
chainId: number,
): ResolvedToken | undefined {
const address = (token.addresses as Record<number, Address>)[chainId]
if (!address) return undefined
return {
address,
currency: token.currency,
decimals: token.decimals,
name: token.name,
popular: token.popular,
symbol: token.symbol,
}
}
/** Finds a {@link Token} by symbol (case-insensitively) on a `tokens` array. @internal */
function findTokenBySymbol(tokens: Tokens, symbol: string): Token | undefined {
const lowerSymbol = symbol.toLowerCase()
for (const token of tokens) {
if (token.symbol?.toLowerCase() === lowerSymbol) return token
}
return undefined
}
/**
* Infers a token's `decimals` from the Client's `tokens` array by matching
* `address` against each token's address for the Client's `chain.id`.
* @internal
*/
function inferDecimals(
client: Client<Transport, Chain | undefined>,
address: Address,
): number | undefined {
const tokens = client.tokens
const chainId = client.chain?.id
if (tokens && chainId !== undefined)
for (const token of tokens) {
const resolved = resolveTokenForChain(token, chainId)
if (resolved && isAddressEqual(resolved.address, address))
return resolved.decimals
}
return undefined
}
/**
* Resolves token decimals, fetching from the token contract when they are not
* provided explicitly or declared on the client.
* @internal
*/
export async function resolveTokenWithDecimals(
client: Client<Transport, Chain | undefined>,
parameters: resolveToken.Parameters,
): Promise<{ address: Address; decimals: number }> {
const { address, decimals } = resolveToken(client, parameters)
if (decimals !== undefined) return { address, decimals }
return {
address,
decimals: await readContract(client, {
abi: erc20Abi,
address,
functionName: 'decimals',
}),
}
}
/**
* Picks the transaction-override fields shared by write actions, so the
* action-specific args (`token`, `amount`, `to`, etc.) don't leak into
* `estimateContractGas` / `simulateContract` requests. @internal
*/
export function pickWriteParameters(parameters: Record<string, unknown>) {
const { account, chain, gas, maxFeePerGas, maxPriorityFeePerGas, nonce } =
parameters
return { account, chain, gas, maxFeePerGas, maxPriorityFeePerGas, nonce }
}
export type ReadParameters = Pick<
viem_ReadContractParameters<never, never, never>,
'account' | 'blockNumber' | 'blockOverrides' | 'blockTag' | 'stateOverride'
>
export type WriteParameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = UnionPick<
viem_WriteContractSyncParameters<never, never, never, chain, account>,
| 'account'
| 'chain'
| 'gas'
| 'maxFeePerGas'
| 'maxPriorityFeePerGas'
| 'nonce'
| 'throwOnReceiptRevert'
>
export function defineCall<
const abi extends Abi,
const functionName extends ContractFunctionName<abi, AbiStateMutability>,
call extends ContractFunctionParameters<
abi,
AbiStateMutability,
functionName
>,
>(
call:
| call
| ContractFunctionParameters<abi, AbiStateMutability, functionName>,
): ContractFunctionParameters<
[ExtractAbiItem<abi, functionName>],
AbiStateMutability,
functionName
> & {
data: Hex
to: Address
} {
return {
...(call as any),
data: encodeFunctionData(call as never),
to: call.address,
} as const
}