Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
b58be75
scaffold actions-cli package skeleton
its-everdred Apr 22, 2026
52ba69f
scope no-console error to cli
its-everdred Apr 22, 2026
2eb5175
extract serializeBigInt to sdk util
its-everdred Apr 23, 2026
394bfef
add pr 408 handoff doc
its-everdred Apr 23, 2026
81d8f8d
add writeJson stdout helper
Apr 23, 2026
9a753f9
add CliError taxonomy and safeDetails
Apr 23, 2026
61df45a
add lazy requireEnv accessor
Apr 23, 2026
5893998
add demo chain configs without bundler
Apr 23, 2026
c0b245a
add demo lend market constants
Apr 23, 2026
113ede0
add demo config and loadConfig
Apr 23, 2026
6c64a9e
flatten demo config jsdoc
Apr 23, 2026
854d440
add asset resolver by symbol
Apr 23, 2026
d9e6f31
add chain resolver and inverse
Apr 23, 2026
24139af
add baseContext for read-only commands
Apr 23, 2026
1119b3f
add walletContext with EOA wallet
Apr 23, 2026
ddb9b4c
add assets command handler
Apr 23, 2026
2d449ee
add chains command handler
Apr 23, 2026
b1e8ad9
add wallet address command
Apr 23, 2026
492846e
add wallet balance command
Apr 23, 2026
323c4d5
wire command tree and EPIPE guards
Apr 23, 2026
5eb9812
add picocolors for help output
Apr 23, 2026
acb0b56
add SKILL.md agent contract and README
Apr 23, 2026
305112e
replace em dashes with hyphens
Apr 23, 2026
c14db41
add spawn-based system tests
Apr 23, 2026
ad6811b
drop handoff doc
Apr 23, 2026
0fc3d9e
add --chain and --chain-id flags
Apr 23, 2026
c03af9d
scrub agent mentions and follow-up refs
Apr 23, 2026
ca45598
add --json flag, consolidate output formatters
Apr 23, 2026
24b549e
lock full-sdk shape for no-chain balance
Apr 23, 2026
d1c16d1
add wallet lend open and close
Apr 24, 2026
632c5fa
add lend read commands and wallet position
Apr 24, 2026
391e4da
add swap namespace and wallet execute
Apr 24, 2026
c0e2fae
drop unichain from demo chain set
Apr 24, 2026
3bd9db9
add presentation hints for llm callers
its-everdred Apr 24, 2026
aa21c87
drop redundant double-wait in sendBatch
its-everdred Apr 27, 2026
9d990b6
default fast chains to 1500ms polling
its-everdred Apr 27, 2026
b88d634
demo cli opts into max approval mode
its-everdred Apr 27, 2026
3e7164b
narrow lend providers iterator to skip settings
its-everdred Apr 27, 2026
3b7f141
fix prettier formatting
its-everdred Apr 28, 2026
81f39ff
merge strict-quote-recipient into swap-perf
its-everdred Apr 28, 2026
12d5f6e
drop unichain from cli resolver
its-everdred Apr 28, 2026
47521bb
fix uniswap v4 exact-out action byte
its-everdred Apr 29, 2026
9fb74ec
fix velo universal router for EOAs
its-everdred Apr 29, 2026
8b2f32a
Merge remote-tracking branch 'origin/main' into kevin/swap-perf-rebased
its-everdred Apr 29, 2026
1b84a8e
add cli check ci job
its-everdred Apr 29, 2026
df85632
guide cross-provider quote comparison
Apr 29, 2026
847755f
add swap namespace and wallet execute
Apr 24, 2026
3129ca9
add presentation hints for llm callers
its-everdred Apr 24, 2026
7df3517
rebase fixups: import paths and types
its-everdred May 5, 2026
8c9f24b
drop unichain from swap tests
its-everdred May 5, 2026
6415a57
import ANVIL_ACCOUNT_0 from shared mock
its-everdred May 5, 2026
16e5a13
add configuredAssets helper
its-everdred May 5, 2026
fa127ed
format chains test array
its-everdred May 5, 2026
e48a42f
drop duplicate velodrome changeset
its-everdred May 5, 2026
f25f556
internalize polling interval split
its-everdred May 5, 2026
04f0b41
tighten l2 polling to 1s
its-everdred May 5, 2026
3a397b4
share polling default with bundler client
its-everdred May 5, 2026
bf44935
attach nonce manager to eoa wallet
its-everdred May 5, 2026
f7d56b7
test nonce manager attachment
its-everdred May 5, 2026
afc2b97
add changeset for eoa perf changes
its-everdred May 5, 2026
fb77d19
guard demo config against mainnet
its-everdred May 5, 2026
db59d1a
merge feat/cli-swap into swap-perf
its-everdred May 6, 2026
3fd8192
barrel-export ApprovalMode and APPROVAL_MODES
May 6, 2026
da2acd3
format DeployMorphoMarket prettier drift
May 6, 2026
c7eb8a0
barrel-export LendProviderName
May 6, 2026
be0d15e
barrel-export LendAction literal
May 6, 2026
b4cd5fc
add getLendMarketAllowlist sdk helper
May 6, 2026
b316879
use getAssetAddress in demo markets
May 6, 2026
8c79cf5
add CHAIN_SHORTNAMES sdk constant
May 6, 2026
7ec9a9b
add Wallet.has capability check
May 6, 2026
5c59eb3
derive lend help examples from config
May 6, 2026
8b199c7
pass market directly to getMarket
May 6, 2026
e24b93a
add changeset for boundary cleanup
May 6, 2026
b65a5a6
merge feat/cli-swap into swap-perf
its-everdred May 7, 2026
d42c925
format DeployMorphoMarket prettier drift
its-everdred May 7, 2026
716975b
hardcode chain examples for help text
its-everdred May 7, 2026
7e678ea
rename polling consts to L2 and MAINNET
its-everdred May 7, 2026
8b02c52
rename mainnet polling const to L1
its-everdred May 7, 2026
b5a85ae
simplify lend allowlist with provider names
its-everdred May 7, 2026
8c141ba
derive chain shortnames from viem names
its-everdred May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/add-actions-cli-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'actions-cli': minor
---

Add actions-cli package: agent-first CLI for the Actions SDK. Ships scaffolding,
JSON output pipeline, smart-wallet bootstrap, and smoke commands (`assets`,
`chains`, `wallet address`, `wallet balance`). Lend and swap namespaces land
in subsequent PRs.
29 changes: 29 additions & 0 deletions .changeset/sdk-cli-boundary-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@eth-optimism/actions-sdk': minor
'actions-cli': patch
---

SDK: barrel-export the lend / approval / capability vocabulary that downstream
tooling was reaching past the public API to consume.

- Re-export `ApprovalMode`, `LendProviderName`, and the new `LendAction` literal
from the package root.
- Add a runtime mirror for each: `APPROVAL_MODES` and `LEND_ACTIONS`.
`ApprovalMode` and `LendAction` are now derived from these tuples, so the
type and the runtime list cannot drift.
- Add `CHAIN_SHORTNAMES`, a canonical `Record<SupportedChainId, string>` of
human-friendly chain shortnames (`base`, `op-sepolia`, ...). Use this as
the source of truth for `--chain` parsing and any other surface that maps
user-typed chain strings to a `SupportedChainId`. Adding a new
`SupportedChainId` requires a corresponding entry here.
- Add `getLendMarketAllowlist(lend)`, which flattens every provider's
`marketAllowlist` from a `LendConfig` and skips the `settings` sibling.
- Add `Wallet.has(namespace)` capability check for the `lend` and `swap`
namespaces. Lets callers branch on whether a namespace was registered
without poking at internal fields.

CLI: drop the local mirrors and reach for the SDK exports instead. Help-text
examples now derive from the resolved config (asset symbols, chain shortnames,
chain ids) rather than hard-coding `USDC_DEMO` / `base-sepolia` / `84532`.
`runLendMarket` passes the resolved `LendMarketConfig` straight through to
`actions.lend.getMarket` instead of rebuilding `{address, chainId}`.
28 changes: 28 additions & 0 deletions .changeset/sdk-eoa-perf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@eth-optimism/actions-sdk': minor
---

Perf: cut EOA swap dispatch wall-time on fast L2s.

- `EOAWallet.sendBatch` no longer waits for `confirmations: 2` between sub-txs.
One inclusion wait per tx is enough now that `EOAWallet.walletClient` attaches
viem's default `nonceManager` to the signer, which keeps nonces sequential
locally instead of relying on `eth_getTransactionCount('pending')` on every
send (avoids races on load-balanced RPCs).
- `ChainManager` now defaults the viem `pollingInterval` per chain class:
1000ms for L2-class chains (~1-2s blocks) and 4000ms for L1 mainnet/sepolia
(~12s blocks). Saves ~3 RPC poll cycles per receipt wait on Base/OP/Unichain.
This default applies to the public client used by `getPublicClient()` and to
the public client wrapping the simple bundler client. There is no override
knob; if a real consumer needs one we'll add it then.

Behavioural notes for consumers:

- `sendBatch` is sequential and assumes a sequencer-ordered chain (e.g.
OP-stack L2s). On reorg-heavy chains, callers should consider an additional
confirmations pass at the call site.
- The Velodrome swap path uses **direct ERC-20 max approval** to its universal
router when `approvalMode: 'max'` is requested — there is no Permit2
intermediary as on Uniswap. The full allowance persists at the router until
manually revoked. Continue to scope `approvalMode: 'max'` to demo/testnet
paths.
3 changes: 3 additions & 0 deletions packages/cli/src/commands/__tests__/walletLendClose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ describe('runWalletLendClose', () => {
openPosition: async () => null,
getPosition: getPosition ?? (async () => null),
},
has(namespace: 'lend' | 'swap') {
return namespace === 'lend'
},
} as never,
})
}
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/commands/__tests__/walletLendOpen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ describe('runWalletLendOpen', () => {
wallet: {
address: '0xabc',
lend: { openPosition, closePosition: async () => null },
has(namespace: 'lend' | 'swap') {
return namespace === 'lend'
},
} as never,
})
}
Expand Down Expand Up @@ -210,7 +213,12 @@ describe('runWalletLendOpen', () => {
config: getDemoConfig(),
actions: {} as never,
signer: {} as never,
wallet: { address: '0xabc' } as never,
wallet: {
address: '0xabc',
has() {
return false
},
} as never,
})
try {
await runWalletLendOpen({ market: 'gauntlet-usdc', amount: '1' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ describe('runWalletLendPosition', () => {
closePosition: async () => null,
}
: undefined,
has(namespace: 'lend' | 'swap') {
return namespace === 'lend' && withLend
},
} as never,
})
}
Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/commands/actions/lend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Command } from 'commander'

import { runLendMarket } from '@/commands/actions/lend/market.js'
import { runLendMarkets } from '@/commands/actions/lend/markets.js'
import { loadConfig } from '@/config/loadConfig.js'
import { configuredAssets } from '@/resolvers/assets.js'
import { CHAIN_EXAMPLES } from '@/resolvers/chains.js'

/**
* @description Builds the root `lend` subcommand tree. Children read
Expand All @@ -11,6 +14,8 @@ import { runLendMarkets } from '@/commands/actions/lend/markets.js'
* @returns Commander `Command` configured with `markets` and `market`.
*/
export function lendCommand(): Command {
const assetExample =
configuredAssets(loadConfig())[0]?.metadata.symbol ?? 'USDC'
const command = new Command('lend').description(
'Read-only lending market commands (no PRIVATE_KEY required).',
)
Expand All @@ -19,15 +24,15 @@ export function lendCommand(): Command {
.description('List all lending markets across configured providers.')
.option(
'--asset <symbol>',
'filter to markets denominated in one asset (e.g. USDC_DEMO). Case-insensitive.',
`filter to markets denominated in one asset (e.g. ${assetExample}). Case-insensitive.`,
)
.option(
'--chain <shortname>',
'filter to markets on one chain by shortname (e.g. base-sepolia); mutually exclusive with --chain-id',
`filter to markets on one chain by shortname (e.g. ${CHAIN_EXAMPLES.shortname}); mutually exclusive with --chain-id`,
)
.option(
'--chain-id <id>',
'filter to markets on one chain by numeric id (e.g. 84532); mutually exclusive with --chain',
`filter to markets on one chain by numeric id (e.g. ${CHAIN_EXAMPLES.chainId}); mutually exclusive with --chain`,
)
.action(runLendMarkets)
command
Expand Down
5 changes: 1 addition & 4 deletions packages/cli/src/commands/actions/lend/market.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ export async function runLendMarket(flags: { market: string }): Promise<void> {
const { actions, config } = baseContext()
const market = resolveMarket(flags.market, configuredMarkets(config))
try {
const result = await actions.lend.getMarket({
address: market.address,
chainId: market.chainId,
})
const result = await actions.lend.getMarket(market)
printOutput('lendMarket', result)
} catch (err) {
rethrowAsCliError(err)
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { runWalletAddress } from '@/commands/wallet/address.js'
import { runWalletBalance } from '@/commands/wallet/balance.js'
import { walletLendCommand } from '@/commands/wallet/lend/index.js'
import { walletSwapCommand } from '@/commands/wallet/swap/index.js'
import { CHAIN_EXAMPLES } from '@/resolvers/chains.js'

/**
* @description Builds the `wallet` subcommand tree. Registered children
Expand All @@ -23,11 +24,11 @@ export function walletCommand(): Command {
.description('Print ETH and ERC-20 balances across every configured chain.')
.option(
'--chain <shortnames>',
'filter to one or more chains by shortname; comma-separated (e.g. base-sepolia or base-sepolia,op-sepolia); mutually exclusive with --chain-id',
`filter to one or more chains by shortname; comma-separated (e.g. ${CHAIN_EXAMPLES.shortname} or ${CHAIN_EXAMPLES.shortnameList}); mutually exclusive with --chain-id`,
)
.option(
'--chain-id <ids>',
'filter to one or more chains by numeric id; comma-separated (e.g. 84532 or 84532,130); mutually exclusive with --chain',
`filter to one or more chains by numeric id; comma-separated (e.g. ${CHAIN_EXAMPLES.chainId} or ${CHAIN_EXAMPLES.chainIdList}); mutually exclusive with --chain`,
)
.action(runWalletBalance)
command.addCommand(walletLendCommand())
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/wallet/lend/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { APPROVAL_MODES } from '@eth-optimism/actions-sdk'
import { Command } from 'commander'

import { runWalletLendClose } from '@/commands/wallet/lend/close.js'
Expand All @@ -24,7 +25,7 @@ export function walletLendCommand(): Command {
'amount to supply in human-readable units (e.g. 10 for 10 USDC)',
)
.option(
'--approval-mode <exact|max>',
`--approval-mode <${APPROVAL_MODES.join('|')}>`,
'ERC-20 approval strategy: "exact" approves only this call (default, gas-heavier on repeat); "max" approves max-uint to amortise across future supplies',
)
.action(runWalletLendOpen)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { Wallet } from '@eth-optimism/actions-sdk'

import { CliError } from '@/output/errors.js'

/**
* @description Asserts that a `Wallet` has the lend namespace configured. Narrows `wallet.lend` to non-null on the caller side so each handler can reach `wallet.lend.openPosition` etc. without re-checking. Throws `CliError('config')` when no lend providers are configured (`ActionsConfig.lend` was omitted or empty).
* @description Asserts that a `Wallet` has the lend namespace configured. Defers the runtime check to `Wallet.has('lend')` and narrows `wallet.lend` to non-null on the caller side so each handler can reach `wallet.lend.openPosition` etc. without re-checking. Throws `CliError('config')` when no lend providers are configured (`ActionsConfig.lend` was omitted or empty).
* @param wallet - Wallet instance from `walletContext()`.
* @throws `CliError` with code `config` when `wallet.lend` is undefined.
*/
export function requireLendCapability<W extends { lend?: unknown }>(
export function requireLendCapability<W extends Wallet>(
wallet: W,
): asserts wallet is W & { lend: NonNullable<W['lend']> } {
if (!wallet.lend) {
if (!wallet.has('lend')) {
throw new CliError(
'config',
'Lending is not configured (no providers in config.lend)',
Expand Down
18 changes: 7 additions & 11 deletions packages/cli/src/commands/wallet/lend/runLendAction.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
APPROVAL_MODES,
type ApprovalMode,
type LendAction,
} from '@eth-optimism/actions-sdk'

import { walletContext } from '@/context/walletContext.js'
import { CliError, rethrowAsCliError } from '@/output/errors.js'
import { printOutput } from '@/output/printOutput.js'
Expand All @@ -7,14 +13,6 @@ import { ensureOnchainSuccess, toReceiptArray } from '@/utils/receipts.js'

import { requireLendCapability } from './requireLendCapability.js'

// Mirrors the SDK's `ApprovalMode = 'exact' | 'max'` (declared in
// `@/types/actions` but not re-exported from the SDK barrel).
type ApprovalMode = 'exact' | 'max'
const APPROVAL_MODES = [
'exact',
'max',
] as const satisfies readonly ApprovalMode[]

export interface LendOpenFlags {
market: string
amount: string
Expand All @@ -29,16 +27,14 @@ export type LendCloseFlags =
| { market: string; amount: string; max?: never }
| { market: string; amount?: never; max: true }

type LendAction = 'open' | 'close'

function parseApprovalMode(raw: string | undefined): ApprovalMode | undefined {
if (raw === undefined) return undefined
if ((APPROVAL_MODES as readonly string[]).includes(raw)) {
return raw as ApprovalMode
}
throw new CliError(
'validation',
`Invalid --approval-mode: ${raw} (expected exact or max)`,
`Invalid --approval-mode: ${raw} (expected ${APPROVAL_MODES.join(' or ')})`,
{ approvalMode: raw },
)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/demo/markets.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
ETH,
getAssetAddress,
type LendMarketConfig,
USDC_DEMO,
WETH,
} from '@eth-optimism/actions-sdk'
import type { Address } from 'viem'
import { baseSepolia, optimismSepolia } from 'viem/chains'

/**
Expand All @@ -26,7 +26,7 @@ export const GauntletUSDCDemo: LendMarketConfig = {
* gateway. Mirrored from the demo backend's config.
*/
export const AaveETH: LendMarketConfig = {
address: WETH.address[optimismSepolia.id] as Address,
address: getAssetAddress(WETH, optimismSepolia.id),
chainId: optimismSepolia.id,
name: 'Aave ETH',
asset: ETH,
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/output/printOutput.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type {
Asset,
LendAction,
LendMarket,
LendMarketPosition,
LendProviderName,
SupportedChainId,
SwapMarket,
SwapQuote,
Expand All @@ -28,12 +30,12 @@ export interface AddressDoc {
}

export interface LendActionDoc {
action: 'open' | 'close'
action: LendAction
market: {
name: string
address: Address
chainId: SupportedChainId
provider: string
provider: LendProviderName
}
asset: { symbol: string }
amount: number
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/resolvers/__tests__/chains.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ const ALL: SupportedChainId[] = [
optimismSepolia.id,
]

const SHORTNAMES = ['base', 'base-sepolia', 'optimism', 'op-sepolia'] as const
const SHORTNAMES = ['base', 'base-sepolia', 'op-mainnet', 'op-sepolia'] as const

describe('resolveChain', () => {
it('resolves each canonical shortname to its chain id', () => {
expect(resolveChain('base-sepolia', ALL)).toBe(baseSepolia.id)
expect(resolveChain('op-sepolia', ALL)).toBe(optimismSepolia.id)
expect(resolveChain('op-mainnet', ALL)).toBe(optimism.id)
})

it('also accepts curated aliases on top of the viem name', () => {
expect(resolveChain('optimism', ALL)).toBe(optimism.id)
})

Expand Down Expand Up @@ -51,7 +55,7 @@ describe('shortnameFor', () => {
it('returns the canonical shortname for each supported chain id', () => {
expect(shortnameFor(baseSepolia.id)).toBe('base-sepolia')
expect(shortnameFor(optimismSepolia.id)).toBe('op-sepolia')
expect(shortnameFor(optimism.id)).toBe('optimism')
expect(shortnameFor(optimism.id)).toBe('op-mainnet')
})
})

Expand Down
Loading