Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
81 changes: 78 additions & 3 deletions ts/packages/defi-cli/src/commands/lending.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Command } from "commander";
import type { OutputMode } from "../output.js";
import type { Executor } from "../executor.js";
import { printOutput } from "../output.js";
import { InterestRateMode } from "@hypurrquant/defi-core";
import { InterestRateMode, type MorphoMarketId } from "@hypurrquant/defi-core";
import type { Address } from "viem";
import { maxUint256 } from "viem";
import { createLending } from "@hypurrquant/defi-protocols";
Expand Down Expand Up @@ -59,6 +59,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make
.requiredOption("--protocol <protocol>", "Protocol slug")
.requiredOption("--asset <token>", "Token symbol or address")
.requiredOption("--amount <amount>", "Amount to supply in wei (or 'max')")
.option("--market <marketId>", "Morpho Blue marketId (32-byte hex) — required for direct Morpho markets, ignored elsewhere")
.option("--on-behalf-of <address>", "On behalf of address")
.action(async (opts) => {
const executor = makeExecutor();
Expand All @@ -67,7 +68,10 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make
const adapter = createLending(ctx.protocol!, ctx.rpcUrl);
const asset = resolveTokenAddress(ctx.registry, ctx.chainName, opts.asset);
const onBehalfOf = resolveWallet(opts.onBehalfOf);
const tx = await adapter.buildSupply({ protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), on_behalf_of: onBehalfOf });
const tx = await adapter.buildSupply({
protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), on_behalf_of: onBehalfOf,
market_id: opts.market as MorphoMarketId | undefined,
});
const result = await executor.execute(tx);
printOutput(result, getOpts());
});
Expand All @@ -78,6 +82,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make
.requiredOption("--asset <token>", "Token symbol or address")
.requiredOption("--amount <amount>", "Amount in wei (or 'max')")
.option("--rate-mode <mode>", "variable or stable", "variable")
.option("--market <marketId>", "Morpho Blue marketId (32-byte hex) — required for direct Morpho markets, ignored elsewhere")
.option("--on-behalf-of <address>", "On behalf of address")
.action(async (opts) => {
const executor = makeExecutor();
Expand All @@ -90,6 +95,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make
protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount),
interest_rate_mode: opts.rateMode === "stable" ? InterestRateMode.Stable : InterestRateMode.Variable,
on_behalf_of: onBehalfOf,
market_id: opts.market as MorphoMarketId | undefined,
});
const result = await executor.execute(tx);
printOutput(result, getOpts());
Expand All @@ -101,6 +107,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make
.requiredOption("--asset <token>", "Token symbol or address")
.requiredOption("--amount <amount>", "Amount in wei (or 'max')")
.option("--rate-mode <mode>", "variable or stable", "variable")
.option("--market <marketId>", "Morpho Blue marketId (32-byte hex) — required for direct Morpho markets, ignored elsewhere")
.option("--on-behalf-of <address>", "On behalf of address")
.action(async (opts) => {
const executor = makeExecutor();
Expand All @@ -113,6 +120,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make
protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount),
interest_rate_mode: opts.rateMode === "stable" ? InterestRateMode.Stable : InterestRateMode.Variable,
on_behalf_of: onBehalfOf,
market_id: opts.market as MorphoMarketId | undefined,
});
const result = await executor.execute(tx);
printOutput(result, getOpts());
Expand All @@ -123,6 +131,7 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make
.requiredOption("--protocol <protocol>", "Protocol slug")
.requiredOption("--asset <token>", "Token symbol or address")
.requiredOption("--amount <amount>", "Amount in wei (or 'max')")
.option("--market <marketId>", "Morpho Blue marketId (32-byte hex) — required for direct Morpho markets, ignored elsewhere")
.option("--to <address>", "Recipient address")
.action(async (opts) => {
const executor = makeExecutor();
Expand All @@ -131,7 +140,10 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make
const adapter = createLending(ctx.protocol!, ctx.rpcUrl);
const asset = resolveTokenAddress(ctx.registry, ctx.chainName, opts.asset);
const to = resolveWallet(opts.to);
const tx = await adapter.buildWithdraw({ protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), to });
const tx = await adapter.buildWithdraw({
protocol: ctx.protocol!.name, asset, amount: parseAmount(opts.amount), to,
market_id: opts.market as MorphoMarketId | undefined,
});
const result = await executor.execute(tx);
printOutput(result, getOpts());
});
Expand Down Expand Up @@ -192,4 +204,67 @@ export function registerLending(parent: Command, getOpts: () => OutputMode, make
const result = await executor.execute(tx);
printOutput(result, getOpts());
});

lending.command("supply-collateral")
.description("Supply the collateral side of a Morpho Blue market (different selector from supply)")
.requiredOption("--protocol <protocol>", "Protocol slug (must be a Morpho Blue adapter)")
.requiredOption("--asset <token>", "Collateral token symbol or address")
.requiredOption("--amount <amount>", "Amount in wei (or 'max')")
.requiredOption("--market <marketId>", "32-byte Morpho marketId (find via Morpho API)")
.option("--on-behalf-of <address>", "On behalf of address")
.action(async (opts) => {
const executor = makeExecutor();
const ctx = resolveContext(parent, getOpts, opts.protocol);
if (!ctx) return;
const adapter = createLending(ctx.protocol!, ctx.rpcUrl);
if (typeof adapter.buildSupplyCollateral !== "function") {
printOutput({
error: `[${ctx.protocol!.name}] adapter does not implement buildSupplyCollateral. ` +
`Only Morpho Blue forks expose this; Aave V3 / Compound use plain supply.`,
}, getOpts());
return;
}
const asset = resolveTokenAddress(ctx.registry, ctx.chainName, opts.asset);
const onBehalfOf = resolveWallet(opts.onBehalfOf);
const tx = await adapter.buildSupplyCollateral({
protocol: ctx.protocol!.name,
asset,
amount: parseAmount(opts.amount),
on_behalf_of: onBehalfOf,
market_id: opts.market as MorphoMarketId,
});
const result = await executor.execute(tx);
printOutput(result, getOpts());
});

lending.command("withdraw-collateral")
.description("Withdraw the collateral side of a Morpho Blue market")
.requiredOption("--protocol <protocol>", "Protocol slug (must be a Morpho Blue adapter)")
.requiredOption("--asset <token>", "Collateral token symbol or address")
.requiredOption("--amount <amount>", "Amount in wei (or 'max')")
.requiredOption("--market <marketId>", "32-byte Morpho marketId")
.option("--to <address>", "Recipient address")
.action(async (opts) => {
const executor = makeExecutor();
const ctx = resolveContext(parent, getOpts, opts.protocol);
if (!ctx) return;
const adapter = createLending(ctx.protocol!, ctx.rpcUrl);
if (typeof adapter.buildWithdrawCollateral !== "function") {
printOutput({
error: `[${ctx.protocol!.name}] adapter does not implement buildWithdrawCollateral.`,
}, getOpts());
return;
}
const asset = resolveTokenAddress(ctx.registry, ctx.chainName, opts.asset);
const to = resolveWallet(opts.to);
const tx = await adapter.buildWithdrawCollateral({
protocol: ctx.protocol!.name,
asset,
amount: parseAmount(opts.amount),
to,
market_id: opts.market as MorphoMarketId,
});
const result = await executor.execute(tx);
printOutput(result, getOpts());
});
}
16 changes: 16 additions & 0 deletions ts/packages/defi-core/src/traits/lending.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type {
BorrowParams,
RepayParams,
WithdrawParams,
SupplyCollateralParams,
WithdrawCollateralParams,
LendingRates,
UserPosition,
DeFiTx,
Expand Down Expand Up @@ -35,4 +37,18 @@ export interface ILending {
* eMode concept leave this undefined.
*/
buildSetEMode?(categoryId: number): Promise<DeFiTx>;

/**
* Optional — supply the *collateral* side of a Morpho Blue market
* (separate selector from `supply`, which is the loan-asset LP path).
* Aave V3 collapses both into supply/withdraw, so its adapter leaves
* this undefined. Morpho Blue's adapter requires `params.market_id`.
*/
buildSupplyCollateral?(params: SupplyCollateralParams): Promise<DeFiTx>;

/**
* Optional — withdraw the collateral side of a Morpho Blue market.
* Aave V3 leaves this undefined; Morpho Blue requires market_id.
*/
buildWithdrawCollateral?(params: WithdrawCollateralParams): Promise<DeFiTx>;
}
35 changes: 35 additions & 0 deletions ts/packages/defi-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,21 @@ export interface RemoveLiquidityParams {

// === Lending Types ===

/**
* Optional 32-byte Morpho Blue marketId. When provided, Morpho-style
* adapters resolve the full MarketParams via `idToMarketParams(id)`
* instead of falling back to a stub. Aave V3 / Compound V2 / Compound V3
* adapters ignore this field — they identify positions by reserve
* address alone.
*/
export type MorphoMarketId = `0x${string}`;

export interface SupplyParams {
protocol: string;
asset: Address;
amount: bigint;
on_behalf_of: Address;
market_id?: MorphoMarketId;
}

export interface BorrowParams {
Expand All @@ -183,6 +193,7 @@ export interface BorrowParams {
amount: bigint;
interest_rate_mode: InterestRateMode;
on_behalf_of: Address;
market_id?: MorphoMarketId;
}

/** Interest rate mode (serde: snake_case) */
Expand All @@ -197,13 +208,37 @@ export interface RepayParams {
amount: bigint;
interest_rate_mode: InterestRateMode;
on_behalf_of: Address;
market_id?: MorphoMarketId;
}

export interface WithdrawParams {
protocol: string;
asset: Address;
amount: bigint;
to: Address;
market_id?: MorphoMarketId;
}

/**
* Morpho Blue distinguishes loan-side liquidity (supply / withdraw) from
* collateral-side liquidity (supplyCollateral / withdrawCollateral).
* Aave V3 collapses both into supply/withdraw; Morpho needs the dedicated
* params type because the underlying selector and accounting differ.
*/
export interface SupplyCollateralParams {
protocol: string;
asset: Address;
amount: bigint;
on_behalf_of: Address;
market_id: MorphoMarketId;
}

export interface WithdrawCollateralParams {
protocol: string;
asset: Address;
amount: bigint;
to: Address;
market_id: MorphoMarketId;
}

export interface LendingRates {
Expand Down
Loading