Building Multi-Party dApps with Reach: Participant Roles and Consensus Actions
Reach lets you write multi-party dApps once and compile to Algorand, Ethereum, or other backends. This guide explains participant roles, interact/commit consensus rounds, and a worked escrow example with frontend integration.
09 Mar 2026, 19:35 UTC

The Problem: Coordinating Multiple Parties Across Ledger Backends
Writing a decentralized application that involves two or more participants—say, an escrow between a buyer and seller—usually means implementing separate smart contracts for each target blockchain. Ethereum uses Solidity, Algorand uses TEAL, and each has different semantics for atomic transactions, state storage, and fee models. Reach addresses this by letting you define participant roles and their consensus interactions once in a domain-specific language, then compiling to backend-specific artifacts.
Useful Takeaway
Reach programs declare participants (e.g., Alice, Bob), off-chain interact functions that collect user input, and on-chain commit/publish actions that execute atomically per consensus round. The compiler emits backend contracts (Algorand TEAL, Ethereum Solidity, etc.) and a JavaScript/TypeScript frontend API that handles deployment, transaction signing, and state reads. Static analysis catches a class of distributed-ledger errors—such as unbalanced transfers or deadlocked consensus steps—before deployment.
Core Mechanism: Participants, Interact, and Commit
A Reach source file (.rsh) typically contains:
- Participant declarations – named roles with optional class interfaces.
- Interact functions – off-chain code (JavaScript/TypeScript) that prompts users, builds transaction parameters, and returns values to the consensus logic.
- Commit/publish blocks – on-chain steps where participants
publishvalues to the shared state andcommitto execute atomic transfers or logic.
Each consensus round advances when all required participants have published. The compiler translates these rounds into backend-specific atomic transaction groups.
Worked Example: Two-Party Escrow
The following minimal Reach program implements an escrow where Alice deposits funds, Bob confirms delivery, and funds release to Bob or refund to Alice. Save as escrow.rsh.
// escrow.rsh
'reach 0.1';
const Escrow = (amount) => {
const Alice = Participant('Alice', {
deposit: Fun([], UInt),
refund: Fun([], Null)
});
const Bob = Participant('Bob', {
confirm: Fun([], Bool),
claim: Fun([], Null)
});
deploy()
.then(() => Alice.only(() => {
const depositAmt = interact.deposit();
return depositAmt;
}))
.then((depositAmt) => {
Alice.publish(depositAmt);
commit();
transfer(depositAmt).from(Alice).to(Contract);
})
.then(() => Bob.only(() => {
const ok = interact.confirm();
return ok;
}))
.then((ok) => {
Bob.publish(ok);
commit();
if (ok) {
transfer(amount).from(Contract).to(Bob);
} else {
transfer(amount).from(Contract).to(Alice);
}
});
};
Escrow(100);
Key points in the example:
Participantdeclares a role with an interface; the frontend must implementdeposit,refundfor Alice andconfirm,claimfor Bob.interactcalls run off-chain (in the user's browser or Node.js) and return values into the consensus flow.publishmakes a value visible to all participants in the current round;commitfinalizes the round and executes transfers atomically.transferexpresses value movement; the compiler enforces balance checks per backend.
Compiling and Running the Frontend
Assuming you have the Reach CLI installed (npm i -g @reach-sh/reach-cli or via Docker), compile and generate the frontend bindings:
# Run in project root with escrow.rsh present
reach compile escrow.rsh
# Output: build/escrow.main.mjs (or .sol/.teal depending on backend)
# and build/index.main.mjs – the JavaScript API
Where to run: Any machine with Node.js ≥ 18 and the Reach CLI. No elevated permissions needed; the CLI writes to ./build.
Expected checks: Compilation succeeds without static-analysis errors (e.g., “transfer amount exceeds balance”). The generated index.main.mjs exports Alice and Bob classes with deploy, attach, and interact method stubs.
Risks: Backend selection is implicit via REACH_CONNECTOR_MODE (ALGO, ETH, CFX, etc.). If unset, the compiler defaults to a simulator backend unsuitable for mainnet. Verify the connector before deploying real funds.
Frontend Integration (JavaScript/TypeScript)
A minimal React or Node script that drives the escrow:
import { loadStdlib } from '@reach-sh/stdlib';
import * as backend from './build/escrow.main.mjs';
const stdlib = loadStdlib(process.env);
const startingBalance = stdlib.parseCurrency(10);
async function runAlice() {
const acc = await stdlib.newTestAccount(startingBalance);
const ctc = acc.contract(backend);
const deployResult = await ctc.deploy();
const contractInfo = deployResult.contractInfo;
await ctc.p.Alice({
deposit: () => stdlib.parseCurrency(5),
refund: () => { /* handle refund UI */ }
});
}
async function runBob(contractInfo) {
const acc = await stdlib.newTestAccount(startingBalance);
const ctc = acc.contract(backend, contractInfo);
await ctc.p.Bob({
confirm: () => true, // or prompt user
claim: () => { /* handle claim UI */ }
});
}
Practical verification: Run both participants against the Reach devnet simulator (REACH_CONNECTOR_MODE=ALGO reach run index.mjs) and confirm the final balances match the escrow logic. The simulator prints round-by-round logs showing publish and commit events.
Limits and Common Mistakes
1. Backend-Specific Semantics Leak Through
Reach abstracts many differences, but not all. Algorand requires explicit closeRemainderTo for account closure; Ethereum gas limits can cause out-of-gas reverts on complex loops. The static checker does not model gas costs. Mitigation: Test on each target testnet before mainnet.
2. Static Check Coverage Varies
The compiler catches unbalanced transfers, unreachable code, and some race conditions. It does not verify economic incentives, oracle correctness, or front-running resistance. Treat static analysis as a necessary but insufficient safety net.
3. Version Drift and API Instability
The research brief notes that syntax, CLI commands, and supported backends change between releases. The 'reach 0.1' pragma in the example may not match the current compiler. Always run reach --version and consult the installed CLI's help (reach compile --help) before writing code.
4. Ambiguous Name Collisions
“Reach” also refers to EU chemical regulation (REACH), the @reach/ui React component library, and other packages. Ensure package.json dependencies reference @reach-sh/stdlib and @reach-sh/reach-cli.
How to Verify Your Setup
- Run
reach versionand record the output. - Compile the example above; confirm
build/contains backend artifacts for your chosen connector. - Execute the frontend against the devnet simulator; inspect console logs for
publish/commitsequence. - Compare the generated participant method signatures (in
build/index.main.mjs) with the interfaces declared in.rsh.
If any step diverges from expectations, consult the official Reach repository documentation for the exact version you have installed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.