From 3ba1fba3dc404285261ead1395a9786c6cead582 Mon Sep 17 00:00:00 2001 From: 0xyoki Date: Mon, 2 Mar 2026 23:39:51 +0100 Subject: [PATCH 1/8] chore: drop network in args, no prompting for amount, infer the token from market --- recipes/borrow.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/recipes/borrow.ts b/recipes/borrow.ts index d109344..6b95336 100644 --- a/recipes/borrow.ts +++ b/recipes/borrow.ts @@ -382,6 +382,29 @@ 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, ...rest } = args; + const out: ArgumentsDto = { ...rest }; + if (amount !== undefined && amount !== "" && amountRaw !== undefined && amountRaw !== "") { + out.amount = amount; + // Omit amountRaw when amount is provided (API accepts either) + } else { + if (amount !== undefined && amount !== "") out.amount = amount; + if (amountRaw !== undefined && amountRaw !== "") out.amountRaw = amountRaw; + } + return out; +} + +function hasValue(v: any): boolean { + return v !== undefined && v !== null && v !== ""; +} + async function promptFromSchema( schema: ArgumentSchemaDto, skipFields: string[] = [], @@ -393,6 +416,10 @@ async function promptFromSchema( for (const [name, prop] of Object.entries(properties)) { if (skipFields.includes(name)) continue; + // "Provide either amount or amountRaw" — skip the other when one is already set + if (name === "amountRaw" && hasValue(result.amount)) continue; + if (name === "amount" && hasValue(result.amountRaw)) continue; + const isRequired = required.includes(name); const type = Array.isArray(prop.type) ? prop.type[0] : prop.type || "string"; @@ -1050,7 +1077,22 @@ async function executeActionFlow( const market = selected.market; const args: ArgumentsDto = { marketId: market.id }; - const collected = await promptFromSchema(actionDef.schema, ["marketId"]); + // For isolated-market protocols (e.g. Morpho), collateral token is determined by the market — + // infer tokenAddress and skip prompting. For pool-based (e.g. Aave), user must choose. + const skipFields: string[] = ["marketId"]; + if ( + market.type === "isolated" && + actionType === BorrowActionType.SUPPLY && + market.collateralTokens.length > 0 + ) { + const collateralToken = market.collateralTokens[0].token; + if (collateralToken.address) { + args.tokenAddress = collateralToken.address; + skipFields.push("tokenAddress"); + } + } + + const collected = await promptFromSchema(actionDef.schema, skipFields); Object.assign(args, collected); console.log("\nAction Summary:"); @@ -1079,11 +1121,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) { @@ -1155,11 +1198,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) { From 3c8eaeef676468576f41a1e30cc2be7b98e2841f Mon Sep 17 00:00:00 2001 From: 0xyoki Date: Tue, 3 Mar 2026 00:04:18 +0100 Subject: [PATCH 2/8] chore: add network to skipped fields and pre-set in args --- recipes/borrow.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/recipes/borrow.ts b/recipes/borrow.ts index 6b95336..7c96ff1 100644 --- a/recipes/borrow.ts +++ b/recipes/borrow.ts @@ -1075,11 +1075,13 @@ 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 }; + + // Network is already selected upfront — skip prompting for it again. + const skipFields: string[] = ["marketId", "network"]; // For isolated-market protocols (e.g. Morpho), collateral token is determined by the market — // infer tokenAddress and skip prompting. For pool-based (e.g. Aave), user must choose. - const skipFields: string[] = ["marketId"]; if ( market.type === "isolated" && actionType === BorrowActionType.SUPPLY && From 5e38fe17c5fd039ca0fcc97b8f8ac9e49357f0eb Mon Sep 17 00:00:00 2001 From: 0xyoki Date: Tue, 3 Mar 2026 00:22:54 +0100 Subject: [PATCH 3/8] chore: skip the second network prompt --- recipes/borrow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/borrow.ts b/recipes/borrow.ts index 7c96ff1..fc6eea5 100644 --- a/recipes/borrow.ts +++ b/recipes/borrow.ts @@ -1158,7 +1158,7 @@ async function executePendingAction( ): Promise { 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, ); From 015d5d0687e044d1dec258b2d561e95cce777a07 Mon Sep 17 00:00:00 2001 From: 0xyoki Date: Tue, 3 Mar 2026 00:33:51 +0100 Subject: [PATCH 4/8] chore: stop suggestions on optional fields --- recipes/borrow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/borrow.ts b/recipes/borrow.ts index fc6eea5..bef713d 100644 --- a/recipes/borrow.ts +++ b/recipes/borrow.ts @@ -478,7 +478,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`; From 4f886b33dcdb5ec5868fd08448bd3baa8db5462d Mon Sep 17 00:00:00 2001 From: 0xyoki Date: Tue, 3 Mar 2026 00:44:39 +0100 Subject: [PATCH 5/8] chore: prefill the address of the token selected to be rapid --- recipes/borrow.ts | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/recipes/borrow.ts b/recipes/borrow.ts index bef713d..f04ed9d 100644 --- a/recipes/borrow.ts +++ b/recipes/borrow.ts @@ -389,15 +389,21 @@ function formatHealthFactor(value: string | null): string { * can provide human-readable amount without being forced to also send amountRaw. */ function sanitizeActionArgs(args: ArgumentsDto): ArgumentsDto { - const { network: _n, amount, amountRaw, ...rest } = args; + const { network: _n, amount, amountRaw, collateralAmount, collateralAmountRaw, ...rest } = args; const out: ArgumentsDto = { ...rest }; - if (amount !== undefined && amount !== "" && amountRaw !== undefined && amountRaw !== "") { + + if (hasValue(amount)) { out.amount = amount; - // Omit amountRaw when amount is provided (API accepts either) - } else { - if (amount !== undefined && amount !== "") out.amount = amount; - if (amountRaw !== undefined && amountRaw !== "") out.amountRaw = amountRaw; + } else if (hasValue(amountRaw)) { + out.amountRaw = amountRaw; + } + + if (hasValue(collateralAmount)) { + out.collateralAmount = collateralAmount; + } else if (hasValue(collateralAmountRaw)) { + out.collateralAmountRaw = collateralAmountRaw; } + return out; } @@ -416,9 +422,11 @@ async function promptFromSchema( for (const [name, prop] of Object.entries(properties)) { if (skipFields.includes(name)) continue; - // "Provide either amount or amountRaw" — skip the other when one is already set + // "Provide either X or XRaw" — skip the raw variant when the human-readable one is set, and vice versa if (name === "amountRaw" && hasValue(result.amount)) continue; if (name === "amount" && hasValue(result.amountRaw)) continue; + if (name === "collateralAmountRaw" && hasValue(result.collateralAmount)) continue; + if (name === "collateralAmount" && hasValue(result.collateralAmountRaw)) continue; const isRequired = required.includes(name); const type = Array.isArray(prop.type) ? prop.type[0] : prop.type || "string"; @@ -882,9 +890,13 @@ 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`, }); } @@ -892,9 +904,13 @@ async function viewPosition( 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`, }); } From f7d833993ae45e63b7d7d69c18b99f45cfc0c496 Mon Sep 17 00:00:00 2001 From: 0xyoki Date: Tue, 3 Mar 2026 00:46:18 +0100 Subject: [PATCH 6/8] chore: skip token address prompt each time you already select the asset --- recipes/borrow.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/recipes/borrow.ts b/recipes/borrow.ts index f04ed9d..2bfde64 100644 --- a/recipes/borrow.ts +++ b/recipes/borrow.ts @@ -1093,21 +1093,23 @@ async function executeActionFlow( const market = selected.market; const args: ArgumentsDto = { marketId: market.id, network }; - // Network is already selected upfront — skip prompting for it again. const skipFields: string[] = ["marketId", "network"]; - // For isolated-market protocols (e.g. Morpho), collateral token is determined by the market — - // infer tokenAddress and skip prompting. For pool-based (e.g. Aave), user must choose. + // Infer tokenAddress from the selected market so the user isn't prompted for it. + // - Borrow/Repay/Withdraw: the relevant token is the market's loanToken. + // - Supply on isolated markets (Morpho): collateral is fixed by the market. + // - Supply on pool-based (Aave): loanToken is also the token being supplied. if ( market.type === "isolated" && actionType === BorrowActionType.SUPPLY && - market.collateralTokens.length > 0 + market.collateralTokens.length > 0 && + market.collateralTokens[0].token.address ) { - const collateralToken = market.collateralTokens[0].token; - if (collateralToken.address) { - args.tokenAddress = collateralToken.address; - skipFields.push("tokenAddress"); - } + args.tokenAddress = market.collateralTokens[0].token.address; + skipFields.push("tokenAddress"); + } else if (market.loanToken.address) { + args.tokenAddress = market.loanToken.address; + skipFields.push("tokenAddress"); } const collected = await promptFromSchema(actionDef.schema, skipFields); From 6c72f14474e8e2f85027fb1c98da11cad94d3d1c Mon Sep 17 00:00:00 2001 From: 0xyoki Date: Tue, 3 Mar 2026 10:12:16 +0100 Subject: [PATCH 7/8] chore: fix prefilled amount triggering amountRaw prompts --- recipes/borrow.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/recipes/borrow.ts b/recipes/borrow.ts index 2bfde64..0e69c7c 100644 --- a/recipes/borrow.ts +++ b/recipes/borrow.ts @@ -422,11 +422,12 @@ 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 - if (name === "amountRaw" && hasValue(result.amount)) continue; - if (name === "amount" && hasValue(result.amountRaw)) continue; - if (name === "collateralAmountRaw" && hasValue(result.collateralAmount)) continue; - if (name === "collateralAmount" && hasValue(result.collateralAmountRaw)) 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"; From bfa7b9e461c02419bc5056feb1cba86aa14c9f98 Mon Sep 17 00:00:00 2001 From: 0xyoki Date: Tue, 3 Mar 2026 10:15:58 +0100 Subject: [PATCH 8/8] chore: update token auto-inference --- recipes/borrow.ts | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/recipes/borrow.ts b/recipes/borrow.ts index 0e69c7c..aa58158 100644 --- a/recipes/borrow.ts +++ b/recipes/borrow.ts @@ -1096,21 +1096,32 @@ async function executeActionFlow( const skipFields: string[] = ["marketId", "network"]; - // Infer tokenAddress from the selected market so the user isn't prompted for it. - // - Borrow/Repay/Withdraw: the relevant token is the market's loanToken. + // 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. - // - Supply on pool-based (Aave): loanToken is also the token being supplied. - 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.loanToken.address) { - args.tokenAddress = market.loanToken.address; - skipFields.push("tokenAddress"); + // - 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, skipFields);