From a4a997b154b0023daae0bbeca2ff58023dfb132b Mon Sep 17 00:00:00 2001 From: Velenir Date: Mon, 9 Dec 2024 12:58:25 +0300 Subject: [PATCH] Feat/Delta support (#183) * update build dependencies * update tsconfig * actions/remove Node18 * fix types * extra gitignore * update typedoc deps * update snapshots * abstract away providers/findPrimaryType * add TransactionParams.maxFeeGas params * providers/viem * export from providers/viem * DEFAULT_VERSION better type * add wagmi & viem deps * SimpleSDK/viem for contractCaller * examples/viem * cleanup * examples/wagmi * fix dependencies * tests/fix types * examples/fix types * tests/fix types * viem/fix account usage * examples/viem/fix account usage * types/TxHash=Hex * add hardhat * legacy/fix types * ignore cache files * some tests * tsconfig/allow BigInt short notation * providers/viem/reuse account * viem/fix tests * tests/viem/remove extra * tests/viem/Order signing * rename stuff * cleanup * reenable tests * tests/viem/const acc * tests/viem/update snapshots * reexport txParamsToViemTxParams * tests/viem/update snapshots * more comments * cleanup * remove extra deps * moved bignumber.js to devDeps * cleanup * update tests jpeg * update README * update snapshots * FetcherError/fix types * viem test/market swap * fix deps * update deps * update snapshots * remove dummy test * change default version * update some deps * replace ganache with hardhat * remove ganache * fix chainId in tests mismatch * try with tevm * tevm test * add hardhat-switch-network plugin * simplify hardhat helpers * make tests work with hardhat * fix method name * disable web3 tests that break with hardhat * fix NFT tests * remove tevm * hardhat config/explicit gasPrice * NFT tests/adjust amounts * cleanup * remove temp tests * update some deps * NFT tests/workaround some errors * update ethers dep * update web3 dep(slightly) * update deps * override some deps * update some deps * hadrhat config/smaller default baseFee * NFT tests/workaround edge case * update web3 dep * update Web3 types * update web3/constructContractCaller * legacy/update types * hardhat config/fixed accounts * LOrder tests/reenable sign with web3 * NFT Order tests/reenable sign with web3 * getBalance tests workaround * cleanup * update some deps * update snapshots * update perrDeps * update required Node v * NFT tests/account for dust * hadrhat config/lower initialBaseFeePerGas * install ethersV6 as alias * providers/ethersV6 * distinct ethersV5 exports * legacy/support ethersV6 * simple SDK/support ethersV6 * simpleSDK.tests/add ethersV6 * partialSDK.tests/add ethersV6 * LOrders.tests/add ethersV6 * NFT_Orders.tests/add ethersV6 * update snapshots * examples/ add ethersV6 * perrDeps/update ethers versions * update snapshots * cleanup * delta/approve * delta/getContract * delta/getOrders * delta/some types * delta/getPrice * delta/getPartnerFee * delta/sanitizeOrderData * delta/composePermit * delta/buildOrderData * delta/buildData * delta/signOrder * extend AdaptersContractsResult * delta/postOrder * delta/submitOrder & allHandlers * delta/buildOrder/explicit BuildDeltaOrderDataParams * add PartnerFeeResponse.takeSurplus * reexport Delta methods & types * BuildDeltaOrderDataParams.partner? * DeltaPriceParams.*decimals: required * examples/delta * fullSDK/delta methods * simpleSDK/delta methods * producePartnerAndFee |= (takeSuplus << 8) * examples/delta/manualDeltaFlow * cleanup * move ethers types to provider/ethers * move web3 types to provider/web3 * untie FetchError type from AxiosError * cleanup * reexport DeltaPriceParams * ethers -> ethersV5, ethersV6 -> ethers to fix types when used as lib * deriveCompactSignature * signDeltaOrder/deriveCompactSignature * move ethers types to provider/ethers * move web3 types to provider/web3 * untie FetchError type from AxiosError * ethers -> ethersV5, ethersV6 -> ethers to fix types when used as lib * cleanup * fetch/remove unnecessary headers * delta/examples/fix slippage * reexport more types * cleanup * delta/getPrices/fix query params * type DeltaPrice/add fields * Partial/delta/approve/fix tx type * ethersV6/more explicit Contract method calling * delta/add tests * delta/getPartnerFee/temp workaround for API response * README/update version * README/update version * less dependency on ethers types * update README * cleanup * remove default partner * remove temp tests * ParaswapDeltaAuction.orderHash: string | null * cleanup * fix typos * cleanup * replace deprecated method * Delta/signDeltaOrder/skip compact sig when incompatible * update Delta Order types * update tests * fix typo * Delta/update types * tests/Delta/update snapshots * Delta/annotate some types * Delta/getDEltaOrders/pagination options * Feat/Quote support (#185) * quote/getQuote * reexport from quote * simpleSDK/add quote * partialSDK/simplify type * fullSDK/add quote * examples/simple/fix wrong Token * quote/getQuote/more specific types * examples/quote * delta/buildDeltaOrders/allow to override partnerFee data * quote/add tests * examples/simpleQuote * examples/quote/ fix amount var * cleanup * README/add quote usage * buildDeltaOrder/partnerAndFee encoding shortcut * cleanup * DeltaAuctionOrder.nonce: number -> string * test/delta/update * tests/quote/update * quote/getQuote res/fallback_reason -> fallbackReason * quote/annotate some types * tests/quote/update snaps * quote/getQuote/update response types * tests/quote/add gas cost error test * tests/quote/fix purposefully failing tests * tests/delta/fix orders order * examples/quote/fix types * fix linter * examples/delta/improve flow * delta/Permit/composeDeltaOrderPermit/special handling * Readme/DELTA.md * Readme/update * cleanup * add type ParaswapDeltaAuction.deltaVersion * tests/delta/update snapshots * fix typo Co-authored-by: andriy-shymkiv <102289654+andriy-shymkiv@users.noreply.github.com> --------- Co-authored-by: andriy-shymkiv <102289654+andriy-shymkiv@users.noreply.github.com> --------- Co-authored-by: andriy-shymkiv <102289654+andriy-shymkiv@users.noreply.github.com> --- DELTA.md | 89 ++++ README.md | 114 ++++- docs/passed_tests.png | Bin 0 -> 80339 bytes hardhat.config.ts | 2 +- src/examples/delta.ts | 127 +++++ src/examples/quote.ts | 205 ++++++++ src/examples/simple.ts | 2 +- src/examples/simpleQuote.ts | 95 ++++ src/helpers/fetchers/fetch.ts | 2 +- src/helpers/misc.ts | 54 ++ src/helpers/providers/ethersV6.ts | 6 +- src/index.ts | 93 ++++ src/methods/delta/approveForDelta.ts | 35 ++ src/methods/delta/buildDeltaOrder.ts | 110 ++++ src/methods/delta/getDeltaContract.ts | 23 + src/methods/delta/getDeltaOrders.ts | 73 +++ src/methods/delta/getDeltaPrice.ts | 86 ++++ src/methods/delta/getPartnerFee.ts | 58 +++ .../delta/helpers/buildDeltaOrderData.ts | 139 ++++++ src/methods/delta/helpers/composePermit.ts | 76 +++ src/methods/delta/helpers/misc.ts | 30 ++ src/methods/delta/helpers/types.ts | 72 +++ src/methods/delta/index.ts | 119 +++++ src/methods/delta/postDeltaOrder.ts | 48 ++ src/methods/delta/signDeltaOrder.ts | 54 ++ src/methods/quote/getQuote.ts | 127 +++++ .../swap/helpers/normalizeRateOptions.ts | 4 +- src/methods/swap/spender.ts | 4 + src/sdk/full.ts | 28 +- src/sdk/partial.ts | 19 +- src/sdk/simple.ts | 66 +++ src/types.ts | 2 +- tests/__snapshots__/delta.test.ts.snap | 269 ++++++++++ tests/__snapshots__/quote.test.ts.snap | 83 +++ tests/delta.test.ts | 471 ++++++++++++++++++ tests/quote.test.ts | 299 +++++++++++ 36 files changed, 3059 insertions(+), 25 deletions(-) create mode 100644 DELTA.md create mode 100644 docs/passed_tests.png create mode 100644 src/examples/delta.ts create mode 100644 src/examples/quote.ts create mode 100644 src/examples/simpleQuote.ts create mode 100644 src/methods/delta/approveForDelta.ts create mode 100644 src/methods/delta/buildDeltaOrder.ts create mode 100644 src/methods/delta/getDeltaContract.ts create mode 100644 src/methods/delta/getDeltaOrders.ts create mode 100644 src/methods/delta/getDeltaPrice.ts create mode 100644 src/methods/delta/getPartnerFee.ts create mode 100644 src/methods/delta/helpers/buildDeltaOrderData.ts create mode 100644 src/methods/delta/helpers/composePermit.ts create mode 100644 src/methods/delta/helpers/misc.ts create mode 100644 src/methods/delta/helpers/types.ts create mode 100644 src/methods/delta/index.ts create mode 100644 src/methods/delta/postDeltaOrder.ts create mode 100644 src/methods/delta/signDeltaOrder.ts create mode 100644 src/methods/quote/getQuote.ts create mode 100644 tests/__snapshots__/delta.test.ts.snap create mode 100644 tests/__snapshots__/quote.test.ts.snap create mode 100644 tests/delta.test.ts create mode 100644 tests/quote.test.ts diff --git a/DELTA.md b/DELTA.md new file mode 100644 index 00000000..5cee9647 --- /dev/null +++ b/DELTA.md @@ -0,0 +1,89 @@ +**ParaSwap Delta** is an intent-based protocol that enables a ParaSwap user to make gasless swaps where multiple agents compete to execute the trade at the best price possible. +This way the user doesn't need to make a transaction themselve but only to sign a Delta Order. +The easiest way to make use of the Delta Order is to use the SDK following these steps: + +### 1. Construct an SDK object + +```ts +const account = userAddress; +const paraSwap = constructSimpleSDK( + {chainId: 1, axios}, + { + ethersProviderOrSigner: provider, // JsonRpcProvider + EthersContract: ethers.Contract, + account, + }); + // for usage with different web3 provider libraries refer to the main [README](./README.md) +``` + +### 2. Request prices for a Token pair + +```ts +const DAI_TOKEN = '0x6b175474e89094c44da98b954eedeac495271d0f'; +const PSP_TOKEN = '0xcafe001067cdef266afb7eb5a286dcfd277f3de5'; +const amount = '1000000000000'; // in wei + +const deltaPrice = await deltaSDK.getDeltaPrice({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + amount, + userAddress: account, + srcDecimals: 18, + destDecimals: 18, + // partner: "..." // if available +}); +``` + + +### 3. Approve srcToken for DeltaContract + +```ts +const tx = await deltaSDK.approveTokenForDelta(amount, DAI_TOKEN); +await tx.wait(); +``` + +Alternatively sign Permit (DAI or Permit1) or Permit2 TransferFrom with DeltaContract as the verifyingContract + +```ts +const DeltaContract = await deltaSDK.getDeltaContract(); + +// values depend on the Permit type and the srcToken +const signature = await signer._signTypedData(domain, types, message); +``` + +See more on accepted Permit variants in [ParaSwap documentation](https://developers.paraswap.network/api/paraswap-delta/build-and-sign-a-delta-order#supported-permits) + + +### 4. Sign and submit a Delta Order + +```ts +// calculate acceptable destAmount +const slippagePercent = 0.5; + const destAmountAfterSlippage = ( + +deltaPrice.destAmount * + (1 - slippagePercent / 100) + ).toString(10); + +const signableOrderData = await deltaSDK.buildDeltaOrder({ + deltaPrice, + owner: account, + // beneficiary: anotherAccount, // if need to send destToken to another account + // permit: "0x1234...", // if signed a Permit1 or Permit2 TransferFrom for DeltaContract + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + srcAmount: amount, + destAmount: destAmountAfterSlippage, // minimum acceptable destAmount +}); +``` + +### 5. Wait for Delta Order execution + +```ts +// poll if necessary +const auction = await deltaSDK.getDeltaOrderById(deltaAuction.id); +if (auction?.status === 'EXECUTED') { + console.log('Auction was executed'); +} +``` + +#### A more detailed example of Delta Order usage can be found in [examples/delta](./src/examples/delta.ts) \ No newline at end of file diff --git a/README.md b/README.md index d40bb8cf..62b7220e 100644 --- a/README.md +++ b/README.md @@ -82,13 +82,20 @@ Can be created by providing `chainId` and either `axios` or `window.fetch` (or a If optional `providerOptions` is provided as the second parameter, then the resulting SDK will also be able to approve Tokens for swap. ```ts - // with ethers.js - const providerOptionsEther = { + // with ethers@5 + const providerOptionsEtherV5 = { ethersProviderOrSigner: provider, // JsonRpcProvider EthersContract: ethers.Contract, account: senderAddress, }; + // with ethers@6 + const providerOptionsEtherV6 = { + ethersV6ProviderOrSigner: provider, // JsonRpcProvider + EthersV6Contract: ethers.Contract, + account: senderAddress, + }; + // or with viem (from wagmi or standalone) const providerOptionsViem = { viemClient, // made with createWalletClient() @@ -101,7 +108,7 @@ If optional `providerOptions` is provided as the second parameter, then the resu account: senderAddress, }; - const paraSwap = constructSimpleSDK({chainId: 1, axios}, providerOptionsEther); + const paraSwap = constructSimpleSDK({chainId: 1, axios}, providerOptionsEtherV5); // approve token through sdk const txHash = await paraSwap.approveToken(amountInWei, DAI); @@ -149,6 +156,105 @@ const priceRoute = await minParaSwap.getRate(params); const allowance = await minParaSwap.getAllowance(userAddress, tokenAddress); ``` +### Basic usage + +The easiest way to make a trade is to rely on Quote method that communicates with [/quote API endpoint](https://developers.paraswap.network/api/paraswap-delta/retrieve-delta-price-with-fallback-to-market-quote) + +```typescript +import axios from 'axios'; +import { ethers } from 'ethersV5'; +import { constructSimpleSDK } from '@paraswap/sdk'; + +const ethersProvider = new ethers.providers.Web3Provider(window.ethereum); + +const accounts = await ethersProvider.listAccounts(); +const account = accounts[0]!; +const signer = ethersProvider.getSigner(account); + +const simpleSDK = constructSimpleSDK( + { chainId: 1, axios }, + { + ethersProviderOrSigner: signer, + EthersContract: ethers.Contract, + account, + } +); + +const amount = '1000000000000'; // wei +const Token1 = '0x1234...' +const Token2 = '0xabcde...' + +const quote = await simpleSDK.quote.getQuote({ + srcToken: Token1, + destToken: Token2, + amount, + userAddress: account, + srcDecimals: 18, + destDecimals: 18, + mode: 'all', // Delta quote if possible, with fallback to Market price + side: 'SELL', + // partner: "..." // if available +}); + +if ('delta' in quote) { + const deltaPrice = quote.delta; + + const DeltaContract = await simpleSDK.delta.getDeltaContract(); + + // or sign a Permit1 or Permit2 TransferFrom for DeltaContract + await simpleSDK.delta.approveTokenForDelta(amount, Token1); + + const slippagePercent = 0.5; + const destAmountAfterSlippage = BigInt( + // get rid of exponential notation + + +(+deltaPrice.destAmount * (1 - slippagePercent / 100)).toFixed(0) + // get rid of decimals + ).toString(10); + + const deltaAuction = await simpleSDK.delta.submitDeltaOrder({ + deltaPrice, + owner: account, + // beneficiary: anotherAccount, // if need to send destToken to another account + // permit: "0x1234...", // if signed a Permit1 or Permit2 TransferFrom for DeltaContract + srcToken: Token1, + destToken: Token2, + srcAmount: amount, + destAmount: destAmountAfterSlippage, // minimum acceptable destAmount + }); + + // poll if necessary + const auction = await simpleSDK.delta.getDeltaOrderById(deltaAuction.id); + if (auction?.status === 'EXECUTED') { + console.log('Auction was executed'); + } +} else { + console.log( + `Delta Quote failed: ${quote.fallbackReason.errorType} - ${quote.fallbackReason.details}` + ); + const priceRoute = quote.market; + + const TokenTransferProxy = await simpleSDK.swap.getSpender(); + + // or sign a Permit1 or Permit2 TransferFrom for TokenTransferProxy + const approveTxHash = simpleSDK.swap.approveToken(amount, Token1); + + const txParams = await simpleSDK.swap.buildTx({ + srcToken: Token1, + destToken: Token2, + srcAmount: amount, + slippage: 250, // 2.5% + priceRoute, + userAddress: account, + // partner: '...' // if available + }); + + const swapTx = await signer.sendTransaction(txParams); +} +``` + +#### For Delta protocol usage refer to [DELTA.md](./DELTA.md) + ### Legacy The `ParaSwap` class is exposed for backwards compatibility with previous versions of the SDK. @@ -205,4 +311,4 @@ Refer to [SDK API documentation](docs/md/modules.md) for detailed documentation To run `yarn test` it is necessary to provide `PROVIDER_URL=` environment variable. If it is necessary to run tests against a different API endpoint, provide `API_URL=url_to_API` environment variable. - \ No newline at end of file + \ No newline at end of file diff --git a/docs/passed_tests.png b/docs/passed_tests.png new file mode 100644 index 0000000000000000000000000000000000000000..cdf08a6eb8e65044828b810a4e7924caf83ab436 GIT binary patch literal 80339 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 DN>PH3 literal 0 HcmV?d00001 diff --git a/hardhat.config.ts b/hardhat.config.ts index fd36f038..d9ac1b8e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -19,7 +19,7 @@ const config: HardhatUserConfig = { chainId: 1, // have to config hardhat with fixed accounts unlocked, otherwise signTx and signTyped data fail when used in RPC calls to Node // which breaks web3.js - // ethers signs data and txs locally off-chain as long as it has provate key + // ethers signs data and txs locally off-chain as long as it has private key // web3 is a bit harder to init wallets for locally. // impersonateAccounts doesn't work, even though it should logically fully unlock accounts, // but only remore accounts are unlocked, fixed are not, diff --git a/src/examples/delta.ts b/src/examples/delta.ts new file mode 100644 index 00000000..5a10b733 --- /dev/null +++ b/src/examples/delta.ts @@ -0,0 +1,127 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import axios from 'axios'; +import { ethers, Wallet } from 'ethersV5'; +import { + constructPartialSDK, + constructEthersContractCaller, + constructAxiosFetcher, + constructAllDeltaOrdersHandlers, +} from '..'; + +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, +}); + +// type AdaptersFunctions & ApproveTokenFunctions +const deltaSDK = constructPartialSDK( + { + chainId: 1, + fetcher, + contractCaller, + }, + constructAllDeltaOrdersHandlers +); + +const DAI_TOKEN = '0x6b175474e89094c44da98b954eedeac495271d0f'; +const PSP_TOKEN = '0xcafe001067cdef266afb7eb5a286dcfd277f3de5'; + +async function simpleDeltaFlow() { + const amount = '1000000000000'; // wei + + const deltaPrice = await deltaSDK.getDeltaPrice({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + amount, + userAddress: account, + srcDecimals: 18, + destDecimals: 18, + // partner: "..." // if available + }); + + const DeltaContract = await deltaSDK.getDeltaContract(); + + // or sign a Permit1 or Permit2 TransferFrom for DeltaContract + const tx = await deltaSDK.approveTokenForDelta(amount, DAI_TOKEN); + await tx.wait(); + + const slippagePercent = 0.5; + const destAmountAfterSlippage = BigInt( + // get rid of exponential notation + + +(+deltaPrice.destAmount * (1 - slippagePercent / 100)).toFixed(0) + // get rid of decimals + ).toString(10); + + const deltaAuction = await deltaSDK.submitDeltaOrder({ + deltaPrice, + owner: account, + // beneficiary: anotherAccount, // if need to send destToken to another account + // permit: "0x1234...", // if signed a Permit1 or Permit2 TransferFrom for DeltaContract + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + srcAmount: amount, + destAmount: destAmountAfterSlippage, // minimum acceptable destAmount + }); + + // poll if necessary + const auction = await deltaSDK.getDeltaOrderById(deltaAuction.id); + if (auction?.status === 'EXECUTED') { + console.log('Auction was executed'); + } +} +async function manualDeltaFlow() { + const amount = '1000000000000'; // wei + + const deltaPrice = await deltaSDK.getDeltaPrice({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + amount, + userAddress: account, + srcDecimals: 18, + destDecimals: 18, + // partner: "..." // if available + }); + + const DeltaContract = await deltaSDK.getDeltaContract(); + + // or sign a Permit1 or Permit2 TransferFrom for DeltaContract + const tx = await deltaSDK.approveTokenForDelta(amount, DAI_TOKEN); + await tx.wait(); + + const slippagePercent = 0.5; + const destAmountAfterSlippage = ( + +deltaPrice.destAmount * + (1 - slippagePercent / 100) + ).toString(10); + + const signableOrderData = await deltaSDK.buildDeltaOrder({ + deltaPrice, + owner: account, + // beneficiary: anotherAccount, // if need to send destToken to another account + // permit: "0x1234...", // if signed a Permit1 or Permit2 TransferFrom for DeltaContract + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + srcAmount: amount, + destAmount: destAmountAfterSlippage, // minimum acceptable destAmount + }); + + const signature = await deltaSDK.signDeltaOrder(signableOrderData); + + const deltaAuction = await deltaSDK.postDeltaOrder({ + // partner: "..." // if available + order: signableOrderData.data, + signature, + }); + + // poll if necessary + const auction = await deltaSDK.getDeltaOrderById(deltaAuction.id); + if (auction?.status === 'EXECUTED') { + console.log('Auction was executed'); + } +} diff --git a/src/examples/quote.ts b/src/examples/quote.ts new file mode 100644 index 00000000..2976841c --- /dev/null +++ b/src/examples/quote.ts @@ -0,0 +1,205 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import axios from 'axios'; +import { ethers, Wallet } from 'ethersV5'; +import { + constructPartialSDK, + constructEthersContractCaller, + constructAxiosFetcher, + constructAllDeltaOrdersHandlers, + constructGetQuote, + constructSwapSDK, + OptimalRate, + DeltaPrice, + isFetcherError, +} from '..'; + +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, +}); + +// type AdaptersFunctions & ApproveTokenFunctions +const quoteSDK = constructPartialSDK( + { + chainId: 1, + fetcher, + contractCaller, + }, + constructAllDeltaOrdersHandlers, + constructSwapSDK, + constructGetQuote +); + +const DAI_TOKEN = '0x6b175474e89094c44da98b954eedeac495271d0f'; +const PSP_TOKEN = '0xcafe001067cdef266afb7eb5a286dcfd277f3de5'; + +/** + * mode='delta' example + */ +async function deltaQuote() { + const amount = '1000000000000'; // wei + + const quote = await quoteSDK.getQuote({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + amount, + userAddress: account, + srcDecimals: 18, + destDecimals: 18, + mode: 'delta', + side: 'SELL', + // partner: "..." // if available + }); + + try { + const deltaPrice = quote.delta; + await handleDeltaQuote({ amount, deltaPrice }); + } catch (error) { + if (isFetcherError(error)) { + const data = error.response?.data; + console.log(`Delta Quote failed: ${data.errorType} - ${data.details}`); + } + } +} + +/** + * mode='market' example + */ +async function marketQuote() { + const amount = '1000000000000'; // wei + + const quote = await quoteSDK.getQuote({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + amount, + userAddress: account, + srcDecimals: 18, + destDecimals: 18, + mode: 'market', + side: 'SELL', + // partner: "..." // if available + }); + + const TokenTransferProxy = await quoteSDK.getSpender(); + + // or sign a Permit1 or Permit2 TransferFrom for TokenTransferProxy + const approveTxHash = quoteSDK.approveToken(amount, DAI_TOKEN); + + const txParams = await quoteSDK.buildTx({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + srcAmount: amount, + slippage: 250, // 2.5% + priceRoute: quote.market, + userAddress: account, + // partner: '...' // if available + }); + + const swapTx = await handleMarketQuote({ amount, priceRoute: quote.market }); +} + +/** + * mode='all' example + */ +async function allQuote() { + const amount = '1000000000000'; // wei + + const quote = await quoteSDK.getQuote({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + amount, + userAddress: account, + srcDecimals: 18, + destDecimals: 18, + mode: 'all', + side: 'SELL', + // partner: "..." // if available + }); + + if ('delta' in quote) { + const deltaPrice = quote.delta; + await handleDeltaQuote({ amount, deltaPrice }); + } else { + console.log( + `Delta Quote failed: ${quote.fallbackReason.errorType} - ${quote.fallbackReason.details}` + ); + const swapTx = await handleMarketQuote({ + amount, + priceRoute: quote.market, + }); + } +} + +async function handleDeltaQuote({ + amount, + deltaPrice, +}: { + amount: string; + deltaPrice: DeltaPrice; +}) { + /** + * refer to examples/delta for more details + */ + const DeltaContract = await quoteSDK.getDeltaContract(); + + // or sign a Permit1 or Permit2 TransferFrom for DeltaContract + await quoteSDK.approveTokenForDelta(amount, DAI_TOKEN); + + const slippagePercent = 0.5; + const destAmountAfterSlippage = BigInt( + // get rid of exponential notation + + +(+deltaPrice.destAmount * (1 - slippagePercent / 100)).toFixed(0) + // get rid of decimals + ).toString(10); + + const deltaAuction = await quoteSDK.submitDeltaOrder({ + deltaPrice, + owner: account, + // beneficiary: anotherAccount, // if need to send destToken to another account + // permit: "0x1234...", // if signed a Permit1 or Permit2 TransferFrom for DeltaContract + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + srcAmount: amount, + destAmount: destAmountAfterSlippage, // minimum acceptable destAmount + }); + + // poll if necessary + const auction = await quoteSDK.getDeltaOrderById(deltaAuction.id); + if (auction?.status === 'EXECUTED') { + console.log('Auction was executed'); + } + + return auction; +} + +async function handleMarketQuote({ + amount, + priceRoute, +}: { + amount: string; + priceRoute: OptimalRate; +}) { + const TokenTransferProxy = await quoteSDK.getSpender(); + + // or sign a Permit1 or Permit2 TransferFrom for TokenTransferProxy + const approveTxHash = quoteSDK.approveToken(amount, DAI_TOKEN); + + const txParams = await quoteSDK.buildTx({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + srcAmount: amount, + slippage: 250, // 2.5% + priceRoute, + userAddress: account, + // partner: '...' // if available + }); + + const swapTx = await signer.sendTransaction(txParams); + return swapTx; +} diff --git a/src/examples/simple.ts b/src/examples/simple.ts index e1457459..4aab765d 100644 --- a/src/examples/simple.ts +++ b/src/examples/simple.ts @@ -40,5 +40,5 @@ const SDKwithApprove = constructSimpleSDK( const approveTxHash = SDKwithApprove.swap.approveToken( '1000000000000', - PSP_TOKEN + DAI_TOKEN ); diff --git a/src/examples/simpleQuote.ts b/src/examples/simpleQuote.ts new file mode 100644 index 00000000..d0716eb2 --- /dev/null +++ b/src/examples/simpleQuote.ts @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import axios from 'axios'; +import { ethers } from 'ethersV5'; +import { constructSimpleSDK } from '..'; + +const DAI_TOKEN = '0x6b175474e89094c44da98b954eedeac495271d0f'; +const PSP_TOKEN = '0xcafe001067cdef266afb7eb5a286dcfd277f3de5'; + +async function allQuote() { + // @ts-expect-error assume window.ethereum is available + const ethersProvider = new ethers.providers.Web3Provider(window.ethereum); + + const accounts = await ethersProvider.listAccounts(); + const account = accounts[0]!; + const signer = ethersProvider.getSigner(account); + + const simpleSDK = constructSimpleSDK( + { chainId: 1, axios }, + { + ethersProviderOrSigner: signer, + EthersContract: ethers.Contract, + account, + } + ); + + const amount = '1000000000000'; // wei + + const quote = await simpleSDK.quote.getQuote({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + amount, + userAddress: account, + srcDecimals: 18, + destDecimals: 18, + mode: 'all', // Delta quote if possible, with fallback to Market price + side: 'SELL', + // partner: "..." // if available + }); + + if ('delta' in quote) { + const deltaPrice = quote.delta; + + const DeltaContract = await simpleSDK.delta.getDeltaContract(); + + // or sign a Permit1 or Permit2 TransferFrom for DeltaContract + await simpleSDK.delta.approveTokenForDelta(amount, DAI_TOKEN); + + const slippagePercent = 0.5; + const destAmountAfterSlippage = BigInt( + // get rid of exponential notation + + +(+deltaPrice.destAmount * (1 - slippagePercent / 100)).toFixed(0) + // get rid of decimals + ).toString(10); + + const deltaAuction = await simpleSDK.delta.submitDeltaOrder({ + deltaPrice, + owner: account, + // beneficiary: anotherAccount, // if need to send destToken to another account + // permit: "0x1234...", // if signed a Permit1 or Permit2 TransferFrom for DeltaContract + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + srcAmount: amount, + destAmount: destAmountAfterSlippage, // minimum acceptable destAmount + }); + + // poll if necessary + const auction = await simpleSDK.delta.getDeltaOrderById(deltaAuction.id); + if (auction?.status === 'EXECUTED') { + console.log('Auction was executed'); + } + } else { + console.log( + `Delta Quote failed: ${quote.fallbackReason.errorType} - ${quote.fallbackReason.details}` + ); + const priceRoute = quote.market; + + const TokenTransferProxy = await simpleSDK.swap.getSpender(); + + // or sign a Permit1 or Permit2 TransferFrom for TokenTransferProxy + const approveTxHash = simpleSDK.swap.approveToken(amount, DAI_TOKEN); + + const txParams = await simpleSDK.swap.buildTx({ + srcToken: DAI_TOKEN, + destToken: PSP_TOKEN, + srcAmount: amount, + slippage: 250, // 2.5% + priceRoute, + userAddress: account, + // partner: '...' // if available + }); + + const swapTx = await signer.sendTransaction(txParams); + } +} diff --git a/src/helpers/fetchers/fetch.ts b/src/helpers/fetchers/fetch.ts index 8be34ed4..afe54a54 100644 --- a/src/helpers/fetchers/fetch.ts +++ b/src/helpers/fetchers/fetch.ts @@ -20,7 +20,7 @@ export const constructFetcher = // adding apiKey to headers if it's provided const apiHeaders = extra?.apiKey - ? { 'X-API-KEY': extra.apiKey, ...params.headers } + ? { 'X-API-KEY': extra.apiKey } : undefined; // all headers combined diff --git a/src/helpers/misc.ts b/src/helpers/misc.ts index 94020308..5add9f00 100644 --- a/src/helpers/misc.ts +++ b/src/helpers/misc.ts @@ -158,3 +158,57 @@ export function runOnceAndCache( return result ?? (result = func(...args)); }; } + +export function deriveCompactSignature(signature: string): string { + // Remove "0x" prefix if present + if (signature.startsWith('0x')) { + signature = signature.slice(2); + } + + // Convert the hex string to a byte array + const bytes = new Uint8Array(signature.length / 2); + for (let i = 0; i < signature.length; i += 2) { + bytes[i / 2] = parseInt(signature.slice(i, i + 2), 16); + } + + // Validate the signature length (64 or 65 bytes) + if (bytes.length !== 64 && bytes.length !== 65) { + throw new Error('Invalid signature length: must be 64 or 65 bytes'); + } + + // Extract r and s components + const r = `0x${Array.from(bytes.slice(0, 32), (b) => + b.toString(16).padStart(2, '0') + ).join('')}`; + let v; + + // Handle 64-byte (EIP-2098 compact) and 65-byte signatures + if (bytes.length === 64) { + // Extract v from the highest bit of s and clear the bit in s + v = 27 + (bytes[32]! >> 7); + bytes[32]! &= 0x7f; // Clear the highest bit + } else { + // Extract v directly for 65-byte signature + v = bytes[64]!; + + // Normalize v to canonical form (27 or 28) + if (v < 27) { + v += 27; + } + } + + // Compute yParityAndS (_vs) for the compact signature + const sBytes = Array.from(bytes.slice(32, 64)); + if (v === 28) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sBytes[0]! |= 0x80; // Set the highest bit if v is 28 + } + const yParityAndS = `0x${sBytes + .map((b) => b.toString(16).padStart(2, '0')) + .join('')}`; + + // Construct the compact signature by concatenating r and yParityAndS + const compactSignature = r + yParityAndS.slice(2); + + return compactSignature; +} diff --git a/src/helpers/providers/ethersV6.ts b/src/helpers/providers/ethersV6.ts index 50be41b2..e1a5db2b 100644 --- a/src/helpers/providers/ethersV6.ts +++ b/src/helpers/providers/ethersV6.ts @@ -43,8 +43,8 @@ export const constructContractCaller = ( const callableContractFunction = contract.getFunction(contractMethod); - // returns whatever the Contract.method returns: BigNumber, string, boolean - return callableContractFunction(...args, normalizedOverrides); + // returns whatever the Contract["method"].staticCall returns: BigNumber, string, boolean + return callableContractFunction.staticCall(...args, normalizedOverrides); }; const transactCall: TransactionContractCallerFn< @@ -77,7 +77,7 @@ export const constructContractCaller = ( // if no method for contractMethod, ethers will throw const callableContractFunction = contract.getFunction(contractMethod); - const txResponse = await callableContractFunction( + const txResponse = await callableContractFunction.send( ...args, normalizedOverrides ); diff --git a/src/index.ts b/src/index.ts index 54302b75..aa60c730 100644 --- a/src/index.ts +++ b/src/index.ts @@ -131,6 +131,58 @@ import type { ParaSwapVersionUnion as ParaSwapVersion, } from './types'; +import type { + DeltaAuctionOrder, + ParaswapDeltaAuction, +} from './methods/delta/helpers/types'; +import { + BuildDeltaOrderDataParams, + BuildDeltaOrderFunctions, + constructBuildDeltaOrder, + SignableDeltaOrderData, +} from './methods/delta/buildDeltaOrder'; +import { + constructPostDeltaOrder, + PostDeltaOrderFunctions, + PostDeltaOrderParams, +} from './methods/delta/postDeltaOrder'; +import { + constructSignDeltaOrder, + SignDeltaOrderFunctions, +} from './methods/delta/signDeltaOrder'; +import { + GetDeltaContractFunctions, + constructGetDeltaContract, +} from './methods/delta/getDeltaContract'; +import { + constructGetDeltaPrice, + GetDeltaPriceFunctions, + DeltaPrice, + DeltaPriceParams, +} from './methods/delta/getDeltaPrice'; +import { + constructGetDeltaOrders, + GetDeltaOrdersFunctions, +} from './methods/delta/getDeltaOrders'; +import { + ApproveTokenForDeltaFunctions, + constructApproveTokenForDelta, +} from './methods/delta/approveForDelta'; +import { + constructGetPartnerFee, + GetPartnerFeeFunctions, +} from './methods/delta/getPartnerFee'; + +import { + constructGetQuote, + GetQuoteFunctions, + QuoteParams, + QuoteResponse, + QuoteWithDeltaPrice, + QuoteWithMarketPrice, + QuoteWithMarketPriceAsFallback, +} from './methods/quote/getQuote'; + export { constructSwapSDK, SwapSDKMethods } from './methods/swap'; export { @@ -139,6 +191,13 @@ export { LimitOrderHandlers, } from './methods/limitOrders'; +export { + constructAllDeltaOrdersHandlers, + constructSubmitDeltaOrder, + DeltaOrderHandlers, + SubmitDeltaOrderParams, +} from './methods/delta'; + export type { TransactionParams, BuildOptions, @@ -198,7 +257,18 @@ export { constructApproveTokenForNFTOrder, constructGetNFTOrdersContract, constructBuildNFTOrderTx, + // Delta methods + constructBuildDeltaOrder, + constructPostDeltaOrder, + constructSignDeltaOrder, + constructGetDeltaContract, + constructGetDeltaPrice, + constructGetDeltaOrders, + constructApproveTokenForDelta, + // Quote methods + constructGetQuote, // different helpers + constructGetPartnerFee, constructEthersContractCaller, // same as constructEthersV5ContractCaller for backwards compatibility constructEthersV5ContractCaller, constructEthersV6ContractCaller, @@ -256,11 +326,34 @@ export type { BuildNFTOrderInput, BuildNFTOrderDataInput, NFTOrdersUserParams, + //types for Delta methods + DeltaPrice, + DeltaPriceParams, + DeltaAuctionOrder, + ParaswapDeltaAuction, + BuildDeltaOrderDataParams, + BuildDeltaOrderFunctions, + SignableDeltaOrderData, + PostDeltaOrderFunctions, + PostDeltaOrderParams, + SignDeltaOrderFunctions, + GetDeltaContractFunctions, + GetDeltaPriceFunctions, + GetDeltaOrdersFunctions, + ApproveTokenForDeltaFunctions, + // types for Quote methods + GetQuoteFunctions, + QuoteParams, + QuoteResponse, + QuoteWithDeltaPrice, + QuoteWithMarketPrice, + QuoteWithMarketPriceAsFallback, //common ConstructFetchInput, ContractCallerFunctions, ConstructProviderFetchInput, // other types + GetPartnerFeeFunctions, Token, Address, AddressOrSymbol, diff --git a/src/methods/delta/approveForDelta.ts b/src/methods/delta/approveForDelta.ts new file mode 100644 index 00000000..49953994 --- /dev/null +++ b/src/methods/delta/approveForDelta.ts @@ -0,0 +1,35 @@ +import type { ConstructProviderFetchInput } from '../../types'; +import { ApproveToken, approveTokenMethodFactory } from '../../helpers/approve'; +import { constructGetDeltaContract } from './getDeltaContract'; + +export type ApproveTokenForDeltaFunctions = { + /** @description approving ParaswapDelta as spender for Token */ + approveTokenForDelta: ApproveToken; +}; + +// returns whatever `contractCaller` returns +// to allow for better versatility +export const constructApproveTokenForDelta = ( + options: ConstructProviderFetchInput +): ApproveTokenForDeltaFunctions => { + // getAugustusRFQ is cached internally for the same instance of SDK + // so should persist across same apiUrl & network + const { getDeltaContract } = constructGetDeltaContract(options); + + const getParaswapDelta = async () => { + const deltaContract = await getDeltaContract(); + if (!deltaContract) { + throw new Error(`Delta is not available on chain ${options.chainId}`); + } + return deltaContract; + }; + + const approveTokenForDelta: ApproveToken = approveTokenMethodFactory( + options.contractCaller, + getParaswapDelta + ); + + return { + approveTokenForDelta, + }; +}; diff --git a/src/methods/delta/buildDeltaOrder.ts b/src/methods/delta/buildDeltaOrder.ts new file mode 100644 index 00000000..2ae781e6 --- /dev/null +++ b/src/methods/delta/buildDeltaOrder.ts @@ -0,0 +1,110 @@ +import type { ConstructFetchInput } from '../../types'; +import { constructGetDeltaContract } from './getDeltaContract'; +import { DeltaPrice } from './getDeltaPrice'; +import { + constructGetPartnerFee, + type PartnerFeeResponse, +} from './getPartnerFee'; +import { + buildDeltaSignableOrderData, + type BuildDeltaOrderDataInput, + type SignableDeltaOrderData, +} from './helpers/buildDeltaOrderData'; +export type { SignableDeltaOrderData } from './helpers/buildDeltaOrderData'; + +export type BuildDeltaOrderDataParams = { + /** @description The address of the order owner */ + owner: string; + /** @description The address of the order beneficiary */ + beneficiary?: string; // beneficiary==owner if no transferTo + /** @description The address of the src token */ + srcToken: string; // lowercase + /** @description The address of the dest token */ + destToken: string; // lowercase + /** @description The amount of src token to swap */ + srcAmount: string; // wei + /** @description The minimum amount of dest token to receive */ + destAmount: string; // wei, deltaPrice.destAmount - slippage + /** @description The deadline for the order */ + deadline?: number; // seconds + /** @description The nonce of the order */ + nonce?: number | string; // can be random, can even be Date.now() + /** @description Optional permit signature for the src token https://developers.paraswap.network/api/paraswap-delta/build-and-sign-a-delta-order#supported-permits */ + permit?: string; //can be "0x" + /** @description Partner string. */ + partner?: string; + + /** @description price response received from /delta/prices (getDeltaPrice method) */ + deltaPrice: Pick; +} & Partial; // can override partnerFee, partnerAddress, takeSurplus, which otherwise will be fetched + +type BuildDeltaOrder = ( + buildOrderParams: BuildDeltaOrderDataParams, + signal?: AbortSignal +) => Promise; + +export type BuildDeltaOrderFunctions = { + /** @description Build Orders to be posted to Delta API for execution */ + buildDeltaOrder: BuildDeltaOrder; +}; + +export const constructBuildDeltaOrder = ( + options: ConstructFetchInput +): BuildDeltaOrderFunctions => { + const { chainId } = options; + + // cached internally + const { getDeltaContract } = constructGetDeltaContract(options); + // cached internally for `partner` + const { getPartnerFee } = constructGetPartnerFee(options); + + const buildDeltaOrder: BuildDeltaOrder = async (options, signal) => { + const ParaswapDelta = await getDeltaContract(signal); + if (!ParaswapDelta) { + throw new Error(`Delta is not available on chain ${chainId}`); + } + + let partnerAddress = options.partnerAddress; + let partnerFee = options.partnerFee ?? options.deltaPrice.partnerFee; + let takeSurplus = options.takeSurplus; + + if ( + partnerAddress === undefined || + partnerFee === undefined || + takeSurplus === undefined + ) { + const partner = options.partner || options.deltaPrice.partner; + const partnerFeeResponse = await getPartnerFee({ partner }, signal); + + partnerAddress = partnerAddress ?? partnerFeeResponse.partnerAddress; + // deltaPrice.partnerFee and partnerFeeResponse.partnerFee should be the same, but give priority to externally provided + partnerFee = partnerFee ?? partnerFeeResponse.partnerFee; + takeSurplus = takeSurplus ?? partnerFeeResponse.takeSurplus; + } + + const input: BuildDeltaOrderDataInput = { + owner: options.owner, + beneficiary: options.beneficiary, + srcToken: options.srcToken, + destToken: options.destToken, + srcAmount: options.srcAmount, + destAmount: options.destAmount, + expectedDestAmount: options.deltaPrice.destAmount, + deadline: options.deadline, + nonce: options.nonce?.toString(10), + permit: options.permit, + + chainId, + paraswapDeltaAddress: ParaswapDelta, + partnerAddress, + takeSurplus, + partnerFee, + }; + + return buildDeltaSignableOrderData(input); + }; + + return { + buildDeltaOrder, + }; +}; diff --git a/src/methods/delta/getDeltaContract.ts b/src/methods/delta/getDeltaContract.ts new file mode 100644 index 00000000..e7326bb4 --- /dev/null +++ b/src/methods/delta/getDeltaContract.ts @@ -0,0 +1,23 @@ +import type { Address, ConstructFetchInput } from '../../types'; +import { constructGetSpender } from '../swap/spender'; + +type GetDeltaContract = (signal?: AbortSignal) => Promise

; +export type GetDeltaContractFunctions = { + /** @description returns ParaswapDelta contract address when Delta is available on current chain */ + getDeltaContract: GetDeltaContract; +}; + +export const constructGetDeltaContract = ( + options: ConstructFetchInput +): GetDeltaContractFunctions => { + // analogous to getSpender() but for Delta Orders Contract = ParaswapDelta + + const { getContracts } = constructGetSpender(options); + + const getDeltaContract: GetDeltaContract = async (signal) => { + const { ParaswapDelta } = await getContracts(signal); + return ParaswapDelta || null; + }; + + return { getDeltaContract }; +}; diff --git a/src/methods/delta/getDeltaOrders.ts b/src/methods/delta/getDeltaOrders.ts new file mode 100644 index 00000000..d0804f29 --- /dev/null +++ b/src/methods/delta/getDeltaOrders.ts @@ -0,0 +1,73 @@ +import { API_URL } from '../../constants'; +import { constructSearchString } from '../../helpers/misc'; +import type { Address, ConstructFetchInput } from '../../types'; +import type { ParaswapDeltaAuction } from './helpers/types'; + +type OrderFromAPI = Omit; + +type GetDeltaOrderById = ( + orderId: string, + signal?: AbortSignal +) => Promise; + +type OrdersFilter = { + /** @description Order.owner to fetch Delta Order for */ + userAddress: Address; + /** @description Pagination option, page. Default 1 */ + page?: number; + /** @description Pagination option, limit. Default 100 */ + limit?: number; +}; +type OrderFiltersQuery = OrdersFilter; + +type GetDeltaOrders = ( + options: OrdersFilter, + signal?: AbortSignal +) => Promise; + +export type GetDeltaOrdersFunctions = { + getDeltaOrderById: GetDeltaOrderById; + getDeltaOrders: GetDeltaOrders; +}; + +export const constructGetDeltaOrders = ({ + apiURL = API_URL, + fetcher, +}: ConstructFetchInput): GetDeltaOrdersFunctions => { + const baseUrl = `${apiURL}/delta/orders` as const; + + const getDeltaOrderById: GetDeltaOrderById = async (orderId, signal) => { + const fetchURL = `${baseUrl}/${orderId}` as const; + + const order = await fetcher({ + url: fetchURL, + method: 'GET', + signal, + }); + + return order; + }; + + const getDeltaOrders: GetDeltaOrders = async (options, signal) => { + const search = constructSearchString({ + userAddress: options.userAddress, + page: options.page, + limit: options.limit, + }); + + const fetchURL = `${baseUrl}${search}` as const; + + const orders = await fetcher({ + url: fetchURL, + method: 'GET', + signal, + }); + + return orders; + }; + + return { + getDeltaOrderById, + getDeltaOrders, + }; +}; diff --git a/src/methods/delta/getDeltaPrice.ts b/src/methods/delta/getDeltaPrice.ts new file mode 100644 index 00000000..d351433d --- /dev/null +++ b/src/methods/delta/getDeltaPrice.ts @@ -0,0 +1,86 @@ +import { API_URL, SwapSide } from '../../constants'; +import { constructSearchString } from '../../helpers/misc'; +import type { ConstructFetchInput } from '../../types'; + +export type DeltaPriceParams = { + /** @description Source Token Address. Not Native Token */ + srcToken: string; + /** @description Destination Token Address */ + destToken: string; + /** @description srcToken amount in wei */ + amount: string; + /** @description Source Token Decimals */ + srcDecimals: number; + /** @description Destination Token Decimals */ + destDecimals: number; + // side?: SwapSide; // no BUY side for now + /** @description User's Wallet Address */ + userAddress?: string; + /** @description Partner string. */ + partner?: string; +}; + +type DeltaPriceQueryOptions = DeltaPriceParams & { + chainId: number; // will return error from API on unsupported chains + side: SwapSide.SELL; +}; + +export type DeltaPrice = { + srcToken: string; + destToken: string; + srcAmount: string; + destAmount: string; + destAmountBeforeFee: string; + gasCost: string; + gasCostBeforeFee: string; + gasCostUSD: string; + gasCostUSDBeforeFee: string; + srcUSD: string; + destUSD: string; + destUSDBeforeFee: string; + partner: string; + partnerFee: number; +}; + +type DeltaPriceResponse = { + price: DeltaPrice; +}; + +type GetDeltaPrice = ( + options: DeltaPriceParams, + signal?: AbortSignal +) => Promise; + +export type GetDeltaPriceFunctions = { + getDeltaPrice: GetDeltaPrice; +}; + +export const constructGetDeltaPrice = ({ + apiURL = API_URL, + chainId, + fetcher, +}: ConstructFetchInput): GetDeltaPriceFunctions => { + const pricesUrl = `${apiURL}/delta/prices` as const; + + const getDeltaPrice: GetDeltaPrice = async (options, signal) => { + const search = constructSearchString({ + ...options, + chainId, + side: SwapSide.SELL, // so far SELL side only + }); + + const fetchURL = `${pricesUrl}/${search}` as const; + + const data = await fetcher({ + url: fetchURL, + method: 'GET', + signal, + }); + + return data.price; + }; + + return { + getDeltaPrice, + }; +}; diff --git a/src/methods/delta/getPartnerFee.ts b/src/methods/delta/getPartnerFee.ts new file mode 100644 index 00000000..dc2b8499 --- /dev/null +++ b/src/methods/delta/getPartnerFee.ts @@ -0,0 +1,58 @@ +import { API_URL } from '../../constants'; +import { constructSearchString } from '../../helpers/misc'; +import type { ConstructFetchInput } from '../../types'; + +export type PartnerFeeResponse = { + partnerFee: number; // in %, e.g. 0.12 + partnerAddress: string; + takeSurplus: boolean; +}; + +type PartnerFeeQueryParams = { + partner: string; +}; + +type GetPartnerFee = ( + options: PartnerFeeQueryParams, + signal?: AbortSignal +) => Promise; + +export type GetPartnerFeeFunctions = { + getPartnerFee: GetPartnerFee; +}; + +export const constructGetPartnerFee = ({ + apiURL = API_URL, + chainId, + fetcher, +}: ConstructFetchInput): GetPartnerFeeFunctions => { + const partnerFeeUrl = `${apiURL}/prices/partnerfee/${chainId}` as const; + + // going on the assumption that one `partner` will correspond to one `partnerFee` during the lifetime of SDK instance, + // to avoid unnecessary network requests + const cachedPartnerFee = new Map(); + + const getPartnerFee: GetPartnerFee = async (options, signal) => { + if (cachedPartnerFee.has(options.partner)) { + return cachedPartnerFee.get(options.partner)!; + } + + const search = constructSearchString(options); + + const fetchURL = `${partnerFeeUrl}/${search}` as const; + + const data = await fetcher({ + url: fetchURL, + method: 'GET', + signal, + }); + + cachedPartnerFee.set(options.partner, data); + + return data; + }; + + return { + getPartnerFee, + }; +}; diff --git a/src/methods/delta/helpers/buildDeltaOrderData.ts b/src/methods/delta/helpers/buildDeltaOrderData.ts new file mode 100644 index 00000000..0b27f778 --- /dev/null +++ b/src/methods/delta/helpers/buildDeltaOrderData.ts @@ -0,0 +1,139 @@ +import { MarkOptional } from 'ts-essentials'; +import { Domain, ZERO_ADDRESS } from '../../common/orders/buildOrderData'; +import { DeltaAuctionOrder } from './types'; +import { composeDeltaOrderPermit } from './composePermit'; +import { DeltaPrice } from '../getDeltaPrice'; + +// Order(address owner,address beneficiary,address srcToken,address destToken,uint256 srcAmount,uint256 destAmount,uint256 deadline,uint256 nonce,bytes permit)"; +const Order = [ + { name: 'owner', type: 'address' }, + { name: 'beneficiary', type: 'address' }, + { name: 'srcToken', type: 'address' }, + { name: 'destToken', type: 'address' }, + { name: 'srcAmount', type: 'uint256' }, + { name: 'destAmount', type: 'uint256' }, + { name: 'expectedDestAmount', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'partnerAndFee', type: 'uint256' }, + { name: 'permit', type: 'bytes' }, +]; + +export type SignableDeltaOrderData = { + types: { + Order: typeof Order; + }; + domain: Domain; + data: DeltaAuctionOrder; +}; + +type SignDeltaOrderInput = { + orderInput: DeltaAuctionOrder; + paraswapDeltaAddress: string; + chainId: number; +}; + +function produceDeltaOrderTypedData({ + orderInput, + chainId, + paraswapDeltaAddress, +}: SignDeltaOrderInput): SignableDeltaOrderData { + const typedData = { + types: { Order }, + domain: { + name: 'Portikus', + version: '2.0.0', + chainId, + verifyingContract: paraswapDeltaAddress, + }, + data: orderInput, + }; + + return typedData; +} + +export type DeltaOrderDataInput = MarkOptional< + Omit, + 'beneficiary' | 'deadline' | 'nonce' | 'permit' +> & + Pick; + +export type BuildDeltaOrderDataInput = DeltaOrderDataInput & { + partnerAddress: string; + paraswapDeltaAddress: string; + takeSurplus: boolean; + chainId: number; +}; + +// default deadline = 1 hour from now (may be changed later) +export const DELTA_DEFAULT_EXPIRY = 60 * 60; // seconds + +export function buildDeltaSignableOrderData({ + owner, + beneficiary = owner, + + srcToken, + destToken, + srcAmount, + destAmount, + expectedDestAmount, + + deadline = Math.floor(Date.now() / 1000 + DELTA_DEFAULT_EXPIRY), + nonce = Date.now().toString(10), // random enough to not cause collisions + + permit = '0x', + + partnerAddress, + partnerFee, + takeSurplus, + + chainId, + paraswapDeltaAddress, +}: BuildDeltaOrderDataInput): SignableDeltaOrderData { + const orderInput: DeltaAuctionOrder = { + owner, + beneficiary, + srcToken, + destToken, + srcAmount, + destAmount, + expectedDestAmount, + deadline, + nonce, + permit: composeDeltaOrderPermit({ permit, nonce }), + partnerAndFee: producePartnerAndFee({ + partnerFee, + partnerAddress, + takeSurplus, + }), + }; + + return produceDeltaOrderTypedData({ + orderInput, + chainId, + paraswapDeltaAddress, + }); +} + +type ProducePartnerAndFeeInput = { + partnerFee: number; + partnerAddress: string; + takeSurplus: boolean; +}; + +// fee and address are encoded together +function producePartnerAndFee({ + partnerFee, + partnerAddress, + takeSurplus, +}: ProducePartnerAndFeeInput): string { + if (partnerAddress === ZERO_ADDRESS) return '0'; + + const partnerFeeBps = BigInt((partnerFee * 100).toFixed(0)); + const partnerAndFee = + (BigInt(partnerAddress) << BigInt(96)) | + partnerFeeBps | + (BigInt(takeSurplus) << BigInt(8)); + + return partnerAndFee.toString(10); +} diff --git a/src/methods/delta/helpers/composePermit.ts b/src/methods/delta/helpers/composePermit.ts new file mode 100644 index 00000000..f6414531 --- /dev/null +++ b/src/methods/delta/helpers/composePermit.ts @@ -0,0 +1,76 @@ +import { DeltaAuctionOrder } from './types'; + +type DeltaOrderPermitInput = Pick; + +export function composeDeltaOrderPermit({ + permit, + nonce, +}: DeltaOrderPermitInput): string { + // Can be empty Permit if allowance is available for srcToken + if (permit === '0x' || permit === '0x01') { + // 0x01 is a special permit value that signifies existing Permit2 allowance. + return permit; + } + + // In the Contract, specifically for Permit2 transferFrom, we have signature consisting of + // bytes32(permit2nonce) + bytes64(compacted signature) = bytes96 Permit2 Transfer format + + if (permit.length >= 194) { + // "0x".length + 96bytes*2 = 194, means permit already concatenated with nonce + // or it's a different type of Permit all together + return permit; + } + + return encodePermit2Transfer(BigInt(nonce), permit); +} + +function uintTo32ByteArrayBuffer(nonce: number | bigint) { + // Create a 32-byte ArrayBuffer + const buffer = new Uint8Array(32); + + // Convert nonce to hex string and pad it to 64 hex characters (32 bytes) + let nonceHex = nonce.toString(16).padStart(64, '0'); + + // Convert the hex string to bytes and fill the ArrayBuffer + for (let i = 0; i < 32; i++) { + buffer[i] = parseInt(nonceHex.slice(i * 2, i * 2 + 2), 16); + } + + return buffer; +} + +function hexToByteArray(hexString: string) { + // Remove "0x" prefix if present + hexString = hexString.replace(/^0x/, ''); + + // Convert hex string to Uint8Array + const byteArray = new Uint8Array(hexString.length / 2); + for (let i = 0; i < hexString.length; i += 2) { + byteArray[i / 2] = parseInt(hexString.slice(i, i + 2), 16); + } + return byteArray; +} + +function encodePermit2Transfer(nonce: number | bigint, signature: string) { + // Get 32-byte ArrayBuffer for nonce + const nonceBuffer = uintTo32ByteArrayBuffer(nonce); + + // Convert signature hex string to Uint8Array (64 bytes) + const signatureBuffer = hexToByteArray(signature); + if (signatureBuffer.length !== 64) { + throw new Error('Signature must be exactly 64 bytes'); + } + + // Concatenate nonceBuffer and signatureBuffer + const packedBuffer = new Uint8Array(32 + 64); + packedBuffer.set(nonceBuffer, 0); // Copy nonceBuffer at the start + packedBuffer.set(signatureBuffer, 32); // Copy signatureBuffer after nonce + + // Convert to hex string for output + return ( + '0x' + + Array.from(packedBuffer) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') + ); +} diff --git a/src/methods/delta/helpers/misc.ts b/src/methods/delta/helpers/misc.ts new file mode 100644 index 00000000..238bc875 --- /dev/null +++ b/src/methods/delta/helpers/misc.ts @@ -0,0 +1,30 @@ +import type { SignableDeltaOrderData } from './buildDeltaOrderData'; + +export function sanitizeDeltaOrderData({ + owner, + beneficiary, + srcToken, + destToken, + srcAmount, + destAmount, + expectedDestAmount, + deadline, + nonce, + permit, + partnerAndFee, +}: SignableDeltaOrderData['data'] & + Record): SignableDeltaOrderData['data'] { + return { + owner, + beneficiary, + srcToken, + destToken, + srcAmount, + destAmount, + expectedDestAmount, + deadline, + nonce, + permit, + partnerAndFee, + }; +} diff --git a/src/methods/delta/helpers/types.ts b/src/methods/delta/helpers/types.ts new file mode 100644 index 00000000..3a9b6e5f --- /dev/null +++ b/src/methods/delta/helpers/types.ts @@ -0,0 +1,72 @@ +export type DeltaAuctionOrder = { + /** @description The address of the order owner */ + owner: string; + /** @description The address of the order beneficiary */ + beneficiary: string; // beneficiary==owner if no transferTo + /** @description The address of the src token */ + srcToken: string; // lowercase + /** @description The address of the dest token */ + destToken: string; // lowercase + /** @description The amount of src token to swap */ + srcAmount: string; // wei + /** @description The minimum amount of dest token to receive */ + destAmount: string; // wei + /** @description The expected amount of dest token to receive */ + expectedDestAmount: string; // wei + /** @description The deadline for the order */ + deadline: number; // seconds + /** @description The nonce of the order */ + nonce: string; // can be random, can even be Date.now() + /** @description Optional permit signature for the src token */ + permit: string; //can be "0x" + /** @description Encoded partner address, fee bps, and flags for the order. partnerAndFee = (partner << 96) | (partnerTakesSurplus << 8) | fee in bps (max fee is 2%) */ + partnerAndFee: string; +}; + +type DeltaAuctionStatus = + | 'NOT_STARTED' + | 'POSTED' + | 'RUNNING' + | 'EXECUTING' + | 'EXECUTED' + | 'FAILED' + | 'EXPIRED'; + +type DeltaAuctionTransaction = { + id: string; + hash: string; + blockNumber: number; + blockHash: string; + gasUsed: bigint; + gasPrice: bigint; + blobGasUsed: bigint; + blobGasPrice: bigint; + index: number; + status: number; + from: string; + to: string; + receivedAmount: string; + spentAmount: string; + filledPercent: number; // in base points + protocolFee: string; + partnerFee: string; + agent: string; + auctionId: string; +}; + +export type ParaswapDeltaAuction = { + id: string; + deltaVersion: string; // 1.0 or 2.0 currently + user: string; + signature: string; + status: DeltaAuctionStatus; + order: DeltaAuctionOrder; + orderHash: string; + transactions: DeltaAuctionTransaction[]; + chainId: number; + partner: string; + expiresAt: string; + createdAt: string; + updatedAt: string; + partiallyFillable: boolean; +}; diff --git a/src/methods/delta/index.ts b/src/methods/delta/index.ts new file mode 100644 index 00000000..afb2296a --- /dev/null +++ b/src/methods/delta/index.ts @@ -0,0 +1,119 @@ +import type { ConstructProviderFetchInput } from '../../types'; +import type { ParaswapDeltaAuction } from './helpers/types'; +import { + BuildDeltaOrderDataParams, + BuildDeltaOrderFunctions, + constructBuildDeltaOrder, +} from './buildDeltaOrder'; +import { + constructPostDeltaOrder, + PostDeltaOrderFunctions, +} from './postDeltaOrder'; +import { + constructSignDeltaOrder, + SignDeltaOrderFunctions, +} from './signDeltaOrder'; +import { + GetDeltaContractFunctions, + constructGetDeltaContract, +} from './getDeltaContract'; +import { + constructGetDeltaPrice, + GetDeltaPriceFunctions, +} from './getDeltaPrice'; +import { + constructGetDeltaOrders, + GetDeltaOrdersFunctions, +} from './getDeltaOrders'; +import { + constructGetPartnerFee, + GetPartnerFeeFunctions, +} from './getPartnerFee'; +import { + ApproveTokenForDeltaFunctions, + constructApproveTokenForDelta, +} from './approveForDelta'; + +export type SubmitDeltaOrderParams = BuildDeltaOrderDataParams & { + /** @description designates the Order as being able to partilly filled, as opposed to fill-or-kill */ + partiallyFillable?: boolean; +}; + +type SubmitDeltaOrder = ( + orderParams: SubmitDeltaOrderParams +) => Promise; + +export type SubmitDeltaOrderFuncs = { + submitDeltaOrder: SubmitDeltaOrder; +}; + +export const constructSubmitDeltaOrder = ( + options: ConstructProviderFetchInput +): SubmitDeltaOrderFuncs => { + const { buildDeltaOrder } = constructBuildDeltaOrder(options); + const { signDeltaOrder } = constructSignDeltaOrder(options); + const { postDeltaOrder } = constructPostDeltaOrder(options); + + const submitDeltaOrder: SubmitDeltaOrder = async (orderParams) => { + const orderData = await buildDeltaOrder(orderParams); + const signature = await signDeltaOrder(orderData); + + const response = await postDeltaOrder({ + signature, + partner: orderParams.partner, + order: orderData.data, + partiallyFillable: orderParams.partiallyFillable, + }); + + return response; + }; + + return { submitDeltaOrder }; +}; + +export type DeltaOrderHandlers = SubmitDeltaOrderFuncs & + ApproveTokenForDeltaFunctions & + BuildDeltaOrderFunctions & + GetDeltaOrdersFunctions & + GetDeltaPriceFunctions & + GetDeltaContractFunctions & + GetPartnerFeeFunctions & + PostDeltaOrderFunctions & + SignDeltaOrderFunctions; + +/** @description construct SDK with every Delta Order-related method, fetching from API and Order signing */ +export const constructAllDeltaOrdersHandlers = ( + options: ConstructProviderFetchInput< + TxResponse, + 'signTypedDataCall' | 'transactCall' + > +): DeltaOrderHandlers => { + const deltaOrdersGetters = constructGetDeltaOrders(options); + const deltaOrdersContractGetter = constructGetDeltaContract(options); + const deltaPrice = constructGetDeltaPrice(options); + + const partnerFee = constructGetPartnerFee(options); + + const approveTokenForDelta = constructApproveTokenForDelta(options); + + const deltaOrdersSubmit = constructSubmitDeltaOrder(options); + + const deltaOrdersBuild = constructBuildDeltaOrder(options); + const deltaOrdersSign = constructSignDeltaOrder(options); + const deltaOrdersPost = constructPostDeltaOrder(options); + + // const DeltaOrdersApproveToken = constructApproveTokenForDeltaOrder(options); + + return { + ...deltaOrdersGetters, + ...deltaOrdersContractGetter, + ...deltaPrice, + ...partnerFee, + ...approveTokenForDelta, + ...deltaOrdersSubmit, + ...deltaOrdersBuild, + ...deltaOrdersSign, + ...deltaOrdersPost, + // ...deltaOrdersApproveToken, + }; +}; diff --git a/src/methods/delta/postDeltaOrder.ts b/src/methods/delta/postDeltaOrder.ts new file mode 100644 index 00000000..c2c285be --- /dev/null +++ b/src/methods/delta/postDeltaOrder.ts @@ -0,0 +1,48 @@ +import { API_URL } from '../../constants'; +import type { ConstructFetchInput } from '../../types'; +import { DeltaAuctionOrder, ParaswapDeltaAuction } from './helpers/types'; + +export type DeltaOrderToPost = { + /** @description Partner string */ + partner?: string; + order: DeltaAuctionOrder; + /** @description Signature of the order from order.owner address. EOA signatures must be submitted in ERC-2098 Compact Representation. */ + signature: string; + chainId: number; + /** @description designates the Order as being able to partilly filled, as opposed to fill-or-kill */ + partiallyFillable?: boolean; +}; + +export type PostDeltaOrderParams = Omit; + +type DeltaOrderApiResponse = ParaswapDeltaAuction; + +type PostDeltaOrder = ( + postData: PostDeltaOrderParams, + signal?: AbortSignal +) => Promise; + +export type PostDeltaOrderFunctions = { + postDeltaOrder: PostDeltaOrder; +}; + +export const constructPostDeltaOrder = ({ + apiURL = API_URL, + chainId, + fetcher, +}: ConstructFetchInput): PostDeltaOrderFunctions => { + const postOrderUrl = `${apiURL}/delta/orders` as const; + + const postDeltaOrder: PostDeltaOrder = (postData, signal) => { + const deltaOrderToPost: DeltaOrderToPost = { ...postData, chainId }; + + return fetcher({ + url: postOrderUrl, + method: 'POST', + data: deltaOrderToPost, + signal, + }); + }; + + return { postDeltaOrder }; +}; diff --git a/src/methods/delta/signDeltaOrder.ts b/src/methods/delta/signDeltaOrder.ts new file mode 100644 index 00000000..3fcd6770 --- /dev/null +++ b/src/methods/delta/signDeltaOrder.ts @@ -0,0 +1,54 @@ +import { deriveCompactSignature } from '../../helpers/misc'; +import type { ConstructProviderFetchInput } from '../../types'; +import { SignableDeltaOrderData } from './helpers/buildDeltaOrderData'; +import { sanitizeDeltaOrderData } from './helpers/misc'; + +export type SignLimitOrderFunctions = { + signLimitOrder: ( + signableOrderData: SignableDeltaOrderData + ) => Promise; +}; + +type SignDeltaOrder = ( + signableOrderData: SignableDeltaOrderData +) => Promise; + +export type SignDeltaOrderFunctions = { + signDeltaOrder: SignDeltaOrder; +}; + +// returns whatever `contractCaller` returns +// to allow for better versatility +export const constructSignDeltaOrder = ( + options: Pick< + ConstructProviderFetchInput, + 'contractCaller' + > +): SignDeltaOrderFunctions => { + const signDeltaOrder: SignDeltaOrder = async (typedData) => { + // types allow to pass OrderData & extra_stuff, but tx will break like that + const typedDataOnly: SignableDeltaOrderData = { + ...typedData, + data: sanitizeDeltaOrderData(typedData.data), + }; + const signature = await options.contractCaller.signTypedDataCall( + typedDataOnly + ); + + if (signature.length > 132) { + // signature more than 65 bytes, likely a multisig + // not compatible with EIP-2098 Compact Signatures + return signature; + } + + // both full and compact signatures work in the ParaswapDelta contract; + // compact signature can be marginally more gas efficient + try { + return deriveCompactSignature(signature); + } catch { + return signature; + } + }; + + return { signDeltaOrder }; +}; diff --git a/src/methods/quote/getQuote.ts b/src/methods/quote/getQuote.ts new file mode 100644 index 00000000..88edbc6f --- /dev/null +++ b/src/methods/quote/getQuote.ts @@ -0,0 +1,127 @@ +import { API_URL, SwapSide } from '../../constants'; +import { constructSearchString } from '../../helpers/misc'; +import type { DeltaPrice } from '../delta/getDeltaPrice'; +import type { + ConstructFetchInput, + EnumerateLiteral, + OptimalRate, +} from '../../types'; + +type TradeMode = 'delta' | 'market' | 'all'; +// enable passing enum value by string +type SwapSideUnion = EnumerateLiteral; + +export type QuoteParams = { + /** @description Source Token Address */ + srcToken: string; + /** @description Destination Token Address */ + destToken: string; + /** @description srcToken amount (in case of SELL) or destToken amount (in case of BUY), in wei */ + amount: string; + /** @description Source Token Decimals. */ + srcDecimals: number; + /** @description Destination Token Decimals */ + destDecimals: number; + /** @description SELL or BUY */ + side: SwapSideUnion; + /** @description User's Wallet Address */ + userAddress?: string; + /** @description Partner string */ + partner?: string; + /** @description Preferred mode for the trade. In case of "all", Delta pricing is returned, with Market as a fallback */ + mode: M; +}; + +type QuoteQueryOptions = QuoteParams & { + chainId: number; // will return error from API on unsupported chains +}; + +type FallbackReason = { + errorType: string; + details: string; +}; + +export type QuoteWithMarketPrice = { + market: OptimalRate; +}; + +export type QuoteWithDeltaPrice = { + delta: DeltaPrice; +}; + +export type QuoteWithMarketPriceAsFallback = QuoteWithMarketPrice & { + fallbackReason: FallbackReason; +}; + +export type QuoteResponse = + | QuoteWithDeltaPrice + | QuoteWithMarketPrice + | QuoteWithMarketPriceAsFallback; + +interface GetQuoteFunc { + ( + options: QuoteParams<'delta'>, + signal?: AbortSignal + ): Promise; + ( + options: QuoteParams<'market'>, + signal?: AbortSignal + ): Promise; + (options: QuoteParams<'all'>, signal?: AbortSignal): Promise< + QuoteWithDeltaPrice | QuoteWithMarketPriceAsFallback // "all" mode tries for deltaPrice and falls back to market priceRoute + >; + (options: QuoteParams, signal?: AbortSignal): Promise; +} + +export type GetQuoteFunctions = { + getQuote: GetQuoteFunc; +}; + +export const constructGetQuote = ({ + apiURL = API_URL, + chainId, + fetcher, +}: ConstructFetchInput): GetQuoteFunctions => { + const pricesUrl = `${apiURL}/quote` as const; + + function getQuote( + options: QuoteParams<'delta'>, + signal?: AbortSignal + ): Promise; + function getQuote( + options: QuoteParams<'market'>, + signal?: AbortSignal + ): Promise; + function getQuote( + options: QuoteParams<'all'>, + signal?: AbortSignal + ): Promise; + function getQuote( + options: QuoteParams, + signal?: AbortSignal + ): Promise; + async function getQuote( + options: QuoteParams, + signal?: AbortSignal + ): Promise { + const search = constructSearchString({ + ...options, + chainId, + // side: SwapSide.SELL, // so far SELL side only for Delta + }); + + const fetchURL = `${pricesUrl}/${search}` as const; + + const data = await fetcher({ + url: fetchURL, + method: 'GET', + signal, + }); + + return data; + } + + return { + getQuote, + }; +}; diff --git a/src/methods/swap/helpers/normalizeRateOptions.ts b/src/methods/swap/helpers/normalizeRateOptions.ts index eb41a311..fc7c4d7a 100644 --- a/src/methods/swap/helpers/normalizeRateOptions.ts +++ b/src/methods/swap/helpers/normalizeRateOptions.ts @@ -13,8 +13,6 @@ type NormalizedRateOptions< Partial> & Omit; -const DEFAULT_PARTNER = 'paraswap.io'; - export function normalizeRateOptions< O extends MinRateOptionsInput, T extends { options?: Partial } @@ -23,7 +21,7 @@ export function normalizeRateOptions< excludePricingMethods, excludeContractMethods, includeContractMethods, - partner = DEFAULT_PARTNER, + partner, includeDEXS, excludeDEXS, excludePools, diff --git a/src/methods/swap/spender.ts b/src/methods/swap/spender.ts index 68aecbe9..16416aea 100644 --- a/src/methods/swap/spender.ts +++ b/src/methods/swap/spender.ts @@ -16,6 +16,10 @@ interface AdaptersContractsResult { AugustusSwapper: string; TokenTransferProxy: string; AugustusRFQ: string; + Executors: { + [key: `Executor${number}`]: string; + }; + ParaswapDelta?: string; // only available on chains with Delta support } export const constructGetSpender = ({ diff --git a/src/sdk/full.ts b/src/sdk/full.ts index 1a27a51f..4f5b8666 100644 --- a/src/sdk/full.ts +++ b/src/sdk/full.ts @@ -8,12 +8,24 @@ import { constructAllNFTOrdersHandlers, NFTOrderHandlers, } from '../methods/nftOrders'; +import { + constructAllDeltaOrdersHandlers, + DeltaOrderHandlers, +} from '../methods/delta'; +import { + constructGetQuote, + GetQuoteFunctions, +} from '../methods/quote/getQuote'; +import { ConstructBaseInput } from '../types'; +import { API_URL, DEFAULT_VERSION } from '../constants'; export type AllSDKMethods = { swap: SwapSDKMethods; limitOrders: LimitOrderHandlers; nftOrders: NFTOrderHandlers; -}; + delta: DeltaOrderHandlers; + quote: GetQuoteFunctions; +} & Required; /** @description construct SDK with every method, for swap and limitOrders */ export const constructFullSDK = ( @@ -25,6 +37,18 @@ export const constructFullSDK = ( constructAllLimitOrdersHandlers(config); const nftOrders: NFTOrderHandlers = constructAllNFTOrdersHandlers(config); + const delta: DeltaOrderHandlers = + constructAllDeltaOrdersHandlers(config); + const quote = constructGetQuote(config); - return { swap, limitOrders, nftOrders }; + return { + swap, + limitOrders, + nftOrders, + delta, + quote, + apiURL: config.apiURL ?? API_URL, + chainId: config.chainId, + version: config.version ?? DEFAULT_VERSION, + }; }; diff --git a/src/sdk/partial.ts b/src/sdk/partial.ts index 05c9b285..56f0d12e 100644 --- a/src/sdk/partial.ts +++ b/src/sdk/partial.ts @@ -10,8 +10,9 @@ import type { CancelLimitOrderFunctions } from '../methods/limitOrders/cancelOrd import type { ApproveTokenForLimitOrderFunctions } from '../methods/limitOrders/approveForOrder'; import type { CancelNFTOrderFunctions } from '../methods/nftOrders/cancelOrder'; import type { ApproveTokenForNFTOrderFunctions } from '../methods/nftOrders/approveForOrder'; +import type { FillOrderDirectlyFunctions } from '../methods/limitOrders/fillOrderDirectly'; +import type { ApproveTokenForDeltaFunctions } from '../methods/delta/approveForDelta'; import { API_URL, DEFAULT_VERSION } from '../constants'; -import { FillOrderDirectlyFunctions } from '../methods/limitOrders/fillOrderDirectly'; export type SDKConfig = ConstructProviderFetchInput< TxResponse, @@ -48,7 +49,8 @@ type InferWithTxResponse< FillOrderDirectlyFunctions, ApproveTokenForLimitOrderFunctions, CancelNFTOrderFunctions, - ApproveTokenForNFTOrderFunctions + ApproveTokenForNFTOrderFunctions, + ApproveTokenForDeltaFunctions ] // then merge IntersectionOfReturns with them recursively > @@ -69,14 +71,13 @@ type MergeExtendableOnce< type MergeExtendableRecursively< Accum extends Record, Replacements extends Record[] -> = Replacements extends [head: infer Head, ...tail: infer Tail] // use [head: infer Head extends Record, ...tail: infer Tail] after Ts update +> = Replacements extends [ + head: infer Head extends Record, + ...tail: infer Tail +] ? Tail extends Record[] - ? Head extends Record - ? MergeExtendableRecursively, Tail> - : Accum - : Head extends Record - ? MergeExtendableOnce - : Accum + ? MergeExtendableRecursively, Tail> + : MergeExtendableOnce : Accum; /** @description construct composable SDK with methods you choose yourself */ diff --git a/src/sdk/simple.ts b/src/sdk/simple.ts index 75319e4a..45da28cc 100644 --- a/src/sdk/simple.ts +++ b/src/sdk/simple.ts @@ -96,6 +96,38 @@ import { import { constructSwapSDK } from '../methods/swap'; import type { AxiosRequirement } from '../helpers/fetchers/axios'; import { API_URL, DEFAULT_VERSION } from '../constants'; +import { + constructAllDeltaOrdersHandlers, + DeltaOrderHandlers, +} from '../methods/delta'; +import { + BuildDeltaOrderFunctions, + constructBuildDeltaOrder, +} from '../methods/delta/buildDeltaOrder'; +import { + constructGetDeltaOrders, + GetDeltaOrdersFunctions, +} from '../methods/delta/getDeltaOrders'; +import { + constructGetDeltaPrice, + GetDeltaPriceFunctions, +} from '../methods/delta/getDeltaPrice'; +import { + constructGetDeltaContract, + GetDeltaContractFunctions, +} from '../methods/delta/getDeltaContract'; +import { + constructGetPartnerFee, + GetPartnerFeeFunctions, +} from '../methods/delta/getPartnerFee'; +import { + constructPostDeltaOrder, + PostDeltaOrderFunctions, +} from '../methods/delta/postDeltaOrder'; +import { + constructGetQuote, + GetQuoteFunctions, +} from '../methods/quote/getQuote'; export type SwapFetchMethods = GetBalancesFunctions & GetTokensFunctions & @@ -117,16 +149,29 @@ export type NFTOrdersFetchMethods = GetNFTOrdersContractFunctions & PostNFTOrderFunctions & BuildNFTOrdersTxFunctions; +export type DeltaFetchMethods = BuildDeltaOrderFunctions & + GetDeltaOrdersFunctions & + GetDeltaPriceFunctions & + GetDeltaContractFunctions & + GetPartnerFeeFunctions & + PostDeltaOrderFunctions; + export type SimpleFetchSDK = { swap: SwapFetchMethods; limitOrders: LimitOrdersFetchMethods; nftOrders: NFTOrdersFetchMethods; + delta: DeltaFetchMethods; + quote: QuoteFetchMethods; } & Required; +export type QuoteFetchMethods = GetQuoteFunctions; + export type SimpleSDK = { swap: SwapSDKMethods; limitOrders: LimitOrderHandlers; nftOrders: NFTOrderHandlers; + delta: DeltaOrderHandlers; + quote: QuoteFetchMethods; } & Required; export type FetcherOptions = ( @@ -216,10 +261,24 @@ export function constructSimpleSDK( constructBuildNFTOrderTx ); + const delta = constructPartialSDK( + config, + constructBuildDeltaOrder, + constructPostDeltaOrder, + constructGetDeltaOrders, + constructGetDeltaPrice, + constructGetDeltaContract, + constructGetPartnerFee + ); + + const quote = constructPartialSDK(config, constructGetQuote); + return { swap, limitOrders, nftOrders, + delta, + quote, apiURL: options.apiURL ?? API_URL, chainId: options.chainId, version: options.version ?? DEFAULT_VERSION, @@ -244,10 +303,17 @@ export function constructSimpleSDK( const nftOrders: NFTOrderHandlers = constructAllNFTOrdersHandlers(config); + const delta: DeltaOrderHandlers = + constructAllDeltaOrdersHandlers(config); + + const quote = constructGetQuote(config); + return { swap, limitOrders, nftOrders, + delta, + quote, apiURL: options.apiURL ?? API_URL, chainId: options.chainId, version: options.version ?? DEFAULT_VERSION, diff --git a/src/types.ts b/src/types.ts index bd32d2af..320c8d1c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,7 +23,7 @@ export type { OptionalRate, }; -type EnumerateLiteral> = { +export type EnumerateLiteral> = { [K in keyof T]: T[K] extends `${infer n}` ? n : never; }[keyof T]; // keeping version as string allows for more flexibility diff --git a/tests/__snapshots__/delta.test.ts.snap b/tests/__snapshots__/delta.test.ts.snap new file mode 100644 index 00000000..f8042633 --- /dev/null +++ b/tests/__snapshots__/delta.test.ts.snap @@ -0,0 +1,269 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Delta:methods Build Delta Order 1`] = ` +{ + "data": { + "beneficiary": "0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9", + "deadline": NaN, + "destAmount": "3147447403157656698880", + "destToken": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "expectedDestAmount": "3163263721766488892666", + "nonce": "dynamic_number", + "owner": "0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9", + "partnerAndFee": "0", + "permit": "0x", + "srcAmount": "1000000000000000000", + "srcToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + }, + "domain": { + "chainId": 1, + "name": "Portikus", + "verifyingContract": "0x0000000000bbf5c5fd284e657f01bd000933c96d", + "version": "2.0.0", + }, + "types": { + "Order": [ + { + "name": "owner", + "type": "address", + }, + { + "name": "beneficiary", + "type": "address", + }, + { + "name": "srcToken", + "type": "address", + }, + { + "name": "destToken", + "type": "address", + }, + { + "name": "srcAmount", + "type": "uint256", + }, + { + "name": "destAmount", + "type": "uint256", + }, + { + "name": "expectedDestAmount", + "type": "uint256", + }, + { + "name": "deadline", + "type": "uint256", + }, + { + "name": "nonce", + "type": "uint256", + }, + { + "name": "partnerAndFee", + "type": "uint256", + }, + { + "name": "permit", + "type": "bytes", + }, + ], + }, +} +`; + +exports[`Delta:methods Get Delta Order by Id 1`] = ` +{ + "chainId": 1, + "createdAt": "2024-10-18T14:44:03.502Z", + "deltaVersion": "1.0", + "expiresAt": "2024-10-18T15:43:16.000Z", + "id": "50950528-d362-4359-a89e-ed6e49be1a20", + "order": { + "beneficiary": "0x76176c2971300217e9f48e3dd4e40591500b96ff", + "deadline": 1729266196, + "destAmount": "2950666627548284", + "destToken": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "nonce": 1729262634617, + "owner": "0x76176c2971300217e9f48e3dd4e40591500b96ff", + "permit": "0x", + "srcAmount": "40000000000000000000", + "srcToken": "0x6b175474e89094c44da98b954eedeac495271d0f", + }, + "orderHash": null, + "partiallyFillable": false, + "partner": "delta-paraswap.io-local", + "receivedAmount": null, + "status": "FAILED", + "transaction": null, + "transactions": [], + "updatedAt": "2024-10-18T14:44:08.895Z", + "user": "0x76176c2971300217e9f48e3dd4e40591500b96ff", +} +`; + +exports[`Delta:methods Get Delta Orders for user 1`] = ` +[ + { + "chainId": 1, + "createdAt": "2024-10-10T16:18:04.727Z", + "deltaVersion": "1.0", + "expiresAt": "2024-10-10T17:17:47.000Z", + "id": "8515cce6-c7c6-486b-9f1e-5702f204edd6", + "order": { + "beneficiary": "0x76176c2971300217e9f48e3dd4e40591500b96ff", + "deadline": 1728580667, + "destAmount": "11302885800000000000", + "destToken": "0x6b175474e89094c44da98b954eedeac495271d0f", + "nonce": 1728577074265, + "owner": "0x76176c2971300217e9f48e3dd4e40591500b96ff", + "permit": "0x00000000000000000000000076176c2971300217e9f48e3dd4e40591500b96ff00000000000000000000000036ff475499e928590659d5b8aa3a34330a583fd900000000000000000000000000000000000000000000000000000000019d278900000000000000000000000000000000000000000000000000000000670bf2ae000000000000000000000000000000000000000000000000000000000000001bf548be9f97f37f0b2ab285bba67c8fdab99c3a08b0fdc0a910267988485535945df93e27958867f1d479c7b7783e98ba586629407e44f8d5c5d4115a7298dca9", + "srcAmount": "27076489", + "srcToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + }, + "orderHash": null, + "partiallyFillable": false, + "partner": "delta-paraswap.io-local", + "receivedAmount": "17225367867506356154", + "status": "EXECUTED", + "transaction": { + "blobGasPrice": null, + "blobGasUsed": null, + "blockHash": "0x21425d0c3625d5e55061ba2fd8ce91621dbee52c7d2846b76dd897d3438a6893", + "blockNumber": 20936359, + "contractAddress": null, + "cumulativeGasUsed": "8881978", + "from": "0x2e5eF37Ade8afb712B8Be858fEc7389Fe32857e2", + "gasPrice": "23525770029", + "gasUsed": "597540", + "hash": "0x3e5040d187288848ca57e4423d60fd31922b6db7fe636580d96200b03a8a8d8f", + "index": 57, + "status": 1, + "to": "0x1D7405DF25FD2fe80390DA3A696dcFd5120cA9Ce", + }, + "transactions": [ + { + "agent": "paraswap", + "auctionId": "8515cce6-c7c6-486b-9f1e-5702f204edd6", + "blobGasPrice": 0, + "blobGasUsed": 0, + "blockHash": "0x21425d0c3625d5e55061ba2fd8ce91621dbee52c7d2846b76dd897d3438a6893", + "blockNumber": 20936359, + "filledPercent": 10000, + "from": "0x2e5eF37Ade8afb712B8Be858fEc7389Fe32857e2", + "gasPrice": 23525770029, + "gasUsed": 597540, + "hash": "0x3e5040d187288848ca57e4423d60fd31922b6db7fe636580d96200b03a8a8d8f", + "id": "c729ff7b-ffc8-4c85-88d8-41f4f26668eb", + "index": 57, + "partnerFee": "0", + "protocolFee": "0", + "receivedAmount": "17225367867506356154", + "spentAmount": "27076489", + "status": 1, + "to": "0x1D7405DF25FD2fe80390DA3A696dcFd5120cA9Ce", + }, + ], + "updatedAt": "2024-10-10T16:18:51.447Z", + "user": "0x76176c2971300217e9f48e3dd4e40591500b96ff", + }, + { + "chainId": 1, + "createdAt": "2024-10-09T16:52:18.826Z", + "deltaVersion": "1.0", + "expiresAt": "2024-10-09T17:52:08.000Z", + "id": "7696f983-4f0d-4bb0-b591-61957abf74de", + "order": { + "beneficiary": "0x76176c2971300217e9f48e3dd4e40591500b96ff", + "deadline": 1728496328, + "destAmount": "736681085000000000000", + "destToken": "0xcafe001067cdef266afb7eb5a286dcfd277f3de5", + "nonce": 1728492729603, + "owner": "0x76176c2971300217e9f48e3dd4e40591500b96ff", + "permit": "0x", + "srcAmount": "21000000000000000", + "srcToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + }, + "orderHash": null, + "partiallyFillable": false, + "partner": "delta-paraswap.io-local", + "receivedAmount": "1635237633557152096036", + "status": "EXECUTED", + "transaction": { + "blobGasPrice": null, + "blobGasUsed": null, + "blockHash": "0x58dbd72e843c1224e8113694b8b4a37e29657f0d52634c0f3f89835a3c7a7187", + "blockNumber": 20929352, + "contractAddress": null, + "cumulativeGasUsed": "19979608", + "from": "0x2e5eF37Ade8afb712B8Be858fEc7389Fe32857e2", + "gasPrice": "47757958240", + "gasUsed": "224182", + "hash": "0x6d5fa34f4723283dc32496acb8b8faf4bb9e713e3a9d43152ebef7d842c59700", + "index": 188, + "status": 1, + "to": "0x1D7405DF25FD2fe80390DA3A696dcFd5120cA9Ce", + }, + "transactions": [ + { + "agent": "paraswap", + "auctionId": "7696f983-4f0d-4bb0-b591-61957abf74de", + "blobGasPrice": 0, + "blobGasUsed": 0, + "blockHash": "0x58dbd72e843c1224e8113694b8b4a37e29657f0d52634c0f3f89835a3c7a7187", + "blockNumber": 20929352, + "filledPercent": 10000, + "from": "0x2e5eF37Ade8afb712B8Be858fEc7389Fe32857e2", + "gasPrice": 47757958240, + "gasUsed": 224182, + "hash": "0x6d5fa34f4723283dc32496acb8b8faf4bb9e713e3a9d43152ebef7d842c59700", + "id": "0531cfdf-0732-4757-8a46-3886487eeedd", + "index": 188, + "partnerFee": "0", + "protocolFee": "0", + "receivedAmount": "1635237633557152096036", + "spentAmount": "21000000000000000", + "status": 1, + "to": "0x1D7405DF25FD2fe80390DA3A696dcFd5120cA9Ce", + }, + ], + "updatedAt": "2024-10-09T16:52:37.585Z", + "user": "0x76176c2971300217e9f48e3dd4e40591500b96ff", + }, +] +`; + +exports[`Delta:methods Get Delta Price 1`] = ` +{ + "destAmount": "dynamic_number", + "destAmountBeforeFee": "dynamic_number", + "destToken": "0x6b175474e89094c44da98b954eedeac495271d0f", + "destUSD": "dynamic_number", + "destUSDBeforeFee": "dynamic_number", + "gasCost": "dynamic_number", + "gasCostBeforeFee": "dynamic_number", + "gasCostUSD": "dynamic_number", + "gasCostUSDBeforeFee": "dynamic_number", + "partner": "anon", + "partnerFee": 0, + "srcAmount": "1000000000000000000", + "srcToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "srcUSD": "dynamic_number", +} +`; + +exports[`Delta:methods Submit(=build+sign+post) Delta Order 1`] = ` +{ + "beneficiary": "0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9", + "deadline": NaN, + "destAmount": "3147447403157656698880", + "destToken": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "expectedDestAmount": "3163263721766488892666", + "nonce": "dynamic_number", + "owner": "0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9", + "partnerAndFee": "0", + "permit": "0x", + "srcAmount": "1000000000000000000", + "srcToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", +} +`; diff --git a/tests/__snapshots__/quote.test.ts.snap b/tests/__snapshots__/quote.test.ts.snap new file mode 100644 index 00000000..cb7c118d --- /dev/null +++ b/tests/__snapshots__/quote.test.ts.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Quote:methods Get Fallback Market Quote for all 2`] = ` +{ + "bestRoute": [ + { + "percent": 100, + "swaps": [ + { + "destDecimals": 18, + "destToken": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "srcDecimals": 6, + "srcToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "swapExchanges": [ + { + "data": "largerly dynamic object", + "destAmount": "dynamic_number", + "exchange": "dynamic_string", + "percent": 100, + "poolAddresses": "dynamic_array", + "srcAmount": "10000000", + }, + ], + }, + ], + }, + ], + "blockNumber": "dynamic_number", + "contractAddress": "0x6a000f20005980200259b80c5102003040001068", + "contractMethod": "swapExactAmountIn", + "destAmount": "dynamic_number", + "destDecimals": 18, + "destToken": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "destUSD": "dynamic_number", + "gasCost": "dynamic_number", + "gasCostUSD": "dynamic_number", + "hmac": "dynamic_number", + "maxImpactReached": false, + "network": 1, + "partner": "anon", + "partnerFee": 0, + "side": "SELL", + "srcAmount": "10000000", + "srcDecimals": 6, + "srcToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "srcUSD": "dynamic_number", + "tokenTransferProxy": "0x6a000f20005980200259b80c5102003040001068", + "version": "6.2", +} +`; + +exports[`Quote:methods Get Quote for market 1`] = ` +{ + "bestRoute": [ + { + "percent": 100, + "percentage": "dynamic_number", + "swaps": "dynamic_array", + }, + ], + "blockNumber": "dynamic_number", + "contractAddress": "0x6a000f20005980200259b80c5102003040001068", + "contractMethod": "dynamic_string", + "destAmount": "dynamic_number", + "destDecimals": 18, + "destToken": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "destUSD": "dynamic_number", + "gasCost": "dynamic_number", + "gasCostUSD": "dynamic_number", + "hmac": "dynamic_number", + "maxImpactReached": false, + "network": 1, + "partner": "anon", + "partnerFee": 0, + "side": "SELL", + "srcAmount": "100000000000", + "srcDecimals": 6, + "srcToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "srcUSD": "dynamic_number", + "tokenTransferProxy": "0x6a000f20005980200259b80c5102003040001068", + "version": "6.2", +} +`; diff --git a/tests/delta.test.ts b/tests/delta.test.ts new file mode 100644 index 00000000..c0fd35c3 --- /dev/null +++ b/tests/delta.test.ts @@ -0,0 +1,471 @@ +import * as dotenv from 'dotenv'; +import Web3 from 'web3'; +import { ethers } from 'ethersV5'; +import { ethers as ethersV6 } from 'ethers'; +import fetch from 'isomorphic-unfetch'; +import { + constructEthersV5ContractCaller, + constructEthersV6ContractCaller, + constructFetchFetcher, + constructPartialSDK, + constructWeb3ContractCaller, + constructGetDeltaContract, + constructGetDeltaOrders, + constructGetDeltaPrice, + constructBuildDeltaOrder, + constructApproveTokenForDelta, + constructSignDeltaOrder, + constructViemContractCaller, + constructGetPartnerFee, + SignableDeltaOrderData, + DeltaPrice, + constructPostDeltaOrder, + constructSubmitDeltaOrder, + PostDeltaOrderParams, + FetcherFunction, +} from '../src'; +import BigNumber from 'bignumber.js'; + +import erc20abi from './abi/ERC20.json'; + +import { assert } from 'ts-essentials'; +import { HardhatProvider } from './helpers/hardhat'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createWalletClient, custom, Hex } from 'viem'; +import { hardhat } from 'viem/chains'; + +dotenv.config(); + +jest.setTimeout(30 * 1000); + +const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; +const DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; + +const chainId = 1; +const srcToken = WETH; +const destToken = DAI; +const srcAmount = (1 * 1e18).toString(); //The source amount multiplied by its decimals + +const TEST_MNEMONIC = + 'radar blur cabbage chef fix engine embark joy scheme fiction master release'; +//0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9 +const wallet = ethers.Wallet.fromMnemonic(TEST_MNEMONIC); +const walletV6 = ethersV6.HDNodeWallet.fromPhrase(TEST_MNEMONIC); + +const web3provider = new Web3(HardhatProvider as any); + +const ethersProvider = new ethers.providers.Web3Provider( + HardhatProvider as any +); + +const ethersV6Provider = new ethersV6.BrowserProvider(HardhatProvider); +const signerV6 = walletV6.connect(ethersV6Provider); + +const fetchFetcher = constructFetchFetcher(fetch); + +const signer = wallet.connect(ethersProvider); +const senderAddress = signer.address; + +const viemWalletClient = createWalletClient({ + // either walletClient needs to have account set at creation + // or provider must own the account (for testing can `await viemTestClient.impersonateAccount({ address: senderAddress });`) + // to be able to sign transactions + account: privateKeyToAccount(wallet.privateKey as Hex), + chain: { ...hardhat, id: chainId }, + transport: custom(HardhatProvider), +}); + +const ethersV5ContractCaller = constructEthersV5ContractCaller( + { + ethersProviderOrSigner: signer, + EthersContract: ethers.Contract, + }, + senderAddress +); + +const ethersV6ContractCaller = constructEthersV6ContractCaller( + { + ethersV6ProviderOrSigner: signerV6, + EthersV6Contract: ethersV6.Contract, + }, + senderAddress +); + +const web3ContractCaller = constructWeb3ContractCaller( + web3provider, + senderAddress +); + +const viemContractCaller = constructViemContractCaller( + viemWalletClient, + senderAddress +); + +describe('Delta:methods', () => { + const deltaSDK = constructPartialSDK( + { + chainId: 1, + fetcher: fetchFetcher, + contractCaller: ethersV5ContractCaller, + }, + constructGetDeltaContract, + constructGetDeltaOrders, + constructGetDeltaPrice, + constructBuildDeltaOrder, + constructApproveTokenForDelta, + constructGetPartnerFee + ); + + test('Get Delta Price', async () => { + const deltaPrice = await deltaSDK.getDeltaPrice({ + srcToken: srcToken, + destToken: destToken, + amount: srcAmount, + userAddress: senderAddress, + srcDecimals: 18, + destDecimals: 18, + }); + + const staticDeltaPrice: typeof deltaPrice = { + ...deltaPrice, + destAmount: 'dynamic_number', + destAmountBeforeFee: 'dynamic_number', + srcUSD: 'dynamic_number', + destUSD: 'dynamic_number', + destUSDBeforeFee: 'dynamic_number', + gasCost: 'dynamic_number', + gasCostBeforeFee: 'dynamic_number', + gasCostUSD: 'dynamic_number', + gasCostUSDBeforeFee: 'dynamic_number', + }; + + expect(staticDeltaPrice).toMatchSnapshot(); + }); + + test('Get Delta Contract', async () => { + const deltaContract = await deltaSDK.getDeltaContract(); + expect(deltaContract).toMatchInlineSnapshot( + `"0x0000000000bbf5c5fd284e657f01bd000933c96d"` + ); + }); + + test('Approve Token For Delta', async () => { + const deltaContract = await deltaSDK.getDeltaContract(); + assert(deltaContract, 'Delta contract not found'); + + const allowanceBefore = await getTokenAllowance({ + tokenAddress: DAI, + owner: senderAddress, + spender: deltaContract, + }); + + expect(allowanceBefore.toString()).toEqual('0'); + + const amount = '1000000000000000000'; // 1 DAI + const tx = await deltaSDK.approveTokenForDelta(amount, DAI); + expect(tx).toBeDefined(); + await tx.wait(); + + const allowanceAfter = await getTokenAllowance({ + tokenAddress: DAI, + owner: senderAddress, + spender: deltaContract, + }); + + expect(allowanceAfter.toString()).toEqual(amount); + }); + + test('Get Delta Orders for user', async () => { + const userWithOrders = '0x76176C2971300217E9f48E3dD4e40591500b96Ff'; + + const deltaOrders = await deltaSDK.getDeltaOrders({ + userAddress: userWithOrders, + }); + + // Orders that we know the user had in the past + const staticSliceOfPastOrders = deltaOrders.slice(-2); // first 2 orders historically + expect(staticSliceOfPastOrders).toMatchSnapshot(); + }); + + test('Get Delta Order by Id', async () => { + const orderId = '50950528-d362-4359-a89e-ed6e49be1a20'; + const deltaOrder = await deltaSDK.getDeltaOrderById(orderId); + expect(deltaOrder).toMatchSnapshot(); + }); + + test('Get PartnerFee', async () => { + const partnerFee = await deltaSDK.getPartnerFee({ partner: 'paraswap.io' }); + expect(partnerFee).toMatchInlineSnapshot(` + { + "partnerAddress": "0x81037e7be71bce9591de0c54bb485ad3e048b8de", + "partnerFee": 0.15, + "takeSurplus": false, + } + `); + }); + + test('Build Delta Order', async () => { + const sampleDeltaPrice: DeltaPrice = { + destAmount: '3163263721766488892666', + destAmountBeforeFee: '3194635547945152526200', + destToken: '0x6b175474e89094c44da98b954eedeac495271d0f', + destUSD: '3166.4269854931', + destUSDBeforeFee: '3197.8301834931', + gasCost: '347788', + gasCostBeforeFee: '124240', + gasCostUSD: '31.403198', + gasCostUSDBeforeFee: '11.218137', + partner: 'anon', + partnerFee: 0, + srcAmount: '1000000000000000000', + srcToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + srcUSD: '3191.5500000000', + }; + + const slippagePercent = 0.5; + const destAmountAfterSlippage = decreaseBySlippage( + sampleDeltaPrice.destAmount, + slippagePercent + ); + + const amount = '1000000000000000000'; // 1 DAI + + const signableOrderData = await deltaSDK.buildDeltaOrder({ + deltaPrice: sampleDeltaPrice, + owner: senderAddress, + // beneficiary: anotherAccount, // if need to send destToken to another account + // permit: "0x1234...", // if signed a Permit1 or Permit2 TransferFrom for DeltaContract + srcToken: WETH, + destToken: DAI, + srcAmount: amount, + destAmount: destAmountAfterSlippage, // minimum acceptable destAmount + }); + + const staticSignableOrderData: typeof signableOrderData = { + ...signableOrderData, + data: { + ...signableOrderData.data, + deadline: NaN, // dynamic number + nonce: 'dynamic_number', + }, + }; + expect(staticSignableOrderData).toMatchSnapshot(); + }); + + let signature = ''; + + test.each([ + ['ethersV5', ethersV5ContractCaller], + ['ethersV6', ethersV6ContractCaller], + ['web3', web3ContractCaller], + ['viem', viemContractCaller], + ])('sign Delta Order with %s', async (libName, contractCaller) => { + const sdk = constructPartialSDK( + { chainId: 1, fetcher: fetchFetcher, contractCaller }, + constructSignDeltaOrder + ); + + const sampleOrder: SignableDeltaOrderData = { + data: { + beneficiary: '0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9', + deadline: 1731328853, + destAmount: '3147447403157656698880', + destToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + expectedDestAmount: '3163263721766488892666', + nonce: '1731325253703', + owner: '0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9', + partnerAndFee: '0', + permit: '0x', + srcAmount: '1000000000000000000', + srcToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + }, + domain: { + chainId: 1, + name: 'Portikus', + verifyingContract: '0x0000000000bbf5c5fd284e657f01bd000933c96d', + version: '2.0.0', + }, + types: { + Order: [ + { + name: 'owner', + type: 'address', + }, + { + name: 'beneficiary', + type: 'address', + }, + { + name: 'srcToken', + type: 'address', + }, + { + name: 'destToken', + type: 'address', + }, + { + name: 'srcAmount', + type: 'uint256', + }, + { + name: 'destAmount', + type: 'uint256', + }, + { + name: 'expectedDestAmount', + type: 'uint256', + }, + { + name: 'deadline', + type: 'uint256', + }, + { + name: 'nonce', + type: 'uint256', + }, + { + name: 'partnerAndFee', + type: 'uint256', + }, + { + name: 'permit', + type: 'bytes', + }, + ], + }, + }; + + const deltaOrderSignature = await sdk.signDeltaOrder(sampleOrder); + if (!signature) signature = deltaOrderSignature; + // signatures match between libraries + expect(deltaOrderSignature).toEqual(signature); + }); + + const dummyFetcher: FetcherFunction = (params) => { + // intercept POST requests + if (params.method === 'POST') { + return params as any; + } + + return fetchFetcher(params); + }; + + const mockFetch = jest.fn(dummyFetcher); + + const dummySDK = constructPartialSDK( + { + chainId: 1, + fetcher: mockFetch as FetcherFunction, + contractCaller: ethersV5ContractCaller, + }, + constructPostDeltaOrder, + constructSubmitDeltaOrder + ); + + test('Post Delta Order', async () => { + const sampleOrderData: SignableDeltaOrderData['data'] = { + beneficiary: '0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9', + deadline: NaN, // dynamic number + destAmount: '3147447403157656698880', + destToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + expectedDestAmount: '3163263721766488892666', + nonce: 'dynamic_number', + owner: '0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9', + partnerAndFee: '0', + permit: '0x', + srcAmount: '1000000000000000000', + srcToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + }; + + const sampleSignature = '0x1234....'; + + const input = { + order: sampleOrderData, + signature: sampleSignature, + }; + + await dummySDK.postDeltaOrder(input); + + expect(mockFetch).toHaveBeenLastCalledWith({ + data: { ...input, chainId: dummySDK.chainId }, + method: 'POST', + url: `${dummySDK.apiURL}/delta/orders`, + }); + }); + + test('Submit(=build+sign+post) Delta Order', async () => { + const sampleDeltaPrice: DeltaPrice = { + destAmount: '3163263721766488892666', + destAmountBeforeFee: '3194635547945152526200', + destToken: '0x6b175474e89094c44da98b954eedeac495271d0f', + destUSD: '3166.4269854931', + destUSDBeforeFee: '3197.8301834931', + gasCost: '347788', + gasCostBeforeFee: '124240', + gasCostUSD: '31.403198', + gasCostUSDBeforeFee: '11.218137', + partner: 'anon', + partnerFee: 0, + srcAmount: '1000000000000000000', + srcToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + srcUSD: '3191.5500000000', + }; + + const slippagePercent = 0.5; + const destAmountAfterSlippage = decreaseBySlippage( + sampleDeltaPrice.destAmount, + slippagePercent + ); + + const amount = '1000000000000000000'; // 1 DAI + + const input = { + deltaPrice: sampleDeltaPrice, + owner: senderAddress, + // beneficiary: anotherAccount, // if need to send destToken to another account + // permit: "0x1234...", // if signed a Permit1 or Permit2 TransferFrom for DeltaContract + srcToken: WETH, + destToken: DAI, + srcAmount: amount, + destAmount: destAmountAfterSlippage, // minimum acceptable destAmount + }; + + await dummySDK.submitDeltaOrder(input); + + const callArgs = mockFetch.mock.lastCall?.[0]; + assert(callArgs, 'No fetch call was made'); + assert('data' in callArgs, 'No data was sent in the fetch call'); + const { order, signature } = callArgs.data as PostDeltaOrderParams; + + expect(signature).toBeDefined(); + + const staticSignedOrderData: SignableDeltaOrderData['data'] = { + ...order, + deadline: NaN, // dynamic number + nonce: 'dynamic_number', + }; + + expect(staticSignedOrderData).toMatchSnapshot(); + }); +}); + +function getTokenAllowance({ + tokenAddress, + owner, + spender, +}: { + tokenAddress: string; + owner: string; + spender: string; +}): Promise { + const contract = new ethers.Contract(tokenAddress, erc20abi, signer); + return contract.allowance(owner, spender); +} + +function decreaseBySlippage(amount: string, slippagePercent: number): string { + const amountAfterSlippage = BigInt( + +(+amount * (1 - slippagePercent / 100)).toFixed(0) + ).toString(10); + + return amountAfterSlippage; +} diff --git a/tests/quote.test.ts b/tests/quote.test.ts new file mode 100644 index 00000000..299436a2 --- /dev/null +++ b/tests/quote.test.ts @@ -0,0 +1,299 @@ +import * as dotenv from 'dotenv'; +import fetch from 'isomorphic-unfetch'; +import { + constructFetchFetcher, + constructPartialSDK, + constructGetQuote, + isFetcherError, +} from '../src'; + +import { assert } from 'ts-essentials'; + +dotenv.config(); + +jest.setTimeout(30 * 1000); + +const ETH = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; +const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; + +const chainId = 1; + +const fetchFetcher = constructFetchFetcher(fetch); + +describe('Quote:methods', () => { + const quoteSDK = constructPartialSDK( + { + chainId, + fetcher: fetchFetcher, + apiURL: process.env.API_URL, + }, + constructGetQuote + ); + + const amount = '100000000000'; // 100000 USDC, + + test('Get Quote for delta', async () => { + const quote = await quoteSDK.getQuote({ + srcToken: USDC, + destToken: ETH, + amount, + srcDecimals: 18, + destDecimals: 18, + mode: 'delta', + side: 'SELL', + }); + + expect('delta' in quote).toBeTruthy(); + assert('delta' in quote, 'Delta price not found in Quote'); + + const staticDeltaPrice: typeof quote.delta = { + ...quote.delta, + destAmount: 'dynamic_number', + destAmountBeforeFee: 'dynamic_number', + srcUSD: 'dynamic_number', + destUSD: 'dynamic_number', + destUSDBeforeFee: 'dynamic_number', + gasCost: 'dynamic_number', + gasCostBeforeFee: 'dynamic_number', + gasCostUSD: 'dynamic_number', + gasCostUSDBeforeFee: 'dynamic_number', + }; + + expect(staticDeltaPrice).toMatchInlineSnapshot(` + { + "destAmount": "dynamic_number", + "destAmountBeforeFee": "dynamic_number", + "destToken": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "destUSD": "dynamic_number", + "destUSDBeforeFee": "dynamic_number", + "gasCost": "dynamic_number", + "gasCostBeforeFee": "dynamic_number", + "gasCostUSD": "dynamic_number", + "gasCostUSDBeforeFee": "dynamic_number", + "partner": "anon", + "partnerFee": 0, + "srcAmount": "100000000000", + "srcToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "srcUSD": "dynamic_number", + } + `); + }); + + test('Fail Quote for delta for small amounts', async () => { + const quotePromise = quoteSDK.getQuote({ + srcToken: USDC, + destToken: ETH, + amount: (+amount / 1e6).toFixed(0), + srcDecimals: 18, + destDecimals: 18, + mode: 'delta', + side: 'SELL', + }); + + await expect(quotePromise).rejects.toThrowErrorMatchingInlineSnapshot( + `"Bad Request"` + ); + + const error = await quotePromise.catch((e) => e); + + assert(isFetcherError(error), 'Error should be a FetchError'); + const { details, errorType } = error.response?.data; + + expect({ details, errorType }).toMatchInlineSnapshot(` + { + "details": "Gas cost exceeds trade amount", + "errorType": "GasCostExceedsTradeAmount", + } + `); + }); + + test('Fail to Get Quote for delta with Native Token', async () => { + const quotePromise = quoteSDK.getQuote({ + srcToken: ETH, + destToken: USDC, + amount, + srcDecimals: 18, + destDecimals: 18, + mode: 'delta', + side: 'SELL', + }); + + await expect(quotePromise).rejects.toThrowErrorMatchingInlineSnapshot( + `"Bad Request"` + ); + + const error = await quotePromise.catch((e) => e); + + assert(isFetcherError(error), 'Error should be a FetchError'); + const { details, errorType } = error.response?.data; + + expect({ details, errorType }).toMatchInlineSnapshot(` + { + "details": "ETH as source token is not supported", + "errorType": "SourceEth", + } + `); + }); + + test('Fail to Get Quote for delta for BUY', async () => { + const quotePromise = quoteSDK.getQuote({ + srcToken: USDC, + destToken: ETH, + amount, + srcDecimals: 18, + destDecimals: 18, + mode: 'delta', + side: 'BUY', + }); + + await expect(quotePromise).rejects.toThrowErrorMatchingInlineSnapshot( + `"Bad Request"` + ); + + const error = await quotePromise.catch((e) => e); + + assert(isFetcherError(error), 'Error should be a FetchError'); + const { details, errorType } = error.response?.data; + + expect({ details, errorType }).toMatchInlineSnapshot(` + { + "details": "BUY is not supported", + "errorType": "UnsupportedSide", + } + `); + }); + + test('Get Quote for market', async () => { + const quote = await quoteSDK.getQuote({ + srcToken: USDC, + destToken: ETH, + amount, + srcDecimals: 18, + destDecimals: 18, + mode: 'market', + side: 'SELL', + }); + + expect(quote.market).toBeDefined(); + const priceRoute = quote.market; + + const bestRouteStable = priceRoute.bestRoute.map((b) => ({ + ...b, + percentage: 'dynamic_number', + swaps: 'dynamic_array', + })); + + const priceRouteStable = { + ...priceRoute, + gasCost: 'dynamic_number', + gasCostUSD: 'dynamic_number', + hmac: 'dynamic_number', + destAmount: 'dynamic_number', + blockNumber: 'dynamic_number', + srcUSD: 'dynamic_number', + destUSD: 'dynamic_number', + contractMethod: 'dynamic_string', + bestRoute: bestRouteStable, + }; + + expect(priceRouteStable).toMatchSnapshot(); + }); + + test('Get Delta Quote for all', async () => { + const quote = await quoteSDK.getQuote({ + srcToken: USDC, + destToken: ETH, + amount, + srcDecimals: 18, + destDecimals: 18, + mode: 'all', + side: 'SELL', + }); + + expect('delta' in quote).toBeTruthy(); + assert('delta' in quote, 'Delta price not found in Quote'); + + const staticDeltaPrice: typeof quote.delta = { + ...quote.delta, + destAmount: 'dynamic_number', + destAmountBeforeFee: 'dynamic_number', + srcUSD: 'dynamic_number', + destUSD: 'dynamic_number', + destUSDBeforeFee: 'dynamic_number', + gasCost: 'dynamic_number', + gasCostBeforeFee: 'dynamic_number', + gasCostUSD: 'dynamic_number', + gasCostUSDBeforeFee: 'dynamic_number', + }; + + expect(staticDeltaPrice).toMatchInlineSnapshot(` + { + "destAmount": "dynamic_number", + "destAmountBeforeFee": "dynamic_number", + "destToken": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "destUSD": "dynamic_number", + "destUSDBeforeFee": "dynamic_number", + "gasCost": "dynamic_number", + "gasCostBeforeFee": "dynamic_number", + "gasCostUSD": "dynamic_number", + "gasCostUSDBeforeFee": "dynamic_number", + "partner": "anon", + "partnerFee": 0, + "srcAmount": "100000000000", + "srcToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "srcUSD": "dynamic_number", + } + `); + }); + + test('Get Fallback Market Quote for all', async () => { + const quote = await quoteSDK.getQuote({ + srcToken: USDC, + destToken: ETH, + amount: (10e6).toString(), + srcDecimals: 18, + destDecimals: 18, + mode: 'all', + side: 'SELL', + }); + + assert(!('delta' in quote), 'Delta price not found in quote'); + + expect(quote.fallbackReason).toMatchInlineSnapshot(` + { + "details": "Gas cost exceeds trade amount", + "errorType": "GasCostExceedsTradeAmount", + } + `); + + const priceRoute = quote.market; + + const bestRouteStable = priceRoute.bestRoute.map((b) => ({ + ...b, + swaps: b.swaps.map((s) => ({ + ...s, + swapExchanges: s.swapExchanges.map((se) => ({ + ...se, + exchange: 'dynamic_string', + destAmount: 'dynamic_number', + data: 'largerly dynamic object', + poolAddresses: 'dynamic_array', + })), + })), + })); + + const priceRouteStable = { + ...priceRoute, + gasCost: 'dynamic_number', + gasCostUSD: 'dynamic_number', + hmac: 'dynamic_number', + destAmount: 'dynamic_number', + blockNumber: 'dynamic_number', + srcUSD: 'dynamic_number', + destUSD: 'dynamic_number', + bestRoute: bestRouteStable, + }; + + expect(priceRouteStable).toMatchSnapshot(); + }); +});