Arkena Docs

The signing model

Arkena uses prepare → sign → execute, not eth_sendTransaction. Here is why and what each step does.

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

Code
// 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 builtwallet (with hints from dApp)backend
What the user signsthe full transactiona hash + a summary card
Counterparty signaturesone (sender)one or many (signatories)
Atomic multi-leg tradesrequires smart contractnative to the protocol
Failed tx costgas burnednothing — 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