diff --git a/package.json b/package.json index 3c6486f..2cfd3b4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "typescript": "^5.7.3" }, "dependencies": { - "@cowprotocol/cow-sdk": "6.0.0-RC.7", + "@cowprotocol/cow-sdk": "6.0.0-RC.19", "axios": "^1.7.9", "dotenv": "^16.4.5", "ethers": "^5.7.2", diff --git a/src/index.ts b/src/index.ts index 31edc8d..9f06fb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,9 @@ import { run as approveTokenArbitrum } from "./scripts/arbitrum/approveTokenArbi import { run as swapAndBridgeAccrossArbitrum } from "./scripts/bridging/swapAndBridgeAccrossArbitrum"; import { run as swapAndBridgeAccrossMainnet } from "./scripts/bridging/swapAndBridgeAccrossMainnet"; import { run as swapAndBridgeSdk } from "./scripts/bridging/swapAndBridgeSdk"; +import { run as getOrderbookQuote } from "./orderbook/getQuote"; +import { run as getOrderbookQuoteWithAppData } from "./orderbook/getQuoteWithAppData"; +import { run as getTradingQuote } from "./scripts/trading/getQuote"; dotenv.config(); // Just to dev things easily using watch-mode :) @@ -63,10 +66,13 @@ const JOBS: (() => Promise)[] = [ // swapAndBridgeSwapsIo, // approveTokenArbitrum, - swapAndBridgeAccrossArbitrum, + // swapAndBridgeAccrossArbitrum, // swapAndBridgeAccrossMainnet, - // swapAndBridgeSdk, + swapAndBridgeSdk, + // getOrderbookQuote, + // getOrderbookQuoteWithAppData, + // getTradingQuote, ]; async function main() { diff --git a/src/orderbook/getQuote.ts b/src/orderbook/getQuote.ts new file mode 100644 index 0000000..2964651 --- /dev/null +++ b/src/orderbook/getQuote.ts @@ -0,0 +1,30 @@ +import { + OrderBookApi, + OrderQuoteSideKindSell, + PriceQuality, + SigningScheme, + SupportedChainId, + enableLogging, +} from "@cowprotocol/cow-sdk"; + +export async function run() { + const orderBookApi = new OrderBookApi({ + chainId: SupportedChainId.ARBITRUM_ONE, + }); + + enableLogging(true); + + const quote = await orderBookApi.getQuote({ + kind: OrderQuoteSideKindSell.SELL, + from: "0x016f34D4f2578c3e9DFfC3f2b811Ba30c0c9e7f3", + sellToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + buyToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + receiver: "0x016f34D4f2578c3e9DFfC3f2b811Ba30c0c9e7f3", + validFor: 1800, + priceQuality: PriceQuality.OPTIMAL, + signingScheme: SigningScheme.EIP712, + sellAmountBeforeFee: "5000000", + }); + + console.log("quote result", quote); +} diff --git a/src/orderbook/getQuoteWithAppData.ts b/src/orderbook/getQuoteWithAppData.ts new file mode 100644 index 0000000..b52605a --- /dev/null +++ b/src/orderbook/getQuoteWithAppData.ts @@ -0,0 +1,46 @@ +import { + OrderBookApi, + OrderQuoteSideKindSell, + PriceQuality, + SigningScheme, + SupportedChainId, + buildAppData, + generateAppDataFromDoc, +} from "@cowprotocol/cow-sdk"; +import { APP_CODE } from "../const"; + +export async function run() { + const orderBookApi = new OrderBookApi({ + chainId: SupportedChainId.ARBITRUM_ONE, + }); + + const kind = OrderQuoteSideKindSell.SELL; + + // const appData = await buildAppData({ + // appCode: APP_CODE, + // orderClass: "market", + // slippageBps: 100, + // partnerFee: { + // bps: 50, + // recipient: "0x016f34D4f2578c3e9DFfC3f2b811Ba30c0c9e7f3", + // }, + // }); + + const quote = await orderBookApi.getQuote({ + kind: OrderQuoteSideKindSell.SELL, + from: "0x016f34D4f2578c3e9DFfC3f2b811Ba30c0c9e7f3", + sellToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + buyToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + receiver: "0x016f34D4f2578c3e9DFfC3f2b811Ba30c0c9e7f3", + validFor: 1800, + priceQuality: PriceQuality.OPTIMAL, + signingScheme: SigningScheme.EIP712, + sellAmountBeforeFee: "5000000", + appData: + '{"appCode":"swap-n-bridge","metadata":{"hooks":{"post":[{"callData":"0x00","gasLimit":"110000","target":"0x0000000000000000000000000000000000000000"}]}},"version":"1.3.0"}', + appDataHash: + "0xc23b1518b9946714c0d396392a308bbbc5cf3a19a0bcace01ef9a8d222ca5f95", + }); + + console.log("quote result", quote); +} diff --git a/src/scripts/bridging/swapAndBridgeSdk.ts b/src/scripts/bridging/swapAndBridgeSdk.ts new file mode 100644 index 0000000..60dd34f --- /dev/null +++ b/src/scripts/bridging/swapAndBridgeSdk.ts @@ -0,0 +1,270 @@ +import { base, arbitrum, APP_CODE } from "../../const"; + +import { + SupportedChainId, + BridgingSdk, + AcrossBridgeProvider, + QuoteBridgeRequest, + OrderKind, + AccountAddress, + assertIsBridgeQuoteAndPost, + getChainInfo, + BridgeQuoteAndPost, +} from "@cowprotocol/cow-sdk"; + +import { ethers } from "ethers"; + +import { confirm, getRpcProvider, getWallet, jsonReplacer } from "../../utils"; +import { getErc20Contract } from "../../contracts/erc20"; + +export async function run() { + // Sell token (USDC in Arbitrum) + const sellTokenChainId = SupportedChainId.ARBITRUM_ONE; + const sellTokenAddress = arbitrum.USDC_ADDRESS; + const sellTokenDecimals = 6; + + // Buy token (WETH in Base) + const buyTokenChainId = SupportedChainId.BASE; + const buyTokenAddress = base.WETH_ADDRESS; + const buyTokenDecimals = 18; + + // Amount to sell + const sellAmount = ethers.utils.parseUnits("5", sellTokenDecimals).toBigInt(); + + // Get wallet + const wallet = await getWallet(sellTokenChainId); + + // Initialize the Across bridge provider + const acrossBridgeProvider = new AcrossBridgeProvider(); + + // Initialize the SDK with the wallet + const sdk = new BridgingSdk({ + providers: [acrossBridgeProvider], + enableLogging: true, + // tradingSdk: new TradingSdk({}, { enableLogging: true }), + }); + + const parameters: QuoteBridgeRequest = { + kind: OrderKind.SELL, + + sellTokenChainId, + sellTokenAddress, + sellTokenDecimals, + + buyTokenChainId, + buyTokenAddress, + buyTokenDecimals, + + amount: sellAmount, + + // TODO: Sort this mess + receiver: wallet.address, + signer: wallet, + account: wallet.address as AccountAddress, // FIXME: Why is this needed + + partiallyFillable: false, + slippageBps: 50, + appCode: APP_CODE, + }; + + const { signer, ...restParameters } = parameters; + console.log( + "đŸ•Ŗ Getting quote...", + JSON.stringify(restParameters, jsonReplacer, 2) + ); + + const quote = await sdk.getQuote(parameters); + assertIsBridgeQuoteAndPost(quote); + + // Get the symbols for the tokens + const sourceChainProvider = await getRpcProvider(parameters.sellTokenChainId); + const targetChainProvider = await getRpcProvider(parameters.buyTokenChainId); + const sellTokenSymbol = await getErc20Contract( + quote.swap.tradeParameters.sellToken, + sourceChainProvider + ).symbol(); + const intermediateSymbol = await getErc20Contract( + quote.swap.tradeParameters.buyToken, + sourceChainProvider + ).symbol(); + const buyTokenSymbol = await getErc20Contract( + quote.bridge.tradeParameters.buyTokenAddress, + targetChainProvider + ).symbol(); + + // Print the quote + const quoteString = await formatQuote({ + parameters, + quote, + sellTokenSymbol, + intermediateSymbol, + buyTokenSymbol, + }); + console.log(quoteString); + + const sellAmountFormatted = formatAmount( + sellAmount, + sellTokenDecimals, + sellTokenSymbol + ); + const minReceiveBuyToken = formatAmount( + quote.bridge.amountsAndCosts.afterSlippage.buyAmount, + buyTokenDecimals, + buyTokenSymbol + ); + + const confirmed = await confirm( + `Sell ${sellAmountFormatted} USDC (Arbitrum) for receive at least ${minReceiveBuyToken} WETH (Base). ok?` + ); + if (!confirmed) { + console.log("đŸšĢ Aborted"); + return; + } + + // Post the order + const orderId = await quote.postSwapOrderFromQuote(); + + // Print the order creation + console.log( + `â„šī¸ Order created, id: https://explorer.cow.fi/orders/${orderId}?tab=overview` + ); + + // Wait for the bridge start + console.log("đŸ•Ŗ Waiting for the bridge to start..."); + console.log("🔗 Across link: "); + // TODO: Implement + + // Wait for the bridging to be completed + console.log("đŸ•Ŗ Waiting for the bridging to be completed..."); + // TODO: Implement + + console.log(`🎉 The WETH is now waiting for you in Base`); +} + +async function formatQuote(params: { + parameters: QuoteBridgeRequest; + quote: BridgeQuoteAndPost; + sellTokenSymbol: string; + intermediateSymbol: string; + buyTokenSymbol: string; +}): Promise { + const { + parameters, + quote, + sellTokenSymbol, + intermediateSymbol, + buyTokenSymbol, + } = params; + const { bridge, swap } = quote; + + const swapAmount = swap.amountsAndCosts; + const bridgeAmount = bridge.amountsAndCosts; + + return ` +Swap details: + - Trader: ${parameters.account} + - Sell + - Chain: ${formatChainId(parameters.sellTokenChainId)} + - Token: ${formatToken( + swap.tradeParameters.sellToken, + swap.tradeParameters.sellTokenDecimals, + sellTokenSymbol + )} + - Amount: ${formatAmount( + swapAmount.afterNetworkCosts.sellAmount, + swap.tradeParameters.sellTokenDecimals, + sellTokenSymbol + )} + - Buy (intermediate token) + - Chain: ${formatChainId(parameters.sellTokenChainId)} + - Token: ${formatToken( + swap.tradeParameters.buyToken, + swap.tradeParameters.buyTokenDecimals, + intermediateSymbol + )} + - Est. Receive: ${formatAmount( + swapAmount.afterPartnerFees.buyAmount, + swap.tradeParameters.buyTokenDecimals, + intermediateSymbol + )} + - Min. Receive: ${formatAmount( + swapAmount.afterSlippage.buyAmount, + swap.tradeParameters.buyTokenDecimals, + intermediateSymbol + )} + - Network fee: + - in sell token: ${formatAmount( + swapAmount.costs.networkFee.amountInSellCurrency, + swap.tradeParameters.sellTokenDecimals, + sellTokenSymbol + )} + - in buy token: ${formatAmount( + swapAmount.costs.networkFee.amountInBuyCurrency, + swap.tradeParameters.buyTokenDecimals, + intermediateSymbol + )} + - Slippage: ${swap.tradeParameters.slippageBps ?? "default"} + - Receiver (cow-shed): ${swap.tradeParameters.receiver} + +Bridge details: + - Bridge provider: + - Name: ${bridge.providerInfo.name} + - Logo: ${bridge.providerInfo.logoUrl} + - Bridged token (intermediate token) + - Chain: ${formatChainId(bridge.tradeParameters.sellTokenChainId)} + - Token: ${formatToken( + bridge.tradeParameters.sellTokenAddress, + bridge.tradeParameters.sellTokenDecimals, + intermediateSymbol + )} + - Amount: ${formatAmount( + bridgeAmount.beforeFee.sellAmount, + bridge.tradeParameters.sellTokenDecimals, + intermediateSymbol + )} + - Buy: + - Chain: ${formatChainId(bridge.tradeParameters.buyTokenChainId)} + - Token: ${formatToken( + bridge.tradeParameters.buyTokenAddress, + bridge.tradeParameters.buyTokenDecimals, + buyTokenSymbol + )} + - Est. Receive (buy token): ${formatAmount( + bridgeAmount.afterFee.buyAmount, + bridge.tradeParameters.buyTokenDecimals, + buyTokenSymbol + )} + - Min. Receive (buy token): ${formatAmount( + bridgeAmount.afterSlippage.buyAmount, + bridge.tradeParameters.buyTokenDecimals, + buyTokenSymbol + )} + - Bridging fee: + - Fee: ${bridgeAmount.costs.bridgingFee.feeBps} BPS + - in sell token: ${formatAmount( + bridgeAmount.costs.bridgingFee.amountInSellCurrency, + bridge.tradeParameters.sellTokenDecimals, + intermediateSymbol + )} + - in buy token: ${formatAmount( + bridgeAmount.costs.bridgingFee.amountInBuyCurrency, + bridge.tradeParameters.buyTokenDecimals, + buyTokenSymbol + )} + - Slippage: ${bridgeAmount.slippageBps} + - Recipient (trader): ${bridge.tradeParameters.receiver} +`; +} + +function formatChainId(chainId: number) { + const chainInfo = getChainInfo(chainId); + return `${chainInfo ? chainInfo.label : "UNKNOWN CHAIN"} (${chainId})`; +} + +function formatToken(address: string, decimals: number, symbol: string) { + return `${symbol} (${address}, decimals: ${decimals})`; +} + +function formatAmount(amount: bigint, decimals: number, symbol: string) { + return `${ethers.utils.formatUnits(amount, decimals)} ${symbol} (${amount})`; +} diff --git a/src/scripts/trading/getQuote.ts b/src/scripts/trading/getQuote.ts new file mode 100644 index 0000000..5792e05 --- /dev/null +++ b/src/scripts/trading/getQuote.ts @@ -0,0 +1,29 @@ +import { base, arbitrum, APP_CODE } from "../../const"; + +import { SupportedChainId, OrderKind, TradingSdk } from "@cowprotocol/cow-sdk"; +import { ethers } from "ethers"; + +import { getWallet, jsonReplacer } from "../../utils"; + +export async function run() { + // Get wallet + const sellTokenChainId = SupportedChainId.ARBITRUM_ONE; + const wallet = await getWallet(sellTokenChainId); + const sellTokenDecimals = 6; + + // Initialize the SDK with the wallet + const sdk = new TradingSdk({ signer: wallet, appCode: APP_CODE }); + const quote = await sdk.getQuote({ + kind: OrderKind.SELL, + chainId: sellTokenChainId, + sellToken: arbitrum.USDC_ADDRESS, + sellTokenDecimals: 6, + buyToken: arbitrum.USDT_ADDRESS, + buyTokenDecimals: 6, + receiver: wallet.address, + amount: ethers.utils.parseUnits("5", sellTokenDecimals).toString(), + validFor: 1800, + }); + + console.log(`🎉 quote`, JSON.stringify(quote, jsonReplacer, 2)); +} diff --git a/src/utils.ts b/src/utils.ts index 47132da..56bd5a8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,20 @@ -import { QuoteResults, SupportedChainId } from "@cowprotocol/cow-sdk"; +import { + QuoteResults, + SupportedChainId, + TargetChainId, +} from "@cowprotocol/cow-sdk"; import { ethers } from "ethers"; import inquirer from "inquirer"; -export async function getWallet(chainId: SupportedChainId) { +const PROVIDERS_CACHE: Partial< + Record +> = {}; + +export async function getRpcProvider(chainId: TargetChainId) { + if (PROVIDERS_CACHE[chainId]) { + return PROVIDERS_CACHE[chainId]; + } + const envName = `RPC_URL_${chainId}`; const rpcUrl = process.env[envName]; if (!rpcUrl) { @@ -11,8 +23,9 @@ export async function getWallet(chainId: SupportedChainId) { ); } - // Make sure the specified provider is for the correct chain const provider = new ethers.providers.JsonRpcProvider(rpcUrl); + + // Make sure the specified provider is for the correct chain const { chainId: providerChainId, name: providerName } = await provider.getNetwork(); @@ -22,7 +35,12 @@ export async function getWallet(chainId: SupportedChainId) { ); } - return new ethers.Wallet(getPk(), provider); + PROVIDERS_CACHE[chainId] = provider; + return provider; +} + +export async function getWallet(chainId: SupportedChainId) { + return new ethers.Wallet(getPk(), await getRpcProvider(chainId)); } export function getPk() { diff --git a/yarn.lock b/yarn.lock index b7d6b65..e3c3028 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42,10 +42,10 @@ resolved "https://registry.npmjs.org/@cowprotocol/contracts/-/contracts-1.7.0.tgz" integrity sha512-tog/0igLaWZv6kK30+aFJ267+yNyMINx6qDHaJQFKZs051XUXnzNFlZgCEfICwDz/821XYjZxOnNbe449tgD5w== -"@cowprotocol/cow-sdk@6.0.0-RC.7": - version "6.0.0-RC.7" - resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-6.0.0-RC.7.tgz#175666db8f2187b02aa0cd32a073e7cbe5c6c34a" - integrity sha512-Vd89n+NjWvqzY1uNwcIboy+C/xtlAcwkTt6dRSZ2ocLBDT7Hy3frrJbbuJZ6gaqnnfJHEjP8slRMTkLhtZyAsA== +"@cowprotocol/cow-sdk@6.0.0-RC.19": + version "6.0.0-RC.19" + resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-6.0.0-RC.19.tgz#20c1b1d738d1b86ee06041c54e856a63b364be37" + integrity sha512-3pUGyCp3afMuyKmmG9X6n66/nnxqbNFsj3XFJCFARl2R1wTjw6dP8ta8cBfQx6HYr/ySkFOEQOUFJbuHv7ZkyA== dependencies: "@cowprotocol/app-data" "^2.4.0" "@cowprotocol/contracts" "^1.6.0"