diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/README.md b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/README.md index b620c6e..60dd4da 100644 --- a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/README.md +++ b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/README.md @@ -1,131 +1,204 @@ --- -title: Eth-Flow +title: Creating order --- -There are two types of wallets: - - EOA (externally owned account) wallets, which are controlled by a private key - - Smart contract wallets, which are controlled by a smart contract +[Eth-flow](https://beta.docs.cow.fi/cow-protocol/reference/contracts/periphery/eth-flow) allows users to create orders selling `ETH` without wrapping it to `WETH` first. -Since smart contract wallets are controlled by a smart contract, they can't sign transactions using `EIP-712`. -Anyway, CoW Protocol supports smart contract wallets by allowing them to create pre-signed orders. -The key of pre-signed orders is a transaction to Settlement contract that proves the ownership of an order. +To create an `Eth-flow` order we will need to interact with the `Eth-Flow` contract. -In this example we will create a pre-signed order using Safe, but you can use any smart contract wallet. +## Contract (`CoWSwapEthFlow`) interaction -## Required dependencies +For interacting with contracts, the tutorials use [ethers.js](https://docs.ethers.io/v5/). -For pre-signed orders, we need to use: - - `OrderBookApi` to send an order to the CoW Protocol order-book - - `MetadataApi` to generate order meta data - - `Safe` to create and sign the transaction to the Settlement contract - - `SafeApiKit` to propose the transaction to Safe owners +To interact with a contract, we need to know: -## Define the order parameters +- the contract address +- the ABI -First of all, we should define the order parameters. -The description of each parameter can be found in the [Order structure docs.](https://TODO) +Additionally, as we want to **make a transaction**, we must have a _signer_ (e.g. a wallet). -``` -const defaultOrder: UnsignedOrder = { - receiver: safeAddress, - buyAmount: '650942340000000000000', - buyToken: '0x91056D4A53E1faa1A84306D4deAEc71085394bC8', - sellAmount: '100000000000000000', - sellToken: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', - validTo: Math.round((Date.now() + 900_000) / 1000), - appData: '0x', - feeAmount: '0', - kind: OrderKind.SELL, - partiallyFillable: true, - +++signingScheme: SigningScheme.PRESIGN+++ +### Contract address + +`EthFlow` is a periphery contract, and it's deployed on each supported network. As `EthFlow` orders are natively indexed by the [autopilot](https://beta.docs.cow.fi/cow-protocol/tutorials/arbitrate/autopilot), there also exists a `production` and `staging` version of the contract on each network. + +For this tutorial, we will use the [`production` version of the contract](https://gnosisscan.io/address/0x40A50cf069e992AA4536211B23F286eF88752187) on Gnosis chain. Let's assign it's address to a constant: + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers'; +import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { MetadataApi, latest } from '@cowprotocol/app-data'; + +export async function run(provider: Web3Provider): Promise { + // ... + const ethFlowAddress = '0x40A50cf069e992AA4536211B23F286eF88752187'; + // ... } ``` -> `signingScheme` is the only difference between a regular order and a pre-signed order +### Eth-flow ABI -## Order meta data +We can retrieve the ABI for the `Eth-flow` contract from the contract's verified code on [Gnosisscan](https://gnosisscan.io/address/0x40A50cf069e992AA4536211B23F286eF88752187#code). As we're going to be using other functions from the `Eth-flow` contract, we will just copy the whole ABI to an `ethFlow.abi.json` file. -For analytics purposes, we can add some metadata to the order. -The metadata is a JSON document that is stored in IPFS and referenced by the order. -The metadata is optional, but it's recommended to add it. -After order creation, the metadata will be displayed in [the Explorer](https://explorer.cow.fi/). +To do so: -``` -const appCode = '' -const environment = 'prod' - -// Slippage percent, it's 0 to 100 -const quote = { slippageBips: '50' } - -// "market" | "limit" | "liquidity" -const orderClass = { orderClass: 'limit' } - -// Generate the app-data document -const appDataDoc = await metadataApi.generateAppDataDoc({ - appCode, - environment, - metadata: { - quote, - orderClass - }, -}) - -const +++{ appDataHex, appDataContent }+++ = await metadataApi.appDataToCid(appDataDoc) -``` +1. Copy the ABI from [Gnosisscan](https://gnosisscan.io/address/0x40A50cf069e992AA4536211B23F286eF88752187#code) +2. Create a new file `ethFlow.abi.json` in the `src` folder +3. Paste the ABI into the file -## Post the order to the order-book +Now that we have the ABI, we can import it into our `run.ts` file: -Having the order and the metadata, we can post the order to the order-book. -`orderId` is the ID of the order in the order-book, and it's a key for the Settlement contract transaction. +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers'; +import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { MetadataApi, latest } from '@cowprotocol/app-data'; ++++import abi from './ethFlow.abi.json';+++ +export async function run(provider: Web3Provider): Promise { + // ... +} ``` -const orderCreation: OrderCreation = { - ...defaultOrder, - from: safeAddress, - signature: safeAddress, - appData: appDataContent, - appDataHash: appDataHex, + +### Connect to the contract + +Now that we have the contract address and the ABI, we can connect to the contract: + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers'; ++++import { Contract } from 'ethers';+++ +import { SupportedChainId } from '@cowprotocol/cow-sdk'; +import { MetadataApi, latest } from '@cowprotocol/app-data'; +import abi from './ethFlow.abi.json'; + +export async function run(provider: Web3Provider): Promise { + // ... + const ethFlowContract = new ethers.Contract(ethFlowAddress, abi, signer); + // ... } +``` -// Send order to CoW Protocol order-book -const +++orderId+++ = await orderBookApi.sendOrder(orderCreation) +> Ensure that you connect using the `signer` as we will be making a transaction. + +### Get a quote + +It's recommended to get a quote before creating an order (though it's not required). We've done this in a [previous tutorial](/tutorial/quote-order), and we will use this as a starting point. + +In this tutorial we are aiming to swap 1 `xDAI` for `COW` on Gnosis Chain. When getting a quote for Eth Flow orders, the `sellToken` should **always** be the wrapped native token (e.g. `WETH` on Ethereum, `WXDAI` on Gnosis Chain, etc.). Here we can get the wrapped native token address using the `wrappedNativeToken` function on the `EthFlow` contract. + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers'; ++++import {+++ ++++ SupportedChainId,+++ ++++ OrderBookApi,+++ ++++ UnsignedOrder,+++ ++++ SigningScheme,+++ ++++ OrderQuoteRequest,+++ ++++ OrderQuoteSideKindSell,+++ ++++} from '@cowprotocol/cow-sdk';+++ +import { MetadataApi, latest } from '@cowprotocol/app-data'; +import abi from './ethFlow.abi.json'; + +export async function run(provider: Web3Provider): Promise { + // ... + +++const orderBookApi = new OrderBookApi({ chainId: SupportedChainID.GNOSIS_CHAIN });+++ + const metadataApi = new MetadataApi(); + // ... + + +++const sellToken = await ethFlowContract.wrappedNativeToken();+++ + const buyToken = '0x177127622c4A00F3d409B75571e12cB3c8973d3c'; + const sellAmount = '1000000000000000000'; + + const quoteRequest: OrderQuoteRequest = { + sellToken, + buyToken, + sellAmountBeforeFee: sellAmount, + kind: OrderQuoteSideKindSell.SELL, + receiver: ownerAddress, + +++from: ownerAddress,+++ + appData: appDataContent, + appDataHash: appDataHex, + +++signingScheme: SigningScheme.EIP1271,+++ + +++onchainOrder: true,+++ + } + + +++const { quote, id: quoteId } = await orderBookApi.getQuote(quoteRequest);+++ +} ``` -## Create the transaction to the Settlement contract +In addition to the `OrderQuoteRequest` fields we've used in the previous tutorial, we've added: + +- `from`: the address of the order's owner (this is required for `EIP1271` orders) +- `signingScheme`: as Eth-flow orders are signed using the `EIP1271` scheme, we need to specify such a scheme +- `onchainOrder`: the order is going to be created on-chain, so we need to specify this -In the previous step, we created an order in the order-book, but it's not valid yet. -To make it valid, we need to create a transaction to the Settlement contract that proves the ownership of the order. +### `EthFlowOrder.Data` struct +Now that we have a quote, we can go about creating an `EthFlow` order. Unfortunately no `EthFlowOrder` type exists in the SDK, so we will need to create one ourselves. Simply we create a type that extends the `UnsignedOrder` type (removing the fields that are not required for [`EthFlow` orders](https://beta.docs.cow.fi/cow-protocol/reference/contracts/periphery/eth-flow#ethfloworderdata)) and add a `quoteId` field: + +```typescript +/// file: run.ts +// ... + ++++type EthFlowOrder = Omit & {+++ ++++ quoteId: number;+++ ++++}+++ + +export async function run(provider: Web3Provider): Promise { + // ... +} ``` -// Create the pre-signature transaction -const presignCallData = settlementContract.interface.encodeFunctionData(+++'setPreSignature'+++, [ - orderId, - true, -]) -const presignRawTx = { - to: settlementContract.address, - data: presignCallData, - value: '0', + +Now that we have the `EthFlowOrder` type, and we have a quote, we can create an order: + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers'; ++++import { BigNumber, Contract } from 'ethers';+++ +// ... +export async function run(provider: Web3Provider): Promise { + // ... + const order: EthFlowOrder = { + ...quote, + +++buyAmount: BigNumber.from(quote.buyAmount).mul(9950).div(10000).toString(),+++ + receiver: ownerAddress, + appData: appDataHex, + quoteId, + } } +``` + +> The `buyAmount` has a slippage of 0.5% applied to it. This is to ensure that it is filled if trading very low amounts of `xDAI` for this tutorial. The slippage amount is arbitrary and can be changed. + +> The `quoteId` is the `id` of the quote we received from the `OrderBookApi`, however this is **not required** for creating an `EthFlow` order and may be populated with any value. -// Send pre-signature transaction to settlement contract -// In this example we are using the Safe SDK, but you can use any other smart-contract wallet -const safeTx = await safeSdk.createTransaction({safeTransactionData: presignRawTx}) -const signedSafeTx = await safeSdk.signTransaction(safeTx) -const safeTxHash = await safeSdk.getTransactionHash(signedSafeTx) -const senderSignature = signedSafeTx.signatures.get(account.toLowerCase())?.data || '' - -// Send the pre-signed transaction to the Safe -await +++safeApiKit.proposeTransaction+++({ - safeAddress, - safeTransactionData: signedSafeTx.data, - safeTxHash, - senderAddress: account, - senderSignature, -}) +### Execute the `createOrder` + +Now we have everything for creating an order, we can execute the `createOrder`. Before we do however, unlike the previous tutorials, we will need to send some `ETH` for this transaction. We can do this by adding a `value` field to the `createOrder` call. The value should be **`sellAmount`** that was originally defined for the quote request. + +```typescript +/// file: run.ts +// ... +export async function run(provider: Web3Provider): Promise { + // ... + + +++const tx = await ethFlowContract.createOrder(order, { value: sellAmount });+++ + console.log('Transaction Hash:', tx.hash); + const receipt = await tx.wait(); + + return receipt; +} ``` -## Sign and execute transaction +## Run the code + +To run the code, we can press the "Run" button in the bottom right panel (the web container). + +When running the script, we may be asked to connect a wallet. We can use Rabby for this. -After the transaction is proposed to the Safe, the Safe owners should sign and execute it. -After the transaction is executed, the order will be valid and can be filled. +1. Accept the connection request in Rabby +2. Press the "Run" button again +3. Observe the `tx` object in the browser's console +4. On successful confirmation of the transaction, the `receipt` object will be returned to the output panel diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/GPv2Settlement.json b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/GPv2Settlement.json deleted file mode 100644 index 9a9e85e..0000000 --- a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/GPv2Settlement.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "bytes", - "name": "orderUid", - "type": "bytes" - }, - { - "internalType": "bool", - "name": "signed", - "type": "bool" - } - ], - "name": "setPreSignature", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } -] diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/const.ts b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/const.ts deleted file mode 100644 index ac9e158..0000000 --- a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/const.ts +++ /dev/null @@ -1,12 +0,0 @@ -import GPv2SettlementAbi from './GPv2Settlement.json' -import {SupportedChainId} from '@cowprotocol/cow-sdk' - -export const SETTLEMENT_CONTRACT_ADDRESS = '0x9008D19f58AAbD9eD0D60971565AA8510560ab41' - -export const SETTLEMENT_CONTRACT_ABI = GPv2SettlementAbi - -export const SAFE_TRANSACTION_SERVICE_URL: Record = { - [SupportedChainId.MAINNET]: 'https://safe-transaction-mainnet.safe.global', - [SupportedChainId.GNOSIS_CHAIN]: 'https://safe-transaction-gnosis-chain.safe.global', - [SupportedChainId.GOERLI]: 'https://safe-transaction-goerli.safe.global', -} diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/getSafeSdkAndKit.ts b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/getSafeSdkAndKit.ts deleted file mode 100644 index fde4c44..0000000 --- a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/getSafeSdkAndKit.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Safe, { EthersAdapter } from '@safe-global/protocol-kit' -import { ethers } from 'ethers' -import SafeApiKit from '@safe-global/api-kit' -import type { Web3Provider } from '@ethersproject/providers' -import {SAFE_TRANSACTION_SERVICE_URL} from './const' - -interface SafeSdkAndKit { - safeApiKit: SafeApiKit, - safeSdk: Safe -} - -export async function getSafeSdkAndKit(chainId: number, provider: Web3Provider, safeAddress: string): Promise { - const ethAdapter = new EthersAdapter({ - ethers, - signerOrProvider: provider.getSigner(0), - }) - const safeApiKit = new SafeApiKit({ - txServiceUrl: SAFE_TRANSACTION_SERVICE_URL[chainId], - ethAdapter - }) - - return Safe.create({ethAdapter, safeAddress}).then(safeSdk => { - return { - safeApiKit, - safeSdk - } - }) -} diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/run.ts b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/run.ts index 44616eb..5c24acb 100644 --- a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/run.ts +++ b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-a/src/lib/run.ts @@ -1,41 +1,34 @@ import type { Web3Provider } from '@ethersproject/providers' -import { Contract } from '@ethersproject/contracts' - -import { OrderBookApi, UnsignedOrder, OrderKind, SigningScheme } from '@cowprotocol/cow-sdk' -import { getSafeSdkAndKit } from './getSafeSdkAndKit' - -import { SETTLEMENT_CONTRACT_ABI, SETTLEMENT_CONTRACT_ADDRESS } from './const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { MetadataApi, latest } from '@cowprotocol/app-data' export async function run(provider: Web3Provider): Promise { - const safeAddress = '' - - // Get chainId and account from the current provider - const accounts = await provider.listAccounts() - const account = accounts[0] - const chainId = +(await provider.send('eth_chainId', [])) - - // Create the CoW Protocol OrderBookApi instance - const orderBookApi = new OrderBookApi({ chainId }) - - // Create the CoW Protocol Settlement contract instance - const settlementContract = new Contract(SETTLEMENT_CONTRACT_ADDRESS, SETTLEMENT_CONTRACT_ABI) - - // Create the Safe SDK and Safe API Kit instances - const {safeApiKit, safeSdk} = await getSafeSdkAndKit(chainId, provider, safeAddress) - - // Create the order - // Pay attention to the `signingScheme` field that is set to `SigningScheme.PRESIGN` - const defaultOrder: UnsignedOrder = { - receiver: safeAddress, - buyAmount: '650942340000000000000', - buyToken: '0x91056D4A53E1faa1A84306D4deAEc71085394bC8', - sellAmount: '100000000000000000', - sellToken: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', - validTo: Math.round((Date.now() + 900_000) / 1000), - appData: '0x', - feeAmount: '0', - kind: OrderKind.SELL, - partiallyFillable: true, - signingScheme: SigningScheme.PRESIGN, + const chainId = +(await provider.send('eth_chainId', [])); + if (chainId !== SupportedChainId.GNOSIS_CHAIN) { + await provider.send('wallet_switchEthereumChain', [{ chainId: SupportedChainId.GNOSIS_CHAIN }]); } + const metadataApi = new MetadataApi(); + + const appCode = 'Decentralized CoW' + const environment = 'production' + const referrer = { address: `0xcA771eda0c70aA7d053aB1B25004559B918FE662` } + const quoteAppDoc: latest.Quote = { slippageBips: '50' } + const orderClass: latest.OrderClass = { orderClass: 'market' } + + const appDataDoc = await metadataApi.generateAppDataDoc({ + appCode, + environment, + metadata: { + referrer, + quote: quoteAppDoc, + orderClass + }, + }) + + const { appDataHex, appDataContent } = await metadataApi.appDataToCid(appDataDoc) + + const signer = provider.getSigner(); + const ownerAddress = await signer.getAddress(); + + // TODO: Implement } diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-b/src/lib/ethFlow.abi.json b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-b/src/lib/ethFlow.abi.json new file mode 100644 index 0000000..92684d3 --- /dev/null +++ b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-b/src/lib/ethFlow.abi.json @@ -0,0 +1,499 @@ +[ + { + "inputs": [ + { + "internalType": "contract ICoWSwapSettlement", + "name": "_cowSwapSettlement", + "type": "address" + }, + { + "internalType": "contract IWrappedNativeToken", + "name": "_wrappedNativeToken", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "EthTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectEthAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "NotAllowedToInvalidateOrder", + "type": "error" + }, + { + "inputs": [], + "name": "NotAllowedZeroSellAmount", + "type": "error" + }, + { + "inputs": [], + "name": "OrderIsAlreadyExpired", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "OrderIsAlreadyOwned", + "type": "error" + }, + { + "inputs": [], + "name": "ReceiverMustBeSet", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + } + ], + "name": "OrderInvalidation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "sellToken", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "kind", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "bytes32", + "name": "sellTokenBalance", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "buyTokenBalance", + "type": "bytes32" + } + ], + "indexed": false, + "internalType": "struct GPv2Order.Data", + "name": "order", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "enum ICoWSwapOnchainOrders.OnchainSigningScheme", + "name": "scheme", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "indexed": false, + "internalType": "struct ICoWSwapOnchainOrders.OnchainSignature", + "name": "signature", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "OrderPlacement", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + }, + { + "indexed": true, + "internalType": "address", + "name": "refunder", + "type": "address" + } + ], + "name": "OrderRefund", + "type": "event" + }, + { + "inputs": [], + "name": "cowSwapSettlement", + "outputs": [ + { + "internalType": "contract ICoWSwapSettlement", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "int64", + "name": "quoteId", + "type": "int64" + } + ], + "internalType": "struct EthFlowOrder.Data", + "name": "order", + "type": "tuple" + } + ], + "name": "createOrder", + "outputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "int64", + "name": "quoteId", + "type": "int64" + } + ], + "internalType": "struct EthFlowOrder.Data", + "name": "order", + "type": "tuple" + } + ], + "name": "invalidateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "int64", + "name": "quoteId", + "type": "int64" + } + ], + "internalType": "struct EthFlowOrder.Data[]", + "name": "orderArray", + "type": "tuple[]" + } + ], + "name": "invalidateOrdersIgnoringNotAllowed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "isValidSignature", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "orders", + "outputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "unwrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "wrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wrapAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wrappedNativeToken", + "outputs": [ + { + "internalType": "contract IWrappedNativeToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-b/src/lib/run.ts b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-b/src/lib/run.ts index 224c7e2..80d3d5a 100644 --- a/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-b/src/lib/run.ts +++ b/content/tutorial/02-advanced-orders/01-eth-flow/01-create-eth-flow/app-b/src/lib/run.ts @@ -1,108 +1,82 @@ import type { Web3Provider } from '@ethersproject/providers' -import { Contract } from '@ethersproject/contracts' - -import { OrderBookApi, UnsignedOrder, OrderKind, SigningScheme, OrderCreation } from '@cowprotocol/cow-sdk' +import { BigNumber, Contract } from 'ethers'; +import { + SupportedChainId, + OrderBookApi, + UnsignedOrder, + SigningScheme, + OrderQuoteRequest, + OrderQuoteSideKindSell, +} from '@cowprotocol/cow-sdk' import { MetadataApi, latest } from '@cowprotocol/app-data' +import abi from './ethFlow.abi.json'; -import { getSafeSdkAndKit } from './getSafeSdkAndKit' -import { SETTLEMENT_CONTRACT_ABI, SETTLEMENT_CONTRACT_ADDRESS } from './const' +type EthFlowOrder = Omit & { + quoteId: number; +} export async function run(provider: Web3Provider): Promise { - const safeAddress = '' - const appCode = '' - const environment = 'prod' - - // Slippage percent, it's 0 to 100 - const quote = { slippageBips: '50' } - - // "market" | "limit" | "liquidity" - const orderClass: latest.OrderClass = { orderClass: 'limit' } - - // Get chainId and account from the current provider - const accounts = await provider.listAccounts() - const account = accounts[0] - const chainId = +(await provider.send('eth_chainId', [])) - - // CoW Protocol OrderBookApi instance - // It will be used to send the order to the order-book - const orderBookApi = new OrderBookApi({ chainId }) - - // Order creation requires meta information about the order - const metadataApi = new MetadataApi() - - // Create the CoW Protocol Settlement contract instance - const settlementContract = new Contract(SETTLEMENT_CONTRACT_ADDRESS, SETTLEMENT_CONTRACT_ABI) - - // Create the Safe SDK and Safe API Kit instances - const { safeApiKit, safeSdk } = await getSafeSdkAndKit(chainId, provider, safeAddress) - - // The order - // Pay attention to the `signingScheme` field that is set to `SigningScheme.PRESIGN` - const defaultOrder: UnsignedOrder = { - receiver: safeAddress, - buyAmount: '650942340000000000000', - buyToken: '0x91056D4A53E1faa1A84306D4deAEc71085394bC8', - sellAmount: '100000000000000000', - sellToken: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', - validTo: Math.round((Date.now() + 900_000) / 1000), - appData: '0x', - feeAmount: '0', - kind: OrderKind.SELL, - partiallyFillable: true, - signingScheme: SigningScheme.PRESIGN, + const chainId = +(await provider.send('eth_chainId', [])); + if (chainId !== SupportedChainId.GNOSIS_CHAIN) { + await provider.send('wallet_switchEthereumChain', [{ chainId: SupportedChainId.GNOSIS_CHAIN }]); } + const orderBookApi = new OrderBookApi({ chainId: SupportedChainId.GNOSIS_CHAIN }); + const metadataApi = new MetadataApi(); + + const appCode = 'Decentralized CoW' + const environment = 'production' + const referrer = { address: `0xcA771eda0c70aA7d053aB1B25004559B918FE662` } + const quoteAppDoc: latest.Quote = { slippageBips: '50' } + const orderClass: latest.OrderClass = { orderClass: 'market' } - // Generate the app-data document const appDataDoc = await metadataApi.generateAppDataDoc({ appCode, environment, metadata: { - quote, + referrer, + quote: quoteAppDoc, orderClass }, }) const { appDataHex, appDataContent } = await metadataApi.appDataToCid(appDataDoc) + const signer = provider.getSigner(); + const ownerAddress = await signer.getAddress(); + + const ethFlowAddress = '0x40A50cf069e992AA4536211B23F286eF88752187'; + const ethFlowContract = new Contract(ethFlowAddress, abi, signer); - // Add all necessary fields to the order creation request - const orderCreation: OrderCreation = { - ...defaultOrder, - from: safeAddress, - signature: safeAddress, + const sellToken = await ethFlowContract.wrappedNativeToken(); + const buyToken = '0x177127622c4A00F3d409B75571e12cB3c8973d3c'; // COW + const sellAmount = '1000000000000000000'; // 1 wxDAI + + const quoteRequest: OrderQuoteRequest = { + sellToken, + buyToken, + sellAmountBeforeFee: sellAmount, + kind: OrderQuoteSideKindSell.SELL, + receiver: ownerAddress, + from: ownerAddress, appData: appDataContent, appDataHash: appDataHex, + signingScheme: SigningScheme.EIP1271, + onchainOrder: true, } - // Send order to CoW Protocol order-book - const orderId = await orderBookApi.sendOrder(orderCreation) + const { quote, id: quoteId } = await orderBookApi.getQuote(quoteRequest); - // Create the pre-signature transaction - const presignCallData = settlementContract.interface.encodeFunctionData('setPreSignature', [ - orderId, - true, - ]) - const presignRawTx = { - to: settlementContract.address, - data: presignCallData, - value: '0', + const order: EthFlowOrder = { + ...quote, + buyAmount: BigNumber.from(quote.buyAmount).mul(9950).div(10000).toString(), + receiver: ownerAddress, + appData: appDataHex, + quoteId, } - // Send pre-signature transaction to settlement contract - // In this example we are using the Safe SDK, but you can use any other smart-contract wallet - const safeTx = await safeSdk.createTransaction({safeTransactionData: presignRawTx}) - const signedSafeTx = await safeSdk.signTransaction(safeTx) - const safeTxHash = await safeSdk.getTransactionHash(signedSafeTx) - const senderSignature = signedSafeTx.signatures.get(account.toLowerCase())?.data || '' - - // Send the pre-signed transaction to the Safe - await safeApiKit.proposeTransaction({ - safeAddress, - safeTransactionData: signedSafeTx.data, - safeTxHash, - senderAddress: account, - senderSignature, - }) + const tx = await ethFlowContract.createOrder(order, { value: sellAmount }); + console.log('Transaction Hash:', tx.hash); + const receipt = await tx.wait(); - return { orderId, safeTxHash, senderSignature } + return receipt; } diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/README.md b/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/README.md index f5536b7..084268c 100644 --- a/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/README.md +++ b/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/README.md @@ -2,4 +2,124 @@ title: Viewing status --- -Something \ No newline at end of file +One of the more difficult things to do with `Eth-flow` orders is to determine their status, as to do so we need to know the [`orderUid`](https://beta.docs.cow.fi/cow-protocol/reference/contracts/core/settlement#orderuid) of the order. + +In this tutorial, we will learn how to determine the `orderUid` of an `Eth-flow` order, and how to use it to determine the order's status. + +## Determining the `orderUid` + +Upon consulting the documentation's Technical reference on [`orderUid`](https://beta.docs.cow.fi/cow-protocol/reference/contracts/core/settlement#orderuid), we can see that the `orderUid` is composed of the following fields: + +- `digest`: The EIP-712 digest of the `GPv2Order.Data` struct +- `owner`: The address of the order's owner +- `expiry`: The order's expiry timestamp + +For the case of **all** Eth-flow orders, it is immediately apparent that: + +- The `owner` is the address of the Eth-flow contract (they are just simple `ERC-1271` orders signed by the Eth-flow contract) +- The `expiry` is the maximum possible value for a `uint32`, ie. `2^32 - 1` + +Therefore, we can create a function that takes an ABI-encoded `GPv2Order.Data` struct, and returns the corresponding Eth-flow `orderUid`: + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers' +import { utils } from "ethers"; +import { SupportedChainId } from '@cowprotocol/cow-sdk' ++++import { onchainOrderToHash } from "/src/lib/gpv2Order";+++ +import abi from './ethFlow.abi.json' + +export async function run(provider: Web3Provider): Promise { + // ... + + const ethFlowOrderUid = (onchainOrder: any) => { + const hash = onchainOrderToHash(onchainOrder, chainId); + return hash + ethFlowAddress.slice(2) + 'ffffffff'; + } + + // ... +} +``` + +> Unfortunately there are no functions exported (yet) from the `cow-sdk` that allow for computing the `EIP-712` digest of an **ABI-encoded** `GPv2Order.Data` struct. The `onchainOrderToHash` function is defined in the `gpv2Order.ts` file and usable for this tutorial. + +## Contract (`CoWSwapEthFlow`) `OrderPlacement` events + +For handling events from the smart contracts, the tutorials use [ethers.js](https://docs.ethers.io/v5/). + +To handle events, we need to know: + +- the ABI +- the contract address (optional, but recommended) + +In the case of this tutorial, we already have the ABI from the previous tutorial, and we can use the `ethFlowAddress` constant from the previous tutorial. + +### Get transaction receipt + +The `CoWSwapEthFlow` contract emits an `OrderPlacement` event whenever an order is created. This event log contains an `order` field, which is the `GPv2Order.Data` struct that we need to determine the `orderUid`. + +Let's use a known transaction hash to extract the `GPv2Order.Data` struct from the `OrderPlacement` event: + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers' +import { utils } from "ethers"; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import abi from './ethFlow.abi.json' + +export async function run(provider: Web3Provider): Promise { + // ... + + const txHash = '0x04d05fc2c953cc63608c19a79869301d62b1f077e0f795f716619b21f693f00c'; + const receipt = await provider.getTransactionReceipt(txHash); +} +``` + +### Process event logs + +Now that we have the transaction receipt, we can extract the `OrderPlacement` event logs from it: + +```typescript +/// file: run.ts +// ... + +export async function run(provider: Web3Provider): Promise { + // ... + + const ethFlowOrderUids: string[] = receipt.logs + .reduce((orderIds, log) => { + if (log.address !== ethFlowAddress) { + return orderIds; + } + + const parsedLog = iface.parseLog(log); + if (parsedLog.name === 'OrderPlacement') { + const [, order, ,] = parsedLog.args; + + orderIds.push(ethFlowOrderUid(order)); + } + + return orderIds; + }, []); +} +``` + +Above we: + +- Filter out all logs that are not from the `ethFlowAddress` (i.e. this way we force that we don't accidentally look at logs from the other environment's `CoWSwapEthFlow` contract) +- Parse the log using the ABI +- Extract the `GPv2Order.Data` struct from the `data` field of the `OrderPlacement` event +- Compute the `orderUid` from the `GPv2Order.Data` struct using the `ethFlowOrderUid` function defined above +- Filter out any `undefined` values (i.e. logs that were not `OrderPlacement` events and/or were not from the `ethFlowAddress`) + +## Run the code + +To run the code, we can press the "Run" button in the bottom right panel (the web container). + +When running the script, we may be asked to connect a wallet. We can use Rabby for this. + +1. Accept the connection request in Rabby +2. Press the "Run" button again +3. Observe the calculated `orderUid` in the output panel + +Now that we have determined the `orderUid` of an Eth-flow order, we can simply look up the order's status using [CoW Explorer](https://explorer.cow.fi). diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/app-a/src/lib/ethFlow.abi.json b/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/app-a/src/lib/ethFlow.abi.json new file mode 100644 index 0000000..92684d3 --- /dev/null +++ b/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/app-a/src/lib/ethFlow.abi.json @@ -0,0 +1,499 @@ +[ + { + "inputs": [ + { + "internalType": "contract ICoWSwapSettlement", + "name": "_cowSwapSettlement", + "type": "address" + }, + { + "internalType": "contract IWrappedNativeToken", + "name": "_wrappedNativeToken", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "EthTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectEthAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "NotAllowedToInvalidateOrder", + "type": "error" + }, + { + "inputs": [], + "name": "NotAllowedZeroSellAmount", + "type": "error" + }, + { + "inputs": [], + "name": "OrderIsAlreadyExpired", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "OrderIsAlreadyOwned", + "type": "error" + }, + { + "inputs": [], + "name": "ReceiverMustBeSet", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + } + ], + "name": "OrderInvalidation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "sellToken", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "kind", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "bytes32", + "name": "sellTokenBalance", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "buyTokenBalance", + "type": "bytes32" + } + ], + "indexed": false, + "internalType": "struct GPv2Order.Data", + "name": "order", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "enum ICoWSwapOnchainOrders.OnchainSigningScheme", + "name": "scheme", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "indexed": false, + "internalType": "struct ICoWSwapOnchainOrders.OnchainSignature", + "name": "signature", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "OrderPlacement", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + }, + { + "indexed": true, + "internalType": "address", + "name": "refunder", + "type": "address" + } + ], + "name": "OrderRefund", + "type": "event" + }, + { + "inputs": [], + "name": "cowSwapSettlement", + "outputs": [ + { + "internalType": "contract ICoWSwapSettlement", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "int64", + "name": "quoteId", + "type": "int64" + } + ], + "internalType": "struct EthFlowOrder.Data", + "name": "order", + "type": "tuple" + } + ], + "name": "createOrder", + "outputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "int64", + "name": "quoteId", + "type": "int64" + } + ], + "internalType": "struct EthFlowOrder.Data", + "name": "order", + "type": "tuple" + } + ], + "name": "invalidateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "int64", + "name": "quoteId", + "type": "int64" + } + ], + "internalType": "struct EthFlowOrder.Data[]", + "name": "orderArray", + "type": "tuple[]" + } + ], + "name": "invalidateOrdersIgnoringNotAllowed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "isValidSignature", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "orders", + "outputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "unwrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "wrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wrapAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wrappedNativeToken", + "outputs": [ + { + "internalType": "contract IWrappedNativeToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/app-a/src/lib/run.ts b/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/app-a/src/lib/run.ts new file mode 100644 index 0000000..48a1204 --- /dev/null +++ b/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/app-a/src/lib/run.ts @@ -0,0 +1,16 @@ +import type { Web3Provider } from '@ethersproject/providers' +import { utils } from "ethers"; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import abi from './ethFlow.abi.json' + +export async function run(provider: Web3Provider): Promise { + const chainId = +(await provider.send('eth_chainId', [])); + if (chainId !== SupportedChainId.GNOSIS_CHAIN) { + await provider.send('wallet_switchEthereumChain', [{ chainId: 100 }]); + } + + const ethFlowAddress = '0x40A50cf069e992AA4536211B23F286eF88752187'; + const iface = new utils.Interface(abi); + + // TODO: Implement +} diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/app-b/src/lib/run.ts b/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/app-b/src/lib/run.ts new file mode 100644 index 0000000..f220fcf --- /dev/null +++ b/content/tutorial/02-advanced-orders/01-eth-flow/02-view-eth-flow/app-b/src/lib/run.ts @@ -0,0 +1,43 @@ +import type { Web3Provider } from '@ethersproject/providers' +import { utils } from "ethers"; +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { onchainOrderToHash } from "/src/lib/gpv2Order"; +import abi from './ethFlow.abi.json' + +export async function run(provider: Web3Provider): Promise { + const chainId = +(await provider.send('eth_chainId', [])); + if (chainId !== SupportedChainId.GNOSIS_CHAIN) { + await provider.send('wallet_switchEthereumChain', [{ chainId: 100 }]); + } + + const ethFlowAddress = '0x40A50cf069e992AA4536211B23F286eF88752187'; + const iface = new utils.Interface(abi); + + const ethFlowOrderUid = (onchainOrder: any) => { + const hash = onchainOrderToHash(onchainOrder, chainId); + return hash + ethFlowAddress.slice(2) + 'ffffffff'; + } + + const txHash = '0x1a1eb56678cf1936711df3de6e9ff02accef52808ecbd704a8547c62dcfb42f5'; + const receipt = await provider.getTransactionReceipt(txHash); + + const ethFlowOrderUids: string[] = receipt.logs + .reduce((orderIds, log) => { + if (log.address !== ethFlowAddress) { + return orderIds; + } + + const parsedLog = iface.parseLog(log); + if (parsedLog.name === 'OrderPlacement') { + const [, order, ,] = parsedLog.args; + + orderIds.push(ethFlowOrderUid(order)); + } + + return orderIds; + }, []); + + return { + ethFlowOrderUids, + } +} diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/README.md b/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/README.md index 29640d3..787be63 100644 --- a/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/README.md +++ b/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/README.md @@ -2,4 +2,179 @@ title: Cancelling order --- -Something \ No newline at end of file +In the previous tutorials we have created an Eth-flow order and showed how to view it. Let's say for some reason we want to cancel the order (like instead of buying 1 xDAI worth of COW, you now want to buy 200 xDAI worth of COW)! In this tutorial we will show how to cancel an order. + +In order to cancel an Eth-flow order, we need to pass to `invalidateOrder` the original parameter that was passed to `createOrder`. There are a couple of ways to do this: + +- Lookup the `calldata` of the transaction that created the order, and replace the function selector with the `invalidateOrder` function selector +- Lookup the logs of the transaction that created the order and reconstruct the `EthFlowOrder.Data` struct from the `OrderPlacement` event log + +We will use the second method in this tutorial. + +## Reconstructing the `EthFlowOrder.Data` struct + +From the [`EthFlowOrder.Data` struct](https://beta.docs.cow.fi/cow-protocol/reference/contracts/periphery/eth-flow#ethfloworderdata) technical reference, we can see that the struct's fields are a subset of the [`GPv2Order.Data` struct](https://beta.docs.cow.fi/cow-protocol/reference/contracts/core/settlement#gpv2orderdata-struct) and an additional `int64` field called `quoteId`. + +Fortunately, in the `OrderPlacement` event log, we can see that the `order` field is the `GPv2Order.Data` struct, and the `data` field is a solidity-packed tuple containing the `quoteId` field and the `validTo` field. This means that we can reconstruct the `EthFlowOrder.Data` struct from the `OrderPlacement` event log! + +### Extract `quoteId` from `data` field + +First let's write a helper function to extract the `quoteId` from the solidity packed `data` log field: + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers' ++++import { BigNumber, Contract, utils } from "ethers";+++ +import { SupportedChainId, UnsignedOrder } from '@cowprotocol/cow-sdk' +import { onchainOrderToHash } from '/src/lib/gpv2Order'; +import abi from './ethFlow.abi.json' + +// ... + +export async function run(provider: Web3Provider): Promise { + // ... + + const decodePackedData = (data: string) => { + // Ensure data is of correct length (8 bytes for int64 + 4 bytes for uint32) + if (data.length !== 2 * (8 + 4) + 2) { // +2 for '0x' + throw new Error('Invalid data length'); + } + + // Extract quoteId (int64) and validTo (uint32) from the data + const quoteIdBytes = data.slice(2, 2 + 16); // 8 bytes for int64 + const validToBytes = data.slice(2 + 16); // 4 bytes for uint32 + + // Convert hex strings to BigNumber + const quoteId = BigNumber.from('0x' + quoteIdBytes).toNumber(); + const validTo = BigNumber.from('0x' + validToBytes).toNumber(); + + return { quoteId, validTo }; + } + + // ... +``` + +As the `data` field is a solidity-packed tuple, the field lengths are fixed. Therefore we can extract the `quoteId` and `validTo` fields by slicing the `data` field into the correct lengths, and converting the hex strings to `BigNumber`s. + +### Reconstruct `EthFlowOrder.Data` struct + +Now that we have a way to extract the `quoteId` from the `data` field, we can define a function to reconstruct the `EthFlowOrder.Data` struct from the `OrderPlacement` event log: + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers' +import { BigNumber, Contract, utils } from "ethers"; +import { SupportedChainId, UnsignedOrder } from '@cowprotocol/cow-sdk' ++++import { onchainOrderToOrder, onchainOrderToHash } from '/src/lib/gpv2Order';+++ +import abi from './ethFlow.abi.json' + +// ... + +export async function run(provider: Web3Provider): Promise { + // ... + + const onchainEthFlowOrder = (onchainOrder: any, data: any): EthFlowOrder => { + const order = onchainOrderToOrder(onchainOrder); + const { quoteId } = decodePackedData(data); + + return { + buyToken: order.buyToken, + sellAmount: order.sellAmount.toString(), + buyAmount: order.buyAmount.toString(), + receiver: order.receiver, + appData: order.appData.toString(), + feeAmount: order.feeAmount.toString(), + partiallyFillable: order.partiallyFillable, + validTo: order.validTo.valueOf(), + quoteId, + } + } + + // ... +} +``` + +### Get transaction receipt + +Let's use a known transaction hash to extract the `OrderPlacement` event log: + +```typescript +/// file: run.ts +// ... +export async function run(provider: Web3Provider): Promise { + // ... + + const txHash = '0x04d05fc2c953cc63608c19a79869301d62b1f077e0f795f716619b21f693f00c'; + const receipt = await provider.getTransactionReceipt(txHash); + + // ... +} +``` + +> The transaction hash above is from the gnosis chain network. Attempting to cancel this order will result in a revert, as the order has already been cancelled. If you want to try this tutorial, [create an Eth-flow order](/tutorial/create-eth-flow) first. + +### Process event logs + +Now that we have the transaction receipt, we can extract the `OrderPlacement` event logs from it, reconstructing the `EthFlowOrder.Data` struct and cancelling the orders: + +```typescript +/// file: run.ts +// ... +export async function run(provider: Web3Provider): Promise { + // ... + + let cancelledOrders = []; + + for (const log of receipt.logs) { + if (log.address !== ethFlowAddress) { + continue; + } + + const parsedLog = iface.parseLog(log); + if (parsedLog.name === 'OrderPlacement') { + const [, onchainOrder, , data] = parsedLog.args; + const ethFlowOrder = onchainEthFlowOrder(onchainOrder, data); + + try { + const tx = await ethFlowContract.invalidateOrder(ethFlowOrder); + const receipt = await tx.wait(); + cancelledOrders.push({ + ethFlowOrderUid: ethFlowOrderUid(onchainOrder), + receipt, + }); + } catch (err) { + throw new Error(err); + } + } + } + + return { + cancelledOrders, + } +} +``` + +Above we: + +- Filter out all logs that are not from the `ethFlowAddress` (i.e. this way we force that we don't accidentally look at logs from the other environment's `CoWSwapEthFlow` contract) +- Parse the log using the ABI +- Extract the `GPv2Order.Data` struct and `data` field from the `OrderPlacement` event +- Reconstruct the `EthFlowOrder.Data` struct from the `OrderPlacement` event log +- Cancel the order using the `ethFlowContract.invalidateOrder` function +- Wait for the transaction to be confirmed +- Push the cancelled order to the `cancelledOrders` array + +## Run the code + +To run the code, we can press the "Run" button in the bottom right panel (the web container). + +When running the script, we may be asked to connect a wallet. We can use Rabby for this. + +1. Accept the connection request in Rabby +2. Press the "Run" button again +3. Confirm transactions in Rabby +4. Observe all orders being cancelled in the output panel + +### Errors + +You may receive some errors associated with reverts with `eth_estimateGas` calls. This is likely because the order has already been cancelled (such as the orders in the example above). \ No newline at end of file diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/app-a/src/lib/ethFlow.abi.json b/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/app-a/src/lib/ethFlow.abi.json new file mode 100644 index 0000000..92684d3 --- /dev/null +++ b/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/app-a/src/lib/ethFlow.abi.json @@ -0,0 +1,499 @@ +[ + { + "inputs": [ + { + "internalType": "contract ICoWSwapSettlement", + "name": "_cowSwapSettlement", + "type": "address" + }, + { + "internalType": "contract IWrappedNativeToken", + "name": "_wrappedNativeToken", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "EthTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectEthAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "NotAllowedToInvalidateOrder", + "type": "error" + }, + { + "inputs": [], + "name": "NotAllowedZeroSellAmount", + "type": "error" + }, + { + "inputs": [], + "name": "OrderIsAlreadyExpired", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "OrderIsAlreadyOwned", + "type": "error" + }, + { + "inputs": [], + "name": "ReceiverMustBeSet", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + } + ], + "name": "OrderInvalidation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "sellToken", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "kind", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "bytes32", + "name": "sellTokenBalance", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "buyTokenBalance", + "type": "bytes32" + } + ], + "indexed": false, + "internalType": "struct GPv2Order.Data", + "name": "order", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "enum ICoWSwapOnchainOrders.OnchainSigningScheme", + "name": "scheme", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "indexed": false, + "internalType": "struct ICoWSwapOnchainOrders.OnchainSignature", + "name": "signature", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "OrderPlacement", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + }, + { + "indexed": true, + "internalType": "address", + "name": "refunder", + "type": "address" + } + ], + "name": "OrderRefund", + "type": "event" + }, + { + "inputs": [], + "name": "cowSwapSettlement", + "outputs": [ + { + "internalType": "contract ICoWSwapSettlement", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "int64", + "name": "quoteId", + "type": "int64" + } + ], + "internalType": "struct EthFlowOrder.Data", + "name": "order", + "type": "tuple" + } + ], + "name": "createOrder", + "outputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "int64", + "name": "quoteId", + "type": "int64" + } + ], + "internalType": "struct EthFlowOrder.Data", + "name": "order", + "type": "tuple" + } + ], + "name": "invalidateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "partiallyFillable", + "type": "bool" + }, + { + "internalType": "int64", + "name": "quoteId", + "type": "int64" + } + ], + "internalType": "struct EthFlowOrder.Data[]", + "name": "orderArray", + "type": "tuple[]" + } + ], + "name": "invalidateOrdersIgnoringNotAllowed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "isValidSignature", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "orders", + "outputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "unwrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "wrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wrapAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wrappedNativeToken", + "outputs": [ + { + "internalType": "contract IWrappedNativeToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/app-a/src/lib/run.ts b/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/app-a/src/lib/run.ts new file mode 100644 index 0000000..480af0f --- /dev/null +++ b/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/app-a/src/lib/run.ts @@ -0,0 +1,30 @@ +import type { Web3Provider } from '@ethersproject/providers' +import { Contract, utils } from "ethers"; +import { SupportedChainId, UnsignedOrder } from '@cowprotocol/cow-sdk' +import { onchainOrderToHash } from '/src/lib/gpv2Order'; +import abi from './ethFlow.abi.json' + +type EthFlowOrder = Omit & { + quoteId: number; +} + +export async function run(provider: Web3Provider): Promise { + const chainId = +(await provider.send('eth_chainId', [])); + if (chainId !== SupportedChainId.GNOSIS_CHAIN) { + await provider.send('wallet_switchEthereumChain', [{ chainId: 100 }]); + } + + const signer = provider.getSigner(); + + const ethFlowAddress = '0x40A50cf069e992AA4536211B23F286eF88752187'; + const ethFlowContract = new Contract(ethFlowAddress, abi, signer); + + const iface = new utils.Interface(abi); + + const ethFlowOrderUid = (onchainOrder: any) => { + const hash = onchainOrderToHash(onchainOrder, chainId); + return hash + ethFlowAddress.slice(2) + 'ffffffff'; + } + + // TODO: Implement +} diff --git a/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/app-b/src/lib/run.ts b/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/app-b/src/lib/run.ts new file mode 100644 index 0000000..701c65d --- /dev/null +++ b/content/tutorial/02-advanced-orders/01-eth-flow/03-cancel-eth-flow/app-b/src/lib/run.ts @@ -0,0 +1,94 @@ +import type { Web3Provider } from '@ethersproject/providers' +import { BigNumber, Contract, utils } from "ethers"; +import { SupportedChainId, UnsignedOrder } from '@cowprotocol/cow-sdk' +import { onchainOrderToOrder, onchainOrderToHash } from '/src/lib/gpv2Order'; +import abi from './ethFlow.abi.json' + +type EthFlowOrder = Omit & { + quoteId: number; +} + +export async function run(provider: Web3Provider): Promise { + const chainId = +(await provider.send('eth_chainId', [])); + if (chainId !== SupportedChainId.GNOSIS_CHAIN) { + await provider.send('wallet_switchEthereumChain', [{ chainId: 100 }]); + } + + const signer = provider.getSigner(); + + const ethFlowAddress = '0x40A50cf069e992AA4536211B23F286eF88752187'; + const ethFlowContract = new Contract(ethFlowAddress, abi, signer); + + const iface = new utils.Interface(abi); + + const ethFlowOrderUid = (onchainOrder: any) => { + const hash = onchainOrderToHash(onchainOrder, chainId); + return hash + ethFlowAddress.slice(2) + 'ffffffff'; + } + + const decodePackedData = (data: string) => { + // Ensure data is of correct length (8 bytes for int64 + 4 bytes for uint32) + if (data.length !== 2 * (8 + 4) + 2) { // +2 for '0x' + throw new Error('Invalid data length'); + } + + // Extract quoteId (int64) and validTo (uint32) from the data + const quoteIdBytes = data.slice(2, 2 + 16); // 8 bytes for int64 + const validToBytes = data.slice(2 + 16); // 4 bytes for uint32 + + // Convert hex strings to BigNumber + const quoteId = BigNumber.from('0x' + quoteIdBytes).toNumber(); + const validTo = BigNumber.from('0x' + validToBytes).toNumber(); + + return { quoteId, validTo }; + } + + const onchainEthFlowOrder = (onchainOrder: any, data: any): EthFlowOrder => { + const order = onchainOrderToOrder(onchainOrder); + const { quoteId } = decodePackedData(data); + + return { + buyToken: order.buyToken, + sellAmount: order.sellAmount.toString(), + buyAmount: order.buyAmount.toString(), + receiver: order.receiver, + appData: order.appData.toString(), + feeAmount: order.feeAmount.toString(), + partiallyFillable: order.partiallyFillable, + validTo: order.validTo.valueOf(), + quoteId, + } + } + + const txHash = '0x04d05fc2c953cc63608c19a79869301d62b1f077e0f795f716619b21f693f00c'; + const receipt = await provider.getTransactionReceipt(txHash); + + let cancelledOrders = []; + + for (const log of receipt.logs) { + if (log.address !== ethFlowAddress) { + continue; + } + + const parsedLog = iface.parseLog(log); + if (parsedLog.name === 'OrderPlacement') { + const [, onchainOrder, , data] = parsedLog.args; + const ethFlowOrder = onchainEthFlowOrder(onchainOrder, data); + + try { + const tx = await ethFlowContract.invalidateOrder(ethFlowOrder); + const receipt = await tx.wait(); + cancelledOrders.push({ + ethFlowOrderUid: ethFlowOrderUid(onchainOrder), + receipt, + }); + } catch (err) { + throw new Error(err); + } + } + } + + return { + cancelledOrders, + } +} diff --git a/content/tutorial/02-advanced-orders/04-milkman/01-create-milkman/README.md b/content/tutorial/02-advanced-orders/04-erc1271/01-create-milkman/README.md similarity index 100% rename from content/tutorial/02-advanced-orders/04-milkman/01-create-milkman/README.md rename to content/tutorial/02-advanced-orders/04-erc1271/01-create-milkman/README.md diff --git a/content/tutorial/02-advanced-orders/04-milkman/02-bot-milkman/README.md b/content/tutorial/02-advanced-orders/04-erc1271/02-bot-milkman/README.md similarity index 100% rename from content/tutorial/02-advanced-orders/04-milkman/02-bot-milkman/README.md rename to content/tutorial/02-advanced-orders/04-erc1271/02-bot-milkman/README.md diff --git a/content/tutorial/02-advanced-orders/04-milkman/03-cancel-milkman/README.md b/content/tutorial/02-advanced-orders/04-erc1271/03-cancel-milkman/README.md similarity index 100% rename from content/tutorial/02-advanced-orders/04-milkman/03-cancel-milkman/README.md rename to content/tutorial/02-advanced-orders/04-erc1271/03-cancel-milkman/README.md diff --git a/content/tutorial/02-advanced-orders/04-milkman/04-triage-milkman/README.md b/content/tutorial/02-advanced-orders/04-erc1271/04-triage-milkman/README.md similarity index 100% rename from content/tutorial/02-advanced-orders/04-milkman/04-triage-milkman/README.md rename to content/tutorial/02-advanced-orders/04-erc1271/04-triage-milkman/README.md diff --git a/content/tutorial/02-advanced-orders/04-milkman/meta.json b/content/tutorial/02-advanced-orders/04-erc1271/meta.json similarity index 100% rename from content/tutorial/02-advanced-orders/04-milkman/meta.json rename to content/tutorial/02-advanced-orders/04-erc1271/meta.json diff --git a/content/tutorial/02-advanced-orders/05-milkman/01-create-milkman/README.md b/content/tutorial/02-advanced-orders/05-milkman/01-create-milkman/README.md new file mode 100644 index 0000000..330fd8c --- /dev/null +++ b/content/tutorial/02-advanced-orders/05-milkman/01-create-milkman/README.md @@ -0,0 +1,5 @@ +--- +title: Create order +--- + +Something \ No newline at end of file diff --git a/content/tutorial/02-advanced-orders/05-milkman/02-bot-milkman/README.md b/content/tutorial/02-advanced-orders/05-milkman/02-bot-milkman/README.md new file mode 100644 index 0000000..22ae55f --- /dev/null +++ b/content/tutorial/02-advanced-orders/05-milkman/02-bot-milkman/README.md @@ -0,0 +1,5 @@ +--- +title: In-browser bot +--- + +Something robotic \ No newline at end of file diff --git a/content/tutorial/02-advanced-orders/05-milkman/03-cancel-milkman/README.md b/content/tutorial/02-advanced-orders/05-milkman/03-cancel-milkman/README.md new file mode 100644 index 0000000..418b72e --- /dev/null +++ b/content/tutorial/02-advanced-orders/05-milkman/03-cancel-milkman/README.md @@ -0,0 +1,5 @@ +--- +title: Cancel order +--- + +Bail \ No newline at end of file diff --git a/content/tutorial/02-advanced-orders/05-milkman/04-triage-milkman/README.md b/content/tutorial/02-advanced-orders/05-milkman/04-triage-milkman/README.md new file mode 100644 index 0000000..7de51a3 --- /dev/null +++ b/content/tutorial/02-advanced-orders/05-milkman/04-triage-milkman/README.md @@ -0,0 +1,7 @@ +--- +title: Triage +--- + +Diagnosing problems + +https://etherscan.io/tx/0x03e18bc923d69b3706e6e7dfc84f1b958e7087b91806955739862c1cfa7b5cd1 \ No newline at end of file diff --git a/content/tutorial/02-advanced-orders/05-milkman/meta.json b/content/tutorial/02-advanced-orders/05-milkman/meta.json new file mode 100644 index 0000000..7fef0a9 --- /dev/null +++ b/content/tutorial/02-advanced-orders/05-milkman/meta.json @@ -0,0 +1,8 @@ +{ + "title": "Milkman", + "scope": { + "prefix": "/src/lib/", + "name": "src" + }, + "focus": "/src/lib/run.ts" +} diff --git a/content/tutorial/02-advanced-orders/05-programmatic/01-config-programmatic/README.md b/content/tutorial/02-advanced-orders/06-programmatic/01-config-programmatic/README.md similarity index 100% rename from content/tutorial/02-advanced-orders/05-programmatic/01-config-programmatic/README.md rename to content/tutorial/02-advanced-orders/06-programmatic/01-config-programmatic/README.md diff --git a/content/tutorial/02-advanced-orders/05-programmatic/02-create-programmatic/README.md b/content/tutorial/02-advanced-orders/06-programmatic/02-create-programmatic/README.md similarity index 100% rename from content/tutorial/02-advanced-orders/05-programmatic/02-create-programmatic/README.md rename to content/tutorial/02-advanced-orders/06-programmatic/02-create-programmatic/README.md diff --git a/content/tutorial/02-advanced-orders/05-programmatic/03-poll-programmatic/README.md b/content/tutorial/02-advanced-orders/06-programmatic/03-poll-programmatic/README.md similarity index 100% rename from content/tutorial/02-advanced-orders/05-programmatic/03-poll-programmatic/README.md rename to content/tutorial/02-advanced-orders/06-programmatic/03-poll-programmatic/README.md diff --git a/content/tutorial/02-advanced-orders/05-programmatic/04-remove-programmatic/README.md b/content/tutorial/02-advanced-orders/06-programmatic/04-remove-programmatic/README.md similarity index 100% rename from content/tutorial/02-advanced-orders/05-programmatic/04-remove-programmatic/README.md rename to content/tutorial/02-advanced-orders/06-programmatic/04-remove-programmatic/README.md diff --git a/content/tutorial/02-advanced-orders/05-programmatic/05-merkle/README.md b/content/tutorial/02-advanced-orders/06-programmatic/05-merkle/README.md similarity index 100% rename from content/tutorial/02-advanced-orders/05-programmatic/05-merkle/README.md rename to content/tutorial/02-advanced-orders/06-programmatic/05-merkle/README.md diff --git a/content/tutorial/02-advanced-orders/05-programmatic/meta.json b/content/tutorial/02-advanced-orders/06-programmatic/meta.json similarity index 100% rename from content/tutorial/02-advanced-orders/05-programmatic/meta.json rename to content/tutorial/02-advanced-orders/06-programmatic/meta.json diff --git a/content/tutorial/02-advanced-orders/common/src/lib/gpv2Order.ts b/content/tutorial/02-advanced-orders/common/src/lib/gpv2Order.ts new file mode 100644 index 0000000..dfd8070 --- /dev/null +++ b/content/tutorial/02-advanced-orders/common/src/lib/gpv2Order.ts @@ -0,0 +1,56 @@ +import { Order, hashOrder, domain, OrderKind, OrderBalance } from "@cowprotocol/contracts"; +import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS } from "@cowprotocol/cow-sdk"; +import { SupportedChainId } from "@cowprotocol/cow-sdk"; +import { utils } from "ethers"; + +export const onchainOrderToOrder = (onchainOrder: any): Order => { + return { + sellToken: onchainOrder[0], + buyToken: onchainOrder[1], + receiver: onchainOrder[2], + sellAmount: onchainOrder[3], + buyAmount: onchainOrder[4], + validTo: onchainOrder[5], + appData: onchainOrder[6], + feeAmount: onchainOrder[7], + kind: getKind(onchainOrder[8]), + partiallyFillable: onchainOrder[9], + sellTokenBalance: getBalance(onchainOrder[10], true), + buyTokenBalance: getBalance(onchainOrder[11], false), + } +} +export const onchainOrderToHash = (onchainOrder: any, chain: SupportedChainId) => hashOrder( + domain(chain, COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chain]), + onchainOrderToOrder(onchainOrder) +); + +const getBalance = (balance: string, isSell: boolean) => { + switch (balance) { + // buy and sell + case utils.keccak256(utils.toUtf8Bytes('erc20')): + return OrderBalance.ERC20; + // buy and sell + case utils.keccak256(utils.toUtf8Bytes('internal')): + return OrderBalance.INTERNAL; + // sell only + case utils.keccak256(utils.toUtf8Bytes('external')): + if (!isSell) { + throw new Error('Invalid balance'); + } + return OrderBalance.EXTERNAL; + default: + throw new Error(`Unknown balance: ${balance}`); + } +} + +const getKind = (kind: string) => { + switch (kind) { + case utils.keccak256(utils.toUtf8Bytes('sell')): + return OrderKind.SELL; + case utils.keccak256(utils.toUtf8Bytes('buy')): + return OrderKind.BUY; + default: + throw new Error(`Unknown kind: ${kind}`); + } + +} \ No newline at end of file diff --git a/content/tutorial/02-advanced-orders/tsconfig.json b/content/tutorial/02-advanced-orders/tsconfig.json index 81738a5..d02864c 100644 --- a/content/tutorial/02-advanced-orders/tsconfig.json +++ b/content/tutorial/02-advanced-orders/tsconfig.json @@ -1,7 +1,12 @@ { "compilerOptions": { "baseUrl": ".", + "resolveJsonModule": true, + "esModuleInterop": true, "paths": { + "/src/lib/*": [ + "./common/src/lib/*" + ], "*": [ "../common/node_modules/*" ] diff --git a/content/tutorial/03-analyze/README.md b/content/tutorial/03-analyze/README.md new file mode 100644 index 0000000..518d8f7 --- /dev/null +++ b/content/tutorial/03-analyze/README.md @@ -0,0 +1,56 @@ +# Querying the Cow Subgraph + +The [Subgraph](https://github.com/cowprotocol/subgraph) is constantly indexing the protocol, making all the information more accessible. It provides information about trades, users, tokens and settlements. Additionally, it has some data aggregations which provides insights on the hourly/daily/totals USD volumes, trades, users, etc. + +The SDK provides just an easy way to access all this information. + +You can query the Cow Subgraph either by running some common queries exposed by the `CowSubgraphApi` or by building your own ones: + +```typescript +import { SubgraphApi, SupportedChainId } from "@cowprotocol/cow-sdk"; + +const subgraphApi = new SubgraphApi({ chainId: SupportedChainId.MAINNET }); + +// Get CoW Protocol totals +const { + tokens, + orders, + traders, + settlements, + volumeUsd, + volumeEth, + feesUsd, + feesEth, +} = await csubgraphApi.getTotals(); +console.log({ + tokens, + orders, + traders, + settlements, + volumeUsd, + volumeEth, + feesUsd, + feesEth, +}); + +// Get last 24 hours volume in usd +const { hourlyTotals } = await cowSubgraphApi.getLastHoursVolume(24); +console.log(hourlyTotals); + +// Get last week volume in usd +const { dailyTotals } = await cowSubgraphApi.getLastDaysVolume(7); +console.log(dailyTotals); + +// Get the last 5 batches +const query = ` + query LastBatches($n: Int!) { + settlements(orderBy: firstTradeTimestamp, orderDirection: desc, first: $n) { + txHash + firstTradeTimestamp + } + } +`; +const variables = { n: 5 }; +const response = await cowSubgraphApi.runQuery(query, variables); +console.log(response); +```