From 9064571eaac9a07618a228c78b02aacb033237e9 Mon Sep 17 00:00:00 2001 From: Juan Garcia Date: Thu, 16 Apr 2026 13:45:52 +0200 Subject: [PATCH] feat: add position market swap commands for supply, borrow and repay --- .changeset/empty-pans-fry.md | 5 + .../cli/src/commands/action/swap/borrow.ts | 110 +++++++++++++++++ .../cli/src/commands/action/swap/repay.ts | 110 +++++++++++++++++ .../cli/src/commands/action/swap/supply.ts | 116 ++++++++++++++++++ 4 files changed, 341 insertions(+) create mode 100644 .changeset/empty-pans-fry.md create mode 100644 packages/cli/src/commands/action/swap/borrow.ts create mode 100644 packages/cli/src/commands/action/swap/repay.ts create mode 100644 packages/cli/src/commands/action/swap/supply.ts diff --git a/.changeset/empty-pans-fry.md b/.changeset/empty-pans-fry.md new file mode 100644 index 00000000..a1f0bbff --- /dev/null +++ b/.changeset/empty-pans-fry.md @@ -0,0 +1,5 @@ +--- +"@aave/cli": patch +--- + +**feat:** add position market swap commands for supply, borrow and repay diff --git a/packages/cli/src/commands/action/swap/borrow.ts b/packages/cli/src/commands/action/swap/borrow.ts new file mode 100644 index 00000000..8c4d1f19 --- /dev/null +++ b/packages/cli/src/commands/action/swap/borrow.ts @@ -0,0 +1,110 @@ +import { + type BorrowSwapQuoteRequest, + bigDecimal, + invariant, + reserveId, + userBorrowItemId, +} from '@aave/client'; +import { borrowSwapQuote } from '@aave/client/actions'; +import { Flags } from '@oclif/core'; +import { createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +import * as common from '../../../common.js'; +import { + executePositionMarketSwap, + quoteRows, +} from '../../../helpers/swaps.js'; + +export default class ActionSwapBorrow extends common.V4Command { + static override description = + 'Swap borrowed position into another reserve using market mode'; + + static override flags = { + 'debt-position-id': Flags.string({ + required: true, + description: 'Debt position ID to swap', + }), + 'buy-reserve-id': Flags.string({ + required: true, + description: 'Reserve ID to receive debt on', + }), + amount: Flags.string({ + required: true, + description: 'Amount to swap', + }), + 'quote-only': Flags.boolean({ + required: false, + default: false, + description: 'Only fetch and display quote', + }), + address: common.address({ + required: false, + description: 'User address for quote generation', + }), + 'private-key': common.privateKey({ + required: false, + }), + }; + + protected override headers = [{ value: 'Field' }, { value: 'Value' }]; + + async run(): Promise { + const { flags } = await this.parse(ActionSwapBorrow); + + const amount = flags.amount.trim(); + invariant(amount.length > 0, 'Amount cannot be empty'); + + const user = common.userAddressFromFlagOrEnv(flags.address); + + const request: BorrowSwapQuoteRequest = { + market: { + debtPosition: userBorrowItemId(flags['debt-position-id']), + buyReserve: reserveId(flags['buy-reserve-id']), + amount: bigDecimal(amount), + user, + }, + }; + + const quote = await borrowSwapQuote(this.client, request); + if (quote.isErr()) { + this.error(quote.error); + } + + this.display(quoteRows(quote.value.quote)); + + if (flags['quote-only']) { + return this.toSuccessJson(quote.value); + } + + const privateKey = (flags['private-key'] ?? + process.env.PRIVATE_KEY) as `0x${string}`; + invariant( + privateKey, + 'Provide --private-key or PRIVATE_KEY environment variable', + ); + + const wallet = createWalletClient({ + account: privateKeyToAccount(privateKey), + transport: http(), + }); + + const result = await executePositionMarketSwap( + this.client, + wallet, + quote.value, + ); + if (result.isErr()) { + this.error(String(result.error)); + } + + const receipt = result.value; + + this.display([ + ['Swap ID', receipt.id], + ['Created At', receipt.createdAt.toISOString()], + ]); + + return this.toSuccessJson(receipt); + } +} diff --git a/packages/cli/src/commands/action/swap/repay.ts b/packages/cli/src/commands/action/swap/repay.ts new file mode 100644 index 00000000..9b309737 --- /dev/null +++ b/packages/cli/src/commands/action/swap/repay.ts @@ -0,0 +1,110 @@ +import { + bigDecimal, + invariant, + type RepayWithSupplyQuoteRequest, + reserveId, + userBorrowItemId, +} from '@aave/client'; +import { repayWithSupplyQuote } from '@aave/client/actions'; +import { Flags } from '@oclif/core'; +import { createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +import * as common from '../../../common.js'; +import { + executePositionMarketSwap, + quoteRows, +} from '../../../helpers/swaps.js'; + +export default class ActionSwapRepay extends common.V4Command { + static override description = + 'Repay debt using another supply position through market swap'; + + static override flags = { + 'debt-position-id': Flags.string({ + required: true, + description: 'Debt position ID to repay', + }), + 'repay-with-reserve-id': Flags.string({ + required: true, + description: 'Reserve ID used as repayment source', + }), + amount: Flags.string({ + required: true, + description: 'Amount to repay', + }), + 'quote-only': Flags.boolean({ + required: false, + default: false, + description: 'Only fetch and display quote', + }), + address: common.address({ + required: false, + description: 'User address for quote generation', + }), + 'private-key': common.privateKey({ + required: false, + }), + }; + + protected override headers = [{ value: 'Field' }, { value: 'Value' }]; + + async run(): Promise { + const { flags } = await this.parse(ActionSwapRepay); + + const amount = flags.amount.trim(); + invariant(amount.length > 0, 'Amount cannot be empty'); + + const user = common.userAddressFromFlagOrEnv(flags.address); + + const request: RepayWithSupplyQuoteRequest = { + market: { + debtPosition: userBorrowItemId(flags['debt-position-id']), + repayWithReserve: reserveId(flags['repay-with-reserve-id']), + amount: bigDecimal(amount), + user, + }, + }; + + const quote = await repayWithSupplyQuote(this.client, request); + if (quote.isErr()) { + this.error(quote.error); + } + + this.display(quoteRows(quote.value.quote)); + + if (flags['quote-only']) { + return this.toSuccessJson(quote.value); + } + + const privateKey = (flags['private-key'] ?? + process.env.PRIVATE_KEY) as `0x${string}`; + invariant( + privateKey, + 'Provide --private-key or PRIVATE_KEY environment variable', + ); + + const wallet = createWalletClient({ + account: privateKeyToAccount(privateKey), + transport: http(), + }); + + const result = await executePositionMarketSwap( + this.client, + wallet, + quote.value, + ); + if (result.isErr()) { + this.error(String(result.error)); + } + + const receipt = result.value; + + this.display([ + ['Swap ID', receipt.id], + ['Created At', receipt.createdAt.toISOString()], + ]); + + return this.toSuccessJson(receipt); + } +} diff --git a/packages/cli/src/commands/action/swap/supply.ts b/packages/cli/src/commands/action/swap/supply.ts new file mode 100644 index 00000000..108cd5f7 --- /dev/null +++ b/packages/cli/src/commands/action/swap/supply.ts @@ -0,0 +1,116 @@ +import { + bigDecimal, + invariant, + reserveId, + type SupplySwapQuoteRequest, + userSupplyItemId, +} from '@aave/client'; +import { supplySwapQuote } from '@aave/client/actions'; +import { Flags } from '@oclif/core'; +import { createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +import * as common from '../../../common.js'; +import { + executePositionMarketSwap, + quoteRows, +} from '../../../helpers/swaps.js'; + +export default class ActionSwapSupply extends common.V4Command { + static override description = + 'Swap supplied position into another reserve using market mode'; + + static override flags = { + 'sell-position-id': Flags.string({ + required: true, + description: 'Supply position ID to sell from', + }), + 'buy-reserve-id': Flags.string({ + required: true, + description: 'Reserve ID to buy into', + }), + amount: Flags.string({ + required: true, + description: 'Amount to swap', + }), + 'enable-collateral': Flags.boolean({ + required: false, + default: false, + description: 'Enable collateral on the resulting supply position', + }), + 'quote-only': Flags.boolean({ + required: false, + default: false, + description: 'Only fetch and display quote', + }), + address: common.address({ + required: false, + description: 'User address for quote generation', + }), + 'private-key': common.privateKey({ + required: false, + }), + }; + + protected override headers = [{ value: 'Field' }, { value: 'Value' }]; + + async run(): Promise { + const { flags } = await this.parse(ActionSwapSupply); + + const amount = flags.amount.trim(); + invariant(amount.length > 0, 'Amount cannot be empty'); + + const user = common.userAddressFromFlagOrEnv(flags.address); + + const request: SupplySwapQuoteRequest = { + market: { + sellPosition: userSupplyItemId(flags['sell-position-id']), + buyReserve: reserveId(flags['buy-reserve-id']), + amount: bigDecimal(amount), + user, + enableCollateral: flags['enable-collateral'], + }, + }; + + const quote = await supplySwapQuote(this.client, request); + if (quote.isErr()) { + this.error(quote.error); + } + + this.display(quoteRows(quote.value.quote)); + + if (flags['quote-only']) { + return this.toSuccessJson(quote.value); + } + + const privateKey = (flags['private-key'] ?? + process.env.PRIVATE_KEY) as `0x${string}`; + invariant( + privateKey, + 'Provide --private-key or PRIVATE_KEY environment variable', + ); + + const wallet = createWalletClient({ + account: privateKeyToAccount(privateKey), + transport: http(), + }); + + const result = await executePositionMarketSwap( + this.client, + wallet, + quote.value, + ); + if (result.isErr()) { + this.error(String(result.error)); + } + + const receipt = result.value; + + this.display([ + ['Swap ID', receipt.id], + ['Created At', receipt.createdAt.toISOString()], + ]); + + return this.toSuccessJson(receipt); + } +}