Address resolution
For dApps where contracts move (registries, proxies, upgrades, per-chain deployments), hardcoding addresses in dap.config.js is brittle. DappQL supports resolving contract addresses at call time via a resolver function wired into the provider.
The basics live in Provider setup. This page covers the deeper patterns.
The resolver signature
type AddressResolverFunction = (contractName: string) => `0x${string}`Called with the contract name exactly as it appears in your dap.config.js (the key in contracts: { ... }). Returns the deploy address for that contract.
Wire it into the provider:
<DappQLProvider addressResolver={(name) => addresses[name]!}>
{children}
</DappQLProvider>Every useContextQuery / useQuery / useMutation that references a singleton contract consults this function to resolve the address. Templates still use .at(addr) or the address option (the resolver doesn't apply to templates since they're address-per-use).
Static map: simplest case
const addresses: Record<string, `0x${string}`> = {
Token: '0x...',
Registry: '0x...',
Oracle: '0x...',
}
<DappQLProvider addressResolver={(name) => addresses[name]!}>Use this when you know addresses at build time but want to keep them out of dap.config.js (e.g., because the file is committed and the addresses are per-environment).
Per-chain resolution
import { useChainId } from 'wagmi'
const addressesByChain: Record<number, Record<string, `0x${string}`>> = {
1: { Token: '0x...', Registry: '0x...' }, // Ethereum
8453: { Token: '0x...', Registry: '0x...' }, // Base
10: { Token: '0x...', Registry: '0x...' }, // Optimism
}
function ProviderRoot({ children }) {
const chainId = useChainId()
const resolver = (name: string) => addressesByChain[chainId]?.[name]!
return (
<DappQLProvider addressResolver={resolver}>
{children}
</DappQLProvider>
)
}When the user switches chains, the resolver changes and every subsequent query resolves to the new chain's addresses.
WARNING
DappQL rebuilds the provider's internal value whenever its props change, so a new resolver function on every render causes stale-cache thrash. Wrap the resolver in useCallback or memoize the map:
const resolver = useCallback(
(name: string) => addressesByChain[chainId]?.[name]!,
[chainId],
)Async resolution: AddressResolverComponent
Some addresses only exist after an async lookup (calling an on-chain registry, fetching from an API, reading a subgraph). For that, use AddressResolverComponent, a component that renders alongside children, resolves asynchronously, and calls onResolved when ready:
import { useContextQuery } from '@dappql/react'
import { UndyHq } from './src/contracts'
function Resolver({ onResolved }: { onResolved: (r: AddressResolverFunction) => void }) {
const { data, isLoading } = useContextQuery({
ledger: UndyHq.call.getAddr(1n),
missionControl: UndyHq.call.getAddr(2n),
legoBook: UndyHq.call.getAddr(3n),
})
useEffect(() => {
if (isLoading) return
onResolved((name) => ({
Ledger: data.ledger,
MissionControl: data.missionControl,
LegoBook: data.legoBook,
}[name])!)
}, [isLoading, data])
return null
}
<DappQLProvider AddressResolverComponent={Resolver}>
{children}
</DappQLProvider>Children don't render until the resolver fires. The provider holds back the tree until onResolved is called once, so by the time your components mount, every contract name has a valid address.
WARNING
addressResolver and AddressResolverComponent are mutually exclusive. Passing both throws at runtime.
From a published DappQL SDK
If you're consuming a published DappQL-generated SDK (like @underscore-finance/sdk), the SDK typically exposes its own resolver after a one-time registry load:
import Underscore from '@underscore-finance/sdk'
const underscore = new Underscore()
await underscore.loadAddresses()
<DappQLProvider addressResolver={underscore.addressResolver}>One line wires the SDK's registry-backed resolution into every hook in your app. See SDK generation for the publisher's side.
Hybrid: some static, some resolved
The resolver is a plain function; combine sources freely:
const staticAddresses = {
Token: '0x...',
USDC: '0x...',
}
const dynamicAddresses = await registry.resolve() // from an async source
const resolver = (name: string) =>
staticAddresses[name] ?? dynamicAddresses[name] ?? fallback(name)
<DappQLProvider addressResolver={resolver}>Debugging
If a hook errors with "Contract X has no deploy address," check:
- Is
Xdeclared incontracts: {}indap.config.js? - Is it a template (
isTemplate: true)? Templates ignore the resolver, use.at(addr)or theaddressoption on mutations. - Does the resolver return a valid address for name
X? Log it inside the function. - If using
AddressResolverComponent, hasonResolvedbeen called yet? Children don't render until it does.
Related
- Provider setup,
addressResolverandAddressResolverComponentbasics. - Template contracts,
.at(addr)for per-instance addressing. - SDK generation, publisher's side of registry-backed resolution.