Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ec6cdb8
feat: treehouse integration
57Ark Oct 26, 2026
68f985f
fix: release next branch without api tests
57Ark Oct 27, 2026
880c99d
fix: permissionless oracles assets methods
57Ark Oct 27, 2026
f85ae8c
fix: add assets list
57Ark Oct 28, 2026
729627a
fix: refactor
57Ark Oct 28, 2026
8ebdc01
feat: testnets sdk
doomsower Oct 28, 2026
8afec6e
Merge pull request #560 from Gearbox-protocol/testnets_sdk
doomsower Oct 28, 2026
0932c08
fix: asset types
57Ark Oct 28, 2026
f9e9d91
feat(permissionless): merge market builders into one GIP
gregorymel Oct 28, 2026
1f04732
fix: asset types
57Ark Oct 29, 2026
0f6a544
fix: move calculations and onchain reads to psdk
57Ark Oct 29, 2026
4043c1e
fix: latest prices fetch
57Ark Oct 29, 2026
8dd61f6
feat(permissionless): add configurators namespace
gregorymel Oct 29, 2026
a0f0cec
feat(permissionless): expose creationState and inputActions in market…
gregorymel Oct 29, 2026
7f6e815
Merge origin/next into feat/permissionless-configurators
gregorymel Oct 29, 2026
ea7877e
fix(permissionless): keep a local hex schema for configurators
gregorymel Oct 29, 2026
46ad9e7
feat(permissionless): name the action in curator param errors
gregorymel Nov 2, 2026
72668c7
feat(permissionless): throw the build error of a blocked curator preview
gregorymel Nov 2, 2026
497a6c3
fix(permissionless): keep skipped shared calls in merged market previews
gregorymel Nov 2, 2026
57f04b0
feat(permissionless): record the RWA periphery setup on a market builder
gregorymel Nov 2, 2026
f6a1cc9
feat(permissionless): name a market's own credit suites by reference
gregorymel Nov 2, 2026
e0674af
feat(permissionless): edit a market's credit suites on its builder
gregorymel Nov 2, 2026
920193a
fix(permissionless): reference a credit suite an action check blocks
gregorymel Nov 2, 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
14 changes: 11 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,22 @@ jobs:
with:
version: ${{ steps.foundry.outputs.version }}

- name: Test and build
- name: Typecheck and test
env:
ALCHEMY_KEY: ${{ secrets.ALCHEMY_KEY_CI }}
RPC_URL: "https://eth-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY_CI }}"
run: |
pnpm typecheck:ci
pnpm test:all
pnpm build
pnpm vitest run --project unit --project e2e

# Backend releases depend on prereleases from `next`, so there failing
# backend client tests must not block the release.
- name: Backend client tests
continue-on-error: ${{ github.ref_name == 'next' }}
run: pnpm test:api

- name: Build
run: pnpm build

- name: Semantic Release
uses: cycjimmy/semantic-release-action@v8
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ example-state.json
.pnpm-store/
.agents/
.codex/
gearbox-sdk-local.tgz
91 changes: 89 additions & 2 deletions docs/curator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ market.addAction(parseMarketScopedAction(JSON.parse(body)));

`addAction` runs the action's Zod schema, so a typed call and a JSON payload
produce identical, normalized parameters (percentages rounded, defaults
applied) and invalid parameters are rejected on the spot.
applied) and invalid parameters are rejected on the spot, with a
`GipScopeError` (`INVALID_ACTION_PARAMS`) that names the action and lists every
rejected param in `invalidParams`.

Bundle payloads are also parsed and normalized when they satisfy their
schemas. For compatibility with legacy catalog entries, an older parameter that
Expand Down Expand Up @@ -354,6 +356,26 @@ reference to a suite no earlier action deploys is an `UNKNOWN_CREDIT_SUITE`
error. `retargetCreditManager`, `copyCmActions` and `copyCmActionsForMarket`
move or copy actions between targets of either kind.

A market builder gives a suite recorded without an `id` one — its name, suffixed
with `#2`, `#3`… when an earlier suite of the builder already uses it — and, when it
compiles, rewrites every action that names a suite of the market by its predicted
address to the suite's reference. `preview.inputActions` therefore names the
market's own suites by reference however they were recorded, so a list persisted
from a preview and recorded again keeps following its suites; the payload is the
same either way. An action recorded once by address and once by reference merges
like any other repeated action. Since ids differ, the same suite recorded twice is
two deployments, and the second is a `CREDIT_MANAGER_EXISTS` error.

Because the market's suites are named by reference, editing one predicts no
address. `market.replaceCreditSuite(creditManager, suite)` swaps a suite's
params and keeps its id, so everything configuring it follows;
`market.copyCreditSuite(creditManager, name)` adds a copy with id `<id>#<name>`
and copies of the actions configuring it; `market.removeAdapter(creditManager,
target, { keepAllowTx })` drops an adapter's actions. Each takes the suite's
predicted address, as a preview shows it, or its reference, and resolves when the
builder compiles; replacing or copying anything but a suite of the market is a
`GipBuilderError` (`CREDIT_SUITE_NOT_FOUND`).

### Adding actions

`addMarketAction(actions, action)` (and `addConfiguratorAction`) is how every
Expand Down Expand Up @@ -586,7 +608,8 @@ preview.blockedAt; // 3
- An **error** stops the projection at that action, as the batch would revert
there: `preview.after` is the state right before it, and `build()` and
`simulate()` reject with a `GipActionError` (`code`, `actionIndex`,
`actionType`, `path`, `diagnostics`).
`actionType`, `path`, `diagnostics`). `assertNotBlocked(preview)` throws the
same error for a preview of any scope, or a merged GIP, without building it.
- A **warning** is reported and the projection goes on, e.g.
`RAMP_START_CLAMPED` for a ramp starting in the past, which the credit
configurator moves to execution time.
Expand Down Expand Up @@ -1038,3 +1061,67 @@ To add a GIP, write its module next to the others — `timelockTxHashes` and
`priceUpdates` come from the TimeLock's `ExecuteTransaction` logs and the
execution transactions — list it in `src/e2e/curator/executedGips.ts`, and
record its fixture.

## One GIP over several markets

`session.merge(builders)` merges market builders of one session into one GIP with
the same `preview()`, `build()` and `simulate()` as a single builder:

```ts
const usdc = session.market({ pool: USDC_POOL }).addAction(registerNft);
const rwa = session.newMarket(rwaParams).addAction(suiteUsingNft);
const gip = session.merge([usdc, rwa]);

const preview = await gip.preview({ eta }); // preview.markets[i] is builders[i]'s MarketPreview
const plan = await gip.build({ author, eta });
const result = await gip.simulate(); // session created with fork: { rpcUrl }
```

Markets share nothing but configurator periphery, so they execute one after another
and each sees the periphery the earlier ones deployed or registered. A market waits
for the markets providing periphery it requires (a deployment before its
registration, a Degen NFT registration before a suite or facade using it), and a
periphery call listed by several markets runs once; a conflicting one is blocked.
A market keeps such a call in its `inputActions` even where it does not run, so
`blockedAt`, `actionDiagnostics[].actionIndex` and `txs[].actionIndex` index the
market's whole action list, and a list persisted from a preview loses nothing.
`preview.executionOrder` names the pools in the order their calls execute, and
`preview.batches` holds every call of the proposal.

Each market is compiled even when another one is blocked, so a preview reports every
problem at once; `build()` and `simulate()` reject while any market is blocked. The
builders stay live: each call compiles what they have recorded by then. The merged
plan is separate from the single-scope versioned plan serialization.

## Integration API

Market and configurator `preview()` results also expose the compiled `txs`, including
per-action decoded calls, without performing a second execution.

Action definitions and parameter types, concrete adapter parameter/state types, and
IRM, loss policy and rate keeper defaults are available through the public permissionless
entry. `MarketActionOf<T>` selects a built-in action by type, and
`GipRegistry.marketAction(type)` retains the concrete definition for a built-in type.

`market.addRwaPeriphery()` records the periphery a Securitize RWA market needs —
its Degen NFT registered and, for a default underlying, its zapper deployed and
registered — and resolves it against the chain when the builder compiles, after the
actions recorded before it. It adds nothing for an ordinary underlying or a complete
setup, and replaces a legacy author-derived zapper, so it both sets up a new market
(`session.newMarket(params).addRwaPeriphery()`) and repairs an existing draft.

Without a session, `planRwaPeripheryActions` takes chain lookups and returns the
periphery actions a new Securitize RWA market is missing;
`completeRwaPeripheryActions`, `reconcileRwaPeripheryActions` and
`insertRwaZapperDeployment` repair action lists without mutating their inputs.

Deployment schemas define supported versions. Legacy adapters belong to application
compatibility registries; they are not made available to new users by this API.

For historical transaction inspection, `MarketExecutor` and `ConfigurationExecutor`
accept `{ checkActions: false }`: they resolve, project and encode the recorded calls
without applying current source-block preflight checks to already executed calls.
Session previews and plans always enforce their normal checks.

External plugins and UI validation can reuse `adapterPluginKit.checkCollateralTokens`
and `adapterPluginKit.getCurveTargetCoins` without duplicating the protocol checks.
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
"types": "./dist/types/permissionless/index.d.ts",
"import": "./dist/esm/permissionless/index.js",
"default": "./dist/cjs/permissionless/index.js"
},
"./testnets": {
"types": "./dist/types/testnets/index.d.ts",
"import": "./dist/esm/testnets/index.js",
"default": "./dist/cjs/testnets/index.js"
}
},
"files": [
Expand All @@ -69,8 +74,9 @@
"dev": "tsdown --watch",
"example": "tsx scripts/example.ts | pino-pretty",
"list-opportunities": "tsx scripts/list-opportunities.ts",
"test:watch": "vitest --project unit",
"test:unit": "vitest run --project unit",
"test:watch": "vitest --project unit --project api",
"test:unit": "vitest run --project unit --project api",
"test:api": "vitest run --project api",
"test:e2e": "vitest run --project e2e",
"test:all": "vitest run",
"test": "vitest run",
Expand Down
50 changes: 50 additions & 0 deletions scripts/create-testnet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { pino } from "pino";
import { ADDRESS_PROVIDER_V310, chains } from "../src/onchain/index.js";
import { TestnetsAPI } from "../src/testnets/index.js";

const logger = pino({
level: process.env.LOG_LEVEL ?? "debug",
base: null,
formatters: {
level: label => {
return {
level: label,
};
},
},
});

async function createTestnet(): Promise<void> {
const apiKey = process.env.ANVIL_MANAGER_API_KEY;
if (!apiKey) {
throw new Error("ANVIL_MANAGER_API_KEY is not set");
}

const testnets = new TestnetsAPI({ apiKey, logger });
const created = await testnets.create({
description: "Mainnet",
network: "Mainnet",
addressProvider: ADDRESS_PROVIDER_V310,
marketConfigurators: chains.Mainnet.defaultMarketConfigurators.keys(),
scripts: [
{
id: "deploy-faucet",
docker: "permissionless-scripts:staging",
timeout: "10m0s",
args: ["deploy-faucet"],
},
],
timeout: "1h",
disableRPCProtection: true,
backend: { version: "staging-testnet" },
});
logger.info(`created testnet ${created.executionId}: ${created.url}`);

const status = await testnets.waitUntilReady(created.executionId);
logger.info({ status }, "testnet is ready");
}

createTestnet().catch(e => {
logger.error(e);
process.exit(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
export const treehouseLiquidationSubcompressorAbi = [
{
type: "function",
name: "contractType",
inputs: [],
outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }],
stateMutability: "view",
},
{
type: "function",
name: "getLiquidationData",
inputs: [
{ name: "liquidator", type: "address", internalType: "address" },
{ name: "creditAccount", type: "address", internalType: "address" },
{ name: "phantomToken", type: "address", internalType: "address" },
{
name: "priceUpdates",
type: "tuple[]",
internalType: "struct PriceUpdate[]",
components: [
{ name: "priceFeed", type: "address", internalType: "address" },
{ name: "data", type: "bytes", internalType: "bytes" },
],
},
],
outputs: [
{
name: "data",
type: "tuple",
internalType: "struct LiquidationData",
components: [
{ name: "requiredToken", type: "address", internalType: "address" },
{ name: "requiredAmount", type: "uint256", internalType: "uint256" },
{
name: "expectedOutputs",
type: "tuple[]",
internalType: "struct LiquidationOutput[]",
components: [
{ name: "token", type: "address", internalType: "address" },
{ name: "amount", type: "uint256", internalType: "uint256" },
{ name: "delayed", type: "bool", internalType: "bool" },
{
name: "redeemerAddress",
type: "address",
internalType: "address",
},
{ name: "claimableAt", type: "uint256", internalType: "uint256" },
],
},
{
name: "liquidationCall",
type: "tuple",
internalType: "struct MultiCall",
components: [
{ name: "target", type: "address", internalType: "address" },
{ name: "callData", type: "bytes", internalType: "bytes" },
],
},
{ name: "isLiquidatorEligible", type: "bool", internalType: "bool" },
{ name: "isCreditAccountFrozen", type: "bool", internalType: "bool" },
{ name: "kycProtocol", type: "string", internalType: "string" },
{ name: "kycToken", type: "address", internalType: "address" },
],
},
],
stateMutability: "nonpayable",
},
{
type: "function",
name: "getRWALiquidatorInfo",
inputs: [{ name: "token", type: "address", internalType: "address" }],
outputs: [
{
name: "info",
type: "tuple",
internalType: "struct RWALiquidatorInfo",
components: [
{ name: "gateway", type: "address", internalType: "address" },
{
name: "liquidatorAddress",
type: "address",
internalType: "address",
},
{ name: "contractType", type: "bytes32", internalType: "bytes32" },
],
},
],
stateMutability: "view",
},
{
type: "function",
name: "version",
inputs: [],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
] as const;
Loading
Loading