Skip to content

feat(runner): add MiniappRunner for the Circles miniapps host#52

Open
Wieedze wants to merge 1 commit into
aboutcircles:mainfrom
Wieedze:feat/runner-miniapp
Open

feat(runner): add MiniappRunner for the Circles miniapps host#52
Wieedze wants to merge 1 commit into
aboutcircles:mainfrom
Wieedze:feat/runner-miniapp

Conversation

@Wieedze

@Wieedze Wieedze commented Jun 3, 2026

Copy link
Copy Markdown

Add MiniappRunnerContractRunner for Circles miniapps iframe host

Summary

This PR adds MiniappRunner to @aboutcircles/sdk-runner, alongside the existing SafeContractRunner and SafeBrowserRunner.

Why: mini-apps embedded in the Circles miniapps host (circles.gnosis.io/playground or any other embedder) cannot reach the user's wallet directly — the EIP-1193 provider lives in the parent frame. The host exposes a sendTransactions(txs) bridge over postMessage instead (see @aboutcircles/miniapp-sdk). Today this leaves miniapp authors with two unappealing options:

  1. Hand-roll calldata everywhere with viem encodeFunctionData(hubAbi, ...) and forward to the host. Reinvents the typed wrappers shipped in @aboutcircles/sdk-core, drifts when ABIs evolve.
  2. Skip the SDK entirely and treat Circles as a black box of opcodes. Forfeits everything the SDK family is designed to give them.

MiniappRunner closes the gap: it implements ContractRunner against the miniapp host's sendTransactions, so the rest of the SDK (core.hubV2.*, wrappers, avatar methods, transfer.direct, transfer.advanced, etc.) works inside a miniapp identically to how it works outside.

What the runner does

  • Writes: forwards TransactionRequest[] to the host's batch-send function. The host serialises them, batches them into a single Safe execution, asks the user to sign once, and returns one hash. The runner waits on waitForTransactionReceipt and returns viem's TransactionReceipt — same shape as SafeBrowserRunner.sendTransaction.
  • Reads: delegates estimateGas, call, and resolveName to the PublicClient injected by the consumer (same pattern as SafeBrowserRunner).
  • Batching: exposes sendBatchTransaction() that returns a MiniappBatchRun, mirroring SafeBrowserBatchRun. Callers can addTransaction(...) then run() to flush.
  • Error handling: uses RunnerError consistently, throws transactionReverted on receipt.status === 'reverted', throws executionFailed on host rejection or missing hash.

What this PR doesn't add

  • A hard dependency on @aboutcircles/miniapp-sdk. The consumer passes sendTransactions in at construction time — this keeps sdk-runner package-graph clean and makes the runner trivially mockable for tests.
  • Changes to existing runners or the ContractRunner interface.

API surface

export interface MiniappHostTransaction {
  to: string;
  data?: string;
  value?: string;
}

export type MiniappSendTransactions = (
  txs: MiniappHostTransaction[],
) => Promise<string[]>;

export class MiniappRunner implements ContractRunner {
  constructor(
    publicClient: PublicClient,
    sendTransactions: MiniappSendTransactions,
    address?: Address,
  );
  static create(
    rpcUrl: string,
    sendTransactions: MiniappSendTransactions,
    chain: ChainLike,
    address?: Address,
  ): Promise<MiniappRunner>;
  init(address?: Address): Promise<void>;
  sendTransaction(txs: TransactionRequest[]): Promise<TransactionReceipt>;
  sendBatchTransaction(): MiniappBatchRun;
}

export class MiniappBatchRun implements BatchRun {
  addTransaction(tx: TransactionRequest): void;
  run(): Promise<TransactionReceipt>;
}

Usage

import { createPublicClient, http } from 'viem';
import { gnosis } from 'viem/chains';
import {
  onWalletChange,
  sendTransactions,
} from '@aboutcircles/miniapp-sdk';
import { MiniappRunner } from '@aboutcircles/sdk-runner';
import { Sdk } from '@aboutcircles/sdk';
import { circlesConfig } from '@aboutcircles/sdk-utils';

const publicClient = createPublicClient({
  chain: gnosis,
  transport: http('https://rpc.gnosischain.com'),
});

onWalletChange(async (address) => {
  if (!address) return;
  const runner = new MiniappRunner(publicClient, sendTransactions, address);
  const sdk = new Sdk(circlesConfig[100], runner);

  const avatar = await sdk.getAvatar(address);
  await avatar.transfer.direct('0xRecipient', BigInt(1e18));
});

Or the one-step factory:

import { chains, MiniappRunner } from '@aboutcircles/sdk-runner';
import { sendTransactions } from '@aboutcircles/miniapp-sdk';

const runner = await MiniappRunner.create(
  'https://rpc.gnosischain.com',
  sendTransactions,
  chains.gnosis,
  viewerAddress,
);

Production use

This runner ships in The Kitty — a Circles V2 mini-app for services + tontines, registered in gnosis-box/CirclesMiniapps. It has been live on Gnosis mainnet since cycle 3 of the Circles Garage program (June 2026), powering:

  • single-signature Hub.trust + Hub.safeTransferFrom + ServiceRegistry.logPayment bundles in the services pay flow
  • KittyGovernance.smallSpend from a group-pot kitty as an alternative pay source
  • every core.hubV2.* call in the app's tx-builders.ts

Repo, contracts, and a 90-second demo: https://github.com/gnosis-box/TheKitty.

Test plan

  • bun run build in packages/runner succeeds and emits dist/miniapp-runner.{js,d.ts}.
  • index.ts re-exports the new symbols (covered by the diff).
  • Smoke test inside The Kitty — open the playground, publish a service, pay another wallet, confirm the bundled tx lands as a single Safe batch on Gnosis.

Notes for reviewers

  • The runner accepts sendTransactions as a constructor arg instead of importing it from @aboutcircles/miniapp-sdk. Happy to take a dependency on miniapp-sdk if the package maintainers prefer the symmetry with how SafeBrowserRunner takes Eip1193Provider directly — flagging because either way works and I went with the lighter-coupling option by default.
  • I kept the JSDoc, naming, and class-field arrow-method style aligned with safe-browser-runner.ts. Open to renaming or trimming to match any internal conventions I missed.
  • The package adds no new runtime dependencies. @safe-global/protocol-kit is unaffected.

Mini-apps embedded in the Circles miniapps host can't reach the user's
wallet directly — the EIP-1193 provider lives in the parent frame. The
host exposes a sendTransactions(txs) bridge over postMessage instead
(@aboutcircles/miniapp-sdk).

MiniappRunner implements ContractRunner against that bridge so the rest
of the SDK (core.hubV2.*, wrappers, avatar.transfer.*) works inside a
mini-app identically to how it works outside.

- One signature per sendTransaction call — host bundles into one Safe batch
- Reads (estimateGas, call, resolveName) delegated to the injected
  PublicClient, same pattern as SafeBrowserRunner
- MiniappBatchRun companion for interactive batching
- RunnerError used consistently for empty batch, host rejection, and
  reverted receipts
- sendTransactions function passed in at construction instead of
  imported from @aboutcircles/miniapp-sdk — keeps the package graph
  clean and makes the runner trivially mockable for unit tests
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Release Notes

  • New MiniappRunner: Introduces a new ContractRunner implementation for miniapps embedded in the Circles miniapps host, enabling SDK-driven contract interactions without direct EIP-1193 provider access
  • Host Bridge Integration: Forwards TransactionRequest[] arrays to a host-provided sendTransactions function via postMessage, with responses mapped to standard viem TransactionReceipt
  • Read Operations: Delegates estimateGas, call, and resolveName to an injected PublicClient
  • Batch Transaction Support: Provides sendBatchTransaction() method returning a MiniappBatchRun helper for accumulating and executing multiple transactions in a single host call
  • Error Handling: Implements RunnerError variants for transaction reversions, host rejections, missing hashes, and empty transaction lists
  • Factory Method: Includes static create() for convenient initialisation with automatic chain and client setup
  • API Additions: Exports MiniappRunner, MiniappBatchRun, MiniappHostTransaction, and MiniappSendTransactions types and classes from @aboutcircles/sdk-runner
  • No Breaking Changes: Existing SafeContractRunner and SafeBrowserRunner remain unchanged; new functionality is purely additive

Walkthrough

This PR adds MiniappRunner, a new ContractRunner implementation enabling miniapps to execute transactions through a host-provided bridge. It includes type contracts for the wire protocol, core read and transaction-sending logic, batch operation support, package exports, and comprehensive documentation with usage examples.

Changes

MiniappRunner implementation

Layer / File(s) Summary
Host bridge types and runner imports
packages/runner/src/miniapp-runner.ts
MiniappHostTransaction and MiniappSendTransactions types define the host-to-miniapp transaction protocol; module imports establish viem, ethers, and error dependencies.
MiniappRunner construction and initialisation
packages/runner/src/miniapp-runner.ts
Constructor wires the host bridge function; static create factory builds a viem PublicClient from RPC and chain configuration and calls init; init sets or updates the runner's viewer address without network calls.
Read operations
packages/runner/src/miniapp-runner.ts
estimateGas, call, and resolveName delegate to PublicClient for gas simulation, read-only calls, and ENS name resolution respectively.
Transaction sending
packages/runner/src/miniapp-runner.ts
sendTransaction maps TransactionRequest[] to wire transactions with value as 0x... hex, calls the host bridge, validates hashes, waits for receipt, and throws RunnerError for empty input, host rejection, missing hash, or reverted transactions.
Batch transaction accumulation and execution
packages/runner/src/miniapp-runner.ts
sendBatchTransaction factory method returns a MiniappBatchRun that accumulates transactions via addTransaction and executes them in one batch call through run().
Package exports
packages/runner/src/index.ts
Re-exports MiniappRunner, MiniappBatchRun, MiniappHostTransaction, and MiniappSendTransactions from the module entry point.
Documentation and usage examples
packages/runner/README.md
Package overview expanded to cover MiniappRunner alongside existing runners; new usage section documents miniapp wallet-change flows and factory construction; API documentation covers constructor, static create, init, sendTransaction, and sendBatchTransaction signatures.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding MiniappRunner, a new ContractRunner implementation for the Circles miniapps host.
Description check ✅ Passed The description comprehensively explains the purpose, design, API surface, usage examples, production context, and test plan—all directly related to the changeset of adding MiniappRunner.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/runner/src/miniapp-runner.ts`:
- Around line 221-232: In sendTransaction, some TransactionRequest entries may
have no destination but you unconditionally map tx.to into
MiniappHostTransaction which leads to host errors; before mapping/filtering,
validate each tx in the txs array and if any tx.to is null/undefined throw a
consistent RunnerError (e.g. RunnerError.executionFailed('Transaction missing
"to"')) so that MiniappHostTransaction.to is always present; update the
sendTransaction function's validation logic to iterate txs, locate entries
without to, and raise the RunnerError rather than forwarding undefined to the
host.
- Around line 27-29: In MiniappRunner.sendTransaction, add the same fail-fast
validation used in estimateGas: check that the incoming tx has a defined tx.to
before building the MiniappHostTransaction and sending the bridge payload; if
tx.to is missing, throw a RunnerError with a clear message instead of using the
non-null assertion (tx.to!) so malformed transactions are rejected early. Ensure
the validation occurs at the start of sendTransaction and that the constructed
MiniappHostTransaction.to uses the validated value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e4f67062-3235-4162-b7ad-5c3cebac271b

📥 Commits

Reviewing files that changed from the base of the PR and between 5163519 and d4ad896.

📒 Files selected for processing (3)
  • packages/runner/README.md
  • packages/runner/src/index.ts
  • packages/runner/src/miniapp-runner.ts

Comment on lines +27 to +29
export type MiniappSendTransactions = (
txs: MiniappHostTransaction[]
) => Promise<string[]>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What does sendTransactionsfrom@aboutcircles/miniapp-sdk return: a single transaction hash string, or an array of hashes?

💡 Result:

I couldn’t verify this because I can’t find any public source (npm/unpkg/jsDelivr/GitHub/docs) that exposes @aboutcircles/miniapp-sdk’s TypeScript typings or implementation for sendTransactions (the npm page is also blocked by a 403 in this environment).

If you paste the sendTransactions type signature from your node_modules/@aboutcircles/miniapp-sdk/**.d.ts (or tell me the exact version and share the relevant file), I can tell you definitively whether it returns Promise<string> (single hash) or Promise<string[]> (array).


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the file under review (bounded output)
sed -n '1,200p' packages/runner/src/miniapp-runner.ts

# Show the specific snippet lines around 27-29
nl -ba packages/runner/src/miniapp-runner.ts | sed -n '15,60p'

# Search for hostSendTransactions / sendTransaction / hashes usage
rg -n "hostSendTransactions|hashes\\[0\\]|sendTransactions|MiniappSendTransactions" packages/runner/src/miniapp-runner.ts packages/runner/src || true

# Search entire repo for host bridge sendTransactions typing/usages
rg -n "sendTransactions\\(" packages || true
rg -n "MiniappHostTransaction|MiniappSendTransactions" packages || true

# If repo has types for host bridge, find them
rg -n "host.*sendTransactions|sendTransactions.*Promise" packages || true

Repository: aboutcircles/sdk

Length of output: 7282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/runner/src/miniapp-runner.ts"

# Find line numbers for the relevant parts
rg -n "sendTransaction|hostSendTransactions|hashes\\[0\\]|hashes\\[|tx\\.to!|MiniappBatchRun" "$FILE" || true

# Print the regions around each match (bounded)
# (Use a few ranges to cover all matches without needing nl)
sed -n '1,120p' "$FILE"
sed -n '120,220p' "$FILE"
sed -n '220,340p' "$FILE"
sed -n '340,460p' "$FILE"

Repository: aboutcircles/sdk

Length of output: 11722


Validate tx.to before building MiniappHostTransaction in MiniappRunner.sendTransaction.

MiniappHostTransaction.to is required, but sendTransaction() maps tx.to! into the bridge payload without checking for a missing destination, so malformed transactions can reach the host. Add a fail-fast RunnerError when tx.to is absent (same assumption pattern exists in estimateGas).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/runner/src/miniapp-runner.ts` around lines 27 - 29, In
MiniappRunner.sendTransaction, add the same fail-fast validation used in
estimateGas: check that the incoming tx has a defined tx.to before building the
MiniappHostTransaction and sending the bridge payload; if tx.to is missing,
throw a RunnerError with a clear message instead of using the non-null assertion
(tx.to!) so malformed transactions are rejected early. Ensure the validation
occurs at the start of sendTransaction and that the constructed
MiniappHostTransaction.to uses the validated value.

Comment on lines +221 to +232
sendTransaction = async (
txs: TransactionRequest[]
): Promise<TransactionReceipt> => {
if (txs.length === 0) {
throw RunnerError.executionFailed('No transactions provided');
}

const hostTxs: MiniappHostTransaction[] = txs.map((tx) => ({
to: tx.to!,
data: tx.data ?? '0x',
value: tx.value == null ? '0x0' : `0x${BigInt(tx.value).toString(16)}`,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject transactions without a destination before calling the host.

MiniappHostTransaction.to is mandatory, but this forwards tx.to! unchecked. A contract-creation request or malformed TransactionRequest will reach the bridge as to: undefined and then fail with a host-specific error instead of a consistent RunnerError.

Suggested guard
   if (txs.length === 0) {
     throw RunnerError.executionFailed('No transactions provided');
   }
+
+  if (txs.some((tx) => !tx.to)) {
+    throw RunnerError.executionFailed(
+      'MiniappRunner only supports transactions with a destination address'
+    );
+  }
 
   const hostTxs: MiniappHostTransaction[] = txs.map((tx) => ({
     to: tx.to!,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sendTransaction = async (
txs: TransactionRequest[]
): Promise<TransactionReceipt> => {
if (txs.length === 0) {
throw RunnerError.executionFailed('No transactions provided');
}
const hostTxs: MiniappHostTransaction[] = txs.map((tx) => ({
to: tx.to!,
data: tx.data ?? '0x',
value: tx.value == null ? '0x0' : `0x${BigInt(tx.value).toString(16)}`,
}));
sendTransaction = async (
txs: TransactionRequest[]
): Promise<TransactionReceipt> => {
if (txs.length === 0) {
throw RunnerError.executionFailed('No transactions provided');
}
if (txs.some((tx) => !tx.to)) {
throw RunnerError.executionFailed(
'MiniappRunner only supports transactions with a destination address'
);
}
const hostTxs: MiniappHostTransaction[] = txs.map((tx) => ({
to: tx.to!,
data: tx.data ?? '0x',
value: tx.value == null ? '0x0' : `0x${BigInt(tx.value).toString(16)}`,
}));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/runner/src/miniapp-runner.ts` around lines 221 - 232, In
sendTransaction, some TransactionRequest entries may have no destination but you
unconditionally map tx.to into MiniappHostTransaction which leads to host
errors; before mapping/filtering, validate each tx in the txs array and if any
tx.to is null/undefined throw a consistent RunnerError (e.g.
RunnerError.executionFailed('Transaction missing "to"')) so that
MiniappHostTransaction.to is always present; update the sendTransaction
function's validation logic to iterate txs, locate entries without to, and raise
the RunnerError rather than forwarding undefined to the host.

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.

1 participant