Documentation

Reference

View as Markdown

@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 over claimDigest(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

Shell
npm install @paralock/evm viem

Version 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

TypeScript
function createEvmHtlc(options: CreateEvmHtlcOptions): EvmHtlc
TypeScript
import { 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 claimed

Options

CreateEvmHtlcOptions and CreateAdaptorEscrowOptions share these fields (EvmConnectionOptions plus the scan settings):

OptionTypeDefaultMeaning
rpcUrlstringrequiredJSON-RPC endpoint.
chainIdnumberrequiredExpected chain id. Not checked here; the SDK checks it against the node before a swap.
contractAddressrequiredParalockHTLC or ParalockAdaptorEscrow address.
accountAccount | HexnoneSigner: a viem Account or a 0x private key. Omit it for a read-only client; sending then throws.
fromBlockbigint0nFirst block to scan for events: the contract's deployment block. Set it on public chains.
maxBlockRangebigint50_000nLargest block span per eth_getLogs request (public RPCs cap it).
feeTokenAddressnoneTempo only: the TIP-20 that pays gas. Implies tempo: true.
tempobooleanby chain idUse Tempo's transaction format. Default: true when feeToken is set or chainId is in TEMPO_CHAIN_IDS.
pollingIntervalMsnumber500Poll interval for receipts and waiters, in milliseconds.
rpcTimeoutMsnumber10_000Per-request RPC timeout, in milliseconds.
rpcRetryCountnumber3Retries per RPC request on network errors and 5xx or 429 responses, with exponential backoff.
receiptTimeoutMsnumber180_000How long to wait for a sent transaction to be mined before throwing, in milliseconds.

EvmHtlc

MemberSignatureBehavior
address, chain, publicClient, walletClientThe contract and the viem clients. walletClient is undefined without an account.
approveIfNeededapproveIfNeeded(token: Address, amount: bigint): Promise<TxResult | undefined>Approves exactly amount unless the allowance already covers it. Never unlimited.
locklock(swap: HtlcSwap): Promise<TxResult>Locks funds. The account must be swap.sender and have approved the contract.
claimclaim(swap: HtlcSwap, preimage: Hex): Promise<TxResult>Pays swap.recipient by revealing the 32-byte secret. Checks sha256(preimage) locally first. Any account may submit it.
refundrefund(swap: HtlcSwap): Promise<TxResult>Returns the funds to swap.sender once swap.expiry has passed. Any account may submit it.
getStategetState(hashlock: Hex): Promise<HtlcState>{ status: "empty" | "active" | "settled", raw }.
getLockedSwapgetLockedSwap(hashlock: Hex): Promise<HtlcSwap | undefined>The swap rebuilt from its Locked event and checked against the on-chain commitment.
getLockgetLock(hashlock: Hex): Promise<LockInfo<HtlcSwap> | undefined>The same, plus blockNumber and transactionHash of the lock. Confirmations = latest block − blockNumber + 1.
waitForLockedwaitForLocked(hashlock: Hex, options?: WaitOptions): Promise<HtlcSwap>Resolves once a swap is locked (at once if it already is).
waitForClaimPreimagewaitForClaimPreimage(hashlock: Hex, options?: WaitOptions): Promise<Hex>Resolves with the preimage once claimed. Rejects with SwapRefundedError if refunded, HtlcTimeoutError on timeout.
tokenBalancetokenBalance(token: Address, owner: Address): Promise<bigint>ERC-20 or TIP-20 balanceOf.

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

TypeScript
function createAdaptorEscrow(options: CreateAdaptorEscrowOptions): AdaptorEscrow

Same options as createEvmHtlc, with contract set to the ParalockAdaptorEscrow address.

TypeScript
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

MemberSignatureBehavior
address, chainId, chain, publicClient, walletClientThe contract, its configured chain id and the viem clients.
approveIfNeededapproveIfNeeded(token: Address, amount: bigint): Promise<TxResult | undefined>Exact allowance, only if needed.
locklock(swap: AdaptorSwap): Promise<TxResult & { id: Hex }>Locks funds and resolves with the swap id. Checks that the contract computes the same id.
claimclaim(swap: AdaptorSwap, signature: ClaimSignatureInput): Promise<TxResult>Pays swap.recipient with swap.signer's signature over claimDigest(id). Normalizes the signature (normalizeClaimSignature) and checks it recovers to swap.signer before sending. Any account may submit it.
refundrefund(swap: AdaptorSwap): Promise<TxResult>Returns the funds to swap.sender once swap.expiry has passed.
idOfidOf(swap: AdaptorSwap): HexThe swap id, computed locally (keccak256(abi.encode(swap))), identical to the contract's idOf.
claimDigestclaimDigest(id: Hex): HexThe digest the signer signs, computed locally for this chain and contract. Synchronous.
getStategetState(id: Hex): Promise<AdaptorState>{ status: "empty" | "active" | "settled", raw: 0 | 1 | 2 }.
getLockedSwap, getLock(id: Hex) => Promise<AdaptorSwap | undefined>, Promise<LockInfo<AdaptorSwap> | undefined>The swap rebuilt from its Locked event and checked against its id; getLock adds blockNumber and transactionHash.
waitForLockedwaitForLocked(id: Hex, options?: WaitOptions): Promise<AdaptorSwap>Resolves once locked.
waitForClaimSignaturewaitForClaimSignature(id: Hex, options?: WaitOptions): Promise<ClaimSignature>Resolves with the published { v, r, s } once claimed. Rejects with SwapRefundedError if refunded, HtlcTimeoutError on timeout.
tokenBalancetokenBalance(token: Address, owner: Address): Promise<bigint>balanceOf.

Swap data

TypeFields
HtlcSwaphashlock: 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)
AdaptorSwaptoken, sender, recipient, signer: Address (whose signature releases the funds), amount, expiry, salt: Hex (32 random bytes that make the id unique and unlinkable)
LockInfo<S>swap: S, blockNumber: bigint, transactionHash: Hex
ClaimSignaturev: number (27 or 28), r: Hex, s: Hex (low)
ClaimSignatureInputr: Hex, s: Hex, and v?: number | bigint (0, 1, 27 or 28) or viem's yParity?: number
HelperMeaning
hashlockOf(preimage: Hex): Hexsha256 of a 32-byte secret: the hashlock on both chains.
commitmentOf(swap: HtlcSwap): Hexkeccak256(abi.encode(swap)): what ParalockHTLC stores while a swap is active.
statusOf(raw: Hex): HtlcStatusMaps a stateOf word: 0 empty, 1 (SETTLED_STATE) settled, anything else active.
SETTLED_STATE, EMPTY_STATEThe two fixed state words.
adaptorSwapId(swap: AdaptorSwap): HexThe escrow id, exactly as the contract computes it.
adaptorClaimDigest({ chainId, contract, id }): Hexkeccak256(abi.encode(keccak256("ParalockAdaptorClaim"), chainId, contract, id)).
ADAPTOR_CLAIM_TYPEHASHkeccak256("ParalockAdaptorClaim").
normalizeClaimSignature(sig: ClaimSignatureInput): ClaimSignaturev to 27 or 28; a high s becomes n − s with the recovery bit flipped. Throws on an out-of-range value.
randomSalt(): Hex32 random bytes for AdaptorSwap.salt.
adaptorStatusOf(raw: number): AdaptorStatusMaps the contract's Status enum.
assertBytes32(value, name)Throws a TypeError 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.

ExportSignature
KNOWN_NETWORKSreadonly KnownNetwork[]
knownNetworkknownNetwork(chainId: number): KnownNetwork | undefined
knownNetworksknownNetworks(chain: EvmChainName): KnownNetwork[] (mainnet first)

KnownNetwork is { chain, chainId, name, testnet, rpcUrl?, explorerUrl?, nativeCurrency?, usdc?, tokens: Record<string, Address> }.

NetworkchainChain idTestnetOfficial RPCExplorer
Tempotempo4217nohttps://rpc.tempo.xyzhttps://explore.tempo.xyz
Tempo Testnet (Moderato)tempo42431yeshttps://rpc.moderato.tempo.xyzhttps://explore.testnet.tempo.xyz
Basebase8453nohttps://mainnet.base.orghttps://basescan.org
Base Sepoliabase84532yeshttps://sepolia.base.orghttps://sepolia.basescan.org
Ethereumethereum1nonone (bring your own)none
Sepoliaethereum11155111yesnone (bring your own)https://sepolia.etherscan.io
Arbitrum Onearbitrum42161nohttps://arb1.arbitrum.io/rpchttps://arbiscan.io
Arbitrum Sepoliaarbitrum421614yeshttps://sepolia-rollup.arbitrum.io/rpchttps://sepolia.arbiscan.io
Robinhood Chainrobinhood4663nohttps://rpc.mainnet.chain.robinhood.comhttps://robinhoodchain.blockscout.com
Robinhood Chain Testnetrobinhood46630yeshttps://rpc.testnet.chain.robinhood.comhttps://explorer.testnet.chain.robinhood.com
HyperEVMhyperevm999nohttps://rpc.hyperliquid.xyz/evmhttps://hyperevmscan.io
HyperEVM Testnethyperevm998yeshttps://rpc.hyperliquid-testnet.xyz/evmnone

Stablecoins (tokens; all have 6 decimals):

NetworkTokens
Tempo, ModeratoOUSD 0x20c0000000000000000000006a37DA5C996874BE, pathUSD 0x20c0000000000000000000000000000000000000, USDC.e (Stargate-bridged; 0x20c000000000000000000000b9537d11c60e8b50 on Tempo, 0x20c0000000000000000000009e8d7eb59b783726 on Moderato)
BaseUSDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Base SepoliaUSDC 0x036CbD53842c5426634e7929541eC2318f3dCF7e
EthereumUSDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
SepoliaUSDC 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
Arbitrum OneUSDC 0xaf88d065e77c8cC2239327C5EDb3A432268e5831
Arbitrum SepoliaUSDC 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d
Robinhood ChainUSDG 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
Robinhood Chain Testnetnone (the Paralock deployment uses a MockUSD test token, see deployments)
HyperEVMUSDC 0xb88339CB7199b77E23DB6E890353E22632Ba630f
HyperEVM TestnetUSDC 0x2B3370eE501B4a559b57D449569354196457D8Ab

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 feeToken set, 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 example TEMPO_TOKENS.PathUSD or OUSD. 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_IDS is [4217, 42431, 31318, 1337, 31337]: mainnet, Moderato, Tempo devnet, tempo node --dev, and the Paralock devnet's anvil --network tempo (ANVIL_TEMPO_CHAIN_ID, 31337). 31337 is also plain anvil's default id: pass tempo: false to 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 ParalockHTLC lock costs about 305k gas on a local Tempo chain.
TypeScript
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 helperMeaning
evmChain(chainId, rpcUrl, options?: { feeToken?; tempo? }): ChainThe viem chain: Tempo's config on Tempo chains, a plain one elsewhere. Throws if feeToken is set on a non-Tempo chain.
tempoChain(chainId, rpcUrl, feeToken?)A viem Tempo chain. Mainnet and Moderato keep their explorer metadata; any other id is treated as a local Tempo node.
plainEvmChain(chainId, rpcUrl, name?): ChainA plain EVM chain. Known networks get their name, gas token and explorer; anything else is an ETH chain.
isTempoChainId(chainId): booleanTrue for an id in TEMPO_CHAIN_IDS.
createEvmClients(options: EvmConnectionOptions): EvmClients{ chain, publicClient, walletClient } for a chain.

Errors

ClassFieldsWhen
TransactionRevertedErrorhashA sent transaction was mined but reverted.
SwapRefundedErrorid, hashlock (the same value)A claim waiter found that the swap was refunded instead.
HtlcTimeoutErrorA waiter gave up after timeoutMs. 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

ExportMeaning
paralockHtlcAbi, paralockHtlcBytecodeParalockHTLC.
paralockAdaptorEscrowAbi, paralockAdaptorEscrowBytecodeParalockAdaptorEscrow.
mockUsdAbi, mockUsdBytecodeMockUSD, a 6-decimal test token.
deployParalockHtlc(options), deployParalockAdaptorEscrow(options), deployMockUsd(options)Deploy to local chains and tests. Options: EvmConnectionOptions with a required account. Resolve with TxResult & { address }. For real networks use contracts/script/Deploy.s.sol (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 fromBlock to the deployment block on public chains: lock lookups scan logs from there.
  • getLock and getLockedSwap rebuild a swap from its Locked event and check it against the on-chain commitment (HTLC) or id (adaptor escrow). Use getLock().blockNumber to 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.