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 00000000..cdf08a6e Binary files /dev/null and b/docs/passed_tests.png differ 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(); + }); +});