From 8ea3e0c5c91b18dd66997699ac66a1cafe976f10 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Thu, 16 Jul 2026 09:22:30 +0200 Subject: [PATCH 01/12] feat(morpho-sdk): add transaction plan Adapt the latest Midnight flows and fork tests to TransactionPlan. --- .changeset/transaction-plan.md | 5 + packages/morpho-sdk/examples/example.ts | 24 +- .../encodeBlueSignatureAuthorization.test.ts | 3 + .../encodeBlueSignatureAuthorization.ts | 2 +- .../encode/encodeErc20Permit.test.ts | 4 + .../requirements/encode/encodeErc20Permit.ts | 2 + .../encode/encodeErc20Permit2Approve.test.ts | 4 + .../encode/encodeErc20Permit2Approve.ts | 2 + .../src/client/morphoViemExtension.ts | 4 +- .../src/entities/blue/blue.refinance.test.ts | 47 +- .../morpho-sdk/src/entities/blue/blue.test.ts | 80 +-- packages/morpho-sdk/src/entities/blue/blue.ts | 293 ++++------- .../src/entities/midnight/midnight.test.ts | 289 +++++----- .../src/entities/midnight/midnight.ts | 350 +++++++------ .../morpho-sdk/src/entities/midnight/types.ts | 76 +-- .../src/entities/vaultV1/vaultV1.test.ts | 88 ++-- .../src/entities/vaultV1/vaultV1.ts | 94 ++-- .../src/entities/vaultV2/vaultV2.test.ts | 17 +- .../src/entities/vaultV2/vaultV2.ts | 90 ++-- .../src/helpers/computeReallocations.ts | 2 +- packages/morpho-sdk/src/index.ts | 1 + .../transactionPlan/TransactionPlan.test.ts | 326 ++++++++++++ .../src/transactionPlan/TransactionPlan.ts | 494 ++++++++++++++++++ .../morpho-sdk/src/transactionPlan/index.ts | 6 + .../morpho-sdk/src/transactionPlan/types.ts | 254 +++++++++ packages/morpho-sdk/src/types/action.ts | 48 +- packages/morpho-sdk/src/types/error.ts | 24 + .../test/actions/blue/authorization.test.ts | 41 +- .../test/actions/blue/borrow.test.ts | 17 +- .../test/actions/blue/reallocations.test.ts | 53 +- .../test/actions/blue/refinance.test.ts | 17 +- .../test/actions/blue/repay.test.ts | 29 +- .../blue/repayWithdrawCollateral.test.ts | 25 +- .../test/actions/blue/supply.test.ts | 21 +- .../actions/blue/supplyCollateral.test.ts | 25 +- .../blue/supplyCollateralBorrow.test.ts | 31 +- .../test/actions/blue/withdraw.test.ts | 25 +- .../actions/blue/withdrawCollateral.test.ts | 5 +- .../morpho-sdk/test/actions/metadata.test.ts | 11 +- .../actions/midnight/requirements.test.ts | 31 +- .../actions/requirements/approval.test.ts | 9 +- .../test/actions/requirements/permit.test.ts | 5 +- .../test/actions/requirements/permit2.test.ts | 23 +- .../test/actions/vaultV1/deposit.test.ts | 29 +- .../test/actions/vaultV1/migrateToV2.test.ts | 9 +- .../test/actions/vaultV1/redeem.test.ts | 11 +- .../test/actions/vaultV1/withdraw.test.ts | 7 +- .../test/actions/vaultV2/deposit.test.ts | 5 +- .../test/actions/vaultV2/forceRedeem.test.ts | 7 +- .../actions/vaultV2/forceWithdraw.test.ts | 7 +- .../test/actions/vaultV2/redeem.test.ts | 3 +- .../test/actions/vaultV2/withdraw.test.ts | 3 +- packages/morpho-sdk/test/actions/wrap.test.ts | 37 +- .../morpho-sdk/test/transactionPlanUtils.ts | 88 ++++ .../src/morpho-protocol-evm.test.ts | 105 ++-- .../src/morpho-protocol-evm.ts | 109 ++-- 56 files changed, 2349 insertions(+), 1068 deletions(-) create mode 100644 .changeset/transaction-plan.md create mode 100644 packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts create mode 100644 packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts create mode 100644 packages/morpho-sdk/src/transactionPlan/index.ts create mode 100644 packages/morpho-sdk/src/transactionPlan/types.ts create mode 100644 packages/morpho-sdk/test/transactionPlanUtils.ts diff --git a/.changeset/transaction-plan.md b/.changeset/transaction-plan.md new file mode 100644 index 000000000..4fa1f1c51 --- /dev/null +++ b/.changeset/transaction-plan.md @@ -0,0 +1,5 @@ +--- +"@morpho-org/morpho-sdk": minor +--- + +Add `TransactionPlan` to expose semantic transaction/signature steps, ordered executable txs, and intent-based helpers for app labels and flow branching. diff --git a/packages/morpho-sdk/examples/example.ts b/packages/morpho-sdk/examples/example.ts index 3715c3ddc..8cdf7162e 100644 --- a/packages/morpho-sdk/examples/example.ts +++ b/packages/morpho-sdk/examples/example.ts @@ -53,7 +53,10 @@ async function main() { userAddress: USER_ADDRESS, vaultData, }); - const depositTx = deposit.buildTx(); + const preparedDeposit = await deposit.prepare(); + const depositCall = preparedDeposit.callRequests.at(-1); + if (depositCall == null) throw new Error("Deposit call is unavailable"); + const depositTx = depositCall.tx; console.log("Deposit transaction:", { to: depositTx.to, @@ -61,12 +64,11 @@ async function main() { value: depositTx.value, }); - // Get requirements (e.g., ERC20 approval) - console.log("\nšŸ“‹ Checking requirements..."); - const requirements = await deposit.getRequirements(); - console.log(`Found ${requirements.length} requirement(s)`); - requirements.forEach((req, index) => { - console.log(` Requirement ${index + 1}:`, req); + // Get requests (e.g., ERC20 approval or permit signature) + console.log("\nšŸ“‹ Checking requests..."); + console.log(`Found ${preparedDeposit.requestCount} request(s)`); + preparedDeposit.steps.forEach((request, index) => { + console.log(` Request ${index + 1}:`, request); }); // Example 2: Create a withdraw transaction @@ -76,7 +78,9 @@ async function main() { amount: withdrawAmount, userAddress: USER_ADDRESS, }); - const withdrawTx = withdraw.buildTx(); + const preparedWithdraw = await withdraw.prepare(); + const withdrawTx = preparedWithdraw.callRequests.at(-1)?.tx; + if (withdrawTx == null) throw new Error("Withdraw call is unavailable"); console.log("Withdraw transaction:", { to: withdrawTx.to, @@ -91,7 +95,9 @@ async function main() { shares: redeemShares, userAddress: USER_ADDRESS, }); - const redeemTx = redeem.buildTx(); + const preparedRedeem = await redeem.prepare(); + const redeemTx = preparedRedeem.callRequests.at(-1)?.tx; + if (redeemTx == null) throw new Error("Redeem call is unavailable"); console.log("Redeem transaction:", { to: redeemTx.to, diff --git a/packages/morpho-sdk/src/actions/requirements/encode/encodeBlueSignatureAuthorization.test.ts b/packages/morpho-sdk/src/actions/requirements/encode/encodeBlueSignatureAuthorization.test.ts index e3780a32b..e7f2aeb12 100644 --- a/packages/morpho-sdk/src/actions/requirements/encode/encodeBlueSignatureAuthorization.test.ts +++ b/packages/morpho-sdk/src/actions/requirements/encode/encodeBlueSignatureAuthorization.test.ts @@ -50,6 +50,9 @@ describe("encodeBlueSignatureAuthorization", () => { }); expect(requirement.action.type).toBe("authorization"); + expect(requirement.action.args.authorized).toBe(generalAdapter1); + expect(requirement.action.args.isAuthorized).toBe(true); + expect(requirement.action.args.chainId).toBe(mainnet.id); const signed = await requirement.sign(client, account.address); diff --git a/packages/morpho-sdk/src/actions/requirements/encode/encodeBlueSignatureAuthorization.ts b/packages/morpho-sdk/src/actions/requirements/encode/encodeBlueSignatureAuthorization.ts index 5e3d954cc..5923fc66a 100644 --- a/packages/morpho-sdk/src/actions/requirements/encode/encodeBlueSignatureAuthorization.ts +++ b/packages/morpho-sdk/src/actions/requirements/encode/encodeBlueSignatureAuthorization.ts @@ -75,7 +75,7 @@ export const encodeBlueSignatureAuthorization = async ( const action: AuthorizationAction = { type: "authorization", - args: { authorized, isAuthorized, deadline }, + args: { authorized, isAuthorized, deadline, chainId }, }; return { diff --git a/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit.test.ts b/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit.test.ts index 89a89868e..fe657d818 100644 --- a/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit.test.ts +++ b/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit.test.ts @@ -188,11 +188,15 @@ describe("encodeErc20Permit", () => { }); expect(permit.action.type).toBe("permit"); + expect(permit.action.args).toHaveProperty("token"); expect(permit.action.args).toHaveProperty("spender"); expect(permit.action.args).toHaveProperty("amount"); expect(permit.action.args).toHaveProperty("deadline"); + expect(permit.action.args).toHaveProperty("chainId"); + expect(permit.action.args.token).toEqual(usdc); expect(permit.action.args.spender).toEqual(generalAdapter1); expect(permit.action.args.amount).toEqual(mockAmount); + expect(permit.action.args.chainId).toEqual(mainnet.id); }); }); }); diff --git a/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit.ts b/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit.ts index 0226130c5..eac084ebe 100644 --- a/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit.ts +++ b/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit.ts @@ -86,9 +86,11 @@ export const encodeErc20Permit = async ( const action: PermitAction = { type: "permit", args: { + token, spender, amount, deadline, + chainId, }, }; diff --git a/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit2Approve.test.ts b/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit2Approve.test.ts index af1c074bb..dd5bff150 100644 --- a/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit2Approve.test.ts +++ b/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit2Approve.test.ts @@ -169,13 +169,17 @@ describe("encodeErc20Permit2Approve", () => { } expect(permit.action.type).toBe("permit2"); + expect(permit.action.args).toHaveProperty("token"); expect(permit.action.args).toHaveProperty("spender"); expect(permit.action.args).toHaveProperty("amount"); expect(permit.action.args).toHaveProperty("deadline"); expect(permit.action.args).toHaveProperty("expiration"); + expect(permit.action.args).toHaveProperty("chainId"); + expect(permit.action.args.token).toEqual(usdc); expect(permit.action.args.spender).toEqual(generalAdapter1); expect(permit.action.args.amount).toEqual(mockAmount); expect(permit.action.args.expiration).toEqual(mockExpiration); + expect(permit.action.args.chainId).toEqual(mainnet.id); }); }); }); diff --git a/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit2Approve.ts b/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit2Approve.ts index fcecd7328..eeebed5e7 100644 --- a/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit2Approve.ts +++ b/packages/morpho-sdk/src/actions/requirements/encode/encodeErc20Permit2Approve.ts @@ -68,10 +68,12 @@ export const encodeErc20Permit2Approve = ( const action: Permit2Action = { type: "permit2", args: { + token, spender: generalAdapter1, amount, deadline, expiration, + chainId, }, }; diff --git a/packages/morpho-sdk/src/client/morphoViemExtension.ts b/packages/morpho-sdk/src/client/morphoViemExtension.ts index cde3be90a..9b0b1f209 100644 --- a/packages/morpho-sdk/src/client/morphoViemExtension.ts +++ b/packages/morpho-sdk/src/client/morphoViemExtension.ts @@ -99,12 +99,12 @@ function createMorphoNamespace( * const block = await client.getBlockNumber(); * const vault = client.morpho.vaultV1(vaultAddress, 1); * const vaultData = await vault.getData(); - * const { buildTx } = vault.deposit({ + * const plan = vault.deposit({ * amount: 1_000_000n, * userAddress: user, * vaultData, * }); - * const tx = buildTx(); + * const tx = (await plan.prepare()).build().callRequests.at(-1)?.tx; * ``` */ export function morphoViemExtension(_options?: { diff --git a/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts b/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts index 5fa58784c..62a1f845e 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts @@ -9,6 +9,7 @@ import { createMockClient } from "@morpho-org/test/mock"; import { type Address, parseUnits } from "viem"; import { mainnet } from "viem/chains"; import { describe, expect, test } from "vitest"; +import { buildPlanTx } from "../../../test/transactionPlanUtils.js"; import { morphoViemExtension } from "../../client/index.js"; import { computeMinBorrowSharePrice } from "../../helpers/index.js"; import { @@ -88,7 +89,7 @@ const makeMarket = () => { }; describe("MorphoBlue.refinance", () => { - test("error: ZeroCollateralAmountError when collateralAmount is zero", () => { + test("error: ZeroCollateralAmountError when collateralAmount is zero", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -111,7 +112,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(ZeroCollateralAmountError); }); - test("error: ChainIdMismatchError when client.chain.id !== entity.chainId", () => { + test("error: ChainIdMismatchError when client.chain.id !== entity.chainId", async () => { const { client } = createMockClient(mainnet); // Construct the entity bound to a different chain id than the client reports. const market = client @@ -137,7 +138,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(ChainIdMismatchError); }); - test("error: RefinanceSameMarketError when source and target are the same market", () => { + test("error: RefinanceSameMarketError when source and target are the same market", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -155,7 +156,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(RefinanceSameMarketError); }); - test("error: RefinanceTokenMismatchError when loanToken differs", () => { + test("error: RefinanceTokenMismatchError when loanToken differs", async () => { const mismatched = new MarketParams({ collateralToken: WETH, loanToken: USDT, @@ -184,7 +185,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(RefinanceTokenMismatchError); }); - test("error: RefinanceTokenMismatchError when collateralToken differs", () => { + test("error: RefinanceTokenMismatchError when collateralToken differs", async () => { const WBTC: Address = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"; const mismatched = new MarketParams({ collateralToken: WBTC, @@ -214,7 +215,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(RefinanceTokenMismatchError); }); - test("error: RefinanceExceedsCollateralError when collateralAmount > position.collateral", () => { + test("error: RefinanceExceedsCollateralError when collateralAmount > position.collateral", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -236,7 +237,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(RefinanceExceedsCollateralError); }); - test("error: RefinanceExceedsBorrowAssetsError when borrowAssets > position.borrowAssets", () => { + test("error: RefinanceExceedsBorrowAssetsError when borrowAssets > position.borrowAssets", async () => { const market = makeMarket(); // borrowShares ā‰ˆ 50 USDC at the 2:1 ratio; asking for 1000 USDC of asset repay exceeds it. const positionData = makePosition({ @@ -261,7 +262,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(RefinanceExceedsBorrowAssetsError); }); - test("error: BorrowAmountAndSharesExclusiveError when both borrowAssets and borrowShares are positive", () => { + test("error: BorrowAmountAndSharesExclusiveError when both borrowAssets and borrowShares are positive", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -286,7 +287,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(BorrowAmountAndSharesExclusiveError); }); - test("error: RefinanceExceedsBorrowSharesError when borrowShares > position.borrowShares", () => { + test("error: RefinanceExceedsBorrowSharesError when borrowShares > position.borrowShares", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -310,7 +311,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(RefinanceExceedsBorrowSharesError); }); - test("error: BorrowExceedsSafeLtvError when partial migration leaves residual unhealthy", () => { + test("error: BorrowExceedsSafeLtvError when partial migration leaves residual unhealthy", async () => { // 1 WETH collateral ($4000 power), ~1000 USDC debt — currently safe on source. // Migrating all collateral while leaving debt creates a zero-collateral residual the LLTV check rejects. const market = makeMarket(); @@ -335,7 +336,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(BorrowExceedsSafeLtvError); }); - test("default: shares-mode full close builds a valid bundle", () => { + test("default: shares-mode full close builds a valid bundle", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -356,7 +357,7 @@ describe("MorphoBlue.refinance", () => { borrowShares: parseUnits("100", 12), }); - const tx = refi.buildTx(); + const tx = await buildPlanTx(refi); expect(tx.action.type).toBe("blueRefinance"); expect(tx.action.args.sourceMarket).toBe(sourceParams.id); expect(tx.action.args.targetMarket).toBe(targetParams.id); @@ -366,7 +367,7 @@ describe("MorphoBlue.refinance", () => { expect(tx.action.args.user).toBe(USER); }); - test("default: assets-mode partial migration builds a valid bundle", () => { + test("default: assets-mode partial migration builds a valid bundle", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -387,12 +388,12 @@ describe("MorphoBlue.refinance", () => { borrowAssets: parseUnits("50", 6), }); - const tx = refi.buildTx(); + const tx = await buildPlanTx(refi); expect(tx.action.args.borrowAssets).toBe(parseUnits("50", 6)); expect(tx.action.args.borrowShares).toBe(0n); }); - test("error: NonPositiveAssetAmountError when borrowAssets is negative", () => { + test("error: NonPositiveAssetAmountError when borrowAssets is negative", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -416,7 +417,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(NonPositiveAssetAmountError); }); - test("error: NegativeBorrowSharesError when borrowShares is negative", () => { + test("error: NegativeBorrowSharesError when borrowShares is negative", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -440,7 +441,7 @@ describe("MorphoBlue.refinance", () => { ).toThrow(NegativeBorrowSharesError); }); - test("behavior: collat-only refinance skips target health validation (no oracle required)", () => { + test("behavior: collat-only refinance skips target health validation (no oracle required)", async () => { // Target has no price; a collat-only refinance skips the target health check and must succeed. const market = makeMarket(); const positionData = makePosition({ @@ -473,7 +474,7 @@ describe("MorphoBlue.refinance", () => { ).not.toThrow(); }); - test("regression: shares-mode low-slippage encodes minBorrowSharePrice from borrowAssetsAdjusted, not projectedBorrowAssets", () => { + test("regression: shares-mode low-slippage encodes minBorrowSharePrice from borrowAssetsAdjusted, not projectedBorrowAssets", async () => { // minBorrowSharePrice must be derived from the encoded borrowAssetsAdjusted, not the smaller // projectedBorrowAssets, or toBorrowShares("Up") rounding can revert a preflight-passing bundle. // Assert the encoded guard matches the borrowAssetsAdjusted-derived one. @@ -498,7 +499,7 @@ describe("MorphoBlue.refinance", () => { borrowShares: parseUnits("100", 12), slippageTolerance, }); - const tx = refi.buildTx(); + const tx = await buildPlanTx(refi); // Recompute the entity's intermediate values (accrual deltas cancel for this fixture). const projectedBorrowAssets = sourceMarket.toBorrowAssets( @@ -523,7 +524,7 @@ describe("MorphoBlue.refinance", () => { expect(tx.action.args.minBorrowSharePrice).toBe(guardFromAdjusted); }); - test("default: collat-only migration builds a valid bundle", () => { + test("default: collat-only migration builds a valid bundle", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -542,12 +543,12 @@ describe("MorphoBlue.refinance", () => { collateralAmount: parseUnits("1", 18), }); - const tx = refi.buildTx(); + const tx = await buildPlanTx(refi); expect(tx.action.args.borrowAssets).toBe(0n); expect(tx.action.args.borrowShares).toBe(0n); }); - test("behavior: targetReallocations are forwarded to the action layer", () => { + test("behavior: targetReallocations are forwarded to the action layer", async () => { const market = makeMarket(); const positionData = makePosition({ market: baseMarket(sourceParams), @@ -587,7 +588,7 @@ describe("MorphoBlue.refinance", () => { targetReallocations, }); - const tx = refi.buildTx(); + const tx = await buildPlanTx(refi); expect(tx.value).toBe(fee); expect(tx.action.args.reallocationFee).toBe(fee); }); diff --git a/packages/morpho-sdk/src/entities/blue/blue.test.ts b/packages/morpho-sdk/src/entities/blue/blue.test.ts index ff6e0c2b9..abc835a32 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.test.ts @@ -12,6 +12,10 @@ import { mainnet } from "viem/chains"; import { describe, expect } from "vitest"; import { CbbtcUsdcBlue, WstethWethBlue } from "../../../test/fixtures/blue.js"; import { test } from "../../../test/setup.js"; +import { + buildPlanTx, + getPlanRequests, +} from "../../../test/transactionPlanUtils.js"; import { morphoViemExtension } from "../../client/index.js"; import { isRequirementApproval, @@ -101,7 +105,7 @@ describe("MorphoBlue builder = signer freedom", () => { amount: parseUnits("1", 18), }); - const tx = supplyCollateral.buildTx(); + const tx = await buildPlanTx(supplyCollateral); expect(tx.action.args.onBehalf).toBe(OTHER_USER); }); @@ -120,7 +124,7 @@ describe("MorphoBlue builder = signer freedom", () => { amount: parseUnits("1", 18), }); - const tx = supplyCollateral.buildTx(); + const tx = await buildPlanTx(supplyCollateral); expect(tx.action.args.onBehalf).toBe(OTHER_USER); }); }); @@ -171,13 +175,13 @@ describe("MorphoBlue validation", () => { ) .morpho.blue(CbbtcUsdcBlue, mainnet.id); - const requirements = await market - .withdraw({ + const requirements = await getPlanRequests( + market.withdraw({ assets: 1n, userAddress: USER, positionData: makePosition({ supplyShares: 10n ** 18n }), - }) - .getRequirements(); + }), + ); expect(requirements).toHaveLength(1); }); @@ -199,13 +203,13 @@ describe("MorphoBlue validation", () => { ) .morpho.blue(CbbtcUsdcBlue, mainnet.id); - const requirements = await market - .withdraw({ + const requirements = await getPlanRequests( + market.withdraw({ assets: 1n, userAddress: USER, positionData: makePosition({ supplyShares: 10n ** 18n }), - }) - .getRequirements(); + }), + ); expect(requirements).toEqual([]); }); @@ -296,14 +300,14 @@ describe("MorphoBlue validation", () => { ) .morpho.blue(CbbtcUsdcBlue, mainnet.id); - const requirements = await market - .repayWithdrawCollateral({ + const requirements = await getPlanRequests( + market.repayWithdrawCollateral({ amount: 1n, withdrawAmount: 1n, userAddress: USER, positionData: makePosition(), - }) - .getRequirements(); + }), + ); expect(requirements).toHaveLength(2); }); @@ -334,14 +338,14 @@ describe("MorphoBlue validation", () => { const amount = parseUnits("0.3", 18); const nativeAmount = parseUnits("0.2", 18); - const tx = market - .repay({ + const tx = await buildPlanTx( + market.repay({ amount, nativeAmount, userAddress: USER, positionData: makeWethPosition(), - }) - .buildTx(); + }), + ); expect(tx.action.args.assets).toBe(amount + nativeAmount); expect(tx.action.args.nativeAmount).toBe(nativeAmount); @@ -356,13 +360,13 @@ describe("MorphoBlue validation", () => { .morpho.blue(WstethWethBlue, mainnet.id); const nativeAmount = parseUnits("0.5", 18); - const requirements = await market - .repay({ + const requirements = await getPlanRequests( + market.repay({ nativeAmount, userAddress: USER, positionData: makeWethPosition(), - }) - .getRequirements(); + }), + ); expect(requirements).toEqual([]); }); @@ -391,7 +395,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = repay.buildTx(); + const tx = await buildPlanTx(repay); expect(tx.action.args.shares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -399,7 +403,7 @@ describe("MorphoBlue validation", () => { expect(tx.action.args.transferAmount).toBe(nativeAmount); // Fully-native repay pulls no ERC-20 ⇒ no approval/permit requirement. - expect(await repay.getRequirements()).toEqual([]); + expect(await getPlanRequests(repay)).toEqual([]); }); test("repay native: shares mode pulls transferAmount net of native (happy path)", async ({ @@ -428,7 +432,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = repay.buildTx(); + const tx = await buildPlanTx(repay); expect(tx.action.args.shares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -438,7 +442,7 @@ describe("MorphoBlue validation", () => { expect(tx.action.args.transferAmount - nativeAmount).toBe(expectedErc20); // getRequirements approves exactly the carved ERC-20 remainder, not the debt. - const requirements = await repay.getRequirements(); + const requirements = await getPlanRequests(repay); const approval = requirements.find(isRequirementApproval); if (!approval) { throw new Error("Approval requirement not found"); @@ -473,15 +477,15 @@ describe("MorphoBlue validation", () => { const amount = parseUnits("0.3", 18); const nativeAmount = parseUnits("0.2", 18); - const tx = market - .repayWithdrawCollateral({ + const tx = await buildPlanTx( + market.repayWithdrawCollateral({ amount, nativeAmount, withdrawAmount: parseUnits("1", 18), userAddress: USER, positionData: makeWethPosition(), - }) - .buildTx(); + }), + ); expect(tx.action.args.repayAssets).toBe(amount + nativeAmount); expect(tx.action.args.nativeAmount).toBe(nativeAmount); @@ -496,14 +500,14 @@ describe("MorphoBlue validation", () => { .morpho.blue(WstethWethBlue, mainnet.id); const nativeAmount = parseUnits("0.5", 18); - const requirements = await market - .repayWithdrawCollateral({ + const requirements = await getPlanRequests( + market.repayWithdrawCollateral({ nativeAmount, withdrawAmount: parseUnits("1", 18), userAddress: USER, positionData: makeWethPosition(), - }) - .getRequirements(); + }), + ); // Fully-native repay pulls no ERC-20 → only the Morpho authorization remains. expect(requirements).toHaveLength(1); @@ -533,7 +537,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = action.buildTx(); + const tx = await buildPlanTx(action); expect(tx.action.args.repayShares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -541,7 +545,7 @@ describe("MorphoBlue validation", () => { expect(tx.action.args.transferAmount).toBe(nativeAmount); // No ERC-20 pulled ⇒ only the Morpho authorization requirement remains. - expect(await action.getRequirements()).toHaveLength(1); + expect(await getPlanRequests(action)).toHaveLength(1); }); test("repayWithdrawCollateral native: shares mode pulls transferAmount net of native (happy path)", async ({ @@ -569,7 +573,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = action.buildTx(); + const tx = await buildPlanTx(action); expect(tx.action.args.repayShares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -580,7 +584,7 @@ describe("MorphoBlue validation", () => { // getRequirements approves exactly the carved ERC-20 remainder (alongside the // Morpho authorization the withdraw leg needs). - const requirements = await action.getRequirements(); + const requirements = await getPlanRequests(action); const approval = requirements.find(isRequirementApproval); if (!approval) { throw new Error("Approval requirement not found"); diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 9178e0897..762b9231c 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -48,10 +48,15 @@ import { validateWithdrawAmount, validateWithdrawShares, } from "../../helpers/index.js"; +import { + TransactionPlan, + type TransactionPlanOperatorAuthorizationRequest, + type TransactionPlanSimplePermitOptions, + type TransactionPlanTokenRequest, +} from "../../transactionPlan/index.js"; import type { FetchParameters } from "../../types/data.js"; import { type AssetsOrSharesArgs, - type BlueAuthorizationAction, type BlueBorrowAction, type BlueRefinanceAction, type BlueRepayAction, @@ -63,7 +68,6 @@ import { type BlueWithdrawCollateralAction, BorrowAmountAndSharesExclusiveError, type DepositAmountArgs, - type ERC20ApprovalAction, MarketIdMismatchError, MissingAccrualPositionError, type MorphoClientType, @@ -77,7 +81,6 @@ import { NonPositiveRepayAmountError, NonPositiveWithdrawAmountError, NonPositiveWithdrawCollateralAmountError, - type PermitRequirementSignature, type ReallocationComputeOptions, RefinanceExceedsBorrowAssetsError, RefinanceExceedsBorrowSharesError, @@ -85,10 +88,7 @@ import { RefinanceSameMarketError, RefinanceTokenMismatchError, type RepayAmountArgs, - type Requirement, - type RequirementSignature, selectRequirementSignatures, - type Transaction, type VaultReallocation, WithdrawExceedsCollateralError, ZeroCollateralAmountError, @@ -96,6 +96,13 @@ import { } from "../../types/index.js"; import { ReallocationData } from "../reallocationData.js"; +/** + * Blue action flows are synchronous so the SDK does not own broad market or position data fetching. + * Consumers pass the state snapshots they already fetched, which lets each app batch, cache, and + * refresh data for its own UX. Each flow returns a TransactionPlan; call `prepare()` when the app is + * ready to compute the minimal signature requests and/or call requests needed to execute that + * intent. + */ export interface BlueActions { /** * Fetches the latest market data with accrued interest. @@ -121,43 +128,32 @@ export interface BlueActions { * Prepares a supply-collateral transaction. * * Routed through bundler via GeneralAdapter1. - * `getRequirements` returns ERC20 approval or permit for GeneralAdapter1. + * `prepare()` resolves ERC20 approval or permit for GeneralAdapter1. * When `nativeAmount` is provided, native token is wrapped; collateral must be wNative. * * @param params - Supply collateral parameters. - * @returns Object with `buildTx` and `getRequirements`. + * @returns TransactionPlan that can be prepared and built for execution. */ - supplyCollateral: (params: { userAddress: Address } & DepositAmountArgs) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: (params?: { - /** - * Prefer the ERC-2612 simple-permit path when the SDK detects support. - * Leave unset or set to `false` to force the Permit2/classic approval fallback when - * a token is known to be incompatible despite passing the SDK's shallow nonce probe. - */ - useSimplePermit?: boolean; - }) => Promise< - ( - | Readonly> - | Requirement - )[] - >; - }; + supplyCollateral: ( + params: { userAddress: Address } & DepositAmountArgs, + ) => TransactionPlan< + BlueSupplyCollateralAction, + TransactionPlanSimplePermitOptions, + TransactionPlanTokenRequest + >; /** * Prepares a loan-asset supply transaction. * * Routed through bundler via GeneralAdapter1. Computes `maxSharePrice` from market supply * state and `slippageTolerance` to protect against share-price inflation. - * `getRequirements` returns ERC20 approval or permit for `GeneralAdapter1` on the loan token. + * `prepare()` resolves ERC20 approval or permit for `GeneralAdapter1` on the loan token. * When `nativeAmount` is provided, native token is wrapped; the loan token must be wNative. * * No Morpho authorization required (supplier is crediting, not withdrawing). * * @param params - Supply parameters. - * @returns Object with `buildTx` and `getRequirements`. + * @returns TransactionPlan that can be prepared and built for execution. */ supply: ( params: { @@ -165,24 +161,11 @@ export interface BlueActions { marketData: Market; slippageTolerance?: bigint; } & DepositAmountArgs, - ) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: (params?: { - /** - * Prefer the ERC-2612 simple-permit path when the SDK detects support. - * Leave unset or set to `false` to force the Permit2/classic approval fallback when - * a token is known to be incompatible despite passing the SDK's shallow nonce probe. - */ - useSimplePermit?: boolean; - }) => Promise< - ( - | Readonly> - | Requirement - )[] - >; - }; + ) => TransactionPlan< + BlueSupplyAction, + TransactionPlanSimplePermitOptions, + TransactionPlanTokenRequest + >; /** * Prepares a loan-asset withdraw transaction. @@ -197,14 +180,14 @@ export interface BlueActions { * moving liquidity from other markets via the PublicAllocator before withdrawing — used to * unblock withdraws that exceed on-market liquidity. * - * `getRequirements` returns `morpho.setAuthorization(generalAdapter1, true)` if GA1 is not + * `prepare()` resolves `morpho.setAuthorization(generalAdapter1, true)` if GA1 is not * yet authorized on Morpho (returns `[]` when already authorized), since the bundler calls * `withdraw(...,onBehalf=user,...)`. * * **Stale `positionData` may cause unexpected supply share calculations.** * * @param params - Withdraw parameters including pre-fetched `positionData`. - * @returns Object with `buildTx` and `getRequirements`. + * @returns TransactionPlan that can be prepared and built for execution. */ withdraw: ( params: { @@ -214,14 +197,11 @@ export interface BlueActions { slippageTolerance?: bigint; reallocations?: readonly VaultReallocation[]; } & AssetsOrSharesArgs, - ) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: () => Promise< - (Readonly> | Requirement)[] - >; - }; + ) => TransactionPlan< + BlueWithdrawAction, + unknown, + TransactionPlanOperatorAuthorizationRequest + >; /** * Prepares a borrow transaction. @@ -233,13 +213,13 @@ export interface BlueActions { * When `reallocations` is provided, `reallocateTo` actions are prepended to the bundle, * moving liquidity from other markets via the PublicAllocator before borrowing. * - * `getRequirements` returns `morpho.setAuthorization(generalAdapter1, true)` if not yet authorized, + * `prepare()` resolves `morpho.setAuthorization(generalAdapter1, true)` if not yet authorized, * since borrowing through bundler3 requires GeneralAdapter1 authorization on Morpho. * * **Stale `positionData` may cause unexpected health.** * * @param params - Borrow parameters including pre-fetched `positionData` for health validation. - * @returns Object with `buildTx` and `getRequirements`. + * @returns TransactionPlan that can be prepared and built for execution. */ borrow: (params: { userAddress: Address; @@ -247,14 +227,11 @@ export interface BlueActions { positionData: AccrualPosition; slippageTolerance?: bigint; reallocations?: readonly VaultReallocation[]; - }) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: () => Promise< - (Readonly> | Requirement)[] - >; - }; + }) => TransactionPlan< + BlueBorrowAction, + unknown, + TransactionPlanOperatorAuthorizationRequest + >; /** * Prepares a repay transaction. @@ -266,13 +243,13 @@ export interface BlueActions { * * Computes `maxSharePrice` from market borrow state and `slippageTolerance`. * - * `getRequirements` returns ERC20 approval for loan token to GeneralAdapter1. + * `prepare()` resolves ERC20 approval for loan token to GeneralAdapter1. * Does NOT require Morpho authorization (anyone can repay on behalf of anyone). * * **Shares mode:** `slippageTolerance` also caps `transferAmount`. * * @param params - Repay parameters including pre-fetched `positionData`. - * @returns Object with `buildTx` and `getRequirements`. + * @returns TransactionPlan that can be prepared and built for execution. */ repay: ( params: { @@ -280,24 +257,11 @@ export interface BlueActions { positionData: AccrualPosition; slippageTolerance?: bigint; } & RepayAmountArgs, - ) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: (params?: { - /** - * Prefer the ERC-2612 simple-permit path when the SDK detects support. - * Leave unset or set to `false` to force the Permit2/classic approval fallback when - * a token is known to be incompatible despite passing the SDK's shallow nonce probe. - */ - useSimplePermit?: boolean; - }) => Promise< - ( - | Readonly> - | Requirement - )[] - >; - }; + ) => TransactionPlan< + BlueRepayAction, + TransactionPlanSimplePermitOptions, + TransactionPlanTokenRequest + >; /** * Prepares a withdraw-collateral transaction. @@ -306,21 +270,19 @@ export interface BlueActions { * The caller (`msg.sender`) must be `onBehalf`. * Validates position health after withdrawal using the LLTV buffer. * - * No `getRequirements` — no ERC20 approval or GeneralAdapter1 authorization needed + * No requirement requests — no ERC20 approval or GeneralAdapter1 authorization needed * (collateral flows out of Morpho, not in). * * **No on-chain slippage guard — stale `positionData` risks liquidation.** * * @param params - Withdraw collateral parameters including pre-fetched `positionData` for health validation. - * @returns Object with `buildTx`. + * @returns TransactionPlan that can be prepared and built for execution. */ withdrawCollateral: (params: { userAddress: Address; amount: bigint; positionData: AccrualPosition; - }) => { - buildTx: () => Readonly>; - }; + }) => TransactionPlan; /** * Prepares an atomic repay-and-withdraw-collateral transaction. @@ -329,14 +291,14 @@ export interface BlueActions { * Validates combined position health: simulates the repay, then checks * that the resulting position can sustain the collateral withdrawal. * - * `getRequirements` returns in parallel: + * `prepare()` resolves in parallel: * - ERC20 approval for loan token to GeneralAdapter1 (for the repay). * - `morpho.setAuthorization(generalAdapter1, true)` if not yet authorized (for the withdraw). * * **Stale `positionData` risks underestimated debt and unsafe withdrawal.** * * @param params - Combined parameters including pre-fetched `positionData`. - * @returns Object with `buildTx` and `getRequirements`. + * @returns TransactionPlan that can be prepared and built for execution. */ repayWithdrawCollateral: ( params: { @@ -345,25 +307,11 @@ export interface BlueActions { positionData: AccrualPosition; slippageTolerance?: bigint; } & RepayAmountArgs, - ) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: (params?: { - /** - * Prefer the ERC-2612 simple-permit path when the SDK detects support. - * Leave unset or set to `false` to force the Permit2/classic approval fallback when - * a token is known to be incompatible despite passing the SDK's shallow nonce probe. - */ - useSimplePermit?: boolean; - }) => Promise< - ( - | Readonly> - | Readonly> - | Requirement - )[] - >; - }; + ) => TransactionPlan< + BlueRepayWithdrawCollateralAction, + TransactionPlanSimplePermitOptions, + TransactionPlanTokenRequest | TransactionPlanOperatorAuthorizationRequest + >; /** * Prepares an atomic supply-collateral-and-borrow transaction. @@ -374,14 +322,14 @@ export interface BlueActions { * When `reallocations` is provided, `reallocateTo` actions are prepended before * `morphoBorrow` in the bundle. * - * `getRequirements` returns in parallel: + * `prepare()` resolves in parallel: * - ERC20 approval or permit for collateral token (to GeneralAdapter1). * - `morpho.setAuthorization(generalAdapter1, true)` if adapter is not yet authorized. * * **Stale `positionData` may cause unexpected health.** * * @param params - Combined parameters including pre-fetched `positionData` for health validation. - * @returns Object with `buildTx` and `getRequirements`. + * @returns TransactionPlan that can be prepared and built for execution. */ supplyCollateralBorrow: ( params: { @@ -391,25 +339,11 @@ export interface BlueActions { slippageTolerance?: bigint; reallocations?: readonly VaultReallocation[]; } & DepositAmountArgs, - ) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: (params?: { - /** - * Prefer the ERC-2612 simple-permit path when the SDK detects support. - * Leave unset or set to `false` to force the Permit2/classic approval fallback when - * a token is known to be incompatible despite passing the SDK's shallow nonce probe. - */ - useSimplePermit?: boolean; - }) => Promise< - ( - | Readonly> - | Readonly> - | Requirement - )[] - >; - }; + ) => TransactionPlan< + BlueSupplyCollateralBorrowAction, + TransactionPlanSimplePermitOptions, + TransactionPlanTokenRequest | TransactionPlanOperatorAuthorizationRequest + >; /** * Prepares an atomic refinance migrating this market's position to another Morpho Blue market @@ -420,7 +354,7 @@ export interface BlueActions { * markets are forward-accrued to `now`; in shares mode the target borrow is overshot by * `slippageTolerance` and the callback sweeps the residual. * - * `getRequirements` returns `morpho.setAuthorization(generalAdapter1, true)` when GA1 is not yet + * `prepare()` resolves `morpho.setAuthorization(generalAdapter1, true)` when GA1 is not yet * authorized (a single global authorization covers both markets). * * @param params.userAddress - Position owner on both markets. @@ -432,7 +366,7 @@ export interface BlueActions { * @param params.borrowShares - Borrow shares to repay on source; exclusive with `borrowAssets`. * @param params.slippageTolerance - WAD slippage tolerance. Defaults to `DEFAULT_SLIPPAGE_TOLERANCE`. * @param params.targetReallocations - PublicAllocator reallocations into the target market. - * @returns Object with `buildTx` and `getRequirements`. + * @returns TransactionPlan that can be prepared and built for execution. */ refinance: (params: { userAddress: Address; @@ -446,14 +380,11 @@ export interface BlueActions { borrowShares?: bigint; slippageTolerance?: bigint; targetReallocations?: readonly VaultReallocation[]; - }) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: () => Promise< - (Readonly> | Requirement)[] - >; - }; + }) => TransactionPlan< + BlueRefinanceAction, + unknown, + TransactionPlanOperatorAuthorizationRequest + >; /** * Fetches all on-chain data needed to construct a {@link ReallocationData} @@ -606,8 +537,8 @@ export class MorphoBlue implements BlueActions { market: marketData, slippageTolerance, }); - return { - getRequirements: (params?: { useSimplePermit?: boolean }) => + return new TransactionPlan({ + getRequirementRequests: (params?: TransactionPlanSimplePermitOptions) => getGeneralAdapterRequirements(this.client.viemClient, { address: this.marketParams.loanToken, chainId: this.chainId, @@ -617,7 +548,7 @@ export class MorphoBlue implements BlueActions { args: { amount, from: userAddress }, }), - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -634,7 +565,7 @@ export class MorphoBlue implements BlueActions { metadata: this.client.options.metadata, }); }, - }; + }); } withdraw( @@ -705,8 +636,8 @@ export class MorphoBlue implements BlueActions { slippageTolerance, }); - return { - getRequirements: async () => { + return new TransactionPlan({ + getRequirementRequests: async () => { const authTx = await getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -716,7 +647,7 @@ export class MorphoBlue implements BlueActions { return authTx ? [authTx] : []; }, - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { authorization } = selectRequirementSignatures(signatures, { authorization: true, }); @@ -734,7 +665,7 @@ export class MorphoBlue implements BlueActions { metadata: this.client.options.metadata, }); }, - }; + }); } supplyCollateral({ @@ -760,8 +691,8 @@ export class MorphoBlue implements BlueActions { if (nativeAmount !== undefined && nativeAmount > 0n) { validateNativeAsset(this.chainId, this.marketParams.collateralToken); } - return { - getRequirements: (params?: { useSimplePermit?: boolean }) => + return new TransactionPlan({ + getRequirementRequests: (params?: TransactionPlanSimplePermitOptions) => getGeneralAdapterRequirements(this.client.viemClient, { address: this.marketParams.collateralToken, chainId: this.chainId, @@ -771,7 +702,7 @@ export class MorphoBlue implements BlueActions { args: { amount, from: userAddress }, }), - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -790,7 +721,7 @@ export class MorphoBlue implements BlueActions { metadata: this.client.options.metadata, }); }, - }; + }); } borrow({ @@ -837,8 +768,8 @@ export class MorphoBlue implements BlueActions { slippageTolerance, }); - return { - getRequirements: async () => { + return new TransactionPlan({ + getRequirementRequests: async () => { const authTx = await getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -848,7 +779,7 @@ export class MorphoBlue implements BlueActions { return authTx ? [authTx] : []; }, - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { authorization } = selectRequirementSignatures(signatures, { authorization: true, }); @@ -868,7 +799,7 @@ export class MorphoBlue implements BlueActions { metadata: this.client.options.metadata, }); }, - }; + }); } repay( @@ -970,8 +901,10 @@ export class MorphoBlue implements BlueActions { market: marketForRepay, slippageTolerance, }); - return { - getRequirements: (reqParams?: { useSimplePermit?: boolean }) => { + return new TransactionPlan({ + getRequirementRequests: ( + reqParams?: TransactionPlanSimplePermitOptions, + ) => { // Fully native repay pulls no ERC-20, so it needs no approval/permit. if (erc20Amount === 0n) return Promise.resolve([]); return getGeneralAdapterRequirements(this.client.viemClient, { @@ -984,7 +917,7 @@ export class MorphoBlue implements BlueActions { }); }, - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -1019,7 +952,7 @@ export class MorphoBlue implements BlueActions { metadata: this.client.options.metadata, }); }, - }; + }); } withdrawCollateral({ @@ -1062,8 +995,8 @@ export class MorphoBlue implements BlueActions { marketId: this.marketParams.id, }); - return { - buildTx: () => + return new TransactionPlan({ + buildPrimaryCall: () => blueWithdrawCollateral({ market: { chainId: this.chainId, @@ -1076,7 +1009,7 @@ export class MorphoBlue implements BlueActions { }, metadata: this.client.options.metadata, }), - }; + }); } repayWithdrawCollateral( @@ -1205,8 +1138,10 @@ export class MorphoBlue implements BlueActions { market: marketForRepay, slippageTolerance, }); - return { - getRequirements: async (reqParams?: { useSimplePermit?: boolean }) => { + return new TransactionPlan({ + getRequirementRequests: async ( + reqParams?: TransactionPlanSimplePermitOptions, + ) => { const [erc20Requirements, authTx] = await Promise.all([ // Fully native repay pulls no ERC-20, so it needs no approval/permit. erc20Amount === 0n @@ -1230,7 +1165,7 @@ export class MorphoBlue implements BlueActions { return [...erc20Requirements, ...(authTx ? [authTx] : [])]; }, - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { permit, authorization } = selectRequirementSignatures( signatures, { permit: true, authorization: true }, @@ -1270,7 +1205,7 @@ export class MorphoBlue implements BlueActions { metadata: this.client.options.metadata, }); }, - }; + }); } supplyCollateralBorrow({ @@ -1336,8 +1271,10 @@ export class MorphoBlue implements BlueActions { market: positionData.market, slippageTolerance, }); - return { - getRequirements: async (params?: { useSimplePermit?: boolean }) => { + return new TransactionPlan({ + getRequirementRequests: async ( + params?: TransactionPlanSimplePermitOptions, + ) => { const [erc20Requirements, authTx] = await Promise.all([ getGeneralAdapterRequirements(this.client.viemClient, { address: this.marketParams.collateralToken, @@ -1358,7 +1295,7 @@ export class MorphoBlue implements BlueActions { return [...erc20Requirements, ...(authTx ? [authTx] : [])]; }, - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { permit, authorization } = selectRequirementSignatures( signatures, { permit: true, authorization: true }, @@ -1383,7 +1320,7 @@ export class MorphoBlue implements BlueActions { metadata: this.client.options.metadata, }); }, - }; + }); } refinance({ @@ -1582,8 +1519,8 @@ export class MorphoBlue implements BlueActions { }) : 0n; - return { - getRequirements: async () => { + return new TransactionPlan({ + getRequirementRequests: async () => { const authTx = await getBlueAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -1593,7 +1530,7 @@ export class MorphoBlue implements BlueActions { return authTx ? [authTx] : []; }, - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { authorization } = selectRequirementSignatures(signatures, { authorization: true, }); @@ -1617,7 +1554,7 @@ export class MorphoBlue implements BlueActions { metadata: this.client.options.metadata, }); }, - }; + }); } /** diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts index 5748299be..7590cf7e2 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts @@ -36,6 +36,11 @@ import { midnightMarketId, midnightOtherMarket, } from "../../../test/fixtures/midnight.js"; +import { + buildPlanTx, + getPlanRequests, +} from "../../../test/transactionPlanUtils.js"; +import { TransactionPlan } from "../../transactionPlan/index.js"; import type { MempoolSubmitOffersAction, MidnightOfferRootSignature, @@ -78,34 +83,40 @@ import type { OffersData, } from "./types.js"; -type BuildSubmitOffersTx = (params: { +type BuildMakeOffersOutputTx = (params: { readonly offersData: OffersData; readonly signatures?: MidnightActionSignatures; readonly signedPayloads?: ReadonlyMap; readonly metadata?: { readonly origin: string }; -}) => Readonly>; - -const buildSubmitOffersTx: BuildSubmitOffersTx = (params) => - ( - Object.assign(Object.create(MorphoMidnight.prototype), { - chainId: midnightChainId, - client: { - options: { - metadata: params.metadata, +}) => Promise>>; + +type CreateMakeOffersOutput = (params: { + readonly offersData: OffersData; + readonly getRequirementRequests: () => Promise; + readonly signedPayloads: ReadonlyMap; +}) => MakeOffersOutput; + +const buildMakeOffersOutputTx: BuildMakeOffersOutputTx = async (params) => + buildPlanTx( + ( + Object.assign(Object.create(MorphoMidnight.prototype), { + chainId: midnightChainId, + client: { + options: { + metadata: params.metadata, + }, }, - }, - }) as { - buildSubmitOffersTx: (params: { - readonly offersData: OffersData; - readonly signatures?: MidnightActionSignatures; - readonly signedPayloads: ReadonlyMap; - }) => Readonly>; - } - ).buildSubmitOffersTx({ - ...params, - signedPayloads: - params.signedPayloads ?? new Map([["0x1234", "0x1234" as Hex]]), - }); + }) as { + createMakeOffersOutput: CreateMakeOffersOutput; + } + ).createMakeOffersOutput({ + offersData: params.offersData, + getRequirementRequests: async () => [], + signedPayloads: + params.signedPayloads ?? new Map([["0x1234", "0x1234" as Hex]]), + }), + params.signatures, + ); const offersData = ( buy = true, @@ -260,15 +271,15 @@ const offerSignerWallet = createWalletClient({ }); const signOfferRootRequirement = async ( - requirements: Awaited>, + output: MakeOffersOutput, ): Promise => { - const requirement = requirements.find( + const request = (await output.prepare()).signatureRequests.find( ({ action }) => action.type === "midnightOfferRootSignature", ); - if (requirement == null || !("sign" in requirement)) { + if (request == null) { throw new Error("Expected midnightOfferRootSignature requirement"); } - const signature = await requirement.sign( + const signature = await request.sign( offerSignerWallet, offerSignerAccount.address, ); @@ -483,7 +494,7 @@ describe("MorphoMidnight", () => { }); describe("takeLend", () => { - test("default", () => { + test("default", async () => { const output = midnight().takeLend({ marketData: marketData(), accountAddress: midnightAddresses.taker, @@ -492,7 +503,8 @@ describe("MorphoMidnight", () => { takeableOffers: [midnightApiTake()], deadline: maxUint256, }); - const tx = output.buildTx(); + expect(output).toBeInstanceOf(TransactionPlan); + const tx = await buildPlanTx(output); expect(tx.action.args).toEqual({ market: midnightMarketId, @@ -504,6 +516,19 @@ describe("MorphoMidnight", () => { }); }); + test("error: MidnightOfferSideMismatchError", () => { + expect(() => + midnight().takeLend({ + marketData: marketData(), + accountAddress: midnightAddresses.taker, + assets: 1_000n, + minUnits: 900n, + takeableOffers: [midnightApiTake({ buy: true })], + deadline: maxUint256, + }), + ).toThrow(MidnightOfferSideMismatchError); + }); + test("behavior: requirements include loan approval and bundle authorization", async () => { const handle = createMockClient(midnightTestChain); mockAllowance({ @@ -521,7 +546,7 @@ describe("MorphoMidnight", () => { takeableOffers: [midnightApiTake()], deadline: maxUint256, }); - const requirements = await output.getRequirements(); + const requirements = await getPlanRequests(output); expect( requirements.map((requirement) => requirement.action.type), @@ -546,10 +571,10 @@ describe("MorphoMidnight", () => { deadline: maxUint256, }); - await expect(output.getRequirements()).resolves.toEqual([]); + await expect(getPlanRequests(output)).resolves.toEqual([]); }); - test("error: amount validation", () => { + test("error: amount validation", async () => { expect(() => midnight().takeLend({ marketData: marketData(), @@ -618,8 +643,9 @@ describe("MorphoMidnight", () => { takeableOffers: [midnightApiTake({ buy: true })], deadline: maxUint256, }); - const requirements = await output.getRequirements(); - const tx = output.buildTx(); + const requirements = await getPlanRequests(output); + expect(output).toBeInstanceOf(TransactionPlan); + const tx = await buildPlanTx(output); expect(tx.action.args.loanAssets).toBe(1_000n); expect( @@ -640,10 +666,23 @@ describe("MorphoMidnight", () => { deadline: maxUint256, }); - await expect(output.getRequirements()).resolves.toEqual([]); + await expect(getPlanRequests(output)).resolves.toEqual([]); }); - test("error: amount validation", () => { + test("error: MidnightOfferSideMismatchError", () => { + expect(() => + midnight().takeBorrow({ + marketData: marketData(), + accountAddress: midnightAddresses.taker, + loanAssets: 1_000n, + maxUnits: 900n, + takeableOffers: [midnightApiTake({ buy: false })], + deadline: maxUint256, + }), + ).toThrow(MidnightOfferSideMismatchError); + }); + + test("error: amount validation", async () => { expect(() => midnight().takeBorrow({ marketData: marketData(), @@ -706,8 +745,9 @@ describe("MorphoMidnight", () => { takeableOffers: [midnightApiTake({ buy: true })], deadline: maxUint256, }); - const requirements = await output.getRequirements(); - const tx = output.buildTx(); + const requirements = await getPlanRequests(output); + expect(output).toBeInstanceOf(TransactionPlan); + const tx = await buildPlanTx(output); expect(tx.action.args).toMatchObject({ collateralAssets: 2_000n, @@ -737,10 +777,10 @@ describe("MorphoMidnight", () => { deadline: maxUint256, }); - await expect(output.getRequirements()).resolves.toEqual([]); + await expect(getPlanRequests(output)).resolves.toEqual([]); }); - test("error: amount validation", () => { + test("error: amount validation", async () => { const params = { marketData: marketData(), accountAddress: midnightAddresses.taker, @@ -787,8 +827,9 @@ describe("MorphoMidnight", () => { collateralAssets: 2_000n, reservedCollateralAssets: 500n, }); - const requirements = await output.getRequirements(); - const tx = output.buildTx(); + const requirements = await getPlanRequests(output); + expect(output).toBeInstanceOf(TransactionPlan); + const tx = await buildPlanTx(output); expect(tx.action.args.assets).toBe(2_000n); expect(requirements[0]?.action).toMatchObject({ @@ -813,7 +854,7 @@ describe("MorphoMidnight", () => { accountAddress: midnightAddresses.taker, collateralAssets: 2_000n, }); - const requirements = await output.getRequirements(); + const requirements = await getPlanRequests(output); const approval = requirements[0]; expect(approval?.action.type).toBe("erc20Approval"); @@ -821,7 +862,7 @@ describe("MorphoMidnight", () => { expect(approval.action.args.amount).toBe(2_000n); }); - test("error: amount validation", () => { + test("error: amount validation", async () => { expect(() => midnight().supplyCollateral({ marketData: marketData(), @@ -841,13 +882,13 @@ describe("MorphoMidnight", () => { }); describe("redeem", () => { - test("default", () => { + test("default", async () => { const market = marketData(); const output = midnight().redeem({ positionData: positionData(market, { credit: 250n, pendingFee: 50n }), accountAddress: midnightAddresses.taker, }); - const tx = output.buildTx(); + const tx = await buildPlanTx(output); expect(tx.action.args).toEqual({ market: midnightMarketId, @@ -857,14 +898,14 @@ describe("MorphoMidnight", () => { }); }); - test("behavior: explicit units may exceed face value up to credit", () => { + test("behavior: explicit units may exceed face value up to credit", async () => { const market = marketData(); const output = midnight().redeem({ positionData: positionData(market, { credit: 250n, pendingFee: 50n }), accountAddress: midnightAddresses.taker, units: 225n, }); - const tx = output.buildTx(); + const tx = await buildPlanTx(output); expect(tx.action.args).toEqual({ market: midnightMarketId, @@ -927,8 +968,8 @@ describe("MorphoMidnight", () => { accountAddress: midnightAddresses.taker, receiver: midnightAddresses.maker, }); - const requirements = await output.getRequirements(); - const tx = output.buildTx(); + const requirements = await getPlanRequests(output); + const tx = await buildPlanTx(output); expect(requirements).toEqual([]); expect(tx.action.args.receiver).toBe(midnightAddresses.maker); @@ -961,7 +1002,7 @@ describe("MorphoMidnight", () => { ).toThrow(ChainIdMismatchError); }); - test("error: NoMidnightCreditToRedeemError", () => { + test("error: NoMidnightCreditToRedeemError", async () => { const market = marketData(); expect(() => @@ -972,7 +1013,7 @@ describe("MorphoMidnight", () => { ).toThrow(NoMidnightCreditToRedeemError); }); - test("error: InsufficientMidnightWithdrawableLiquidityError", () => { + test("error: InsufficientMidnightWithdrawableLiquidityError", async () => { const market = marketData({ withdrawable: 50n }); expect(() => @@ -1310,11 +1351,12 @@ describe("MorphoMidnight", () => { loanToken: midnightAddresses.loanToken, loanAssets: 1_000n, }); - const requirements = await output.getRequirements(); - const signature = await signOfferRootRequirement(requirements); - const tx = output.buildTx(signature); + expect(output).toBeInstanceOf(TransactionPlan); + const signature = await signOfferRootRequirement(output); + const tx = await buildPlanTx(output, signature); expect(output.groups).toEqual(data.groups); + expect(output).toBeInstanceOf(TransactionPlan); expect(output.root).toBe(data.tree.root); expect(output.ratifierType).toBe("ecrecover"); expect(tx.action.args).toMatchObject({ @@ -1400,7 +1442,11 @@ describe("MorphoMidnight", () => { loanToken: midnightAddresses.loanToken, loanAssets: 1_000n, }); - const requirements = await output.getRequirements(); + const prepared = await output.prepare(); + expect(output).toBeInstanceOf(TransactionPlan); + expect(prepared.callRequests).toEqual([]); + expect(prepared.hasIntent("midnightOfferRootSignature")).toBe(true); + const requirements = await getPlanRequests(output); const requirement = requirements.find( ({ action }) => action.type === "midnightOfferRootSignature", ); @@ -1467,7 +1513,7 @@ describe("MorphoMidnight", () => { loanAssets: 1_000n, reservedLoanAssets: 250n, }); - const requirements = await output.getRequirements(); + const requirements = await getPlanRequests(output); expect( requirements.find( @@ -1601,7 +1647,7 @@ describe("MorphoMidnight", () => { collateralAssets: 1_000n, reservedCollateralAssets: 250n, }); - const requirements = await output.getRequirements(); + const requirements = await getPlanRequests(output); expect( requirements.find( @@ -1623,8 +1669,8 @@ describe("MorphoMidnight", () => { assets: 1_000n, }, }); - const signature = await signOfferRootRequirement(requirements); - const tx = output.buildTx(signature); + const signature = await signOfferRootRequirement(output); + const tx = await buildPlanTx(output, signature); expect(tx.action.args.maker).toBe(data.accountAddress); }); @@ -1650,7 +1696,7 @@ describe("MorphoMidnight", () => { }), collateralAssets: 1_000n, }); - const requirements = await output.getRequirements(); + const requirements = await getPlanRequests(output); const approval = requirements.find( (requirement) => requirement.action.type === "erc20Approval", ); @@ -1719,9 +1765,8 @@ describe("MorphoMidnight", () => { offers: data.tree, validation: offerValidation, }); - const requirements = await output.getRequirements(); - const signature = await signOfferRootRequirement(requirements); - const tx = output.buildTx(signature); + const signature = await signOfferRootRequirement(output); + const tx = await buildPlanTx(output, signature); expect(output.groups).toEqual(data.groups); expect(tx.action.args.maker).toBe(offerSignerAccount.address); @@ -1737,7 +1782,7 @@ describe("MorphoMidnight", () => { offers: data.tree, validation: offerValidation, }); - const requirements = await output.getRequirements(); + const requirements = await getPlanRequests(output); expect( requirements.map((requirement) => requirement.action.type), @@ -1754,8 +1799,8 @@ describe("MorphoMidnight", () => { offers: data.tree, validation: offerValidation, }); - const requirements = await output.getRequirements(); - const tx = output.buildTx(); + const requirements = await getPlanRequests(output); + const tx = await buildPlanTx(output); expect( requirements.map((requirement) => requirement.action.type), @@ -1774,7 +1819,7 @@ describe("MorphoMidnight", () => { validation: offerValidation, }); - await expect(output.getRequirements()).resolves.toEqual([]); + await expect(getPlanRequests(output)).resolves.toEqual([]); }); test("error: MidnightOfferSideMismatchError", async () => { @@ -1817,8 +1862,8 @@ describe("MorphoMidnight", () => { withdrawCollateralAssets: 2_000n, deadline: maxUint256, }); - const requirements = await output.getRequirements(); - const tx = output.buildTx(); + const requirements = await getPlanRequests(output); + const tx = await buildPlanTx(output); expect(tx.action.args).toMatchObject({ repayAssets: 1_000n, @@ -1840,12 +1885,12 @@ describe("MorphoMidnight", () => { withdrawCollateralAssets: 2_000n, deadline: maxUint256, }); - const requirements = await output.getRequirements(); + const requirements = await getPlanRequests(output); expect(requirements).toEqual([]); }); - test("error: amount validation", () => { + test("error: amount validation", async () => { const params = { marketData: marketData(), accountAddress: midnightAddresses.taker, @@ -1897,18 +1942,18 @@ describe("MorphoMidnight", () => { group: data.groups[0]!, accountAddress: midnightAddresses.maker, }); - const requirements = await output.getRequirements(); - const tx = output.buildTx(); + const requirements = await getPlanRequests(output); + const tx = await buildPlanTx(output); expect(requirements).toEqual([]); expect(tx.action.args.group).toBe(data.groups[0]); }); }); - describe("buildSubmitOffersTx", () => { - test("default", () => { + describe("make-offer TransactionPlan build", () => { + test("default", async () => { const data = offersData(); - const tx = buildSubmitOffersTx({ + const tx = await buildMakeOffersOutputTx({ offersData: data, signatures: offerRootSignature(data), }); @@ -1923,9 +1968,9 @@ describe("MorphoMidnight", () => { }); }); - test("behavior: appends metadata", () => { + test("behavior: appends metadata", async () => { const data = offersData(); - const tx = buildSubmitOffersTx({ + const tx = await buildMakeOffersOutputTx({ offersData: data, signatures: offerRootSignature(data), metadata: { origin: "a1b2c3d4" }, @@ -1935,10 +1980,10 @@ describe("MorphoMidnight", () => { expect(tx.data.includes("a1b2c3d4")).toBe(true); }); - test("behavior: ignores untrusted payload bytes in the signature wrapper", () => { + test("behavior: ignores untrusted payload bytes in the signature wrapper", async () => { const data = offersData(); const signature = offerRootSignature(data); - const tx = buildSubmitOffersTx({ + const tx = await buildMakeOffersOutputTx({ offersData: data, signatures: { ...signature, @@ -1949,9 +1994,9 @@ describe("MorphoMidnight", () => { expect(tx.data).toBe("0x1234"); }); - test("behavior: setter ratifier uses prepared payload without signatures", () => { + test("behavior: setter ratifier uses prepared payload without signatures", async () => { const data = setterOffersData(); - const tx = buildSubmitOffersTx({ + const tx = await buildMakeOffersOutputTx({ offersData: data, }); @@ -1959,24 +2004,24 @@ describe("MorphoMidnight", () => { expect(tx.action.args.ratifierType).toBe("setter"); }); - test("error: MissingMidnightOfferRootSignatureError", () => { + test("error: MissingMidnightOfferRootSignatureError", async () => { const data = offersData(); - expect(() => - buildSubmitOffersTx({ + await expect( + buildMakeOffersOutputTx({ offersData: data, }), - ).toThrow(MissingMidnightOfferRootSignatureError); + ).rejects.toThrow(MissingMidnightOfferRootSignatureError); }); - test("error: MidnightOfferRootMismatchError", () => { + test("error: MidnightOfferRootMismatchError", async () => { const data = offersData(); const signature = offerRootSignature(data); const otherRoot = "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0" as Hex; - expect(() => - buildSubmitOffersTx({ + await expect( + buildMakeOffersOutputTx({ offersData: data, signatures: { ...signature, @@ -1986,9 +2031,9 @@ describe("MorphoMidnight", () => { }, }, }), - ).toThrow(MidnightOfferRootMismatchError); - expect(() => - buildSubmitOffersTx({ + ).rejects.toThrow(MidnightOfferRootMismatchError); + await expect( + buildMakeOffersOutputTx({ offersData: data, signatures: { ...signature, @@ -2001,92 +2046,92 @@ describe("MorphoMidnight", () => { }, }, }), - ).toThrow(MidnightOfferRootMismatchError); + ).rejects.toThrow(MidnightOfferRootMismatchError); }); - test("error: MidnightOfferRootOwnerMismatchError", () => { + test("error: MidnightOfferRootOwnerMismatchError", async () => { const data = offersData(); - expect(() => - buildSubmitOffersTx({ + await expect( + buildMakeOffersOutputTx({ offersData: data, signatures: offerRootSignature(data, { owner: midnightAddresses.taker, }), }), - ).toThrow(MidnightOfferRootOwnerMismatchError); + ).rejects.toThrow(MidnightOfferRootOwnerMismatchError); }); - test("error: MidnightOfferRootRatifierMismatchError", () => { + test("error: MidnightOfferRootRatifierMismatchError", async () => { const data = offersData(); - expect(() => - buildSubmitOffersTx({ + await expect( + buildMakeOffersOutputTx({ offersData: data, signatures: offerRootSignature(data, { ratifier: midnightAddresses.setterRatifier, }), }), - ).toThrow(MidnightOfferRootRatifierMismatchError); + ).rejects.toThrow(MidnightOfferRootRatifierMismatchError); }); - test("error: MidnightOfferRootOfferCountMismatchError", () => { + test("error: MidnightOfferRootOfferCountMismatchError", async () => { const data = offersData(); - expect(() => - buildSubmitOffersTx({ + await expect( + buildMakeOffersOutputTx({ offersData: data, signatures: offerRootSignature(data, { offers: data.tree.offers.length + 1, }), }), - ).toThrow(MidnightOfferRootOfferCountMismatchError); + ).rejects.toThrow(MidnightOfferRootOfferCountMismatchError); }); - test("error: UnpreparedMidnightOfferRootSignatureError", () => { + test("error: UnpreparedMidnightOfferRootSignatureError", async () => { const data = offersData(); - expect(() => - buildSubmitOffersTx({ + await expect( + buildMakeOffersOutputTx({ offersData: data, signatures: offerRootSignature(data), signedPayloads: new Map(), }), - ).toThrow(UnpreparedMidnightOfferRootSignatureError); + ).rejects.toThrow(UnpreparedMidnightOfferRootSignatureError); }); - test("error: AmbiguousRequirementSignaturesError", () => { + test("error: AmbiguousRequirementSignaturesError", async () => { const data = offersData(); const signature = offerRootSignature(data); - expect(() => - buildSubmitOffersTx({ + await expect( + buildMakeOffersOutputTx({ offersData: data, signatures: [signature, signature], }), - ).toThrow(AmbiguousRequirementSignaturesError); + ).rejects.toThrow(AmbiguousRequirementSignaturesError); }); - test("error: UnexpectedRequirementSignatureError", () => { + test("error: UnexpectedRequirementSignatureError", async () => { const data = offersData(); - expect(() => - buildSubmitOffersTx({ + await expect( + buildMakeOffersOutputTx({ offersData: data, signatures: unexpectedSignature, }), - ).toThrow(UnexpectedRequirementSignatureError); + ).rejects.toThrow(UnexpectedRequirementSignatureError); }); - test("error: UnexpectedRequirementSignatureError for setter ratifier", () => { + test("error: UnexpectedRequirementSignatureError for setter ratifier", async () => { const data = setterOffersData(); - expect(() => - buildSubmitOffersTx({ + await expect( + buildMakeOffersOutputTx({ offersData: data, signatures: [offerRootSignature(data)], }), - ).toThrow(UnexpectedRequirementSignatureError); + ).rejects.toThrow(UnexpectedRequirementSignatureError); }); }); }); diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.ts b/packages/morpho-sdk/src/entities/midnight/midnight.ts index b73945bf4..a10b4b963 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.ts @@ -40,12 +40,14 @@ import { signAndVerifyTypedData } from "../../helpers/signAndVerifyTypedData.js" import { validateMidnightMarket } from "../../helpers/validateMidnightMarket.js"; import { validateOfferSides } from "../../helpers/validateOfferSides.js"; import { validateTakeableOffers } from "../../helpers/validateTakeableOffers.js"; +import { TransactionPlan } from "../../transactionPlan/index.js"; import type { MorphoClientType } from "../../types/client.js"; import { AccrualPositionUserMismatchError, type ActionRequirement, InsufficientMidnightWithdrawableLiquidityError, MarketIdMismatchError, + type MempoolSubmitOffersAction, type MidnightCancelOfferAction, MidnightOfferMakerMismatchError, MidnightOfferMarketAddressMismatchError, @@ -93,10 +95,10 @@ import type { /** * Entity methods exposed by `client.morpho.midnight(chainId)`. * - * Use this surface for app flows: fetch market or position data first when a - * method asks for it, call the flow method to receive lazy `getRequirements` - * and `buildTx` handles, collect requirements, then build the final - * transaction synchronously. + * Midnight action flows are synchronous after the caller provides market, position, or offer data: + * the SDK does not own broad data fetching, letting apps batch, cache, and refresh state for their + * own UX. Each flow returns a TransactionPlan; call `prepare()` when the app is ready to compute the + * minimal signature requests and/or call requests needed to execute that intent. * * @example * ```ts @@ -104,7 +106,7 @@ import type { * * const midnight = client.morpho.midnight(8453); * const marketData = await midnight.getMarketData(marketId); - * const output = midnight.takeLend({ + * const plan = midnight.takeLend({ * accountAddress: lender, * marketData, * assets: BigInt(quote.data.availableAssets), @@ -112,8 +114,9 @@ import type { * takeableOffers: quote.data.takeableOffers, * deadline: maxUint256, * }); - * const requirements = await output.getRequirements(); - * const tx = output.buildTx(); + * const prepared = await plan.prepare(); + * const executable = prepared.build(await prepared.signAll(walletClient, lender)); + * const calls = executable.callRequests.map((request) => request.call); * ``` */ export type MidnightActions = Pick< @@ -149,9 +152,10 @@ const validateMarketData = (market: Market, chainId: number) => { /** * Entity facade for Midnight fixed-rate action flows. * - * `MorphoMidnight` keeps reads and transaction construction separated. Methods - * that need chain or API state fetch it up front, while returned `buildTx` - * callbacks are synchronous and only consume the data already passed in. + * Reads stay explicit and owned by the consumer. Methods that need chain or API state ask for it as + * input, then synchronously return a TransactionPlan. `prepare()` performs the targeted dependency + * discovery for that already chosen action, resolving only the signature requests and/or call + * requests needed before execution. * * @example * ```ts @@ -164,11 +168,12 @@ const validateMarketData = (market: Market, chainId: number) => { * parameters: { blockNumber: block.number }, * }) * ).accrueInterest(block.timestamp); - * const { buildTx } = midnight.redeem({ + * const plan = midnight.redeem({ * accountAddress: user, * positionData, * }); - * const tx = buildTx(); + * const executable = (await plan.prepare()).build(); + * const tx = executable.callRequests.at(-1)?.tx; * ``` */ export class MorphoMidnight { @@ -400,8 +405,12 @@ export class MorphoMidnight { const market = params.marketData; const midnightBundles = getChainAddress(this.chainId, "midnightBundles"); - return { - getRequirements: async () => { + return new TransactionPlan< + MidnightTakeLendAction, + undefined, + ActionRequirement + >({ + getRequirementRequests: async () => { const requirements: ActionRequirement[] = [ ...(await getMidnightApprovalRequirements({ viemClient: this.client.viemClient, @@ -422,7 +431,7 @@ export class MorphoMidnight { return requirements; }, - buildTx: () => + buildPrimaryCall: () => midnightTakeLend({ chainId: this.chainId, market: market.params, @@ -433,7 +442,7 @@ export class MorphoMidnight { deadline: params.deadline, metadata: this.client.options.metadata, }), - }; + }); } /** @@ -484,8 +493,12 @@ export class MorphoMidnight { const market = params.marketData; const midnightBundles = getChainAddress(this.chainId, "midnightBundles"); - return { - getRequirements: async () => { + return new TransactionPlan< + MidnightTakeBorrowAction, + undefined, + ActionRequirement + >({ + getRequirementRequests: async () => { const requirements: ActionRequirement[] = []; const authorization = await getMidnightAuthorizationRequirement({ viemClient: this.client.viemClient, @@ -497,7 +510,7 @@ export class MorphoMidnight { return requirements; }, - buildTx: () => + buildPrimaryCall: () => midnightTakeBorrow({ chainId: this.chainId, market: market.params, @@ -508,7 +521,7 @@ export class MorphoMidnight { deadline: params.deadline, metadata: this.client.options.metadata, }), - }; + }); } /** @@ -566,8 +579,12 @@ export class MorphoMidnight { const midnightBundles = getChainAddress(this.chainId, "midnightBundles"); const collateral = market.getCollateralByIndex(collateralIndex); - return { - getRequirements: async () => { + return new TransactionPlan< + MidnightSupplyCollateralTakeBorrowAction, + undefined, + ActionRequirement + >({ + getRequirementRequests: async () => { const requirements: ActionRequirement[] = [ ...(await getMidnightApprovalRequirements({ viemClient: this.client.viemClient, @@ -588,7 +605,7 @@ export class MorphoMidnight { return requirements; }, - buildTx: () => + buildPrimaryCall: () => midnightSupplyCollateralTakeBorrow({ chainId: this.chainId, market: market.params, @@ -601,7 +618,7 @@ export class MorphoMidnight { deadline: params.deadline, metadata: this.client.options.metadata, }), - }; + }); } /** @@ -644,8 +661,12 @@ export class MorphoMidnight { const collateral = market.getCollateralByIndex(collateralIndex); const midnight = getChainAddress(this.chainId, "midnight"); - return { - getRequirements: async () => + return new TransactionPlan< + MidnightSupplyCollateralAction, + undefined, + ActionRequirement + >({ + getRequirementRequests: async () => await getMidnightApprovalRequirements({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -655,7 +676,7 @@ export class MorphoMidnight { amount: params.collateralAssets + (params.reservedCollateralAssets ?? 0n), }), - buildTx: () => + buildPrimaryCall: () => midnightSupplyCollateral({ chainId: this.chainId, market: market.params, @@ -664,7 +685,7 @@ export class MorphoMidnight { onBehalf: params.accountAddress, metadata: this.client.options.metadata, }), - }; + }); } /** @@ -725,11 +746,9 @@ export class MorphoMidnight { const midnight = getChainAddress(this.chainId, "midnight"); const signedPayloads = new Map(); - return { - groups: data.groups, - root: data.tree.root, - ratifierType: data.ratifierType, - getRequirements: async () => { + return this.createMakeOffersOutput({ + offersData: data, + getRequirementRequests: async () => { const requirements: ActionRequirement[] = []; requirements.push( ...(await getMidnightApprovalRequirements({ @@ -750,13 +769,8 @@ export class MorphoMidnight { return requirements; }, - buildTx: (signatures?: MidnightActionSignatures) => - this.buildSubmitOffersTx({ - offersData: data, - signatures, - signedPayloads, - }), - }; + signedPayloads, + }); } /** @@ -789,23 +803,16 @@ export class MorphoMidnight { validateOfferSides(data.tree.offers, false); const signedPayloads = new Map(); - return { - groups: data.groups, - root: data.tree.root, - ratifierType: data.ratifierType, - getRequirements: async () => { + return this.createMakeOffersOutput({ + offersData: data, + getRequirementRequests: async () => { return await this.getRatifierRequirements({ offersData: data, signedPayloads, }); }, - buildTx: (signatures?: MidnightActionSignatures) => - this.buildSubmitOffersTx({ - offersData: data, - signatures, - signedPayloads, - }), - }; + signedPayloads, + }); } /** @@ -875,11 +882,9 @@ export class MorphoMidnight { const midnight = getChainAddress(this.chainId, "midnight"); const signedPayloads = new Map(); - return { - groups: data.groups, - root: data.tree.root, - ratifierType: data.ratifierType, - getRequirements: async () => { + return this.createMakeOffersOutput({ + offersData: data, + getRequirementRequests: async () => { const requirements: ActionRequirement[] = [ ...(await getMidnightApprovalRequirements({ viemClient: this.client.viemClient, @@ -906,13 +911,8 @@ export class MorphoMidnight { return requirements; }, - buildTx: (signatures?: MidnightActionSignatures) => - this.buildSubmitOffersTx({ - offersData: data, - signatures, - signedPayloads, - }), - }; + signedPayloads, + }); } /** @@ -974,9 +974,13 @@ export class MorphoMidnight { }); } - return { - getRequirements: async () => [], - buildTx: () => + return new TransactionPlan< + MidnightRedeemAction, + undefined, + ActionRequirement + >({ + getRequirementRequests: async () => [], + buildPrimaryCall: () => midnightRedeem({ chainId: this.chainId, market: market.params, @@ -985,7 +989,7 @@ export class MorphoMidnight { receiver: params.receiver, metadata: this.client.options.metadata, }), - }; + }); } /** @@ -1053,8 +1057,12 @@ export class MorphoMidnight { const midnightBundles = getChainAddress(this.chainId, "midnightBundles"); - return { - getRequirements: async () => { + return new TransactionPlan< + MidnightRepayWithdrawCollateralAction, + undefined, + ActionRequirement + >({ + getRequirementRequests: async () => { const requirements: ActionRequirement[] = []; if (params.repayAssets > 0n) { requirements.push( @@ -1078,7 +1086,7 @@ export class MorphoMidnight { return requirements; }, - buildTx: () => + buildPrimaryCall: () => midnightRepayWithdrawCollateral({ chainId: this.chainId, market: market.params, @@ -1089,7 +1097,7 @@ export class MorphoMidnight { deadline: params.deadline, metadata: this.client.options.metadata, }), - }; + }); } /** @@ -1114,16 +1122,126 @@ export class MorphoMidnight { }): MidnightActionOutput { validateChainId(this.client.viemClient.chain?.id, this.chainId); - return { - getRequirements: async () => [], - buildTx: () => + return new TransactionPlan< + MidnightCancelOfferAction, + undefined, + ActionRequirement + >({ + getRequirementRequests: async () => [], + buildPrimaryCall: () => midnightCancelOffer({ chainId: this.chainId, group: params.group, onBehalf: params.accountAddress, metadata: this.client.options.metadata, }), - }; + }); + } + + private createMakeOffersOutput(params: { + readonly offersData: OffersData; + readonly getRequirementRequests: () => Promise< + readonly ActionRequirement[] + >; + readonly signedPayloads: ReadonlyMap; + }): MakeOffersOutput { + const data = params.offersData; + return Object.assign( + new TransactionPlan< + MempoolSubmitOffersAction, + undefined, + ActionRequirement, + MidnightActionSignatures + >({ + getRequirementRequests: params.getRequirementRequests, + previewPrimaryCall: false, + buildPrimaryCall: (signatures) => { + const collectedSignatures = + signatures == null + ? undefined + : "action" in signatures + ? [signatures] + : signatures; + let payload = data.setterPayload; + if (data.ratifierType === "ecrecover") { + const { midnightOfferRoot: signature } = + selectRequirementSignatures(collectedSignatures, { + midnightOfferRoot: true, + }); + + if (signature == null) { + throw new MissingMidnightOfferRootSignatureError(); + } + if (!isAddressEqual(signature.args.owner, data.accountAddress)) { + throw new MidnightOfferRootOwnerMismatchError({ + expectedOwner: data.accountAddress, + actualOwner: signature.args.owner, + }); + } + if ( + signature.args.root.toLowerCase() !== data.tree.root.toLowerCase() + ) { + throw new MidnightOfferRootMismatchError({ + expectedRoot: data.tree.root, + actualRoot: signature.args.root, + }); + } + if ( + signature.action.args.root.toLowerCase() !== + data.tree.root.toLowerCase() + ) { + throw new MidnightOfferRootMismatchError({ + expectedRoot: data.tree.root, + actualRoot: signature.action.args.root, + }); + } + if ( + !isAddressEqual(signature.action.args.ratifier, data.ratifier) + ) { + throw new MidnightOfferRootRatifierMismatchError({ + expectedRatifier: data.ratifier, + actualRatifier: signature.action.args.ratifier, + }); + } + if (signature.action.args.offers !== data.tree.offers.length) { + throw new MidnightOfferRootOfferCountMismatchError({ + expectedOffers: data.tree.offers.length, + actualOffers: signature.action.args.offers, + }); + } + const signedPayload = params.signedPayloads.get( + signature.args.signature.toLowerCase(), + ); + if (signedPayload == null) { + throw new UnpreparedMidnightOfferRootSignatureError(); + } + payload = signedPayload; + } else { + selectRequirementSignatures(collectedSignatures, {}); + } + + if (payload == null) + throw new MissingMidnightOfferRootSignatureError(); + + return mempoolSubmitOffers({ + chainId: this.chainId, + groups: data.groups, + root: data.tree.root, + maker: data.accountAddress, + ratifier: data.ratifier, + ratifierType: data.ratifierType, + offers: data.tree.offers.length, + payload, + metadata: this.client.options.metadata, + }); + }, + }), + { + groups: data.groups, + root: data.tree.root, + ratifierType: data.ratifierType, + } as const, + ); } private async getRatifierRequirements(params: { @@ -1205,84 +1323,4 @@ export class MorphoMidnight { return requirements; } - - private buildSubmitOffersTx(params: { - readonly offersData: OffersData; - readonly signatures?: MidnightActionSignatures; - readonly signedPayloads: ReadonlyMap; - }) { - const data = params.offersData; - const collectedSignatures = - params.signatures == null - ? undefined - : "action" in params.signatures - ? [params.signatures] - : params.signatures; - let payload: Hex; - if (data.ratifierType === "ecrecover") { - const { midnightOfferRoot: signature } = selectRequirementSignatures( - collectedSignatures, - { midnightOfferRoot: true }, - ); - - if (signature == null) { - throw new MissingMidnightOfferRootSignatureError(); - } - if (!isAddressEqual(signature.args.owner, data.accountAddress)) { - throw new MidnightOfferRootOwnerMismatchError({ - expectedOwner: data.accountAddress, - actualOwner: signature.args.owner, - }); - } - if (signature.args.root.toLowerCase() !== data.tree.root.toLowerCase()) { - throw new MidnightOfferRootMismatchError({ - expectedRoot: data.tree.root, - actualRoot: signature.args.root, - }); - } - if ( - signature.action.args.root.toLowerCase() !== - data.tree.root.toLowerCase() - ) { - throw new MidnightOfferRootMismatchError({ - expectedRoot: data.tree.root, - actualRoot: signature.action.args.root, - }); - } - if (!isAddressEqual(signature.action.args.ratifier, data.ratifier)) { - throw new MidnightOfferRootRatifierMismatchError({ - expectedRatifier: data.ratifier, - actualRatifier: signature.action.args.ratifier, - }); - } - if (signature.action.args.offers !== data.tree.offers.length) { - throw new MidnightOfferRootOfferCountMismatchError({ - expectedOffers: data.tree.offers.length, - actualOffers: signature.action.args.offers, - }); - } - const signedPayload = params.signedPayloads.get( - signature.args.signature.toLowerCase(), - ); - if (signedPayload == null) { - throw new UnpreparedMidnightOfferRootSignatureError(); - } - payload = signedPayload; - } else { - selectRequirementSignatures(collectedSignatures, {}); - payload = data.setterPayload; - } - - return mempoolSubmitOffers({ - chainId: this.chainId, - groups: data.groups, - root: data.tree.root, - maker: data.accountAddress, - ratifier: data.ratifier, - ratifierType: data.ratifierType, - offers: data.tree.offers.length, - payload, - metadata: this.client.options.metadata, - }); - } } diff --git a/packages/morpho-sdk/src/entities/midnight/types.ts b/packages/morpho-sdk/src/entities/midnight/types.ts index ad989bdcb..df4aaa498 100644 --- a/packages/morpho-sdk/src/entities/midnight/types.ts +++ b/packages/morpho-sdk/src/entities/midnight/types.ts @@ -9,11 +9,13 @@ import type { } from "@morpho-org/midnight-sdk"; import type { Address, Hex } from "viem"; import type { MidnightTakeableOffer } from "../../actions/midnight/types.js"; +import type { TransactionPlan } from "../../transactionPlan/index.js"; import type { - ActionOutput, - BaseAction, + ActionRequirement, MempoolSubmitOffersAction, MidnightOfferRootSignature, + RequirementSignature, + TransactionAction, } from "../../types/action.js"; /** Optional Midnight API validation controls for make-offer flows. */ @@ -76,69 +78,47 @@ export type MidnightActionSignatures = | readonly MidnightOfferRootSignature[]; /** - * Lazy Midnight entity result with async requirements and sync transaction building. - * - * Call `getRequirements()` first to collect approvals, authorizations, or - * signatures. Once those are handled, pass any requirement signatures to - * `buildTx`; the transaction builder itself performs no fetching or signing. + * Lazy Midnight transaction plan. Call `prepare()` to resolve the signature requests and/or call + * requests needed by the already chosen Midnight action. * * @example * ```ts - * const output = midnight.takeLend(params); - * const requirements = await output.getRequirements(); - * for (const requirement of requirements) { - * if (!("sign" in requirement)) { - * await walletClient.sendTransaction({ - * to: requirement.to, - * data: requirement.data, - * value: requirement.value, - * }); - * } - * } - * const tx = output.buildTx(); + * const plan = midnight.takeLend(params); + * const prepared = await plan.prepare(); + * const signatures = await prepared.signAll(walletClient, taker); + * const executable = prepared.build(signatures); + * const calls = executable.callRequests.map((request) => request.call); * ``` */ export type MidnightActionOutput< - TAction extends BaseAction, - TSignatures = undefined, -> = ActionOutput; + TAction extends TransactionAction, + TSignatures = readonly RequirementSignature[], +> = TransactionPlan; /** - * Output returned by maker-offer flows after offer-tree preparation. + * Transaction plan returned by maker-offer flows after offer-tree preparation. * - * Use `groups` and `root` for review UI, call `getRequirements()` to collect - * ratifier approval or signature requirements, then call `buildTx(signatures)` - * to publish the encoded payload to the Midnight mempool. + * Use `groups` and `root` for review UI. Call `prepare()` to resolve ratifier approval/signature + * requests, then build the executable call requests with the collected signatures. * * @example * ```ts - * const output = await midnight.makeLend(params); - * console.log(output.root, output.groups); - * const requirements = await output.getRequirements(); - * const signatures = []; - * for (const requirement of requirements) { - * if ("sign" in requirement) { - * signatures.push(await requirement.sign(walletClient, maker)); - * } else { - * await walletClient.sendTransaction({ - * to: requirement.to, - * data: requirement.data, - * value: requirement.value, - * }); - * } - * } - * const tx = output.buildTx(signatures); + * const plan = await midnight.makeLend(params); + * console.log(plan.root, plan.groups); + * const prepared = await plan.prepare(); + * const signatures = await prepared.signAll(walletClient, maker); + * const executable = prepared.build(signatures); + * const calls = executable.callRequests.map((request) => request.call); * ``` */ -export interface MakeOffersOutput - extends MidnightActionOutput< - MempoolSubmitOffersAction, - MidnightActionSignatures - > { +export type MakeOffersOutput = MidnightActionOutput< + MempoolSubmitOffersAction, + MidnightActionSignatures +> & { readonly groups: readonly Hex[]; readonly root: Hex; readonly ratifierType: "ecrecover" | "setter"; -} +}; /** Parameters shared by Midnight market action flows. */ export interface MarketActionParams { diff --git a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts index 64b74961c..488d0d73d 100644 --- a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts @@ -12,6 +12,10 @@ import { KpkWETHVaultV2, } from "../../../test/fixtures/vaultV2.js"; import { test } from "../../../test/setup.js"; +import { + buildPlanTx, + getPlanRequests, +} from "../../../test/transactionPlanUtils.js"; import { morphoViemExtension } from "../../client/index.js"; import { MAX_SLIPPAGE_TOLERANCE } from "../../helpers/constant.js"; import { @@ -42,7 +46,7 @@ describe("MorphoVaultV1 entity tests", () => { await expect(vault.getData()).rejects.toThrow(ChainIdMismatchError); }); - test("deposit throws ChainIdMismatchError when client chain differs", () => { + test("deposit throws ChainIdMismatchError when client chain differs", async () => { const publicClient = createPublicClient({ chain: mainnet, transport: http("https://rpc.example"), @@ -60,7 +64,7 @@ describe("MorphoVaultV1 entity tests", () => { ).toThrow(ChainIdMismatchError); }); - test("withdraw and redeem throw ChainIdMismatchError when client chain differs", () => { + test("withdraw and redeem throw ChainIdMismatchError when client chain differs", async () => { const publicClient = createPublicClient({ chain: mainnet, transport: http("https://rpc.example"), @@ -105,11 +109,7 @@ describe("MorphoVaultV1 entity tests", () => { vaultData, slippageTolerance: 0n, }); - - expect(result.buildTx).toBeDefined(); - expect(result.getRequirements).toBeDefined(); - - const tx = result.buildTx(); + const tx = await buildPlanTx(result); expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -134,11 +134,7 @@ describe("MorphoVaultV1 entity tests", () => { vaultData, slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - - expect(result.buildTx).toBeDefined(); - expect(result.getRequirements).toBeDefined(); - - const tx = result.buildTx(); + const tx = await buildPlanTx(result); expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -312,7 +308,7 @@ describe("MorphoVaultV1 entity tests", () => { ).toThrow(NativeAmountOnNonWNativeVaultError); }); - test("should throw ChainWNativeMissingError when native deposit is requested on a chain without wNative", () => { + test("should throw ChainWNativeMissingError when native deposit is requested on a chain without wNative", async () => { const publicClient = createPublicClient({ chain: celo, transport: http("https://rpc.example"), @@ -350,13 +346,13 @@ describe("MorphoVaultV1 entity tests", () => { ); const vaultData = await vault.getData(); - const { getRequirements } = vault.deposit({ - amount: parseUnits("100", 6), - userAddress: client.account.address, - vaultData, - }); - - const requirements = await getRequirements(); + const requirements = await getPlanRequests( + vault.deposit({ + amount: parseUnits("100", 6), + userAddress: client.account.address, + vaultData, + }), + ); expect(requirements).toHaveLength(1); @@ -368,7 +364,7 @@ describe("MorphoVaultV1 entity tests", () => { }); describe("migrateToV2", () => { - test("should throw ChainIdMismatchError when client chain differs", () => { + test("should throw ChainIdMismatchError when client chain differs", async () => { const publicClient = createPublicClient({ chain: mainnet, transport: http("https://rpc.example"), @@ -506,11 +502,7 @@ describe("MorphoVaultV1 entity tests", () => { targetVault, shares: parseUnits("1000", 18), }); - - expect(result.buildTx).toBeDefined(); - expect(result.getRequirements).toBeDefined(); - - const tx = result.buildTx(); + const tx = await buildPlanTx(result); expect(tx.action.type).toBe("vaultV1MigrateToV2"); expect(tx.action.args.sourceVault).toBe(SteakhouseUsdcVaultV1.address); expect(tx.action.args.targetVault).toBe(KeyrockUsdcVaultV2.address); @@ -638,9 +630,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), slippageTolerance: 0n, }); - - expect(result.buildTx).toBeDefined(); - const tx = result.buildTx(); + const tx = await buildPlanTx(result); expect(tx.data).toBeDefined(); }); @@ -671,9 +661,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - - expect(result.buildTx).toBeDefined(); - const tx = result.buildTx(); + const tx = await buildPlanTx(result); expect(tx.data).toBeDefined(); }); @@ -703,14 +691,14 @@ describe("MorphoVaultV1 entity tests", () => { { chainId: mainnet.id }, ); - const { getRequirements } = vault.migrateToV2({ - userAddress: client.account.address, - sourceVault, - targetVault, - shares, - }); - - const requirements = await getRequirements(); + const requirements = await getPlanRequests( + vault.migrateToV2({ + userAddress: client.account.address, + sourceVault, + targetVault, + shares, + }), + ); expect(requirements).toHaveLength(1); @@ -746,14 +734,14 @@ describe("MorphoVaultV1 entity tests", () => { { chainId: mainnet.id }, ); - const { getRequirements } = vault.migrateToV2({ - userAddress: client.account.address, - sourceVault, - targetVault, - shares, - }); - - const requirements = await getRequirements(); + const requirements = await getPlanRequests( + vault.migrateToV2({ + userAddress: client.account.address, + sourceVault, + targetVault, + shares, + }), + ); expect(requirements.length).toBeGreaterThanOrEqual(1); }); @@ -792,7 +780,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), }); - const tx = result.buildTx(); + const tx = await buildPlanTx(result); expect(tx.action.args.recipient).toBe(OTHER_USER); }); @@ -825,7 +813,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), }); - const tx = result.buildTx(); + const tx = await buildPlanTx(result); expect(tx.action.args.recipient).toBe(OTHER_USER); }); }); diff --git a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts index 5f9f0def7..723e2f91e 100644 --- a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts +++ b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts @@ -20,12 +20,16 @@ import { MAX_SLIPPAGE_TOLERANCE, } from "../../helpers/constant.js"; import { validateChainId } from "../../helpers/index.js"; +import { + TransactionPlan, + type TransactionPlanSimplePermitOptions, + type TransactionPlanTokenRequest, +} from "../../transactionPlan/index.js"; import type { FetchParameters } from "../../types/data.js"; import { ChainIdMismatchError, ChainWNativeMissingError, type DepositAmountArgs, - type ERC20ApprovalAction, ExcessiveSlippageToleranceError, type MorphoClientType, NativeAmountOnNonWNativeVaultError, @@ -33,11 +37,7 @@ import { NegativeSlippageToleranceError, NonPositiveAssetAmountError, NonPositiveSharesAmountError, - type PermitRequirementSignature, - type Requirement, - type RequirementSignature, selectRequirementSignatures, - type Transaction, VaultAddressMismatchError, VaultAssetMismatchError, type VaultV1DepositAction, @@ -46,6 +46,12 @@ import { type VaultV1WithdrawAction, } from "../../types/index.js"; +/** + * VaultV1 action flows are synchronous so the SDK does not own vault data fetching. Consumers pass + * the vault snapshots they already fetched, which lets each app batch, cache, and refresh data for + * its own UX. Each flow returns a TransactionPlan; call `prepare()` when the app is ready to compute + * the minimal signature requests and/or call requests needed to execute that intent. + */ export interface VaultV1Actions { /** * Fetches the latest vault data with accrued interest. @@ -60,7 +66,7 @@ export interface VaultV1Actions { * Prepares a deposit into a VaultV1 (MetaMorpho) contract. * * Uses pre-fetched vault data to compute `maxSharePrice` with slippage tolerance, - * then returns `buildTx` and `getRequirements` for lazy evaluation. + * then returns a TransactionPlan for lazy request discovery and execution. * * @param {Object} params - The deposit parameters. * @param {bigint} params.amount - Amount of assets to deposit. @@ -76,19 +82,11 @@ export interface VaultV1Actions { vaultData: AccrualVault; slippageTolerance?: bigint; } & DepositAmountArgs, - ) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: (params?: { - useSimplePermit?: boolean; - }) => Promise< - ( - | Readonly> - | Requirement - )[] - >; - }; + ) => TransactionPlan< + VaultV1DepositAction, + TransactionPlanSimplePermitOptions, + TransactionPlanTokenRequest + >; /** * Prepares a withdraw from a VaultV1 (MetaMorpho) contract. * @@ -97,9 +95,10 @@ export interface VaultV1Actions { * @param {Address} params.userAddress - User address initiating the withdraw. * @returns {Object} Object with `buildTx`. */ - withdraw: (params: { amount: bigint; userAddress: Address }) => { - buildTx: () => Readonly>; - }; + withdraw: (params: { + amount: bigint; + userAddress: Address; + }) => TransactionPlan; /** * Prepares a redeem from a VaultV1 (MetaMorpho) contract. * @@ -108,9 +107,10 @@ export interface VaultV1Actions { * @param {Address} params.userAddress - User address initiating the redeem. * @returns {Object} Object with `buildTx`. */ - redeem: (params: { shares: bigint; userAddress: Address }) => { - buildTx: () => Readonly>; - }; + redeem: (params: { + shares: bigint; + userAddress: Address; + }) => TransactionPlan; /** * Prepares a full migration from VaultV1 to VaultV2. * @@ -131,17 +131,11 @@ export interface VaultV1Actions { targetVault: AccrualVaultV2; shares: bigint; slippageTolerance?: bigint; - }) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: () => Promise< - ( - | Readonly> - | Requirement - )[] - >; - }; + }) => TransactionPlan< + VaultV1MigrateToV2Action, + unknown, + TransactionPlanTokenRequest + >; } export class MorphoVaultV1 implements VaultV1Actions { @@ -236,8 +230,8 @@ export class MorphoVaultV1 implements VaultV1Actions { ), MAX_ABSOLUTE_SHARE_PRICE, ); - return { - getRequirements: (params?: { useSimplePermit?: boolean }) => + return new TransactionPlan({ + getRequirementRequests: (params?: TransactionPlanSimplePermitOptions) => getGeneralAdapterRequirements(this.client.viemClient, { address: vaultData.asset, chainId: this.chainId, @@ -250,7 +244,7 @@ export class MorphoVaultV1 implements VaultV1Actions { }, }), - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -271,7 +265,7 @@ export class MorphoVaultV1 implements VaultV1Actions { metadata: this.client.options.metadata, }); }, - }; + }); } withdraw({ amount, userAddress }: { amount: bigint; userAddress: Address }) { @@ -282,8 +276,8 @@ export class MorphoVaultV1 implements VaultV1Actions { ); } - return { - buildTx: () => + return new TransactionPlan({ + buildPrimaryCall: () => vaultV1Withdraw({ vault: { address: this.vault }, args: { @@ -293,7 +287,7 @@ export class MorphoVaultV1 implements VaultV1Actions { }, metadata: this.client.options.metadata, }), - }; + }); } redeem({ shares, userAddress }: { shares: bigint; userAddress: Address }) { @@ -304,8 +298,8 @@ export class MorphoVaultV1 implements VaultV1Actions { ); } - return { - buildTx: () => + return new TransactionPlan({ + buildPrimaryCall: () => vaultV1Redeem({ vault: { address: this.vault }, args: { @@ -315,7 +309,7 @@ export class MorphoVaultV1 implements VaultV1Actions { }, metadata: this.client.options.metadata, }), - }; + }); } migrateToV2({ @@ -379,8 +373,8 @@ export class MorphoVaultV1 implements VaultV1Actions { ), MAX_ABSOLUTE_SHARE_PRICE, ); - return { - getRequirements: () => + return new TransactionPlan({ + getRequirementRequests: () => getGeneralAdapterRequirements(this.client.viemClient, { address: this.vault, chainId: this.chainId, @@ -394,7 +388,7 @@ export class MorphoVaultV1 implements VaultV1Actions { }, }), - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -417,6 +411,6 @@ export class MorphoVaultV1 implements VaultV1Actions { metadata: this.client.options.metadata, }); }, - }; + }); } } diff --git a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts index 85aeecf22..37aec294c 100644 --- a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts @@ -7,6 +7,7 @@ import { KpkWETHVaultV2, } from "../../../test/fixtures/vaultV2.js"; import { test } from "../../../test/setup.js"; +import { buildPlanTx } from "../../../test/transactionPlanUtils.js"; import { morphoViemExtension } from "../../client/index.js"; import { MAX_SLIPPAGE_TOLERANCE } from "../../helpers/constant.js"; import { @@ -35,7 +36,7 @@ describe("MorphoVaultV2 entity tests", () => { await expect(vault.getData()).rejects.toThrow(ChainIdMismatchError); }); - test("deposit throws ChainIdMismatchError when client chain differs", () => { + test("deposit throws ChainIdMismatchError when client chain differs", async () => { const publicClient = createPublicClient({ chain: mainnet, transport: http("https://rpc.example"), @@ -106,11 +107,7 @@ describe("MorphoVaultV2 entity tests", () => { vaultData, slippageTolerance: 0n, }); - - expect(result.buildTx).toBeDefined(); - expect(result.getRequirements).toBeDefined(); - - const tx = result.buildTx(); + const tx = await buildPlanTx(result); expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -135,11 +132,7 @@ describe("MorphoVaultV2 entity tests", () => { vaultData, slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - - expect(result.buildTx).toBeDefined(); - expect(result.getRequirements).toBeDefined(); - - const tx = result.buildTx(); + const tx = await buildPlanTx(result); expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -263,7 +256,7 @@ describe("MorphoVaultV2 entity tests", () => { ).toThrow(NegativeNativeAmountError); }); - test("should throw ChainWNativeMissingError when native deposit is requested on a chain without wNative", () => { + test("should throw ChainWNativeMissingError when native deposit is requested on a chain without wNative", async () => { const publicClient = createPublicClient({ chain: celo, transport: http("https://rpc.example"), diff --git a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts index a724bbff7..fc6540bae 100644 --- a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts +++ b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts @@ -16,6 +16,11 @@ import { vaultV2Withdraw, } from "../../actions/index.js"; import { MAX_SLIPPAGE_TOLERANCE } from "../../helpers/constant.js"; +import { + TransactionPlan, + type TransactionPlanSimplePermitOptions, + type TransactionPlanTokenRequest, +} from "../../transactionPlan/index.js"; import type { FetchParameters } from "../../types/data.js"; import { ChainIdMismatchError, @@ -32,7 +37,6 @@ import { NonPositiveSharesAmountError, type PermitRequirementSignature, type Requirement, - type RequirementSignature, selectRequirementSignatures, type Transaction, VaultAddressMismatchError, @@ -43,6 +47,12 @@ import { type VaultV2WithdrawAction, } from "../../types/index.js"; +/** + * VaultV2 action flows are synchronous so the SDK does not own vault data fetching. Consumers pass + * the vault snapshots they already fetched, which lets each app batch, cache, and refresh data for + * its own UX. Each flow returns a TransactionPlan; call `prepare()` when the app is ready to compute + * the minimal signature requests and/or call requests needed to execute that intent. + */ export interface VaultV2Actions { /** * Fetches the latest vault data. @@ -71,7 +81,7 @@ export interface VaultV2Actions { * @param {bigint} [params.nativeAmount] - Amount of native token to wrap into wNative. Vault asset must be wNative. * @returns {Object} The result object. * @returns {Readonly>} returns.tx The prepared deposit transaction. - * @returns {Promise<(Readonly> | Requirement)[]>} returns.getRequirements The function for retrieving all required approval transactions. + * @returns {Promise<(Readonly> | Requirement)[]>} returns.prepare Resolves supporting approval/signature requests plus the primary call. */ deposit: ( params: { @@ -79,19 +89,11 @@ export interface VaultV2Actions { vaultData: AccrualVaultV2; slippageTolerance?: bigint; } & DepositAmountArgs, - ) => { - buildTx: ( - signatures?: readonly RequirementSignature[], - ) => Readonly>; - getRequirements: (params?: { - useSimplePermit?: boolean; - }) => Promise< - ( - | Readonly> - | Requirement - )[] - >; - }; + ) => TransactionPlan< + VaultV2DepositAction, + TransactionPlanSimplePermitOptions, + TransactionPlanTokenRequest + >; /** * Prepares a withdraw transaction for the VaultV2 contract. * @@ -103,9 +105,10 @@ export interface VaultV2Actions { * @returns {Object} The result object. * @returns {Readonly>} returns.tx The prepared withdraw transaction. */ - withdraw: (params: { amount: bigint; userAddress: Address }) => { - buildTx: () => Readonly>; - }; + withdraw: (params: { + amount: bigint; + userAddress: Address; + }) => TransactionPlan; /** * Prepares a redeem transaction for the VaultV2 contract. * @@ -117,9 +120,10 @@ export interface VaultV2Actions { * @returns {Object} The result object. * @returns {Readonly>} returns.tx The prepared redeem transaction. */ - redeem: (params: { shares: bigint; userAddress: Address }) => { - buildTx: () => Readonly>; - }; + redeem: (params: { + shares: bigint; + userAddress: Address; + }) => TransactionPlan; /** * Prepares a force withdraw transaction for the VaultV2 contract using the vault's native multicall. * @@ -133,15 +137,13 @@ export interface VaultV2Actions { * @param {bigint} params.withdraw.amount - The amount of assets to withdraw. * @param {Address} params.userAddress - User address (penalty source and withdraw recipient). * @returns {Object} The result object. - * @returns {Readonly>} returns.buildTx The prepared multicall transaction. + * @returns {Readonly>} returns.prepare Resolves the primary multicall request. */ forceWithdraw: (params: { deallocations: readonly Deallocation[]; withdraw: { amount: bigint }; userAddress: Address; - }) => { - buildTx: () => Readonly>; - }; + }) => TransactionPlan; /** * Prepares a force redeem transaction for the VaultV2 contract using the vault's native multicall. * @@ -162,15 +164,13 @@ export interface VaultV2Actions { * @param {bigint} params.redeem.shares - The amount of shares to redeem. * @param {Address} params.userAddress - User address (penalty source and redeem recipient). * @returns {Object} The result object. - * @returns {Readonly>} returns.buildTx The prepared multicall transaction. + * @returns {Readonly>} returns.prepare Resolves the primary multicall request. */ forceRedeem: (params: { deallocations: readonly Deallocation[]; redeem: { shares: bigint }; userAddress: Address; - }) => { - buildTx: () => Readonly>; - }; + }) => TransactionPlan; } export class MorphoVaultV2 implements VaultV2Actions { @@ -271,8 +271,8 @@ export class MorphoVaultV2 implements VaultV2Actions { ), MathLib.RAY * 100n, ); - return { - getRequirements: (params?: { useSimplePermit?: boolean }) => + return new TransactionPlan({ + getRequirementRequests: (params?: TransactionPlanSimplePermitOptions) => getGeneralAdapterRequirements(this.client.viemClient, { address: vaultData.asset, chainId: this.chainId, @@ -285,7 +285,7 @@ export class MorphoVaultV2 implements VaultV2Actions { }, }), - buildTx: (signatures?: readonly RequirementSignature[]) => { + buildPrimaryCall: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -306,7 +306,7 @@ export class MorphoVaultV2 implements VaultV2Actions { metadata: this.client.options.metadata, }); }, - }; + }); } withdraw({ amount, userAddress }: { amount: bigint; userAddress: Address }) { @@ -317,8 +317,8 @@ export class MorphoVaultV2 implements VaultV2Actions { ); } - return { - buildTx: () => + return new TransactionPlan({ + buildPrimaryCall: () => vaultV2Withdraw({ vault: { address: this.vault }, args: { @@ -328,7 +328,7 @@ export class MorphoVaultV2 implements VaultV2Actions { }, metadata: this.client.options.metadata, }), - }; + }); } redeem({ shares, userAddress }: { shares: bigint; userAddress: Address }) { @@ -339,8 +339,8 @@ export class MorphoVaultV2 implements VaultV2Actions { ); } - return { - buildTx: () => + return new TransactionPlan({ + buildPrimaryCall: () => vaultV2Redeem({ vault: { address: this.vault }, args: { @@ -350,7 +350,7 @@ export class MorphoVaultV2 implements VaultV2Actions { }, metadata: this.client.options.metadata, }), - }; + }); } forceWithdraw({ @@ -369,8 +369,8 @@ export class MorphoVaultV2 implements VaultV2Actions { ); } - return { - buildTx: () => + return new TransactionPlan({ + buildPrimaryCall: () => vaultV2ForceWithdraw({ vault: { address: this.vault }, args: { @@ -383,7 +383,7 @@ export class MorphoVaultV2 implements VaultV2Actions { }, metadata: this.client.options.metadata, }), - }; + }); } forceRedeem({ @@ -402,8 +402,8 @@ export class MorphoVaultV2 implements VaultV2Actions { ); } - return { - buildTx: () => + return new TransactionPlan({ + buildPrimaryCall: () => vaultV2ForceRedeem({ vault: { address: this.vault }, args: { @@ -416,6 +416,6 @@ export class MorphoVaultV2 implements VaultV2Actions { }, metadata: this.client.options.metadata, }), - }; + }); } } diff --git a/packages/morpho-sdk/src/helpers/computeReallocations.ts b/packages/morpho-sdk/src/helpers/computeReallocations.ts index 9603b6440..a322ac766 100644 --- a/packages/morpho-sdk/src/helpers/computeReallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeReallocations.ts @@ -145,7 +145,7 @@ const capVaultWithdrawals = ( * positionData, * reallocations, * }); - * // borrow.buildTx() includes any required PublicAllocator reallocations. + * // borrow.prepare().build().callRequests includes any required PublicAllocator reallocations. * ``` */ export const computeReallocations = ({ diff --git a/packages/morpho-sdk/src/index.ts b/packages/morpho-sdk/src/index.ts index 4630402d8..b6b48df6a 100644 --- a/packages/morpho-sdk/src/index.ts +++ b/packages/morpho-sdk/src/index.ts @@ -1,4 +1,5 @@ export * from "./actions/index.js"; export * from "./client/index.js"; export * from "./helpers/index.js"; +export * from "./transactionPlan/index.js"; export * from "./types/index.js"; diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts new file mode 100644 index 000000000..684a98b30 --- /dev/null +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts @@ -0,0 +1,326 @@ +import type { Address, Hex, WalletClient } from "viem"; +import { describe, expect, test, vi } from "vitest"; +import { + type AuthorizationRequirementSignature, + type BlueAuthorizationAction, + type ERC20ApprovalAction, + MissingTransactionPlanSignaturesError, + type PermitRequirementSignature, + type Requirement, + type RequirementSignature, + type Transaction, + type VaultV2DepositAction, +} from "../types/index.js"; +import { + TransactionPlan, + type TransactionPlanHandler, +} from "./TransactionPlan.js"; + +const USER: Address = "0x1111111111111111111111111111111111111111"; +const TOKEN: Address = "0x2222222222222222222222222222222222222222"; +const SPENDER: Address = "0x3333333333333333333333333333333333333333"; +const BUNDLER: Address = "0x4444444444444444444444444444444444444444"; +const VAULT: Address = "0x5555555555555555555555555555555555555555"; +const SIGNATURE: Hex = `0x${"ab".repeat(65)}`; + +const approvalTx: Transaction = { + to: TOKEN, + value: 0n, + data: "0xa1", + action: { + type: "erc20Approval", + args: { spender: SPENDER, amount: 100n }, + }, +}; + +const authorizationTx: Transaction = { + to: VAULT, + value: 0n, + data: "0xa2", + action: { + type: "blueAuthorization", + args: { authorized: SPENDER, isAuthorized: true }, + }, +}; + +const primaryTx: Transaction = { + to: BUNDLER, + value: 0n, + data: "0xf1", + action: { + type: "vaultV2Deposit", + args: { + vault: VAULT, + amount: 100n, + maxSharePrice: 1_000_000_000_000_000_000n, + recipient: USER, + }, + }, +}; + +const permitSignature: PermitRequirementSignature = { + action: { + type: "permit2", + args: { + token: TOKEN, + spender: SPENDER, + amount: 100n, + deadline: 1_900_000_000n, + expiration: 2_000_000_000n, + chainId: 1, + }, + }, + args: { + owner: USER, + asset: TOKEN, + amount: 100n, + nonce: 0n, + deadline: 1_900_000_000n, + expiration: 2_000_000_000n, + signature: SIGNATURE, + }, +}; + +const authorizationSignature: AuthorizationRequirementSignature = { + action: { + type: "authorization", + args: { + authorized: SPENDER, + isAuthorized: true, + deadline: 1_900_000_000n, + chainId: 1, + }, + }, + args: { + owner: USER, + authorized: SPENDER, + isAuthorized: true, + nonce: 0n, + deadline: 1_900_000_000n, + signature: SIGNATURE, + }, +}; + +function permitRequest(params?: { + readonly sign?: Requirement["sign"]; +}): Requirement { + return { + action: permitSignature.action, + sign: params?.sign ?? vi.fn(async () => permitSignature), + }; +} + +function authorizationRequest(params?: { + readonly sign?: Requirement["sign"]; +}): Requirement { + return { + action: authorizationSignature.action, + sign: params?.sign ?? vi.fn(async () => authorizationSignature), + }; +} + +describe("TransactionPlan", () => { + test("default: exposes prepare as the single public entry point and prepares semantic requests", async () => { + const handler: TransactionPlanHandler = { + getRequirementRequests: vi.fn(async () => [ + approvalTx, + permitRequest(), + authorizationRequest(), + ]), + buildPrimaryCall: vi.fn(() => primaryTx), + }; + + const plan = TransactionPlan.create(handler); + const prepared = await plan.prepare(); + + expect(plan).toBeInstanceOf(TransactionPlan); + expect("buildTx" in plan).toBe(false); + expect("getRequirements" in plan).toBe(false); + expect(Object.hasOwn(plan, "prepare")).toBe(false); + expect(prepared.flowKind).toBe("mixed_requests"); + expect(prepared.hasSignatureRequests).toBe(true); + expect(prepared.hasCallRequests).toBe(true); + expect(prepared.steps.map((request) => request.kind)).toEqual([ + "call", + "signature", + "signature", + "call", + ]); + expect(prepared.callRequests.map((request) => request.id)).toEqual([ + "request-0", + "primary", + ]); + expect(prepared.callRequests.map((request) => request.phase)).toEqual([ + "preparation", + "primary", + ]); + expect(prepared.signatureRequests).toHaveLength(2); + expect(prepared.requestCount).toBe(4); + expect(prepared.hasIntent("tokenApproval")).toBe(true); + expect(prepared.hasIntent("operatorAuthorization")).toBe(true); + expect(prepared.hasIntent("primaryTransaction")).toBe(true); + expect( + prepared.findIntent("tokenApproval").map((request) => request.intent), + ).toEqual([ + { + type: "tokenApproval", + method: "tx", + token: TOKEN, + spender: SPENDER, + amount: 100n, + }, + { + type: "tokenApproval", + method: "permit2", + token: TOKEN, + spender: SPENDER, + amount: 100n, + deadline: 1_900_000_000n, + expiration: 2_000_000_000n, + chainId: 1, + }, + ]); + }); + + test("behavior: signs every signature request sequentially", async () => { + let resolveFirst: ((value: PermitRequirementSignature) => void) | undefined; + const firstSign = vi.fn( + () => + new Promise((resolve) => { + resolveFirst = resolve; + }), + ); + const secondSign = vi.fn(async () => authorizationSignature); + const plan = TransactionPlan.create({ + getRequirementRequests: vi.fn(async () => [ + permitRequest({ sign: firstSign }), + authorizationRequest({ sign: secondSign }), + ]), + buildPrimaryCall: vi.fn(() => primaryTx), + }); + const prepared = await plan.prepare(); + + const promise = prepared.signAll({} as WalletClient, USER); + await Promise.resolve(); + + expect(firstSign).toHaveBeenCalledWith({}, USER); + expect(secondSign).not.toHaveBeenCalled(); + + resolveFirst?.(permitSignature); + const signatures = await promise; + + expect(secondSign).toHaveBeenCalledWith({}, USER); + expect(signatures).toEqual([permitSignature, authorizationSignature]); + }); + + test("behavior: builds ordered call requests only after requested signatures are provided", async () => { + const permit = permitRequest(); + const authorization = authorizationRequest(); + const buildPrimaryCall = vi.fn( + (_signatures?: readonly RequirementSignature[]) => primaryTx, + ); + const plan = TransactionPlan.create({ + getRequirementRequests: vi.fn(async () => [ + approvalTx, + permit, + authorization, + ]), + buildPrimaryCall, + }); + + const prepared = await plan.prepare(); + + expect(() => prepared.build()).toThrow( + MissingTransactionPlanSignaturesError, + ); + expect(() => prepared.build([permitSignature])).toThrow( + MissingTransactionPlanSignaturesError, + ); + + const executable = prepared.build([ + permitSignature, + authorizationSignature, + ]); + + expect(buildPrimaryCall).toHaveBeenCalledWith([ + permitSignature, + authorizationSignature, + ]); + expect(executable.callRequests.map((request) => request.id)).toEqual([ + "request-0", + "primary", + ]); + expect(executable.callRequests.at(-1)?.tx).toBe(primaryTx); + expect(executable.viemCalls).toEqual([ + { to: TOKEN, value: 0n, data: "0xa1" }, + { to: BUNDLER, value: 0n, data: "0xf1" }, + ]); + }); + + test("behavior: requirement call requests keep authorization intent separate from token approval", async () => { + const plan = TransactionPlan.create({ + getRequirementRequests: vi.fn(async () => [authorizationTx]), + buildPrimaryCall: vi.fn(() => primaryTx), + }); + + const prepared = await plan.prepare(); + + expect(prepared.flowKind).toBe("call_requests"); + expect( + prepared + .findIntent("operatorAuthorization") + .map((request) => request.intent), + ).toEqual([ + { + type: "operatorAuthorization", + method: "tx", + operator: SPENDER, + isAuthorized: true, + }, + ]); + expect(prepared.findIntent("tokenApproval")).toEqual([]); + }); + + test("behavior: a non-previewable primary call appears after build", async () => { + const plan = TransactionPlan.create({ + getRequirementRequests: vi.fn(async () => [permitRequest()]), + previewPrimaryCall: false, + buildPrimaryCall: vi.fn(() => primaryTx), + }); + + const prepared = await plan.prepare(); + + expect(prepared.flowKind).toBe("signature_requests"); + expect(prepared.callRequests).toEqual([]); + expect(prepared.steps.map((request) => request.id)).toEqual(["request-0"]); + + const executable = prepared.build([permitSignature]); + + expect(executable.callRequests.at(-1)?.tx).toBe(primaryTx); + expect(executable.callRequests.map((request) => request.id)).toEqual([ + "primary", + ]); + }); + + test("behavior: a handler without requirement requests is a single call", async () => { + const plan = TransactionPlan.create({ + buildPrimaryCall: vi.fn(() => primaryTx), + }); + + const prepared = await plan.prepare(); + const executable = prepared.build(); + + expect(prepared.flowKind).toBe("single_call"); + expect(prepared.requestCount).toBe(1); + expect(prepared.signatureRequests).toEqual([]); + expect(prepared.callRequests.map((request) => request.id)).toEqual([ + "primary", + ]); + expect(executable.callRequests.map((request) => request.id)).toEqual([ + "primary", + ]); + expect(executable.viemCalls).toEqual([ + { to: BUNDLER, value: 0n, data: "0xf1" }, + ]); + }); +}); diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts new file mode 100644 index 000000000..803597a8b --- /dev/null +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts @@ -0,0 +1,494 @@ +import type { Address, WalletClient } from "viem"; +import { + type CallRequirement, + isRequirementApproval, + isRequirementBlueAuthorization, + type RequirementSignature, + type SignatureRequirement, + type TransactionAction, +} from "../types/action.js"; +import { MissingTransactionPlanSignaturesError } from "../types/error.js"; +import type { + ExecutableTransactionPlanShape, + PreparedTransactionPlanShape, + TransactionPlanBuildPrimaryCall, + TransactionPlanCallRequest, + TransactionPlanContractCallIntent, + TransactionPlanFlowKind, + TransactionPlanHandler, + TransactionPlanIntent, + TransactionPlanMidnightOfferRootIntent, + TransactionPlanOperatorAuthorizationIntent, + TransactionPlanPrepareOptions, + TransactionPlanRequest, + TransactionPlanSignatureRequest, + TransactionPlanStep, + TransactionPlanStepForIntent, + TransactionPlanTokenApprovalIntent, + TransactionPlanViemCall, +} from "./types.js"; + +export type { + ExecutableTransactionPlanShape, + PreparedTransactionPlanShape, + TransactionPlanHandler, + TransactionPlanIntent, + TransactionPlanPrepareOptions, +} from "./types.js"; + +/** + * Lazy transaction plan returned by Morpho SDK action flows. + * + * Entity action flows stay synchronous: the SDK does not fetch or own the full state needed to + * decide user intent. Consumers pass already-fetched market, position, and vault snapshots into the + * flow, so apps can batch, cache, stale-while-revalidate, or otherwise optimize state fetching for + * their own UX. Calling `prepare()` performs only the minimal request discovery for the already + * chosen intent: it resolves the signature requests and/or viem-compatible call requests that the + * primary action depends on, while avoiding extra reads when no approval or operator authorization + * check is needed. + * + * @example Vault app review branching and labels + * ```ts + * const plan = vault.deposit({ amount, userAddress, vaultData }); + * const prepared = await plan.prepare({ requestOptions: { useSimplePermit } }); + * + * const txFlowType = prepared.flowKind === "single_call" + * ? "simple" + * : prepared.hasSignatureRequests + * ? "signature_required" + * : "bundled"; + * + * const labels = prepared.steps.map((request) => { + * switch (request.intent.type) { + * case "tokenApproval": + * return request.intent.method === "tx" + * ? `Approve ${request.intent.amount}` + * : `Sign ${request.intent.method}`; + * case "operatorAuthorization": + * return "Authorize operator"; + * case "contractCall": + * return `Review ${request.intent.actionType}`; + * case "midnightOfferRootSignature": + * return "Sign offer root"; + * case "primaryTransaction": + * return "Vault deposit"; + * } + * }); + * ``` + * + * @example Markets app action-flow shape + * ```ts + * const prepared = await market.supplyCollateralBorrow(params).prepare(); + * + * const signatureRequests = prepared.signatureRequests.map((request) => ({ + * label: request.intent.type === "operatorAuthorization" ? "Authorize operator" : "Sign", + * sign: (client) => request.sign(client, accountAddress), + * })); + * + * const executable = prepared.build(await prepared.signAll(walletClient, accountAddress)); + * const callRequests = executable.callRequests.map((request) => ({ + * label: request.intent.type === "primaryTransaction" ? "Submit transaction" : "Approve token", + * getCall: () => request.call, + * })); + * ``` + */ +export class TransactionPlan< + TPrimaryAction extends TransactionAction = TransactionAction, + TRequestOptions = unknown, + TRequest extends TransactionPlanRequest = TransactionPlanRequest, + TSignatures = readonly RequirementSignature[], +> { + private readonly handler: TransactionPlanHandler< + TPrimaryAction, + TRequestOptions, + TRequest, + TSignatures + >; + + /** + * Creates a lazy TransactionPlan from SDK handler callbacks. + * + * @param handler - SDK handler callbacks used by `prepare()` and `PreparedTransactionPlan.build(...)`. + */ + constructor( + handler: TransactionPlanHandler< + TPrimaryAction, + TRequestOptions, + TRequest, + TSignatures + >, + ) { + this.handler = handler; + } + + /** Prepares semantic review and execution metadata by resolving current requests. */ + async prepare( + options?: TransactionPlanPrepareOptions, + ): Promise> { + const requests = this.handler.getRequirementRequests + ? await this.handler.getRequirementRequests(options?.requestOptions) + : []; + const requestSteps = requests.map((request, index) => { + const id = `request-${index}`; + if (isRequirementApproval(request)) { + return { + kind: "call", + id, + phase: "preparation", + tx: request, + action: request.action, + intent: { + type: "tokenApproval", + method: "tx", + token: request.to, + spender: request.action.args.spender, + amount: request.action.args.amount, + }, + call: { to: request.to, value: request.value, data: request.data }, + } satisfies TransactionPlanCallRequest; + } + if (isRequirementBlueAuthorization(request)) { + return { + kind: "call", + id, + phase: "preparation", + tx: request, + action: request.action, + intent: { + type: "operatorAuthorization", + method: "tx", + operator: request.action.args.authorized, + isAuthorized: request.action.args.isAuthorized, + }, + call: { to: request.to, value: request.value, data: request.data }, + } satisfies TransactionPlanCallRequest; + } + if ( + typeof request === "object" && + request !== null && + "to" in request && + "value" in request && + "data" in request && + "action" in request + ) { + const tx = request as CallRequirement; + const intent: + | TransactionPlanOperatorAuthorizationIntent + | TransactionPlanContractCallIntent = + tx.action.type === "midnightAuthorization" + ? { + type: "operatorAuthorization", + method: "tx", + operator: tx.action.args.authorized, + isAuthorized: tx.action.args.isAuthorized, + owner: tx.action.args.onBehalf, + } + : { type: "contractCall", actionType: tx.action.type }; + return { + kind: "call", + id, + phase: "preparation", + tx, + action: tx.action, + intent, + call: { to: tx.to, value: tx.value, data: tx.data }, + } satisfies TransactionPlanCallRequest; + } + + const signatureRequest = request as SignatureRequirement; + let intent: + | TransactionPlanTokenApprovalIntent + | TransactionPlanOperatorAuthorizationIntent + | TransactionPlanMidnightOfferRootIntent; + switch (signatureRequest.action.type) { + case "permit": + intent = { + type: "tokenApproval", + method: "permit", + token: signatureRequest.action.args.token, + spender: signatureRequest.action.args.spender, + amount: signatureRequest.action.args.amount, + deadline: signatureRequest.action.args.deadline, + chainId: signatureRequest.action.args.chainId, + }; + break; + case "permit2": + intent = { + type: "tokenApproval", + method: "permit2", + token: signatureRequest.action.args.token, + spender: signatureRequest.action.args.spender, + amount: signatureRequest.action.args.amount, + deadline: signatureRequest.action.args.deadline, + expiration: signatureRequest.action.args.expiration, + chainId: signatureRequest.action.args.chainId, + }; + break; + case "authorization": + intent = { + type: "operatorAuthorization", + method: "signature", + operator: signatureRequest.action.args.authorized, + isAuthorized: signatureRequest.action.args.isAuthorized, + deadline: signatureRequest.action.args.deadline, + chainId: signatureRequest.action.args.chainId, + }; + break; + case "midnightOfferRootSignature": + intent = { + type: "midnightOfferRootSignature", + root: signatureRequest.action.args.root, + ratifier: signatureRequest.action.args.ratifier, + offers: signatureRequest.action.args.offers, + }; + break; + } + return { + kind: "signature", + id, + request: signatureRequest, + action: signatureRequest.action, + intent, + sign: (client, userAddress) => + signatureRequest.sign(client, userAddress), + } satisfies TransactionPlanSignatureRequest; + }); + const primaryTx = + this.handler.previewPrimaryCall === false + ? undefined + : ( + this.handler.previewPrimaryCall ?? + (() => this.handler.buildPrimaryCall()) + )(); + const primaryCall = + primaryTx == null + ? undefined + : ({ + kind: "call", + id: "primary", + phase: "primary", + tx: primaryTx, + action: primaryTx.action, + intent: { + type: "primaryTransaction", + actionType: primaryTx.action.type, + }, + call: { + to: primaryTx.to, + value: primaryTx.value, + data: primaryTx.data, + }, + } satisfies TransactionPlanCallRequest); + + return new PreparedTransactionPlan({ + buildPrimaryCall: this.handler.buildPrimaryCall, + requestSteps, + primaryCall, + }); + } + + /** + * Creates a lazy TransactionPlan from SDK handler callbacks. + * + * @param handler - SDK handler callbacks used by `prepare()` and `PreparedTransactionPlan.build(...)`. + * @returns A TransactionPlan exposing `prepare()` as the public execution entry point. + */ + static create< + TCreatedPrimaryAction extends TransactionAction, + TCreatedRequestOptions = unknown, + TCreatedRequest extends TransactionPlanRequest = TransactionPlanRequest, + TCreatedSignatures = readonly RequirementSignature[], + >( + handler: TransactionPlanHandler< + TCreatedPrimaryAction, + TCreatedRequestOptions, + TCreatedRequest, + TCreatedSignatures + >, + ): TransactionPlan< + TCreatedPrimaryAction, + TCreatedRequestOptions, + TCreatedRequest, + TCreatedSignatures + > { + return new TransactionPlan(handler); + } +} + +/** Prepared transaction plan with resolved requests and app-facing helpers. */ +export class PreparedTransactionPlan< + TPrimaryAction extends TransactionAction = TransactionAction, + TSignatures = readonly RequirementSignature[], +> implements PreparedTransactionPlanShape +{ + private readonly buildPrimaryCall: TransactionPlanBuildPrimaryCall< + TPrimaryAction, + TSignatures + >; + + /** Ordered signable requests to present to the user. */ + readonly signatureRequests: readonly TransactionPlanSignatureRequest[]; + + /** Ordered viem-compatible calls. Includes the primary action call last when previewable. */ + readonly callRequests: readonly TransactionPlanCallRequest[]; + + /** All signature requests and call requests in review order. */ + readonly steps: readonly TransactionPlanStep[]; + + /** + * Creates a prepared plan from resolved request steps. + * + * @param params - Build callback, resolved requests, and optional preview primary call. + */ + constructor(params: { + readonly buildPrimaryCall: TransactionPlanBuildPrimaryCall< + TPrimaryAction, + TSignatures + >; + readonly requestSteps: readonly ( + | TransactionPlanSignatureRequest + | TransactionPlanCallRequest + )[]; + readonly primaryCall?: TransactionPlanCallRequest; + }) { + this.buildPrimaryCall = params.buildPrimaryCall; + this.signatureRequests = Object.freeze( + params.requestSteps.filter( + (request): request is TransactionPlanSignatureRequest => + request.kind === "signature", + ), + ); + const requirementCallRequests = params.requestSteps.filter( + (request): request is TransactionPlanCallRequest => + request.kind === "call", + ); + const primaryCall = params.primaryCall; + this.callRequests = Object.freeze( + primaryCall == null + ? [...requirementCallRequests] + : [...requirementCallRequests, primaryCall], + ); + this.steps = Object.freeze( + primaryCall == null + ? [...params.requestSteps] + : [...params.requestSteps, primaryCall], + ); + } + + /** Number of signature requests plus call requests. */ + get requestCount(): number { + return this.signatureRequests.length + this.callRequests.length; + } + + /** Whether the plan requires at least one signature prompt. */ + get hasSignatureRequests(): boolean { + return this.signatureRequests.length > 0; + } + + /** Whether the plan has at least one viem-compatible call request available now. */ + get hasCallRequests(): boolean { + return this.callRequests.length > 0; + } + + /** High-level flow kind derived from the request mix. */ + get flowKind(): TransactionPlanFlowKind { + const signatureRequests = this.signatureRequests.length; + const preparationCalls = this.callRequests.filter( + (request) => request.phase === "preparation", + ).length; + const primaryCalls = this.callRequests.filter( + (request) => request.phase === "primary", + ).length; + + if (signatureRequests > 0 && this.callRequests.length > 0) { + return "mixed_requests"; + } + if (signatureRequests > 0) return "signature_requests"; + if (preparationCalls === 0 && primaryCalls <= 1) return "single_call"; + return "call_requests"; + } + + /** Returns true when at least one request carries the requested intent type. */ + hasIntent(type: TType): boolean { + return this.steps.some((request) => request.intent.type === type); + } + + /** Returns all requests carrying the requested semantic intent type. */ + findIntent( + type: TType, + ): readonly TransactionPlanStepForIntent[] { + return this.steps.filter( + (request): request is TransactionPlanStepForIntent => + request.intent.type === type, + ); + } + + /** Signs every signature request sequentially and returns the produced signatures. */ + async signAll( + client: WalletClient, + userAddress: Address, + ): Promise { + const signatures: RequirementSignature[] = []; + for (const request of this.signatureRequests) { + signatures.push(await request.sign(client, userAddress)); + } + return Object.freeze(signatures); + } + + /** Builds an executable transaction plan from already-collected signatures. */ + build(signatures?: TSignatures): ExecutableTransactionPlan { + const expected = this.signatureRequests.length; + const received = + signatures == null + ? 0 + : Array.isArray(signatures) + ? signatures.length + : 1; + if (received < expected) { + throw new MissingTransactionPlanSignaturesError(expected, received); + } + const requirementCallRequests = this.callRequests.filter( + (request) => request.phase === "preparation", + ); + const primaryTx = this.buildPrimaryCall(signatures); + const primaryCall = { + kind: "call", + id: "primary", + phase: "primary", + tx: primaryTx, + action: primaryTx.action, + intent: { type: "primaryTransaction", actionType: primaryTx.action.type }, + call: { to: primaryTx.to, value: primaryTx.value, data: primaryTx.data }, + } satisfies TransactionPlanCallRequest; + return new ExecutableTransactionPlan({ + signatureRequests: Object.freeze([...this.signatureRequests]), + callRequests: Object.freeze([...requirementCallRequests, primaryCall]), + }); + } +} + +/** Executable transaction plan built from a PreparedTransactionPlan after signature collection. */ +export class ExecutableTransactionPlan + implements ExecutableTransactionPlanShape +{ + /** Ordered signable requests used to produce the signatures passed to `PreparedTransactionPlan.build(...)`. */ + readonly signatureRequests: readonly TransactionPlanSignatureRequest[]; + + /** Ordered viem-compatible calls to submit, with the primary action call last. */ + readonly callRequests: readonly TransactionPlanCallRequest[]; + + /** + * Creates an executable plan from built transaction requests. + * + * @param params - Executable plan shape. + */ + constructor(params: ExecutableTransactionPlanShape) { + this.signatureRequests = Object.freeze([...params.signatureRequests]); + this.callRequests = Object.freeze([...params.callRequests]); + } + + /** Ordered viem-compatible calls for transaction submission. */ + get viemCalls(): readonly TransactionPlanViemCall[] { + return this.callRequests.map((request) => request.call); + } +} diff --git a/packages/morpho-sdk/src/transactionPlan/index.ts b/packages/morpho-sdk/src/transactionPlan/index.ts new file mode 100644 index 000000000..aa6afc58b --- /dev/null +++ b/packages/morpho-sdk/src/transactionPlan/index.ts @@ -0,0 +1,6 @@ +export { + ExecutableTransactionPlan, + PreparedTransactionPlan, + TransactionPlan, +} from "./TransactionPlan.js"; +export type * from "./types.js"; diff --git a/packages/morpho-sdk/src/transactionPlan/types.ts b/packages/morpho-sdk/src/transactionPlan/types.ts new file mode 100644 index 000000000..048e04707 --- /dev/null +++ b/packages/morpho-sdk/src/transactionPlan/types.ts @@ -0,0 +1,254 @@ +import type { Address, Hex, WalletClient } from "viem"; +import type { + AuthorizationRequirementSignature, + BaseAction, + BlueAuthorizationAction, + CallRequirement, + ERC20ApprovalAction, + PermitRequirementSignature, + Requirement, + RequirementSignature, + SignatureRequirement, + Transaction, + TransactionAction, +} from "../types/action.js"; + +/** Options forwarded to token-approval request discovery. */ +export interface TransactionPlanSimplePermitOptions { + /** + * Prefer the ERC-2612 simple-permit path when the SDK detects support. + * Leave unset or set to `false` to force the Permit2/classic approval fallback when + * a token is known to be incompatible despite passing the SDK's shallow nonce probe. + */ + readonly useSimplePermit?: boolean; +} + +/** Token approval request: on-chain approval or off-chain permit signature. */ +export type TransactionPlanTokenRequest = + | Readonly> + | Requirement; + +/** Operator authorization request: on-chain authorization or off-chain signature. */ +export type TransactionPlanOperatorAuthorizationRequest = + | Readonly> + | Requirement; + +/** Raw viem-compatible call data extracted from a Morpho SDK transaction. */ +export interface TransactionPlanViemCall { + /** Contract that receives the call. */ + readonly to: Address; + /** Encoded calldata. */ + readonly data: Hex; + /** Native value attached to the call. */ + readonly value: bigint; +} + +/** On-chain or signable request discovered while preparing a transaction plan. */ +export type TransactionPlanRequest = CallRequirement | SignatureRequirement; + +/** Builds the primary transaction call, optionally consuming previously signed requests. */ +export type TransactionPlanBuildPrimaryCall< + TPrimaryAction extends TransactionAction = TransactionAction, + TSignatures = readonly RequirementSignature[], +> = (signatures?: TSignatures) => Readonly>; + +/** Resolves requirement calls and signature requests before the primary transaction call. */ +export type TransactionPlanGetRequirementRequests< + TRequestOptions = unknown, + TRequest extends TransactionPlanRequest = TransactionPlanRequest, +> = (options?: TRequestOptions) => Promise; + +/** Handler shape used internally by Morpho SDK entity methods such as `vault.deposit(...)`. */ +export interface TransactionPlanHandler< + TPrimaryAction extends TransactionAction = TransactionAction, + TRequestOptions = unknown, + TRequest extends TransactionPlanRequest = TransactionPlanRequest, + TSignatures = readonly RequirementSignature[], +> { + /** Builds the primary transaction call, optionally consuming previously signed requests. */ + readonly buildPrimaryCall: TransactionPlanBuildPrimaryCall< + TPrimaryAction, + TSignatures + >; + /** + * Controls whether `prepare()` can include an unsigned preview of the primary call. + * + * Leave unset for normal flows where the primary call can be encoded without signatures. Set to + * `false` for signature-dependent flows such as Midnight maker ECDSA offers, where the final + * mempool payload is produced by the offer-root signature and can only be encoded inside + * `prepared.build(signatures)`. + */ + readonly previewPrimaryCall?: + | false + | (() => Readonly>); + /** Resolves requirement calls and signature requests before the primary transaction call. */ + readonly getRequirementRequests?: TransactionPlanGetRequirementRequests< + TRequestOptions, + TRequest + >; +} + +/** Options for preparing a TransactionPlan. */ +export interface TransactionPlanPrepareOptions { + /** Options forwarded to request discovery, for example `{ useSimplePermit }`. */ + readonly requestOptions?: TRequestOptions; +} + +/** Token approval intent shared by approval transactions, ERC-2612 permits, and Permit2 signatures. */ +export interface TransactionPlanTokenApprovalIntent { + /** Intent discriminator for filtering and app-specific rendering. */ + readonly type: "tokenApproval"; + /** Approval mechanism: on-chain approval tx, ERC-2612 permit, or Permit2 signature. */ + readonly method: "tx" | "permit" | "permit2"; + /** Token being approved when known from SDK-generated metadata. */ + readonly token?: Address; + /** Spender receiving the approval. */ + readonly spender: Address; + /** Amount approved or signed. */ + readonly amount: bigint; + /** Signature deadline when the intent is signature-backed. */ + readonly deadline?: bigint; + /** Permit2 allowance expiration when the intent is Permit2-backed. */ + readonly expiration?: bigint; + /** Chain id when the intent is signature-backed and produced by the SDK. */ + readonly chainId?: number; +} + +/** Operator authorization intent shared by on-chain authorization calls and signatures. */ +export interface TransactionPlanOperatorAuthorizationIntent { + /** Intent discriminator for filtering and app-specific rendering. */ + readonly type: "operatorAuthorization"; + /** Authorization mechanism: on-chain transaction or EIP-712 signature. */ + readonly method: "tx" | "signature"; + /** Operator being authorized. */ + readonly operator: Address; + /** Whether the operator is granted or revoked. */ + readonly isAuthorized: boolean; + /** Account authorizing the operator when the action metadata carries it. */ + readonly owner?: Address; + /** Signature deadline when the intent is signature-backed. */ + readonly deadline?: bigint; + /** Chain id when the intent is signature-backed and produced by the SDK. */ + readonly chainId?: number; +} + +/** Midnight offer-root signature intent used by maker action flows. */ +export interface TransactionPlanMidnightOfferRootIntent { + /** Intent discriminator for filtering and app-specific rendering. */ + readonly type: "midnightOfferRootSignature"; + /** Merkle root being ratified. */ + readonly root: Hex; + /** Ratifier contract that verifies the signature. */ + readonly ratifier: Address; + /** Number of offers covered by the root. */ + readonly offers: number; +} + +/** Generic on-chain transaction intent for neutral requirement calls. */ +export interface TransactionPlanContractCallIntent< + TAction extends TransactionAction = TransactionAction, +> { + /** Intent discriminator for filtering and app-specific rendering. */ + readonly type: "contractCall"; + /** SDK action type attached to the call metadata. */ + readonly actionType: TAction["type"]; +} + +/** Primary Morpho SDK transaction intent requested by the user action. */ +export interface TransactionPlanPrimaryTransactionIntent< + TAction extends TransactionAction = TransactionAction, +> { + /** Intent discriminator for filtering and app-specific rendering. */ + readonly type: "primaryTransaction"; + /** Primary SDK action type, for example `vaultV2Deposit` or `blueRepayWithdrawCollateral`. */ + readonly actionType: TAction["type"]; +} + +/** Semantic intent attached to every transaction-plan request. */ +export type TransactionPlanIntent< + TPrimaryAction extends TransactionAction = TransactionAction, +> = + | TransactionPlanTokenApprovalIntent + | TransactionPlanOperatorAuthorizationIntent + | TransactionPlanMidnightOfferRootIntent + | TransactionPlanContractCallIntent + | TransactionPlanPrimaryTransactionIntent; + +/** Flow shape derived from a plan's signature and call request mix. */ +export type TransactionPlanFlowKind = + | "single_call" + | "call_requests" + | "signature_requests" + | "mixed_requests"; + +/** A signable request in a transaction plan. */ +export interface TransactionPlanSignatureRequest { + /** Request kind discriminator. */ + readonly kind: "signature"; + /** Stable id based on original request order, e.g. `request-1`. */ + readonly id: string; + /** Original SDK signable descriptor. */ + readonly request: SignatureRequirement; + /** Exact SDK/protocol action metadata used to encode this signature request. */ + readonly action: SignatureRequirement["action"]; + /** Higher-level plan category for app labels, filtering, and analytics. */ + readonly intent: + | TransactionPlanTokenApprovalIntent + | TransactionPlanOperatorAuthorizationIntent + | TransactionPlanMidnightOfferRootIntent; + /** Signs the request with the provided wallet client and user address. */ + readonly sign: ( + client: WalletClient, + userAddress: Address, + ) => Promise; +} + +/** A viem-compatible call request in a transaction plan. */ +export interface TransactionPlanCallRequest< + TAction extends BaseAction = TransactionAction, +> { + /** Request kind discriminator. */ + readonly kind: "call"; + /** Stable id based on original request order, or `primary` for the requested action call. */ + readonly id: string; + /** Whether this call supports the flow or executes the requested primary action. */ + readonly phase: "preparation" | "primary"; + /** Original SDK transaction with Morpho action metadata. */ + readonly tx: Readonly>; + /** Exact SDK/protocol action metadata used to encode this call. */ + readonly action: TAction; + /** Higher-level plan category for app labels, filtering, and analytics. */ + readonly intent: TransactionPlanIntent>; + /** viem-compatible call extracted from the transaction. */ + readonly call: TransactionPlanViemCall; +} + +/** A request in a transaction plan: either a signature prompt or a transaction call. */ +export type TransactionPlanStep = + | TransactionPlanSignatureRequest + | TransactionPlanCallRequest; + +/** Narrows transaction-plan steps by semantic intent type. */ +export type TransactionPlanStepForIntent< + TType extends TransactionPlanIntent["type"], +> = TransactionPlanStep & { + readonly intent: Extract; +}; + +/** Prepared transaction plan shape consumed by app review/execution code. */ +export interface PreparedTransactionPlanShape { + /** Ordered signable requests to present to the user. */ + readonly signatureRequests: readonly TransactionPlanSignatureRequest[]; + /** Ordered viem-compatible calls. Includes the primary action call last when previewable. */ + readonly callRequests: readonly TransactionPlanCallRequest[]; + /** All signature requests and call requests in review order. */ + readonly steps: readonly TransactionPlanStep[]; +} + +/** Built plan ready for transaction submission after signatures have been collected. */ +export interface ExecutableTransactionPlanShape { + /** Ordered signable requests used to produce the signatures passed to `build(...)`. */ + readonly signatureRequests: readonly TransactionPlanSignatureRequest[]; + /** Ordered viem-compatible calls to submit, with the primary action call last. */ + readonly callRequests: readonly TransactionPlanCallRequest[]; +} diff --git a/packages/morpho-sdk/src/types/action.ts b/packages/morpho-sdk/src/types/action.ts index 41cb8f0a3..d8538e4b3 100644 --- a/packages/morpho-sdk/src/types/action.ts +++ b/packages/morpho-sdk/src/types/action.ts @@ -542,13 +542,26 @@ export interface MidnightOfferRootSignatureArgs { export interface PermitAction extends BaseAction< "permit", - { spender: Address; amount: bigint; deadline: bigint } + { + token?: Address; + spender: Address; + amount: bigint; + deadline: bigint; + chainId?: number; + } > {} export interface Permit2Action extends BaseAction< "permit2", - { spender: Address; amount: bigint; deadline: bigint; expiration: bigint } + { + token?: Address; + spender: Address; + amount: bigint; + deadline: bigint; + expiration: bigint; + chainId?: number; + } > {} /** @@ -558,7 +571,12 @@ export interface Permit2Action export interface AuthorizationAction extends BaseAction< "authorization", - { authorized: Address; isAuthorized: boolean; deadline: bigint } + { + authorized: Address; + isAuthorized: boolean; + deadline: bigint; + chainId?: number; + } > {} /** Metadata for a Midnight offer-root signature request. */ @@ -701,30 +719,6 @@ export type CallRequirement = Readonly>; /** Onchain call or signature prerequisite returned by an entity action output. */ export type ActionRequirement = CallRequirement | SignatureRequirement; -/** Optional controls used while resolving action prerequisites. */ -interface ActionRequirementsParams { - /** - * Prefer the ERC-2612 simple-permit path when the SDK detects support. - * Leave unset or set to `false` to force the Permit2/classic approval fallback when - * a token is known to be incompatible despite passing the SDK's shallow nonce probe. - */ - readonly useSimplePermit?: boolean; -} - -/** Lazy entity result exposing prerequisite resolution and synchronous transaction building. */ -export interface ActionOutput< - TAction extends BaseAction = TransactionAction, - TSignatures = RequirementSignature, - TRequirementsParams = ActionRequirementsParams, -> { - readonly buildTx: ( - signatures?: TSignatures, - ) => Readonly>; - readonly getRequirements: ( - params?: TRequirementsParams, - ) => Promise; -} - export function isRequirementApproval( requirement: unknown, ): requirement is Transaction { diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index df615be97..263382137 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -130,6 +130,30 @@ export class UnexpectedRequirementSignatureError extends Error { } } +/** + * Thrown when a prepared transaction plan is built without all requested signatures. + * + * @example + * ```ts + * import { MissingTransactionPlanSignaturesError } from "@morpho-org/morpho-sdk"; + * + * if (error instanceof MissingTransactionPlanSignaturesError) { + * // Collect every `prepared.signatureRequests` signature before calling build(). + * } + * ``` + */ +export class MissingTransactionPlanSignaturesError extends Error { + /** + * @param expected - Number of signature requests in the prepared plan. + * @param received - Number of signatures passed to `build`. + */ + constructor(expected: number, received: number) { + super( + `Expected ${expected} transaction-plan signatures but received ${received}. Sign all prepared signature requests before building the executable transaction plan.`, + ); + } +} + /** Thrown when an asset amount is required to be positive but is zero or negative. */ export class NonPositiveAssetAmountError extends Error { constructor(origin: Address) { diff --git a/packages/morpho-sdk/test/actions/blue/authorization.test.ts b/packages/morpho-sdk/test/actions/blue/authorization.test.ts index afd2eeff2..f5c4dbe29 100644 --- a/packages/morpho-sdk/test/actions/blue/authorization.test.ts +++ b/packages/morpho-sdk/test/actions/blue/authorization.test.ts @@ -9,6 +9,7 @@ import { import { WethUsdsBlue } from "../../fixtures/blue.js"; import { supplyCollateral } from "../../helpers/blue.js"; import { test } from "../../setup.js"; +import { getPlanRequests } from "../../transactionPlanUtils.js"; describe("AuthorizationBlue", () => { describe("authorization requirements", () => { @@ -31,13 +32,13 @@ describe("AuthorizationBlue", () => { const market = morphoClient.blue(WethUsdsBlue, mainnet.id); const positionData = await market.getPositionData(client.account.address); - const requirements = await market - .borrow({ + const requirements = await getPlanRequests( + market.borrow({ userAddress: client.account.address, amount: parseUnits("100", 18), positionData, - }) - .getRequirements(); + }), + ); expect(requirements).toHaveLength(1); const authTx = requirements[0]!; @@ -64,22 +65,22 @@ describe("AuthorizationBlue", () => { const market = morphoClient.blue(WethUsdsBlue, mainnet.id); const positionData = await market.getPositionData(client.account.address); - const borrowRequirements = await market - .borrow({ + const borrowRequirements = await getPlanRequests( + market.borrow({ userAddress: client.account.address, amount: parseUnits("100", 18), positionData, - }) - .getRequirements(); + }), + ); - const scbRequirements = await market - .supplyCollateralBorrow({ + const scbRequirements = await getPlanRequests( + market.supplyCollateralBorrow({ userAddress: client.account.address, nativeAmount: parseUnits("5", 18), borrowAmount: parseUnits("100", 18), positionData, - }) - .getRequirements(); + }), + ); expect(borrowRequirements).toStrictEqual(scbRequirements); @@ -101,13 +102,13 @@ describe("AuthorizationBlue", () => { const market = morphoClient.blue(WethUsdsBlue, mainnet.id); const positionData = await market.getPositionData(client.account.address); - const requirementsBefore = await market - .borrow({ + const requirementsBefore = await getPlanRequests( + market.borrow({ userAddress: client.account.address, amount: parseUnits("100", 18), positionData, - }) - .getRequirements(); + }), + ); const requirementAuthorization = requirementsBefore[0]; if (!isRequirementBlueAuthorization(requirementAuthorization)) { @@ -115,13 +116,13 @@ describe("AuthorizationBlue", () => { } await client.sendTransaction(requirementAuthorization); - const requirementsAfter = await market - .borrow({ + const requirementsAfter = await getPlanRequests( + market.borrow({ userAddress: client.account.address, amount: parseUnits("100", 18), positionData, - }) - .getRequirements(); + }), + ); expect(requirementsAfter).toHaveLength(0); }); diff --git a/packages/morpho-sdk/test/actions/blue/borrow.test.ts b/packages/morpho-sdk/test/actions/blue/borrow.test.ts index 071dd224b..0367bca58 100644 --- a/packages/morpho-sdk/test/actions/blue/borrow.test.ts +++ b/packages/morpho-sdk/test/actions/blue/borrow.test.ts @@ -21,6 +21,7 @@ import { WethUsdsBlue } from "../../fixtures/blue.js"; import { supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("BorrowBlue", () => { test("should compute minSharePrice from real market borrow state", async ({ @@ -44,13 +45,13 @@ describe("BorrowBlue", () => { const { totalBorrowAssets, totalBorrowShares } = positionData.market; - const tx = market - .borrow({ + const tx = await buildPlanTx( + market.borrow({ userAddress: client.account.address, amount: parseUnits("100", 18), positionData, - }) - .buildTx(); + }), + ); const expectedMinSharePrice = MathLib.mulDivDown( totalBorrowAssets + SharesMath.VIRTUAL_ASSETS, @@ -95,7 +96,7 @@ describe("BorrowBlue", () => { positionData, }); - const requirements = await borrow.getRequirements(); + const requirements = await getPlanRequests(borrow); const requirementAuthorization = requirements[0]; if (!isRequirementBlueAuthorization(requirementAuthorization)) { throw new Error("Authorization requirement not found"); @@ -103,7 +104,7 @@ describe("BorrowBlue", () => { await client.sendTransaction(requirementAuthorization); - const tx = borrow.buildTx(); + const tx = await buildPlanTx(borrow); await client.sendTransaction(tx); }, @@ -170,7 +171,7 @@ describe("BorrowBlue", () => { // With supportSignature the authorization requirement is signable — // no standalone setAuthorization transaction is sent. - const requirements = await borrow.getRequirements(); + const requirements = await getPlanRequests(borrow); const requirement = requirements[0]; if (!isRequirementSignature(requirement)) { throw new Error("Expected a signable authorization requirement"); @@ -197,7 +198,7 @@ describe("BorrowBlue", () => { }), ).toBe(false); - const tx = borrow.buildTx([authorizationSignature]); + const tx = await buildPlanTx(borrow, [authorizationSignature]); await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts index bd0428716..e221be962 100644 --- a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts +++ b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts @@ -26,6 +26,7 @@ import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; /** PublicAllocator admin for the Steakhouse vault at the fork block. */ const PA_ADMIN: Address = "0x9E9110cFd24cd851ea5bc73a27975B33E308f9e1"; @@ -79,14 +80,14 @@ describe("Borrow with single vault reallocation (e2e)", () => { reallocations, }); - const requirements = await borrow.getRequirements(); + const requirements = await getPlanRequests(borrow); const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = borrow.buildTx(); + const tx = await buildPlanTx(borrow); expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); @@ -164,14 +165,14 @@ describe("Borrow with multiple source market withdrawals", () => { reallocations, }); - const requirements = await borrow.getRequirements(); + const requirements = await getPlanRequests(borrow); const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = borrow.buildTx(); + const tx = await buildPlanTx(borrow); expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -265,14 +266,14 @@ describe("Borrow with reallocation fee", () => { reallocations, }); - const requirements = await borrow.getRequirements(); + const requirements = await getPlanRequests(borrow); const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = borrow.buildTx(); + const tx = await buildPlanTx(borrow); expect(tx.value).toBe(reallocationFee); expect(tx.action.args.reallocationFee).toBe(reallocationFee); @@ -355,7 +356,7 @@ describe("SupplyCollateralBorrow with single vault reallocation", () => { reallocations, }); - const requirements = await scb.getRequirements(); + const requirements = await getPlanRequests(scb); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -368,7 +369,7 @@ describe("SupplyCollateralBorrow with single vault reallocation", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = scb.buildTx(); + const tx = await buildPlanTx(scb); expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); @@ -449,7 +450,7 @@ describe("SupplyCollateralBorrow with multiple source market withdrawals", () => reallocations, }); - const requirements = await scb.getRequirements(); + const requirements = await getPlanRequests(scb); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -462,7 +463,7 @@ describe("SupplyCollateralBorrow with multiple source market withdrawals", () => await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = scb.buildTx(); + const tx = await buildPlanTx(scb); expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -562,7 +563,7 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { reallocations, }); - const requirements = await scb.getRequirements(); + const requirements = await getPlanRequests(scb); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -575,7 +576,7 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = scb.buildTx(); + const tx = await buildPlanTx(scb); expect(tx.value).toBe(reallocationFee); expect(tx.action.args.reallocationFee).toBe(reallocationFee); @@ -692,14 +693,14 @@ describe("getReallocationData and getReallocations", () => { reallocations, }); - const requirements = await borrow.getRequirements(); + const requirements = await getPlanRequests(borrow); const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = borrow.buildTx(); + const tx = await buildPlanTx(borrow); await client.sendTransaction(tx); }, }); @@ -762,7 +763,7 @@ describe("getReallocationData and getReallocations", () => { reallocations, }); - const requirements = await scb.getRequirements(); + const requirements = await getPlanRequests(scb); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -775,7 +776,7 @@ describe("getReallocationData and getReallocations", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = scb.buildTx(); + const tx = await buildPlanTx(scb); await client.sendTransaction(tx); }, }); @@ -818,7 +819,7 @@ describe("getReallocationData and getReallocations", () => { describe("Reallocation validation errors", () => { describe("blueBorrow", () => { - test("should throw NegativeReallocationFeeError for negative fee", () => { + test("should throw NegativeReallocationFeeError for negative fee", async () => { expect(() => blueBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, @@ -843,7 +844,7 @@ describe("Reallocation validation errors", () => { ).toThrow(NegativeReallocationFeeError); }); - test("should throw EmptyReallocationWithdrawalsError for empty withdrawals", () => { + test("should throw EmptyReallocationWithdrawalsError for empty withdrawals", async () => { expect(() => blueBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, @@ -863,7 +864,7 @@ describe("Reallocation validation errors", () => { ).toThrow(EmptyReallocationWithdrawalsError); }); - test("should throw NonPositiveReallocationAmountError for zero withdrawal amount", () => { + test("should throw NonPositiveReallocationAmountError for zero withdrawal amount", async () => { expect(() => blueBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, @@ -885,7 +886,7 @@ describe("Reallocation validation errors", () => { ).toThrow(NonPositiveReallocationAmountError); }); - test("should throw NonPositiveReallocationAmountError for negative withdrawal amount", () => { + test("should throw NonPositiveReallocationAmountError for negative withdrawal amount", async () => { expect(() => blueBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, @@ -907,7 +908,7 @@ describe("Reallocation validation errors", () => { ).toThrow(NonPositiveReallocationAmountError); }); - test("should throw ReallocationWithdrawalOnTargetMarketError when withdrawal includes a market with borrow", () => { + test("should throw ReallocationWithdrawalOnTargetMarketError when withdrawal includes a market with borrow", async () => { expect(() => blueBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, @@ -934,7 +935,7 @@ describe("Reallocation validation errors", () => { }); describe("blueSupplyCollateralBorrow", () => { - test("should throw NegativeReallocationFeeError for negative fee", () => { + test("should throw NegativeReallocationFeeError for negative fee", async () => { expect(() => blueSupplyCollateralBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, @@ -961,7 +962,7 @@ describe("Reallocation validation errors", () => { ).toThrow(NegativeReallocationFeeError); }); - test("should throw EmptyReallocationWithdrawalsError for empty withdrawals", () => { + test("should throw EmptyReallocationWithdrawalsError for empty withdrawals", async () => { expect(() => blueSupplyCollateralBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, @@ -983,7 +984,7 @@ describe("Reallocation validation errors", () => { ).toThrow(EmptyReallocationWithdrawalsError); }); - test("should throw NonPositiveReallocationAmountError for zero withdrawal amount", () => { + test("should throw NonPositiveReallocationAmountError for zero withdrawal amount", async () => { expect(() => blueSupplyCollateralBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, @@ -1007,7 +1008,7 @@ describe("Reallocation validation errors", () => { ).toThrow(NonPositiveReallocationAmountError); }); - test("should throw NonPositiveReallocationAmountError for negative withdrawal amount", () => { + test("should throw NonPositiveReallocationAmountError for negative withdrawal amount", async () => { expect(() => blueSupplyCollateralBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, @@ -1031,7 +1032,7 @@ describe("Reallocation validation errors", () => { ).toThrow(NonPositiveReallocationAmountError); }); - test("should throw ReallocationWithdrawalOnTargetMarketError when withdrawal includes a market with borrow", () => { + test("should throw ReallocationWithdrawalOnTargetMarketError when withdrawal includes a market with borrow", async () => { expect(() => blueSupplyCollateralBorrow({ market: { chainId: mainnet.id, marketParams: CbbtcUsdcBlue }, diff --git a/packages/morpho-sdk/test/actions/blue/refinance.test.ts b/packages/morpho-sdk/test/actions/blue/refinance.test.ts index 8e9adb818..d9c0183ff 100644 --- a/packages/morpho-sdk/test/actions/blue/refinance.test.ts +++ b/packages/morpho-sdk/test/actions/blue/refinance.test.ts @@ -13,6 +13,7 @@ import { YearnUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { borrow, supplyCollateral, supplyLoan } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; // Two real wstETH/wNative markets sharing the loan + collateral pair but differing oracles, at the pinned fork. const wstEthWeth_v1 = new MarketParams({ @@ -95,7 +96,7 @@ describe("RefinanceBlue (fork)", () => { }); // Tolerate either a clean wallet (one auth requirement) or a pre-authorized one (none). - const requirements = await refi.getRequirements(); + const requirements = await getPlanRequests(refi); expect(requirements.length).toBeLessThanOrEqual(1); for (const requirement of requirements) { if (!isRequirementBlueAuthorization(requirement)) { @@ -104,7 +105,7 @@ describe("RefinanceBlue (fork)", () => { await client.sendTransaction(requirement); } - await client.sendTransaction(refi.buildTx()); + await client.sendTransaction(await buildPlanTx(refi)); }, }); // testInvariants already asserts every bundler3 component ends with 0 balance — nothing left behind. @@ -176,7 +177,7 @@ describe("RefinanceBlue (fork)", () => { slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const requirements = await refi.getRequirements(); + const requirements = await getPlanRequests(refi); expect(requirements.length).toBeLessThanOrEqual(1); for (const requirement of requirements) { if (!isRequirementBlueAuthorization(requirement)) { @@ -184,7 +185,7 @@ describe("RefinanceBlue (fork)", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(refi.buildTx()); + await client.sendTransaction(await buildPlanTx(refi)); }, }); @@ -240,7 +241,7 @@ describe("RefinanceBlue (fork)", () => { collateralAmount, }); - const requirements = await refi.getRequirements(); + const requirements = await getPlanRequests(refi); // Collat-only still needs GA1 authorized: the callback's withdrawCollateral runs onBehalf=user. expect(requirements).toHaveLength(1); const auth = requirements[0]!; @@ -248,7 +249,7 @@ describe("RefinanceBlue (fork)", () => { throw new Error("Authorization requirement not found"); } await client.sendTransaction(auth); - await client.sendTransaction(refi.buildTx()); + await client.sendTransaction(await buildPlanTx(refi)); }, }); @@ -340,7 +341,7 @@ describe("RefinanceBlue (fork)", () => { targetReallocations: reallocations, }); - const requirements = await refi.getRequirements(); + const requirements = await getPlanRequests(refi); expect(requirements.length).toBeLessThanOrEqual(1); for (const requirement of requirements) { if (!isRequirementBlueAuthorization(requirement)) { @@ -349,7 +350,7 @@ describe("RefinanceBlue (fork)", () => { await client.sendTransaction(requirement); } - const tx = refi.buildTx(); + const tx = await buildPlanTx(refi); expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); diff --git a/packages/morpho-sdk/test/actions/blue/repay.test.ts b/packages/morpho-sdk/test/actions/blue/repay.test.ts index 5f2b4669d..764ae39dd 100644 --- a/packages/morpho-sdk/test/actions/blue/repay.test.ts +++ b/packages/morpho-sdk/test/actions/blue/repay.test.ts @@ -19,6 +19,7 @@ import { WethUsdsBlue, WstethWethBlue } from "../../fixtures/blue.js"; import { borrow, supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("RepayBlue", () => { afterEach(() => { @@ -64,7 +65,7 @@ describe("RepayBlue", () => { positionData, }); - const requirements = await repay.getRequirements(); + const requirements = await getPlanRequests(repay); // Repay should NOT have morpho authorization requirement const approval = requirements[0]; @@ -73,7 +74,7 @@ describe("RepayBlue", () => { } await client.sendTransaction(approval); - const tx = repay.buildTx(); + const tx = await buildPlanTx(repay); await client.sendTransaction(tx); }, }); @@ -129,7 +130,7 @@ describe("RepayBlue", () => { positionData, }); - const requirements = await repay.getRequirements(); + const requirements = await getPlanRequests(repay); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -142,7 +143,7 @@ describe("RepayBlue", () => { }); await client.sendTransaction(approval); - const tx = repay.buildTx(); + const tx = await buildPlanTx(repay); await client.sendTransaction(tx); }, }); @@ -225,14 +226,14 @@ describe("RepayBlue", () => { positionData, }); - const requirements = await repay.getRequirements(); + const requirements = await getPlanRequests(repay); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); } await client.sendTransaction(approval); - const tx = repay.buildTx(); + const tx = await buildPlanTx(repay); await client.sendTransaction(tx); }, }); @@ -292,10 +293,10 @@ describe("RepayBlue", () => { }); // A fully-native repay pulls no ERC-20, so it needs no approval. - const requirements = await repay.getRequirements(); + const requirements = await getPlanRequests(repay); expect(requirements.length).toBe(0); - const tx = repay.buildTx(); + const tx = await buildPlanTx(repay); expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -374,7 +375,7 @@ describe("RepayBlue", () => { }); // The approval must cover ONLY the ERC-20 portion, not the wrapped native. - const requirements = await repay.getRequirements(); + const requirements = await getPlanRequests(repay); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -382,7 +383,7 @@ describe("RepayBlue", () => { expect(approval.action.args.amount).toEqual(erc20Part); await client.sendTransaction(approval); - const tx = repay.buildTx(); + const tx = await buildPlanTx(repay); // Only the native portion rides as tx.value. expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); @@ -461,7 +462,7 @@ describe("RepayBlue", () => { positionData, }); - const requirements = await repay.getRequirements(); + const requirements = await getPlanRequests(repay); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -475,7 +476,7 @@ describe("RepayBlue", () => { }); await client.sendTransaction(approval); - const tx = repay.buildTx(); + const tx = await buildPlanTx(repay); expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); }, @@ -551,10 +552,10 @@ describe("RepayBlue", () => { }); // Native covers the full debt ⇒ no ERC-20 approval/permit requirement. - const requirements = await repay.getRequirements(); + const requirements = await getPlanRequests(repay); expect(requirements).toEqual([]); - const tx = repay.buildTx(); + const tx = await buildPlanTx(repay); // Everything is wrapped native; no ERC-20 pulled. expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.transferAmount).toEqual(nativeAmount); diff --git a/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts b/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts index a476bcc72..4ef5b5d69 100644 --- a/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts +++ b/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts @@ -21,6 +21,7 @@ import { WethUsdsBlue, WstethWethBlue } from "../../fixtures/blue.js"; import { borrow, supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("RepayWithdrawCollateralBlue", () => { afterEach(() => { @@ -74,7 +75,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await action.getRequirements(); + const requirements = await getPlanRequests(action); const approval = requirements[0]; if (!isRequirementApproval(approval)) { @@ -82,7 +83,7 @@ describe("RepayWithdrawCollateralBlue", () => { } await client.sendTransaction(approval); - const tx = action.buildTx(); + const tx = await buildPlanTx(action); await client.sendTransaction(tx); }, }); @@ -144,7 +145,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await action.getRequirements(); + const requirements = await getPlanRequests(action); for (const req of requirements) { if (isRequirementApproval(req)) { // Shares-mode repayments use a forward-accrued transfer amount; @@ -159,7 +160,7 @@ describe("RepayWithdrawCollateralBlue", () => { } } - const tx = action.buildTx(); + const tx = await buildPlanTx(action); await client.sendTransaction(tx); }, }); @@ -241,7 +242,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await action.getRequirements(); + const requirements = await getPlanRequests(action); for (const req of requirements) { if (isRequirementApproval(req)) { // Shares-mode repayments use a forward-accrued transfer amount; @@ -256,7 +257,7 @@ describe("RepayWithdrawCollateralBlue", () => { } } - const tx = action.buildTx(); + const tx = await buildPlanTx(action); await client.sendTransaction(tx); }, }); @@ -320,7 +321,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await action.getRequirements(); + const requirements = await getPlanRequests(action); // A fully-native repay pulls no ERC-20, so there is no approval to make; // GeneralAdapter1 was already authorized when the position was opened. expect(requirements.some(isRequirementApproval)).toBe(false); @@ -330,7 +331,7 @@ describe("RepayWithdrawCollateralBlue", () => { } } - const tx = action.buildTx(); + const tx = await buildPlanTx(action); expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -414,7 +415,7 @@ describe("RepayWithdrawCollateralBlue", () => { // GA1 was authorized when the position opened, so the only requirement is // the ERC-20 approval — and it must cover ONLY the ERC-20 portion. - const requirements = await action.getRequirements(); + const requirements = await getPlanRequests(action); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -422,7 +423,7 @@ describe("RepayWithdrawCollateralBlue", () => { expect(approval.action.args.amount).toEqual(erc20Part); await client.sendTransaction(approval); - const tx = action.buildTx(); + const tx = await buildPlanTx(action); // Only the native portion rides as tx.value. expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); @@ -507,7 +508,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await action.getRequirements(); + const requirements = await getPlanRequests(action); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -521,7 +522,7 @@ describe("RepayWithdrawCollateralBlue", () => { }); await client.sendTransaction(approval); - const tx = action.buildTx(); + const tx = await buildPlanTx(action); expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/blue/supply.test.ts b/packages/morpho-sdk/test/actions/blue/supply.test.ts index 223077dbe..3e109d597 100644 --- a/packages/morpho-sdk/test/actions/blue/supply.test.ts +++ b/packages/morpho-sdk/test/actions/blue/supply.test.ts @@ -21,6 +21,7 @@ import { } from "../../fixtures/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("SupplyBlue", () => { test("should supply loan token end-to-end", async ({ client }) => { @@ -49,14 +50,14 @@ describe("SupplyBlue", () => { marketData, }); - const requirements = await supply.getRequirements(); + const requirements = await getPlanRequests(supply); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); } await client.sendTransaction(approval); - const tx = supply.buildTx(); + const tx = await buildPlanTx(supply); expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -90,7 +91,7 @@ describe("SupplyBlue", () => { marketData, }); - const requirements = await supply.getRequirements(); + const requirements = await getPlanRequests(supply); expect(requirements.length).toBeGreaterThan(0); const approval = requirements[0]; expect(approval).toBeDefined(); @@ -111,7 +112,7 @@ describe("SupplyBlue", () => { marketData, }); - const tx = supply.buildTx(); + const tx = await buildPlanTx(supply); // Sanity bound only — exact value depends on virtual-share scaling. expect(tx.action.args.maxSharePrice).toBeGreaterThan(0n); @@ -154,7 +155,7 @@ describe("SupplyBlue", () => { marketData, }); - const requirements = await supply.getRequirements(); + const requirements = await getPlanRequests(supply); expect(requirements.length).toBe(2); const approvalPermit2 = requirements[0]; @@ -178,7 +179,7 @@ describe("SupplyBlue", () => { ); expect(isHex(requirementSignature.args.signature)).toBe(true); - const tx = supply.buildTx([requirementSignature]); + const tx = await buildPlanTx(supply, [requirementSignature]); expect(tx.value).toBe(0n); await client.sendTransaction(tx); }, @@ -222,10 +223,10 @@ describe("SupplyBlue", () => { }); // No ERC20 approval needed: only native wrapping inside the bundle. - const requirements = await supply.getRequirements(); + const requirements = await getPlanRequests(supply); expect(requirements.length).toBe(0); - const tx = supply.buildTx(); + const tx = await buildPlanTx(supply); expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -274,14 +275,14 @@ describe("SupplyBlue", () => { marketData, }); - const requirements = await supply.getRequirements(); + const requirements = await getPlanRequests(supply); const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); } await client.sendTransaction(approval); - const tx = supply.buildTx(); + const tx = await buildPlanTx(supply); expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts b/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts index 5dc18cdba..2a7b713dd 100644 --- a/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts +++ b/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts @@ -18,6 +18,7 @@ import { } from "../../fixtures/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("SupplyCollateralBlue", () => { test("should supply 1 collateral with approval", async ({ client }) => { @@ -45,7 +46,7 @@ describe("SupplyCollateralBlue", () => { amount, }); - const requirements = await supplyCollateral.getRequirements(); + const requirements = await getPlanRequests(supplyCollateral); const approveTx = requirements[0]; if (!isRequirementApproval(approveTx)) { @@ -54,7 +55,7 @@ describe("SupplyCollateralBlue", () => { await client.sendTransaction(approveTx); - const tx = supplyCollateral.buildTx(); + const tx = await buildPlanTx(supplyCollateral); await client.sendTransaction(tx); }, }); @@ -106,10 +107,10 @@ describe("SupplyCollateralBlue", () => { amount, }); - const requirements = await supplyCollateral.getRequirements(); + const requirements = await getPlanRequests(supplyCollateral); expect(requirements.length).toBe(0); - const tx = supplyCollateral.buildTx(); + const tx = await buildPlanTx(supplyCollateral); await client.sendTransaction(tx); }, @@ -152,10 +153,10 @@ describe("SupplyCollateralBlue", () => { nativeAmount, }); - const requirements = await supplyCollateral.getRequirements(); + const requirements = await getPlanRequests(supplyCollateral); expect(requirements.length).toBe(0); - const tx = supplyCollateral.buildTx(); + const tx = await buildPlanTx(supplyCollateral); expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); @@ -209,7 +210,7 @@ describe("SupplyCollateralBlue", () => { nativeAmount, }); - const requirements = await supplyCollateral.getRequirements(); + const requirements = await getPlanRequests(supplyCollateral); expect(requirements.length).toBe(1); const approveTx = requirements[0]; @@ -219,7 +220,7 @@ describe("SupplyCollateralBlue", () => { await client.sendTransaction(approveTx); - const tx = supplyCollateral.buildTx(); + const tx = await buildPlanTx(supplyCollateral); expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); @@ -268,7 +269,7 @@ describe("SupplyCollateralBlue", () => { amount, }); - const requirements = await supplyCollateral.getRequirements({ + const requirements = await getPlanRequests(supplyCollateral, { useSimplePermit: true, }); expect(requirements.length).toBe(1); @@ -282,7 +283,7 @@ describe("SupplyCollateralBlue", () => { client.account.address, ); - const tx = supplyCollateral.buildTx([requirementSignature]); + const tx = await buildPlanTx(supplyCollateral, [requirementSignature]); await client.sendTransaction(tx); }, }); @@ -332,7 +333,7 @@ describe("SupplyCollateralBlue", () => { amount, }); - const requirements = await supplyCollateral.getRequirements(); + const requirements = await getPlanRequests(supplyCollateral); expect(requirements.length).toBe(2); const approvalPermit2 = requirements[0]; @@ -365,7 +366,7 @@ describe("SupplyCollateralBlue", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = supplyCollateral.buildTx([requirementSignature]); + const tx = await buildPlanTx(supplyCollateral, [requirementSignature]); await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts b/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts index ae5a8210b..84baa0978 100644 --- a/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts +++ b/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts @@ -18,6 +18,7 @@ import { UsdcEurcvBlue, WethUsdsBlue } from "../../fixtures/blue.js"; import { supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("SupplyCollateralBorrowBlue", () => { test("should supply collateral and borrow with ERC20 approval and authorization", async ({ @@ -49,7 +50,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await scb.getRequirements(); + const requirements = await getPlanRequests(scb); expect(requirements).toHaveLength(2); expect(requirements[0]!.action.type).toBe("erc20Approval"); expect(requirements[1]!.action.type).toBe("blueAuthorization"); @@ -66,7 +67,7 @@ describe("SupplyCollateralBorrowBlue", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = scb.buildTx(); + const tx = await buildPlanTx(scb); await client.sendTransaction(tx); }, @@ -124,7 +125,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await scb.getRequirements(); + const requirements = await getPlanRequests(scb); expect(requirements).toHaveLength(1); expect(requirements[0]!.action.type).toBe("blueAuthorization"); @@ -135,7 +136,7 @@ describe("SupplyCollateralBorrowBlue", () => { await client.sendTransaction(authorization); - const tx = scb.buildTx(); + const tx = await buildPlanTx(scb); expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -197,7 +198,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await scb.getRequirements(); + const requirements = await getPlanRequests(scb); expect(requirements).toHaveLength(2); expect(requirements[0]!.action.type).toBe("erc20Approval"); expect(requirements[1]!.action.type).toBe("blueAuthorization"); @@ -213,7 +214,7 @@ describe("SupplyCollateralBorrowBlue", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = scb.buildTx(); + const tx = await buildPlanTx(scb); expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -282,7 +283,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await scb.getRequirements(); + const requirements = await getPlanRequests(scb); expect(requirements).toHaveLength(1); const approval = requirements[0]; @@ -291,7 +292,7 @@ describe("SupplyCollateralBorrowBlue", () => { } await client.sendTransaction(approval); - const tx = scb.buildTx(); + const tx = await buildPlanTx(scb); await client.sendTransaction(tx); }, }); @@ -346,7 +347,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await scb.getRequirements({ + const requirements = await getPlanRequests(scb, { useSimplePermit: true, }); expect(requirements).toHaveLength(2); @@ -375,7 +376,10 @@ describe("SupplyCollateralBorrowBlue", () => { client.account.address, ); - const tx = scb.buildTx([permitSignature, authorizationSignature]); + const tx = await buildPlanTx(scb, [ + permitSignature, + authorizationSignature, + ]); await client.sendTransaction(tx); }, }); @@ -436,7 +440,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await scb.getRequirements(); + const requirements = await getPlanRequests(scb); // permit2 requirements: approve token -> permit2 + permit2 signature + morpho authorization expect(requirements).toHaveLength(3); @@ -472,7 +476,10 @@ describe("SupplyCollateralBorrowBlue", () => { client.account.address, ); - const tx = scb.buildTx([permit2Signature, authorizationSignature]); + const tx = await buildPlanTx(scb, [ + permit2Signature, + authorizationSignature, + ]); await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/withdraw.test.ts b/packages/morpho-sdk/test/actions/blue/withdraw.test.ts index 25316aa0e..d95a49013 100644 --- a/packages/morpho-sdk/test/actions/blue/withdraw.test.ts +++ b/packages/morpho-sdk/test/actions/blue/withdraw.test.ts @@ -21,6 +21,7 @@ import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { supplyLoan } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("WithdrawBlue", () => { test("should withdraw loan token end-to-end (assets mode)", async ({ @@ -56,14 +57,14 @@ describe("WithdrawBlue", () => { positionData, }); - const requirements = await withdraw.getRequirements(); + const requirements = await getPlanRequests(withdraw); const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -111,14 +112,14 @@ describe("WithdrawBlue", () => { positionData, }); - const requirements = await withdraw.getRequirements(); + const requirements = await getPlanRequests(withdraw); const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -156,7 +157,7 @@ describe("WithdrawBlue", () => { positionData, }); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); expect(tx.action.args.receiver).toBe(receiver); }); @@ -182,7 +183,7 @@ describe("WithdrawBlue", () => { positionData, }); - const requirements = await withdraw.getRequirements(); + const requirements = await getPlanRequests(withdraw); expect(requirements.length).toBe(1); expect(isRequirementBlueAuthorization(requirements[0])).toBe(true); }); @@ -236,14 +237,14 @@ describe("WithdrawBlue", () => { reallocations, }); - const requirements = await withdraw.getRequirements(); + const requirements = await getPlanRequests(withdraw); const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); @@ -307,14 +308,14 @@ describe("WithdrawBlue", () => { reallocations, }); - const requirements = await withdraw.getRequirements(); + const requirements = await getPlanRequests(withdraw); const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); expect(tx.value).toBe(0n); expect(tx.action.args.shares).toBe(withdrawShares); expect(tx.action.args.assets).toBe(0n); @@ -382,14 +383,14 @@ describe("WithdrawBlue", () => { reallocations, }); - const requirements = await withdraw.getRequirements(); + const requirements = await getPlanRequests(withdraw); const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); expect(tx.action.args.receiver).toBe(receiver); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts b/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts index dd5429dcd..ecb6ac6ae 100644 --- a/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts +++ b/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts @@ -13,6 +13,7 @@ import { WethUsdsBlue } from "../../fixtures/blue.js"; import { borrow, supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("WithdrawCollateralBlue", () => { test("should withdraw collateral (no debt)", async ({ client }) => { @@ -48,7 +49,7 @@ describe("WithdrawCollateralBlue", () => { positionData, }); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); await client.sendTransaction(tx); }, }); @@ -97,7 +98,7 @@ describe("WithdrawCollateralBlue", () => { positionData, }); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/metadata.test.ts b/packages/morpho-sdk/test/actions/metadata.test.ts index 2559003b6..ce1100a08 100644 --- a/packages/morpho-sdk/test/actions/metadata.test.ts +++ b/packages/morpho-sdk/test/actions/metadata.test.ts @@ -6,6 +6,7 @@ import { isRequirementApproval, morphoViemExtension } from "../../src/index.js"; import { KeyrockUsdcVaultV2 } from "../fixtures/vaultV2.js"; import { testInvariants } from "../helpers/invariants.js"; import { test } from "../setup.js"; +import { buildPlanTx, getPlanRequests } from "../transactionPlanUtils.js"; describe("Metadata", () => { test("should create deposit bundle with origin and timestamp metadata", async ({ @@ -43,7 +44,7 @@ describe("Metadata", () => { vaultData, }); - const tx_1 = deposit.buildTx(); + const tx_1 = await buildPlanTx(deposit); expect(tx_1.data).toContain("25AFEA44"); const position = tx_1.data.indexOf("25AFEA44"); expect(position).toBeGreaterThanOrEqual(8); @@ -54,7 +55,7 @@ describe("Metadata", () => { expect(typeof timestampNumber).toBe("number"); expect(timestampNumber).toBeLessThanOrEqual(Number(Time.timestamp())); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); const approveTx = requirements[0]; if (!approveTx) { @@ -64,7 +65,7 @@ describe("Metadata", () => { throw new Error("Approve transaction is not an approval transaction"); } - const tx_2 = deposit.buildTx(); + const tx_2 = await buildPlanTx(deposit); await client.sendTransaction(approveTx); await client.sendTransaction(tx_2); }, @@ -115,7 +116,7 @@ describe("Metadata", () => { vaultData, }); - const tx = deposit.buildTx(); + const tx = await buildPlanTx(deposit); expect(tx.data).toContain("25AFEA44"); const position = tx.data.indexOf("25AFEA44"); expect(position).toBeGreaterThanOrEqual(8); @@ -123,7 +124,7 @@ describe("Metadata", () => { const timestampHex = tx.data.slice(position - 8, position); expect(timestampHex).toBe("00000000"); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); const approveTx = requirements[0]; if (!approveTx) { diff --git a/packages/morpho-sdk/test/actions/midnight/requirements.test.ts b/packages/morpho-sdk/test/actions/midnight/requirements.test.ts index 6fc1f4657..f8b519216 100644 --- a/packages/morpho-sdk/test/actions/midnight/requirements.test.ts +++ b/packages/morpho-sdk/test/actions/midnight/requirements.test.ts @@ -32,6 +32,7 @@ import { isRequirementApproval, morphoViemExtension, } from "../../../src/index.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; const test = createViemTest(base, { forkUrl: process.env.BASE_RPC_URL, @@ -146,7 +147,7 @@ const prepareTakeableOffer = async (params: { reservedCollateralAssets: 0n, }); await params.client.sendTransaction({ - ...supply.buildTx(), + ...(await buildPlanTx(supply)), account: offerMaker, }); await expect( @@ -328,7 +329,7 @@ describe("Midnight requirements on fork", () => { reservedCollateralAssets, }); - const requirements = await output.getRequirements(); + const requirements = await getPlanRequests(output); expect(requirements).toHaveLength(1); const approval = requirements[0]; if (!isRequirementApproval(approval)) { @@ -342,8 +343,8 @@ describe("Midnight requirements on fork", () => { ); await client.sendTransaction(approval); - await expect(output.getRequirements()).resolves.toEqual([]); - expect(output.buildTx().to).toBe( + await expect(getPlanRequests(output)).resolves.toEqual([]); + expect((await buildPlanTx(output)).to).toBe( getChainAddress(ChainId.BaseMainnet, "midnight"), ); }); @@ -370,7 +371,7 @@ describe("Midnight requirements on fork", () => { takeableOffers: [takeableOffer], deadline: maxUint256, }); - const requirements = await output.getRequirements(); + const requirements = await getPlanRequests(output); expect(requirements.map((requirement) => requirement.action.type)).toEqual([ "erc20Approval", "midnightAuthorization", @@ -381,9 +382,9 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await expect(output.getRequirements()).resolves.toEqual([]); + await expect(getPlanRequests(output)).resolves.toEqual([]); - await client.sendTransaction(output.buildTx()); + await client.sendTransaction(await buildPlanTx(output)); await expect( client.readContract({ address: midnight, @@ -410,7 +411,7 @@ describe("Midnight requirements on fork", () => { collateralAssets, reservedCollateralAssets: 0n, }); - const supplyRequirements = await supply.getRequirements(); + const supplyRequirements = await getPlanRequests(supply); expect( supplyRequirements.map((requirement) => requirement.action.type), ).toEqual(["erc20Approval"]); @@ -420,7 +421,7 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(supply.buildTx()); + await client.sendTransaction(await buildPlanTx(supply)); await expect( client.readContract({ address: midnight, @@ -443,7 +444,7 @@ describe("Midnight requirements on fork", () => { takeableOffers: [takeableOffer], deadline: maxUint256, }); - const borrowRequirements = await borrow.getRequirements(); + const borrowRequirements = await getPlanRequests(borrow); expect( borrowRequirements.map((requirement) => requirement.action.type), ).toEqual(["midnightAuthorization"]); @@ -453,7 +454,7 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(borrow.buildTx()); + await client.sendTransaction(await buildPlanTx(borrow)); await expect( client.readContract({ address: midnight, @@ -488,7 +489,7 @@ describe("Midnight requirements on fork", () => { takeableOffers: [takeableOffer], deadline: maxUint256, }); - const borrowRequirements = await borrow.getRequirements(); + const borrowRequirements = await getPlanRequests(borrow); expect( borrowRequirements.map((requirement) => requirement.action.type), ).toEqual(["erc20Approval", "midnightAuthorization"]); @@ -498,7 +499,7 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(borrow.buildTx()); + await client.sendTransaction(await buildPlanTx(borrow)); await expect( client.readContract({ address: midnight, @@ -522,7 +523,7 @@ describe("Midnight requirements on fork", () => { withdrawCollateralAssets: 0n, deadline: maxUint256, }); - const repayRequirements = await repay.getRequirements(); + const repayRequirements = await getPlanRequests(repay); expect( repayRequirements.map((requirement) => requirement.action.type), ).toEqual(["erc20Approval"]); @@ -532,7 +533,7 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(repay.buildTx()); + await client.sendTransaction(await buildPlanTx(repay)); await expect( client.readContract({ address: midnight, diff --git a/packages/morpho-sdk/test/actions/requirements/approval.test.ts b/packages/morpho-sdk/test/actions/requirements/approval.test.ts index 281210a9c..25342c4c4 100644 --- a/packages/morpho-sdk/test/actions/requirements/approval.test.ts +++ b/packages/morpho-sdk/test/actions/requirements/approval.test.ts @@ -9,6 +9,7 @@ import { import { Re7UsdtVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("Approval", () => { test("should approve once for USDT vaultV2 with allowance 0", async ({ @@ -39,7 +40,7 @@ describe("Approval", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(1); expect(requirements[0]?.action.args.spender).toBe(generalAdapter); @@ -51,7 +52,7 @@ describe("Approval", () => { await client.sendTransaction(requirements[0]); - const tx = deposit.buildTx(); + const tx = await buildPlanTx(deposit); await client.sendTransaction(tx); }, @@ -91,7 +92,7 @@ describe("Approval", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(2); expect(requirements[0]?.action.args.spender).toBe(generalAdapter); @@ -109,7 +110,7 @@ describe("Approval", () => { await client.sendTransaction(requirements[0]); await client.sendTransaction(requirements[1]); - const tx = deposit.buildTx(); + const tx = await buildPlanTx(deposit); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/requirements/permit.test.ts b/packages/morpho-sdk/test/actions/requirements/permit.test.ts index f7c88aea0..f59d01662 100644 --- a/packages/morpho-sdk/test/actions/requirements/permit.test.ts +++ b/packages/morpho-sdk/test/actions/requirements/permit.test.ts @@ -8,6 +8,7 @@ import { import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("Permit", () => { test("should deposit USDC with permit version 2", async ({ client }) => { @@ -39,7 +40,7 @@ describe("Permit", () => { amount: amount, vaultData, }); - const requirements_1 = await deposit.getRequirements({ + const requirements_1 = await getPlanRequests(deposit, { useSimplePermit: true, }); @@ -59,7 +60,7 @@ describe("Permit", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx_1 = deposit.buildTx([requirementSignature]); + const tx_1 = await buildPlanTx(deposit, [requirementSignature]); await client.sendTransaction(tx_1); }, diff --git a/packages/morpho-sdk/test/actions/requirements/permit2.test.ts b/packages/morpho-sdk/test/actions/requirements/permit2.test.ts index fff5e6502..0b6a3cd46 100644 --- a/packages/morpho-sdk/test/actions/requirements/permit2.test.ts +++ b/packages/morpho-sdk/test/actions/requirements/permit2.test.ts @@ -15,6 +15,7 @@ import { import { testInvariants } from "../../helpers/invariants.js"; import { createVaultV2 } from "../../helpers/vaultV2.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("Permit2", () => { const { @@ -58,7 +59,7 @@ describe("Permit2", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); // USDT may require two signature requirements (reset approval permit2 + approve permit2 + set allowance) expect(requirements.length).toBe(3); @@ -101,7 +102,7 @@ describe("Permit2", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = deposit.buildTx([requirementSignature]); + const tx = await buildPlanTx(deposit, [requirementSignature]); await client.sendTransaction(tx); }, @@ -148,7 +149,7 @@ describe("Permit2", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); // USDT may require two signature requirements (approve permit2 + set allowance) expect(requirements.length).toBe(2); @@ -179,7 +180,7 @@ describe("Permit2", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = deposit.buildTx([requirementSignature]); + const tx = await buildPlanTx(deposit, [requirementSignature]); await client.sendTransaction(tx); }, @@ -233,7 +234,7 @@ describe("Permit2", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(2); @@ -271,7 +272,9 @@ describe("Permit2", () => { BigInt(Math.floor(Date.now() / 1000)), ); - await client.sendTransaction(deposit.buildTx([requirementSignature])); + await client.sendTransaction( + await buildPlanTx(deposit, [requirementSignature]), + ); }, }); @@ -326,7 +329,7 @@ describe("Permit2", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(2); @@ -364,7 +367,7 @@ describe("Permit2", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = deposit.buildTx([requirementSignature]); + const tx = await buildPlanTx(deposit, [requirementSignature]); await client.sendTransaction(tx); }, @@ -411,7 +414,7 @@ describe("Permit2", () => { vaultData, }); - const requirements = await deposit.getRequirements({ + const requirements = await getPlanRequests(deposit, { useSimplePermit: false, }); @@ -451,7 +454,7 @@ describe("Permit2", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = deposit.buildTx([requirementSignature]); + const tx = await buildPlanTx(deposit, [requirementSignature]); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts b/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts index 58d04106b..8c8ba9179 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts @@ -18,6 +18,7 @@ import { } from "../../fixtures/vaultV1.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("DepositVaultV1", () => { test("should deposit 1K USDC in vaultV1", async ({ client }) => { @@ -49,8 +50,8 @@ describe("DepositVaultV1", () => { vaultData, }); - const tx = deposit.buildTx(); - const requirements = await deposit.getRequirements(); + const tx = await buildPlanTx(deposit); + const requirements = await getPlanRequests(deposit); const approveTx = requirements[0]; if (!approveTx) { @@ -117,8 +118,8 @@ describe("DepositVaultV1", () => { vaultData, }); - const tx = deposit.buildTx(); - const requirements = await deposit.getRequirements(); + const tx = await buildPlanTx(deposit); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(2); @@ -182,7 +183,7 @@ describe("DepositVaultV1", () => { amount: amount, vaultData, }); - const requirements = await deposit.getRequirements({ + const requirements = await getPlanRequests(deposit, { useSimplePermit: true, }); @@ -206,7 +207,7 @@ describe("DepositVaultV1", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = deposit.buildTx([requirementSignature]); + const tx = await buildPlanTx(deposit, [requirementSignature]); await client.sendTransaction(tx); }, @@ -259,7 +260,7 @@ describe("DepositVaultV1", () => { vaultData, }); - const requirements = await deposit.getRequirements({ + const requirements = await getPlanRequests(deposit, { useSimplePermit: false, }); @@ -301,7 +302,7 @@ describe("DepositVaultV1", () => { await client.sendTransaction(approvalPermit2); - const tx = deposit.buildTx([requirementSignature]); + const tx = await buildPlanTx(deposit, [requirementSignature]); await client.sendTransaction(tx); }, @@ -360,7 +361,7 @@ describe("DepositVaultV1", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(2); @@ -398,7 +399,9 @@ describe("DepositVaultV1", () => { BigInt(Math.floor(Date.now() / 1000)), ); - await client.sendTransaction(deposit.buildTx([requirementSignature])); + await client.sendTransaction( + await buildPlanTx(deposit, [requirementSignature]), + ); }, }); @@ -445,13 +448,13 @@ describe("DepositVaultV1", () => { amount: amount, vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); const approveTx = requirements[0]; if (!isRequirementApproval(approveTx)) { throw new Error("Approve transaction not found"); } await client.sendTransaction(approveTx); - const depositTx = deposit.buildTx(); + const depositTx = await buildPlanTx(deposit); await client.sendTransaction(depositTx); const withdraw = vaultV1.withdraw({ @@ -459,7 +462,7 @@ describe("DepositVaultV1", () => { amount: amount, }); - const withdrawTx = withdraw.buildTx(); + const withdrawTx = await buildPlanTx(withdraw); await client.sendTransaction(withdrawTx); }, diff --git a/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts b/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts index 12561f5b7..a9a4d6ec3 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts @@ -10,6 +10,7 @@ import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("MigrateToV2 VaultV1", () => { test("should migrate full USDC position from V1 to V2", async ({ @@ -49,7 +50,7 @@ describe("MigrateToV2 VaultV1", () => { shares, }); - const requirements = await migrate.getRequirements(); + const requirements = await getPlanRequests(migrate); expect(requirements.length).toBe(1); @@ -63,7 +64,7 @@ describe("MigrateToV2 VaultV1", () => { await client.sendTransaction(approveTx); - const tx = migrate.buildTx(); + const tx = await buildPlanTx(migrate); await client.sendTransaction(tx); }, }); @@ -120,7 +121,7 @@ describe("MigrateToV2 VaultV1", () => { shares, }); - const requirements = await migrate.getRequirements(); + const requirements = await getPlanRequests(migrate); if (!isRequirementSignature(requirements[0])) { throw new Error("Requirement is not a signature requirement"); @@ -142,7 +143,7 @@ describe("MigrateToV2 VaultV1", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = migrate.buildTx([requirementSignature]); + const tx = await buildPlanTx(migrate, [requirementSignature]); await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts b/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts index 094d154d7..ff83c63f0 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts @@ -8,6 +8,7 @@ import { import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("Redeem VaultV1", () => { test("should redeem 1K shares in vaultV1", async ({ client }) => { @@ -36,7 +37,7 @@ describe("Redeem VaultV1", () => { userAddress: client.account.address, shares, }); - const tx = redeem.buildTx(); + const tx = await buildPlanTx(redeem); await client.sendTransaction(tx); }, @@ -85,13 +86,13 @@ describe("Redeem VaultV1", () => { amount: depositAmount, vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); const approveTx = requirements[0]; if (!isRequirementApproval(approveTx)) { throw new Error("Approve transaction not found"); } await client.sendTransaction(approveTx); - await client.sendTransaction(deposit.buildTx()); + await client.sendTransaction(await buildPlanTx(deposit)); const shares = await client.balanceOf({ erc20: SteakhouseUsdcVaultV1.address, @@ -101,7 +102,7 @@ describe("Redeem VaultV1", () => { userAddress: client.account.address, shares, }); - await client.sendTransaction(redeem.buildTx()); + await client.sendTransaction(await buildPlanTx(redeem)); }, }); @@ -145,7 +146,7 @@ describe("Redeem VaultV1", () => { userAddress: client.account.address, shares: redeemShares, }); - await client.sendTransaction(redeem.buildTx()); + await client.sendTransaction(await buildPlanTx(redeem)); }, }); diff --git a/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts b/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts index 9b0bd956a..bab58e2a3 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts @@ -5,6 +5,7 @@ import { morphoViemExtension } from "../../../src/index.js"; import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("Withdraw VaultV1", () => { test("should withdraw 1K assets in vaultV1", async ({ client }) => { @@ -34,7 +35,7 @@ describe("Withdraw VaultV1", () => { userAddress: client.account.address, amount: assets, }); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); await client.sendTransaction(tx); }, @@ -81,13 +82,13 @@ describe("Withdraw VaultV1", () => { userAddress: client.account.address, amount: firstWithdraw, }); - await client.sendTransaction(withdraw1.buildTx()); + await client.sendTransaction(await buildPlanTx(withdraw1)); const withdraw2 = vaultV1.withdraw({ userAddress: client.account.address, amount: secondWithdraw, }); - await client.sendTransaction(withdraw2.buildTx()); + await client.sendTransaction(await buildPlanTx(withdraw2)); }, }); diff --git a/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts b/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts index ba54cfdff..cd4c0cbab 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts @@ -8,6 +8,7 @@ import { import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("DepositVaultV2", () => { test("should deposit 1K USDC in vaultV2", async ({ client }) => { @@ -36,8 +37,8 @@ describe("DepositVaultV2", () => { vaultData, }); - const tx = deposit.buildTx(); - const requirements = await deposit.getRequirements(); + const tx = await buildPlanTx(deposit); + const requirements = await getPlanRequests(deposit); const approveTx = requirements[0]; if (!approveTx) { diff --git a/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts b/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts index 4d7edd521..e7098a86d 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts @@ -7,6 +7,7 @@ import { morphoViemExtension, vaultV2ForceRedeem } from "../../../src/index.js"; import { ReEcosystemUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx } from "../../transactionPlanUtils.js"; // Accrue VaultV2 to the current block timestamp so `toShares` matches the // post-accrue math the contract will run at redeem block (ā‰ˆ same block here). @@ -72,7 +73,7 @@ describe("ForceRedeem VaultV2", () => { redeem: { shares: redeemShares }, userAddress: client.account.address, }); - const tx_1 = forceRedeem.buildTx(); + const tx_1 = await buildPlanTx(forceRedeem); const tx_2 = vaultV2ForceRedeem({ vault: { address: ReEcosystemUsdcVaultV2.address }, @@ -134,7 +135,7 @@ describe("ForceRedeem VaultV2", () => { redeem: { shares: redeemShares }, userAddress: client.account.address, }); - const tx = forceRedeem.buildTx(); + const tx = await buildPlanTx(forceRedeem); const { vaults: { @@ -199,7 +200,7 @@ describe("ForceRedeem VaultV2", () => { redeem: { shares: redeemShares }, userAddress: client.account.address, }); - const tx = forceRedeem.buildTx(); + const tx = await buildPlanTx(forceRedeem); const { vaults: { diff --git a/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts b/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts index 179f6d22c..2cb80e319 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts @@ -9,6 +9,7 @@ import { import { ReEcosystemUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("ForceWithdraw VaultV2", () => { // Blue adapter addresses @@ -59,7 +60,7 @@ describe("ForceWithdraw VaultV2", () => { withdraw: { amount: assetsWithdraw }, userAddress: client.account.address, }); - const tx_1 = forceWithdraw.buildTx(); + const tx_1 = await buildPlanTx(forceWithdraw); const tx_2 = vaultV2ForceWithdraw({ vault: { address: ReEcosystemUsdcVaultV2.address }, @@ -124,7 +125,7 @@ describe("ForceWithdraw VaultV2", () => { withdraw: { amount: assets }, userAddress: client.account.address, }); - const tx = forceWithdraw.buildTx(); + const tx = await buildPlanTx(forceWithdraw); const { vaults: { @@ -190,7 +191,7 @@ describe("ForceWithdraw VaultV2", () => { withdraw: { amount: withdrawAssets }, userAddress: client.account.address, }); - const tx = forceWithdraw.buildTx(); + const tx = await buildPlanTx(forceWithdraw); const { vaults: { diff --git a/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts b/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts index 704f6f466..f81f0003a 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts @@ -5,6 +5,7 @@ import { morphoViemExtension } from "../../../src/index.js"; import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("Redeem VaultV2", () => { test("should redeem 1K USDC in vaultV2", async ({ client }) => { @@ -30,7 +31,7 @@ describe("Redeem VaultV2", () => { userAddress: client.account.address, shares, }); - const tx = redeem.buildTx(); + const tx = await buildPlanTx(redeem); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts b/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts index 603d90bfb..173c8de62 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts @@ -5,6 +5,7 @@ import { morphoViemExtension } from "../../../src/index.js"; import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; +import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("Withdraw VaultV2", () => { test("should withdraw 1K assets in vaultV2", async ({ client }) => { @@ -31,7 +32,7 @@ describe("Withdraw VaultV2", () => { userAddress: client.account.address, amount: assets, }); - const tx = withdraw.buildTx(); + const tx = await buildPlanTx(withdraw); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/wrap.test.ts b/packages/morpho-sdk/test/actions/wrap.test.ts index 78c38ec3b..010a3288a 100644 --- a/packages/morpho-sdk/test/actions/wrap.test.ts +++ b/packages/morpho-sdk/test/actions/wrap.test.ts @@ -19,6 +19,7 @@ import { import { KeyrockUsdcVaultV2, KpkWETHVaultV2 } from "../fixtures/vaultV2.js"; import { testInvariants } from "../helpers/invariants.js"; import { test } from "../setup.js"; +import { buildPlanTx, getPlanRequests } from "../transactionPlanUtils.js"; describe("WrapNative - VaultV1", () => { test("should deposit native ETH only in WETH vaultV1", async ({ client }) => { @@ -49,10 +50,10 @@ describe("WrapNative - VaultV1", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(0); - const tx = deposit.buildTx(); + const tx = await buildPlanTx(deposit); expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(0n); @@ -118,10 +119,10 @@ describe("WrapNative - VaultV1", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(0); - const tx = deposit.buildTx(); + const tx = await buildPlanTx(deposit); expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -180,7 +181,7 @@ describe("WrapNative - VaultV1", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(2); @@ -203,7 +204,7 @@ describe("WrapNative - VaultV1", () => { expect(isHex(requirementSignature.args.signature)).toBe(true); expect(requirementSignature.args.signature.length).toBe(132); - const tx = deposit.buildTx([requirementSignature]); + const tx = await buildPlanTx(deposit, [requirementSignature]); expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -223,7 +224,7 @@ describe("WrapNative - VaultV1", () => { ); }); - test("should throw NativeAmountOnNonWNativeVaultError for non-WETH vaultV1", () => { + test("should throw NativeAmountOnNonWNativeVaultError for non-WETH vaultV1", async () => { expect(() => vaultV1Deposit({ vault: { @@ -240,7 +241,7 @@ describe("WrapNative - VaultV1", () => { ).toThrow(NativeAmountOnNonWNativeVaultError); }); - test("should throw ZeroDepositAmountError when both amounts are zero in vaultV1", () => { + test("should throw ZeroDepositAmountError when both amounts are zero in vaultV1", async () => { expect(() => vaultV1Deposit({ vault: { @@ -257,7 +258,7 @@ describe("WrapNative - VaultV1", () => { ).toThrow(ZeroDepositAmountError); }); - test("should throw NegativeNativeAmountError for negative nativeAmount in vaultV1", () => { + test("should throw NegativeNativeAmountError for negative nativeAmount in vaultV1", async () => { expect(() => vaultV1Deposit({ vault: { @@ -304,10 +305,10 @@ describe("WrapNative - VaultV2", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(0); - const tx = deposit.buildTx(); + const tx = await buildPlanTx(deposit); expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(0n); @@ -373,10 +374,10 @@ describe("WrapNative - VaultV2", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(0); - const tx = deposit.buildTx(); + const tx = await buildPlanTx(deposit); expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -435,7 +436,7 @@ describe("WrapNative - VaultV2", () => { vaultData, }); - const requirements = await deposit.getRequirements(); + const requirements = await getPlanRequests(deposit); expect(requirements.length).toBe(2); @@ -458,7 +459,7 @@ describe("WrapNative - VaultV2", () => { expect(isHex(requirementSignature.args.signature)).toBe(true); expect(requirementSignature.args.signature.length).toBe(132); - const tx = deposit.buildTx([requirementSignature]); + const tx = await buildPlanTx(deposit, [requirementSignature]); expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -478,7 +479,7 @@ describe("WrapNative - VaultV2", () => { ); }); - test("should throw NativeAmountOnNonWNativeVaultError for non-WETH vaultV2", () => { + test("should throw NativeAmountOnNonWNativeVaultError for non-WETH vaultV2", async () => { expect(() => vaultV2Deposit({ vault: { @@ -495,7 +496,7 @@ describe("WrapNative - VaultV2", () => { ).toThrow(NativeAmountOnNonWNativeVaultError); }); - test("should throw ZeroDepositAmountError when both amounts are zero in vaultV2", () => { + test("should throw ZeroDepositAmountError when both amounts are zero in vaultV2", async () => { expect(() => vaultV2Deposit({ vault: { @@ -512,7 +513,7 @@ describe("WrapNative - VaultV2", () => { ).toThrow(ZeroDepositAmountError); }); - test("should throw NegativeNativeAmountError for negative nativeAmount in vaultV2", () => { + test("should throw NegativeNativeAmountError for negative nativeAmount in vaultV2", async () => { expect(() => vaultV2Deposit({ vault: { diff --git a/packages/morpho-sdk/test/transactionPlanUtils.ts b/packages/morpho-sdk/test/transactionPlanUtils.ts new file mode 100644 index 000000000..272042137 --- /dev/null +++ b/packages/morpho-sdk/test/transactionPlanUtils.ts @@ -0,0 +1,88 @@ +import type { + TransactionPlan, + TransactionPlanHandler, + TransactionPlanRequest, +} from "../src/transactionPlan/index.js"; +import type { + RequirementSignature, + Transaction, + TransactionAction, +} from "../src/types/index.js"; + +function getTestHandler< + TAction extends TransactionAction, + TOptions, + TRequest extends TransactionPlanRequest, + TSignatures, +>( + plan: TransactionPlan, +): TransactionPlanHandler { + return ( + plan as unknown as { + readonly handler: TransactionPlanHandler< + TAction, + TOptions, + TRequest, + TSignatures + >; + } + ).handler; +} + +export async function buildPlanTx< + TAction extends TransactionAction, + TOptions, + TRequest extends TransactionPlanRequest, + TSignatures, +>( + plan: TransactionPlan, + signatures?: TSignatures, +): Promise>> { + return getTestHandler(plan).buildPrimaryCall(signatures); +} + +export async function getPlanRequests< + TAction extends TransactionAction, + TOptions, + TRequest extends TransactionPlanRequest, + TSignatures, +>( + plan: TransactionPlan, + requestOptions?: TOptions, +): Promise; +export async function getPlanRequests( + plan: { readonly prepare: unknown }, + requestOptions?: unknown, +): Promise; +export async function getPlanRequests( + plan: unknown, + requestOptions?: unknown, +): Promise { + const getRequirementRequests = getTestHandler( + plan as TransactionPlan< + TransactionAction, + unknown, + TransactionPlanRequest, + readonly RequirementSignature[] + >, + ).getRequirementRequests; + return getRequirementRequests ? getRequirementRequests(requestOptions) : []; +} + +export async function buildPlanCalls< + TAction extends TransactionAction, + TOptions, + TRequest extends TransactionPlanRequest, + TSignatures, +>( + plan: TransactionPlan, + signatures?: TSignatures, +) { + return (await plan.prepare()).build(signatures).callRequests; +} + +export function asSignatureArray( + signature: RequirementSignature, +): readonly RequirementSignature[] { + return [signature]; +} diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index a4ee6d057..8555377f6 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -63,36 +63,63 @@ const positionData = { collateral: 44n, }; -const supplyAction = { - getRequirements: vi - .fn() - .mockResolvedValue([{ action: { type: "erc20Approval" } }]), - buildTx: vi.fn().mockReturnValue(SUPPLY_TX), -}; -const withdrawAction = { - buildTx: vi.fn().mockReturnValue(WITHDRAW_TX), -}; -const borrowAction = { - getRequirements: vi - .fn() - .mockResolvedValue([{ action: { type: "blueAuthorization" } }]), - buildTx: vi.fn().mockReturnValue(BORROW_TX), -}; -const repayAction = { - getRequirements: vi - .fn() - .mockResolvedValue([{ action: { type: "erc20Approval" } }]), - buildTx: vi.fn().mockReturnValue(REPAY_TX), -}; -const supplyCollateralAction = { - getRequirements: vi - .fn() - .mockResolvedValue([{ action: { type: "erc20Approval" } }]), - buildTx: vi.fn().mockReturnValue(SUPPLY_COLLATERAL_TX), -}; -const withdrawCollateralAction = { - buildTx: vi.fn().mockReturnValue(WITHDRAW_COLLATERAL_TX), -}; +function mockTransactionPlan(tx: typeof SUPPLY_TX, requirements = []) { + const getRequirementRequests = vi.fn().mockResolvedValue(requirements); + const buildPrimaryCall = vi.fn().mockReturnValue(tx); + const prepare = vi.fn(async (options?: { requestOptions?: unknown }) => { + const resolvedRequirements = await getRequirementRequests( + options?.requestOptions, + ); + return { + signatureRequests: [], + callRequests: [{ id: "primary", kind: "call", phase: "primary", tx }], + steps: [ + ...resolvedRequirements.map((requirement, index) => + "sign" in requirement + ? { + id: `request-${index}`, + kind: "signature", + request: requirement, + } + : { + id: `request-${index}`, + kind: "call", + phase: "preparation", + tx: requirement, + }, + ), + { id: "primary", kind: "call", phase: "primary", tx }, + ], + build: (signatures?: readonly RequirementSignature[]) => ({ + signatureRequests: [], + callRequests: [ + { + id: "primary", + kind: "call", + phase: "primary", + tx: buildPrimaryCall(signatures), + }, + ], + }), + }; + }); + return { prepare, getRequirementRequests, buildPrimaryCall }; +} + +const supplyAction = mockTransactionPlan(SUPPLY_TX, [ + { action: { type: "erc20Approval" } }, +]); +const withdrawAction = mockTransactionPlan(WITHDRAW_TX); +const borrowAction = mockTransactionPlan(BORROW_TX, [ + { action: { type: "blueAuthorization" } }, +]); +const repayAction = mockTransactionPlan(REPAY_TX, [ + { action: { type: "erc20Approval" } }, +]); +const supplyCollateralAction = mockTransactionPlan(SUPPLY_COLLATERAL_TX, [ + { action: { type: "erc20Approval" } }, +]); +const withdrawCollateralAction = mockTransactionPlan(WITHDRAW_COLLATERAL_TX); const vaultV2Entity = { getData: vi.fn().mockResolvedValue(vaultData), @@ -213,7 +240,7 @@ describe.sequential("MorphoProtocolEvm", () => { ); expect(requirements).toEqual([{ action: { type: "erc20Approval" } }]); - expect(supplyAction.getRequirements).toHaveBeenCalledWith( + expect(supplyAction.getRequirementRequests).toHaveBeenCalledWith( requirementOptions, ); }); @@ -522,7 +549,7 @@ describe.sequential("MorphoProtocolEvm", () => { }); expect(requirements).toEqual([{ action: { type: "blueAuthorization" } }]); - expect(borrowAction.getRequirements).toHaveBeenCalled(); + expect(borrowAction.getRequirementRequests).toHaveBeenCalled(); }); test("should build the borrow without signatures by default", async () => { @@ -532,7 +559,7 @@ describe.sequential("MorphoProtocolEvm", () => { await protocol.borrow({ token: TOKEN, amount: 100_000n }); - expect(borrowAction.buildTx).toHaveBeenCalledWith(undefined); + expect(borrowAction.buildPrimaryCall).toHaveBeenCalledWith(undefined); }); test("should fold a signed authorization into the bundle when provided", async () => { @@ -549,7 +576,9 @@ describe.sequential("MorphoProtocolEvm", () => { requirementSignature, }); - expect(borrowAction.buildTx).toHaveBeenCalledWith([requirementSignature]); + expect(borrowAction.buildPrimaryCall).toHaveBeenCalledWith([ + requirementSignature, + ]); }); test("should throw if 'onBehalfOf' differs from the wallet address", async () => { @@ -619,7 +648,7 @@ describe.sequential("MorphoProtocolEvm", () => { ); expect(requirements).toEqual([{ action: { type: "erc20Approval" } }]); - expect(repayAction.getRequirements).toHaveBeenCalledWith( + expect(repayAction.getRequirementRequests).toHaveBeenCalledWith( requirementOptions, ); }); @@ -700,9 +729,9 @@ describe.sequential("MorphoProtocolEvm", () => { ); expect(requirements).toEqual([{ action: { type: "erc20Approval" } }]); - expect(supplyCollateralAction.getRequirements).toHaveBeenCalledWith( - requirementOptions, - ); + expect( + supplyCollateralAction.getRequirementRequests, + ).toHaveBeenCalledWith(requirementOptions); }); }); diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index f4d126bba..9bcabaaec 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -13,6 +13,9 @@ import { type Requirement, type RequirementSignature, type Transaction, + type TransactionAction, + type TransactionPlan, + type TransactionPlanRequest, type VaultReallocation, } from "@morpho-org/morpho-sdk"; import type { @@ -394,6 +397,42 @@ function toWdkTransaction(tx: { }; } +async function getTransactionPlanRequests< + TAction extends TransactionAction, + TOptions, + TRequest extends TransactionPlanRequest, + TSignatures, +>( + plan: TransactionPlan, + requestOptions?: TOptions, +): Promise { + const prepared = await plan.prepare( + requestOptions === undefined ? undefined : { requestOptions }, + ); + return prepared.steps + .filter((request) => request.id !== "primary") + .map((request) => + request.kind === "signature" ? request.request : request.tx, + ) as unknown as readonly TRequest[]; +} + +async function getTransactionPlanTx< + TAction extends TransactionAction, + TOptions, + TRequest extends TransactionPlanRequest, + TSignatures, +>( + plan: TransactionPlan, + signatures?: TSignatures, +): Promise { + const executable = (await plan.prepare()).build(signatures); + const primaryCall = executable.callRequests.at(-1); + if (primaryCall == null) { + throw new Error("Transaction plan produced no executable call request."); + } + return toWdkTransaction(primaryCall.tx); +} + /** * Morpho lending protocol adapter for WDK-compatible EVM wallet accounts. * @@ -498,7 +537,7 @@ export default class MorphoProtocolEvm extends LendingProtocol { ): Promise { const action = await this._getSupplyAction(options); - return await action.getRequirements(requirementOptions); + return await getTransactionPlanRequests(action, requirementOptions); } /** @@ -558,12 +597,9 @@ export default class MorphoProtocolEvm extends LendingProtocol { ): Promise { const action = await this._getSupplyAction(options, depositAmounts); - return toWdkTransaction( - action.buildTx( - options.requirementSignature - ? [options.requirementSignature] - : undefined, - ), + return await getTransactionPlanTx( + action, + options.requirementSignature ? [options.requirementSignature] : undefined, ); } @@ -627,13 +663,11 @@ export default class MorphoProtocolEvm extends LendingProtocol { ); } - return toWdkTransaction( - vault.entity - .withdraw({ - amount: normalizedAmount, - userAddress, - }) - .buildTx(), + return await getTransactionPlanTx( + vault.entity.withdraw({ + amount: normalizedAmount, + userAddress, + }), ); } @@ -677,7 +711,7 @@ export default class MorphoProtocolEvm extends LendingProtocol { ): Promise<(RequirementAuthorization | RequirementSignatureRequest)[]> { const action = await this._getBorrowAction(options); - return await action.getRequirements(); + return await getTransactionPlanRequests(action); } /** @@ -732,12 +766,9 @@ export default class MorphoProtocolEvm extends LendingProtocol { ): Promise { const action = await this._getBorrowAction(options); - return toWdkTransaction( - action.buildTx( - options.requirementSignature - ? [options.requirementSignature] - : undefined, - ), + return await getTransactionPlanTx( + action, + options.requirementSignature ? [options.requirementSignature] : undefined, ); } @@ -781,7 +812,7 @@ export default class MorphoProtocolEvm extends LendingProtocol { ): Promise { const action = await this._getRepayAction(options); - return await action.getRequirements(requirementOptions); + return await getTransactionPlanRequests(action, requirementOptions); } /** @@ -838,12 +869,9 @@ export default class MorphoProtocolEvm extends LendingProtocol { ): Promise { const action = await this._getRepayAction(options, amount); - return toWdkTransaction( - action.buildTx( - options.requirementSignature - ? [options.requirementSignature] - : undefined, - ), + return await getTransactionPlanTx( + action, + options.requirementSignature ? [options.requirementSignature] : undefined, ); } @@ -894,7 +922,7 @@ export default class MorphoProtocolEvm extends LendingProtocol { ): Promise { const action = await this._getSupplyCollateralAction(options); - return await action.getRequirements(requirementOptions); + return await getTransactionPlanRequests(action, requirementOptions); } /** @@ -948,12 +976,9 @@ export default class MorphoProtocolEvm extends LendingProtocol { depositAmounts, ); - return toWdkTransaction( - action.buildTx( - options.requirementSignature - ? [options.requirementSignature] - : undefined, - ), + return await getTransactionPlanTx( + action, + options.requirementSignature ? [options.requirementSignature] : undefined, ); } @@ -1018,14 +1043,12 @@ export default class MorphoProtocolEvm extends LendingProtocol { const positionData = await market.entity.getPositionData(userAddress); - return toWdkTransaction( - market.entity - .withdrawCollateral({ - amount: normalizedAmount, - userAddress, - positionData, - }) - .buildTx(), + return await getTransactionPlanTx( + market.entity.withdrawCollateral({ + amount: normalizedAmount, + userAddress, + positionData, + }), ); } From 964d38c40405e3353bfb4f39ce06954ab7b172c6 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 15 Jul 2026 15:44:51 +0200 Subject: [PATCH 02/12] fix: correct transaction plan release notes and WDK typing --- .changeset/quiet-midnight-flows.md | 4 ++-- .changeset/transaction-plan.md | 4 ++-- .changeset/wdk-transaction-plan.md | 5 +++++ .../src/morpho-protocol-evm.test.ts | 19 +++++++++++++++---- .../src/morpho-protocol-evm.ts | 16 ++++++++++------ 5 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 .changeset/wdk-transaction-plan.md diff --git a/.changeset/quiet-midnight-flows.md b/.changeset/quiet-midnight-flows.md index 085948bd1..93f99a669 100644 --- a/.changeset/quiet-midnight-flows.md +++ b/.changeset/quiet-midnight-flows.md @@ -5,11 +5,11 @@ Add first-iteration Midnight action flows under `client.morpho.midnight(chainId)`, expose Midnight SDK API helpers through `morpho-sdk/midnight-api` and shared ABI/constant/error/utility entrypoints, and expose pure Midnight transaction builders for the markets-app taker, maker, redeem, repay/withdraw, and cancel flows. -The Midnight entity returns lazy action outputs with `getRequirements()` and synchronous `buildTx(...)` methods, matching the existing `morpho-sdk` action pattern while accepting fixed-rate API quote takeable offers directly. UI labels, rate display logic, and offer-chain presentation stay on the integrator side. +The Midnight entity returns lazy `TransactionPlan` outputs. Call `prepare()` to resolve signature requests and prerequisite call requests, then call `prepared.build(signatures)` to produce the ordered executable calls while accepting fixed-rate API quote takeable offers directly. UI labels, rate display logic, and offer-chain presentation stay on the integrator side. Midnight market transaction builders are synchronous and consume caller-provided `marketData` state, while `redeem` consumes a single caller-provided `positionData` snapshot that includes its hydrated market. Maker-offer action builders consume caller-provided `offersData` from `getOffersData(...)`, which creates the tree from the same entries accepted by `Tree.create(...)` and runs mempool validation. `getMarketData(...)`, `getPositionData(...)`, and `getOffersData(...)` remain async helpers so integrators can prepare state once, compose UI/validation around it, and then build transactions without hidden reads. Callers can pin several reads to one externally fetched block by forwarding its `blockNumber` through the fetch parameters. -Midnight Bundles token pulls use approval-based execution in this first iteration: `getRequirements()` returns ERC20 approval calls, and encoded bundle calls pass `PermitKind.None`. ERC2612 token permits, Permit2 SignatureTransfer token pulls, unit-target take helpers, exposed taker `reduceOnly`, referral fee controls, max-continuous-fee controls, and generic collateral withdrawal lists are left to follow-up work. +Midnight Bundles token pulls use approval-based execution in this first iteration: `prepare()` includes ERC20 approval calls when needed, and encoded bundle calls pass `PermitKind.None`. ERC2612 token permits, Permit2 SignatureTransfer token pulls, unit-target take helpers, exposed taker `reduceOnly`, referral fee controls, max-continuous-fee controls, and generic collateral withdrawal lists are left to follow-up work. Allow fork tests to select Anvil's Osaka hardfork so deployed Midnight bytecode using the `CLZ` opcode can be exercised end to end. diff --git a/.changeset/transaction-plan.md b/.changeset/transaction-plan.md index 4fa1f1c51..352334dd2 100644 --- a/.changeset/transaction-plan.md +++ b/.changeset/transaction-plan.md @@ -1,5 +1,5 @@ --- -"@morpho-org/morpho-sdk": minor +"@morpho-org/morpho-sdk": major --- -Add `TransactionPlan` to expose semantic transaction/signature steps, ordered executable txs, and intent-based helpers for app labels and flow branching. +Replace entity action outputs' `{ buildTx, getRequirements }` shape with `TransactionPlan`, exposing semantic transaction/signature steps, ordered executable txs, and intent-based helpers for app labels and flow branching. diff --git a/.changeset/wdk-transaction-plan.md b/.changeset/wdk-transaction-plan.md new file mode 100644 index 000000000..7b3e32bf8 --- /dev/null +++ b/.changeset/wdk-transaction-plan.md @@ -0,0 +1,5 @@ +--- +"@morpho-org/wdk-protocol-lending-morpho-evm": patch +--- + +Adapt Morpho SDK-backed flows to consume `TransactionPlan` outputs while preserving the WDK-facing requirement and transaction methods. diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index 8555377f6..7b9f38400 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -63,9 +63,21 @@ const positionData = { collateral: 44n, }; -function mockTransactionPlan(tx: typeof SUPPLY_TX, requirements = []) { - const getRequirementRequests = vi.fn().mockResolvedValue(requirements); - const buildPrimaryCall = vi.fn().mockReturnValue(tx); +type MockRequirement = { + readonly action: { readonly type: string }; + readonly sign?: () => Promise; +}; + +function mockTransactionPlan( + tx: typeof SUPPLY_TX, + requirements: readonly MockRequirement[] = [], +) { + const getRequirementRequests = vi.fn( + async (_requestOptions?: unknown) => requirements, + ); + const buildPrimaryCall = vi.fn( + (_signatures?: readonly RequirementSignature[]) => tx, + ); const prepare = vi.fn(async (options?: { requestOptions?: unknown }) => { const resolvedRequirements = await getRequirementRequests( options?.requestOptions, @@ -742,7 +754,6 @@ describe.sequential("MorphoProtocolEvm", () => { chainId: 1, provider: "https://dummy-rpc-url.com", bundlerUrl: "https://dummy-bundler-url.com", - entryPointAddress: "0x0000000000000000000000000000000000000007", safeModulesVersion: "0.3.0", isSponsored: false, useNativeCoins: true, diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 9bcabaaec..af25b7334 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -405,15 +405,19 @@ async function getTransactionPlanRequests< >( plan: TransactionPlan, requestOptions?: TOptions, -): Promise { +): Promise { const prepared = await plan.prepare( requestOptions === undefined ? undefined : { requestOptions }, ); - return prepared.steps - .filter((request) => request.id !== "primary") - .map((request) => - request.kind === "signature" ? request.request : request.tx, - ) as unknown as readonly TRequest[]; + const requests: TRequest[] = []; + for (const request of prepared.steps) { + if (request.id === "primary") continue; + requests.push( + (request.kind === "signature" ? request.request : request.tx) as TRequest, + ); + } + + return requests; } async function getTransactionPlanTx< From 86dce4187ee9a47bb0cf9bbfa1bbe13d90589fb5 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Wed, 15 Jul 2026 16:01:01 +0200 Subject: [PATCH 03/12] fix: update transaction plan package metadata --- .changeset/liquidity-morpho-six-peer.md | 5 +++++ packages/liquidity-sdk-viem/package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/liquidity-morpho-six-peer.md diff --git a/.changeset/liquidity-morpho-six-peer.md b/.changeset/liquidity-morpho-six-peer.md new file mode 100644 index 000000000..068a9b04a --- /dev/null +++ b/.changeset/liquidity-morpho-six-peer.md @@ -0,0 +1,5 @@ +--- +"@morpho-org/liquidity-sdk-viem": patch +--- + +Widen the Morpho SDK peer dependency range to accept the upcoming v6 transaction-plan release. diff --git a/packages/liquidity-sdk-viem/package.json b/packages/liquidity-sdk-viem/package.json index af533b8fd..3b13584f5 100644 --- a/packages/liquidity-sdk-viem/package.json +++ b/packages/liquidity-sdk-viem/package.json @@ -31,7 +31,7 @@ "peerDependencies": { "@morpho-org/blue-sdk": "^6.0.0", "@morpho-org/blue-sdk-viem": "^5.0.0", - "@morpho-org/morpho-sdk": "^5.0.0", + "@morpho-org/morpho-sdk": "^5.0.0 || ^6.0.0", "@morpho-org/morpho-ts": "^2.7.0", "dataloader": "^2.2.3", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", From 29c934e7f424046096de531974f4b3d248a14bbe Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Thu, 16 Jul 2026 09:15:20 +0200 Subject: [PATCH 04/12] fix(morpho-sdk): address transaction plan review --- AGENTS.md | 2 +- ...-06-03-midnight-action-output-interface.md | 198 +++---- packages/morpho-sdk/ARCHITECTURE.md | 20 +- packages/morpho-sdk/BUNDLER3.md | 4 +- .../src/client/morphoViemExtension.ts | 12 +- packages/morpho-sdk/src/entities/AGENTS.md | 2 +- .../morpho-sdk/src/entities/blue/AGENTS.md | 4 +- .../src/entities/midnight/midnight.test.ts | 1 - .../src/entities/midnight/midnight.ts | 10 +- .../morpho-sdk/src/entities/midnight/types.ts | 12 +- .../src/entities/vaultV1/vaultV1.ts | 10 +- .../src/entities/vaultV2/vaultV2.ts | 23 +- .../transactionPlan/TransactionPlan.test.ts | 83 +-- .../src/transactionPlan/TransactionPlan.ts | 514 ++++++++++++------ .../morpho-sdk/src/transactionPlan/types.ts | 83 ++- packages/morpho-sdk/src/types/AGENTS.md | 4 +- packages/morpho-sdk/src/types/action.ts | 38 +- .../src/errors.test.ts | 11 + .../src/errors.ts | 20 + .../src/index.ts | 1 + .../src/morpho-protocol-evm.ts | 9 +- 21 files changed, 664 insertions(+), 397 deletions(-) create mode 100644 packages/wdk-protocol-lending-morpho-evm/src/errors.test.ts create mode 100644 packages/wdk-protocol-lending-morpho-evm/src/errors.ts diff --git a/AGENTS.md b/AGENTS.md index 163d281e4..8695465e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ The three pillars: **layering, modularity, testability**. Everything else (types | Layer | Reads state? | Encodes calldata? | Async? | Returns | | --- | --- | --- | --- | --- | | **Client** | no | no | no | factory for entities | -| **Entity** | yes (RPC) | no | yes | lazy `{ buildTx, getRequirements }` | +| **Entity** | yes (RPC) | no | yes | lazy `TransactionPlan` | | **Action** | no | yes | **no** | deep-frozen `Transaction` | | **Helpers** | no | encode-only helpers (ABI-only); validators and constants are pure | no | new objects | diff --git a/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md b/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md index 3d97bdafb..d9118392e 100644 --- a/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md +++ b/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md @@ -2,7 +2,7 @@ | Field | Value | | ---------- | ------------------------------------ | -| **Status** | Proposed | +| **Status** | Accepted | | **Date** | 2026-06-03 | | **Author** | Romain / Carapulse draft | | **Scope** | Package: `morpho-sdk` / Midnight SDK | @@ -13,26 +13,33 @@ This TIB specifies the implementation of Midnight action flows in `morpho-sdk`. The source behavior is the markets app (`morpho-apps/apps/markets-app`): its home-made action builders already encode the protocol paths, requirement ordering, token-pull policy, ratifier selection, and mempool submission behavior future integrators need. The SDK should lift that protocol logic into reusable Midnight entity / action flows, while keeping the markets app migration as close as possible to an adapter swap. -The markets app is also the compatibility target. To minimize its diff, the SDK keeps the lazy action output shape already used by existing `morpho-sdk` action flows and widens only the requirement list and maker-offer signature arguments needed by the current app flows: +The markets app is also the compatibility target. To minimize its diff, the SDK exposes each flow +as a lazy `TransactionPlan`: the app prepares the plan when it is ready to discover prerequisites, +renders the typed signature and call requests, collects signatures at its wallet boundary, and then +builds the executable call sequence. ```ts -{ - getRequirements: () => Promise; - buildTx: ( - requirementSignatures?: - | MidnightOfferRootSignature - | readonly MidnightOfferRootSignature[], - ) => Readonly>; +const prepared = await plan.prepare(); + +const signatures: RequirementSignature[] = []; +for (const request of prepared.signatureRequests) { + signatures.push(await request.sign(walletClient, accountAddress)); } + +const executable = prepared.build(signatures); +const calls = executable.callRequests.map((request) => request.call); ``` -The concrete implementation is still MarketV1 / vault oriented: +The transaction-plan contract is shared by Midnight, Blue, MarketV1, and vault entities: -- `buildTx(...)` returns one final `Transaction`. -- `getRequirements(...)` returns prerequisite approval / permit / authorization items. -- `Requirement` currently means only a signature requirement (`permit` / `permit2`). -- Transaction requirements are raw `Transaction` or `Transaction` values. -- The shared `getRequirements(...)` helper is tuned for `bundler3.generalAdapter1` as spender. +- `TransactionPlan.prepare(...)` resolves only the prerequisites for the already selected action. +- `PreparedTransactionPlan.signatureRequests` exposes signable requirements without signing them. +- `PreparedTransactionPlan.callRequests` exposes ordered viem-compatible prerequisite calls and, + when previewable, the primary call. +- `PreparedTransactionPlan.build(...)` consumes collected signatures and returns an + `ExecutableTransactionPlan` whose primary call is last. +- `TransactionPlan` preserves the primary action, request-options, prerequisite-request, and + signature types through preparation and execution. The markets app (`morpho-apps/apps/markets-app`) already implements the Midnight flows, but under its UI-specific `ActionFlow` abstraction: @@ -42,11 +49,19 @@ The markets app (`morpho-apps/apps/markets-app`) already implements the Midnight - repay / withdraw collateral already goes through `MidnightBundles.midnightBundlesV1RepayAndWithdrawCollateral(...)`, so the app sees one final bundled tx plus optional pre-execution approval / authorization items; - none of the current markets app builders use `ActionFlow` `before` / `after` callbacks. -This TIB freezes the minimal SDK output-shape change needed before migrating those Midnight action builders into `morpho-sdk`. +This TIB freezes the `TransactionPlan` output shape used to migrate those Midnight action builders +into `morpho-sdk`. -That minimal change still touches shared `morpho-sdk` action-flow types and interfaces. `Requirement` can no longer mean only "signature requirement", transaction requirements can no longer mean only optional approval / authorization prerequisites, and `buildTx(...)` must accept the collected signature list the markets app already passes through its `ActionFlow` engine. Existing Blue / MarketV1 / vault methods may keep their narrower concrete return types, but the shared interfaces need to become compatible with the markets app's current execution model so the Midnight implementation does not force a bespoke integrator migration. +The plan model makes prerequisite ordering and semantic intent explicit without importing the app's +execution engine. Requirement descriptors remain either signable requests or typed transactions; +the prepared plan converts them into app-facing signature and call requests. Blue, MarketV1, +vault, and Midnight methods use narrower `TransactionPlan` generic arguments where their request +sets are known. -The compatibility constraint is intentionally two-sided. For existing `morpho-sdk` consumers, implementing Midnight action flows should not turn the shared action interface into a broad breaking migration: existing flows should keep the same `{ getRequirements, buildTx }` execution model, and any shared type widening should be source-compatible wherever the current method can stay narrower. For the markets app, those same shared types must become wide enough to represent its existing signature-first `ActionFlow` model, ordered call requests, and mandatory prelude transactions. This keeps Midnight reusable for future integrators without making current SDK consumers absorb large unrelated changes, while keeping the markets app migration diff mostly limited to replacing app-owned protocol builders with SDK calls plus one adapter. +The compatibility constraint is intentionally two-sided. The SDK provides one typed plan lifecycle +across protocols, while the markets app keeps its signature-first `ActionFlow` UX and adapts the +plan's ordered requests through one boundary. The replacement of the former action-output API is a +major-version migration and is not described as source-compatible. ## Goals / Non-Goals @@ -54,12 +69,12 @@ The compatibility constraint is intentionally two-sided. For existing `morpho-sd - Implement Midnight action flows in `morpho-sdk` from the markets app's working protocol implementation, so future integrators can reuse the same paths instead of rebuilding them app-side. - Minimize the markets app migration by preserving its `ActionFlow` execution model, centralizing the adapter, and moving only protocol construction into the SDK. -- Keep the public SDK contract centered on `{ getRequirements, buildTx }`. +- Keep the public SDK contract centered on `TransactionPlan`. - Represent every currently implemented markets app flow without adding an SDK `ActionFlow` engine. - Preserve the existing `Transaction` shape: `{ to, value, data, action }`. - Preserve action-layer purity: actions stay synchronous, encode-only, and deep-frozen. -- Keep existing Blue / MarketV1 / vault methods source-compatible; widen shared action-flow types / interfaces only where needed for the markets app's minimum-change migration. -- Avoid large breaking changes for existing `morpho-sdk` consumers while maximizing compatibility with the markets app's current action-flow shape. +- Keep identical `TransactionPlan` lifecycle semantics across Blue, MarketV1, vault, and Midnight flows. +- Preserve narrow action and prerequisite request types through prepared and executable plans. - Make requirement ordering explicit enough for multi-step Midnight flows. - Keep Midnight bundle token pulls approval-only in the first implementation, matching the current markets app builders. - Add SDK support for constructing fixed-rate offer chains, because the markets app currently needs this protocol utility to build one maker order from several time-bounded offers. @@ -86,39 +101,40 @@ The first implementation should be limited to surface area the current markets a ## Decision -Implement Midnight as regular `morpho-sdk` entity / action flows that return the same lazy output shape as existing SDK flows. Do not introduce a second SDK flow engine. Instead, widen the existing action output / requirement interfaces just enough for the markets app's current `ActionFlow` engine to adapt the SDK result with one shared adapter. - -Concretely, keep `buildTx` as the final transaction builder and widen `getRequirements` into an **ordered list of pre-execution items**. +Implement Midnight as regular `morpho-sdk` entity / action flows returning `TransactionPlan`. +Do not introduce a second SDK flow engine. The plan prepares an ordered review surface, leaves +wallet signing to the integrator, and builds one executable sequence after signatures are supplied. ```ts -export interface ActionOutput< - TAction extends BaseAction = TransactionAction, - TSignatures = RequirementSignature, -> { - readonly getRequirements: () => Promise; - readonly buildTx: (signatures?: TSignatures) => Readonly>; -} - -export type MidnightActionSignatures = - | MidnightOfferRootSignature - | readonly MidnightOfferRootSignature[]; +const plan: TransactionPlan< + MempoolSubmitOffersAction, + undefined, + ActionRequirement, + MidnightActionSignatures +> = await midnight.makeLend(params); + +const prepared = await plan.prepare(); +// The app presents prepared.signatureRequests and prepared.callRequests. +const executable = prepared.build(collectedSignatures); ``` Semantics: -1. `getRequirements()` returns every item that must be satisfied **before** `buildTx()`'s transaction is sent. -2. Items are already filtered: if an approval / authorization is not needed, it is omitted. -3. Returned transaction items are ordered and must be executed in relative order. -4. Maker signature items in the initial Midnight implementation may be collected before transaction items because the signed offer-tree typed data is fully determined during entity resolution and does not depend on a prerequisite transaction being mined. -5. A transaction item is not necessarily an approval; it can be an authorization, contract-wallet ratify-root, or mandatory prelude transaction. -6. A signature item returns a `RequirementSignature` value. Existing one-signature flows may pass that value directly into `buildTx(signature)`; Midnight maker flows use `MidnightActionSignatures` and may pass the collected `readonly MidnightOfferRootSignature[]` into `buildTx(signatures)`. -7. Existing methods may keep narrower return types; new Midnight methods use `ActionRequirement`. +1. `prepare()` resolves the currently required prerequisite descriptors and returns semantic request metadata. +2. Already-satisfied approvals or authorizations are omitted. +3. `steps` and `callRequests` preserve relative execution order; the primary call is last when present. +4. Signature descriptors are exposed through `signatureRequests`; the SDK never prompts the wallet on its own. +5. A prerequisite call can be an approval, authorization, contract-wallet ratify-root, or mandatory prelude transaction. +6. `build(signatures)` validates and consumes the collected signature values before encoding the primary call. +7. `TRequest` remains available on prepared steps, and `TPrimaryAction` remains available on executable calls. -This is the smallest compatible change: Midnight flows that are one final tx remain one final tx, flows with required prelude txs place those prelude txs in `getRequirements()`, and the markets app can forward the maker signature it already collects instead of learning a keyed SDK-owned flow engine. +Midnight flows that need one final transaction remain single-call plans. Flows with mandatory prelude +transactions expose them as preparation-phase call requests, and the markets app forwards the maker +signature it already collects into `build(...)`. ## Description: markets app migration boundary -The markets app can keep its UI-specific `ActionFlow` execution engine. Because the SDK implementation is based on the app's current protocol builders, the migration target is a thin adapter from the proposed SDK `ActionOutput` into the app's existing `signatureRequests` / `callRequests` shape, not a port of `ActionFlow` into the SDK. +The markets app can keep its UI-specific `ActionFlow` execution engine. Because the SDK implementation is based on the app's current protocol builders, the migration target is a thin adapter from the proposed SDK `TransactionPlan` into the app's existing `signatureRequests` / `callRequests` shape, not a port of `ActionFlow` into the SDK. The concrete SDK implementation in the stacked implementation PR moves protocol execution into `morpho-sdk`, while leaving rate-form and display decisions in the markets app: @@ -153,17 +169,17 @@ Maker flows remain mempool flows, not bundle flows: The markets app can adapt SDK output once and reuse the adapter across every screen. The adapter description here is intentionally illustrative, not an implementation-ready patch. The implementation PR can choose different function names, labels, and control flow as long as the boundary stays the same: -- SDK action outputs expose requirements and one final transaction builder; +- SDK transaction plans expose typed signature and call requests plus a final executable builder; - the app wallet layer turns SDK signature requirements into signature prompts; - the app turns SDK transaction requirements into ordered call requests; -- collected maker signatures are passed back to the final `buildTx` call; +- collected maker signatures are passed to `PreparedTransactionPlan.build(...)`; - labels, token roles, display copy, and success behavior stay in the app. The adapter preserves the current markets app UX where maker signature prompts are collected before transactions are sent. EOA maker offer-tree signatures do not depend on prior Midnight authorization or collateral-supply transactions, so grouping signatures first is protocol-compatible for the flows covered by this TIB. If a future Midnight flow introduces a signature that depends on a mined prerequisite transaction, the app adapter should gain an explicit dependency concept then. The label mapper stays in the markets app. It can map requirement types such as offer-tree signatures, ERC20 approvals, Midnight authorizations, ratify-root transactions, and collateral-supply transactions to screen-specific copy. This remains app-side because it depends on display concepts (`loan token`, `collateral token`, token symbols, and screen-specific final labels) that do not belong in `morpho-sdk`. -If a markets app screen needs protocol metadata for follow-up behavior, the Midnight method can return a method-specific subtype that structurally extends `ActionOutput` with readonly metadata. The concrete maker flows return protocol fields such as `group`, `root`, and `ratifierType`; review-only display state such as `offerExpiry` stays in the markets app because the app owns display preparation while the SDK owns offer-set normalization, tree construction, and submit payload construction. That does not change the core `{ getRequirements, buildTx }` interface, and the app decides how to display the metadata. +If a markets app screen needs protocol metadata for follow-up behavior, the Midnight method can return a method-specific subtype that structurally extends `TransactionPlan` with readonly metadata. The concrete maker flows return protocol fields such as `group`, `root`, and `ratifierType`; review-only display state such as `offerExpiry` stays in the markets app because the app owns display preparation while the SDK owns offer-set normalization, tree construction, and submit payload construction. That does not change the core `TransactionPlan` interface, and the app decides how to display the metadata. ### Example 1: take-lend taker flow @@ -418,7 +434,7 @@ export interface MidnightOfferRootSignatureArgs { } ``` -`MidnightOfferRootSignatureArgs.payload` is the encoded mempool payload produced after the root signature is collected by the SDK requirement. `buildTx(signatures?)` selects the `midnightOfferRootSignature` result from the collected signature list, validates that its owner, root, ratifier, and offer count match the prepared flow, then uses its payload as the final submit calldata. +`MidnightOfferRootSignatureArgs.payload` is the encoded mempool payload produced after the root signature is collected by the SDK requirement. `PreparedTransactionPlan.build(signatures)` selects the `midnightOfferRootSignature` result from the collected signature list, validates that its owner, root, ratifier, and offer count match the prepared flow, then uses its payload as the final submit calldata. ### New final action metadata @@ -529,7 +545,7 @@ Extend `TransactionAction` with these action interfaces and the Midnight require ### Midnight approval requirement helper -Do not reuse the top-level Blue / MarketV1 `getRequirements(...)` helper for Midnight, because it hardcodes `bundler3.generalAdapter1` as spender and its signature paths emit Bundler3 actions. The first Midnight action-flow implementation mirrors the current markets app and returns classic ERC-20 approval transactions only for token pulls. The spender is explicit because current flows need both `Midnight` (maker reserves and direct collateral supply) and `MidnightBundles` (taker and repay/withdraw bundle calls). +Do not reuse `getGeneralAdapterRequirements(...)` for Midnight, because it hardcodes `bundler3.generalAdapter1` as spender and its signature paths emit Bundler3 actions. The first Midnight transaction-plan implementation mirrors the current markets app and returns classic ERC-20 approval transactions only for token pulls. The spender is explicit because current flows need both `Midnight` (maker reserves and direct collateral supply) and `MidnightBundles` (taker and repay/withdraw bundle calls). ```ts async function getMidnightApprovalRequirements({ @@ -619,13 +635,13 @@ EOA / EIP-7702 maker: - optional `MidnightAuthorizationAction` for `EcrecoverRatifier`; - one private `makeOfferRootRequirement(...)` result with `action.type === "midnightOfferRootSignature"`; - `Requirement.sign(...)` calls the same typed-data root-signing path as the markets app and returns `{ action, args: { root, signature, payload } }`; -- `buildTx(signatures?)` selects the `midnightOfferRootSignature` result, validates the owner / root / ratifier / offer-count metadata against the prepared flow, and uses `signature.args.payload` as mempool calldata. +- `PreparedTransactionPlan.build(signatures)` selects the `midnightOfferRootSignature` result, validates the owner / root / ratifier / offer-count metadata against the prepared flow, and uses `signature.args.payload` as mempool calldata. Contract-wallet maker: - optional `MidnightAuthorizationAction` for `SetterRatifier`; - one `SetterRatifierRatifyRootAction` transaction requirement from `getSetterRatifierRatifyRootRequirement(...)`, calling `SetterRatifier.setIsRootRatified(maker, root, true)` only when missing; -- `buildTx()` uses precomputed `Payload.encode(SetterRatifierUtils.ratify({ tree }))` as mempool calldata. +- `PreparedTransactionPlan.build()` uses precomputed `Payload.encode(SetterRatifierUtils.ratify({ tree }))` as mempool calldata. ## Layering @@ -657,12 +673,12 @@ These fields should not be first-iteration public inputs. Exposing them before t ### Take lend -`getRequirements()` returns: +`prepare()` exposes these prerequisite requests: 1. optional loan-token approval requirement for `MidnightBundles`; 2. optional `MidnightAuthorizationAction` for `Midnight.setIsAuthorized(MidnightBundles, true, taker)`. -`buildTx()` returns `MidnightTakeLendAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightTakeLendAction`: ```ts MidnightBundles.midnightBundlesV1BuyWithAssetsTargetAndWithdrawCollateral( @@ -685,12 +701,12 @@ No offer-root or token signature is involved in the first implementation. ### Take borrow with `loanAssets > 0` -`getRequirements()` returns: +`prepare()` exposes these prerequisite requests: 1. optional collateral-token approval requirement for `MidnightBundles` when new collateral is supplied; 2. optional `MidnightAuthorizationAction` for `Midnight.setIsAuthorized(MidnightBundles, true, taker)`. -`buildTx()` returns `MidnightTakeBorrowAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightTakeBorrowAction`: ```ts MidnightBundles.midnightBundlesV1SupplyCollateralAndSellWithAssetsTarget( @@ -712,9 +728,9 @@ No offer-root or token signature is involved in the first implementation. ### Take borrow supply-only branch -`getRequirements()` returns optional collateral approval to `Midnight`. +`prepare()` exposes an optional collateral approval to `Midnight`. -`buildTx()` returns `MidnightSupplyCollateralAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightSupplyCollateralAction`: ```ts Midnight.supplyCollateral(market, 0n, collateralAssets, onBehalf) @@ -724,7 +740,7 @@ This branch remains approval-based because direct `Midnight.supplyCollateral(... ### Make lend -`getRequirements()` returns: +`prepare()` exposes these prerequisite requests: EOA / EIP-7702: @@ -738,7 +754,7 @@ Contract wallet: 2. optional `MidnightAuthorizationAction` for the chosen ratifier; 3. one `setterRatifierRatifyRoot` transaction requirement. -`buildTx(signatures?)` returns `MempoolSubmitOffersAction` to the mempool contract. +`PreparedTransactionPlan.build(signatures)` produces an executable plan whose primary call carries `MempoolSubmitOffersAction` and targets the mempool contract. The make-lend method accepts a tree-like offer set. It must accept multi-market offer legs in the same tree when the markets share one loan token, matching the markets app's multi-limit-order / OCA basket flow. For those baskets, offers in the same group share one `consumed[maker][group]` counter on Midnight, so the new group contributes one reserve amount: the maximum leg reserve, with equal leg reserves expected for the current OCA shape. It is not the sum of every offer leg. The SDK throws a typed error when the tree would exceed the Midnight tree-size limit. @@ -750,15 +766,15 @@ Maker reserve approvals stay transaction approvals in this migration. The final ### Borrow limit collateral-only branch -`getRequirements()` returns optional collateral approval to `Midnight`. +`prepare()` exposes an optional collateral approval to `Midnight`. -`buildTx()` returns `MidnightSupplyCollateralAction`. +`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightSupplyCollateralAction`. This branch remains approval-based because direct `Midnight.supplyCollateral(...)` has no `TokenPermit` argument. ### Make borrow loan-only branch -`getRequirements()` returns: +`prepare()` exposes these prerequisite requests: EOA / EIP-7702: @@ -770,11 +786,11 @@ Contract wallet: 1. optional `MidnightAuthorizationAction` for the chosen ratifier; 2. one `setterRatifierRatifyRoot` transaction requirement. -`buildTx(signatures?)` returns `MempoolSubmitOffersAction` to the mempool contract. +`PreparedTransactionPlan.build(signatures)` produces an executable plan whose primary call carries `MempoolSubmitOffersAction` and targets the mempool contract. ### Borrow limit collateral + loan branch -`getRequirements()` returns: +`prepare()` exposes these prerequisite requests: EOA / EIP-7702: @@ -790,13 +806,13 @@ Contract wallet: 3. optional `MidnightAuthorizationAction` for the chosen ratifier; 4. one `setterRatifierRatifyRoot` transaction requirement. -`buildTx(signatures?)` returns only the final `MempoolSubmitOffersAction`. +`PreparedTransactionPlan.build(signatures)` produces an executable plan containing only the final `MempoolSubmitOffersAction` primary call. -This branch is the reason `getRequirements()` must be allowed to return mandatory prelude transactions, not only optional prerequisites. +This branch is the reason `prepare()` must be allowed to return mandatory prelude transactions, not only optional prerequisites. ### Redeem at maturity -Pre-read / validation happens before returning the action output: +Pre-read / validation happens before returning the transaction plan: - `updatePositionView(...)` gives accrued `creditUnits` and remaining `pendingFeeUnits`; - compute `redeemUnits = creditUnits - pendingFeeUnits`, using the `midnight-sdk` `positionData.faceValue` getter when the SDK consumer provides an `AccrualPosition`; @@ -811,9 +827,9 @@ Do not default this flow to raw, unaccrued `positionData.credit`. `Midnight.with The latest `morpho-org/midnight` implementation does not cap withdrawals at net face value. After `_updatePosition(...)`, `Midnight.withdraw(...)` decreases `pendingFee` pro rata and burns `units` from the updated `credit`; the protocol-compatible cap for explicit `units` is therefore the lower of accrued `creditUnits` and market `withdrawable` liquidity. This intentionally keeps the SDK default at net face value while still allowing integrators to request another protocol-valid partial amount explicitly. -`getRequirements()` returns `[]`. +`prepare()` exposes no prerequisite requests. -`buildTx()` returns `MidnightRedeemAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightRedeemAction`: ```ts Midnight.withdraw(market, redeemUnits, onBehalf, receiver) @@ -825,18 +841,18 @@ All three app branches keep the current bundled execution path. Repay only: -- `getRequirements()` returns optional loan-token approval requirement for `MidnightBundles`, then optional `MidnightAuthorizationAction` for `MidnightBundles`; -- `buildTx()` returns `MidnightRepayWithdrawCollateralAction`. +- `prepare()` exposes an optional loan-token approval requirement for `MidnightBundles`, then optional `MidnightAuthorizationAction` for `MidnightBundles`; +- `PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightRepayWithdrawCollateralAction`. Withdraw-only: -- `getRequirements()` returns optional `MidnightAuthorizationAction` for `MidnightBundles`; -- `buildTx()` returns `MidnightRepayWithdrawCollateralAction` with `repayAssets === 0n`. +- `prepare()` exposes an optional `MidnightAuthorizationAction` for `MidnightBundles`; +- `PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightRepayWithdrawCollateralAction` with `repayAssets === 0n`. Repay + withdraw: -- `getRequirements()` returns optional loan-token approval requirement for `MidnightBundles`, then optional `MidnightAuthorizationAction` for `MidnightBundles`; -- `buildTx()` returns `MidnightRepayWithdrawCollateralAction`. +- `prepare()` exposes an optional loan-token approval requirement for `MidnightBundles`, then optional `MidnightAuthorizationAction` for `MidnightBundles`; +- `PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightRepayWithdrawCollateralAction`. ```ts MidnightBundles.midnightBundlesV1RepayAndWithdrawCollateral( @@ -854,9 +870,9 @@ MidnightBundles.midnightBundlesV1RepayAndWithdrawCollateral( ### Cancel offer -`getRequirements()` returns `[]`. +`prepare()` exposes no prerequisite requests. -`buildTx()` returns `MidnightCancelOfferAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightCancelOfferAction`: ```ts Midnight.setConsumed(group, maxUint256, onBehalf) @@ -864,10 +880,10 @@ Midnight.setConsumed(group, maxUint256, onBehalf) ## Compatibility checklist -This proposal is compatible with the current markets app flows, with the documented redeem default divergence, because: +This implementation represents the current markets app flows, with the documented redeem default divergence, because: -- every app `CallRequest` maps either to a `CallRequirement` or to `buildTx()`; -- every app maker `SignatureRequest` maps to `Requirement.sign(...)`; +- every prerequisite transaction maps to a preparation-phase `callRequest`, and every primary transaction maps to the primary `callRequest` returned by `PreparedTransactionPlan.build()`; +- every maker signature requirement maps to a `signatureRequest` whose `sign(...)` delegates to `Requirement.sign(...)`; - the `Transaction` wire shape is unchanged; - no app builder currently needs `before` / `after` callback semantics; - bundle token pulls keep the approval-transaction behavior the markets app uses today; @@ -876,7 +892,7 @@ This proposal is compatible with the current markets app flows, with the documen - contract-wallet maker flow needs zero signatures and one ratify-root tx; - EOA maker signatures can be surfaced before transaction requirements, preserving the markets app's current signature-before-calls UX; - repay / withdraw keeps the app's existing single final `MidnightBundles.midnightBundlesV1RepayAndWithdrawCollateral(...)` transaction; -- all multi-tx app flows can be represented by ordered requirements plus final `buildTx()`. +- all multi-tx app flows can be represented by ordered requirements plus final `PreparedTransactionPlan.build()`. - redeem defaults to net face value, while explicit `units` can still reproduce the current app's post-update credit behavior as long as the amount does not exceed accrued credit or market withdrawable liquidity. The only semantic expansion is documented: returned transaction requirements are **ordered pre-execution items** and can include mandatory prelude transactions. Consumers must execute every returned item in order unless they intentionally replace it with an equivalent already-satisfied state. @@ -885,9 +901,9 @@ The only semantic expansion is documented: returned transaction requirements are ### Alternative 1: Add `buildTxs()` -Return the whole transaction sequence from the action output. +Return the whole transaction sequence from the transaction plan. -**Why rejected:** larger public API change, duplicates `getRequirements`, and forces every existing SDK consumer to learn a second execution model. The current app flows only need one final tx plus ordered pre-execution items. +**Why rejected:** it would add a second way to expose the ordered call sequence already represented by prepared and executable plans. The current app flows only need one final transaction plus ordered pre-execution items. ### Alternative 2: Port the app `ActionFlow` abstraction @@ -895,7 +911,7 @@ Copy `signatureRequests`, `callRequests`, `before`, and `after` into the SDK. **Why rejected:** this imports UI execution-engine concepts into a pure SDK package. The current markets app builders define no `before` / `after` requirements, so the extra machinery buys nothing for the initial migration. -### Alternative 3: Keep `getRequirements()` limited to approvals / authorizations +### Alternative 3: Keep preparation-phase calls limited to approvals / authorizations Expose only optional prerequisites and force callers to build prelude txs manually. @@ -909,18 +925,18 @@ Add ERC2612 and Permit2 SignatureTransfer support to the first action-flow imple ## Implementation phases -- **Phase 1 — Shared action-flow types / interfaces.** Add `ActionRequirement`, `CallRequirement`, widened `Requirement` / `RequirementSignature` unions, Midnight action interfaces, and type guards. This is the compatibility layer that lets the markets app keep its existing `ActionFlow` signature / call collection model while consuming SDK-built Midnight flows. Existing Blue / MarketV1 / vault methods keep their narrow return types. -- **Phase 2 — Requirement helpers.** Export / reuse `getRequirementsApproval` with explicit spender; add Midnight approval, authorization, and ratifier helpers. +- **Phase 1 — Shared transaction-plan types / interfaces.** Add `TransactionPlan`, `PreparedTransactionPlan`, and `ExecutableTransactionPlan`; preserve the primary action and prerequisite-request types across every lifecycle stage; and add the Midnight action interfaces and type guards. The markets app keeps its existing `ActionFlow` signature / call collection model through one adapter, while Blue, MarketV1, vault, and Midnight methods expose narrow plan types. +- **Phase 2 — Requirement helpers.** Reuse `getRequirementsApproval` with an explicit spender rather than `getGeneralAdapterRequirements`; add Midnight approval, authorization, and ratifier helpers. - **Phase 3 — Pure action encoders.** Add `src/actions/midnight/*` encoders for final txs and prelude txs. Every encoder returns a deep-frozen `Transaction` and has colocated unit tests. -- **Phase 4 — Entity methods.** Add `MorphoMidnight` methods that perform RPC/off-chain reads, router validation, amount math, group generation, and return `{ getRequirements, buildTx }`. +- **Phase 4 — Entity methods.** Add `MorphoMidnight` methods that perform RPC/off-chain reads, router validation, amount math, group generation, and return `TransactionPlan`. - **Phase 5 — Integration tests.** Fork-test each flow shape: no requirement, approval reset, missing authorization, EOA root signature, contract-wallet ratify-root, mandatory prelude txs, and cancel offer. -- **Phase 6 — Docs / changeset.** Update package `AGENTS.md`, generated docs/JSDoc, README snippets, and add a minor changeset when code lands. +- **Phase 6 — Docs / changeset.** Update package `AGENTS.md`, generated docs/JSDoc, README snippets, and add the major changeset required by the action-output-to-transaction-plan API migration. ## Security - **Wallet-decodable offer-tree signing.** The SDK must build the offer tree locally from the SDK input and validate the router response before exposing `midnightOfferRootSignature`. The wallet signs EIP-712 `OfferTree` typed data whose leaves are visible to the user, and the SDK verifies that the signed tree hashes to the root used in ratifier data. - **No signing inside actions.** `Requirement.sign(...)` is the only signing boundary and takes a `WalletClient` from the integrator. -- **No hidden prelude txs.** Mandatory prelude transactions are visible in `getRequirements()` as typed `Transaction` values. +- **No hidden prelude txs.** Mandatory prelude transactions are visible as typed preparation-phase `callRequests` on the prepared plan. - **Authorization target is explicit.** `MidnightAuthorizationAction.args.authorized` is either `MidnightBundles`, `EcrecoverRatifier`, or `SetterRatifier`; never inferred by a consumer. - **Approval target is explicit.** Midnight approval helper callers pass `spender`; no default to `GeneralAdapter1`. - **Bundle token-pull policy is explicit.** Bundle flows use `MidnightBundles` as spender and never consume or reset the core `Midnight` allowance reserved by maker offers. @@ -930,12 +946,12 @@ Add ERC2612 and Permit2 SignatureTransfer support to the first action-flow imple ## Future considerations - If a future Midnight flow needs a real wait condition (`before` / `after` equivalent), add a small `wait` requirement kind at that time. Do not preemptively port app `ActionFlow`. -- If consumers strongly reject mandatory prelude transactions inside `getRequirements()`, revisit `buildTxs()` with evidence from integration feedback. +- If consumers strongly reject mandatory preparation-phase `callRequests`, revisit a dedicated sequence-building API with evidence from integration feedback. ## References - `packages/morpho-sdk/src/types/action.ts` — current `Transaction`, `Requirement`, and action unions. -- `packages/morpho-sdk/src/actions/requirements/getRequirements.ts` — current GeneralAdapter1-oriented requirement helper. +- `packages/morpho-sdk/src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.ts` — current GeneralAdapter1-oriented requirement helper. - `packages/morpho-sdk/src/actions/requirements/getRequirementsApproval.ts` — lower-level approval helper to reuse with explicit spender. - `packages/midnight-sdk/src/signatures/{Group,Tree,Payload,EcrecoverRatifierUtils,SetterRatifierUtils}.ts` — existing framework-free Midnight group, tree, payload, and ratifier utilities that the hypothetical `morpho-sdk` flows should reuse or mirror. - `morpho-org/morpho-apps/apps/markets-app/lib/modules/order/actions/lend-market/buildLendMarketOrderActionFlow.ts` — lend-market app flow. diff --git a/packages/morpho-sdk/ARCHITECTURE.md b/packages/morpho-sdk/ARCHITECTURE.md index 2350c5358..a89010d50 100644 --- a/packages/morpho-sdk/ARCHITECTURE.md +++ b/packages/morpho-sdk/ARCHITECTURE.md @@ -117,7 +117,7 @@ at the SDK level. The differences are at the protocol layer: - **Maker routing**: Maker flows build and validate offer trees, collect an Ecrecover root signature or SetterRatifier transaction, then submit the payload to the Midnight mempool. - **SDK data**: `MorphoMidnight` fetches hydrated market and position snapshots and exposes - the same lazy `{ getRequirements, buildTx }` contract as the other entities. + the same lazy `TransactionPlan` lifecycle as the other entities. ### Force Deallocation (V2 only) @@ -283,16 +283,16 @@ getRequirements(viemClient, params) ### How signatures flow into deposits When requirements return a `Requirement` object (permit, permit2, or Morpho authorization path), -the consuming application calls `requirement.sign(client, userAddress)` to obtain a -`RequirementSignature`. The collected signatures are then passed to `buildTx` as an array -(`buildTx([...signatures])`), letting a permit and a Morpho authorization signature travel -together: +`TransactionPlan.prepare()` exposes it as a signature request. The consuming application calls +`request.sign(client, userAddress)` to obtain a `RequirementSignature`, then passes the collected +signatures to `PreparedTransactionPlan.build([...signatures])`, letting a permit and a Morpho +authorization signature travel together: ``` -getRequirements() → Requirement { sign() } → RequirementSignature → buildTx([sig, ...]) +prepare() → signatureRequests → RequirementSignature → build([sig, ...]) ``` -Inside `buildTx`, `getTokenRequirementActions()` converts the signature into bundler actions: +Inside the primary transaction builder, `getTokenRequirementActions()` converts the signature into bundler actions: - **Permit path**: `permit` action + `erc20TransferFrom` to generalAdapter1. - **Permit2 path**: `approve2` action + `transferFrom2` to generalAdapter1. @@ -300,9 +300,9 @@ Inside `buildTx`, `getTokenRequirementActions()` converts the signature into bun These actions are prepended to the `erc4626Deposit` action in the bundle. The entire sequence executes atomically in a single transaction. -When no signature is provided (classic approval path), `buildTx()` uses a simple -`erc20TransferFrom` action to move tokens from the user to the general adapter before the -deposit. +When no signature is provided (classic approval path), the prepared plan exposes the required +approval as a preparation-phase call and the primary builder uses a simple `erc20TransferFrom` +action to move tokens from the user to the general adapter before the deposit. ### Guard functions diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index 849a9e1a9..b0e450122 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -18,7 +18,7 @@ Instead of exposing the user directly to target contracts (ERC-4626 vault, Morph - executes `morphoSupplyCollateral`, `morphoBorrow`, `morphoRepay`, `morphoWithdrawCollateral` on Morpho Blue, - forwards `reallocateTo` calls to the `PublicAllocator` for shared liquidity. -The **spender** of every approval / permit / permit2 is therefore **always** `generalAdapter1`, never the vault or Morpho directly. See [src/actions/requirements/getRequirements.ts](src/actions/requirements/getRequirements.ts) and the "Requirements System" section of [ARCHITECTURE.md](ARCHITECTURE.md#requirements-system). +The **spender** of every approval / permit / permit2 is therefore **always** `generalAdapter1`, never the vault or Morpho directly. See [`getGeneralAdapterRequirements`](src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.ts) and the "Requirements System" section of [ARCHITECTURE.md](ARCHITECTURE.md#requirements-system). ## Composability & modularity @@ -70,7 +70,7 @@ For every ERC-4626 deposit (VaultV1 / VaultV2), GeneralAdapter1 calls `erc4626De ### 5. A single approval surface -Whether it's a V1 deposit, a V2 deposit, a `supplyCollateral`, a `repay`, or a `supplyCollateralBorrow`: the spender is **always** `generalAdapter1`. A user who has already approved GA1 for a given token transparently reuses that approval. The approval / permit / permit2 decision is centralized in [`getRequirements`](src/actions/requirements/getRequirements.ts). +Whether it's a V1 deposit, a V2 deposit, a `supplyCollateral`, a `repay`, or a `supplyCollateralBorrow`: the spender is **always** `generalAdapter1`. A user who has already approved GA1 for a given token transparently reuses that approval. The approval / permit / permit2 decision is centralized in [`getGeneralAdapterRequirements`](src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.ts). ## Dangers & limits diff --git a/packages/morpho-sdk/src/client/morphoViemExtension.ts b/packages/morpho-sdk/src/client/morphoViemExtension.ts index 9b0b1f209..ac52c7a91 100644 --- a/packages/morpho-sdk/src/client/morphoViemExtension.ts +++ b/packages/morpho-sdk/src/client/morphoViemExtension.ts @@ -85,7 +85,10 @@ function createMorphoNamespace( * ```ts * import { createWalletClient, http, publicActions } from "viem"; * import { mainnet } from "viem/chains"; - * import { morphoViemExtension } from "@morpho-org/morpho-sdk"; + * import { + * morphoViemExtension, + * type RequirementSignature, + * } from "@morpho-org/morpho-sdk"; * * const client = createWalletClient({ * chain: mainnet, @@ -104,7 +107,12 @@ function createMorphoNamespace( * userAddress: user, * vaultData, * }); - * const tx = (await plan.prepare()).build().callRequests.at(-1)?.tx; + * const prepared = await plan.prepare(); + * const signatures: RequirementSignature[] = []; + * for (const request of prepared.signatureRequests) { + * signatures.push(await request.sign(client, user)); + * } + * const tx = prepared.build(signatures).callRequests.at(-1)?.tx; * ``` */ export function morphoViemExtension(_options?: { diff --git a/packages/morpho-sdk/src/entities/AGENTS.md b/packages/morpho-sdk/src/entities/AGENTS.md index 363eb78d0..8e6c1f339 100644 --- a/packages/morpho-sdk/src/entities/AGENTS.md +++ b/packages/morpho-sdk/src/entities/AGENTS.md @@ -7,7 +7,7 @@ - Fetch on-chain state (vault accrual data, market/position data). - Compute derived values (e.g. `maxSharePrice` with slippage, LLTV buffer health). - Validate `chainId` matches the client before any on-chain read or transaction construction. Entities do not enforce builder = signer at build time — callers MUST keep `userAddress` aligned with the signing account. The invariant is enforced at `sign()` time on the signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) via `validateUserAddress`. -- Return lazy `{ buildTx, getRequirements }` handles — no side effects at construction. +- Return lazy `TransactionPlan` instances — no side effects at construction. `prepare()` performs targeted prerequisite discovery; the prepared plan exposes signature/call requests and builds the executable call sequence. ## Routing diff --git a/packages/morpho-sdk/src/entities/blue/AGENTS.md b/packages/morpho-sdk/src/entities/blue/AGENTS.md index 8281c48ae..43f58d6e8 100644 --- a/packages/morpho-sdk/src/entities/blue/AGENTS.md +++ b/packages/morpho-sdk/src/entities/blue/AGENTS.md @@ -18,11 +18,11 @@ ## Authorization requirements -`getRequirements` returns: +`TransactionPlan.prepare()` returns: - ERC-20 approval for **GeneralAdapter1** on the collateral token (any path that supplies collateral) or the loan token (`supply`, `repay`, `repayWithdrawCollateral`). The approved amount is the **ERC-20 portion actually pulled**, not the total: for a native-funded repay it is `amount` (assets mode) or `max(0, toBorrowAssets(shares) āˆ’ nativeAmount)` (shares mode — clamped at 0 so a `nativeAmount` that covers or exceeds the borrow assets pulls nothing). A fully-native repay pulls no ERC-20, so no approval requirement is emitted; in shares mode any wrapped native beyond the on-chain repay is skimmed back to the receiver. - `morpho.setAuthorization(generalAdapter1, true)` when authorization is not yet set on Morpho — read via `publicActions`. Required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, and `withdraw` (loan-asset). -When `supportSignature` is enabled on the client, the authorization requirement is returned as a signable `Requirement` instead of a transaction; signing it produces an `AuthorizationRequirementSignature` that `buildTx` consumes and folds into the bundle as a `setAuthorizationWithSig` call, so no standalone authorization transaction is needed. `buildTx` accepts a `readonly RequirementSignature[]` and splits permit vs. authorization signatures via `isPermitSignature` / `isAuthorizationSignature`. +When `supportSignature` is enabled on the client, the authorization request is returned as a signable `Requirement` instead of a transaction; signing it produces an `AuthorizationRequirementSignature` that `PreparedTransactionPlan.build(...)` consumes and folds into the bundle as a `setAuthorizationWithSig` call, so no standalone authorization transaction is needed. The prepared plan accepts a `readonly RequirementSignature[]` and splits permit vs. authorization signatures via `isPermitSignature` / `isAuthorizationSignature`. `withdrawCollateral` has no requirements. `repay` and `supply` need only loan-token approval (native wrapping requires the loan token to be the chain's wNative). Loan-asset `withdraw` needs only the Morpho authorization. diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts index 7590cf7e2..e1ee946ed 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts @@ -1356,7 +1356,6 @@ describe("MorphoMidnight", () => { const tx = await buildPlanTx(output, signature); expect(output.groups).toEqual(data.groups); - expect(output).toBeInstanceOf(TransactionPlan); expect(output.root).toBe(data.tree.root); expect(output.ratifierType).toBe("ecrecover"); expect(tx.action.args).toMatchObject({ diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.ts b/packages/morpho-sdk/src/entities/midnight/midnight.ts index a10b4b963..c119122a1 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.ts @@ -115,7 +115,11 @@ import type { * deadline: maxUint256, * }); * const prepared = await plan.prepare(); - * const executable = prepared.build(await prepared.signAll(walletClient, lender)); + * const signatures = []; + * for (const request of prepared.signatureRequests) { + * signatures.push(await request.sign(walletClient, lender)); + * } + * const executable = prepared.build(signatures); * const calls = executable.callRequests.map((request) => request.call); * ``` */ @@ -692,7 +696,7 @@ export class MorphoMidnight { * Validates lend-side maker offers and prepares their reserve and ratifier requirements. * * Calls the Midnight mempool validation API while preparing the offer tree; - * allowance and ratifier state are read lazily by `getRequirements()`. + * allowance and ratifier state are read lazily by `TransactionPlan.prepare()`. * * @param params - Maker, raw offers, loan token, reserve amounts, and validation controls. * @param params.accountAddress - Maker expected on every offer. @@ -701,7 +705,7 @@ export class MorphoMidnight { * @param params.loanToken - Loan token shared by every offer market. * @param params.loanAssets - New loan reserve assigned to the submitted groups. * @param params.reservedLoanAssets - Existing loan assets reserved by other open groups. - * @returns Prepared group metadata, lazy requirements, and a synchronous mempool transaction builder. + * @returns Prepared group metadata and a lazy transaction plan for the mempool submission flow. * @throws {ChainIdMismatchError} when the client targets another chain. * @throws {MidnightOfferMarketAddressMismatchError} when an offer targets another Midnight deployment. * @throws {NonPositiveMidnightAmountError} when `loanAssets` is non-positive. diff --git a/packages/morpho-sdk/src/entities/midnight/types.ts b/packages/morpho-sdk/src/entities/midnight/types.ts index df4aaa498..4e258b93c 100644 --- a/packages/morpho-sdk/src/entities/midnight/types.ts +++ b/packages/morpho-sdk/src/entities/midnight/types.ts @@ -72,7 +72,7 @@ export interface SupplyCollateralMakeBorrowParams extends MakeOffersParams { readonly collateralIndex?: bigint; } -/** Signatures accepted by Midnight action-output transaction builders. */ +/** Signatures accepted when building Midnight transaction plans. */ export type MidnightActionSignatures = | MidnightOfferRootSignature | readonly MidnightOfferRootSignature[]; @@ -85,7 +85,10 @@ export type MidnightActionSignatures = * ```ts * const plan = midnight.takeLend(params); * const prepared = await plan.prepare(); - * const signatures = await prepared.signAll(walletClient, taker); + * const signatures = []; + * for (const request of prepared.signatureRequests) { + * signatures.push(await request.sign(walletClient, taker)); + * } * const executable = prepared.build(signatures); * const calls = executable.callRequests.map((request) => request.call); * ``` @@ -106,7 +109,10 @@ export type MidnightActionOutput< * const plan = await midnight.makeLend(params); * console.log(plan.root, plan.groups); * const prepared = await plan.prepare(); - * const signatures = await prepared.signAll(walletClient, maker); + * const signatures = []; + * for (const request of prepared.signatureRequests) { + * signatures.push(await request.sign(walletClient, maker)); + * } * const executable = prepared.build(signatures); * const calls = executable.callRequests.map((request) => request.call); * ``` diff --git a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts index 723e2f91e..92286a2e3 100644 --- a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts +++ b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts @@ -74,7 +74,8 @@ export interface VaultV1Actions { * @param {AccrualVault} params.vaultData - Pre-fetched vault data with asset address and share conversion. * @param {bigint} [params.slippageTolerance=DEFAULT_SLIPPAGE_TOLERANCE] - Slippage tolerance (default 0.03%, max 10%). * @param {bigint} [params.nativeAmount] - Amount of native ETH to wrap into WETH. Vault asset must be wNative. - * @returns {Object} Object with `buildTx` and `getRequirements`. + * @returns A lazy `TransactionPlan` whose `prepare()` resolves token approval or permit requests + * and whose prepared form builds the executable deposit call. */ deposit: ( params: { @@ -93,7 +94,7 @@ export interface VaultV1Actions { * @param {Object} params - The withdraw parameters. * @param {bigint} params.amount - Amount of assets to withdraw. * @param {Address} params.userAddress - User address initiating the withdraw. - * @returns {Object} Object with `buildTx`. + * @returns A lazy `TransactionPlan` that prepares and builds the direct withdraw call. */ withdraw: (params: { amount: bigint; @@ -105,7 +106,7 @@ export interface VaultV1Actions { * @param {Object} params - The redeem parameters. * @param {bigint} params.shares - Amount of shares to redeem. * @param {Address} params.userAddress - User address initiating the redeem. - * @returns {Object} Object with `buildTx`. + * @returns A lazy `TransactionPlan` that prepares and builds the direct redeem call. */ redeem: (params: { shares: bigint; @@ -123,7 +124,8 @@ export interface VaultV1Actions { * @param {AccrualVaultV2} params.targetVault - Pre-fetched V2 vault data. * @param {bigint} params.shares - User's V1 share balance to migrate. * @param {bigint} [params.slippageTolerance=DEFAULT_SLIPPAGE_TOLERANCE] - Slippage tolerance (default 0.03%, max 10%). - * @returns {Object} Object with `buildTx` and `getRequirements`. + * @returns A lazy `TransactionPlan` whose `prepare()` resolves the V1-share approval request + * and whose prepared form builds the atomic migration call. */ migrateToV2: (params: { userAddress: Address; diff --git a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts index fc6540bae..840f968ff 100644 --- a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts +++ b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts @@ -27,7 +27,6 @@ import { ChainWNativeMissingError, type Deallocation, type DepositAmountArgs, - type ERC20ApprovalAction, ExcessiveSlippageToleranceError, type MorphoClientType, NativeAmountOnNonWNativeVaultError, @@ -35,10 +34,7 @@ import { NegativeSlippageToleranceError, NonPositiveAssetAmountError, NonPositiveSharesAmountError, - type PermitRequirementSignature, - type Requirement, selectRequirementSignatures, - type Transaction, VaultAddressMismatchError, type VaultV2DepositAction, type VaultV2ForceRedeemAction, @@ -70,7 +66,7 @@ export interface VaultV2Actions { * * This function constructs the transaction data required to deposit a specified amount of assets into the vault. * Uses pre-fetched vault data for accurate calculations of slippage and asset address, - * then returns the prepared deposit transaction and a function for retrieving all required approval transactions. + * then returns a lazy transaction plan for resolving approval or signature requests and building the deposit call. * Bundler Integration: This flow uses the bundler to atomically execute the user's asset transfer and vault deposit in a single transaction for slippage protection. * * @param {Object} params - The deposit parameters. @@ -79,9 +75,8 @@ export interface VaultV2Actions { * @param {AccrualVaultV2} params.vaultData - Pre-fetched vault data with asset address and share conversion. * @param {bigint} [params.slippageTolerance=DEFAULT_SLIPPAGE_TOLERANCE] - Optional slippage tolerance value. Default is 0.03%. Slippage tolerance must be less than 10%. * @param {bigint} [params.nativeAmount] - Amount of native token to wrap into wNative. Vault asset must be wNative. - * @returns {Object} The result object. - * @returns {Readonly>} returns.tx The prepared deposit transaction. - * @returns {Promise<(Readonly> | Requirement)[]>} returns.prepare Resolves supporting approval/signature requests plus the primary call. + * @returns A lazy `TransactionPlan` whose `prepare()` resolves token approval or permit requests + * and whose prepared form builds the executable deposit call. */ deposit: ( params: { @@ -102,8 +97,7 @@ export interface VaultV2Actions { * @param {Object} params - The withdraw parameters. * @param {bigint} params.amount - The amount of assets to withdraw. * @param {Address} params.userAddress - User address initiating the withdraw. - * @returns {Object} The result object. - * @returns {Readonly>} returns.tx The prepared withdraw transaction. + * @returns A lazy `TransactionPlan` that prepares and builds the direct withdraw call. */ withdraw: (params: { amount: bigint; @@ -117,8 +111,7 @@ export interface VaultV2Actions { * @param {Object} params - The redeem parameters. * @param {bigint} params.shares - The amount of shares to redeem. * @param {Address} params.userAddress - User address initiating the redeem. - * @returns {Object} The result object. - * @returns {Readonly>} returns.tx The prepared redeem transaction. + * @returns A lazy `TransactionPlan` that prepares and builds the direct redeem call. */ redeem: (params: { shares: bigint; @@ -136,8 +129,7 @@ export interface VaultV2Actions { * @param {Object} params.withdraw - The withdraw parameters applied after deallocations. * @param {bigint} params.withdraw.amount - The amount of assets to withdraw. * @param {Address} params.userAddress - User address (penalty source and withdraw recipient). - * @returns {Object} The result object. - * @returns {Readonly>} returns.prepare Resolves the primary multicall request. + * @returns A lazy `TransactionPlan` that prepares and builds the force-withdraw multicall. */ forceWithdraw: (params: { deallocations: readonly Deallocation[]; @@ -163,8 +155,7 @@ export interface VaultV2Actions { * @param {Object} params.redeem - The redeem parameters applied after deallocations. * @param {bigint} params.redeem.shares - The amount of shares to redeem. * @param {Address} params.userAddress - User address (penalty source and redeem recipient). - * @returns {Object} The result object. - * @returns {Readonly>} returns.prepare Resolves the primary multicall request. + * @returns A lazy `TransactionPlan` that prepares and builds the force-redeem multicall. */ forceRedeem: (params: { deallocations: readonly Deallocation[]; diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts index 684a98b30..b19c62e10 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts @@ -1,5 +1,5 @@ -import type { Address, Hex, WalletClient } from "viem"; -import { describe, expect, test, vi } from "vitest"; +import type { Address, Hex } from "viem"; +import { describe, expect, expectTypeOf, test, vi } from "vitest"; import { type AuthorizationRequirementSignature, type BlueAuthorizationAction, @@ -101,21 +101,17 @@ const authorizationSignature: AuthorizationRequirementSignature = { }, }; -function permitRequest(params?: { - readonly sign?: Requirement["sign"]; -}): Requirement { +function permitRequest(): Requirement { return { action: permitSignature.action, - sign: params?.sign ?? vi.fn(async () => permitSignature), + sign: vi.fn(async () => permitSignature), }; } -function authorizationRequest(params?: { - readonly sign?: Requirement["sign"]; -}): Requirement { +function authorizationRequest(): Requirement { return { action: authorizationSignature.action, - sign: params?.sign ?? vi.fn(async () => authorizationSignature), + sign: vi.fn(async () => authorizationSignature), }; } @@ -182,37 +178,6 @@ describe("TransactionPlan", () => { ]); }); - test("behavior: signs every signature request sequentially", async () => { - let resolveFirst: ((value: PermitRequirementSignature) => void) | undefined; - const firstSign = vi.fn( - () => - new Promise((resolve) => { - resolveFirst = resolve; - }), - ); - const secondSign = vi.fn(async () => authorizationSignature); - const plan = TransactionPlan.create({ - getRequirementRequests: vi.fn(async () => [ - permitRequest({ sign: firstSign }), - authorizationRequest({ sign: secondSign }), - ]), - buildPrimaryCall: vi.fn(() => primaryTx), - }); - const prepared = await plan.prepare(); - - const promise = prepared.signAll({} as WalletClient, USER); - await Promise.resolve(); - - expect(firstSign).toHaveBeenCalledWith({}, USER); - expect(secondSign).not.toHaveBeenCalled(); - - resolveFirst?.(permitSignature); - const signatures = await promise; - - expect(secondSign).toHaveBeenCalledWith({}, USER); - expect(signatures).toEqual([permitSignature, authorizationSignature]); - }); - test("behavior: builds ordered call requests only after requested signatures are provided", async () => { const permit = permitRequest(); const authorization = authorizationRequest(); @@ -257,6 +222,42 @@ describe("TransactionPlan", () => { ]); }); + test("behavior: preserves prerequisite and primary action types through execution", async () => { + const plan = TransactionPlan.create< + VaultV2DepositAction, + unknown, + | Readonly> + | Requirement + >({ + getRequirementRequests: vi.fn(async () => [approvalTx, permitRequest()]), + buildPrimaryCall: vi.fn(() => primaryTx), + }); + + const prepared = await plan.prepare(); + type Prepared = typeof prepared; + type SignatureRequest = Prepared["signatureRequests"][number]; + type PreparationCall = Extract< + Prepared["callRequests"][number], + { readonly phase: "preparation" } + >; + type Executable = ReturnType; + type PrimaryCall = Extract< + Executable["callRequests"][number], + { readonly phase: "primary" } + >; + + expectTypeOf().toEqualTypeOf< + Requirement + >(); + expectTypeOf>().toEqualTypeOf< + Promise + >(); + expectTypeOf().toEqualTypeOf< + Readonly> + >(); + expectTypeOf().toEqualTypeOf(); + }); + test("behavior: requirement call requests keep authorization intent separate from token approval", async () => { const plan = TransactionPlan.create({ getRequirementRequests: vi.fn(async () => [authorizationTx]), diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts index 803597a8b..9c9ccaec0 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts @@ -1,10 +1,10 @@ -import type { Address, WalletClient } from "viem"; import { type CallRequirement, isRequirementApproval, isRequirementBlueAuthorization, type RequirementSignature, type SignatureRequirement, + type Transaction, type TransactionAction, } from "../types/action.js"; import { MissingTransactionPlanSignaturesError } from "../types/error.js"; @@ -19,6 +19,7 @@ import type { TransactionPlanIntent, TransactionPlanMidnightOfferRootIntent, TransactionPlanOperatorAuthorizationIntent, + TransactionPlanPreparedRequest, TransactionPlanPrepareOptions, TransactionPlanRequest, TransactionPlanSignatureRequest, @@ -85,7 +86,11 @@ export type { * sign: (client) => request.sign(client, accountAddress), * })); * - * const executable = prepared.build(await prepared.signAll(walletClient, accountAddress)); + * const signatures = []; + * for (const request of prepared.signatureRequests) { + * signatures.push(await request.sign(walletClient, accountAddress)); + * } + * const executable = prepared.build(signatures); * const callRequests = executable.callRequests.map((request) => ({ * label: request.intent.type === "primaryTransaction" ? "Submit transaction" : "Approve token", * getCall: () => request.call, @@ -109,6 +114,13 @@ export class TransactionPlan< * Creates a lazy TransactionPlan from SDK handler callbacks. * * @param handler - SDK handler callbacks used by `prepare()` and `PreparedTransactionPlan.build(...)`. + * @example + * ```ts + * const plan = new TransactionPlan({ + * getRequirementRequests: async () => requests, + * buildPrimaryCall: () => transaction, + * }); + * ``` */ constructor( handler: TransactionPlanHandler< @@ -121,138 +133,156 @@ export class TransactionPlan< this.handler = handler; } - /** Prepares semantic review and execution metadata by resolving current requests. */ + /** + * Resolves the prerequisite requests and semantic review metadata for this plan. + * + * @param options - Optional request-discovery controls forwarded to the entity handler. + * @returns A prepared plan whose request types and primary action match this plan. + * @example + * ```ts + * const prepared = await vault.deposit(params).prepare({ + * requestOptions: { useSimplePermit: true }, + * }); + * // prepared.steps contains the typed prerequisites followed by the primary call preview. + * ``` + */ async prepare( options?: TransactionPlanPrepareOptions, - ): Promise> { + ): Promise> { const requests = this.handler.getRequirementRequests ? await this.handler.getRequirementRequests(options?.requestOptions) : []; - const requestSteps = requests.map((request, index) => { - const id = `request-${index}`; - if (isRequirementApproval(request)) { - return { - kind: "call", - id, - phase: "preparation", - tx: request, - action: request.action, - intent: { - type: "tokenApproval", - method: "tx", - token: request.to, - spender: request.action.args.spender, - amount: request.action.args.amount, - }, - call: { to: request.to, value: request.value, data: request.data }, - } satisfies TransactionPlanCallRequest; - } - if (isRequirementBlueAuthorization(request)) { - return { - kind: "call", - id, - phase: "preparation", - tx: request, - action: request.action, - intent: { - type: "operatorAuthorization", - method: "tx", - operator: request.action.args.authorized, - isAuthorized: request.action.args.isAuthorized, - }, - call: { to: request.to, value: request.value, data: request.data }, - } satisfies TransactionPlanCallRequest; - } - if ( - typeof request === "object" && - request !== null && - "to" in request && - "value" in request && - "data" in request && - "action" in request - ) { - const tx = request as CallRequirement; - const intent: + const requestSteps = requests.map( + (request, index): TransactionPlanPreparedRequest => { + const id = `request-${index}`; + if (isRequirementApproval(request)) { + const tx = request as Extract; + return { + kind: "call", + id, + phase: "preparation", + tx, + action: tx.action, + intent: { + type: "tokenApproval", + method: "tx", + token: request.to, + spender: request.action.args.spender, + amount: request.action.args.amount, + }, + call: { to: request.to, value: request.value, data: request.data }, + } satisfies TransactionPlanPreparedRequest; + } + if (isRequirementBlueAuthorization(request)) { + const tx = request as Extract; + return { + kind: "call", + id, + phase: "preparation", + tx, + action: tx.action, + intent: { + type: "operatorAuthorization", + method: "tx", + operator: request.action.args.authorized, + isAuthorized: request.action.args.isAuthorized, + }, + call: { to: request.to, value: request.value, data: request.data }, + } satisfies TransactionPlanPreparedRequest; + } + if ( + typeof request === "object" && + request !== null && + "to" in request && + "value" in request && + "data" in request && + "action" in request + ) { + const tx = request as Extract; + const intent: + | TransactionPlanOperatorAuthorizationIntent + | TransactionPlanContractCallIntent = + tx.action.type === "midnightAuthorization" + ? { + type: "operatorAuthorization", + method: "tx", + operator: tx.action.args.authorized, + isAuthorized: tx.action.args.isAuthorized, + owner: tx.action.args.onBehalf, + } + : { type: "contractCall", actionType: tx.action.type }; + return { + kind: "call", + id, + phase: "preparation", + tx, + action: tx.action, + intent, + call: { to: tx.to, value: tx.value, data: tx.data }, + } satisfies TransactionPlanPreparedRequest; + } + + const signatureRequest = request as Extract< + TRequest, + SignatureRequirement + >; + let intent: + | TransactionPlanTokenApprovalIntent | TransactionPlanOperatorAuthorizationIntent - | TransactionPlanContractCallIntent = - tx.action.type === "midnightAuthorization" - ? { - type: "operatorAuthorization", - method: "tx", - operator: tx.action.args.authorized, - isAuthorized: tx.action.args.isAuthorized, - owner: tx.action.args.onBehalf, - } - : { type: "contractCall", actionType: tx.action.type }; + | TransactionPlanMidnightOfferRootIntent; + switch (signatureRequest.action.type) { + case "permit": + intent = { + type: "tokenApproval", + method: "permit", + token: signatureRequest.action.args.token, + spender: signatureRequest.action.args.spender, + amount: signatureRequest.action.args.amount, + deadline: signatureRequest.action.args.deadline, + chainId: signatureRequest.action.args.chainId, + }; + break; + case "permit2": + intent = { + type: "tokenApproval", + method: "permit2", + token: signatureRequest.action.args.token, + spender: signatureRequest.action.args.spender, + amount: signatureRequest.action.args.amount, + deadline: signatureRequest.action.args.deadline, + expiration: signatureRequest.action.args.expiration, + chainId: signatureRequest.action.args.chainId, + }; + break; + case "authorization": + intent = { + type: "operatorAuthorization", + method: "signature", + operator: signatureRequest.action.args.authorized, + isAuthorized: signatureRequest.action.args.isAuthorized, + deadline: signatureRequest.action.args.deadline, + chainId: signatureRequest.action.args.chainId, + }; + break; + case "midnightOfferRootSignature": + intent = { + type: "midnightOfferRootSignature", + root: signatureRequest.action.args.root, + ratifier: signatureRequest.action.args.ratifier, + offers: signatureRequest.action.args.offers, + }; + break; + } return { - kind: "call", + kind: "signature", id, - phase: "preparation", - tx, - action: tx.action, + request: signatureRequest, + action: signatureRequest.action, intent, - call: { to: tx.to, value: tx.value, data: tx.data }, - } satisfies TransactionPlanCallRequest; - } - - const signatureRequest = request as SignatureRequirement; - let intent: - | TransactionPlanTokenApprovalIntent - | TransactionPlanOperatorAuthorizationIntent - | TransactionPlanMidnightOfferRootIntent; - switch (signatureRequest.action.type) { - case "permit": - intent = { - type: "tokenApproval", - method: "permit", - token: signatureRequest.action.args.token, - spender: signatureRequest.action.args.spender, - amount: signatureRequest.action.args.amount, - deadline: signatureRequest.action.args.deadline, - chainId: signatureRequest.action.args.chainId, - }; - break; - case "permit2": - intent = { - type: "tokenApproval", - method: "permit2", - token: signatureRequest.action.args.token, - spender: signatureRequest.action.args.spender, - amount: signatureRequest.action.args.amount, - deadline: signatureRequest.action.args.deadline, - expiration: signatureRequest.action.args.expiration, - chainId: signatureRequest.action.args.chainId, - }; - break; - case "authorization": - intent = { - type: "operatorAuthorization", - method: "signature", - operator: signatureRequest.action.args.authorized, - isAuthorized: signatureRequest.action.args.isAuthorized, - deadline: signatureRequest.action.args.deadline, - chainId: signatureRequest.action.args.chainId, - }; - break; - case "midnightOfferRootSignature": - intent = { - type: "midnightOfferRootSignature", - root: signatureRequest.action.args.root, - ratifier: signatureRequest.action.args.ratifier, - offers: signatureRequest.action.args.offers, - }; - break; - } - return { - kind: "signature", - id, - request: signatureRequest, - action: signatureRequest.action, - intent, - sign: (client, userAddress) => - signatureRequest.sign(client, userAddress), - } satisfies TransactionPlanSignatureRequest; - }); + sign: signatureRequest.sign, + } satisfies TransactionPlanPreparedRequest; + }, + ); const primaryTx = this.handler.previewPrimaryCall === false ? undefined @@ -278,9 +308,13 @@ export class TransactionPlan< value: primaryTx.value, data: primaryTx.data, }, - } satisfies TransactionPlanCallRequest); + } satisfies TransactionPlanCallRequest< + TPrimaryAction, + Readonly>, + "primary" + >); - return new PreparedTransactionPlan({ + return new PreparedTransactionPlan({ buildPrimaryCall: this.handler.buildPrimaryCall, requestSteps, primaryCall, @@ -292,6 +326,13 @@ export class TransactionPlan< * * @param handler - SDK handler callbacks used by `prepare()` and `PreparedTransactionPlan.build(...)`. * @returns A TransactionPlan exposing `prepare()` as the public execution entry point. + * @example + * ```ts + * const plan = TransactionPlan.create({ + * getRequirementRequests: async () => requests, + * buildPrimaryCall: () => transaction, + * }); + * ``` */ static create< TCreatedPrimaryAction extends TransactionAction, @@ -315,11 +356,20 @@ export class TransactionPlan< } } -/** Prepared transaction plan with resolved requests and app-facing helpers. */ +/** + * Prepared transaction plan with resolved requests and app-facing helpers. + * + * @example + * ```ts + * const prepared = await plan.prepare(); + * const approvals = prepared.findIntent("tokenApproval"); + * ``` + */ export class PreparedTransactionPlan< TPrimaryAction extends TransactionAction = TransactionAction, + TRequest extends TransactionPlanRequest = TransactionPlanRequest, TSignatures = readonly RequirementSignature[], -> implements PreparedTransactionPlanShape +> implements PreparedTransactionPlanShape { private readonly buildPrimaryCall: TransactionPlanBuildPrimaryCall< TPrimaryAction, @@ -327,40 +377,61 @@ export class PreparedTransactionPlan< >; /** Ordered signable requests to present to the user. */ - readonly signatureRequests: readonly TransactionPlanSignatureRequest[]; + readonly signatureRequests: readonly TransactionPlanSignatureRequest< + Extract + >[]; /** Ordered viem-compatible calls. Includes the primary action call last when previewable. */ - readonly callRequests: readonly TransactionPlanCallRequest[]; + readonly callRequests: PreparedTransactionPlanShape< + TPrimaryAction, + TRequest + >["callRequests"]; /** All signature requests and call requests in review order. */ - readonly steps: readonly TransactionPlanStep[]; + readonly steps: readonly TransactionPlanStep[]; /** * Creates a prepared plan from resolved request steps. * * @param params - Build callback, resolved requests, and optional preview primary call. + * @example + * ```ts + * const prepared = new PreparedTransactionPlan({ + * buildPrimaryCall: () => transaction, + * requestSteps: [], + * primaryCall, + * }); + * ``` */ constructor(params: { readonly buildPrimaryCall: TransactionPlanBuildPrimaryCall< TPrimaryAction, TSignatures >; - readonly requestSteps: readonly ( - | TransactionPlanSignatureRequest - | TransactionPlanCallRequest - )[]; - readonly primaryCall?: TransactionPlanCallRequest; + readonly requestSteps: readonly TransactionPlanPreparedRequest[]; + readonly primaryCall?: TransactionPlanCallRequest< + TPrimaryAction, + Readonly>, + "primary" + >; }) { this.buildPrimaryCall = params.buildPrimaryCall; this.signatureRequests = Object.freeze( params.requestSteps.filter( - (request): request is TransactionPlanSignatureRequest => - request.kind === "signature", + ( + request, + ): request is TransactionPlanSignatureRequest< + Extract + > => request.kind === "signature", ), ); const requirementCallRequests = params.requestSteps.filter( - (request): request is TransactionPlanCallRequest => - request.kind === "call", + ( + request, + ): request is Extract< + TransactionPlanPreparedRequest, + { readonly kind: "call" } + > => request.kind === "call", ); const primaryCall = params.primaryCall; this.callRequests = Object.freeze( @@ -375,22 +446,54 @@ export class PreparedTransactionPlan< ); } - /** Number of signature requests plus call requests. */ + /** + * Counts the signature and call requests currently exposed by the plan. + * + * @returns The total number of exposed requests. + * @example + * ```ts + * console.log(prepared.requestCount); + * ``` + */ get requestCount(): number { return this.signatureRequests.length + this.callRequests.length; } - /** Whether the plan requires at least one signature prompt. */ + /** + * Reports whether the plan requires at least one signature prompt. + * + * @returns `true` when `signatureRequests` is non-empty. + * @example + * ```ts + * if (prepared.hasSignatureRequests) showSignatureReview(); + * ``` + */ get hasSignatureRequests(): boolean { return this.signatureRequests.length > 0; } - /** Whether the plan has at least one viem-compatible call request available now. */ + /** + * Reports whether the plan currently exposes a viem-compatible call. + * + * @returns `true` when `callRequests` is non-empty. + * @example + * ```ts + * if (prepared.hasCallRequests) showCallReview(); + * ``` + */ get hasCallRequests(): boolean { return this.callRequests.length > 0; } - /** High-level flow kind derived from the request mix. */ + /** + * Classifies the plan from its current signature and call request mix. + * + * @returns The high-level transaction-plan flow kind. + * @example + * ```ts + * const reviewMode = prepared.flowKind; + * ``` + */ get flowKind(): TransactionPlanFlowKind { const signatureRequests = this.signatureRequests.length; const preparationCalls = this.callRequests.filter( @@ -408,35 +511,62 @@ export class PreparedTransactionPlan< return "call_requests"; } - /** Returns true when at least one request carries the requested intent type. */ + /** + * Checks whether at least one step carries the requested semantic intent. + * + * @param type - Intent discriminator to search for. + * @returns `true` when a matching step exists. + * @example + * ```ts + * const needsApproval = prepared.hasIntent("tokenApproval"); + * ``` + */ hasIntent(type: TType): boolean { return this.steps.some((request) => request.intent.type === type); } - /** Returns all requests carrying the requested semantic intent type. */ + /** + * Returns every step carrying the requested semantic intent. + * + * @param type - Intent discriminator used to narrow the returned steps. + * @returns The matching typed steps in execution order. + * @example + * ```ts + * const approvals = prepared.findIntent("tokenApproval"); + * ``` + */ findIntent( type: TType, - ): readonly TransactionPlanStepForIntent[] { + ): readonly TransactionPlanStepForIntent[] { return this.steps.filter( - (request): request is TransactionPlanStepForIntent => - request.intent.type === type, + ( + request, + ): request is TransactionPlanStepForIntent< + TType, + TPrimaryAction, + TRequest + > => request.intent.type === type, ); } - /** Signs every signature request sequentially and returns the produced signatures. */ - async signAll( - client: WalletClient, - userAddress: Address, - ): Promise { - const signatures: RequirementSignature[] = []; - for (const request of this.signatureRequests) { - signatures.push(await request.sign(client, userAddress)); - } - return Object.freeze(signatures); - } - - /** Builds an executable transaction plan from already-collected signatures. */ - build(signatures?: TSignatures): ExecutableTransactionPlan { + /** + * Builds the executable calls from signatures collected by the integrator. + * + * @param signatures - Signatures produced from `signatureRequests`, in request order. + * @returns An executable plan that preserves the prerequisite and primary-action types. + * @throws {MissingTransactionPlanSignaturesError} when fewer signatures are supplied than requested. + * @example + * ```ts + * const signatures = []; + * for (const request of prepared.signatureRequests) { + * signatures.push(await request.sign(walletClient, userAddress)); + * } + * const executable = prepared.build(signatures); + * ``` + */ + build( + signatures?: TSignatures, + ): ExecutableTransactionPlan { const expected = this.signatureRequests.length; const received = signatures == null @@ -459,35 +589,73 @@ export class PreparedTransactionPlan< action: primaryTx.action, intent: { type: "primaryTransaction", actionType: primaryTx.action.type }, call: { to: primaryTx.to, value: primaryTx.value, data: primaryTx.data }, - } satisfies TransactionPlanCallRequest; - return new ExecutableTransactionPlan({ + } satisfies TransactionPlanCallRequest< + TPrimaryAction, + Readonly>, + "primary" + >; + return new ExecutableTransactionPlan({ signatureRequests: Object.freeze([...this.signatureRequests]), callRequests: Object.freeze([...requirementCallRequests, primaryCall]), }); } } -/** Executable transaction plan built from a PreparedTransactionPlan after signature collection. */ -export class ExecutableTransactionPlan - implements ExecutableTransactionPlanShape +/** + * Executable transaction plan built after signature collection. + * + * @example + * ```ts + * const executable = prepared.build(signatures); + * for (const request of executable.callRequests) { + * await walletClient.sendTransaction(request.call); + * } + * ``` + */ +export class ExecutableTransactionPlan< + TPrimaryAction extends TransactionAction = TransactionAction, + TRequest extends TransactionPlanRequest = TransactionPlanRequest, +> implements ExecutableTransactionPlanShape { /** Ordered signable requests used to produce the signatures passed to `PreparedTransactionPlan.build(...)`. */ - readonly signatureRequests: readonly TransactionPlanSignatureRequest[]; + readonly signatureRequests: readonly TransactionPlanSignatureRequest< + Extract + >[]; /** Ordered viem-compatible calls to submit, with the primary action call last. */ - readonly callRequests: readonly TransactionPlanCallRequest[]; + readonly callRequests: ExecutableTransactionPlanShape< + TPrimaryAction, + TRequest + >["callRequests"]; /** * Creates an executable plan from built transaction requests. * * @param params - Executable plan shape. + * @example + * ```ts + * const executable = new ExecutableTransactionPlan({ + * signatureRequests: [], + * callRequests, + * }); + * ``` */ - constructor(params: ExecutableTransactionPlanShape) { + constructor( + params: ExecutableTransactionPlanShape, + ) { this.signatureRequests = Object.freeze([...params.signatureRequests]); this.callRequests = Object.freeze([...params.callRequests]); } - /** Ordered viem-compatible calls for transaction submission. */ + /** + * Extracts the ordered viem-compatible calls for transaction submission. + * + * @returns The raw calls in execution order. + * @example + * ```ts + * const calls = executable.viemCalls; + * ``` + */ get viemCalls(): readonly TransactionPlanViemCall[] { return this.callRequests.map((request) => request.call); } diff --git a/packages/morpho-sdk/src/transactionPlan/types.ts b/packages/morpho-sdk/src/transactionPlan/types.ts index 048e04707..1ee3b2744 100644 --- a/packages/morpho-sdk/src/transactionPlan/types.ts +++ b/packages/morpho-sdk/src/transactionPlan/types.ts @@ -1,4 +1,4 @@ -import type { Address, Hex, WalletClient } from "viem"; +import type { Address, Hex } from "viem"; import type { AuthorizationRequirementSignature, BaseAction, @@ -182,39 +182,42 @@ export type TransactionPlanFlowKind = | "mixed_requests"; /** A signable request in a transaction plan. */ -export interface TransactionPlanSignatureRequest { +export interface TransactionPlanSignatureRequest< + TRequest extends SignatureRequirement = SignatureRequirement, +> { /** Request kind discriminator. */ readonly kind: "signature"; /** Stable id based on original request order, e.g. `request-1`. */ readonly id: string; /** Original SDK signable descriptor. */ - readonly request: SignatureRequirement; + readonly request: TRequest; /** Exact SDK/protocol action metadata used to encode this signature request. */ - readonly action: SignatureRequirement["action"]; + readonly action: TRequest["action"]; /** Higher-level plan category for app labels, filtering, and analytics. */ readonly intent: | TransactionPlanTokenApprovalIntent | TransactionPlanOperatorAuthorizationIntent | TransactionPlanMidnightOfferRootIntent; /** Signs the request with the provided wallet client and user address. */ - readonly sign: ( - client: WalletClient, - userAddress: Address, - ) => Promise; + readonly sign: TRequest["sign"]; } /** A viem-compatible call request in a transaction plan. */ export interface TransactionPlanCallRequest< TAction extends BaseAction = TransactionAction, + TTransaction extends Readonly> = Readonly< + Transaction + >, + TPhase extends "preparation" | "primary" = "preparation" | "primary", > { /** Request kind discriminator. */ readonly kind: "call"; /** Stable id based on original request order, or `primary` for the requested action call. */ readonly id: string; /** Whether this call supports the flow or executes the requested primary action. */ - readonly phase: "preparation" | "primary"; + readonly phase: TPhase; /** Original SDK transaction with Morpho action metadata. */ - readonly tx: Readonly>; + readonly tx: TTransaction; /** Exact SDK/protocol action metadata used to encode this call. */ readonly action: TAction; /** Higher-level plan category for app labels, filtering, and analytics. */ @@ -223,32 +226,68 @@ export interface TransactionPlanCallRequest< readonly call: TransactionPlanViemCall; } -/** A request in a transaction plan: either a signature prompt or a transaction call. */ -export type TransactionPlanStep = - | TransactionPlanSignatureRequest - | TransactionPlanCallRequest; +/** Prepared representation of one prerequisite request discovered by a transaction plan. */ +export type TransactionPlanPreparedRequest< + TRequest extends TransactionPlanRequest = TransactionPlanRequest, +> = + | TransactionPlanSignatureRequest> + | TransactionPlanCallRequest< + Extract["action"], + Extract, + "preparation" + >; + +/** A request in a transaction plan: either a prerequisite step or the primary transaction. */ +export type TransactionPlanStep< + TPrimaryAction extends TransactionAction = TransactionAction, + TRequest extends TransactionPlanRequest = TransactionPlanRequest, +> = + | TransactionPlanPreparedRequest + | TransactionPlanCallRequest< + TPrimaryAction, + Readonly>, + "primary" + >; /** Narrows transaction-plan steps by semantic intent type. */ export type TransactionPlanStepForIntent< TType extends TransactionPlanIntent["type"], -> = TransactionPlanStep & { + TPrimaryAction extends TransactionAction = TransactionAction, + TRequest extends TransactionPlanRequest = TransactionPlanRequest, +> = TransactionPlanStep & { readonly intent: Extract; }; /** Prepared transaction plan shape consumed by app review/execution code. */ -export interface PreparedTransactionPlanShape { +export interface PreparedTransactionPlanShape< + TPrimaryAction extends TransactionAction = TransactionAction, + TRequest extends TransactionPlanRequest = TransactionPlanRequest, +> { /** Ordered signable requests to present to the user. */ - readonly signatureRequests: readonly TransactionPlanSignatureRequest[]; + readonly signatureRequests: readonly TransactionPlanSignatureRequest< + Extract + >[]; /** Ordered viem-compatible calls. Includes the primary action call last when previewable. */ - readonly callRequests: readonly TransactionPlanCallRequest[]; + readonly callRequests: readonly Extract< + TransactionPlanStep, + { readonly kind: "call" } + >[]; /** All signature requests and call requests in review order. */ - readonly steps: readonly TransactionPlanStep[]; + readonly steps: readonly TransactionPlanStep[]; } /** Built plan ready for transaction submission after signatures have been collected. */ -export interface ExecutableTransactionPlanShape { +export interface ExecutableTransactionPlanShape< + TPrimaryAction extends TransactionAction = TransactionAction, + TRequest extends TransactionPlanRequest = TransactionPlanRequest, +> { /** Ordered signable requests used to produce the signatures passed to `build(...)`. */ - readonly signatureRequests: readonly TransactionPlanSignatureRequest[]; + readonly signatureRequests: readonly TransactionPlanSignatureRequest< + Extract + >[]; /** Ordered viem-compatible calls to submit, with the primary action call last. */ - readonly callRequests: readonly TransactionPlanCallRequest[]; + readonly callRequests: readonly Extract< + TransactionPlanStep, + { readonly kind: "call" } + >[]; } diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index 344f656a6..6ee2897f3 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -7,7 +7,9 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. - `BaseAction` — discriminated union base, keyed on `type`. - `Transaction` — immutable `{ to, value, data, action }`. Returned from every action; deep-frozen. - `Requirement` / `RequirementSignature` — prerequisite signing flow for permit/permit2 and Midnight offer roots. -- `ActionOutput` — lazy entity output with `getRequirements()` plus synchronous `buildTx(...)`. +- `TransactionPlan` — lazy entity output whose `prepare()` call resolves typed prerequisite requests and whose prepared form builds an executable plan. +- `PreparedTransactionPlan` — resolved, typed review surface containing signature requests, call requests, and semantic intents. +- `ExecutableTransactionPlan` — ordered prerequisite and primary calls ready for submission. - `Metadata` — optional `{ origin, timestamp? }` for calldata tracing. - `DepositAmountArgs` — union enforcing at least one of `amount` / `nativeAmount`. Reused for vault deposits, market collateral supply, and market loan-asset supply. - `AssetsOrSharesArgs` — discriminated union `{ assets } | { shares }`. Used by withdraw (supply-side). diff --git a/packages/morpho-sdk/src/types/action.ts b/packages/morpho-sdk/src/types/action.ts index d8538e4b3..a322496ea 100644 --- a/packages/morpho-sdk/src/types/action.ts +++ b/packages/morpho-sdk/src/types/action.ts @@ -543,11 +543,11 @@ export interface PermitAction extends BaseAction< "permit", { - token?: Address; - spender: Address; - amount: bigint; - deadline: bigint; - chainId?: number; + readonly token?: Address; + readonly spender: Address; + readonly amount: bigint; + readonly deadline: bigint; + readonly chainId?: number; } > {} @@ -555,12 +555,12 @@ export interface Permit2Action extends BaseAction< "permit2", { - token?: Address; - spender: Address; - amount: bigint; - deadline: bigint; - expiration: bigint; - chainId?: number; + readonly token?: Address; + readonly spender: Address; + readonly amount: bigint; + readonly deadline: bigint; + readonly expiration: bigint; + readonly chainId?: number; } > {} @@ -572,10 +572,10 @@ export interface AuthorizationAction extends BaseAction< "authorization", { - authorized: Address; - isAuthorized: boolean; - deadline: bigint; - chainId?: number; + readonly authorized: Address; + readonly isAuthorized: boolean; + readonly deadline: bigint; + readonly chainId?: number; } > {} @@ -693,13 +693,13 @@ export type Bundler3TokenRequirementSignature = PermitRequirementSignature; /** Permit or Permit2 token signature result. */ export type TokenRequirementSignature = Bundler3TokenRequirementSignature; -/** Any signature result returned by an action-output signature requirement. */ +/** Any signature result returned by a transaction-plan signature request. */ export type AnyRequirementSignature = | TokenRequirementSignature | AuthorizationRequirementSignature | MidnightOfferRootSignature; -/** Any signature requirement returned by an entity action output. */ +/** Any signature requirement discovered while preparing an entity transaction plan. */ export type SignatureRequirement = | TokenSignatureRequirement | MidnightOfferRootRequirement @@ -713,10 +713,10 @@ export type CallRequirementAction = | SetterRatifierRatifyRootAction | MidnightSupplyCollateralAction; -/** Onchain call prerequisite returned by action-output `getRequirements()`. */ +/** Onchain call prerequisite discovered while preparing an entity transaction plan. */ export type CallRequirement = Readonly>; -/** Onchain call or signature prerequisite returned by an entity action output. */ +/** Onchain call or signature prerequisite discovered while preparing an entity transaction plan. */ export type ActionRequirement = CallRequirement | SignatureRequirement; export function isRequirementApproval( diff --git a/packages/wdk-protocol-lending-morpho-evm/src/errors.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/errors.test.ts new file mode 100644 index 000000000..6188e0c68 --- /dev/null +++ b/packages/wdk-protocol-lending-morpho-evm/src/errors.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, test } from "vitest"; +import { MissingTransactionPlanCallError } from "./errors.js"; + +describe("MissingTransactionPlanCallError", () => { + test("default", () => { + const error = new MissingTransactionPlanCallError(); + + expect(error).toBeInstanceOf(Error); + expect(error.name).toBe("MissingTransactionPlanCallError"); + }); +}); diff --git a/packages/wdk-protocol-lending-morpho-evm/src/errors.ts b/packages/wdk-protocol-lending-morpho-evm/src/errors.ts new file mode 100644 index 000000000..8e337e0ba --- /dev/null +++ b/packages/wdk-protocol-lending-morpho-evm/src/errors.ts @@ -0,0 +1,20 @@ +/** + * Thrown when a prepared Morpho transaction plan does not produce its primary call. + * + * @example + * ```ts + * import { MissingTransactionPlanCallError } from "@morpho-org/wdk-protocol-lending-morpho-evm"; + * + * if (error instanceof MissingTransactionPlanCallError) { + * // Recreate the plan from current protocol state before retrying. + * } + * ``` + */ +export class MissingTransactionPlanCallError extends Error { + constructor() { + super( + "Transaction plan produced no executable primary call. Recreate the plan before retrying.", + ); + this.name = "MissingTransactionPlanCallError"; + } +} diff --git a/packages/wdk-protocol-lending-morpho-evm/src/index.ts b/packages/wdk-protocol-lending-morpho-evm/src/index.ts index 9d82ccb75..8911eb7de 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/index.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/index.ts @@ -14,6 +14,7 @@ export type { WithdrawOptions, WithdrawResult, } from "@tetherto/wdk-wallet/protocols"; +export { MissingTransactionPlanCallError } from "./errors.js"; export { type Market, type MarketPresetKey, diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index af25b7334..6fb7e8691 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -53,6 +53,7 @@ import { zeroAddress, } from "viem"; import { arbitrum, base, mainnet, optimism, polygon } from "viem/chains"; +import { MissingTransactionPlanCallError } from "./errors.js"; import { type MarketPresetKey, MORPHO_MARKET_PRESETS, @@ -411,10 +412,8 @@ async function getTransactionPlanRequests< ); const requests: TRequest[] = []; for (const request of prepared.steps) { - if (request.id === "primary") continue; - requests.push( - (request.kind === "signature" ? request.request : request.tx) as TRequest, - ); + if (request.kind === "call" && request.phase === "primary") continue; + requests.push(request.kind === "signature" ? request.request : request.tx); } return requests; @@ -432,7 +431,7 @@ async function getTransactionPlanTx< const executable = (await plan.prepare()).build(signatures); const primaryCall = executable.callRequests.at(-1); if (primaryCall == null) { - throw new Error("Transaction plan produced no executable call request."); + throw new MissingTransactionPlanCallError(); } return toWdkTransaction(primaryCall.tx); } From aa087913f0cec1fe30a011a6dabf0d956b1bcb21 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Thu, 16 Jul 2026 10:30:00 +0200 Subject: [PATCH 05/12] fix(morpho-sdk): update transaction plan docs and signature types --- .agents/personas/web3-security.md | 2 +- .changeset/quiet-midnight-flows.md | 2 +- packages/morpho-sdk/ARCHITECTURE.md | 57 +++--- packages/morpho-sdk/BUNDLER3.md | 2 +- packages/morpho-sdk/README.md | 176 ++++++++++-------- .../morpho-sdk/src/actions/blue/AGENTS.md | 2 +- .../morpho-sdk/src/actions/blue/refinance.ts | 2 +- .../actions/blue/supplyCollateralBorrow.ts | 2 +- .../src/actions/midnight/authorization.ts | 4 +- .../actions/midnight/mempoolSubmitOffers.ts | 4 +- .../morpho-sdk/src/actions/midnight/redeem.ts | 2 +- .../midnight/setterRatifierRatifyRoot.ts | 2 +- .../src/actions/midnight/supplyCollateral.ts | 2 +- .../getGeneralAdapterRequirements.test.ts | 3 +- .../getMidnightApprovalRequirements.ts | 2 +- .../getMidnightAuthorizationRequirement.ts | 2 +- .../getSetterRatifierRatifyRootRequirement.ts | 2 +- .../src/actions/vaultV1/migrateToV2.ts | 2 +- .../morpho-sdk/src/entities/blue/blue.test.ts | 10 +- packages/morpho-sdk/src/entities/blue/blue.ts | 6 +- .../src/entities/vaultV1/vaultV1.test.ts | 4 +- .../transactionPlan/TransactionPlan.test.ts | 12 +- packages/morpho-sdk/src/types/action.test.ts | 20 +- packages/morpho-sdk/src/types/action.ts | 97 +++------- packages/morpho-sdk/src/types/error.ts | 16 +- 25 files changed, 222 insertions(+), 213 deletions(-) diff --git a/.agents/personas/web3-security.md b/.agents/personas/web3-security.md index 95e981935..068632543 100644 --- a/.agents/personas/web3-security.md +++ b/.agents/personas/web3-security.md @@ -58,7 +58,7 @@ The boundary between the SDK and the chain. Authoritative rules live in [`AGENTS ### Action-layer purity (§1) -- **State read in the Action layer.** An action whose `buildTx` calls `readContract` or otherwise hits the network — Actions are pure encoders per §1's table. State reads belong in the Entity layer. +- **State read in the Action layer.** An action encoder that calls `readContract` or otherwise hits the network — Actions are pure encoders per §1's table. State reads belong in the Entity layer and are resolved before it returns a `TransactionPlan`. - **Async in an Action.** The §1 table forbids `async` in actions. Flag any encoder marked `async` (signing belongs at the Client edge, not in encoders). - **Mutation of input arguments.** Encoders return new objects; mutation of incoming structs is a §1 invariant break. diff --git a/.changeset/quiet-midnight-flows.md b/.changeset/quiet-midnight-flows.md index 93f99a669..479078604 100644 --- a/.changeset/quiet-midnight-flows.md +++ b/.changeset/quiet-midnight-flows.md @@ -19,4 +19,4 @@ Named take transaction builders validate that their takeable offers match the ex Validation runs before requirements are exposed: takeable offers must match the requested flow, redemption cannot exceed position credit, approval amounts and operators are checked before allowance short-circuits, and market inputs must belong to the selected Midnight deployment. Maker preparation also preserves caller-owned offer group arrays. -Bind security-sensitive flow artifacts to their preparation context: Ecrecover submissions use the canonical payload retained for the signed tree instead of trusting payload bytes supplied to `buildTx`, typed-data signing rejects wallets on another chain, and redemption accepts only owner-bound position snapshots for the requested account. +Bind security-sensitive flow artifacts to their preparation context: Ecrecover submissions use the canonical payload retained for the signed tree instead of trusting payload bytes supplied to `PreparedTransactionPlan.build()`, typed-data signing rejects wallets on another chain, and redemption accepts only owner-bound position snapshots for the requested account. diff --git a/packages/morpho-sdk/ARCHITECTURE.md b/packages/morpho-sdk/ARCHITECTURE.md index a89010d50..0f7202ba4 100644 --- a/packages/morpho-sdk/ARCHITECTURE.md +++ b/packages/morpho-sdk/ARCHITECTURE.md @@ -13,9 +13,10 @@ operations on EVM-compatible chains for Morpho protocol. - **Deterministic transaction building.** Given the same inputs and on-chain state, the SDK always produces the same `Transaction` object. No simulation, no gas estimation, no sending — the consumer handles those concerns. -- **Predictable developer experience.** Every operation returns a `{ buildTx, getRequirements }` - pair (for deposits) or `{ buildTx }` (for withdrawals/redeems). The interface is identical - across V1 and V2 vaults. +- **Predictable developer experience.** Every entity operation returns a lazy `TransactionPlan`. + `prepare()` resolves prerequisite calls and signature requests, while + `PreparedTransactionPlan.build(signatures)` returns the ordered executable calls. The lifecycle + is identical across VaultV1, VaultV2, Blue, and Midnight. - **Immutability.** Every returned `Transaction` is deep-frozen via `@morpho-org/morpho-ts`'s `deepFreeze`. Once built, a transaction object cannot be mutated. - **No `any`.** Strict TypeScript throughout, with discriminated unions for action types and @@ -251,33 +252,35 @@ bundle. ### Decision tree ``` -getRequirements(viemClient, params) +TransactionPlan.prepare() │ -ā”œā”€ supportSignature: false (default) -│ └─► getRequirementsApproval() -│ Spender: generalAdapter1 -│ Returns: Transaction[] -│ • Checks current allowance — skips if sufficient. -│ • For APPROVE_ONLY_ONCE_TOKENS (e.g. USDT): prepends -│ a reset-to-zero approval before the actual approval. -│ -└─ supportSignature: true - │ - ā”œā”€ Token supports EIP-2612 AND useSimplePermit: true - │ └─► getRequirementsPermit() - │ Returns: Requirement[] with sign() → PermitAction - │ • Checks generalAdapter1 allowance — skips if sufficient. - │ • Produces a signable permit for the generalAdapter1 spender. +└─► getGeneralAdapterRequirements(viemClient, params) │ - ā”œā”€ Permit2 contract exists on this chain - │ └─► getRequirementsPermit2() - │ Returns: (Transaction | Requirement)[] - │ Two-step: - │ 1. ERC20 → Permit2: classic approve() if needed (infinite). - │ 2. Permit2 → generalAdapter1: signature if needed or expiring. + ā”œā”€ supportSignature: false (default) + │ └─► getRequirementsApproval() + │ Spender: generalAdapter1 + │ Returns: Transaction[] + │ • Checks current allowance — skips if sufficient. + │ • For APPROVE_ONLY_ONCE_TOKENS (e.g. USDT): prepends + │ a reset-to-zero approval before the actual approval. │ - └─ Fallback - └─► getRequirementsApproval() (same as supportSignature: false) + └─ supportSignature: true + │ + ā”œā”€ Token supports EIP-2612 AND useSimplePermit: true + │ └─► getRequirementsPermit() + │ Returns: Requirement[] with sign() → PermitAction + │ • Checks generalAdapter1 allowance — skips if sufficient. + │ • Produces a signable permit for the generalAdapter1 spender. + │ + ā”œā”€ Permit2 contract exists on this chain + │ └─► getRequirementsPermit2() + │ Returns: (Transaction | Requirement)[] + │ Two-step: + │ 1. ERC20 → Permit2: classic approve() if needed (infinite). + │ 2. Permit2 → generalAdapter1: signature if needed or expiring. + │ + └─ Fallback + └─► getRequirementsApproval() (same as supportSignature: false) ``` ### How signatures flow into deposits diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index b0e450122..243bdf162 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -98,7 +98,7 @@ This is the main design caveat. For the following operations the SDK emits a **d ### Other pitfalls -- **Blue authorization for GA1 required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`.** A user who has never granted it will receive a requirement through [`getBlueAuthorizationRequirement`](src/actions/requirements/blue/getBlueAuthorizationRequirement.ts). Without signature support, this is a `setAuthorization` transaction to execute beforehand. With `supportSignature`, this is a signable requirement; pass the resulting `AuthorizationRequirementSignature` to `buildTx`, which folds it into the bundle as `setAuthorizationWithSig`. +- **Blue authorization for GA1 required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`.** A user who has never granted it will receive a request through [`getBlueAuthorizationRequirement`](src/actions/requirements/blue/getBlueAuthorizationRequirement.ts) when the plan is prepared. Without signature support, this is a `setAuthorization` call that appears before the primary call. With `supportSignature`, it is a signable request; pass the resulting `AuthorizationRequirementSignature` to `PreparedTransactionPlan.build()`, which folds it into the bundle as `setAuthorizationWithSig`. - **Critical order in `repayWithdrawCollateral`**: `morphoRepay` **must** precede `morphoWithdrawCollateral` in the bundle, otherwise the position is deemed unhealthy at withdraw time and the tx reverts. - **Builder must equal signer.** Bundler actions reference accounts in two different ways: some take an explicit `onBehalf` and act on `userAddress` (e.g. `morphoRepay`), others act implicitly on the **initiator** — the `msg.sender` of `bundler3.multicall`, i.e. the EOA signing the tx, not the adapter — (e.g. `erc20TransferFrom`, `morphoWithdrawCollateral`, the latter exposing no `onBehalf` parameter on GA1). `repayWithdrawCollateral` is the canonical example: the repay leg targets `userAddress` while the transfer-from and the withdraw target the initiator. If the address that built the tx (and filled `userAddress`) is not the address that signs/executes it, the bundle would repay one account's debt while pulling tokens from and withdrawing collateral against the signer. Transaction builders do not validate this at build time — callers MUST keep `userAddress` aligned with the signing account. The signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) take a `WalletClient` and enforce this at `sign()` time via `validateUserAddress` (throws `MissingClientPropertyError` / `AddressMismatchError`). - **Tricky `tx.value`**: whenever a `nativeAmount` or a `reallocateTo` (native fee) is involved, `BundlerAction.encodeBundle` computes `tx.value`. Do not overwrite it on the caller side. diff --git a/packages/morpho-sdk/README.md b/packages/morpho-sdk/README.md index ea4ba2fd4..96fbf2df3 100644 --- a/packages/morpho-sdk/README.md +++ b/packages/morpho-sdk/README.md @@ -46,38 +46,53 @@ pnpm add @morpho-org/morpho-sdk | | `supplyCollateral` / `redeem` / `cancelOffer` | Direct Midnight call | Encodes direct collateral, credit, and offer-management operations. | | | `repayWithdrawCollateral` | MidnightBundles | Atomically repays credit and withdraws collateral. | -### The `getRequirements` flow +### Transaction plans -Every action that touches a user's tokens or positions returns two things: +Every entity action returns a lazy `TransactionPlan`: -- `buildTx(signatures?)` — builds the final viem `Transaction` object. Takes the collected signature results required by the flow, including permit / Permit2, Morpho authorization, or a Midnight offer-root signature. -- `getRequirements()` — returns the list of on-chain pre-requisites that must be satisfied first. +- `plan.prepare()` resolves the prerequisite call and signature requests for the chosen action. +- `prepared.signatureRequests` contains permits, Morpho authorizations, or Midnight offer-root + signatures that the wallet must sign. +- `prepared.callRequests` contains prerequisite calls and, when it can be previewed without a + signature, the primary call last. +- `prepared.build(signatures)` returns the executable calls, ordered with the primary call last. Typical requirements: - **ERC-20 approval** — the user must approve the bundler (or Morpho directly) to pull tokens. Returned as a standard `approve` transaction the consumer sends first. -- **Permit / Permit2 signature** — off-chain approvals that go into `buildTx` in the `signatures` array, avoiding the extra approval tx. Enabled via `morphoViemExtension({ supportSignature: true })`. +- **Permit / Permit2 signature** — off-chain approvals passed to `prepared.build(signatures)`, + avoiding the extra approval tx. Enabled via + `morphoViemExtension({ supportSignature: true })`. - **Morpho authorization** — `borrow`, `supplyCollateralBorrow`, and `repayWithdrawCollateral` require the user to authorize `GeneralAdapter1` on the Morpho contract once (`setAuthorization`). The SDK returns this as an extra transaction if it's missing. - **Midnight authorization or ratification** — Midnight flows return the required operator authorization, Ecrecover offer-root signature, or SetterRatifier transaction before the final take or mempool submission. Usage pattern: ```typescript -const { buildTx, getRequirements } = await vault.deposit({ +const plan = vault.deposit({ /* ... */ }); -const requirements = await getRequirements(); -// → [{ type: "approval", tx: {...} }, { type: "permit", sign: async () => {...} }] +const prepared = await plan.prepare(); +const signatures = await Promise.all( + prepared.signatureRequests.map((request) => + request.sign(walletClient, userAddress), + ), +); +const executable = prepared.build(signatures); -// Consumer satisfies each requirement (send tx / sign permit), collects the signature, -// then calls buildTx to get the final transaction: -const tx = buildTx([permitSignature]); +// Send every call in order; prerequisite calls come before the primary action. +for (const request of executable.callRequests) { + await walletClient.sendTransaction(request.call); +} ``` +The focused examples below use `signatures` for the array collected from +`prepared.signatureRequests` with this pattern. + ### Integration invariant — builder = signer -**`userAddress` MUST equal the account that ends up signing/executing the tx.** Critical for `repayWithdrawCollateral`, whose bundle mixes explicit `onBehalf` (repay) with implicit `msg.sender` (transfer-from + withdraw) — see [BUNDLER3.md](./BUNDLER3.md#other-pitfalls). Transaction builders do not validate this at build time, so callers MUST keep `userAddress` aligned with the signing account themselves. The signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) take a `WalletClient` and enforce the invariant at `sign()` time via `validateUserAddress`, rejecting any `sign(client, userAddress)` where `client.account.address !== userAddress` with `MissingClientPropertyError` / `AddressMismatchError`. +**`userAddress` MUST equal the account that ends up signing/executing the tx.** Critical for `repayWithdrawCollateral`, whose bundle mixes explicit `onBehalf` (repay) with implicit `msg.sender` (transfer-from + withdraw) — see [BUNDLER3.md](./BUNDLER3.md#other-pitfalls). Transaction plans do not validate this at build time, so callers MUST keep `userAddress` aligned with the signing account themselves. The signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) take a `WalletClient` and enforce the invariant at `sign()` time via `validateUserAddress`, rejecting any `sign(client, userAddress)` where `client.account.address !== userAddress` with `MissingClientPropertyError` / `AddressMismatchError`. | Entity | Action | Route | Why | | ------------ | ------------------------ | ------------------------- | --------------------------------------------------------------------------------------------------- | @@ -118,13 +133,15 @@ const vault = client.morpho.vaultV2("0xVault...", 1); #### Deposit ```typescript -const { buildTx, getRequirements } = await vault.deposit({ +const vaultData = await vault.getData(); +const plan = vault.deposit({ amount: 1000000000000000000n, userAddress: "0xUser...", + vaultData, }); -const requirements = await getRequirements(); -const tx = buildTx([requirementSignature]); +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); ``` ##### Deposit with native token wrapping @@ -133,16 +150,18 @@ For vaults whose underlying asset is wNative, you can deposit native token that ```typescript // Native ETH only — wraps 1 ETH to WETH and deposits -const { buildTx, getRequirements } = await vault.deposit({ +const nativeDepositPlan = vault.deposit({ nativeAmount: 1000000000000000000n, userAddress: "0xUser...", + vaultData, }); // Mixed — 0.5 WETH (ERC-20) + 0.5 native ETH wrapped to WETH -const { buildTx, getRequirements } = await vault.deposit({ +const mixedDepositPlan = vault.deposit({ amount: 500000000000000000n, nativeAmount: 500000000000000000n, userAddress: "0xUser...", + vaultData, }); ``` @@ -151,47 +170,47 @@ The bundler atomically transfers native token, wraps it to wNative, and deposits #### Withdraw ```typescript -const { buildTx } = vault.withdraw({ +const plan = vault.withdraw({ amount: 500000000000000000n, userAddress: "0xUser...", }); -const tx = buildTx(); +const executable = (await plan.prepare()).build(); ``` #### Redeem ```typescript -const { buildTx } = vault.redeem({ +const plan = vault.redeem({ shares: 1000000000000000000n, userAddress: "0xUser...", }); -const tx = buildTx(); +const executable = (await plan.prepare()).build(); ``` #### Force Withdraw ```typescript -const { buildTx } = vault.forceWithdraw({ +const plan = vault.forceWithdraw({ deallocations: [{ adapter: "0xAdapter...", amount: 100n }], withdraw: { amount: 500000000000000000n }, userAddress: "0xUser...", }); -const tx = buildTx(); +const executable = (await plan.prepare()).build(); ``` #### Force Redeem ```typescript -const { buildTx } = vault.forceRedeem({ +const plan = vault.forceRedeem({ deallocations: [{ adapter: "0xAdapter...", amount: 100n }], redeem: { shares: 1000000000000000000n }, userAddress: "0xUser...", }); -const tx = buildTx(); +const executable = (await plan.prepare()).build(); ``` ### VaultV1 @@ -203,35 +222,37 @@ const vault = client.morpho.vaultV1("0xVault...", 1); #### Deposit ```typescript -const { buildTx, getRequirements } = await vault.deposit({ +const vaultData = await vault.getData(); +const plan = vault.deposit({ amount: 1000000000000000000n, userAddress: "0xUser...", + vaultData, }); -const requirements = await getRequirements(); -const tx = buildTx([requirementSignature]); +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); ``` #### Withdraw ```typescript -const { buildTx } = vault.withdraw({ +const plan = vault.withdraw({ amount: 500000000000000000n, userAddress: "0xUser...", }); -const tx = buildTx(); +const executable = (await plan.prepare()).build(); ``` #### Redeem ```typescript -const { buildTx } = vault.redeem({ +const plan = vault.redeem({ shares: 1000000000000000000n, userAddress: "0xUser...", }); -const tx = buildTx(); +const executable = (await plan.prepare()).build(); ``` #### Migrate to V2 @@ -242,15 +263,15 @@ Atomically migrate a full position from a VaultV1 (MetaMorpho) vault into a Vaul const sourceVault = client.morpho.vaultV1("0xV1Vault...", 1); const targetVault = client.morpho.vaultV2("0xV2Vault...", 1); -const { buildTx, getRequirements } = sourceVault.migrateToV2({ +const plan = sourceVault.migrateToV2({ userAddress: "0xUser...", sourceVault: await sourceVault.getData(), targetVault: await targetVault.getData(), shares: 1000000000000000000n, }); -const requirements = await getRequirements(); -const tx = buildTx([requirementSignature]); +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); ``` ### Blue @@ -278,14 +299,14 @@ Supply the loan asset to earn yield on the market. ```typescript const marketData = await market.getMarketData(); -const { buildTx, getRequirements } = market.supply({ +const plan = market.supply({ amount: 1000000000n, userAddress: "0xUser...", marketData, }); -const requirements = await getRequirements(); -const tx = buildTx([requirementSignature]); +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); ``` ##### Supply with native token wrapping @@ -293,7 +314,7 @@ const tx = buildTx([requirementSignature]); If the market's `loanToken` is the chain's wNative, you can supply native token that will be wrapped automatically: ```typescript -const { buildTx, getRequirements } = market.supply({ +const plan = market.supply({ nativeAmount: 1000000000000000000n, userAddress: "0xUser...", marketData, @@ -305,13 +326,13 @@ The bundle routes through `GeneralAdapter1` with `maxSharePrice` slippage protec #### Supply Collateral ```typescript -const { buildTx, getRequirements } = market.supplyCollateral({ +const plan = market.supplyCollateral({ amount: 1000000000000000000n, userAddress: "0xUser...", }); -const requirements = await getRequirements(); -const tx = buildTx([requirementSignature]); +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); ``` #### Borrow @@ -319,14 +340,14 @@ const tx = buildTx([requirementSignature]); ```typescript const positionData = await market.getPositionData("0xUser..."); -const { buildTx, getRequirements } = market.borrow({ +const plan = market.borrow({ amount: 500000000000000000n, userAddress: "0xUser...", positionData, }); -const requirements = await getRequirements(); -const tx = buildTx(); +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); ``` #### Supply Collateral & Borrow @@ -334,15 +355,15 @@ const tx = buildTx(); ```typescript const positionData = await market.getPositionData("0xUser..."); -const { buildTx, getRequirements } = market.supplyCollateralBorrow({ +const plan = market.supplyCollateralBorrow({ amount: 1000000000000000000n, borrowAmount: 500000000000000000n, userAddress: "0xUser...", positionData, }); -const requirements = await getRequirements(); -const tx = buildTx([requirementSignature]); +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); ``` #### Repay @@ -353,21 +374,21 @@ Two modes depending on whether the caller specifies `amount` (partial repay) or const positionData = await market.getPositionData("0xUser..."); // Partial repay — by amount -const { buildTx, getRequirements } = market.repay({ +const partialRepayPlan = market.repay({ amount: 250000000000000000n, userAddress: "0xUser...", positionData, }); // Full repay — by shares (recommended to clear the full debt atomically) -const { buildTx, getRequirements } = market.repay({ +const fullRepayPlan = market.repay({ shares: positionData.borrowShares, userAddress: "0xUser...", positionData, }); -const requirements = await getRequirements(); -const tx = buildTx([requirementSignature]); +const prepared = await fullRepayPlan.prepare(); +const executable = prepared.build(signatures); ``` Repay does **not** require Morpho authorization (it only requires a loan token approval for `GeneralAdapter1`). @@ -380,37 +401,38 @@ Two modes — `assets` (exact amount) or `shares` (full close, immune to interes const positionData = await market.getPositionData("0xUser..."); // Withdraw an exact amount of loan asset -const { buildTx, getRequirements } = market.withdraw({ +const assetWithdrawPlan = market.withdraw({ assets: 500000000n, userAddress: "0xUser...", positionData, }); // Or close the full supply position by shares -const { buildTx, getRequirements } = market.withdraw({ +const shareWithdrawPlan = market.withdraw({ shares: positionData.supplyShares, userAddress: "0xUser...", positionData, }); -const requirements = await getRequirements(); -const tx = buildTx(); +const prepared = await shareWithdrawPlan.prepare(); +const executable = prepared.build(signatures); ``` -Loan-asset withdraw requires Morpho authorization for `GeneralAdapter1` (returned by `getRequirements` when missing). It does **not** require a token approval. +Loan-asset withdraw requires Morpho authorization for `GeneralAdapter1` (exposed by +`plan.prepare()` when missing). It does **not** require a token approval. #### Withdraw Collateral ```typescript const positionData = await market.getPositionData("0xUser..."); -const { buildTx } = market.withdrawCollateral({ +const plan = market.withdrawCollateral({ amount: 500000000000000000n, userAddress: "0xUser...", positionData, }); -const tx = buildTx(); +const executable = (await plan.prepare()).build(); ``` Direct call to `morpho.withdrawCollateral()` — no bundler, no `GeneralAdapter1` authorization needed. The SDK validates position health after withdrawal against the LLTV buffer to prevent instant liquidation. @@ -420,15 +442,15 @@ Direct call to `morpho.withdrawCollateral()` — no bundler, no `GeneralAdapter1 ```typescript const positionData = await market.getPositionData("0xUser..."); -const { buildTx, getRequirements } = market.repayWithdrawCollateral({ +const plan = market.repayWithdrawCollateral({ amount: 250000000000000000n, // or shares: ... withdrawAmount: 500000000000000000n, userAddress: "0xUser...", positionData, }); -const requirements = await getRequirements(); -const tx = buildTx([requirementSignature]); +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); ``` Atomically bundles repay → withdraw collateral via bundler3. Bundle order is critical: repay runs first to reduce debt, then withdraw. Requires both a loan token approval (for repay) and a Morpho authorization (for withdraw). The SDK validates combined position health by simulating the repay before checking withdrawal safety. @@ -445,7 +467,7 @@ const positionData = await source.getPositionData("0xUser..."); const targetPositionData = await target.getPositionData("0xUser..."); // Refinance by assets — exact-asset borrow and repay, no GA1 dust -const { buildTx, getRequirements } = source.refinance({ +const assetRefinancePlan = source.refinance({ userAddress: "0xUser...", positionData, target: { marketParams: targetMarketParams, positionData: targetPositionData }, @@ -454,7 +476,7 @@ const { buildTx, getRequirements } = source.refinance({ }); // Or migrate the full debt by shares (immune to interest accrual between quote and inclusion) -const { buildTx, getRequirements } = source.refinance({ +const shareRefinancePlan = source.refinance({ userAddress: "0xUser...", positionData, target: { marketParams: targetMarketParams, positionData: targetPositionData }, @@ -463,18 +485,18 @@ const { buildTx, getRequirements } = source.refinance({ }); // Collateral-only migration — omit both borrow fields -const { buildTx, getRequirements } = source.refinance({ +const collateralRefinancePlan = source.refinance({ userAddress: "0xUser...", positionData, target: { marketParams: targetMarketParams, positionData: targetPositionData }, collateralAmount: 1000000000000000000n, }); -const requirements = await getRequirements(); -const tx = buildTx(); +const prepared = await shareRefinancePlan.prepare(); +const executable = prepared.build(signatures); ``` -The SDK validates ownership, token/id match, and that amounts do not exceed the source position. Health is checked against `LLTV āˆ’ buffer` where it can degrade: the residual source position is validated whenever debt remains after the repay, and the aggregate target position is validated whenever a borrow leg is migrated. Collateral-only migrations (both borrow fields omitted) skip the target health check — they can't degrade target health and would otherwise fail on missing-oracle target markets. Both markets are forward-accrued to `now`; in shares mode the target borrow overshoots by `slippageTolerance` and the callback sweeps the residual back into the target debt (or skims it to the user). `getRequirements` returns the `setAuthorization(generalAdapter1, true)` transaction when GA1 is not yet authorized — a single global authorization covers both markets. Optional `targetReallocations` top up target-market liquidity via the **PublicAllocator** (same mechanism as `borrow`); their fees add to `tx.value`. +The SDK validates ownership, token/id match, and that amounts do not exceed the source position. Health is checked against `LLTV āˆ’ buffer` where it can degrade: the residual source position is validated whenever debt remains after the repay, and the aggregate target position is validated whenever a borrow leg is migrated. Collateral-only migrations (both borrow fields omitted) skip the target health check — they can't degrade target health and would otherwise fail on missing-oracle target markets. Both markets are forward-accrued to `now`; in shares mode the target borrow overshoots by `slippageTolerance` and the callback sweeps the residual back into the target debt (or skims it to the user). `plan.prepare()` exposes the `setAuthorization(generalAdapter1, true)` transaction when GA1 is not yet authorized — a single global authorization covers both markets. Optional `targetReallocations` top up target-market liquidity via the **PublicAllocator** (same mechanism as `borrow`); their fees add to the primary call's `value`. #### Borrow with Shared Liquidity (Reallocations) @@ -499,22 +521,22 @@ const reallocations: VaultReallocation[] = [ const positionData = await market.getPositionData("0xUser..."); // Borrow with reallocations -const { buildTx, getRequirements } = market.borrow({ +const plan = market.borrow({ amount: 500000000000000000n, userAddress: "0xUser...", positionData, reallocations, }); -const requirements = await getRequirements(); -const tx = buildTx(); -// tx.value includes the sum of all reallocation fees +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); +// The primary call's value includes the sum of all reallocation fees. ``` Reallocations also work with `supplyCollateralBorrow`: ```typescript -const { buildTx, getRequirements } = market.supplyCollateralBorrow({ +const plan = market.supplyCollateralBorrow({ amount: 1000000000000000000n, borrowAmount: 500000000000000000n, userAddress: "0xUser...", @@ -532,7 +554,7 @@ transaction synchronously. ```typescript const midnight = client.morpho.midnight(8453); const marketData = await midnight.getMarketData(marketId); -const output = midnight.takeLend({ +const plan = midnight.takeLend({ accountAddress: lender, marketData, assets: 1_000_000n, @@ -541,8 +563,8 @@ const output = midnight.takeLend({ deadline, }); -const requirements = await output.getRequirements(); -const tx = output.buildTx(); +const prepared = await plan.prepare(); +const executable = prepared.build(signatures); ``` ### Architecture @@ -615,7 +637,7 @@ graph LR subgraph Shared - REQ[getRequirements] + REQ[TransactionPlan.prepare] end MV1 -.->|approval / permit| REQ diff --git a/packages/morpho-sdk/src/actions/blue/AGENTS.md b/packages/morpho-sdk/src/actions/blue/AGENTS.md index 775e8b12e..9c9b10b0a 100644 --- a/packages/morpho-sdk/src/actions/blue/AGENTS.md +++ b/packages/morpho-sdk/src/actions/blue/AGENTS.md @@ -50,7 +50,7 @@ ERC-20 approval spender is **GeneralAdapter1** for any bundled path — never th ## Required pre-conditions -Enforced by the entity layer's `getRequirements`; see [`entities/blue/AGENTS.md`](../../entities/blue/AGENTS.md): +Enforced while the entity layer prepares its `TransactionPlan`; see [`entities/blue/AGENTS.md`](../../entities/blue/AGENTS.md): - `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, `withdraw`, and `refinance` require GeneralAdapter1 to be authorized on Morpho (`setAuthorization`). When an `authorizationSignature` is passed, these builders prepend a `setAuthorizationWithSig` call to the bundle instead of relying on a separate `setAuthorization` transaction. - Native wrapping requires the collateral token (collateral-supply paths) or the loan token (`supply`, `repay`, `repayWithdrawCollateral`) to be the configured wNative for the chain. diff --git a/packages/morpho-sdk/src/actions/blue/refinance.ts b/packages/morpho-sdk/src/actions/blue/refinance.ts index da94303e5..0e3396dc7 100644 --- a/packages/morpho-sdk/src/actions/blue/refinance.ts +++ b/packages/morpho-sdk/src/actions/blue/refinance.ts @@ -94,7 +94,7 @@ export interface BlueRefinanceParams { * then an `erc20Transfer` skims any residual to the user if that repay is skipped. * - **Collat-only** (both zero/omitted): only collateral is migrated; borrow/repay legs omitted. * - * Prerequisite: GA1 must be authorized on Blue — the entity's `getRequirements()` returns the + * Prerequisite: GA1 must be authorized on Blue — `TransactionPlan.prepare()` exposes the * `setAuthorization` transaction when needed. * * @param params.source.chainId - The chain both markets live on. diff --git a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts index 0d812bd6d..6eaa96c03 100644 --- a/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts +++ b/packages/morpho-sdk/src/actions/blue/supplyCollateralBorrow.ts @@ -58,7 +58,7 @@ export interface BlueSupplyCollateralBorrowParams { * `GeneralAdapter1.wrapNative()` before the supply leg. * * Prerequisite: `GeneralAdapter1` must be authorized on Morpho to borrow on behalf of the user. - * Use `getRequirements()` on the entity to check and obtain the authorization transaction. + * Use `TransactionPlan.prepare()` on the entity output to discover the authorization request. * * Zero loss: all collateral reaches Morpho, all borrowed tokens reach the receiver. No dust left * in bundler or adapter. diff --git a/packages/morpho-sdk/src/actions/midnight/authorization.ts b/packages/morpho-sdk/src/actions/midnight/authorization.ts index 1ffb6804f..0d86bb453 100644 --- a/packages/morpho-sdk/src/actions/midnight/authorization.ts +++ b/packages/morpho-sdk/src/actions/midnight/authorization.ts @@ -21,8 +21,8 @@ export interface MidnightSetIsAuthorizedParams { /** * Encodes `Midnight.setIsAuthorized` for a bundle spender or ratifier. * - * Prefer requirement helpers or entity `getRequirements()` in app flows; they - * read `isAuthorized` first and return this transaction only when needed. Use + * Prefer requirement helpers or `TransactionPlan.prepare()` in app flows; they + * read `isAuthorized` first and expose this transaction only when needed. Use * this builder directly when the caller intentionally wants to set or revoke * authorization without reading state. * diff --git a/packages/morpho-sdk/src/actions/midnight/mempoolSubmitOffers.ts b/packages/morpho-sdk/src/actions/midnight/mempoolSubmitOffers.ts index d0f831af0..31d4a50be 100644 --- a/packages/morpho-sdk/src/actions/midnight/mempoolSubmitOffers.ts +++ b/packages/morpho-sdk/src/actions/midnight/mempoolSubmitOffers.ts @@ -23,8 +23,8 @@ export interface MempoolSubmitOffersParams { /** * Encodes a Midnight mempool payload submission transaction. * - * App make-offer flows should call `getOffersData`, gather the returned - * requirements, then call the output `buildTx(signatures)`. Use this builder + * App make-offer flows should call `getOffersData`, prepare the returned + * transaction plan, then call `PreparedTransactionPlan.build(signatures)`. Use this builder * directly only when the payload has already been encoded with ratifier data * and the caller wants to submit those bytes to the mempool contract. * diff --git a/packages/morpho-sdk/src/actions/midnight/redeem.ts b/packages/morpho-sdk/src/actions/midnight/redeem.ts index 8a979aeb8..8d79a16b6 100644 --- a/packages/morpho-sdk/src/actions/midnight/redeem.ts +++ b/packages/morpho-sdk/src/actions/midnight/redeem.ts @@ -30,7 +30,7 @@ export interface MidnightRedeemParams { * Use this low-level builder after the caller has fetched and checked position * credit and market withdrawable liquidity. App flows should usually call * `client.morpho.midnight(chainId).redeem(...)`, which performs those checks - * from the supplied `positionData` snapshot before exposing `buildTx`. + * from the supplied `positionData` snapshot before returning the transaction plan. * * @param params.chainId - Chain id used to resolve `Midnight`. * @param params.market - Midnight market whose credit is redeemed. diff --git a/packages/morpho-sdk/src/actions/midnight/setterRatifierRatifyRoot.ts b/packages/morpho-sdk/src/actions/midnight/setterRatifierRatifyRoot.ts index 2c9fbc9d9..66145aecb 100644 --- a/packages/morpho-sdk/src/actions/midnight/setterRatifierRatifyRoot.ts +++ b/packages/morpho-sdk/src/actions/midnight/setterRatifierRatifyRoot.ts @@ -27,7 +27,7 @@ export interface SetterRatifierRatifyRootParams { * * Use this after building a Setter-ratified offer tree and before submitting * its mempool payload. Entity make-offer flows expose the same transaction - * through `getRequirements()` when the root is not already approved. + * through `TransactionPlan.prepare()` when the root is not already approved. * * @param params.chainId - Chain id used to resolve `SetterRatifier`. * @param params.maker - Maker whose root approval is updated. diff --git a/packages/morpho-sdk/src/actions/midnight/supplyCollateral.ts b/packages/morpho-sdk/src/actions/midnight/supplyCollateral.ts index 24e63acbf..a54b6580d 100644 --- a/packages/morpho-sdk/src/actions/midnight/supplyCollateral.ts +++ b/packages/morpho-sdk/src/actions/midnight/supplyCollateral.ts @@ -30,7 +30,7 @@ export interface MidnightSupplyCollateralParams { * Use this low-level builder when a flow already knows the market, collateral * index, and approval state. App flows should usually call * `client.morpho.midnight(chainId).supplyCollateral(...)`, which resolves the - * ERC20 approval requirement before exposing `buildTx`. + * ERC20 approval call request when the transaction plan is prepared. * * @param params.chainId - Chain id used to resolve `Midnight`. * @param params.market - Midnight market receiving collateral. diff --git a/packages/morpho-sdk/src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.test.ts b/packages/morpho-sdk/src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.test.ts index b98568e3b..f32460bc8 100644 --- a/packages/morpho-sdk/src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.test.ts +++ b/packages/morpho-sdk/src/actions/requirements/generalAdapter/getGeneralAdapterRequirements.test.ts @@ -24,6 +24,7 @@ import { isRequirementApproval, isRequirementSignature, Permit2ExpirationMissingError, + type PermitRequirementSignature, } from "../../../types/index.js"; import { getTokenRequirementActions } from "../../signatures/getTokenRequirementActions.js"; import { getRequirementsApproval } from "../getRequirementsApproval.js"; @@ -671,7 +672,7 @@ describe("getGeneralAdapterRequirements", () => { expiration: 2n, }, }, - }, + } as unknown as PermitRequirementSignature, }), ).toThrow(Permit2ExpirationMissingError); }); diff --git a/packages/morpho-sdk/src/actions/requirements/midnight/getMidnightApprovalRequirements.ts b/packages/morpho-sdk/src/actions/requirements/midnight/getMidnightApprovalRequirements.ts index da75bc708..1f68fe5f5 100644 --- a/packages/morpho-sdk/src/actions/requirements/midnight/getMidnightApprovalRequirements.ts +++ b/packages/morpho-sdk/src/actions/requirements/midnight/getMidnightApprovalRequirements.ts @@ -25,7 +25,7 @@ export interface GetMidnightApprovalRequirementsParams { /** * Resolves classic ERC20 approval requirements for a Midnight spender. * - * Entity flows call this from `getRequirements()`. Direct low-level consumers + * Entity flows call this while preparing a transaction plan. Direct low-level consumers * should call it before encoding a Midnight action that lets `Midnight` or * `MidnightBundles` pull ERC20 tokens from the user. * diff --git a/packages/morpho-sdk/src/actions/requirements/midnight/getMidnightAuthorizationRequirement.ts b/packages/morpho-sdk/src/actions/requirements/midnight/getMidnightAuthorizationRequirement.ts index bf5d21896..eb81ada7b 100644 --- a/packages/morpho-sdk/src/actions/requirements/midnight/getMidnightAuthorizationRequirement.ts +++ b/packages/morpho-sdk/src/actions/requirements/midnight/getMidnightAuthorizationRequirement.ts @@ -22,7 +22,7 @@ export interface GetMidnightAuthorizationRequirementParams { * * Call before actions that rely on `Midnight.isAuthorized(owner, authorized)`, * such as bundle takes or Ecrecover offer-root ratification. Entity flows call - * this from `getRequirements()` and omit the transaction when authorization is + * this while preparing a transaction plan and omit the transaction when authorization is * already set. * * @param params.viemClient - Viem client used to read `Midnight.isAuthorized`. diff --git a/packages/morpho-sdk/src/actions/requirements/midnight/getSetterRatifierRatifyRootRequirement.ts b/packages/morpho-sdk/src/actions/requirements/midnight/getSetterRatifierRatifyRootRequirement.ts index 3a19af07d..f11ea5193 100644 --- a/packages/morpho-sdk/src/actions/requirements/midnight/getSetterRatifierRatifyRootRequirement.ts +++ b/packages/morpho-sdk/src/actions/requirements/midnight/getSetterRatifierRatifyRootRequirement.ts @@ -20,7 +20,7 @@ export interface GetSetterRatifierRatifyRootRequirementParams { * Resolves the SetterRatifier root approval transaction for a maker offer tree. * * Call after building a Setter-ratified tree and before encoding/submitting its - * payload. Entity make-offer flows call this from `getRequirements()` and omit + * payload. Entity make-offer flows call this while preparing a transaction plan and omit * the transaction when the maker has already approved the root. * * @param params.viemClient - Viem client used to read root approval state. diff --git a/packages/morpho-sdk/src/actions/vaultV1/migrateToV2.ts b/packages/morpho-sdk/src/actions/vaultV1/migrateToV2.ts index d8eb8cd86..55c39c74f 100644 --- a/packages/morpho-sdk/src/actions/vaultV1/migrateToV2.ts +++ b/packages/morpho-sdk/src/actions/vaultV1/migrateToV2.ts @@ -51,7 +51,7 @@ export interface VaultV1MigrateToV2Params { * * Prerequisite: the user must either approve `GeneralAdapter1` to spend their V1 vault shares * (classic approve) or provide a pre-signed permit/permit2 via `requirementSignature`. Use - * `getRequirements()` on the entity to resolve the appropriate approval. + * `TransactionPlan.prepare()` on the entity output to resolve the appropriate approval. * * @param params.vault.chainId - The chain the source vault lives on (used to resolve bundler * addresses). diff --git a/packages/morpho-sdk/src/entities/blue/blue.test.ts b/packages/morpho-sdk/src/entities/blue/blue.test.ts index abc835a32..a2a83f8de 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.test.ts @@ -164,7 +164,7 @@ describe("MorphoBlue validation", () => { ).toThrow(NonPositiveBorrowAmountError); }); - test("withdraw getRequirements includes Blue authorization when missing", async ({ + test("withdraw prepare includes Blue authorization when missing", async ({ client, }) => { const market = client @@ -186,7 +186,7 @@ describe("MorphoBlue validation", () => { expect(requirements).toHaveLength(1); }); - test("withdraw getRequirements returns no authorization when already authorized", async () => { + test("withdraw prepare returns no authorization when already authorized", async () => { const handle = createMockClient(mainnet); const { morpho } = getChainAddresses(mainnet.id); mockRead(handle, { @@ -289,7 +289,7 @@ describe("MorphoBlue validation", () => { ).toThrow(WithdrawExceedsCollateralError); }); - test("repayWithdrawCollateral getRequirements includes Blue authorization when missing", async ({ + test("repayWithdrawCollateral prepare includes Blue authorization when missing", async ({ client, }) => { const market = client @@ -441,7 +441,7 @@ describe("MorphoBlue validation", () => { expect(tx.action.args.transferAmount).toBe(borrowAssets); expect(tx.action.args.transferAmount - nativeAmount).toBe(expectedErc20); - // getRequirements approves exactly the carved ERC-20 remainder, not the debt. + // Preparation approves exactly the carved ERC-20 remainder, not the debt. const requirements = await getPlanRequests(repay); const approval = requirements.find(isRequirementApproval); if (!approval) { @@ -582,7 +582,7 @@ describe("MorphoBlue validation", () => { expect(tx.action.args.transferAmount).toBe(borrowAssets); expect(tx.action.args.transferAmount - nativeAmount).toBe(expectedErc20); - // getRequirements approves exactly the carved ERC-20 remainder (alongside the + // Preparation approves exactly the carved ERC-20 remainder (alongside the // Morpho authorization the withdraw leg needs). const requirements = await getPlanRequests(action); const approval = requirements.find(isRequirementApproval); diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 762b9231c..fac82e223 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -877,7 +877,7 @@ export class MorphoBlue implements BlueActions { const amount = params.amount ?? 0n; // Reject a negative ERC-20 amount before it can be masked by nativeAmount // in the sum below (a negative amount would otherwise yield a negative - // erc20Amount and a negative approval in getRequirements). + // erc20Amount and a negative approval during plan preparation). if (amount < 0n) { throw new NonPositiveRepayAmountError(this.marketParams.id); } @@ -1094,7 +1094,7 @@ export class MorphoBlue implements BlueActions { const amount = params.amount ?? 0n; // Reject a negative ERC-20 amount before it can be masked by nativeAmount // in the sum below (a negative amount would otherwise yield a negative - // erc20Amount and a negative approval in getRequirements). + // erc20Amount and a negative approval during plan preparation). if (amount < 0n) { throw new NonPositiveRepayAmountError(this.marketParams.id); } @@ -1391,7 +1391,7 @@ export class MorphoBlue implements BlueActions { const requestedAssets = borrowAssets ?? 0n; const requestedShares = borrowShares ?? 0n; - // Reject negative debt up front; otherwise it slips past the `> 0n` mode checks and only fails at buildTx(). + // Reject negative debt up front; otherwise it slips past the `> 0n` mode checks and only fails when the prepared plan is built. if (requestedAssets < 0n) { throw new NonPositiveAssetAmountError(this.marketParams.loanToken); } diff --git a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts index 488d0d73d..f897f1a5a 100644 --- a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts @@ -331,7 +331,7 @@ describe("MorphoVaultV1 entity tests", () => { }); }); - describe("getRequirements with supportSignature: false", () => { + describe("prepare with supportSignature: false", () => { test("should return classic approval requirements when supportSignature is false", async ({ client, }) => { @@ -478,7 +478,7 @@ describe("MorphoVaultV1 entity tests", () => { ).toThrow(NonPositiveSharesAmountError); }); - test("should return buildTx and getRequirements", async ({ client }) => { + test("returns a transaction plan", async ({ client }) => { const morphoClient = client.extend( morphoViemExtension({ supportSignature: false, diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts index b19c62e10..feb8a2221 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts @@ -58,7 +58,7 @@ const primaryTx: Transaction = { }, }; -const permitSignature: PermitRequirementSignature = { +const permitSignature = { action: { type: "permit2", args: { @@ -79,7 +79,7 @@ const permitSignature: PermitRequirementSignature = { expiration: 2_000_000_000n, signature: SIGNATURE, }, -}; +} as const satisfies PermitRequirementSignature; const authorizationSignature: AuthorizationRequirementSignature = { action: { @@ -101,7 +101,7 @@ const authorizationSignature: AuthorizationRequirementSignature = { }, }; -function permitRequest(): Requirement { +function permitRequest(): Requirement { return { action: permitSignature.action, sign: vi.fn(async () => permitSignature), @@ -227,7 +227,7 @@ describe("TransactionPlan", () => { VaultV2DepositAction, unknown, | Readonly> - | Requirement + | Requirement >({ getRequirementRequests: vi.fn(async () => [approvalTx, permitRequest()]), buildPrimaryCall: vi.fn(() => primaryTx), @@ -247,10 +247,10 @@ describe("TransactionPlan", () => { >; expectTypeOf().toEqualTypeOf< - Requirement + Requirement >(); expectTypeOf>().toEqualTypeOf< - Promise + Promise >(); expectTypeOf().toEqualTypeOf< Readonly> diff --git a/packages/morpho-sdk/src/types/action.test.ts b/packages/morpho-sdk/src/types/action.test.ts index 7edfcae37..9da912831 100644 --- a/packages/morpho-sdk/src/types/action.test.ts +++ b/packages/morpho-sdk/src/types/action.test.ts @@ -1,5 +1,5 @@ import type { Address, Hex } from "viem"; -import { describe, expect, test } from "vitest"; +import { describe, expect, expectTypeOf, test } from "vitest"; import { type AnyRequirementSignature, type AuthorizationRequirementSignature, @@ -7,6 +7,8 @@ import { isMidnightOfferRootSignature, isPermitSignature, type MidnightOfferRootSignature, + type Permit2Args, + type PermitArgs, type PermitRequirementSignature, selectRequirementSignatures, } from "./action.js"; @@ -85,6 +87,22 @@ const midnightOfferRootSignature: MidnightOfferRootSignature = { }, }; +describe("RequirementSignature", () => { + test("behavior: permit action discriminators preserve their matching args", () => { + type PermitSignature = Extract< + PermitRequirementSignature, + { readonly action: { readonly type: "permit" } } + >; + type Permit2Signature = Extract< + PermitRequirementSignature, + { readonly action: { readonly type: "permit2" } } + >; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); +}); + describe("isPermitSignature", () => { test("default: true for permit", () => { expect(isPermitSignature(permitSignature)).toBe(true); diff --git a/packages/morpho-sdk/src/types/action.ts b/packages/morpho-sdk/src/types/action.ts index a322496ea..fc8583f4f 100644 --- a/packages/morpho-sdk/src/types/action.ts +++ b/packages/morpho-sdk/src/types/action.ts @@ -590,30 +590,21 @@ export interface MidnightOfferRootSignatureAction } > {} -/** Action metadata supported by signature requirements. */ -export type SignatureRequirementAction = - | PermitAction - | Permit2Action - | AuthorizationAction - | MidnightOfferRootSignatureAction; - -/** Argument payloads returned by signature requirements. */ -export type RequirementSignatureArgs = - | PermitArgs - | Permit2Args - | AuthorizationSignatureArgs - | MidnightOfferRootSignatureArgs; - /** A signed ERC-2612 permit or Permit2 approval requirement. */ -export interface PermitRequirementSignature { - args: PermitArgs | Permit2Args; - action: PermitAction | Permit2Action; -} +export type PermitRequirementSignature = + | { + readonly args: PermitArgs; + readonly action: PermitAction; + } + | { + readonly args: Permit2Args; + readonly action: Permit2Action; + }; /** A signed Morpho authorization requirement (consumed via `setAuthorizationWithSig`). */ export interface AuthorizationRequirementSignature { - args: AuthorizationSignatureArgs; - action: AuthorizationAction; + readonly args: AuthorizationSignatureArgs; + readonly action: AuthorizationAction; } /** A signed Midnight Ecrecover offer-root requirement. */ @@ -627,62 +618,34 @@ export interface MidnightOfferRootSignature { * `"permit"` / `"permit2"` carry Bundler3 token-approval args, `"authorization"` carries the * signed Morpho authorization, and Midnight adds `"midnightOfferRootSignature"`. */ -export type RequirementSignature< - TAction extends SignatureRequirementAction | undefined = undefined, - TArgs extends RequirementSignatureArgs | undefined = undefined, -> = TAction extends SignatureRequirementAction - ? TArgs extends RequirementSignatureArgs - ? { - args: TArgs; - action: TAction; - } - : never - : - | PermitRequirementSignature - | AuthorizationRequirementSignature - | MidnightOfferRootSignature; - -type RequirementResult< - TSignatureOrAction extends RequirementSignature | SignatureRequirementAction, - TArgs extends RequirementSignatureArgs | undefined, -> = TSignatureOrAction extends SignatureRequirementAction - ? RequirementSignature< - TSignatureOrAction, - Extract - > - : Extract; +export type RequirementSignature = + | PermitRequirementSignature + | AuthorizationRequirementSignature + | MidnightOfferRootSignature; /** * A signable approval / authorization requirement. `sign()` returns the matching * {@link RequirementSignature}; `action` describes the requirement without signing. * - * Generic over the signature it produces so permit encoders narrow to - * {@link PermitRequirementSignature} and the authorization encoder to - * {@link AuthorizationRequirementSignature}; the two-parameter form is kept for - * Midnight action requirements that are parameterized by action and args. + * Generic over the exact signature it produces so the returned `args` and `action` + * remain correlated through their action discriminator. */ -export interface Requirement< - TSignatureOrAction extends - | RequirementSignature - | SignatureRequirementAction = RequirementSignature, - TArgs extends RequirementSignatureArgs | undefined = undefined, -> { - sign: ( - client: WalletClient, - userAddress: Address, - ) => Promise>; - action: RequirementResult["action"]; -} +export type Requirement< + TSignature extends RequirementSignature = RequirementSignature, +> = TSignature extends RequirementSignature + ? { + sign: (client: WalletClient, userAddress: Address) => Promise; + readonly action: TSignature["action"]; + } + : never; /** Bundler3 token signature requirement. */ export type Bundler3TokenSignatureRequirement = Requirement; /** Midnight Ecrecover offer-root signature requirement. */ -export type MidnightOfferRootRequirement = Requirement< - MidnightOfferRootSignatureAction, - MidnightOfferRootSignatureArgs ->; +export type MidnightOfferRootRequirement = + Requirement; /** Permit or Permit2 token signature requirement. */ export type TokenSignatureRequirement = Bundler3TokenSignatureRequirement; @@ -813,7 +776,7 @@ export function isMidnightOfferRootSignature( return signature.action.type === "midnightOfferRootSignature"; } -/** The typed requirement-signature slots a transaction builder consumes, split from a `buildTx` array. */ +/** Typed requirement-signature slots consumed while building a prepared transaction plan. */ export interface SelectedRequirementSignatures { /** The single permit / Permit2 signature, when present. */ permit?: PermitRequirementSignature; @@ -824,7 +787,7 @@ export interface SelectedRequirementSignatures { } /** - * Splits a `buildTx` signature array into its typed requirement-signature slots, rejecting + * Splits transaction-plan signatures into typed requirement-signature slots, rejecting * ambiguous or unexpected input so a path never silently consumes the wrong signature. * * A bundled path consumes at most one signature of each accepted kind. Passing several of the same @@ -832,7 +795,7 @@ export interface SelectedRequirementSignatures { * dropping the extras — the latter could otherwise leave a required authorization or permit * unsigned (and the bundle reverting on-chain) or apply the wrong signature. * - * @param signatures - The signatures passed to `buildTx`. + * @param signatures - The signatures passed to `PreparedTransactionPlan.build()`. * @param accepts - Which signature kinds this operation consumes. * @param accepts.permit - Whether a permit / Permit2 signature is consumed. * @param accepts.authorization - Whether a Morpho authorization signature is consumed. diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 263382137..3d7d4665f 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -73,14 +73,15 @@ export namespace BundlerErrors { } } -/** Requirement signature kind accepted by action-output transaction builders. */ +/** Requirement signature kind accepted by prepared transaction plans. */ export type RequirementSignatureKind = | "permit" | "authorization" | "midnightOfferRootSignature"; /** - * Thrown when `buildTx` receives more than one requirement signature of the same kind. + * Thrown when `PreparedTransactionPlan.build()` receives more than one requirement signature of + * the same kind. * * A bundled path consumes at most one signature per accepted kind; passing several of the same * kind is ambiguous and would silently drop all but the first, so it is rejected instead. @@ -90,7 +91,7 @@ export type RequirementSignatureKind = * import { AmbiguousRequirementSignaturesError } from "@morpho-org/morpho-sdk"; * * if (error instanceof AmbiguousRequirementSignaturesError) { - * // Pass a single signature per accepted kind to buildTx. + * // Pass a single signature per accepted kind to prepared.build(). * } * ``` */ @@ -101,13 +102,14 @@ export class AmbiguousRequirementSignaturesError extends Error { */ constructor(kind: RequirementSignatureKind, count: number) { super( - `Expected at most one ${kind} signature but received ${count}. Pass a single ${kind} signature to buildTx.`, + `Expected at most one ${kind} signature but received ${count}. Pass a single ${kind} signature to PreparedTransactionPlan.build().`, ); } } /** - * Thrown when `buildTx` receives a requirement signature of a kind the operation does not consume. + * Thrown when `PreparedTransactionPlan.build()` receives a requirement signature of a kind the + * operation does not consume. * Surfacing it prevents a signed requirement from being silently ignored. * * @example @@ -115,7 +117,7 @@ export class AmbiguousRequirementSignaturesError extends Error { * import { UnexpectedRequirementSignatureError } from "@morpho-org/morpho-sdk"; * * if (error instanceof UnexpectedRequirementSignatureError) { - * // Remove the signature this operation does not use from the buildTx array. + * // Remove the signature this operation does not use from prepared.build(). * } * ``` */ @@ -125,7 +127,7 @@ export class UnexpectedRequirementSignatureError extends Error { */ constructor(kind: RequirementSignatureKind) { super( - `Received a ${kind} signature that this operation does not consume. Remove it from the buildTx signatures array.`, + `Received a ${kind} signature that this operation does not consume. Remove it from the signatures passed to PreparedTransactionPlan.build().`, ); } } From c2caf1d6fe801ff2a4375fcbc0d5ce534cc69b8f Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 17 Jul 2026 10:06:13 +0200 Subject: [PATCH 06/12] refactor(morpho-sdk): simplify transaction plan API --- ...-06-03-midnight-action-output-interface.md | 69 ++-- packages/morpho-sdk/ARCHITECTURE.md | 4 +- packages/morpho-sdk/BUNDLER3.md | 2 +- packages/morpho-sdk/README.md | 20 +- packages/morpho-sdk/examples/example.ts | 16 +- .../src/actions/midnight/supplyCollateral.ts | 2 +- .../src/client/morphoViemExtension.ts | 2 +- packages/morpho-sdk/src/entities/AGENTS.md | 2 +- .../src/entities/blue/blue.refinance.test.ts | 25 +- .../morpho-sdk/src/entities/blue/blue.test.ts | 146 +++---- packages/morpho-sdk/src/entities/blue/blue.ts | 20 +- .../src/entities/midnight/midnight.test.ts | 100 ++--- .../src/entities/midnight/midnight.ts | 24 +- .../morpho-sdk/src/entities/midnight/types.ts | 10 +- .../src/entities/vaultV1/vaultV1.test.ts | 70 ++-- .../src/entities/vaultV1/vaultV1.ts | 14 +- .../src/entities/vaultV2/vaultV2.test.ts | 5 +- .../src/entities/vaultV2/vaultV2.ts | 20 +- .../src/helpers/computeReallocations.ts | 2 +- .../transactionPlan/TransactionPlan.test.ts | 106 +++-- .../src/transactionPlan/TransactionPlan.ts | 383 ++++++++++++------ .../morpho-sdk/src/transactionPlan/types.ts | 110 +++-- packages/morpho-sdk/src/types/AGENTS.md | 4 +- packages/morpho-sdk/src/types/action.ts | 18 +- .../test/actions/blue/authorization.test.ts | 85 ++-- .../test/actions/blue/borrow.test.ts | 27 +- .../test/actions/blue/reallocations.test.ts | 33 +- .../test/actions/blue/refinance.test.ts | 23 +- .../test/actions/blue/repay.test.ts | 29 +- .../blue/repayWithdrawCollateral.test.ts | 25 +- .../test/actions/blue/supply.test.ts | 23 +- .../actions/blue/supplyCollateral.test.ts | 41 +- .../blue/supplyCollateralBorrow.test.ts | 37 +- .../test/actions/blue/withdraw.test.ts | 25 +- .../actions/blue/withdrawCollateral.test.ts | 5 +- .../morpho-sdk/test/actions/metadata.test.ts | 11 +- .../actions/midnight/requirements.test.ts | 41 +- .../actions/requirements/approval.test.ts | 9 +- .../test/actions/requirements/permit.test.ts | 15 +- .../test/actions/requirements/permit2.test.ts | 38 +- .../test/actions/vaultV1/deposit.test.ts | 49 ++- .../test/actions/vaultV1/migrateToV2.test.ts | 11 +- .../test/actions/vaultV1/redeem.test.ts | 17 +- .../test/actions/vaultV1/withdraw.test.ts | 11 +- .../test/actions/vaultV2/deposit.test.ts | 5 +- .../test/actions/vaultV2/forceRedeem.test.ts | 7 +- .../actions/vaultV2/forceWithdraw.test.ts | 7 +- .../test/actions/vaultV2/redeem.test.ts | 3 +- .../test/actions/vaultV2/withdraw.test.ts | 3 +- packages/morpho-sdk/test/actions/wrap.test.ts | 29 +- .../morpho-sdk/test/transactionPlanUtils.ts | 88 ---- .../src/errors.test.ts | 11 - .../src/errors.ts | 20 - .../src/index.ts | 1 - .../src/morpho-protocol-evm.test.ts | 61 ++- .../src/morpho-protocol-evm.ts | 15 +- 56 files changed, 1070 insertions(+), 909 deletions(-) delete mode 100644 packages/morpho-sdk/test/transactionPlanUtils.ts delete mode 100644 packages/wdk-protocol-lending-morpho-evm/src/errors.test.ts delete mode 100644 packages/wdk-protocol-lending-morpho-evm/src/errors.ts diff --git a/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md b/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md index d9118392e..7cd8b2413 100644 --- a/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md +++ b/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md @@ -15,8 +15,8 @@ This TIB specifies the implementation of Midnight action flows in `morpho-sdk`. The markets app is also the compatibility target. To minimize its diff, the SDK exposes each flow as a lazy `TransactionPlan`: the app prepares the plan when it is ready to discover prerequisites, -renders the typed signature and call requests, collects signatures at its wallet boundary, and then -builds the executable call sequence. +renders the typed signature requests and transaction steps, collects signatures at its wallet +boundary, and then builds the executable transaction sequence. ```ts const prepared = await plan.prepare(); @@ -27,17 +27,18 @@ for (const request of prepared.signatureRequests) { } const executable = prepared.build(signatures); -const calls = executable.callRequests.map((request) => request.call); +const calls = executable.calls; ``` The transaction-plan contract is shared by Midnight, Blue, MarketV1, and vault entities: - `TransactionPlan.prepare(...)` resolves only the prerequisites for the already selected action. - `PreparedTransactionPlan.signatureRequests` exposes signable requirements without signing them. -- `PreparedTransactionPlan.callRequests` exposes ordered viem-compatible prerequisite calls and, - when previewable, the primary call. +- `PreparedTransactionPlan.transactionSteps` exposes ordered prerequisite transactions and, when + previewable, the primary step. Its `.calls` getter converts those transactions to + viem-compatible call parameters for wallet batching. - `PreparedTransactionPlan.build(...)` consumes collected signatures and returns an - `ExecutableTransactionPlan` whose primary call is last. + `ExecutableTransactionPlan` whose primary step is last. - `TransactionPlan` preserves the primary action, request-options, prerequisite-request, and signature types through preparation and execution. @@ -54,7 +55,7 @@ into `morpho-sdk`. The plan model makes prerequisite ordering and semantic intent explicit without importing the app's execution engine. Requirement descriptors remain either signable requests or typed transactions; -the prepared plan converts them into app-facing signature and call requests. Blue, MarketV1, +the prepared plan converts them into app-facing signature requests and transaction steps. Blue, MarketV1, vault, and Midnight methods use narrower `TransactionPlan` generic arguments where their request sets are known. @@ -114,7 +115,7 @@ const plan: TransactionPlan< > = await midnight.makeLend(params); const prepared = await plan.prepare(); -// The app presents prepared.signatureRequests and prepared.callRequests. +// The app presents prepared.signatureRequests and prepared.transactionSteps. const executable = prepared.build(collectedSignatures); ``` @@ -122,14 +123,14 @@ Semantics: 1. `prepare()` resolves the currently required prerequisite descriptors and returns semantic request metadata. 2. Already-satisfied approvals or authorizations are omitted. -3. `steps` and `callRequests` preserve relative execution order; the primary call is last when present. +3. `steps` and `transactionSteps` preserve relative execution order; the primary step is last when present. 4. Signature descriptors are exposed through `signatureRequests`; the SDK never prompts the wallet on its own. -5. A prerequisite call can be an approval, authorization, contract-wallet ratify-root, or mandatory prelude transaction. -6. `build(signatures)` validates and consumes the collected signature values before encoding the primary call. -7. `TRequest` remains available on prepared steps, and `TPrimaryAction` remains available on executable calls. +5. A prerequisite transaction step can be an approval, authorization, contract-wallet ratify-root, or mandatory prelude transaction. +6. `build(signatures)` validates and consumes the collected signature values before encoding the primary transaction. +7. `TRequest` remains available on prepared steps, and `TPrimaryAction` remains available on executable steps. -Midnight flows that need one final transaction remain single-call plans. Flows with mandatory prelude -transactions expose them as preparation-phase call requests, and the markets app forwards the maker +Midnight flows that need one final transaction remain single-transaction plans. Flows with mandatory +prelude transactions expose them as preparation-phase transaction steps, and the markets app forwards the maker signature it already collects into `build(...)`. ## Description: markets app migration boundary @@ -169,7 +170,7 @@ Maker flows remain mempool flows, not bundle flows: The markets app can adapt SDK output once and reuse the adapter across every screen. The adapter description here is intentionally illustrative, not an implementation-ready patch. The implementation PR can choose different function names, labels, and control flow as long as the boundary stays the same: -- SDK transaction plans expose typed signature and call requests plus a final executable builder; +- SDK transaction plans expose typed signature requests and transaction steps plus a final executable builder; - the app wallet layer turns SDK signature requirements into signature prompts; - the app turns SDK transaction requirements into ordered call requests; - collected maker signatures are passed to `PreparedTransactionPlan.build(...)`; @@ -373,16 +374,16 @@ This is action-encoding metadata, not UI state. The first implementation only en Add a named call-requirement union. Existing raw `Transaction<...>` requirement values stay valid. ```ts -export type CallRequirementAction = +export type TransactionRequirementAction = | ERC20ApprovalAction | MorphoAuthorizationAction | MidnightAuthorizationAction | SetterRatifierRatifyRootAction | MidnightSupplyCollateralAction; -export type CallRequirement = Readonly>; +export type TransactionRequirement = Readonly>; -export type ActionRequirement = CallRequirement | SignatureRequirement; +export type ActionRequirement = TransactionRequirement | SignatureRequirement; ``` `MidnightSupplyCollateralAction` is included because it can be a mandatory prelude transaction for a currently implemented app flow: @@ -678,7 +679,7 @@ These fields should not be first-iteration public inputs. Exposing them before t 1. optional loan-token approval requirement for `MidnightBundles`; 2. optional `MidnightAuthorizationAction` for `Midnight.setIsAuthorized(MidnightBundles, true, taker)`. -`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightTakeLendAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary step carries `MidnightTakeLendAction`: ```ts MidnightBundles.midnightBundlesV1BuyWithAssetsTargetAndWithdrawCollateral( @@ -706,7 +707,7 @@ No offer-root or token signature is involved in the first implementation. 1. optional collateral-token approval requirement for `MidnightBundles` when new collateral is supplied; 2. optional `MidnightAuthorizationAction` for `Midnight.setIsAuthorized(MidnightBundles, true, taker)`. -`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightTakeBorrowAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary step carries `MidnightTakeBorrowAction`: ```ts MidnightBundles.midnightBundlesV1SupplyCollateralAndSellWithAssetsTarget( @@ -730,7 +731,7 @@ No offer-root or token signature is involved in the first implementation. `prepare()` exposes an optional collateral approval to `Midnight`. -`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightSupplyCollateralAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary step carries `MidnightSupplyCollateralAction`: ```ts Midnight.supplyCollateral(market, 0n, collateralAssets, onBehalf) @@ -754,7 +755,7 @@ Contract wallet: 2. optional `MidnightAuthorizationAction` for the chosen ratifier; 3. one `setterRatifierRatifyRoot` transaction requirement. -`PreparedTransactionPlan.build(signatures)` produces an executable plan whose primary call carries `MempoolSubmitOffersAction` and targets the mempool contract. +`PreparedTransactionPlan.build(signatures)` produces an executable plan whose primary step carries `MempoolSubmitOffersAction` and targets the mempool contract. The make-lend method accepts a tree-like offer set. It must accept multi-market offer legs in the same tree when the markets share one loan token, matching the markets app's multi-limit-order / OCA basket flow. For those baskets, offers in the same group share one `consumed[maker][group]` counter on Midnight, so the new group contributes one reserve amount: the maximum leg reserve, with equal leg reserves expected for the current OCA shape. It is not the sum of every offer leg. The SDK throws a typed error when the tree would exceed the Midnight tree-size limit. @@ -768,7 +769,7 @@ Maker reserve approvals stay transaction approvals in this migration. The final `prepare()` exposes an optional collateral approval to `Midnight`. -`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightSupplyCollateralAction`. +`PreparedTransactionPlan.build()` produces an executable plan whose primary step carries `MidnightSupplyCollateralAction`. This branch remains approval-based because direct `Midnight.supplyCollateral(...)` has no `TokenPermit` argument. @@ -786,7 +787,7 @@ Contract wallet: 1. optional `MidnightAuthorizationAction` for the chosen ratifier; 2. one `setterRatifierRatifyRoot` transaction requirement. -`PreparedTransactionPlan.build(signatures)` produces an executable plan whose primary call carries `MempoolSubmitOffersAction` and targets the mempool contract. +`PreparedTransactionPlan.build(signatures)` produces an executable plan whose primary step carries `MempoolSubmitOffersAction` and targets the mempool contract. ### Borrow limit collateral + loan branch @@ -806,7 +807,7 @@ Contract wallet: 3. optional `MidnightAuthorizationAction` for the chosen ratifier; 4. one `setterRatifierRatifyRoot` transaction requirement. -`PreparedTransactionPlan.build(signatures)` produces an executable plan containing only the final `MempoolSubmitOffersAction` primary call. +`PreparedTransactionPlan.build(signatures)` produces an executable plan containing only the final `MempoolSubmitOffersAction` primary step. This branch is the reason `prepare()` must be allowed to return mandatory prelude transactions, not only optional prerequisites. @@ -829,7 +830,7 @@ The latest `morpho-org/midnight` implementation does not cap withdrawals at net `prepare()` exposes no prerequisite requests. -`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightRedeemAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary step carries `MidnightRedeemAction`: ```ts Midnight.withdraw(market, redeemUnits, onBehalf, receiver) @@ -842,17 +843,17 @@ All three app branches keep the current bundled execution path. Repay only: - `prepare()` exposes an optional loan-token approval requirement for `MidnightBundles`, then optional `MidnightAuthorizationAction` for `MidnightBundles`; -- `PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightRepayWithdrawCollateralAction`. +- `PreparedTransactionPlan.build()` produces an executable plan whose primary step carries `MidnightRepayWithdrawCollateralAction`. Withdraw-only: - `prepare()` exposes an optional `MidnightAuthorizationAction` for `MidnightBundles`; -- `PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightRepayWithdrawCollateralAction` with `repayAssets === 0n`. +- `PreparedTransactionPlan.build()` produces an executable plan whose primary step carries `MidnightRepayWithdrawCollateralAction` with `repayAssets === 0n`. Repay + withdraw: - `prepare()` exposes an optional loan-token approval requirement for `MidnightBundles`, then optional `MidnightAuthorizationAction` for `MidnightBundles`; -- `PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightRepayWithdrawCollateralAction`. +- `PreparedTransactionPlan.build()` produces an executable plan whose primary step carries `MidnightRepayWithdrawCollateralAction`. ```ts MidnightBundles.midnightBundlesV1RepayAndWithdrawCollateral( @@ -872,7 +873,7 @@ MidnightBundles.midnightBundlesV1RepayAndWithdrawCollateral( `prepare()` exposes no prerequisite requests. -`PreparedTransactionPlan.build()` produces an executable plan whose primary call carries `MidnightCancelOfferAction`: +`PreparedTransactionPlan.build()` produces an executable plan whose primary step carries `MidnightCancelOfferAction`: ```ts Midnight.setConsumed(group, maxUint256, onBehalf) @@ -882,7 +883,7 @@ Midnight.setConsumed(group, maxUint256, onBehalf) This implementation represents the current markets app flows, with the documented redeem default divergence, because: -- every prerequisite transaction maps to a preparation-phase `callRequest`, and every primary transaction maps to the primary `callRequest` returned by `PreparedTransactionPlan.build()`; +- every prerequisite transaction maps to a preparation-phase transaction step, and every primary transaction maps to the `primaryStep` returned by `PreparedTransactionPlan.build()`; - every maker signature requirement maps to a `signatureRequest` whose `sign(...)` delegates to `Requirement.sign(...)`; - the `Transaction` wire shape is unchanged; - no app builder currently needs `before` / `after` callback semantics; @@ -903,7 +904,7 @@ The only semantic expansion is documented: returned transaction requirements are Return the whole transaction sequence from the transaction plan. -**Why rejected:** it would add a second way to expose the ordered call sequence already represented by prepared and executable plans. The current app flows only need one final transaction plus ordered pre-execution items. +**Why rejected:** it would add a second way to expose the ordered transaction sequence already represented by prepared and executable plans. The current app flows only need one final transaction plus ordered pre-execution items. ### Alternative 2: Port the app `ActionFlow` abstraction @@ -936,7 +937,7 @@ Add ERC2612 and Permit2 SignatureTransfer support to the first action-flow imple - **Wallet-decodable offer-tree signing.** The SDK must build the offer tree locally from the SDK input and validate the router response before exposing `midnightOfferRootSignature`. The wallet signs EIP-712 `OfferTree` typed data whose leaves are visible to the user, and the SDK verifies that the signed tree hashes to the root used in ratifier data. - **No signing inside actions.** `Requirement.sign(...)` is the only signing boundary and takes a `WalletClient` from the integrator. -- **No hidden prelude txs.** Mandatory prelude transactions are visible as typed preparation-phase `callRequests` on the prepared plan. +- **No hidden prelude txs.** Mandatory prelude transactions are visible as typed preparation-phase `transactionSteps` on the prepared plan. - **Authorization target is explicit.** `MidnightAuthorizationAction.args.authorized` is either `MidnightBundles`, `EcrecoverRatifier`, or `SetterRatifier`; never inferred by a consumer. - **Approval target is explicit.** Midnight approval helper callers pass `spender`; no default to `GeneralAdapter1`. - **Bundle token-pull policy is explicit.** Bundle flows use `MidnightBundles` as spender and never consume or reset the core `Midnight` allowance reserved by maker offers. @@ -946,7 +947,7 @@ Add ERC2612 and Permit2 SignatureTransfer support to the first action-flow imple ## Future considerations - If a future Midnight flow needs a real wait condition (`before` / `after` equivalent), add a small `wait` requirement kind at that time. Do not preemptively port app `ActionFlow`. -- If consumers strongly reject mandatory preparation-phase `callRequests`, revisit a dedicated sequence-building API with evidence from integration feedback. +- If consumers strongly reject mandatory preparation-phase `transactionSteps`, revisit a dedicated sequence-building API with evidence from integration feedback. ## References diff --git a/packages/morpho-sdk/ARCHITECTURE.md b/packages/morpho-sdk/ARCHITECTURE.md index 0f7202ba4..0efc73c92 100644 --- a/packages/morpho-sdk/ARCHITECTURE.md +++ b/packages/morpho-sdk/ARCHITECTURE.md @@ -14,8 +14,8 @@ operations on EVM-compatible chains for Morpho protocol. always produces the same `Transaction` object. No simulation, no gas estimation, no sending — the consumer handles those concerns. - **Predictable developer experience.** Every entity operation returns a lazy `TransactionPlan`. - `prepare()` resolves prerequisite calls and signature requests, while - `PreparedTransactionPlan.build(signatures)` returns the ordered executable calls. The lifecycle + `prepare()` resolves prerequisite transactions and signature requests, while + `PreparedTransactionPlan.build(signatures)` returns the ordered executable transactions. The lifecycle is identical across VaultV1, VaultV2, Blue, and Midnight. - **Immutability.** Every returned `Transaction` is deep-frozen via `@morpho-org/morpho-ts`'s `deepFreeze`. Once built, a transaction object cannot be mutated. diff --git a/packages/morpho-sdk/BUNDLER3.md b/packages/morpho-sdk/BUNDLER3.md index 243bdf162..d8ed6fa72 100644 --- a/packages/morpho-sdk/BUNDLER3.md +++ b/packages/morpho-sdk/BUNDLER3.md @@ -98,7 +98,7 @@ This is the main design caveat. For the following operations the SDK emits a **d ### Other pitfalls -- **Blue authorization for GA1 required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`.** A user who has never granted it will receive a request through [`getBlueAuthorizationRequirement`](src/actions/requirements/blue/getBlueAuthorizationRequirement.ts) when the plan is prepared. Without signature support, this is a `setAuthorization` call that appears before the primary call. With `supportSignature`, it is a signable request; pass the resulting `AuthorizationRequirementSignature` to `PreparedTransactionPlan.build()`, which folds it into the bundle as `setAuthorizationWithSig`. +- **Blue authorization for GA1 required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`.** A user who has never granted it will receive a request through [`getBlueAuthorizationRequirement`](src/actions/requirements/blue/getBlueAuthorizationRequirement.ts) when the plan is prepared. Without signature support, this is a `setAuthorization` transaction step that appears before the primary step. With `supportSignature`, it is a signable request; pass the resulting `AuthorizationRequirementSignature` to `PreparedTransactionPlan.build()`, which folds it into the bundle as `setAuthorizationWithSig`. - **Critical order in `repayWithdrawCollateral`**: `morphoRepay` **must** precede `morphoWithdrawCollateral` in the bundle, otherwise the position is deemed unhealthy at withdraw time and the tx reverts. - **Builder must equal signer.** Bundler actions reference accounts in two different ways: some take an explicit `onBehalf` and act on `userAddress` (e.g. `morphoRepay`), others act implicitly on the **initiator** — the `msg.sender` of `bundler3.multicall`, i.e. the EOA signing the tx, not the adapter — (e.g. `erc20TransferFrom`, `morphoWithdrawCollateral`, the latter exposing no `onBehalf` parameter on GA1). `repayWithdrawCollateral` is the canonical example: the repay leg targets `userAddress` while the transfer-from and the withdraw target the initiator. If the address that built the tx (and filled `userAddress`) is not the address that signs/executes it, the bundle would repay one account's debt while pulling tokens from and withdrawing collateral against the signer. Transaction builders do not validate this at build time — callers MUST keep `userAddress` aligned with the signing account. The signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) take a `WalletClient` and enforce this at `sign()` time via `validateUserAddress` (throws `MissingClientPropertyError` / `AddressMismatchError`). - **Tricky `tx.value`**: whenever a `nativeAmount` or a `reallocateTo` (native fee) is involved, `BundlerAction.encodeBundle` computes `tx.value`. Do not overwrite it on the caller side. diff --git a/packages/morpho-sdk/README.md b/packages/morpho-sdk/README.md index 96fbf2df3..14d68394c 100644 --- a/packages/morpho-sdk/README.md +++ b/packages/morpho-sdk/README.md @@ -53,9 +53,10 @@ Every entity action returns a lazy `TransactionPlan`: - `plan.prepare()` resolves the prerequisite call and signature requests for the chosen action. - `prepared.signatureRequests` contains permits, Morpho authorizations, or Midnight offer-root signatures that the wallet must sign. -- `prepared.callRequests` contains prerequisite calls and, when it can be previewed without a - signature, the primary call last. -- `prepared.build(signatures)` returns the executable calls, ordered with the primary call last. +- `prepared.transactionSteps` contains prerequisite transaction steps and, when it can be previewed + without a signature, the primary step last. +- `prepared.build(signatures)` returns the executable transaction steps, ordered with the primary + step last. Typical requirements: @@ -81,10 +82,13 @@ const signatures = await Promise.all( ); const executable = prepared.build(signatures); -// Send every call in order; prerequisite calls come before the primary action. -for (const request of executable.callRequests) { - await walletClient.sendTransaction(request.call); +// Send every transaction step in order; prerequisite steps come before the primary action. +for (const step of executable.transactionSteps) { + await walletClient.sendTransaction(step.transaction); } + +// Or convert the executable transactions for an EIP-5792 wallet batch. +await walletClient.sendCalls({ calls: executable.calls }); ``` The focused examples below use `signatures` for the array collected from @@ -496,7 +500,7 @@ const prepared = await shareRefinancePlan.prepare(); const executable = prepared.build(signatures); ``` -The SDK validates ownership, token/id match, and that amounts do not exceed the source position. Health is checked against `LLTV āˆ’ buffer` where it can degrade: the residual source position is validated whenever debt remains after the repay, and the aggregate target position is validated whenever a borrow leg is migrated. Collateral-only migrations (both borrow fields omitted) skip the target health check — they can't degrade target health and would otherwise fail on missing-oracle target markets. Both markets are forward-accrued to `now`; in shares mode the target borrow overshoots by `slippageTolerance` and the callback sweeps the residual back into the target debt (or skims it to the user). `plan.prepare()` exposes the `setAuthorization(generalAdapter1, true)` transaction when GA1 is not yet authorized — a single global authorization covers both markets. Optional `targetReallocations` top up target-market liquidity via the **PublicAllocator** (same mechanism as `borrow`); their fees add to the primary call's `value`. +The SDK validates ownership, token/id match, and that amounts do not exceed the source position. Health is checked against `LLTV āˆ’ buffer` where it can degrade: the residual source position is validated whenever debt remains after the repay, and the aggregate target position is validated whenever a borrow leg is migrated. Collateral-only migrations (both borrow fields omitted) skip the target health check — they can't degrade target health and would otherwise fail on missing-oracle target markets. Both markets are forward-accrued to `now`; in shares mode the target borrow overshoots by `slippageTolerance` and the callback sweeps the residual back into the target debt (or skims it to the user). `plan.prepare()` exposes the `setAuthorization(generalAdapter1, true)` transaction when GA1 is not yet authorized — a single global authorization covers both markets. Optional `targetReallocations` top up target-market liquidity via the **PublicAllocator** (same mechanism as `borrow`); their fees add to the primary transaction's `value`. #### Borrow with Shared Liquidity (Reallocations) @@ -530,7 +534,7 @@ const plan = market.borrow({ const prepared = await plan.prepare(); const executable = prepared.build(signatures); -// The primary call's value includes the sum of all reallocation fees. +// The primary transaction's value includes the sum of all reallocation fees. ``` Reallocations also work with `supplyCollateralBorrow`: diff --git a/packages/morpho-sdk/examples/example.ts b/packages/morpho-sdk/examples/example.ts index 8cdf7162e..bee045249 100644 --- a/packages/morpho-sdk/examples/example.ts +++ b/packages/morpho-sdk/examples/example.ts @@ -54,9 +54,8 @@ async function main() { vaultData, }); const preparedDeposit = await deposit.prepare(); - const depositCall = preparedDeposit.callRequests.at(-1); - if (depositCall == null) throw new Error("Deposit call is unavailable"); - const depositTx = depositCall.tx; + const depositTx = preparedDeposit.primaryTransaction; + if (depositTx == null) throw new Error("Deposit transaction is unavailable"); console.log("Deposit transaction:", { to: depositTx.to, @@ -66,7 +65,7 @@ async function main() { // Get requests (e.g., ERC20 approval or permit signature) console.log("\nšŸ“‹ Checking requests..."); - console.log(`Found ${preparedDeposit.requestCount} request(s)`); + console.log(`Found ${preparedDeposit.stepCount} request(s)`); preparedDeposit.steps.forEach((request, index) => { console.log(` Request ${index + 1}:`, request); }); @@ -79,8 +78,9 @@ async function main() { userAddress: USER_ADDRESS, }); const preparedWithdraw = await withdraw.prepare(); - const withdrawTx = preparedWithdraw.callRequests.at(-1)?.tx; - if (withdrawTx == null) throw new Error("Withdraw call is unavailable"); + const withdrawTx = preparedWithdraw.primaryTransaction; + if (withdrawTx == null) + throw new Error("Withdraw transaction is unavailable"); console.log("Withdraw transaction:", { to: withdrawTx.to, @@ -96,8 +96,8 @@ async function main() { userAddress: USER_ADDRESS, }); const preparedRedeem = await redeem.prepare(); - const redeemTx = preparedRedeem.callRequests.at(-1)?.tx; - if (redeemTx == null) throw new Error("Redeem call is unavailable"); + const redeemTx = preparedRedeem.primaryTransaction; + if (redeemTx == null) throw new Error("Redeem transaction is unavailable"); console.log("Redeem transaction:", { to: redeemTx.to, diff --git a/packages/morpho-sdk/src/actions/midnight/supplyCollateral.ts b/packages/morpho-sdk/src/actions/midnight/supplyCollateral.ts index a54b6580d..2ff80c87d 100644 --- a/packages/morpho-sdk/src/actions/midnight/supplyCollateral.ts +++ b/packages/morpho-sdk/src/actions/midnight/supplyCollateral.ts @@ -30,7 +30,7 @@ export interface MidnightSupplyCollateralParams { * Use this low-level builder when a flow already knows the market, collateral * index, and approval state. App flows should usually call * `client.morpho.midnight(chainId).supplyCollateral(...)`, which resolves the - * ERC20 approval call request when the transaction plan is prepared. + * ERC20 approval transaction step when the transaction plan is prepared. * * @param params.chainId - Chain id used to resolve `Midnight`. * @param params.market - Midnight market receiving collateral. diff --git a/packages/morpho-sdk/src/client/morphoViemExtension.ts b/packages/morpho-sdk/src/client/morphoViemExtension.ts index ac52c7a91..6717fe578 100644 --- a/packages/morpho-sdk/src/client/morphoViemExtension.ts +++ b/packages/morpho-sdk/src/client/morphoViemExtension.ts @@ -112,7 +112,7 @@ function createMorphoNamespace( * for (const request of prepared.signatureRequests) { * signatures.push(await request.sign(client, user)); * } - * const tx = prepared.build(signatures).callRequests.at(-1)?.tx; + * const tx = prepared.build(signatures).primaryTransaction; * ``` */ export function morphoViemExtension(_options?: { diff --git a/packages/morpho-sdk/src/entities/AGENTS.md b/packages/morpho-sdk/src/entities/AGENTS.md index 8e6c1f339..5f1631c33 100644 --- a/packages/morpho-sdk/src/entities/AGENTS.md +++ b/packages/morpho-sdk/src/entities/AGENTS.md @@ -7,7 +7,7 @@ - Fetch on-chain state (vault accrual data, market/position data). - Compute derived values (e.g. `maxSharePrice` with slippage, LLTV buffer health). - Validate `chainId` matches the client before any on-chain read or transaction construction. Entities do not enforce builder = signer at build time — callers MUST keep `userAddress` aligned with the signing account. The invariant is enforced at `sign()` time on the signature requirements (`encodeErc20Permit` / `encodeErc20Permit2Approve`) via `validateUserAddress`. -- Return lazy `TransactionPlan` instances — no side effects at construction. `prepare()` performs targeted prerequisite discovery; the prepared plan exposes signature/call requests and builds the executable call sequence. +- Return lazy `TransactionPlan` instances — no side effects at construction. `prepare()` performs targeted prerequisite discovery; the prepared plan exposes signature requests and transaction steps and builds the executable transaction sequence. ## Routing diff --git a/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts b/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts index 62a1f845e..f6fc40cef 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts @@ -1,15 +1,16 @@ import { AccrualPosition, + getChainAddresses, Market, MarketParams, MathLib, ORACLE_PRICE_SCALE, } from "@morpho-org/blue-sdk"; -import { createMockClient } from "@morpho-org/test/mock"; +import { blueAbi } from "@morpho-org/blue-sdk-viem"; +import { createMockClient, mockRead } from "@morpho-org/test/mock"; import { type Address, parseUnits } from "viem"; import { mainnet } from "viem/chains"; import { describe, expect, test } from "vitest"; -import { buildPlanTx } from "../../../test/transactionPlanUtils.js"; import { morphoViemExtension } from "../../client/index.js"; import { computeMinBorrowSharePrice } from "../../helpers/index.js"; import { @@ -82,8 +83,14 @@ const makePosition = (params: { ); const makeMarket = () => { - const { client } = createMockClient(mainnet); - return client + const handle = createMockClient(mainnet); + mockRead(handle, { + address: getChainAddresses(mainnet.id).morpho, + abi: blueAbi, + functionName: "isAuthorized", + result: true, + }); + return handle.client .extend(morphoViemExtension()) .morpho.blue(sourceParams, mainnet.id); }; @@ -357,7 +364,7 @@ describe("MorphoBlue.refinance", () => { borrowShares: parseUnits("100", 12), }); - const tx = await buildPlanTx(refi); + const tx = (await refi.prepare()).build().primaryTransaction; expect(tx.action.type).toBe("blueRefinance"); expect(tx.action.args.sourceMarket).toBe(sourceParams.id); expect(tx.action.args.targetMarket).toBe(targetParams.id); @@ -388,7 +395,7 @@ describe("MorphoBlue.refinance", () => { borrowAssets: parseUnits("50", 6), }); - const tx = await buildPlanTx(refi); + const tx = (await refi.prepare()).build().primaryTransaction; expect(tx.action.args.borrowAssets).toBe(parseUnits("50", 6)); expect(tx.action.args.borrowShares).toBe(0n); }); @@ -499,7 +506,7 @@ describe("MorphoBlue.refinance", () => { borrowShares: parseUnits("100", 12), slippageTolerance, }); - const tx = await buildPlanTx(refi); + const tx = (await refi.prepare()).build().primaryTransaction; // Recompute the entity's intermediate values (accrual deltas cancel for this fixture). const projectedBorrowAssets = sourceMarket.toBorrowAssets( @@ -543,7 +550,7 @@ describe("MorphoBlue.refinance", () => { collateralAmount: parseUnits("1", 18), }); - const tx = await buildPlanTx(refi); + const tx = (await refi.prepare()).build().primaryTransaction; expect(tx.action.args.borrowAssets).toBe(0n); expect(tx.action.args.borrowShares).toBe(0n); }); @@ -588,7 +595,7 @@ describe("MorphoBlue.refinance", () => { targetReallocations, }); - const tx = await buildPlanTx(refi); + const tx = (await refi.prepare()).build().primaryTransaction; expect(tx.value).toBe(fee); expect(tx.action.args.reallocationFee).toBe(fee); }); diff --git a/packages/morpho-sdk/src/entities/blue/blue.test.ts b/packages/morpho-sdk/src/entities/blue/blue.test.ts index a2a83f8de..4e3a8a3d1 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.test.ts @@ -12,10 +12,6 @@ import { mainnet } from "viem/chains"; import { describe, expect } from "vitest"; import { CbbtcUsdcBlue, WstethWethBlue } from "../../../test/fixtures/blue.js"; import { test } from "../../../test/setup.js"; -import { - buildPlanTx, - getPlanRequests, -} from "../../../test/transactionPlanUtils.js"; import { morphoViemExtension } from "../../client/index.js"; import { isRequirementApproval, @@ -105,7 +101,7 @@ describe("MorphoBlue builder = signer freedom", () => { amount: parseUnits("1", 18), }); - const tx = await buildPlanTx(supplyCollateral); + const tx = (await supplyCollateral.prepare()).build().primaryTransaction; expect(tx.action.args.onBehalf).toBe(OTHER_USER); }); @@ -124,7 +120,7 @@ describe("MorphoBlue builder = signer freedom", () => { amount: parseUnits("1", 18), }); - const tx = await buildPlanTx(supplyCollateral); + const tx = (await supplyCollateral.prepare()).build().primaryTransaction; expect(tx.action.args.onBehalf).toBe(OTHER_USER); }); }); @@ -175,13 +171,15 @@ describe("MorphoBlue validation", () => { ) .morpho.blue(CbbtcUsdcBlue, mainnet.id); - const requirements = await getPlanRequests( - market.withdraw({ - assets: 1n, - userAddress: USER, - positionData: makePosition({ supplyShares: 10n ** 18n }), - }), - ); + const requirements = ( + await market + .withdraw({ + assets: 1n, + userAddress: USER, + positionData: makePosition({ supplyShares: 10n ** 18n }), + }) + .prepare() + ).requirements; expect(requirements).toHaveLength(1); }); @@ -203,13 +201,15 @@ describe("MorphoBlue validation", () => { ) .morpho.blue(CbbtcUsdcBlue, mainnet.id); - const requirements = await getPlanRequests( - market.withdraw({ - assets: 1n, - userAddress: USER, - positionData: makePosition({ supplyShares: 10n ** 18n }), - }), - ); + const requirements = ( + await market + .withdraw({ + assets: 1n, + userAddress: USER, + positionData: makePosition({ supplyShares: 10n ** 18n }), + }) + .prepare() + ).requirements; expect(requirements).toEqual([]); }); @@ -300,14 +300,16 @@ describe("MorphoBlue validation", () => { ) .morpho.blue(CbbtcUsdcBlue, mainnet.id); - const requirements = await getPlanRequests( - market.repayWithdrawCollateral({ - amount: 1n, - withdrawAmount: 1n, - userAddress: USER, - positionData: makePosition(), - }), - ); + const requirements = ( + await market + .repayWithdrawCollateral({ + amount: 1n, + withdrawAmount: 1n, + userAddress: USER, + positionData: makePosition(), + }) + .prepare() + ).requirements; expect(requirements).toHaveLength(2); }); @@ -338,14 +340,16 @@ describe("MorphoBlue validation", () => { const amount = parseUnits("0.3", 18); const nativeAmount = parseUnits("0.2", 18); - const tx = await buildPlanTx( - market.repay({ - amount, - nativeAmount, - userAddress: USER, - positionData: makeWethPosition(), - }), - ); + const tx = ( + await market + .repay({ + amount, + nativeAmount, + userAddress: USER, + positionData: makeWethPosition(), + }) + .prepare() + ).build().primaryTransaction; expect(tx.action.args.assets).toBe(amount + nativeAmount); expect(tx.action.args.nativeAmount).toBe(nativeAmount); @@ -360,13 +364,15 @@ describe("MorphoBlue validation", () => { .morpho.blue(WstethWethBlue, mainnet.id); const nativeAmount = parseUnits("0.5", 18); - const requirements = await getPlanRequests( - market.repay({ - nativeAmount, - userAddress: USER, - positionData: makeWethPosition(), - }), - ); + const requirements = ( + await market + .repay({ + nativeAmount, + userAddress: USER, + positionData: makeWethPosition(), + }) + .prepare() + ).requirements; expect(requirements).toEqual([]); }); @@ -395,7 +401,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = await buildPlanTx(repay); + const tx = (await repay.prepare()).build().primaryTransaction; expect(tx.action.args.shares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -403,7 +409,7 @@ describe("MorphoBlue validation", () => { expect(tx.action.args.transferAmount).toBe(nativeAmount); // Fully-native repay pulls no ERC-20 ⇒ no approval/permit requirement. - expect(await getPlanRequests(repay)).toEqual([]); + expect((await repay.prepare()).requirements).toEqual([]); }); test("repay native: shares mode pulls transferAmount net of native (happy path)", async ({ @@ -432,7 +438,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = await buildPlanTx(repay); + const tx = (await repay.prepare()).build().primaryTransaction; expect(tx.action.args.shares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -442,7 +448,7 @@ describe("MorphoBlue validation", () => { expect(tx.action.args.transferAmount - nativeAmount).toBe(expectedErc20); // Preparation approves exactly the carved ERC-20 remainder, not the debt. - const requirements = await getPlanRequests(repay); + const requirements = (await repay.prepare()).requirements; const approval = requirements.find(isRequirementApproval); if (!approval) { throw new Error("Approval requirement not found"); @@ -477,15 +483,17 @@ describe("MorphoBlue validation", () => { const amount = parseUnits("0.3", 18); const nativeAmount = parseUnits("0.2", 18); - const tx = await buildPlanTx( - market.repayWithdrawCollateral({ - amount, - nativeAmount, - withdrawAmount: parseUnits("1", 18), - userAddress: USER, - positionData: makeWethPosition(), - }), - ); + const tx = ( + await market + .repayWithdrawCollateral({ + amount, + nativeAmount, + withdrawAmount: parseUnits("1", 18), + userAddress: USER, + positionData: makeWethPosition(), + }) + .prepare() + ).build().primaryTransaction; expect(tx.action.args.repayAssets).toBe(amount + nativeAmount); expect(tx.action.args.nativeAmount).toBe(nativeAmount); @@ -500,14 +508,16 @@ describe("MorphoBlue validation", () => { .morpho.blue(WstethWethBlue, mainnet.id); const nativeAmount = parseUnits("0.5", 18); - const requirements = await getPlanRequests( - market.repayWithdrawCollateral({ - nativeAmount, - withdrawAmount: parseUnits("1", 18), - userAddress: USER, - positionData: makeWethPosition(), - }), - ); + const requirements = ( + await market + .repayWithdrawCollateral({ + nativeAmount, + withdrawAmount: parseUnits("1", 18), + userAddress: USER, + positionData: makeWethPosition(), + }) + .prepare() + ).requirements; // Fully-native repay pulls no ERC-20 → only the Morpho authorization remains. expect(requirements).toHaveLength(1); @@ -537,7 +547,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = await buildPlanTx(action); + const tx = (await action.prepare()).build().primaryTransaction; expect(tx.action.args.repayShares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -545,7 +555,7 @@ describe("MorphoBlue validation", () => { expect(tx.action.args.transferAmount).toBe(nativeAmount); // No ERC-20 pulled ⇒ only the Morpho authorization requirement remains. - expect(await getPlanRequests(action)).toHaveLength(1); + expect((await action.prepare()).requirements).toHaveLength(1); }); test("repayWithdrawCollateral native: shares mode pulls transferAmount net of native (happy path)", async ({ @@ -573,7 +583,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = await buildPlanTx(action); + const tx = (await action.prepare()).build().primaryTransaction; expect(tx.action.args.repayShares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -584,7 +594,7 @@ describe("MorphoBlue validation", () => { // Preparation approves exactly the carved ERC-20 remainder (alongside the // Morpho authorization the withdraw leg needs). - const requirements = await getPlanRequests(action); + const requirements = (await action.prepare()).requirements; const approval = requirements.find(isRequirementApproval); if (!approval) { throw new Error("Approval requirement not found"); diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index fac82e223..d1c4598b9 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -100,7 +100,7 @@ import { ReallocationData } from "../reallocationData.js"; * Blue action flows are synchronous so the SDK does not own broad market or position data fetching. * Consumers pass the state snapshots they already fetched, which lets each app batch, cache, and * refresh data for its own UX. Each flow returns a TransactionPlan; call `prepare()` when the app is - * ready to compute the minimal signature requests and/or call requests needed to execute that + * ready to compute the minimal signature requests and/or transaction steps needed to execute that * intent. */ export interface BlueActions { @@ -548,7 +548,7 @@ export class MorphoBlue implements BlueActions { args: { amount, from: userAddress }, }), - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -647,7 +647,7 @@ export class MorphoBlue implements BlueActions { return authTx ? [authTx] : []; }, - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { authorization } = selectRequirementSignatures(signatures, { authorization: true, }); @@ -702,7 +702,7 @@ export class MorphoBlue implements BlueActions { args: { amount, from: userAddress }, }), - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -779,7 +779,7 @@ export class MorphoBlue implements BlueActions { return authTx ? [authTx] : []; }, - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { authorization } = selectRequirementSignatures(signatures, { authorization: true, }); @@ -917,7 +917,7 @@ export class MorphoBlue implements BlueActions { }); }, - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -996,7 +996,7 @@ export class MorphoBlue implements BlueActions { }); return new TransactionPlan({ - buildPrimaryCall: () => + buildPrimaryTransaction: () => blueWithdrawCollateral({ market: { chainId: this.chainId, @@ -1165,7 +1165,7 @@ export class MorphoBlue implements BlueActions { return [...erc20Requirements, ...(authTx ? [authTx] : [])]; }, - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { permit, authorization } = selectRequirementSignatures( signatures, { permit: true, authorization: true }, @@ -1295,7 +1295,7 @@ export class MorphoBlue implements BlueActions { return [...erc20Requirements, ...(authTx ? [authTx] : [])]; }, - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { permit, authorization } = selectRequirementSignatures( signatures, { permit: true, authorization: true }, @@ -1530,7 +1530,7 @@ export class MorphoBlue implements BlueActions { return authTx ? [authTx] : []; }, - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { authorization } = selectRequirementSignatures(signatures, { authorization: true, }); diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts index e1ee946ed..3b8451561 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts @@ -36,10 +36,6 @@ import { midnightMarketId, midnightOtherMarket, } from "../../../test/fixtures/midnight.js"; -import { - buildPlanTx, - getPlanRequests, -} from "../../../test/transactionPlanUtils.js"; import { TransactionPlan } from "../../transactionPlan/index.js"; import type { MempoolSubmitOffersAction, @@ -97,8 +93,8 @@ type CreateMakeOffersOutput = (params: { }) => MakeOffersOutput; const buildMakeOffersOutputTx: BuildMakeOffersOutputTx = async (params) => - buildPlanTx( - ( + ( + await ( Object.assign(Object.create(MorphoMidnight.prototype), { chainId: midnightChainId, client: { @@ -109,14 +105,15 @@ const buildMakeOffersOutputTx: BuildMakeOffersOutputTx = async (params) => }) as { createMakeOffersOutput: CreateMakeOffersOutput; } - ).createMakeOffersOutput({ - offersData: params.offersData, - getRequirementRequests: async () => [], - signedPayloads: - params.signedPayloads ?? new Map([["0x1234", "0x1234" as Hex]]), - }), - params.signatures, - ); + ) + .createMakeOffersOutput({ + offersData: params.offersData, + getRequirementRequests: async () => [], + signedPayloads: + params.signedPayloads ?? new Map([["0x1234", "0x1234" as Hex]]), + }) + .prepare() + ).build(params.signatures).primaryTransaction; const offersData = ( buy = true, @@ -495,7 +492,14 @@ describe("MorphoMidnight", () => { describe("takeLend", () => { test("default", async () => { - const output = midnight().takeLend({ + const handle = createMockClient(midnightTestChain); + mockAllowance({ + handle, + token: midnightAddresses.loanToken, + result: maxUint256, + }); + mockMidnightAuthorization(handle, true); + const output = midnightWithHandle(handle).takeLend({ marketData: marketData(), accountAddress: midnightAddresses.taker, assets: 1_000n, @@ -504,7 +508,7 @@ describe("MorphoMidnight", () => { deadline: maxUint256, }); expect(output).toBeInstanceOf(TransactionPlan); - const tx = await buildPlanTx(output); + const tx = (await output.prepare()).build().primaryTransaction; expect(tx.action.args).toEqual({ market: midnightMarketId, @@ -546,7 +550,7 @@ describe("MorphoMidnight", () => { takeableOffers: [midnightApiTake()], deadline: maxUint256, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect( requirements.map((requirement) => requirement.action.type), @@ -571,7 +575,7 @@ describe("MorphoMidnight", () => { deadline: maxUint256, }); - await expect(getPlanRequests(output)).resolves.toEqual([]); + await expect((await output.prepare()).requirements).toEqual([]); }); test("error: amount validation", async () => { @@ -643,9 +647,9 @@ describe("MorphoMidnight", () => { takeableOffers: [midnightApiTake({ buy: true })], deadline: maxUint256, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect(output).toBeInstanceOf(TransactionPlan); - const tx = await buildPlanTx(output); + const tx = (await output.prepare()).build().primaryTransaction; expect(tx.action.args.loanAssets).toBe(1_000n); expect( @@ -666,7 +670,7 @@ describe("MorphoMidnight", () => { deadline: maxUint256, }); - await expect(getPlanRequests(output)).resolves.toEqual([]); + await expect((await output.prepare()).requirements).toEqual([]); }); test("error: MidnightOfferSideMismatchError", () => { @@ -745,9 +749,9 @@ describe("MorphoMidnight", () => { takeableOffers: [midnightApiTake({ buy: true })], deadline: maxUint256, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect(output).toBeInstanceOf(TransactionPlan); - const tx = await buildPlanTx(output); + const tx = (await output.prepare()).build().primaryTransaction; expect(tx.action.args).toMatchObject({ collateralAssets: 2_000n, @@ -777,7 +781,7 @@ describe("MorphoMidnight", () => { deadline: maxUint256, }); - await expect(getPlanRequests(output)).resolves.toEqual([]); + await expect((await output.prepare()).requirements).toEqual([]); }); test("error: amount validation", async () => { @@ -827,9 +831,9 @@ describe("MorphoMidnight", () => { collateralAssets: 2_000n, reservedCollateralAssets: 500n, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect(output).toBeInstanceOf(TransactionPlan); - const tx = await buildPlanTx(output); + const tx = (await output.prepare()).build().primaryTransaction; expect(tx.action.args.assets).toBe(2_000n); expect(requirements[0]?.action).toMatchObject({ @@ -854,7 +858,7 @@ describe("MorphoMidnight", () => { accountAddress: midnightAddresses.taker, collateralAssets: 2_000n, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; const approval = requirements[0]; expect(approval?.action.type).toBe("erc20Approval"); @@ -888,7 +892,7 @@ describe("MorphoMidnight", () => { positionData: positionData(market, { credit: 250n, pendingFee: 50n }), accountAddress: midnightAddresses.taker, }); - const tx = await buildPlanTx(output); + const tx = (await output.prepare()).build().primaryTransaction; expect(tx.action.args).toEqual({ market: midnightMarketId, @@ -905,7 +909,7 @@ describe("MorphoMidnight", () => { accountAddress: midnightAddresses.taker, units: 225n, }); - const tx = await buildPlanTx(output); + const tx = (await output.prepare()).build().primaryTransaction; expect(tx.action.args).toEqual({ market: midnightMarketId, @@ -968,8 +972,8 @@ describe("MorphoMidnight", () => { accountAddress: midnightAddresses.taker, receiver: midnightAddresses.maker, }); - const requirements = await getPlanRequests(output); - const tx = await buildPlanTx(output); + const requirements = (await output.prepare()).requirements; + const tx = (await output.prepare()).build().primaryTransaction; expect(requirements).toEqual([]); expect(tx.action.args.receiver).toBe(midnightAddresses.maker); @@ -1353,7 +1357,7 @@ describe("MorphoMidnight", () => { }); expect(output).toBeInstanceOf(TransactionPlan); const signature = await signOfferRootRequirement(output); - const tx = await buildPlanTx(output, signature); + const tx = (await output.prepare()).build(signature).primaryTransaction; expect(output.groups).toEqual(data.groups); expect(output.root).toBe(data.tree.root); @@ -1443,9 +1447,9 @@ describe("MorphoMidnight", () => { }); const prepared = await output.prepare(); expect(output).toBeInstanceOf(TransactionPlan); - expect(prepared.callRequests).toEqual([]); + expect(prepared.transactionSteps).toEqual([]); expect(prepared.hasIntent("midnightOfferRootSignature")).toBe(true); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; const requirement = requirements.find( ({ action }) => action.type === "midnightOfferRootSignature", ); @@ -1512,7 +1516,7 @@ describe("MorphoMidnight", () => { loanAssets: 1_000n, reservedLoanAssets: 250n, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect( requirements.find( @@ -1646,7 +1650,7 @@ describe("MorphoMidnight", () => { collateralAssets: 1_000n, reservedCollateralAssets: 250n, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect( requirements.find( @@ -1669,7 +1673,7 @@ describe("MorphoMidnight", () => { }, }); const signature = await signOfferRootRequirement(output); - const tx = await buildPlanTx(output, signature); + const tx = (await output.prepare()).build(signature).primaryTransaction; expect(tx.action.args.maker).toBe(data.accountAddress); }); @@ -1695,7 +1699,7 @@ describe("MorphoMidnight", () => { }), collateralAssets: 1_000n, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; const approval = requirements.find( (requirement) => requirement.action.type === "erc20Approval", ); @@ -1765,7 +1769,7 @@ describe("MorphoMidnight", () => { validation: offerValidation, }); const signature = await signOfferRootRequirement(output); - const tx = await buildPlanTx(output, signature); + const tx = (await output.prepare()).build(signature).primaryTransaction; expect(output.groups).toEqual(data.groups); expect(tx.action.args.maker).toBe(offerSignerAccount.address); @@ -1781,7 +1785,7 @@ describe("MorphoMidnight", () => { offers: data.tree, validation: offerValidation, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect( requirements.map((requirement) => requirement.action.type), @@ -1798,8 +1802,8 @@ describe("MorphoMidnight", () => { offers: data.tree, validation: offerValidation, }); - const requirements = await getPlanRequests(output); - const tx = await buildPlanTx(output); + const requirements = (await output.prepare()).requirements; + const tx = (await output.prepare()).build().primaryTransaction; expect( requirements.map((requirement) => requirement.action.type), @@ -1818,7 +1822,7 @@ describe("MorphoMidnight", () => { validation: offerValidation, }); - await expect(getPlanRequests(output)).resolves.toEqual([]); + await expect((await output.prepare()).requirements).toEqual([]); }); test("error: MidnightOfferSideMismatchError", async () => { @@ -1861,8 +1865,8 @@ describe("MorphoMidnight", () => { withdrawCollateralAssets: 2_000n, deadline: maxUint256, }); - const requirements = await getPlanRequests(output); - const tx = await buildPlanTx(output); + const requirements = (await output.prepare()).requirements; + const tx = (await output.prepare()).build().primaryTransaction; expect(tx.action.args).toMatchObject({ repayAssets: 1_000n, @@ -1884,7 +1888,7 @@ describe("MorphoMidnight", () => { withdrawCollateralAssets: 2_000n, deadline: maxUint256, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect(requirements).toEqual([]); }); @@ -1941,8 +1945,8 @@ describe("MorphoMidnight", () => { group: data.groups[0]!, accountAddress: midnightAddresses.maker, }); - const requirements = await getPlanRequests(output); - const tx = await buildPlanTx(output); + const requirements = (await output.prepare()).requirements; + const tx = (await output.prepare()).build().primaryTransaction; expect(requirements).toEqual([]); expect(tx.action.args.group).toBe(data.groups[0]); diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.ts b/packages/morpho-sdk/src/entities/midnight/midnight.ts index c119122a1..ea9fb9ded 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.ts @@ -98,7 +98,7 @@ import type { * Midnight action flows are synchronous after the caller provides market, position, or offer data: * the SDK does not own broad data fetching, letting apps batch, cache, and refresh state for their * own UX. Each flow returns a TransactionPlan; call `prepare()` when the app is ready to compute the - * minimal signature requests and/or call requests needed to execute that intent. + * minimal signature requests and/or transaction steps needed to execute that intent. * * @example * ```ts @@ -120,7 +120,7 @@ import type { * signatures.push(await request.sign(walletClient, lender)); * } * const executable = prepared.build(signatures); - * const calls = executable.callRequests.map((request) => request.call); + * const calls = executable.calls; * ``` */ export type MidnightActions = Pick< @@ -177,7 +177,7 @@ const validateMarketData = (market: Market, chainId: number) => { * positionData, * }); * const executable = (await plan.prepare()).build(); - * const tx = executable.callRequests.at(-1)?.tx; + * const tx = executable.primaryTransaction; * ``` */ export class MorphoMidnight { @@ -435,7 +435,7 @@ export class MorphoMidnight { return requirements; }, - buildPrimaryCall: () => + buildPrimaryTransaction: () => midnightTakeLend({ chainId: this.chainId, market: market.params, @@ -514,7 +514,7 @@ export class MorphoMidnight { return requirements; }, - buildPrimaryCall: () => + buildPrimaryTransaction: () => midnightTakeBorrow({ chainId: this.chainId, market: market.params, @@ -609,7 +609,7 @@ export class MorphoMidnight { return requirements; }, - buildPrimaryCall: () => + buildPrimaryTransaction: () => midnightSupplyCollateralTakeBorrow({ chainId: this.chainId, market: market.params, @@ -680,7 +680,7 @@ export class MorphoMidnight { amount: params.collateralAssets + (params.reservedCollateralAssets ?? 0n), }), - buildPrimaryCall: () => + buildPrimaryTransaction: () => midnightSupplyCollateral({ chainId: this.chainId, market: market.params, @@ -984,7 +984,7 @@ export class MorphoMidnight { ActionRequirement >({ getRequirementRequests: async () => [], - buildPrimaryCall: () => + buildPrimaryTransaction: () => midnightRedeem({ chainId: this.chainId, market: market.params, @@ -1090,7 +1090,7 @@ export class MorphoMidnight { return requirements; }, - buildPrimaryCall: () => + buildPrimaryTransaction: () => midnightRepayWithdrawCollateral({ chainId: this.chainId, market: market.params, @@ -1132,7 +1132,7 @@ export class MorphoMidnight { ActionRequirement >({ getRequirementRequests: async () => [], - buildPrimaryCall: () => + buildPrimaryTransaction: () => midnightCancelOffer({ chainId: this.chainId, group: params.group, @@ -1158,8 +1158,8 @@ export class MorphoMidnight { MidnightActionSignatures >({ getRequirementRequests: params.getRequirementRequests, - previewPrimaryCall: false, - buildPrimaryCall: (signatures) => { + previewPrimaryTransaction: false, + buildPrimaryTransaction: (signatures) => { const collectedSignatures = signatures == null ? undefined diff --git a/packages/morpho-sdk/src/entities/midnight/types.ts b/packages/morpho-sdk/src/entities/midnight/types.ts index 4e258b93c..b9da63d08 100644 --- a/packages/morpho-sdk/src/entities/midnight/types.ts +++ b/packages/morpho-sdk/src/entities/midnight/types.ts @@ -78,8 +78,8 @@ export type MidnightActionSignatures = | readonly MidnightOfferRootSignature[]; /** - * Lazy Midnight transaction plan. Call `prepare()` to resolve the signature requests and/or call - * requests needed by the already chosen Midnight action. + * Lazy Midnight transaction plan. Call `prepare()` to resolve the signature requests and/or + * prerequisite transactions needed by the already chosen Midnight action. * * @example * ```ts @@ -90,7 +90,7 @@ export type MidnightActionSignatures = * signatures.push(await request.sign(walletClient, taker)); * } * const executable = prepared.build(signatures); - * const calls = executable.callRequests.map((request) => request.call); + * const calls = executable.calls; * ``` */ export type MidnightActionOutput< @@ -102,7 +102,7 @@ export type MidnightActionOutput< * Transaction plan returned by maker-offer flows after offer-tree preparation. * * Use `groups` and `root` for review UI. Call `prepare()` to resolve ratifier approval/signature - * requests, then build the executable call requests with the collected signatures. + * requests, then build the executable transaction steps with the collected signatures. * * @example * ```ts @@ -114,7 +114,7 @@ export type MidnightActionOutput< * signatures.push(await request.sign(walletClient, maker)); * } * const executable = prepared.build(signatures); - * const calls = executable.callRequests.map((request) => request.call); + * const calls = executable.calls; * ``` */ export type MakeOffersOutput = MidnightActionOutput< diff --git a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts index f897f1a5a..ce2f6daab 100644 --- a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts @@ -12,10 +12,6 @@ import { KpkWETHVaultV2, } from "../../../test/fixtures/vaultV2.js"; import { test } from "../../../test/setup.js"; -import { - buildPlanTx, - getPlanRequests, -} from "../../../test/transactionPlanUtils.js"; import { morphoViemExtension } from "../../client/index.js"; import { MAX_SLIPPAGE_TOLERANCE } from "../../helpers/constant.js"; import { @@ -109,7 +105,7 @@ describe("MorphoVaultV1 entity tests", () => { vaultData, slippageTolerance: 0n, }); - const tx = await buildPlanTx(result); + const tx = (await result.prepare()).build().primaryTransaction; expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -134,7 +130,7 @@ describe("MorphoVaultV1 entity tests", () => { vaultData, slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const tx = await buildPlanTx(result); + const tx = (await result.prepare()).build().primaryTransaction; expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -346,13 +342,15 @@ describe("MorphoVaultV1 entity tests", () => { ); const vaultData = await vault.getData(); - const requirements = await getPlanRequests( - vault.deposit({ - amount: parseUnits("100", 6), - userAddress: client.account.address, - vaultData, - }), - ); + const requirements = ( + await vault + .deposit({ + amount: parseUnits("100", 6), + userAddress: client.account.address, + vaultData, + }) + .prepare() + ).requirements; expect(requirements).toHaveLength(1); @@ -502,7 +500,7 @@ describe("MorphoVaultV1 entity tests", () => { targetVault, shares: parseUnits("1000", 18), }); - const tx = await buildPlanTx(result); + const tx = (await result.prepare()).build().primaryTransaction; expect(tx.action.type).toBe("vaultV1MigrateToV2"); expect(tx.action.args.sourceVault).toBe(SteakhouseUsdcVaultV1.address); expect(tx.action.args.targetVault).toBe(KeyrockUsdcVaultV2.address); @@ -630,7 +628,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), slippageTolerance: 0n, }); - const tx = await buildPlanTx(result); + const tx = (await result.prepare()).build().primaryTransaction; expect(tx.data).toBeDefined(); }); @@ -661,7 +659,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const tx = await buildPlanTx(result); + const tx = (await result.prepare()).build().primaryTransaction; expect(tx.data).toBeDefined(); }); @@ -691,14 +689,16 @@ describe("MorphoVaultV1 entity tests", () => { { chainId: mainnet.id }, ); - const requirements = await getPlanRequests( - vault.migrateToV2({ - userAddress: client.account.address, - sourceVault, - targetVault, - shares, - }), - ); + const requirements = ( + await vault + .migrateToV2({ + userAddress: client.account.address, + sourceVault, + targetVault, + shares, + }) + .prepare() + ).requirements; expect(requirements).toHaveLength(1); @@ -734,14 +734,16 @@ describe("MorphoVaultV1 entity tests", () => { { chainId: mainnet.id }, ); - const requirements = await getPlanRequests( - vault.migrateToV2({ - userAddress: client.account.address, - sourceVault, - targetVault, - shares, - }), - ); + const requirements = ( + await vault + .migrateToV2({ + userAddress: client.account.address, + sourceVault, + targetVault, + shares, + }) + .prepare() + ).requirements; expect(requirements.length).toBeGreaterThanOrEqual(1); }); @@ -780,7 +782,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), }); - const tx = await buildPlanTx(result); + const tx = (await result.prepare()).build().primaryTransaction; expect(tx.action.args.recipient).toBe(OTHER_USER); }); @@ -813,7 +815,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), }); - const tx = await buildPlanTx(result); + const tx = (await result.prepare()).build().primaryTransaction; expect(tx.action.args.recipient).toBe(OTHER_USER); }); }); diff --git a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts index 92286a2e3..c2e082c27 100644 --- a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts +++ b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts @@ -50,7 +50,7 @@ import { * VaultV1 action flows are synchronous so the SDK does not own vault data fetching. Consumers pass * the vault snapshots they already fetched, which lets each app batch, cache, and refresh data for * its own UX. Each flow returns a TransactionPlan; call `prepare()` when the app is ready to compute - * the minimal signature requests and/or call requests needed to execute that intent. + * the minimal signature requests and/or transaction steps needed to execute that intent. */ export interface VaultV1Actions { /** @@ -94,7 +94,7 @@ export interface VaultV1Actions { * @param {Object} params - The withdraw parameters. * @param {bigint} params.amount - Amount of assets to withdraw. * @param {Address} params.userAddress - User address initiating the withdraw. - * @returns A lazy `TransactionPlan` that prepares and builds the direct withdraw call. + * @returns A lazy `TransactionPlan` that prepares and builds the direct withdraw transaction. */ withdraw: (params: { amount: bigint; @@ -106,7 +106,7 @@ export interface VaultV1Actions { * @param {Object} params - The redeem parameters. * @param {bigint} params.shares - Amount of shares to redeem. * @param {Address} params.userAddress - User address initiating the redeem. - * @returns A lazy `TransactionPlan` that prepares and builds the direct redeem call. + * @returns A lazy `TransactionPlan` that prepares and builds the direct redeem transaction. */ redeem: (params: { shares: bigint; @@ -246,7 +246,7 @@ export class MorphoVaultV1 implements VaultV1Actions { }, }), - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -279,7 +279,7 @@ export class MorphoVaultV1 implements VaultV1Actions { } return new TransactionPlan({ - buildPrimaryCall: () => + buildPrimaryTransaction: () => vaultV1Withdraw({ vault: { address: this.vault }, args: { @@ -301,7 +301,7 @@ export class MorphoVaultV1 implements VaultV1Actions { } return new TransactionPlan({ - buildPrimaryCall: () => + buildPrimaryTransaction: () => vaultV1Redeem({ vault: { address: this.vault }, args: { @@ -390,7 +390,7 @@ export class MorphoVaultV1 implements VaultV1Actions { }, }), - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); diff --git a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts index 37aec294c..1a501cca0 100644 --- a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts @@ -7,7 +7,6 @@ import { KpkWETHVaultV2, } from "../../../test/fixtures/vaultV2.js"; import { test } from "../../../test/setup.js"; -import { buildPlanTx } from "../../../test/transactionPlanUtils.js"; import { morphoViemExtension } from "../../client/index.js"; import { MAX_SLIPPAGE_TOLERANCE } from "../../helpers/constant.js"; import { @@ -107,7 +106,7 @@ describe("MorphoVaultV2 entity tests", () => { vaultData, slippageTolerance: 0n, }); - const tx = await buildPlanTx(result); + const tx = (await result.prepare()).build().primaryTransaction; expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -132,7 +131,7 @@ describe("MorphoVaultV2 entity tests", () => { vaultData, slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const tx = await buildPlanTx(result); + const tx = (await result.prepare()).build().primaryTransaction; expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); diff --git a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts index 840f968ff..e830a5077 100644 --- a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts +++ b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts @@ -47,7 +47,7 @@ import { * VaultV2 action flows are synchronous so the SDK does not own vault data fetching. Consumers pass * the vault snapshots they already fetched, which lets each app batch, cache, and refresh data for * its own UX. Each flow returns a TransactionPlan; call `prepare()` when the app is ready to compute - * the minimal signature requests and/or call requests needed to execute that intent. + * the minimal signature requests and/or transaction steps needed to execute that intent. */ export interface VaultV2Actions { /** @@ -66,7 +66,7 @@ export interface VaultV2Actions { * * This function constructs the transaction data required to deposit a specified amount of assets into the vault. * Uses pre-fetched vault data for accurate calculations of slippage and asset address, - * then returns a lazy transaction plan for resolving approval or signature requests and building the deposit call. + * then returns a lazy transaction plan for resolving approval or signature requests and building the deposit transaction. * Bundler Integration: This flow uses the bundler to atomically execute the user's asset transfer and vault deposit in a single transaction for slippage protection. * * @param {Object} params - The deposit parameters. @@ -76,7 +76,7 @@ export interface VaultV2Actions { * @param {bigint} [params.slippageTolerance=DEFAULT_SLIPPAGE_TOLERANCE] - Optional slippage tolerance value. Default is 0.03%. Slippage tolerance must be less than 10%. * @param {bigint} [params.nativeAmount] - Amount of native token to wrap into wNative. Vault asset must be wNative. * @returns A lazy `TransactionPlan` whose `prepare()` resolves token approval or permit requests - * and whose prepared form builds the executable deposit call. + * and whose prepared form builds the executable deposit transaction. */ deposit: ( params: { @@ -97,7 +97,7 @@ export interface VaultV2Actions { * @param {Object} params - The withdraw parameters. * @param {bigint} params.amount - The amount of assets to withdraw. * @param {Address} params.userAddress - User address initiating the withdraw. - * @returns A lazy `TransactionPlan` that prepares and builds the direct withdraw call. + * @returns A lazy `TransactionPlan` that prepares and builds the direct withdraw transaction. */ withdraw: (params: { amount: bigint; @@ -111,7 +111,7 @@ export interface VaultV2Actions { * @param {Object} params - The redeem parameters. * @param {bigint} params.shares - The amount of shares to redeem. * @param {Address} params.userAddress - User address initiating the redeem. - * @returns A lazy `TransactionPlan` that prepares and builds the direct redeem call. + * @returns A lazy `TransactionPlan` that prepares and builds the direct redeem transaction. */ redeem: (params: { shares: bigint; @@ -276,7 +276,7 @@ export class MorphoVaultV2 implements VaultV2Actions { }, }), - buildPrimaryCall: (signatures) => { + buildPrimaryTransaction: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -309,7 +309,7 @@ export class MorphoVaultV2 implements VaultV2Actions { } return new TransactionPlan({ - buildPrimaryCall: () => + buildPrimaryTransaction: () => vaultV2Withdraw({ vault: { address: this.vault }, args: { @@ -331,7 +331,7 @@ export class MorphoVaultV2 implements VaultV2Actions { } return new TransactionPlan({ - buildPrimaryCall: () => + buildPrimaryTransaction: () => vaultV2Redeem({ vault: { address: this.vault }, args: { @@ -361,7 +361,7 @@ export class MorphoVaultV2 implements VaultV2Actions { } return new TransactionPlan({ - buildPrimaryCall: () => + buildPrimaryTransaction: () => vaultV2ForceWithdraw({ vault: { address: this.vault }, args: { @@ -394,7 +394,7 @@ export class MorphoVaultV2 implements VaultV2Actions { } return new TransactionPlan({ - buildPrimaryCall: () => + buildPrimaryTransaction: () => vaultV2ForceRedeem({ vault: { address: this.vault }, args: { diff --git a/packages/morpho-sdk/src/helpers/computeReallocations.ts b/packages/morpho-sdk/src/helpers/computeReallocations.ts index a322ac766..868438dc9 100644 --- a/packages/morpho-sdk/src/helpers/computeReallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeReallocations.ts @@ -145,7 +145,7 @@ const capVaultWithdrawals = ( * positionData, * reallocations, * }); - * // borrow.prepare().build().callRequests includes any required PublicAllocator reallocations. + * // borrow.prepare().build().transactionSteps includes any required PublicAllocator reallocations. * ``` */ export const computeReallocations = ({ diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts index feb8a2221..0276530e5 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts @@ -123,7 +123,7 @@ describe("TransactionPlan", () => { permitRequest(), authorizationRequest(), ]), - buildPrimaryCall: vi.fn(() => primaryTx), + buildPrimaryTransaction: vi.fn(() => primaryTx), }; const plan = TransactionPlan.create(handler); @@ -133,25 +133,35 @@ describe("TransactionPlan", () => { expect("buildTx" in plan).toBe(false); expect("getRequirements" in plan).toBe(false); expect(Object.hasOwn(plan, "prepare")).toBe(false); - expect(prepared.flowKind).toBe("mixed_requests"); + expect(prepared.flowKind).toBe("mixed_steps"); expect(prepared.hasSignatureRequests).toBe(true); - expect(prepared.hasCallRequests).toBe(true); - expect(prepared.steps.map((request) => request.kind)).toEqual([ - "call", + expect(prepared.hasTransactionSteps).toBe(true); + expect(prepared.steps.map((step) => step.kind)).toEqual([ + "transaction", "signature", "signature", - "call", + "transaction", ]); - expect(prepared.callRequests.map((request) => request.id)).toEqual([ + expect(prepared.transactionSteps.map((step) => step.id)).toEqual([ "request-0", "primary", ]); - expect(prepared.callRequests.map((request) => request.phase)).toEqual([ + expect(prepared.transactionSteps.map((step) => step.phase)).toEqual([ "preparation", "primary", ]); + expect(prepared.requirements.map((request) => request.action.type)).toEqual( + ["erc20Approval", "permit2", "authorization"], + ); + expect(Object.isFrozen(prepared.requirements)).toBe(true); + expect(prepared.primaryStep?.transaction).toBe(primaryTx); + expect(prepared.primaryTransaction).toBe(primaryTx); + expect(prepared.calls).toEqual([ + { to: TOKEN, value: 0n, data: "0xa1" }, + { to: BUNDLER, value: 0n, data: "0xf1" }, + ]); expect(prepared.signatureRequests).toHaveLength(2); - expect(prepared.requestCount).toBe(4); + expect(prepared.stepCount).toBe(4); expect(prepared.hasIntent("tokenApproval")).toBe(true); expect(prepared.hasIntent("operatorAuthorization")).toBe(true); expect(prepared.hasIntent("primaryTransaction")).toBe(true); @@ -178,10 +188,10 @@ describe("TransactionPlan", () => { ]); }); - test("behavior: builds ordered call requests only after requested signatures are provided", async () => { + test("behavior: builds ordered transaction steps only after requested signatures are provided", async () => { const permit = permitRequest(); const authorization = authorizationRequest(); - const buildPrimaryCall = vi.fn( + const buildPrimaryTransaction = vi.fn( (_signatures?: readonly RequirementSignature[]) => primaryTx, ); const plan = TransactionPlan.create({ @@ -190,7 +200,7 @@ describe("TransactionPlan", () => { permit, authorization, ]), - buildPrimaryCall, + buildPrimaryTransaction, }); const prepared = await plan.prepare(); @@ -207,16 +217,17 @@ describe("TransactionPlan", () => { authorizationSignature, ]); - expect(buildPrimaryCall).toHaveBeenCalledWith([ + expect(buildPrimaryTransaction).toHaveBeenCalledWith([ permitSignature, authorizationSignature, ]); - expect(executable.callRequests.map((request) => request.id)).toEqual([ + expect(executable.transactionSteps.map((step) => step.id)).toEqual([ "request-0", "primary", ]); - expect(executable.callRequests.at(-1)?.tx).toBe(primaryTx); - expect(executable.viemCalls).toEqual([ + expect(executable.primaryStep.transaction).toBe(primaryTx); + expect(executable.primaryTransaction).toBe(primaryTx); + expect(executable.calls).toEqual([ { to: TOKEN, value: 0n, data: "0xa1" }, { to: BUNDLER, value: 0n, data: "0xf1" }, ]); @@ -230,43 +241,55 @@ describe("TransactionPlan", () => { | Requirement >({ getRequirementRequests: vi.fn(async () => [approvalTx, permitRequest()]), - buildPrimaryCall: vi.fn(() => primaryTx), + buildPrimaryTransaction: vi.fn(() => primaryTx), }); const prepared = await plan.prepare(); type Prepared = typeof prepared; type SignatureRequest = Prepared["signatureRequests"][number]; - type PreparationCall = Extract< - Prepared["callRequests"][number], + type PreparationTransactionStep = Extract< + Prepared["transactionSteps"][number], { readonly phase: "preparation" } >; type Executable = ReturnType; - type PrimaryCall = Extract< - Executable["callRequests"][number], + type PrimaryTransactionStep = Extract< + Executable["transactionSteps"][number], { readonly phase: "primary" } >; + expectTypeOf().toEqualTypeOf< + | Readonly> + | Requirement + >(); + expectTypeOf().toEqualTypeOf< + Readonly> | undefined + >(); + expectTypeOf().toEqualTypeOf< + Readonly> + >(); expectTypeOf().toEqualTypeOf< Requirement >(); expectTypeOf>().toEqualTypeOf< Promise >(); - expectTypeOf().toEqualTypeOf< + expectTypeOf().toEqualTypeOf< Readonly> >(); - expectTypeOf().toEqualTypeOf(); + expectTypeOf< + PrimaryTransactionStep["action"] + >().toEqualTypeOf(); }); - test("behavior: requirement call requests keep authorization intent separate from token approval", async () => { + test("behavior: requirement transaction steps keep authorization intent separate from token approval", async () => { const plan = TransactionPlan.create({ getRequirementRequests: vi.fn(async () => [authorizationTx]), - buildPrimaryCall: vi.fn(() => primaryTx), + buildPrimaryTransaction: vi.fn(() => primaryTx), }); const prepared = await plan.prepare(); - expect(prepared.flowKind).toBe("call_requests"); + expect(prepared.flowKind).toBe("transaction_steps"); expect( prepared .findIntent("operatorAuthorization") @@ -282,45 +305,48 @@ describe("TransactionPlan", () => { expect(prepared.findIntent("tokenApproval")).toEqual([]); }); - test("behavior: a non-previewable primary call appears after build", async () => { + test("behavior: a non-previewable primary step appears after build", async () => { const plan = TransactionPlan.create({ getRequirementRequests: vi.fn(async () => [permitRequest()]), - previewPrimaryCall: false, - buildPrimaryCall: vi.fn(() => primaryTx), + previewPrimaryTransaction: false, + buildPrimaryTransaction: vi.fn(() => primaryTx), }); const prepared = await plan.prepare(); - expect(prepared.flowKind).toBe("signature_requests"); - expect(prepared.callRequests).toEqual([]); + expect(prepared.flowKind).toBe("signature_steps"); + expect(prepared.primaryStep).toBeUndefined(); + expect(prepared.primaryTransaction).toBeUndefined(); + expect(prepared.calls).toEqual([]); + expect(prepared.transactionSteps).toEqual([]); expect(prepared.steps.map((request) => request.id)).toEqual(["request-0"]); const executable = prepared.build([permitSignature]); - expect(executable.callRequests.at(-1)?.tx).toBe(primaryTx); - expect(executable.callRequests.map((request) => request.id)).toEqual([ + expect(executable.primaryTransaction).toBe(primaryTx); + expect(executable.transactionSteps.map((step) => step.id)).toEqual([ "primary", ]); }); - test("behavior: a handler without requirement requests is a single call", async () => { + test("behavior: a handler without requirement requests is a single transaction", async () => { const plan = TransactionPlan.create({ - buildPrimaryCall: vi.fn(() => primaryTx), + buildPrimaryTransaction: vi.fn(() => primaryTx), }); const prepared = await plan.prepare(); const executable = prepared.build(); - expect(prepared.flowKind).toBe("single_call"); - expect(prepared.requestCount).toBe(1); + expect(prepared.flowKind).toBe("single_transaction"); + expect(prepared.stepCount).toBe(1); expect(prepared.signatureRequests).toEqual([]); - expect(prepared.callRequests.map((request) => request.id)).toEqual([ + expect(prepared.transactionSteps.map((step) => step.id)).toEqual([ "primary", ]); - expect(executable.callRequests.map((request) => request.id)).toEqual([ + expect(executable.transactionSteps.map((step) => step.id)).toEqual([ "primary", ]); - expect(executable.viemCalls).toEqual([ + expect(executable.calls).toEqual([ { to: BUNDLER, value: 0n, data: "0xf1" }, ]); }); diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts index 9c9ccaec0..c37548fc9 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts @@ -1,32 +1,32 @@ import { - type CallRequirement, isRequirementApproval, isRequirementBlueAuthorization, type RequirementSignature, type SignatureRequirement, type Transaction, type TransactionAction, + type TransactionRequirement, } from "../types/action.js"; import { MissingTransactionPlanSignaturesError } from "../types/error.js"; import type { ExecutableTransactionPlanShape, PreparedTransactionPlanShape, - TransactionPlanBuildPrimaryCall, - TransactionPlanCallRequest, - TransactionPlanContractCallIntent, + TransactionPlanBatchCall, + TransactionPlanBuildPrimaryTransaction, + TransactionPlanContractTransactionIntent, TransactionPlanFlowKind, TransactionPlanHandler, TransactionPlanIntent, TransactionPlanMidnightOfferRootIntent, TransactionPlanOperatorAuthorizationIntent, - TransactionPlanPreparedRequest, + TransactionPlanPreparedStep, TransactionPlanPrepareOptions, TransactionPlanRequest, TransactionPlanSignatureRequest, TransactionPlanStep, TransactionPlanStepForIntent, TransactionPlanTokenApprovalIntent, - TransactionPlanViemCall, + TransactionPlanTransactionStep, } from "./types.js"; export type { @@ -44,7 +44,7 @@ export type { * decide user intent. Consumers pass already-fetched market, position, and vault snapshots into the * flow, so apps can batch, cache, stale-while-revalidate, or otherwise optimize state fetching for * their own UX. Calling `prepare()` performs only the minimal request discovery for the already - * chosen intent: it resolves the signature requests and/or viem-compatible call requests that the + * chosen intent: it resolves the signature requests and/or viem-compatible transaction steps that the * primary action depends on, while avoiding extra reads when no approval or operator authorization * check is needed. * @@ -53,22 +53,22 @@ export type { * const plan = vault.deposit({ amount, userAddress, vaultData }); * const prepared = await plan.prepare({ requestOptions: { useSimplePermit } }); * - * const txFlowType = prepared.flowKind === "single_call" + * const txFlowType = prepared.flowKind === "single_transaction" * ? "simple" * : prepared.hasSignatureRequests * ? "signature_required" * : "bundled"; * - * const labels = prepared.steps.map((request) => { - * switch (request.intent.type) { + * const labels = prepared.steps.map((step) => { + * switch (step.intent.type) { * case "tokenApproval": - * return request.intent.method === "tx" - * ? `Approve ${request.intent.amount}` - * : `Sign ${request.intent.method}`; + * return step.intent.method === "tx" + * ? `Approve ${step.intent.amount}` + * : `Sign ${step.intent.method}`; * case "operatorAuthorization": * return "Authorize operator"; - * case "contractCall": - * return `Review ${request.intent.actionType}`; + * case "contractTransaction": + * return `Review ${step.intent.actionType}`; * case "midnightOfferRootSignature": * return "Sign offer root"; * case "primaryTransaction": @@ -91,10 +91,11 @@ export type { * signatures.push(await request.sign(walletClient, accountAddress)); * } * const executable = prepared.build(signatures); - * const callRequests = executable.callRequests.map((request) => ({ - * label: request.intent.type === "primaryTransaction" ? "Submit transaction" : "Approve token", - * getCall: () => request.call, + * const transactionSteps = executable.transactionSteps.map((step) => ({ + * label: step.intent.type === "primaryTransaction" ? "Submit transaction" : "Approve token", + * transaction: step.transaction, * })); + * const calls = executable.calls; * ``` */ export class TransactionPlan< @@ -118,7 +119,7 @@ export class TransactionPlan< * ```ts * const plan = new TransactionPlan({ * getRequirementRequests: async () => requests, - * buildPrimaryCall: () => transaction, + * buildPrimaryTransaction: () => transaction, * }); * ``` */ @@ -143,7 +144,7 @@ export class TransactionPlan< * const prepared = await vault.deposit(params).prepare({ * requestOptions: { useSimplePermit: true }, * }); - * // prepared.steps contains the typed prerequisites followed by the primary call preview. + * // prepared.steps contains the typed prerequisites followed by the primary step preview. * ``` */ async prepare( @@ -152,16 +153,16 @@ export class TransactionPlan< const requests = this.handler.getRequirementRequests ? await this.handler.getRequirementRequests(options?.requestOptions) : []; - const requestSteps = requests.map( - (request, index): TransactionPlanPreparedRequest => { + const requirementSteps = requests.map( + (request, index): TransactionPlanPreparedStep => { const id = `request-${index}`; if (isRequirementApproval(request)) { - const tx = request as Extract; + const tx = request as Extract; return { - kind: "call", + kind: "transaction", id, phase: "preparation", - tx, + transaction: tx, action: tx.action, intent: { type: "tokenApproval", @@ -170,16 +171,15 @@ export class TransactionPlan< spender: request.action.args.spender, amount: request.action.args.amount, }, - call: { to: request.to, value: request.value, data: request.data }, - } satisfies TransactionPlanPreparedRequest; + } satisfies TransactionPlanPreparedStep; } if (isRequirementBlueAuthorization(request)) { - const tx = request as Extract; + const tx = request as Extract; return { - kind: "call", + kind: "transaction", id, phase: "preparation", - tx, + transaction: tx, action: tx.action, intent: { type: "operatorAuthorization", @@ -187,8 +187,7 @@ export class TransactionPlan< operator: request.action.args.authorized, isAuthorized: request.action.args.isAuthorized, }, - call: { to: request.to, value: request.value, data: request.data }, - } satisfies TransactionPlanPreparedRequest; + } satisfies TransactionPlanPreparedStep; } if ( typeof request === "object" && @@ -198,10 +197,12 @@ export class TransactionPlan< "data" in request && "action" in request ) { - const tx = request as Extract; + const tx = request as Extract; const intent: | TransactionPlanOperatorAuthorizationIntent - | TransactionPlanContractCallIntent = + | TransactionPlanContractTransactionIntent< + TransactionRequirement["action"] + > = tx.action.type === "midnightAuthorization" ? { type: "operatorAuthorization", @@ -210,16 +211,15 @@ export class TransactionPlan< isAuthorized: tx.action.args.isAuthorized, owner: tx.action.args.onBehalf, } - : { type: "contractCall", actionType: tx.action.type }; + : { type: "contractTransaction", actionType: tx.action.type }; return { - kind: "call", + kind: "transaction", id, phase: "preparation", - tx, + transaction: tx, action: tx.action, intent, - call: { to: tx.to, value: tx.value, data: tx.data }, - } satisfies TransactionPlanPreparedRequest; + } satisfies TransactionPlanPreparedStep; } const signatureRequest = request as Extract< @@ -280,44 +280,39 @@ export class TransactionPlan< action: signatureRequest.action, intent, sign: signatureRequest.sign, - } satisfies TransactionPlanPreparedRequest; + } satisfies TransactionPlanPreparedStep; }, ); const primaryTx = - this.handler.previewPrimaryCall === false + this.handler.previewPrimaryTransaction === false ? undefined : ( - this.handler.previewPrimaryCall ?? - (() => this.handler.buildPrimaryCall()) + this.handler.previewPrimaryTransaction ?? + (() => this.handler.buildPrimaryTransaction()) )(); - const primaryCall = + const primaryStep = primaryTx == null ? undefined : ({ - kind: "call", + kind: "transaction", id: "primary", phase: "primary", - tx: primaryTx, + transaction: primaryTx, action: primaryTx.action, intent: { type: "primaryTransaction", actionType: primaryTx.action.type, }, - call: { - to: primaryTx.to, - value: primaryTx.value, - data: primaryTx.data, - }, - } satisfies TransactionPlanCallRequest< + } satisfies TransactionPlanTransactionStep< TPrimaryAction, Readonly>, "primary" >); return new PreparedTransactionPlan({ - buildPrimaryCall: this.handler.buildPrimaryCall, - requestSteps, - primaryCall, + buildPrimaryTransaction: this.handler.buildPrimaryTransaction, + requirementSteps, + primaryStep, }); } @@ -330,7 +325,7 @@ export class TransactionPlan< * ```ts * const plan = TransactionPlan.create({ * getRequirementRequests: async () => requests, - * buildPrimaryCall: () => transaction, + * buildPrimaryTransaction: () => transaction, * }); * ``` */ @@ -371,92 +366,168 @@ export class PreparedTransactionPlan< TSignatures = readonly RequirementSignature[], > implements PreparedTransactionPlanShape { - private readonly buildPrimaryCall: TransactionPlanBuildPrimaryCall< + private readonly buildPrimaryTransaction: TransactionPlanBuildPrimaryTransaction< TPrimaryAction, TSignatures >; + private readonly rawRequirements: readonly TRequest[]; + + private readonly previewPrimaryStep?: TransactionPlanTransactionStep< + TPrimaryAction, + Readonly>, + "primary" + >; + /** Ordered signable requests to present to the user. */ readonly signatureRequests: readonly TransactionPlanSignatureRequest< Extract >[]; - /** Ordered viem-compatible calls. Includes the primary action call last when previewable. */ - readonly callRequests: PreparedTransactionPlanShape< + /** Ordered transaction steps. Includes the primary action step last when previewable. */ + readonly transactionSteps: PreparedTransactionPlanShape< TPrimaryAction, TRequest - >["callRequests"]; + >["transactionSteps"]; - /** All signature requests and call requests in review order. */ + /** All signature requests and transaction steps in review order. */ readonly steps: readonly TransactionPlanStep[]; /** * Creates a prepared plan from resolved request steps. * - * @param params - Build callback, resolved requests, and optional preview primary call. + * @param params - Build callback, resolved requirements, and optional preview primary step. * @example * ```ts * const prepared = new PreparedTransactionPlan({ - * buildPrimaryCall: () => transaction, - * requestSteps: [], - * primaryCall, + * buildPrimaryTransaction: () => transaction, + * requirementSteps: [], + * primaryStep, * }); * ``` */ constructor(params: { - readonly buildPrimaryCall: TransactionPlanBuildPrimaryCall< + readonly buildPrimaryTransaction: TransactionPlanBuildPrimaryTransaction< TPrimaryAction, TSignatures >; - readonly requestSteps: readonly TransactionPlanPreparedRequest[]; - readonly primaryCall?: TransactionPlanCallRequest< + readonly requirementSteps: readonly TransactionPlanPreparedStep[]; + readonly primaryStep?: TransactionPlanTransactionStep< TPrimaryAction, Readonly>, "primary" >; }) { - this.buildPrimaryCall = params.buildPrimaryCall; + this.buildPrimaryTransaction = params.buildPrimaryTransaction; + this.rawRequirements = Object.freeze( + params.requirementSteps.map( + (step): TRequest => + step.kind === "transaction" ? step.transaction : step.request, + ), + ); + this.previewPrimaryStep = params.primaryStep; this.signatureRequests = Object.freeze( - params.requestSteps.filter( + params.requirementSteps.filter( ( - request, - ): request is TransactionPlanSignatureRequest< + step, + ): step is TransactionPlanSignatureRequest< Extract - > => request.kind === "signature", + > => step.kind === "signature", ), ); - const requirementCallRequests = params.requestSteps.filter( + const requirementTransactionSteps = params.requirementSteps.filter( ( - request, - ): request is Extract< - TransactionPlanPreparedRequest, - { readonly kind: "call" } - > => request.kind === "call", + step, + ): step is Extract< + TransactionPlanPreparedStep, + { readonly kind: "transaction" } + > => step.kind === "transaction", ); - const primaryCall = params.primaryCall; - this.callRequests = Object.freeze( - primaryCall == null - ? [...requirementCallRequests] - : [...requirementCallRequests, primaryCall], + const primaryStep = params.primaryStep; + this.transactionSteps = Object.freeze( + primaryStep == null + ? [...requirementTransactionSteps] + : [...requirementTransactionSteps, primaryStep], ); this.steps = Object.freeze( - primaryCall == null - ? [...params.requestSteps] - : [...params.requestSteps, primaryCall], + primaryStep == null + ? [...params.requirementSteps] + : [...params.requirementSteps, primaryStep], ); } /** - * Counts the signature and call requests currently exposed by the plan. + * Returns the raw prerequisite requirements in discovery order. + * + * @returns The original typed call and signature requirements returned by the plan handler. + * @example + * ```ts + * const requirements = (await plan.prepare()).requirements; + * ``` + */ + get requirements(): readonly TRequest[] { + return this.rawRequirements; + } + + /** + * Returns the previewable primary step. + * + * @returns The primary transaction step, or `undefined` when signatures are required before encoding. + * @example + * ```ts + * const primaryStep = (await plan.prepare()).primaryStep; + * ``` + */ + get primaryStep(): PreparedTransactionPlanShape< + TPrimaryAction, + TRequest + >["primaryStep"] { + return this.previewPrimaryStep; + } + + /** + * Returns the previewable primary SDK transaction. + * + * @returns The primary transaction, or `undefined` when signatures are required before encoding. + * @example + * ```ts + * const tx = (await plan.prepare()).primaryTransaction; + * ``` + */ + get primaryTransaction(): Readonly> | undefined { + return this.previewPrimaryStep?.transaction; + } + + /** + * Converts the currently previewable transaction steps to viem-compatible calls. + * + * Signature-dependent primary transactions are omitted until `build(...)` returns an executable plan. + * + * @returns The currently previewable calls in execution order. + * @example + * ```ts + * const calls = prepared.calls; + * ``` + */ + get calls(): readonly TransactionPlanBatchCall[] { + return this.transactionSteps.map(({ transaction }) => ({ + to: transaction.to, + value: transaction.value, + data: transaction.data, + })); + } + + /** + * Counts the signature requests and transaction steps currently exposed by the plan. * * @returns The total number of exposed requests. * @example * ```ts - * console.log(prepared.requestCount); + * console.log(prepared.stepCount); * ``` */ - get requestCount(): number { - return this.signatureRequests.length + this.callRequests.length; + get stepCount(): number { + return this.signatureRequests.length + this.transactionSteps.length; } /** @@ -473,20 +544,20 @@ export class PreparedTransactionPlan< } /** - * Reports whether the plan currently exposes a viem-compatible call. + * Reports whether the plan currently exposes a transaction step. * - * @returns `true` when `callRequests` is non-empty. + * @returns `true` when `transactionSteps` is non-empty. * @example * ```ts - * if (prepared.hasCallRequests) showCallReview(); + * if (prepared.hasTransactionSteps) showTransactionReview(); * ``` */ - get hasCallRequests(): boolean { - return this.callRequests.length > 0; + get hasTransactionSteps(): boolean { + return this.transactionSteps.length > 0; } /** - * Classifies the plan from its current signature and call request mix. + * Classifies the plan from its current signature-request and transaction-step mix. * * @returns The high-level transaction-plan flow kind. * @example @@ -496,19 +567,20 @@ export class PreparedTransactionPlan< */ get flowKind(): TransactionPlanFlowKind { const signatureRequests = this.signatureRequests.length; - const preparationCalls = this.callRequests.filter( - (request) => request.phase === "preparation", + const preparationSteps = this.transactionSteps.filter( + (step) => step.phase === "preparation", ).length; - const primaryCalls = this.callRequests.filter( - (request) => request.phase === "primary", + const primarySteps = this.transactionSteps.filter( + (step) => step.phase === "primary", ).length; - if (signatureRequests > 0 && this.callRequests.length > 0) { - return "mixed_requests"; + if (signatureRequests > 0 && this.transactionSteps.length > 0) { + return "mixed_steps"; } - if (signatureRequests > 0) return "signature_requests"; - if (preparationCalls === 0 && primaryCalls <= 1) return "single_call"; - return "call_requests"; + if (signatureRequests > 0) return "signature_steps"; + if (preparationSteps === 0 && primarySteps <= 1) + return "single_transaction"; + return "transaction_steps"; } /** @@ -522,7 +594,7 @@ export class PreparedTransactionPlan< * ``` */ hasIntent(type: TType): boolean { - return this.steps.some((request) => request.intent.type === type); + return this.steps.some((step) => step.intent.type === type); } /** @@ -540,17 +612,17 @@ export class PreparedTransactionPlan< ): readonly TransactionPlanStepForIntent[] { return this.steps.filter( ( - request, - ): request is TransactionPlanStepForIntent< + step, + ): step is TransactionPlanStepForIntent< TType, TPrimaryAction, TRequest - > => request.intent.type === type, + > => step.intent.type === type, ); } /** - * Builds the executable calls from signatures collected by the integrator. + * Builds the executable transaction steps from signatures collected by the integrator. * * @param signatures - Signatures produced from `signatureRequests`, in request order. * @returns An executable plan that preserves the prerequisite and primary-action types. @@ -577,26 +649,29 @@ export class PreparedTransactionPlan< if (received < expected) { throw new MissingTransactionPlanSignaturesError(expected, received); } - const requirementCallRequests = this.callRequests.filter( - (request) => request.phase === "preparation", + const requirementTransactionSteps = this.transactionSteps.filter( + (step) => step.phase === "preparation", ); - const primaryTx = this.buildPrimaryCall(signatures); - const primaryCall = { - kind: "call", + const primaryTx = this.buildPrimaryTransaction(signatures); + const primaryStep = { + kind: "transaction", id: "primary", phase: "primary", - tx: primaryTx, + transaction: primaryTx, action: primaryTx.action, intent: { type: "primaryTransaction", actionType: primaryTx.action.type }, - call: { to: primaryTx.to, value: primaryTx.value, data: primaryTx.data }, - } satisfies TransactionPlanCallRequest< + } satisfies TransactionPlanTransactionStep< TPrimaryAction, Readonly>, "primary" >; return new ExecutableTransactionPlan({ + primaryStep, signatureRequests: Object.freeze([...this.signatureRequests]), - callRequests: Object.freeze([...requirementCallRequests, primaryCall]), + transactionSteps: Object.freeze([ + ...requirementTransactionSteps, + primaryStep, + ]), }); } } @@ -607,9 +682,10 @@ export class PreparedTransactionPlan< * @example * ```ts * const executable = prepared.build(signatures); - * for (const request of executable.callRequests) { - * await walletClient.sendTransaction(request.call); + * for (const step of executable.transactionSteps) { + * await walletClient.sendTransaction(step.transaction); * } + * await walletClient.sendCalls({ calls: executable.calls }); * ``` */ export class ExecutableTransactionPlan< @@ -617,16 +693,22 @@ export class ExecutableTransactionPlan< TRequest extends TransactionPlanRequest = TransactionPlanRequest, > implements ExecutableTransactionPlanShape { + private readonly builtPrimaryStep: TransactionPlanTransactionStep< + TPrimaryAction, + Readonly>, + "primary" + >; + /** Ordered signable requests used to produce the signatures passed to `PreparedTransactionPlan.build(...)`. */ readonly signatureRequests: readonly TransactionPlanSignatureRequest< Extract >[]; - /** Ordered viem-compatible calls to submit, with the primary action call last. */ - readonly callRequests: ExecutableTransactionPlanShape< + /** Ordered transaction steps to submit, with the primary action step last. */ + readonly transactionSteps: ExecutableTransactionPlanShape< TPrimaryAction, TRequest - >["callRequests"]; + >["transactionSteps"]; /** * Creates an executable plan from built transaction requests. @@ -635,16 +717,51 @@ export class ExecutableTransactionPlan< * @example * ```ts * const executable = new ExecutableTransactionPlan({ + * primaryStep, * signatureRequests: [], - * callRequests, + * transactionSteps, * }); * ``` */ constructor( - params: ExecutableTransactionPlanShape, + params: Omit< + ExecutableTransactionPlanShape, + "primaryTransaction" | "calls" + >, ) { + this.builtPrimaryStep = params.primaryStep; this.signatureRequests = Object.freeze([...params.signatureRequests]); - this.callRequests = Object.freeze([...params.callRequests]); + this.transactionSteps = Object.freeze([...params.transactionSteps]); + } + + /** + * Returns the built primary step. + * + * @returns The final primary transaction step after signatures are applied. + * @example + * ```ts + * const primaryStep = executable.primaryStep; + * ``` + */ + get primaryStep(): TransactionPlanTransactionStep< + TPrimaryAction, + Readonly>, + "primary" + > { + return this.builtPrimaryStep; + } + + /** + * Returns the built primary SDK transaction. + * + * @returns The final primary transaction after signatures are applied. + * @example + * ```ts + * const tx = executable.primaryTransaction; + * ``` + */ + get primaryTransaction(): Readonly> { + return this.builtPrimaryStep.transaction; } /** @@ -653,10 +770,14 @@ export class ExecutableTransactionPlan< * @returns The raw calls in execution order. * @example * ```ts - * const calls = executable.viemCalls; + * const calls = executable.calls; * ``` */ - get viemCalls(): readonly TransactionPlanViemCall[] { - return this.callRequests.map((request) => request.call); + get calls(): readonly TransactionPlanBatchCall[] { + return this.transactionSteps.map(({ transaction }) => ({ + to: transaction.to, + value: transaction.value, + data: transaction.data, + })); } } diff --git a/packages/morpho-sdk/src/transactionPlan/types.ts b/packages/morpho-sdk/src/transactionPlan/types.ts index 1ee3b2744..609bb6b18 100644 --- a/packages/morpho-sdk/src/transactionPlan/types.ts +++ b/packages/morpho-sdk/src/transactionPlan/types.ts @@ -3,7 +3,6 @@ import type { AuthorizationRequirementSignature, BaseAction, BlueAuthorizationAction, - CallRequirement, ERC20ApprovalAction, PermitRequirementSignature, Requirement, @@ -11,6 +10,7 @@ import type { SignatureRequirement, Transaction, TransactionAction, + TransactionRequirement, } from "../types/action.js"; /** Options forwarded to token-approval request discovery. */ @@ -33,8 +33,8 @@ export type TransactionPlanOperatorAuthorizationRequest = | Readonly> | Requirement; -/** Raw viem-compatible call data extracted from a Morpho SDK transaction. */ -export interface TransactionPlanViemCall { +/** EIP-5792-compatible batch call converted from a Morpho SDK transaction. */ +export interface TransactionPlanBatchCall { /** Contract that receives the call. */ readonly to: Address; /** Encoded calldata. */ @@ -44,15 +44,17 @@ export interface TransactionPlanViemCall { } /** On-chain or signable request discovered while preparing a transaction plan. */ -export type TransactionPlanRequest = CallRequirement | SignatureRequirement; +export type TransactionPlanRequest = + | TransactionRequirement + | SignatureRequirement; -/** Builds the primary transaction call, optionally consuming previously signed requests. */ -export type TransactionPlanBuildPrimaryCall< +/** Builds the primary transaction, optionally consuming previously signed requests. */ +export type TransactionPlanBuildPrimaryTransaction< TPrimaryAction extends TransactionAction = TransactionAction, TSignatures = readonly RequirementSignature[], > = (signatures?: TSignatures) => Readonly>; -/** Resolves requirement calls and signature requests before the primary transaction call. */ +/** Resolves transaction and signature requirements before the primary transaction. */ export type TransactionPlanGetRequirementRequests< TRequestOptions = unknown, TRequest extends TransactionPlanRequest = TransactionPlanRequest, @@ -65,23 +67,23 @@ export interface TransactionPlanHandler< TRequest extends TransactionPlanRequest = TransactionPlanRequest, TSignatures = readonly RequirementSignature[], > { - /** Builds the primary transaction call, optionally consuming previously signed requests. */ - readonly buildPrimaryCall: TransactionPlanBuildPrimaryCall< + /** Builds the primary transaction, optionally consuming previously signed requests. */ + readonly buildPrimaryTransaction: TransactionPlanBuildPrimaryTransaction< TPrimaryAction, TSignatures >; /** - * Controls whether `prepare()` can include an unsigned preview of the primary call. + * Controls whether `prepare()` can include an unsigned preview of the primary transaction. * - * Leave unset for normal flows where the primary call can be encoded without signatures. Set to + * Leave unset for normal flows where the primary transaction can be encoded without signatures. Set to * `false` for signature-dependent flows such as Midnight maker ECDSA offers, where the final * mempool payload is produced by the offer-root signature and can only be encoded inside * `prepared.build(signatures)`. */ - readonly previewPrimaryCall?: + readonly previewPrimaryTransaction?: | false | (() => Readonly>); - /** Resolves requirement calls and signature requests before the primary transaction call. */ + /** Resolves transaction and signature requirements before the primary transaction. */ readonly getRequirementRequests?: TransactionPlanGetRequirementRequests< TRequestOptions, TRequest @@ -144,12 +146,12 @@ export interface TransactionPlanMidnightOfferRootIntent { readonly offers: number; } -/** Generic on-chain transaction intent for neutral requirement calls. */ -export interface TransactionPlanContractCallIntent< +/** Generic on-chain transaction intent for neutral transaction requirements. */ +export interface TransactionPlanContractTransactionIntent< TAction extends TransactionAction = TransactionAction, > { /** Intent discriminator for filtering and app-specific rendering. */ - readonly type: "contractCall"; + readonly type: "contractTransaction"; /** SDK action type attached to the call metadata. */ readonly actionType: TAction["type"]; } @@ -171,15 +173,15 @@ export type TransactionPlanIntent< | TransactionPlanTokenApprovalIntent | TransactionPlanOperatorAuthorizationIntent | TransactionPlanMidnightOfferRootIntent - | TransactionPlanContractCallIntent + | TransactionPlanContractTransactionIntent | TransactionPlanPrimaryTransactionIntent; -/** Flow shape derived from a plan's signature and call request mix. */ +/** Flow shape derived from a plan's signature requests and transaction steps. */ export type TransactionPlanFlowKind = - | "single_call" - | "call_requests" - | "signature_requests" - | "mixed_requests"; + | "single_transaction" + | "transaction_steps" + | "signature_steps" + | "mixed_steps"; /** A signable request in a transaction plan. */ export interface TransactionPlanSignatureRequest< @@ -202,8 +204,8 @@ export interface TransactionPlanSignatureRequest< readonly sign: TRequest["sign"]; } -/** A viem-compatible call request in a transaction plan. */ -export interface TransactionPlanCallRequest< +/** A transaction step containing an SDK transaction and plan metadata. */ +export interface TransactionPlanTransactionStep< TAction extends BaseAction = TransactionAction, TTransaction extends Readonly> = Readonly< Transaction @@ -211,29 +213,27 @@ export interface TransactionPlanCallRequest< TPhase extends "preparation" | "primary" = "preparation" | "primary", > { /** Request kind discriminator. */ - readonly kind: "call"; - /** Stable id based on original request order, or `primary` for the requested action call. */ + readonly kind: "transaction"; + /** Stable id based on original requirement order, or `primary` for the requested action. */ readonly id: string; - /** Whether this call supports the flow or executes the requested primary action. */ + /** Whether this step supports the flow or executes the requested primary action. */ readonly phase: TPhase; /** Original SDK transaction with Morpho action metadata. */ - readonly tx: TTransaction; - /** Exact SDK/protocol action metadata used to encode this call. */ + readonly transaction: TTransaction; + /** Exact SDK/protocol action metadata used to encode this transaction. */ readonly action: TAction; /** Higher-level plan category for app labels, filtering, and analytics. */ readonly intent: TransactionPlanIntent>; - /** viem-compatible call extracted from the transaction. */ - readonly call: TransactionPlanViemCall; } /** Prepared representation of one prerequisite request discovered by a transaction plan. */ -export type TransactionPlanPreparedRequest< +export type TransactionPlanPreparedStep< TRequest extends TransactionPlanRequest = TransactionPlanRequest, > = | TransactionPlanSignatureRequest> - | TransactionPlanCallRequest< - Extract["action"], - Extract, + | TransactionPlanTransactionStep< + Extract["action"], + Extract, "preparation" >; @@ -242,8 +242,8 @@ export type TransactionPlanStep< TPrimaryAction extends TransactionAction = TransactionAction, TRequest extends TransactionPlanRequest = TransactionPlanRequest, > = - | TransactionPlanPreparedRequest - | TransactionPlanCallRequest< + | TransactionPlanPreparedStep + | TransactionPlanTransactionStep< TPrimaryAction, Readonly>, "primary" @@ -263,16 +263,28 @@ export interface PreparedTransactionPlanShape< TPrimaryAction extends TransactionAction = TransactionAction, TRequest extends TransactionPlanRequest = TransactionPlanRequest, > { + /** Raw prerequisite requirements in discovery order. */ + readonly requirements: readonly TRequest[]; + /** Preview of the primary step, when its transaction can be encoded without signatures. */ + readonly primaryStep?: TransactionPlanTransactionStep< + TPrimaryAction, + Readonly>, + "primary" + >; + /** Preview of the primary SDK transaction, when it can be encoded without signatures. */ + readonly primaryTransaction?: Readonly>; /** Ordered signable requests to present to the user. */ readonly signatureRequests: readonly TransactionPlanSignatureRequest< Extract >[]; - /** Ordered viem-compatible calls. Includes the primary action call last when previewable. */ - readonly callRequests: readonly Extract< + /** Ordered transaction steps. Includes the primary action step last when previewable. */ + readonly transactionSteps: readonly Extract< TransactionPlanStep, - { readonly kind: "call" } + { readonly kind: "transaction" } >[]; - /** All signature requests and call requests in review order. */ + /** Viem-compatible calls converted from the currently previewable transaction steps. */ + readonly calls: readonly TransactionPlanBatchCall[]; + /** All signature requests and transaction steps in review order. */ readonly steps: readonly TransactionPlanStep[]; } @@ -281,13 +293,23 @@ export interface ExecutableTransactionPlanShape< TPrimaryAction extends TransactionAction = TransactionAction, TRequest extends TransactionPlanRequest = TransactionPlanRequest, > { + /** Built primary transaction step. */ + readonly primaryStep: TransactionPlanTransactionStep< + TPrimaryAction, + Readonly>, + "primary" + >; + /** Built primary SDK transaction. */ + readonly primaryTransaction: Readonly>; /** Ordered signable requests used to produce the signatures passed to `build(...)`. */ readonly signatureRequests: readonly TransactionPlanSignatureRequest< Extract >[]; - /** Ordered viem-compatible calls to submit, with the primary action call last. */ - readonly callRequests: readonly Extract< + /** Ordered transaction steps to submit, with the primary action step last. */ + readonly transactionSteps: readonly Extract< TransactionPlanStep, - { readonly kind: "call" } + { readonly kind: "transaction" } >[]; + /** Viem-compatible calls converted from the executable transaction steps. */ + readonly calls: readonly TransactionPlanBatchCall[]; } diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index 6ee2897f3..48f7ead49 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -8,8 +8,8 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. - `Transaction` — immutable `{ to, value, data, action }`. Returned from every action; deep-frozen. - `Requirement` / `RequirementSignature` — prerequisite signing flow for permit/permit2 and Midnight offer roots. - `TransactionPlan` — lazy entity output whose `prepare()` call resolves typed prerequisite requests and whose prepared form builds an executable plan. -- `PreparedTransactionPlan` — resolved, typed review surface containing signature requests, call requests, and semantic intents. -- `ExecutableTransactionPlan` — ordered prerequisite and primary calls ready for submission. +- `PreparedTransactionPlan` — resolved, typed review surface containing signature requests, transaction steps, and semantic intents. +- `ExecutableTransactionPlan` — ordered prerequisite and primary transaction steps ready for submission. - `Metadata` — optional `{ origin, timestamp? }` for calldata tracing. - `DepositAmountArgs` — union enforcing at least one of `amount` / `nativeAmount`. Reused for vault deposits, market collateral supply, and market loan-asset supply. - `AssetsOrSharesArgs` — discriminated union `{ assets } | { shares }`. Used by withdraw (supply-side). diff --git a/packages/morpho-sdk/src/types/action.ts b/packages/morpho-sdk/src/types/action.ts index fc8583f4f..15f35afbf 100644 --- a/packages/morpho-sdk/src/types/action.ts +++ b/packages/morpho-sdk/src/types/action.ts @@ -668,19 +668,21 @@ export type SignatureRequirement = | MidnightOfferRootRequirement | Requirement; -/** Call action metadata that can appear as an action prerequisite. */ -export type CallRequirementAction = +/** Transaction action metadata that can appear as an action prerequisite. */ +export type TransactionRequirementAction = | ERC20ApprovalAction | BlueAuthorizationAction | MidnightAuthorizationAction | SetterRatifierRatifyRootAction | MidnightSupplyCollateralAction; -/** Onchain call prerequisite discovered while preparing an entity transaction plan. */ -export type CallRequirement = Readonly>; +/** Onchain transaction prerequisite discovered while preparing an entity transaction plan. */ +export type TransactionRequirement = Readonly< + Transaction +>; -/** Onchain call or signature prerequisite discovered while preparing an entity transaction plan. */ -export type ActionRequirement = CallRequirement | SignatureRequirement; +/** Onchain transaction or signature prerequisite discovered while preparing an entity transaction plan. */ +export type ActionRequirement = TransactionRequirement | SignatureRequirement; export function isRequirementApproval( requirement: unknown, @@ -720,10 +722,10 @@ export function isRequirementBlueAuthorization( export function isRequirementSignature< T extends RequirementSignature = RequirementSignature, >( - requirement: CallRequirement | Requirement | undefined, + requirement: TransactionRequirement | Requirement | undefined, ): requirement is Requirement; export function isRequirementSignature( - requirement: CallRequirement | Requirement | undefined, + requirement: TransactionRequirement | Requirement | undefined, ): requirement is Requirement; export function isRequirementSignature( requirement: ActionRequirement | undefined, diff --git a/packages/morpho-sdk/test/actions/blue/authorization.test.ts b/packages/morpho-sdk/test/actions/blue/authorization.test.ts index f5c4dbe29..8e083f715 100644 --- a/packages/morpho-sdk/test/actions/blue/authorization.test.ts +++ b/packages/morpho-sdk/test/actions/blue/authorization.test.ts @@ -9,7 +9,6 @@ import { import { WethUsdsBlue } from "../../fixtures/blue.js"; import { supplyCollateral } from "../../helpers/blue.js"; import { test } from "../../setup.js"; -import { getPlanRequests } from "../../transactionPlanUtils.js"; describe("AuthorizationBlue", () => { describe("authorization requirements", () => { @@ -32,13 +31,15 @@ describe("AuthorizationBlue", () => { const market = morphoClient.blue(WethUsdsBlue, mainnet.id); const positionData = await market.getPositionData(client.account.address); - const requirements = await getPlanRequests( - market.borrow({ - userAddress: client.account.address, - amount: parseUnits("100", 18), - positionData, - }), - ); + const requirements = ( + await market + .borrow({ + userAddress: client.account.address, + amount: parseUnits("100", 18), + positionData, + }) + .prepare() + ).requirements; expect(requirements).toHaveLength(1); const authTx = requirements[0]!; @@ -65,22 +66,26 @@ describe("AuthorizationBlue", () => { const market = morphoClient.blue(WethUsdsBlue, mainnet.id); const positionData = await market.getPositionData(client.account.address); - const borrowRequirements = await getPlanRequests( - market.borrow({ - userAddress: client.account.address, - amount: parseUnits("100", 18), - positionData, - }), - ); - - const scbRequirements = await getPlanRequests( - market.supplyCollateralBorrow({ - userAddress: client.account.address, - nativeAmount: parseUnits("5", 18), - borrowAmount: parseUnits("100", 18), - positionData, - }), - ); + const borrowRequirements = ( + await market + .borrow({ + userAddress: client.account.address, + amount: parseUnits("100", 18), + positionData, + }) + .prepare() + ).requirements; + + const scbRequirements = ( + await market + .supplyCollateralBorrow({ + userAddress: client.account.address, + nativeAmount: parseUnits("5", 18), + borrowAmount: parseUnits("100", 18), + positionData, + }) + .prepare() + ).requirements; expect(borrowRequirements).toStrictEqual(scbRequirements); @@ -102,13 +107,15 @@ describe("AuthorizationBlue", () => { const market = morphoClient.blue(WethUsdsBlue, mainnet.id); const positionData = await market.getPositionData(client.account.address); - const requirementsBefore = await getPlanRequests( - market.borrow({ - userAddress: client.account.address, - amount: parseUnits("100", 18), - positionData, - }), - ); + const requirementsBefore = ( + await market + .borrow({ + userAddress: client.account.address, + amount: parseUnits("100", 18), + positionData, + }) + .prepare() + ).requirements; const requirementAuthorization = requirementsBefore[0]; if (!isRequirementBlueAuthorization(requirementAuthorization)) { @@ -116,13 +123,15 @@ describe("AuthorizationBlue", () => { } await client.sendTransaction(requirementAuthorization); - const requirementsAfter = await getPlanRequests( - market.borrow({ - userAddress: client.account.address, - amount: parseUnits("100", 18), - positionData, - }), - ); + const requirementsAfter = ( + await market + .borrow({ + userAddress: client.account.address, + amount: parseUnits("100", 18), + positionData, + }) + .prepare() + ).requirements; expect(requirementsAfter).toHaveLength(0); }); diff --git a/packages/morpho-sdk/test/actions/blue/borrow.test.ts b/packages/morpho-sdk/test/actions/blue/borrow.test.ts index 0367bca58..3b15263a2 100644 --- a/packages/morpho-sdk/test/actions/blue/borrow.test.ts +++ b/packages/morpho-sdk/test/actions/blue/borrow.test.ts @@ -21,7 +21,6 @@ import { WethUsdsBlue } from "../../fixtures/blue.js"; import { supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("BorrowBlue", () => { test("should compute minSharePrice from real market borrow state", async ({ @@ -45,13 +44,15 @@ describe("BorrowBlue", () => { const { totalBorrowAssets, totalBorrowShares } = positionData.market; - const tx = await buildPlanTx( - market.borrow({ - userAddress: client.account.address, - amount: parseUnits("100", 18), - positionData, - }), - ); + const tx = ( + await market + .borrow({ + userAddress: client.account.address, + amount: parseUnits("100", 18), + positionData, + }) + .prepare() + ).build().primaryTransaction; const expectedMinSharePrice = MathLib.mulDivDown( totalBorrowAssets + SharesMath.VIRTUAL_ASSETS, @@ -96,7 +97,7 @@ describe("BorrowBlue", () => { positionData, }); - const requirements = await getPlanRequests(borrow); + const requirements = (await borrow.prepare()).requirements; const requirementAuthorization = requirements[0]; if (!isRequirementBlueAuthorization(requirementAuthorization)) { throw new Error("Authorization requirement not found"); @@ -104,7 +105,7 @@ describe("BorrowBlue", () => { await client.sendTransaction(requirementAuthorization); - const tx = await buildPlanTx(borrow); + const tx = (await borrow.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, @@ -171,7 +172,7 @@ describe("BorrowBlue", () => { // With supportSignature the authorization requirement is signable — // no standalone setAuthorization transaction is sent. - const requirements = await getPlanRequests(borrow); + const requirements = (await borrow.prepare()).requirements; const requirement = requirements[0]; if (!isRequirementSignature(requirement)) { throw new Error("Expected a signable authorization requirement"); @@ -198,7 +199,9 @@ describe("BorrowBlue", () => { }), ).toBe(false); - const tx = await buildPlanTx(borrow, [authorizationSignature]); + const tx = (await borrow.prepare()).build([ + authorizationSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts index e221be962..b3cd5779c 100644 --- a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts +++ b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts @@ -26,7 +26,6 @@ import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; /** PublicAllocator admin for the Steakhouse vault at the fork block. */ const PA_ADMIN: Address = "0x9E9110cFd24cd851ea5bc73a27975B33E308f9e1"; @@ -80,14 +79,14 @@ describe("Borrow with single vault reallocation (e2e)", () => { reallocations, }); - const requirements = await getPlanRequests(borrow); + const requirements = (await borrow.prepare()).requirements; const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = await buildPlanTx(borrow); + const tx = (await borrow.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); @@ -165,14 +164,14 @@ describe("Borrow with multiple source market withdrawals", () => { reallocations, }); - const requirements = await getPlanRequests(borrow); + const requirements = (await borrow.prepare()).requirements; const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = await buildPlanTx(borrow); + const tx = (await borrow.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -266,14 +265,14 @@ describe("Borrow with reallocation fee", () => { reallocations, }); - const requirements = await getPlanRequests(borrow); + const requirements = (await borrow.prepare()).requirements; const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = await buildPlanTx(borrow); + const tx = (await borrow.prepare()).build().primaryTransaction; expect(tx.value).toBe(reallocationFee); expect(tx.action.args.reallocationFee).toBe(reallocationFee); @@ -356,7 +355,7 @@ describe("SupplyCollateralBorrow with single vault reallocation", () => { reallocations, }); - const requirements = await getPlanRequests(scb); + const requirements = (await scb.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -369,7 +368,7 @@ describe("SupplyCollateralBorrow with single vault reallocation", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = await buildPlanTx(scb); + const tx = (await scb.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); @@ -450,7 +449,7 @@ describe("SupplyCollateralBorrow with multiple source market withdrawals", () => reallocations, }); - const requirements = await getPlanRequests(scb); + const requirements = (await scb.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -463,7 +462,7 @@ describe("SupplyCollateralBorrow with multiple source market withdrawals", () => await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = await buildPlanTx(scb); + const tx = (await scb.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -563,7 +562,7 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { reallocations, }); - const requirements = await getPlanRequests(scb); + const requirements = (await scb.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -576,7 +575,7 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = await buildPlanTx(scb); + const tx = (await scb.prepare()).build().primaryTransaction; expect(tx.value).toBe(reallocationFee); expect(tx.action.args.reallocationFee).toBe(reallocationFee); @@ -693,14 +692,14 @@ describe("getReallocationData and getReallocations", () => { reallocations, }); - const requirements = await getPlanRequests(borrow); + const requirements = (await borrow.prepare()).requirements; const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = await buildPlanTx(borrow); + const tx = (await borrow.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -763,7 +762,7 @@ describe("getReallocationData and getReallocations", () => { reallocations, }); - const requirements = await getPlanRequests(scb); + const requirements = (await scb.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -776,7 +775,7 @@ describe("getReallocationData and getReallocations", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = await buildPlanTx(scb); + const tx = (await scb.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/refinance.test.ts b/packages/morpho-sdk/test/actions/blue/refinance.test.ts index d9c0183ff..e558f4ac7 100644 --- a/packages/morpho-sdk/test/actions/blue/refinance.test.ts +++ b/packages/morpho-sdk/test/actions/blue/refinance.test.ts @@ -13,7 +13,6 @@ import { YearnUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { borrow, supplyCollateral, supplyLoan } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; // Two real wstETH/wNative markets sharing the loan + collateral pair but differing oracles, at the pinned fork. const wstEthWeth_v1 = new MarketParams({ @@ -96,7 +95,7 @@ describe("RefinanceBlue (fork)", () => { }); // Tolerate either a clean wallet (one auth requirement) or a pre-authorized one (none). - const requirements = await getPlanRequests(refi); + const requirements = (await refi.prepare()).requirements; expect(requirements.length).toBeLessThanOrEqual(1); for (const requirement of requirements) { if (!isRequirementBlueAuthorization(requirement)) { @@ -105,7 +104,9 @@ describe("RefinanceBlue (fork)", () => { await client.sendTransaction(requirement); } - await client.sendTransaction(await buildPlanTx(refi)); + await client.sendTransaction( + (await refi.prepare()).build().primaryTransaction, + ); }, }); // testInvariants already asserts every bundler3 component ends with 0 balance — nothing left behind. @@ -177,7 +178,7 @@ describe("RefinanceBlue (fork)", () => { slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const requirements = await getPlanRequests(refi); + const requirements = (await refi.prepare()).requirements; expect(requirements.length).toBeLessThanOrEqual(1); for (const requirement of requirements) { if (!isRequirementBlueAuthorization(requirement)) { @@ -185,7 +186,9 @@ describe("RefinanceBlue (fork)", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(await buildPlanTx(refi)); + await client.sendTransaction( + (await refi.prepare()).build().primaryTransaction, + ); }, }); @@ -241,7 +244,7 @@ describe("RefinanceBlue (fork)", () => { collateralAmount, }); - const requirements = await getPlanRequests(refi); + const requirements = (await refi.prepare()).requirements; // Collat-only still needs GA1 authorized: the callback's withdrawCollateral runs onBehalf=user. expect(requirements).toHaveLength(1); const auth = requirements[0]!; @@ -249,7 +252,9 @@ describe("RefinanceBlue (fork)", () => { throw new Error("Authorization requirement not found"); } await client.sendTransaction(auth); - await client.sendTransaction(await buildPlanTx(refi)); + await client.sendTransaction( + (await refi.prepare()).build().primaryTransaction, + ); }, }); @@ -341,7 +346,7 @@ describe("RefinanceBlue (fork)", () => { targetReallocations: reallocations, }); - const requirements = await getPlanRequests(refi); + const requirements = (await refi.prepare()).requirements; expect(requirements.length).toBeLessThanOrEqual(1); for (const requirement of requirements) { if (!isRequirementBlueAuthorization(requirement)) { @@ -350,7 +355,7 @@ describe("RefinanceBlue (fork)", () => { await client.sendTransaction(requirement); } - const tx = await buildPlanTx(refi); + const tx = (await refi.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); diff --git a/packages/morpho-sdk/test/actions/blue/repay.test.ts b/packages/morpho-sdk/test/actions/blue/repay.test.ts index 764ae39dd..dd9e73d11 100644 --- a/packages/morpho-sdk/test/actions/blue/repay.test.ts +++ b/packages/morpho-sdk/test/actions/blue/repay.test.ts @@ -19,7 +19,6 @@ import { WethUsdsBlue, WstethWethBlue } from "../../fixtures/blue.js"; import { borrow, supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("RepayBlue", () => { afterEach(() => { @@ -65,7 +64,7 @@ describe("RepayBlue", () => { positionData, }); - const requirements = await getPlanRequests(repay); + const requirements = (await repay.prepare()).requirements; // Repay should NOT have morpho authorization requirement const approval = requirements[0]; @@ -74,7 +73,7 @@ describe("RepayBlue", () => { } await client.sendTransaction(approval); - const tx = await buildPlanTx(repay); + const tx = (await repay.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -130,7 +129,7 @@ describe("RepayBlue", () => { positionData, }); - const requirements = await getPlanRequests(repay); + const requirements = (await repay.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -143,7 +142,7 @@ describe("RepayBlue", () => { }); await client.sendTransaction(approval); - const tx = await buildPlanTx(repay); + const tx = (await repay.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -226,14 +225,14 @@ describe("RepayBlue", () => { positionData, }); - const requirements = await getPlanRequests(repay); + const requirements = (await repay.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); } await client.sendTransaction(approval); - const tx = await buildPlanTx(repay); + const tx = (await repay.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -293,10 +292,10 @@ describe("RepayBlue", () => { }); // A fully-native repay pulls no ERC-20, so it needs no approval. - const requirements = await getPlanRequests(repay); + const requirements = (await repay.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = await buildPlanTx(repay); + const tx = (await repay.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -375,7 +374,7 @@ describe("RepayBlue", () => { }); // The approval must cover ONLY the ERC-20 portion, not the wrapped native. - const requirements = await getPlanRequests(repay); + const requirements = (await repay.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -383,7 +382,7 @@ describe("RepayBlue", () => { expect(approval.action.args.amount).toEqual(erc20Part); await client.sendTransaction(approval); - const tx = await buildPlanTx(repay); + const tx = (await repay.prepare()).build().primaryTransaction; // Only the native portion rides as tx.value. expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); @@ -462,7 +461,7 @@ describe("RepayBlue", () => { positionData, }); - const requirements = await getPlanRequests(repay); + const requirements = (await repay.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -476,7 +475,7 @@ describe("RepayBlue", () => { }); await client.sendTransaction(approval); - const tx = await buildPlanTx(repay); + const tx = (await repay.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); }, @@ -552,10 +551,10 @@ describe("RepayBlue", () => { }); // Native covers the full debt ⇒ no ERC-20 approval/permit requirement. - const requirements = await getPlanRequests(repay); + const requirements = (await repay.prepare()).requirements; expect(requirements).toEqual([]); - const tx = await buildPlanTx(repay); + const tx = (await repay.prepare()).build().primaryTransaction; // Everything is wrapped native; no ERC-20 pulled. expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.transferAmount).toEqual(nativeAmount); diff --git a/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts b/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts index 4ef5b5d69..26dd62ceb 100644 --- a/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts +++ b/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts @@ -21,7 +21,6 @@ import { WethUsdsBlue, WstethWethBlue } from "../../fixtures/blue.js"; import { borrow, supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("RepayWithdrawCollateralBlue", () => { afterEach(() => { @@ -75,7 +74,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await getPlanRequests(action); + const requirements = (await action.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { @@ -83,7 +82,7 @@ describe("RepayWithdrawCollateralBlue", () => { } await client.sendTransaction(approval); - const tx = await buildPlanTx(action); + const tx = (await action.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -145,7 +144,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await getPlanRequests(action); + const requirements = (await action.prepare()).requirements; for (const req of requirements) { if (isRequirementApproval(req)) { // Shares-mode repayments use a forward-accrued transfer amount; @@ -160,7 +159,7 @@ describe("RepayWithdrawCollateralBlue", () => { } } - const tx = await buildPlanTx(action); + const tx = (await action.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -242,7 +241,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await getPlanRequests(action); + const requirements = (await action.prepare()).requirements; for (const req of requirements) { if (isRequirementApproval(req)) { // Shares-mode repayments use a forward-accrued transfer amount; @@ -257,7 +256,7 @@ describe("RepayWithdrawCollateralBlue", () => { } } - const tx = await buildPlanTx(action); + const tx = (await action.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -321,7 +320,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await getPlanRequests(action); + const requirements = (await action.prepare()).requirements; // A fully-native repay pulls no ERC-20, so there is no approval to make; // GeneralAdapter1 was already authorized when the position was opened. expect(requirements.some(isRequirementApproval)).toBe(false); @@ -331,7 +330,7 @@ describe("RepayWithdrawCollateralBlue", () => { } } - const tx = await buildPlanTx(action); + const tx = (await action.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -415,7 +414,7 @@ describe("RepayWithdrawCollateralBlue", () => { // GA1 was authorized when the position opened, so the only requirement is // the ERC-20 approval — and it must cover ONLY the ERC-20 portion. - const requirements = await getPlanRequests(action); + const requirements = (await action.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -423,7 +422,7 @@ describe("RepayWithdrawCollateralBlue", () => { expect(approval.action.args.amount).toEqual(erc20Part); await client.sendTransaction(approval); - const tx = await buildPlanTx(action); + const tx = (await action.prepare()).build().primaryTransaction; // Only the native portion rides as tx.value. expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); @@ -508,7 +507,7 @@ describe("RepayWithdrawCollateralBlue", () => { positionData, }); - const requirements = await getPlanRequests(action); + const requirements = (await action.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); @@ -522,7 +521,7 @@ describe("RepayWithdrawCollateralBlue", () => { }); await client.sendTransaction(approval); - const tx = await buildPlanTx(action); + const tx = (await action.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/blue/supply.test.ts b/packages/morpho-sdk/test/actions/blue/supply.test.ts index 3e109d597..1724e7131 100644 --- a/packages/morpho-sdk/test/actions/blue/supply.test.ts +++ b/packages/morpho-sdk/test/actions/blue/supply.test.ts @@ -21,7 +21,6 @@ import { } from "../../fixtures/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("SupplyBlue", () => { test("should supply loan token end-to-end", async ({ client }) => { @@ -50,14 +49,14 @@ describe("SupplyBlue", () => { marketData, }); - const requirements = await getPlanRequests(supply); + const requirements = (await supply.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); } await client.sendTransaction(approval); - const tx = await buildPlanTx(supply); + const tx = (await supply.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -91,7 +90,7 @@ describe("SupplyBlue", () => { marketData, }); - const requirements = await getPlanRequests(supply); + const requirements = (await supply.prepare()).requirements; expect(requirements.length).toBeGreaterThan(0); const approval = requirements[0]; expect(approval).toBeDefined(); @@ -112,7 +111,7 @@ describe("SupplyBlue", () => { marketData, }); - const tx = await buildPlanTx(supply); + const tx = (await supply.prepare()).build().primaryTransaction; // Sanity bound only — exact value depends on virtual-share scaling. expect(tx.action.args.maxSharePrice).toBeGreaterThan(0n); @@ -155,7 +154,7 @@ describe("SupplyBlue", () => { marketData, }); - const requirements = await getPlanRequests(supply); + const requirements = (await supply.prepare()).requirements; expect(requirements.length).toBe(2); const approvalPermit2 = requirements[0]; @@ -179,7 +178,9 @@ describe("SupplyBlue", () => { ); expect(isHex(requirementSignature.args.signature)).toBe(true); - const tx = await buildPlanTx(supply, [requirementSignature]); + const tx = (await supply.prepare()).build([ + requirementSignature, + ]).primaryTransaction; expect(tx.value).toBe(0n); await client.sendTransaction(tx); }, @@ -223,10 +224,10 @@ describe("SupplyBlue", () => { }); // No ERC20 approval needed: only native wrapping inside the bundle. - const requirements = await getPlanRequests(supply); + const requirements = (await supply.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = await buildPlanTx(supply); + const tx = (await supply.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -275,14 +276,14 @@ describe("SupplyBlue", () => { marketData, }); - const requirements = await getPlanRequests(supply); + const requirements = (await supply.prepare()).requirements; const approval = requirements[0]; if (!isRequirementApproval(approval)) { throw new Error("Approval requirement not found"); } await client.sendTransaction(approval); - const tx = await buildPlanTx(supply); + const tx = (await supply.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts b/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts index 2a7b713dd..5d2fde522 100644 --- a/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts +++ b/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts @@ -18,7 +18,6 @@ import { } from "../../fixtures/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("SupplyCollateralBlue", () => { test("should supply 1 collateral with approval", async ({ client }) => { @@ -46,7 +45,7 @@ describe("SupplyCollateralBlue", () => { amount, }); - const requirements = await getPlanRequests(supplyCollateral); + const requirements = (await supplyCollateral.prepare()).requirements; const approveTx = requirements[0]; if (!isRequirementApproval(approveTx)) { @@ -55,7 +54,8 @@ describe("SupplyCollateralBlue", () => { await client.sendTransaction(approveTx); - const tx = await buildPlanTx(supplyCollateral); + const tx = (await supplyCollateral.prepare()).build() + .primaryTransaction; await client.sendTransaction(tx); }, }); @@ -107,10 +107,11 @@ describe("SupplyCollateralBlue", () => { amount, }); - const requirements = await getPlanRequests(supplyCollateral); + const requirements = (await supplyCollateral.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = await buildPlanTx(supplyCollateral); + const tx = (await supplyCollateral.prepare()).build() + .primaryTransaction; await client.sendTransaction(tx); }, @@ -153,10 +154,11 @@ describe("SupplyCollateralBlue", () => { nativeAmount, }); - const requirements = await getPlanRequests(supplyCollateral); + const requirements = (await supplyCollateral.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = await buildPlanTx(supplyCollateral); + const tx = (await supplyCollateral.prepare()).build() + .primaryTransaction; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); @@ -210,7 +212,7 @@ describe("SupplyCollateralBlue", () => { nativeAmount, }); - const requirements = await getPlanRequests(supplyCollateral); + const requirements = (await supplyCollateral.prepare()).requirements; expect(requirements.length).toBe(1); const approveTx = requirements[0]; @@ -220,7 +222,8 @@ describe("SupplyCollateralBlue", () => { await client.sendTransaction(approveTx); - const tx = await buildPlanTx(supplyCollateral); + const tx = (await supplyCollateral.prepare()).build() + .primaryTransaction; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); @@ -269,9 +272,13 @@ describe("SupplyCollateralBlue", () => { amount, }); - const requirements = await getPlanRequests(supplyCollateral, { - useSimplePermit: true, - }); + const requirements = ( + await supplyCollateral.prepare({ + requestOptions: { + useSimplePermit: true, + }, + }) + ).requirements; expect(requirements.length).toBe(1); if (!isRequirementSignature(requirements[0])) { @@ -283,7 +290,9 @@ describe("SupplyCollateralBlue", () => { client.account.address, ); - const tx = await buildPlanTx(supplyCollateral, [requirementSignature]); + const tx = (await supplyCollateral.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, }); @@ -333,7 +342,7 @@ describe("SupplyCollateralBlue", () => { amount, }); - const requirements = await getPlanRequests(supplyCollateral); + const requirements = (await supplyCollateral.prepare()).requirements; expect(requirements.length).toBe(2); const approvalPermit2 = requirements[0]; @@ -366,7 +375,9 @@ describe("SupplyCollateralBlue", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = await buildPlanTx(supplyCollateral, [requirementSignature]); + const tx = (await supplyCollateral.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts b/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts index 84baa0978..71753f8c8 100644 --- a/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts +++ b/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts @@ -18,7 +18,6 @@ import { UsdcEurcvBlue, WethUsdsBlue } from "../../fixtures/blue.js"; import { supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("SupplyCollateralBorrowBlue", () => { test("should supply collateral and borrow with ERC20 approval and authorization", async ({ @@ -50,7 +49,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await getPlanRequests(scb); + const requirements = (await scb.prepare()).requirements; expect(requirements).toHaveLength(2); expect(requirements[0]!.action.type).toBe("erc20Approval"); expect(requirements[1]!.action.type).toBe("blueAuthorization"); @@ -67,7 +66,7 @@ describe("SupplyCollateralBorrowBlue", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = await buildPlanTx(scb); + const tx = (await scb.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, @@ -125,7 +124,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await getPlanRequests(scb); + const requirements = (await scb.prepare()).requirements; expect(requirements).toHaveLength(1); expect(requirements[0]!.action.type).toBe("blueAuthorization"); @@ -136,7 +135,7 @@ describe("SupplyCollateralBorrowBlue", () => { await client.sendTransaction(authorization); - const tx = await buildPlanTx(scb); + const tx = (await scb.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -198,7 +197,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await getPlanRequests(scb); + const requirements = (await scb.prepare()).requirements; expect(requirements).toHaveLength(2); expect(requirements[0]!.action.type).toBe("erc20Approval"); expect(requirements[1]!.action.type).toBe("blueAuthorization"); @@ -214,7 +213,7 @@ describe("SupplyCollateralBorrowBlue", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = await buildPlanTx(scb); + const tx = (await scb.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -283,7 +282,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await getPlanRequests(scb); + const requirements = (await scb.prepare()).requirements; expect(requirements).toHaveLength(1); const approval = requirements[0]; @@ -292,7 +291,7 @@ describe("SupplyCollateralBorrowBlue", () => { } await client.sendTransaction(approval); - const tx = await buildPlanTx(scb); + const tx = (await scb.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -347,9 +346,13 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await getPlanRequests(scb, { - useSimplePermit: true, - }); + const requirements = ( + await scb.prepare({ + requestOptions: { + useSimplePermit: true, + }, + }) + ).requirements; expect(requirements).toHaveLength(2); const permitRequirement = requirements[0]; @@ -376,10 +379,10 @@ describe("SupplyCollateralBorrowBlue", () => { client.account.address, ); - const tx = await buildPlanTx(scb, [ + const tx = (await scb.prepare()).build([ permitSignature, authorizationSignature, - ]); + ]).primaryTransaction; await client.sendTransaction(tx); }, }); @@ -440,7 +443,7 @@ describe("SupplyCollateralBorrowBlue", () => { positionData, }); - const requirements = await getPlanRequests(scb); + const requirements = (await scb.prepare()).requirements; // permit2 requirements: approve token -> permit2 + permit2 signature + morpho authorization expect(requirements).toHaveLength(3); @@ -476,10 +479,10 @@ describe("SupplyCollateralBorrowBlue", () => { client.account.address, ); - const tx = await buildPlanTx(scb, [ + const tx = (await scb.prepare()).build([ permit2Signature, authorizationSignature, - ]); + ]).primaryTransaction; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/withdraw.test.ts b/packages/morpho-sdk/test/actions/blue/withdraw.test.ts index d95a49013..976ac344a 100644 --- a/packages/morpho-sdk/test/actions/blue/withdraw.test.ts +++ b/packages/morpho-sdk/test/actions/blue/withdraw.test.ts @@ -21,7 +21,6 @@ import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { supplyLoan } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("WithdrawBlue", () => { test("should withdraw loan token end-to-end (assets mode)", async ({ @@ -57,14 +56,14 @@ describe("WithdrawBlue", () => { positionData, }); - const requirements = await getPlanRequests(withdraw); + const requirements = (await withdraw.prepare()).requirements; const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -112,14 +111,14 @@ describe("WithdrawBlue", () => { positionData, }); - const requirements = await getPlanRequests(withdraw); + const requirements = (await withdraw.prepare()).requirements; const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -157,7 +156,7 @@ describe("WithdrawBlue", () => { positionData, }); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; expect(tx.action.args.receiver).toBe(receiver); }); @@ -183,7 +182,7 @@ describe("WithdrawBlue", () => { positionData, }); - const requirements = await getPlanRequests(withdraw); + const requirements = (await withdraw.prepare()).requirements; expect(requirements.length).toBe(1); expect(isRequirementBlueAuthorization(requirements[0])).toBe(true); }); @@ -237,14 +236,14 @@ describe("WithdrawBlue", () => { reallocations, }); - const requirements = await getPlanRequests(withdraw); + const requirements = (await withdraw.prepare()).requirements; const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); @@ -308,14 +307,14 @@ describe("WithdrawBlue", () => { reallocations, }); - const requirements = await getPlanRequests(withdraw); + const requirements = (await withdraw.prepare()).requirements; const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; expect(tx.value).toBe(0n); expect(tx.action.args.shares).toBe(withdrawShares); expect(tx.action.args.assets).toBe(0n); @@ -383,14 +382,14 @@ describe("WithdrawBlue", () => { reallocations, }); - const requirements = await getPlanRequests(withdraw); + const requirements = (await withdraw.prepare()).requirements; const authorization = requirements[0]; if (!isRequirementBlueAuthorization(authorization)) { throw new Error("Authorization requirement not found"); } await client.sendTransaction(authorization); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; expect(tx.action.args.receiver).toBe(receiver); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts b/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts index ecb6ac6ae..68ea697e7 100644 --- a/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts +++ b/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts @@ -13,7 +13,6 @@ import { WethUsdsBlue } from "../../fixtures/blue.js"; import { borrow, supplyCollateral } from "../../helpers/blue.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("WithdrawCollateralBlue", () => { test("should withdraw collateral (no debt)", async ({ client }) => { @@ -49,7 +48,7 @@ describe("WithdrawCollateralBlue", () => { positionData, }); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -98,7 +97,7 @@ describe("WithdrawCollateralBlue", () => { positionData, }); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/metadata.test.ts b/packages/morpho-sdk/test/actions/metadata.test.ts index ce1100a08..eeb712ef8 100644 --- a/packages/morpho-sdk/test/actions/metadata.test.ts +++ b/packages/morpho-sdk/test/actions/metadata.test.ts @@ -6,7 +6,6 @@ import { isRequirementApproval, morphoViemExtension } from "../../src/index.js"; import { KeyrockUsdcVaultV2 } from "../fixtures/vaultV2.js"; import { testInvariants } from "../helpers/invariants.js"; import { test } from "../setup.js"; -import { buildPlanTx, getPlanRequests } from "../transactionPlanUtils.js"; describe("Metadata", () => { test("should create deposit bundle with origin and timestamp metadata", async ({ @@ -44,7 +43,7 @@ describe("Metadata", () => { vaultData, }); - const tx_1 = await buildPlanTx(deposit); + const tx_1 = (await deposit.prepare()).build().primaryTransaction; expect(tx_1.data).toContain("25AFEA44"); const position = tx_1.data.indexOf("25AFEA44"); expect(position).toBeGreaterThanOrEqual(8); @@ -55,7 +54,7 @@ describe("Metadata", () => { expect(typeof timestampNumber).toBe("number"); expect(timestampNumber).toBeLessThanOrEqual(Number(Time.timestamp())); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; const approveTx = requirements[0]; if (!approveTx) { @@ -65,7 +64,7 @@ describe("Metadata", () => { throw new Error("Approve transaction is not an approval transaction"); } - const tx_2 = await buildPlanTx(deposit); + const tx_2 = (await deposit.prepare()).build().primaryTransaction; await client.sendTransaction(approveTx); await client.sendTransaction(tx_2); }, @@ -116,7 +115,7 @@ describe("Metadata", () => { vaultData, }); - const tx = await buildPlanTx(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; expect(tx.data).toContain("25AFEA44"); const position = tx.data.indexOf("25AFEA44"); expect(position).toBeGreaterThanOrEqual(8); @@ -124,7 +123,7 @@ describe("Metadata", () => { const timestampHex = tx.data.slice(position - 8, position); expect(timestampHex).toBe("00000000"); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; const approveTx = requirements[0]; if (!approveTx) { diff --git a/packages/morpho-sdk/test/actions/midnight/requirements.test.ts b/packages/morpho-sdk/test/actions/midnight/requirements.test.ts index f8b519216..a1aeed899 100644 --- a/packages/morpho-sdk/test/actions/midnight/requirements.test.ts +++ b/packages/morpho-sdk/test/actions/midnight/requirements.test.ts @@ -32,7 +32,6 @@ import { isRequirementApproval, morphoViemExtension, } from "../../../src/index.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; const test = createViemTest(base, { forkUrl: process.env.BASE_RPC_URL, @@ -147,7 +146,7 @@ const prepareTakeableOffer = async (params: { reservedCollateralAssets: 0n, }); await params.client.sendTransaction({ - ...(await buildPlanTx(supply)), + ...(await supply.prepare()).build().primaryTransaction, account: offerMaker, }); await expect( @@ -329,7 +328,7 @@ describe("Midnight requirements on fork", () => { reservedCollateralAssets, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect(requirements).toHaveLength(1); const approval = requirements[0]; if (!isRequirementApproval(approval)) { @@ -343,8 +342,8 @@ describe("Midnight requirements on fork", () => { ); await client.sendTransaction(approval); - await expect(getPlanRequests(output)).resolves.toEqual([]); - expect((await buildPlanTx(output)).to).toBe( + await expect((await output.prepare()).requirements).toEqual([]); + expect((await output.prepare()).build().primaryTransaction.to).toBe( getChainAddress(ChainId.BaseMainnet, "midnight"), ); }); @@ -371,7 +370,7 @@ describe("Midnight requirements on fork", () => { takeableOffers: [takeableOffer], deadline: maxUint256, }); - const requirements = await getPlanRequests(output); + const requirements = (await output.prepare()).requirements; expect(requirements.map((requirement) => requirement.action.type)).toEqual([ "erc20Approval", "midnightAuthorization", @@ -382,9 +381,11 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await expect(getPlanRequests(output)).resolves.toEqual([]); + await expect((await output.prepare()).requirements).toEqual([]); - await client.sendTransaction(await buildPlanTx(output)); + await client.sendTransaction( + (await output.prepare()).build().primaryTransaction, + ); await expect( client.readContract({ address: midnight, @@ -411,7 +412,7 @@ describe("Midnight requirements on fork", () => { collateralAssets, reservedCollateralAssets: 0n, }); - const supplyRequirements = await getPlanRequests(supply); + const supplyRequirements = (await supply.prepare()).requirements; expect( supplyRequirements.map((requirement) => requirement.action.type), ).toEqual(["erc20Approval"]); @@ -421,7 +422,9 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(await buildPlanTx(supply)); + await client.sendTransaction( + (await supply.prepare()).build().primaryTransaction, + ); await expect( client.readContract({ address: midnight, @@ -444,7 +447,7 @@ describe("Midnight requirements on fork", () => { takeableOffers: [takeableOffer], deadline: maxUint256, }); - const borrowRequirements = await getPlanRequests(borrow); + const borrowRequirements = (await borrow.prepare()).requirements; expect( borrowRequirements.map((requirement) => requirement.action.type), ).toEqual(["midnightAuthorization"]); @@ -454,7 +457,9 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(await buildPlanTx(borrow)); + await client.sendTransaction( + (await borrow.prepare()).build().primaryTransaction, + ); await expect( client.readContract({ address: midnight, @@ -489,7 +494,7 @@ describe("Midnight requirements on fork", () => { takeableOffers: [takeableOffer], deadline: maxUint256, }); - const borrowRequirements = await getPlanRequests(borrow); + const borrowRequirements = (await borrow.prepare()).requirements; expect( borrowRequirements.map((requirement) => requirement.action.type), ).toEqual(["erc20Approval", "midnightAuthorization"]); @@ -499,7 +504,9 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(await buildPlanTx(borrow)); + await client.sendTransaction( + (await borrow.prepare()).build().primaryTransaction, + ); await expect( client.readContract({ address: midnight, @@ -523,7 +530,7 @@ describe("Midnight requirements on fork", () => { withdrawCollateralAssets: 0n, deadline: maxUint256, }); - const repayRequirements = await getPlanRequests(repay); + const repayRequirements = (await repay.prepare()).requirements; expect( repayRequirements.map((requirement) => requirement.action.type), ).toEqual(["erc20Approval"]); @@ -533,7 +540,9 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction(await buildPlanTx(repay)); + await client.sendTransaction( + (await repay.prepare()).build().primaryTransaction, + ); await expect( client.readContract({ address: midnight, diff --git a/packages/morpho-sdk/test/actions/requirements/approval.test.ts b/packages/morpho-sdk/test/actions/requirements/approval.test.ts index 25342c4c4..f03c7fd8b 100644 --- a/packages/morpho-sdk/test/actions/requirements/approval.test.ts +++ b/packages/morpho-sdk/test/actions/requirements/approval.test.ts @@ -9,7 +9,6 @@ import { import { Re7UsdtVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("Approval", () => { test("should approve once for USDT vaultV2 with allowance 0", async ({ @@ -40,7 +39,7 @@ describe("Approval", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(1); expect(requirements[0]?.action.args.spender).toBe(generalAdapter); @@ -52,7 +51,7 @@ describe("Approval", () => { await client.sendTransaction(requirements[0]); - const tx = await buildPlanTx(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, @@ -92,7 +91,7 @@ describe("Approval", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(2); expect(requirements[0]?.action.args.spender).toBe(generalAdapter); @@ -110,7 +109,7 @@ describe("Approval", () => { await client.sendTransaction(requirements[0]); await client.sendTransaction(requirements[1]); - const tx = await buildPlanTx(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/requirements/permit.test.ts b/packages/morpho-sdk/test/actions/requirements/permit.test.ts index f59d01662..b0e6405f8 100644 --- a/packages/morpho-sdk/test/actions/requirements/permit.test.ts +++ b/packages/morpho-sdk/test/actions/requirements/permit.test.ts @@ -8,7 +8,6 @@ import { import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("Permit", () => { test("should deposit USDC with permit version 2", async ({ client }) => { @@ -40,9 +39,13 @@ describe("Permit", () => { amount: amount, vaultData, }); - const requirements_1 = await getPlanRequests(deposit, { - useSimplePermit: true, - }); + const requirements_1 = ( + await deposit.prepare({ + requestOptions: { + useSimplePermit: true, + }, + }) + ).requirements; if (!isRequirementSignature(requirements_1[0])) { throw new Error("Requirement is not a signature requirement"); @@ -60,7 +63,9 @@ describe("Permit", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx_1 = await buildPlanTx(deposit, [requirementSignature]); + const tx_1 = (await deposit.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx_1); }, diff --git a/packages/morpho-sdk/test/actions/requirements/permit2.test.ts b/packages/morpho-sdk/test/actions/requirements/permit2.test.ts index 0b6a3cd46..dba511c6e 100644 --- a/packages/morpho-sdk/test/actions/requirements/permit2.test.ts +++ b/packages/morpho-sdk/test/actions/requirements/permit2.test.ts @@ -15,7 +15,6 @@ import { import { testInvariants } from "../../helpers/invariants.js"; import { createVaultV2 } from "../../helpers/vaultV2.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("Permit2", () => { const { @@ -59,7 +58,7 @@ describe("Permit2", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; // USDT may require two signature requirements (reset approval permit2 + approve permit2 + set allowance) expect(requirements.length).toBe(3); @@ -102,7 +101,9 @@ describe("Permit2", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = await buildPlanTx(deposit, [requirementSignature]); + const tx = (await deposit.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, @@ -149,7 +150,7 @@ describe("Permit2", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; // USDT may require two signature requirements (approve permit2 + set allowance) expect(requirements.length).toBe(2); @@ -180,7 +181,9 @@ describe("Permit2", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = await buildPlanTx(deposit, [requirementSignature]); + const tx = (await deposit.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, @@ -234,7 +237,7 @@ describe("Permit2", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(2); @@ -273,7 +276,8 @@ describe("Permit2", () => { ); await client.sendTransaction( - await buildPlanTx(deposit, [requirementSignature]), + (await deposit.prepare()).build([requirementSignature]) + .primaryTransaction, ); }, }); @@ -329,7 +333,7 @@ describe("Permit2", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(2); @@ -367,7 +371,9 @@ describe("Permit2", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = await buildPlanTx(deposit, [requirementSignature]); + const tx = (await deposit.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, @@ -414,9 +420,13 @@ describe("Permit2", () => { vaultData, }); - const requirements = await getPlanRequests(deposit, { - useSimplePermit: false, - }); + const requirements = ( + await deposit.prepare({ + requestOptions: { + useSimplePermit: false, + }, + }) + ).requirements; expect(requirements.length).toBe(2); @@ -454,7 +464,9 @@ describe("Permit2", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = await buildPlanTx(deposit, [requirementSignature]); + const tx = (await deposit.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts b/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts index 8c8ba9179..1637988ee 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts @@ -18,7 +18,6 @@ import { } from "../../fixtures/vaultV1.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("DepositVaultV1", () => { test("should deposit 1K USDC in vaultV1", async ({ client }) => { @@ -50,8 +49,8 @@ describe("DepositVaultV1", () => { vaultData, }); - const tx = await buildPlanTx(deposit); - const requirements = await getPlanRequests(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; + const requirements = (await deposit.prepare()).requirements; const approveTx = requirements[0]; if (!approveTx) { @@ -118,8 +117,8 @@ describe("DepositVaultV1", () => { vaultData, }); - const tx = await buildPlanTx(deposit); - const requirements = await getPlanRequests(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(2); @@ -183,9 +182,13 @@ describe("DepositVaultV1", () => { amount: amount, vaultData, }); - const requirements = await getPlanRequests(deposit, { - useSimplePermit: true, - }); + const requirements = ( + await deposit.prepare({ + requestOptions: { + useSimplePermit: true, + }, + }) + ).requirements; if (!isRequirementSignature(requirements[0])) { throw new Error("Requirement is not a signature requirement"); @@ -207,7 +210,9 @@ describe("DepositVaultV1", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = await buildPlanTx(deposit, [requirementSignature]); + const tx = (await deposit.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, @@ -260,9 +265,13 @@ describe("DepositVaultV1", () => { vaultData, }); - const requirements = await getPlanRequests(deposit, { - useSimplePermit: false, - }); + const requirements = ( + await deposit.prepare({ + requestOptions: { + useSimplePermit: false, + }, + }) + ).requirements; expect(requirements.length).toBe(2); @@ -302,7 +311,9 @@ describe("DepositVaultV1", () => { await client.sendTransaction(approvalPermit2); - const tx = await buildPlanTx(deposit, [requirementSignature]); + const tx = (await deposit.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, @@ -361,7 +372,7 @@ describe("DepositVaultV1", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(2); @@ -400,7 +411,8 @@ describe("DepositVaultV1", () => { ); await client.sendTransaction( - await buildPlanTx(deposit, [requirementSignature]), + (await deposit.prepare()).build([requirementSignature]) + .primaryTransaction, ); }, }); @@ -448,13 +460,13 @@ describe("DepositVaultV1", () => { amount: amount, vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; const approveTx = requirements[0]; if (!isRequirementApproval(approveTx)) { throw new Error("Approve transaction not found"); } await client.sendTransaction(approveTx); - const depositTx = await buildPlanTx(deposit); + const depositTx = (await deposit.prepare()).build().primaryTransaction; await client.sendTransaction(depositTx); const withdraw = vaultV1.withdraw({ @@ -462,7 +474,8 @@ describe("DepositVaultV1", () => { amount: amount, }); - const withdrawTx = await buildPlanTx(withdraw); + const withdrawTx = (await withdraw.prepare()).build() + .primaryTransaction; await client.sendTransaction(withdrawTx); }, diff --git a/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts b/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts index a9a4d6ec3..7c3314fac 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts @@ -10,7 +10,6 @@ import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("MigrateToV2 VaultV1", () => { test("should migrate full USDC position from V1 to V2", async ({ @@ -50,7 +49,7 @@ describe("MigrateToV2 VaultV1", () => { shares, }); - const requirements = await getPlanRequests(migrate); + const requirements = (await migrate.prepare()).requirements; expect(requirements.length).toBe(1); @@ -64,7 +63,7 @@ describe("MigrateToV2 VaultV1", () => { await client.sendTransaction(approveTx); - const tx = await buildPlanTx(migrate); + const tx = (await migrate.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, }); @@ -121,7 +120,7 @@ describe("MigrateToV2 VaultV1", () => { shares, }); - const requirements = await getPlanRequests(migrate); + const requirements = (await migrate.prepare()).requirements; if (!isRequirementSignature(requirements[0])) { throw new Error("Requirement is not a signature requirement"); @@ -143,7 +142,9 @@ describe("MigrateToV2 VaultV1", () => { BigInt(Math.floor(Date.now() / 1000)), ); - const tx = await buildPlanTx(migrate, [requirementSignature]); + const tx = (await migrate.prepare()).build([ + requirementSignature, + ]).primaryTransaction; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts b/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts index ff83c63f0..143a65bb8 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts @@ -8,7 +8,6 @@ import { import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("Redeem VaultV1", () => { test("should redeem 1K shares in vaultV1", async ({ client }) => { @@ -37,7 +36,7 @@ describe("Redeem VaultV1", () => { userAddress: client.account.address, shares, }); - const tx = await buildPlanTx(redeem); + const tx = (await redeem.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, @@ -86,13 +85,15 @@ describe("Redeem VaultV1", () => { amount: depositAmount, vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; const approveTx = requirements[0]; if (!isRequirementApproval(approveTx)) { throw new Error("Approve transaction not found"); } await client.sendTransaction(approveTx); - await client.sendTransaction(await buildPlanTx(deposit)); + await client.sendTransaction( + (await deposit.prepare()).build().primaryTransaction, + ); const shares = await client.balanceOf({ erc20: SteakhouseUsdcVaultV1.address, @@ -102,7 +103,9 @@ describe("Redeem VaultV1", () => { userAddress: client.account.address, shares, }); - await client.sendTransaction(await buildPlanTx(redeem)); + await client.sendTransaction( + (await redeem.prepare()).build().primaryTransaction, + ); }, }); @@ -146,7 +149,9 @@ describe("Redeem VaultV1", () => { userAddress: client.account.address, shares: redeemShares, }); - await client.sendTransaction(await buildPlanTx(redeem)); + await client.sendTransaction( + (await redeem.prepare()).build().primaryTransaction, + ); }, }); diff --git a/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts b/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts index bab58e2a3..5c8ff6a88 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts @@ -5,7 +5,6 @@ import { morphoViemExtension } from "../../../src/index.js"; import { SteakhouseUsdcVaultV1 } from "../../fixtures/vaultV1.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("Withdraw VaultV1", () => { test("should withdraw 1K assets in vaultV1", async ({ client }) => { @@ -35,7 +34,7 @@ describe("Withdraw VaultV1", () => { userAddress: client.account.address, amount: assets, }); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, @@ -82,13 +81,17 @@ describe("Withdraw VaultV1", () => { userAddress: client.account.address, amount: firstWithdraw, }); - await client.sendTransaction(await buildPlanTx(withdraw1)); + await client.sendTransaction( + (await withdraw1.prepare()).build().primaryTransaction, + ); const withdraw2 = vaultV1.withdraw({ userAddress: client.account.address, amount: secondWithdraw, }); - await client.sendTransaction(await buildPlanTx(withdraw2)); + await client.sendTransaction( + (await withdraw2.prepare()).build().primaryTransaction, + ); }, }); diff --git a/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts b/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts index cd4c0cbab..5b41bf5c8 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts @@ -8,7 +8,6 @@ import { import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx, getPlanRequests } from "../../transactionPlanUtils.js"; describe("DepositVaultV2", () => { test("should deposit 1K USDC in vaultV2", async ({ client }) => { @@ -37,8 +36,8 @@ describe("DepositVaultV2", () => { vaultData, }); - const tx = await buildPlanTx(deposit); - const requirements = await getPlanRequests(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; + const requirements = (await deposit.prepare()).requirements; const approveTx = requirements[0]; if (!approveTx) { diff --git a/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts b/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts index e7098a86d..21581ff2f 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts @@ -7,7 +7,6 @@ import { morphoViemExtension, vaultV2ForceRedeem } from "../../../src/index.js"; import { ReEcosystemUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx } from "../../transactionPlanUtils.js"; // Accrue VaultV2 to the current block timestamp so `toShares` matches the // post-accrue math the contract will run at redeem block (ā‰ˆ same block here). @@ -73,7 +72,7 @@ describe("ForceRedeem VaultV2", () => { redeem: { shares: redeemShares }, userAddress: client.account.address, }); - const tx_1 = await buildPlanTx(forceRedeem); + const tx_1 = (await forceRedeem.prepare()).build().primaryTransaction; const tx_2 = vaultV2ForceRedeem({ vault: { address: ReEcosystemUsdcVaultV2.address }, @@ -135,7 +134,7 @@ describe("ForceRedeem VaultV2", () => { redeem: { shares: redeemShares }, userAddress: client.account.address, }); - const tx = await buildPlanTx(forceRedeem); + const tx = (await forceRedeem.prepare()).build().primaryTransaction; const { vaults: { @@ -200,7 +199,7 @@ describe("ForceRedeem VaultV2", () => { redeem: { shares: redeemShares }, userAddress: client.account.address, }); - const tx = await buildPlanTx(forceRedeem); + const tx = (await forceRedeem.prepare()).build().primaryTransaction; const { vaults: { diff --git a/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts b/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts index 2cb80e319..64a1fc288 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts @@ -9,7 +9,6 @@ import { import { ReEcosystemUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("ForceWithdraw VaultV2", () => { // Blue adapter addresses @@ -60,7 +59,7 @@ describe("ForceWithdraw VaultV2", () => { withdraw: { amount: assetsWithdraw }, userAddress: client.account.address, }); - const tx_1 = await buildPlanTx(forceWithdraw); + const tx_1 = (await forceWithdraw.prepare()).build().primaryTransaction; const tx_2 = vaultV2ForceWithdraw({ vault: { address: ReEcosystemUsdcVaultV2.address }, @@ -125,7 +124,7 @@ describe("ForceWithdraw VaultV2", () => { withdraw: { amount: assets }, userAddress: client.account.address, }); - const tx = await buildPlanTx(forceWithdraw); + const tx = (await forceWithdraw.prepare()).build().primaryTransaction; const { vaults: { @@ -191,7 +190,7 @@ describe("ForceWithdraw VaultV2", () => { withdraw: { amount: withdrawAssets }, userAddress: client.account.address, }); - const tx = await buildPlanTx(forceWithdraw); + const tx = (await forceWithdraw.prepare()).build().primaryTransaction; const { vaults: { diff --git a/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts b/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts index f81f0003a..aedf717b4 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts @@ -5,7 +5,6 @@ import { morphoViemExtension } from "../../../src/index.js"; import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("Redeem VaultV2", () => { test("should redeem 1K USDC in vaultV2", async ({ client }) => { @@ -31,7 +30,7 @@ describe("Redeem VaultV2", () => { userAddress: client.account.address, shares, }); - const tx = await buildPlanTx(redeem); + const tx = (await redeem.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts b/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts index 173c8de62..be8877457 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts @@ -5,7 +5,6 @@ import { morphoViemExtension } from "../../../src/index.js"; import { KeyrockUsdcVaultV2 } from "../../fixtures/vaultV2.js"; import { testInvariants } from "../../helpers/invariants.js"; import { test } from "../../setup.js"; -import { buildPlanTx } from "../../transactionPlanUtils.js"; describe("Withdraw VaultV2", () => { test("should withdraw 1K assets in vaultV2", async ({ client }) => { @@ -32,7 +31,7 @@ describe("Withdraw VaultV2", () => { userAddress: client.account.address, amount: assets, }); - const tx = await buildPlanTx(withdraw); + const tx = (await withdraw.prepare()).build().primaryTransaction; await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/wrap.test.ts b/packages/morpho-sdk/test/actions/wrap.test.ts index 010a3288a..0f9dd31ce 100644 --- a/packages/morpho-sdk/test/actions/wrap.test.ts +++ b/packages/morpho-sdk/test/actions/wrap.test.ts @@ -19,7 +19,6 @@ import { import { KeyrockUsdcVaultV2, KpkWETHVaultV2 } from "../fixtures/vaultV2.js"; import { testInvariants } from "../helpers/invariants.js"; import { test } from "../setup.js"; -import { buildPlanTx, getPlanRequests } from "../transactionPlanUtils.js"; describe("WrapNative - VaultV1", () => { test("should deposit native ETH only in WETH vaultV1", async ({ client }) => { @@ -50,10 +49,10 @@ describe("WrapNative - VaultV1", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = await buildPlanTx(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(0n); @@ -119,10 +118,10 @@ describe("WrapNative - VaultV1", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = await buildPlanTx(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -181,7 +180,7 @@ describe("WrapNative - VaultV1", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(2); @@ -204,7 +203,9 @@ describe("WrapNative - VaultV1", () => { expect(isHex(requirementSignature.args.signature)).toBe(true); expect(requirementSignature.args.signature.length).toBe(132); - const tx = await buildPlanTx(deposit, [requirementSignature]); + const tx = (await deposit.prepare()).build([ + requirementSignature, + ]).primaryTransaction; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -305,10 +306,10 @@ describe("WrapNative - VaultV2", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = await buildPlanTx(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(0n); @@ -374,10 +375,10 @@ describe("WrapNative - VaultV2", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = await buildPlanTx(deposit); + const tx = (await deposit.prepare()).build().primaryTransaction; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -436,7 +437,7 @@ describe("WrapNative - VaultV2", () => { vaultData, }); - const requirements = await getPlanRequests(deposit); + const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(2); @@ -459,7 +460,9 @@ describe("WrapNative - VaultV2", () => { expect(isHex(requirementSignature.args.signature)).toBe(true); expect(requirementSignature.args.signature.length).toBe(132); - const tx = await buildPlanTx(deposit, [requirementSignature]); + const tx = (await deposit.prepare()).build([ + requirementSignature, + ]).primaryTransaction; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); diff --git a/packages/morpho-sdk/test/transactionPlanUtils.ts b/packages/morpho-sdk/test/transactionPlanUtils.ts deleted file mode 100644 index 272042137..000000000 --- a/packages/morpho-sdk/test/transactionPlanUtils.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { - TransactionPlan, - TransactionPlanHandler, - TransactionPlanRequest, -} from "../src/transactionPlan/index.js"; -import type { - RequirementSignature, - Transaction, - TransactionAction, -} from "../src/types/index.js"; - -function getTestHandler< - TAction extends TransactionAction, - TOptions, - TRequest extends TransactionPlanRequest, - TSignatures, ->( - plan: TransactionPlan, -): TransactionPlanHandler { - return ( - plan as unknown as { - readonly handler: TransactionPlanHandler< - TAction, - TOptions, - TRequest, - TSignatures - >; - } - ).handler; -} - -export async function buildPlanTx< - TAction extends TransactionAction, - TOptions, - TRequest extends TransactionPlanRequest, - TSignatures, ->( - plan: TransactionPlan, - signatures?: TSignatures, -): Promise>> { - return getTestHandler(plan).buildPrimaryCall(signatures); -} - -export async function getPlanRequests< - TAction extends TransactionAction, - TOptions, - TRequest extends TransactionPlanRequest, - TSignatures, ->( - plan: TransactionPlan, - requestOptions?: TOptions, -): Promise; -export async function getPlanRequests( - plan: { readonly prepare: unknown }, - requestOptions?: unknown, -): Promise; -export async function getPlanRequests( - plan: unknown, - requestOptions?: unknown, -): Promise { - const getRequirementRequests = getTestHandler( - plan as TransactionPlan< - TransactionAction, - unknown, - TransactionPlanRequest, - readonly RequirementSignature[] - >, - ).getRequirementRequests; - return getRequirementRequests ? getRequirementRequests(requestOptions) : []; -} - -export async function buildPlanCalls< - TAction extends TransactionAction, - TOptions, - TRequest extends TransactionPlanRequest, - TSignatures, ->( - plan: TransactionPlan, - signatures?: TSignatures, -) { - return (await plan.prepare()).build(signatures).callRequests; -} - -export function asSignatureArray( - signature: RequirementSignature, -): readonly RequirementSignature[] { - return [signature]; -} diff --git a/packages/wdk-protocol-lending-morpho-evm/src/errors.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/errors.test.ts deleted file mode 100644 index 6188e0c68..000000000 --- a/packages/wdk-protocol-lending-morpho-evm/src/errors.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { MissingTransactionPlanCallError } from "./errors.js"; - -describe("MissingTransactionPlanCallError", () => { - test("default", () => { - const error = new MissingTransactionPlanCallError(); - - expect(error).toBeInstanceOf(Error); - expect(error.name).toBe("MissingTransactionPlanCallError"); - }); -}); diff --git a/packages/wdk-protocol-lending-morpho-evm/src/errors.ts b/packages/wdk-protocol-lending-morpho-evm/src/errors.ts deleted file mode 100644 index 8e337e0ba..000000000 --- a/packages/wdk-protocol-lending-morpho-evm/src/errors.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Thrown when a prepared Morpho transaction plan does not produce its primary call. - * - * @example - * ```ts - * import { MissingTransactionPlanCallError } from "@morpho-org/wdk-protocol-lending-morpho-evm"; - * - * if (error instanceof MissingTransactionPlanCallError) { - * // Recreate the plan from current protocol state before retrying. - * } - * ``` - */ -export class MissingTransactionPlanCallError extends Error { - constructor() { - super( - "Transaction plan produced no executable primary call. Recreate the plan before retrying.", - ); - this.name = "MissingTransactionPlanCallError"; - } -} diff --git a/packages/wdk-protocol-lending-morpho-evm/src/index.ts b/packages/wdk-protocol-lending-morpho-evm/src/index.ts index 8911eb7de..9d82ccb75 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/index.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/index.ts @@ -14,7 +14,6 @@ export type { WithdrawOptions, WithdrawResult, } from "@tetherto/wdk-wallet/protocols"; -export { MissingTransactionPlanCallError } from "./errors.js"; export { type Market, type MarketPresetKey, diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index 7b9f38400..53d3cd5dd 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -75,16 +75,26 @@ function mockTransactionPlan( const getRequirementRequests = vi.fn( async (_requestOptions?: unknown) => requirements, ); - const buildPrimaryCall = vi.fn( + const buildPrimaryTransaction = vi.fn( (_signatures?: readonly RequirementSignature[]) => tx, ); const prepare = vi.fn(async (options?: { requestOptions?: unknown }) => { const resolvedRequirements = await getRequirementRequests( options?.requestOptions, ); + const primaryStep = { + id: "primary", + kind: "transaction", + phase: "primary", + transaction: tx, + } as const; return { + requirements: resolvedRequirements, + primaryStep, + primaryTransaction: tx, signatureRequests: [], - callRequests: [{ id: "primary", kind: "call", phase: "primary", tx }], + transactionSteps: [primaryStep], + calls: [{ to: tx.to, value: tx.value, data: tx.data }], steps: [ ...resolvedRequirements.map((requirement, index) => "sign" in requirement @@ -95,27 +105,38 @@ function mockTransactionPlan( } : { id: `request-${index}`, - kind: "call", + kind: "transaction", phase: "preparation", - tx: requirement, + transaction: requirement, }, ), - { id: "primary", kind: "call", phase: "primary", tx }, + primaryStep, ], - build: (signatures?: readonly RequirementSignature[]) => ({ - signatureRequests: [], - callRequests: [ - { - id: "primary", - kind: "call", - phase: "primary", - tx: buildPrimaryCall(signatures), - }, - ], - }), + build: (signatures?: readonly RequirementSignature[]) => { + const primaryTransaction = buildPrimaryTransaction(signatures); + const builtPrimaryStep = { + id: "primary", + kind: "transaction", + phase: "primary", + transaction: primaryTransaction, + } as const; + return { + primaryStep: builtPrimaryStep, + primaryTransaction, + signatureRequests: [], + transactionSteps: [builtPrimaryStep], + calls: [ + { + to: primaryTransaction.to, + value: primaryTransaction.value, + data: primaryTransaction.data, + }, + ], + }; + }, }; }); - return { prepare, getRequirementRequests, buildPrimaryCall }; + return { prepare, getRequirementRequests, buildPrimaryTransaction }; } const supplyAction = mockTransactionPlan(SUPPLY_TX, [ @@ -571,7 +592,9 @@ describe.sequential("MorphoProtocolEvm", () => { await protocol.borrow({ token: TOKEN, amount: 100_000n }); - expect(borrowAction.buildPrimaryCall).toHaveBeenCalledWith(undefined); + expect(borrowAction.buildPrimaryTransaction).toHaveBeenCalledWith( + undefined, + ); }); test("should fold a signed authorization into the bundle when provided", async () => { @@ -588,7 +611,7 @@ describe.sequential("MorphoProtocolEvm", () => { requirementSignature, }); - expect(borrowAction.buildPrimaryCall).toHaveBeenCalledWith([ + expect(borrowAction.buildPrimaryTransaction).toHaveBeenCalledWith([ requirementSignature, ]); }); diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 6fb7e8691..325661d54 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -53,7 +53,6 @@ import { zeroAddress, } from "viem"; import { arbitrum, base, mainnet, optimism, polygon } from "viem/chains"; -import { MissingTransactionPlanCallError } from "./errors.js"; import { type MarketPresetKey, MORPHO_MARKET_PRESETS, @@ -410,13 +409,7 @@ async function getTransactionPlanRequests< const prepared = await plan.prepare( requestOptions === undefined ? undefined : { requestOptions }, ); - const requests: TRequest[] = []; - for (const request of prepared.steps) { - if (request.kind === "call" && request.phase === "primary") continue; - requests.push(request.kind === "signature" ? request.request : request.tx); - } - - return requests; + return [...prepared.requirements]; } async function getTransactionPlanTx< @@ -429,11 +422,7 @@ async function getTransactionPlanTx< signatures?: TSignatures, ): Promise { const executable = (await plan.prepare()).build(signatures); - const primaryCall = executable.callRequests.at(-1); - if (primaryCall == null) { - throw new MissingTransactionPlanCallError(); - } - return toWdkTransaction(primaryCall.tx); + return toWdkTransaction(executable.primaryTransaction); } /** From 131256d153a1076840a8f7d4132000ca81ef46d1 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 17 Jul 2026 10:21:18 +0200 Subject: [PATCH 07/12] refactor(morpho-sdk): shorten transaction plan tx names --- ...-06-03-midnight-action-output-interface.md | 16 +- packages/morpho-sdk/README.md | 6 +- packages/morpho-sdk/examples/example.ts | 6 +- .../src/client/morphoViemExtension.ts | 2 +- .../src/entities/blue/blue.refinance.test.ts | 10 +- .../morpho-sdk/src/entities/blue/blue.test.ts | 16 +- packages/morpho-sdk/src/entities/blue/blue.ts | 18 +- .../src/entities/midnight/midnight.test.ts | 30 +-- .../src/entities/midnight/midnight.ts | 20 +- .../src/entities/vaultV1/vaultV1.test.ts | 14 +- .../src/entities/vaultV1/vaultV1.ts | 8 +- .../src/entities/vaultV2/vaultV2.test.ts | 4 +- .../src/entities/vaultV2/vaultV2.ts | 10 +- .../src/helpers/computeReallocations.ts | 2 +- .../transactionPlan/TransactionPlan.test.ts | 78 ++++---- .../src/transactionPlan/TransactionPlan.ts | 179 +++++++++--------- .../morpho-sdk/src/transactionPlan/types.ts | 62 +++--- packages/morpho-sdk/src/types/action.ts | 12 +- .../test/actions/blue/borrow.test.ts | 6 +- .../test/actions/blue/reallocations.test.ts | 16 +- .../test/actions/blue/refinance.test.ts | 14 +- .../test/actions/blue/repay.test.ts | 14 +- .../blue/repayWithdrawCollateral.test.ts | 12 +- .../test/actions/blue/supply.test.ts | 10 +- .../actions/blue/supplyCollateral.test.ts | 16 +- .../blue/supplyCollateralBorrow.test.ts | 12 +- .../test/actions/blue/withdraw.test.ts | 12 +- .../actions/blue/withdrawCollateral.test.ts | 4 +- .../morpho-sdk/test/actions/metadata.test.ts | 6 +- .../actions/midnight/requirements.test.ts | 24 +-- .../actions/requirements/approval.test.ts | 4 +- .../test/actions/requirements/permit.test.ts | 2 +- .../test/actions/requirements/permit2.test.ts | 11 +- .../test/actions/vaultV1/deposit.test.ts | 16 +- .../test/actions/vaultV1/migrateToV2.test.ts | 4 +- .../test/actions/vaultV1/redeem.test.ts | 8 +- .../test/actions/vaultV1/withdraw.test.ts | 6 +- .../test/actions/vaultV2/deposit.test.ts | 2 +- .../test/actions/vaultV2/forceRedeem.test.ts | 6 +- .../actions/vaultV2/forceWithdraw.test.ts | 6 +- .../test/actions/vaultV2/redeem.test.ts | 2 +- .../test/actions/vaultV2/withdraw.test.ts | 2 +- packages/morpho-sdk/test/actions/wrap.test.ts | 12 +- .../src/morpho-protocol-evm.test.ts | 38 ++-- .../src/morpho-protocol-evm.ts | 2 +- 45 files changed, 358 insertions(+), 402 deletions(-) diff --git a/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md b/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md index 7cd8b2413..934f49a70 100644 --- a/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md +++ b/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md @@ -34,7 +34,7 @@ The transaction-plan contract is shared by Midnight, Blue, MarketV1, and vault e - `TransactionPlan.prepare(...)` resolves only the prerequisites for the already selected action. - `PreparedTransactionPlan.signatureRequests` exposes signable requirements without signing them. -- `PreparedTransactionPlan.transactionSteps` exposes ordered prerequisite transactions and, when +- `PreparedTransactionPlan.txSteps` exposes ordered prerequisite transactions and, when previewable, the primary step. Its `.calls` getter converts those transactions to viem-compatible call parameters for wallet batching. - `PreparedTransactionPlan.build(...)` consumes collected signatures and returns an @@ -115,7 +115,7 @@ const plan: TransactionPlan< > = await midnight.makeLend(params); const prepared = await plan.prepare(); -// The app presents prepared.signatureRequests and prepared.transactionSteps. +// The app presents prepared.signatureRequests and prepared.txSteps. const executable = prepared.build(collectedSignatures); ``` @@ -123,7 +123,7 @@ Semantics: 1. `prepare()` resolves the currently required prerequisite descriptors and returns semantic request metadata. 2. Already-satisfied approvals or authorizations are omitted. -3. `steps` and `transactionSteps` preserve relative execution order; the primary step is last when present. +3. `steps` and `txSteps` preserve relative execution order; the primary step is last when present. 4. Signature descriptors are exposed through `signatureRequests`; the SDK never prompts the wallet on its own. 5. A prerequisite transaction step can be an approval, authorization, contract-wallet ratify-root, or mandatory prelude transaction. 6. `build(signatures)` validates and consumes the collected signature values before encoding the primary transaction. @@ -374,16 +374,16 @@ This is action-encoding metadata, not UI state. The first implementation only en Add a named call-requirement union. Existing raw `Transaction<...>` requirement values stay valid. ```ts -export type TransactionRequirementAction = +export type TxRequirementAction = | ERC20ApprovalAction | MorphoAuthorizationAction | MidnightAuthorizationAction | SetterRatifierRatifyRootAction | MidnightSupplyCollateralAction; -export type TransactionRequirement = Readonly>; +export type TxRequirement = Readonly>; -export type ActionRequirement = TransactionRequirement | SignatureRequirement; +export type ActionRequirement = TxRequirement | SignatureRequirement; ``` `MidnightSupplyCollateralAction` is included because it can be a mandatory prelude transaction for a currently implemented app flow: @@ -937,7 +937,7 @@ Add ERC2612 and Permit2 SignatureTransfer support to the first action-flow imple - **Wallet-decodable offer-tree signing.** The SDK must build the offer tree locally from the SDK input and validate the router response before exposing `midnightOfferRootSignature`. The wallet signs EIP-712 `OfferTree` typed data whose leaves are visible to the user, and the SDK verifies that the signed tree hashes to the root used in ratifier data. - **No signing inside actions.** `Requirement.sign(...)` is the only signing boundary and takes a `WalletClient` from the integrator. -- **No hidden prelude txs.** Mandatory prelude transactions are visible as typed preparation-phase `transactionSteps` on the prepared plan. +- **No hidden prelude txs.** Mandatory prelude transactions are visible as typed preparation-phase `txSteps` on the prepared plan. - **Authorization target is explicit.** `MidnightAuthorizationAction.args.authorized` is either `MidnightBundles`, `EcrecoverRatifier`, or `SetterRatifier`; never inferred by a consumer. - **Approval target is explicit.** Midnight approval helper callers pass `spender`; no default to `GeneralAdapter1`. - **Bundle token-pull policy is explicit.** Bundle flows use `MidnightBundles` as spender and never consume or reset the core `Midnight` allowance reserved by maker offers. @@ -947,7 +947,7 @@ Add ERC2612 and Permit2 SignatureTransfer support to the first action-flow imple ## Future considerations - If a future Midnight flow needs a real wait condition (`before` / `after` equivalent), add a small `wait` requirement kind at that time. Do not preemptively port app `ActionFlow`. -- If consumers strongly reject mandatory preparation-phase `transactionSteps`, revisit a dedicated sequence-building API with evidence from integration feedback. +- If consumers strongly reject mandatory preparation-phase `txSteps`, revisit a dedicated sequence-building API with evidence from integration feedback. ## References diff --git a/packages/morpho-sdk/README.md b/packages/morpho-sdk/README.md index 14d68394c..72ae427dd 100644 --- a/packages/morpho-sdk/README.md +++ b/packages/morpho-sdk/README.md @@ -53,7 +53,7 @@ Every entity action returns a lazy `TransactionPlan`: - `plan.prepare()` resolves the prerequisite call and signature requests for the chosen action. - `prepared.signatureRequests` contains permits, Morpho authorizations, or Midnight offer-root signatures that the wallet must sign. -- `prepared.transactionSteps` contains prerequisite transaction steps and, when it can be previewed +- `prepared.txSteps` contains prerequisite transaction steps and, when it can be previewed without a signature, the primary step last. - `prepared.build(signatures)` returns the executable transaction steps, ordered with the primary step last. @@ -83,8 +83,8 @@ const signatures = await Promise.all( const executable = prepared.build(signatures); // Send every transaction step in order; prerequisite steps come before the primary action. -for (const step of executable.transactionSteps) { - await walletClient.sendTransaction(step.transaction); +for (const step of executable.txSteps) { + await walletClient.sendTransaction(step.tx); } // Or convert the executable transactions for an EIP-5792 wallet batch. diff --git a/packages/morpho-sdk/examples/example.ts b/packages/morpho-sdk/examples/example.ts index bee045249..32ee5761c 100644 --- a/packages/morpho-sdk/examples/example.ts +++ b/packages/morpho-sdk/examples/example.ts @@ -54,7 +54,7 @@ async function main() { vaultData, }); const preparedDeposit = await deposit.prepare(); - const depositTx = preparedDeposit.primaryTransaction; + const depositTx = preparedDeposit.primaryTx; if (depositTx == null) throw new Error("Deposit transaction is unavailable"); console.log("Deposit transaction:", { @@ -78,7 +78,7 @@ async function main() { userAddress: USER_ADDRESS, }); const preparedWithdraw = await withdraw.prepare(); - const withdrawTx = preparedWithdraw.primaryTransaction; + const withdrawTx = preparedWithdraw.primaryTx; if (withdrawTx == null) throw new Error("Withdraw transaction is unavailable"); @@ -96,7 +96,7 @@ async function main() { userAddress: USER_ADDRESS, }); const preparedRedeem = await redeem.prepare(); - const redeemTx = preparedRedeem.primaryTransaction; + const redeemTx = preparedRedeem.primaryTx; if (redeemTx == null) throw new Error("Redeem transaction is unavailable"); console.log("Redeem transaction:", { diff --git a/packages/morpho-sdk/src/client/morphoViemExtension.ts b/packages/morpho-sdk/src/client/morphoViemExtension.ts index 6717fe578..d2bf5a0e4 100644 --- a/packages/morpho-sdk/src/client/morphoViemExtension.ts +++ b/packages/morpho-sdk/src/client/morphoViemExtension.ts @@ -112,7 +112,7 @@ function createMorphoNamespace( * for (const request of prepared.signatureRequests) { * signatures.push(await request.sign(client, user)); * } - * const tx = prepared.build(signatures).primaryTransaction; + * const tx = prepared.build(signatures).primaryTx; * ``` */ export function morphoViemExtension(_options?: { diff --git a/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts b/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts index a44e8163b..33e898b5d 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.refinance.test.ts @@ -363,7 +363,7 @@ describe("MorphoBlue.refinance", () => { borrowShares: parseUnits("100", 12), }); - const tx = (await refi.prepare()).build().primaryTransaction; + const tx = (await refi.prepare()).build().primaryTx; expect(tx.action.type).toBe("blueRefinance"); expect(tx.action.args.sourceMarket).toBe(sourceParams.id); expect(tx.action.args.targetMarket).toBe(targetParams.id); @@ -394,7 +394,7 @@ describe("MorphoBlue.refinance", () => { borrowAssets: parseUnits("50", 6), }); - const tx = (await refi.prepare()).build().primaryTransaction; + const tx = (await refi.prepare()).build().primaryTx; expect(tx.action.args.borrowAssets).toBe(parseUnits("50", 6)); expect(tx.action.args.borrowShares).toBe(0n); }); @@ -505,7 +505,7 @@ describe("MorphoBlue.refinance", () => { borrowShares: parseUnits("100", 12), slippageTolerance, }); - const tx = (await refi.prepare()).build().primaryTransaction; + const tx = (await refi.prepare()).build().primaryTx; // Recompute the entity's intermediate values (accrual deltas cancel for this fixture). const projectedBorrowAssets = sourceMarket.toBorrowAssets( @@ -549,7 +549,7 @@ describe("MorphoBlue.refinance", () => { collateralAmount: parseUnits("1", 18), }); - const tx = (await refi.prepare()).build().primaryTransaction; + const tx = (await refi.prepare()).build().primaryTx; expect(tx.action.args.borrowAssets).toBe(0n); expect(tx.action.args.borrowShares).toBe(0n); }); @@ -594,7 +594,7 @@ describe("MorphoBlue.refinance", () => { targetReallocations, }); - const tx = (await refi.prepare()).build().primaryTransaction; + const tx = (await refi.prepare()).build().primaryTx; expect(tx.value).toBe(fee); expect(tx.action.args.reallocationFee).toBe(fee); }); diff --git a/packages/morpho-sdk/src/entities/blue/blue.test.ts b/packages/morpho-sdk/src/entities/blue/blue.test.ts index c82a6fd10..410523e63 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.test.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.test.ts @@ -98,7 +98,7 @@ describe("MorphoBlue builder = signer freedom", () => { amount: parseUnits("1", 18), }); - const tx = (await supplyCollateral.prepare()).build().primaryTransaction; + const tx = (await supplyCollateral.prepare()).build().primaryTx; expect(tx.action.args.onBehalf).toBe(OTHER_USER); }); @@ -117,7 +117,7 @@ describe("MorphoBlue builder = signer freedom", () => { amount: parseUnits("1", 18), }); - const tx = (await supplyCollateral.prepare()).build().primaryTransaction; + const tx = (await supplyCollateral.prepare()).build().primaryTx; expect(tx.action.args.onBehalf).toBe(OTHER_USER); }); }); @@ -378,7 +378,7 @@ describe("MorphoBlue validation", () => { positionData: makeWethPosition(), }) .prepare() - ).build().primaryTransaction; + ).build().primaryTx; expect(tx.action.args.assets).toBe(amount + nativeAmount); expect(tx.action.args.nativeAmount).toBe(nativeAmount); @@ -430,7 +430,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = (await repay.prepare()).build().primaryTransaction; + const tx = (await repay.prepare()).build().primaryTx; expect(tx.action.args.shares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -467,7 +467,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = (await repay.prepare()).build().primaryTransaction; + const tx = (await repay.prepare()).build().primaryTx; expect(tx.action.args.shares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -522,7 +522,7 @@ describe("MorphoBlue validation", () => { positionData: makeWethPosition(), }) .prepare() - ).build().primaryTransaction; + ).build().primaryTx; expect(tx.action.args.repayAssets).toBe(amount + nativeAmount); expect(tx.action.args.nativeAmount).toBe(nativeAmount); @@ -576,7 +576,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = (await action.prepare()).build().primaryTransaction; + const tx = (await action.prepare()).build().primaryTx; expect(tx.action.args.repayShares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); @@ -612,7 +612,7 @@ describe("MorphoBlue validation", () => { positionData, }); - const tx = (await action.prepare()).build().primaryTransaction; + const tx = (await action.prepare()).build().primaryTx; expect(tx.action.args.repayShares).toBe(positionData.borrowShares); expect(tx.action.args.nativeAmount).toBe(nativeAmount); expect(tx.value).toBe(nativeAmount); diff --git a/packages/morpho-sdk/src/entities/blue/blue.ts b/packages/morpho-sdk/src/entities/blue/blue.ts index 4a56f668d..0f0164730 100644 --- a/packages/morpho-sdk/src/entities/blue/blue.ts +++ b/packages/morpho-sdk/src/entities/blue/blue.ts @@ -541,7 +541,7 @@ export class MorphoBlue implements BlueActions { args: { amount, from: userAddress }, }), - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -649,7 +649,7 @@ export class MorphoBlue implements BlueActions { return authTx ? [authTx] : []; }, - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { authorization } = selectRequirementSignatures(signatures, { authorization: true, }); @@ -704,7 +704,7 @@ export class MorphoBlue implements BlueActions { args: { amount, from: userAddress }, }), - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -784,7 +784,7 @@ export class MorphoBlue implements BlueActions { return authTx ? [authTx] : []; }, - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { authorization } = selectRequirementSignatures(signatures, { authorization: true, }); @@ -929,7 +929,7 @@ export class MorphoBlue implements BlueActions { }); }, - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -1008,7 +1008,7 @@ export class MorphoBlue implements BlueActions { }); return new TransactionPlan({ - buildPrimaryTransaction: () => + buildPrimaryTx: () => blueWithdrawCollateral({ market: { chainId: this.chainId, @@ -1184,7 +1184,7 @@ export class MorphoBlue implements BlueActions { return [...erc20Requirements, ...(authTx ? [authTx] : [])]; }, - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { permit, authorization } = selectRequirementSignatures( signatures, { permit: true, authorization: true }, @@ -1317,7 +1317,7 @@ export class MorphoBlue implements BlueActions { return [...erc20Requirements, ...(authTx ? [authTx] : [])]; }, - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { permit, authorization } = selectRequirementSignatures( signatures, { permit: true, authorization: true }, @@ -1552,7 +1552,7 @@ export class MorphoBlue implements BlueActions { return authTx ? [authTx] : []; }, - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { authorization } = selectRequirementSignatures(signatures, { authorization: true, }); diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts index 4685ccb0d..c149a39f0 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts @@ -113,7 +113,7 @@ const buildMakeOffersOutputTx: BuildMakeOffersOutputTx = async (params) => params.signedPayloads ?? new Map([["0x1234", "0x1234" as Hex]]), }) .prepare() - ).build(params.signatures).primaryTransaction; + ).build(params.signatures).primaryTx; const offersData = ( buy = true, @@ -508,7 +508,7 @@ describe("MorphoMidnight", () => { deadline: maxUint256, }); expect(output).toBeInstanceOf(TransactionPlan); - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect(tx.action.args).toEqual({ market: midnightMarketId, @@ -649,7 +649,7 @@ describe("MorphoMidnight", () => { }); const requirements = (await output.prepare()).requirements; expect(output).toBeInstanceOf(TransactionPlan); - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect(tx.action.args.loanAssets).toBe(1_000n); expect( @@ -751,7 +751,7 @@ describe("MorphoMidnight", () => { }); const requirements = (await output.prepare()).requirements; expect(output).toBeInstanceOf(TransactionPlan); - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect(tx.action.args).toMatchObject({ collateralAssets: 2_000n, @@ -833,7 +833,7 @@ describe("MorphoMidnight", () => { }); const requirements = (await output.prepare()).requirements; expect(output).toBeInstanceOf(TransactionPlan); - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect(tx.action.args.assets).toBe(2_000n); expect(requirements[0]?.action).toMatchObject({ @@ -892,7 +892,7 @@ describe("MorphoMidnight", () => { positionData: positionData(market, { credit: 250n, pendingFee: 50n }), accountAddress: midnightAddresses.taker, }); - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect(tx.action.args).toEqual({ market: midnightMarketId, @@ -909,7 +909,7 @@ describe("MorphoMidnight", () => { accountAddress: midnightAddresses.taker, units: 225n, }); - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect(tx.action.args).toEqual({ market: midnightMarketId, @@ -973,7 +973,7 @@ describe("MorphoMidnight", () => { receiver: midnightAddresses.maker, }); const requirements = (await output.prepare()).requirements; - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect(requirements).toEqual([]); expect(tx.action.args.receiver).toBe(midnightAddresses.maker); @@ -1377,7 +1377,7 @@ describe("MorphoMidnight", () => { }); expect(output).toBeInstanceOf(TransactionPlan); const signature = await signOfferRootRequirement(output); - const tx = (await output.prepare()).build(signature).primaryTransaction; + const tx = (await output.prepare()).build(signature).primaryTx; expect(output.groups).toEqual(data.groups); expect(output.root).toBe(data.tree.root); @@ -1467,7 +1467,7 @@ describe("MorphoMidnight", () => { }); const prepared = await output.prepare(); expect(output).toBeInstanceOf(TransactionPlan); - expect(prepared.transactionSteps).toEqual([]); + expect(prepared.txSteps).toEqual([]); expect(prepared.hasIntent("midnightOfferRootSignature")).toBe(true); const requirements = (await output.prepare()).requirements; const requirement = requirements.find( @@ -1693,7 +1693,7 @@ describe("MorphoMidnight", () => { }, }); const signature = await signOfferRootRequirement(output); - const tx = (await output.prepare()).build(signature).primaryTransaction; + const tx = (await output.prepare()).build(signature).primaryTx; expect(tx.action.args.maker).toBe(data.accountAddress); }); @@ -1806,7 +1806,7 @@ describe("MorphoMidnight", () => { validation: offerValidation, }); const signature = await signOfferRootRequirement(output); - const tx = (await output.prepare()).build(signature).primaryTransaction; + const tx = (await output.prepare()).build(signature).primaryTx; expect(output.groups).toEqual(data.groups); expect(tx.action.args.maker).toBe(offerSignerAccount.address); @@ -1840,7 +1840,7 @@ describe("MorphoMidnight", () => { validation: offerValidation, }); const requirements = (await output.prepare()).requirements; - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect( requirements.map((requirement) => requirement.action.type), @@ -1903,7 +1903,7 @@ describe("MorphoMidnight", () => { deadline: maxUint256, }); const requirements = (await output.prepare()).requirements; - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect(tx.action.args).toMatchObject({ repayAssets: 1_000n, @@ -1991,7 +1991,7 @@ describe("MorphoMidnight", () => { accountAddress: midnightAddresses.maker, }); const requirements = (await output.prepare()).requirements; - const tx = (await output.prepare()).build().primaryTransaction; + const tx = (await output.prepare()).build().primaryTx; expect(requirements).toEqual([]); expect(tx.action.args.group).toBe(data.groups[0]); diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.ts b/packages/morpho-sdk/src/entities/midnight/midnight.ts index 07ee74ee9..000dd9ccd 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.ts @@ -177,7 +177,7 @@ const validateMarketData = (market: Market, chainId: number) => { * positionData, * }); * const executable = (await plan.prepare()).build(); - * const tx = executable.primaryTransaction; + * const tx = executable.primaryTx; * ``` */ export class MorphoMidnight { @@ -435,7 +435,7 @@ export class MorphoMidnight { return requirements; }, - buildPrimaryTransaction: () => + buildPrimaryTx: () => midnightTakeLend({ chainId: this.chainId, market: market.params, @@ -514,7 +514,7 @@ export class MorphoMidnight { return requirements; }, - buildPrimaryTransaction: () => + buildPrimaryTx: () => midnightTakeBorrow({ chainId: this.chainId, market: market.params, @@ -609,7 +609,7 @@ export class MorphoMidnight { return requirements; }, - buildPrimaryTransaction: () => + buildPrimaryTx: () => midnightSupplyCollateralTakeBorrow({ chainId: this.chainId, market: market.params, @@ -680,7 +680,7 @@ export class MorphoMidnight { amount: params.collateralAssets + (params.reservedCollateralAssets ?? 0n), }), - buildPrimaryTransaction: () => + buildPrimaryTx: () => midnightSupplyCollateral({ chainId: this.chainId, market: market.params, @@ -987,7 +987,7 @@ export class MorphoMidnight { ActionRequirement >({ getRequirementRequests: async () => [], - buildPrimaryTransaction: () => + buildPrimaryTx: () => midnightRedeem({ chainId: this.chainId, market: market.params, @@ -1093,7 +1093,7 @@ export class MorphoMidnight { return requirements; }, - buildPrimaryTransaction: () => + buildPrimaryTx: () => midnightRepayWithdrawCollateral({ chainId: this.chainId, market: market.params, @@ -1135,7 +1135,7 @@ export class MorphoMidnight { ActionRequirement >({ getRequirementRequests: async () => [], - buildPrimaryTransaction: () => + buildPrimaryTx: () => midnightCancelOffer({ chainId: this.chainId, group: params.group, @@ -1161,8 +1161,8 @@ export class MorphoMidnight { MidnightActionSignatures >({ getRequirementRequests: params.getRequirementRequests, - previewPrimaryTransaction: false, - buildPrimaryTransaction: (signatures) => { + previewPrimaryTx: false, + buildPrimaryTx: (signatures) => { const collectedSignatures = signatures == null ? undefined diff --git a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts index 4384faa59..6d1aaf7bd 100644 --- a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts @@ -103,7 +103,7 @@ describe("MorphoVaultV1 entity tests", () => { vaultData, slippageTolerance: 0n, }); - const tx = (await result.prepare()).build().primaryTransaction; + const tx = (await result.prepare()).build().primaryTx; expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -128,7 +128,7 @@ describe("MorphoVaultV1 entity tests", () => { vaultData, slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const tx = (await result.prepare()).build().primaryTransaction; + const tx = (await result.prepare()).build().primaryTx; expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -475,7 +475,7 @@ describe("MorphoVaultV1 entity tests", () => { targetVault, shares: parseUnits("1000", 18), }); - const tx = (await result.prepare()).build().primaryTransaction; + const tx = (await result.prepare()).build().primaryTx; expect(tx.action.type).toBe("vaultV1MigrateToV2"); expect(tx.action.args.sourceVault).toBe(SteakhouseUsdcVaultV1.address); expect(tx.action.args.targetVault).toBe(KeyrockUsdcVaultV2.address); @@ -603,7 +603,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), slippageTolerance: 0n, }); - const tx = (await result.prepare()).build().primaryTransaction; + const tx = (await result.prepare()).build().primaryTx; expect(tx.data).toBeDefined(); }); @@ -634,7 +634,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const tx = (await result.prepare()).build().primaryTransaction; + const tx = (await result.prepare()).build().primaryTx; expect(tx.data).toBeDefined(); }); @@ -757,7 +757,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), }); - const tx = (await result.prepare()).build().primaryTransaction; + const tx = (await result.prepare()).build().primaryTx; expect(tx.action.args.recipient).toBe(OTHER_USER); }); @@ -790,7 +790,7 @@ describe("MorphoVaultV1 entity tests", () => { shares: parseUnits("1000", 18), }); - const tx = (await result.prepare()).build().primaryTransaction; + const tx = (await result.prepare()).build().primaryTx; expect(tx.action.args.recipient).toBe(OTHER_USER); }); }); diff --git a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts index e612a7037..d86043bf5 100644 --- a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts +++ b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.ts @@ -241,7 +241,7 @@ export class MorphoVaultV1 implements VaultV1Actions { }, }), - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -274,7 +274,7 @@ export class MorphoVaultV1 implements VaultV1Actions { } return new TransactionPlan({ - buildPrimaryTransaction: () => + buildPrimaryTx: () => vaultV1Withdraw({ vault: { address: this.vault }, args: { @@ -296,7 +296,7 @@ export class MorphoVaultV1 implements VaultV1Actions { } return new TransactionPlan({ - buildPrimaryTransaction: () => + buildPrimaryTx: () => vaultV1Redeem({ vault: { address: this.vault }, args: { @@ -380,7 +380,7 @@ export class MorphoVaultV1 implements VaultV1Actions { }, }), - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); diff --git a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts index 06cf03251..47981dd4f 100644 --- a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts @@ -103,7 +103,7 @@ describe("MorphoVaultV2 entity tests", () => { vaultData, slippageTolerance: 0n, }); - const tx = (await result.prepare()).build().primaryTransaction; + const tx = (await result.prepare()).build().primaryTx; expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); @@ -128,7 +128,7 @@ describe("MorphoVaultV2 entity tests", () => { vaultData, slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const tx = (await result.prepare()).build().primaryTransaction; + const tx = (await result.prepare()).build().primaryTx; expect(tx.data).toBeDefined(); expect(tx.value).toBe(0n); }); diff --git a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts index 619eaa962..77a06ceef 100644 --- a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts +++ b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.ts @@ -271,7 +271,7 @@ export class MorphoVaultV2 implements VaultV2Actions { }, }), - buildPrimaryTransaction: (signatures) => { + buildPrimaryTx: (signatures) => { const { permit } = selectRequirementSignatures(signatures, { permit: true, }); @@ -304,7 +304,7 @@ export class MorphoVaultV2 implements VaultV2Actions { } return new TransactionPlan({ - buildPrimaryTransaction: () => + buildPrimaryTx: () => vaultV2Withdraw({ vault: { address: this.vault }, args: { @@ -326,7 +326,7 @@ export class MorphoVaultV2 implements VaultV2Actions { } return new TransactionPlan({ - buildPrimaryTransaction: () => + buildPrimaryTx: () => vaultV2Redeem({ vault: { address: this.vault }, args: { @@ -356,7 +356,7 @@ export class MorphoVaultV2 implements VaultV2Actions { } return new TransactionPlan({ - buildPrimaryTransaction: () => + buildPrimaryTx: () => vaultV2ForceWithdraw({ vault: { address: this.vault }, args: { @@ -389,7 +389,7 @@ export class MorphoVaultV2 implements VaultV2Actions { } return new TransactionPlan({ - buildPrimaryTransaction: () => + buildPrimaryTx: () => vaultV2ForceRedeem({ vault: { address: this.vault }, args: { diff --git a/packages/morpho-sdk/src/helpers/computeReallocations.ts b/packages/morpho-sdk/src/helpers/computeReallocations.ts index 868438dc9..08e4690da 100644 --- a/packages/morpho-sdk/src/helpers/computeReallocations.ts +++ b/packages/morpho-sdk/src/helpers/computeReallocations.ts @@ -145,7 +145,7 @@ const capVaultWithdrawals = ( * positionData, * reallocations, * }); - * // borrow.prepare().build().transactionSteps includes any required PublicAllocator reallocations. + * // borrow.prepare().build().txSteps includes any required PublicAllocator reallocations. * ``` */ export const computeReallocations = ({ diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts index 0276530e5..192d76b37 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts @@ -123,7 +123,7 @@ describe("TransactionPlan", () => { permitRequest(), authorizationRequest(), ]), - buildPrimaryTransaction: vi.fn(() => primaryTx), + buildPrimaryTx: vi.fn(() => primaryTx), }; const plan = TransactionPlan.create(handler); @@ -135,18 +135,18 @@ describe("TransactionPlan", () => { expect(Object.hasOwn(plan, "prepare")).toBe(false); expect(prepared.flowKind).toBe("mixed_steps"); expect(prepared.hasSignatureRequests).toBe(true); - expect(prepared.hasTransactionSteps).toBe(true); + expect(prepared.hasTxSteps).toBe(true); expect(prepared.steps.map((step) => step.kind)).toEqual([ - "transaction", + "tx", "signature", "signature", - "transaction", + "tx", ]); - expect(prepared.transactionSteps.map((step) => step.id)).toEqual([ + expect(prepared.txSteps.map((step) => step.id)).toEqual([ "request-0", "primary", ]); - expect(prepared.transactionSteps.map((step) => step.phase)).toEqual([ + expect(prepared.txSteps.map((step) => step.phase)).toEqual([ "preparation", "primary", ]); @@ -154,8 +154,8 @@ describe("TransactionPlan", () => { ["erc20Approval", "permit2", "authorization"], ); expect(Object.isFrozen(prepared.requirements)).toBe(true); - expect(prepared.primaryStep?.transaction).toBe(primaryTx); - expect(prepared.primaryTransaction).toBe(primaryTx); + expect(prepared.primaryStep?.tx).toBe(primaryTx); + expect(prepared.primaryTx).toBe(primaryTx); expect(prepared.calls).toEqual([ { to: TOKEN, value: 0n, data: "0xa1" }, { to: BUNDLER, value: 0n, data: "0xf1" }, @@ -164,7 +164,7 @@ describe("TransactionPlan", () => { expect(prepared.stepCount).toBe(4); expect(prepared.hasIntent("tokenApproval")).toBe(true); expect(prepared.hasIntent("operatorAuthorization")).toBe(true); - expect(prepared.hasIntent("primaryTransaction")).toBe(true); + expect(prepared.hasIntent("primaryTx")).toBe(true); expect( prepared.findIntent("tokenApproval").map((request) => request.intent), ).toEqual([ @@ -191,7 +191,7 @@ describe("TransactionPlan", () => { test("behavior: builds ordered transaction steps only after requested signatures are provided", async () => { const permit = permitRequest(); const authorization = authorizationRequest(); - const buildPrimaryTransaction = vi.fn( + const buildPrimaryTx = vi.fn( (_signatures?: readonly RequirementSignature[]) => primaryTx, ); const plan = TransactionPlan.create({ @@ -200,7 +200,7 @@ describe("TransactionPlan", () => { permit, authorization, ]), - buildPrimaryTransaction, + buildPrimaryTx, }); const prepared = await plan.prepare(); @@ -217,16 +217,16 @@ describe("TransactionPlan", () => { authorizationSignature, ]); - expect(buildPrimaryTransaction).toHaveBeenCalledWith([ + expect(buildPrimaryTx).toHaveBeenCalledWith([ permitSignature, authorizationSignature, ]); - expect(executable.transactionSteps.map((step) => step.id)).toEqual([ + expect(executable.txSteps.map((step) => step.id)).toEqual([ "request-0", "primary", ]); - expect(executable.primaryStep.transaction).toBe(primaryTx); - expect(executable.primaryTransaction).toBe(primaryTx); + expect(executable.primaryStep.tx).toBe(primaryTx); + expect(executable.primaryTx).toBe(primaryTx); expect(executable.calls).toEqual([ { to: TOKEN, value: 0n, data: "0xa1" }, { to: BUNDLER, value: 0n, data: "0xf1" }, @@ -241,19 +241,19 @@ describe("TransactionPlan", () => { | Requirement >({ getRequirementRequests: vi.fn(async () => [approvalTx, permitRequest()]), - buildPrimaryTransaction: vi.fn(() => primaryTx), + buildPrimaryTx: vi.fn(() => primaryTx), }); const prepared = await plan.prepare(); type Prepared = typeof prepared; type SignatureRequest = Prepared["signatureRequests"][number]; - type PreparationTransactionStep = Extract< - Prepared["transactionSteps"][number], + type PreparationTxStep = Extract< + Prepared["txSteps"][number], { readonly phase: "preparation" } >; type Executable = ReturnType; - type PrimaryTransactionStep = Extract< - Executable["transactionSteps"][number], + type PrimaryTxStep = Extract< + Executable["txSteps"][number], { readonly phase: "primary" } >; @@ -261,10 +261,10 @@ describe("TransactionPlan", () => { | Readonly> | Requirement >(); - expectTypeOf().toEqualTypeOf< + expectTypeOf().toEqualTypeOf< Readonly> | undefined >(); - expectTypeOf().toEqualTypeOf< + expectTypeOf().toEqualTypeOf< Readonly> >(); expectTypeOf().toEqualTypeOf< @@ -273,23 +273,23 @@ describe("TransactionPlan", () => { expectTypeOf>().toEqualTypeOf< Promise >(); - expectTypeOf().toEqualTypeOf< + expectTypeOf().toEqualTypeOf< Readonly> >(); expectTypeOf< - PrimaryTransactionStep["action"] + PrimaryTxStep["action"] >().toEqualTypeOf(); }); test("behavior: requirement transaction steps keep authorization intent separate from token approval", async () => { const plan = TransactionPlan.create({ getRequirementRequests: vi.fn(async () => [authorizationTx]), - buildPrimaryTransaction: vi.fn(() => primaryTx), + buildPrimaryTx: vi.fn(() => primaryTx), }); const prepared = await plan.prepare(); - expect(prepared.flowKind).toBe("transaction_steps"); + expect(prepared.flowKind).toBe("tx_steps"); expect( prepared .findIntent("operatorAuthorization") @@ -308,44 +308,38 @@ describe("TransactionPlan", () => { test("behavior: a non-previewable primary step appears after build", async () => { const plan = TransactionPlan.create({ getRequirementRequests: vi.fn(async () => [permitRequest()]), - previewPrimaryTransaction: false, - buildPrimaryTransaction: vi.fn(() => primaryTx), + previewPrimaryTx: false, + buildPrimaryTx: vi.fn(() => primaryTx), }); const prepared = await plan.prepare(); expect(prepared.flowKind).toBe("signature_steps"); expect(prepared.primaryStep).toBeUndefined(); - expect(prepared.primaryTransaction).toBeUndefined(); + expect(prepared.primaryTx).toBeUndefined(); expect(prepared.calls).toEqual([]); - expect(prepared.transactionSteps).toEqual([]); + expect(prepared.txSteps).toEqual([]); expect(prepared.steps.map((request) => request.id)).toEqual(["request-0"]); const executable = prepared.build([permitSignature]); - expect(executable.primaryTransaction).toBe(primaryTx); - expect(executable.transactionSteps.map((step) => step.id)).toEqual([ - "primary", - ]); + expect(executable.primaryTx).toBe(primaryTx); + expect(executable.txSteps.map((step) => step.id)).toEqual(["primary"]); }); test("behavior: a handler without requirement requests is a single transaction", async () => { const plan = TransactionPlan.create({ - buildPrimaryTransaction: vi.fn(() => primaryTx), + buildPrimaryTx: vi.fn(() => primaryTx), }); const prepared = await plan.prepare(); const executable = prepared.build(); - expect(prepared.flowKind).toBe("single_transaction"); + expect(prepared.flowKind).toBe("single_tx"); expect(prepared.stepCount).toBe(1); expect(prepared.signatureRequests).toEqual([]); - expect(prepared.transactionSteps.map((step) => step.id)).toEqual([ - "primary", - ]); - expect(executable.transactionSteps.map((step) => step.id)).toEqual([ - "primary", - ]); + expect(prepared.txSteps.map((step) => step.id)).toEqual(["primary"]); + expect(executable.txSteps.map((step) => step.id)).toEqual(["primary"]); expect(executable.calls).toEqual([ { to: BUNDLER, value: 0n, data: "0xf1" }, ]); diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts index c37548fc9..52e5aaef0 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts @@ -5,15 +5,15 @@ import { type SignatureRequirement, type Transaction, type TransactionAction, - type TransactionRequirement, + type TxRequirement, } from "../types/action.js"; import { MissingTransactionPlanSignaturesError } from "../types/error.js"; import type { ExecutableTransactionPlanShape, PreparedTransactionPlanShape, TransactionPlanBatchCall, - TransactionPlanBuildPrimaryTransaction, - TransactionPlanContractTransactionIntent, + TransactionPlanBuildPrimaryTx, + TransactionPlanContractTxIntent, TransactionPlanFlowKind, TransactionPlanHandler, TransactionPlanIntent, @@ -26,7 +26,7 @@ import type { TransactionPlanStep, TransactionPlanStepForIntent, TransactionPlanTokenApprovalIntent, - TransactionPlanTransactionStep, + TransactionPlanTxStep, } from "./types.js"; export type { @@ -53,7 +53,7 @@ export type { * const plan = vault.deposit({ amount, userAddress, vaultData }); * const prepared = await plan.prepare({ requestOptions: { useSimplePermit } }); * - * const txFlowType = prepared.flowKind === "single_transaction" + * const txFlowType = prepared.flowKind === "single_tx" * ? "simple" * : prepared.hasSignatureRequests * ? "signature_required" @@ -67,11 +67,11 @@ export type { * : `Sign ${step.intent.method}`; * case "operatorAuthorization": * return "Authorize operator"; - * case "contractTransaction": + * case "contractTx": * return `Review ${step.intent.actionType}`; * case "midnightOfferRootSignature": * return "Sign offer root"; - * case "primaryTransaction": + * case "primaryTx": * return "Vault deposit"; * } * }); @@ -91,9 +91,9 @@ export type { * signatures.push(await request.sign(walletClient, accountAddress)); * } * const executable = prepared.build(signatures); - * const transactionSteps = executable.transactionSteps.map((step) => ({ - * label: step.intent.type === "primaryTransaction" ? "Submit transaction" : "Approve token", - * transaction: step.transaction, + * const txSteps = executable.txSteps.map((step) => ({ + * label: step.intent.type === "primaryTx" ? "Submit transaction" : "Approve token", + * tx: step.tx, * })); * const calls = executable.calls; * ``` @@ -119,7 +119,7 @@ export class TransactionPlan< * ```ts * const plan = new TransactionPlan({ * getRequirementRequests: async () => requests, - * buildPrimaryTransaction: () => transaction, + * buildPrimaryTx: () => transaction, * }); * ``` */ @@ -157,12 +157,12 @@ export class TransactionPlan< (request, index): TransactionPlanPreparedStep => { const id = `request-${index}`; if (isRequirementApproval(request)) { - const tx = request as Extract; + const tx = request as Extract; return { - kind: "transaction", + kind: "tx", id, phase: "preparation", - transaction: tx, + tx, action: tx.action, intent: { type: "tokenApproval", @@ -174,12 +174,12 @@ export class TransactionPlan< } satisfies TransactionPlanPreparedStep; } if (isRequirementBlueAuthorization(request)) { - const tx = request as Extract; + const tx = request as Extract; return { - kind: "transaction", + kind: "tx", id, phase: "preparation", - transaction: tx, + tx, action: tx.action, intent: { type: "operatorAuthorization", @@ -197,12 +197,10 @@ export class TransactionPlan< "data" in request && "action" in request ) { - const tx = request as Extract; + const tx = request as Extract; const intent: | TransactionPlanOperatorAuthorizationIntent - | TransactionPlanContractTransactionIntent< - TransactionRequirement["action"] - > = + | TransactionPlanContractTxIntent = tx.action.type === "midnightAuthorization" ? { type: "operatorAuthorization", @@ -211,12 +209,12 @@ export class TransactionPlan< isAuthorized: tx.action.args.isAuthorized, owner: tx.action.args.onBehalf, } - : { type: "contractTransaction", actionType: tx.action.type }; + : { type: "contractTx", actionType: tx.action.type }; return { - kind: "transaction", + kind: "tx", id, phase: "preparation", - transaction: tx, + tx, action: tx.action, intent, } satisfies TransactionPlanPreparedStep; @@ -284,33 +282,33 @@ export class TransactionPlan< }, ); const primaryTx = - this.handler.previewPrimaryTransaction === false + this.handler.previewPrimaryTx === false ? undefined : ( - this.handler.previewPrimaryTransaction ?? - (() => this.handler.buildPrimaryTransaction()) + this.handler.previewPrimaryTx ?? + (() => this.handler.buildPrimaryTx()) )(); const primaryStep = primaryTx == null ? undefined : ({ - kind: "transaction", + kind: "tx", id: "primary", phase: "primary", - transaction: primaryTx, + tx: primaryTx, action: primaryTx.action, intent: { - type: "primaryTransaction", + type: "primaryTx", actionType: primaryTx.action.type, }, - } satisfies TransactionPlanTransactionStep< + } satisfies TransactionPlanTxStep< TPrimaryAction, Readonly>, "primary" >); return new PreparedTransactionPlan({ - buildPrimaryTransaction: this.handler.buildPrimaryTransaction, + buildPrimaryTx: this.handler.buildPrimaryTx, requirementSteps, primaryStep, }); @@ -325,7 +323,7 @@ export class TransactionPlan< * ```ts * const plan = TransactionPlan.create({ * getRequirementRequests: async () => requests, - * buildPrimaryTransaction: () => transaction, + * buildPrimaryTx: () => transaction, * }); * ``` */ @@ -366,14 +364,14 @@ export class PreparedTransactionPlan< TSignatures = readonly RequirementSignature[], > implements PreparedTransactionPlanShape { - private readonly buildPrimaryTransaction: TransactionPlanBuildPrimaryTransaction< + private readonly buildPrimaryTx: TransactionPlanBuildPrimaryTx< TPrimaryAction, TSignatures >; private readonly rawRequirements: readonly TRequest[]; - private readonly previewPrimaryStep?: TransactionPlanTransactionStep< + private readonly previewPrimaryStep?: TransactionPlanTxStep< TPrimaryAction, Readonly>, "primary" @@ -385,10 +383,10 @@ export class PreparedTransactionPlan< >[]; /** Ordered transaction steps. Includes the primary action step last when previewable. */ - readonly transactionSteps: PreparedTransactionPlanShape< + readonly txSteps: PreparedTransactionPlanShape< TPrimaryAction, TRequest - >["transactionSteps"]; + >["txSteps"]; /** All signature requests and transaction steps in review order. */ readonly steps: readonly TransactionPlanStep[]; @@ -400,29 +398,28 @@ export class PreparedTransactionPlan< * @example * ```ts * const prepared = new PreparedTransactionPlan({ - * buildPrimaryTransaction: () => transaction, + * buildPrimaryTx: () => transaction, * requirementSteps: [], * primaryStep, * }); * ``` */ constructor(params: { - readonly buildPrimaryTransaction: TransactionPlanBuildPrimaryTransaction< + readonly buildPrimaryTx: TransactionPlanBuildPrimaryTx< TPrimaryAction, TSignatures >; readonly requirementSteps: readonly TransactionPlanPreparedStep[]; - readonly primaryStep?: TransactionPlanTransactionStep< + readonly primaryStep?: TransactionPlanTxStep< TPrimaryAction, Readonly>, "primary" >; }) { - this.buildPrimaryTransaction = params.buildPrimaryTransaction; + this.buildPrimaryTx = params.buildPrimaryTx; this.rawRequirements = Object.freeze( params.requirementSteps.map( - (step): TRequest => - step.kind === "transaction" ? step.transaction : step.request, + (step): TRequest => (step.kind === "tx" ? step.tx : step.request), ), ); this.previewPrimaryStep = params.primaryStep; @@ -435,19 +432,19 @@ export class PreparedTransactionPlan< > => step.kind === "signature", ), ); - const requirementTransactionSteps = params.requirementSteps.filter( + const requirementTxSteps = params.requirementSteps.filter( ( step, ): step is Extract< TransactionPlanPreparedStep, - { readonly kind: "transaction" } - > => step.kind === "transaction", + { readonly kind: "tx" } + > => step.kind === "tx", ); const primaryStep = params.primaryStep; - this.transactionSteps = Object.freeze( + this.txSteps = Object.freeze( primaryStep == null - ? [...requirementTransactionSteps] - : [...requirementTransactionSteps, primaryStep], + ? [...requirementTxSteps] + : [...requirementTxSteps, primaryStep], ); this.steps = Object.freeze( primaryStep == null @@ -491,11 +488,11 @@ export class PreparedTransactionPlan< * @returns The primary transaction, or `undefined` when signatures are required before encoding. * @example * ```ts - * const tx = (await plan.prepare()).primaryTransaction; + * const tx = (await plan.prepare()).primaryTx; * ``` */ - get primaryTransaction(): Readonly> | undefined { - return this.previewPrimaryStep?.transaction; + get primaryTx(): Readonly> | undefined { + return this.previewPrimaryStep?.tx; } /** @@ -510,10 +507,10 @@ export class PreparedTransactionPlan< * ``` */ get calls(): readonly TransactionPlanBatchCall[] { - return this.transactionSteps.map(({ transaction }) => ({ - to: transaction.to, - value: transaction.value, - data: transaction.data, + return this.txSteps.map(({ tx }) => ({ + to: tx.to, + value: tx.value, + data: tx.data, })); } @@ -527,7 +524,7 @@ export class PreparedTransactionPlan< * ``` */ get stepCount(): number { - return this.signatureRequests.length + this.transactionSteps.length; + return this.signatureRequests.length + this.txSteps.length; } /** @@ -546,14 +543,14 @@ export class PreparedTransactionPlan< /** * Reports whether the plan currently exposes a transaction step. * - * @returns `true` when `transactionSteps` is non-empty. + * @returns `true` when `txSteps` is non-empty. * @example * ```ts - * if (prepared.hasTransactionSteps) showTransactionReview(); + * if (prepared.hasTxSteps) showTxReview(); * ``` */ - get hasTransactionSteps(): boolean { - return this.transactionSteps.length > 0; + get hasTxSteps(): boolean { + return this.txSteps.length > 0; } /** @@ -567,20 +564,19 @@ export class PreparedTransactionPlan< */ get flowKind(): TransactionPlanFlowKind { const signatureRequests = this.signatureRequests.length; - const preparationSteps = this.transactionSteps.filter( + const preparationSteps = this.txSteps.filter( (step) => step.phase === "preparation", ).length; - const primarySteps = this.transactionSteps.filter( + const primarySteps = this.txSteps.filter( (step) => step.phase === "primary", ).length; - if (signatureRequests > 0 && this.transactionSteps.length > 0) { + if (signatureRequests > 0 && this.txSteps.length > 0) { return "mixed_steps"; } if (signatureRequests > 0) return "signature_steps"; - if (preparationSteps === 0 && primarySteps <= 1) - return "single_transaction"; - return "transaction_steps"; + if (preparationSteps === 0 && primarySteps <= 1) return "single_tx"; + return "tx_steps"; } /** @@ -649,18 +645,18 @@ export class PreparedTransactionPlan< if (received < expected) { throw new MissingTransactionPlanSignaturesError(expected, received); } - const requirementTransactionSteps = this.transactionSteps.filter( + const requirementTxSteps = this.txSteps.filter( (step) => step.phase === "preparation", ); - const primaryTx = this.buildPrimaryTransaction(signatures); + const primaryTx = this.buildPrimaryTx(signatures); const primaryStep = { - kind: "transaction", + kind: "tx", id: "primary", phase: "primary", - transaction: primaryTx, + tx: primaryTx, action: primaryTx.action, - intent: { type: "primaryTransaction", actionType: primaryTx.action.type }, - } satisfies TransactionPlanTransactionStep< + intent: { type: "primaryTx", actionType: primaryTx.action.type }, + } satisfies TransactionPlanTxStep< TPrimaryAction, Readonly>, "primary" @@ -668,10 +664,7 @@ export class PreparedTransactionPlan< return new ExecutableTransactionPlan({ primaryStep, signatureRequests: Object.freeze([...this.signatureRequests]), - transactionSteps: Object.freeze([ - ...requirementTransactionSteps, - primaryStep, - ]), + txSteps: Object.freeze([...requirementTxSteps, primaryStep]), }); } } @@ -682,8 +675,8 @@ export class PreparedTransactionPlan< * @example * ```ts * const executable = prepared.build(signatures); - * for (const step of executable.transactionSteps) { - * await walletClient.sendTransaction(step.transaction); + * for (const step of executable.txSteps) { + * await walletClient.sendTransaction(step.tx); * } * await walletClient.sendCalls({ calls: executable.calls }); * ``` @@ -693,7 +686,7 @@ export class ExecutableTransactionPlan< TRequest extends TransactionPlanRequest = TransactionPlanRequest, > implements ExecutableTransactionPlanShape { - private readonly builtPrimaryStep: TransactionPlanTransactionStep< + private readonly builtPrimaryStep: TransactionPlanTxStep< TPrimaryAction, Readonly>, "primary" @@ -705,10 +698,10 @@ export class ExecutableTransactionPlan< >[]; /** Ordered transaction steps to submit, with the primary action step last. */ - readonly transactionSteps: ExecutableTransactionPlanShape< + readonly txSteps: ExecutableTransactionPlanShape< TPrimaryAction, TRequest - >["transactionSteps"]; + >["txSteps"]; /** * Creates an executable plan from built transaction requests. @@ -719,19 +712,19 @@ export class ExecutableTransactionPlan< * const executable = new ExecutableTransactionPlan({ * primaryStep, * signatureRequests: [], - * transactionSteps, + * txSteps, * }); * ``` */ constructor( params: Omit< ExecutableTransactionPlanShape, - "primaryTransaction" | "calls" + "primaryTx" | "calls" >, ) { this.builtPrimaryStep = params.primaryStep; this.signatureRequests = Object.freeze([...params.signatureRequests]); - this.transactionSteps = Object.freeze([...params.transactionSteps]); + this.txSteps = Object.freeze([...params.txSteps]); } /** @@ -743,7 +736,7 @@ export class ExecutableTransactionPlan< * const primaryStep = executable.primaryStep; * ``` */ - get primaryStep(): TransactionPlanTransactionStep< + get primaryStep(): TransactionPlanTxStep< TPrimaryAction, Readonly>, "primary" @@ -757,11 +750,11 @@ export class ExecutableTransactionPlan< * @returns The final primary transaction after signatures are applied. * @example * ```ts - * const tx = executable.primaryTransaction; + * const tx = executable.primaryTx; * ``` */ - get primaryTransaction(): Readonly> { - return this.builtPrimaryStep.transaction; + get primaryTx(): Readonly> { + return this.builtPrimaryStep.tx; } /** @@ -774,10 +767,10 @@ export class ExecutableTransactionPlan< * ``` */ get calls(): readonly TransactionPlanBatchCall[] { - return this.transactionSteps.map(({ transaction }) => ({ - to: transaction.to, - value: transaction.value, - data: transaction.data, + return this.txSteps.map(({ tx }) => ({ + to: tx.to, + value: tx.value, + data: tx.data, })); } } diff --git a/packages/morpho-sdk/src/transactionPlan/types.ts b/packages/morpho-sdk/src/transactionPlan/types.ts index 609bb6b18..274a7baed 100644 --- a/packages/morpho-sdk/src/transactionPlan/types.ts +++ b/packages/morpho-sdk/src/transactionPlan/types.ts @@ -10,7 +10,7 @@ import type { SignatureRequirement, Transaction, TransactionAction, - TransactionRequirement, + TxRequirement, } from "../types/action.js"; /** Options forwarded to token-approval request discovery. */ @@ -44,12 +44,10 @@ export interface TransactionPlanBatchCall { } /** On-chain or signable request discovered while preparing a transaction plan. */ -export type TransactionPlanRequest = - | TransactionRequirement - | SignatureRequirement; +export type TransactionPlanRequest = TxRequirement | SignatureRequirement; /** Builds the primary transaction, optionally consuming previously signed requests. */ -export type TransactionPlanBuildPrimaryTransaction< +export type TransactionPlanBuildPrimaryTx< TPrimaryAction extends TransactionAction = TransactionAction, TSignatures = readonly RequirementSignature[], > = (signatures?: TSignatures) => Readonly>; @@ -68,7 +66,7 @@ export interface TransactionPlanHandler< TSignatures = readonly RequirementSignature[], > { /** Builds the primary transaction, optionally consuming previously signed requests. */ - readonly buildPrimaryTransaction: TransactionPlanBuildPrimaryTransaction< + readonly buildPrimaryTx: TransactionPlanBuildPrimaryTx< TPrimaryAction, TSignatures >; @@ -80,7 +78,7 @@ export interface TransactionPlanHandler< * mempool payload is produced by the offer-root signature and can only be encoded inside * `prepared.build(signatures)`. */ - readonly previewPrimaryTransaction?: + readonly previewPrimaryTx?: | false | (() => Readonly>); /** Resolves transaction and signature requirements before the primary transaction. */ @@ -147,21 +145,21 @@ export interface TransactionPlanMidnightOfferRootIntent { } /** Generic on-chain transaction intent for neutral transaction requirements. */ -export interface TransactionPlanContractTransactionIntent< +export interface TransactionPlanContractTxIntent< TAction extends TransactionAction = TransactionAction, > { /** Intent discriminator for filtering and app-specific rendering. */ - readonly type: "contractTransaction"; + readonly type: "contractTx"; /** SDK action type attached to the call metadata. */ readonly actionType: TAction["type"]; } /** Primary Morpho SDK transaction intent requested by the user action. */ -export interface TransactionPlanPrimaryTransactionIntent< +export interface TransactionPlanPrimaryTxIntent< TAction extends TransactionAction = TransactionAction, > { /** Intent discriminator for filtering and app-specific rendering. */ - readonly type: "primaryTransaction"; + readonly type: "primaryTx"; /** Primary SDK action type, for example `vaultV2Deposit` or `blueRepayWithdrawCollateral`. */ readonly actionType: TAction["type"]; } @@ -173,13 +171,13 @@ export type TransactionPlanIntent< | TransactionPlanTokenApprovalIntent | TransactionPlanOperatorAuthorizationIntent | TransactionPlanMidnightOfferRootIntent - | TransactionPlanContractTransactionIntent - | TransactionPlanPrimaryTransactionIntent; + | TransactionPlanContractTxIntent + | TransactionPlanPrimaryTxIntent; /** Flow shape derived from a plan's signature requests and transaction steps. */ export type TransactionPlanFlowKind = - | "single_transaction" - | "transaction_steps" + | "single_tx" + | "tx_steps" | "signature_steps" | "mixed_steps"; @@ -205,21 +203,19 @@ export interface TransactionPlanSignatureRequest< } /** A transaction step containing an SDK transaction and plan metadata. */ -export interface TransactionPlanTransactionStep< +export interface TransactionPlanTxStep< TAction extends BaseAction = TransactionAction, - TTransaction extends Readonly> = Readonly< - Transaction - >, + TTx extends Readonly> = Readonly>, TPhase extends "preparation" | "primary" = "preparation" | "primary", > { /** Request kind discriminator. */ - readonly kind: "transaction"; + readonly kind: "tx"; /** Stable id based on original requirement order, or `primary` for the requested action. */ readonly id: string; /** Whether this step supports the flow or executes the requested primary action. */ readonly phase: TPhase; /** Original SDK transaction with Morpho action metadata. */ - readonly transaction: TTransaction; + readonly tx: TTx; /** Exact SDK/protocol action metadata used to encode this transaction. */ readonly action: TAction; /** Higher-level plan category for app labels, filtering, and analytics. */ @@ -231,9 +227,9 @@ export type TransactionPlanPreparedStep< TRequest extends TransactionPlanRequest = TransactionPlanRequest, > = | TransactionPlanSignatureRequest> - | TransactionPlanTransactionStep< - Extract["action"], - Extract, + | TransactionPlanTxStep< + Extract["action"], + Extract, "preparation" >; @@ -243,7 +239,7 @@ export type TransactionPlanStep< TRequest extends TransactionPlanRequest = TransactionPlanRequest, > = | TransactionPlanPreparedStep - | TransactionPlanTransactionStep< + | TransactionPlanTxStep< TPrimaryAction, Readonly>, "primary" @@ -266,21 +262,21 @@ export interface PreparedTransactionPlanShape< /** Raw prerequisite requirements in discovery order. */ readonly requirements: readonly TRequest[]; /** Preview of the primary step, when its transaction can be encoded without signatures. */ - readonly primaryStep?: TransactionPlanTransactionStep< + readonly primaryStep?: TransactionPlanTxStep< TPrimaryAction, Readonly>, "primary" >; /** Preview of the primary SDK transaction, when it can be encoded without signatures. */ - readonly primaryTransaction?: Readonly>; + readonly primaryTx?: Readonly>; /** Ordered signable requests to present to the user. */ readonly signatureRequests: readonly TransactionPlanSignatureRequest< Extract >[]; /** Ordered transaction steps. Includes the primary action step last when previewable. */ - readonly transactionSteps: readonly Extract< + readonly txSteps: readonly Extract< TransactionPlanStep, - { readonly kind: "transaction" } + { readonly kind: "tx" } >[]; /** Viem-compatible calls converted from the currently previewable transaction steps. */ readonly calls: readonly TransactionPlanBatchCall[]; @@ -294,21 +290,21 @@ export interface ExecutableTransactionPlanShape< TRequest extends TransactionPlanRequest = TransactionPlanRequest, > { /** Built primary transaction step. */ - readonly primaryStep: TransactionPlanTransactionStep< + readonly primaryStep: TransactionPlanTxStep< TPrimaryAction, Readonly>, "primary" >; /** Built primary SDK transaction. */ - readonly primaryTransaction: Readonly>; + readonly primaryTx: Readonly>; /** Ordered signable requests used to produce the signatures passed to `build(...)`. */ readonly signatureRequests: readonly TransactionPlanSignatureRequest< Extract >[]; /** Ordered transaction steps to submit, with the primary action step last. */ - readonly transactionSteps: readonly Extract< + readonly txSteps: readonly Extract< TransactionPlanStep, - { readonly kind: "transaction" } + { readonly kind: "tx" } >[]; /** Viem-compatible calls converted from the executable transaction steps. */ readonly calls: readonly TransactionPlanBatchCall[]; diff --git a/packages/morpho-sdk/src/types/action.ts b/packages/morpho-sdk/src/types/action.ts index 15f35afbf..d037241b0 100644 --- a/packages/morpho-sdk/src/types/action.ts +++ b/packages/morpho-sdk/src/types/action.ts @@ -669,7 +669,7 @@ export type SignatureRequirement = | Requirement; /** Transaction action metadata that can appear as an action prerequisite. */ -export type TransactionRequirementAction = +export type TxRequirementAction = | ERC20ApprovalAction | BlueAuthorizationAction | MidnightAuthorizationAction @@ -677,12 +677,10 @@ export type TransactionRequirementAction = | MidnightSupplyCollateralAction; /** Onchain transaction prerequisite discovered while preparing an entity transaction plan. */ -export type TransactionRequirement = Readonly< - Transaction ->; +export type TxRequirement = Readonly>; /** Onchain transaction or signature prerequisite discovered while preparing an entity transaction plan. */ -export type ActionRequirement = TransactionRequirement | SignatureRequirement; +export type ActionRequirement = TxRequirement | SignatureRequirement; export function isRequirementApproval( requirement: unknown, @@ -722,10 +720,10 @@ export function isRequirementBlueAuthorization( export function isRequirementSignature< T extends RequirementSignature = RequirementSignature, >( - requirement: TransactionRequirement | Requirement | undefined, + requirement: TxRequirement | Requirement | undefined, ): requirement is Requirement; export function isRequirementSignature( - requirement: TransactionRequirement | Requirement | undefined, + requirement: TxRequirement | Requirement | undefined, ): requirement is Requirement; export function isRequirementSignature( requirement: ActionRequirement | undefined, diff --git a/packages/morpho-sdk/test/actions/blue/borrow.test.ts b/packages/morpho-sdk/test/actions/blue/borrow.test.ts index 3b15263a2..86979b14c 100644 --- a/packages/morpho-sdk/test/actions/blue/borrow.test.ts +++ b/packages/morpho-sdk/test/actions/blue/borrow.test.ts @@ -52,7 +52,7 @@ describe("BorrowBlue", () => { positionData, }) .prepare() - ).build().primaryTransaction; + ).build().primaryTx; const expectedMinSharePrice = MathLib.mulDivDown( totalBorrowAssets + SharesMath.VIRTUAL_ASSETS, @@ -105,7 +105,7 @@ describe("BorrowBlue", () => { await client.sendTransaction(requirementAuthorization); - const tx = (await borrow.prepare()).build().primaryTransaction; + const tx = (await borrow.prepare()).build().primaryTx; await client.sendTransaction(tx); }, @@ -201,7 +201,7 @@ describe("BorrowBlue", () => { const tx = (await borrow.prepare()).build([ authorizationSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts index 9b18d10d2..7ae26efb6 100644 --- a/packages/morpho-sdk/test/actions/blue/reallocations.test.ts +++ b/packages/morpho-sdk/test/actions/blue/reallocations.test.ts @@ -86,7 +86,7 @@ describe("Borrow with single vault reallocation (e2e)", () => { } await client.sendTransaction(authorization); - const tx = (await borrow.prepare()).build().primaryTransaction; + const tx = (await borrow.prepare()).build().primaryTx; expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); @@ -171,7 +171,7 @@ describe("Borrow with multiple source market withdrawals", () => { } await client.sendTransaction(authorization); - const tx = (await borrow.prepare()).build().primaryTransaction; + const tx = (await borrow.prepare()).build().primaryTx; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -272,7 +272,7 @@ describe("Borrow with reallocation fee", () => { } await client.sendTransaction(authorization); - const tx = (await borrow.prepare()).build().primaryTransaction; + const tx = (await borrow.prepare()).build().primaryTx; expect(tx.value).toBe(reallocationFee); expect(tx.action.args.reallocationFee).toBe(reallocationFee); @@ -368,7 +368,7 @@ describe("SupplyCollateralBorrow with single vault reallocation", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = (await scb.prepare()).build().primaryTransaction; + const tx = (await scb.prepare()).build().primaryTx; expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); @@ -462,7 +462,7 @@ describe("SupplyCollateralBorrow with multiple source market withdrawals", () => await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = (await scb.prepare()).build().primaryTransaction; + const tx = (await scb.prepare()).build().primaryTx; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -575,7 +575,7 @@ describe("SupplyCollateralBorrow with reallocation fee", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = (await scb.prepare()).build().primaryTransaction; + const tx = (await scb.prepare()).build().primaryTx; expect(tx.value).toBe(reallocationFee); expect(tx.action.args.reallocationFee).toBe(reallocationFee); @@ -699,7 +699,7 @@ describe("getReallocationData and getReallocations", () => { } await client.sendTransaction(authorization); - const tx = (await borrow.prepare()).build().primaryTransaction; + const tx = (await borrow.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -775,7 +775,7 @@ describe("getReallocationData and getReallocations", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = (await scb.prepare()).build().primaryTransaction; + const tx = (await scb.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/refinance.test.ts b/packages/morpho-sdk/test/actions/blue/refinance.test.ts index e558f4ac7..274cc4ce8 100644 --- a/packages/morpho-sdk/test/actions/blue/refinance.test.ts +++ b/packages/morpho-sdk/test/actions/blue/refinance.test.ts @@ -104,9 +104,7 @@ describe("RefinanceBlue (fork)", () => { await client.sendTransaction(requirement); } - await client.sendTransaction( - (await refi.prepare()).build().primaryTransaction, - ); + await client.sendTransaction((await refi.prepare()).build().primaryTx); }, }); // testInvariants already asserts every bundler3 component ends with 0 balance — nothing left behind. @@ -186,9 +184,7 @@ describe("RefinanceBlue (fork)", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction( - (await refi.prepare()).build().primaryTransaction, - ); + await client.sendTransaction((await refi.prepare()).build().primaryTx); }, }); @@ -252,9 +248,7 @@ describe("RefinanceBlue (fork)", () => { throw new Error("Authorization requirement not found"); } await client.sendTransaction(auth); - await client.sendTransaction( - (await refi.prepare()).build().primaryTransaction, - ); + await client.sendTransaction((await refi.prepare()).build().primaryTx); }, }); @@ -355,7 +349,7 @@ describe("RefinanceBlue (fork)", () => { await client.sendTransaction(requirement); } - const tx = (await refi.prepare()).build().primaryTransaction; + const tx = (await refi.prepare()).build().primaryTx; expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); diff --git a/packages/morpho-sdk/test/actions/blue/repay.test.ts b/packages/morpho-sdk/test/actions/blue/repay.test.ts index 392395ce1..d029cdf0b 100644 --- a/packages/morpho-sdk/test/actions/blue/repay.test.ts +++ b/packages/morpho-sdk/test/actions/blue/repay.test.ts @@ -73,7 +73,7 @@ describe("RepayBlue", () => { } await client.sendTransaction(approval); - const tx = (await repay.prepare()).build().primaryTransaction; + const tx = (await repay.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -142,7 +142,7 @@ describe("RepayBlue", () => { }); await client.sendTransaction(approval); - const tx = (await repay.prepare()).build().primaryTransaction; + const tx = (await repay.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -232,7 +232,7 @@ describe("RepayBlue", () => { } await client.sendTransaction(approval); - const tx = (await repay.prepare()).build().primaryTransaction; + const tx = (await repay.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -295,7 +295,7 @@ describe("RepayBlue", () => { const requirements = (await repay.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = (await repay.prepare()).build().primaryTransaction; + const tx = (await repay.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -382,7 +382,7 @@ describe("RepayBlue", () => { expect(approval.action.args.amount).toEqual(erc20Part); await client.sendTransaction(approval); - const tx = (await repay.prepare()).build().primaryTransaction; + const tx = (await repay.prepare()).build().primaryTx; // Only the native portion rides as tx.value. expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); @@ -475,7 +475,7 @@ describe("RepayBlue", () => { }); await client.sendTransaction(approval); - const tx = (await repay.prepare()).build().primaryTransaction; + const tx = (await repay.prepare()).build().primaryTx; expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); }, @@ -554,7 +554,7 @@ describe("RepayBlue", () => { const requirements = (await repay.prepare()).requirements; expect(requirements).toEqual([]); - const tx = (await repay.prepare()).build().primaryTransaction; + const tx = (await repay.prepare()).build().primaryTx; // Everything is wrapped native; no ERC-20 pulled. expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.transferAmount).toEqual(nativeAmount); diff --git a/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts b/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts index 2424a07ca..a06001200 100644 --- a/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts +++ b/packages/morpho-sdk/test/actions/blue/repayWithdrawCollateral.test.ts @@ -81,7 +81,7 @@ describe("RepayWithdrawCollateralBlue", () => { } await client.sendTransaction(approval); - const tx = (await action.prepare()).build().primaryTransaction; + const tx = (await action.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -158,7 +158,7 @@ describe("RepayWithdrawCollateralBlue", () => { } } - const tx = (await action.prepare()).build().primaryTransaction; + const tx = (await action.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -255,7 +255,7 @@ describe("RepayWithdrawCollateralBlue", () => { } } - const tx = (await action.prepare()).build().primaryTransaction; + const tx = (await action.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -329,7 +329,7 @@ describe("RepayWithdrawCollateralBlue", () => { } } - const tx = (await action.prepare()).build().primaryTransaction; + const tx = (await action.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -421,7 +421,7 @@ describe("RepayWithdrawCollateralBlue", () => { expect(approval.action.args.amount).toEqual(erc20Part); await client.sendTransaction(approval); - const tx = (await action.prepare()).build().primaryTransaction; + const tx = (await action.prepare()).build().primaryTx; // Only the native portion rides as tx.value. expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); @@ -520,7 +520,7 @@ describe("RepayWithdrawCollateralBlue", () => { }); await client.sendTransaction(approval); - const tx = (await action.prepare()).build().primaryTransaction; + const tx = (await action.prepare()).build().primaryTx; expect(tx.value).toEqual(nativePart); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/blue/supply.test.ts b/packages/morpho-sdk/test/actions/blue/supply.test.ts index 84171e788..8b21cd7f8 100644 --- a/packages/morpho-sdk/test/actions/blue/supply.test.ts +++ b/packages/morpho-sdk/test/actions/blue/supply.test.ts @@ -54,7 +54,7 @@ describe("SupplyBlue", () => { } await client.sendTransaction(approval); - const tx = (await supply.prepare()).build().primaryTransaction; + const tx = (await supply.prepare()).build().primaryTx; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -109,7 +109,7 @@ describe("SupplyBlue", () => { marketData, }); - const tx = (await supply.prepare()).build().primaryTransaction; + const tx = (await supply.prepare()).build().primaryTx; // Sanity bound only — exact value depends on virtual-share scaling. expect(tx.action.args.maxSharePrice).toBeGreaterThan(0n); @@ -178,7 +178,7 @@ describe("SupplyBlue", () => { const tx = (await supply.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; expect(tx.value).toBe(0n); await client.sendTransaction(tx); }, @@ -225,7 +225,7 @@ describe("SupplyBlue", () => { const requirements = (await supply.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = (await supply.prepare()).build().primaryTransaction; + const tx = (await supply.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -281,7 +281,7 @@ describe("SupplyBlue", () => { } await client.sendTransaction(approval); - const tx = (await supply.prepare()).build().primaryTransaction; + const tx = (await supply.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts b/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts index 5d2fde522..4265eab96 100644 --- a/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts +++ b/packages/morpho-sdk/test/actions/blue/supplyCollateral.test.ts @@ -54,8 +54,7 @@ describe("SupplyCollateralBlue", () => { await client.sendTransaction(approveTx); - const tx = (await supplyCollateral.prepare()).build() - .primaryTransaction; + const tx = (await supplyCollateral.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -110,8 +109,7 @@ describe("SupplyCollateralBlue", () => { const requirements = (await supplyCollateral.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = (await supplyCollateral.prepare()).build() - .primaryTransaction; + const tx = (await supplyCollateral.prepare()).build().primaryTx; await client.sendTransaction(tx); }, @@ -157,8 +155,7 @@ describe("SupplyCollateralBlue", () => { const requirements = (await supplyCollateral.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = (await supplyCollateral.prepare()).build() - .primaryTransaction; + const tx = (await supplyCollateral.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); @@ -222,8 +219,7 @@ describe("SupplyCollateralBlue", () => { await client.sendTransaction(approveTx); - const tx = (await supplyCollateral.prepare()).build() - .primaryTransaction; + const tx = (await supplyCollateral.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); @@ -292,7 +288,7 @@ describe("SupplyCollateralBlue", () => { const tx = (await supplyCollateral.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, }); @@ -377,7 +373,7 @@ describe("SupplyCollateralBlue", () => { const tx = (await supplyCollateral.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts b/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts index 71753f8c8..36f263a62 100644 --- a/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts +++ b/packages/morpho-sdk/test/actions/blue/supplyCollateralBorrow.test.ts @@ -66,7 +66,7 @@ describe("SupplyCollateralBorrowBlue", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = (await scb.prepare()).build().primaryTransaction; + const tx = (await scb.prepare()).build().primaryTx; await client.sendTransaction(tx); }, @@ -135,7 +135,7 @@ describe("SupplyCollateralBorrowBlue", () => { await client.sendTransaction(authorization); - const tx = (await scb.prepare()).build().primaryTransaction; + const tx = (await scb.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -213,7 +213,7 @@ describe("SupplyCollateralBorrowBlue", () => { await client.sendTransaction(approval); await client.sendTransaction(authorization); - const tx = (await scb.prepare()).build().primaryTransaction; + const tx = (await scb.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); await client.sendTransaction(tx); }, @@ -291,7 +291,7 @@ describe("SupplyCollateralBorrowBlue", () => { } await client.sendTransaction(approval); - const tx = (await scb.prepare()).build().primaryTransaction; + const tx = (await scb.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -382,7 +382,7 @@ describe("SupplyCollateralBorrowBlue", () => { const tx = (await scb.prepare()).build([ permitSignature, authorizationSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, }); @@ -482,7 +482,7 @@ describe("SupplyCollateralBorrowBlue", () => { const tx = (await scb.prepare()).build([ permit2Signature, authorizationSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/blue/withdraw.test.ts b/packages/morpho-sdk/test/actions/blue/withdraw.test.ts index dcd254ae2..2f6607f97 100644 --- a/packages/morpho-sdk/test/actions/blue/withdraw.test.ts +++ b/packages/morpho-sdk/test/actions/blue/withdraw.test.ts @@ -62,7 +62,7 @@ describe("WithdrawBlue", () => { } await client.sendTransaction(authorization); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -117,7 +117,7 @@ describe("WithdrawBlue", () => { } await client.sendTransaction(authorization); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; expect(tx.value).toBe(0n); await client.sendTransaction(tx); @@ -155,7 +155,7 @@ describe("WithdrawBlue", () => { positionData, }); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; expect(tx.action.args.receiver).toBe(receiver); }); @@ -242,7 +242,7 @@ describe("WithdrawBlue", () => { } await client.sendTransaction(authorization); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; expect(tx.value).toBe(0n); expect(tx.action.args.reallocationFee).toBe(0n); @@ -313,7 +313,7 @@ describe("WithdrawBlue", () => { } await client.sendTransaction(authorization); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; expect(tx.value).toBe(0n); expect(tx.action.args.shares).toBe(withdrawShares); expect(tx.action.args.assets).toBe(0n); @@ -388,7 +388,7 @@ describe("WithdrawBlue", () => { } await client.sendTransaction(authorization); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; expect(tx.action.args.receiver).toBe(receiver); await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts b/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts index ad3279a13..28ee30be1 100644 --- a/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts +++ b/packages/morpho-sdk/test/actions/blue/withdrawCollateral.test.ts @@ -48,7 +48,7 @@ describe("WithdrawCollateralBlue", () => { positionData, }); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -97,7 +97,7 @@ describe("WithdrawCollateralBlue", () => { positionData, }); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/metadata.test.ts b/packages/morpho-sdk/test/actions/metadata.test.ts index eeb712ef8..87a9f093f 100644 --- a/packages/morpho-sdk/test/actions/metadata.test.ts +++ b/packages/morpho-sdk/test/actions/metadata.test.ts @@ -43,7 +43,7 @@ describe("Metadata", () => { vaultData, }); - const tx_1 = (await deposit.prepare()).build().primaryTransaction; + const tx_1 = (await deposit.prepare()).build().primaryTx; expect(tx_1.data).toContain("25AFEA44"); const position = tx_1.data.indexOf("25AFEA44"); expect(position).toBeGreaterThanOrEqual(8); @@ -64,7 +64,7 @@ describe("Metadata", () => { throw new Error("Approve transaction is not an approval transaction"); } - const tx_2 = (await deposit.prepare()).build().primaryTransaction; + const tx_2 = (await deposit.prepare()).build().primaryTx; await client.sendTransaction(approveTx); await client.sendTransaction(tx_2); }, @@ -115,7 +115,7 @@ describe("Metadata", () => { vaultData, }); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; expect(tx.data).toContain("25AFEA44"); const position = tx.data.indexOf("25AFEA44"); expect(position).toBeGreaterThanOrEqual(8); diff --git a/packages/morpho-sdk/test/actions/midnight/requirements.test.ts b/packages/morpho-sdk/test/actions/midnight/requirements.test.ts index a1aeed899..73b35be3d 100644 --- a/packages/morpho-sdk/test/actions/midnight/requirements.test.ts +++ b/packages/morpho-sdk/test/actions/midnight/requirements.test.ts @@ -146,7 +146,7 @@ const prepareTakeableOffer = async (params: { reservedCollateralAssets: 0n, }); await params.client.sendTransaction({ - ...(await supply.prepare()).build().primaryTransaction, + ...(await supply.prepare()).build().primaryTx, account: offerMaker, }); await expect( @@ -343,7 +343,7 @@ describe("Midnight requirements on fork", () => { await client.sendTransaction(approval); await expect((await output.prepare()).requirements).toEqual([]); - expect((await output.prepare()).build().primaryTransaction.to).toBe( + expect((await output.prepare()).build().primaryTx.to).toBe( getChainAddress(ChainId.BaseMainnet, "midnight"), ); }); @@ -383,9 +383,7 @@ describe("Midnight requirements on fork", () => { } await expect((await output.prepare()).requirements).toEqual([]); - await client.sendTransaction( - (await output.prepare()).build().primaryTransaction, - ); + await client.sendTransaction((await output.prepare()).build().primaryTx); await expect( client.readContract({ address: midnight, @@ -422,9 +420,7 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction( - (await supply.prepare()).build().primaryTransaction, - ); + await client.sendTransaction((await supply.prepare()).build().primaryTx); await expect( client.readContract({ address: midnight, @@ -457,9 +453,7 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction( - (await borrow.prepare()).build().primaryTransaction, - ); + await client.sendTransaction((await borrow.prepare()).build().primaryTx); await expect( client.readContract({ address: midnight, @@ -504,9 +498,7 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction( - (await borrow.prepare()).build().primaryTransaction, - ); + await client.sendTransaction((await borrow.prepare()).build().primaryTx); await expect( client.readContract({ address: midnight, @@ -540,9 +532,7 @@ describe("Midnight requirements on fork", () => { } await client.sendTransaction(requirement); } - await client.sendTransaction( - (await repay.prepare()).build().primaryTransaction, - ); + await client.sendTransaction((await repay.prepare()).build().primaryTx); await expect( client.readContract({ address: midnight, diff --git a/packages/morpho-sdk/test/actions/requirements/approval.test.ts b/packages/morpho-sdk/test/actions/requirements/approval.test.ts index f03c7fd8b..71b2d5836 100644 --- a/packages/morpho-sdk/test/actions/requirements/approval.test.ts +++ b/packages/morpho-sdk/test/actions/requirements/approval.test.ts @@ -51,7 +51,7 @@ describe("Approval", () => { await client.sendTransaction(requirements[0]); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; await client.sendTransaction(tx); }, @@ -109,7 +109,7 @@ describe("Approval", () => { await client.sendTransaction(requirements[0]); await client.sendTransaction(requirements[1]); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/requirements/permit.test.ts b/packages/morpho-sdk/test/actions/requirements/permit.test.ts index b0e6405f8..962371b53 100644 --- a/packages/morpho-sdk/test/actions/requirements/permit.test.ts +++ b/packages/morpho-sdk/test/actions/requirements/permit.test.ts @@ -65,7 +65,7 @@ describe("Permit", () => { const tx_1 = (await deposit.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx_1); }, diff --git a/packages/morpho-sdk/test/actions/requirements/permit2.test.ts b/packages/morpho-sdk/test/actions/requirements/permit2.test.ts index dba511c6e..47c7e82d3 100644 --- a/packages/morpho-sdk/test/actions/requirements/permit2.test.ts +++ b/packages/morpho-sdk/test/actions/requirements/permit2.test.ts @@ -103,7 +103,7 @@ describe("Permit2", () => { const tx = (await deposit.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, @@ -183,7 +183,7 @@ describe("Permit2", () => { const tx = (await deposit.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, @@ -276,8 +276,7 @@ describe("Permit2", () => { ); await client.sendTransaction( - (await deposit.prepare()).build([requirementSignature]) - .primaryTransaction, + (await deposit.prepare()).build([requirementSignature]).primaryTx, ); }, }); @@ -373,7 +372,7 @@ describe("Permit2", () => { const tx = (await deposit.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, @@ -466,7 +465,7 @@ describe("Permit2", () => { const tx = (await deposit.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts b/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts index 1637988ee..eb576d917 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/deposit.test.ts @@ -49,7 +49,7 @@ describe("DepositVaultV1", () => { vaultData, }); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; const requirements = (await deposit.prepare()).requirements; const approveTx = requirements[0]; @@ -117,7 +117,7 @@ describe("DepositVaultV1", () => { vaultData, }); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(2); @@ -212,7 +212,7 @@ describe("DepositVaultV1", () => { const tx = (await deposit.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, @@ -313,7 +313,7 @@ describe("DepositVaultV1", () => { const tx = (await deposit.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, @@ -411,8 +411,7 @@ describe("DepositVaultV1", () => { ); await client.sendTransaction( - (await deposit.prepare()).build([requirementSignature]) - .primaryTransaction, + (await deposit.prepare()).build([requirementSignature]).primaryTx, ); }, }); @@ -466,7 +465,7 @@ describe("DepositVaultV1", () => { throw new Error("Approve transaction not found"); } await client.sendTransaction(approveTx); - const depositTx = (await deposit.prepare()).build().primaryTransaction; + const depositTx = (await deposit.prepare()).build().primaryTx; await client.sendTransaction(depositTx); const withdraw = vaultV1.withdraw({ @@ -474,8 +473,7 @@ describe("DepositVaultV1", () => { amount: amount, }); - const withdrawTx = (await withdraw.prepare()).build() - .primaryTransaction; + const withdrawTx = (await withdraw.prepare()).build().primaryTx; await client.sendTransaction(withdrawTx); }, diff --git a/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts b/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts index 7c3314fac..bf6263b3a 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/migrateToV2.test.ts @@ -63,7 +63,7 @@ describe("MigrateToV2 VaultV1", () => { await client.sendTransaction(approveTx); - const tx = (await migrate.prepare()).build().primaryTransaction; + const tx = (await migrate.prepare()).build().primaryTx; await client.sendTransaction(tx); }, }); @@ -144,7 +144,7 @@ describe("MigrateToV2 VaultV1", () => { const tx = (await migrate.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; await client.sendTransaction(tx); }, }); diff --git a/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts b/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts index 143a65bb8..43e407c06 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/redeem.test.ts @@ -36,7 +36,7 @@ describe("Redeem VaultV1", () => { userAddress: client.account.address, shares, }); - const tx = (await redeem.prepare()).build().primaryTransaction; + const tx = (await redeem.prepare()).build().primaryTx; await client.sendTransaction(tx); }, @@ -92,7 +92,7 @@ describe("Redeem VaultV1", () => { } await client.sendTransaction(approveTx); await client.sendTransaction( - (await deposit.prepare()).build().primaryTransaction, + (await deposit.prepare()).build().primaryTx, ); const shares = await client.balanceOf({ @@ -104,7 +104,7 @@ describe("Redeem VaultV1", () => { shares, }); await client.sendTransaction( - (await redeem.prepare()).build().primaryTransaction, + (await redeem.prepare()).build().primaryTx, ); }, }); @@ -150,7 +150,7 @@ describe("Redeem VaultV1", () => { shares: redeemShares, }); await client.sendTransaction( - (await redeem.prepare()).build().primaryTransaction, + (await redeem.prepare()).build().primaryTx, ); }, }); diff --git a/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts b/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts index 5c8ff6a88..21c0a699e 100644 --- a/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV1/withdraw.test.ts @@ -34,7 +34,7 @@ describe("Withdraw VaultV1", () => { userAddress: client.account.address, amount: assets, }); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; await client.sendTransaction(tx); }, @@ -82,7 +82,7 @@ describe("Withdraw VaultV1", () => { amount: firstWithdraw, }); await client.sendTransaction( - (await withdraw1.prepare()).build().primaryTransaction, + (await withdraw1.prepare()).build().primaryTx, ); const withdraw2 = vaultV1.withdraw({ @@ -90,7 +90,7 @@ describe("Withdraw VaultV1", () => { amount: secondWithdraw, }); await client.sendTransaction( - (await withdraw2.prepare()).build().primaryTransaction, + (await withdraw2.prepare()).build().primaryTx, ); }, }); diff --git a/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts b/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts index 5b41bf5c8..cf82c4166 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/deposit.test.ts @@ -36,7 +36,7 @@ describe("DepositVaultV2", () => { vaultData, }); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; const requirements = (await deposit.prepare()).requirements; const approveTx = requirements[0]; diff --git a/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts b/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts index 21581ff2f..87e00ae8e 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/forceRedeem.test.ts @@ -72,7 +72,7 @@ describe("ForceRedeem VaultV2", () => { redeem: { shares: redeemShares }, userAddress: client.account.address, }); - const tx_1 = (await forceRedeem.prepare()).build().primaryTransaction; + const tx_1 = (await forceRedeem.prepare()).build().primaryTx; const tx_2 = vaultV2ForceRedeem({ vault: { address: ReEcosystemUsdcVaultV2.address }, @@ -134,7 +134,7 @@ describe("ForceRedeem VaultV2", () => { redeem: { shares: redeemShares }, userAddress: client.account.address, }); - const tx = (await forceRedeem.prepare()).build().primaryTransaction; + const tx = (await forceRedeem.prepare()).build().primaryTx; const { vaults: { @@ -199,7 +199,7 @@ describe("ForceRedeem VaultV2", () => { redeem: { shares: redeemShares }, userAddress: client.account.address, }); - const tx = (await forceRedeem.prepare()).build().primaryTransaction; + const tx = (await forceRedeem.prepare()).build().primaryTx; const { vaults: { diff --git a/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts b/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts index 64a1fc288..f1397ead6 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/forceWithdraw.test.ts @@ -59,7 +59,7 @@ describe("ForceWithdraw VaultV2", () => { withdraw: { amount: assetsWithdraw }, userAddress: client.account.address, }); - const tx_1 = (await forceWithdraw.prepare()).build().primaryTransaction; + const tx_1 = (await forceWithdraw.prepare()).build().primaryTx; const tx_2 = vaultV2ForceWithdraw({ vault: { address: ReEcosystemUsdcVaultV2.address }, @@ -124,7 +124,7 @@ describe("ForceWithdraw VaultV2", () => { withdraw: { amount: assets }, userAddress: client.account.address, }); - const tx = (await forceWithdraw.prepare()).build().primaryTransaction; + const tx = (await forceWithdraw.prepare()).build().primaryTx; const { vaults: { @@ -190,7 +190,7 @@ describe("ForceWithdraw VaultV2", () => { withdraw: { amount: withdrawAssets }, userAddress: client.account.address, }); - const tx = (await forceWithdraw.prepare()).build().primaryTransaction; + const tx = (await forceWithdraw.prepare()).build().primaryTx; const { vaults: { diff --git a/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts b/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts index aedf717b4..ef44cab15 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/redeem.test.ts @@ -30,7 +30,7 @@ describe("Redeem VaultV2", () => { userAddress: client.account.address, shares, }); - const tx = (await redeem.prepare()).build().primaryTransaction; + const tx = (await redeem.prepare()).build().primaryTx; await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts b/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts index be8877457..cc4fc06e7 100644 --- a/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts +++ b/packages/morpho-sdk/test/actions/vaultV2/withdraw.test.ts @@ -31,7 +31,7 @@ describe("Withdraw VaultV2", () => { userAddress: client.account.address, amount: assets, }); - const tx = (await withdraw.prepare()).build().primaryTransaction; + const tx = (await withdraw.prepare()).build().primaryTx; await client.sendTransaction(tx); }, diff --git a/packages/morpho-sdk/test/actions/wrap.test.ts b/packages/morpho-sdk/test/actions/wrap.test.ts index b79cfcdc3..bb7633084 100644 --- a/packages/morpho-sdk/test/actions/wrap.test.ts +++ b/packages/morpho-sdk/test/actions/wrap.test.ts @@ -52,7 +52,7 @@ describe("WrapNative - VaultV1", () => { const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(0n); @@ -121,7 +121,7 @@ describe("WrapNative - VaultV1", () => { const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -205,7 +205,7 @@ describe("WrapNative - VaultV1", () => { const tx = (await deposit.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -309,7 +309,7 @@ describe("WrapNative - VaultV2", () => { const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(0n); @@ -378,7 +378,7 @@ describe("WrapNative - VaultV2", () => { const requirements = (await deposit.prepare()).requirements; expect(requirements.length).toBe(0); - const tx = (await deposit.prepare()).build().primaryTransaction; + const tx = (await deposit.prepare()).build().primaryTx; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); @@ -462,7 +462,7 @@ describe("WrapNative - VaultV2", () => { const tx = (await deposit.prepare()).build([ requirementSignature, - ]).primaryTransaction; + ]).primaryTx; expect(tx.value).toEqual(nativeAmount); expect(tx.action.args.nativeAmount).toEqual(nativeAmount); expect(tx.action.args.amount).toEqual(amount); diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index 53d3cd5dd..95bdf0c50 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -75,7 +75,7 @@ function mockTransactionPlan( const getRequirementRequests = vi.fn( async (_requestOptions?: unknown) => requirements, ); - const buildPrimaryTransaction = vi.fn( + const buildPrimaryTx = vi.fn( (_signatures?: readonly RequirementSignature[]) => tx, ); const prepare = vi.fn(async (options?: { requestOptions?: unknown }) => { @@ -84,16 +84,16 @@ function mockTransactionPlan( ); const primaryStep = { id: "primary", - kind: "transaction", + kind: "tx", phase: "primary", - transaction: tx, + tx, } as const; return { requirements: resolvedRequirements, primaryStep, - primaryTransaction: tx, + primaryTx: tx, signatureRequests: [], - transactionSteps: [primaryStep], + txSteps: [primaryStep], calls: [{ to: tx.to, value: tx.value, data: tx.data }], steps: [ ...resolvedRequirements.map((requirement, index) => @@ -105,38 +105,38 @@ function mockTransactionPlan( } : { id: `request-${index}`, - kind: "transaction", + kind: "tx", phase: "preparation", - transaction: requirement, + tx: requirement, }, ), primaryStep, ], build: (signatures?: readonly RequirementSignature[]) => { - const primaryTransaction = buildPrimaryTransaction(signatures); + const primaryTx = buildPrimaryTx(signatures); const builtPrimaryStep = { id: "primary", - kind: "transaction", + kind: "tx", phase: "primary", - transaction: primaryTransaction, + tx: primaryTx, } as const; return { primaryStep: builtPrimaryStep, - primaryTransaction, + primaryTx, signatureRequests: [], - transactionSteps: [builtPrimaryStep], + txSteps: [builtPrimaryStep], calls: [ { - to: primaryTransaction.to, - value: primaryTransaction.value, - data: primaryTransaction.data, + to: primaryTx.to, + value: primaryTx.value, + data: primaryTx.data, }, ], }; }, }; }); - return { prepare, getRequirementRequests, buildPrimaryTransaction }; + return { prepare, getRequirementRequests, buildPrimaryTx }; } const supplyAction = mockTransactionPlan(SUPPLY_TX, [ @@ -592,9 +592,7 @@ describe.sequential("MorphoProtocolEvm", () => { await protocol.borrow({ token: TOKEN, amount: 100_000n }); - expect(borrowAction.buildPrimaryTransaction).toHaveBeenCalledWith( - undefined, - ); + expect(borrowAction.buildPrimaryTx).toHaveBeenCalledWith(undefined); }); test("should fold a signed authorization into the bundle when provided", async () => { @@ -611,7 +609,7 @@ describe.sequential("MorphoProtocolEvm", () => { requirementSignature, }); - expect(borrowAction.buildPrimaryTransaction).toHaveBeenCalledWith([ + expect(borrowAction.buildPrimaryTx).toHaveBeenCalledWith([ requirementSignature, ]); }); diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 325661d54..47eae9727 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -422,7 +422,7 @@ async function getTransactionPlanTx< signatures?: TSignatures, ): Promise { const executable = (await plan.prepare()).build(signatures); - return toWdkTransaction(executable.primaryTransaction); + return toWdkTransaction(executable.primaryTx); } /** From 7bb2801190fa47d55196c2b5e74ddb55393d584a Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 17 Jul 2026 11:01:15 +0200 Subject: [PATCH 08/12] refactor(morpho-sdk): strengthen transaction plan signature types --- .changeset/transaction-plan.md | 2 +- .changeset/wdk-transaction-plan.md | 4 +- ...-06-03-midnight-action-output-interface.md | 3 +- .../morpho-sdk/src/entities/blue/AGENTS.md | 2 +- .../src/entities/midnight/midnight.test.ts | 112 ++++++++++-------- .../src/entities/midnight/midnight.ts | 53 ++++----- .../morpho-sdk/src/entities/midnight/types.ts | 24 ++-- .../transactionPlan/TransactionPlan.test.ts | 27 ++++- .../src/transactionPlan/TransactionPlan.ts | 103 +++++++--------- .../morpho-sdk/src/transactionPlan/types.ts | 22 +++- packages/morpho-sdk/src/types/AGENTS.md | 4 +- .../src/morpho-protocol-evm.test.ts | 24 +++- .../src/morpho-protocol-evm.ts | 32 +++-- 13 files changed, 228 insertions(+), 184 deletions(-) diff --git a/.changeset/transaction-plan.md b/.changeset/transaction-plan.md index 352334dd2..a438d12c8 100644 --- a/.changeset/transaction-plan.md +++ b/.changeset/transaction-plan.md @@ -2,4 +2,4 @@ "@morpho-org/morpho-sdk": major --- -Replace entity action outputs' `{ buildTx, getRequirements }` shape with `TransactionPlan`, exposing semantic transaction/signature steps, ordered executable txs, and intent-based helpers for app labels and flow branching. +Replace entity action outputs' `{ buildTx, getRequirements }` shape with `TransactionPlan`, exposing semantic transaction/signature steps, ordered executable txs, and intent-based helpers for app labels and flow branching. Derive each prepared plan's accepted signature array from its requirement type so unrelated signature kinds are rejected at compile time. diff --git a/.changeset/wdk-transaction-plan.md b/.changeset/wdk-transaction-plan.md index 7b3e32bf8..cf9bd9963 100644 --- a/.changeset/wdk-transaction-plan.md +++ b/.changeset/wdk-transaction-plan.md @@ -1,5 +1,5 @@ --- -"@morpho-org/wdk-protocol-lending-morpho-evm": patch +"@morpho-org/wdk-protocol-lending-morpho-evm": major --- -Adapt Morpho SDK-backed flows to consume `TransactionPlan` outputs while preserving the WDK-facing requirement and transaction methods. +Adapt Morpho SDK-backed flows to consume `TransactionPlan` outputs while preserving the WDK-facing requirement and transaction methods. Narrow each operation's requirement signature option to the signature kind produced by that operation. diff --git a/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md b/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md index 934f49a70..3da321acb 100644 --- a/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md +++ b/docs/tibs/TIB-2026-06-03-midnight-action-output-interface.md @@ -110,8 +110,7 @@ wallet signing to the integrator, and builds one executable sequence after signa const plan: TransactionPlan< MempoolSubmitOffersAction, undefined, - ActionRequirement, - MidnightActionSignatures + MidnightMakeOffersRequest > = await midnight.makeLend(params); const prepared = await plan.prepare(); diff --git a/packages/morpho-sdk/src/entities/blue/AGENTS.md b/packages/morpho-sdk/src/entities/blue/AGENTS.md index 43f58d6e8..cecbc6a6b 100644 --- a/packages/morpho-sdk/src/entities/blue/AGENTS.md +++ b/packages/morpho-sdk/src/entities/blue/AGENTS.md @@ -23,6 +23,6 @@ - ERC-20 approval for **GeneralAdapter1** on the collateral token (any path that supplies collateral) or the loan token (`supply`, `repay`, `repayWithdrawCollateral`). The approved amount is the **ERC-20 portion actually pulled**, not the total: for a native-funded repay it is `amount` (assets mode) or `max(0, toBorrowAssets(shares) āˆ’ nativeAmount)` (shares mode — clamped at 0 so a `nativeAmount` that covers or exceeds the borrow assets pulls nothing). A fully-native repay pulls no ERC-20, so no approval requirement is emitted; in shares mode any wrapped native beyond the on-chain repay is skimmed back to the receiver. - `morpho.setAuthorization(generalAdapter1, true)` when authorization is not yet set on Morpho — read via `publicActions`. Required for `borrow`, `supplyCollateralBorrow`, `repayWithdrawCollateral`, and `withdraw` (loan-asset). -When `supportSignature` is enabled on the client, the authorization request is returned as a signable `Requirement` instead of a transaction; signing it produces an `AuthorizationRequirementSignature` that `PreparedTransactionPlan.build(...)` consumes and folds into the bundle as a `setAuthorizationWithSig` call, so no standalone authorization transaction is needed. The prepared plan accepts a `readonly RequirementSignature[]` and splits permit vs. authorization signatures via `isPermitSignature` / `isAuthorizationSignature`. +When `supportSignature` is enabled on the client, the authorization request is returned as a signable `Requirement` instead of a transaction; signing it produces an `AuthorizationRequirementSignature` that `PreparedTransactionPlan.build(...)` consumes and folds into the bundle as a `setAuthorizationWithSig` call, so no standalone authorization transaction is needed. The prepared plan derives its accepted signature union from its request type and splits permit vs. authorization signatures via `isPermitSignature` / `isAuthorizationSignature`. `withdrawCollateral` has no requirements. `repay` and `supply` need only loan-token approval (native wrapping requires the loan token to be the chain's wNative). Loan-asset `withdraw` needs only the Morpho authorization. diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts index c149a39f0..b49a6c168 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts @@ -73,15 +73,11 @@ import { UnpreparedMidnightOfferRootSignatureError, } from "../../types/error.js"; import { MorphoMidnight } from "./midnight.js"; -import type { - MakeOffersOutput, - MidnightActionSignatures, - OffersData, -} from "./types.js"; +import type { MakeOffersOutput, OffersData } from "./types.js"; type BuildMakeOffersOutputTx = (params: { readonly offersData: OffersData; - readonly signatures?: MidnightActionSignatures; + readonly signatures?: readonly MidnightOfferRootSignature[]; readonly signedPayloads?: ReadonlyMap; readonly metadata?: { readonly origin: string }; }) => Promise>>; @@ -223,24 +219,26 @@ const offerRootSignature = ( }, }); -const unexpectedSignature = { - action: { - type: "authorization", +const unexpectedSignature = [ + { + action: { + type: "authorization", + args: { + authorized: midnightAddresses.midnightBundles, + isAuthorized: true, + deadline: 123n, + }, + }, args: { + owner: midnightAddresses.taker, authorized: midnightAddresses.midnightBundles, isAuthorized: true, + nonce: 42n, + signature: "0x1234", deadline: 123n, }, }, - args: { - owner: midnightAddresses.taker, - authorized: midnightAddresses.midnightBundles, - isAuthorized: true, - nonce: 42n, - signature: "0x1234", - deadline: 123n, - }, -} as unknown as MidnightActionSignatures; +] as unknown as readonly MidnightOfferRootSignature[]; const client = { viemClient: { chain: { id: midnightChainId } }, @@ -1377,7 +1375,7 @@ describe("MorphoMidnight", () => { }); expect(output).toBeInstanceOf(TransactionPlan); const signature = await signOfferRootRequirement(output); - const tx = (await output.prepare()).build(signature).primaryTx; + const tx = (await output.prepare()).build([signature]).primaryTx; expect(output.groups).toEqual(data.groups); expect(output.root).toBe(data.tree.root); @@ -1693,7 +1691,7 @@ describe("MorphoMidnight", () => { }, }); const signature = await signOfferRootRequirement(output); - const tx = (await output.prepare()).build(signature).primaryTx; + const tx = (await output.prepare()).build([signature]).primaryTx; expect(tx.action.args.maker).toBe(data.accountAddress); }); @@ -1806,7 +1804,7 @@ describe("MorphoMidnight", () => { validation: offerValidation, }); const signature = await signOfferRootRequirement(output); - const tx = (await output.prepare()).build(signature).primaryTx; + const tx = (await output.prepare()).build([signature]).primaryTx; expect(output.groups).toEqual(data.groups); expect(tx.action.args.maker).toBe(offerSignerAccount.address); @@ -2003,7 +2001,7 @@ describe("MorphoMidnight", () => { const data = offersData(); const tx = await buildMakeOffersOutputTx({ offersData: data, - signatures: offerRootSignature(data), + signatures: [offerRootSignature(data)], }); expect(tx.action.args).toEqual({ @@ -2020,7 +2018,7 @@ describe("MorphoMidnight", () => { const data = offersData(); const tx = await buildMakeOffersOutputTx({ offersData: data, - signatures: offerRootSignature(data), + signatures: [offerRootSignature(data)], metadata: { origin: "a1b2c3d4" }, }); @@ -2033,10 +2031,12 @@ describe("MorphoMidnight", () => { const signature = offerRootSignature(data); const tx = await buildMakeOffersOutputTx({ offersData: data, - signatures: { - ...signature, - args: { ...signature.args, payload: "0xdeadbeef" }, - }, + signatures: [ + { + ...signature, + args: { ...signature.args, payload: "0xdeadbeef" }, + }, + ], }); expect(tx.data).toBe("0x1234"); @@ -2071,28 +2071,32 @@ describe("MorphoMidnight", () => { await expect( buildMakeOffersOutputTx({ offersData: data, - signatures: { - ...signature, - args: { - ...signature.args, - root: otherRoot, + signatures: [ + { + ...signature, + args: { + ...signature.args, + root: otherRoot, + }, }, - }, + ], }), ).rejects.toThrow(MidnightOfferRootMismatchError); await expect( buildMakeOffersOutputTx({ offersData: data, - signatures: { - ...signature, - action: { - ...signature.action, - args: { - ...signature.action.args, - root: otherRoot, + signatures: [ + { + ...signature, + action: { + ...signature.action, + args: { + ...signature.action.args, + root: otherRoot, + }, }, }, - }, + ], }), ).rejects.toThrow(MidnightOfferRootMismatchError); }); @@ -2103,9 +2107,11 @@ describe("MorphoMidnight", () => { await expect( buildMakeOffersOutputTx({ offersData: data, - signatures: offerRootSignature(data, { - owner: midnightAddresses.taker, - }), + signatures: [ + offerRootSignature(data, { + owner: midnightAddresses.taker, + }), + ], }), ).rejects.toThrow(MidnightOfferRootOwnerMismatchError); }); @@ -2116,9 +2122,11 @@ describe("MorphoMidnight", () => { await expect( buildMakeOffersOutputTx({ offersData: data, - signatures: offerRootSignature(data, { - ratifier: midnightAddresses.setterRatifier, - }), + signatures: [ + offerRootSignature(data, { + ratifier: midnightAddresses.setterRatifier, + }), + ], }), ).rejects.toThrow(MidnightOfferRootRatifierMismatchError); }); @@ -2129,9 +2137,11 @@ describe("MorphoMidnight", () => { await expect( buildMakeOffersOutputTx({ offersData: data, - signatures: offerRootSignature(data, { - offers: data.tree.offers.length + 1, - }), + signatures: [ + offerRootSignature(data, { + offers: data.tree.offers.length + 1, + }), + ], }), ).rejects.toThrow(MidnightOfferRootOfferCountMismatchError); }); @@ -2142,7 +2152,7 @@ describe("MorphoMidnight", () => { await expect( buildMakeOffersOutputTx({ offersData: data, - signatures: offerRootSignature(data), + signatures: [offerRootSignature(data)], signedPayloads: new Map(), }), ).rejects.toThrow(UnpreparedMidnightOfferRootSignatureError); diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.ts b/packages/morpho-sdk/src/entities/midnight/midnight.ts index 000dd9ccd..05aabebb1 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.ts @@ -44,7 +44,6 @@ import { TransactionPlan } from "../../transactionPlan/index.js"; import type { MorphoClientType } from "../../types/client.js"; import { AccrualPositionUserMismatchError, - type ActionRequirement, InsufficientMidnightWithdrawableLiquidityError, MarketIdMismatchError, type MempoolSubmitOffersAction, @@ -71,6 +70,7 @@ import { NoMidnightCreditToRedeemError, NonPositiveInputError, selectRequirementSignatures, + type TxRequirement, UnknownMidnightRatifierError, UnpreparedMidnightOfferRootSignatureError, } from "../../types/index.js"; @@ -81,7 +81,7 @@ import type { MakeOffersOutput, MakeOffersParams, MidnightActionOutput, - MidnightActionSignatures, + MidnightMakeOffersRequest, OffersData, RedeemParams, RepayWithdrawCollateralParams, @@ -412,10 +412,10 @@ export class MorphoMidnight { return new TransactionPlan< MidnightTakeLendAction, undefined, - ActionRequirement + TxRequirement >({ getRequirementRequests: async () => { - const requirements: ActionRequirement[] = [ + const requirements: TxRequirement[] = [ ...(await getMidnightApprovalRequirements({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -500,10 +500,10 @@ export class MorphoMidnight { return new TransactionPlan< MidnightTakeBorrowAction, undefined, - ActionRequirement + TxRequirement >({ getRequirementRequests: async () => { - const requirements: ActionRequirement[] = []; + const requirements: TxRequirement[] = []; const authorization = await getMidnightAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -586,10 +586,10 @@ export class MorphoMidnight { return new TransactionPlan< MidnightSupplyCollateralTakeBorrowAction, undefined, - ActionRequirement + TxRequirement >({ getRequirementRequests: async () => { - const requirements: ActionRequirement[] = [ + const requirements: TxRequirement[] = [ ...(await getMidnightApprovalRequirements({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -668,7 +668,7 @@ export class MorphoMidnight { return new TransactionPlan< MidnightSupplyCollateralAction, undefined, - ActionRequirement + TxRequirement >({ getRequirementRequests: async () => await getMidnightApprovalRequirements({ @@ -753,7 +753,7 @@ export class MorphoMidnight { return this.createMakeOffersOutput({ offersData: data, getRequirementRequests: async () => { - const requirements: ActionRequirement[] = []; + const requirements: MidnightMakeOffersRequest[] = []; requirements.push( ...(await getMidnightApprovalRequirements({ viemClient: this.client.viemClient, @@ -889,7 +889,7 @@ export class MorphoMidnight { return this.createMakeOffersOutput({ offersData: data, getRequirementRequests: async () => { - const requirements: ActionRequirement[] = [ + const requirements: MidnightMakeOffersRequest[] = [ ...(await getMidnightApprovalRequirements({ viemClient: this.client.viemClient, chainId: this.chainId, @@ -981,11 +981,7 @@ export class MorphoMidnight { }); } - return new TransactionPlan< - MidnightRedeemAction, - undefined, - ActionRequirement - >({ + return new TransactionPlan({ getRequirementRequests: async () => [], buildPrimaryTx: () => midnightRedeem({ @@ -1067,10 +1063,10 @@ export class MorphoMidnight { return new TransactionPlan< MidnightRepayWithdrawCollateralAction, undefined, - ActionRequirement + TxRequirement >({ getRequirementRequests: async () => { - const requirements: ActionRequirement[] = []; + const requirements: TxRequirement[] = []; if (params.repayAssets > 0n) { requirements.push( ...(await getMidnightApprovalRequirements({ @@ -1132,7 +1128,7 @@ export class MorphoMidnight { return new TransactionPlan< MidnightCancelOfferAction, undefined, - ActionRequirement + TxRequirement >({ getRequirementRequests: async () => [], buildPrimaryTx: () => @@ -1148,7 +1144,7 @@ export class MorphoMidnight { private createMakeOffersOutput(params: { readonly offersData: OffersData; readonly getRequirementRequests: () => Promise< - readonly ActionRequirement[] + readonly MidnightMakeOffersRequest[] >; readonly signedPayloads: ReadonlyMap; }): MakeOffersOutput { @@ -1157,22 +1153,15 @@ export class MorphoMidnight { new TransactionPlan< MempoolSubmitOffersAction, undefined, - ActionRequirement, - MidnightActionSignatures + MidnightMakeOffersRequest >({ getRequirementRequests: params.getRequirementRequests, previewPrimaryTx: false, buildPrimaryTx: (signatures) => { - const collectedSignatures = - signatures == null - ? undefined - : "action" in signatures - ? [signatures] - : signatures; let payload = data.setterPayload; if (data.ratifierType === "ecrecover") { const { midnightOfferRoot: signature } = - selectRequirementSignatures(collectedSignatures, { + selectRequirementSignatures(signatures, { midnightOfferRoot: true, }); @@ -1224,7 +1213,7 @@ export class MorphoMidnight { } payload = signedPayload; } else { - selectRequirementSignatures(collectedSignatures, {}); + selectRequirementSignatures(signatures, {}); } if (payload == null) @@ -1254,9 +1243,9 @@ export class MorphoMidnight { private async getRatifierRequirements(params: { readonly offersData: OffersData; readonly signedPayloads: Map; - }): Promise { + }): Promise { const data = params.offersData; - const requirements: ActionRequirement[] = []; + const requirements: MidnightMakeOffersRequest[] = []; const authorization = await getMidnightAuthorizationRequirement({ viemClient: this.client.viemClient, chainId: this.chainId, diff --git a/packages/morpho-sdk/src/entities/midnight/types.ts b/packages/morpho-sdk/src/entities/midnight/types.ts index b9da63d08..f2c4c372b 100644 --- a/packages/morpho-sdk/src/entities/midnight/types.ts +++ b/packages/morpho-sdk/src/entities/midnight/types.ts @@ -9,13 +9,15 @@ import type { } from "@morpho-org/midnight-sdk"; import type { Address, Hex } from "viem"; import type { MidnightTakeableOffer } from "../../actions/midnight/types.js"; -import type { TransactionPlan } from "../../transactionPlan/index.js"; import type { - ActionRequirement, + TransactionPlan, + TransactionPlanRequest, +} from "../../transactionPlan/index.js"; +import type { MempoolSubmitOffersAction, - MidnightOfferRootSignature, - RequirementSignature, + MidnightOfferRootRequirement, TransactionAction, + TxRequirement, } from "../../types/action.js"; /** Optional Midnight API validation controls for make-offer flows. */ @@ -72,10 +74,10 @@ export interface SupplyCollateralMakeBorrowParams extends MakeOffersParams { readonly collateralIndex?: bigint; } -/** Signatures accepted when building Midnight transaction plans. */ -export type MidnightActionSignatures = - | MidnightOfferRootSignature - | readonly MidnightOfferRootSignature[]; +/** Prerequisite transactions and offer-root signature supported by Midnight maker plans. */ +export type MidnightMakeOffersRequest = + | TxRequirement + | MidnightOfferRootRequirement; /** * Lazy Midnight transaction plan. Call `prepare()` to resolve the signature requests and/or @@ -95,8 +97,8 @@ export type MidnightActionSignatures = */ export type MidnightActionOutput< TAction extends TransactionAction, - TSignatures = readonly RequirementSignature[], -> = TransactionPlan; + TRequest extends TransactionPlanRequest = TxRequirement, +> = TransactionPlan; /** * Transaction plan returned by maker-offer flows after offer-tree preparation. @@ -119,7 +121,7 @@ export type MidnightActionOutput< */ export type MakeOffersOutput = MidnightActionOutput< MempoolSubmitOffersAction, - MidnightActionSignatures + MidnightMakeOffersRequest > & { readonly groups: readonly Hex[]; readonly root: Hex; diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts index 192d76b37..0e0af1f2e 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts @@ -117,7 +117,13 @@ function authorizationRequest(): Requirement describe("TransactionPlan", () => { test("default: exposes prepare as the single public entry point and prepares semantic requests", async () => { - const handler: TransactionPlanHandler = { + const handler: TransactionPlanHandler< + VaultV2DepositAction, + unknown, + | Readonly> + | Requirement + | Requirement + > = { getRequirementRequests: vi.fn(async () => [ approvalTx, permitRequest(), @@ -194,7 +200,7 @@ describe("TransactionPlan", () => { const buildPrimaryTx = vi.fn( (_signatures?: readonly RequirementSignature[]) => primaryTx, ); - const plan = TransactionPlan.create({ + const plan = TransactionPlan.create({ getRequirementRequests: vi.fn(async () => [ approvalTx, permit, @@ -205,6 +211,11 @@ describe("TransactionPlan", () => { const prepared = await plan.prepare(); + expectTypeOf[0]>().toEqualTypeOf< + | readonly (typeof permitSignature | AuthorizationRequirementSignature)[] + | undefined + >(); + expect(() => prepared.build()).toThrow( MissingTransactionPlanSignaturesError, ); @@ -252,6 +263,7 @@ describe("TransactionPlan", () => { { readonly phase: "preparation" } >; type Executable = ReturnType; + type BuildSignatures = Parameters[0]; type PrimaryTxStep = Extract< Executable["txSteps"][number], { readonly phase: "primary" } @@ -273,6 +285,9 @@ describe("TransactionPlan", () => { expectTypeOf>().toEqualTypeOf< Promise >(); + expectTypeOf().toEqualTypeOf< + readonly (typeof permitSignature)[] | undefined + >(); expectTypeOf().toEqualTypeOf< Readonly> >(); @@ -282,7 +297,7 @@ describe("TransactionPlan", () => { }); test("behavior: requirement transaction steps keep authorization intent separate from token approval", async () => { - const plan = TransactionPlan.create({ + const plan = TransactionPlan.create({ getRequirementRequests: vi.fn(async () => [authorizationTx]), buildPrimaryTx: vi.fn(() => primaryTx), }); @@ -306,7 +321,7 @@ describe("TransactionPlan", () => { }); test("behavior: a non-previewable primary step appears after build", async () => { - const plan = TransactionPlan.create({ + const plan = TransactionPlan.create({ getRequirementRequests: vi.fn(async () => [permitRequest()]), previewPrimaryTx: false, buildPrimaryTx: vi.fn(() => primaryTx), @@ -335,6 +350,10 @@ describe("TransactionPlan", () => { const prepared = await plan.prepare(); const executable = prepared.build(); + expectTypeOf[0]>().toEqualTypeOf< + readonly never[] | undefined + >(); + expect(prepared.flowKind).toBe("single_tx"); expect(prepared.stepCount).toBe(1); expect(prepared.signatureRequests).toEqual([]); diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts index 52e5aaef0..e46571160 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts @@ -1,7 +1,6 @@ import { isRequirementApproval, isRequirementBlueAuthorization, - type RequirementSignature, type SignatureRequirement, type Transaction, type TransactionAction, @@ -23,6 +22,7 @@ import type { TransactionPlanPrepareOptions, TransactionPlanRequest, TransactionPlanSignatureRequest, + TransactionPlanSignatures, TransactionPlanStep, TransactionPlanStepForIntent, TransactionPlanTokenApprovalIntent, @@ -101,14 +101,12 @@ export type { export class TransactionPlan< TPrimaryAction extends TransactionAction = TransactionAction, TRequestOptions = unknown, - TRequest extends TransactionPlanRequest = TransactionPlanRequest, - TSignatures = readonly RequirementSignature[], + TRequest extends TransactionPlanRequest = never, > { private readonly handler: TransactionPlanHandler< TPrimaryAction, TRequestOptions, - TRequest, - TSignatures + TRequest >; /** @@ -124,12 +122,7 @@ export class TransactionPlan< * ``` */ constructor( - handler: TransactionPlanHandler< - TPrimaryAction, - TRequestOptions, - TRequest, - TSignatures - >, + handler: TransactionPlanHandler, ) { this.handler = handler; } @@ -149,46 +142,13 @@ export class TransactionPlan< */ async prepare( options?: TransactionPlanPrepareOptions, - ): Promise> { + ): Promise> { const requests = this.handler.getRequirementRequests ? await this.handler.getRequirementRequests(options?.requestOptions) : []; const requirementSteps = requests.map( (request, index): TransactionPlanPreparedStep => { const id = `request-${index}`; - if (isRequirementApproval(request)) { - const tx = request as Extract; - return { - kind: "tx", - id, - phase: "preparation", - tx, - action: tx.action, - intent: { - type: "tokenApproval", - method: "tx", - token: request.to, - spender: request.action.args.spender, - amount: request.action.args.amount, - }, - } satisfies TransactionPlanPreparedStep; - } - if (isRequirementBlueAuthorization(request)) { - const tx = request as Extract; - return { - kind: "tx", - id, - phase: "preparation", - tx, - action: tx.action, - intent: { - type: "operatorAuthorization", - method: "tx", - operator: request.action.args.authorized, - isAuthorized: request.action.args.isAuthorized, - }, - } satisfies TransactionPlanPreparedStep; - } if ( typeof request === "object" && request !== null && @@ -197,7 +157,39 @@ export class TransactionPlan< "data" in request && "action" in request ) { + // Inline structural narrowing does not resolve a generic TRequest to its Extract branch. const tx = request as Extract; + if (isRequirementApproval(tx)) { + return { + kind: "tx", + id, + phase: "preparation", + tx, + action: tx.action, + intent: { + type: "tokenApproval", + method: "tx", + token: tx.to, + spender: tx.action.args.spender, + amount: tx.action.args.amount, + }, + } satisfies TransactionPlanPreparedStep; + } + if (isRequirementBlueAuthorization(tx)) { + return { + kind: "tx", + id, + phase: "preparation", + tx, + action: tx.action, + intent: { + type: "operatorAuthorization", + method: "tx", + operator: tx.action.args.authorized, + isAuthorized: tx.action.args.isAuthorized, + }, + } satisfies TransactionPlanPreparedStep; + } const intent: | TransactionPlanOperatorAuthorizationIntent | TransactionPlanContractTxIntent = @@ -220,6 +212,7 @@ export class TransactionPlan< } satisfies TransactionPlanPreparedStep; } + // The same generic-narrowing limitation applies to the complementary signature branch. const signatureRequest = request as Extract< TRequest, SignatureRequirement @@ -307,7 +300,7 @@ export class TransactionPlan< "primary" >); - return new PreparedTransactionPlan({ + return new PreparedTransactionPlan({ buildPrimaryTx: this.handler.buildPrimaryTx, requirementSteps, primaryStep, @@ -330,20 +323,17 @@ export class TransactionPlan< static create< TCreatedPrimaryAction extends TransactionAction, TCreatedRequestOptions = unknown, - TCreatedRequest extends TransactionPlanRequest = TransactionPlanRequest, - TCreatedSignatures = readonly RequirementSignature[], + TCreatedRequest extends TransactionPlanRequest = never, >( handler: TransactionPlanHandler< TCreatedPrimaryAction, TCreatedRequestOptions, - TCreatedRequest, - TCreatedSignatures + TCreatedRequest >, ): TransactionPlan< TCreatedPrimaryAction, TCreatedRequestOptions, - TCreatedRequest, - TCreatedSignatures + TCreatedRequest > { return new TransactionPlan(handler); } @@ -360,13 +350,12 @@ export class TransactionPlan< */ export class PreparedTransactionPlan< TPrimaryAction extends TransactionAction = TransactionAction, - TRequest extends TransactionPlanRequest = TransactionPlanRequest, - TSignatures = readonly RequirementSignature[], + TRequest extends TransactionPlanRequest = never, > implements PreparedTransactionPlanShape { private readonly buildPrimaryTx: TransactionPlanBuildPrimaryTx< TPrimaryAction, - TSignatures + TRequest >; private readonly rawRequirements: readonly TRequest[]; @@ -407,7 +396,7 @@ export class PreparedTransactionPlan< constructor(params: { readonly buildPrimaryTx: TransactionPlanBuildPrimaryTx< TPrimaryAction, - TSignatures + TRequest >; readonly requirementSteps: readonly TransactionPlanPreparedStep[]; readonly primaryStep?: TransactionPlanTxStep< @@ -633,7 +622,7 @@ export class PreparedTransactionPlan< * ``` */ build( - signatures?: TSignatures, + signatures?: TransactionPlanSignatures, ): ExecutableTransactionPlan { const expected = this.signatureRequests.length; const received = diff --git a/packages/morpho-sdk/src/transactionPlan/types.ts b/packages/morpho-sdk/src/transactionPlan/types.ts index 274a7baed..1c59043e0 100644 --- a/packages/morpho-sdk/src/transactionPlan/types.ts +++ b/packages/morpho-sdk/src/transactionPlan/types.ts @@ -6,7 +6,6 @@ import type { ERC20ApprovalAction, PermitRequirementSignature, Requirement, - RequirementSignature, SignatureRequirement, Transaction, TransactionAction, @@ -46,11 +45,23 @@ export interface TransactionPlanBatchCall { /** On-chain or signable request discovered while preparing a transaction plan. */ export type TransactionPlanRequest = TxRequirement | SignatureRequirement; +/** Signature produced by a transaction-plan request, or `never` for on-chain-only requests. */ +export type TransactionPlanSignatureForRequest< + TRequest extends TransactionPlanRequest, +> = TRequest extends Requirement ? TSignature : never; + +/** Ordered signatures accepted by a transaction plan, derived from its request types. */ +export type TransactionPlanSignatures< + TRequest extends TransactionPlanRequest = TransactionPlanRequest, +> = readonly TransactionPlanSignatureForRequest[]; + /** Builds the primary transaction, optionally consuming previously signed requests. */ export type TransactionPlanBuildPrimaryTx< TPrimaryAction extends TransactionAction = TransactionAction, - TSignatures = readonly RequirementSignature[], -> = (signatures?: TSignatures) => Readonly>; + TRequest extends TransactionPlanRequest = never, +> = ( + signatures?: TransactionPlanSignatures, +) => Readonly>; /** Resolves transaction and signature requirements before the primary transaction. */ export type TransactionPlanGetRequirementRequests< @@ -62,13 +73,12 @@ export type TransactionPlanGetRequirementRequests< export interface TransactionPlanHandler< TPrimaryAction extends TransactionAction = TransactionAction, TRequestOptions = unknown, - TRequest extends TransactionPlanRequest = TransactionPlanRequest, - TSignatures = readonly RequirementSignature[], + TRequest extends TransactionPlanRequest = never, > { /** Builds the primary transaction, optionally consuming previously signed requests. */ readonly buildPrimaryTx: TransactionPlanBuildPrimaryTx< TPrimaryAction, - TSignatures + TRequest >; /** * Controls whether `prepare()` can include an unsigned preview of the primary transaction. diff --git a/packages/morpho-sdk/src/types/AGENTS.md b/packages/morpho-sdk/src/types/AGENTS.md index 41ff808fa..479dab161 100644 --- a/packages/morpho-sdk/src/types/AGENTS.md +++ b/packages/morpho-sdk/src/types/AGENTS.md @@ -7,8 +7,8 @@ Centralized type definitions and error classes. Barrel-exported via `index.ts`. - `BaseAction` — discriminated union base, keyed on `type`. - `Transaction` — immutable `{ to, value, data, action }`. Returned from every action; deep-frozen. - `Requirement` / `RequirementSignature` — prerequisite signing flow for permit/permit2 and Midnight offer roots. -- `TransactionPlan` — lazy entity output whose `prepare()` call resolves typed prerequisite requests and whose prepared form builds an executable plan. -- `PreparedTransactionPlan` — resolved, typed review surface containing signature requests, transaction steps, and semantic intents. +- `TransactionPlan` — lazy entity output whose `prepare()` call resolves typed prerequisite requests and whose prepared form builds an executable plan. Its accepted build signatures are derived from `TRequest`. +- `PreparedTransactionPlan` — resolved, typed review surface containing signature requests, transaction steps, and semantic intents. - `ExecutableTransactionPlan` — ordered prerequisite and primary transaction steps ready for submission. - `Metadata` — optional `{ origin, timestamp? }` for calldata tracing. - `DepositAmountArgs` — union enforcing at least one of `amount` / `nativeAmount`. Reused for vault deposits, market collateral supply, and market loan-asset supply. diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index 95bdf0c50..09a3fcba9 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -1,4 +1,7 @@ -import type { RequirementSignature } from "@morpho-org/morpho-sdk"; +import type { + AuthorizationRequirementSignature, + RequirementSignature, +} from "@morpho-org/morpho-sdk"; import * as viem from "viem"; import { beforeEach, describe, expect, test, vi } from "vitest"; @@ -600,8 +603,23 @@ describe.sequential("MorphoProtocolEvm", () => { .fn() .mockResolvedValue({ hash: "dummy-borrow-hash", fee: 12_345n }); const requirementSignature = { - action: { type: "authorization" }, - } as unknown as RequirementSignature; + action: { + type: "authorization", + args: { + authorized: ADDRESS, + isAuthorized: true, + deadline: 1_900_000_000n, + }, + }, + args: { + owner: ADDRESS, + authorized: ADDRESS, + isAuthorized: true, + nonce: 0n, + deadline: 1_900_000_000n, + signature: "0x00", + }, + } satisfies AuthorizationRequirementSignature; await protocol.borrow({ token: TOKEN, diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 47eae9727..2b3495600 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -5,17 +5,20 @@ import { } from "@morpho-org/blue-sdk"; import { fetchMarket } from "@morpho-org/blue-sdk-viem"; import { + type AuthorizationRequirementSignature, type BlueAuthorizationAction, type ERC20ApprovalAction, type Metadata, type MorphoClientType, morphoViemExtension, + type PermitRequirementSignature, type Requirement, type RequirementSignature, type Transaction, type TransactionAction, type TransactionPlan, type TransactionPlanRequest, + type TransactionPlanSignatures, type VaultReallocation, } from "@morpho-org/morpho-sdk"; import type { @@ -108,10 +111,12 @@ export type Erc4337TransactionConfig = Partial< export type RequirementApproval = Transaction; export type RequirementAuthorization = Transaction; -export type RequirementSignatureRequest = Requirement; +export type RequirementSignatureRequest< + TSignature extends RequirementSignature = RequirementSignature, +> = Requirement; export type ApprovalOrSignatureRequirement = | RequirementApproval - | RequirementSignatureRequest; + | RequirementSignatureRequest; export interface RequirementOptions { /** Prefer the Morpho SDK simple permit flow when generating approval requirements. */ @@ -128,7 +133,7 @@ export interface MorphoErc20SupplyOptions { /** The address on behalf of which the supply operation should be performed. Must match the wallet account address when set. */ onBehalfOf?: string; /** Signature returned by a Morpho SDK approval requirement. */ - requirementSignature?: RequirementSignature; + requirementSignature?: PermitRequirementSignature; /** Optional Morpho SDK slippage tolerance in WAD precision. */ slippageTolerance?: bigint; } @@ -143,7 +148,7 @@ export interface MorphoNativeSupplyOptions { /** The address on behalf of which the supply operation should be performed. Must match the wallet account address when set. */ onBehalfOf?: string; /** Signature returned by a Morpho SDK approval requirement. */ - requirementSignature?: RequirementSignature; + requirementSignature?: PermitRequirementSignature; /** Optional Morpho SDK slippage tolerance in WAD precision. */ slippageTolerance?: bigint; } @@ -162,7 +167,7 @@ export interface MorphoBorrowOptions { /** Optional Morpho Vault V2 reallocations to include in the borrow action. */ reallocations?: readonly VaultReallocation[]; /** Signature returned by a Morpho SDK authorization requirement, folded into the bundle as `setAuthorizationWithSig`. */ - requirementSignature?: RequirementSignature; + requirementSignature?: AuthorizationRequirementSignature; /** Optional Morpho SDK slippage tolerance in WAD precision. */ slippageTolerance?: bigint; } @@ -175,7 +180,7 @@ export interface MorphoRepayOptions { /** The address on behalf of which the repay operation should be performed. Must match the wallet account address when set. */ onBehalfOf?: string; /** Signature returned by a Morpho SDK approval requirement. */ - requirementSignature?: RequirementSignature; + requirementSignature?: PermitRequirementSignature; /** Optional Morpho SDK slippage tolerance in WAD precision. */ slippageTolerance?: bigint; } @@ -401,9 +406,8 @@ async function getTransactionPlanRequests< TAction extends TransactionAction, TOptions, TRequest extends TransactionPlanRequest, - TSignatures, >( - plan: TransactionPlan, + plan: TransactionPlan, requestOptions?: TOptions, ): Promise { const prepared = await plan.prepare( @@ -416,10 +420,9 @@ async function getTransactionPlanTx< TAction extends TransactionAction, TOptions, TRequest extends TransactionPlanRequest, - TSignatures, >( - plan: TransactionPlan, - signatures?: TSignatures, + plan: TransactionPlan, + signatures?: TransactionPlanSignatures, ): Promise { const executable = (await plan.prepare()).build(signatures); return toWdkTransaction(executable.primaryTx); @@ -700,7 +703,12 @@ export default class MorphoProtocolEvm extends LendingProtocol { */ async getBorrowRequirements( options: MorphoBorrowOptions, - ): Promise<(RequirementAuthorization | RequirementSignatureRequest)[]> { + ): Promise< + ( + | RequirementAuthorization + | RequirementSignatureRequest + )[] + > { const action = await this._getBorrowAction(options); return await getTransactionPlanRequests(action); From 6180966891064bc9c0b87f347d7bc1bc7c26fbda Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 17 Jul 2026 11:18:30 +0200 Subject: [PATCH 09/12] test(morpho-sdk): inspect prepared vault transactions --- .../morpho-sdk/src/entities/vaultV1/vaultV1.test.ts | 12 ++++++------ .../morpho-sdk/src/entities/vaultV2/vaultV2.test.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts index 6d1aaf7bd..41bace06c 100644 --- a/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV1/vaultV1.test.ts @@ -103,9 +103,9 @@ describe("MorphoVaultV1 entity tests", () => { vaultData, slippageTolerance: 0n, }); - const tx = (await result.prepare()).build().primaryTx; - expect(tx.data).toBeDefined(); - expect(tx.value).toBe(0n); + const tx = (await result.prepare()).primaryTx; + expect(tx?.data).toBeDefined(); + expect(tx?.value).toBe(0n); }); test("should accept slippageTolerance of exactly MAX_SLIPPAGE_TOLERANCE", async ({ @@ -128,9 +128,9 @@ describe("MorphoVaultV1 entity tests", () => { vaultData, slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const tx = (await result.prepare()).build().primaryTx; - expect(tx.data).toBeDefined(); - expect(tx.value).toBe(0n); + const tx = (await result.prepare()).primaryTx; + expect(tx?.data).toBeDefined(); + expect(tx?.value).toBe(0n); }); test("should throw ExcessiveSlippageToleranceError when slippageTolerance exceeds MAX", async ({ diff --git a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts index 47981dd4f..ec5c13a7e 100644 --- a/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts +++ b/packages/morpho-sdk/src/entities/vaultV2/vaultV2.test.ts @@ -103,9 +103,9 @@ describe("MorphoVaultV2 entity tests", () => { vaultData, slippageTolerance: 0n, }); - const tx = (await result.prepare()).build().primaryTx; - expect(tx.data).toBeDefined(); - expect(tx.value).toBe(0n); + const tx = (await result.prepare()).primaryTx; + expect(tx?.data).toBeDefined(); + expect(tx?.value).toBe(0n); }); test("should accept slippageTolerance of exactly MAX_SLIPPAGE_TOLERANCE", async ({ @@ -128,9 +128,9 @@ describe("MorphoVaultV2 entity tests", () => { vaultData, slippageTolerance: MAX_SLIPPAGE_TOLERANCE, }); - const tx = (await result.prepare()).build().primaryTx; - expect(tx.data).toBeDefined(); - expect(tx.value).toBe(0n); + const tx = (await result.prepare()).primaryTx; + expect(tx?.data).toBeDefined(); + expect(tx?.value).toBe(0n); }); test("should throw ExcessiveSlippageToleranceError when slippageTolerance exceeds MAX", async ({ From 1a1a14cfcbaa4113ff36e416943ae9fb1e9673f8 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 17 Jul 2026 11:46:05 +0200 Subject: [PATCH 10/12] fix(morpho-sdk): reject surplus plan signatures --- .../transactionPlan/TransactionPlan.test.ts | 26 ++++++++++++++++--- .../src/transactionPlan/TransactionPlan.ts | 15 ++++------- packages/morpho-sdk/src/types/error.ts | 18 ++++++++----- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts index 0e0af1f2e..2290c43d6 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.test.ts @@ -4,11 +4,11 @@ import { type AuthorizationRequirementSignature, type BlueAuthorizationAction, type ERC20ApprovalAction, - MissingTransactionPlanSignaturesError, type PermitRequirementSignature, type Requirement, type RequirementSignature, type Transaction, + TransactionPlanSignatureCountMismatchError, type VaultV2DepositAction, } from "../types/index.js"; import { @@ -217,10 +217,10 @@ describe("TransactionPlan", () => { >(); expect(() => prepared.build()).toThrow( - MissingTransactionPlanSignaturesError, + TransactionPlanSignatureCountMismatchError, ); expect(() => prepared.build([permitSignature])).toThrow( - MissingTransactionPlanSignaturesError, + TransactionPlanSignatureCountMismatchError, ); const executable = prepared.build([ @@ -244,6 +244,26 @@ describe("TransactionPlan", () => { ]); }); + test("error: TransactionPlanSignatureCountMismatchError on a stale surplus signature", async () => { + const buildPrimaryTx = vi.fn(() => primaryTx); + const plan = TransactionPlan.create< + VaultV2DepositAction, + unknown, + Requirement + >({ + getRequirementRequests: vi.fn(async () => []), + buildPrimaryTx, + }); + + const prepared = await plan.prepare(); + buildPrimaryTx.mockClear(); + + expect(() => prepared.build([permitSignature])).toThrow( + TransactionPlanSignatureCountMismatchError, + ); + expect(buildPrimaryTx).not.toHaveBeenCalled(); + }); + test("behavior: preserves prerequisite and primary action types through execution", async () => { const plan = TransactionPlan.create< VaultV2DepositAction, diff --git a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts index e46571160..87daf8976 100644 --- a/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts +++ b/packages/morpho-sdk/src/transactionPlan/TransactionPlan.ts @@ -6,7 +6,7 @@ import { type TransactionAction, type TxRequirement, } from "../types/action.js"; -import { MissingTransactionPlanSignaturesError } from "../types/error.js"; +import { TransactionPlanSignatureCountMismatchError } from "../types/error.js"; import type { ExecutableTransactionPlanShape, PreparedTransactionPlanShape, @@ -611,7 +611,7 @@ export class PreparedTransactionPlan< * * @param signatures - Signatures produced from `signatureRequests`, in request order. * @returns An executable plan that preserves the prerequisite and primary-action types. - * @throws {MissingTransactionPlanSignaturesError} when fewer signatures are supplied than requested. + * @throws {TransactionPlanSignatureCountMismatchError} when the signature count differs from the prepared requests. * @example * ```ts * const signatures = []; @@ -625,14 +625,9 @@ export class PreparedTransactionPlan< signatures?: TransactionPlanSignatures, ): ExecutableTransactionPlan { const expected = this.signatureRequests.length; - const received = - signatures == null - ? 0 - : Array.isArray(signatures) - ? signatures.length - : 1; - if (received < expected) { - throw new MissingTransactionPlanSignaturesError(expected, received); + const received = signatures?.length ?? 0; + if (received !== expected) { + throw new TransactionPlanSignatureCountMismatchError(expected, received); } const requirementTxSteps = this.txSteps.filter( (step) => step.phase === "preparation", diff --git a/packages/morpho-sdk/src/types/error.ts b/packages/morpho-sdk/src/types/error.ts index 0ff10fbb1..445f9a35c 100644 --- a/packages/morpho-sdk/src/types/error.ts +++ b/packages/morpho-sdk/src/types/error.ts @@ -297,25 +297,29 @@ export class UnexpectedRequirementSignatureError extends Error { } /** - * Thrown when a prepared transaction plan is built without all requested signatures. + * Thrown when the signatures passed to a prepared transaction plan do not exactly match its + * discovered signature requests. * * @example * ```ts - * import { MissingTransactionPlanSignaturesError } from "@morpho-org/morpho-sdk"; + * import { TransactionPlanSignatureCountMismatchError } from "@morpho-org/morpho-sdk"; * - * if (error instanceof MissingTransactionPlanSignaturesError) { - * // Collect every `prepared.signatureRequests` signature before calling build(). + * if (error instanceof TransactionPlanSignatureCountMismatchError) { + * // Pass one signature for each `prepared.signatureRequests` entry and no others. * } * ``` */ -export class MissingTransactionPlanSignaturesError extends Error { +export class TransactionPlanSignatureCountMismatchError extends Error { /** * @param expected - Number of signature requests in the prepared plan. * @param received - Number of signatures passed to `build`. */ - constructor(expected: number, received: number) { + constructor( + public readonly expected: number, + public readonly received: number, + ) { super( - `Expected ${expected} transaction-plan signatures but received ${received}. Sign all prepared signature requests before building the executable transaction plan.`, + `Expected exactly ${expected} transaction-plan signatures but received ${received}. Pass one signature for each prepared signature request and no others.`, ); } } From bcce33538f5af9e5ff5d207e2027a71eb97b1a5b Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 17 Jul 2026 12:09:56 +0200 Subject: [PATCH 11/12] test(morpho-sdk): prepare Midnight offer signatures --- .../src/entities/midnight/midnight.test.ts | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts index b49a6c168..54094aadc 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts @@ -73,18 +73,25 @@ import { UnpreparedMidnightOfferRootSignatureError, } from "../../types/error.js"; import { MorphoMidnight } from "./midnight.js"; -import type { MakeOffersOutput, OffersData } from "./types.js"; +import type { + MakeOffersOutput, + MidnightMakeOffersRequest, + OffersData, +} from "./types.js"; type BuildMakeOffersOutputTx = (params: { readonly offersData: OffersData; readonly signatures?: readonly MidnightOfferRootSignature[]; readonly signedPayloads?: ReadonlyMap; readonly metadata?: { readonly origin: string }; + readonly signatureRequestCount?: number; }) => Promise>>; type CreateMakeOffersOutput = (params: { readonly offersData: OffersData; - readonly getRequirementRequests: () => Promise; + readonly getRequirementRequests: () => Promise< + readonly MidnightMakeOffersRequest[] + >; readonly signedPayloads: ReadonlyMap; }) => MakeOffersOutput; @@ -104,7 +111,16 @@ const buildMakeOffersOutputTx: BuildMakeOffersOutputTx = async (params) => ) .createMakeOffersOutput({ offersData: params.offersData, - getRequirementRequests: async () => [], + getRequirementRequests: async () => { + const count = + params.signatureRequestCount ?? + (params.offersData.ratifierType === "ecrecover" ? 1 : 0); + const signature = offerRootSignature(params.offersData); + return Array.from({ length: count }, () => ({ + action: signature.action, + sign: async () => signature, + })); + }, signedPayloads: params.signedPayloads ?? new Map([["0x1234", "0x1234" as Hex]]), }) @@ -2058,6 +2074,7 @@ describe("MorphoMidnight", () => { await expect( buildMakeOffersOutputTx({ offersData: data, + signatureRequestCount: 0, }), ).rejects.toThrow(MissingMidnightOfferRootSignatureError); }); @@ -2166,6 +2183,7 @@ describe("MorphoMidnight", () => { buildMakeOffersOutputTx({ offersData: data, signatures: [signature, signature], + signatureRequestCount: 2, }), ).rejects.toThrow(AmbiguousRequirementSignaturesError); }); @@ -2188,6 +2206,7 @@ describe("MorphoMidnight", () => { buildMakeOffersOutputTx({ offersData: data, signatures: [offerRootSignature(data)], + signatureRequestCount: 1, }), ).rejects.toThrow(UnexpectedRequirementSignatureError); }); From f68ec9eba6f761398925e476d4e592307a2d6c70 Mon Sep 17 00:00:00 2001 From: "Romain (Rubilmax) Milon" Date: Fri, 17 Jul 2026 12:28:54 +0200 Subject: [PATCH 12/12] fix: use transaction plan previews where available --- .../src/client/morphoViemExtension.ts | 12 +- .../src/entities/midnight/midnight.test.ts | 18 ++- .../src/entities/midnight/midnight.ts | 2 +- .../src/morpho-protocol-evm.test.ts | 149 +++++++++++++++++- .../src/morpho-protocol-evm.ts | 31 +++- 5 files changed, 194 insertions(+), 18 deletions(-) diff --git a/packages/morpho-sdk/src/client/morphoViemExtension.ts b/packages/morpho-sdk/src/client/morphoViemExtension.ts index d2bf5a0e4..6587f6884 100644 --- a/packages/morpho-sdk/src/client/morphoViemExtension.ts +++ b/packages/morpho-sdk/src/client/morphoViemExtension.ts @@ -85,10 +85,7 @@ function createMorphoNamespace( * ```ts * import { createWalletClient, http, publicActions } from "viem"; * import { mainnet } from "viem/chains"; - * import { - * morphoViemExtension, - * type RequirementSignature, - * } from "@morpho-org/morpho-sdk"; + * import { morphoViemExtension } from "@morpho-org/morpho-sdk"; * * const client = createWalletClient({ * chain: mainnet, @@ -108,10 +105,9 @@ function createMorphoNamespace( * vaultData, * }); * const prepared = await plan.prepare(); - * const signatures: RequirementSignature[] = []; - * for (const request of prepared.signatureRequests) { - * signatures.push(await request.sign(client, user)); - * } + * const signatures = await Promise.all( + * prepared.signatureRequests.map((request) => request.sign(client, user)), + * ); * const tx = prepared.build(signatures).primaryTx; * ``` */ diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts index 54094aadc..e52603268 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.test.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.test.ts @@ -1853,13 +1853,16 @@ describe("MorphoMidnight", () => { offers: data.tree, validation: offerValidation, }); - const requirements = (await output.prepare()).requirements; - const tx = (await output.prepare()).build().primaryTx; + const prepared = await output.prepare(); expect( - requirements.map((requirement) => requirement.action.type), + prepared.requirements.map((requirement) => requirement.action.type), ).toEqual(["setterRatifierRatifyRoot"]); - expect(tx.action.args.ratifierType).toBe("setter"); + expect(prepared.primaryTx?.action.args.ratifierType).toBe("setter"); + expect(prepared.txSteps.map((step) => step.id)).toEqual([ + "request-0", + "primary", + ]); }); test("behavior: setter ratifier returns no requirements when root is ratified", async () => { @@ -1872,8 +1875,13 @@ describe("MorphoMidnight", () => { offers: data.tree, validation: offerValidation, }); + const prepared = await output.prepare(); - await expect((await output.prepare()).requirements).toEqual([]); + expect(prepared.requirements).toEqual([]); + expect(prepared.primaryTx?.action.args.ratifierType).toBe("setter"); + expect(prepared.txSteps.map((step) => step.id)).toEqual(["primary"]); + expect(prepared.calls).toHaveLength(1); + expect(prepared.flowKind).toBe("single_tx"); }); test("error: MidnightOfferSideMismatchError", async () => { diff --git a/packages/morpho-sdk/src/entities/midnight/midnight.ts b/packages/morpho-sdk/src/entities/midnight/midnight.ts index 05aabebb1..7f3dd3ebc 100644 --- a/packages/morpho-sdk/src/entities/midnight/midnight.ts +++ b/packages/morpho-sdk/src/entities/midnight/midnight.ts @@ -1156,7 +1156,7 @@ export class MorphoMidnight { MidnightMakeOffersRequest >({ getRequirementRequests: params.getRequirementRequests, - previewPrimaryTx: false, + previewPrimaryTx: data.ratifierType === "ecrecover" ? false : undefined, buildPrimaryTx: (signatures) => { let payload = data.setterPayload; if (data.ratifierType === "ecrecover") { diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts index 09a3fcba9..a37941bca 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.test.ts @@ -1,5 +1,6 @@ import type { AuthorizationRequirementSignature, + PermitRequirementSignature, RequirementSignature, } from "@morpho-org/morpho-sdk"; import * as viem from "viem"; @@ -21,6 +22,29 @@ const MARKET_PARAMS = { lltv: 860000000000000000n, } as const; +const PERMIT_SIGNATURE = { + action: { + type: "permit2", + args: { + token: TOKEN, + spender: ADDRESS, + amount: 100_000n, + deadline: 1_900_000_000n, + expiration: 2_000_000_000n, + chainId: 1, + }, + }, + args: { + owner: ADDRESS, + asset: TOKEN, + amount: 100_000n, + nonce: 0n, + deadline: 1_900_000_000n, + expiration: 2_000_000_000n, + signature: "0x00", + }, +} as const satisfies PermitRequirementSignature; + const SUPPLY_TX = { to: "0x0000000000000000000000000000000000000001", value: 0n, @@ -95,7 +119,9 @@ function mockTransactionPlan( requirements: resolvedRequirements, primaryStep, primaryTx: tx, - signatureRequests: [], + signatureRequests: resolvedRequirements.filter( + (requirement) => requirement.sign !== undefined, + ), txSteps: [primaryStep], calls: [{ to: tx.to, value: tx.value, data: tx.data }], steps: [ @@ -116,6 +142,12 @@ function mockTransactionPlan( primaryStep, ], build: (signatures?: readonly RequirementSignature[]) => { + const expected = resolvedRequirements.filter( + (requirement) => requirement.sign !== undefined, + ).length; + if (expected > 0 && (signatures?.length ?? 0) !== expected) { + throw new Error("signature count mismatch"); + } const primaryTx = buildPrimaryTx(signatures); const builtPrimaryStep = { id: "primary", @@ -156,6 +188,21 @@ const supplyCollateralAction = mockTransactionPlan(SUPPLY_COLLATERAL_TX, [ { action: { type: "erc20Approval" } }, ]); const withdrawCollateralAction = mockTransactionPlan(WITHDRAW_COLLATERAL_TX); +const signatureSupplyAction = mockTransactionPlan(SUPPLY_TX, [ + { action: { type: "permit" }, sign: vi.fn(async () => PERMIT_SIGNATURE) }, +]); +const signatureRepayAction = mockTransactionPlan(REPAY_TX, [ + { action: { type: "permit" }, sign: vi.fn(async () => PERMIT_SIGNATURE) }, +]); +const signatureSupplyCollateralAction = mockTransactionPlan( + SUPPLY_COLLATERAL_TX, + [ + { + action: { type: "permit" }, + sign: vi.fn(async () => PERMIT_SIGNATURE), + }, + ], +); const vaultV2Entity = { getData: vi.fn().mockResolvedValue(vaultData), @@ -476,6 +523,54 @@ describe.sequential("MorphoProtocolEvm", () => { expect(account.quoteSendTransaction).toHaveBeenCalledWith(SUPPLY_TX); expect(result).toEqual({ fee: 12_345n }); }); + + test("should quote the primary preview before a permit is signed", async () => { + vaultV2Entity.deposit.mockReturnValueOnce(signatureSupplyAction); + account.quoteSendTransaction = vi + .fn() + .mockResolvedValue({ fee: 12_345n }); + // biome-ignore lint/suspicious/noShadow: test-local protocol shadowing the suite default + const protocol = new MorphoProtocolEvm(account, { + chainId: 1, + earnVaultAddress: VAULT, + borrowMarketParams: MARKET_PARAMS, + supportSignature: true, + }); + + const result = await protocol.quoteSupply({ + token: TOKEN, + amount: 100_000n, + }); + + expect(signatureSupplyAction.buildPrimaryTx).not.toHaveBeenCalled(); + expect(account.quoteSendTransaction).toHaveBeenCalledWith(SUPPLY_TX); + expect(result).toEqual({ fee: 12_345n }); + }); + + test("should quote the signed transaction when a permit is supplied", async () => { + vaultV2Entity.deposit.mockReturnValueOnce(signatureSupplyAction); + account.quoteSendTransaction = vi + .fn() + .mockResolvedValue({ fee: 12_345n }); + // biome-ignore lint/suspicious/noShadow: test-local protocol shadowing the suite default + const protocol = new MorphoProtocolEvm(account, { + chainId: 1, + earnVaultAddress: VAULT, + borrowMarketParams: MARKET_PARAMS, + supportSignature: true, + }); + + await protocol.quoteSupply({ + token: TOKEN, + amount: 100_000n, + requirementSignature: PERMIT_SIGNATURE, + }); + + expect(signatureSupplyAction.buildPrimaryTx).toHaveBeenCalledWith([ + PERMIT_SIGNATURE, + ]); + expect(account.quoteSendTransaction).toHaveBeenCalledWith(SUPPLY_TX); + }); }); describe("withdraw", () => { @@ -703,6 +798,29 @@ describe.sequential("MorphoProtocolEvm", () => { requirementOptions, ); }); + + test("should quote the primary preview before a permit is signed", async () => { + marketEntity.repay.mockReturnValueOnce(signatureRepayAction); + account.quoteSendTransaction = vi + .fn() + .mockResolvedValue({ fee: 12_345n }); + // biome-ignore lint/suspicious/noShadow: test-local protocol shadowing the suite default + const protocol = new MorphoProtocolEvm(account, { + chainId: 1, + earnVaultAddress: VAULT, + borrowMarketParams: MARKET_PARAMS, + supportSignature: true, + }); + + const result = await protocol.quoteRepay({ + token: TOKEN, + amount: 100_000n, + }); + + expect(signatureRepayAction.buildPrimaryTx).not.toHaveBeenCalled(); + expect(account.quoteSendTransaction).toHaveBeenCalledWith(REPAY_TX); + expect(result).toEqual({ fee: 12_345n }); + }); }); describe("collateral", () => { @@ -784,6 +902,35 @@ describe.sequential("MorphoProtocolEvm", () => { supplyCollateralAction.getRequirementRequests, ).toHaveBeenCalledWith(requirementOptions); }); + + test("should quote the primary preview before a permit is signed", async () => { + marketEntity.supplyCollateral.mockReturnValueOnce( + signatureSupplyCollateralAction, + ); + account.quoteSendTransaction = vi + .fn() + .mockResolvedValue({ fee: 12_345n }); + // biome-ignore lint/suspicious/noShadow: test-local protocol shadowing the suite default + const protocol = new MorphoProtocolEvm(account, { + chainId: 1, + earnVaultAddress: VAULT, + borrowMarketParams: MARKET_PARAMS, + supportSignature: true, + }); + + const result = await protocol.quoteSupplyCollateral({ + token: COLLATERAL, + amount: 100_000n, + }); + + expect( + signatureSupplyCollateralAction.buildPrimaryTx, + ).not.toHaveBeenCalled(); + expect(account.quoteSendTransaction).toHaveBeenCalledWith( + SUPPLY_COLLATERAL_TX, + ); + expect(result).toEqual({ fee: 12_345n }); + }); }); describe("erc-4337", () => { diff --git a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts index 2b3495600..e8c0b047b 100644 --- a/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts +++ b/packages/wdk-protocol-lending-morpho-evm/src/morpho-protocol-evm.ts @@ -428,6 +428,22 @@ async function getTransactionPlanTx< return toWdkTransaction(executable.primaryTx); } +async function getTransactionPlanQuoteTx< + TAction extends TransactionAction, + TOptions, + TRequest extends TransactionPlanRequest, +>( + plan: TransactionPlan, + signatures?: TransactionPlanSignatures, +): Promise { + const prepared = await plan.prepare(); + const tx = + signatures === undefined + ? (prepared.primaryTx ?? prepared.build().primaryTx) + : prepared.build(signatures).primaryTx; + return toWdkTransaction(tx); +} + /** * Morpho lending protocol adapter for WDK-compatible EVM wallet accounts. * @@ -546,7 +562,10 @@ export default class MorphoProtocolEvm extends LendingProtocol { options: MorphoSupplyOptions, config?: Erc4337TransactionConfig, ): Promise> { - const tx = await this._getSupplyTransaction(options); + const tx = await getTransactionPlanQuoteTx( + await this._getSupplyAction(options), + options.requirementSignature ? [options.requirementSignature] : undefined, + ); return await this._quoteTransaction(tx, config); } @@ -826,7 +845,10 @@ export default class MorphoProtocolEvm extends LendingProtocol { options: MorphoRepayOptions, config?: Erc4337TransactionConfig, ): Promise> { - const tx = await this._getRepayTransaction(options); + const tx = await getTransactionPlanQuoteTx( + await this._getRepayAction(options), + options.requirementSignature ? [options.requirementSignature] : undefined, + ); return await this._quoteTransaction(tx, config); } @@ -936,7 +958,10 @@ export default class MorphoProtocolEvm extends LendingProtocol { options: MorphoSupplyOptions, config?: Erc4337TransactionConfig, ): Promise> { - const tx = await this._getSupplyCollateralTransaction(options); + const tx = await getTransactionPlanQuoteTx( + await this._getSupplyCollateralAction(options), + options.requirementSignature ? [options.requirementSignature] : undefined, + ); return await this._quoteTransaction(tx, config); }