From 844161b41c2669e302c9dbe7eeb2e14c3ee575bd Mon Sep 17 00:00:00 2001 From: mfw78 <53399572+mfw78@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:49:46 +0000 Subject: [PATCH] feat: pre-signed (#26) # Description Contains advanced order tutorials for Pre-Sign **NOTE**: Only Pre-Sign tutorials are within scope for this PR. # Changes - [x] Expand on Pre-Sign tutorials --- .../01-create-pre-signed-order/README.md | 131 ------- .../app-a/src/lib/GPv2Settlement.json | 20 - .../app-a/src/lib/const.ts | 12 - .../app-a/src/lib/getSafeSdkAndKit.ts | 28 -- .../app-a/src/lib/run.ts | 41 -- .../app-b/src/lib/run.ts | 108 ----- .../02-view-pre-signed-order/README.md | 5 - .../03-cancel-pre-signed-order/README.md | 5 - .../01-create-pre-signed-order/README.md | 370 ++++++++++++++++++ .../app-a/src/lib/run.ts | 38 ++ .../app-b/src/lib/run.ts | 144 +++++++ .../02-view-pre-signed-order/README.md | 7 + .../03-cancel-pre-signed-order/README.md | 74 ++++ .../app-a/src/lib/run.ts | 144 +++++++ .../app-b/src/lib/run.ts | 87 ++++ .../meta.json | 0 16 files changed, 864 insertions(+), 350 deletions(-) delete mode 100644 content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/README.md delete mode 100644 content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/GPv2Settlement.json delete mode 100644 content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/const.ts delete mode 100644 content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/getSafeSdkAndKit.ts delete mode 100644 content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/run.ts delete mode 100644 content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-b/src/lib/run.ts delete mode 100644 content/tutorial/02-advanced-orders/02-pre-signed-orders/02-view-pre-signed-order/README.md delete mode 100644 content/tutorial/02-advanced-orders/02-pre-signed-orders/03-cancel-pre-signed-order/README.md create mode 100644 content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/README.md create mode 100644 content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/run.ts create mode 100644 content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/app-b/src/lib/run.ts create mode 100644 content/tutorial/02-advanced-orders/03-pre-signed-orders/02-view-pre-signed-order/README.md create mode 100644 content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/README.md create mode 100644 content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/app-a/src/lib/run.ts create mode 100644 content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/app-b/src/lib/run.ts rename content/tutorial/02-advanced-orders/{02-pre-signed-orders => 03-pre-signed-orders}/meta.json (100%) diff --git a/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/README.md b/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/README.md deleted file mode 100644 index a493de5..0000000 --- a/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/README.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Pre-signed 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 - -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. - -In this example we will create a pre-signed order using Safe, but you can use any smart contract wallet. - -## Required dependencies - -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 - -## Define the order parameters - -First of all, we should define the order parameters. -The description of each parameter can be found in the [Order structure docs.](https://TODO) - -``` -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+++ -} -``` - -> `signingScheme` is the only difference between a regular order and a pre-signed order - -## Order meta data - -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/). - -``` -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) -``` - -## Post the order to the order-book - -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. - -``` -const orderCreation: OrderCreation = { - ...defaultOrder, - from: safeAddress, - signature: safeAddress, - appData: appDataContent, - appDataHash: appDataHex, -} - -// Send order to CoW Protocol order-book -const +++orderId+++ = await orderBookApi.sendOrder(orderCreation) -``` - -## Create the transaction to the Settlement contract - -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. - -``` -// Create the pre-signature transaction -const presignCallData = settlementContract.interface.encodeFunctionData(+++'setPreSignature'+++, [ - orderId, - true, -]) -const presignRawTx = { - to: settlementContract.address, - data: presignCallData, - value: '0', -} - -// 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, -}) -``` - -## Sign and execute transaction - -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. diff --git a/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/GPv2Settlement.json b/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/GPv2Settlement.json deleted file mode 100644 index 9a9e85e..0000000 --- a/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/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/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/const.ts b/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/const.ts deleted file mode 100644 index ac9e158..0000000 --- a/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/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/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/getSafeSdkAndKit.ts b/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/getSafeSdkAndKit.ts deleted file mode 100644 index fde4c44..0000000 --- a/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/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/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/run.ts b/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/run.ts deleted file mode 100644 index 44616eb..0000000 --- a/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/run.ts +++ /dev/null @@ -1,41 +0,0 @@ -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' - -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, - } -} diff --git a/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-b/src/lib/run.ts b/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-b/src/lib/run.ts deleted file mode 100644 index 4719927..0000000 --- a/content/tutorial/02-advanced-orders/02-pre-signed-orders/01-create-pre-signed-order/app-b/src/lib/run.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { Web3Provider } from '@ethersproject/providers' -import { Contract } from '@ethersproject/contracts' - -import { OrderBookApi, UnsignedOrder, OrderKind, SigningScheme, OrderCreation } from '@cowprotocol/cow-sdk' -import { MetadataApi } from '@cowprotocol/app-data' - -import { getSafeSdkAndKit } from './getSafeSdkAndKit' -import { SETTLEMENT_CONTRACT_ABI, SETTLEMENT_CONTRACT_ADDRESS } from './const' - -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 = { 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, - } - - // Generate the app-data document - const appDataDoc = await metadataApi.generateAppDataDoc({ - appCode, - environment, - metadata: { - quote, - orderClass - }, - }) - - const { appDataHex, appDataContent } = await metadataApi.appDataToCid(appDataDoc) - - - // Add all necessary fields to the order creation request - const orderCreation: OrderCreation = { - ...defaultOrder, - from: safeAddress, - signature: safeAddress, - appData: appDataContent, - appDataHash: appDataHex, - } - - // Send order to CoW Protocol order-book - const orderId = await orderBookApi.sendOrder(orderCreation) - - // Create the pre-signature transaction - const presignCallData = settlementContract.interface.encodeFunctionData('setPreSignature', [ - orderId, - true, - ]) - const presignRawTx = { - to: settlementContract.address, - data: presignCallData, - value: '0', - } - - // 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, - }) - - return { orderId, safeTxHash, senderSignature } -} diff --git a/content/tutorial/02-advanced-orders/02-pre-signed-orders/02-view-pre-signed-order/README.md b/content/tutorial/02-advanced-orders/02-pre-signed-orders/02-view-pre-signed-order/README.md deleted file mode 100644 index f5536b7..0000000 --- a/content/tutorial/02-advanced-orders/02-pre-signed-orders/02-view-pre-signed-order/README.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Viewing status ---- - -Something \ No newline at end of file diff --git a/content/tutorial/02-advanced-orders/02-pre-signed-orders/03-cancel-pre-signed-order/README.md b/content/tutorial/02-advanced-orders/02-pre-signed-orders/03-cancel-pre-signed-order/README.md deleted file mode 100644 index 29640d3..0000000 --- a/content/tutorial/02-advanced-orders/02-pre-signed-orders/03-cancel-pre-signed-order/README.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Cancelling order ---- - -Something \ No newline at end of file diff --git a/content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/README.md b/content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/README.md new file mode 100644 index 0000000..d80ddbb --- /dev/null +++ b/content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/README.md @@ -0,0 +1,370 @@ +--- +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 + +Since smart contract wallets, such as [`Safe`](https://safe.global) are controlled by a smart contract, they can't sign transactions using [`EIP-712`](https://beta.docs.cow.fi/cow-protocol/reference/core/signing-schemes#eip-712). However, CoW Protocol supports smart contract wallets by allowing them to sign using: +- [`EIP-1271`](https://beta.docs.cow.fi/cow-protocol/reference/core/signing-schemes#eip-1271) +- [`PRESIGN`](https://beta.docs.cow.fi/cow-protocol/reference/core/signing-schemes#presign) + +This tutorial will show you how to create an order using `PreSign` signing scheme, using a `Safe` wallet. It is assumed that you have a `Safe` wallet with at least one owner, and that the `owner` is the account you're using to run the tutorial. + +> While this tutorial demonstrates how to create an order using `PreSign` from a `Safe` wallet, you can use any smart contract, including one you create yourself. + +## Required dependencies + +For pre-signed orders, we need to use: +- `OrderBookApi` to get a quote 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 + +## Contract (`GPv2Settlement`) interaction + +For interacting with contracts, the tutorials use [ethers.js](https://docs.ethers.io/v5/). + +To interact with a contract, we need to know: + +- the contract address +- the ABI + +> As we're going to be sending the transaction from a `Safe` wallet, in this case we don't need to connect to the contract with a `signer`, and just a `provider` is enough. + +### Contract address + +`GPv2Settlement` is a core contract and it's deployed on each supported network. Core contracts deployment addresses can be found in the [CoW Protocol docs](https://beta.docs.cow.fi/cow-protocol/reference/contracts/core). + +This is such a common use case that the SDK provides an export for the `GPv2Settlement` contract address: + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers' +import { + SupportedChainId, + OrderBookApi, + COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, +} from '@cowprotocol/cow-sdk' +import { MetadataApi, latest } from '@cowprotocol/app-data' + +export async function run(provider: Web3Provider): Promise { + // ... +} +``` + +### `GPv2Settlement` ABI + +We can retrieve the ABI for the `GPv2Settlement` contract from the contract's verified code on [Gnosisscan](https://gnosisscan.io/address/0x9008D19f58AAbD9eD0D60971565AA8510560ab41#code). We're just going to be using the `setPreSignature` function from the `GPv2Settlement` contract, so we can define that as a `const`: + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers' +import { + SupportedChainId, + OrderBookApi, + COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, +} from '@cowprotocol/cow-sdk' +import { MetadataApi, latest } from '@cowprotocol/app-data' + +export async function run(provider: Web3Provider): Promise { + // ... + const abi = [ + { + "inputs": [ + { "internalType": "bytes", "name": "orderUid", "type": "bytes" }, + { "internalType": "bool", "name": "signed", "type": "bool" } + ], + "name": "setPreSignature", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] + // ... +} +``` + +### `Safe` wallet + +This tutorial uses the [`Safe`](https://safe.global) wallet to sign the transaction to the `GPv2Settlement` contract. To interact with the `Safe` wallet, we need: + +- an owner's wallet +- the `Safe` SDK +- the `Safe` address + +#### Helper functions + +To make the code more readable, we are going to define helper functions that: + +- provide retrievable transaction service URLs by chain ID +- retrieve instances of the `Safe` SDK and the `safeApiKit` +- propose a given transaction to a `Safe` + +##### Transaction service URLs + +This is relatively simple and we can just define a `const`: + +```typescript +/// file: run.ts +// ... +export async function run(provider: Web3Provider): Promise { + // ... + + 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', + } + + // ... +} +``` + +##### `Safe` SDK and `safeApiKit` + +```typescript +/// file: run.ts +import type { Web3Provider } from '@ethersproject/providers' ++++import { ethers } from 'ethers'+++ +import { + SupportedChainId, + OrderBookApi, +} from '@cowprotocol/cow-sdk' +import { MetadataApi, latest } from '@cowprotocol/app-data' ++++import { MetaTransactionData } from '@safe-global/safe-core-sdk-types'+++ ++++import Safe, { EthersAdapter } from '@safe-global/protocol-kit'+++ ++++import SafeApiKit from '@safe-global/api-kit'+++ + +export async function run(provider: Web3Provider): Promise { + // ... + const getSafeSdkAndKit = async (safeAddress: string) => { + const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer }) + const txServiceUrl = SAFE_TRANSACTION_SERVICE_URL[chainId] + const safeApiKit = new SafeApiKit({ txServiceUrl, ethAdapter }) + + const safeSdk = await Safe.create({ethAdapter, safeAddress}); + + return { safeApiKit, safeSdk } + } + // ... +} +``` + +The above function returns an object with the `safeApiKit` and the `safeSdk` instances, initialized with the `signer` and a nominated `safeAddress`. + +##### Propose transaction to `Safe` + +```typescript +/// file: run.ts +// ... + +export function run(provider: Web3Provider): Promise { + // ... + + const proposeSafeTx = async (params: MetaTransactionData) => { + const safeTx = await safeSdk.createTransaction({safeTransactionData: params}) + const signedSafeTx = await safeSdk.signTransaction(safeTx) + const safeTxHash = await safeSdk.getTransactionHash(signedSafeTx) + const senderSignature = signedSafeTx.signatures.get(ownerAddress.toLowerCase())?.data || '' + + // Send the pre-signed transaction to the Safe + await safeApiKit.proposeTransaction({ + safeAddress, + safeTransactionData: signedSafeTx.data, + safeTxHash, + senderAddress: ownerAddress, + senderSignature, + }) + + return { safeTxHash, senderSignature } + } + + // ... +} +``` + +The above function takes a `MetaTransactionData` object (i.e. `to`, `value`, `data` tuple) and then creates, signs and sends the transaction to the `Safe` wallet. It returns the `safeTxHash` and the `senderSignature`. + +#### `Safe` address + +The `Safe` address is the address of the smart contract wallet that we're going to be using to make the trade. Replace the `safeAddress` with the address of your `Safe` wallet: + +```typescript +/// file: run.ts +// ... + +export async function run(provider: Web3Provider): Promise { + // ... + const safeAddress = '0x075E706842751c28aAFCc326c8E7a26777fe3Cc2' + // ... +} +``` + +### Contract instance + +Now that we have the contract address and the ABI, we can create the contract instance. As we are just going to be ABI encoding the `setPreSignature` function, we don't need to connect to the contract with a `signer` or a `provider`: + +```typescript +/// file: run.ts +// ... + +export async function run(provider: Web3Provider): Promise { + // ... + const settlementContract = new Contract(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], abi) + // ... +} +``` + +### Connect to the Safe + +Now that we have the `Safe` address and associated helper functions, we can connect to the `Safe`: + +```typescript +/// file: run.ts +// ... + +export async function run(provider: Web3Provider): Promise { + // ... + const { safeApiKit, safeSdk } = await getSafeSdkAndKit(safeAddress) + // ... +} +``` + +### Get a quote + +As per normal, we are going to request a quote to buy COW tokens with wxDAI using `OrderBookApi`: + +```typescript +/// file: run.ts +// ... +import { + SupportedChainId, + OrderBookApi, ++++ SigningScheme,+++ ++++ OrderQuoteRequest,+++ ++++ OrderQuoteSideKindSell,+++ ++++ OrderCreation,+++ + COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS +} from '@cowprotocol/cow-sdk' +// ... + +export function run(provider: Web3Provider): Promise { + // ... + const sellAmount = '1000000000000000000'; + const sellToken = '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d'; + const buyToken = '0x177127622c4A00F3d409B75571e12cB3c8973d3c'; + + const quoteRequest: OrderQuoteRequest = { + sellToken, + buyToken, + receiver: safeAddress, + sellAmountBeforeFee: sellAmount, + kind: OrderQuoteSideKindSell.SELL, + appData: appDataContent, + appDataHash: appDataHex, + from: ownerAddress, + } + + const { quote } = await orderBookApi.getQuote(quoteRequest); + // ... +} +``` + +### Submit the order + +Now that we have the quote, we can submit the order to the order book. As we're using `PreSign`, we need to pay special attention to the fields: + +- `from`: the address of the `Safe` wallet +- `signature`: empty bytes, i.e. `0x` +- `signingScheme`: `SigningScheme.PRESIGN` + +```typescript +/// file: run.ts +// ... ++++import { BigNumber, Contract, ethers } from 'ethers'+++ +// ... + +export function run(provider: Web3Provider): Promise { + // ... + const order: OrderCreation = { + ...quote, + sellAmount, + buyAmount: BigNumber.from(quote.buyAmount).mul(9950).div(10000).toString(), + feeAmount: '0', + appData: appDataContent, + appDataHash: appDataHex, + partiallyFillable: true, + +++from: safeAddress,+++ + +++signature: '0x',+++ + +++signingScheme: SigningScheme.PRESIGN,+++ + } + + const orderUid = await orderBookApi.sendOrder(order) + // ... +} +``` +> The above applies a 0.5% slippage to the order. + +> This is the first instance demonstrating the use of a partially-fillable `limit` order. **Caution:** Market orders must be fill-or-kill and *cannot* be partially fillable. + +At this stage, the order is created in the order book, but it is not valid (it will show as 'Signing' in the [Explorer](https://explorer.cow.fi/)). To make the order valid, we need to create a transaction to the `GPv2Settlement` contract that sets the pre-signature (i.e. `setPreSignature`). + +### Sign the order + +Now that we have the `orderUid`, we can create the transaction to the `GPv2Settlement` contract to set the pre-signature: + +```typescript +/// file: run.ts +// ... + +export function run(provider: Web3Provider): Promise { + // ... + const presignCallData = settlementContract.interface.encodeFunctionData('setPreSignature', [ + orderUid, + true, + ]) + + const presignRawTx: MetaTransactionData = { + to: settlementContract.address, + value: '0', + data: presignCallData, + } + + const { safeTxHash, senderSignature } = await proposeSafeTx(presignRawTx) + + return { orderUid, safeTxHash, senderSignature } +} +``` + +In the above code, we: + +- encode the `setPreSignature` function with the `orderUid` and `true` (i.e. `signed`) +- populate the transaction for the `Safe` wallet (i.e. `to` to call the `GPv2Settlement` contract, `value` to `0` and `data` to the encoded `setPreSignature` function) +- propose the transaction to the `Safe` wallet + +## 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 `orderUid`, `safeTxHash` and `senderSignature` in the output panel +4. Browse to your `Safe` wallet and confirm the transaction +5. On successful confirmation of the transaction, the order will be valid and can be filled + +The output should look similar to: + +```json +/// file: output.json +{ + "orderUid": "0x83b53f9252440ca4b5e78dbbff309c90149cd4789efcef5128685c8ac35d3f8d075e706842751c28aafcc326c8e7a26777fe3cc2659ae2e7", + "safeTxHash": "0x86d38bed8d424ccae082090407040741e7683487343611a016a47430c0e1a2a6", + "senderSignature": "0x67675bf119b7d850f7d2daf814c921aa4f3a1202e83121002a73935bb7d89ad9397508a4067dde81f5653604130bb7b5d2d92f712354389cdb478d4dca751d1b1b" +} +``` + +Keep the `orderUid` around for the next tutorial! \ No newline at end of file diff --git a/content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/run.ts b/content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/run.ts new file mode 100644 index 0000000..1b36a9f --- /dev/null +++ b/content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/app-a/src/lib/run.ts @@ -0,0 +1,38 @@ +import type { Web3Provider } from '@ethersproject/providers' +import { + SupportedChainId, + OrderBookApi, +} from '@cowprotocol/cow-sdk' +import { MetadataApi, latest } from '@cowprotocol/app-data' + +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: SupportedChainId.GNOSIS_CHAIN }]); + } + const orderBookApi = new OrderBookApi({ chainId }) + 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: 'limit' } + + 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/03-pre-signed-orders/01-create-pre-signed-order/app-b/src/lib/run.ts b/content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/app-b/src/lib/run.ts new file mode 100644 index 0000000..7496050 --- /dev/null +++ b/content/tutorial/02-advanced-orders/03-pre-signed-orders/01-create-pre-signed-order/app-b/src/lib/run.ts @@ -0,0 +1,144 @@ +import type { Web3Provider } from '@ethersproject/providers' +import { BigNumber, Contract, ethers } from 'ethers' +import { + SupportedChainId, + OrderBookApi, + SigningScheme, + OrderQuoteRequest, + OrderQuoteSideKindSell, + OrderCreation, + COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS +} from '@cowprotocol/cow-sdk' +import { MetadataApi, latest } from '@cowprotocol/app-data' +import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' +import Safe, { EthersAdapter } from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' + +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: SupportedChainId.GNOSIS_CHAIN }]); + } + const orderBookApi = new OrderBookApi({ chainId }) + 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: 'limit' } + + 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(); + + const abi = [ + { + "inputs": [ + { "internalType": "bytes", "name": "orderUid", "type": "bytes" }, + { "internalType": "bool", "name": "signed", "type": "bool" } + ], + "name": "setPreSignature", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] + + 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', + } + + const getSafeSdkAndKit = async (safeAddress: string) => { + const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer }) + const txServiceUrl = SAFE_TRANSACTION_SERVICE_URL[chainId] + const safeApiKit = new SafeApiKit({ txServiceUrl, ethAdapter }) + + const safeSdk = await Safe.create({ethAdapter, safeAddress}); + + return { safeApiKit, safeSdk } + } + + const proposeSafeTx = async (params: MetaTransactionData) => { + const safeTx = await safeSdk.createTransaction({safeTransactionData: params}) + const signedSafeTx = await safeSdk.signTransaction(safeTx) + const safeTxHash = await safeSdk.getTransactionHash(signedSafeTx) + const senderSignature = signedSafeTx.signatures.get(ownerAddress.toLowerCase())?.data || '' + + // Send the pre-signed transaction to the Safe + await safeApiKit.proposeTransaction({ + safeAddress, + safeTransactionData: signedSafeTx.data, + safeTxHash, + senderAddress: ownerAddress, + senderSignature, + }) + + return { safeTxHash, senderSignature } + } + + const safeAddress = '0x075E706842751c28aAFCc326c8E7a26777fe3Cc2' + const settlementContract = new Contract(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], abi) + + const { safeApiKit, safeSdk } = await getSafeSdkAndKit(safeAddress) + + const sellAmount = '1000000000000000000'; + const sellToken = '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d'; + const buyToken = '0x177127622c4A00F3d409B75571e12cB3c8973d3c'; + + const quoteRequest: OrderQuoteRequest = { + sellToken, + buyToken, + receiver: safeAddress, + sellAmountBeforeFee: sellAmount, + kind: OrderQuoteSideKindSell.SELL, + appData: appDataContent, + appDataHash: appDataHex, + from: ownerAddress, + } + + const { quote } = await orderBookApi.getQuote(quoteRequest); + + const order: OrderCreation = { + ...quote, + sellAmount, + buyAmount: BigNumber.from(quote.buyAmount).mul(9950).div(10000).toString(), + feeAmount: '0', + appData: appDataContent, + appDataHash: appDataHex, + partiallyFillable: true, + from: safeAddress, + signature: '0x', + signingScheme: SigningScheme.PRESIGN, + } + + const orderUid = await orderBookApi.sendOrder(order) + + const presignCallData = settlementContract.interface.encodeFunctionData('setPreSignature', [ + orderUid, + true, + ]) + + const presignRawTx: MetaTransactionData = { + to: settlementContract.address, + value: '0', + data: presignCallData, + } + + const { safeTxHash, senderSignature } = await proposeSafeTx(presignRawTx) + + return { orderUid, safeTxHash, senderSignature } +} diff --git a/content/tutorial/02-advanced-orders/03-pre-signed-orders/02-view-pre-signed-order/README.md b/content/tutorial/02-advanced-orders/03-pre-signed-orders/02-view-pre-signed-order/README.md new file mode 100644 index 0000000..92bee79 --- /dev/null +++ b/content/tutorial/02-advanced-orders/03-pre-signed-orders/02-view-pre-signed-order/README.md @@ -0,0 +1,7 @@ +--- +title: Viewing status +--- + +As we have created an order, we can check its status on [CoW Explorer](https://explorer.cow.fi/). Fortunately, we have the `orderUid` from the previous step, so we can use it to find the order. + +Simply go to [CoW Explorer](https://explorer.cow.fi/) and paste the `orderUid` into the search bar. diff --git a/content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/README.md b/content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/README.md new file mode 100644 index 0000000..0ef05d5 --- /dev/null +++ b/content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/README.md @@ -0,0 +1,74 @@ +--- +title: Cancelling order +--- + +In the previous tutorials we have create a pre-signed 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. + +There are actually two ways to cancel a **pre-signed** order: + +1. Set `signed` to `false` using `setPreSignature` +2. Invalidate the order using `invalidateOrder`, as seen in [cancelling on-chain](/tutorial/cancel-on-chain-order) + +We will use the first method in this tutorial (as it results in a cheaper transaction fee). + +## Contract `GPv2Settlement` interaction + +In order to revoke the signature of a pre-signed order, we need to call the `setPreSignature` with: + +- `orderUid` - the same `orderUid` that was pre-signed +- `signed` - `false` + +We will start at the same point as the previous tutorial, where we have created a pre-signed order and signed the transaction with a `Safe`. + +### Define the `orderUid` + +Instead of using the API to generate the `orderUid` when sending an order, we will simply use the `orderUid` that was previously generated and define that as a constant: + +```typescript +/// file: run.ts +// ... + +export async function run(provider: Web3Provider): Promise { + // ... + const orderUid = '0x83b53f9252440ca4b5e78dbbff309c90149cd4789efcef5128685c8ac35d3f8d075e706842751c28aafcc326c8e7a26777fe3cc2659ae2e7'; + // ... +} +``` + +> Replace the `orderUid` with the one that was generated in the previous tutorial + +### Set `signed` to `false` + +Now that we have the `orderUid`, we can call `setPreSignature` with the `orderUid` and `signed` set to `false`: + +```typescript +/// file: run.ts +// ... + +export async function run(provider: Web3Provider): Promise { + // ... + const presignCallData = settlementContract.interface.encodeFunctionData('setPreSignature', [ + orderUid, + false, + ]) + // ... +} +``` + +This should be all the changes required in order to revoke the pre-signed order with your `Safe`. + +> As the `orderUid` is now statically defined, a lot of code can be removed from the previous tutorial. You can check the optimum solution by clicking 'Solve'. + +## 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 `orderUid`, `safeTxHash`, and `senderSignature` in the output panel +4. Browse to your `Safe` wallet and confirm the transaction +5. On successful confirmation of the transaction, the order's signature should be revoked. + +> When checking the `orderUid` status in [CoW Explorer](https://explorer.cow.fi), the order should now be marked as 'Signing' instead of 'Open'. The means that the order is no longer valid and can't be filled. Once the expiry time has passed, the order will be marked as 'Expired'. diff --git a/content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/app-a/src/lib/run.ts b/content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/app-a/src/lib/run.ts new file mode 100644 index 0000000..7496050 --- /dev/null +++ b/content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/app-a/src/lib/run.ts @@ -0,0 +1,144 @@ +import type { Web3Provider } from '@ethersproject/providers' +import { BigNumber, Contract, ethers } from 'ethers' +import { + SupportedChainId, + OrderBookApi, + SigningScheme, + OrderQuoteRequest, + OrderQuoteSideKindSell, + OrderCreation, + COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS +} from '@cowprotocol/cow-sdk' +import { MetadataApi, latest } from '@cowprotocol/app-data' +import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' +import Safe, { EthersAdapter } from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' + +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: SupportedChainId.GNOSIS_CHAIN }]); + } + const orderBookApi = new OrderBookApi({ chainId }) + 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: 'limit' } + + 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(); + + const abi = [ + { + "inputs": [ + { "internalType": "bytes", "name": "orderUid", "type": "bytes" }, + { "internalType": "bool", "name": "signed", "type": "bool" } + ], + "name": "setPreSignature", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] + + 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', + } + + const getSafeSdkAndKit = async (safeAddress: string) => { + const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer }) + const txServiceUrl = SAFE_TRANSACTION_SERVICE_URL[chainId] + const safeApiKit = new SafeApiKit({ txServiceUrl, ethAdapter }) + + const safeSdk = await Safe.create({ethAdapter, safeAddress}); + + return { safeApiKit, safeSdk } + } + + const proposeSafeTx = async (params: MetaTransactionData) => { + const safeTx = await safeSdk.createTransaction({safeTransactionData: params}) + const signedSafeTx = await safeSdk.signTransaction(safeTx) + const safeTxHash = await safeSdk.getTransactionHash(signedSafeTx) + const senderSignature = signedSafeTx.signatures.get(ownerAddress.toLowerCase())?.data || '' + + // Send the pre-signed transaction to the Safe + await safeApiKit.proposeTransaction({ + safeAddress, + safeTransactionData: signedSafeTx.data, + safeTxHash, + senderAddress: ownerAddress, + senderSignature, + }) + + return { safeTxHash, senderSignature } + } + + const safeAddress = '0x075E706842751c28aAFCc326c8E7a26777fe3Cc2' + const settlementContract = new Contract(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], abi) + + const { safeApiKit, safeSdk } = await getSafeSdkAndKit(safeAddress) + + const sellAmount = '1000000000000000000'; + const sellToken = '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d'; + const buyToken = '0x177127622c4A00F3d409B75571e12cB3c8973d3c'; + + const quoteRequest: OrderQuoteRequest = { + sellToken, + buyToken, + receiver: safeAddress, + sellAmountBeforeFee: sellAmount, + kind: OrderQuoteSideKindSell.SELL, + appData: appDataContent, + appDataHash: appDataHex, + from: ownerAddress, + } + + const { quote } = await orderBookApi.getQuote(quoteRequest); + + const order: OrderCreation = { + ...quote, + sellAmount, + buyAmount: BigNumber.from(quote.buyAmount).mul(9950).div(10000).toString(), + feeAmount: '0', + appData: appDataContent, + appDataHash: appDataHex, + partiallyFillable: true, + from: safeAddress, + signature: '0x', + signingScheme: SigningScheme.PRESIGN, + } + + const orderUid = await orderBookApi.sendOrder(order) + + const presignCallData = settlementContract.interface.encodeFunctionData('setPreSignature', [ + orderUid, + true, + ]) + + const presignRawTx: MetaTransactionData = { + to: settlementContract.address, + value: '0', + data: presignCallData, + } + + const { safeTxHash, senderSignature } = await proposeSafeTx(presignRawTx) + + return { orderUid, safeTxHash, senderSignature } +} diff --git a/content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/app-b/src/lib/run.ts b/content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/app-b/src/lib/run.ts new file mode 100644 index 0000000..e0098ac --- /dev/null +++ b/content/tutorial/02-advanced-orders/03-pre-signed-orders/03-cancel-pre-signed-order/app-b/src/lib/run.ts @@ -0,0 +1,87 @@ +import type { Web3Provider } from '@ethersproject/providers' +import { Contract, ethers } from 'ethers' +import { + SupportedChainId, + COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS +} from '@cowprotocol/cow-sdk' +import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' +import Safe, { EthersAdapter } from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' + +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: SupportedChainId.GNOSIS_CHAIN }]); + } + const signer = provider.getSigner(); + const ownerAddress = await signer.getAddress(); + + const abi = [ + { + "inputs": [ + { "internalType": "bytes", "name": "orderUid", "type": "bytes" }, + { "internalType": "bool", "name": "signed", "type": "bool" } + ], + "name": "setPreSignature", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] + + 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', + } + + const getSafeSdkAndKit = async (safeAddress: string) => { + const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer }) + const txServiceUrl = SAFE_TRANSACTION_SERVICE_URL[chainId] + const safeApiKit = new SafeApiKit({ txServiceUrl, ethAdapter }) + + const safeSdk = await Safe.create({ethAdapter, safeAddress}); + + return { safeApiKit, safeSdk } + } + + const proposeSafeTx = async (params: MetaTransactionData) => { + const safeTx = await safeSdk.createTransaction({safeTransactionData: params}) + const signedSafeTx = await safeSdk.signTransaction(safeTx) + const safeTxHash = await safeSdk.getTransactionHash(signedSafeTx) + const senderSignature = signedSafeTx.signatures.get(ownerAddress.toLowerCase())?.data || '' + + // Send the pre-signed transaction to the Safe + await safeApiKit.proposeTransaction({ + safeAddress, + safeTransactionData: signedSafeTx.data, + safeTxHash, + senderAddress: ownerAddress, + senderSignature, + }) + + return { safeTxHash, senderSignature } + } + + const safeAddress = '0x075E706842751c28aAFCc326c8E7a26777fe3Cc2' + const settlementContract = new Contract(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], abi) + + const { safeApiKit, safeSdk } = await getSafeSdkAndKit(safeAddress) + + const orderUid = '0x83b53f9252440ca4b5e78dbbff309c90149cd4789efcef5128685c8ac35d3f8d075e706842751c28aafcc326c8e7a26777fe3cc2659ae2e7'; + + const presignCallData = settlementContract.interface.encodeFunctionData('setPreSignature', [ + orderUid, + false, + ]) + + const presignRawTx: MetaTransactionData = { + to: settlementContract.address, + value: '0', + data: presignCallData, + } + + const { safeTxHash, senderSignature } = await proposeSafeTx(presignRawTx) + + return { orderUid, safeTxHash, senderSignature } +} diff --git a/content/tutorial/02-advanced-orders/02-pre-signed-orders/meta.json b/content/tutorial/02-advanced-orders/03-pre-signed-orders/meta.json similarity index 100% rename from content/tutorial/02-advanced-orders/02-pre-signed-orders/meta.json rename to content/tutorial/02-advanced-orders/03-pre-signed-orders/meta.json