If you've worked with Ethereum, you're used to one call doing everything:
eth_sendTransaction({ to, value, data }) and the wallet handles
construction, signing, broadcasting. Arkena splits this into three steps,
two of which involve the backend. This page explains why and what to
expect at each step.
Why three steps
Canton transactions are explicit about which parties co-sign. A trade between Alice and Bob isn't "Alice transfers X to Bob" — it's "Alice and Bob jointly archive their existing balance contracts and create new ones with the swapped amounts." Both signatures are required for the transaction to settle.
The wallet doesn't know what Bob's current balance contract is. It can't build the transaction alone. Canton calls this the interactive submission model: an indexer-aware participant constructs the transaction, the signing parties review and sign the resulting hash, and the participant submits.
Arkena's backend plays the indexer-aware participant role. It has the indexed view of the ledger needed to fill in current contract IDs. The wallet provides the cryptographic signature.
The three steps
// 1. Prepare — backend builds the transaction
const prep = await fetch("/api/nft/buy/atomic/prepare", {
method: "POST",
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
body: JSON.stringify({ nftId, buyer: party }),
}).then((r) => r.json());
// prep === {
// commandId: string,
// hash: string,
// hashingSchemeVersion: string,
// transaction: string, // opaque blob with disclosed contracts
// summary: { type: "buy_nft", nft, price, fees, total }
// }
// 2. Sign — wallet shows the summary, signs the hash
const { signature } = await provider.request({
method: "canton_signTransaction",
params: {
hash: prep.hash,
commandId: prep.commandId,
hashingSchemeVersion: prep.hashingSchemeVersion,
summary: prep.summary,
},
});
// 3. Execute — backend submits the signed transaction
const result = await fetch("/api/nft/buy/atomic/execute", {
method: "POST",
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
body: JSON.stringify({
commandId: prep.commandId,
transaction: prep.transaction,
signature,
}),
}).then((r) => r.json());
// result === { commandId, status: "settled" | "pending" | "failed", error? }What the user sees at the sign step
The prep.summary is a TxSummary object — a structured, human-readable
description of the transaction. The wallet renders it as a card:
type, amounts, counterparty, fees, total. The user reads the card before
approving.
Your dApp doesn't render the summary itself; the wallet handles that. But
the contents of summary are what the user sees, so the backend
constructs them carefully — the user is reading the summary as their
proof of what they're signing.
Errors at each stage
Errors can fire in three places. Distinguish them — the recovery is different.
Prepare-time errors are HTTP errors from the backend. Common cases: the user can't afford the trade, the listing has expired, the authentication is invalid. The backend returns the canonical error shape described in Error codes.
Sign-time errors are thrown from provider.request(...) with a
numeric code. The most common is 4001 (user rejected). Others:
-32003 (wallet policy rejected), 4100 (locked).
Execute-time errors are HTTP errors from the backend, similar shape to prepare. Common cases: another buyer raced you to the listing, network conflict, settlement timeout. The transaction hasn't applied; you can retry from prepare.
Idempotency
Each prepare call returns a unique commandId. The execute endpoint is
idempotent on this commandId — if the network drops your execute
response and you retry with the same body, the backend recognises the
in-flight command and returns the existing result instead of submitting
twice.
This makes "retry on network error" safe. It does not make "retry on
sign-time error" safe — a fresh sign means a fresh signature, and you
should request a new prepare for that.
Comparing to Ethereum
Ethereum (eth_sendTransaction) | Arkena (prepare → sign → execute) | |
|---|---|---|
| Where the tx is built | wallet (with hints from dApp) | backend |
| What the user signs | the full transaction | a hash + a summary card |
| Counterparty signatures | one (sender) | one or many (signatories) |
| Atomic multi-leg trades | requires smart contract | native to the protocol |
| Failed tx cost | gas burned | nothing — failed prepare/execute is free |
The trade-off is clear. Arkena loses the simplicity of sendTransaction
in exchange for atomic multi-party trades and zero-cost failed
transactions. For a marketplace and a swap, this trade-off pays off.
What's next
- Sign and submit transactions — the procedure version of this page, with copyable code.
- Sign-In with Canton — authentication for the prepare and execute calls.
- Error codes — the full error shape reference.