feat(runner): add MiniappRunner for the Circles miniapps host#52
feat(runner): add MiniappRunner for the Circles miniapps host#52Wieedze wants to merge 1 commit into
Conversation
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
Release Notes
WalkthroughThis PR adds ChangesMiniappRunner implementation
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
packages/runner/README.mdpackages/runner/src/index.tspackages/runner/src/miniapp-runner.ts
| export type MiniappSendTransactions = ( | ||
| txs: MiniappHostTransaction[] | ||
| ) => Promise<string[]>; |
There was a problem hiding this comment.
🧩 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 || trueRepository: 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.
| 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)}`, | ||
| })); |
There was a problem hiding this comment.
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.
| 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.
Add
MiniappRunner—ContractRunnerfor Circles miniapps iframe hostSummary
This PR adds
MiniappRunnerto@aboutcircles/sdk-runner, alongside the existingSafeContractRunnerandSafeBrowserRunner.Why: mini-apps embedded in the Circles miniapps host (
circles.gnosis.io/playgroundor any other embedder) cannot reach the user's wallet directly — the EIP-1193 provider lives in the parent frame. The host exposes asendTransactions(txs)bridge overpostMessageinstead (see@aboutcircles/miniapp-sdk). Today this leaves miniapp authors with two unappealing options:encodeFunctionData(hubAbi, ...)and forward to the host. Reinvents the typed wrappers shipped in@aboutcircles/sdk-core, drifts when ABIs evolve.MiniappRunnercloses the gap: it implementsContractRunneragainst the miniapp host'ssendTransactions, 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
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 onwaitForTransactionReceiptand returns viem'sTransactionReceipt— same shape asSafeBrowserRunner.sendTransaction.estimateGas,call, andresolveNameto thePublicClientinjected by the consumer (same pattern asSafeBrowserRunner).sendBatchTransaction()that returns aMiniappBatchRun, mirroringSafeBrowserBatchRun. Callers canaddTransaction(...)thenrun()to flush.RunnerErrorconsistently, throwstransactionRevertedonreceipt.status === 'reverted', throwsexecutionFailedon host rejection or missing hash.What this PR doesn't add
@aboutcircles/miniapp-sdk. The consumer passessendTransactionsin at construction time — this keepssdk-runnerpackage-graph clean and makes the runner trivially mockable for tests.ContractRunnerinterface.API surface
Usage
Or the one-step factory:
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:Hub.trust + Hub.safeTransferFrom + ServiceRegistry.logPaymentbundles in the services pay flowKittyGovernance.smallSpendfrom a group-pot kitty as an alternative pay sourcecore.hubV2.*call in the app'stx-builders.tsRepo, contracts, and a 90-second demo: https://github.com/gnosis-box/TheKitty.
Test plan
bun run buildinpackages/runnersucceeds and emitsdist/miniapp-runner.{js,d.ts}.index.tsre-exports the new symbols (covered by the diff).Notes for reviewers
sendTransactionsas 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 howSafeBrowserRunnertakesEip1193Providerdirectly — flagging because either way works and I went with the lighter-coupling option by default.safe-browser-runner.ts. Open to renaming or trimming to match any internal conventions I missed.@safe-global/protocol-kitis unaffected.