Skip to content

Wallet integration guide

How to wire @erc7730/sdk into a wallet as a clear-signing drop-in. Structure matches the Sourcify TS guide: smallest call first, then the production path, then the rest.

Terminal window
npm install @erc7730/sdk
# optional peer for adapters / public clients
npm install viem

The published package does not include a descriptor catalog. @erc7730/sdk/lite is a deprecated narrower entry (it omits the Sourcify client and generateDescriptor from that graph). It is not a separate install and it is not how descriptors are loaded. Importing @erc7730/sdk does not register an ABI loader.

createOfficialRegistry() with no pin uses VENDORED_REGISTRY_COMMIT (a commit SHA shipped with the SDK). Paste this, print an intent, then harden:

import { createOfficialRegistry, decodeTransaction } from '@erc7730/sdk';
const registry = createOfficialRegistry();
const result = await decodeTransaction(
{
to: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
data: '0xd0e30db0',
value: 10n ** 18n,
chainId: 1,
},
{ registry }
);
console.log(result.interpolatedIntent ?? result.intent);
console.log(result.fields);

Same call with a frozen SHA and officialOnlyPolicy(). That policy rejects Sourcify / generateDescriptor / inferred / basic as high confidence. Clear signing is not ABI pretty-printing.

import {
createOfficialRegistry,
decodeTransaction,
officialOnlyPolicy,
VENDORED_REGISTRY_COMMIT,
} from '@erc7730/sdk';
const registry = createOfficialRegistry({ pin: VENDORED_REGISTRY_COMMIT });
const tx = {
to: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
data: '0xd0e30db0',
value: 10n ** 18n,
chainId: 1,
} as const;
const result = await decodeTransaction(tx, {
registry,
trust: officialOnlyPolicy(),
});
console.log(result.interpolatedIntent ?? result.intent);
console.log(result.source, result.confidence, result.trust.accepted);

Full source × policy × confidence table: trust-table.md (also /trust on the docs site). Or require ERC-8176 attestations — see Attestations below.

Descriptors live in ethereum/clear-signing-erc7730-registry. Fetch the indexes once at app boot and keep the object yourself:

import {
createOfficialRegistry,
fetchPrebuiltRegistryIndex,
VENDORED_REGISTRY_COMMIT,
} from '@erc7730/sdk';
const pin = VENDORED_REGISTRY_COMMIT;
const indexes = await fetchPrebuiltRegistryIndex({ pin });
const registry = createOfficialRegistry({ pin, indexes });

Passing indexes means those two files are not fetched again. You can also bundle the JSON at build time. With no network, pass indexes and a cache. ERC-20, ERC-721, and WETH builtins (ERC20_DESCRIPTOR, ERC721_DESCRIPTOR, WETH_DESCRIPTOR) stay local fallbacks. They are not a catalog.

App-local descriptors sit on top of the pinned registry. extend() mutates the registry and returns void — do not chain it onto the constructor result.

import {
createOfficialRegistry,
officialOrLocalPolicy,
VENDORED_REGISTRY_COMMIT,
type InputDescriptor,
} from '@erc7730/sdk';
const registry = createOfficialRegistry({ pin: VENDORED_REGISTRY_COMMIT });
const localDescriptors: InputDescriptor[] = [
// Your validated app-local descriptors.
];
registry.extend(localDescriptors);
// Under officialOnlyPolicy(), local overrides are never confidence high.
// When you intentionally accept them:
const trust = officialOrLocalPolicy();

The decode core does no RPC, ENS, or token-list I/O of its own. Inject an ExternalDataProvider when you want resolved token amounts, ENS / local names, or NFT collection labels:

const externalDataProvider = {
resolveToken: async (chainId, address) => ({
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
}),
resolveEnsName: async (address) => 'alice.eth',
resolveLocalName: async (address) => 'Alice',
resolveNftCollectionName: async (chainId, address) => 'Bored Ape Yacht Club',
resolveBlockTimestamp: async (chainId, blockHeight) => 1_715_000_000,
resolveChainInfo: async (chainId) => ({
name: 'Ethereum Mainnet',
symbol: 'ETH',
decimals: 18,
}),
// Same hook attestedPolicy uses for EAS revocation.
chainClient: {
call: async (chainId, { to, data }) => rpcEthCall(chainId, to, data),
},
};
Hook Use
resolveToken ERC-20 decimals / symbol for tokenAmount
resolveEnsName / resolveLocalName Human names for addressName
resolveNftCollectionName Collection label for NFT formats
resolveBlockTimestamp / resolveChainInfo Date and chain metadata
chainClient.call eth_call for EAS revocation under attestedPolicy

Omit a method to fall back to raw formatting / local catalogs. Do not treat KNOWN_TOKENS or Sourcify ABI as trusted clear-signing metadata.

import {
decodeTransaction,
decodeTypedData,
decodeBatch,
decodeUserOp,
format,
composePolicies,
officialOnlyPolicy,
officialOrLocalPolicy,
attestedPolicy,
} from '@erc7730/sdk';
const opts = {
registry,
trust: officialOnlyPolicy(),
externalDataProvider,
trustedTokens,
useSourcifyFallback: false, // default; ABI fallback is opt-in
// Optional: known routers / operators suppress `untrusted_spender`
// spenderAllowlist: [router],
};
const txDisplay = await decodeTransaction(tx, opts);
const typedDisplay = await decodeTypedData(typedData, opts);
const batchDisplay = await decodeBatch(
{ chainId: 1, from: user, calls: [{ to, data }, { to, data }] },
opts
);
const userOpDisplay = await decodeUserOp(
{ chainId: 1, sender: account, callData },
opts
);
Function When
decodeTransaction eth_sendTransaction / eth_signTransaction (Multicall3 / Safe CALL → children)
decodeTypedData eth_signTypedData
decodeBatch EIP-5792 wallet_sendCalls
decodeUserOp ERC-4337 Simple Account execute / executeBatch
format / formatTypedData Compat aliases of decode* (+ default officialOrLocalPolicy when a registry is set)

Batch / nested interpolatedIntent joins per-call sentences with " and ".

The registry cannot hold every ERC-20 / ERC-721. List tokens your wallet already trusts; on a miss the SDK renders from bundled templates. Under officialOnlyPolicy() this path is never confidence: "high".

const trustedTokens = {
1: {
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 'erc20',
'0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d': 'erc721',
},
} as const;

A registry descriptor for the same address always wins over the template.

Recipe Code
Pin only trust: officialOnlyPolicy()
Pin + local extend() trust: officialOrLocalPolicy()
ERC-8176 attesters trust: attestedPolicy({ attesters, eas })
Pin or attested trust: composePolicies([officialOnlyPolicy(), attestedPolicy(...)], 'any')
Pin and attested composePolicies([officialOnlyPolicy(), attestedPolicy(...)], 'all')

trust.reasons are stable codes (source:official-registry:accepted, ATTESTED, …) — safe for telemetry / i18n. Prefer them over free-form sentences.

import { attestedPolicy } from '@erc7730/sdk';
const trustAttested = attestedPolicy({
attesters: ['0x3846c3A30E62075Fa916216b35EF04B8F53931f6'],
eas: {
// Required. eth_call to the EAS contract on Ethereum mainnet for revokeOffchain.
call: async (chainId, { to, data }) => rpcEthCall(chainId, to, data),
},
});

Without eas.call, attestedPolicy fails closed (ATTESTATION_OPTIONS_INCOMPLETE / NO_TRUSTED_ATTESTATION). The SDK never issues attestations.

Load attestation JSON with createOfficialRegistry({ pin, attachAttestations: true }), or set ResolvedDescriptor.attestations yourself in tests.

  • Prefer interpolatedIntent when present (spec option 1). Fields may still be shown.
  • Otherwise show intent + fields.
  • Surface warnings (e.g. NO_TRUSTED_ATTESTATION, interpolation_failed, infinite_approval).
  • Nested format: "calldata" fields expose field.embedded. Multicall3 / Safe CALL / UserOp expose children: DecodedOperation[] with per-child source and trust.
  • locale only formats numbers and dates. Descriptor intent strings are never translated.

Importing @erc7730/sdk, @erc7730/sdk/lite, or @erc7730/sdk/viem does not contact Sourcify and does not install a default ABI loader. fetchFromSourcify stays exported for apps that want the client.

Opt in per call. useSourcifyFallback: true still does nothing until a loader is set:

import {
decodeTransaction,
enableSourcifyAbiLoader,
sourcifyVerifiedAbiLoader,
} from '@erc7730/sdk';
// Process-wide default. Inert until useSourcifyFallback is true.
enableSourcifyAbiLoader();
await decodeTransaction(tx, { useSourcifyFallback: true });
// Or pass the loader on this call only.
await decodeTransaction(tx, {
useSourcifyFallback: true,
loadVerifiedAbi: sourcifyVerifiedAbiLoader,
});

Result source is "sourcify" and confidence is never high. That path verifies an ABI; it is not clear-signing metadata. @erc7730/sdk/lite does not import the Sourcify client. That entry is not the descriptor lookup.