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 are controlled by a smart contract, they can't sign transactions using EIP-712. However, CoW Protocol supports smart contract wallets by allowing them to sign using:
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
PreSignfrom aSafewallet, you can use any smart contract, including one you create yourself.
Required dependencies
For pre-signed orders, we need to use:
OrderBookApito get a quote send an order to the CoW Protocol order bookMetadataApito generate order meta dataSafeto create and sign the transaction to the Settlement contractSafeApiKitto propose the transaction to Safe owners
Contract (GPv2Settlement) interaction
For interacting with contracts, the tutorials use ethers.js.
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
Safewallet, in this case we don't need to connect to the contract with asigner, and just aprovideris 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.
This is such a common use case that the SDK provides an export for the GPv2Settlement contract address:
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<unknown> {
// ...
}GPv2Settlement ABI
We can retrieve the ABI for the GPv2Settlement contract from the contract's verified code on Gnosisscan. We're just going to be using the setPreSignature function from the GPv2Settlement contract, so we can define that as a const:
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<unknown> {
// ...
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 wallet to sign the transaction to the GPv2Settlement contract. To interact with the Safe wallet, we need:
- an owner's wallet
- the
SafeSDK - the
Safeaddress
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
SafeSDK and thesafeApiKit - propose a given transaction to a
Safe
Transaction service URLs
This is relatively simple and we can just define a const:
// ...
export async function run(provider: Web3Provider): Promise<unknown> {
// ...
const SAFE_TRANSACTION_SERVICE_URL: Record<SupportedChainId, string> = {
[SupportedChainId.MAINNET]: 'https://safe-transaction-mainnet.safe.global',
[SupportedChainId.GNOSIS_CHAIN]: 'https://safe-transaction-gnosis-chain.safe.global',
[SupportedChainId.SEPOLIA]: 'https://safe-transaction-sepolia.safe.global',
}
// ...
}Safe SDK and safeApiKit
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<unknown> {
// ...
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
// ...
export function run(provider: Web3Provider): Promise<unknown> {
// ...
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:
// ...
export async function run(provider: Web3Provider): Promise<unknown> {
// ...
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:
// ...
export async function run(provider: Web3Provider): Promise<unknown> {
// ...
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:
// ...
export async function run(provider: Web3Provider): Promise<unknown> {
// ...
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:
// ...
import {
SupportedChainId,
OrderBookApi,
SigningScheme,
OrderQuoteRequest,
OrderQuoteSideKindSell,
OrderCreation,
COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS
} from '@cowprotocol/cow-sdk'
// ...
export function run(provider: Web3Provider): Promise<unknown> {
// ...
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 theSafewalletsignature: empty bytes, i.e.0xsigningScheme:SigningScheme.PRESIGN
// ...
import { BigNumber, Contract, ethers } from 'ethers'
// ...
export function run(provider: Web3Provider): Promise<unknown> {
// ...
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
limitorder. 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). 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:
// ...
export function run(provider: Web3Provider): Promise<unknown> {
// ...
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
setPreSignaturefunction with theorderUidandtrue(i.e.signed) - populate the transaction for the
Safewallet (i.e.toto call theGPv2Settlementcontract,valueto0anddatato the encodedsetPreSignaturefunction) - propose the transaction to the
Safewallet
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.
- Accept the connection request in Rabby
- Press the "Run" button again
- Observe the
orderUid,safeTxHashandsenderSignaturein the output panel - Browse to your
Safewallet and confirm the transaction - On successful confirmation of the transaction, the order will be valid and can be filled
The output should look similar to:
{
"orderUid": "0x83b53f9252440ca4b5e78dbbff309c90149cd4789efcef5128685c8ac35d3f8d075e706842751c28aafcc326c8e7a26777fe3cc2659ae2e7",
"safeTxHash": "0x86d38bed8d424ccae082090407040741e7683487343611a016a47430c0e1a2a6",
"senderSignature": "0x67675bf119b7d850f7d2daf814c921aa4f3a1202e83121002a73935bb7d89ad9397508a4067dde81f5653604130bb7b5d2d92f712354389cdb478d4dca751d1b1b"
}Keep the orderUid around for the next tutorial!
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<unknown> { 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!
}