Skip to content

docs: add Arc Transaction Memo integration example with Multi-Agent O…#182

Open
consumeobeydie wants to merge 1 commit into
circlefin:mainfrom
consumeobeydie:docs/transaction-memo-integration-example
Open

docs: add Arc Transaction Memo integration example with Multi-Agent O…#182
consumeobeydie wants to merge 1 commit into
circlefin:mainfrom
consumeobeydie:docs/transaction-memo-integration-example

Conversation

@consumeobeydie

Copy link
Copy Markdown

Summary

This PR adds an example showing how to integrate Arc's newly announced Transaction Memos feature (June 18, 2026) with an existing contract, without modifying it.

What's included

  • Predeployed Memo contract address and function signature
  • Step-by-step calldata construction example
  • Common pitfalls discovered during integration (empty revert data, inner call requirements, bytes32 formatting)
  • Live verified transaction on Arc Testnet

Live Verification

Tested against the MultiAgentOrchestrator contract from arc-multi-agent:

Note on signature derivation

Since the official ABI wasn't published in docs.arc.io at the time of writing, the function signature was reverse-engineered by inspecting the Memo contract bytecode dispatcher and decoding a real on-chain transaction's calldata.

Related PRs

This is the 9th example added to this series:
1-8. Foundry, X402, ERC-8004, ERC-8183, Unified Flow, Dashboard, MCP Server, Multi-Agent
9. Transaction Memo integration (this PR)

@osr21

osr21 commented Jun 21, 2026

Copy link
Copy Markdown

Great work documenting this — we've been integrating the Memo precompile into Arc Swap (a token swap + gasless payment DApp on Arc Testnet) and have some findings that might be useful.

The function name is memo, not callWithMemo

The selector 0xc3b2c4f8 you correctly decoded from on-chain calldata corresponds to:

memo(address target, bytes data, bytes32 memoId, bytes memoData)

Not callWithMemo(address,bytes,bytes32,string) — that signature produces selector 0x688231a5. Since function names aren't stored in bytecode, the reverse-engineering step gets the selector right but the name is a guess. We discovered the actual name from a working integration where we pass the ABI explicitly to viem and have confirmed live transactions.

The 4th parameter is also bytes not string (same ABI encoding, but matters for the selector).

There's also a simpler single-argument overload

memo(bytes data)   →   selector 0x58ea363f

This one attaches metadata to the current transaction without routing an inner call — useful for fire-and-forget labeling where you don't need atomicity with another call.

Two integration patterns we've confirmed live

Pattern 1 — atomic inner call (payment route): wraps an ERC20 transfer so the payment and its metadata land in one atomic on-chain event.

// viem — calls memo(address,bytes,bytes32,bytes)
const transferCalldata = encodeFunctionData({
  abi: ERC20_ABI,
  functionName: "transfer",
  args: [recipient, amountOut],
});

const memoId = `0x${(BigInt(Date.now()) * 0x100000000n + BigInt(randPart))
  .toString(16).padStart(64, "0")}` as `0x${string}`;

await walletClient.writeContract({
  address: "0x5294E9927c3306DcBaDb03fe70b92e01cCede505",
  abi: [{ name: "memo", type: "function", stateMutability: "nonpayable",
    inputs: [
      { name: "target",   type: "address" },
      { name: "data",     type: "bytes"   },
      { name: "memoId",   type: "bytes32" },
      { name: "memoData", type: "bytes"   },
    ], outputs: [] }],
  functionName: "memo",
  args: [tokenAddress, transferCalldata, memoId, toHex(Buffer.from(memoText, "utf8"))],
  gas: 200_000n,  // eth_estimateGas is broken on Arc Testnet — always pass explicit gas
});

Pattern 2 — fire-and-forget label (swap route): non-atomic, appended after a swap is already settled.

// calls memo(bytes) — attaches a label to this tx, no inner call
await walletClient.writeContract({
  address: "0x5294E9927c3306DcBaDb03fe70b92e01cCede505",
  abi: [{ name: "memo", type: "function", stateMutability: "nonpayable",
    inputs: [{ name: "data", type: "bytes" }], outputs: [] }],
  functionName: "memo",
  args: [toHex(Buffer.from("Arc Swap: USDC → EURC | 10 in → 8.72 out", "utf8"))],
  gas: 100_000n,
});

One critical Arc Testnet gotcha worth adding

eth_estimateGas is broken on Arc Testnet — it returns incorrect values or fails entirely. Every writeContract call to the Memo contract (or any contract) needs an explicit gas: override or it will revert silently. We've found 200_000n works reliably for the atomic inner-call pattern and 100_000n for the simple label pattern.

Hope this helps — happy to share any live tx hashes for verification if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants