@paralock/evm
Reference for the viem clients of ParalockHTLC and ParalockAdaptorEscrow, the verified network registry, Tempo specifics, errors and ABIs.
@paralock/evm provides viem clients for Paralock's two EVM escrows, on Tempo and any other EVM chain (Base, Ethereum, Arbitrum, Robinhood Chain, HyperEVM, local anvils):
ParalockHTLC(htlc mode): a sha256 hash-time-locked escrow.createEvmHtlc.ParalockAdaptorEscrow(adaptor mode): releases to a fixed recipient on a one-time signer's signature overclaimDigest(id). No hash on-chain.createAdaptorEscrow.
Both contracts store one slot per swap, pay only the recipient fixed at lock time (anyone may submit the claim, for example a relayer), and mark ids and hashlocks settled forever. See Contracts and deployments for the contracts themselves and their addresses.
Install
npm install @paralock/evm viemVersion 0.2.0. Its only dependency is viem. Amounts are bigint token base units (the supported stablecoins have 6 decimals: 25 USDC = 25_000_000n). Expiries are Unix seconds.
createEvmHtlc
function createEvmHtlc(options: CreateEvmHtlcOptions): EvmHtlcimport { createEvmHtlc, knownNetwork } from "@paralock/evm";
import type { Hex } from "viem";
const base = knownNetwork(84532)!; // Base Sepolia
const usdc = base.tokens.USDC!;
const htlc = createEvmHtlc({
rpcUrl: base.rpcUrl!,
chainId: base.chainId,
contract: HTLC,
account: process.env.EVM_KEY as Hex,
fromBlock: DEPLOY_BLOCK, // the contract's deployment block
});
const swap = {
hashlock, // sha256 of the 32-byte secret: the same value as the Zcash HTLC's
token: usdc,
sender: htlc.walletClient!.account.address,
recipient,
amount: 25_000_000n, // 25 USDC
expiry: BigInt(Math.floor(Date.now() / 1000) + 3600), // Unix seconds, 1 h from now
};
await htlc.approveIfNeeded(usdc, swap.amount); // exact allowance, only if needed
await htlc.lock(swap);
const preimage = await htlc.waitForClaimPreimage(hashlock, { timeoutMs: 3_600_000 }); // the secret, once claimedOptions
CreateEvmHtlcOptions and CreateAdaptorEscrowOptions share these fields (EvmConnectionOptions plus the scan settings):
| Option | Type | Default | Meaning |
|---|---|---|---|
rpcUrl | string | required | JSON-RPC endpoint. |
chainId | number | required | Expected chain id. Not checked here; the SDK checks it against the node before a swap. |
contract | Address | required | Paralock or Paralock address. |
account | Account | | none | Signer: a viem Account or a 0x private key. Omit it for a read-only client; sending then throws. |
fromBlock | bigint | 0n | First block to scan for events: the contract's deployment block. Set it on public chains. |
maxBlockRange | bigint | 50_ | Largest block span per eth_ request (public RPCs cap it). |
feeToken | Address | none | Tempo only: the TIP-20 that pays gas. Implies tempo: true. |
tempo | boolean | by chain id | Use Tempo's transaction format. Default: true when fee is set or chain is in TEMPO_. |
pollingIntervalMs | number | 500 | Poll interval for receipts and waiters, in milliseconds. |
rpcTimeoutMs | number | 10_ | Per-request RPC timeout, in milliseconds. |
rpcRetryCount | number | 3 | Retries per RPC request on network errors and 5xx or 429 responses, with exponential backoff. |
receiptTimeoutMs | number | 180_ | How long to wait for a sent transaction to be mined before throwing, in milliseconds. |
EvmHtlc
| Member | Signature | Behavior |
|---|---|---|
address, chain, publicClient, walletClient | The contract and the viem clients. wallet is undefined without an account. | |
approveIfNeeded | approve | Approves exactly amount unless the allowance already covers it. Never unlimited. |
lock | lock( | Locks funds. The account must be swap. and have approved the contract. |
claim | claim( | Pays swap. by revealing the 32-byte secret. Checks sha256( locally first. Any account may submit it. |
refund | refund( | Returns the funds to swap. once swap. has passed. Any account may submit it. |
getState | get | { status: "empty" |. |
getLockedSwap | get | The swap rebuilt from its Locked event and checked against the on-chain commitment. |
getLock | get | The same, plus block and transaction of the lock. Confirmations = latest block − block + 1. |
waitForLocked | wait | Resolves once a swap is locked (at once if it already is). |
waitForClaimPreimage | wait | Resolves with the preimage once claimed. Rejects with Swap if refunded, Htlc on timeout. |
tokenBalance | token | ERC-20 or TIP-20 balance. |
WaitOptions is { timeoutMs?: number; signal?: AbortSignal }. Without timeoutMs a waiter waits forever. TxResult is { hash: Hex; blockNumber: bigint; gasUsed: bigint }, returned once the transaction is mined and successful.
createAdaptorEscrow
function createAdaptorEscrow(options: CreateAdaptorEscrowOptions): AdaptorEscrowSame options as createEvmHtlc, with contract set to the ParalockAdaptorEscrow address.
import { createAdaptorEscrow, randomSalt } from "@paralock/evm";
const escrow = createAdaptorEscrow({ rpcUrl, chainId: 84532, contract: ESCROW, account, fromBlock: DEPLOY_BLOCK });
const swap = { token: usdc, sender, recipient, signer, amount: 25_000_000n, expiry, salt: randomSalt() };
await escrow.approveIfNeeded(usdc, swap.amount);
const { id } = await escrow.lock(swap);
const digest = escrow.claimDigest(id); // what `signer` signs: raw, no EIP-191 prefix
// Later, anyone submits signer's signature; the funds go to swap.recipient.
await escrow.claim(swap, signature);
const { v, r, s } = await escrow.waitForClaimSignature(id, { timeoutMs: 60_000 });AdaptorEscrow
| Member | Signature | Behavior |
|---|---|---|
address, chainId, chain, publicClient, walletClient | The contract, its configured chain id and the viem clients. | |
approveIfNeeded | approve | Exact allowance, only if needed. |
lock | lock( | Locks funds and resolves with the swap id. Checks that the contract computes the same id. |
claim | claim( | Pays swap. with swap.'s signature over claim. Normalizes the signature (normalize) and checks it recovers to swap. before sending. Any account may submit it. |
refund | refund( | Returns the funds to swap. once swap. has passed. |
idOf | id | The swap id, computed locally (keccak256(), identical to the contract's id. |
claimDigest | claim | The digest the signer signs, computed locally for this chain and contract. Synchronous. |
getState | get | { status: "empty" |. |
getLockedSwap, getLock | (, Promise< | The swap rebuilt from its Locked event and checked against its id; get adds block and transaction. |
waitForLocked | wait | Resolves once locked. |
waitForClaimSignature | wait | Resolves with the published { v, once claimed. Rejects with Swap if refunded, Htlc on timeout. |
tokenBalance | token | balance. |
Swap data
| Type | Fields |
|---|---|
HtlcSwap | hashlock: Hex, token: Address, sender: Address (locks, refunded after expiry), recipient: Address (paid on claim), amount: bigint, expiry: bigint (Unix seconds from which only a refund is possible) |
AdaptorSwap | token, sender, recipient, signer: Address (whose signature releases the funds), amount, expiry, salt: Hex (32 random bytes that make the id unique and unlinkable) |
LockInfo< | swap: S, block, transaction |
ClaimSignature | v: number (27 or 28), r: Hex, s: Hex (low) |
ClaimSignatureInput | r: Hex, s: Hex, and v?: number | (0, 1, 27 or 28) or viem's y |
| Helper | Meaning |
|---|---|
hashlockOf( | sha256 of a 32-byte secret: the hashlock on both chains. |
commitmentOf( | keccak256(: what Paralock stores while a swap is active. |
statusOf( | Maps a state word: 0 empty, 1 (SETTLED_) settled, anything else active. |
SETTLED_STATE, EMPTY_STATE | The two fixed state words. |
adaptorSwapId( | The escrow id, exactly as the contract computes it. |
adaptorClaimDigest( | keccak256(. |
ADAPTOR_CLAIM_TYPEHASH | keccak256(. |
normalizeClaimSignature( | v to 27 or 28; a high s becomes n − s with the recovery bit flipped. Throws on an out-of-range value. |
randomSalt( | 32 random bytes for Adaptor. |
adaptorStatusOf( | Maps the contract's Status enum. |
assertBytes32( | Throws a Type unless value is 0x plus 64 hex characters. |
Known networks
KNOWN_NETWORKS is a registry of public networks, each value checked on 2026-10-11 against the chain's official docs (and Circle's USDC list), with a live eth_chainId call per RPC and symbol() / decimals() per token. A field is left out where no official source gives it.
| Export | Signature |
|---|---|
KNOWN_NETWORKS | readonly Known |
knownNetwork | known |
knownNetworks | known (mainnet first) |
KnownNetwork is { chain, chainId, name, testnet, rpcUrl?, explorerUrl?, nativeCurrency?, usdc?, tokens: Record<string, Address> }.
| Network | chain | Chain id | Testnet | Official RPC | Explorer |
|---|---|---|---|---|---|
| Tempo | tempo | 4217 | no | https:/ | https:/ |
| Tempo Testnet (Moderato) | tempo | 42431 | yes | https:/ | https:/ |
| Base | base | 8453 | no | https:/ | https:/ |
| Base Sepolia | base | 84532 | yes | https:/ | https:/ |
| Ethereum | ethereum | 1 | no | none (bring your own) | none |
| Sepolia | ethereum | 11155111 | yes | none (bring your own) | https:/ |
| Arbitrum One | arbitrum | 42161 | no | https:/ | https:/ |
| Arbitrum Sepolia | arbitrum | 421614 | yes | https:/ | https:/ |
| Robinhood Chain | robinhood | 4663 | no | https:/ | https:/ |
| Robinhood Chain Testnet | robinhood | 46630 | yes | https:/ | https:/ |
| HyperEVM | hyperevm | 999 | no | https:/ | https:/ |
| HyperEVM Testnet | hyperevm | 998 | yes | https:/ | none |
Stablecoins (tokens; all have 6 decimals):
| Network | Tokens |
|---|---|
| Tempo, Moderato | OUSD 0x20c0000000000000000000006a37, path 0x20c0000000000000000000000000000000000000, USDC. (Stargate-bridged; 0x20c000000000000000000000b9537d11c60e8b50 on Tempo, 0x20c0000000000000000000009e8d7eb59b783726 on Moderato) |
| Base | USDC 0x833589f |
| Base Sepolia | USDC 0x036 |
| Ethereum | USDC 0x |
| Sepolia | USDC 0x1c7 |
| Arbitrum One | USDC 0xaf88d065e77c8c |
| Arbitrum Sepolia | USDC 0x75faf114eafb1 |
| Robinhood Chain | USDG 0x5fc5360 |
| Robinhood Chain Testnet | none (the Paralock deployment uses a MockUSD test token, see deployments) |
| HyperEVM | USDC 0xb88339 |
| HyperEVM Testnet | USDC 0x2 |
The SDK's createTaker uses these tokens as the default accepted stablecoins for a chain id. plainEvmChain uses the name, gas token and explorer.
Tempo
Tempo has no native gas token: fees are paid in TIP-20 USD stablecoins. Every TIP-20 token lives at an address starting with 0x20c0….
- Fee token. With
feeTokenset, every transaction is sent as a Tempo (0x76) transaction paying fees in that token. Set it on Moderato and mainnet to a stablecoin the account holds, for exampleTEMPO_TOKENS.PathUSDorOUSD. Without it, plain EIP-1559 transactions are sent and the node picks the account's fee token (its FeeManager preference, preset for anvil dev accounts). - Chain detection.
TEMPO_CHAIN_IDSis[4217, 42431, 31318, 1337, 31337]: mainnet, Moderato, Tempo devnet,tempo node --dev, and the Paralock devnet'sanvil --network tempo(ANVIL_TEMPO_CHAIN_ID, 31337). 31337 is also plain anvil's default id: passtempo: falseto use it as an ordinary EVM chain. - Memos. On TIP-20 tokens the contracts move funds with
transferFromWithMemo/transferWithMemo, with the hashlock or escrow id as memo, so explorers can link every movement to its swap. - Storage cost. Each new storage slot costs about 250k gas (TIP-1000), so a
ParalockHTLClock costs about 305k gas on a local Tempo chain.
import { TEMPO_TOKENS, createEvmHtlc } from "@paralock/evm";
const htlc = createEvmHtlc({
rpcUrl: "https://rpc.moderato.tempo.xyz",
chainId: 42431,
contract: HTLC,
account,
fromBlock: DEPLOY_BLOCK,
feeToken: TEMPO_TOKENS.PathUSD, // pay gas in pathUSD
});TEMPO_TOKENS lists PathUSD, AlphaUSD, BetaUSD, ThetaUSD (dev tokens, pre-funded on local chains and from the Moderato faucet) and OUSD (Open USD on Tempo mainnet and Moderato).
| Chain helper | Meaning |
|---|---|
evmChain( | The viem chain: Tempo's config on Tempo chains, a plain one elsewhere. Throws if fee is set on a non-Tempo chain. |
tempoChain( | A viem Tempo chain. Mainnet and Moderato keep their explorer metadata; any other id is treated as a local Tempo node. |
plainEvmChain( | A plain EVM chain. Known networks get their name, gas token and explorer; anything else is an ETH chain. |
isTempoChainId( | True for an id in TEMPO_. |
createEvmClients( | { chain, for a chain. |
Errors
| Class | Fields | When |
|---|---|---|
TransactionRevertedError | hash | A sent transaction was mined but reverted. |
SwapRefundedError | id, hashlock (the same value) | A claim waiter found that the swap was refunded instead. |
HtlcTimeoutError | A waiter gave up after timeout. Thrown by both clients. |
Every write is simulated first (simulateContract), so a call that would revert throws viem's contract error, carrying the contract's custom error (for example SwapExpired), before any transaction is sent. Plain Errors are thrown for a read-only client asked to send, a preimage that doesn't match the hashlock, a claim signature that doesn't recover to swap.signer, and a Locked event that doesn't match the on-chain state.
ABIs and deployment
| Export | Meaning |
|---|---|
paralockHtlcAbi, paralockHtlcBytecode | Paralock. |
paralockAdaptorEscrowAbi, paralockAdaptorEscrowBytecode | Paralock. |
mockUsdAbi, mockUsdBytecode | Mock, a 6-decimal test token. |
deployParalockHtlc(, deployParalockAdaptorEscrow(, deployMockUsd( | Deploy to local chains and tests. Options: Evm with a required account. Resolve with Tx. For real networks use contracts/ (see Contracts). |
The ABIs are generated from the Foundry build (pnpm --filter @paralock/evm generate:abi) and typed as const, so viem infers argument and event types.
The aliases from the package's earlier name, @paralock/tempo, remain: createTempoHtlc, createTempoClients, TempoHtlc, TempoClients, TempoConnectionOptions, CreateTempoHtlcOptions.
Security notes
- Set
fromBlockto the deployment block on public chains: lock lookups scan logs from there. getLockandgetLockedSwaprebuild a swap from itsLockedevent and check it against the on-chain commitment (HTLC) or id (adaptor escrow). UsegetLock().blockNumberto require confirmations before acting on a lock.- Allowances are exact, never unlimited.
- Fee-on-transfer and other non-standard ERC-20s are rejected by the contracts.