Skip to content
92 changes: 84 additions & 8 deletions recipes/borrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,35 @@ function formatHealthFactor(value: string | null): string {
return `${num.toFixed(2)}${indicator}`;
}

/**
* Sanitize action args before sending to the API:
* - Remove `network` (API infers it from marketId; sending it causes validation error).
* - When both `amount` and `amountRaw` are present, send only `amount` so the user
* can provide human-readable amount without being forced to also send amountRaw.
*/
function sanitizeActionArgs(args: ArgumentsDto): ArgumentsDto {
const { network: _n, amount, amountRaw, collateralAmount, collateralAmountRaw, ...rest } = args;
const out: ArgumentsDto = { ...rest };

if (hasValue(amount)) {
out.amount = amount;
} else if (hasValue(amountRaw)) {
out.amountRaw = amountRaw;
}

if (hasValue(collateralAmount)) {
out.collateralAmount = collateralAmount;
} else if (hasValue(collateralAmountRaw)) {
out.collateralAmountRaw = collateralAmountRaw;
}

return out;
}

function hasValue(v: any): boolean {
return v !== undefined && v !== null && v !== "";
}

async function promptFromSchema(
schema: ArgumentSchemaDto,
skipFields: string[] = [],
Expand All @@ -393,6 +422,13 @@ async function promptFromSchema(
for (const [name, prop] of Object.entries(properties)) {
if (skipFields.includes(name)) continue;

// "Provide either X or XRaw" — skip the raw variant when the human-readable one is set, and vice versa.
// Also check skipFields to handle values pre-filled upstream (which bypass result).
if (name === "amountRaw" && (hasValue(result.amount) || skipFields.includes("amount"))) continue;
if (name === "amount" && (hasValue(result.amountRaw) || skipFields.includes("amountRaw"))) continue;
if (name === "collateralAmountRaw" && (hasValue(result.collateralAmount) || skipFields.includes("collateralAmount"))) continue;
if (name === "collateralAmount" && (hasValue(result.collateralAmountRaw) || skipFields.includes("collateralAmountRaw"))) continue;

const isRequired = required.includes(name);
const type = Array.isArray(prop.type) ? prop.type[0] : prop.type || "string";

Expand Down Expand Up @@ -451,7 +487,7 @@ async function promptFromSchema(
type: "input",
name: "value",
message,
initial: (prop.placeholder || prop.default) as string,
initial: isRequired ? (prop.placeholder || prop.default) as string : undefined,
validate: (input: string) => {
if (!isRequired && input === "") return true;
if (isRequired && input === "") return `${prop.label || name} is required`;
Expand Down Expand Up @@ -855,19 +891,27 @@ async function viewPosition(

for (const supply of position.supplyBalances) {
for (const pa of supply.pendingActions) {
const enrichedAction = {
...pa,
args: { ...pa.args, tokenAddress: pa.args.tokenAddress || supply.tokenAddress },
};
allPendingActions.push({
display: `[Supply] ${supply.tokenSymbol} - ${pa.label}`,
pendingAction: pa,
pendingAction: enrichedAction,
source: `${supply.tokenSymbol} supply`,
});
}
}

for (const debt of position.debtBalances) {
for (const pa of debt.pendingActions) {
const enrichedAction = {
...pa,
args: { ...pa.args, tokenAddress: pa.args.tokenAddress || debt.tokenAddress },
};
allPendingActions.push({
display: `[Debt] ${debt.tokenSymbol} - ${pa.label}`,
pendingAction: pa,
pendingAction: enrichedAction,
source: `${debt.tokenSymbol} debt`,
});
}
Expand Down Expand Up @@ -1048,9 +1092,39 @@ async function executeActionFlow(
if (!selected) throw new Error("Invalid market selected");

const market = selected.market;
const args: ArgumentsDto = { marketId: market.id };
const args: ArgumentsDto = { marketId: market.id, network };

const skipFields: string[] = ["marketId", "network"];

// Infer tokenAddress from the selected market when the action unambiguously targets a specific token.
// - Supply on isolated markets (Morpho): collateral is fixed by the market.
// - Pool-based (Aave): all actions target the market's loanToken.
// - Borrow/Repay on any market type: always the loanToken.
// - Isolated withdraw / collateral toggles: leave user-driven (could target collateral).
const schemaHasTokenAddress = Boolean(actionDef.schema.properties?.tokenAddress);
if (schemaHasTokenAddress) {
if (
market.type === "isolated" &&
actionType === BorrowActionType.SUPPLY &&
market.collateralTokens.length > 0 &&
market.collateralTokens[0].token.address
) {
args.tokenAddress = market.collateralTokens[0].token.address;
skipFields.push("tokenAddress");
} else if (
(
market.type === "pool" ||
actionType === BorrowActionType.BORROW ||
actionType === BorrowActionType.REPAY
) &&
market.loanToken.address
) {
args.tokenAddress = market.loanToken.address;
skipFields.push("tokenAddress");
}
}

const collected = await promptFromSchema(actionDef.schema, ["marketId"]);
const collected = await promptFromSchema(actionDef.schema, skipFields);
Object.assign(args, collected);

console.log("\nAction Summary:");
Expand Down Expand Up @@ -1079,11 +1153,12 @@ async function executeActionFlow(
}

console.log("\nCreating action...\n");
const argsForApi = sanitizeActionArgs(args);
const actionResponse = await apiClient.createAction({
integrationId: integration.id,
action: actionType,
address,
args,
args: argsForApi,
});

if (actionResponse.metadata) {
Expand Down Expand Up @@ -1113,7 +1188,7 @@ async function executePendingAction(
): Promise<void> {
const actionDef = integration.actions.find((a) => a.id === pendingAction.type);
const schema = actionDef?.schema;
const preFilledArgs = { ...pendingAction.args };
const preFilledArgs: ArgumentsDto = { ...pendingAction.args, network };
const preFilledFields = Object.keys(preFilledArgs).filter(
(k) => preFilledArgs[k] !== undefined && preFilledArgs[k] !== null,
);
Comment on lines +1191 to 1194
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Treat empty-string prefilled values as missing.

Line 1180-Line 1182 currently marks "" as prefilled, which can skip required prompts and forward invalid blanks.

Suggested fix
-  const preFilledFields = Object.keys(preFilledArgs).filter(
-    (k) => preFilledArgs[k] !== undefined && preFilledArgs[k] !== null,
-  );
+  const preFilledFields = Object.keys(preFilledArgs).filter((k) => hasValue(preFilledArgs[k]));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@recipes/borrow.ts` around lines 1179 - 1182, The current preFilledFields
calculation treats empty strings as valid values, allowing blank inputs to
bypass prompts; update the filter for preFilledArgs used in preFilledFields
(derived from pendingAction.args and network) to also exclude values that are
empty strings (e.g., value === "" or value.trim() === "" for strings) so only
non-null, non-undefined, non-empty-string entries are considered prefilled.

Expand Down Expand Up @@ -1155,11 +1230,12 @@ async function executePendingAction(
}

console.log("\nCreating action...\n");
const argsForApi = sanitizeActionArgs(args);
const actionResponse = await apiClient.createAction({
integrationId: integration.id,
action: pendingAction.type,
address,
args,
args: argsForApi,
});

if (actionResponse.metadata) {
Expand Down