diff --git a/README.md b/README.md index e3fc76cf..31c26a58 100644 --- a/README.md +++ b/README.md @@ -471,6 +471,8 @@ startStatusCheck(deltaAuction.id); For more Delta protocol usage, and **Crosschain Delta Orders**, refer to [DELTA.md](./docs/DELTA.md) +For **External Delta Orders** (orders that delegate token handling to an external handler contract, enabling complex DeFi strategies like Aave collateral/debt swaps), refer to [EXTERNAL_ORDERS.md](./docs/EXTERNAL_ORDERS.md) + ------------ ### Market Swap handling diff --git a/docs/EXTERNAL_ORDERS.md b/docs/EXTERNAL_ORDERS.md new file mode 100644 index 00000000..f9cf20cf --- /dev/null +++ b/docs/EXTERNAL_ORDERS.md @@ -0,0 +1,217 @@ +## External Delta Orders + +External orders delegate token handling to an external **handler** contract, enabling complex DeFi strategies that go beyond simple token swaps. The handler manages the actual token logic (e.g. Aave flash loan + collateral/debt swap), while the Delta protocol handles the auction, settlement, and signature verification. + +### Handler Contract + +The handler contract must be deployed by the integrator and implement the `IExternalProtocolHandler` interface: + +```solidity +interface IExternalProtocolHandler { + function execute( + address user, + address tokenIn, + uint256 amountIn, + address tokenOut, + uint256 amountOut, + bytes calldata data + ) external; +} +``` + +The `data` field carries protocol-specific parameters (e.g. an Aave operation type). A reference implementation is the [Aave handler deployed on Ethereum](https://etherscan.io/address/0xb4a2c36668cf8b19fe08f263e3685a5e16e82912#code). + +### Key Differences from Standard Delta Orders + +| Feature | Standard Order | External Order | +|---|---|---| +| `beneficiary` field | yes | no (output goes to owner) | +| `bridge` field | yes | no | +| `handler` field | no | yes (required) | +| `data` field | no | yes (required) | +| Cross-chain support | yes | no | + + +--- + +### 1. Construct an SDK object + +```ts +const account = userAddress; +const deltaSDK = constructSimpleSDK( + { chainId: 1, axios }, // Ethereum + { + ethersProviderOrSigner: signer, + EthersContract: ethers.Contract, + account, + } +); +// for usage with different web3 provider libraries refer to the main README +``` + +### 2. Request prices for a Token pair + +```ts +const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; +const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; +const AAVE_HANDLER = '0xb4a2c36668cf8b19fe08f263e3685a5e16e82912'; +const amount = ethers.utils.parseUnits('1', 6).toString(); // 1 USDC + +const deltaPrice = await deltaSDK.getDeltaPrice({ + srcToken: USDC, + destToken: WETH, + amount, + userAddress: account, + srcDecimals: 6, + destDecimals: 18, + side: SwapSide.SELL, +}); +``` + +### 3. Perform any prerequisites required by the handler + +Each handler may have its own prerequisites (token approvals, delegation, position setup, etc.). Refer to your handler's documentation for details. + +For example, the [Aave handler](https://etherscan.io/address/0xb4a2c36668cf8b19fe08f263e3685a5e16e82912#code) requires aToken approval before a collateral swap: + +```ts +const aUSDC = new ethers.Contract(USDC, ['function approve(address,uint256)'], signer); +await aUSDC.approve(AAVE_HANDLER, amount); +``` + +### 4. Build, sign, and submit the External Order + +#### Using `submitExternalDeltaOrder` (recommended) + +The simplest approach — builds, signs, and posts the order in one call: + +```ts +const deltaAuction = await deltaSDK.submitExternalDeltaOrder({ + deltaPrice, + owner: account, + handler: AAVE_HANDLER, // your deployed handler contract + data: '0x0000000000000000000000000000000000000000000000000000000000000000', // protocol-specific bytes + srcToken: USDC, + destToken: WETH, + srcAmount: amount, + slippage: 50, // 0.5% in bps +}); +``` + +#### Using individual steps + +For more control, you can build, sign, and post separately: + +```ts +const signableOrderData = await deltaSDK.buildExternalDeltaOrder({ + deltaPrice, + owner: account, + handler: AAVE_HANDLER, + data: '0x0000000000000000000000000000000000000000000000000000000000000000', + srcToken: USDC, + destToken: WETH, + srcAmount: amount, + slippage: 50, // 0.5% in bps +}); + +const signature = await deltaSDK.signExternalDeltaOrder(signableOrderData); + +const deltaAuction = await deltaSDK.postExternalDeltaOrder({ + order: signableOrderData.data, + signature, +}); +``` + +### 5. Wait for execution + +```ts +const intervalId = setInterval(async () => { + const auction = await deltaSDK.getDeltaOrderById(deltaAuction.id); + console.log('Status:', auction.status); + + if (auction.status === 'EXECUTED' || auction.status === 'FAILED') { + clearInterval(intervalId); + } +}, 3000); +``` + +### 6. Query External Orders + +```ts +// fetch a specific external order +const order = await deltaSDK.getDeltaOrderById(orderId); + +// list external orders only +const orders = await deltaSDK.getDeltaOrders({ + owner: account, + onChainOrderType: 'ExternalOrder', +}); +``` + +--- + +## Specifying Amounts + +There are two ways to specify amounts: + +**With `slippage` (recommended)** — the SDK computes the slippage-adjusted amount from `deltaPrice` automatically: + +```ts +// SELL: provide srcAmount + slippage → destAmount auto-computed +await deltaSDK.buildExternalDeltaOrder({ + // ... + srcAmount: amount, + slippage: 50, // 0.5% in bps +}); + +// BUY: provide destAmount + slippage → srcAmount auto-computed +await deltaSDK.buildExternalDeltaOrder({ + // ... + destAmount: amount, + slippage: 50, +}); +``` + +**With explicit amounts** — you compute both amounts yourself: + +```ts +await deltaSDK.buildExternalDeltaOrder({ + // ... + srcAmount: amount, + destAmount: destAmountAfterSlippage, + side: SwapSide.SELL, // or SwapSide.BUY +}); +``` + +--- + +## Pre-signing External Orders + +For smart contract wallets or other cases where EIP-712 signatures are not available, you can pre-sign an external order on-chain: + +```ts +const signableOrderData = await deltaSDK.buildExternalDeltaOrder({ + deltaPrice, + owner: account, + handler: AAVE_HANDLER, + data: '0x0000000000000000000000000000000000000000000000000000000000000000', + srcToken: USDC, + destToken: WETH, + srcAmount: amount, + slippage: 50, +}); + +// on-chain pre-sign transaction +const tx = await deltaSDK.preSignExternalDeltaOrder(signableOrderData); +await tx.wait(); + +// post with empty signature +const deltaAuction = await deltaSDK.postExternalDeltaOrder({ + order: signableOrderData.data, + signature: '0x', +}); +``` + +--- + +#### A more detailed example can be found in [examples/externalDelta](../src/examples/externalDelta.ts) diff --git a/src/examples/delta.ts b/src/examples/delta.ts index 3f2577f4..208e4d94 100644 --- a/src/examples/delta.ts +++ b/src/examples/delta.ts @@ -14,10 +14,13 @@ const fetcher = constructAxiosFetcher(axios); const provider = ethers.getDefaultProvider(1); const signer = Wallet.createRandom().connect(provider); const account = signer.address; -const contractCaller = constructEthersContractCaller({ - ethersProviderOrSigner: provider, - EthersContract: ethers.Contract, -}); +const contractCaller = constructEthersContractCaller( + { + ethersProviderOrSigner: provider, + EthersContract: ethers.Contract, + }, + account +); // type AdaptersFunctions & ApproveTokenFunctions const deltaSDK = constructPartialSDK( diff --git a/src/examples/externalDelta.ts b/src/examples/externalDelta.ts new file mode 100644 index 00000000..6144d5b4 --- /dev/null +++ b/src/examples/externalDelta.ts @@ -0,0 +1,162 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import axios from 'axios'; +import { ethers, Wallet } from 'ethersV5'; +import { + constructPartialSDK, + constructEthersContractCaller, + constructAxiosFetcher, + constructAllDeltaOrdersHandlers, + SwapSide, +} from '..'; +import { startStatusCheck } from './helpers/delta'; + +const fetcher = constructAxiosFetcher(axios); + +const provider = ethers.getDefaultProvider(1); // Ethereum +const signer = Wallet.createRandom().connect(provider); +const account = signer.address; +const contractCaller = constructEthersContractCaller( + { + ethersProviderOrSigner: signer, + EthersContract: ethers.Contract, + }, + account +); + +const deltaSDK = constructPartialSDK( + { + chainId: 1, // Ethereum + fetcher, + contractCaller, + }, + constructAllDeltaOrdersHandlers +); + +// Ethereum tokens +const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; +const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; + +// Aave external handler on Ethereum (https://etherscan.io/address/0xb4a2c36668cf8b19fe08f263e3685a5e16e82912#code) +// The handler contract is provided by the integrator and must implement IExternalProtocolHandler. +// Different handlers serve different purposes and may have different prerequisites +// (e.g. token approvals, credit delegation, position setup). +const AAVE_HANDLER = '0xb4a2c36668cf8b19fe08f263e3685a5e16e82912'; + +// Aave-specific order types passed as `data` field. +// The `data` encoding is handler-specific — each handler defines its own format. +const AaveOrderTypes = { + COLLATERAL_SWAP: + '0x0000000000000000000000000000000000000000000000000000000000000000', + DEBT_SWAP: + '0x0000000000000000000000000000000000000000000000000000000000000001', + REPAY_WITH_COLLATERAL: + '0x0000000000000000000000000000000000000000000000000000000000000002', +}; + +// Aave Collateral Swap: swap one collateral asset for another (SELL side) +// Prerequisites: user must approve the source aToken to the handler +async function collateralSwapFlow() { + const amount = ethers.utils.parseUnits('1', 6).toString(); // 1 USDC + + const deltaPrice = await deltaSDK.getDeltaPrice({ + srcToken: USDC, + destToken: WETH, + amount, + userAddress: account, + srcDecimals: 6, + destDecimals: 18, + side: SwapSide.SELL, + }); + + const signableOrderData = await deltaSDK.buildExternalDeltaOrder({ + deltaPrice, + owner: account, + handler: AAVE_HANDLER, + data: AaveOrderTypes.COLLATERAL_SWAP, + srcToken: USDC, + destToken: WETH, + srcAmount: amount, + slippage: 50, // 0.5% slippage in bps + }); + + const signature = await deltaSDK.signExternalDeltaOrder(signableOrderData); + + const deltaAuction = await deltaSDK.postExternalDeltaOrder({ + order: signableOrderData.data, + signature, + }); + + startStatusCheck(() => deltaSDK.getDeltaOrderById(deltaAuction.id)); +} + +// Aave Debt Swap: swap one debt for another (BUY side) +// Prerequisites: user must grant borrowAllowance on the source variable debt token to the handler +async function debtSwapFlow() { + const debtAmount = ethers.utils.parseUnits('1', 6).toString(); // amount of debt to swap + + const deltaPrice = await deltaSDK.getDeltaPrice({ + srcToken: USDC, + destToken: WETH, + amount: debtAmount, + userAddress: account, + srcDecimals: 6, + destDecimals: 18, + side: SwapSide.BUY, + }); + + const signableOrderData = await deltaSDK.buildExternalDeltaOrder({ + deltaPrice, + owner: account, + handler: AAVE_HANDLER, + data: AaveOrderTypes.DEBT_SWAP, + srcToken: USDC, + destToken: WETH, + destAmount: debtAmount, + slippage: 50, // 0.5% slippage in bps + }); + + const signature = await deltaSDK.signExternalDeltaOrder(signableOrderData); + + const deltaAuction = await deltaSDK.postExternalDeltaOrder({ + order: signableOrderData.data, + signature, + }); + + startStatusCheck(() => deltaSDK.getDeltaOrderById(deltaAuction.id)); +} + +// Aave Repay with Collateral: use collateral to repay debt (BUY side) +// Prerequisites: user must approve the source aToken to the handler +async function repayWithCollateralFlow() { + const collateralAmount = ethers.utils.parseUnits('1', 6).toString(); + + const deltaPrice = await deltaSDK.getDeltaPrice({ + srcToken: USDC, + destToken: WETH, + amount: collateralAmount, + userAddress: account, + srcDecimals: 6, + destDecimals: 18, + side: SwapSide.BUY, + }); + + const signableOrderData = await deltaSDK.buildExternalDeltaOrder({ + deltaPrice, + owner: account, + handler: AAVE_HANDLER, + data: AaveOrderTypes.REPAY_WITH_COLLATERAL, + srcToken: USDC, + destToken: WETH, + destAmount: collateralAmount, + slippage: 50, // 0.5% slippage in bps + }); + + const signature = await deltaSDK.signExternalDeltaOrder(signableOrderData); + + const deltaAuction = await deltaSDK.postExternalDeltaOrder({ + order: signableOrderData.data, + signature, + }); + + startStatusCheck(() => deltaSDK.getDeltaOrderById(deltaAuction.id)); +} diff --git a/src/examples/helpers/delta.ts b/src/examples/helpers/delta.ts index a2972df1..0c124738 100644 --- a/src/examples/helpers/delta.ts +++ b/src/examples/helpers/delta.ts @@ -1,20 +1,24 @@ -import { DeltaAuction, DeltaOrderFromAPI } from '../..'; +import { DeltaAuction } from '../..'; function isExecutedDeltaAuction( - auction: Omit, + auction: DeltaAuction, waitForCrosschain = true // only consider executed when destChain work is done ) { if (auction.status !== 'EXECUTED') return false; // crosschain Order is executed on destChain if bridgeStatus is filled - if (waitForCrosschain && auction.order.bridge.destinationChainId !== 0) { + if ( + waitForCrosschain && + auction.onChainOrderType === 'Order' && + auction.order.bridge.destinationChainId !== 0 + ) { return auction.bridgeStatus === 'filled'; } return true; } -type GetDeltaOrderFn = () => Promise; +type GetDeltaOrderFn = () => Promise; function fetchOrderPeriodically(getDeltaOrder: GetDeltaOrderFn) { const intervalId = setInterval(async () => { diff --git a/src/index.ts b/src/index.ts index d9b56e88..3fbd2b73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -144,17 +144,18 @@ import type { BridgeMetadata, BridgeStatus, Bridge, + ExternalDeltaOrder, + OnChainOrderType, + SwapSideUnion, } from './methods/delta/helpers/types'; import { BuildDeltaOrderDataParams, BuildDeltaOrderFunctions, constructBuildDeltaOrder, SignableDeltaOrderData, - SwapSideUnion, } from './methods/delta/buildDeltaOrder'; import { constructPostDeltaOrder, - DeltaOrderApiResponse, PostDeltaOrderFunctions, PostDeltaOrderParams, } from './methods/delta/postDeltaOrder'; @@ -203,6 +204,26 @@ import { IsTokenSupportedInDeltaFunctions, } from './methods/delta/isTokenSupportedInDelta'; +import { + constructBuildExternalDeltaOrder, + BuildExternalDeltaOrderFunctions, + BuildExternalDeltaOrderParams, +} from './methods/delta/buildExternalDeltaOrder'; +import type { SignableExternalOrderData } from './methods/delta/helpers/buildExternalOrderData'; +import { + constructSignExternalDeltaOrder, + SignExternalDeltaOrderFunctions, +} from './methods/delta/signExternalDeltaOrder'; +import { + constructPostExternalDeltaOrder, + PostExternalDeltaOrderFunctions, + PostExternalDeltaOrderParams, +} from './methods/delta/postExternalDeltaOrder'; +import { + constructPreSignExternalDeltaOrder, + PreSignExternalDeltaOrderFunctions, +} from './methods/delta/preSignExternalDeltaOrder'; + import { constructGetQuote, GetQuoteFunctions, @@ -235,8 +256,10 @@ export { export { constructAllDeltaOrdersHandlers, constructSubmitDeltaOrder, + constructSubmitExternalDeltaOrder, DeltaOrderHandlers, SubmitDeltaOrderParams, + SubmitExternalDeltaOrderParams, } from './methods/delta'; export type { @@ -309,6 +332,11 @@ export { constructCancelDeltaOrder, constructDeltaTokenModule, constructApproveTokenForDelta, + // External Delta methods + constructBuildExternalDeltaOrder, + constructSignExternalDeltaOrder, + constructPostExternalDeltaOrder, + constructPreSignExternalDeltaOrder, // Quote methods constructGetQuote, // different helpers @@ -382,7 +410,6 @@ export type { DeltaAuction, DeltaAuctionStatus, DeltaOrderFilterByStatus, - DeltaOrderApiResponse, DeltaOrderFromAPI, // bridge part of DeltaOrder BridgeMetadata, @@ -407,6 +434,16 @@ export type { CancelAndWithdrawDeltaOrderParams, DepositNativeAndPreSignParams, DepositNativeAndPreSignDeltaOrderParams, + // External Delta types + ExternalDeltaOrder, + OnChainOrderType, + SignableExternalOrderData, + BuildExternalDeltaOrderParams, + BuildExternalDeltaOrderFunctions, + SignExternalDeltaOrderFunctions, + PostExternalDeltaOrderFunctions, + PostExternalDeltaOrderParams, + PreSignExternalDeltaOrderFunctions, // types for Quote methods GetQuoteFunctions, QuoteParams, diff --git a/src/methods/delta/buildDeltaOrder.ts b/src/methods/delta/buildDeltaOrder.ts index 0261cea2..c84479cf 100644 --- a/src/methods/delta/buildDeltaOrder.ts +++ b/src/methods/delta/buildDeltaOrder.ts @@ -1,8 +1,4 @@ -import type { - ConstructFetchInput, - EnumerateLiteral, - RequestParameters, -} from '../../types'; +import type { ConstructFetchInput, RequestParameters } from '../../types'; import { constructGetDeltaContract } from './getDeltaContract'; import type { BridgePrice } from './getDeltaPrice'; import { constructGetPartnerFee } from './getPartnerFee'; @@ -11,14 +7,12 @@ import { type BuildDeltaOrderDataInput, type SignableDeltaOrderData, } from './helpers/buildDeltaOrderData'; +import type { AmountsWithSlippage } from './helpers/types'; import { SwapSideToOrderKind } from './helpers/types'; -import { SwapSide } from '../../constants'; -import { assert, type MarkOptional } from 'ts-essentials'; -import { ZERO_ADDRESS } from '../common/orders/buildOrderData'; +import { resolvePartnerFee, resolveAmounts } from './helpers/misc'; +import type { MarkOptional } from 'ts-essentials'; export type { SignableDeltaOrderData } from './helpers/buildDeltaOrderData'; -export type SwapSideUnion = EnumerateLiteral; - type BuildDeltaOrderDataParamsBase = { /** @description The address of the order owner */ owner: string; @@ -68,39 +62,8 @@ type BuildDeltaOrderDataParamsBase = { metadata?: string; }; -/** @description SELL with slippage: srcAmount provided, destAmount auto-computed from deltaPrice.destAmount */ -type DeltaAmountsSellSlippage = { - /** @description Slippage in basis points (bps). 10000 = 100%, 50 = 0.5% */ - slippage: number; - /** @description The amount of src token to swap */ - srcAmount: string; - destAmount?: never; - /** @description The side of the order */ - side?: 'SELL'; -}; -/** @description BUY with slippage: destAmount provided, srcAmount auto-computed from deltaPrice.srcAmount */ -type DeltaAmountsBuySlippage = { - /** @description Slippage in basis points (bps). 10000 = 100%, 50 = 0.5% */ - slippage: number; - /** @description The minimum amount of dest token to receive */ - destAmount: string; - srcAmount?: never; - /** @description The side of the order */ - side?: 'BUY'; -}; -/** @description Explicit amounts, no slippage (backward-compatible) */ -type DeltaAmountsExplicit = { - slippage?: never; - /** @description The amount of src token to swap */ - srcAmount: string; - /** @description The minimum amount of dest token to receive */ - destAmount: string; - /** @description The side of the order. Default is SELL */ - side?: SwapSideUnion; -}; - export type BuildDeltaOrderDataParams = BuildDeltaOrderDataParamsBase & - (DeltaAmountsSellSlippage | DeltaAmountsBuySlippage | DeltaAmountsExplicit); + AmountsWithSlippage; type BuildDeltaOrder = ( buildOrderParams: BuildDeltaOrderDataParams, @@ -128,83 +91,11 @@ export const constructBuildDeltaOrder = ( throw new Error(`Delta is not available on chain ${chainId}`); } - ////// Partner logic ////// - - // externally supplied partner fee data takes precedence - let partnerAddress = options.partnerAddress; - let partnerFeeBps = - options.partnerFeeBps ?? - (options.deltaPrice.partnerFee - ? options.deltaPrice.partnerFee * 100 - : undefined); - let partnerTakesSurplus = options.partnerTakesSurplus; - - // if fee given, takeSurplus is ignored - const feeOrTakeSurplusSupplied = - partnerFeeBps !== undefined || partnerTakesSurplus !== undefined; - - if (partnerAddress === undefined || feeOrTakeSurplusSupplied) { - const partner = options.partner || options.deltaPrice.partner; - if (!partner) { - // if no partner given in options or deltaPrice, default partnerAddress to zero, - // unless supplied explicitly - partnerAddress = partnerAddress ?? ZERO_ADDRESS; - } else { - const partnerFeeResponse = await getPartnerFee( - { partner }, - requestParams - ); - - partnerAddress = partnerAddress ?? partnerFeeResponse.partnerAddress; - // deltaPrice.partnerFee and partnerFeeResponse.partnerFee should be the same, but give priority to externally provided - partnerFeeBps = partnerFeeBps ?? partnerFeeResponse.partnerFee; - partnerTakesSurplus = - partnerTakesSurplus ?? partnerFeeResponse.takeSurplus; - } - } - - partnerFeeBps = partnerFeeBps ?? 0; - partnerTakesSurplus = partnerTakesSurplus ?? false; - - // Resolve srcAmount, destAmount and side. - // When slippage is used, side is inferred from which amount is present. - let srcAmount: string; - let destAmount: string; - - const swapSide: SwapSideUnion = - options.slippage !== undefined - ? options.srcAmount !== undefined - ? SwapSide.SELL - : SwapSide.BUY - : options.side ?? SwapSide.SELL; - - if (options.slippage !== undefined) { - if (options.srcAmount !== undefined) { - // SELL with slippage: destAmount auto-computed - srcAmount = options.srcAmount; - destAmount = applySlippage({ - amount: options.deltaPrice.destAmount, - slippageBps: options.slippage, - increase: false, - }); - } else { - // BUY with slippage: srcAmount auto-computed - destAmount = options.destAmount; - srcAmount = applySlippage({ - amount: options.deltaPrice.srcAmount, - slippageBps: options.slippage, - increase: true, - }); - } - } else { - srcAmount = options.srcAmount; - destAmount = options.destAmount; - } + const { partnerAddress, partnerFeeBps, partnerTakesSurplus } = + await resolvePartnerFee(options, getPartnerFee, requestParams); - const expectedAmount = - swapSide === SwapSide.SELL - ? options.deltaPrice.destAmount - : options.deltaPrice.srcAmount; + const { srcAmount, destAmount, expectedAmount, swapSide } = + resolveAmounts(options); const input: BuildDeltaOrderDataInput = { owner: options.owner, @@ -240,28 +131,3 @@ export const constructBuildDeltaOrder = ( buildDeltaOrder, }; }; - -type ApplySlippageInput = { - amount: string; - slippageBps: number; - increase: boolean; -}; - -function applySlippage({ - amount, - slippageBps, - increase, -}: ApplySlippageInput): string { - assert( - Number.isInteger(slippageBps) && slippageBps >= 0 && slippageBps <= 10_000, - 'slippageBps must be an integer between 0 and 10_000' - ); - - const BPS_BASE = 10_000n; - const amt = BigInt(amount); - const bps = BigInt(slippageBps); - - return increase - ? ((amt * (BPS_BASE + bps)) / BPS_BASE).toString(10) - : ((amt * (BPS_BASE - bps)) / BPS_BASE).toString(10); -} diff --git a/src/methods/delta/buildExternalDeltaOrder.ts b/src/methods/delta/buildExternalDeltaOrder.ts new file mode 100644 index 00000000..693571b6 --- /dev/null +++ b/src/methods/delta/buildExternalDeltaOrder.ts @@ -0,0 +1,124 @@ +import type { ConstructFetchInput, RequestParameters } from '../../types'; +import { constructGetDeltaContract } from './getDeltaContract'; +import type { DeltaPrice } from './getDeltaPrice'; +import { constructGetPartnerFee } from './getPartnerFee'; +import { + buildExternalOrderSignableData, + type BuildExternalOrderDataInput, + type SignableExternalOrderData, +} from './helpers/buildExternalOrderData'; +import type { AmountsWithSlippage } from './helpers/types'; +import { SwapSideToOrderKind } from './helpers/types'; +import type { MarkOptional } from 'ts-essentials'; +import { resolvePartnerFee, resolveAmounts } from './helpers/misc'; +export type { SignableExternalOrderData } from './helpers/buildExternalOrderData'; + +type BuildExternalDeltaOrderParamsBase = { + /** @description The address of the order owner */ + owner: string; + /** @description The address of the external handler contract */ + handler: string; + /** @description Protocol-specific encoded bytes for the external handler */ + data: string; + /** @description The address of the src token */ + srcToken: string; + /** @description The address of the dest token */ + destToken: string; + /** @description The deadline for the order */ + deadline?: number; + /** @description The nonce of the order */ + nonce?: number | string; + /** @description Optional permit signature for the src token */ + permit?: string; + /** @description Partner string */ + partner?: string; + /** @description partner fee in basis points (bps), 50bps=0.5% */ + partnerFeeBps?: number; + /** @description partner address */ + partnerAddress?: string; + /** @description take surplus */ + partnerTakesSurplus?: boolean; + /** @description A boolean indicating whether the surplus should be capped. True by default */ + capSurplus?: boolean; + /** @description Metadata for the order, hex string */ + metadata?: string; + + /** @description price response received from /delta/prices (getDeltaPrice method) */ + deltaPrice: MarkOptional< + Pick< + DeltaPrice, + 'destAmount' | 'partner' | 'partnerFee' | 'destToken' | 'srcAmount' + >, + 'partner' | 'partnerFee' + >; +}; + +export type BuildExternalDeltaOrderParams = BuildExternalDeltaOrderParamsBase & + AmountsWithSlippage; + +type BuildExternalDeltaOrder = ( + buildOrderParams: BuildExternalDeltaOrderParams, + requestParams?: RequestParameters +) => Promise; + +export type BuildExternalDeltaOrderFunctions = { + /** @description Build External Orders to be posted to Delta API for execution */ + buildExternalDeltaOrder: BuildExternalDeltaOrder; +}; + +export const constructBuildExternalDeltaOrder = ( + options: ConstructFetchInput +): BuildExternalDeltaOrderFunctions => { + const { chainId } = options; + + // cached internally + const { getDeltaContract } = constructGetDeltaContract(options); + // cached internally for `partner` + const { getPartnerFee } = constructGetPartnerFee(options); + + const buildExternalDeltaOrder: BuildExternalDeltaOrder = async ( + options, + requestParams + ) => { + const ParaswapDelta = await getDeltaContract(requestParams); + if (!ParaswapDelta) { + throw new Error(`Delta is not available on chain ${chainId}`); + } + + const { partnerAddress, partnerFeeBps, partnerTakesSurplus } = + await resolvePartnerFee(options, getPartnerFee, requestParams); + + const { srcAmount, destAmount, expectedAmount, swapSide } = + resolveAmounts(options); + + const input: BuildExternalOrderDataInput = { + owner: options.owner, + handler: options.handler, + srcToken: options.srcToken, + destToken: options.deltaPrice.destToken, + srcAmount, + destAmount, + expectedAmount, + deadline: options.deadline, + nonce: options.nonce?.toString(10), + permit: options.permit, + kind: SwapSideToOrderKind[swapSide], + metadata: options.metadata, + data: options.data, + + chainId, + paraswapDeltaAddress: ParaswapDelta, + partnerAddress, + partnerTakesSurplus, + partnerFeeBps, + + capSurplus: options.capSurplus, + }; + + return buildExternalOrderSignableData(input); + }; + + return { + buildExternalDeltaOrder, + }; +}; diff --git a/src/methods/delta/getDeltaOrders.ts b/src/methods/delta/getDeltaOrders.ts index 3c9c4146..412a1362 100644 --- a/src/methods/delta/getDeltaOrders.ts +++ b/src/methods/delta/getDeltaOrders.ts @@ -5,9 +5,14 @@ import type { ConstructFetchInput, RequestParameters, } from '../../types'; -import type { DeltaAuction, DeltaAuctionStatus } from './helpers/types'; +import type { + DeltaAuction, + DeltaAuctionStatus, + OnChainOrderType, +} from './helpers/types'; -export type DeltaOrderFromAPI = Omit; +/** @deprecated Use DeltaAuction directly */ +export type DeltaOrderFromAPI = DeltaAuction; export type DeltaOrderFilterByStatus = | DeltaAuctionStatus @@ -20,14 +25,14 @@ export type DeltaOrderFilterByStatus = type GetDeltaOrderById = ( orderId: string, requestParams?: RequestParameters -) => Promise; +) => Promise; type GetDeltaOrderByHash = ( orderHash: string, requestParams?: RequestParameters -) => Promise; +) => Promise; -type OrdersFilter = { +type OrdersFilter = { /** @description Order.owner to fetch Delta Order for */ userAddress: Address; /** @description Pagination option, page. Default 1 */ @@ -48,16 +53,23 @@ type OrdersFilter = { status?: DeltaOrderFilterByStatus[]; /** @description Filter by type. MARKET, LIMIT. Orders with both types are returned if not specified */ type?: 'MARKET' | 'LIMIT'; + /** @description Filter by on-chain order type. Order, ExternalOrder. Orders of all types are returned if not specified */ + onChainOrderType?: T; }; type OrderFiltersQuery = Omit & { chainId?: string; status?: string; }; -type GetDeltaOrders = ( - options: OrdersFilter, - requestParams?: RequestParameters -) => Promise; +type GetDeltaOrders = { + ( + options: OrdersFilter & { onChainOrderType: T }, + requestParams?: RequestParameters + ): Promise[]>; + (options: OrdersFilter, requestParams?: RequestParameters): Promise< + DeltaAuction[] + >; +}; type GetRequiredBalanceParams = { userAddress: Address; @@ -89,7 +101,7 @@ export const constructGetDeltaOrders = ({ ) => { const fetchURL = `${baseUrl}/${orderId}` as const; - const order = await fetcher({ + const order = await fetcher({ url: fetchURL, method: 'GET', requestParams, @@ -104,7 +116,7 @@ export const constructGetDeltaOrders = ({ ) => { const fetchURL = `${baseUrl}/hash/${orderHash}` as const; - const order = await fetcher({ + const order = await fetcher({ url: fetchURL, method: 'GET', requestParams, @@ -113,7 +125,12 @@ export const constructGetDeltaOrders = ({ return order; }; - const getDeltaOrders: GetDeltaOrders = async (options, requestParams) => { + const getDeltaOrders: GetDeltaOrders = async < + T extends OnChainOrderType = OnChainOrderType + >( + options: OrdersFilter, + requestParams?: RequestParameters + ) => { const chainIdString = options.chainId?.join(','); const statusString = options.status?.join(','); @@ -122,13 +139,14 @@ export const constructGetDeltaOrders = ({ page: options.page, limit: options.limit, type: options.type, + onChainOrderType: options.onChainOrderType, chainId: chainIdString, status: statusString, }); const fetchURL = `${baseUrl}${search}` as const; - const orders = await fetcher({ + const orders = await fetcher[]>({ url: fetchURL, method: 'GET', requestParams, diff --git a/src/methods/delta/helpers/abi.ts b/src/methods/delta/helpers/abi.ts new file mode 100644 index 00000000..6b310891 --- /dev/null +++ b/src/methods/delta/helpers/abi.ts @@ -0,0 +1,20 @@ +export const PreSignatureModuleAbi = [ + { + inputs: [ + { + internalType: 'bytes32', + name: 'orderHash', + type: 'bytes32', + }, + { + internalType: 'bool', + name: 'preSigned', + type: 'bool', + }, + ], + name: 'setPreSignature', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const; diff --git a/src/methods/delta/helpers/buildDeltaOrderData.ts b/src/methods/delta/helpers/buildDeltaOrderData.ts index c8bfb267..3913c3e7 100644 --- a/src/methods/delta/helpers/buildDeltaOrderData.ts +++ b/src/methods/delta/helpers/buildDeltaOrderData.ts @@ -1,6 +1,7 @@ import { MarkOptional } from 'ts-essentials'; -import { Domain, ZERO_ADDRESS } from '../../common/orders/buildOrderData'; +import { Domain } from '../../common/orders/buildOrderData'; import { Bridge, DeltaAuctionOrder } from './types'; +import { DELTA_DEFAULT_EXPIRY, producePartnerAndFee } from './misc'; // Order(address owner,address beneficiary,address srcToken,address destToken,uint256 srcAmount,uint256 destAmount,uint256 deadline,uint256 nonce,bytes permit, bridge Bridge)"; const SWAP_ORDER_EIP_712_TYPES = { @@ -64,7 +65,7 @@ export function produceDeltaOrderTypedData({ chainId, paraswapDeltaAddress, }: SignDeltaOrderInput): SignableDeltaOrderData { - const typedData = { + return { types: { Order: SWAP_ORDER_EIP_712_TYPES.Order, Bridge: SWAP_ORDER_EIP_712_TYPES.Bridge, @@ -77,8 +78,6 @@ export function produceDeltaOrderTypedData({ }, data: orderInput, }; - - return typedData; } export type DeltaOrderDataInput = MarkOptional< @@ -99,9 +98,6 @@ export type BuildDeltaOrderDataInput = MarkOptional< bridge: Bridge; }; -// default deadline = 1 hour for now (may be changed later) -export const DELTA_DEFAULT_EXPIRY = 60 * 60; // seconds - export function buildDeltaSignableOrderData({ owner, beneficiary = owner, @@ -157,31 +153,3 @@ export function buildDeltaSignableOrderData({ paraswapDeltaAddress, }); } - -type ProducePartnerAndFeeInput = { - partnerFeeBps: number; - partnerAddress: string; - partnerTakesSurplus: boolean; - capSurplus: boolean; -}; - -// fee and address are encoded together -function producePartnerAndFee({ - partnerFeeBps, - partnerAddress, - partnerTakesSurplus, - capSurplus, -}: ProducePartnerAndFeeInput): string { - const capSurplusShifted = BigInt(capSurplus) << BigInt(9); - if (partnerAddress === ZERO_ADDRESS) { - return capSurplusShifted.toString(10); - } else { - const partnerAndFee = - (BigInt(partnerAddress) << BigInt(96)) | - BigInt(partnerFeeBps.toFixed(0)) | - (BigInt(partnerTakesSurplus) << BigInt(8)) | - capSurplusShifted; - - return partnerAndFee.toString(10); - } -} diff --git a/src/methods/delta/helpers/buildExternalOrderData.ts b/src/methods/delta/helpers/buildExternalOrderData.ts new file mode 100644 index 00000000..75d70a32 --- /dev/null +++ b/src/methods/delta/helpers/buildExternalOrderData.ts @@ -0,0 +1,129 @@ +import { MarkOptional } from 'ts-essentials'; +import { Domain } from '../../common/orders/buildOrderData'; +import { ExternalDeltaOrder } from './types'; +import { DELTA_DEFAULT_EXPIRY, producePartnerAndFee } from './misc'; + +const EXTERNAL_ORDER_EIP_712_TYPES = { + ExternalOrder: [ + { name: 'owner', type: 'address' }, + { name: 'handler', type: 'address' }, + { name: 'srcToken', type: 'address' }, + { name: 'destToken', type: 'address' }, + { name: 'srcAmount', type: 'uint256' }, + { name: 'destAmount', type: 'uint256' }, + { name: 'expectedAmount', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + { name: 'kind', type: 'uint8' }, + { name: 'nonce', type: 'uint256' }, + { name: 'partnerAndFee', type: 'uint256' }, + { name: 'permit', type: 'bytes' }, + { name: 'metadata', type: 'bytes' }, + { name: 'data', type: 'bytes' }, + ], +}; + +export type SignableExternalOrderData = { + types: { + ExternalOrder: typeof EXTERNAL_ORDER_EIP_712_TYPES.ExternalOrder; + }; + domain: Domain; + data: ExternalDeltaOrder; +}; + +type SignExternalOrderInput = { + orderInput: ExternalDeltaOrder; + paraswapDeltaAddress: string; + chainId: number; +}; + +export function produceExternalOrderTypedData({ + orderInput, + chainId, + paraswapDeltaAddress, +}: SignExternalOrderInput): SignableExternalOrderData { + return { + types: { + ExternalOrder: EXTERNAL_ORDER_EIP_712_TYPES.ExternalOrder, + }, + domain: { + name: 'Portikus', + version: '2.0.0', + chainId, + verifyingContract: paraswapDeltaAddress, + }, + data: orderInput, + }; +} + +export type ExternalOrderDataInput = MarkOptional< + Omit, + 'deadline' | 'nonce' | 'permit' +>; + +export type BuildExternalOrderDataInput = MarkOptional< + ExternalOrderDataInput, + 'metadata' +> & { + partnerAddress: string; + paraswapDeltaAddress: string; + partnerFeeBps: number; + partnerTakesSurplus?: boolean; + capSurplus?: boolean; + chainId: number; +}; + +export function buildExternalOrderSignableData({ + owner, + handler, + + srcToken, + destToken, + srcAmount, + destAmount, + expectedAmount, + + deadline = Math.floor(Date.now() / 1000 + DELTA_DEFAULT_EXPIRY), + nonce = Date.now().toString(10), + + permit = '0x', + + kind, + metadata = '0x', + data, + + partnerAddress, + partnerFeeBps, + partnerTakesSurplus = false, + capSurplus = true, + + chainId, + paraswapDeltaAddress, +}: BuildExternalOrderDataInput): SignableExternalOrderData { + const orderInput: ExternalDeltaOrder = { + owner, + handler, + srcToken, + destToken, + srcAmount, + destAmount, + expectedAmount, + deadline, + nonce, + permit, + partnerAndFee: producePartnerAndFee({ + partnerFeeBps, + partnerAddress, + partnerTakesSurplus, + capSurplus, + }), + kind, + metadata, + data, + }; + + return produceExternalOrderTypedData({ + orderInput, + chainId, + paraswapDeltaAddress, + }); +} diff --git a/src/methods/delta/helpers/misc.ts b/src/methods/delta/helpers/misc.ts index 472bb7f4..dc0660c6 100644 --- a/src/methods/delta/helpers/misc.ts +++ b/src/methods/delta/helpers/misc.ts @@ -1,4 +1,179 @@ +import { ZERO_ADDRESS } from '../../common/orders/buildOrderData'; import type { SignableDeltaOrderData } from './buildDeltaOrderData'; +import type { SignableExternalOrderData } from './buildExternalOrderData'; +import type { GetPartnerFeeFunctions } from '../getPartnerFee'; +import type { RequestParameters } from '../../../types'; +import type { AmountsWithSlippage, SwapSideUnion } from './types'; +import { SwapSide } from '../../../constants'; +import { assert } from 'ts-essentials'; + +// default deadline = 10 min +export const DELTA_DEFAULT_EXPIRY = 10 * 60; // seconds + +type ProducePartnerAndFeeInput = { + partnerFeeBps: number; + partnerAddress: string; + partnerTakesSurplus: boolean; + capSurplus: boolean; +}; + +// fee and address are encoded together +export function producePartnerAndFee({ + partnerFeeBps, + partnerAddress, + partnerTakesSurplus, + capSurplus, +}: ProducePartnerAndFeeInput): string { + const capSurplusShifted = BigInt(capSurplus) << BigInt(9); + if (partnerAddress === ZERO_ADDRESS) { + return capSurplusShifted.toString(10); + } else { + const partnerAndFee = + (BigInt(partnerAddress) << BigInt(96)) | + BigInt(partnerFeeBps.toFixed(0)) | + (BigInt(partnerTakesSurplus) << BigInt(8)) | + capSurplusShifted; + + return partnerAndFee.toString(10); + } +} + +type ApplySlippageInput = { + amount: string; + slippageBps: number; + increase: boolean; +}; + +function applySlippage({ + amount, + slippageBps, + increase, +}: ApplySlippageInput): string { + assert( + Number.isInteger(slippageBps) && slippageBps >= 0 && slippageBps <= 10_000, + 'slippageBps must be an integer between 0 and 10_000' + ); + + const BPS_BASE = 10_000n; + const amt = BigInt(amount); + const bps = BigInt(slippageBps); + + return increase + ? ((amt * (BPS_BASE + bps)) / BPS_BASE).toString(10) + : ((amt * (BPS_BASE - bps)) / BPS_BASE).toString(10); +} + +export type ResolvePartnerFeeInput = { + partnerAddress?: string; + partnerFeeBps?: number; + partnerTakesSurplus?: boolean; + partner?: string; + deltaPrice: { partner?: string; partnerFee?: number }; +}; + +export type ResolvedPartnerFee = { + partnerAddress: string; + partnerFeeBps: number; + partnerTakesSurplus: boolean; +}; + +export async function resolvePartnerFee( + options: ResolvePartnerFeeInput, + getPartnerFee: GetPartnerFeeFunctions['getPartnerFee'], + requestParams?: RequestParameters +): Promise { + // externally supplied partner fee data takes precedence + let partnerAddress = options.partnerAddress; + let partnerFeeBps = + options.partnerFeeBps ?? + (options.deltaPrice.partnerFee + ? options.deltaPrice.partnerFee * 100 + : undefined); + let partnerTakesSurplus = options.partnerTakesSurplus; + + // if fee given, takeSurplus is ignored + const feeOrTakeSurplusSupplied = + partnerFeeBps !== undefined || partnerTakesSurplus !== undefined; + + if (partnerAddress === undefined || feeOrTakeSurplusSupplied) { + const partner = options.partner || options.deltaPrice.partner; + if (!partner) { + // if no partner given in options or deltaPrice, default partnerAddress to zero, + // unless supplied explicitly + partnerAddress = partnerAddress ?? ZERO_ADDRESS; + } else { + const partnerFeeResponse = await getPartnerFee( + { partner }, + requestParams + ); + + partnerAddress = partnerAddress ?? partnerFeeResponse.partnerAddress; + // deltaPrice.partnerFee and partnerFeeResponse.partnerFee should be the same, but give priority to externally provided + partnerFeeBps = partnerFeeBps ?? partnerFeeResponse.partnerFee * 100; + partnerTakesSurplus = + partnerTakesSurplus ?? partnerFeeResponse.takeSurplus; + } + } + + return { + partnerAddress: partnerAddress!, + partnerFeeBps: partnerFeeBps ?? 0, + partnerTakesSurplus: partnerTakesSurplus ?? false, + }; +} + +export type ResolveAmountsInput = AmountsWithSlippage & { + deltaPrice: { destAmount: string; srcAmount: string }; +}; + +export type ResolvedAmounts = { + srcAmount: string; + destAmount: string; + expectedAmount: string; + swapSide: SwapSideUnion; +}; + +export function resolveAmounts(options: ResolveAmountsInput): ResolvedAmounts { + let srcAmount: string; + let destAmount: string; + + const swapSide: SwapSideUnion = + options.slippage !== undefined + ? options.srcAmount !== undefined + ? SwapSide.SELL + : SwapSide.BUY + : options.side ?? SwapSide.SELL; + + if (options.slippage !== undefined) { + if (options.srcAmount !== undefined) { + // SELL with slippage: destAmount auto-computed + srcAmount = options.srcAmount; + destAmount = applySlippage({ + amount: options.deltaPrice.destAmount, + slippageBps: options.slippage, + increase: false, + }); + } else { + // BUY with slippage: srcAmount auto-computed + destAmount = options.destAmount; + srcAmount = applySlippage({ + amount: options.deltaPrice.srcAmount, + slippageBps: options.slippage, + increase: true, + }); + } + } else { + srcAmount = options.srcAmount; + destAmount = options.destAmount; + } + + const expectedAmount = + swapSide === SwapSide.SELL + ? options.deltaPrice.destAmount + : options.deltaPrice.srcAmount; + + return { srcAmount, destAmount, expectedAmount, swapSide }; +} export function sanitizeDeltaOrderData({ owner, @@ -34,3 +209,38 @@ export function sanitizeDeltaOrderData({ metadata, }; } + +export function sanitizeExternalOrderData({ + owner, + handler, + srcToken, + destToken, + srcAmount, + destAmount, + expectedAmount, + deadline, + nonce, + permit, + partnerAndFee, + kind, + metadata, + data, +}: SignableExternalOrderData['data'] & + Record): SignableExternalOrderData['data'] { + return { + owner, + handler, + srcToken, + destToken, + srcAmount, + destAmount, + expectedAmount, + deadline, + nonce, + permit, + partnerAndFee, + kind, + metadata, + data, + }; +} diff --git a/src/methods/delta/helpers/types.ts b/src/methods/delta/helpers/types.ts index 1feeffbd..f8b750fa 100644 --- a/src/methods/delta/helpers/types.ts +++ b/src/methods/delta/helpers/types.ts @@ -1,5 +1,44 @@ +import type { EnumerateLiteral } from '../../../types'; import { SwapSide } from '../../../constants'; +export type SwapSideUnion = EnumerateLiteral; + +/** @description SELL with slippage: srcAmount provided, destAmount auto-computed from deltaPrice.destAmount */ +export type AmountsSellSlippage = { + /** @description Slippage in basis points (bps). 10000 = 100%, 50 = 0.5% */ + slippage: number; + /** @description The amount of src token to swap */ + srcAmount: string; + destAmount?: never; + /** @description The side of the order */ + side?: 'SELL'; +}; +/** @description BUY with slippage: destAmount provided, srcAmount auto-computed from deltaPrice.srcAmount */ +export type AmountsBuySlippage = { + /** @description Slippage in basis points (bps). 10000 = 100%, 50 = 0.5% */ + slippage: number; + /** @description The minimum amount of dest token to receive */ + destAmount: string; + srcAmount?: never; + /** @description The side of the order */ + side?: 'BUY'; +}; +/** @description Explicit amounts, no slippage (backward-compatible) */ +export type AmountsExplicit = { + slippage?: never; + /** @description The amount of src token to swap */ + srcAmount: string; + /** @description The minimum amount of dest token to receive */ + destAmount: string; + /** @description The side of the order. Default is SELL */ + side?: SwapSideUnion; +}; + +export type AmountsWithSlippage = + | AmountsSellSlippage + | AmountsBuySlippage + | AmountsExplicit; + enum OrderKind { Sell = 0, Buy = 1, @@ -52,6 +91,37 @@ export type Bridge = { protocolData: string; // Hex string }; +export type ExternalDeltaOrder = { + /** @description The address of the order owner */ + owner: string; + /** @description The address of the external handler contract */ + handler: string; + /** @description The address of the src token */ + srcToken: string; + /** @description The address of the dest token */ + destToken: string; + /** @description The amount of src token to swap */ + srcAmount: string; + /** @description The minimum amount of dest token to receive */ + destAmount: string; + /** @description The expected amount of token to receive */ + expectedAmount: string; + /** @description The kind of the order */ + kind: OrderKind; + /** @description Metadata for the order, hex string */ + metadata: string; + /** @description The deadline for the order */ + deadline: number; + /** @description The nonce of the order */ + nonce: string; + /** @description Optional permit signature for the src token */ + permit: string; + /** @description Encoded partner address, fee bps, and flags for the order */ + partnerAndFee: string; + /** @description Protocol-specific encoded bytes for the external handler */ + data: string; +}; + export type DeltaAuctionStatus = | 'NOT_STARTED' | 'AWAITING_PRE_SIGNATURE' @@ -91,13 +161,16 @@ type DeltaAuctionTransaction = { auctionId: string; }; -export type DeltaAuction = { +export type OnChainOrderMap = { + Order: DeltaAuctionOrder; + ExternalOrder: ExternalDeltaOrder; +}; + +type DeltaAuctionBase = { id: string; deltaVersion: string; // 1.0 or 2.0 currently user: string; - signature: string; status: DeltaAuctionStatus; - order: DeltaAuctionOrder; orderHash: string | null; // not available on old Orders only transactions: DeltaAuctionTransaction[]; chainId: number; @@ -114,13 +187,17 @@ export type DeltaAuction = { bridgeMetadata: BridgeMetadata | null; bridgeStatus: BridgeStatus | null; - // @TODO only returned after POST Order so far - // orderVersion: string; // "2.0.0" - // deltaGasOverhead: number; - - type: 'MARKET' | 'LIMIT'; // @TODO when available in API for individual /order/:hash|:id + type: 'MARKET' | 'LIMIT'; }; +export type DeltaAuction = + T extends T + ? DeltaAuctionBase & { + onChainOrderType: T; + order: OnChainOrderMap[T]; + } + : never; + export type BridgeMetadata = { /** @description The amount that user should expect to get */ outputAmount: string; @@ -137,6 +214,8 @@ export type BridgeMetadata = { // refunded is basically failed export type BridgeStatus = 'pending' | 'filled' | 'expired' | 'refunded'; +export type OnChainOrderType = 'Order' | 'ExternalOrder'; + //// available on BridgePrice //// type BridgeQuoteFee = { diff --git a/src/methods/delta/index.ts b/src/methods/delta/index.ts index e18ebc63..c7fca266 100644 --- a/src/methods/delta/index.ts +++ b/src/methods/delta/index.ts @@ -1,4 +1,5 @@ import type { ConstructProviderFetchInput } from '../../types'; +import type { DeltaAuction } from './helpers/types'; import { BuildDeltaOrderDataParams, BuildDeltaOrderFunctions, @@ -6,7 +7,6 @@ import { } from './buildDeltaOrder'; import { constructPostDeltaOrder, - DeltaOrderApiResponse, DeltaOrderToPost, PostDeltaOrderFunctions, } from './postDeltaOrder'; @@ -54,6 +54,23 @@ import { DeltaTokenModuleFunctions, constructDeltaTokenModule, } from './deltaTokenModule'; +import { + BuildExternalDeltaOrderParams, + BuildExternalDeltaOrderFunctions, + constructBuildExternalDeltaOrder, +} from './buildExternalDeltaOrder'; +import { + constructSignExternalDeltaOrder, + SignExternalDeltaOrderFunctions, +} from './signExternalDeltaOrder'; +import { + constructPostExternalDeltaOrder, + PostExternalDeltaOrderFunctions, +} from './postExternalDeltaOrder'; +import { + constructPreSignExternalDeltaOrder, + PreSignExternalDeltaOrderFunctions, +} from './preSignExternalDeltaOrder'; export type SubmitDeltaOrderParams = BuildDeltaOrderDataParams & { /** @description designates the Order as being able to be partially filled, as opposed to fill-or-kill */ @@ -64,7 +81,7 @@ export type SubmitDeltaOrderParams = BuildDeltaOrderDataParams & { type SubmitDeltaOrder = ( orderParams: SubmitDeltaOrderParams -) => Promise; +) => Promise>; export type SubmitDeltaOrderFuncs = { submitDeltaOrder: SubmitDeltaOrder; @@ -99,6 +116,51 @@ export const constructSubmitDeltaOrder = ( return { submitDeltaOrder }; }; +export type SubmitExternalDeltaOrderParams = BuildExternalDeltaOrderParams & { + /** @description designates the Order as being able to be partially filled, as opposed to fill-or-kill */ + partiallyFillable?: boolean; + /** @description Referrer address */ + referrerAddress?: string; +} & Pick; + +type SubmitExternalDeltaOrder = ( + orderParams: SubmitExternalDeltaOrderParams +) => Promise>; + +export type SubmitExternalDeltaOrderFuncs = { + submitExternalDeltaOrder: SubmitExternalDeltaOrder; +}; + +export const constructSubmitExternalDeltaOrder = ( + options: ConstructProviderFetchInput +): SubmitExternalDeltaOrderFuncs => { + const { buildExternalDeltaOrder } = constructBuildExternalDeltaOrder(options); + const { signExternalDeltaOrder } = constructSignExternalDeltaOrder(options); + const { postExternalDeltaOrder } = constructPostExternalDeltaOrder(options); + + const submitExternalDeltaOrder: SubmitExternalDeltaOrder = async ( + orderParams + ) => { + const orderData = await buildExternalDeltaOrder(orderParams); + const signature = await signExternalDeltaOrder(orderData); + + const response = await postExternalDeltaOrder({ + signature, + partner: orderParams.partner, + order: orderData.data, + partiallyFillable: orderParams.partiallyFillable, + referrerAddress: orderParams.referrerAddress, + type: orderParams.type, + includeAgents: orderParams.includeAgents, + excludeAgents: orderParams.excludeAgents, + }); + + return response; + }; + + return { submitExternalDeltaOrder }; +}; + export type DeltaOrderHandlers = SubmitDeltaOrderFuncs & ApproveTokenForDeltaFunctions & BuildDeltaOrderFunctions & @@ -112,7 +174,12 @@ export type DeltaOrderHandlers = SubmitDeltaOrderFuncs & SignDeltaOrderFunctions & PreSignDeltaOrderFunctions & CancelDeltaOrderFunctions & - DeltaTokenModuleFunctions; + DeltaTokenModuleFunctions & + SubmitExternalDeltaOrderFuncs & + BuildExternalDeltaOrderFunctions & + SignExternalDeltaOrderFunctions & + PostExternalDeltaOrderFunctions & + PreSignExternalDeltaOrderFunctions; /** @description construct SDK with every Delta Order-related method, fetching from API and Order signing */ export const constructAllDeltaOrdersHandlers = ( @@ -142,6 +209,13 @@ export const constructAllDeltaOrdersHandlers = ( const deltaTokenModule = constructDeltaTokenModule(options); + const externalDeltaOrdersSubmit = constructSubmitExternalDeltaOrder(options); + const externalDeltaOrdersBuild = constructBuildExternalDeltaOrder(options); + const externalDeltaOrdersSign = constructSignExternalDeltaOrder(options); + const externalDeltaOrdersPost = constructPostExternalDeltaOrder(options); + const externalDeltaOrdersPreSign = + constructPreSignExternalDeltaOrder(options); + return { ...deltaOrdersGetters, ...deltaOrdersContractGetter, @@ -157,5 +231,10 @@ export const constructAllDeltaOrdersHandlers = ( ...deltaOrdersPost, ...deltaOrdersCancel, ...deltaTokenModule, + ...externalDeltaOrdersSubmit, + ...externalDeltaOrdersBuild, + ...externalDeltaOrdersSign, + ...externalDeltaOrdersPost, + ...externalDeltaOrdersPreSign, }; }; diff --git a/src/methods/delta/postDeltaOrder.ts b/src/methods/delta/postDeltaOrder.ts index 5c457ec0..4745c7f7 100644 --- a/src/methods/delta/postDeltaOrder.ts +++ b/src/methods/delta/postDeltaOrder.ts @@ -1,13 +1,17 @@ import { API_URL } from '../../constants'; import type { ConstructFetchInput, RequestParameters } from '../../types'; -import { DeltaAuctionOrder, DeltaAuction } from './helpers/types'; +import type { + DeltaAuction, + OnChainOrderMap, + OnChainOrderType, +} from './helpers/types'; -export type DeltaOrderToPost = { +export type DeltaOrderToPost = { /** @description Partner string */ partner?: string; /** @description Referrer address */ referrerAddress?: string; - order: DeltaAuctionOrder; + order: OnChainOrderMap[T]; /** @description Signature of the order from order.owner address. EOA signatures must be submitted in ERC-2098 Compact Representation. */ signature: string; chainId: number; @@ -23,16 +27,10 @@ export type DeltaOrderToPost = { export type PostDeltaOrderParams = Omit; -export type DeltaOrderApiResponse = Omit & { - orderVersion: string; // "2.0.0" - deltaGasOverhead: number; // @TODO may be removed - type: 'MARKET' | 'LIMIT'; -}; - type PostDeltaOrder = ( postData: PostDeltaOrderParams, requestParams?: RequestParameters -) => Promise; +) => Promise>; export type PostDeltaOrderFunctions = { postDeltaOrder: PostDeltaOrder; @@ -48,7 +46,7 @@ export const constructPostDeltaOrder = ({ const postDeltaOrder: PostDeltaOrder = (postData, requestParams) => { const deltaOrderToPost: DeltaOrderToPost = { ...postData, chainId }; - return fetcher({ + return fetcher>({ url: postOrderUrl, method: 'POST', data: deltaOrderToPost, diff --git a/src/methods/delta/postExternalDeltaOrder.ts b/src/methods/delta/postExternalDeltaOrder.ts new file mode 100644 index 00000000..e3257d6d --- /dev/null +++ b/src/methods/delta/postExternalDeltaOrder.ts @@ -0,0 +1,45 @@ +import { API_URL } from '../../constants'; +import type { ConstructFetchInput, RequestParameters } from '../../types'; +import type { DeltaAuction } from './helpers/types'; +import type { DeltaOrderToPost } from './postDeltaOrder'; + +export type PostExternalDeltaOrderParams = Omit< + DeltaOrderToPost<'ExternalOrder'>, + 'chainId' +>; + +type PostExternalDeltaOrder = ( + postData: PostExternalDeltaOrderParams, + requestParams?: RequestParameters +) => Promise>; + +export type PostExternalDeltaOrderFunctions = { + postExternalDeltaOrder: PostExternalDeltaOrder; +}; + +export const constructPostExternalDeltaOrder = ({ + apiURL = API_URL, + chainId, + fetcher, +}: ConstructFetchInput): PostExternalDeltaOrderFunctions => { + const postOrderUrl = `${apiURL}/delta/orders` as const; + + const postExternalDeltaOrder: PostExternalDeltaOrder = ( + postData, + requestParams + ) => { + const deltaOrderToPost: DeltaOrderToPost<'ExternalOrder'> = { + ...postData, + chainId, + }; + + return fetcher>({ + url: postOrderUrl, + method: 'POST', + data: deltaOrderToPost, + requestParams, + }); + }; + + return { postExternalDeltaOrder }; +}; diff --git a/src/methods/delta/preSignDeltaOrder.ts b/src/methods/delta/preSignDeltaOrder.ts index 8109ea9c..ab511dbe 100644 --- a/src/methods/delta/preSignDeltaOrder.ts +++ b/src/methods/delta/preSignDeltaOrder.ts @@ -9,6 +9,7 @@ import { SignableDeltaOrderData, } from './helpers/buildDeltaOrderData'; import { sanitizeDeltaOrderData } from './helpers/misc'; +import { PreSignatureModuleAbi } from './helpers/abi'; import type { ExtractAbiMethodNames } from '../../helpers/misc'; import { findPrimaryType } from '../../helpers/providers/helpers'; import { constructGetDeltaContract } from './getDeltaContract'; @@ -42,27 +43,6 @@ export type PreSignDeltaOrderFunctions = { preSignDeltaOrder: PreSignDeltaOrder; }; -const PreSignatureModuleAbi = [ - { - inputs: [ - { - internalType: 'bytes32', - name: 'orderHash', - type: 'bytes32', - }, - { - internalType: 'bool', - name: 'preSigned', - type: 'bool', - }, - ], - name: 'setPreSignature', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const; - type AvailableMethods = ExtractAbiMethodNames; // returns whatever `contractCaller` returns diff --git a/src/methods/delta/preSignExternalDeltaOrder.ts b/src/methods/delta/preSignExternalDeltaOrder.ts new file mode 100644 index 00000000..4095aa2a --- /dev/null +++ b/src/methods/delta/preSignExternalDeltaOrder.ts @@ -0,0 +1,141 @@ +import { hashTypedData } from 'viem/utils'; +import type { + ConstructProviderFetchInput, + RequestParameters, + TxSendOverrides, +} from '../../types'; +import { + produceExternalOrderTypedData, + SignableExternalOrderData, +} from './helpers/buildExternalOrderData'; +import { sanitizeExternalOrderData } from './helpers/misc'; +import { PreSignatureModuleAbi } from './helpers/abi'; +import type { ExtractAbiMethodNames } from '../../helpers/misc'; +import { findPrimaryType } from '../../helpers/providers/helpers'; +import { constructGetDeltaContract } from './getDeltaContract'; +import type { ExternalDeltaOrder } from './helpers/types'; + +type HashExternalDeltaOrderTypedData = ( + signableOrderData: SignableExternalOrderData +) => string; + +type HashExternalDeltaOrder = ( + orderData: ExternalDeltaOrder, + requestParams?: RequestParameters +) => Promise; + +export type SetExternalDeltaOrderPreSignature = ( + orderHash: string, + overrides?: TxSendOverrides, + requestParams?: RequestParameters +) => Promise; + +export type PreSignExternalDeltaOrder = ( + signableOrderData: SignableExternalOrderData, + overrides?: TxSendOverrides, + requestParams?: RequestParameters +) => Promise; + +export type PreSignExternalDeltaOrderFunctions = { + hashExternalDeltaOrderTypedData: HashExternalDeltaOrderTypedData; + hashExternalDeltaOrder: HashExternalDeltaOrder; + setExternalDeltaOrderPreSignature: SetExternalDeltaOrderPreSignature; + preSignExternalDeltaOrder: PreSignExternalDeltaOrder; +}; + +type AvailableMethods = ExtractAbiMethodNames; + +// returns whatever `contractCaller` returns +// to allow for better versatility +export const constructPreSignExternalDeltaOrder = ( + options: ConstructProviderFetchInput +): PreSignExternalDeltaOrderFunctions => { + const hashExternalDeltaOrderTypedData: HashExternalDeltaOrderTypedData = ( + typedData + ) => { + // types allow to pass OrderData & extra_stuff, but tx will break like that + const typedDataOnly: SignableExternalOrderData = { + ...typedData, + data: sanitizeExternalOrderData(typedData.data), + }; + + const orderHash = produceExternalOrderHash(typedDataOnly); + + return orderHash; + }; + // cached internally + const { getDeltaContract } = constructGetDeltaContract(options); + + const hashExternalDeltaOrder: HashExternalDeltaOrder = async ( + orderData, + requestParams + ) => { + const ParaswapDelta = await getDeltaContract(requestParams); + if (!ParaswapDelta) { + throw new Error(`Delta is not available on chain ${options.chainId}`); + } + + const typedData = produceExternalOrderTypedData({ + orderInput: orderData, + chainId: options.chainId, + paraswapDeltaAddress: ParaswapDelta, + }); + return hashExternalDeltaOrderTypedData(typedData); + }; + + const setExternalDeltaOrderPreSignature: SetExternalDeltaOrderPreSignature< + T + > = async (orderHash, overrides = {}, requestParams) => { + const ParaswapDelta = await getDeltaContract(requestParams); + if (!ParaswapDelta) { + throw new Error(`Delta is not available on chain ${options.chainId}`); + } + + const res = await options.contractCaller.transactCall({ + address: ParaswapDelta, + abi: PreSignatureModuleAbi, + contractMethod: 'setPreSignature', + args: [orderHash, true], + overrides, + }); + + return res; + }; + + const preSignExternalDeltaOrder: PreSignExternalDeltaOrder = async ( + signableOrderData, + overrides = {}, + requestParams + ) => { + const orderHash = hashExternalDeltaOrderTypedData(signableOrderData); + const res = await setExternalDeltaOrderPreSignature( + orderHash, + overrides, + requestParams + ); + return res; + }; + + return { + hashExternalDeltaOrderTypedData, + hashExternalDeltaOrder, + setExternalDeltaOrderPreSignature, + preSignExternalDeltaOrder, + }; +}; + +export function produceExternalOrderHash( + typedData: SignableExternalOrderData +): string { + return hashTypedData({ + domain: { + name: typedData.domain.name, + version: typedData.domain.version, + chainId: typedData.domain.chainId, + verifyingContract: typedData.domain.verifyingContract as `0x${string}`, + }, + types: typedData.types, + primaryType: findPrimaryType(typedData.types), + message: typedData.data, + }); +} diff --git a/src/methods/delta/signExternalDeltaOrder.ts b/src/methods/delta/signExternalDeltaOrder.ts new file mode 100644 index 00000000..2fc4d6e1 --- /dev/null +++ b/src/methods/delta/signExternalDeltaOrder.ts @@ -0,0 +1,35 @@ +import type { ConstructProviderFetchInput } from '../../types'; +import { SignableExternalOrderData } from './helpers/buildExternalOrderData'; +import { sanitizeExternalOrderData } from './helpers/misc'; + +type SignExternalDeltaOrder = ( + signableOrderData: SignableExternalOrderData +) => Promise; + +export type SignExternalDeltaOrderFunctions = { + signExternalDeltaOrder: SignExternalDeltaOrder; +}; + +// returns whatever `contractCaller` returns +// to allow for better versatility +export const constructSignExternalDeltaOrder = ( + options: Pick< + ConstructProviderFetchInput, + 'contractCaller' + > +): SignExternalDeltaOrderFunctions => { + const signExternalDeltaOrder: SignExternalDeltaOrder = async (typedData) => { + // types allow to pass OrderData & extra_stuff, but tx will break like that + const typedDataOnly: SignableExternalOrderData = { + ...typedData, + data: sanitizeExternalOrderData(typedData.data), + }; + const signature = await options.contractCaller.signTypedDataCall( + typedDataOnly + ); + + return signature; + }; + + return { signExternalDeltaOrder }; +}; diff --git a/src/sdk/partial.ts b/src/sdk/partial.ts index 437e95dd..29e6f7ba 100644 --- a/src/sdk/partial.ts +++ b/src/sdk/partial.ts @@ -13,6 +13,7 @@ import type { ApproveTokenForNFTOrderFunctions } from '../methods/nftOrders/appr import type { FillOrderDirectlyFunctions } from '../methods/limitOrders/fillOrderDirectly'; import type { ApproveTokenForDeltaFunctions } from '../methods/delta/approveForDelta'; import type { PreSignDeltaOrderFunctions } from '../methods/delta/preSignDeltaOrder'; +import type { PreSignExternalDeltaOrderFunctions } from '../methods/delta/preSignExternalDeltaOrder'; import type { DeltaTokenModuleFunctions } from '../methods/delta/deltaTokenModule'; import { API_URL, DEFAULT_VERSION } from '../constants'; @@ -54,6 +55,7 @@ type InferWithTxResponse< ApproveTokenForNFTOrderFunctions, ApproveTokenForDeltaFunctions, PreSignDeltaOrderFunctions, + PreSignExternalDeltaOrderFunctions, DeltaTokenModuleFunctions ] // then merge IntersectionOfReturns with them recursively diff --git a/tests/__snapshots__/delta.test.ts.snap b/tests/__snapshots__/delta.test.ts.snap index 8f203e3a..4c36b4de 100644 --- a/tests/__snapshots__/delta.test.ts.snap +++ b/tests/__snapshots__/delta.test.ts.snap @@ -126,6 +126,7 @@ exports[`Delta:methods Get Delta Order by Id and Hash 1`] = ` "expiresAt": "2025-04-18T15:03:33.000Z", "id": "7ec0dc82-98ad-4501-9f46-03e31e51098f", "includeAgents": null, + "onChainOrderType": "Order", "order": { "beneficiary": "0x0ddc793680ff4f5793849c8c6992be1695cbe72a", "bridge": { @@ -195,6 +196,7 @@ exports[`Delta:methods Get Delta Orders for user 1`] = ` "expiresAt": "2024-10-10T17:17:47.000Z", "id": "8515cce6-c7c6-486b-9f1e-5702f204edd6", "includeAgents": null, + "onChainOrderType": "Order", "order": { "beneficiary": "0x76176c2971300217e9f48e3dd4e40591500b96ff", "bridge": { @@ -259,6 +261,7 @@ exports[`Delta:methods Get Delta Orders for user 1`] = ` "expiresAt": "2024-10-09T17:52:08.000Z", "id": "7696f983-4f0d-4bb0-b591-61957abf74de", "includeAgents": null, + "onChainOrderType": "Order", "order": { "beneficiary": "0x76176c2971300217e9f48e3dd4e40591500b96ff", "bridge": {