Skip to content

Commit bec6564

Browse files
authored
Merge pull request #14 from morpho-org/blue-api-version
Feat: Blue api version
2 parents 0790e3e + 118ab8a commit bec6564

35 files changed

Lines changed: 3678 additions & 4406 deletions

README.md

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
# Morpho Blue Reallocation Bot
22

3-
A simple, fast, and easily deployable reallocation bot for the **Morpho Blue** protocol. This bot is entirely based on **RPC calls** and is designed to
4-
meet borrow demand within MetaMorpho vaults by equalizing utilization rates across markets.
3+
A simple, fast, and easily deployable reallocation bot for the **Morpho Blue** protocol. This bot is entirely based on **RPC calls and the Morpho API** and is designed to automate Morpho vaultsV1 reallocations according customable strategies.
54

65
## Features
76

87
- Automatically rebalances assets within MetaMorpho vaults to maintain capital efficiency
98
- Equalizes utilization rates across markets
109
- Multi-chain compatible (Ethereum, Base, and more)
11-
- Minimal setup and dependencies (RPC-only, no extra infra required)
1210
- Configurable minimum threshold for utilization changes (2.5% by default)
1311

1412
### ⚠️ Disclaimer
@@ -21,7 +19,6 @@ Use at your own risk.
2119

2220
- Node.js >= 20
2321
- [pnpm](https://pnpm.io/) (this repo uses `pnpm` as package manager)
24-
- [Docker](https://www.docker.com/) (optional, only needed if you want to run the database locally)
2522
- A valid RPC URL (via Alchemy, Infura, etc)
2623
- The private key of an EOA with enough funds to pay for gas
2724

@@ -35,26 +32,7 @@ pnpm install
3532

3633
## Chain Configuration
3734

38-
The bot can be configured to run on any EVM-compatible chain where the Morpho stack has been deployed. The chain configuration is done in the `apps/config/config.ts` file.
39-
40-
### Morpho Stack parameters (addresses and start blocks)
41-
42-
**If you don't plan on supporting a new chain, you can ignore this section.**
43-
44-
Morpho Blue:
45-
46-
- `morpho.address`: The address of the Morpho contract.
47-
- `morpho.startBlock`: The block number of the Morpho contract deployment.
48-
49-
Adaptive Curve IRM:
50-
51-
- `adaptiveCurveIrm.address`: The address of the Adaptive Curve IRM contract.
52-
- `adaptiveCurveIrm.startBlock`: The block number of the Adaptive Curve IRM contract deployment.
53-
54-
Meta Morpho Factories:
55-
56-
- `metaMorphoFactories.addresses`: The addresses of the MetaMorpho factories.
57-
- `metaMorphoFactories.startBlock`: The block number of the oldest MetaMorpho factory deployment.
35+
The bot can be configured to run on any EVM-compatible chain where the Morpho stack has been deployed and supported by the Morpho API. The chain configuration is done in the `apps/config/config.ts` file. Just fill the array with the chains where you want the bot to run.
5836

5937
### Secrets
6038

@@ -122,14 +100,4 @@ Once the bot is installed and configured, you can run it by executing the follow
122100
pnpm reallocate
123101
```
124102

125-
This command will start the bot, which will start reallocating once the configured chains are fully indexed.
126-
127-
⚠⏱️ The indexing process can take some time depending on the number of blocks to index.
128-
129-
## Config Changes
130-
131-
Unfortunately, Ponder doesn't allow the same schema to be used with different configs.
132-
In this project, the config changes only if you add, remove, or modify a chain.
133-
134-
So, if you try to run the bot with a set of chains that's different from the one used in your initial run, indexing will fail.
135-
You should use another schema (this handled in `apps/client/src/script.ts`).
103+
This command will start the bot.

apps/client/codegen.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { CodegenConfig } from "@graphql-codegen/cli";
2+
3+
// Option 1: If you have @morpho-org/morpho-ts installed
4+
// import { BLUE_API_GRAPHQL_URL } from "@morpho-org/morpho-ts";
5+
6+
// Option 2: Or use the URL directly
7+
const BLUE_API_GRAPHQL_URL = "https://api.morpho.org/graphql";
8+
9+
const config: CodegenConfig = {
10+
overwrite: true,
11+
schema: BLUE_API_GRAPHQL_URL,
12+
documents: ["graphql/**/*.{query,fragment}.gql"], // Adjust path to your GraphQL files
13+
generates: {
14+
"src/api/types.ts": {
15+
plugins: ["typescript-operations"],
16+
preset: "import-types",
17+
presetConfig: {
18+
typesPath: "@morpho-org/blue-api-sdk", // If using blue-api-sdk types
19+
// OR use "./types.js" if generating base types yourself
20+
},
21+
config: {
22+
avoidOptionals: {
23+
field: true,
24+
inputValue: false,
25+
defaultValue: true,
26+
},
27+
scalars: {
28+
BigInt: {
29+
input: `Types.Scalars["BigInt"]["input"]`,
30+
output: `Types.Scalars["BigInt"]["output"]`,
31+
},
32+
HexString: {
33+
input: `Types.Scalars["HexString"]["input"]`,
34+
output: `Types.Scalars["HexString"]["output"]`,
35+
},
36+
Address: {
37+
input: `Types.Scalars["Address"]["input"]`,
38+
output: `Types.Scalars["Address"]["output"]`,
39+
},
40+
MarketId: {
41+
input: `Types.Scalars["MarketId"]["input"]`,
42+
output: `Types.Scalars["MarketId"]["output"]`,
43+
},
44+
},
45+
},
46+
},
47+
"src/api/sdk.ts": {
48+
plugins: ["typescript-graphql-request"],
49+
preset: "import-types",
50+
presetConfig: {
51+
typesPath: "./types.js",
52+
},
53+
},
54+
},
55+
};
56+
57+
export default config;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
query getVaultsData($chainId: Int!, $addresses: [String!]!) {
2+
vaults(first: 1000, where: { chainId_in: [$chainId], address_in: $addresses }) {
3+
items {
4+
address
5+
state {
6+
allocation {
7+
market {
8+
uniqueKey
9+
collateralAsset {
10+
address
11+
}
12+
loanAsset {
13+
address
14+
}
15+
oracle {
16+
address
17+
}
18+
irmAddress
19+
lltv
20+
state {
21+
supplyAssets
22+
supplyShares
23+
borrowAssets
24+
borrowShares
25+
rateAtTarget
26+
fee
27+
timestamp
28+
}
29+
}
30+
supplyAssets
31+
supplyCap
32+
}
33+
}
34+
}
35+
}
36+
}

apps/client/package.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@
66
"scripts": {
77
"dev": "tsc && concurrently \"tsc --watch\" \"node dist/client/src/index.js\"",
88
"lint": "eslint . --fix",
9-
"typecheck": "tsc --noEmit"
9+
"typecheck": "tsc --noEmit",
10+
"codegen": "graphql-codegen --config codegen.ts"
1011
},
1112
"dependencies": {
1213
"@morpho-blue-reallocation-bot/config": "workspace:*",
13-
"@ponder/client": "^0.9.27"
14+
"@morpho-org/blue-api-sdk": "^2.17.2",
15+
"@ponder/client": "^0.9.27",
16+
"graphql-request": "^7.4.0",
17+
"graphql-tag": "^2.12.6"
1418
},
1519
"devDependencies": {
20+
"@graphql-codegen/cli": "^6.1.0",
21+
"@graphql-codegen/import-types-preset": "^3.0.1",
22+
"@graphql-codegen/typescript": "^5.0.7",
23+
"@graphql-codegen/typescript-graphql-request": "^6.3.0",
24+
"@graphql-codegen/typescript-operations": "^5.0.7",
1625
"@morpho-org/test": "^2.1.1",
1726
"nock": "^14.0.2"
1827
}

apps/client/src/api/index.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { GraphQLClient } from "graphql-request";
2+
3+
import type { InputMaybe, OrderDirection } from "@morpho-org/blue-api-sdk";
4+
import { getSdk } from "./sdk.js";
5+
6+
export * from "./sdk.js";
7+
export * from "./types.js";
8+
9+
const BLUE_API_GRAPHQL_URL = "https://api.morpho.org/graphql";
10+
11+
export const apiSdk: ReturnType<typeof getSdk> = getSdk(new GraphQLClient(BLUE_API_GRAPHQL_URL));
12+
13+
type QueryVariables = Record<string, unknown>;
14+
15+
export type PaginationVariables<V extends QueryVariables> = V & {
16+
first?: number;
17+
skip?: number;
18+
orderDirection?: InputMaybe<OrderDirection>;
19+
};
20+
21+
export interface PageInfo {
22+
count: number;
23+
countTotal: number;
24+
limit: number;
25+
skip: number;
26+
}
27+
28+
export interface PageResult<T> {
29+
items: T[] | null;
30+
pageInfo: PageInfo | null;
31+
}
32+
33+
export async function paginatedQuery<T, V extends QueryVariables = {}>(
34+
fetchPage: (vars: PaginationVariables<V>) => Promise<PageResult<T>>,
35+
{
36+
pageSize,
37+
orderBy,
38+
orderDirection,
39+
args,
40+
}: {
41+
pageSize: number;
42+
orderBy?: string;
43+
orderDirection?: InputMaybe<OrderDirection>;
44+
args: PaginationVariables<V>;
45+
},
46+
): Promise<T[]> {
47+
// 1) Fetch the first page serially to get total count
48+
const firstVars: PaginationVariables<V> = {
49+
...args,
50+
first: pageSize,
51+
...(orderBy != null ? { orderBy } : {}),
52+
...(orderDirection != null ? { orderDirection } : {}),
53+
skip: 0,
54+
};
55+
const firstPage = await fetchPage(firstVars);
56+
const countTotal = firstPage.pageInfo?.countTotal ?? 0;
57+
58+
if (firstPage.items == null) {
59+
console.warn(`paginatedQuery: received null items on first page`);
60+
}
61+
if (firstPage.pageInfo == null) {
62+
console.warn(`paginatedQuery: received null pageInfo on first page`);
63+
}
64+
65+
const allItems: T[] = firstPage.items ?? [];
66+
67+
// 2) Calculate how many additional pages we need
68+
const totalPages = Math.ceil(countTotal / pageSize);
69+
if (totalPages <= 1) {
70+
return allItems;
71+
}
72+
73+
// 3) Kick off all remaining page fetches in parallel
74+
const pagePromises: Promise<PageResult<T>>[] = [];
75+
for (let pageIndex = 1; pageIndex < totalPages; pageIndex++) {
76+
pagePromises.push(fetchPage({ ...firstVars, skip: pageIndex * pageSize }));
77+
}
78+
79+
// 4) Wait for them all
80+
const otherPages = await Promise.all(pagePromises);
81+
82+
// 5) Flatten, warning on nulls
83+
otherPages.forEach((page, idx) => {
84+
const { items } = page;
85+
const pageNum = idx + 2; // human-friendly page number
86+
if (items == null) {
87+
console.warn(`paginateGraphQLConcurrent: received null items on page ${pageNum}`);
88+
} else {
89+
allItems.push(...items);
90+
}
91+
});
92+
93+
// 6) Return full list + last pageInfo (from the last fetch)
94+
return allItems;
95+
}

apps/client/src/api/sdk.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as Types from './types.js';
2+
3+
import { GraphQLClient, RequestOptions } from 'graphql-request';
4+
import gql from 'graphql-tag';
5+
type GraphQLClientRequestHeaders = RequestOptions['requestHeaders'];
6+
7+
export const GetVaultsDataDocument = gql`
8+
query getVaultsData($chainId: Int!, $addresses: [String!]!) {
9+
vaults(first: 1000, where: {chainId_in: [$chainId], address_in: $addresses}) {
10+
items {
11+
address
12+
state {
13+
allocation {
14+
market {
15+
uniqueKey
16+
collateralAsset {
17+
address
18+
}
19+
loanAsset {
20+
address
21+
}
22+
oracle {
23+
address
24+
}
25+
irmAddress
26+
lltv
27+
state {
28+
supplyAssets
29+
supplyShares
30+
borrowAssets
31+
borrowShares
32+
rateAtTarget
33+
fee
34+
timestamp
35+
}
36+
}
37+
supplyAssets
38+
supplyCap
39+
}
40+
}
41+
}
42+
}
43+
}
44+
`;
45+
46+
export type SdkFunctionWrapper = <T>(action: (requestHeaders?:Record<string, string>) => Promise<T>, operationName: string, operationType?: string, variables?: any) => Promise<T>;
47+
48+
49+
const defaultWrapper: SdkFunctionWrapper = (action, _operationName, _operationType, _variables) => action();
50+
51+
export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
52+
return {
53+
getVaultsData(variables: Types.GetVaultsDataQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<Types.GetVaultsDataQuery> {
54+
return withWrapper((wrappedRequestHeaders) => client.request<Types.GetVaultsDataQuery>({ document: GetVaultsDataDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'getVaultsData', 'query', variables);
55+
}
56+
};
57+
}
58+
export type Sdk = ReturnType<typeof getSdk>;

apps/client/src/api/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Types from '@morpho-org/blue-api-sdk';
2+
3+
export type GetVaultsDataQueryVariables = Types.Exact<{
4+
chainId: Types.Scalars['Int']['input'];
5+
addresses: Array<Types.Scalars['String']['input']> | Types.Scalars['String']['input'];
6+
}>;
7+
8+
9+
export type GetVaultsDataQuery = { __typename?: 'Query', vaults: { __typename?: 'PaginatedMetaMorphos', items: Array<{ __typename?: 'Vault', address: Types.Scalars["Address"]["output"], state: { __typename?: 'VaultState', allocation: Array<{ __typename?: 'VaultAllocation', supplyAssets: Types.Scalars["BigInt"]["output"], supplyCap: Types.Scalars["BigInt"]["output"], market: { __typename?: 'Market', uniqueKey: Types.Scalars["MarketId"]["output"], irmAddress: Types.Scalars["Address"]["output"], lltv: Types.Scalars["BigInt"]["output"], collateralAsset: { __typename?: 'Asset', address: Types.Scalars["Address"]["output"] } | null, loanAsset: { __typename?: 'Asset', address: Types.Scalars["Address"]["output"] }, oracle: { __typename?: 'Oracle', address: Types.Scalars["Address"]["output"] } | null, state: { __typename?: 'MarketState', supplyAssets: Types.Scalars["BigInt"]["output"], supplyShares: Types.Scalars["BigInt"]["output"], borrowAssets: Types.Scalars["BigInt"]["output"], borrowShares: Types.Scalars["BigInt"]["output"], rateAtTarget: Types.Scalars["BigInt"]["output"] | null, fee: number, timestamp: Types.Scalars["BigInt"]["output"] } | null } }> } | null }> | null } };

apps/client/src/bot.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ export class ReallocationBot {
3232

3333
async run() {
3434
const { client } = this;
35-
const { vaultWhitelist } = this;
36-
const vaultsData = await Promise.all(
37-
vaultWhitelist.map((vault) => fetchVaultData(this.chainId, vault)),
38-
);
35+
const vaultsData = await fetchVaultData(this.chainId, this.vaultWhitelist);
3936

4037
await Promise.all(
4138
vaultsData.map(async (vaultData) => {

0 commit comments

Comments
 (0)