maroo-viem-poc
v0.1.1
Published
PoC viem extension for the Maroo chain — namespaced precompile actions (pcl/okrw/eas/agent), PCL policy codec, runOnPcl wrapper, and typed policy-violation decoding. Verified against live marooTestnet.
Maintainers
Readme
maroo-viem-poc
A viem extension for the Maroo chain — proof of concept. It exposes Maroo's
four precompiles (pcl / okrw / eas / agent) as viem client decorators and fills
in what the ABI alone cannot tell you: PCL policy encoding, the runOnPcl
routing rule, and what each of the 40 policy-violation errors actually means.
Everything is verified against live marooTestnet (chainId 450815) — reads, revert decoding, and signed transactions. 41 unit tests plus 22 live tests back that claim.
npm i maroo-viem-poc viemQuickstart
import { createPublicClient, createWalletClient, http } from 'viem'
import { marooTestnet } from 'viem/chains' // shipped in viem 2.55+
import { marooPublicActions, marooWalletActions } from 'maroo-viem-poc'
const client = createPublicClient({ chain: marooTestnet, transport: http() })
.extend(marooPublicActions())
// All four precompiles expose getParams, so namespacing is mandatory.
const { policyAdmin, entrypoints } = await client.pcl.getParams()
const { minter, mintDenom } = await client.okrw.getParams() // mintDenom: 'atokrw'
const { schemaRegistry, eas, indexer } = await client.eas.getParams()
const [agentIds] = await client.agent.getAgentIds({
args: [wallet, { key: '0x', offset: 0n, limit: 10n, countTotal: false, reverse: false }],
})Why the ABI alone is not enough
This is why the package exists — and what you must know before building a dApp on Maroo.
Contract-scoped policies only apply through runOnPcl. Calling a target
contract directly checks global policies only; contract-scoped policies are
silently skipped. No error, no event on the failure path.
const wallet = createWalletClient({ account, chain: marooTestnet, transport: http() })
.extend(marooWalletActions())
// Same shape as writeContract — switching a call site is a one-word change.
const hash = await wallet.pcl.runOnPcl({
address: token,
abi: erc20Abi,
functionName: 'transfer',
args: [recipient, amount],
})
// Native transfers can be policy-checked too.
await wallet.pcl.runOnPcl({ address: recipient, value: parseEther('1') })Attaching msg.value to runOnPcl strands your funds. The forwarded value
rides in the argument — the chain debits the caller directly inside
evm.Call(caller, target, data, gas, value) — while outer-tx msg.value is
neither forwarded nor refunded and accumulates at the precompile address
forever (over 120k tOKRW is already stuck at 0x…0005 on testnet this way).
This SDK blocks that path at both the type level and at runtime.
PolicySet.policy is not self-describing. The templateId string decides
the layout of the opaque bytes, and that mapping exists nowhere in the ABI.
This package's policy codec is the mapping — all 9 templates including the
recursive ones, pinned against live chain bytes.
import { decodePolicySet } from 'maroo-viem-poc'
const { policies } = await client.pcl.globalPolicies()
const tree = policies.map((p) => decodePolicySet(p))
// FOR_EACH(Every of AgentOwners) → LOGICAL(Or) → [PERIODIC_VOLUME 10M/24h, EAS …]
// Unknown templateIds degrade to { unknown: true, raw } instead of throwing.Policy-violation reverts should be human-readable. viem decodes the error
name and args once it has the ABI, but it cannot tell you which of the 40
errors mean "a policy rejected this user" — or when resetAt is.
import { decodePclError } from 'maroo-viem-poc'
try {
await wallet.pcl.runOnPcl({ ... })
} catch (err) {
const v = decodePclError(err) // null for non-PCL errors (selector-gated)
if (v?.kind === 'periodic-volume')
console.log(`24h limit hit — resets at ${v.resetAt.toLocaleString()}`)
if (v?.kind === 'any-of-rejected')
console.log('every branch of the OR policy rejected:', v.children) // recursive, depth-capped
}Batch reads with .call(). Every read action also exposes .call(), which
builds a multicall/simulate descriptor instead of executing.
const volumes = await client.multicall({
contracts: users.map((u) =>
client.pcl.globalOkrwEasPeriodicVolume.call({ args: [u] })),
allowFailure: false,
})
// PeriodicVolume has FOUR fields: amount · maxAmount · resetPeriodSeconds · resetAt
// (decoding only the first two happens to work by accident — and silently
// loses the reset schedule)Chain quirks worth knowing
- Precompiles return
0xfrometh_getCode. Do not gate on "is a contract deployed here" preflights. - The testnet native denom is
atokrw; mainnet usesaokrw.VOLUME_POLICY.tokens[]is keyed by denom string, so hardcoding one breaks on the other network. - The template id is
FOR_EACH_POLICY— notFOREACH_POLICY. The underscore is load-bearing. - Some PCL error arguments return addresses in bech32 (
maroo1…), which cannot be compared to hex addresses directly. - Much of the write surface is permission-tiered:
okrw.mintis minter-only (a chain param),pcl.setGlobalPolicies/registerPolicyTemplatearepolicyAdmin-only, andchangeContractPoliciesis restricted to the admin fixed at registration time.
API
| Export | Purpose |
|---|---|
| marooPublicActions() | client.{pcl,okrw,eas,agent}.* reads + .call() descriptors |
| marooWalletActions() | client.pcl.runOnPcl wrapper + writes (see JSDoc for permission tiers) |
| decodePolicySet / encodePolicy | PCL policy codec (9 templates, recursive, live-verified) |
| decodePclError | revert → PclViolation union; null for foreign errors |
| precompileAddresses / easContracts | address constants, pinned by drift-guard tests |
| precompileActions(abi, addr) | typed action factory for any as const ABI |
| normalizeSelector / SELECTOR_ALL / NO_MAX_LIMIT | policy selector / sentinel utilities |
Testing
pnpm test # unit — no network required
pnpm test:live # live marooTestnet reads / revert decoding (no gas needed)
MAROO_TEST_PRIVATE_KEY=0x… pnpm test:live # signed write paths too (needs tOKRW)Address and ABI drift guards are included: if a @maroo-chain/contracts bump
moves a precompile or changes a function signature the SDK relies on, unit
tests fail loudly.
Out of scope (deliberately)
- The privacy precompile (
0x…000b) — that is the clairveil SDK's responsibility; only the address constant is exposed here. (Not yet deployed on testnet.) - Custom tx types / formatters / serializers — Maroo uses standard EVM transactions, so none are needed.
- A wagmi hooks layer — a separate package once the viem layer settles.
- This is a PoC: unaudited, and the API may change without notice.
License
MIT
